Files
WebMonkeyBall/src/physics.ts
2026-02-01 11:58:49 -05:00

1023 lines
30 KiB
TypeScript

import { mat3, mat4, vec3 } from 'gl-matrix';
import { BALL_FLAGS, BALL_STATES, COLI_FLAGS } from './constants.js';
import {
MatrixStack,
atan2S16,
quatFromDirs,
quatFromAxisAngle,
quatMul,
quatNormalize,
sqrt,
sumSq2,
toS16,
vecCross,
vecDotNormalized,
vecLen,
vecNormalizeLen,
vecSetLen,
} from './math.js';
import {
applySeesawCollision,
collideBallWithStage,
collideBallWithStageObjects,
collideBallWithBonusWave,
raycastStageDown,
testLineIntersectsRect,
tfPhysballToAnimGroupSpace,
} from './collision.js';
import {
spawnCollisionStars,
spawnMovementSparks,
spawnPostGoalSparkle,
} from './effects.js';
const RAD_TO_S16 = 0x8000 / Math.PI;
const FLT_EPSILON = 1.1920929e-7;
export const GOAL_FLOAT_FRAMES = 90;
const WORMHOLE_TRIGGER_HEIGHT = 4;
const WORMHOLE_TRIGGER_WIDTH = 4;
const WORMHOLE_TRIGGER_OFFSET_Y = 2;
const WORMHOLE_OFFSET_Y = 2.2;
const WORMHOLE_COOLDOWN_FRAMES = 30;
const stack = new MatrixStack();
const rotYTmp = { value: 0 };
const rotXTmp = { value: 0 };
const rotZTmp = { value: 0 };
const adjustPlanePoint = { x: 0, y: 0, z: 0 };
const adjustPlaneNormal = { x: 0, y: 0, z: 0 };
const adjustSp38 = { x: 0, y: 0, z: 0 };
const adjustSp44 = { x: 0, y: 0, z: 0 };
const adjustSp2C = { x: 0, y: 0, z: 0 };
const adjustSp20 = { x: 0, y: 0, z: 0 };
const adjustSp14 = { x: 0, y: 0, z: 0 };
const movementTmp = { x: 0, y: 0, z: 0 };
const planeNormalWorldTmp = { x: 0, y: 0, z: 0 };
const contactOffsetTmp = { x: 0, y: 0, z: 0 };
const movementLocalTmp = { x: 0, y: 0, z: 0 };
const axisTmp = { x: 0, y: 0, z: 0 };
const accelTmp = { x: 0, y: 0, z: 0 };
const wormholeUnitY = { x: 0, y: 1, z: 0 };
const wormholeForwardPoint = { x: 0, y: 0, z: -1 };
const wormholeBackwardPoint = { x: 0, y: 0, z: 1 };
const wormholeEye = { x: 0, y: 0, z: 0 };
const wormholeUp = { x: 0, y: 0, z: 0 };
const wormholeTarget = { x: 0, y: 0, z: 0 };
const wormholeVecA = vec3.create();
const wormholeVecB = vec3.create();
const wormholeVecC = vec3.create();
const wormholeMatA = mat4.create();
const wormholeMatB = mat4.create();
const wormholeMatC = mat4.create();
const wormholeMat3 = mat3.create();
const seesawBall = {
pos: { x: 0, y: 0, z: 0 },
prevPos: { x: 0, y: 0, z: 0 },
vel: { x: 0, y: 0, z: 0 },
animGroupId: 0,
};
export function createBallState() {
return {
playerId: 0,
pos: { x: 0, y: 0, z: 0 },
prevPos: { x: 0, y: 0, z: 0 },
vel: { x: 0, y: 0, z: 0 },
rotX: 0,
rotY: 0,
rotZ: 0,
flags: 0,
state: BALL_STATES.READY,
startPos: { x: 0, y: 0, z: 0 },
startRotY: 0,
goalTimer: 0,
currRadius: 0.5,
accel: 0.009799992,
restitution: 0.5,
unk60: 0,
unk62: 0,
unk64: 0,
unk80: 0,
unk92: 0,
apeYaw: 0,
unkA8: { x: 0, y: 0, z: 0, w: 1 },
unkB8: { x: 0, y: 0, z: 0 },
unkC4: 0,
unkF8: 0,
apeQuat: { x: 0, y: 0, z: 0, w: 1 },
apeFlags: 0,
transform: new Float32Array(12),
prevTransform: new Float32Array(12),
unk114: { x: 0, y: 1, z: 0 },
deltaQuat: { x: 0, y: 0, z: 0, w: 1 },
orientation: { x: 0, y: 0, z: 0, w: 1 },
prevOrientation: { x: 0, y: 0, z: 0, w: 1 },
speed: 0,
bananas: 0,
audio: {
lastImpactFrame: -9999,
rollingVol: 0,
rollingPitch: 0,
bumperHit: false,
lastColiSpeed: 0,
lastColiFlags: 0,
},
wormholeCooldown: 0,
wormholeTransform: null,
physBall: createPhysicsBall(),
};
}
function createPhysicsBall() {
return {
flags: 0,
pos: { x: 0, y: 0, z: 0 },
prevPos: { x: 0, y: 0, z: 0 },
vel: { x: 0, y: 0, z: 0 },
radius: 0.5,
gravityAccel: 0.009799992,
restitution: 0.5,
hardestColiSpeed: 0,
hardestColiPlane: {
point: { x: 0, y: 0, z: 0 },
normal: { x: 0, y: 1, z: 0 },
},
hardestColiAnimGroupId: 0,
friction: 0.01,
frictionMode: 'smb1',
animGroupId: 0,
};
}
export function resetBall(ball, startPos, startRotY = ball.startRotY) {
ball.pos.x = startPos.x;
ball.pos.y = startPos.y;
ball.pos.z = startPos.z;
ball.prevPos.x = startPos.x;
ball.prevPos.y = startPos.y;
ball.prevPos.z = startPos.z;
ball.vel.x = 0;
ball.vel.y = 0;
ball.vel.z = 0;
ball.rotX = 0;
ball.rotY = 0;
ball.rotZ = 0;
ball.flags = 0;
ball.state = BALL_STATES.PLAY;
ball.goalTimer = 0;
ball.startRotY = startRotY;
ball.unk80 = 0;
ball.unk92 = 0;
ball.apeYaw = toS16(startRotY - 0x4000);
ball.unkA8.x = 0;
ball.unkA8.y = 0;
ball.unkA8.z = 0;
ball.unkA8.w = 1;
ball.unkB8.x = 0;
ball.unkB8.y = 0;
ball.unkB8.z = 0;
ball.unkC4 = 0;
ball.unkF8 = 0;
setApeQuatFromYaw(ball, startRotY - 0x4000);
ball.apeFlags = 0;
ball.unk114.x = 0;
ball.unk114.y = 1;
ball.unk114.z = 0;
ball.deltaQuat.x = 0;
ball.deltaQuat.y = 0;
ball.deltaQuat.z = 0;
ball.deltaQuat.w = 1;
ball.orientation.x = 0;
ball.orientation.y = 0;
ball.orientation.z = 0;
ball.orientation.w = 1;
ball.prevOrientation.x = ball.orientation.x;
ball.prevOrientation.y = ball.orientation.y;
ball.prevOrientation.z = ball.orientation.z;
ball.prevOrientation.w = ball.orientation.w;
ball.wormholeCooldown = 0;
ball.wormholeTransform = null;
updateBallTransform(ball);
ball.prevTransform.set(ball.transform);
}
export function initBallForStage(ball, startPos, startRotY) {
ball.startPos.x = startPos.x;
ball.startPos.y = startPos.y;
ball.startPos.z = startPos.z;
ball.startRotY = startRotY;
ball.flags = BALL_FLAGS.INVISIBLE;
ball.state = BALL_STATES.READY;
ball.goalTimer = 0;
ball.vel.x = 0;
ball.vel.y = 0;
ball.vel.z = 0;
ball.rotX = 0;
ball.rotY = 0;
ball.rotZ = 0;
const dropFrames = 24;
const dropOffset = ((ball.accel * dropFrames) * dropFrames) * 0.5;
ball.pos.x = startPos.x;
ball.pos.y = startPos.y + dropOffset;
ball.pos.z = startPos.z;
ball.prevPos.x = ball.pos.x;
ball.prevPos.y = ball.pos.y;
ball.prevPos.z = ball.pos.z;
ball.unk80 = 0;
ball.unk92 = 0;
ball.apeYaw = toS16(startRotY - 0x4000);
ball.unkA8.x = 0;
ball.unkA8.y = 0;
ball.unkA8.z = 0;
ball.unkA8.w = 1;
ball.unkB8.x = 0;
ball.unkB8.y = 0;
ball.unkB8.z = 0;
ball.unkC4 = 0;
ball.unkF8 = 0;
setApeQuatFromYaw(ball, startRotY - 0x4000);
ball.apeFlags = 0;
ball.unk114.x = 0;
ball.unk114.y = 1;
ball.unk114.z = 0;
ball.deltaQuat.x = 0;
ball.deltaQuat.y = 0;
ball.deltaQuat.z = 0;
ball.deltaQuat.w = 1;
ball.orientation.x = 0;
ball.orientation.y = 0;
ball.orientation.z = 0;
ball.orientation.w = 1;
ball.prevOrientation.x = ball.orientation.x;
ball.prevOrientation.y = ball.orientation.y;
ball.prevOrientation.z = ball.orientation.z;
ball.prevOrientation.w = ball.orientation.w;
ball.wormholeCooldown = 0;
ball.wormholeTransform = null;
updateBallTransform(ball);
ball.prevTransform.set(ball.transform);
}
export function startBallDrop(ball, frames = 24) {
const f4 = frames;
if (f4 <= 0) {
return;
}
ball.prevPos.x = ball.pos.x;
ball.prevPos.y = ball.pos.y;
ball.prevPos.z = ball.pos.z;
const f2 = (ball.startPos.y - ball.pos.y) / f4;
ball.vel.x = 0;
ball.vel.y = (ball.accel * f4) * 0.5 + f2;
ball.vel.z = 0;
ball.rotX = 0x2000;
ball.rotY = toS16(ball.startRotY - 0x4000);
ball.rotZ = 0;
stack.fromIdentity();
stack.rotateY(ball.rotY);
stack.rotateX(ball.rotX);
stack.rotateZ(ball.rotZ);
stack.toQuat(ball.orientation);
ball.flags &= ~BALL_FLAGS.INVISIBLE;
ball.flags |= BALL_FLAGS.FLAG_14;
ball.state = BALL_STATES.PLAY;
ball.unk80 = 0;
ball.unk92 = 0;
ball.apeYaw = toS16(ball.startRotY - 0x4000);
setApeQuatFromYaw(ball, ball.startRotY - 0x4000);
ball.apeFlags = 0;
ball.wormholeCooldown = 0;
ball.wormholeTransform = null;
updateBallTransform(ball);
ball.prevTransform.set(ball.transform);
}
export function resolveBallBallCollision(ballA, ballB) {
const dx = ballB.pos.x - ballA.pos.x;
const dy = ballB.pos.y - ballA.pos.y;
const dz = ballB.pos.z - ballA.pos.z;
const minDist = (ballA.currRadius ?? 0.5) + (ballB.currRadius ?? 0.5);
const distSq = dx * dx + dy * dy + dz * dz;
if (distSq <= FLT_EPSILON || distSq >= minDist * minDist) {
return;
}
const dist = sqrt(distSq);
const nx = dx / dist;
const ny = dy / dist;
const nz = dz / dist;
const overlap = minDist - dist;
const correction = overlap * 0.5;
ballA.pos.x -= nx * correction;
ballA.pos.y -= ny * correction;
ballA.pos.z -= nz * correction;
ballB.pos.x += nx * correction;
ballB.pos.y += ny * correction;
ballB.pos.z += nz * correction;
const rvx = ballB.vel.x - ballA.vel.x;
const rvy = ballB.vel.y - ballA.vel.y;
const rvz = ballB.vel.z - ballA.vel.z;
const relVel = rvx * nx + rvy * ny + rvz * nz;
if (relVel >= 0) {
return;
}
const restitution = (ballA.restitution + ballB.restitution) * 0.5;
const impulse = -((1 + restitution) * relVel) * 0.5;
ballA.vel.x -= impulse * nx;
ballA.vel.y -= impulse * ny;
ballA.vel.z -= impulse * nz;
ballB.vel.x += impulse * nx;
ballB.vel.y += impulse * ny;
ballB.vel.z += impulse * nz;
}
function updateBallCameraSteerYaw(ball) {
const speed = sqrt(sumSq2(ball.vel.x, ball.vel.z));
let velYaw = ball.apeYaw;
if (speed > FLT_EPSILON) {
velYaw = atan2S16(ball.vel.x, ball.vel.z) - 0x8000;
}
let blend = 0;
if (speed >= 0.37037037037037035) {
blend = 1;
} else if (speed >= 0.23148148148148145) {
blend = (speed - 0.23148148148148145) / 0.1388888888888889;
}
const delta = toS16(velYaw - ball.apeYaw);
ball.unk92 = toS16(ball.apeYaw + delta * blend);
}
function updateBallTransform(ball) {
stack.fromQuat(ball.orientation);
stack.setTranslate(ball.pos);
stack.toMtx(ball.transform);
}
function setApeQuatFromYaw(ball, yaw) {
stack.fromIdentity();
stack.rotateY(toS16(yaw));
stack.toQuat(ball.apeQuat);
}
function updateBallApeBasis(ball) {
const sp64 = { x: 0, y: 0, z: 0 };
const sp58 = { x: 0, y: 0, z: 0 };
const sp4C = { x: 0, y: 0, z: 0 };
const sp40 = { x: 0, y: 0, z: 0 };
const sp34 = { x: 0, y: 0, z: 0 };
const sp28 = { x: 0, y: 0, z: 0 };
const sp1C = { x: 0, y: 0, z: 0 };
stack.fromMtx(ball.transform);
ball.unkC4 = vecLen(ball.unkB8);
stack.tfVec({ x: 0, y: 1, z: 0 }, sp58);
stack.tfVec({ x: 1, y: 0, z: 0 }, sp40);
stack.tfVec({ x: 0, y: 0, z: 1 }, sp28);
sp1C.x = 0;
sp1C.y = -ball.currRadius;
sp1C.z = 0;
stack.rigidInvTfVec(sp1C, sp1C);
stack.fromMtx(ball.prevTransform);
stack.tfVec(sp1C, ball.unkB8);
ball.unkB8.y += ball.currRadius;
stack.tfVec({ x: 0, y: 1, z: 0 }, sp64);
stack.tfVec({ x: 1, y: 0, z: 0 }, sp4C);
stack.tfVec({ x: 0, y: 0, z: 1 }, sp34);
let f31 = vecDotNormalized(sp64, sp58);
let f1 = vecDotNormalized(sp4C, sp40);
if (f31 > f1) {
f31 = f1;
sp64.x = sp4C.x;
sp64.y = sp4C.y;
sp64.z = sp4C.z;
sp58.x = sp40.x;
sp58.y = sp40.y;
sp58.z = sp40.z;
}
f1 = vecDotNormalized(sp34, sp28);
if (f31 > f1) {
f31 = f1;
sp64.x = sp34.x;
sp64.y = sp34.y;
sp64.z = sp34.z;
sp58.x = sp28.x;
sp58.y = sp28.y;
sp58.z = sp28.z;
}
if (f31 > -0.9998 && f31 < 0.9998) {
quatFromDirs(ball.unkA8, sp64, sp58);
} else {
ball.unkA8.x = 0;
ball.unkA8.y = 0;
ball.unkA8.z = 0;
ball.unkA8.w = 1;
}
}
function updateApeBaseOrientation(ball) {
const tmpQuat = { x: ball.unkA8.x, y: ball.unkA8.y, z: ball.unkA8.z, w: ball.unkA8.w };
tmpQuat.w /= 0.65;
tmpQuat.x *= 0.65;
tmpQuat.y *= 0.65;
tmpQuat.z *= 0.65;
quatNormalize(tmpQuat);
stack.fromQuat(tmpQuat);
stack.toMtx(stack.mtxB);
stack.fromQuat(ball.apeQuat);
stack.normalizeBasis();
stack.multLeft(stack.mtxB);
stack.normalizeBasis();
if (!(ball.flags & BALL_FLAGS.FLAG_05)) {
const f31 = vecLen(ball.vel);
const f1 = vecLen(ball.unkB8);
if (f31 > 0.032407406717538834) {
if (f1 * 100.0 < ball.unkC4) {
ball.flags |= BALL_FLAGS.FLAG_05;
} else if (f1 * 3.0 < ball.unkC4 && f31 * 1.5 < ball.unkF8) {
ball.flags |= BALL_FLAGS.FLAG_05;
}
}
}
if (tmpQuat.w < 0.9941) {
return false;
}
stack.toMtx(stack.mtxB);
const sp48 = { x: 0, y: 1, z: 0 };
stack.rigidInvTfVec(sp48, sp48);
const sp3C = { x: 0, y: 1, z: 0 };
const f1 = 1.0 - vecDotNormalized(sp48, sp3C);
const quat = { x: 0, y: 0, z: 0, w: 1 };
if (f1 > 0.01) {
const sp30 = { x: 0, y: 1, z: 0 };
if (f1 > 1.999) {
sp48.x = 1;
sp48.y = 0;
sp48.z = 0;
} else {
vecCross(sp30, sp48, sp48);
}
quatFromAxisAngle(sp48, 0x38e, quat);
} else {
quatFromDirs(quat, { x: 0, y: 1, z: 0 }, sp48);
}
quatNormalize(quat);
stack.fromQuat(quat);
stack.normalizeBasis();
stack.multLeft(stack.mtxB);
return true;
}
function updateApeFromVelocity(ball) {
const sp4C = { x: ball.vel.x, y: 0, z: ball.vel.z };
if (vecLen(sp4C) < 0.00027777777) {
return 0;
}
vecNormalizeLen(sp4C);
stack.rigidInvTfVec(sp4C, sp4C);
const sp40 = { x: 0, y: 0, z: 0 };
let var1 = sp4C.x;
const quat = { x: 0, y: 0, z: 0, w: 1 };
if (var1 > -0.992) {
const sp24 = { x: -1, y: 0, z: 0 };
var1 = 1.0 - var1;
if (var1 > 9.99999993922529e-09) {
vecCross(sp24, sp4C, sp40);
} else {
sp40.x = 0;
sp40.y = 1;
sp40.z = 0;
}
quatFromAxisAngle(sp40, 0x2d8, quat);
} else {
quatFromDirs(quat, { x: -1, y: 0, z: 0 }, sp4C);
}
stack.push();
stack.fromQuat(quat);
stack.normalizeBasis();
stack.toMtx(stack.mtxB);
stack.pop();
stack.multRight(stack.mtxB);
return vecLen(ball.unkB8) * 1.5;
}
function updateApeSpinCompensation() {
const tmp = new Float32Array(12);
stack.toMtx(tmp);
const sp24 = { x: 0, y: 0, z: 0 };
stack.tfVec({ x: -1, y: 0, z: 0 }, sp24);
const quat = { x: 0, y: 0, z: 0, w: 1 };
if (sp24.y < 0.99) {
const sp18 = { x: 0, y: 1, z: 0 };
vecCross(sp24, sp18, sp24);
quatFromAxisAngle(sp24, 0x38e, quat);
} else {
quatFromDirs(quat, sp24, { x: 0, y: 1, z: 0 });
}
stack.fromQuat(quat);
stack.normalizeBasis();
stack.multRight(tmp);
}
function updateApeOrientation(ball, physBall, stageRuntime) {
const APE_FLAG_ON_GROUND = 1 << 0;
const APE_FLAG_IN_AIR = 1 << 1;
ball.apeFlags &= ~0x3;
const rayHit = raycastStageDown(ball.pos, stageRuntime);
if (!rayHit && ball.vel.y < -0.16203702986240387) {
ball.apeFlags |= APE_FLAG_IN_AIR;
} else if (vecLen(ball.unkB8) < 0.00027777777) {
ball.apeFlags |= APE_FLAG_ON_GROUND;
}
let r27 = (ball.flags & BALL_FLAGS.GOAL) !== 0;
r27 = r27 || !(ball.apeFlags & 0x3);
updateApeBaseOrientation(ball);
if (r27) {
updateApeFromVelocity(ball);
} else {
stack.fromQuat(ball.apeQuat);
stack.normalizeBasis();
if (ball.apeFlags & APE_FLAG_IN_AIR) {
updateApeSpinCompensation();
}
}
stack.toQuat(ball.apeQuat);
const tmpDir = { x: -1, y: 0, z: 0 };
stack.fromQuat(ball.apeQuat);
stack.tfVec(tmpDir, tmpDir);
ball.apeYaw = toS16(atan2S16(tmpDir.x, tmpDir.z) - 0x8000);
}
export function startGoal(ball) {
ball.state = BALL_STATES.GOAL_MAIN;
ball.flags |= BALL_FLAGS.GOAL | BALL_FLAGS.FLAG_08 | BALL_FLAGS.FLAG_10;
if (ball.flags & (BALL_FLAGS.GOAL | BALL_FLAGS.FLAG_13)) {
ball.flags |= BALL_FLAGS.FLAG_06;
}
ball.deltaQuat.x = 0;
ball.deltaQuat.y = 0;
ball.deltaQuat.z = 0;
ball.deltaQuat.w = 1;
ball.orientation.x = 0;
ball.orientation.y = 0;
ball.orientation.z = 0;
ball.orientation.w = 1;
ball.goalTimer = 0;
}
function initPhysBallFromBall(ball, physBall, stageFormat = 'smb1') {
physBall.flags = 0;
physBall.pos.x = ball.pos.x;
physBall.pos.y = ball.pos.y;
physBall.pos.z = ball.pos.z;
physBall.prevPos.x = ball.prevPos.x;
physBall.prevPos.y = ball.prevPos.y;
physBall.prevPos.z = ball.prevPos.z;
physBall.vel.x = ball.vel.x;
physBall.vel.y = ball.vel.y;
physBall.vel.z = ball.vel.z;
physBall.radius = ball.currRadius;
physBall.gravityAccel = ball.accel;
physBall.restitution = ball.restitution;
physBall.hardestColiSpeed = 0;
physBall.animGroupId = 0;
physBall.hardestColiAnimGroupId = 0;
physBall.friction = 0.01;
physBall.frictionMode = stageFormat === 'smb2' ? 'smb2' : 'smb1';
}
function syncBallFromPhysBall(ball, physBall) {
if (physBall.flags & COLI_FLAGS.OCCURRED) {
ball.flags |= BALL_FLAGS.FLAG_00;
}
ball.pos.x = physBall.pos.x;
ball.pos.y = physBall.pos.y;
ball.pos.z = physBall.pos.z;
ball.vel.x = physBall.vel.x;
ball.vel.y = physBall.vel.y;
ball.vel.z = physBall.vel.z;
}
function adjustMovementForAnimGroup(ball, physBall, movement, animGroups) {
const group = animGroups[physBall.hardestColiAnimGroupId];
adjustPlanePoint.x = physBall.hardestColiPlane.point.x;
adjustPlanePoint.y = physBall.hardestColiPlane.point.y;
adjustPlanePoint.z = physBall.hardestColiPlane.point.z;
adjustPlaneNormal.x = physBall.hardestColiPlane.normal.x;
adjustPlaneNormal.y = physBall.hardestColiPlane.normal.y;
adjustPlaneNormal.z = physBall.hardestColiPlane.normal.z;
stack.fromMtx(group.transform);
stack.tfPoint(adjustPlanePoint, adjustSp38);
stack.tfVec(adjustPlaneNormal, adjustSp44);
adjustSp14.x = adjustSp38.x - ball.pos.x;
adjustSp14.y = adjustSp38.y - ball.pos.y;
adjustSp14.z = adjustSp38.z - ball.pos.z;
adjustSp2C.x = ball.pos.x + adjustSp14.x * adjustSp44.x;
adjustSp2C.y = ball.pos.y + adjustSp14.y * adjustSp44.y;
adjustSp2C.z = ball.pos.z + adjustSp14.z * adjustSp44.z;
stack.rigidInvTfPoint(adjustSp2C, adjustSp14);
stack.fromMtx(group.prevTransform);
stack.tfPoint(adjustSp14, adjustSp20);
adjustSp14.x = adjustSp2C.x - adjustSp20.x;
adjustSp14.y = adjustSp2C.y - adjustSp20.y;
adjustSp14.z = adjustSp2C.z - adjustSp20.z;
movement.x -= adjustSp14.x;
movement.y -= adjustSp14.y;
movement.z -= adjustSp14.z;
}
function updateBallOrientation(ball) {
quatMul(ball.orientation, ball.orientation, ball.deltaQuat);
quatNormalize(ball.orientation);
stack.fromQuat(ball.orientation);
stack.toEulerYXZ(rotYTmp, rotXTmp, rotZTmp);
ball.rotY = rotYTmp.value;
ball.rotX = rotXTmp.value;
ball.rotZ = rotZTmp.value;
}
function handleBallRotationalKinematics(ball, physBall, animGroups, stageRuntime, goalMode) {
let doRot = 0;
if (goalMode) {
if (physBall.hardestColiSpeed < 0) {
doRot = 1;
}
} else if (physBall.flags & COLI_FLAGS.OCCURRED) {
doRot = 1;
}
if (!doRot) {
if (ball.flags & BALL_FLAGS.FLAG_10) {
ball.vel.x *= 0.95;
ball.vel.y *= 0.95;
ball.vel.z *= 0.95;
}
return;
}
movementTmp.x = ball.pos.x - ball.prevPos.x;
movementTmp.y = ball.pos.y - ball.prevPos.y;
movementTmp.z = ball.pos.z - ball.prevPos.z;
if (physBall.hardestColiAnimGroupId > 0) {
adjustMovementForAnimGroup(ball, physBall, movementTmp, animGroups);
}
const planeNormal = physBall.hardestColiPlane.normal;
stack.fromMtx(animGroups[physBall.hardestColiAnimGroupId].transform);
stack.tfVec(planeNormal, planeNormalWorldTmp);
contactOffsetTmp.x = -planeNormalWorldTmp.x * ball.currRadius;
contactOffsetTmp.y = -planeNormalWorldTmp.y * ball.currRadius;
contactOffsetTmp.z = -planeNormalWorldTmp.z * ball.currRadius;
stack.fromRotateY(ball.rotY);
stack.rotateX(ball.rotX);
stack.rotateZ(ball.rotZ);
stack.rigidInvTfVec(contactOffsetTmp, contactOffsetTmp);
movementLocalTmp.x = -movementTmp.x;
movementLocalTmp.y = -movementTmp.y;
movementLocalTmp.z = -movementTmp.z;
stack.rigidInvTfVec(movementLocalTmp, movementLocalTmp);
vecCross(contactOffsetTmp, movementLocalTmp, axisTmp);
let invRadiusSq = 0;
if (ball.currRadius > FLT_EPSILON) {
invRadiusSq = RAD_TO_S16 / (ball.currRadius * ball.currRadius);
}
ball.unk60 = axisTmp.x * invRadiusSq;
ball.unk62 = axisTmp.y * invRadiusSq;
ball.unk64 = axisTmp.z * invRadiusSq;
if (ball.currRadius > FLT_EPSILON) {
let len = vecLen(axisTmp);
if (len !== 0) {
len = vecNormalizeLen(axisTmp);
quatFromAxisAngle(
axisTmp,
(len * 2.0) / (ball.currRadius * ball.currRadius * Math.PI) * RAD_TO_S16,
ball.deltaQuat,
);
}
}
}
export function stepBall(ball, stageRuntime, world) {
const stage = stageRuntime.stage;
const animGroups = stageRuntime.animGroups;
if (ball.state === BALL_STATES.GOAL_MAIN) {
ball.goalTimer += 1;
if (ball.goalTimer >= GOAL_FLOAT_FRAMES && !(ball.flags & BALL_FLAGS.FLAG_09)) {
ball.flags &= ~(BALL_FLAGS.FLAG_08 | BALL_FLAGS.FLAG_10);
ball.flags |= BALL_FLAGS.FLAG_09;
}
}
if (ball.transform[0] === 0 && ball.transform[5] === 0 && ball.transform[10] === 0) {
updateBallTransform(ball);
}
ball.prevTransform.set(ball.transform);
ball.prevPos.x = ball.pos.x;
ball.prevPos.y = ball.pos.y;
ball.prevPos.z = ball.pos.z;
ball.prevOrientation.x = ball.orientation.x;
ball.prevOrientation.y = ball.orientation.y;
ball.prevOrientation.z = ball.orientation.z;
ball.prevOrientation.w = ball.orientation.w;
ball.speed = vecLen(ball.vel);
ball.unkF8 = ball.speed;
ball.flags &= ~BALL_FLAGS.FLAG_05;
accelTmp.x = 0;
accelTmp.y = -ball.accel;
accelTmp.z = 0;
if (ball.flags & BALL_FLAGS.FLAG_09) {
accelTmp.y = -accelTmp.y;
} else if (ball.flags & BALL_FLAGS.FLAG_08) {
accelTmp.y = 0;
}
stack.fromIdentity();
stack.rotateX(world.xrot);
stack.rotateZ(world.zrot);
stack.rigidInvTfVec(accelTmp, accelTmp);
ball.vel.x += accelTmp.x;
ball.vel.y += accelTmp.y;
ball.vel.z += accelTmp.z;
ball.pos.x += ball.vel.x;
ball.pos.y += ball.vel.y;
ball.pos.z += ball.vel.z;
const physBall = ball.physBall;
initPhysBallFromBall(ball, physBall, stage.format);
collideBallWithStage(physBall, stage, animGroups);
const stageColiFlags = physBall.flags;
const stageColiSpeed = physBall.hardestColiSpeed;
if (stage.format === 'smb2' && (physBall.flags & COLI_FLAGS.OCCURRED)) {
const seesawState = animGroups[physBall.hardestColiAnimGroupId]?.seesawState;
if (seesawState) {
seesawBall.pos.x = physBall.pos.x;
seesawBall.pos.y = physBall.pos.y;
seesawBall.pos.z = physBall.pos.z;
seesawBall.prevPos.x = physBall.prevPos.x;
seesawBall.prevPos.y = physBall.prevPos.y;
seesawBall.prevPos.z = physBall.prevPos.z;
seesawBall.vel.x = 0;
seesawBall.vel.y = 0;
seesawBall.vel.z = 0;
seesawBall.animGroupId = 0;
tfPhysballToAnimGroupSpace(seesawBall, physBall.hardestColiAnimGroupId, animGroups);
seesawBall.vel.x = physBall.hardestColiPlane.normal.x;
seesawBall.vel.y = physBall.hardestColiPlane.normal.y;
seesawBall.vel.z = physBall.hardestColiPlane.normal.z;
vecSetLen(seesawBall.vel, seesawBall.vel, physBall.hardestColiSpeed);
applySeesawCollision(seesawBall, seesawState);
}
}
if (stage.format !== 'smb2' && stage.stageId === 92 && physBall.animGroupId !== 0) {
tfPhysballToAnimGroupSpace(physBall, 0, animGroups);
}
collideBallWithBonusWave(physBall, stageRuntime);
syncBallFromPhysBall(ball, physBall);
if (physBall.flags & COLI_FLAGS.OCCURRED) {
if (physBall.hardestColiAnimGroupId === 0) {
ball.unk114.x = -physBall.hardestColiPlane.normal.x;
ball.unk114.y = -physBall.hardestColiPlane.normal.y;
ball.unk114.z = -physBall.hardestColiPlane.normal.z;
} else {
stack.fromMtx(animGroups[physBall.hardestColiAnimGroupId].transform);
stack.tfVec(physBall.hardestColiPlane.normal, ball.unk114);
ball.unk114.x = -ball.unk114.x;
ball.unk114.y = -ball.unk114.y;
ball.unk114.z = -ball.unk114.z;
}
}
if (stageRuntime.effects) {
const onGround = (physBall.flags & COLI_FLAGS.OCCURRED) !== 0 && physBall.hardestColiPlane.normal.y > 0;
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, rng);
}
if (ball.state === BALL_STATES.GOAL_MAIN && (ball.unk80 & 1)) {
spawnPostGoalSparkle(stageRuntime.effects, ball, rng);
}
}
const goalMode = ball.state === BALL_STATES.GOAL_MAIN;
handleBallRotationalKinematics(ball, physBall, animGroups, stageRuntime, goalMode);
updateBallOrientation(ball);
updateBallTransform(ball);
updateBallApeBasis(ball);
updateApeOrientation(ball, physBall, stageRuntime);
updateBallCameraSteerYaw(ball);
initPhysBallFromBall(ball, physBall, stage.format);
collideBallWithStageObjects(physBall, stageRuntime);
syncBallFromPhysBall(ball, physBall);
if (ball.audio) {
ball.audio.lastColiFlags = stageColiFlags | physBall.flags;
ball.audio.lastColiSpeed = Math.min(stageColiSpeed, physBall.hardestColiSpeed);
}
if (stage?.format === 'smb2') {
if (ball.wormholeCooldown > 0) {
ball.wormholeCooldown -= 1;
} else {
const hit = checkBallEnteredWormhole(ball, stageRuntime);
if (hit && hit.wormhole.dest) {
teleportBallToWormhole(ball, stageRuntime, hit.wormhole, hit.wormhole.dest);
ball.wormholeCooldown = WORMHOLE_COOLDOWN_FRAMES;
}
}
}
ball.unk80 += 1;
}
function buildWormholeView(stageRuntime, wormhole, forwardPoint, outMat4) {
const animGroupIndex = wormhole.animGroupIndex ?? 0;
if (animGroupIndex > 0 && stageRuntime.animGroups[animGroupIndex]) {
stack.fromMtx(stageRuntime.animGroups[animGroupIndex].transform);
} else {
stack.fromIdentity();
}
stack.translate(wormhole.pos);
stack.rotateZ(wormhole.rot.z);
stack.rotateY(wormhole.rot.y);
stack.rotateX(wormhole.rot.x);
stack.translateXYZ(0, WORMHOLE_OFFSET_Y, 0);
stack.getTranslateAlt(wormholeEye);
stack.tfVec(wormholeUnitY, wormholeUp);
stack.tfPoint(forwardPoint, wormholeTarget);
vec3.set(wormholeVecA, wormholeEye.x, wormholeEye.y, wormholeEye.z);
vec3.set(wormholeVecB, wormholeTarget.x, wormholeTarget.y, wormholeTarget.z);
vec3.set(wormholeVecC, wormholeUp.x, wormholeUp.y, wormholeUp.z);
mat4.lookAt(outMat4, wormholeVecA, wormholeVecB, wormholeVecC);
}
function computeWormholeTransform(stageRuntime, srcWormhole, destWormhole, outMat4) {
buildWormholeView(stageRuntime, srcWormhole, wormholeForwardPoint, wormholeMatA);
buildWormholeView(stageRuntime, destWormhole, wormholeBackwardPoint, wormholeMatB);
if (!mat4.invert(wormholeMatC, wormholeMatB)) {
return false;
}
mat4.multiply(outMat4, wormholeMatC, wormholeMatA);
return true;
}
function teleportBallToWormhole(ball, stageRuntime, srcWormhole, destWormhole) {
const wormholeTf = mat4.create();
if (!computeWormholeTransform(stageRuntime, srcWormhole, destWormhole, wormholeTf)) {
const destPos = { x: destWormhole.pos.x, y: destWormhole.pos.y, z: destWormhole.pos.z };
const animGroupIndex = destWormhole.animGroupIndex ?? 0;
if (animGroupIndex > 0) {
stack.fromMtx(stageRuntime.animGroups[animGroupIndex].transform);
stack.tfPoint(destPos, destPos);
}
ball.pos.x = destPos.x;
ball.pos.y = destPos.y;
ball.pos.z = destPos.z;
ball.prevPos.x = destPos.x;
ball.prevPos.y = destPos.y;
ball.prevPos.z = destPos.z;
updateBallTransform(ball);
ball.prevTransform.set(ball.transform);
return;
}
vec3.set(wormholeVecA, ball.pos.x, ball.pos.y, ball.pos.z);
vec3.transformMat4(wormholeVecA, wormholeVecA, wormholeTf);
ball.pos.x = wormholeVecA[0];
ball.pos.y = wormholeVecA[1];
ball.pos.z = wormholeVecA[2];
vec3.set(wormholeVecA, ball.prevPos.x, ball.prevPos.y, ball.prevPos.z);
vec3.transformMat4(wormholeVecA, wormholeVecA, wormholeTf);
ball.prevPos.x = wormholeVecA[0];
ball.prevPos.y = wormholeVecA[1];
ball.prevPos.z = wormholeVecA[2];
mat3.fromMat4(wormholeMat3, wormholeTf);
vec3.set(wormholeVecA, ball.vel.x, ball.vel.y, ball.vel.z);
vec3.transformMat3(wormholeVecA, wormholeVecA, wormholeMat3);
ball.vel.x = wormholeVecA[0];
ball.vel.y = wormholeVecA[1];
ball.vel.z = wormholeVecA[2];
vec3.set(wormholeVecA, ball.unk114.x, ball.unk114.y, ball.unk114.z);
vec3.transformMat3(wormholeVecA, wormholeVecA, wormholeMat3);
ball.unk114.x = wormholeVecA[0];
ball.unk114.y = wormholeVecA[1];
ball.unk114.z = wormholeVecA[2];
updateBallTransform(ball);
ball.prevTransform.set(ball.transform);
ball.wormholeTransform = wormholeTf;
}
function checkBallEnteredWormhole(ball, stageRuntime) {
const stage = stageRuntime.stage;
const animGroups = stageRuntime.animGroups;
const physBall = ball.physBall;
initPhysBallFromBall(ball, physBall, stage.format);
for (let animGroupId = 0; animGroupId < stage.animGroupCount; animGroupId += 1) {
const stageAg = stage.animGroups[animGroupId];
const stageWormholes = animGroupId === 0 ? (stage.wormholes ?? []) : [];
const wormholes = stageWormholes.length
? (stageAg.wormholes ?? []).concat(stageWormholes)
: (stageAg.wormholes ?? []);
if (!wormholes.length) {
continue;
}
if (animGroupId !== physBall.animGroupId) {
tfPhysballToAnimGroupSpace(physBall, animGroupId, animGroups);
}
for (const wormhole of wormholes) {
const trigger = {
pos: { x: 0, y: WORMHOLE_TRIGGER_OFFSET_Y, z: 0 },
rot: { x: wormhole.rot.x, y: wormhole.rot.y, z: wormhole.rot.z },
width: WORMHOLE_TRIGGER_WIDTH,
height: WORMHOLE_TRIGGER_HEIGHT,
};
stack.fromTranslate(wormhole.pos);
stack.rotateZ(wormhole.rot.z);
stack.rotateY(wormhole.rot.y);
stack.rotateX(wormhole.rot.x);
stack.tfPoint(trigger.pos, trigger.pos);
if (testLineIntersectsRect(physBall.pos, physBall.prevPos, trigger)) {
return { wormhole };
}
}
}
if (physBall.animGroupId !== 0) {
tfPhysballToAnimGroupSpace(physBall, 0, animGroups);
}
return null;
}
export function checkBallEnteredGoal(ball, stageRuntime) {
const stage = stageRuntime.stage;
const animGroups = stageRuntime.animGroups;
const physBall = ball.physBall;
initPhysBallFromBall(ball, physBall, stage.format);
let goalId = 0;
for (let animGroupId = 0; animGroupId < stage.animGroupCount; animGroupId += 1) {
const stageAg = stage.animGroups[animGroupId];
if (stageAg.goalCount > 0) {
if (animGroupId !== physBall.animGroupId) {
tfPhysballToAnimGroupSpace(physBall, animGroupId, animGroups);
}
for (const goal of stageAg.goals) {
const trigger = {
pos: { x: 0, y: 1, z: 0 },
rot: { x: goal.rot.x, y: goal.rot.y, z: goal.rot.z },
width: 2,
height: 2,
};
stack.fromTranslate(goal.pos);
stack.rotateZ(goal.rot.z);
stack.rotateY(goal.rot.y);
stack.rotateX(goal.rot.x);
stack.tfPoint(trigger.pos, trigger.pos);
if (testLineIntersectsRect(physBall.pos, physBall.prevPos, trigger)) {
return { goalId, animGroupId, goalType: goal.type };
}
goalId += 1;
}
}
}
return null;
}