diff --git a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/arcade-core.js b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/arcade-core.js index 4e437be..45f94f5 100644 --- a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/arcade-core.js +++ b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/arcade-core.js @@ -3,7 +3,7 @@ export const GAMES = [ {id:'sheep',title:'Sheep Escape',tag:'BRIDGE BUILDER',description:'Hold to grow a bridge upward, then release to lower it and lead your sheep across.',controls:'Hold mouse / touch / Space: grow · Release: lower bridge',color:'#a1d9a1'}, {id:'gravity',title:'Gravity Flip',tag:'ENDLESS RUNNER',description:'Swap floor and ceiling to dodge the neon barriers.',controls:'Space / tap: flip gravity',color:'#d5a2ff'}, {id:'bowling',title:'Pocket Bowling',tag:'FIVE FRAMES',description:'Aim carefully and knock down all ten pins. Two rolls per frame.',controls:'Left / right: aim · Up / down: power · Q / E: spin · Space / Roll: bowl',color:'#7ed9ee'}, - {id:'rally',title:'Neon Rally',tag:'3D · TEN TRACKS',description:'Race four fast GT opponents on longer circuits. Climb mountain switchbacks, brake for hairpins and pass on the straights.',controls:'Up: accelerate · Down: brake / reverse · Left / right: steer · R: cockpit / chase camera',color:'#ff97b3',three:true}, + {id:'rally',title:'Neon Rally',tag:'3D · TWELVE TRACKS',description:'Race four fast GT opponents on longer circuits. Climb mountain switchbacks, brake for hairpins and pass on the straights.',controls:'Up: accelerate · Down: brake / reverse · Left / right: steer · R: cockpit / chase camera',color:'#ff97b3',three:true}, {id:'dash',title:'Neon Dash',tag:'FIVE LEVELS',description:'Five long portal courses: jump as a cube, fly a ship, flip a ball and steer a wave.',controls:'Hold / tap: jump, fly or flip · Blue platforms are safe',color:'#a8b6ff',local:true}, {id:'marble',title:'Marble Run 3D',tag:'3D · THREE COURSES',description:'Race against the clock through boosts, jump gaps, narrow turns and moving sweepers.',controls:'Arrow keys / WASD: roll · Space: jump · Q: brake',color:'#9de6da',local:true,three:true}, {id:'space',title:'Space Survival 3D',tag:'3D · ARCADE',description:'Battle formations of alien ships, dodge diving fighters and collect shield energy.',controls:'Arrow keys / WASD: fly · Space: fire',color:'#e7bbff',local:true,three:true}, 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 c5fa30a..18c2775 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 @@ -123,12 +123,14 @@ export const RALLY_COURSES=[ {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]]}, - {name:'Coastal Sprint',width:24,scale:1,heights:[1,3,6,8,7,4,1,0,0,1],laps:2,theme:'harbor',points:[[150,0],[145,65],[80,100],[-35,105],[-145,65],[-170,-10],[-120,-90],[-20,-105],[90,-90],[155,-55]]}, - {name:'Red Mesa Run',width:20,scale:1,heights:[0,5,14,25,31,27,20,12,6,2,0,0],laps:2,theme:'canyon',points:[[160,0],[150,75],[75,120],[0,95],[-70,130],[-155,85],[-175,0],[-130,-75],[-55,-110],[10,-75],[80,-110],[150,-65]]}, - {name:'Pine Ridge',width:20,scale:1,heights:[0,6,14,22,30,35,30,24,17,10,4,0],laps:2,theme:'alpine',mountains:[[-220,160,65,90],[190,205,78,95],[-205,-155,60,85],[50,-220,68,90]],points:[[140,-10],[130,65],[60,105],[-10,70],[-75,115],[-145,75],[-165,0],[-120,-80],[-40,-110],[25,-60],[95,-100],[150,-65]]}, - {name:'Old Town Loop',width:20,scale:1,heights:[0,1,4,8,7,4,1,0,0,2,5,6,3,0],laps:2,theme:'city',points:[[150,0],[145,85],[65,110],[5,65],[-55,110],[-145,85],[-165,10],[-120,-25],[-145,-105],[-55,-120],[5,-70],[75,-115],[155,-95],[185,-40]]}, - {name:'Summit Grand Prix',width:20,scale:1,heights:[0,8,20,35,50,63,70,62,52,42,32,22,12,4,0,0],laps:2,theme:'alpine',mountains:[[-240,160,140,100],[205,210,155,95],[-210,-180,125,100],[40,-245,130,90]],points:[[185,-60],[190,30],[145,105],[70,150],[-15,115],[-85,165],[-170,120],[-200,40],[-150,-20],[-200,-95],[-155,-170],[-65,-190],[0,-125],[70,-175],[150,-150],[205,-115]]}, - {name:'Sunset Docks',width:22,scale:1,heights:[0,2,5,9,12,10,5,1,0,2,6,9,5,0],laps:2,theme:'harbor',points:[[160,-10],[150,70],[80,120],[0,90],[-80,125],[-160,70],[-175,-10],[-105,-50],[-155,-125],[-65,-150],[10,-105],[85,-150],[165,-110],[200,-65]]}, + {name:'Coastal Sprint',scenery:'coast',width:20,scale:1,heights:[1,3,6,8,7,4,1,0,0,1],laps:2,theme:'harbor',points:[[150,0],[145,65],[80,100],[-35,105],[-145,65],[-170,-10],[-120,-90],[-20,-105],[90,-90],[155,-55]]}, + {name:'Red Mesa Run',scenery:'mesa',width:18,scale:1,heights:[0,5,14,25,31,27,20,12,6,2,0,0],laps:2,theme:'canyon',points:[[160,0],[150,75],[75,120],[0,95],[-70,130],[-155,85],[-175,0],[-130,-75],[-55,-110],[10,-75],[80,-110],[150,-65]]}, + {name:'Pine Ridge',scenery:'ridge',width:18,scale:1,heights:[0,6,14,22,30,35,30,24,17,10,4,0],laps:2,theme:'alpine',mountains:[[-220,160,65,90],[190,205,78,95],[-205,-155,60,85],[50,-220,68,90]],points:[[140,-10],[130,65],[60,105],[-10,70],[-75,115],[-145,75],[-165,0],[-120,-80],[-40,-110],[25,-60],[95,-100],[150,-65]]}, + {name:'Old Town Loop',scenery:'town',width:18,scale:1,heights:[0,1,4,8,7,4,1,0,0,2,5,6,3,0],laps:2,theme:'city',points:[[150,0],[145,85],[65,110],[5,65],[-55,110],[-145,85],[-165,10],[-120,-25],[-145,-105],[-55,-120],[5,-70],[75,-115],[155,-95],[185,-40]]}, + {name:'Summit Grand Prix',scenery:'summit',width:18,scale:1,heights:[0,8,20,35,50,63,70,62,52,42,32,22,12,4,0,0],laps:2,theme:'alpine',mountains:[[-240,160,140,100],[205,210,155,95],[-210,-180,125,100],[40,-245,130,90]],points:[[185,-60],[190,30],[145,105],[70,150],[-15,115],[-85,165],[-170,120],[-200,40],[-150,-20],[-200,-95],[-155,-170],[-65,-190],[0,-125],[70,-175],[150,-150],[205,-115]]}, + {name:'Sunset Docks',scenery:'docks',width:20,scale:1,heights:[0,2,5,9,12,10,5,1,0,2,6,9,5,0],laps:2,theme:'harbor',points:[[160,-10],[150,70],[80,120],[0,90],[-80,125],[-160,70],[-175,-10],[-105,-50],[-155,-125],[-65,-150],[10,-105],[85,-150],[165,-110],[200,-65]]}, + {name:'Aurora Fjord',scenery:'aurora',width:18,scale:1,heights:[0,4,11,20,28,34,30,23,15,8,3,0],laps:2,theme:'aurora',mountains:[[-210,170,90,85],[200,170,110,95],[-220,-150,80,90]],points:[[150,-15],[150,65],[80,120],[0,85],[-70,130],[-150,85],[-180,0],[-125,-75],[-50,-120],[25,-75],[100,-115],[165,-75]]}, + {name:'Polar Night Pass',scenery:'polar',width:18,scale:1,heights:[0,6,15,26,39,44,38,30,21,12,5,0,0,0],laps:2,theme:'polar',mountains:[[-215,175,110,95],[210,190,125,85],[-220,-180,95,90],[40,-220,80,80]],points:[[165,-35],[175,50],[110,120],[30,155],[-40,110],[-120,155],[-185,85],[-195,-5],[-125,-45],[-165,-125],[-75,-165],[5,-110],[95,-165],[180,-110]]}, ]; 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/game-mechanics.test.js b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/game-mechanics.test.js index 5a7683a..3441064 100644 --- a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/game-mechanics.test.js +++ b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/game-mechanics.test.js @@ -64,7 +64,7 @@ test('every full Dash course is completable across all four modes without losing } }); test('Rally tracks differ and road proximity follows their curves, not an ellipse',()=>{ - assert.equal(RALLY_COURSES.length,10);assert.equal(new Set(RALLY_COURSES.map(c=>c.theme)).size,4); + assert.equal(RALLY_COURSES.length,12);assert.equal(new Set(RALLY_COURSES.map(c=>c.theme)).size,6); assert.equal(new Set(RALLY_COURSES.map(c=>c.name)).size,RALLY_COURSES.length); assert.equal(new Set(RALLY_COURSES.map(c=>JSON.stringify(c.points))).size,RALLY_COURSES.length); for(const c of RALLY_COURSES){const curve=new THREE.CatmullRomCurve3(c.points.map(([x,z])=>new THREE.Vector3(x,0,z)),true,'centripetal'),p=curve.getSpacedPoints(128).slice(0,-1);assert.ok(roadDistance(p,p[20].x,p[20].z)<.01);assert.ok(roadDistance(p,10000,10000)>c.width);assert.ok(curve.getLength()>150);} 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 a542dc5..bc71bb4 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 @@ -5,6 +5,7 @@ import {rallyRoad,rallySample,newRallyRace,rallyStep,newRallyView,rallyViewStep} import {buildRallyCar,rallyNavigation} from './rally-car.js'; export {buildRallyCar} from './rally-car.js'; import {rallyTerrain} from './rally-terrain.js'; +import {rallyEnvironment,addRallyScenery} from './rally-scenery.js'; // Continuous ribbons share section vertices; inclines have no stacked road boxes. export function rallyRibbon(road,inner,outer,lift=0){ const pos=[],uv=[],indices=[],n=road.points.length; @@ -15,40 +16,42 @@ export function rallyRibbon(road,inner,outer,lift=0){ const geo=new THREE.BufferGeometry();geo.setAttribute('position',new THREE.Float32BufferAttribute(pos,3));geo.setAttribute('uv',new THREE.Float32BufferAttribute(uv,2));geo.setIndex(indices);geo.computeVertexNormals();return geo; } export function setupRally({scene,camera,light,high,level,laps,player,mesh,mat,textures,materials,finish,notify}){ - const course=RALLY_COURSES[level]||RALLY_COURSES[0],road=rallyRoad(course,laps),race=newRallyRace(road),view=newRallyView(),night=course.theme==='city'; - scene.background=new THREE.Color(night?'#14243a':course.theme==='canyon'?'#cda385':'#a7c9dd');scene.fog=new THREE.Fog(scene.background,180,540);camera.far=650;camera.updateProjectionMatrix(); + const course=RALLY_COURSES[level]||RALLY_COURSES[0],road=rallyRoad(course,laps),race=newRallyRace(road),view=newRallyView(),style=rallyEnvironment(course),night=style?.night??(course.theme==='city'); + scene.background=new THREE.Color(style?.sky||(night?'#14243a':course.theme==='canyon'?'#cda385':'#a7c9dd'));scene.fog=new THREE.Fog(scene.background,180,540);camera.far=650;camera.updateProjectionMatrix(); light.color.set(night?'#a7cfff':'#ffe5c5');light.intensity=night?2.1:3.4; light.shadow.bias=-.00012;light.shadow.normalBias=.025; Object.assign(light.shadow.camera,{left:-32,right:32,top:32,bottom:-32,far:140}); if(high){const sky=new Uint8Array(256*128*4);for(let y=0;y<128;y++)for(let x=0;x<256;x++){const h=y/127,sun=Math.exp(-((x-172)**2+(y-40)**2)/35),i=(y*256+x)*4;sky[i]=night?18+35*h:80+150*h+sun*25;sky[i+1]=night?28+50*h:145+100*h;sky[i+2]=night?50+65*h:200+35*h;sky[i+3]=255;} + if(style){const top=new THREE.Color(style.sky).convertLinearToSRGB(),horizon=new THREE.Color(style.horizon).convertLinearToSRGB();for(let y=0;y<128;y++){const color=top.clone().lerp(horizon,y/127);for(let x=0;x<256;x++){const i=(y*256+x)*4;sky[i]=color.r*255;sky[i+1]=color.g*255;sky[i+2]=color.b*255;}}} const env=new THREE.DataTexture(sky,256,128);env.mapping=THREE.EquirectangularReflectionMapping;env.colorSpace=THREE.SRGBColorSpace;env.needsUpdate=true;scene.environment=env;scene.background=env;textures.push(env); } - const asphalt=mat('#3b4147');asphalt.roughness=.92;asphalt.metalness=0; + const asphalt=mat(style?.road||'#3b4147');asphalt.roughness=.92;asphalt.metalness=0; if(high){const data=new Uint8Array(64*64*4);for(let i=0;i<4096;i++){const v=145+(i*31+i%19*7)%65;data[i*4]=data[i*4+1]=data[i*4+2]=v;data[i*4+3]=255;}const grain=new THREE.DataTexture(data,64,64);grain.wrapS=grain.wrapT=THREE.RepeatWrapping;grain.needsUpdate=true;asphalt.map=grain;asphalt.bumpMap=grain;asphalt.bumpScale=.025;textures.push(grain);} mesh(rallyRibbon(road,-course.width/2,course.width/2),asphalt); - const verge=mat(course.theme==='canyon'?'#a87856':course.theme==='alpine'?'#739371':'#7c947c');verge.metalness=0;verge.roughness=1; + const verge=mat(style?.verge||(course.theme==='canyon'?'#a87856':course.theme==='alpine'?'#739371':'#7c947c'));verge.metalness=0;verge.roughness=1; for(const side of [-1,1]){const lo=side<0?-course.width/2-3:course.width/2,hi=lo+3;mesh(rallyRibbon(road,lo,hi,-.08),verge);} - const red=mat('#bb4b43'),white=mat('#e6dfce');red.metalness=white.metalness=0; + const red=mat(style?.accent||'#bb4b43'),white=mat('#e6dfce');red.metalness=white.metalness=0; for(const side of [-1,1]){const lo=side<0?-course.width/2-.5:course.width/2;mesh(rallyRibbon(road,lo,lo+.5,.045),red);} const stripe=mat('#fff5cc');stripe.polygonOffset=true;stripe.polygonOffsetFactor=-1;stripe.polygonOffsetUnits=-1;mesh(rallyRibbon(road,-.05,.05,.014),stripe); // Elevated roads are supported by piers; terrain does not clip through the road. - const terrainMat=mat(course.theme==='alpine'?'#ffffff':course.theme==='canyon'?'#b18a65':'#719077');terrainMat.vertexColors=course.theme==='alpine';terrainMat.metalness=0;terrainMat.roughness=.96;const terrain=mesh(rallyTerrain(course,road,high),terrainMat);terrain.userData.softwareHidden=true; + const terrainMat=mat(style?.terrain?'#ffffff':style?.ground||(course.theme==='alpine'?'#ffffff':course.theme==='canyon'?'#b18a65':'#719077'));terrainMat.vertexColors=Boolean(style?.terrain)||course.theme==='alpine';terrainMat.metalness=0;terrainMat.roughness=.96;const terrain=mesh(rallyTerrain(course,road,high),terrainMat);terrain.userData.softwareHidden=true; for(let i=0;i1)mesh(new THREE.CylinderGeometry(.75,1.1,point.y+1.1,8),mat('#949996'),point.x,(point.y-1.1)/2,point.z);} const start=rallySample(road,0),banner=new THREE.Group();scene.add(banner);banner.position.set(start.x,start.y,start.z);banner.rotation.y=-start.heading; mesh(new THREE.BoxGeometry(.15,5,.15),white,0,2.5,-course.width/2-1,banner);mesh(new THREE.BoxGeometry(.15,5,.15),white,0,2.5,course.width/2+1,banner);mesh(new THREE.BoxGeometry(.25,.6,course.width+2.2),red,0,5,0,banner); for(let i=0;i<14;i++)mesh(new THREE.BoxGeometry(.8,.016,course.width/14),i%2?white:asphalt,0,.018,-course.width/2+(i+.5)*course.width/14,banner); // Trackside details are distributed along the longer routes, away from the road. const treeMat=mat('#345342'),bark=mat('#62574d'),rockMat=mat('#93968a'),buildingMat=mat(night?'#354352':'#7c898b'); - const count=high?90:28; + const count=style?0:high?90:28; for(let i=0;i20){const side=bend>0?-1:1,g=rallySample(road,d,side*(course.width/2+1));const beam=mesh(new THREE.BoxGeometry(high?12:28,.28,.12),rail,g.x,g.y+.8,g.z);beam.rotation.y=-g.heading;beam.rotation.z=Math.atan(g.grade);mesh(new THREE.CylinderGeometry(.055,.055,1.15,6),rail,g.x,g.y+.42,g.z);} + if((course.theme==='alpine'||style?.aurora)&&f.y>20){const side=bend>0?-1:1,g=rallySample(road,d,side*(course.width/2+1));const beam=mesh(new THREE.BoxGeometry(high?12:28,.28,.12),rail,g.x,g.y+.8,g.z);beam.rotation.y=-g.heading;beam.rotation.z=Math.atan(g.grade);mesh(new THREE.CylinderGeometry(.055,.055,1.15,6),rail,g.x,g.y+.42,g.z);} if(Math.abs(bend)>.65&&Math.floor(d/12)%3===0){const g=rallySample(road,d,course.width/2+2),sign=mesh(new THREE.BoxGeometry(.08,.9,1.1),white,g.x,g.y+1.8,g.z);sign.rotation.y=-g.heading;mesh(new THREE.CylinderGeometry(.055,.055,1.5,6),rail,g.x,g.y+.75,g.z);} } const paints=['#d84e71','#3da5bd','#f3bd47','#536ed0','#78a755'],cars=[]; @@ -64,6 +67,7 @@ export function setupRally({scene,camera,light,high,level,laps,player,mesh,mat,t if(view.changed){camera.fov=view.cockpit?88:58;camera.near=view.cockpit?.025:.1;camera.updateProjectionMatrix();} if(view.cockpit){player.updateMatrixWorld(true);const eye=player.localToWorld(new THREE.Vector3(-.64,1.23,.34)),target=player.localToWorld(new THREE.Vector3(24,1.18,.34));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);} + animateScenery(race.time,camera); } 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); @@ -72,3 +76,4 @@ export function setupRally({scene,camera,light,high,level,laps,player,mesh,mat,t 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 · ${road.laps} laps in ${race.time.toFixed(1)} seconds.`,score);} }}; } + 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 393de1a..c39fc96 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 @@ -46,7 +46,7 @@ export async function createRallyRenderer(canvas,onGraphics){ 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; + 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.fogEnabled=source.fog!==false;m.disableDepthWrite=source.depthWrite===false;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; @@ -68,3 +68,4 @@ export async function createRallyRenderer(canvas,onGraphics){ scene.render(); },dispose(){disposed=true;scene.dispose();engine.dispose();}}; } + diff --git a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-scenery.js b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-scenery.js new file mode 100644 index 0000000..45ebcaa --- /dev/null +++ b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-scenery.js @@ -0,0 +1,75 @@ +import * as THREE from 'three'; + +// Bundled colors and original procedural scenery: no image downloads or network access. +export const RALLY_ENVIRONMENTS={ + coast:{sky:'#559cbd',horizon:'#ffe4b2',ground:'#dfc28b',verge:'#e4ce9f',road:'#4b5159',accent:'#36c5b6',props:'palms'}, + mesa:{sky:'#c58262',horizon:'#ffd19b',ground:'#a95537',verge:'#d38c57',road:'#493d37',accent:'#ffd06d',props:'cacti'}, + ridge:{sky:'#7a9ca2',horizon:'#edd1a5',ground:'#717e42',verge:'#a39355',road:'#3b4340',accent:'#f4ab4c',props:'autumn',terrain:['#718044','#80765a','#eee0bf']}, + town:{sky:'#72649b',horizon:'#f7b7a2',ground:'#987c79',verge:'#c9b4a2',road:'#574d62',accent:'#ef96c4',props:'houses'}, + summit:{sky:'#42759c',horizon:'#d9f1ff',ground:'#e1edf0',verge:'#b9d5df',road:'#445466',accent:'#61d7f0',props:'snow',terrain:['#93b8c5','#c0d2dc','#f3fbff']}, + docks:{sky:'#874b64',horizon:'#ffb274',ground:'#325f73',verge:'#9c826d',road:'#4a4249',accent:'#ffb869',props:'cargo'}, + aurora:{sky:'#071c32',horizon:'#173b4f',ground:'#adcdd9',verge:'#d4edf0',road:'#364c60',accent:'#63ffd3',props:'frozen',night:true,aurora:true,terrain:['#87b2c2','#bdd9e1','#ecffff']}, + polar:{sky:'#151530',horizon:'#30345c',ground:'#c1bbdf',verge:'#e5def7',road:'#464960',accent:'#ce9cff',props:'crystals',night:true,aurora:true,terrain:['#909ac0','#c5c4df','#f0ecff']}, +}; +export const rallyEnvironment=course=>RALLY_ENVIRONMENTS[course.scenery]||null; + +export function auroraCurtain(radius,phase,segments=80){ + const positions=[],colors=[],indices=[]; + for(let i=0;i<=segments;i++){ + const a=i/segments*Math.PI*2,x=Math.cos(a)*radius,z=Math.sin(a)*radius; + const base=50+12*Math.sin(a*3+phase)+7*Math.cos(a*7-phase); + const tint=new THREE.Color().setHSL(.39+.2*(.5+.5*Math.sin(a*2+phase)),.9,.58); + for(const height of [base,base+28+12*Math.sin(a*5+phase)]){ + positions.push(x,height,z);colors.push(tint.r,tint.g,tint.b); + } + if(i{}; + const leaf=mat(style.props==='autumn'?'#d88336':'#35866d'),trunk=mat('#79604e'),stone=mat(style.ground),ice=mat('#a2e4f0'),wood=mat('#997258'); + const count=high?54:26; + for(let i=0;i{sky.position.set(camera.position.x,0,camera.position.z);curtains.forEach((curtain,i)=>{curtain.rotation.y=Math.sin(time*.06+i)*.035;curtain.material.emissiveIntensity=.8+.18*Math.sin(time*.3+i);});}; + } + return ()=>{}; +} diff --git a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-scenery.test.js b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-scenery.test.js new file mode 100644 index 0000000..bf88d8b --- /dev/null +++ b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-scenery.test.js @@ -0,0 +1,39 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import * as THREE from 'three'; +import {RALLY_COURSES} from './game-mechanics.js'; +import {rallyRoad,rallySample} from './racing-mechanics.js'; +import {rallyEnvironment,addRallyScenery,auroraCurtain} from './rally-scenery.js'; + +test('original tracks retain their scenery while all eight additions have different environments',()=>{ + assert.deepEqual(RALLY_COURSES.slice(0,4).map(c=>[c.name,c.width,c.scenery]),[['Harbor Circuit',20,undefined],['Canyon Switchback',18,undefined],['Alpine Hairpins',18,undefined],['Midnight Metro',20,undefined]]); + const added=RALLY_COURSES.slice(4); + assert.equal(added.length,8);assert.equal(new Set(added.map(c=>c.scenery)).size,8); + assert.equal(new Set(added.map(c=>rallyEnvironment(c).sky)).size,8); + for(const course of added){assert.ok(course.width>=18&&course.width<=20);assert.ok(rallyEnvironment(course).props);} + assert.equal(added.filter(c=>rallyEnvironment(c).aurora).length,2); +}); + +test('aurora curtains form closed finite ribbons with visible color variation',()=>{ + for(const segments of [48,112]){ + const geometry=auroraCurtain(320,1.7,segments),positions=geometry.attributes.position,colors=geometry.attributes.color; + assert.equal(positions.count,(segments+1)*2);assert.equal(geometry.index.count,segments*6); + assert.ok(new Set(Array.from(colors.array).map(v=>v.toFixed(3))).size>20); + for(const value of positions.array)assert.ok(Number.isFinite(value)); + for(const value of colors.array)assert.ok(value>=0&&value<=1); + for(let j=0;j<2;j++)assert.ok(new THREE.Vector3().fromBufferAttribute(positions,j).distanceTo(new THREE.Vector3().fromBufferAttribute(positions,segments*2+j))<.001); + geometry.dispose(); + } +}); + +test('all new scenery builds in both qualities and northern lights animate without casting shadows',()=>{ + for(const high of [false,true])for(const course of RALLY_COURSES.slice(4)){ + const scene=new THREE.Scene(),geometries=[],materials=[],road=rallyRoad(course); + 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=scene)=>{geometries.push(g);const node=new THREE.Mesh(g,m);node.position.set(x,y,z);parent.add(node);return node;}; + const animate=addRallyScenery({course,road,high,scene,mesh,mat,sample:rallySample}),camera=new THREE.PerspectiveCamera();camera.position.set(30,4,-20);animate(2,camera); + assert.ok(geometries.length>20,course.name); + if(rallyEnvironment(course).aurora){const curtain=scene.getObjectByName('northern-lights-0'),before=curtain.rotation.y;assert.ok(curtain);assert.equal(curtain.castShadow,false);assert.equal(curtain.material.fog,false);assert.equal(curtain.parent.position.x,30);animate(8,camera);assert.notEqual(curtain.rotation.y,before);} + for(const geometry of geometries)geometry.dispose();for(const material of materials)material.dispose(); + } +}); diff --git a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-terrain.js b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-terrain.js index dbd0566..b95c625 100644 --- a/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-terrain.js +++ b/BeeBright-Full-Stack/beebright-spelling-bee/frontend/src/rally-terrain.js @@ -1,7 +1,8 @@ import * as THREE from 'three'; import {clamp} from './game-mechanics.js'; +import {rallyEnvironment} from './rally-scenery.js'; export function mountainHeight(course,x,z){let h=-2;for(const [mx,mz,peak,radius] of course.mountains||[])h+=peak*Math.exp(-((x-mx)**2+(z-mz)**2)/(radius*radius));return h+Math.sin(x*.07)*Math.cos(z*.055)*3;} -export function rallyTerrain(course,road,high){const alpine=course.theme==='alpine',extent=Math.max(...road.points.flatMap(p=>[Math.abs(p.x),Math.abs(p.z)])),size=Math.max(alpine?650:360,2*(extent+course.width/2+50)),geo=new THREE.PlaneGeometry(size,size,alpine?(high?128:64):1,alpine?(high?128:64):1);geo.rotateX(-Math.PI/2);const pos=geo.attributes.position,colors=[],grass=new THREE.Color('#657755'),rock=new THREE.Color('#858885'),snow=new THREE.Color('#e4e9e8'); +export function rallyTerrain(course,road,high){const alpine=course.theme==='alpine'||Boolean(course.mountains),style=rallyEnvironment(course),extent=Math.max(...road.points.flatMap(p=>[Math.abs(p.x),Math.abs(p.z)])),size=Math.max(alpine?650:360,2*(extent+course.width/2+50)),geo=new THREE.PlaneGeometry(size,size,alpine?(high?128:64):1,alpine?(high?128:64):1);geo.rotateX(-Math.PI/2);const pos=geo.attributes.position,colors=[],grass=new THREE.Color(style?.terrain?.[0]||'#657755'),rock=new THREE.Color(style?.terrain?.[1]||'#858885'),snow=new THREE.Color(style?.terrain?.[2]||'#e4e9e8'); for(let k=0;k{const e=document.querySelector('[aria-label=\"Neon Dash course\"]');e.value='4';e.dispatchEvent(new Event('change',{bubbles:true}));})()") w.evaluate_js("(()=>{const e=document.querySelector('[aria-label=\"Marble Run course\"]');e.value='2';e.dispatchEvent(new Event('change',{bubbles:true}));})()") assert w.evaluate_js("document.querySelector('[aria-label=\"Neon Dash course\"]').value==='4' && document.querySelector('[aria-label=\"Marble Run course\"]').value==='2'") - assert w.evaluate_js("document.querySelector('[aria-label=\"Neon Rally track\"]').options.length===10") + assert w.evaluate_js("document.querySelector('[aria-label=\"Neon Rally track\"]').options.length===12") w.evaluate_js("Array.from(document.querySelectorAll('.game-card')).find(c=>c.querySelector('h2').innerText==='Pocket Bowling').querySelector('button').click()") wait(w,"document.querySelector('.game-overlay .primary') !== null && !document.querySelector('.game-overlay .primary').disabled") w.evaluate_js("document.querySelector('.game-overlay .primary').click()") @@ -133,9 +133,9 @@ def test(w): wait(w,"document.querySelectorAll('.game-card').length===8") # Exercise every independent Rally track in the installed High quality UI. track_names = w.evaluate_js("Array.from(document.querySelector('[aria-label=\"Neon Rally track\"]').options, o=>o.text.replace(/^\\d+\\. /,''))") - assert len(track_names) == 10 and len(set(track_names)) == 10 + assert len(track_names) == 12 and len(set(track_names)) == 12 for track, track_name in enumerate(track_names): - w.evaluate_js("(()=>{const s=document.querySelector('[aria-label=\"Race laps\"]');s.value='"+str(track+1)+"';s.dispatchEvent(new Event('change',{bubbles:true}));})()") + w.evaluate_js("(()=>{const s=document.querySelector('[aria-label=\"Race laps\"]');s.value='"+str(track%10+1)+"';s.dispatchEvent(new Event('change',{bubbles:true}));})()") w.evaluate_js("(()=>{const s=document.querySelector('[aria-label=\"Neon Rally track\"]');s.value='"+str(track)+"';s.dispatchEvent(new Event('change',{bubbles:true}));})()") w.evaluate_js("Array.from(document.querySelectorAll('.game-card')).find(c=>c.querySelector('h2').innerText==='Neon Rally').querySelector('button').click()") wait(w,"document.querySelector('.game-overlay .primary')!==null && !document.querySelector('.game-overlay .primary').disabled") @@ -186,7 +186,7 @@ def test(w): wait(w,"document.querySelector('.arcade-player-bar').innerText.includes('Lowering') || document.querySelector('.arcade-player-bar').innerText.includes('Crossing')") w.evaluate_js("document.querySelector('.arcade-player-bar button').click()") wait(w,"document.querySelectorAll('.game-card').length===8") - print('Ten High quality Rally tracks and hold/release bridge controls passed.') + print('Twelve High quality Rally tracks and hold/release bridge controls passed.') # Leaving and resuming must retain the earned break and its original deadline. w.evaluate_js("document.querySelector('.brand').click()") wait(w,"document.querySelector('.hero-copy') !== null")