Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

arena: fix grenade coord convention + lava-respawn lock

The arena uses cam.x / cam.z directly as logical world X/Z (same coord
that Form.position[0/2] and tileAt accept) — I had double-flipped both,
so the grenade spawned behind the player and rendered mirrored. Now
matches the hover-ray exactly: position is (cam.x, -cam.y, cam.z) and
forward is (-fx, fy, -fz).

Also fix two follow-on issues:
- Explosion impulse direction was inverted on X/Z (cam-doll's applyImpulse
expects worldVx in its own convention where cam.x = -worldX), so the
blast pushed inward instead of outward; pass negated x/z now.
- After a fatal blast, the knockback grace period kept reconciliation
suspended, so a manual respawn would teleport cam-doll to spawn but
the lava view persisted until grace expired. tryRespawn now clears
myGrenade, activeExplosion, and knockbackGraceUntil so the server
snap immediately resyncs the camera.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+37 -24
+37 -24
system/public/aesthetic.computer/disks/arena.mjs
··· 632 632 633 633 function fireGrenade(cam) { 634 634 if (!cam || myGrenade || !playerAlive || netSpectator) return; 635 - // Camera world position (cam stores negated world coords for x,z; cam.y is 636 - // the eye world-Y inverted). Spawning at cam (not the 3P offset) means the 637 - // grenade leaves the muzzle, not the chase cam. 638 - const cx = -cam.x, cy = -cam.y, cz = -cam.z; 639 - // View forward in world space — same convention as the hover-ray (see sim). 635 + // Match the hover-ray exactly. Arena uses (cam.x, -cam.y, cam.z) as the 636 + // logical "world" position (same coord that Form.position and tileAt accept), 637 + // and the ray direction is (-fx, fy, -fz) when hoverFlipMode = 3. 638 + const cx = cam.x, cy = -cam.y, cz = cam.z; 640 639 const yaw = cam.rotY * Math.PI / 180; 641 640 const pit = cam.rotX * Math.PI / 180; 642 - const cy2 = Math.cos(yaw), sy = Math.sin(yaw); 643 - const cp = Math.cos(pit), sp = Math.sin(pit); 641 + const cyR = Math.cos(yaw), syR = Math.sin(yaw); 642 + const cpR = Math.cos(pit), spR = Math.sin(pit); 644 643 const flipX = (hoverFlipMode & 1) !== 0; 645 644 const flipZ = (hoverFlipMode & 2) !== 0; 646 - let fx = sy * cp; 647 - let fy = sp; 648 - let fz = cy2 * cp; 645 + let fx = syR * cpR; 646 + let fy = spR; 647 + let fz = cyR * cpR; 649 648 if (flipX) fx = -fx; 650 649 if (flipZ) fz = -fz; 651 650 myGrenade = { ··· 706 705 activeExplosion = { x, y, z, age: 0 }; 707 706 playBoom(soundRef); 708 707 if (!doll || !cam) return; 708 + // Player position in arena's logical world (same coord as the grenade). 709 709 const phys = doll.physics; 710 - const pcx = phys?.playerCamX ?? cam.x; 711 - const pcy = phys?.playerCamY ?? cam.y; 712 - const pcz = phys?.playerCamZ ?? cam.z; 713 - const wx = -pcx, wy = -pcy, wz = -pcz; 714 - const dx = wx - x, dy = wy - y, dz = wz - z; 710 + const ax = phys?.playerCamX ?? cam.x; 711 + const ay = -(phys?.playerCamY ?? cam.y); 712 + const az = phys?.playerCamZ ?? cam.z; 713 + const dx = ax - x, dy = ay - y, dz = az - z; 715 714 const dist = Math.sqrt(dx * dx + dy * dy + dz * dz); 716 715 if (dist >= EXPLOSION_RADIUS) return; 717 - const t = 1 - dist / EXPLOSION_RADIUS; // 1 at center → 0 at edge 716 + const falloff = 1 - dist / EXPLOSION_RADIUS; // 1 at center → 0 at edge 718 717 const minD = Math.max(dist, 0.4); 719 718 const nx = dx / minD; 719 + const ny = dy / minD; 720 720 const nz = dz / minD; 721 - const horForce = EXPLOSION_KICK_HOR * (0.4 + 0.6 * t); 722 - const vertForce = EXPLOSION_KICK_VERT * (0.3 + 0.7 * t) + EXPLOSION_VERT_BIAS; 721 + const horForce = EXPLOSION_KICK_HOR * (0.4 + 0.6 * falloff); 722 + // Vertical = radial component scaled by falloff + a flat upward bias so 723 + // ground-level blasts still launch you (ny ≈ 0 when grenade and feet share Y). 724 + const vertImpulse = ny * EXPLOSION_KICK_VERT * (0.3 + 0.7 * falloff) 725 + + EXPLOSION_VERT_BIAS * falloff; 726 + // cam-doll's applyImpulse takes "world" velocity in its own convention where 727 + // cam.x = -worldX. Arena's logical X is just cam.x — so to push the player 728 + // in arena's +X we feed the negated value here. 723 729 doll.applyImpulse?.({ 724 - x: nx * horForce, 725 - y: vertForce, 726 - z: nz * horForce, 730 + x: -nx * horForce, 731 + y: vertImpulse, 732 + z: -nz * horForce, 727 733 }); 728 734 // Suspend pmove reconciliation briefly so the local knockback isn't yanked 729 735 // back to the server's prediction (server doesn't know about the blast). ··· 756 762 positions.push(v[a], v[b], v[c]); 757 763 colors.push(col, col, col); 758 764 } 759 - // Form.position uses (-worldX, worldY, -worldZ) — same as remote bodies. 765 + // Form.position uses arena's logical world (cam.x → pos[0], cam.z → pos[2], 766 + // true world Y → pos[1]) — same convention as bodyFeet/shadows above. 760 767 const f = new FormRef( 761 768 { type: "triangle", positions, colors }, 762 - { pos: [-g.x, g.y, -g.z], rot: [0, g.t * 240, 0], scale: 1 }, 769 + { pos: [g.x, g.y, g.z], rot: [0, g.t * 240, 0], scale: 1 }, 763 770 ); 764 771 f.noFade = true; 765 772 return f; ··· 811 818 } 812 819 const f = new FormRef( 813 820 { type: "line", positions, colors }, 814 - { pos: [-ex.x, ex.y, -ex.z], rot: [0, 0, 0], scale: 1 }, 821 + { pos: [ex.x, ex.y, ex.z], rot: [0, 0, 0], scale: 1 }, 815 822 ); 816 823 f.noFade = true; 817 824 return f; ··· 825 832 deathTickAge = 0; 826 833 walkedTiles.clear(); 827 834 prevPlayerTile = null; 835 + // 🎆 Drop any in-flight grenade and resume server reconciliation right away, 836 + // otherwise the suspended reconcile from a fatal blast keeps the camera 837 + // glued to the lava view even after cam-doll teleported back to spawn. 838 + myGrenade = null; 839 + activeExplosion = null; 840 + knockbackGraceUntil = 0; 828 841 system?.fps?.doll?.respawn?.(0, 0); 829 842 return true; 830 843 }