From fd8ab446ba81b11b78e65bc816f6b18e4d6f4fcd Mon Sep 17 00:00:00 2001 From: viking <163542798+Dev-v1@users.noreply.github.com> Date: Sun, 11 Oct 2026 14:00:35 -0600 Subject: [PATCH 1/8] Improve racing driving, Babylon graphics and cockpit; add standalone beta pipeline --- .github/workflows/racing-beta.yml | 53 ++ .gitignore | 3 + .../frontend/package-lock.json | 7 + .../frontend/package.json | 1 + .../frontend/src/game-mechanics.js | 8 +- .../frontend/src/games-3d.js | 4 +- .../frontend/src/games-rally.js | 10 +- .../frontend/src/racing-mechanics.js | 42 +- .../frontend/src/racing-mechanics.test.js | 12 +- .../frontend/src/rally-babylon.js | 70 ++ .../frontend/src/rally-car.js | 60 +- local/CHANGELOG.md | 8 + local/release.json | 2 +- racing-beta/README.md | 13 + racing-beta/launcher.py | 58 ++ racing-beta/package-lock.json | 817 ++++++++++++++++++ racing-beta/package.json | 1 + racing-beta/sync.yml | 113 +++ racing-beta/web/index.html | 1 + racing-beta/web/main.js | 17 + racing-beta/web/style.css | 1 + racing-beta/web/vite.config.js | 2 + tools/build-local.mjs | 3 +- tools/export-racing.mjs | 12 + 24 files changed, 1287 insertions(+), 31 deletions(-) create mode 100644 .github/workflows/racing-beta.yml create mode 100644 BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-babylon.js create mode 100644 racing-beta/README.md create mode 100644 racing-beta/launcher.py create mode 100644 racing-beta/package-lock.json create mode 100644 racing-beta/package.json create mode 100644 racing-beta/sync.yml create mode 100644 racing-beta/web/index.html create mode 100644 racing-beta/web/main.js create mode 100644 racing-beta/web/style.css create mode 100644 racing-beta/web/vite.config.js create mode 100644 tools/export-racing.mjs diff --git a/.github/workflows/racing-beta.yml b/.github/workflows/racing-beta.yml new file mode 100644 index 0000000..520b3b0 --- /dev/null +++ b/.github/workflows/racing-beta.yml @@ -0,0 +1,53 @@ +name: Standalone racing executable +on: + pull_request: + push: + branches: [main] +permissions: + contents: read +jobs: + windows-executable: + runs-on: windows-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 + with: + python-version: '3.14' + - run: node tools/export-racing.mjs racing-beta/export + - run: npm ci + working-directory: racing-beta/export + - run: npm test + working-directory: racing-beta/export + - run: npm run build + working-directory: racing-beta/export + - run: python -m pip install pywebview==6.2.1 pythonnet==3.2.0 pyinstaller==6.22.3 + - run: python -m PyInstaller --noconfirm --onefile --hide-console hide-early --name Beta-Car-Racing-Game --collect-all webview --collect-all clr_loader --add-data "dist;dist" launcher.py + working-directory: racing-beta/export + - name: Install official WebView2 if missing + shell: pwsh + run: | + $Found = (Get-ChildItem "${env:ProgramFiles(x86)}/Microsoft/EdgeWebView/Application/*/msedgewebview2.exe" -ErrorAction SilentlyContinue) + if (-not $Found) { + $Setup = Join-Path $env:RUNNER_TEMP 'webview2-setup.exe' + Invoke-WebRequest 'https://go.microsoft.com/fwlink/p/?LinkId=2124703' -OutFile $Setup + $Signature = Get-AuthenticodeSignature $Setup + if ($Signature.Status -ne 'Valid' -or $Signature.SignerCertificate.Subject -notmatch 'Microsoft Corporation') { throw 'Invalid Microsoft installer signature' } + $Process = Start-Process $Setup -ArgumentList '/silent','/install' -Wait -PassThru + if ($Process.ExitCode -notin @(0,3010)) { throw 'WebView2 installation failed' } + } + - name: Launch the actual frozen Windows executable + shell: pwsh + env: + RACING_TEST_OUTPUT: ${{ runner.temp }}/racing-beta-views + run: | + & ./racing-beta/export/dist/Beta-Car-Racing-Game.exe --self-test + if ($LASTEXITCODE -ne 0) { throw 'Standalone executable test failed' } + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + if: always() + with: + name: Racing-beta-executable-and-views + path: | + racing-beta/export/dist/Beta-Car-Racing-Game.exe + ${{ runner.temp }}/racing-beta-views/*.png + if-no-files-found: error + retention-days: 14 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..7612339 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){ 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..84e6a2e --- /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,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;} + engine.setHardwareScalingLevel(1/Math.min(2,globalThis.devicePixelRatio||1));engine.setSize(800,500); + 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(false);} + 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,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(object.quaternion.x,object.quaternion.y,object.quaternion.z,object.quaternion.w);target.setEnabled(object.visible);} + for(const [source,m] of materials)m.emissiveColor=color(source.emissive).scale(source.emissiveIntensity??0); + for(const texture of dynamic)texture.update(false); + camera.position.set(view.position.x,view.position.y,view.position.z);camera.rotationQuaternion=new Quaternion(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..0574bac 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,.07,1.55,.025),rubber,-.43,.80,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(-.29,.94,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(.047,12,8),glove,0,0,0,group);palm.scale.set(.9,1.2,.65);for(let i=0;i<4;i++)mesh(round(.016,.047,.019,.007),glove,.025-i*.018,.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(.62,.025,1.25,.015),leather,-.44,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*.08,1.025-phase*.05,.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/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..30e3edc --- /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 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..872741e --- /dev/null +++ b/racing-beta/launcher.py @@ -0,0 +1,58 @@ +"""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): + def log_message(self,*args):pass + def do_GET(self): + if self.path.split('?')[0].endswith('.py'):self.send_error(404);return + super().do_GET() + server=http.server.ThreadingHTTPServer(('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: + end=time.monotonic()+40 + while time.monotonic()=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/sync.yml b/racing-beta/sync.yml new file mode 100644 index 0000000..b778fda --- /dev/null +++ b/racing-beta/sync.yml @@ -0,0 +1,113 @@ +name: Sync racing fixes and publish Windows beta +on: + push: + branches: [main] + workflow_dispatch: + schedule: + - cron: '7,22,37,52 * * * *' +permissions: + contents: write +concurrency: + group: racing-beta-publisher + cancel-in-progress: false +jobs: + sync: + runs-on: ubuntu-latest + outputs: + build: ${{ steps.sync.outputs.build }} + commit: ${{ steps.sync.outputs.commit }} + tag: ${{ steps.sync.outputs.tag }} + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - name: Copy canonical racing code and detect changes + id: sync + env: + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + git clone --depth 1 https://github.com/Dev-v1/beebright.git "$RUNNER_TEMP/canonical" + node "$RUNNER_TEMP/canonical/tools/export-racing.mjs" "$GITHUB_WORKSPACE" + HASH=$(node -p "JSON.parse(require('fs').readFileSync('source.json','utf8')).racingHash") + TAG="beta-0.1.0-${HASH:0:12}" + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add src index.html main.js style.css vite.config.js package.json package-lock.json launcher.py README.md LICENSE source.json + if ! git diff --cached --quiet; then + git commit -m "Sync canonical racing source ${HASH:0:12}" + git push origin HEAD:main + fi + echo "commit=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + if gh release view "$TAG" >/dev/null 2>&1; then + echo 'build=false' >> "$GITHUB_OUTPUT" + else + echo 'build=true' >> "$GITHUB_OUTPUT" + fi + windows-release: + needs: sync + if: needs.sync.outputs.build == 'true' + runs-on: windows-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + ref: ${{ needs.sync.outputs.commit }} + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 + with: + python-version: '3.14' + - run: npm ci + - run: npm test + - run: npm run build + - run: python -m pip install pywebview==6.2.1 pythonnet==3.2.0 pyinstaller==6.22.3 pip-licenses==5.5.0 + - name: Bundle third-party license notices + shell: pwsh + run: | + Copy-Item node_modules/three/LICENSE dist/THREE-LICENSE.txt + Copy-Item node_modules/@babylonjs/core/license.md dist/BABYLON-LICENSE.txt + Copy-Item LICENSE dist/PROJECT-LICENSE.txt + python -c "import sys,shutil;from pathlib import Path;shutil.copyfile(Path(sys.base_prefix)/'LICENSE.txt','dist/PYTHON-LICENSE.txt')" + python -m piplicenses --with-license-file --no-license-path --format=plain-vertical --output-file=THIRD_PARTY_NOTICES.txt + if ($LASTEXITCODE -ne 0) { throw 'License notice generation failed' } + Copy-Item THIRD_PARTY_NOTICES.txt dist/THIRD_PARTY_NOTICES.txt + - run: python -m PyInstaller --noconfirm --onefile --hide-console hide-early --name Beta-Car-Racing-Game --collect-all webview --collect-all clr_loader --add-data "dist;dist" launcher.py + - name: Install official WebView2 if missing + shell: pwsh + run: | + $Found = (Get-ChildItem "${env:ProgramFiles(x86)}/Microsoft/EdgeWebView/Application/*/msedgewebview2.exe" -ErrorAction SilentlyContinue) + if (-not $Found) { + $Setup = Join-Path $env:RUNNER_TEMP 'webview2-setup.exe' + Invoke-WebRequest 'https://go.microsoft.com/fwlink/p/?LinkId=2124703' -OutFile $Setup + $Signature = Get-AuthenticodeSignature $Setup + if ($Signature.Status -ne 'Valid' -or $Signature.SignerCertificate.Subject -notmatch 'Microsoft Corporation') { throw 'Invalid Microsoft installer signature' } + $Process = Start-Process $Setup -ArgumentList '/silent','/install' -Wait -PassThru + if ($Process.ExitCode -notin @(0,3010)) { throw 'WebView2 installation failed' } + } + - name: Test the actual downloadable executable + shell: pwsh + env: + RACING_TEST_OUTPUT: ${{ runner.temp }}/racing-beta-views + run: | + & ./dist/Beta-Car-Racing-Game.exe --self-test + if ($LASTEXITCODE -ne 0) { throw 'Frozen racing executable failed its UI test' } + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + if: always() + with: + name: Tested-Windows-racing-beta + path: | + dist/Beta-Car-Racing-Game.exe + ${{ runner.temp }}/racing-beta-views/*.png + if-no-files-found: error + retention-days: 14 + - name: Publish the private Windows prerelease + shell: pwsh + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ needs.sync.outputs.tag }} + run: | + $Source = Get-Content source.json -Raw | ConvertFrom-Json + "Racing beta 0.1.0. Shared source: Dev-v1/beebright@$($Source.commit). Includes tested Windows executable, source ZIP and license notices. This unsigned beta requires Microsoft Edge WebView2 Runtime." | Set-Content release-notes.txt + Compress-Archive -Path src,index.html,main.js,style.css,vite.config.js,package.json,package-lock.json,launcher.py,README.md,LICENSE,source.json,THIRD_PARTY_NOTICES.txt -DestinationPath racing-source.zip + $Checksum = (Get-FileHash dist/Beta-Car-Racing-Game.exe -Algorithm SHA256).Hash.ToLower() + "$Checksum Beta-Car-Racing-Game.exe" | Set-Content SHA256SUMS.txt + gh release create $env:RELEASE_TAG dist/Beta-Car-Racing-Game.exe racing-source.zip SHA256SUMS.txt THIRD_PARTY_NOTICES.txt --target $env:GITHUB_SHA --title "Beta Car Racing Game 0.1.0 · $($Source.commit.Substring(0,7))" --notes-file release-notes.txt --prerelease + if ($LASTEXITCODE -ne 0) { throw 'Private release publication failed' } 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..a677977 --- /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'}}); 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); From d6353eccde3a1f84911d2490da3608dc9053a7eb Mon Sep 17 00:00:00 2001 From: viking <163542798+Dev-v1@users.noreply.github.com> Date: Sun, 11 Oct 2026 14:04:50 -0600 Subject: [PATCH 2/8] Verify actual rendered pixels in the frozen racing executable --- .../beebright-spelling-bee/frontend/src/rally-babylon.js | 2 +- racing-beta/launcher.py | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) 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 index 84e6a2e..6cea03b 100644 --- a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-babylon.js +++ b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-babylon.js @@ -23,7 +23,7 @@ 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,powerPreference:'high-performance'}); + 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;} diff --git a/racing-beta/launcher.py b/racing-beta/launcher.py index 872741e..6ece332 100644 --- a/racing-beta/launcher.py +++ b/racing-beta/launcher.py @@ -42,11 +42,18 @@ def test(): assert window.evaluate_js("document.querySelector('#status').innerText.includes('Cockpit')") assert not window.evaluate_js("document.querySelector('#status').innerText.includes('NaN')") assert window.evaluate_js("document.querySelector('#result').hidden"),window.evaluate_js("document.querySelector('#result').innerText") + # Capture in the render frame. WebGL normally clears its back buffer + # after presentation, so an arbitrary later toDataURL is black. + window.evaluate_js("window.racingCapture=null;requestAnimationFrame(()=>{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);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=window.evaluate_js("document.querySelector('canvas').toDataURL('image/png').split(',')[1]") + 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) From c9c344220620ae139642199995dc854aad5e2c19 Mon Sep 17 00:00:00 2001 From: viking <163542798+Dev-v1@users.noreply.github.com> Date: Sun, 11 Oct 2026 14:10:46 -0600 Subject: [PATCH 3/8] Improve cockpit visibility and render High graphics at display resolution --- .../frontend/src/rally-babylon.js | 2 +- .../beebright-spelling-bee/frontend/src/rally-car.js | 10 +++++----- racing-beta/launcher.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) 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 index 6cea03b..8fa25a2 100644 --- a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-babylon.js +++ b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-babylon.js @@ -27,7 +27,7 @@ export async function createRallyRenderer(canvas,onGraphics){ 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;} - engine.setHardwareScalingLevel(1/Math.min(2,globalThis.devicePixelRatio||1));engine.setSize(800,500); + 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); 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 0574bac..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 @@ -38,7 +38,7 @@ export function buildRallyCar({high,paint,mesh,mat,parent}){ 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,.07,1.55,.025),rubber,-.43,.80,0,interior); + 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); @@ -62,7 +62,7 @@ export function buildRallyCar({high,paint,mesh,mat,parent}){ 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(-.29,.94,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); + 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); @@ -75,18 +75,18 @@ export function buildRallyCar({high,paint,mesh,mat,parent}){ 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(.047,12,8),glove,0,0,0,group);palm.scale.set(.9,1.2,.65);for(let i=0;i<4;i++)mesh(round(.016,.047,.019,.007),glove,.025-i*.018,.025,-.015,group);mesh(round(.075,.045,.048,.018),suit,-.075,-.01,0,group);} + 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(.62,.025,1.25,.015),leather,-.44,1.245,0,cockpitFrame);mesh(round(.09,.065,.21,.012),alloy,.26,1.18,0,cockpitFrame); + 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*.08,1.025-phase*.05,.225-phase*.21); + 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;} } diff --git a/racing-beta/launcher.py b/racing-beta/launcher.py index 6ece332..4b6d3e3 100644 --- a/racing-beta/launcher.py +++ b/racing-beta/launcher.py @@ -44,7 +44,7 @@ def test(): assert window.evaluate_js("document.querySelector('#result').hidden"),window.evaluate_js("document.querySelector('#result').innerText") # Capture in the render frame. WebGL normally clears its back buffer # after presentation, so an arbitrary later toDataURL is black. - window.evaluate_js("window.racingCapture=null;requestAnimationFrame(()=>{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);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};})") + window.evaluate_js("window.racingCapture=null;requestAnimationFrame(()=>{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() Date: Sun, 11 Oct 2026 14:14:03 -0600 Subject: [PATCH 4/8] Keep beta executable builds and release automation exclusively private --- .github/workflows/racing-beta.yml | 53 -------------- racing-beta/sync.yml | 113 ------------------------------ 2 files changed, 166 deletions(-) delete mode 100644 .github/workflows/racing-beta.yml delete mode 100644 racing-beta/sync.yml diff --git a/.github/workflows/racing-beta.yml b/.github/workflows/racing-beta.yml deleted file mode 100644 index 520b3b0..0000000 --- a/.github/workflows/racing-beta.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: Standalone racing executable -on: - pull_request: - push: - branches: [main] -permissions: - contents: read -jobs: - windows-executable: - runs-on: windows-latest - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 - with: - python-version: '3.14' - - run: node tools/export-racing.mjs racing-beta/export - - run: npm ci - working-directory: racing-beta/export - - run: npm test - working-directory: racing-beta/export - - run: npm run build - working-directory: racing-beta/export - - run: python -m pip install pywebview==6.2.1 pythonnet==3.2.0 pyinstaller==6.22.3 - - run: python -m PyInstaller --noconfirm --onefile --hide-console hide-early --name Beta-Car-Racing-Game --collect-all webview --collect-all clr_loader --add-data "dist;dist" launcher.py - working-directory: racing-beta/export - - name: Install official WebView2 if missing - shell: pwsh - run: | - $Found = (Get-ChildItem "${env:ProgramFiles(x86)}/Microsoft/EdgeWebView/Application/*/msedgewebview2.exe" -ErrorAction SilentlyContinue) - if (-not $Found) { - $Setup = Join-Path $env:RUNNER_TEMP 'webview2-setup.exe' - Invoke-WebRequest 'https://go.microsoft.com/fwlink/p/?LinkId=2124703' -OutFile $Setup - $Signature = Get-AuthenticodeSignature $Setup - if ($Signature.Status -ne 'Valid' -or $Signature.SignerCertificate.Subject -notmatch 'Microsoft Corporation') { throw 'Invalid Microsoft installer signature' } - $Process = Start-Process $Setup -ArgumentList '/silent','/install' -Wait -PassThru - if ($Process.ExitCode -notin @(0,3010)) { throw 'WebView2 installation failed' } - } - - name: Launch the actual frozen Windows executable - shell: pwsh - env: - RACING_TEST_OUTPUT: ${{ runner.temp }}/racing-beta-views - run: | - & ./racing-beta/export/dist/Beta-Car-Racing-Game.exe --self-test - if ($LASTEXITCODE -ne 0) { throw 'Standalone executable test failed' } - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 - if: always() - with: - name: Racing-beta-executable-and-views - path: | - racing-beta/export/dist/Beta-Car-Racing-Game.exe - ${{ runner.temp }}/racing-beta-views/*.png - if-no-files-found: error - retention-days: 14 diff --git a/racing-beta/sync.yml b/racing-beta/sync.yml deleted file mode 100644 index b778fda..0000000 --- a/racing-beta/sync.yml +++ /dev/null @@ -1,113 +0,0 @@ -name: Sync racing fixes and publish Windows beta -on: - push: - branches: [main] - workflow_dispatch: - schedule: - - cron: '7,22,37,52 * * * *' -permissions: - contents: write -concurrency: - group: racing-beta-publisher - cancel-in-progress: false -jobs: - sync: - runs-on: ubuntu-latest - outputs: - build: ${{ steps.sync.outputs.build }} - commit: ${{ steps.sync.outputs.commit }} - tag: ${{ steps.sync.outputs.tag }} - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - - name: Copy canonical racing code and detect changes - id: sync - env: - GH_TOKEN: ${{ github.token }} - shell: bash - run: | - set -euo pipefail - git clone --depth 1 https://github.com/Dev-v1/beebright.git "$RUNNER_TEMP/canonical" - node "$RUNNER_TEMP/canonical/tools/export-racing.mjs" "$GITHUB_WORKSPACE" - HASH=$(node -p "JSON.parse(require('fs').readFileSync('source.json','utf8')).racingHash") - TAG="beta-0.1.0-${HASH:0:12}" - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add src index.html main.js style.css vite.config.js package.json package-lock.json launcher.py README.md LICENSE source.json - if ! git diff --cached --quiet; then - git commit -m "Sync canonical racing source ${HASH:0:12}" - git push origin HEAD:main - fi - echo "commit=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - echo "tag=$TAG" >> "$GITHUB_OUTPUT" - if gh release view "$TAG" >/dev/null 2>&1; then - echo 'build=false' >> "$GITHUB_OUTPUT" - else - echo 'build=true' >> "$GITHUB_OUTPUT" - fi - windows-release: - needs: sync - if: needs.sync.outputs.build == 'true' - runs-on: windows-latest - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - ref: ${{ needs.sync.outputs.commit }} - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 - with: - python-version: '3.14' - - run: npm ci - - run: npm test - - run: npm run build - - run: python -m pip install pywebview==6.2.1 pythonnet==3.2.0 pyinstaller==6.22.3 pip-licenses==5.5.0 - - name: Bundle third-party license notices - shell: pwsh - run: | - Copy-Item node_modules/three/LICENSE dist/THREE-LICENSE.txt - Copy-Item node_modules/@babylonjs/core/license.md dist/BABYLON-LICENSE.txt - Copy-Item LICENSE dist/PROJECT-LICENSE.txt - python -c "import sys,shutil;from pathlib import Path;shutil.copyfile(Path(sys.base_prefix)/'LICENSE.txt','dist/PYTHON-LICENSE.txt')" - python -m piplicenses --with-license-file --no-license-path --format=plain-vertical --output-file=THIRD_PARTY_NOTICES.txt - if ($LASTEXITCODE -ne 0) { throw 'License notice generation failed' } - Copy-Item THIRD_PARTY_NOTICES.txt dist/THIRD_PARTY_NOTICES.txt - - run: python -m PyInstaller --noconfirm --onefile --hide-console hide-early --name Beta-Car-Racing-Game --collect-all webview --collect-all clr_loader --add-data "dist;dist" launcher.py - - name: Install official WebView2 if missing - shell: pwsh - run: | - $Found = (Get-ChildItem "${env:ProgramFiles(x86)}/Microsoft/EdgeWebView/Application/*/msedgewebview2.exe" -ErrorAction SilentlyContinue) - if (-not $Found) { - $Setup = Join-Path $env:RUNNER_TEMP 'webview2-setup.exe' - Invoke-WebRequest 'https://go.microsoft.com/fwlink/p/?LinkId=2124703' -OutFile $Setup - $Signature = Get-AuthenticodeSignature $Setup - if ($Signature.Status -ne 'Valid' -or $Signature.SignerCertificate.Subject -notmatch 'Microsoft Corporation') { throw 'Invalid Microsoft installer signature' } - $Process = Start-Process $Setup -ArgumentList '/silent','/install' -Wait -PassThru - if ($Process.ExitCode -notin @(0,3010)) { throw 'WebView2 installation failed' } - } - - name: Test the actual downloadable executable - shell: pwsh - env: - RACING_TEST_OUTPUT: ${{ runner.temp }}/racing-beta-views - run: | - & ./dist/Beta-Car-Racing-Game.exe --self-test - if ($LASTEXITCODE -ne 0) { throw 'Frozen racing executable failed its UI test' } - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 - if: always() - with: - name: Tested-Windows-racing-beta - path: | - dist/Beta-Car-Racing-Game.exe - ${{ runner.temp }}/racing-beta-views/*.png - if-no-files-found: error - retention-days: 14 - - name: Publish the private Windows prerelease - shell: pwsh - env: - GH_TOKEN: ${{ github.token }} - RELEASE_TAG: ${{ needs.sync.outputs.tag }} - run: | - $Source = Get-Content source.json -Raw | ConvertFrom-Json - "Racing beta 0.1.0. Shared source: Dev-v1/beebright@$($Source.commit). Includes tested Windows executable, source ZIP and license notices. This unsigned beta requires Microsoft Edge WebView2 Runtime." | Set-Content release-notes.txt - Compress-Archive -Path src,index.html,main.js,style.css,vite.config.js,package.json,package-lock.json,launcher.py,README.md,LICENSE,source.json,THIRD_PARTY_NOTICES.txt -DestinationPath racing-source.zip - $Checksum = (Get-FileHash dist/Beta-Car-Racing-Game.exe -Algorithm SHA256).Hash.ToLower() - "$Checksum Beta-Car-Racing-Game.exe" | Set-Content SHA256SUMS.txt - gh release create $env:RELEASE_TAG dist/Beta-Car-Racing-Game.exe racing-source.zip SHA256SUMS.txt THIRD_PARTY_NOTICES.txt --target $env:GITHUB_SHA --title "Beta Car Racing Game 0.1.0 · $($Source.commit.Substring(0,7))" --notes-file release-notes.txt --prerelease - if ($LASTEXITCODE -ne 0) { throw 'Private release publication failed' } From 31a94b57f254bb8df3773325cf2a486c6bfc050b Mon Sep 17 00:00:00 2001 From: viking <163542798+Dev-v1@users.noreply.github.com> Date: Sun, 11 Oct 2026 14:17:20 -0600 Subject: [PATCH 5/8] Serve frozen JavaScript chunks with explicit MIME types and test all assets --- racing-beta/launcher.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/racing-beta/launcher.py b/racing-beta/launcher.py index 4b6d3e3..e049cd2 100644 --- a/racing-beta/launcher.py +++ b/racing-beta/launcher.py @@ -8,7 +8,10 @@ def main(): 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): - def log_message(self,*args):pass + # 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() @@ -20,6 +23,11 @@ def do_GET(self): 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() Date: Sun, 11 Oct 2026 14:19:55 -0600 Subject: [PATCH 6/8] Orient cockpit instruments correctly in the Babylon renderer --- .../beebright-spelling-bee/frontend/src/rally-babylon.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 8fa25a2..fa3990d 100644 --- a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-babylon.js +++ b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-babylon.js @@ -43,7 +43,7 @@ export async function createRallyRenderer(canvas,onGraphics){ const color=c=>new 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(false);} + 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; @@ -61,7 +61,7 @@ export async function createRallyRenderer(canvas,onGraphics){ 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(object.quaternion.x,object.quaternion.y,object.quaternion.z,object.quaternion.w);target.setEnabled(object.visible);} for(const [source,m] of materials)m.emissiveColor=color(source.emissive).scale(source.emissiveIntensity??0); - for(const texture of dynamic)texture.update(false); + for(const texture of dynamic)texture.update(true); camera.position.set(view.position.x,view.position.y,view.position.z);camera.rotationQuaternion=new Quaternion(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; From 32e627d1b748c407d9c7dbe3b7df9e32deb6ff3f Mon Sep 17 00:00:00 2001 From: viking <163542798+Dev-v1@users.noreply.github.com> Date: Sun, 11 Oct 2026 14:26:24 -0600 Subject: [PATCH 7/8] Wait for High racing shaders, reduce import requests and avoid frame allocations --- .../beebright-spelling-bee/frontend/src/games-3d.js | 8 +++++--- .../beebright-spelling-bee/frontend/src/rally-babylon.js | 8 ++++---- .../beebright-spelling-bee/frontend/vite.config.js | 4 +++- racing-beta/launcher.py | 4 +++- racing-beta/web/vite.config.js | 2 +- 5 files changed, 16 insertions(+), 10 deletions(-) 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 7612339..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 @@ -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/rally-babylon.js b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-babylon.js index fa3990d..393de1a 100644 --- a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-babylon.js +++ b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-babylon.js @@ -57,12 +57,12 @@ export async function createRallyRenderer(canvas,onGraphics){ } let initialized=false,disposed=false; onGraphics?.('High quality · Babylon.js · PBR lighting · HDR · soft shadows'); - return {software:false,babylon:true,render(source,view){if(disposed)return; + 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(object.quaternion.x,object.quaternion.y,object.quaternion.z,object.quaternion.w);target.setEnabled(object.visible);} - for(const [source,m] of materials)m.emissiveColor=color(source.emissive).scale(source.emissiveIntensity??0); + 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(view.quaternion.x,view.quaternion.y,view.quaternion.z,view.quaternion.w);camera.fov=view.fov*Math.PI/180;camera.minZ=view.near; + 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(); 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/racing-beta/launcher.py b/racing-beta/launcher.py index e049cd2..4de0ba5 100644 --- a/racing-beta/launcher.py +++ b/racing-beta/launcher.py @@ -15,7 +15,9 @@ def log_message(self,format,*values): def do_GET(self): if self.path.split('?')[0].endswith('.py'):self.send_error(404);return super().do_GET() - server=http.server.ThreadingHTTPServer(('127.0.0.1',0),functools.partial(Handler,directory=str(root))) + 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' diff --git a/racing-beta/web/vite.config.js b/racing-beta/web/vite.config.js index a677977..040105f 100644 --- a/racing-beta/web/vite.config.js +++ b/racing-beta/web/vite.config.js @@ -1,2 +1,2 @@ import {defineConfig} from 'vite'; -export default defineConfig({base:'./',build:{outDir:'dist',target:'es2022'}}); +export default defineConfig({base:'./',build:{outDir:'dist',target:'es2022',rolldownOptions:{output:{codeSplitting:{groups:[{name:'racing-engine',test:/@babylonjs\/core/,priority:20}]}}}}}); From d81f6eed43011378029a5baaad84988a6cfba621 Mon Sep 17 00:00:00 2001 From: viking <163542798+Dev-v1@users.noreply.github.com> Date: Sun, 11 Oct 2026 14:30:08 -0600 Subject: [PATCH 8/8] Match racing beta README license to the existing project license --- racing-beta/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/racing-beta/README.md b/racing-beta/README.md index 30e3edc..a6846b6 100644 --- a/racing-beta/README.md +++ b/racing-beta/README.md @@ -8,6 +8,6 @@ The game uses the exact same racing mechanics and models as BeeBright. Its priva 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 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. +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.