From 538f393576b9cbfee7df2e128834ecdded008b7f Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Sun, 27 Sep 2026 17:28:58 -0700 Subject: [PATCH] oskiewar v187: quiet wheels, audible pulse, idle close-up and film finish --- xbox/live/oskiewar.js | 25 ++++++++++----- xbox/live/tests/pool-playground.test.mjs | 39 ++++++++++++++++++++++-- xbox/native-bios/PostPixelShader.hlsl | 19 +++++++----- 3 files changed, 66 insertions(+), 17 deletions(-) diff --git a/xbox/live/oskiewar.js b/xbox/live/oskiewar.js index 40ccf4eee2..0aae247e9b 100644 --- a/xbox/live/oskiewar.js +++ b/xbox/live/oskiewar.js @@ -102,7 +102,7 @@ if (hostAnalytics) const hostAc = typeof ac === "function" ? ac : null; // Monotonic count of committed revisions to this piece (next revision included). -const buildVersion = 186; +const buildVersion = 187; const floorY = 1800; // Oskiewar now opens as a versus game. An ordinary web visit hosts a room — // the URL becomes the invitation — and until a friend opens it, all you can @@ -9539,6 +9539,7 @@ function deathOrbitShot(target, width, progress) { } let poolCameraYaw = 0, poolCameraDip = 0, poolCameraReturnYaw = null; +let poolIdleSeconds=0,poolIdleClose=0; // Keep the line from the lens to the rider above the coping and curved walls. function clearPoolCamera(position, subject) { // Follow the rider into the lot without putting a solid wall between @@ -9565,6 +9566,10 @@ function clearPoolCamera(position, subject) { function updateCameraDoll(dt, now) { if (poolOnly() && freeskateActive()) { const p=players[0],heading=p.poolYaw||0,speed=Math.hypot(p.vx,p.vz||0); + const idle=p.alive&&p.grounded&&speed<35&&Math.abs(p.vy||0)<40&& + !p.spin&&!p.attackKind&&Math.abs(p.inputX||0)<.1&&Math.abs(p.inputY||0)<.1; + poolIdleSeconds=idle?poolIdleSeconds+dt:0; + poolIdleClose+=((poolIdleSeconds>2?1:0)-poolIdleClose)*(1-Math.exp(-dt*(idle?1.2:7))); const wall=poolDistance(p.x,p.z||0),vert=p.poolVert; let goal=heading; if(vert){ @@ -9589,8 +9594,8 @@ function updateCameraDoll(dt, now) { poolCameraDip+=(descent-poolCameraDip)*(1-Math.exp(-dt*4.5)); const zoom=playerCameraZoom,angle=poolCameraYaw+.14*(1-poolCameraDip)+playerCameraYaw; const airHeight=Math.max(0,poolFloorAt(p.x,p.z||0)-p.y); - const distance=(1350+Math.min(850,airHeight*.35)-poolCameraDip*120)*zoom; - const look=340+Math.min(450,speed*.16)+(poolCameraReturnYaw!==null?lerp(140,420,clamp(airHeight/600,0,1)):0); + const distance=lerp(1350+Math.min(850,airHeight*.35)-poolCameraDip*120,270,poolIdleClose)*zoom; + const look=(340+Math.min(450,speed*.16)+(poolCameraReturnYaw!==null?lerp(140,420,clamp(airHeight/600,0,1)):0))*(1-poolIdleClose); const lookX=p.x+Math.cos(goal)*look,lookZ=(p.z||0)+Math.sin(goal)*look; const groundFocus=Math.max(poolCameraDip*.28,clamp(airHeight/650,0,1)*.32); const target={x:lerp(p.x,lookX,.65+poolCameraDip*.2), @@ -9602,7 +9607,7 @@ function updateCameraDoll(dt, now) { target.y=clamp(target.y,p.y-240,p.y+50); const subject={x:p.x,y:p.y-2,z:p.z||0}; const position=clearPoolCamera({x:target.x-Math.cos(angle)*distance, - y:p.y-85-(lerp(145,480,clamp(airHeight/500,0,1))+poolCameraDip*110+Math.min(240,airHeight*.12))*zoom-playerCameraPitch*600, + y:p.y-85-lerp(lerp(145,480,clamp(airHeight/500,0,1))+poolCameraDip*110+Math.min(240,airHeight*.12),60,poolIdleClose)*zoom-playerCameraPitch*600, z:target.z-Math.sin(angle)*distance},subject); cameraCenter=target.x;cameraCenterY=target.y;cameraWidth=1650*zoom; cameraDoll.track({target,position,width:cameraWidth,perspective:1,fov:58+poolCameraDip*5,roll:0},dt,8); @@ -22539,14 +22544,17 @@ let skateAudioLevel=0; function updateSkateAudio(dt){ let speed=0; for(const p of activePlayers())if(p.alive&&p.skateboard&&!p.onewheel&&p.grounded) - speed=Math.max(speed,Math.hypot(p.vx,p.vy,p.vz||0)); + speed=Math.max(speed,Math.hypot(p.vx,p.vz||0)); const rider=activePlayers()[0]; for(const b of balls)if(b.active&&b.type==='skateboard'&&Math.abs(b.y+18-terrainFloorAt(b.x,b.z||0))<5) speed=Math.max(speed,Math.hypot(b.vx,b.vz||0)*clamp(1-Math.hypot(b.x-rider.x,(b.z||0)-(rider.z||0))/1800,0,1)); - const amount=clamp(speed/3200,0,1),target=speed>30?.025+Math.sqrt(amount)*.28:0; + // Keep the native loop below its buzzing high-speed register. Ground speed + // controls a quiet rumble, with no fixed-volume step when wheels start moving. + const amount=clamp(speed/3200,0,1); + const target=speed>30?(.006+Math.sqrt(amount)*.055)*clamp((speed-30)/150,0,1):0; skateAudioLevel+=(target-skateAudioLevel)*(1-Math.exp(-dt*(target?12:45))); if(skateAudioLevel<.0005)skateAudioLevel=0; - if(!netSilent&&typeof skateAudio==='function')skateAudio(amount,skateAudioLevel); + if(!netSilent&&typeof skateAudio==='function')skateAudio(.03+amount*.17,skateAudioLevel); } let motorFrequency=75,motorRunning=false,motorSpeed=0; function updateMotorAudio(dt){ @@ -23132,7 +23140,10 @@ function updateSeatHeartbeat(dt,now){ danger=Math.max(danger,Math.min(1,(p.hit||0)*2)); const rest=62+(p.pad*7%19),effort=Math.min(1,Math.hypot(p.vx,p.vy)/1800),target=rest+danger*85+effort*25; p.heartRate=(p.heartRate||rest)+(target-(p.heartRate||rest))*(1-Math.exp(-realDt*(target>(p.heartRate||rest)?1.8+(p.pad%3)*.2:.23+(p.pad%4)*.04))); + const previousHeartPhase=p.heartPhase; p.heartDanger=danger;p.heartPhase=((p.heartPhase ?? (p.pad*.173)%1)+realDt*p.heartRate/60)%1; + if(p===players[0]&&previousHeartPhase!==undefined&&p.heartPhase({halfpipe:parkHalfPipe3D})};`)(()=>({monotonicUs:now}),()=>({platform:'web'}),noop,noop,(name,gain,pan)=>drums.push({name,gain,pan}),noop,noop,noop,noop,noop,noop,(hz,gain)=>audio.push({hz,gain}),()=>audio.push({stop:true})); const p=api.players[0];Object.assign(p,{x:1080,z:0,y:api.poolFloorAt(1080,0),grounded:true,alive:true,dummy:false,skateboard:false,poolYaw:0,previous:[],spin:null,directionChanges:[],poolLastSteer:0}); api.step=(down=[],dt=1/60)=>{now+=dt*1e6;api.updatePlayer(p,{down,leftX:0,leftY:0},dt,now);}; @@ -115,7 +115,7 @@ test('shots break window glass and continue into the lot',()=>{ a.updateBullets(1/60,a.now(),false);assert.equal(a.brokenParkWindows.size,1);assert.ok(a.bullets[0].z{ - const a=playground(),p=a.players[0];Object.assign(p,{x:2300,z:1700,y:a.poolFloorAt(2300,1700),vx:0,vz:0,vy:0,poolYaw:0}); + const a=playground(),p=a.players[0];Object.assign(p,{x:2300,z:1700,y:a.poolFloorAt(2300,1700),vx:400,vz:0,vy:0,poolYaw:0}); for(let i=0;i<180;i++)a.updateCameraDoll(1/60,a.now()); assert.ok(a.cameraDoll.target.x-p.x>200,'frame leads the rider'); assert.ok(p.y-a.cameraDoll.position.y<300,'lower than the old 345-unit boom'); @@ -191,3 +191,36 @@ test('3D pistol layers a crack and low report onto its shot',()=>{ const a=playground(),p=a.players[0];p.gunAmmo=3;p.gunMode='HANDGUN';a.step(['Y']); assert.ok(a.drums.some(d=>d.name==='snare'));assert.ok(a.drums.some(d=>d.name==='kick')); }); + +test('skateboard rumble stays low, follows horizontal movement, and fades at rest',()=>{ + const a=playground(),p=a.players[0],calls=[]; + globalThis.skateAudio=(...args)=>calls.push(args); + try{ + for(const b of a.balls)b.active=false; + Object.assign(p,{skateboard:true,onewheel:false,grounded:true,vx:3200,vy:0,vz:0}); + for(let i=0;i<120;i++)a.updateSkateAudio(1/60); + assert.ok(calls.at(-1)[0]<=.201,'native playback stays in its low register'); + assert.ok(calls.at(-1)[1]<.062,'rolling sound stays below the old .3 gain'); + p.vx=0;p.vy=1800; + for(let i=0;i<30;i++)a.updateSkateAudio(1/60); + assert.equal(calls.at(-1)[1],0,'vertical velocity does not sustain wheel sound'); + p.vx=1400;p.grounded=false; + a.updateSkateAudio(1/60);assert.equal(calls.at(-1)[1],0,'airborne wheels are silent'); + }finally{delete globalThis.skateAudio;} +}); + +test('idle camera eases close then opens back up when movement resumes',()=>{ + const a=playground(),p=a.players[0];Object.assign(p,{x:2300,z:1700,y:a.poolFloorAt(2300,1700),vx:0,vy:0,vz:0,grounded:true}); + for(let i=0;i<600;i++)a.updateCameraDoll(1/60,a.now()); + const gap=()=>Math.hypot(a.cameraDoll.position.x-p.x,a.cameraDoll.position.z-p.z); + assert.ok(gap()<300);assert.ok(Math.abs(a.cameraDoll.target.x-p.x)<5); + p.vx=400;for(let i=0;i<120;i++)a.updateCameraDoll(1/60,a.now()); + assert.ok(gap()>900);assert.ok(a.cameraDoll.target.x-p.x>200); +}); +test('BPM emits one quiet beat when the local heart phase wraps',()=>{ + const a=playground(),p=a.players[0];p.heartPhase=.999;p.heartRate=68; + a.updateSeatHeartbeat(1/60,a.now()); + assert.equal(a.drums.filter(d=>d.name==='kick'&&d.gain===.075).length,1); + a.updateSeatHeartbeat(1/60,a.now()); + assert.equal(a.drums.filter(d=>d.name==='kick'&&d.gain===.075).length,1); +}); diff --git a/xbox/native-bios/PostPixelShader.hlsl b/xbox/native-bios/PostPixelShader.hlsl index 2153ec1ac1..45f170e3dd 100644 --- a/xbox/native-bios/PostPixelShader.hlsl +++ b/xbox/native-bios/PostPixelShader.hlsl @@ -45,12 +45,11 @@ float4 main(PixelInput input) : SV_TARGET { const float3 center = sceneTexture.Sample(sceneSampler, input.uv).rgb; float3 color = center; - // Stable one-pixel dither and a very light scan modulation keep the output - // alive without hiding pixel art or softening nearest-neighbour sprites. - const float dither = frac(sin(dot(floor(input.position.xy), - float2(12.9898, 78.233))) * 43758.5453) - 0.5; - const float scan = 0.975 + 0.025 * sin(input.position.y * 3.14159265); - const float vignette = saturate(1.08 - radial * 0.18); + // Fine changing grain and gentle lens falloff, without raster scanlines. + const float grainFrame = floor(timeSeconds * 24.0); + const float grain = frac(sin(dot(floor(input.position.xy), + float2(12.9898, 78.233)) + grainFrame * 17.17) * 43758.5453) - 0.5; + const float vignette = 1.0 - smoothstep(0.25, 2.0, radial) * 0.13; if (stencilPass > 0.5) { // Compact FXAA runs only where triangle geometry wrote stencil. const float3 nw = sceneTexture.Sample(sceneSampler, @@ -89,6 +88,12 @@ float4 main(PixelInput input) : SV_TARGET { const float tilt = tiltPx * smoothstep(0.0, feather, away); if (tilt > 0.35) color = lerp(color, discBlur(input.uv, tilt), saturate(tilt / 2.0)); } - color = saturate(color * scan * vignette + dither / 255.0); + if (!hud) { + // A mild shoulder compresses bright paint without crushing dark detail. + color = color * (1.12 / (1.0 + color * 0.16)); + const float luminance = dot(color, float3(0.299, 0.587, 0.114)); + color = lerp(luminance.xxx, color, 0.96); + color = saturate(color * vignette + grain * (1.0 - luminance * 0.5) / 180.0); + } return float4(color, 1.0); } -- 2.51.2