From f29c55fb109691c7a0e2523ebab7f3d3ec6f1649 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Sun, 27 Sep 2026 17:24:41 -0700 Subject: [PATCH] oskiewar v186: restore 3D bullet ricochets and strengthen pistol audio --- xbox/live/oskiewar.js | 45 ++++++++++++++++++++---- xbox/live/tests/pool-playground.test.mjs | 25 +++++++++++-- 2 files changed, 60 insertions(+), 10 deletions(-) diff --git a/xbox/live/oskiewar.js b/xbox/live/oskiewar.js index e981ad1651..40ccf4eee2 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 = 185; +const buildVersion = 186; 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 @@ -10133,6 +10133,10 @@ function fireGun(player, input) { for (let shot = 0; shot < shots; shot++) { playDrum(laser ? "whoosh" : smg ? "block" : "hat", laser ? 1.35 : smg ? .62 + shot * .08 : 1.05, panPlayer(player)); + if (poolOnly() && !laser && !smg) { + playDrum("snare", .95, panPlayer(player)); + playDrum("kick", .65, panPlayer(player)); + } playSine(laser ? 1180 : (smg ? 310 : 760) + shot * 55, laser ? .13 : smg ? .055 : .08); } @@ -10439,23 +10443,50 @@ function recordBulletTrail(bullet) { bullet.trailCount++; } +// Reflect the full 3D velocity; keep one brief sound/flash per contact. +function ricochetPoolBullet(b,nx,ny,nz,now){ + if(b.laser){b.life=0;return;} + const length=Math.hypot(nx,ny,nz)||1;nx/=length;ny/=length;nz/=length; + const dot=b.vx*nx+b.vy*ny+(b.vz||0)*nz; + b.vx-=2*dot*nx;b.vy-=2*dot*ny;b.vz=(b.vz||0)-2*dot*nz; + if(now>=(b.nextRicochetSoundAt||0)){ + playDrum('hat',.85,panAt(b.x,b.z)); + playSine(1550+Math.min(900,Math.abs(dot)*.15),.045); + spawnImpact({x:b.x,y:b.y,z:b.z,life:.12,duration:.12,death:false,explosion:false}); + b.nextRicochetSoundAt=now+45000; + } +} function updatePoolBullets(dt,now,combat){ const t=(now-startedAt)/1e6; for(const b of bullets){ if(b.life<=0)continue; - b.previousX=b.x;b.previousY=b.y;b.previousZ=b.z;b.life-=dt; + b.previousX=b.x;b.previousY=b.y;b.previousZ=b.z; const steps=Math.max(1,Math.ceil(Math.hypot(b.vx,b.vy,b.vz||0)*dt/18)); for(let step=0;step0;step++){ const from={x:b.x,y:b.y,z:b.z}; b.x+=b.vx*dt/steps;b.y+=b.vy*dt/steps;b.z+=(b.vz||0)*dt/steps; - for(const {w,bay,u} of parkWallContact(from,b,0)){ + for(const {w,bay,u,old,fraction} of parkWallContact(from,b,0)){ + const hitY=lerp(from.y,b.y,fraction); + if(hitYparkDeckY)continue; const inGlass=u>bay*w.width+109&&u<(bay+1)*w.width-109&& - b.y>parkDeckY-parkBuildingHeight+269&&b.yparkDeckY-parkBuildingHeight+269&&hitY=0?1:-1; + b.x=w.ax+w.ux*u+w.nx*side*2;b.z=w.az+w.uz*u+w.nz*side*2;b.y=hitY; + ricochetPoolBullet(b,w.nx,0,w.nz,now); + } } if(b.life<=0)break; - if(b.y>=poolFloorAt(b.x,b.z)-4||(insidePark(b.x,b.z)&&b.ygridLeft+gridWidth+parkLotMargin|| + const floor=poolFloorAt(b.x,b.z)-4; + if(b.y>=floor){ + const slope=poolSlopeAt(b.x,b.z);b.y=floor-.1; + ricochetPoolBullet(b,slope.x,-1,slope.z,now); + }else if(insidePark(b.x,b.z)&&b.ygridLeft+gridWidth+parkLotMargin|| b.zworldFar+parkLotMargin){b.life=0;break;} if(!combat)continue; for(const target of [...activePlayers(),...parkKids]){ diff --git a/xbox/live/tests/pool-playground.test.mjs b/xbox/live/tests/pool-playground.test.mjs index ddbb47de0a..639d5b0378 100644 --- a/xbox/live/tests/pool-playground.test.mjs +++ b/xbox/live/tests/pool-playground.test.mjs @@ -3,7 +3,7 @@ import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; const source=await readFile(new URL('../oskiewar.js',import.meta.url),'utf8'); function playground(){ - let now=1e6;const noop=()=>{},audio=[]; + let now=1e6;const noop=()=>{},audio=[],drums=[]; const api=new Function('runtime','capabilities','telemetry','gameSignal','drum','wipe','box','line','triangle','write','systemWrite','oscillator','oscillatorStop',`${source} configureWorldMap('skatepark','pool');fightOpponent='freeskate';gameMode='fight'; return {players,updatePlayer,runnerWorldGeometry,projectRunnerWorldGeometry,cameraDoll, @@ -13,10 +13,10 @@ function playground(){ boundParkBody,parkWindowWalls,brokenParkWindows,parkWindowShards,resetParkWindows, breakParkWindow,updateParkWindowShards,insidePark,parkLotMargin,updateCameraDoll,clearPoolCamera,updateMotorAudio, sceneBoundsVisible,buildParkScene,parkActorVisible,figureLod, - state:()=>({halfpipe:parkHalfPipe3D})};`)(()=>({monotonicUs:now}),()=>({platform:'web'}),noop,noop,noop,noop,noop,noop,noop,noop,noop,(hz,gain)=>audio.push({hz,gain}),()=>audio.push({stop:true})); + state:()=>({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);}; - api.now=()=>now;api.audio=audio;return api; + api.now=()=>now;api.audio=audio;api.drums=drums;return api; } test('pool flicks start, sustain and release a spin on foot and on a board',()=>{ for(const board of [false,true]){ @@ -172,3 +172,22 @@ test('figure LOD follows projected size and holds its tier at a boundary',()=>{ assert.equal(a.figureLod(p,geometry(20)),2); assert.equal(a.figureLod(p,geometry(8)),3); }); + +test('3D rounds reflect off solid walls in depth and retain their lifetime',()=>{ + const a=playground(),w=a.parkWindowWalls()[0]; + const b={x:w.ax+w.width*.5,y:a.parkDeckY-150,z:w.az+10,vx:300,vy:0,vz:-4200,life:1,owner:0,safeUntil:Infinity}; + a.bullets.push(b);const speed=Math.hypot(b.vx,b.vy,b.vz); + a.updateBullets(1/60,a.now(),false); + assert.ok(b.vz>0);assert.equal(b.vx,300);assert.ok(b.z>w.az); + assert.equal(b.life,1);assert.ok(Math.abs(Math.hypot(b.vx,b.vy,b.vz)-speed)<1e-8); + assert.ok(a.drums.some(d=>d.name==='hat'),'ricochet is audible'); +}); +test('3D laser rounds are absorbed by solid walls',()=>{ + const a=playground(),w=a.parkWindowWalls()[0]; + a.bullets.push({x:w.ax+w.width*.5,y:a.parkDeckY-150,z:w.az+10,vx:0,vy:0,vz:-4200,life:1,laser:true}); + a.updateBullets(1/60,a.now(),false);assert.equal(a.bullets.length,0); +}); +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')); +}); -- 2.51.2