File size: 6,597 Bytes
c7b3e0f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
// 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;
}