From 995ee1977b8cb31e1495ef2ab0b1367315613035 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Sun, 27 Sep 2026 14:13:20 -0700 Subject: [PATCH] oskiewar v176: forward drive, carving turns and back braking --- xbox/live/oskiewar.js | 54 +++++++++++++----------- xbox/live/tests/xbox-feedback.test.mjs | 58 ++++++++++++++++++++++---- 2 files changed, 78 insertions(+), 34 deletions(-) diff --git a/xbox/live/oskiewar.js b/xbox/live/oskiewar.js index c21ffc819f..a36cddf346 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 = 175; +const buildVersion = 176; 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 @@ -9392,13 +9392,13 @@ function updateCameraDoll(dt, now) { if (poolOnly() && freeskateActive()) { const p = players[0], heading = p.poolYaw || 0; poolCameraYaw += Math.atan2(Math.sin(heading-poolCameraYaw), Math.cos(heading-poolCameraYaw)) * - (1-Math.exp(-dt*1.8)); - const angle = poolCameraYaw + .65 + playerCameraYaw; + (1-Math.exp(-dt*6)); + const angle = poolCameraYaw + .14 + playerCameraYaw; const zoom = playerCameraZoom, distance = 1350 * zoom; const target = { x:p.x+clamp(p.vx*.1,-160,160), y:p.y-85, z:(p.z||0)+clamp((p.vz||0)*.1,-160,160) }; const position = { x:target.x-Math.cos(angle)*distance, - y:Math.min(target.y-1050*zoom-playerCameraPitch*600, floorY-pool.radius-300), + y:Math.min(target.y-880*zoom-playerCameraPitch*600, floorY-pool.radius-300), z:target.z-Math.sin(angle)*distance }; cameraCenter=target.x; cameraCenterY=target.y; cameraWidth=1650*zoom; cameraDoll.track({target,position,width:cameraWidth,perspective:1,fov:58,roll:0},dt,8); @@ -12090,10 +12090,21 @@ function updateTurn(player, now) { function updatePoolPlayer(p, pad, dt, now) { const held=pad.down, pressed=key=>held.includes(key)&&!p.previous.includes(key); const axis=value=>Math.abs(value)>.18?Math.sign(value)*(Math.abs(value)-.18)/.82:0; - let ix=Number(held.includes('ArrowRight'))-Number(held.includes('ArrowLeft')) || axis(pad.leftX||0); - let iz=Number(held.includes('ArrowUp'))-Number(held.includes('ArrowDown')) || axis(pad.leftY||0); - const inputLength=Math.hypot(ix,iz); if(inputLength>1){ix/=inputLength;iz/=inputLength;} - p.previousY=p.y; p.inputX=ix; p.inputZ=iz; p.inputY=held.includes('X')?-1:held.includes('A')?1:0; + const turn=Number(held.includes('ArrowRight'))-Number(held.includes('ArrowLeft')) || axis(pad.leftX||0); + const throttle=Number(held.includes('ArrowUp'))-Number(held.includes('ArrowDown')) || axis(pad.leftY||0); + const forward=Math.max(0,throttle),brake=Math.max(0,-throttle); + const oldHeading=p.poolYaw||0; + const speedBefore=Math.hypot(p.vx,p.vz||0); + const turnRate=p.onewheel?lerp(2.8,1.9,clamp(speedBefore/3000,0,1)):3.2; + // Steering changes the rider's heading. Forward always pushes along that heading. + p.poolYaw=oldHeading-turn*turnRate*dt; + const headingDelta=p.poolYaw-oldHeading; + if(p.grounded&&headingDelta){ + const vx=p.vx,vz=p.vz||0,c=Math.cos(headingDelta),sn=Math.sin(headingDelta); + p.vx=vx*c-vz*sn;p.vz=vx*sn+vz*c; + } + const driveX=Math.cos(p.poolYaw),driveZ=Math.sin(p.poolYaw); + p.previousY=p.y; p.inputX=turn; p.inputZ=throttle; p.inputY=held.includes('X')?-1:held.includes('A')?1:0; p.ducking=held.includes('X'); p.crouchBlend+=(Number(p.ducking)-(p.crouchBlend||0))*(1-Math.exp(-dt*14)); p.facing=1; p.turnAt=-1e12; p.footSurf=false; p.strafe=null; p.grabHeld=false; if(!held.includes('RightShoulder'))p.shieldLocked=false; @@ -12113,19 +12124,16 @@ function updatePoolPlayer(p, pad, dt, now) { for(let n=0;n=floor){ @@ -12159,10 +12167,6 @@ function updatePoolPlayer(p, pad, dt, now) { } } const horizontal=Math.hypot(p.vx,p.vz||0); - if(horizontal>30){ - const desired=Math.atan2(p.vz,p.vx),heading=p.poolYaw||0; - p.poolYaw=heading+Math.atan2(Math.sin(desired-heading),Math.cos(desired-heading))*(1-Math.exp(-dt*12)); - } const slope=poolSlopeAt(p.x,p.z||0),heading=p.poolYaw||0; const pitch=p.grounded?Math.atan(slope.x*Math.cos(heading)+slope.z*Math.sin(heading)):0; p.skatePitch+=(pitch-(p.skatePitch||0))*(1-Math.exp(-dt*16)); diff --git a/xbox/live/tests/xbox-feedback.test.mjs b/xbox/live/tests/xbox-feedback.test.mjs index a7a46b1418..1172831b41 100644 --- a/xbox/live/tests/xbox-feedback.test.mjs +++ b/xbox/live/tests/xbox-feedback.test.mjs @@ -174,7 +174,7 @@ test('bubble keeps ground steering and wheel momentum in both control layouts', Object.assign(p, {x:950,y:1800,vx:riding?1700:880,grounded:true, skateboard:riding,onewheel:riding,skateVx:riding?1700:0}); g.api.monowheel.active = !riding; - g.pads[0].down = [classic?'X':'RightShoulder','ArrowRight']; + g.pads[0].down = [classic?'X':'RightShoulder',classic?'ArrowRight':'ArrowUp']; g.tick(); assert.equal(p.blocking, true); assert.ok(p.x > 950 && p.vx > 800, `bubble stopped ${classic?'classic':'3D'} movement`); @@ -258,12 +258,20 @@ test('pool floor rises on every side and through rounded corners', () => { assert.equal(a.terrainFloorAt(cx,720),1320); }); -test('depth steering turns the monowheel and rider together', () => { +test('left and right steer the rider, wheel and chase camera without strafing', () => { const g=game(),p=g.api.players[0]; Object.assign(p,{onewheel:true,skateboard:true,vx:0,vz:0,skateVx:0}); - g.api.monowheel.active=false;g.pads[0].down=['ArrowUp']; - for(let i=0;i<30;i++)g.tick(); - assert.ok(p.z>20);assert.ok(p.poolYaw>1.3); + g.api.monowheel.active=false;g.pads[0].down=['ArrowLeft']; + const x=p.x,z=p.z; + for(let i=0;i<40;i++)g.tick(); + assert.equal(p.x,x);assert.equal(p.z,z,'turning does not move sideways'); + assert.ok(p.poolYaw>1.3); + g.pads[0].down=['ArrowUp']; + for(let i=0;i<20;i++)g.tick(); + assert.ok(p.z>20); + assert.ok(g.api.cameraDoll.position.z80,'wheel points through depth'); const pose=g.api.runnerWorldGeometry(p,1); @@ -272,8 +280,8 @@ test('depth steering turns the monowheel and rider together', () => { }); test('pool movement stays on the surface in all four directions and corners', () => { - for(const down of [['ArrowRight'],['ArrowLeft'],['ArrowUp'],['ArrowDown'],['ArrowUp','ArrowRight']]){ - const g=game(),p=g.api.players[0];g.pads[0].down=down; + for(const heading of [0,Math.PI,Math.PI/2,-Math.PI/2,Math.PI/4]){ + const g=game(),p=g.api.players[0];p.poolYaw=heading;g.pads[0].down=['ArrowUp']; for(let i=0;i<450;i++){ g.tick(); assert.ok([p.x,p.y,p.z,p.vx,p.vy,p.vz].every(Number.isFinite)); @@ -306,10 +314,10 @@ test('raster decals follow pool depth curvature', () => { }); test('pool monowheel rides into vert air and returns without passing through the bowl', () => { - for(const direction of ['ArrowRight','ArrowUp']){ + for(const heading of [0,Math.PI/2]){ const g=game(),p=g.api.players[0]; Object.assign(p,{onewheel:true,skateboard:true,vx:0,vz:0,skateVx:0}); - g.api.monowheel.active=false;g.pads[0].down=[direction]; + g.api.monowheel.active=false;p.poolYaw=heading;g.pads[0].down=['ArrowUp']; let air=false,landed=false,maxSpeed=0,turbo=false; for(let i=0;i<900;i++){ g.tick();maxSpeed=Math.max(maxSpeed,p.skateVx);turbo ||= p.wheelTurbo; @@ -320,3 +328,35 @@ test('pool monowheel rides into vert air and returns without passing through the assert.ok(air,'wheel launches off the coping');assert.ok(landed,'rider returns to surface'); } }); + + +test('forward pushes in the facing direction, and back brakes without reversing', () => { + for(const riding of [false,true])for(const heading of [0,Math.PI/2,Math.PI,-Math.PI/2]){ + const g=game(),p=g.api.players[0],x=1080,z=0; + Object.assign(p,{x,z,y:1800,onewheel:riding,skateboard:riding,poolYaw:heading,vx:0,vz:0}); + g.api.monowheel.active=!riding;g.pads[0].down=['ArrowUp']; + for(let i=0;i<15;i++)g.tick(); + const forward=(p.x-x)*Math.cos(heading)+(p.z-z)*Math.sin(heading); + const sideways=-(p.x-x)*Math.sin(heading)+(p.z-z)*Math.cos(heading); + assert.ok(forward>20);assert.ok(Math.abs(sideways)<.001); + const peak=Math.hypot(p.vx,p.vz); + g.pads[0].down=['ArrowDown']; + for(let i=0;i<30;i++){ + g.tick(); + assert.ok(p.vx*Math.cos(heading)+p.vz*Math.sin(heading)>=-.001,'back never reverses'); + } + assert.ok(peak>100);assert.ok(Math.hypot(p.vx,p.vz)<.001,'back stops'); + const stopped={x:p.x,z:p.z}; + for(let i=0;i<30;i++)g.tick(); + assert.ok(Math.hypot(p.x-stopped.x,p.z-stopped.z)<.001,'holding back stays stopped'); + } +}); + +test('monowheel coasts and carves while the stick turns without forward held', () => { + const g=game(),p=g.api.players[0]; + Object.assign(p,{x:1080,z:0,y:1800,onewheel:true,skateboard:true,poolYaw:0,vx:1400,vz:0}); + g.api.monowheel.active=false;g.pads[0].down=['ArrowRight']; + for(let i=0;i<12;i++)g.tick(); + assert.ok(p.x>1080&&p.z<0);assert.ok(p.poolYaw<-.2); + assert.ok(Math.hypot(p.vx,p.vz)>1200,'turning preserves rolling momentum'); +}); -- 2.54.0