Movement System¶
What the SC6 binary actually contains for chara movement, with each claim
tied to a specific Ghidra address. All addresses are absolute (image base
0x140000000).
Earlier drafts retracted
Earlier versions of this page mixed binary-verified facts with general fighting-game knowledge. This rewrite includes only what has been read directly from Ghidra. Anything that could not be verified is flagged "unverified" rather than asserted.
The per-tick chara update flow¶
LuxBattle_TickCharaMainSimulation @ 0x14034da70 runs once per chara per
battle tick. The movement-relevant calls it makes, in order:
LuxMoveVM_ExecuteOpStream(chara, 0)— runs move bytecode for slot 0LuxMoveVM_ExecuteOpStream(chara, 1)— slot 1LuxMoveVM_ExecuteOpStream(chara, 2)— slot 2LuxBattleChara_UpdateProximityBlendWeight(chara)LuxBattleChara_UpdateStanceCategory(chara)LuxBattleChara_TickHitStateStateMachine(chara)— hit-state slide decayLuxBattleChara_IntegratePhysics_PerTick(chara)— integrates velocity into world positionLuxBattleChara_EvaluateDefenseMode(chara)LuxBattleChara_UpdateBlockStateStochastic+LuxBattleChara_TickDamageAndBehaviorLockLuxBattle_TickCharaTerrainContactBlend(chara)LuxBattleChara_FinalizeTickPoseAndState(chara)
The position update is step 7. Steps 1-3 (the move VM) run before the integrator, so any opcode that writes velocity has already executed by the time integration happens.
What IntegratePhysics_PerTick actually does¶
Decompiled directly from 0x140306bb0:
Inputs read from chara:
| Offset | What |
|---|---|
+0x130 / +0x138 |
MoveVelocity X/Z (per-tick velocity) |
+0x140 / +0x148 |
"Ground" velocity X/Z (third additive term) |
+0x1f0 / +0x1f8 |
Pushback velocity X/Z (decayed by hit-state machine) |
+0x150 / +0x158 |
One-shot offset X/Z (added without time-scaling) |
+0x3490 |
Float scalar that multiplies only the +0x130/+0x138 term |
+0x16fb |
Damped-mode flag (selects alternate path) |
+0x198c |
Motion state code (0x12 = airborne uncapped) |
+0x44dc2 |
Bone-anchor index (-1 disables bone anchor integration) |
Outputs written to chara:
| Offset | What |
|---|---|
+0xa0 / +0xa8 |
World XZ position (the sum is added in) |
+0xa4 |
World Y position |
+0x1d0 / +0x1d8 |
"This frame's position delta" cache |
Normal-path math (when chara+0x16fb == 0):
delta_x = (chara+0x3490 * chara+0x130
+ chara+0x140
+ chara+0x1f0) * TimeDilation
+ chara+0x150
chara+0xa0 += delta_x
(same shape for Z using +0x138, +0x148, +0x1f8, +0x158)
Damped path (when chara+0x16fb != 0):
chara+0x130 *= DAT_143e8a2d4 (decay constant)
chara+0x138 *= DAT_143e8a2d4
chara+0x134 *= DAT_143e8a2e8 (different Y decay)
chara+0xa0 += TimeDilation * chara+0x130
chara+0xa8 += TimeDilation * chara+0x138
TimeDilation comes from LuxMoveVM_GetTimeDilationScalar @ 0x14030a8c0
and gates the whole thing: when g_LuxBattle_VMFreezeRecord.bVMFreezeByte
is 0, the scalar returns 0 and no position change happens. That is how
hitstop / super-freeze pauses movement.
The MoveVelocity field is unverified in source¶
No function that writes chara+0x130 during a normal walking move
has been found yet. What is confirmed:
- It is read by
IntegratePhysics_PerTickand added to position. - It is written by
LuxMoveVM_ApplyMoveOffsetToChara @ 0x140344fc0for attacks that include forward lunges (decoded from a 3-int16 cell payload). - It is decayed by the damped-mode path inside
IntegratePhysics_PerTickitself (when+0x16fbis set).
The earlier claim — that animation root motion drives chara+0x130 —
is plausible, because ULuxRootMotionComponent @ chara+0x3b0 exists
and LuxBattleChara_ApplyHitCueRootMotion_DirectPositionWrite @ 0x140306530
does process pose-derived motion. But that hit-cue path bypasses
+0x130 entirely and writes directly to chara+0xa0/+0xa8, so it
cannot be asserted that animation root motion drives the +0x130 value
used by the integrator.
A definitive answer needs one of:
- Tracing every writer of chara+0x130 (which would mean finding all
stores to that offset across the binary).
- Finding the move VM opcode that writes velocity (the available docs
list 0x40002 ATK and a few others, but no "velocity write" opcode
by name).
Until then, the source of normal-walking velocity is unverified.
What's verified about ApplyHitCueRootMotion_DirectPositionWrite¶
LuxBattleChara_ApplyHitCueRootMotion_DirectPositionWrite @ 0x140306530
applies pose-driven motion to the chara: it reads per-slot hit-cue data
(4 slots, indexed by the slotIdx parameter) and writes directly to
chara+0xa0/+0xa8. It does NOT go through the velocity field.
Slot data (one per slotIdx in 0..3):
| Field | What |
|---|---|
(slotIdx + 0x649) * 0xb0 |
Hit-cue slot base on chara |
+0x16 |
Hit-cue index (i16, -1 = inactive) |
+0x3c |
Per-slot weight gate (float, 0 = skip slot) |
+0x60..+0x68 |
Cached local-axis hit-cue translation |
+0x70..+0x78 |
Cached world-axis hit-cue translation |
slotIdx * 0xc0 + 0x95fa0 |
Pose pack on chara |
+0x818 |
Pose-active byte |
+0x820 |
Active weight |
+0x8c0.. |
4 sub-pose tail (0x300 stride each) |
Algorithm:
1. Skip if airborne (chara+0x16e2) or hit-cue inactive
2. Compute final blend weight by multiplying through (1 - subPoseWeight) chain
3. LuxBattleChara_EvaluateHitCueQuaternion extracts the local-axis translation
4. Scale by chara+0x3490 (same multiplier as the integrator's MoveVel term)
5. Optionally scale by per-slot multiplier at chara+0x2084 + slotIdx*4
6. Transform local→world via Euler matrix (chara+0x90/+0x94/+0x98)
7. Add directly to chara+0xa0/+0xa8
8. Also adds to chara+0x1c0/+0x1c8 (residual velocity for next frame's
ApplyResidualVelocity_PostTick)
So this is the post-hit follow-through path: when a hit-reaction animation pulls the chara through space, this is where the per-frame displacement comes from. It does NOT handle normal walking.
Motion-input flag bank¶
LuxBattleChara_SetMotionInputFlag @ 0x140304c00 is the canonical writer
to a 64-byte flag array at chara+0x16d0..+0x170f. Each byte is one
binary motion-input flag, indexed by the flagIdx parameter (0..0x3f).
Verified flag indices:
| Flag | Meaning (from binary context) |
|---|---|
0x0b |
Fall reaction. Written by TickCharaMainSimulation when chara goes off platform (airborne flag set + falling Y velocity + matching state in DAT_144125c50 table) |
0x12 |
Airborne state. Special handler: when CLEARED, snaps Y to terrain floor at chara+0x3c8 and clears airborne pose at chara+0x96f68 |
0x29 |
Recursive: clearing this also calls SetMotionInputFlag(0x21, 0) and SetMotionInputFlag(0x30, 1). High-level "movement done" trigger |
0x2a |
When SET: copies chara[1].field_0x4a0..+0x4a3 (4 bytes) into chara->bActiveAttackStateFlag area |
Mask 0x2000200003f000 for flags < 0x36 |
Cluster {0xc, 0xd, 0xe, 0xf, 0x10, 0x11, 0x29, 0x35} — setting any triggers a chained SetMotionInputFlag(0xb, ...) (fall reaction) |
Flags read by CanTrackOpponentLookAt @ 0x14030a550 (any non-zero
disables auto-rotation toward opponent):
+0x16d3, +0x16d4, +0x16d8, +0x16db, +0x16df,
+0x16e5, +0x16e6, +0x16e9, +0x16ec, +0x16fe
The semantics of these flags have NOT been individually verified. Their names ("hit-reaction", "step-state", "movement-commit") are guesses based on which other systems gate on them. The precise meaning of each individual byte is unverified.
TimeDilation system (verified)¶
LuxMoveVM_GetTimeDilationScalar @ 0x14030a8c0 returns a per-chara
multiplier. Verified inputs:
| Source | What |
|---|---|
g_LuxBattle_VMFreezeRecord.bVMFreezeByte @ 0x1448462d0 |
Consulted only on Path B (see below). When non-zero on Path B, returns 0. Often-cited as the "primary hitstop control" — true for normal hit-stop / super-flash / Critical Edge, not true for replay viewing. |
g_LuxBattle_VMFreezeRecord.flBaseAlpha |
Base alpha multiplier (used when freeze byte is 0 on Path B). |
chara+0x3500 |
Per-chara base time-scale slot. |
chara+0x3510 |
Per-chara fallback slot (negative triggers Path A — special-move slow-mo). |
chara+0x23c |
CharaKindByte (0/1 selects perCharaCap). |
chara+0x19ec |
Match-state id (must be 2 for normal play). |
chara+0x973e8 |
Opponent chara back-ref (read for the P2-ish entry-gate predicate). |
DAT_144846458 / DAT_14484645c |
Global mode array. |
The four return paths¶
The function has four distinct returns, and only one of them honours
bVMFreezeByte:
float LuxMoveVM_GetTimeDilationScalar(LuxBattleChara* chara) {
byte charaKindByte = chara->CharaKindByte; // +0x23C
// OUTER GUARD — per-opp-mode cell. If 3, skip everything below.
if ((&DAT_14484645c)[charaKindByte ^ 1] != 3) {
if ((int)DAT_144846458 < 0) { // global mode
// ENTRY GATE — only reaches Path A/B if state != 2 OR
// (P2 AND opp.state == 2). For P1 in normal play (state==2),
// condition is FALSE → fall through to chara+0x3500 below.
if ((chara->state_at0x19EC != 2) ||
(charaKindByte != 0 &&
chara->Opp->state_at0x19EC == 2)) {
if (chara->slot_at0x3510 < 0.0f) {
// PATH A — special slow-mo (super-flash, finishing-blow).
// *** BYPASSES VMFreezeByte ***
return flOutBlendW1 * chara->scale_at0x3500;
}
// PATH B — normal play, honours VMFreezeByte.
float fVar1 = (g_VMFreezeRecord.bVMFreezeByte == 0)
? g_VMFreezeRecord.flBaseAlpha : 0.0f;
return fVar1 * min(chara->scale_at0x3500, perCharaCap);
}
// P1 in state==2 falls through ↓
}
else if (DAT_144846458 != charaKindByte) {
// PATH C — mode mismatch.
return 0.0f;
}
}
// FALL-THROUGH — per-chara base time-scale (typically 1.0).
// *** BYPASSES VMFreezeByte ***
return chara->scale_at0x3500;
}
| Path | Condition | Honours bVMFreezeByte? |
Returns |
|---|---|---|---|
| A | +0x3510 < 0 (super-flash, finishing-blow) |
No | flOutBlendW1 * chara+0x3500 |
| B | normal play, entry gate matches | Yes | (VMFreezeByte == 0 ? flBaseAlpha : 0) * scale |
| C | global-mode mismatch on charaKindByte | n/a | 0.0f |
| Fall-through | outer guard == 3, OR DAT_144846458 < 0 and entry gate doesn't match (P1 in state==2) |
No | chara+0x3500 (typically 1.0f) |
Why bVMFreezeByte = 1 alone fails replay viewing¶
In replay watching, the chara's match-state byte at +0x19EC stays at
2 (normal play), even though inputs come from a recorded file. For
P1 (charaKindByte == 0) the entry gate evaluates
(2 != 2) || (0 != 0 && ...) = FALSE, so Path A/B is skipped and the
function falls through to chara+0x3500 (≈ 1.0). Setting bVMFreezeByte
does nothing: UE4 anim instances scale by the engine's tick dilation,
the BM round timer keeps draining, and the replay auto-advances.
Hit-stop / super-freeze still work in stock SC6 because the engine writes
+0x3510 < 0 (Path A) or transitions chara state out of 2 (Path B), so
the VMFreezeByte path actually executes. A mod that wants to freeze
regardless of chara state needs an entry hook on this function — see
Replay System: HorseMod gate stack.
What VMFreezeByte halts (verified caller list)¶
Setting bVMFreezeByte != 0 zeros the scalar across the full Move-VM and physics stack:
LuxMoveVM_TickDriver, LuxBattle_TickCharaMainSimulation,
LuxBattle_TickHitStopSchedulerAndInputMirror, LuxBattleChara_FinalizeTickPoseAndState,
LuxMoveVM_ExecuteOpStream, LuxMoveVM_AdvanceLinkedMotionObject,
LuxMoveVM_AdvanceLaneFrameStep, LuxMoveVM_AdvanceCharaAnimClipPlayer,
LuxBattleChara_ApplyKnockbackForce, LuxBattleChara_IntegratePhysics,
LuxBattle_TickCharaCollisionPhysics, LuxBattle_DispatchFootstepEvents,
LuxBattle_DispatchWeaponTraceContactVFX, LuxBattle_SetupPoseFromENSTData,
LuxBattle_BattlePhase_Tick, plus a handful of visual-FX selectors. Effectively every
"this is part of the gameplay simulation" tick.
What VMFreezeByte does NOT halt¶
Anything that runs off UE4's World::DeltaTime rather than the VM scalar. Notable:
- UMG widget Tick (HUD, training-mode panels, input-display widgets)
- Particle / Niagara render time (visual decay continues)
- Audio playback engine
- The animation root-motion blender as it samples per-frame poses for non-battle actors
Practical consequence: a freeze mod that uses only VMFreezeByte will halt the gameplay
loop cleanly, but display-rate widgets reading chara state at render rate (input-history
overlays, etc.) keep polling and may visually decouple from the simulation.
For full pause semantics including UI, use UE4's bGamePaused; for simulation-only
freeze, VMFreezeByte is enough.
Step types — what's actually in the binary¶
The training-mode dummy enum ELuxBattleDummyMoveType lists six
step-type entries:
Run_Right (0x1433b00c0)
Run_Left (0x1433b0110)
Run_FrontStep (0x1433b0160)
Run_BackStep (0x1433b01b0)
Run_RightStep (0x1433b0200)
Run_LeftStep (0x1433b0250)
These are dummy behaviors — they tell the training dummy to perform
that motion. They are labels, not input commands. The Run_ prefix
suggests they all execute out of a "run" state, but the binary does not
spell out what input the player presses to trigger each one — that
mapping lives in per-character move-bank bytecode that has not been decoded.
The CPU AI has matching action classes for these too: HgCpuDirectRightStep,
HgCpuDirectLeftStep, HgCpuDirectFrontStep, HgCpuDirectBackStep (RTTI
strings at 0x144141918 etc.).
ELuxBattleMoveCategory (the move-class enum) has separate entries:
HorizontalAttacks, VerticalAttacks, Kicks, EightWayRunMoves,
Throws, etc. The EightWayRunMoves category covers attacks that
come out of 8WR, not the 8WR movement itself.
Counter-hit messages¶
ELuxBattleMessage::EMS_RunCounter exists at 0x1432712e8. It sits in
the same enum, next to EMS_AttackCounter, EMS_BreakCounter, and
EMS_ImpactCounter. Where it is fired has not been traced — there are no
xrefs to the string itself in static code, only references in the
reflection enum table. So the existence of a "RunCounter" category
is verified, but the trigger conditions are not.
What CHANGES movement (verified levers only)¶
| Lever | Mechanism (verified) |
|---|---|
g_LuxBattle_VMFreezeRecord.bVMFreezeByte at 0x1448462d0 |
When 0, GetTimeDilationScalar returns 0; integrator multiplies delta by 0; position frozen. Hitstop / super-freeze. |
chara+0x3510 |
Negative value triggers slow-mo path inside GetTimeDilationScalar. Per-chara special-move time scale. |
chara+0x3500 |
Per-chara base time-scale slot. |
chara+0x3490 |
Multiplied into the chara+0x130 MoveVel term of the integrator AND the hit-cue translation in ApplyHitCueRootMotion. Per-frame motion blend weight. |
chara+0x16fb |
Picks the damped path in IntegratePhysics_PerTick (decays MoveVel each frame instead of integrating it). |
chara+0x198c == 0x12 |
Selects airborne uncapped Y integration (no terrain clamp). |
chara+0x16e2 |
Airborne flag (read by ApplyHitCueRootMotion to skip; read by TickCharaMainSimulation to detect off-platform fall) |
The cluster of flags at chara+0x16d0..+0x170f collectively gates
auto-rotation via CanTrackOpponentLookAt. What individually toggles
each one is unverified — they are written by SetMotionInputFlag from
move bytecode, but which moves write which flags has not been traced.
Things I claimed earlier and now retract¶
- "Per-character step distance lives in animation
.uassetfiles." Plausible but unverified.chara+0x130is read by the integrator; whether it is written by anim root motion or by VM bytecode is not known from what has been decoded. - "Soul Charge mode flag (
chara+0x170c) doesn't affect normal stepping." Half-right: it does not appear inIntegratePhysics_PerTickorApplyHitCueRootMotion, but it could feed into VM bytecode that writeschara+0x130. Unverified either way. - "Best stepping strategy" recommendations. Those were extrapolated
from the existence of
EMS_RunCounterand the move-class enum, plus general fighting-game knowledge. The binary confirms only that: EMS_RunCounteris a defined message category.Run_*step types exist in the dummy enum.HorizontalAttacksandVerticalAttacksare distinct move categories.
Whether vertical attacks actually have narrower hitboxes than horizontal ones, whether RunCounter actually fires for steps mistimed against horizontals, and what frame ranges step-G covers — none of those are verified from the binary alone. They are game-design conventions consistent with the systems that exist, but consistency is not proof.
Code references¶
| Function / Symbol | RVA | What's verified |
|---|---|---|
LuxBattle_TickCharaMainSimulation |
0x14034da70 |
Master per-tick chara update; calls the movement subsystems in the order listed above |
LuxBattleChara_IntegratePhysics_PerTick |
0x140306bb0 |
The integrator math (above). Confirmed reads / writes |
LuxBattleChara_ApplyHitCueRootMotion_DirectPositionWrite |
0x140306530 |
Direct world-position writer for hit-reaction follow-through. Bypasses +0x130 |
LuxBattleChara_ApplyResidualVelocity_PostTick |
0x140304280 |
Adds +0x1c0/+0x1c8 to position with decay |
LuxBattleChara_DecayHitstunSlideVelocity |
0x140309160 |
Decays +0x1f0/+0x1f8 (the pushback term in the integrator) per hit-type curve |
LuxBattleChara_TickHitStateStateMachine |
0x140308ec0 |
Calls the slide decay; switches phases by +0x1994 state |
LuxBattleChara_CanTrackOpponentLookAt |
0x14030a550 |
Returns false when any flag in +0x16d0..+0x170f cluster is set |
LuxBattleChara_SetMotionInputFlag |
0x140304c00 |
Canonical writer to flag bank +0x16d0..+0x170f |
LuxMoveVM_GetTimeDilationScalar |
0x14030a8c0 |
Per-chara time scale; returns 0 when VMFreezeByte is 0 |
LuxMoveVM_ApplyMoveOffsetToChara |
0x140344fc0 |
Writes +0x130 from a 3-int16 cell payload (attack lunges only — verified writer for attack-driven velocity) |
LuxMoveVM_ResolveRangeAndAngleOffset |
0x140307bd0 |
Decoder used by the above |
g_LuxBattle_VMFreezeRecord |
0x1448462d0 |
Master freeze byte + multipliers struct |
g_LuxBattle_HitReactionSlideTable |
0x1448554e8 |
Per-hit-type slide-decay curves (formerly misnamed g_LuxMoveVM_PerSlotScaleTable) |
ULuxRootMotionComponent_StaticClass |
0x140196680 |
Class registration. Component is at chara+0x3b0. Whether it writes +0x130 is unverified |
Z_Construct_UClass_ULuxRootMotionComponent |
0x140b7ce50 |
Property layout: Owner, RootMotionParams, CachedRootMotionTransform |
When moves retrack against the opponent — verified¶
Retracking — the chara's body rotating to face the opponent during a move —
runs once per tick from LuxBattle_TickCharaMainSimulation step (J),
which calls LuxBattleChara_UpdateOpponentRelativeAngles_PerTick @ 0x140305e50,
which in turn calls LuxBattleChara_RetrackFacingTowardOpponent @ 0x140369450.
The retrack function is the gate. Verified read of 0x140369450:
target_angle = chara+0x15a4
if (chara+0x16f0 == 0 AND chara+0x16d9 != 0):
target_angle += 0.5 // 180° side-mirror flip
★ THE GATE:
if (chara+0x16e6 != 0 AND chara+0x16e1 == 0):
return // no rotation this frame
// Otherwise: compute SLERP-clamped delta and call ApplyFacingRotationDelta
So a move retracks iff chara+0x16e6 == 0 OR chara+0x16e1 != 0.
Correction (2026-05-01). An earlier version of this page interpreted
chara+0x16e1as a per-move "homing override" / "tracking flag". That interpretation was wrong. Both+0x16e6and+0x16e1are entries in the 64-element motion-input flag bank atchara+0x16D0..+0x170F, documented underLuxBattleChara_SetMotionInputFlag @ 0x140304c00.
chara+0x16e6= motion-input flag0x16— set by various move-state transitions (acts as "in-some-non-walk-state" inside the gate).chara+0x16e1= motion-input flag0x11— part of the fall-reaction cluster{0x0c..0x11, 0x29, 0x35}that gets OR'd to derive flag0x0b(master fall flag). Verified writer:LuxBattle_ComputeHitReactionParams @ 0x140343b90, case 0xd (a specific knockback / recovery reaction type), where it is CLEARED (= 0). The line is:Re-reading the gate truth table with corrected semantics:
+0x16e6+0x16e1Retrack Real meaning 0 any runs idle / walking — natural facing 1 0 blocked mid-move, NOT in fall — facing locked 1 1 runs mid-move AND in fall-reaction — engine realigns There is no "homing override" flag in this gate. Moves that track the opponent in SC6 (homing throws, certain supers) implement that through a different mechanism — most likely the SLERP-weight system at
chara+0x971ac..+0x971b8(set up at move-start with non-default presets), or by the move script writingchara+0x94directly.
Verified writer of chara+0x16e6:
LuxMoveVM_OnMoveStart_SnapPositionAndFacing_LockRetrack @ 0x1402ff3e0
sets chara+0x16e6 = 1 at move start. It also:
1. Snaps chara world XZ to chara+0x300/+0x308 (cached look-at position)
2. Computes a target rotation from move-attribute data at chara+0x96928
3. Calls ApplyFacingRotationDelta to snap facing to that target
4. Sets bit 0x400000 of chara+0x44930 + animState*0x468
This is the "commit to the move's facing direction" function. Once it
fires, retracking is suppressed for the rest of the move — unless
the chara enters a fall-reaction state and that path toggles
chara+0x16e1 (motion-input flag 0x11).
Verified consumer of chara+0x16e1:
UpdateOpponentRelativeAngles_PerTick @ 0x140305e50 reads chara+0x16e1
to gate the animation-sector pointer update at chara+0x97248:
if (chara+0x16e1 == 0
AND (chara+0x198c & 0xfffc) == 0
AND chara+0x198c != 1):
// skip: don't change which 8-direction animation variant plays
else:
update sector pointer, recalc per-frame look-target scalar at +0x15c0
So chara+0x16e1 is read in two places: by the retrack gate (above)
and by the animation-sector update (here). In both, the meaning is
the same — when the chara is in a fall-reaction state, the engine
re-allows aiming and animation alignment toward the opponent.
Per-frame rotation caps inside RetrackFacingTowardOpponent:
| Stance / state | Cap |
|---|---|
Default (chara+0x19b0 != 1) |
0.5 (i.e. 180°/frame — effectively no cap) |
chara+0x19b0 == 1 |
DAT_143e8a040 (smaller cap — restricted stance) |
chara+0x19b0 == 1 AND chara+0x16e5 != 0 |
DAT_143e8a084 (smallest cap) |
SLERP weight at chara+0x971b0:
The rotation delta is multiplied by a SLERP weight at chara+0x971b0
before clamping. The weight system has four fields:
| Field | Role |
|---|---|
chara+0x971ac |
Countdown timer (frames remaining) |
chara+0x971b0 |
Current weight value, 0..1 |
chara+0x971b4 |
Preset value loaded when countdown reaches 0 |
chara+0x971b8 |
Per-frame increment (added each tick while countdown active) |
chara+0x971a8 |
Mode selector: 0 = scale-then-clamp, 1 = clamp-by-weight, other = no rotation |
Tracking moves that should retrack gradually rather than snap write to these fields to control the ramp-in. Move bytecode is the suspected writer, but the writer is unverified — finding it would reveal how per-move tracking strength is encoded.
Bone-target retracking (secondary system):
When chara+0x44dc2 != -1, RetrackFacingTowardOpponent runs a second
pass that aims at a specific opponent bone (read from a pose at
chara+0x35a0 vtable[8], offset +0xc0 or +0x300 depending on
chara+0x971d0). It uses atan2 of the bone delta against the cached
opponent position at chara+0x964a0/+0x964a8, with its own SLERP
weight system at chara+0x971c0..+0x971cc. This is what aligns
attacks that need to hit a specific bone (likely throws and grabs).
Summary of "when does a move retrack"¶
| Frame state | Retracks? |
|---|---|
No move active (idle / walking / 8WR) — +0x16e6 == 0 |
Yes, every frame, full speed (capped at 180°/frame) |
Move active, +0x16e6 == 1, +0x16e1 == 0 (normal animation) |
No, facing locked |
Move active, +0x16e6 == 1, +0x16e1 == 1 (fall-reaction during move) |
Yes, gradually (SLERP weight ramps in) |
Restricted stance +0x19b0 == 1 |
Caps per-frame angle to DAT_143e8a040 |
Restricted stance + +0x16e5 set |
Caps even tighter to DAT_143e8a084 |
The third row is not a "homing move" path; it is the engine
realigning the chara during a fall-reaction state mid-move. A move's
"tracking / homing" attribute — in the gameplay sense, moves that
track the opponent during their own active frames — is implemented
through a different mechanism, likely the SLERP-weight system at
chara+0x971ac..+0x971b8 configured at move-start, but that has
not yet been verified end-to-end.
Verified callers / writers¶
| Symbol | RVA | Role |
|---|---|---|
LuxBattleChara_RetrackFacingTowardOpponent |
0x140369450 |
The retrack gate + math + ApplyFacingRotationDelta call |
LuxBattleChara_UpdateOpponentRelativeAngles_PerTick |
0x140305e50 |
Per-tick caller; computes target angle and bone-target data; gates animation-sector update on +0x16e1 |
LuxBattleChara_ApplyFacingRotationDelta |
0x140311350 |
Wrapping/clamping writer to chara+0x94 (facing), +0x22c, +0x15a4, +0x15ac |
LuxMoveVM_OnMoveStart_SnapPositionAndFacing_LockRetrack |
0x1402ff3e0 |
Sets chara+0x16e6 = 1 at move start |
Unverified¶
- How "homing" / "tracking" moves are actually authored. The
per-move "track the opponent" attribute is most likely the SLERP-
weight preset at
chara+0x971b4(loaded intochara+0x971b0when the countdown atchara+0x971acreaches zero). Find the writer of+0x971b4 / +0x971b8 / +0x971acto pin this down. - The writers of
chara+0x16e6at move end (the path that lets default retracking resume after the move). The lock-retrack writer at move-start is found, but the cleanup path is not. - The writers of
chara+0x16e1beyond the verified case-0xd hit-reaction site. Other entry points to that flag are unmapped. - The actual values of
DAT_143e8a040andDAT_143e8a084(the restricted-stance per-frame angle caps).
What needs verification next¶
- Find the writer of
chara+0x130during a non-attack move (walking, 8WR, sidestep). This is the central unanswered question. - Find the writers of the SLERP weight fields at
chara+0x971ac..+0x971b8. That pins down the per-move "homing" / "tracking" attribute encoding. (The flag pair+0x16e6/+0x16e1is NOT how homing is encoded — see the correction note above.) - Trace which motion-input flags get set by which moves. The flag bank and the flag-clearing predicate are known, but not the per-flag semantics.
- Find where
EMS_RunCounteris actually fired. The string has no static xref, but it gets emitted somewhere at runtime. - Confirm or refute the V-vs-H hitbox-width hypothesis by inspecting KHit node geometry for representative moves of each category.