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 01b8628..bab85b8 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 @@ -2,7 +2,7 @@ import * as THREE from 'three'; import {RoundedBoxGeometry} from 'three/addons/geometries/RoundedBoxGeometry.js'; import {RALLY_COURSES,clamp} from './game-mechanics.js'; import {rallyRoad,rallySample,newRallyRace,rallyStep,newRallyView,rallyViewStep} from './racing-mechanics.js'; -import {buildRallyCar} from './rally-car.js'; +import {buildRallyCar,rallyNavigation} from './rally-car.js'; export {buildRallyCar} from './rally-car.js'; import {rallyTerrain} from './rally-terrain.js'; // Continuous ribbons share section vertices; inclines have no stacked road boxes. @@ -56,13 +56,13 @@ export function setupRally({scene,camera,light,high,level,player,mesh,mat,textur 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.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); + 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,state===race.player?rallyNavigation(road,race):null); 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?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);} + cars[0].setCockpit(view.cockpit); + if(view.changed){camera.fov=view.cockpit?84:58;camera.near=view.cockpit?.025:.1;camera.updateProjectionMatrix();} + if(view.cockpit){player.updateMatrixWorld(true);const eye=player.localToWorld(new THREE.Vector3(-.72,1.20,.37)),target=player.localToWorld(new THREE.Vector3(24,1.13,.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); 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 70127fc..fda41f9 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 @@ -6,6 +6,12 @@ import {rallyRoad,rallySample,rallyProject,rallyProgress,newRallyRace,rallyStep, import {rallyRibbon,buildRallyCar} from './games-rally.js'; import {rallyTerrain,mountainHeight} from './rally-terrain.js'; import {marbleBoardGeometry} from './track-geometry.js'; +import {rallyNavigation} from './rally-car.js'; +test('navigation follows every car and reports the nearest opponent behind across lap progress',()=>{ + const road=rallyRoad(RALLY_COURSES[0]),race=newRallyRace(road);race.player.travel=road.length+10;race.opponents[0].travel=road.length+2; + const before=rallyNavigation(road,race);assert.equal(before.markers.length,5);assert.equal(before.markers[1].gap,-8);assert.ok(before.path.every(p=>p.x>=0&&p.x<=1&&p.y>=0&&p.y<=1)); + Object.assign(race.opponents[0],rallySample(road,180),{travel:road.length+180});const after=rallyNavigation(road,race);assert.notDeepEqual(after.markers[1],before.markers[1]);assert.equal(after.markers[1].gap,170);assert.equal(after.markers[0].player,true); +}); 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>=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 group=new THREE.Group(),geometries=[],materials=[];const mat=color=>{const m=new THREE.MeshStandardMaterial({color});materials.push(m);return m;};const mesh=(g,m,x=0,y=0,z=0,parent=group)=>{geometries.push(g);const node=new THREE.Mesh(g,m);node.position.set(x,y,z);parent.add(node);return node;}; const paint=new THREE.MeshPhysicalMaterial({color:'#d84e71',clearcoat:1}),car=buildRallyCar({high:true,paint,mesh,mat,parent:group});assert.equal(car.wheels.length,4);assert.equal(car.wheels.filter(w=>w.front).length,2);car.body.computeBoundingBox();assert.ok(car.body.boundingBox.max.x-car.body.boundingBox.min.x>4.4);assert.ok(car.body.attributes.position.count>200);assert.ok(car.canopy.children.length>=8);assert.ok(car.interior.children.length>=6);assert.equal(car.body.index.count%3,0);assert.ok(geometries.some(g=>g.type==='SphereGeometry'));assert.ok(materials.some(m=>m.roughness<.15)); + car.setCockpit(true);assert.equal(car.roof.visible,true);assert.equal(car.canopy.visible,true);assert.ok(car.glassPanes.every(p=>!p.visible));assert.equal(car.interior.visible,true);assert.ok(car.roof.position.y>1.4); + car.setCockpit(false);assert.equal(car.roof.visible,true);assert.ok(car.glassPanes.every(p=>p.visible));assert.equal(car.interior.visible,false); for(const g of geometries)g.dispose();for(const m of materials)m.dispose();paint.dispose(); }); test('marble board geometry has positive top normals and no duplicated triangle faces',()=>{ 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 de17c65..f68d3a6 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 @@ -1,5 +1,13 @@ import * as THREE from 'three'; import {RoundedBoxGeometry} from 'three/addons/geometries/RoundedBoxGeometry.js'; +const navigationRoutes=new WeakMap(); +export function rallyNavigation(road,race){ + let route=navigationRoutes.get(road); + if(!route){const xs=road.points.map(p=>p.x),zs=road.points.map(p=>p.z),minX=Math.min(...xs),minZ=Math.min(...zs),span=Math.max(Math.max(...xs)-minX,Math.max(...zs)-minZ,1),centerX=(Math.max(...xs)+minX)/2,centerZ=(Math.max(...zs)+minZ)/2; + const project=p=>({x:.5+(p.x-centerX)/span,y:.5+(p.z-centerZ)/span});route={project,path:road.points.map(project)};navigationRoutes.set(road,route); + } + return {path:route.path,markers:[race.player,...race.opponents].map((p,i)=>({...route.project(p),number:i+1,player:i===0,gap:p.travel-race.player.travel,finished:Boolean(p.finished)}))}; +} // Original, unbranded long-hood GT silhouette. X is forward; dimensions are metres. export function buildRallyCar({high,paint,mesh,mat,parent}){ const rubber=mat('#101419');rubber.roughness=.96;rubber.metalness=0; @@ -19,14 +27,14 @@ export function buildRallyCar({high,paint,mesh,mat,parent}){ const body=new THREE.BufferGeometry();body.setAttribute('position',new THREE.Float32BufferAttribute(pos,3));body.setIndex(indices);body.computeVertexNormals();mesh(body,paint,0,0,0,parent); // Muscular wheel haunches and a short fastback rear are present in both quality modes. for(const x of [-1.28,1.32])for(const z of [-.82,.82]){const f=mesh(new THREE.SphereGeometry(.6,high?24:10,high?14:6),paint,x,.59,z,parent);f.scale.set(1,.54,.36);} - const canopy=new THREE.Group();parent.add(canopy); - const glassSections=[[-1.55,.70,.78],[-.78,.66,1.22],[-.12,.64,1.25],[.68,.73,.82]]; + const canopy=new THREE.Group();parent.add(canopy);const glassPanes=[]; + const glassSections=[[-1.55,.76,.78],[-1.02,.73,1.55],[-.21,.73,1.58],[.78,.79,.80]]; for(let i=0;i0?Math.sin(Math.PI*(1-state.shift/.32)):0;shifter.rotation.z=phase*.22; leftHand.position.set(-.21,1.025,.515);rightHand.position.set(-.21+phase*.01,1.025-phase*.055,.225-phase*.21); leftHand.rotation.x=-(state.steer||0)*.5;rightHand.rotation.x=-(state.steer||0)*.5*(1-phase); - if(dashboardCanvas){const c=dashboardCanvas.getContext('2d');c.fillStyle='#07131d';c.fillRect(0,0,512,256);c.fillStyle='#b2eef1';c.font='bold 90px sans-serif';c.fillText(state.gear===-1?'R':String(state.gear??1),24,108);c.font='bold 52px sans-serif';c.fillText(String(Math.round(Math.abs(state.speed)*3.6)).padStart(3,'0'),170,97);c.font='22px sans-serif';c.fillText('km/h',178,129);c.fillText(`${Math.round(state.rpm??900)} RPM`,24,193);c.fillStyle=phase>0?'#ffb955':'#66c9c9';c.fillRect(24,216,Math.min(464,(state.rpm??900)/7500*464),9);dashboardTexture.needsUpdate=true;} + if(dashboardCanvas){const c=dashboardCanvas.getContext('2d');c.fillStyle='#07131d';c.fillRect(0,0,512,384);c.fillStyle='#b2eef1';c.font='bold 23px sans-serif';c.fillText('LIVE TRACK · YOU 1',18,28); + if(navigation){const size=202,ox=144,oy=40;c.strokeStyle='#64778b';c.lineWidth=9;c.lineJoin='round';c.beginPath();navigation.path.forEach((p,i)=>c[i?'lineTo':'moveTo'](ox+p.x*size,oy+p.y*size));c.closePath();c.stroke(); + for(const p of navigation.markers){c.beginPath();c.fillStyle=p.player?'#ffffff':['','#ffffff','#3da5bd','#f3bd47','#536ed0','#78a755'][p.number];c.arc(ox+p.x*size,oy+p.y*size,p.player?9:7,0,Math.PI*2);c.fill();c.font='bold 16px sans-serif';c.fillText(String(p.number),ox+p.x*size+10,oy+p.y*size-6);} + const behind=navigation.markers.filter(p=>!p.player&&p.gap<0&&!p.finished).sort((a,b)=>b.gap-a.gap)[0];c.fillStyle='#b2eef1';c.font='20px sans-serif';c.fillText(behind?`BEHIND: ${behind.number} · ${Math.round(-behind.gap)} m`:'BEHIND: clear',18,274); + }c.fillStyle='#b2eef1';c.font='bold 46px sans-serif';c.fillText(state.gear===-1?'R':String(state.gear??1),20,328);c.font='bold 32px sans-serif';c.fillText(`${Math.round(Math.abs(state.speed)*3.6)} km/h`,94,320);c.font='18px sans-serif';c.fillText(`${Math.round(state.rpm??900)} RPM`,326,320);c.fillStyle=phase>0?'#ffb955':'#66c9c9';c.fillRect(18,355,Math.min(476,(state.rpm??900)/7500*476),8);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; @@ -98,5 +111,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,shifter,needles,animate,dashboardTexture}; + return {parent,wheels,tail,canopy,roof,glassPanes,interior,steering,body,shifter,needles,animate,setCockpit,dashboardTexture}; } diff --git a/local/CHANGELOG.md b/local/CHANGELOG.md index 33fb9bd..57e1eb4 100644 --- a/local/CHANGELOG.md +++ b/local/CHANGELOG.md @@ -1,5 +1,11 @@ # BeeBright releases +## 2.5 bugfix + +- Solid GT roof stays visible in both cockpit and chase cameras. +- Taller, wider windshield, slimmer interior pillars and a wider cockpit field of view. +- Live dashboard navigation shows the complete circuit, all five cars, and the nearest car behind with its distance. Speed, gear and RPM remain visible. + ## 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. diff --git a/local/release.json b/local/release.json index af49dc8..337c36a 100644 --- a/local/release.json +++ b/local/release.json @@ -1 +1 @@ -{"name":"BeeBright","version":"2.5 racing beta"} +{"name":"BeeBright","version":"2.5 bugfix"}