// engine.js - Core constants & physics ported from Ikemen-GO (MIT, Suehiro & contributors) // Values mirror src/bytecode.go, src/char.go (CharVelocity/CharMovement init) and src/stage.go. // ---- State types (src/bytecode.go ST_*) ---- export const ST_S = 1, ST_C = 2, ST_A = 4, ST_L = 8, ST_N = 16, ST_U = 32; export const ST_MASK = 63; export const ST_SCA = ST_S | ST_C | ST_A; // Move types export const MT_I = 0, MT_U = 1, MT_H = 2; // ---- Attack types (src/bytecode.go AT_*) ---- export const AT_NA = 1 << 6, AT_NT = 1 << 7, AT_NP = 1 << 8; export const AT_SA = 1 << 9, AT_ST = 1 << 10, AT_SP = 1 << 11; export const AT_HA = 1 << 12, AT_HT = 1 << 13, AT_HP = 1 << 14; export const AT_AA = AT_NA | AT_SA | AT_HA; export const AT_AT = AT_NT | AT_ST | AT_HT; export const AT_AP = AT_NP | AT_SP | AT_HP; export const AT_ALL = AT_AA | AT_AT | AT_AP; // ---- Character flags ---- export const CF_HITSTOP = 1 << 0; export const CF_UNGUARDED = 1 << 1; export const CF_GUARDED = 1 << 2; export const CF_DIZZY = 1 << 3; export const CF_KO = 1 << 4; export const CF_JUGGLE = 1 << 5; // ---- Global constants ---- export const MaxPlayerNo = 6; export const GRAVITY_DEFAULT = 0.44; // cm.yaccel export const STAND_FRICTION = 0.85; // cm.stand.friction export const STAND_FRICTION_THRESHOLD = 2.0; export const CROUCH_FRICTION = 0.82; export const CROUCH_FRICTION_THRESHOLD = 0.0; export const DOWN_FRICTION_THRESHOLD = 0.05; // air.gethit (CharVelocity.init) export const AIR_GETHIT_GROUNDRECOVER = [-0.15, -3.5]; export const AIR_GETHIT_AIRRECOVER_MUL = [0.5, 0.2]; export const AIR_GETHIT_AIRRECOVER_ADD = [0.0, -4.5]; export const AIR_GETHIT_AIRRECOVER_BACK = -1.0; export const AIR_GETHIT_AIRRECOVER_FWD = 0.0; export const AIR_GETHIT_AIRRECOVER_UP = -2.0; export const AIR_GETHIT_AIRRECOVER_DOWN = 1.5; export const AIR_GETHIT_KO_ADD = [-2.5, -2]; export const AIR_GETHIT_KO_YMIN = -3; export const GROUND_GETHIT_KO_XMUL = 0.66; export const GROUND_GETHIT_KO_ADD = [-2.5, -2]; export const GROUND_GETHIT_KO_YMIN = -6; // movement (CharMovement.init) export const AIR_GETHIT_GROUNDRECOVER_GROUND_THRESHOLD = -20.0; export const AIR_GETHIT_GROUNDRECOVER_GROUNDLEVEL = 10.0; export const AIR_GETHIT_AIRRECOVER_THRESHOLD = -1.0; export const AIR_GETHIT_AIRRECOVER_YACCEL = 0.35; // stage defaults (newStage) export const STAGE_LEFTBOUND = -1000; export const STAGE_RIGHTBOUND = 1000; export const STAGE_SCREENLEFT = 15; export const STAGE_SCREENRIGHT = 15; export const STAGE_TOPBOUND = -60; export const STAGE_BOTBOUND = 80; // Canvas stage geometry (MUGEN-style local + camera) export const STAGE_LOCALWIDTH = 1280; export const STAGE_LOCALHEIGHT = 720; export const STAGE_FLOORY = 640; // where char.pos[1] === 0 sits in local coords export const STAGE_CAMERA_ZOOM = 1.0; export const ROUND_TIME = 99 * 60; // 99-second round timer (MUGEN default) export const MAX_JUGGLE = 8; export const MAX_GUARD_JUGGLE = 10; export const MAX_CHAIN = 10; export const clamp = (v, lo, hi) => v < lo ? lo : (v > hi ? hi : v); export const sign = v => v > 0 ? 1 : (v < 0 ? -1 : 0); export const abs = Math.abs; export const Btoi = v => (v > 0 ? Math.floor(v) : Math.ceil(v)); export const roundTo = Math.round; /** * Rect intersection test using MUGEN box semantics. * Box = [x1, y1, x2, y2] in character local space (y negative = up). */ export function boxOverlap(b1, b2) { return b1[0] < b2[2] && b1[2] > b2[0] && b1[1] < b2[3] && b1[3] > b2[1]; } export function rectWidth(b) { return b[2] - b[0]; } export function rectHeight(b) { return b[3] - b[1]; } /** * Physics integration ported from Ikemen-GO's char physics step. * Called once per active frame while the char is not paused. */ export function physicsStep(c) { const d = c.data; const air = c.pos[1] < 0 || c.stateType === ST_A; // horizontal velocity c.vel[0] += c.accel[0]; // friction (src/char.go uses stand/crouch friction w/ thresholds) const fric = c.stateType === ST_C ? CROUCH_FRICTION : STAND_FRICTION; const fricTh = c.stateType === ST_C ? CROUCH_FRICTION_THRESHOLD : STAND_FRICTION_THRESHOLD; if (Math.abs(c.vel[0]) > fricTh) { c.vel[0] -= sign(c.vel[0]) * fric; if (sign(c.vel[0]) !== sign(c.vel[0] - sign(c.vel[0]) * fric)) c.vel[0] = 0; } // vertical: yaccel only while airborne (MUGEN: vel[1] += yaccel down) if (air && !c.velFreezeY) { c.vel[1] += (c.yaccelOverride || d.yaccel); } // integrate c.pos[0] += c.vel[0]; c.pos[1] += c.vel[1]; c.pos[2] += c.vel[2]; // stage bounds if (c.pos[0] < d.leftbound) { c.pos[0] = d.leftbound; c.vel[0] = 0; } if (c.pos[0] > d.rightbound) { c.pos[0] = d.rightbound; c.vel[0] = 0; } if (c.pos[1] < d.topbound) { c.pos[1] = d.topbound; c.vel[1] = 0; } } /** Ground contact test: pos[1] === 0 in MUGEN space (y is negative up) */ export function onGround(c) { return c.pos[1] >= 0 && !c.airborne; } /** * Compute get-hit velocity, ported from Ikemen-GO hit velocity resolution. * Mirrors air.gethit.airrecover mul/add semantics. */ export function getHitVelocity(victim, attacker, hit) { const facing = attacker.pos[0] < victim.pos[0] ? 1 : -1; let vx = 0, vy = 0; if (victim.stateType === ST_A) { // airborne recovery if (hit.grounded) { vx = AIR_GETHIT_GROUNDRECOVER[0] * facing; vy = AIR_GETHIT_GROUNDRECOVER[1]; } else { vx = (AIR_GETHIT_AIRRECOVER_BACK * facing) * Math.abs(hit.vel[0]) * 0.5; vy = AIR_GETHIT_AIRRECOVER_UP; // juggle scaling: upward component decays with juggle count vy *= Math.max(0.35, 1 - victim.juggle * 0.075); if (victim.vel[1] > 0) vy += AIR_GETHIT_AIRRECOVER_DOWN * 0.4; } // apply hitdef velocity multiplier/add vx = (hit.vel[0] * facing) + vx * 0.5; vy = (hit.vel[1]) + vy * 0.5; } else { // grounded recovery: horizontal push scaled by xmul vx = (hit.vel[0] * facing) * GROUND_GETHIT_KO_XMUL; vy = hit.vel[1]; if (hit.guard) { // blocking: much weaker pushback vx = (hit.guard.vel[0] * facing); vy = hit.guard.vel[1]; } } return [vx, vy]; } /** Debug helper to render attr strings like Ikemen's attrString() */ export function attrString(attr) { if (!attr) return ''; let b = ''; if (attr & ST_S) b += 'S'; if (attr & ST_C) b += 'C'; if (attr & ST_A) b += 'A'; const pairs = [[AT_NA, 'NA'], [AT_NT, 'NT'], [AT_NP, 'NP'], [AT_SA, 'SA'], [AT_ST, 'ST'], [AT_SP, 'SP'], [AT_HA, 'HA'], [AT_HT, 'HT'], [AT_HP, 'HP']]; for (const [bit, label] of pairs) if (attr & bit) b += ', ' + label; return b; }