diff --git a/.gitignore b/.gitignore index b62fa4b..a5b2017 100644 --- a/.gitignore +++ b/.gitignore @@ -18,8 +18,11 @@ /local/beebright_local/ui/ /local/ui-source/ **/dist-local/ +/racing-beta/export/ /BeeBright-Full-Stack/beebright-spelling-bee/frontend/public/install.sh /BeeBright-Full-Stack/beebright-spelling-bee/frontend/public/study-catalog.json /BeeBright-Full-Stack/beebright-spelling-bee/frontend/public/local-changelog.md + +/local/THIRD_PARTY_BABYLON_LICENSE.txt diff --git a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/package-lock.json b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/package-lock.json index b7590e5..ce96492 100644 --- a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/package-lock.json +++ b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/package-lock.json @@ -8,6 +8,7 @@ "name": "beebright-frontend", "version": "1.0.0", "dependencies": { + "@babylonjs/core": "9.30.0", "@clerk/react": "^6.14.4", "lucide-react": "0.468.0", "react": "19.2.6", @@ -22,6 +23,12 @@ "node": ">=22.12.0" } }, + "node_modules/@babylonjs/core": { + "version": "9.30.0", + "resolved": "https://registry.npmjs.org/@babylonjs/core/-/core-9.30.0.tgz", + "integrity": "sha512-rV+kc7B9oEtREaWh0YYNinoqYshRbHLHj7t4s4woDjDXFXWKHu1d8S914CNfs3KTOI8V8DvoVKLe2yDHZXfZEA==", + "license": "Apache-2.0" + }, "node_modules/@clerk/react": { "version": "6.14.4", "resolved": "https://registry.npmjs.org/@clerk/react/-/react-6.14.4.tgz", diff --git a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/package.json b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/package.json index ac1766e..ae948dd 100644 --- a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/package.json +++ b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/package.json @@ -12,6 +12,7 @@ "node": ">=22.12.0" }, "dependencies": { + "@babylonjs/core": "9.30.0", "@clerk/react": "^6.14.4", "lucide-react": "0.468.0", "react": "19.2.6", diff --git a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/game-mechanics.js b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/game-mechanics.js index 8be606d..5be0add 100644 --- a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/game-mechanics.js +++ b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/game-mechanics.js @@ -119,10 +119,10 @@ export function marbleStep(p,input,dt,surfaces,{jump=false,boost=false}={}){ } export const RALLY_COURSES=[ - {name:'Harbor Circuit',width:14,scale:2.8,heights:[0,2,6,9,5,2,0,1,3,1],laps:2,theme:'harbor',points:[[28,0],[25,25],[0,34],[-27,23],[-38,-5],[-26,-30],[-6,-36],[7,-18],[23,-30],[37,-16]]}, - {name:'Canyon Switchback',width:13,scale:2.8,heights:[0,8,18,12,4,0,7,20,13,5,2,10,5],laps:2,theme:'canyon',points:[[30,0],[32,24],[9,29],[4,10],[-13,9],[-14,32],[-36,23],[-34,-20],[-14,-30],[-2,-10],[13,-15],[17,-33],[34,-25]]}, - {name:'Alpine Hairpins',width:13,scale:1,heights:[0,8,20,36,46,60,65,58,53,47,40,36,28,18,8,4,2,0,0],laps:2,theme:'alpine',mountains:[[-35,155,110,90],[155,115,145,80],[-170,95,135,85],[0,-180,90,80]],points:[[145,-100],[170,-35],[140,35],[80,60],[70,120],[0,150],[-65,110],[-35,70],[25,80],[30,40],[-40,25],[-115,55],[-150,0],[-135,-65],[-75,-90],[-45,-40],[15,-55],[55,-100],[80,-145]]}, - {name:'Midnight Metro',width:14,scale:2.8,heights:[0,2,6,4,0,0,10,15,8,2,0,0,3,0,5],laps:2,theme:'city',points:[[35,0],[34,28],[10,29],[8,12],[-10,12],[-12,32],[-34,28],[-36,4],[-20,2],[-20,-18],[-35,-20],[-25,-36],[3,-34],[7,-17],[32,-24]]}, + {name:'Harbor Circuit',width:20,scale:2.8,heights:[0,2,6,9,5,2,0,1,3,1],laps:2,theme:'harbor',points:[[28,0],[25,25],[0,34],[-27,23],[-38,-5],[-26,-30],[-6,-36],[7,-18],[23,-30],[37,-16]]}, + {name:'Canyon Switchback',width:18,scale:2.8,heights:[0,8,18,12,4,0,7,20,13,5,2,10,5],laps:2,theme:'canyon',points:[[30,0],[32,24],[9,29],[4,10],[-13,9],[-14,32],[-36,23],[-34,-20],[-14,-30],[-2,-10],[13,-15],[17,-33],[34,-25]]}, + {name:'Alpine Hairpins',width:18,scale:1,heights:[0,8,20,36,46,60,65,58,53,47,40,36,28,18,8,4,2,0,0],laps:2,theme:'alpine',mountains:[[-35,155,110,90],[155,115,145,80],[-170,95,135,85],[0,-180,90,80]],points:[[145,-100],[170,-35],[140,35],[80,60],[70,120],[0,150],[-65,110],[-35,70],[25,80],[30,40],[-40,25],[-115,55],[-150,0],[-135,-65],[-75,-90],[-45,-40],[15,-55],[55,-100],[80,-145]]}, + {name:'Midnight Metro',width:20,scale:2.8,heights:[0,2,6,4,0,0,10,15,8,2,0,0,3,0,5],laps:2,theme:'city',points:[[35,0],[34,28],[10,29],[8,12],[-10,12],[-12,32],[-34,28],[-36,4],[-20,2],[-20,-18],[-35,-20],[-25,-36],[3,-34],[7,-17],[32,-24]]}, ]; export function roadDistance(points,x,z){let best=Infinity;for(let i=0;i=next.x&&tip<=next.x+next.w;} diff --git a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/games-3d.js b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/games-3d.js index d157430..70be3ed 100644 --- a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/games-3d.js +++ b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/games-3d.js @@ -1,11 +1,11 @@ import * as THREE from 'three'; import {setupRally} from './games-rally.js'; import {marbleBoardGeometry} from './track-geometry.js'; -import {createGameRenderer,createHighGameRenderer} from './game-renderer.js'; +import {createGameRenderer} from './game-renderer.js'; import {clamp,MARBLE_COURSES,marblePath,marbleSurfaces,marbleStep,marbleStart,keepInFrame} from './game-mechanics.js'; export function mount3D(canvas,{id,level=0,input,onFinish,onStatus,onGraphics,quality='light'}) { const options={id,level,input,onFinish,onStatus,onGraphics,quality}; - if(id==='rally'&&quality==='high')return createHighGameRenderer(canvas,onGraphics).then(renderer=>mountScene(canvas,options,renderer)); + if(id==='rally'&&quality==='high')return import('./rally-babylon.js').then(({createRallyRenderer})=>createRallyRenderer(canvas,onGraphics)).then(renderer=>mountScene(canvas,options,renderer)); return mountScene(canvas,options,createGameRenderer(canvas,quality,onGraphics)); } function mountScene(canvas,{id,level,input,onFinish,onStatus,quality},renderer){ @@ -17,7 +17,7 @@ function mountScene(canvas,{id,level,input,onFinish,onStatus,quality},renderer){ const teal=mat('#8de9df'),purple=mat('#b4a2ef'),stone=mat('#7b93bd'),green=mat('#abec9e'),pink=mat('#f691ba'),white=mat('#e6efff'); function mesh(geometry,material,x=0,y=0,z=0,parent=scene){geometries.push(geometry);const m=new THREE.Mesh(geometry,material);m.position.set(x,y,z);m.castShadow=high;m.receiveShadow=high;parent.add(m);return m;} function box(x,y,z,w,h,d,material=stone){return mesh(new THREE.BoxGeometry(w,h,d),material,x,y,z);} - let animation,last=0,t=0,paused=false,done=false,score=0,status='',redraw=true; + let loading=Boolean(renderer.ready),animation,last=0,t=0,paused=loading,done=false,score=0,status='',redraw=true; const p={x:0,y:.56,z:3,vx:0,vz:0,vy:0,speed:0,heading:Math.PI/2}; const player=new THREE.Group();scene.add(player);let ball,course,segments,surfaces,crystals=[],taken=0,checkpoint={x:0,z:3,y:.56},falls=0,grounded=true; let rally,hazards=[],boostUntil=0,boostCooldown=0,hazardCooldown=0; @@ -105,7 +105,9 @@ function mountScene(canvas,{id,level,input,onFinish,onStatus,quality},renderer){ light.userData.followTarget.position.copy(center);light.position.copy(center).add(new THREE.Vector3(16,28,12)); } } - function loop(now){const dt=last?Math.min(.034,(now-last)/1000):0;last=now;if(!paused&&!done){update(dt);redraw=true;}if(redraw){try{renderer.render(scene,camera);}catch(error){finish(false,`Graphics stopped: ${error.message}. Retry or choose Lightweight graphics.`);}redraw=false;}animation=requestAnimationFrame(loop);} + function loop(now){const dt=last?Math.min(.034,(now-last)/1000):0;last=now;if(!paused&&!done){update(dt);redraw=true;}if(loading)redraw=true;if(redraw){try{renderer.render(scene,camera);}catch(error){finish(false,`Graphics stopped: ${error.message}. Retry or choose Lightweight graphics.`);}redraw=false;}animation=requestAnimationFrame(loop);} const contextLost=e=>{e.preventDefault();finish(false,'The graphics context was interrupted. Retry, or open local practice in your browser.');};canvas.addEventListener('webglcontextlost',contextLost);player.position.set(p.x,p.y,p.z);renderer.render(scene,camera);animation=requestAnimationFrame(loop); - return {pause(value){paused=value;redraw=true;input.previousAction=Boolean(input.action);rally?.pause(input);},setMuted(){},dispose(){cancelAnimationFrame(animation);canvas.removeEventListener('webglcontextlost',contextLost);for(const g of new Set(geometries))g.dispose();for(const m of materials)m.dispose();for(const texture of textures)texture.dispose();renderer.dispose();}}; + const controls={pause(value){paused=value;redraw=true;input.previousAction=Boolean(input.action);rally?.pause(input);},setMuted(){},dispose(){cancelAnimationFrame(animation);canvas.removeEventListener('webglcontextlost',contextLost);for(const g of new Set(geometries))g.dispose();for(const m of materials)m.dispose();for(const texture of textures)texture.dispose();renderer.dispose();}}; + if(renderer.ready)return renderer.ready().then(()=>{loading=false;paused=false;last=0;return controls;},error=>{controls.dispose();throw error;}); + return controls; } diff --git a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/games-rally.js b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/games-rally.js index b7170b7..01b8628 100644 --- a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/games-rally.js +++ b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/games-rally.js @@ -55,20 +55,20 @@ export function setupRally({scene,camera,light,high,level,player,mesh,mat,textur for(let i=0;i<5;i++){const group=i===0?player:new THREE.Group();if(i)scene.add(group);let paint=mat(paints[i]);if(high){paint.dispose();materials.splice(materials.indexOf(paint),1);paint=new THREE.MeshPhysicalMaterial({color:paints[i],metalness:.6,roughness:.2,clearcoat:1,clearcoatRoughness:.09,envMapIntensity:1.55});materials.push(paint);}cars.push(buildRallyCar({high,paint,mesh,mat,parent:group}));} if(high&&night){const headlight=new THREE.SpotLight('#dff2ff',25,35,.48,.45,1.5),target=new THREE.Object3D();headlight.position.set(1.5,.6,0);target.position.set(12,-.1,0);player.add(headlight,target);headlight.target=target;} const gateMarker=mesh(new THREE.TorusGeometry(.8,.08,6,16),mat('#a7e0a2')); - function pose(car,state,dt){car.parent.position.set(state.x,state.y+.02,state.z);car.parent.rotation.order='YXZ';car.parent.rotation.set(clamp((state.lane??0)*state.speed*.0005,-.035,.035),-state.heading,Math.atan(state.grade||0)); - for(const wheel of car.wheels){wheel.spin.rotation.z-=state.speed*dt/.375;if(wheel.front)wheel.steering.rotation.y=-(state.steer||0)*.32;}car.steering.rotation.z=-(state.steer||0)*.7; + function pose(car,state,dt){car.parent.visible=!state.finished||(state.finishAge??0)<5;car.parent.position.set(state.x,state.y+.02,state.z);car.parent.rotation.order='YXZ';car.parent.rotation.set(clamp((state.lane??0)*state.speed*.0005,-.035,.035),-state.heading,Math.atan(state.grade||0)); + for(const wheel of car.wheels){wheel.spin.rotation.z-=state.speed*dt/.375;if(wheel.front)wheel.steering.rotation.y=-(state.steer||0)*.32;}car.animate(state,dt); car.tail.emissiveIntensity=state===race.player&&race.input?.down?2.5:.4; } function render(dt){pose(cars[0],race.player,dt);race.opponents.forEach((p,i)=>pose(cars[i+1],p,dt));const p=race.player,next=rallySample(road,(p.gates+1)*road.length/12);gateMarker.position.set(next.x,next.y+1.2,next.z);gateMarker.rotation.y=-next.heading; cars[0].canopy.visible=!view.cockpit;cars[0].interior.visible=view.cockpit; - if(view.changed){camera.fov=view.cockpit?74:58;camera.near=view.cockpit?.025:.1;camera.updateProjectionMatrix();} - if(view.cockpit){player.updateMatrixWorld(true);const eye=player.localToWorld(new THREE.Vector3(-.46,1.06,.37)),target=player.localToWorld(new THREE.Vector3(24,1.06,.37));camera.position.copy(eye);camera.up.set(0,1,0);camera.lookAt(target);} + if(view.changed){camera.fov=view.cockpit?78:58;camera.near=view.cockpit?.025:.1;camera.updateProjectionMatrix();} + if(view.cockpit){player.updateMatrixWorld(true);const eye=player.localToWorld(new THREE.Vector3(-.68,1.15,.37)),target=player.localToWorld(new THREE.Vector3(24,1.11,.37));camera.position.copy(eye);camera.up.set(0,1,0);camera.lookAt(target);} else{const behind=new THREE.Vector3(p.x-Math.cos(p.heading)*10,p.y+4.2,p.z-Math.sin(p.heading)*10);if(view.changed)camera.position.copy(behind);else camera.position.lerp(behind,1-Math.exp(-dt*7));const look=rallySample(road,p.travel+8);camera.lookAt(p.x+Math.cos(p.heading)*6,(p.y+look.y)/2+.85,p.z+Math.sin(p.heading)*6);} } const p=race.player;camera.position.set(p.x-Math.cos(p.heading)*10,p.y+5.3,p.z-Math.sin(p.heading)*10);render(0); return {player:p,road,race,view,pause(input){view.previous=Boolean(input.camera);},update(input,dt){rallyViewStep(view,input.camera);race.input=input;rallyStep(race,road,input,dt);render(dt); const lap=Math.min(course.laps,Math.floor(Math.max(0,p.travel)/road.length)+1),speed=p.speed<-.2?'REVERSE':Math.round(Math.abs(p.speed)*3.6)+' km/h'; - notify(`${course.name} · ${view.cockpit?'Cockpit':'Chase'} · Place ${race.place}/5 · Lap ${lap}/${course.laps} · ${speed} · ${race.time.toFixed(1)}s · Hill ${Math.round(p.y)}m`); + notify(`${course.name} · ${view.cockpit?'Cockpit':'Chase'} · Place ${race.place}/5 · Lap ${lap}/${course.laps} · ${speed} · Gear ${p.gear===-1?'R':p.gear??1} · ${race.time.toFixed(1)}s · Hill ${Math.round(p.y)}m`); if(race.finished){const score=Math.round(600000/race.time)+(6-race.place)*500;finish(race.place===1,`${course.name}: finished ${race.place} of 5 · ${course.laps} laps in ${race.time.toFixed(1)} seconds.`,score);} }}; } diff --git a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/racing-mechanics.js b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/racing-mechanics.js index bda0ec5..30b8e47 100644 --- a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/racing-mechanics.js +++ b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/racing-mechanics.js @@ -25,8 +25,20 @@ export function rallyCornerPlan(road,travel,cruise){let target=cruise,turn=0;con const corner=Math.min(cruise,Math.sqrt(8.5/Math.max(.0001,curvature)));target=Math.min(target,Math.sqrt(corner*corner+2*10*d));} return {speed:Math.max(9,target),lane:clamp(turn*5,-2.5,2.5),turn}; } +// Separate transmission state makes the dashboard and shift animation follow actual speed. +export function rallyTransmission(state,dt){ + const speed=Math.abs(state.speed),prior=state.gear??1,limits=[0,9,17,26,35,44,56];let gear=prior; + if(state.speed<-.3)gear=-1; + else if(prior<1)gear=1; + else if(prior<6&&speed>limits[prior]+1.1)gear++; + else if(prior>1&&speed({travel:-7-i*6,lane:(i%2?1:-1)*2.8,speed:0,cruise:32+i*1.6,index:i,finished:false,steer:0,overtaking:false})); + const opponents=Array.from({length:4},(_,i)=>({travel:-7-i*7,lane:(i%2?1:-1)*3.2,laneVelocity:0,speed:0,cruise:42+i*2,index:i,finished:false,steer:0,overtaking:false,pass:null,passCooldown:0})); for(const car of opponents)Object.assign(car,rallySample(road,car.travel,car.lane)); return {player,opponents,time:0,place:1,finished:false}; } @@ -48,22 +60,30 @@ export function rallyStep(race,road,input,dt){ const delta=rallyProgress(p.previous,f.progress,road.length,Math.abs(p.speed)*h+1); if(f.distance=next&&p.travel-nextother!==car)]; - let target=plan.speed,lane=plan.lane;car.overtaking=false; + for(const car of race.opponents){if(car.finished){car.finishAge=(car.finishAge??0)+h;car.travel+=car.speed*h;Object.assign(car,rallySample(road,car.travel,car.lane));continue;} + const now=rallySample(road,car.travel),plan=rallyCornerPlan(road,car.travel,car.cruise),traffic=[{...p,lane:f.lateral},...race.opponents.filter(other=>other!==car&&!other.finished)]; + let target=plan.speed*1.12,lane=plan.lane;car.passCooldown=Math.max(0,car.passCooldown-h); + if(car.pass){car.pass.age+=h;const other=car.pass.other; + if(car.pass.age>2.5&&(car.travel>other.travel+9||car.pass.age>8)){car.pass=null;car.passCooldown=1.5;} + else lane=car.pass.lane; + } const blocker=traffic.find(other=>other.travel-car.travel>0&&other.travel-car.travelMath.abs(a-car.lane)-Math.abs(b-car.lane)); + if(blocker&&!car.pass&&car.passCooldown===0){const choices=[-4.5,4.5].sort((a,b)=>Math.abs(a-car.lane)-Math.abs(b-car.lane)); const clear=choices.find(candidate=>traffic.every(other=>Math.abs(other.travel-car.travel)>12||Math.abs(other.lane-candidate)>2.35)); - if(clear!==undefined){lane=clear;car.overtaking=true;}else target=Math.min(target,Math.max(0,blocker.speed-1)); + if(clear!==undefined){lane=clear;car.pass={lane,other:blocker.index===undefined?p:blocker,age:0};}else target=Math.min(target,Math.max(0,blocker.speed-1)); } - // Blend passing and apex lines instead of teleporting laterally. - const priorLane=car.lane;car.lane+=clamp(lane-car.lane,-2.8*h,2.8*h);car.lane=clamp(car.lane,-road.width/2+1.5,road.width/2-1.5); + car.overtaking=Boolean(car.pass); + // A damped lane controller and latched pass prevent substep left/right hunting. + car.laneVelocity+=clamp((lane-car.lane)*3-car.laneVelocity*3.2,-4,4)*h; + car.laneVelocity=clamp(car.laneVelocity,-2.4,2.4);car.lane+=car.laneVelocity*h;car.lane=clamp(car.lane,-road.width/2+1.5,road.width/2-1.5); for(const other of traffic){const gap=other.travel-car.travel;if(gap>0&&gap<5&&Math.abs(other.lane-car.lane)<2.2)target=Math.min(target,Math.max(0,other.speed-2));} - car.speed+=clamp(target-car.speed,-12*h,(9.3-Math.max(0,now.grade)*9.81)*h);car.speed=Math.max(0,car.speed);car.travel+=car.speed*h; - Object.assign(car,rallySample(road,car.travel,car.lane));car.heading+=Math.atan2((car.lane-priorLane)/h,Math.max(1,car.speed));car.steer=clamp(plan.turn,-.45,.45); + car.speed+=clamp(target-car.speed,-14*h,(11.5-Math.max(0,now.grade)*9.81)*h);car.speed=Math.max(0,car.speed);car.travel+=car.speed*h; + const smoothHeading=car.heading;Object.assign(car,rallySample(road,car.travel,car.lane));const ahead=rallySample(road,car.travel+2,car.lane),desired=Math.atan2(ahead.z-car.z,ahead.x-car.x)+Math.atan2(car.laneVelocity,Math.max(5,car.speed)); + car.heading=smoothHeading+angle(desired-smoothHeading)*(1-Math.exp(-8*h));car.steer+=(clamp(plan.turn,-.45,.45)-car.steer)*(1-Math.exp(-6*h));rallyTransmission(car,h); if(car.travel>=finish){car.finished=true;car.finishTime=race.time;} } - for(const car of race.opponents){const dx=p.x-car.x,dz=p.z-car.z,d=Math.hypot(dx,dz),radius=2.5;if(d1e-6){const overlap=radius-d;p.x+=dx/d*overlap*.65;p.z+=dz/d*overlap*.65;car.lane=clamp(car.lane-(dx/d*-f.tz+dz/d*f.tx)*overlap*.35,-road.width/2+1.5,road.width/2-1.5); + rallyTransmission(p,h); + for(const car of race.opponents){if(car.finished)continue;const dx=p.x-car.x,dz=p.z-car.z,d=Math.hypot(dx,dz),radius=2.5;if(d1e-6){const overlap=radius-d;p.x+=dx/d*overlap*.65;p.z+=dz/d*overlap*.65;car.lane=clamp(car.lane-(dx/d*-f.tz+dz/d*f.tx)*overlap*.35,-road.width/2+1.5,road.width/2-1.5); const closing=p.speed*Math.cos(p.heading-car.heading)-car.speed;if(closing>0){p.speed-=closing*.45;car.speed+=closing*.2;}} } race.place=1+race.opponents.filter(car=>car.finished||car.travel>p.travel).length; diff --git a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/racing-mechanics.test.js b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/racing-mechanics.test.js index 8c73ddb..70127fc 100644 --- a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/racing-mechanics.test.js +++ b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/racing-mechanics.test.js @@ -2,16 +2,24 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import * as THREE from 'three'; import {RALLY_COURSES,marbleSurfaces,marblePath} from './game-mechanics.js'; -import {rallyRoad,rallySample,rallyProject,rallyProgress,newRallyRace,rallyStep,rallyCornerPlan,newRallyView,rallyViewStep} from './racing-mechanics.js'; +import {rallyRoad,rallySample,rallyProject,rallyProgress,newRallyRace,rallyStep,rallyCornerPlan,newRallyView,rallyViewStep,rallyTransmission} from './racing-mechanics.js'; import {rallyRibbon,buildRallyCar} from './games-rally.js'; import {rallyTerrain,mountainHeight} from './rally-terrain.js'; import {marbleBoardGeometry} from './track-geometry.js'; test('racing roads are wider, genuinely inclined, and retain consistent projection',()=>{ - for(const course of RALLY_COURSES){const road=rallyRoad(course);assert.ok(course.width>=13);assert.ok(road.length>680);assert.equal(course.heights.length,course.points.length);assert.ok(Math.max(...course.heights)-Math.min(...course.heights)>=3); + for(const course of RALLY_COURSES){const road=rallyRoad(course);assert.ok(course.width>=18);assert.ok(road.length>680);assert.equal(course.heights.length,course.points.length);assert.ok(Math.max(...course.heights)-Math.min(...course.heights)>=3); for(let d=0;d{ + const road=rallyRoad(RALLY_COURSES[0]),race=newRallyRace(road),car=race.opponents[0];Object.assign(car,rallySample(road,20,0),{travel:20,lane:0,speed:28,laneVelocity:0});Object.assign(race.player,rallySample(road,35,0),{travel:35,previous:35,speed:6});let pass=null,age=0,maxAcceleration=0; + for(let i=0;i<180;i++){const prior=car.laneVelocity;rallyStep(race,road,{},1/90);maxAcceleration=Math.max(maxAcceleration,Math.abs(car.laneVelocity-prior)*90);if(car.pass&&!pass){pass=car.pass;age=car.pass.age;}if(pass&&car.pass&&car.pass.age<2.5){assert.equal(car.pass.lane,pass.lane);assert.ok(car.pass.age>=age);age=car.pass.age;}} + assert.ok(pass,'a passing maneuver was started');assert.ok(maxAcceleration<=4.001);assert.ok(car.cruise>=42); +}); +test('transmission shifts with hysteresis, reports reverse and drives cockpit animation',()=>{ + const p={speed:12,gear:1};rallyTransmission(p,1/60);assert.equal(p.gear,2);assert.equal(p.shift,.32);p.speed=10;rallyTransmission(p,.1);assert.equal(p.gear,2);assert.ok(p.shift<.32);p.speed=5;rallyTransmission(p,.1);assert.equal(p.gear,1);p.speed=-2;rallyTransmission(p,.1);assert.equal(p.gear,-1);assert.ok(p.rpm>=900&&p.rpm<=7400); +}); test('rally progress unwraps forward and reverse at the line and rejects teleports',()=>{ assert.equal(rallyProgress(299,1,300,3),2);assert.equal(rallyProgress(1,299,300,3),-2);assert.equal(rallyProgress(10,170,300,3),0); }); diff --git a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-babylon.js b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-babylon.js new file mode 100644 index 0000000..393de1a --- /dev/null +++ b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-babylon.js @@ -0,0 +1,70 @@ +import {Engine} from '@babylonjs/core/Engines/engine.js'; +import {Scene} from '@babylonjs/core/scene.js'; +import {FreeCamera} from '@babylonjs/core/Cameras/freeCamera.js'; +import {Vector3,Quaternion} from '@babylonjs/core/Maths/math.vector.js'; +import {Color3,Color4} from '@babylonjs/core/Maths/math.color.js'; +import {Mesh} from '@babylonjs/core/Meshes/mesh.js'; +import {VertexData} from '@babylonjs/core/Meshes/mesh.vertexData.js'; +import {TransformNode} from '@babylonjs/core/Meshes/transformNode.js'; +import {PBRMaterial} from '@babylonjs/core/Materials/PBR/pbrMaterial.js'; +import {HemisphericLight} from '@babylonjs/core/Lights/hemisphericLight.js'; +import {DirectionalLight} from '@babylonjs/core/Lights/directionalLight.js'; +import {ShadowGenerator} from '@babylonjs/core/Lights/Shadows/shadowGenerator.js'; +import {DefaultRenderingPipeline} from '@babylonjs/core/PostProcesses/RenderPipeline/Pipelines/defaultRenderingPipeline.js'; +import {RawTexture} from '@babylonjs/core/Materials/Textures/rawTexture.js'; +import {RawCubeTexture} from '@babylonjs/core/Materials/Textures/rawCubeTexture.js'; +import {DynamicTexture} from '@babylonjs/core/Materials/Textures/dynamicTexture.js'; +import {Texture} from '@babylonjs/core/Materials/Textures/texture.js'; +import {Constants} from '@babylonjs/core/Engines/constants.js'; +import {ImageProcessingConfiguration} from '@babylonjs/core/Materials/imageProcessingConfiguration.js'; +import {Material} from '@babylonjs/core/Materials/material.js'; +import {createGameRenderer} from './game-renderer.js'; + +// High racing uses Babylon's PBR/render pipeline. The shared scene authoring and +// physics stay engine-independent; Light mode retains the low-cost renderer. +export async function createRallyRenderer(canvas,onGraphics){ + const probe=canvas.getContext('webgl2',{alpha:false,antialias:true,preserveDrawingBuffer:true,powerPreference:'high-performance'}); + if(!probe)return createGameRenderer(canvas,'light',onGraphics); + let engine; + try{engine=new Engine(canvas,true,{alpha:false,preserveDrawingBuffer:true,stencil:true,powerPreference:'high-performance'});}catch(error){onGraphics?.(`High graphics unavailable: ${error.message}`);throw error;} + const ratio=Math.min(2,globalThis.devicePixelRatio||1);engine.setHardwareScalingLevel(1/ratio);engine.setSize(Math.round(800*ratio),Math.round(500*ratio)); + const scene=new Scene(engine);scene.useRightHandedSystem=true;scene.skipPointerMovePicking=true; + const camera=new FreeCamera('racing-camera',new Vector3(0,3,10),scene);camera.inputs.clear();camera.minZ=.025;camera.maxZ=650; + const sky=new HemisphericLight('sky-fill',new Vector3(0,1,0),scene);sky.intensity=.85;sky.groundColor=new Color3(.14,.17,.15); + const sun=new DirectionalLight('sun',new Vector3(-.45,-1,-.35),scene);sun.intensity=2.8;sun.shadowMinZ=1;sun.shadowMaxZ=160;sun.autoUpdateExtends=false;sun.orthoLeft=-34;sun.orthoRight=34;sun.orthoBottom=-34;sun.orthoTop=34; + const shadows=new ShadowGenerator(2048,sun);shadows.useBlurExponentialShadowMap=true;shadows.blurKernel=24;shadows.bias=.00035;shadows.normalBias=.03; + const pipeline=new DefaultRenderingPipeline('racing-hdr',true,scene,[camera]);pipeline.samples=4;pipeline.fxaaEnabled=true;pipeline.bloomEnabled=true;pipeline.bloomThreshold=1.5;pipeline.bloomWeight=.12;pipeline.bloomKernel=48; + scene.imageProcessingConfiguration.toneMappingEnabled=true;scene.imageProcessingConfiguration.toneMappingType=ImageProcessingConfiguration.TONEMAPPING_ACES;scene.imageProcessingConfiguration.exposure=1.1;scene.imageProcessingConfiguration.contrast=1.12; + // Original offline reflection cube. No CDN requests or downloaded artwork. + const side=32,faces=[]; + for(let face=0;face<6;face++){const data=new Uint8Array(side*side*4);for(let y=0;ynew Color3(c?.r??1,c?.g??1,c?.b??1); + function texture(source){if(!source)return null;if(textures.has(source))return textures.get(source);const image=source.image;let result; + if(image?.data){result=new RawTexture(image.data,image.width,image.height,Constants.TEXTUREFORMAT_RGBA,scene,true,false,Texture.TRILINEAR_SAMPLINGMODE);} + else if(image?.getContext){result=new DynamicTexture('instrument-screen',image,scene,true);dynamic.push(result);result.update(true);} + if(result){result.wrapU=source.wrapS===1000?Texture.WRAP_ADDRESSMODE:Texture.CLAMP_ADDRESSMODE;result.wrapV=source.wrapT===1000?Texture.WRAP_ADDRESSMODE:Texture.CLAMP_ADDRESSMODE;result.uScale=source.repeat?.x??1;result.vScale=source.repeat?.y??1;textures.set(source,result);}return result; + } + function material(source){if(materials.has(source))return materials.get(source);const m=new PBRMaterial('gt-material',scene);m.albedoColor=color(source.color);m.metallic=source.metalness??0;m.roughness=source.roughness??.7;m.emissiveColor=color(source.emissive).scale(source.emissiveIntensity??0);m.alpha=source.opacity??1;m.backFaceCulling=source.side!==2;m.sideOrientation=Material.CounterClockWiseSideOrientation;m.useVertexColors=Boolean(source.vertexColors);m.albedoTexture=texture(source.map);m.emissiveTexture=texture(source.emissiveMap);m.bumpTexture=texture(source.bumpMap);if(m.bumpTexture)m.bumpTexture.level=source.bumpScale??.02; + if(source.clearcoat){m.clearCoat.isEnabled=true;m.clearCoat.intensity=source.clearcoat;m.clearCoat.roughness=source.clearcoatRoughness??.12;}if(source.transparent){m.transparencyMode=PBRMaterial.PBRMATERIAL_ALPHABLEND;m.separateCullingPass=true;}materials.set(source,m);return m; + } + function convert(source,parent=null){let target; + if(source.isMesh){target=new Mesh(source.name||'gt-mesh',scene);const g=source.geometry,data=new VertexData();data.positions=Array.from(g.attributes.position.array);if(g.attributes.normal)data.normals=Array.from(g.attributes.normal.array);if(g.attributes.uv)data.uvs=Array.from(g.attributes.uv.array);if(g.attributes.color){data.colors=[];const a=g.attributes.color;for(let i=0;ii);data.applyToMesh(target);target.material=material(source.material);target.receiveShadows=true;if(source.castShadow)shadows.addShadowCaster(target);} + else if(source.isLight||source.isCamera)return; + else target=new TransformNode(source.name||'gt-group',scene); + target.parent=parent;objects.set(source,target);for(const child of source.children)convert(child,target); + } + let initialized=false,disposed=false; + onGraphics?.('High quality · Babylon.js · PBR lighting · HDR · soft shadows'); + return {software:false,babylon:true,ready(){return scene.whenReadyAsync(true);},render(source,view){if(disposed)return; + if(!initialized){convert(source);initialized=true;} + for(const [object,target] of objects){target.position.set(object.position.x,object.position.y,object.position.z);target.scaling.set(object.scale.x,object.scale.y,object.scale.z);target.rotationQuaternion??=new Quaternion();target.rotationQuaternion.copyFromFloats(object.quaternion.x,object.quaternion.y,object.quaternion.z,object.quaternion.w);target.setEnabled(object.visible);} + for(const [source,m] of materials){const e=source.emissive,k=source.emissiveIntensity??0;m.emissiveColor.copyFromFloats((e?.r??0)*k,(e?.g??0)*k,(e?.b??0)*k);} + for(const texture of dynamic)texture.update(true); + camera.position.set(view.position.x,view.position.y,view.position.z);camera.rotationQuaternion??=new Quaternion();camera.rotationQuaternion.copyFromFloats(view.quaternion.x,view.quaternion.y,view.quaternion.z,view.quaternion.w);camera.fov=view.fov*Math.PI/180;camera.minZ=view.near; + const directional=source.children.find(child=>child.isDirectionalLight);if(directional){sun.position.set(directional.position.x,directional.position.y,directional.position.z);const t=directional.target.position;sun.direction=new Vector3(t.x-sun.position.x,t.y-sun.position.y,t.z-sun.position.z).normalize();sun.diffuse=color(directional.color);sun.intensity=directional.intensity*.85;} + const bg=source.fog?.color;scene.clearColor=new Color4(bg?.r??.4,bg?.g??.6,bg?.b??.8,1);scene.fogMode=Scene.FOGMODE_LINEAR;scene.fogColor=color(bg);scene.fogStart=source.fog?.near??180;scene.fogEnd=source.fog?.far??540; + scene.render(); + },dispose(){disposed=true;scene.dispose();engine.dispose();}}; +} diff --git a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-car.js b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-car.js index 59e9610..de17c65 100644 --- a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-car.js +++ b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-car.js @@ -35,11 +35,61 @@ export function buildRallyCar({high,paint,mesh,mat,parent}){ mesh(round(.19,.045,1.66,.02),paint,-1.95,.84,0,parent); for(const z of [-.69,.69]){const front=mesh(round(.1,.09,.38,.03),lamp,2.13,.65,z,parent);front.rotation.x=Math.sign(z)*.12;mesh(round(.065,.075,.53,.028),tail,-2.22,.66,z,parent);mesh(new THREE.CylinderGeometry(.085,.085,.21,12),alloy,-2.30,.31,z*.78,parent).rotation.z=Math.PI/2;} const interior=new THREE.Group();parent.add(interior); - mesh(round(.42,.19,1.40,.05),leather,.27,.79,0,interior);mesh(round(.055,.1,.26,.025),trim,.035,.94,.37,interior); - const screen=mat('#72bcc1');screen.emissive.set('#6dbabd');screen.emissiveIntensity=.45;mesh(new THREE.BoxGeometry(.012,.064,.19),screen,.004,.943,.37,interior); - mesh(round(.6,.1,.24,.04),trim,-.4,.58,0,interior); + const stitch=mat('#8b949a'),display=mat('#080e14');display.roughness=.35; + const screen=mat('#81bcc2');screen.emissive.set('#84d8db');screen.emissiveIntensity=.8; + // Full dark cabin lining prevents exterior body paint showing through the footwell. + mesh(round(1.8,.045,1.65,.025),rubber,-.43,.845,0,interior); + mesh(round(.53,.32,1.47,.10),leather,.27,.86,0,interior); + mesh(round(.08,.055,1.41,.02),alloy,-.02,.85,0,interior); + mesh(round(.12,.24,.38,.065),leather,.055,.98,.37,interior); + mesh(round(.022,.195,.32,.04),display,-.018,.975,.37,interior); + const needles=[]; + for(const z of [.285,.455]){ + const face=mesh(new THREE.CircleGeometry(.073,high?40:16),display,-.033,.98,z,interior);face.rotation.y=-Math.PI/2; + const ring=mesh(new THREE.TorusGeometry(.076,.004,5,high?40:16),alloy,-.035,.98,z,interior);ring.rotation.y=Math.PI/2; + for(let i=0;i<11;i++){const a=-2.35+i*.47,tick=mesh(new THREE.BoxGeometry(.004,.003,.009),screen,-.038,.98+Math.cos(a)*.063,z+Math.sin(a)*.063,interior);tick.rotation.x=-a;} + const needle=new THREE.Group();needle.position.set(-.040,.98,z);interior.add(needle);mesh(round(.003,.052,.003,.001),tail,0,.024,0,needle);needles.push(needle); + } + const centerDisplay=mesh(round(.018,.16,.28,.018),display,-.011,.98,-.17,interior); + let dashboardTexture=null,dashboardCanvas=null; + if(typeof document!=='undefined'){ + dashboardCanvas=document.createElement('canvas');dashboardCanvas.width=512;dashboardCanvas.height=256;dashboardTexture=new THREE.CanvasTexture(dashboardCanvas);dashboardTexture.colorSpace=THREE.SRGBColorSpace;const m=screen.clone();m.color.set('#ffffff');m.emissive.set('#ffffff');m.map=dashboardTexture;m.emissiveMap=dashboardTexture;centerDisplay.material=m; + // Face the driver (-X); a real screen shows speed, gear, RPM and shift indication. + centerDisplay.geometry.dispose();centerDisplay.geometry=new THREE.PlaneGeometry(.27,.145);centerDisplay.rotation.y=-Math.PI/2;centerDisplay.position.x=-.023; + } + for(const z of [-.61,-.39,.64]){ + const vent=mesh(new THREE.TorusGeometry(.05,.008,6,high?32:12),alloy,-.022,.88,z,interior);vent.rotation.y=Math.PI/2; + for(let j=-2;j<=2;j++)mesh(new THREE.BoxGeometry(.005,.005,.083),trim,-.034,.88+j*.014,z,interior); + } + mesh(round(.94,.12,.31,.045),leather,-.55,.87,0,interior); + const shifter=new THREE.Group();shifter.position.set(-.20,.86,0);interior.add(shifter);mesh(round(.105,.018,.105,.02),alloy,0,0,0,shifter);rod([0,0,0],[0,.09,0],.012,alloy,shifter);mesh(new THREE.SphereGeometry(.035,12,8),leather,0,.105,0,shifter); + for(const z of [-.76,.76]){ + mesh(round(1.35,.28,.045,.025),leather,-.59,.95,z,interior); + mesh(round(.46,.04,.085,.02),alloy,-.60,.97,z*.94,interior); + mesh(round(.11,.025,.045,.01),alloy,-.16,1.05,z*.95,interior); + mesh(round(1.15,.007,.007,.003),stitch,-.64,1.07,z*.985,interior); + } for(const z of [-.37,.37]){mesh(round(.55,.13,.48,.06),leather,-.73,.46,z,interior);const seat=mesh(round(.14,.55,.48,.065),leather,-1.0,.72,z,interior);seat.rotation.z=.16;} - const steering=new THREE.Group();steering.position.set(.0,.88,.37);steering.rotation.y=Math.PI/2;interior.add(steering);mesh(new THREE.TorusGeometry(.16,.022,6,high?24:12),leather,0,0,0,steering);mesh(round(.18,.035,.035,.01),alloy,0,-.025,0,steering); + const steering=new THREE.Group();steering.position.set(-.16,.99,.37);steering.rotation.y=Math.PI/2;interior.add(steering);mesh(new THREE.TorusGeometry(.16,.021,high?10:6,high?48:12),leather,0,0,0,steering); + mesh(round(.13,.067,.035,.02),leather,0,-.01,0,steering); + for(const sign of [-1,1]){const spoke=mesh(round(.10,.018,.023,.006),alloy,sign*.09,-.025,0,steering);spoke.rotation.z=sign*-.20;mesh(round(.045,.07,.015,.007),alloy,sign*.11,.025,-.032,steering);} + mesh(round(.023,.10,.025,.006),alloy,0,-.09,0,steering); + const glove=mat('#343b46'),suit=mat('#242e3e'),leftHand=new THREE.Group(),rightHand=new THREE.Group();interior.add(leftHand,rightHand); + function hand(group){const palm=mesh(new THREE.SphereGeometry(.038,12,8),glove,0,0,0,group);palm.scale.set(.9,1.2,.65);for(let i=0;i<4;i++)mesh(round(.013,.041,.016,.006),glove,.021-i*.015,.025,-.015,group);mesh(round(.075,.045,.048,.018),suit,-.075,-.01,0,group);} + hand(leftHand);hand(rightHand); + const cockpitFrame=new THREE.Group();interior.add(cockpitFrame); + for(const z of [-1,1]){rod([.65,.86,z*.74],[-.12,1.25,z*.65],.026,leather,cockpitFrame);mesh(round(.68,.035,.045,.01),leather,-.45,1.23,z*.63,cockpitFrame);} + // Interior rear-view mirror, roof liner and sun visors, without hiding the road. + mesh(round(.50,.025,1.25,.015),leather,-.86,1.245,0,cockpitFrame);mesh(round(.09,.065,.21,.012),alloy,.26,1.18,0,cockpitFrame); + for(const z of [-.36,.36])mesh(round(.15,.016,.25,.006),leather,-.03,1.22,z,cockpitFrame); + function animate(state,dt){ + steering.rotation.z=-(state.steer||0)*.85; + needles[0].rotation.x=2.35-Math.min(1,(state.rpm??900)/7500)*4.7;needles[1].rotation.x=2.35-Math.min(1,Math.abs(state.speed)/60)*4.7; + const phase=state.shift>0?Math.sin(Math.PI*(1-state.shift/.32)):0;shifter.rotation.z=phase*.22; + leftHand.position.set(-.21,1.025,.515);rightHand.position.set(-.21+phase*.01,1.025-phase*.055,.225-phase*.21); + leftHand.rotation.x=-(state.steer||0)*.5;rightHand.rotation.x=-(state.steer||0)*.5*(1-phase); + if(dashboardCanvas){const c=dashboardCanvas.getContext('2d');c.fillStyle='#07131d';c.fillRect(0,0,512,256);c.fillStyle='#b2eef1';c.font='bold 90px sans-serif';c.fillText(state.gear===-1?'R':String(state.gear??1),24,108);c.font='bold 52px sans-serif';c.fillText(String(Math.round(Math.abs(state.speed)*3.6)).padStart(3,'0'),170,97);c.font='22px sans-serif';c.fillText('km/h',178,129);c.fillText(`${Math.round(state.rpm??900)} RPM`,24,193);c.fillStyle=phase>0?'#ffb955':'#66c9c9';c.fillRect(24,216,Math.min(464,(state.rpm??900)/7500*464),9);dashboardTexture.needsUpdate=true;} + } const wheels=[];for(const x of [-1.28,1.32])for(const z of [-.96,.96]){const axle=new THREE.Group();axle.position.set(x,.37,z);parent.add(axle);const spin=new THREE.Group();axle.add(spin); const tire=mesh(new THREE.TorusGeometry(.27,.105,high?12:6,high?32:12),rubber,0,0,0,spin);tire.scale.z=1.30; const rim=mesh(new THREE.CylinderGeometry(.225,.225,.20,high?32:10),alloy,0,0,0,spin);rim.rotation.x=Math.PI/2; @@ -48,5 +98,5 @@ export function buildRallyCar({high,paint,mesh,mat,parent}){ mesh(round(.075,.16,.045,.015),tail,.12,.04,-Math.sign(z)*.09,axle); }wheels.push({steering:axle,spin,front:x>0}); } - return {parent,wheels,tail,canopy,interior,steering,body}; + return {parent,wheels,tail,canopy,interior,steering,body,shifter,needles,animate,dashboardTexture}; } diff --git a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/vite.config.js b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/vite.config.js index ab29491..f0d6fb0 100644 --- a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/vite.config.js +++ b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/vite.config.js @@ -6,6 +6,8 @@ export default defineConfig(({ mode }) => ({ base: mode === 'desktop' ? './' : '/', publicDir: mode === 'desktop' ? false : 'public', define: { 'import.meta.env.VITE_LOCAL_APP': JSON.stringify(mode === 'desktop' ? 'true' : 'false') }, - build: mode === 'desktop' ? { outDir: 'dist-local', rollupOptions: { input: 'local.html' } } : {}, + build: { ...(mode === 'desktop' ? { outDir: 'dist-local' } : {}), + rolldownOptions:{...(mode === 'desktop' ? {input:'local.html'} : {}),output:{codeSplitting:{groups:[{name:'racing-engine',test:/@babylonjs\/core/,priority:20}]}}}, + }, })); diff --git a/local/CHANGELOG.md b/local/CHANGELOG.md index ec98ad9..33fb9bd 100644 --- a/local/CHANGELOG.md +++ b/local/CHANGELOG.md @@ -1,5 +1,13 @@ # BeeBright releases +## 2.5 racing beta + +- Tracks widened to 18–20 metres, with faster rivals and stable, committed overtaking instead of rapid side-to-side lane changes. +- Finished opponents clear the racing line instead of blocking the finish. +- High racing now uses Babylon.js PBR, HDR tone mapping, clearcoat reflections, bloom and soft shadows. Lightweight graphics remains available. +- Full cockpit lining, doors, vents, roof trim, instruments, steering and driver hands. Automatic gearbox with shift animations, a live speed/gear screen and moving tachometer. +- Private standalone racing testing edition with a bundled Windows executable and automatic source synchronization. + ## 2.4 fixed - Original logo-free GT cars with long bonnets, fastback roofs, wide wheel haunches, detailed wheels and a driving cockpit. diff --git a/local/release.json b/local/release.json index a4d6714..af49dc8 100644 --- a/local/release.json +++ b/local/release.json @@ -1 +1 @@ -{"name":"BeeBright","version":"2.4 fixed"} +{"name":"BeeBright","version":"2.5 racing beta"} diff --git a/racing-beta/README.md b/racing-beta/README.md new file mode 100644 index 0000000..a6846b6 --- /dev/null +++ b/racing-beta/README.md @@ -0,0 +1,13 @@ +# Beta Car Racing Game + +Private testing edition of BeeBright's racing game. Download the latest **Beta-Car-Racing-Game.exe** from this repository's Releases and double-click it. No Python installation, login, admin account, cloud save or public website is required. Windows 10/11 with Microsoft Edge WebView2 Runtime is supported. Windows can warn about an unsigned beta executable; this build is not code-signed. + +Choose Harbor Circuit, Canyon Switchback, Alpine Hairpins or Midnight Metro, then Start race. WASD/arrows drive and reverse, R switches cockpit/chase camera, and Escape pauses. High graphics uses Babylon.js with clearcoat paint, HDR tone mapping, bloom, reflections and soft shadows. Lightweight graphics retains a fallback for weaker devices. A transmission animates shifts, driver hands and dashboard gauges. + +The game uses the exact same racing mechanics and models as BeeBright. Its private Actions workflow checks the public canonical repository every 15 minutes and on manual dispatch. When racing source changes, it copies only racing files, tests them, builds and actually launches the Windows executable, then publishes a private prerelease. Updates are downloaded from Releases; existing executables do not silently update themselves. GitHub can delay scheduled runs. + +For development: install Node 22.12 or newer, run `npm ci`, `npm test`, then `npm run dev`. `npm run build` creates the offline interface. On Windows, install Python 3.14, `pip install pywebview==6.2.1 pythonnet==3.2.0 pyinstaller==6.22.3`, then `python -m PyInstaller --onefile --name Beta-Car-Racing-Game --collect-all webview --collect-all clr_loader --add-data "dist;dist" launcher.py`. The downloadable executable bundles Python; it does not install a shared runtime on your computer. + +Original unbranded cars and procedural art; no Mercedes logos or Assetto Corsa assets. This remains a small racing beta, with visual fidelity below a commercial racing simulator. Source and original content retain BeeBright's Creative Commons Attribution-NonCommercial-ShareAlike 4.0 license. Babylon.js (Apache-2.0), Three.js (MIT), pywebview (BSD), Python and other dependencies retain their own licenses, bundled in THIRD_PARTY_NOTICES.txt. + +Uninstall: delete the downloaded executable. It keeps no accounts, cloud data or saved racing progress. No administrator rights or system Python removal is needed. diff --git a/racing-beta/launcher.py b/racing-beta/launcher.py new file mode 100644 index 0000000..4de0ba5 --- /dev/null +++ b/racing-beta/launcher.py @@ -0,0 +1,75 @@ +"""Frozen standalone racing beta. Serves only bundled assets on loopback.""" +from __future__ import annotations +import argparse, functools, http.server, json, os, sys, threading, time +from pathlib import Path + +def main(): + parser=argparse.ArgumentParser();parser.add_argument('--self-test',action='store_true');args=parser.parse_args() + root=Path(getattr(sys,'_MEIPASS',Path(__file__).parent))/'dist' + if not (root/'index.html').is_file():raise RuntimeError('Bundled racing interface is missing.') + class Handler(http.server.SimpleHTTPRequestHandler): + # Windows registry associations can otherwise serve JS as text/plain. + extensions_map={**http.server.SimpleHTTPRequestHandler.extensions_map,'.js':'text/javascript','.mjs':'text/javascript','.css':'text/css','.json':'application/json'} + def log_message(self,format,*values): + if args.self_test:print('Asset request:',format%values,flush=True) + def do_GET(self): + if self.path.split('?')[0].endswith('.py'):self.send_error(404);return + super().do_GET() + class AssetServer(http.server.ThreadingHTTPServer): + request_queue_size=128 + server=AssetServer(('127.0.0.1',0),functools.partial(Handler,directory=str(root))) + server.daemon_threads=True;threading.Thread(target=server.serve_forever,daemon=True).start() + import webview + url=f'http://127.0.0.1:{server.server_port}/index.html' + window=webview.create_window('Beta Car Racing Game · 0.1.0',url,width=1280,height=900,min_size=(850,650)) + failure=[] + def test(): + try: + # Validate the same HTTP asset paths consumed by the embedded browser. + import urllib.request + for file in (root/'assets').glob('*.js'): + with urllib.request.urlopen(url.rsplit('/',1)[0]+'/assets/'+file.name) as response: + assert response.status==200 and 'javascript' in response.headers.get('Content-Type',''),file.name + end=time.monotonic()+40 + while time.monotonic(){const source=document.querySelector('canvas'),copy=document.createElement('canvas');copy.width=800;copy.height=500;const c=copy.getContext('2d');c.drawImage(source,0,0,800,500);const pixels=c.getImageData(0,0,800,500).data,colors=new Set();let bright=0;for(let i=0;i90)bright++;}window.racingCapture={url:copy.toDataURL('image/png'),colors:colors.size,bright};})") + end=time.monotonic()+15 + while time.monotonic()100 and capture['bright']>100, f'Blank rendered racing frame: {capture and (capture["colors"],capture["bright"])}' + folder=os.environ.get('RACING_TEST_OUTPUT') + if folder: + import base64 + Path(folder).mkdir(parents=True,exist_ok=True) + data=capture['url'].split(',')[1] + Path(folder,f'{quality}-cockpit.png').write_bytes(base64.b64decode(data)) + print('Executable UI passed:',quality,window.evaluate_js("document.querySelector('#graphics').innerText"),flush=True) + except Exception as error:failure.append(str(error));print('Self-test failed:',error,flush=True) + finally:window.destroy() + try:webview.start(test if args.self_test else None,gui='edgechromium' if sys.platform=='win32' else None,private_mode=True) + finally:server.shutdown();server.server_close() + if failure:raise SystemExit(1) + +if __name__=='__main__':main() diff --git a/racing-beta/package-lock.json b/racing-beta/package-lock.json new file mode 100644 index 0000000..3c4a9d9 --- /dev/null +++ b/racing-beta/package-lock.json @@ -0,0 +1,817 @@ +{ + "name": "beta-car-racing-game", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "beta-car-racing-game", + "version": "0.1.0", + "dependencies": { + "@babylonjs/core": "9.30.0", + "three": "0.186.1" + }, + "devDependencies": { + "vite": "8.3.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@babylonjs/core": { + "version": "9.30.0", + "resolved": "https://registry.npmjs.org/@babylonjs/core/-/core-9.30.0.tgz", + "integrity": "sha512-rV+kc7B9oEtREaWh0YYNinoqYshRbHLHj7t4s4woDjDXFXWKHu1d8S914CNfs3KTOI8V8DvoVKLe2yDHZXfZEA==", + "license": "Apache-2.0" + }, + "node_modules/@oxc-project/types": { + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.153.0.tgz", + "integrity": "sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.13.tgz", + "integrity": "sha512-Vu/NICrgdRvUOeaJZIyB2FP63s6fO/4ud5l8dXmxDx9yDyn+mN0y+gsvsVviSli+T361Aqo0h46EuMoobrwawA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.13.tgz", + "integrity": "sha512-6LEoIrGy5vvCd2boNnv2soPSUZj/PRvhp8R/t9KlX+mIZxThIE/ZqxrGXxmuNX0FuvQFvLdZr7S4HrYpYBVhEQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.13.tgz", + "integrity": "sha512-CaJSRzlQ6Q+zkDMVA9OkVxBjVoPRCDEE3v0FSJ9kIQBgRjFApA3Si4ijhbNVUji7ZJt+PfO0e46qy4nPInHUPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.13.tgz", + "integrity": "sha512-e4r340I5hxSzJkD3YJBxgzq/rJ3wwxwdLLb2FPt1Zpyem20nrQPT11DDEEkazwxaHumH8wpglsp/Ozgj/wT6mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.13.tgz", + "integrity": "sha512-hoBWDZ4/K/mJKVMbsgUd1jfpA2DiOrJ54mpegObU9TBHBBrtX4FLIXJJQRDPZ/2Dn91GvPob+Z2hwKJzf4E5SQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.13.tgz", + "integrity": "sha512-oF9fZu3vTWnTiVAkDFxU9PJLt1nh20K9RRpuHsR7ZnE2L7XookBbDBVLojCVSLGbvtSlxeUNnjAj6DbNsKyzbg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.13.tgz", + "integrity": "sha512-DTpb/+jMiqWMPgphn4ksW1zW+XLJ6dJJ9FMvnIk9I291LhCctMPhMYJrP5t9p6C9qa3KXadDdUEZHBaPh3kNxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.13.tgz", + "integrity": "sha512-xU+Kvv9N2A3SY35zFh1FWWNjxbadE/BKeqnkXe7vScU4GZsw757BlVmDLITZEq8DPpk6m7uoDYkK7P+x/uU5Ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.13.tgz", + "integrity": "sha512-XMk/hnhh25oQ756Lclb1Q11h4ll0xC0Esa5w4Vzp+mAE2S3AGJgeM0SonrIgY8fHhjarb6hcoTuwoFG0VVP2ig==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.13.tgz", + "integrity": "sha512-U1BoTHYmlywE2n3HUWjzZ+8EQsKr+OwpxYuDWaubx42tUL8QSL7nOqRz/NVHanibbFP3M1Rxuprz/wNcbMKjqw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.13.tgz", + "integrity": "sha512-J4cm80Pc6wEwV5AZ42o62sIQgDk+Y7R2s646jIwuvO+4V46QB58N4vVuMPWCBaNgursXLpmUWJV//WQAkPvHBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.13.tgz", + "integrity": "sha512-W0euZS40U4V6JkmEnVCIX9mcsL6cbXQ4X8c2w+MMOIjFg68TXUW+u3RT+2e1t1pv9mg/EP3pDtK3POXdQ7Rxyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.13.tgz", + "integrity": "sha512-do/AF/4n9MRshia3a5aXoy8xM2c6mgfT6D7H/dY6my5z+hfCIR4ab7mk4IRj++nsEQpKy39kd0DQnDq+r692tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.13.tgz", + "integrity": "sha512-6FtUf2cJZobutJdxnGYX8z0vtbgqU0b2v8vfBhkXb9wp2MppMFtkunE+IaOntyi01rkIQoPW2GHABtfzsC0BRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.13.tgz", + "integrity": "sha512-UKhFaZTeTcbKrb3iU82m17jVcPEAoqDkCN5MBqgPyZSwp/cQmib21crTS0DdH7T1B7wZRWEI89YQAOJMet5yhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.29", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz", + "integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.19", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rolldown": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.13.tgz", + "integrity": "sha512-/5apOkwWxtACcQtZ5WlrAsKS4JwXVThQPJZ3RqIruRY9UG9M8e0VlXN068xum/JRKA4aIUoJ13ghe/kGCvidiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.153.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.13", + "@rolldown/binding-android-arm64": "1.2.13", + "@rolldown/binding-darwin-arm64": "1.2.13", + "@rolldown/binding-darwin-x64": "1.2.13", + "@rolldown/binding-freebsd-x64": "1.2.13", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.13", + "@rolldown/binding-linux-arm64-gnu": "1.2.13", + "@rolldown/binding-linux-arm64-musl": "1.2.13", + "@rolldown/binding-linux-ppc64-gnu": "1.2.13", + "@rolldown/binding-linux-s390x-gnu": "1.2.13", + "@rolldown/binding-linux-x64-gnu": "1.2.13", + "@rolldown/binding-linux-x64-musl": "1.2.13", + "@rolldown/binding-openharmony-arm64": "1.2.13", + "@rolldown/binding-win32-arm64-msvc": "1.2.13", + "@rolldown/binding-win32-x64-msvc": "1.2.13" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/three": { + "version": "0.186.1", + "resolved": "https://registry.npmjs.org/three/-/three-0.186.1.tgz", + "integrity": "sha512-blFeqb49wRCSGUGj7gtpfnSGHy2lwDk94RhUmS1c/hTby70kvChbWpkJ4Pm1390LqzzvTmzgXKHPEafJwCb8jA==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/vite": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz", + "integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.6", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/racing-beta/package.json b/racing-beta/package.json new file mode 100644 index 0000000..d38651d --- /dev/null +++ b/racing-beta/package.json @@ -0,0 +1 @@ +{"name":"beta-car-racing-game","version":"0.1.0","private":true,"type":"module","scripts":{"build":"vite build","dev":"vite --host 127.0.0.1","test":"node --test src/racing-mechanics.test.js"},"dependencies":{"@babylonjs/core":"9.30.0","three":"0.186.1"},"devDependencies":{"vite":"8.3.0"},"engines":{"node":">=22.12.0"}} diff --git a/racing-beta/web/index.html b/racing-beta/web/index.html new file mode 100644 index 0000000..f98667f --- /dev/null +++ b/racing-beta/web/index.html @@ -0,0 +1 @@ +Beta Car Racing Game
PRIVATE RACING BETA · 0.1.0

Open road. Full throttle.

Four circuits. Four rivals. Your next lap.

Choose a circuit, then start your engines.

WASD / arrow keys: drive and reverse · R: cockpit / chase · Esc: pause
Runs on your computer. No accounts, cloud saves, ads, or background data collection.
diff --git a/racing-beta/web/main.js b/racing-beta/web/main.js new file mode 100644 index 0000000..6200a80 --- /dev/null +++ b/racing-beta/web/main.js @@ -0,0 +1,17 @@ +import {RALLY_COURSES} from './src/game-mechanics.js'; +import {mount3D} from './src/games-3d.js'; +const $=id=>document.getElementById(id),track=$('track'),quality=$('quality'),status=$('status'),graphics=$('graphics'),result=$('result'); +for(const [i,c] of RALLY_COURSES.entries())track.add(new Option(c.name,String(i))); +let game=null,input={},paused=false,generation=0; +function setPause(value){paused=value;game?.pause(value);input.up=input.down=input.left=input.right=input.camera=false;$('pause').textContent=paused?'Resume · Esc':'Pause · Esc';} +async function start(){const token=++generation;game?.dispose();game=null;input={};paused=false;result.hidden=true;status.textContent='';graphics.textContent='Preparing the racing engine…'; + // New canvas owns its graphics context, allowing quality changes without reuse conflicts. + const old=document.querySelector('canvas'),canvas=old.cloneNode();old.replaceWith(canvas);canvas.focus(); + try{const mounted=await mount3D(canvas,{id:'rally',level:Number(track.value),quality:quality.value,input,onStatus:s=>{status.textContent=s;},onGraphics:s=>{graphics.textContent=s;},onFinish:r=>{result.hidden=false;result.textContent=r.text+' Select Start race to drive again.';}});if(token!==generation)mounted.dispose();else{game=mounted;setPause(false);}} + catch(error){graphics.textContent='Unable to start: '+error.message+'. Select Lightweight graphics and retry.';} +} +$('start').addEventListener('click',start);$('pause').addEventListener('click',()=>setPause(!paused));$('camera').addEventListener('click',()=>{input.camera=true;setTimeout(()=>{input.camera=false;},100);}); +const keys={w:'up',arrowup:'up',s:'down',arrowdown:'down',a:'left',arrowleft:'left',d:'right',arrowright:'right',r:'camera'}; +window.addEventListener('keydown',e=>{if(['SELECT','BUTTON'].includes(e.target.tagName)&&!keys[e.key.toLowerCase()])return;if(e.key==='Escape'){if(!e.repeat)setPause(!paused);e.preventDefault();return;}const action=keys[e.key.toLowerCase()];if(action&&game&&!paused){e.preventDefault();input[action]=true;}}); +window.addEventListener('keyup',e=>{const action=keys[e.key.toLowerCase()];if(action)input[action]=false;});window.addEventListener('blur',()=>setPause(true));document.addEventListener('visibilitychange',()=>{if(document.hidden)setPause(true);}); +window.addEventListener('beforeunload',()=>game?.dispose()); diff --git a/racing-beta/web/style.css b/racing-beta/web/style.css new file mode 100644 index 0000000..4ab7f64 --- /dev/null +++ b/racing-beta/web/style.css @@ -0,0 +1 @@ +:root{font-family:system-ui,sans-serif;color:#eaf3ff;background:#0b111b;color-scheme:dark}*{box-sizing:border-box}body{margin:0;background:radial-gradient(ellipse at 20% 0,#172e48,transparent 65%),#0b111b}main{max-width:1150px;margin:auto;padding:30px 30px 20px}header h1{font-size:clamp(28px,4vw,48px);letter-spacing:-.04em;margin:8px 0}header p,footer,#graphics{color:#aebed1}.eyebrow{color:#8bdae6;font-size:12px;letter-spacing:.14em}nav{display:flex;flex-wrap:wrap;align-items:end;gap:12px;margin:20px 0}label{display:grid;gap:6px;font-size:13px;color:#becfe1}select,button{font:inherit;border:1px solid #425771;border-radius:8px;padding:10px 14px;background:#152133;color:#eff7ff}button{cursor:pointer;min-height:41px}#start{background:#87dde2;color:#102730;border-color:transparent;font-weight:700}.stage{position:relative;aspect-ratio:8/5;width:100%;background:#101d2e;border:1px solid #324a64;border-radius:14px;overflow:hidden}canvas{display:block;width:100%;height:100%;outline:none}#result{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);background:#0b182ce8;padding:25px;border:1px solid #7790ab;border-radius:12px;max-width:85%;text-align:center}#status{font-variant-numeric:tabular-nums;min-height:28px;font-size:15px}footer{font-size:12px;line-height:1.8}button:focus-visible,select:focus-visible{outline:2px solid #9ee3ff;outline-offset:3px} diff --git a/racing-beta/web/vite.config.js b/racing-beta/web/vite.config.js new file mode 100644 index 0000000..040105f --- /dev/null +++ b/racing-beta/web/vite.config.js @@ -0,0 +1,2 @@ +import {defineConfig} from 'vite'; +export default defineConfig({base:'./',build:{outDir:'dist',target:'es2022',rolldownOptions:{output:{codeSplitting:{groups:[{name:'racing-engine',test:/@babylonjs\/core/,priority:20}]}}}}}); diff --git a/tools/build-local.mjs b/tools/build-local.mjs index af978bc..cfad0d2 100644 --- a/tools/build-local.mjs +++ b/tools/build-local.mjs @@ -39,7 +39,7 @@ function collect(dir,prefix) { collect(path.join(frontend,'dist-local'),'beebright_local/ui'); files.set('beebright_local/ui/CHANGELOG.md',fs.readFileSync(path.join(local,'CHANGELOG.md'))); files.set('beebright_local/ui/bee.svg',fs.readFileSync(path.join(frontend,'public/bee.svg'))); -for(const name of ['App.jsx','api.js','hints.js','local-api.js','local-main.jsx','styles.css','fonts.css','studio.jsx','studio-core.js','arcade-core.js','arcade.jsx','arcade.css','games-2d.js','games-3d.js','game-renderer.js','game-mechanics.js','track-geometry.js','games-rally.js','racing-mechanics.js','rally-car.js','rally-terrain.js']) { +for(const name of ['App.jsx','api.js','hints.js','local-api.js','local-main.jsx','styles.css','fonts.css','studio.jsx','studio-core.js','arcade-core.js','arcade.jsx','arcade.css','games-2d.js','games-3d.js','game-renderer.js','game-mechanics.js','track-geometry.js','games-rally.js','racing-mechanics.js','rally-car.js','rally-terrain.js','rally-babylon.js']) { let source=fs.readFileSync(path.join(frontend,'src',name)); if(name==='App.jsx') source=Buffer.from(source.toString().replace('../../../../local/release.json','../../release.json')); files.set('ui-source/src/'+name,source); @@ -48,6 +48,7 @@ for(const name of ['package.json','package-lock.json','vite.config.js','local.ht files.set('ui-source/'+name,fs.readFileSync(path.join(frontend,name))); } files.set('THIRD_PARTY_THREE_LICENSE.txt',fs.readFileSync(path.join(frontend,'node_modules/three/LICENSE'))); +files.set('THIRD_PARTY_BABYLON_LICENSE.txt',fs.readFileSync(path.join(frontend,'node_modules/@babylonjs/core/license.md'))); files.set('LICENSE',fs.readFileSync(path.join(root,'LICENSE'))); for (const [name, data] of Object.entries({hints,lists,distractors:readJson('distractors.json')})) { files.set(`beebright_local/data/${name}.json`,Buffer.from(JSON.stringify(data))); diff --git a/tools/export-racing.mjs b/tools/export-racing.mjs new file mode 100644 index 0000000..0c46400 --- /dev/null +++ b/tools/export-racing.mjs @@ -0,0 +1,12 @@ +import fs from 'node:fs';import path from 'node:path';import crypto from 'node:crypto';import {fileURLToPath} from 'node:url';import {execFileSync} from 'node:child_process'; +const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..'),target=path.resolve(process.argv[2]||path.join(root,'racing-beta/export')),frontend=path.join(root,'BeeBright-Full-Stack/beebright-spelling-bee/frontend'); +fs.mkdirSync(path.join(target,'src'),{recursive:true}); +const names=['game-mechanics.js','track-geometry.js','games-3d.js','game-renderer.js','games-rally.js','racing-mechanics.js','racing-mechanics.test.js','rally-car.js','rally-terrain.js','rally-babylon.js']; +const hash=crypto.createHash('sha256'); +function copy(from,to){const data=fs.readFileSync(from);hash.update(to);hash.update(data);fs.mkdirSync(path.dirname(path.join(target,to)),{recursive:true});fs.writeFileSync(path.join(target,to),data);} +for(const name of names)copy(path.join(frontend,'src',name),'src/'+name); +for(const name of ['index.html','main.js','style.css','vite.config.js'])copy(path.join(root,'racing-beta/web',name),name); +for(const name of ['package.json','package-lock.json','launcher.py','README.md'])copy(path.join(root,'racing-beta',name),name); +copy(path.join(root,'LICENSE'),'LICENSE'); +fs.writeFileSync(path.join(target,'source.json'),JSON.stringify({canonical:'Dev-v1/beebright',commit:execFileSync('git',['rev-parse','HEAD'],{cwd:root,encoding:'utf8'}).trim(),racingHash:hash.digest('hex'),version:'0.1.0'},null,2)+'\n'); +console.log('Exported shared racing source to',target);