From 997bf80eb0544a9f1b61bedaa4420f68be1cc2f1 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Sun, 27 Sep 2026 20:25:39 -0700 Subject: [PATCH] Fix NPC signal crash, settle pipe camera and separate held items --- xbox/live/oskiewar.js | 113 ++++++++++++++++----- xbox/live/shaders/park-speed.hlsl | 120 +++++++++++++++++++++++ xbox/live/tests/pool-playground.test.mjs | 31 +++++- 3 files changed, 235 insertions(+), 29 deletions(-) create mode 100644 xbox/live/shaders/park-speed.hlsl diff --git a/xbox/live/oskiewar.js b/xbox/live/oskiewar.js index e8265b1ca9..ac99d34b3f 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 = 201; +const buildVersion = 202; 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 @@ -1695,7 +1695,7 @@ const parkRoofCell=600; function parkRoofKey(x,z){return Math.floor((x-gridLeft)/parkRoofCell)+':'+Math.floor((z-worldNear)/parkRoofCell);} function breakParkRoof(p){ const key=parkRoofKey(p.x,p.z||0);if(brokenParkRoof.has(key))return false; - brokenParkRoof.add(key);releaseQuadMesh(parkBuildingMesh);parkBuildingMesh=null; + brokenParkRoof.add(key);releaseQuadMesh(parkBuildingMesh);parkBuildingMesh=null;releaseQuadMesh(parkRoofMesh);parkRoofMesh=null; for(let i=0;i<16;i++){const a=i*2.39996;parkWindowShards.push({x:p.x+Math.cos(a)*100,y:parkDeckY-parkBuildingHeight,z:(p.z||0)+Math.sin(a)*100,vx:Math.cos(a)*160,vy:-180-i*10,vz:Math.sin(a)*160,life:2.4,size:10+i%4*5,rotation:a});} if(parkWindowShards.length>216)parkWindowShards.splice(0,parkWindowShards.length-216); playDrum('glass',.35,panPlayer(p));p.lastButton='ROOF BREAK';p.lastButtonAt=runtime().monotonicUs;return true; @@ -1724,7 +1724,7 @@ function breakParkWindow(wall,bay,at,velocity={}){ function resetParkWindows(){ brokenParkWindows.clear();brokenParkRoof.clear();parkWindowShards.length=0; for(const mesh of parkWindowMeshes.values())releaseQuadMesh(mesh);parkWindowMeshes.clear(); - releaseQuadMesh(parkBuildingMesh);parkBuildingMesh=null; + releaseQuadMesh(parkBuildingMesh);parkBuildingMesh=null;releaseQuadMesh(parkRoofMesh);parkRoofMesh=null; } function parkWallContact(from,to,radius=25){ const contacts=[]; @@ -3311,7 +3311,21 @@ function decalShape(decal) { return corners; } +const lensBlood=[]; +function drawLensBlood(){ + const now=runtime().monotonicUs,old=triangleDepth;triangleDepth=-1.46; + for(let i=lensBlood.length-1;i>=0;i--){const d=lensBlood[i],age=(now-d.at)/1e6; + if(age>=3){lensBlood.splice(i,1);continue;} + filledDisc(d.x*viewWidth(),(d.y+age*.012)*viewHeight,d.r*clamp((3-age)/.8,0,1),[116,12,31]); + } + triangleDepth=old; +} function spawnBlood(x, y, z, direction, amount, spread = 1) { + if(poolOnly()&&amount>=8&&Math.hypot(x-cameraDoll.position.x,y-cameraDoll.position.y,z-cameraDoll.position.z)<750){ + const point=projectPoint(x,y,z); + if(!point.behind)for(let i=0;i<5&&lensBlood.length<20;i++)lensBlood.push({x:clamp(point.x/viewWidth()+(decalRandom()-.5)*.35,.06,.94),y:clamp(point.y/viewHeight+(decalRandom()-.5)*.35,.1,.9),r:4+decalRandom()*12,at:runtime().monotonicUs}); + } + for (let drop = 0; drop < amount && bloodDrops.length < 90; drop++) { const push = direction ? direction * (180 + decalRandom() * 720) : (decalRandom() - .5) * 900; bloodDrops.push({ x, y, z, @@ -5150,7 +5164,7 @@ function emitSignal(event, player = -1, value = 0, value2 = 0) { if (replay) replay.events.push([demoTick(runtime().monotonicUs), event, player, Math.round(value * 1000) / 1000, Math.round(value2 * 1000) / 1000]); if (netSilent) return; - if (typeof gameSignal === "function") gameSignal(event, player, value, value2); + if (typeof gameSignal === "function") gameSignal(event, Number.isInteger(player) && player >= 0 && player <= 3 ? player : -1, value, value2); } // Older native hosts may not recognize the newer sound names. Keep their @@ -9735,14 +9749,15 @@ function updateCameraDoll(dt, now) { } if(p.poolPipeLocked){ const pipe=parkHalfPipe3D,air=Math.max(0,parkDeckY-pipe.radius-p.y); - let yaw=heading; - if(p.poolVert?.pipe){const progress=clamp((p.poolVert.speed+p.vy)/(2*p.poolVert.speed),0,1);yaw=p.poolVert.heading+Math.PI*(progress*progress*(3-2*progress));} - const error=Math.atan2(Math.sin(yaw-poolCameraYaw),Math.cos(yaw-poolCameraYaw));poolCameraYaw+=error*(1-Math.exp(-dt*2)); + // A small sweep follows travel without orbiting on every turnaround. + const yaw=Math.sin(heading)*.22; + const error=Math.atan2(Math.sin(yaw-poolCameraYaw),Math.cos(yaw-poolCameraYaw)); + poolCameraYaw+=clamp(error*(1-Math.exp(-dt*.8)),-dt*.3,dt*.3); const angle=poolCameraYaw+Math.PI*.6,radius=3100+air*.85; const target={x:pipe.x,y:parkDeckY-pipe.radius*.5-air*.2,z:pipe.z}; const position={x:pipe.x-Math.cos(angle)*radius,y:parkDeckY-2100-air*.85,z:pipe.z-Math.sin(angle)*radius}; cameraCenter=target.x;cameraCenterY=target.y;cameraWidth=3200; - cameraDoll.track({target,position,width:3200,perspective:1,fov:65,roll:0},dt,5); + cameraDoll.track({target,position,width:3200,perspective:1,fov:65,roll:0},dt,1.8); cameraDoll.position.y=Math.min(cameraDoll.position.y,parkDeckY-700); cameraDoll.dirty=true;poolIdleSeconds=poolIdleClose=0;return; } @@ -10428,10 +10443,11 @@ function updateGunPickups(now) { if (!pickup.active) continue; for (const player of players) { if (now<(pickup.safeUntil||0) || !player.alive || player.dummy || !nearRunner(player, pickup.x, pickup.y, pickup.z, 90) || - runnerDistanceToPoint(player, poseTime, pickup.x, pickup.y, pickup.z) > 90 || !availableArm(player)) continue; + runnerDistanceToPoint(player, poseTime, pickup.x, pickup.y, pickup.z) > 90 || !(poolOnly()?freeItemArm(player,"gun"):availableArm(player))) continue; + const arm=poolOnly()?freeItemArm(player,"gun"):availableArm(player); player.gunAmmo = Math.min(30, player.gunAmmo + pickup.amount); player.gunMode = pickup.kind || "HANDGUN"; - player.itemArm = availableArm(player); + player.itemArm = arm;if(poolOnly()){player.handItems ||= {};player.handItems.gun=arm;} pickup.active = false; remember(player, player.gunMode + " +" + pickup.amount); playDrum("clap", 1.1, panPlayer(player)); @@ -11027,6 +11043,19 @@ const availableArm = (player) => { const other = lead === "right-arm" ? "left-arm" : "right-arm"; return hasPart(player, other) ? other : ""; }; +function heldHandItems(p){return [p.gunAmmo>0?'gun':null,p.axeHeld?'axe':null,p.chalkColor?'chalk':null,p.swordHeld?'sword':null,p.grenadeAmmo>0?'grenade':null,p.heldBall>=0?'ball':null].filter(Boolean);} +function assignedItemArm(p,kind){ + const items=heldHandItems(p);p.handItems ||= {}; + for(const key of Object.keys(p.handItems))if(!items.includes(key)||!hasPart(p,p.handItems[key]))delete p.handItems[key]; + const used=new Set(); + for(const item of items){let arm=p.handItems[item];if(!arm||used.has(arm))arm=['right-arm','left-arm'].find(a=>hasPart(p,a)&&!used.has(a));if(arm){p.handItems[item]=arm;used.add(arm);}} + return p.handItems[kind]||''; +} +function freeItemArm(p,kind){ + const items=heldHandItems(p);if(items.includes(kind)||items.length>=2)return ''; + for(const item of items)assignedItemArm(p,item); + return ['right-arm','left-arm'].find(a=>hasPart(p,a)&&!items.some(i=>p.handItems[i]===a))||''; +} // Ownership stays attached to the arm that picked the object up; turning or // boarding cannot teleport a gun between hands. const itemHand = (player) => player.itemArm || availableArm(player); @@ -18165,7 +18194,7 @@ function drawHandgun(hand, toward, facing, scale) { } function itemForearm(player, geometry) { - const arm = itemHand(player); + const arm = poolOnly()&&player.gunAmmo>0?assignedItemArm(player,"gun"):itemHand(player); return geometry.segments.find(segment => segment.part === arm && (segment.role === "item-forearm" || segment.role === "attack-forearm")) || geometry.segments.find(segment => segment.part === arm && @@ -20447,7 +20476,7 @@ function drawParkWindows(){ drawQuadMesh(mesh); } } -let parkBuildingMesh=null; +let parkBuildingMesh=null,parkRoofMesh=null; function drawParkBuildingGeometry(){ const p=parkPalette,top=parkDeckY-parkBuildingHeight; const left=gridLeft,right=gridLeft+gridWidth,near=worldNear,far=worldFar; @@ -20474,6 +20503,10 @@ function drawParkBuildingGeometry(){ // Traverse counter-clockwise so depth offsets face into the room. wall(left,near,right,near,0);wall(right,near,right,far,1); wall(right,far,left,far,2);wall(left,far,left,near,3); + } +function drawParkRoofGeometry(){ + const p=parkPalette,top=parkDeckY-parkBuildingHeight; + const left=gridLeft,right=gridLeft+gridWidth,near=worldNear,far=worldFar; for(let x=left;xparkDeckY-parkBuildingHeight+100){ + if(!parkRoofMesh)parkRoofMesh=captureQuadMesh(drawParkRoofGeometry); + drawParkScene(parkRoofMesh); + } + drawParkWindows();drawParkWindowShards(); } function drawRoomSurfaces(left, right, top, bottom, color) { if(poolOnly()){drawParkBuilding();return;} @@ -22538,6 +22578,7 @@ function drawTitleHeadDoor(t, ink, suppressed) { // fighters sharp and softens the street above and the water below. The HUD // stays sharp in the shader. __oskiewarPost = false turns both off. let postCameraX = null, postCameraY = null; +function parkSpeedEffect(p){return clamp((mph(Math.hypot(p?.vx||0,p?.vy||0,p?.vz||0)*gameSpeed)-8)/100,0,1);} function steerPostEffects() { if (!nativePostEffects) return; const on = globalThis.__oskiewarPost !== false && shellMode === "GAME" && !selecting; @@ -22573,7 +22614,7 @@ function steerPostEffects() { bottom = clamp(bottom + 24, 0, viewHeight); const focusY = visible ? (top + bottom) / (2 * viewHeight) : .5; const band = visible ? Math.max(.06, (bottom - top) / (2 * viewHeight)) : 1; - nativePostEffects(focusY, band, .2+(poolOnly()?parkMusicPulse()*.015:0), visible ? 4 : 0, + nativePostEffects(focusY, band, .2+(poolOnly()?parkMusicPulse()*.015:0), poolOnly()?parkSpeedEffect(players[0])*4:0, clamp(moveX, -24, 24), clamp(moveY, -24, 24)); } @@ -22921,6 +22962,7 @@ function gamePaint() { } } if(poolOnly()){drawCerealMilk(t);drawParkStereo();drawAeselFairy(t);} + if(poolOnly())drawLensBlood(); // Debug geometry shares the unfiltered overlay pass, behind screen UI. triangleDepth = -1.465; for(const p of activePlayers()){drawDebugHitboxes(p,t);if(debugHitboxes)drawAxeHitbox(p,t);} @@ -23310,7 +23352,7 @@ function updateFootprints(p,now){ } function drawHeldChalk(p,t){ if(!p.chalkColor||!p.alive)return; - const world=runnerWorldGeometry(p,t),arm=world.segments.find(s=>s.part===itemHand(p)&&/forearm$/.test(s.role));if(!arm)return; + const world=runnerWorldGeometry(p,t),arm=world.segments.find(s=>s.part===assignedItemArm(p,'chalk')&&/forearm$/.test(s.role));if(!arm)return; const hand={x:arm.x2,y:arm.y2,z:arm.z2},tip=p.chalkDrawing?chalkTip(p):{x:hand.x+8,y:hand.y+16,z:hand.z}; worldCapsule(hand.x,hand.y,hand.z,tip.x,tip.y,tip.z,6*projectionScaleAt(tip),p.chalkColor.rgb); } @@ -23358,11 +23400,11 @@ function updateParkSupply(dt,now){ if(!freeskateActive())return; if(poolOnly()){ for(const kart of parkKarts)if(kart.active&&now>=kart.safeUntil)for(const p of activePlayers())if(p.alive&&!p.skateboard&&Math.hypot(p.x-kart.x,(p.z||0)-kart.z)<75&&Math.abs(p.y-kart.y)<100){p.goKart=kart;p.skateboard=true;p.onewheel=false;p.poolYaw=kart.poolYaw; kart.active=false;break;} - for(const chalk of chalkPickups)if(chalk.active&&now>=(chalk.safeUntil||0))for(const p of activePlayers())if(p.alive&&availableArm(p)&&Math.hypot(p.x-chalk.x,(p.z||0)-chalk.z)<35&&Math.abs(p.y-chalk.y)<70){p.chalkColor=chalk.color;p.itemArm=availableArm(p);chalk.active=false;playDrum('hat',.25,panPlayer(p));break;} - for(const axe of parkAxes)if(axe.active&&now>=(axe.safeUntil||0))for(const p of activePlayers())if(p.alive&&availableArm(p)&&Math.hypot(p.x-axe.x,(p.z||0)-axe.z)<55&&Math.abs(p.y-axe.y-65)<100){p.axeHeld=true;p.itemArm=availableArm(p);axe.active=false;break;} - if(axePickup.active)for(const p of activePlayers())if(p.alive&&availableArm(p)&& + for(const chalk of chalkPickups)if(chalk.active&&now>=(chalk.safeUntil||0))for(const p of activePlayers())if(p.alive&&freeItemArm(p,'chalk')&&Math.hypot(p.x-chalk.x,(p.z||0)-chalk.z)<35&&Math.abs(p.y-chalk.y)<70){p.handItems ||= {};p.handItems.chalk=freeItemArm(p,'chalk');p.chalkColor=chalk.color;chalk.active=false;playDrum('hat',.25,panPlayer(p));break;} + for(const axe of parkAxes)if(axe.active&&now>=(axe.safeUntil||0))for(const p of activePlayers())if(p.alive&&freeItemArm(p,'axe')&&Math.hypot(p.x-axe.x,(p.z||0)-axe.z)<55&&Math.abs(p.y-axe.y-65)<100){p.handItems ||= {};p.handItems.axe=freeItemArm(p,'axe')||availableArm(p);p.axeHeld=true;axe.active=false;break;} + if(axePickup.active)for(const p of activePlayers())if(p.alive&&freeItemArm(p,'axe')&& Math.hypot(p.x-axePickup.x,(p.z||0)-(axePickup.z||0))<65&&Math.abs(p.y-axePickup.y-65)<100){ - p.axeHeld=true;p.itemArm=availableArm(p);axePickup.active=false;break; + p.handItems ||= {};p.handItems.axe=freeItemArm(p,'axe')||availableArm(p);p.axeHeld=true;axePickup.active=false;break; } return; } @@ -23377,7 +23419,7 @@ function updateParkSupply(dt,now){ if(now>=parkSupply.ko.until){const paired=freeskateSecond && !players[1].dummy,scores=players.map(p=>p.score);beginFreeskate(now);updateFreeskatePlayers(paired,now);players.forEach((p,i)=>p.score=scores[i]);} return; } - if(axePickup.active)for(const p of activePlayers())if(p.alive&&availableArm(p)&&Math.abs(p.x-axePickup.x)<65&&Math.abs(p.y-(axePickup.y+65))<150){p.axeHeld=true;p.itemArm=availableArm(p);axePickup.active=false;break;} + if(axePickup.active)for(const p of activePlayers())if(p.alive&&availableArm(p)&&Math.abs(p.x-axePickup.x)<65&&Math.abs(p.y-(axePickup.y+65))<150){p.handItems ||= {};p.handItems.axe=freeItemArm(p,'axe')||availableArm(p);p.axeHeld=true;axePickup.active=false;break;} // Drone arrives before the scheduled drop; its package is a heal, not a weapon. parkSupply.drone=null;parkSupply.drop=null; for(const c of hallCubes){ @@ -23564,14 +23606,15 @@ function updateMotorAudio(dt){ const amount=clamp(speed/3200,0,1),load=clamp(Math.abs(speed-motorSpeed)/Math.max(dt,.001)/5000,0,1); motorSpeed=speed; // Low electric hum with a soft acceleration whine; no standing idle tone. - const target=65+Math.pow(amount,.72)*280+load*28; + const note=[33,29,36,31][Math.floor(parkStereo.beat/8)%4]+(p.wheelTurbo?12:0); + const target=440*Math.pow(2,(note-69)/12); motorFrequency+=(target-motorFrequency)*(1-Math.exp(-Math.max(0,dt)*8)); - const gain=(.003+Math.sqrt(amount)*.011+load*.003)*clamp((speed-12)/90,0,1); + const gain=(.002+Math.sqrt(amount)*.005)*clamp((speed-12)/90,0,1); oscillator(motorFrequency,gain);motorRunning=true; } function axeFrame(p,t){ if(!p.axeHeld || !p.alive || isHeadOnly(p) || !availableArm(p))return null; - const g=runnerWorldGeometry(p,t),arm=g.segments.find(b=>b.part===itemHand(p)&&b.role?.endsWith('forearm'))||g.segments.find(b=>b.role?.endsWith('forearm')); + const g=runnerWorldGeometry(p,t),arm=g.segments.find(b=>b.part===(poolOnly()?assignedItemArm(p,'axe'):itemHand(p))&&b.role?.endsWith('forearm'))||g.segments.find(b=>b.role?.endsWith('forearm')); if(!arm)return null; const hand={x:arm.x2,y:arm.y2,z:arm.z2??p.z??0}; let dx=arm.x2-arm.x1,dy=-Math.abs(arm.y2-arm.y1),dz=(arm.z2||0)-(arm.z1||0); @@ -23984,9 +24027,25 @@ function updateParkKids(dt,now){ } } if(kid.headless){ - const h=kid.looseHead;h.vy+=1100*dt;h.x+=h.vx*dt;h.y+=h.vy*dt;h.z=clamp(h.z+h.vz*dt,worldNear+30,worldFar-30); - const floor=terrainFloorAt(h.x,h.z)-h.radius; - if(h.y>floor){h.y=floor;h.vy=-Math.abs(h.vy)*.35;h.vx*=Math.exp(-dt*3);h.vz*=Math.exp(-dt*3);} + const h=kid.looseHead,previous={x:h.x,z:h.z}; + h.vy+=1100*dt;h.x+=h.vx*dt;h.y+=h.vy*dt;h.z+=h.vz*dt; + if(poolOnly())for(const {w,old} of parkWallContact(previous,h,h.radius)){ + const dot=h.vx*w.nx+h.vz*w.nz; + if(dot*(old>=0?1:-1)<0){h.vx-=1.6*dot*w.nx;h.vz-=1.6*dot*w.nz;h.x=previous.x;h.z=previous.z;} + } + const floor=(poolOnly()?poolFloorAt(h.x,h.z):terrainFloorAt(h.x,h.z))-h.radius; + if(h.y>floor){h.y=floor;h.vy=Math.abs(h.vy)>65?-Math.abs(h.vy)*.55:0;h.vx*=Math.exp(-dt*.8);h.vz*=Math.exp(-dt*.8);} + for(const p of activePlayers()){ + if(!p.alive||!nearRunner(p,h.x,h.y,h.z,h.radius+40)||now<(h.hitAfter||0))continue; + const dx=h.x-p.x,dz=h.z-(p.z||0),distance=Math.hypot(dx,dz); + const attack=p.attackKind&&nowpointBoxDistance(box,h.x,h.y,h.z)1?dx/distance:Math.cos(yaw),nz=distance>1?dz/distance:Math.sin(yaw); + const force=contact?(/KICK/.test(p.attackKind)?1250:850):Math.max(140,Math.hypot(p.vx,p.vz||0)*.85); + h.vx=nx*force;h.vz=nz*force;h.vy=contact?-360:-80;h.hitAfter=now+180000; + } continue; } kid.z=(kid.patrolZ||0)+Math.sin(now/3500000+kid.pad*1.2)*(poolOnly()?35:140); @@ -24024,7 +24083,7 @@ function updateWheelTurbo(dt,now){ }else p.wheelChargeAt=0; } if(p.wheelTurbo){ - if(now>=(p.nextTurboTone||0)){playSine(motorFrequency*2,.11);p.nextTurboTone=now+90000;} + // The quiet, chord-tuned motor voice carries turbo too. if(now>=(p.nextTurboParticle||0)){ p.nextTurboParticle=now+30000; turboParticles.push({x:p.x,y:p.y-20,z:p.z||0,vx:-Math.sign(p.skateVx)*180,vy:-60-(now%130),life:.45}); diff --git a/xbox/live/shaders/park-speed.hlsl b/xbox/live/shaders/park-speed.hlsl new file mode 100644 index 0000000000..e33dd74f63 --- /dev/null +++ b/xbox/live/shaders/park-speed.hlsl @@ -0,0 +1,120 @@ +Texture2D sceneTexture : register(t0); +Texture2D depthTexture : register(t1); +SamplerState sceneSampler : register(s0); + +cbuffer PostConstants : register(b0) { + float2 texel; + float timeSeconds; + float stencilPass; + float focusY; // centre of the sharp band, 0 top .. 1 bottom + float band; // half-height of the sharp band + float feather; // distance over which blur ramps to full + float tiltPx; // full tilt-shift radius in pixels + float2 motion; // camera travel this frame, pixels + float hudDepth; // depth at or in front of which pixels are HUD and stay sharp + float pad; +}; + +// Never pull a HUD pixel into a neighbouring scene pixel's blur. +float3 sceneTap(float2 uv, float2 origin) { + int2 pixel = int2(clamp(uv, texel * 0.5, 1.0 - texel * 0.5) / texel); + if (depthTexture.Load(int3(pixel, 0)) <= hudDepth) + return sceneTexture.Sample(sceneSampler, origin).rgb; + return sceneTexture.Sample(sceneSampler, uv).rgb; +} + +// Twelve taps on a golden-angle spiral: a soft disc without a second pass. +float3 discBlur(float2 uv, float radiusPx) { + float3 sum = 0; + [unroll] for (int i = 0; i < 12; ++i) { + const float r = sqrt((i + 0.5) / 12.0) * radiusPx; + const float a = i * 2.39996323; + sum += sceneTap(uv + float2(cos(a), sin(a)) * r * texel, uv); + } + return sum / 12.0; +} + +// Camera motion blur: eight taps across the frame's travel, centred. +float3 motionBlur(float2 uv) { + float3 sum = 0; + [unroll] for (int i = 0; i < 8; ++i) + sum += sceneTap(uv + motion * texel * ((i + 0.5) / 8.0 - 0.5), uv); + return sum / 8.0; +} + +struct PixelInput { + float4 position : SV_POSITION; + float2 uv : TEXCOORD0; +}; + +float4 main(PixelInput input) : SV_TARGET { + const float2 centered = input.uv * 2.0 - 1.0; + const float radial = dot(centered, centered); + const float3 center = sceneTexture.Sample(sceneSampler, input.uv).rgb; + const bool hud = depthTexture.Load(int3(input.position.xy, 0)) <= hudDepth; + if (hud) return float4(center, 1.0); + float3 color = center; + + // 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, + input.uv + float2(-texel.x, -texel.y)).rgb; + const float3 ne = sceneTexture.Sample(sceneSampler, + input.uv + float2(texel.x, -texel.y)).rgb; + const float3 sw = sceneTexture.Sample(sceneSampler, + input.uv + float2(-texel.x, texel.y)).rgb; + const float3 se = sceneTexture.Sample(sceneSampler, + input.uv + float2(texel.x, texel.y)).rgb; + const float3 weights = float3(0.299, 0.587, 0.114); + const float lc = dot(center, weights), lnw = dot(nw, weights); + const float lne = dot(ne, weights), lsw = dot(sw, weights); + const float lse = dot(se, weights); + float2 direction = float2(-((lnw + lne) - (lsw + lse)), + (lnw + lsw) - (lne + lse)); + const float reduce = max((lnw + lne + lsw + lse) / 32.0, 1.0 / 128.0); + direction = clamp(direction / + (min(abs(direction.x), abs(direction.y)) + reduce), -8.0, 8.0) * texel; + const float3 sampleA = 0.5 * ( + sceneTexture.Sample(sceneSampler, input.uv + direction * (-1.0 / 6.0)).rgb + + sceneTexture.Sample(sceneSampler, input.uv + direction * (1.0 / 6.0)).rgb); + const float3 sampleB = sampleA * 0.5 + 0.25 * ( + sceneTexture.Sample(sceneSampler, input.uv + direction * -0.5).rgb + + sceneTexture.Sample(sceneSampler, input.uv + direction * 0.5).rgb); + const float lmin = min(lc, min(min(lnw, lne), min(lsw, lse))); + const float lmax = max(lc, max(max(lnw, lne), max(lsw, lse))); + const float lb = dot(sampleB, weights); + color = lb < lmin || lb > lmax ? sampleA : sampleB; + } + // tiltPx carries normalized MPH (0..4); feather carries heartbeat pulse. + const float speed = saturate(tiltPx / 4.0); + const float pulse = saturate((feather - 0.2) / 0.015); + const float edge = smoothstep(0.12, 1.5, radial); + const float shake = speed * 0.65 + pulse * 0.15; + const float2 jitter = float2(sin(timeSeconds*37.0),sin(timeSeconds*43.0))*texel*shake; + if (speed > 0.01) { + float3 trails = color; + [unroll] for(int i=1;i<=3;i++) + trails += sceneTap(input.uv-centered*(i*0.0018*speed*edge)+jitter,input.uv); + color=lerp(color,trails*0.25,edge*speed*0.55); + } + color *= 1.0 + pulse*0.012; + if (!hud) { + const float2 bleed = float2((0.6+speed*1.1) * texel.x, 0.0); + const float3 left = sceneTap(input.uv - bleed, input.uv); + const float3 right = sceneTap(input.uv + bleed, input.uv); + color = lerp(color, float3(right.r, color.g, left.b), 0.16); + const float tapeBand = sin(input.uv.y * 22.0 - timeSeconds * 0.65); + color *= 1.0 - 0.008 * pow(max(0.0, tapeBand), 8.0); + // 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) * (1.0+speed) / 200.0); + } + return float4(color, 1.0); +} diff --git a/xbox/live/tests/pool-playground.test.mjs b/xbox/live/tests/pool-playground.test.mjs index 0fa98cfd7f..f452ae6937 100644 --- a/xbox/live/tests/pool-playground.test.mjs +++ b/xbox/live/tests/pool-playground.test.mjs @@ -7,14 +7,14 @@ function playground(legacyAudio=false){ 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 {outdoorCircuit,drawOutdoorCircuit,outdoorChunks,rasterDecalPatches,updateBoomerboard,inputPads,updateGarments,garmentStates,parkSpeakerLocations,parkKarts,dropParkItem,parkDropName,brokenParkRoof,parkRoofKey,parkBuildingHeight,generateParkProfile,drawParkStereoGeometry,updateSpin,bloodDrops,popCivilianHead,updateFootprints,playDrum,updateParkMusic,clock:()=>runtime().monotonicUs,raceTrack,enterRaceLoop,updateRaceLoop,strikeParkWindow,updateChalk,chalkTip,chalkColors,chalkPickups,decals,stereoGain,parkStereo,seatActionRuns,seatHudReadout,milkAt,swimMilk,parkPools,drawCerealMilk,ragdollBodies,updateRagdolls,ragdollGeometry,OskiewarRagdoll,seatActionText,looseRunnerGeometry,drawLooseRunner,mainNativeCamera,characterLocalCamera,spectatorState,netDrainHostInbox,players,updatePlayer,runnerWorldGeometry,projectRunnerWorldGeometry,cameraDoll, + return {freeItemArm,assignedItemArm,heldHandItems,emitSignal,outdoorCircuit,drawOutdoorCircuit,outdoorChunks,rasterDecalPatches,updateBoomerboard,inputPads,updateGarments,garmentStates,parkSpeakerLocations,parkKarts,dropParkItem,parkDropName,brokenParkRoof,parkRoofKey,parkBuildingHeight,generateParkProfile,drawParkStereoGeometry,updateSpin,bloodDrops,popCivilianHead,updateFootprints,playDrum,updateParkMusic,clock:()=>runtime().monotonicUs,raceTrack,enterRaceLoop,updateRaceLoop,strikeParkWindow,updateChalk,chalkTip,chalkColors,chalkPickups,decals,stereoGain,parkStereo,seatActionRuns,seatHudReadout,milkAt,swimMilk,parkPools,drawCerealMilk,ragdollBodies,updateRagdolls,ragdollGeometry,OskiewarRagdoll,seatActionText,looseRunnerGeometry,drawLooseRunner,mainNativeCamera,characterLocalCamera,spectatorState,netDrainHostInbox,players,updatePlayer,runnerWorldGeometry,projectRunnerWorldGeometry,cameraDoll, parkHalfPipe3D,parkHalfPipeHeight,parkDeckY,poolFloorAt,poolSlopeAt,gunPickups,axePickup, resetParkSupply,updateParkSupply,updateGunPickups,resetParkKids,updateParkKids,parkKids, bullets,updateBullets,gunPose,drawPoolGeometry,captureQuadMesh,drawRunner, boundParkBody,parkWindowWalls,brokenParkWindows,parkWindowShards,resetParkWindows, breakParkWindow,updateParkWindowShards,insidePark,parkLotMargin,updateCameraDoll,clearPoolCamera,updateMotorAudio,updateSkateAudio,balls, updateSeatHeartbeat,updatePonytail,ponytailAnchor,ponytailStates,drawSpatialRunner,sampleCombatBoxes,sweptProjectileContact,damageParkCivilian,drawAeselFairy,sceneBoundsVisible,buildParkScene,parkActorVisible,figureLod, - state:()=>({halfpipe:parkHalfPipe3D})};`)(()=>({monotonicUs:now}),()=>({platform:'web'}),noop,noop,(name,gain,pan)=>{if(legacyAudio&&!['kick','snare','hat','block'].includes(name))throw new RangeError('unknown drum');drums.push({name,gain,pan});},noop,noop,noop,noop,noop,noop,(hz,gain)=>audio.push({hz,gain}),()=>audio.push({stop:true})); + state:()=>({halfpipe:parkHalfPipe3D})};`)(()=>({monotonicUs:now}),()=>({platform:'web'}),noop,(event,seat,value,value2)=>{assert.ok(seat>=-1&&seat<=3);assert.ok(Number.isFinite(value)&&Number.isFinite(value2));},(name,gain,pan)=>{if(legacyAudio&&!['kick','snare','hat','block'].includes(name))throw new RangeError('unknown drum');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,api.clock());}; api.now=api.clock;api.audio=audio;api.drums=drums;return api; @@ -620,3 +620,30 @@ test('chalk uses an opaque texel, lifts the camera, and punch does not recall th a.inputPads[p.pad]={down:['B']};a.updateBoomerboard(1,a.now()+1000000);assert.ok(!p.boomerSince);assert.ok(!p.skateboard); p.x+=10;a.updateChalk(p,['B'],a.now()+1000000);const mark=a.decals.find(d=>d.kind==='chalk');assert.ok(mark);assert.deepEqual(a.rasterDecalPatches(mark)[0].uv,[0,0,1,1]); }); + +test('NPC removal signals stay inside the native controller-seat contract',()=>{ + const a=playground();for(let id=2;id<10;id++)a.emitSignal('partremoved',id,0,1); +}); +test('half-pipe turnaround keeps a gradual camera sweep without a half orbit',()=>{ + const a=playground(),p=a.players[0],pipe=a.parkHalfPipe3D; + Object.assign(p,{x:pipe.x,z:pipe.z,y:a.parkDeckY,skateboard:true,poolPipeLocked:true,poolYaw:0}); + for(let i=0;i<300;i++)a.updateCameraDoll(1/60,a.now()); + const start={...a.cameraDoll.position};p.poolYaw=Math.PI; + for(let i=0;i<120;i++)a.updateCameraDoll(1/60,a.now()); + assert.ok(Math.hypot(a.cameraDoll.position.x-start.x,a.cameraDoll.position.z-start.z)<50); +}); +test('loose civilian heads persist, bounce and respond to foot contact',()=>{ + const a=playground(),p=a.players[0];a.resetParkKids();const kid=a.parkKids[0]; + const g=a.runnerWorldGeometry(kid,0);a.popCivilianHead(kid,p,g,a.now()); + const h=kid.looseHead;Object.assign(h,{x:p.x+20,z:p.z,y:p.y-h.radius,vx:0,vy:0,vz:0}); + a.updateParkKids(1/60,a.now());assert.ok(h.vx>100); + for(let i=0;i<600;i++)a.updateParkKids(1/60,a.now()+i*16667); + assert.equal(kid.looseHead,h);assert.ok([h.x,h.y,h.z,h.vx,h.vy,h.vz].every(Number.isFinite)); +}); + +test('two held items occupy different arms and block another pickup until dropped',()=>{ + const a=playground(),p=a.players[0];Object.assign(p,{gunAmmo:5,axeHeld:true,chalkColor:null,swordHeld:false,grenadeAmmo:0,heldBall:-1}); + assert.notEqual(a.assignedItemArm(p,'gun'),a.assignedItemArm(p,'axe')); + assert.equal(a.freeItemArm(p,'chalk'),'');assert.equal(a.freeItemArm(p,'gun'),''); + a.dropParkItem(p,a.now());assert.ok(a.freeItemArm(p,'chalk'));assert.equal(p.gunAmmo,5); +}); -- 2.51.2