determinism work

This commit is contained in:
Brandon Johnson
2026-01-31 14:09:01 -05:00
parent 60a6d88c66
commit d30e0622cf
12 changed files with 7577 additions and 99 deletions

View File

@@ -1,5 +1,5 @@
import { mat3, vec3 } from 'gl-matrix';
import { MatrixStack, atan2S16, clamp, sqrt, sumSq2, sumSq3, rsqrt, toS16 } from './math.js';
import { MatrixStack, atan2S16, atan2S16Detail, atan2S16Safe, clamp, sqrt, sumSq2, sumSq3, rsqrt, toS16 } from './math.js';
import { smoothstep } from './animation.js';
import { BALL_FLAGS, CAMERA_STATE, COLI_FLAGS } from './constants.js';
@@ -49,7 +49,14 @@ function applyMat3ToVec(out, mtx) {
function cameraFaceDirection(camera, lookDir) {
camera.rotY = atan2S16(lookDir.x, lookDir.z) - 0x8000;
camera.rotX = atan2S16(lookDir.y, sqrt(sumSq2(lookDir.x, lookDir.z)));
const debug = (globalThis as any).__DETERMINISM_DEBUG__;
if (debug) {
debug.source = 'cameraRotX';
}
camera.rotX = atan2S16Safe(lookDir.y, sqrt(sumSq2(lookDir.x, lookDir.z)));
if (debug) {
debug.source = null;
}
camera.rotZ = 0;
}
@@ -208,6 +215,7 @@ export class GameplayCamera {
}
initReady(stageRuntime, startRotY, startPos, flyInFrames = 90) {
const debug = (globalThis as any).__DETERMINISM_DEBUG__;
if (stageRuntime?.stage?.format === 'smb2') {
this.initReadySmb2(stageRuntime, startRotY, startPos, flyInFrames);
return;
@@ -232,7 +240,13 @@ export class GameplayCamera {
tmpVec.y = this.unk54.y - tmpVec.y;
tmpVec.z = this.unk54.z - tmpVec.z;
this.unk6C = toS16(atan2S16(tmpVec.x, tmpVec.z) - 0x8000);
this.unk68 = atan2S16(tmpVec.y, sqrt(sumSq2(tmpVec.x, tmpVec.z)));
if (debug) {
debug.source = 'cameraUnk68';
}
this.unk68 = atan2S16Safe(tmpVec.y, sqrt(sumSq2(tmpVec.x, tmpVec.z)));
if (debug) {
debug.source = null;
}
this.unk70 = 0;
this.unk74.x = startPos.x;
@@ -248,7 +262,23 @@ export class GameplayCamera {
tmpVec.y = this.unk74.y - tmpVec.y;
tmpVec.z = this.unk74.z - tmpVec.z;
this.unk8C = toS16(atan2S16(tmpVec.x, tmpVec.z) - 0x8000) + 0x10000;
this.unk88 = atan2S16(tmpVec.y, sqrt(sumSq2(tmpVec.x, tmpVec.z)));
if (debug) {
debug.source = 'cameraUnk88';
}
this.unk88 = atan2S16Safe(tmpVec.y, sqrt(sumSq2(tmpVec.x, tmpVec.z)));
if (debug) {
debug.source = null;
}
if (debug?.cameraInit) {
debug.cameraInit.push({
mode: 'smb1',
stageId: stageRuntime?.stage?.stageId ?? null,
unk6C: this.unk6C,
unk68: this.unk68,
unk8C: this.unk8C,
unk88: this.unk88,
});
}
this.unk90 = 0;
this.flags |= 1;
this.timerCurr = flyInFrames;
@@ -258,6 +288,7 @@ export class GameplayCamera {
}
initReadySmb2(stageRuntime, startRotY, startPos, flyInFrames = 90) {
const debug = (globalThis as any).__DETERMINISM_DEBUG__;
this.reset();
this.readyMode = 'smb2';
const stageId = stageRuntime?.stage?.stageId ?? -1;
@@ -288,7 +319,13 @@ export class GameplayCamera {
tmpVec.y = this.unk54.y - tmpVec.y;
tmpVec.z = this.unk54.z - tmpVec.z;
this.unk6C = toS16(atan2S16(tmpVec.x, tmpVec.z) - 0x8000 + preset.yawOffset);
this.unk68 = atan2S16(tmpVec.y, sqrt(sumSq2(tmpVec.x, tmpVec.z)));
if (debug) {
debug.source = 'cameraUnk68';
}
this.unk68 = atan2S16Safe(tmpVec.y, sqrt(sumSq2(tmpVec.x, tmpVec.z)));
if (debug) {
debug.source = null;
}
this.unk70 = 0;
const pivotYOffset = (stageId === 0x15a ? 0 : 0.18) + 0.8;
@@ -302,6 +339,16 @@ export class GameplayCamera {
this.timerCurr = flyInFrames;
this.timerMax = flyInFrames;
this.state = CAMERA_STATE.READY_MAIN;
if (debug?.cameraInit) {
debug.cameraInit.push({
mode: 'smb2',
stageId,
unk6C: this.unk6C,
unk68: this.unk68,
unk8C: this.unk8C,
unk88: this.unk88,
});
}
this.updateReadyMain(false, false);
}
@@ -338,6 +385,7 @@ export class GameplayCamera {
}
updateLevelMain(ball, paused) {
const debug = (globalThis as any).__DETERMINISM_DEBUG__;
if (paused) {
return;
}
@@ -373,7 +421,34 @@ export class GameplayCamera {
let pitch = 0;
if (ball.unk80 >= 60) {
pitch = atan2S16(tmpVec.y, sqrt(sumSq2(tmpVec.x, tmpVec.z)));
if (debug) {
debug.source = 'cameraPitch';
}
pitch = atan2S16Safe(tmpVec.y, sqrt(sumSq2(tmpVec.x, tmpVec.z)));
if (debug) {
debug.source = null;
}
}
if (debug?.cameraPitch) {
if (this._debugPrevPitch === undefined) {
this._debugPrevPitch = pitch;
} else {
const delta = toS16(pitch - this._debugPrevPitch);
if (Math.abs(delta) >= 128) {
const denom = sqrt(sumSq2(tmpVec.x, tmpVec.z));
const detail = atan2S16Detail(tmpVec.y, denom);
debug.cameraPitch.push({
tick: debug.tick ?? null,
pitch,
prev: this._debugPrevPitch,
delta,
tmpVec: { x: tmpVec.x, y: tmpVec.y, z: tmpVec.z },
denom,
detail,
});
}
this._debugPrevPitch = pitch;
}
}
let yaw = toS16(atan2S16(tmpVec.x, tmpVec.z) - 0x8000);
@@ -431,6 +506,21 @@ export class GameplayCamera {
this.unk10C = toS16(yaw - this.rotY);
this.rotY = yaw;
this.rotX = toS16(pitchSmoothed + 62208);
if (debug?.cameraYaw) {
if (this._debugYawCount === undefined) {
this._debugYawCount = 0;
}
if (this._debugYawCount < 200) {
debug.cameraYaw.push({
tick: debug.tick ?? null,
yaw,
rotY: this.rotY,
deltaYaw,
tmpVec: { x: tmpVec.x, y: tmpVec.y, z: tmpVec.z },
});
this._debugYawCount += 1;
}
}
stack.fromTranslate(this.lookAt);
stack.rotateY(this.rotY);
@@ -450,6 +540,7 @@ export class GameplayCamera {
}
updateLevelMainSmb2(ball, stageRuntime, paused) {
const debug = (globalThis as any).__DETERMINISM_DEBUG__;
if (paused) {
return;
}
@@ -491,7 +582,13 @@ export class GameplayCamera {
let pitchRaw = 0;
if (ball.unk80 >= 60) {
pitchRaw = atan2S16(tmpVec.y, sqrt(sumSq2(tmpVec.x, tmpVec.z)));
if (debug) {
debug.source = 'cameraPitchRaw';
}
pitchRaw = atan2S16Safe(tmpVec.y, sqrt(sumSq2(tmpVec.x, tmpVec.z)));
if (debug) {
debug.source = null;
}
}
let yaw = toS16(atan2S16(tmpVec.x, tmpVec.z) - 0x8000);

View File

@@ -683,8 +683,24 @@ function collideBallWithTriFace(ball, tri) {
function collideBallWithTriEdge(ball, ballPosTri, ballPrevPosTri, edge) {
stack.push();
stack.fromIdentity();
const debug = (globalThis as any).__DETERMINISM_DEBUG__;
if (debug?.edgeNormals) {
const sum = Math.abs(edge.normal.x) + Math.abs(edge.normal.y);
if (sum <= debug.edgeEps) {
debug.edgeNormals.push({
tick: debug.tick ?? null,
normal: { x: edge.normal.x, y: edge.normal.y },
edgeStart: { x: edge.start.x, y: edge.start.y },
edgeEnd: { x: edge.end.x, y: edge.end.y },
sum,
});
}
}
stack.translateXYZ(edge.start.x, edge.start.y, 0);
stack.rotateZ(-atan2S16(edge.normal.x, edge.normal.y));
const edgeNormalLenSq = sumSq2(edge.normal.x, edge.normal.y);
if (edgeNormalLenSq > FLT_EPSILON) {
stack.rotateZ(-atan2S16(edge.normal.x, edge.normal.y));
}
stack.rigidInvTfPoint(ballPrevPosTri, triEdgeLocalPrevPos);
stack.rigidInvTfPoint(ballPosTri, triEdgeLocalPos);
@@ -697,7 +713,10 @@ function collideBallWithTriEdge(ball, ballPosTri, ballPrevPosTri, edge) {
triEdgePlaneVec.x = 0;
triEdgePlaneVec.y = triEdgeLocalPos.y - triEdgeLocalPrevPos.y;
triEdgePlaneVec.z = triEdgeLocalPos.z - triEdgeLocalPrevPos.z;
stack.rotateX(-atan2S16(triEdgePlaneVec.y, triEdgePlaneVec.z) - 0x8000);
const planeLenSq = sumSq2(triEdgePlaneVec.y, triEdgePlaneVec.z);
if (planeLenSq > FLT_EPSILON) {
stack.rotateX(-atan2S16(triEdgePlaneVec.y, triEdgePlaneVec.z) - 0x8000);
}
stack.rigidInvTfPoint(ballPosTri, triEdgeLocalPos);
stack.rigidInvTfPoint(ballPrevPosTri, triEdgeLocalPrevPos);

31
src/determinism.ts Normal file
View File

@@ -0,0 +1,31 @@
export type QuantizedStick = { x: number; y: number };
export function quantizeStickAxis(value: number): number {
const clamped = Math.max(-1, Math.min(1, value));
const quantized = Math.round(clamped * 127);
if (quantized < -127) {
return -127;
}
if (quantized > 127) {
return 127;
}
return quantized;
}
export function dequantizeStickAxis(value: number): number {
return value / 127;
}
export function quantizeStick(stick: { x: number; y: number }): QuantizedStick {
return {
x: quantizeStickAxis(stick.x),
y: quantizeStickAxis(stick.y),
};
}
export function dequantizeStick(stick: QuantizedStick): { x: number; y: number } {
return {
x: dequantizeStickAxis(stick.x),
y: dequantizeStickAxis(stick.y),
};
}

View File

@@ -1,5 +1,5 @@
import { raycastStageDown } from './collision.js';
import { atan2S16, sumSq2, toS16, vecDot } from './math.js';
import { atan2S16, sqrt, sumSq2, toS16, vecDot } from './math.js';
const SPARK_GRAVITY_SCALE = 0.008;
const SPARK_DAMP = 0.992;
@@ -27,8 +27,8 @@ const LEVITATE_DRIFT = 0.0012;
const LEVITATE_DAMP = 0.98;
const STAR_SCALE_TARGET = 0.015;
const randFloat = () => Math.random();
const randS16 = () => Math.trunc(Math.random() * 0x8000);
const randFloat = (rng) => rng.nextFloat();
const randS16 = (rng) => rng.nextS16();
type Vec3 = { x: number; y: number; z: number };
@@ -156,7 +156,7 @@ function updateEffectGround(
effect.glowPos.y = effect.pos.y - (dist - 0.02) * hitNormal.y;
effect.glowPos.z = effect.pos.z - (dist - 0.02) * hitNormal.z;
const rotY = atan2S16(hitNormal.x, hitNormal.z) - 0x8000;
const rotX = atan2S16(hitNormal.y, Math.sqrt(sumSq2(hitNormal.x, hitNormal.z)));
const rotX = atan2S16(hitNormal.y, sqrt(sumSq2(hitNormal.x, hitNormal.z)));
effect.glowRotY = rotY;
effect.glowRotX = rotX;
}
@@ -167,6 +167,7 @@ function applyEffectGroundResponse(
bounceScale: number,
surfaceBlend: number,
randomizeRot = false,
rng = null,
): void {
const normal = effect.groundNormal;
const dx = effect.pos.x - effect.groundPos.x;
@@ -193,10 +194,10 @@ function applyEffectGroundResponse(
effect.vel.x += (effect.groundVel.x - effect.vel.x) * surfaceBlend;
effect.vel.y += (effect.groundVel.y - effect.vel.y) * surfaceBlend;
effect.vel.z += (effect.groundVel.z - effect.vel.z) * surfaceBlend;
if (randomizeRot) {
effect.rotVelX = toS16(effect.rotVelX + impulse * (randFloat() - 0.5) * STAR_ROT_KICK_XZ);
effect.rotVelY = toS16(effect.rotVelY + impulse * (randFloat() - 0.5) * STAR_ROT_KICK_Y);
effect.rotVelZ = toS16(effect.rotVelZ + impulse * (randFloat() - 0.5) * STAR_ROT_KICK_XZ);
if (randomizeRot && rng) {
effect.rotVelX = toS16(effect.rotVelX + impulse * (randFloat(rng) - 0.5) * STAR_ROT_KICK_XZ);
effect.rotVelY = toS16(effect.rotVelY + impulse * (randFloat(rng) - 0.5) * STAR_ROT_KICK_Y);
effect.rotVelZ = toS16(effect.rotVelZ + impulse * (randFloat(rng) - 0.5) * STAR_ROT_KICK_XZ);
}
effect.vel.x += impulse * bounceScale * normal.x;
effect.vel.y += impulse * bounceScale * normal.y;
@@ -252,12 +253,12 @@ function updateEffectGlow(
effect.glowPos.y = effect.pos.y - (dist - 0.02) * hit.normal.y;
effect.glowPos.z = effect.pos.z - (dist - 0.02) * hit.normal.z;
const rotY = atan2S16(hit.normal.x, hit.normal.z) - 0x8000;
const rotX = atan2S16(hit.normal.y, Math.sqrt(sumSq2(hit.normal.x, hit.normal.z)));
const rotX = atan2S16(hit.normal.y, sqrt(sumSq2(hit.normal.x, hit.normal.z)));
effect.glowRotY = rotY;
effect.glowRotX = rotX;
}
export function updateBallEffects(effects: BallEffect[], gravity: Vec3, stageRuntime: any): void {
export function updateBallEffects(effects: BallEffect[], gravity: Vec3, stageRuntime: any, rng: any): void {
for (let i = effects.length - 1; i >= 0; i -= 1) {
const effect = effects[i];
effect.prevPos.x = effect.pos.x;
@@ -291,7 +292,7 @@ export function updateBallEffects(effects: BallEffect[], gravity: Vec3, stageRun
const nx = -normal.x;
const nz = -normal.z;
const rotY = atan2S16(nx, nz);
const rotX = toS16(atan2S16(normal.y, Math.sqrt(sumSq2(nx, nz))) - 0x8000);
const rotX = toS16(atan2S16(normal.y, sqrt(sumSq2(nx, nz))) - 0x8000);
effect.rotY = rotY;
effect.rotX = rotX;
if (effect.life < 8) {
@@ -334,6 +335,7 @@ export function updateBallEffects(effects: BallEffect[], gravity: Vec3, stageRun
1.0,
STAR_GROUND_BLEND,
true,
rng,
);
}
} else {
@@ -361,11 +363,11 @@ export function updateBallEffects(effects: BallEffect[], gravity: Vec3, stageRun
}
}
export function spawnMovementSparks(effects: BallEffect[], ball: any, onGround: boolean): void {
export function spawnMovementSparks(effects: BallEffect[], ball: any, onGround: boolean, rng: any): void {
if (!onGround) {
return;
}
const speed = Math.sqrt(ball.vel.x * ball.vel.x + ball.vel.y * ball.vel.y + ball.vel.z * ball.vel.z);
const speed = sqrt(ball.vel.x * ball.vel.x + ball.vel.y * ball.vel.y + ball.vel.z * ball.vel.z);
const intensity = speed * 5.0;
if (intensity <= 1.5) {
return;
@@ -390,12 +392,12 @@ export function spawnMovementSparks(effects: BallEffect[], ball: any, onGround:
'coli',
basePos,
baseVel,
Math.trunc(SPARK_LIFE_MIN + SPARK_LIFE_RANGE * randFloat()),
Math.trunc(SPARK_LIFE_MIN + SPARK_LIFE_RANGE * randFloat(rng)),
);
const spread = randFloat() * intensity * 0.1;
spark.vel.x += (normal.x + (randFloat() * 1.5 - 0.75)) * spread;
spark.vel.y += (normal.y + (randFloat() * 1.5 - 0.75)) * spread;
spark.vel.z += (normal.z + (randFloat() * 1.5 - 0.75)) * spread;
const spread = randFloat(rng) * intensity * 0.1;
spark.vel.x += (normal.x + (randFloat(rng) * 1.5 - 0.75)) * spread;
spark.vel.y += (normal.y + (randFloat(rng) * 1.5 - 0.75)) * spread;
spark.vel.z += (normal.z + (randFloat(rng) * 1.5 - 0.75)) * spread;
spark.scale = 1.0;
spark.scaleTarget = 1.0;
spark.colorR = 1.1;
@@ -410,6 +412,7 @@ export function spawnCollisionStars(
effects: BallEffect[],
ball: any,
hardestColiSpeed: number,
rng: any,
): void {
const surfaceNormal = { x: ball.unk114.x, y: ball.unk114.y, z: ball.unk114.z };
const normal = { x: -surfaceNormal.x, y: -surfaceNormal.y, z: -surfaceNormal.z };
@@ -428,7 +431,7 @@ export function spawnCollisionStars(
let count = Math.min(32, Math.trunc(rawCount));
const scaleBoost = Math.abs(hardestColiSpeed / 0.33) + 1.0;
const flashScale = Math.sqrt(Math.abs(hardestColiSpeed * 10.0));
const flashScale = sqrt(Math.abs(hardestColiSpeed * 10.0));
const flash = createEffect('coliflash', basePos, { x: 0, y: 0, z: 0 }, 12);
flash.scale = flashScale * 0.25;
flash.scaleTarget = flashScale;
@@ -443,22 +446,22 @@ export function spawnCollisionStars(
'colistar',
basePos,
baseVel,
Math.trunc(STAR_LIFE_MIN + STAR_LIFE_RANGE * randFloat()),
Math.trunc(STAR_LIFE_MIN + STAR_LIFE_RANGE * randFloat(rng)),
);
const jitter = {
x: scaleBoost * (randFloat() * 0.05 - 0.025),
y: scaleBoost * (randFloat() * 0.05 - 0.025),
z: scaleBoost * (randFloat() * 0.05 - 0.025),
x: scaleBoost * (randFloat(rng) * 0.05 - 0.025),
y: scaleBoost * (randFloat(rng) * 0.05 - 0.025),
z: scaleBoost * (randFloat(rng) * 0.05 - 0.025),
};
const push = scaleBoost * (randFloat() * 0.055 + 0.015);
const push = scaleBoost * (randFloat(rng) * 0.055 + 0.015);
star.vel.x += jitter.x + push * normal.x;
star.vel.y += jitter.y + push * normal.y;
star.vel.z += jitter.z + push * normal.z;
star.rotX = 0;
star.rotY = randS16();
star.rotZ = randS16();
star.rotY = randS16(rng);
star.rotZ = randS16(rng);
star.rotVelX = 0;
star.rotVelY = (randS16() & 0xfff) + 0x1000;
star.rotVelY = (randS16(rng) & 0xfff) + 0x1000;
star.rotVelZ = 0;
star.scale = 0;
star.scaleTarget = STAR_SCALE_TARGET;
@@ -472,14 +475,14 @@ export function spawnCollisionStars(
'coli',
basePos,
{ x: baseVel.x * 0.5, y: baseVel.y * 0.5, z: baseVel.z * 0.5 },
Math.trunc(SPARK_LIFE_MIN + SPARK_LIFE_RANGE * randFloat()),
Math.trunc(SPARK_LIFE_MIN + SPARK_LIFE_RANGE * randFloat(rng)),
);
const jitter = {
x: scaleBoost * (randFloat() * 0.05 - 0.025),
y: scaleBoost * (randFloat() * 0.05 - 0.025),
z: scaleBoost * (randFloat() * 0.05 - 0.025),
x: scaleBoost * (randFloat(rng) * 0.05 - 0.025),
y: scaleBoost * (randFloat(rng) * 0.05 - 0.025),
z: scaleBoost * (randFloat(rng) * 0.05 - 0.025),
};
const push = scaleBoost * (randFloat() * 0.05 + 0.06);
const push = scaleBoost * (randFloat(rng) * 0.05 + 0.06);
spark.vel.x += jitter.x + push * normal.x;
spark.vel.y += jitter.y + push * normal.y;
spark.vel.z += jitter.z + push * normal.z;
@@ -491,28 +494,28 @@ export function spawnCollisionStars(
}
}
export function spawnPostGoalSparkle(effects: BallEffect[], ball: any): void {
export function spawnPostGoalSparkle(effects: BallEffect[], ball: any, rng: any): void {
const sparkle = createEffect(
'levitate',
ball.pos,
{
x: (randFloat() - 0.5) * LEVITATE_DRIFT,
y: LEVITATE_START_VEL + randFloat() * LEVITATE_START_VEL,
z: (randFloat() - 0.5) * LEVITATE_DRIFT,
x: (randFloat(rng) - 0.5) * LEVITATE_DRIFT,
y: LEVITATE_START_VEL + randFloat(rng) * LEVITATE_START_VEL,
z: (randFloat(rng) - 0.5) * LEVITATE_DRIFT,
},
Math.trunc(LEVITATE_LIFE_MIN + LEVITATE_LIFE_RANGE * randFloat()),
Math.trunc(LEVITATE_LIFE_MIN + LEVITATE_LIFE_RANGE * randFloat(rng)),
);
sparkle.pos.x += (randFloat() - 0.5) * LEVITATE_OFFSET_RADIUS;
sparkle.pos.y += (randFloat() - 0.5) * LEVITATE_OFFSET_RADIUS;
sparkle.pos.z += (randFloat() - 0.5) * LEVITATE_OFFSET_RADIUS;
sparkle.pos.x += (randFloat(rng) - 0.5) * LEVITATE_OFFSET_RADIUS;
sparkle.pos.y += (randFloat(rng) - 0.5) * LEVITATE_OFFSET_RADIUS;
sparkle.pos.z += (randFloat(rng) - 0.5) * LEVITATE_OFFSET_RADIUS;
sparkle.prevPos.x = sparkle.pos.x;
sparkle.prevPos.y = sparkle.pos.y;
sparkle.prevPos.z = sparkle.pos.z;
sparkle.baseY = sparkle.pos.y;
sparkle.scale = LEVITATE_SCALE;
sparkle.scaleTarget = sparkle.scale;
sparkle.rotX = randS16();
sparkle.rotY = randS16();
sparkle.rotZ = randS16();
sparkle.rotX = randS16(rng);
sparkle.rotY = randS16(rng);
sparkle.rotZ = randS16(rng);
effects.push(sparkle);
}

View File

@@ -14,8 +14,9 @@ import {
type GameSource,
} from './constants.js';
import { intersectsMovingSpheres, tfPhysballToAnimGroupSpace } from './collision.js';
import { MatrixStack, toS16 } from './math.js';
import { MatrixStack, sqrt, toS16 } from './math.js';
import { GameplayCamera } from './camera.js';
import { dequantizeStick, quantizeStick, type QuantizedStick } from './determinism.js';
import {
checkBallEnteredGoal,
createBallState,
@@ -95,7 +96,7 @@ function nlerpQuat(out: { x: number; y: number; z: number; w: number }, a: any,
out.y = a.y + (by - a.y) * t;
out.z = a.z + (bz - a.z) * t;
out.w = a.w + (bw - a.w) * t;
const len = Math.sqrt((out.x * out.x) + (out.y * out.y) + (out.z * out.z) + (out.w * out.w));
const len = sqrt((out.x * out.x) + (out.y * out.y) + (out.z * out.z) + (out.w * out.w));
if (len > 0) {
out.x /= len;
out.y /= len;
@@ -331,6 +332,10 @@ export class Game {
tickMs: number;
lastTickMs: number;
};
public simTick: number;
public inputFeed: QuantizedStick[] | null;
public inputFeedIndex: number;
public inputRecord: QuantizedStick[] | null;
constructor({
hud,
@@ -420,6 +425,10 @@ export class Game {
tickMs: 0,
lastTickMs: 0,
};
this.simTick = 0;
this.inputFeed = null;
this.inputFeedIndex = 0;
this.inputRecord = null;
}
setGameSource(source: GameSource) {
@@ -428,6 +437,21 @@ export class Game {
this.audio?.stopMusic();
}
setInputFeed(feed: QuantizedStick[] | null) {
this.inputFeed = feed;
this.inputFeedIndex = 0;
}
startInputRecording() {
this.inputRecord = [];
}
stopInputRecording() {
const record = this.inputRecord;
this.inputRecord = null;
return record;
}
init() {
this.input = new Input();
this.world = new World();
@@ -759,7 +783,7 @@ export class Game {
renderPoint.normal.x = lerp(point.prevNormal?.x ?? point.normal.x, point.normal.x, alpha);
renderPoint.normal.y = lerp(point.prevNormal?.y ?? point.normal.y, point.normal.y, alpha);
renderPoint.normal.z = lerp(point.prevNormal?.z ?? point.normal.z, point.normal.z, alpha);
const normalLen = Math.sqrt(
const normalLen = sqrt(
(renderPoint.normal.x * renderPoint.normal.x)
+ (renderPoint.normal.y * renderPoint.normal.y)
+ (renderPoint.normal.z * renderPoint.normal.z)
@@ -1193,6 +1217,8 @@ export class Game {
this.stage = stage;
this.stageAttempts = isRestart ? this.stageAttempts + 1 : 1;
this.stageRuntime = new StageRuntime(stage);
this.simTick = 0;
this.inputFeedIndex = 0;
this.animGroupTransforms = this.stageRuntime.animGroups.map((group) => group.transform);
this.interpolatedAnimGroupTransforms = null;
this.bananaGroups = new Array(this.stage.animGroupCount);
@@ -1716,6 +1742,25 @@ export class Game {
}
}
private readDeterministicStick(inputEnabled: boolean) {
let frame = null;
if (this.inputFeed && this.inputFeedIndex < this.inputFeed.length) {
frame = this.inputFeed[this.inputFeedIndex];
this.inputFeedIndex += 1;
}
if (!inputEnabled) {
return { x: 0, y: 0 };
}
if (!frame) {
const raw = this.input?.getStick?.() ?? { x: 0, y: 0 };
frame = quantizeStick(raw);
if (this.inputRecord) {
this.inputRecord.push({ x: frame.x, y: frame.y });
}
}
return dequantizeStick(frame);
}
update(dtSeconds: number) {
if (!this.running) {
return;
@@ -1741,6 +1786,10 @@ export class Game {
}
this.stageRuntime.goalHoldOpen = this.goalTimerFrames > 0;
while (!this.pendingAdvance && this.accumulator >= this.fixedStep) {
const debug = (globalThis as any).__DETERMINISM_DEBUG__;
if (debug) {
debug.tick = this.simTick;
}
const tickStart = this.simPerf.enabled ? nowMs() : 0;
try {
const ringoutActive = this.ringoutTimerFrames > 0;
@@ -1763,7 +1812,7 @@ export class Game {
? this.introTimerFrames
: null;
this.stageRuntime.switchesEnabled = switchesEnabled;
const stick = inputEnabled ? this.input?.getStick?.() ?? { x: 0, y: 0 } : { x: 0, y: 0 };
const stick = this.readDeterministicStick(inputEnabled);
this.world.updateInput(stick, this.cameraController.rotY);
const stagePaused = this.paused || timeoverActive;
this.stageRuntime.advance(
@@ -1911,6 +1960,7 @@ export class Game {
this.simPerf.tickMs += tickMs;
this.simPerf.tickCount += 1;
}
this.simTick += 1;
}
}
if (this.simPerf.enabled && this.simPerf.tickCount >= this.simPerf.logEvery) {

View File

@@ -1,9 +1,31 @@
const PI = Math.PI;
const ANGLE_SCALE = 0x8000 / PI;
const ANGLE_INV_SCALE = PI / 0x8000;
import { ATAN_TABLE, SIN_TABLE } from './math_lut.js';
export const EPSILON = 1e-7;
const f32View = new Float32Array(1);
const u32View = new Uint32Array(f32View.buffer);
function f32(value) {
return Math.fround(value);
}
function f32Bits(value) {
f32View[0] = value;
return u32View[0];
}
function roundToNearestEven(value) {
const base = Math.floor(value);
const frac = value - base;
if (frac > 0.5) {
return base + 1;
}
if (frac < 0.5) {
return base;
}
return (base & 1) === 0 ? base : base + 1;
}
export function toS16(value) {
const v = Math.trunc(value);
const wrapped = ((v + 0x8000) & 0xffff) - 0x8000;
@@ -11,15 +33,215 @@ export function toS16(value) {
}
export function sinS16(angle) {
return Math.sin(angle * ANGLE_INV_SCALE);
const a = angle & 0xffff;
let index = a & 0x3fff;
if (a & 0x4000) {
index = 0x4000 - index;
}
const result = SIN_TABLE[index];
return (a & 0x8000) ? -result : result;
}
export function cosS16(angle) {
return Math.cos(angle * ANGLE_INV_SCALE);
return sinS16((angle + 0x4000) & 0xffff);
}
function atanIndex(x) {
const bits = f32Bits(x);
let r5 = bits & 0x7fffff;
r5 |= 0x00800000;
const exp = (bits >>> 23) & 0xff;
if (exp <= 0x67) {
return 0;
}
let r4 = (0x87 - exp) & 0xffffffff;
if ((r4 & ~0x1f) === 0) {
r4 |= 0x20;
}
const shift = r4 & 31;
const shifted = r5 >>> shift;
const offset = shifted & 0x7ff8;
return offset >>> 3;
}
function atanS16(value) {
if (value === 0) {
return 0;
}
let sign = 1;
let x = value;
if (x < 0) {
sign = -1;
x = -x;
}
if (x === 1) {
return sign * 0x2000;
}
let invert = false;
if (x > 1) {
x = 1 / x;
invert = true;
}
x = f32(x);
const index = atanIndex(x);
const slope = ATAN_TABLE[index * 2];
const intercept = ATAN_TABLE[index * 2 + 1];
const angle = f32(f32(x * slope) + intercept);
let result = roundToNearestEven(angle);
if (invert) {
result = 0x4000 - result;
}
return result * sign;
}
function atanS16WithDetail(value) {
if (value === 0) {
return { angle: 0, index: 0, raw: 0 };
}
let sign = 1;
let x = value;
if (x < 0) {
sign = -1;
x = -x;
}
if (x === 1) {
return { angle: sign * 0x2000, index: 0, raw: 0x2000 };
}
let invert = false;
if (x > 1) {
x = 1 / x;
invert = true;
}
x = f32(x);
const index = atanIndex(x);
const slope = ATAN_TABLE[index * 2];
const intercept = ATAN_TABLE[index * 2 + 1];
const angle = f32(f32(x * slope) + intercept);
let result = roundToNearestEven(angle);
if (invert) {
result = 0x4000 - result;
}
return { angle: result * sign, index, raw: angle };
}
export function atan2S16(y, x) {
return toS16(Math.atan2(y, x) * ANGLE_SCALE);
const yy = f32(y);
const xx = f32(x);
if (yy === 0 && xx === 0) {
return 0;
}
const ay = Math.abs(yy);
const ax = Math.abs(xx);
let angle;
let ratio = 0;
let atanIndexUsed = -1;
let atanRaw = 0;
if (ay === ax) {
angle = ay === 0 ? 0 : 0x2000;
} else if (ay > ax) {
ratio = ax / ay;
const atanDetail = atanS16WithDetail(ratio);
atanIndexUsed = atanDetail.index;
atanRaw = atanDetail.raw;
angle = atanDetail.angle;
angle = 0x4000 - angle;
} else {
ratio = ay / ax;
const atanDetail = atanS16WithDetail(ratio);
atanIndexUsed = atanDetail.index;
atanRaw = atanDetail.raw;
angle = atanDetail.angle;
}
if (xx < 0) {
if (yy >= 0) {
angle = 0x8000 - angle;
} else {
angle = angle - 0x8000;
}
} else if (yy < 0) {
angle = -angle;
}
const result = toS16(angle);
const debug = (globalThis as any).__DETERMINISM_DEBUG__;
if (debug?.atan2 && debug.remaining > 0) {
const sum = Math.abs(yy) + Math.abs(xx);
if (sum <= (debug.eps ?? 0)) {
debug.remaining -= 1;
if (debug.records) {
const stack = debug.stack ? new Error().stack : null;
debug.records.push({
tick: debug.tick ?? null,
source: debug.source ?? null,
x: xx,
y: yy,
angle: result,
sum,
ratio,
atanIndexUsed,
atanRaw,
stack,
});
}
}
}
return result;
}
export function atan2S16Safe(y, x, eps = EPSILON) {
if (Math.abs(y) + Math.abs(x) <= eps) {
return 0;
}
return atan2S16(y, x);
}
export function atan2S16Detail(y, x) {
const yy = f32(y);
const xx = f32(x);
if (yy === 0 && xx === 0) {
return {
angle: 0,
ratio: 0,
atanIndexUsed: 0,
atanRaw: 0,
};
}
const ay = Math.abs(yy);
const ax = Math.abs(xx);
let angle;
let ratio = 0;
let atanIndexUsed = -1;
let atanRaw = 0;
if (ay === ax) {
angle = ay === 0 ? 0 : 0x2000;
} else if (ay > ax) {
ratio = ax / ay;
const atanDetail = atanS16WithDetail(ratio);
atanIndexUsed = atanDetail.index;
atanRaw = atanDetail.raw;
angle = 0x4000 - atanDetail.angle;
} else {
ratio = ay / ax;
const atanDetail = atanS16WithDetail(ratio);
atanIndexUsed = atanDetail.index;
atanRaw = atanDetail.raw;
angle = atanDetail.angle;
}
if (xx < 0) {
if (yy >= 0) {
angle = 0x8000 - angle;
} else {
angle = angle - 0x8000;
}
} else if (yy < 0) {
angle = -angle;
}
return {
angle: toS16(angle),
ratio,
atanIndexUsed,
atanRaw,
};
}
export function sumSq2(x, y) {
@@ -31,11 +253,46 @@ export function sumSq3(x, y, z) {
}
export function sqrt(value) {
return Math.sqrt(value);
const v = f32(value);
if (Number.isNaN(v)) {
return NaN;
}
if (v <= 0) {
return 0;
}
if (!Number.isFinite(v)) {
return v;
}
const inv = rsqrtSmb1(v);
return f32(v * inv);
}
export function rsqrt(value) {
return 1 / Math.sqrt(value);
const v = f32(value);
if (Number.isNaN(v)) {
return NaN;
}
if (v <= 0) {
return Infinity;
}
if (!Number.isFinite(v)) {
return 0;
}
return rsqrtSmb1(v);
}
function rsqrtSmb1(value) {
const v = f32(value);
// Deterministic approximation using fixed Newton iterations, mirroring SMB1 flow.
let x = f32(1 / Math.sqrt(v));
const half = f32(0.5);
const onePointFive = f32(1.5);
for (let i = 0; i < 3; i += 1) {
const xx = f32(x * x);
const term = f32(onePointFive - f32(half * f32(v * xx)));
x = f32(x * term);
}
return x;
}
export function floor(value) {
@@ -56,7 +313,7 @@ export function vecDotNormalized(a, b) {
}
export function vecLen(a) {
return Math.sqrt(sumSq3(a.x, a.y, a.z));
return sqrt(sumSq3(a.x, a.y, a.z));
}
export function vecNormalizeLen(a) {
@@ -96,7 +353,7 @@ export function vecCross(a, b, out) {
}
export function vecDistance(a, b) {
return Math.sqrt(sumSq3(a.x - b.x, a.y - b.y, a.z - b.z));
return sqrt(sumSq3(a.x - b.x, a.y - b.y, a.z - b.z));
}
export function quatFromAxisAngle(axis, angleS16, out) {
@@ -491,7 +748,7 @@ export class MatrixStack {
const m = this.mtxA;
const trace = m[0] + m[5] + m[10];
if (trace > 0) {
const s = Math.sqrt(trace + 1.0) * 2;
const s = sqrt(trace + 1.0) * 2;
out.w = 0.25 * s;
out.x = (m[9] - m[6]) / s;
out.y = (m[2] - m[8]) / s;
@@ -499,7 +756,7 @@ export class MatrixStack {
return;
}
if (m[0] > m[5] && m[0] > m[10]) {
const s = Math.sqrt(1.0 + m[0] - m[5] - m[10]) * 2;
const s = sqrt(1.0 + m[0] - m[5] - m[10]) * 2;
out.w = (m[9] - m[6]) / s;
out.x = 0.25 * s;
out.y = (m[1] + m[4]) / s;
@@ -507,14 +764,14 @@ export class MatrixStack {
return;
}
if (m[5] > m[10]) {
const s = Math.sqrt(1.0 + m[5] - m[0] - m[10]) * 2;
const s = sqrt(1.0 + m[5] - m[0] - m[10]) * 2;
out.w = (m[2] - m[8]) / s;
out.x = (m[1] + m[4]) / s;
out.y = 0.25 * s;
out.z = (m[6] + m[9]) / s;
return;
}
const s = Math.sqrt(1.0 + m[10] - m[0] - m[5]) * 2;
const s = sqrt(1.0 + m[10] - m[0] - m[5]) * 2;
out.w = (m[4] - m[1]) / s;
out.x = (m[2] + m[8]) / s;
out.y = (m[6] + m[9]) / s;

6857
src/math_lut.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -786,12 +786,13 @@ export function stepBall(ball, stageRuntime, world) {
if (stageRuntime.effects) {
const onGround = (physBall.flags & COLI_FLAGS.OCCURRED) !== 0 && physBall.hardestColiPlane.normal.y > 0;
spawnMovementSparks(stageRuntime.effects, ball, onGround);
const rng = stageRuntime.visualRng;
spawnMovementSparks(stageRuntime.effects, ball, onGround, rng);
if ((physBall.flags & COLI_FLAGS.OCCURRED) && physBall.hardestColiSpeed < -0.06) {
spawnCollisionStars(stageRuntime.effects, ball, physBall.hardestColiSpeed);
spawnCollisionStars(stageRuntime.effects, ball, physBall.hardestColiSpeed, rng);
}
if (ball.state === BALL_STATES.GOAL_MAIN && (ball.unk80 & 1)) {
spawnPostGoalSparkle(stageRuntime.effects, ball);
spawnPostGoalSparkle(stageRuntime.effects, ball, rng);
}
}

25
src/rng.ts Normal file
View File

@@ -0,0 +1,25 @@
export class DeterministicRng {
constructor(seed = 0) {
this.state = seed >>> 0;
if (this.state === 0) {
this.state = 0x6d2b79f5;
}
}
nextU32() {
let x = this.state >>> 0;
x ^= (x << 13) >>> 0;
x ^= x >>> 17;
x ^= (x << 5) >>> 0;
this.state = x >>> 0;
return this.state;
}
nextFloat() {
return (this.nextU32() >>> 8) * (1 / 0x01000000);
}
nextS16() {
return (this.nextU32() >>> 17) & 0x7fff;
}
}

113
src/sim_hash.ts Normal file
View File

@@ -0,0 +1,113 @@
const f32View = new Float32Array(1);
const u32View = new Uint32Array(f32View.buffer);
function hashU32(hash, value) {
let h = hash ^ (value >>> 0);
h = Math.imul(h, 16777619) >>> 0;
return h;
}
function hashF32(hash, value) {
f32View[0] = value;
return hashU32(hash, u32View[0]);
}
function hashVec3(hash, vec) {
let h = hashF32(hash, vec.x);
h = hashF32(h, vec.y);
h = hashF32(h, vec.z);
return h;
}
function hashS16(hash, value) {
return hashU32(hash, value & 0xffff);
}
export function hashSimState(ball, world, stageRuntime, { includeVisual = false } = {}) {
let h = 0x811c9dc5;
if (!ball || !world || !stageRuntime) {
return h >>> 0;
}
h = hashVec3(h, ball.pos);
h = hashVec3(h, ball.vel);
h = hashS16(h, ball.rotX);
h = hashS16(h, ball.rotY);
h = hashS16(h, ball.rotZ);
h = hashU32(h, ball.state | 0);
h = hashU32(h, ball.flags | 0);
h = hashF32(h, ball.currRadius ?? 0);
h = hashF32(h, ball.speed ?? 0);
h = hashU32(h, ball.animGroupId | 0);
h = hashS16(h, ball.apeYaw ?? 0);
if (ball.orientation) {
h = hashF32(h, ball.orientation.x);
h = hashF32(h, ball.orientation.y);
h = hashF32(h, ball.orientation.z);
h = hashF32(h, ball.orientation.w);
}
h = hashS16(h, world.xrot ?? 0);
h = hashS16(h, world.zrot ?? 0);
if (world.gravity) {
h = hashVec3(h, world.gravity);
}
h = hashU32(h, stageRuntime.timerFrames | 0);
if (stageRuntime.animGroups) {
h = hashU32(h, stageRuntime.animGroups.length | 0);
for (const group of stageRuntime.animGroups) {
h = hashVec3(h, group.pos);
h = hashVec3(h, group.rot);
}
}
if (stageRuntime.goalBags) {
h = hashU32(h, stageRuntime.goalBags.length | 0);
for (const bag of stageRuntime.goalBags) {
h = hashU32(h, bag.state | 0);
h = hashF32(h, bag.openness ?? 0);
h = hashVec3(h, bag.localPos);
h = hashVec3(h, bag.localVel);
h = hashS16(h, bag.rotX ?? 0);
h = hashS16(h, bag.rotY ?? 0);
h = hashS16(h, bag.rotZ ?? 0);
}
}
if (stageRuntime.bananas) {
h = hashU32(h, stageRuntime.bananas.length | 0);
for (const banana of stageRuntime.bananas) {
h = hashU32(h, banana.state | 0);
h = hashVec3(h, banana.localPos);
h = hashVec3(h, banana.vel ?? { x: 0, y: 0, z: 0 });
h = hashS16(h, banana.rotX ?? 0);
h = hashS16(h, banana.rotY ?? 0);
h = hashS16(h, banana.rotZ ?? 0);
h = hashF32(h, banana.scale ?? 0);
}
}
if (includeVisual) {
if (stageRuntime.confetti) {
h = hashU32(h, stageRuntime.confetti.length | 0);
for (const frag of stageRuntime.confetti) {
h = hashVec3(h, frag.pos);
h = hashVec3(h, frag.vel);
h = hashS16(h, frag.rotX ?? 0);
h = hashS16(h, frag.rotY ?? 0);
h = hashS16(h, frag.rotZ ?? 0);
}
}
if (stageRuntime.effects) {
h = hashU32(h, stageRuntime.effects.length | 0);
for (const fx of stageRuntime.effects) {
h = hashVec3(h, fx.pos);
h = hashVec3(h, fx.vel);
h = hashS16(h, fx.rotX ?? 0);
h = hashS16(h, fx.rotY ?? 0);
h = hashS16(h, fx.rotZ ?? 0);
}
}
}
return h >>> 0;
}

11
src/sim_runner.ts Normal file
View File

@@ -0,0 +1,11 @@
import { hashSimState } from './sim_hash.js';
export function runDeterminismTest(game, tickCount, inputFeed = null, { includeVisual = false } = {}) {
game.setInputFeed(inputFeed);
const hashes = [];
for (let i = 0; i < tickCount; i += 1) {
game.update(game.fixedStep);
hashes.push(hashSimState(game.ball, game.world, game.stageRuntime, { includeVisual }));
}
return hashes;
}

View File

@@ -20,6 +20,7 @@ import {
import {
MatrixStack,
atan2S16,
atan2S16Safe,
floor,
sinS16,
sqrt,
@@ -33,6 +34,7 @@ import { raycastStageDown } from './collision.js';
import { updateBallEffects } from './effects.js';
import { parseGma } from './gma.js';
import { parseAVTpl } from './tpl.js';
import { DeterministicRng } from './rng.js';
const TEXT_DECODER = new TextDecoder('utf-8');
const STAGE_START_POS_SIZE = 0x14;
@@ -65,7 +67,7 @@ const ANIM_LOOP = 0;
const ANIM_PLAY_ONCE = 1;
const ANIM_SEESAW = 2;
const SMB2_STAGE_LOADIN_FRAMES = 0x168;
const JAMABAR_BOUND_RADIUS = Math.sqrt((1.75 * 1.75) + (0.5 * 0.5) + (0.5 * 0.5));
const JAMABAR_BOUND_RADIUS = sqrt((1.75 * 1.75) + (0.5 * 0.5) + (0.5 * 0.5));
const GOAL_BAG_LOCAL_START = { x: 0, y: -1, z: 0.1 };
const CONFETTI_GRAVITY_SCALE = 0.004;
const CONFETTI_VEL_DAMP = 0.95;
@@ -118,12 +120,12 @@ const SWITCH_MODEL_SUFFIXES = [
'BUTTON_FR',
];
function randS16() {
return Math.trunc(Math.random() * 0x8000);
function randS16(rng) {
return rng.nextS16();
}
function randFloat() {
return Math.random();
function randFloat(rng) {
return rng.nextFloat();
}
function formatStageId(stageId) {
@@ -1179,7 +1181,7 @@ class StageParserSmb2 extends StageParser {
}
export class StageRuntime {
constructor(stage) {
constructor(stage, seed = stage.stageId ?? 0) {
this.stage = stage;
this.format = stage.format ?? 'smb1';
this.timerFrames = 0;
@@ -1208,6 +1210,8 @@ export class StageRuntime {
this.matrixStack = new MatrixStack();
this.goalHoldOpen = false;
this.switchesEnabled = true;
this.simRng = new DeterministicRng(seed);
this.visualRng = new DeterministicRng((seed ^ 0x9e3779b9) >>> 0);
this.initAnimGroups();
this.initObjects();
}
@@ -1769,10 +1773,10 @@ export class StageRuntime {
updateGoalTape(tape, animGroups, gravity, stack);
}
for (const bag of this.goalBags) {
updateGoalBag(bag, animGroups, gravity, this.goalHoldOpen, stack);
updateGoalBag(bag, animGroups, gravity, this.goalHoldOpen, stack, this.simRng);
}
updateConfetti(this, gravity);
updateBallEffects(this.effects, gravity, this);
updateBallEffects(this.effects, gravity, this, this.visualRng);
for (let i = 0; i < animGroups.length; i += 1) {
const bumperStates = this.bumpers[i];
for (const bumper of bumperStates) {
@@ -2025,7 +2029,7 @@ export class StageRuntime {
bag.openFrame = this.timerFrames;
}
updateGoalBagTransform(bag, this.matrixStack);
spawnGoalBagConfetti(this, bag, ball);
spawnGoalBagConfetti(this, bag, ball, this.visualRng);
}
breakGoalTape(goalId, ball) {
@@ -2182,7 +2186,7 @@ export class StageRuntime {
y: (max.y - min.y) * 0.5,
z: (max.z - min.z) * 0.5,
};
this.boundSphere.radius = Math.sqrt((half.x * half.x) + (half.y * half.y) + (half.z * half.z));
this.boundSphere.radius = sqrt((half.x * half.x) + (half.y * half.y) + (half.z * half.z));
if (this.boundSphere.radius < FLY_IN_MIN_RADIUS) {
this.boundSphere.radius = FLY_IN_MIN_RADIUS;
}
@@ -2357,7 +2361,7 @@ function updateGoalBagTransform(bag, stack) {
stack.tfPoint(bag.modelOrigin, bag.position);
}
function updateGoalBag(bag, animGroups, gravity, holdOpen, stack) {
function updateGoalBag(bag, animGroups, gravity, holdOpen, stack, rng) {
bag.prevOpenness = bag.openness;
bag.prevRotX = bag.rotX;
bag.prevRotY = bag.rotY;
@@ -2369,7 +2373,7 @@ function updateGoalBag(bag, animGroups, gravity, holdOpen, stack) {
case 3:
bag.state = 4;
bag.counter = -1;
bag.unk8 = 0.05 + 0.1 * Math.random();
bag.unk8 = 0.05 + 0.1 * randFloat(rng);
// fall through
case 4:
if (bag.counter > 0) {
@@ -2397,7 +2401,7 @@ function updateGoalBag(bag, animGroups, gravity, holdOpen, stack) {
case 6:
bag.state = 7;
bag.counter = 60;
bag.unk8 = 0.05 + 0.1 * Math.random();
bag.unk8 = 0.05 + 0.1 * randFloat(rng);
// fall through
case 7:
bag.counter -= 1;
@@ -2519,8 +2523,18 @@ function updateGoalBag(bag, animGroups, gravity, holdOpen, stack) {
stack.fromRotateY(-bag.rotY);
stack.rotateX(0);
stack.tfVec(rotVec, rotVec);
bag.rotX = atan2S16(rotVec.z, rotVec.y) - 0x8000;
bag.rotZ = atan2S16(rotVec.x, sqrt(sumSq2(rotVec.z, rotVec.y)));
const debug = (globalThis as any).__DETERMINISM_DEBUG__;
if (debug) {
debug.source = 'goalBagRotX';
}
bag.rotX = atan2S16Safe(rotVec.z, rotVec.y) - 0x8000;
if (debug) {
debug.source = 'goalBagRotZ';
}
bag.rotZ = atan2S16Safe(rotVec.x, sqrt(sumSq2(rotVec.z, rotVec.y)));
if (debug) {
debug.source = null;
}
updateGoalBagTransform(bag, stack);
}
@@ -2819,7 +2833,7 @@ function createConfettiParticle(modelIndex, pos, vel, rotX, rotY, rotZ, scale, l
};
}
function spawnGoalBagConfetti(stageRuntime, bag, ball) {
function spawnGoalBagConfetti(stageRuntime, bag, ball, rng) {
const animGroup = stageRuntime.animGroups[bag.animGroupId];
const stack = stageRuntime.matrixStack;
const vel = { x: ball.vel.x, y: ball.vel.y, z: ball.vel.z };
@@ -2859,22 +2873,22 @@ function spawnGoalBagConfetti(stageRuntime, bag, ball) {
const spawnPos = { x: 0, y: 0, z: 0 };
const spawnCount = 160;
for (let i = 0; i < spawnCount; i += 1) {
localPos.z = 0.5 * (baseRadius * (1.0 + randFloat()));
stack.rotateY(randS16());
stack.rotateX(randS16());
localPos.z = 0.5 * (baseRadius * (1.0 + randFloat(rng)));
stack.rotateY(randS16(rng));
stack.rotateX(randS16(rng));
localPos.x = 0;
localPos.y = 0;
stack.tfPoint(localPos, spawnPos);
const rotX = randS16();
const rotY = randS16();
const rotZ = randS16();
const scale = 0.5 + 0.5 * randFloat();
const life = Math.trunc(CONFETTI_LIFE_BASE + CONFETTI_LIFE_RANGE * randFloat());
const rotX = randS16(rng);
const rotY = randS16(rng);
const rotZ = randS16(rng);
const scale = 0.5 + 0.5 * randFloat(rng);
const life = Math.trunc(CONFETTI_LIFE_BASE + CONFETTI_LIFE_RANGE * randFloat(rng));
const modelIndex = (modelIndexBase + (i % CONFETTI_MODEL_COUNT)) % CONFETTI_MODEL_COUNT;
const frag = createConfettiParticle(modelIndex, spawnPos, vel, rotX, rotY, rotZ, scale, life);
frag.groundBias = 0.0001 * randFloat();
frag.groundBias = 0.0001 * randFloat(rng);
stageRuntime.confetti.push(frag);
}
}
@@ -3089,7 +3103,7 @@ function computeGmaBoundSphere(gma, modelNames = null) {
y: (max.y - min.y) * 0.5,
z: (max.z - min.z) * 0.5,
};
const radius = Math.sqrt((half.x * half.x) + (half.y * half.y) + (half.z * half.z));
const radius = sqrt((half.x * half.x) + (half.y * half.y) + (half.z * half.z));
return { pos, radius: Math.max(radius, FLY_IN_MIN_RADIUS) };
}