Creature runtime contract (creature_update_all)¶
This page defines the minimum runtime contract we need from creature_update_all
(crimsonland.exe FUN_00426220) to unblock Survival in the rewrite.
Source of truth: the decompiles (Ghidra/IDA/Binary Ninja). Code under src/ is our
reimplementation and may be wrong; use it as a porting aid, not an authority.
The rewrite already contains several pure (unit-testable) building blocks:
- AI target selection:
docs/creatures/ai.md+src/crimson/creatures/ai.py - Animation phase + frame selection:
docs/creatures/animations.md+src/crimson/creatures/anim.py - Spawning (templates + spawn slots):
docs/creatures/spawning.md+src/crimson/creatures/spawn.py - Spawn-slot tick behavior is tested in
tests/test_spawn_slots.py.
What is missing is the “glue”: the per-tick order, state mutation, and event emission that ties these pieces into a realtime loop.
Data model (what the loop owns)¶
The source-of-truth layout is the native creature_t pool; see
docs/creatures/struct.md.
For Survival parity, the runtime update loop must at least track:
- Per-creature motion state:
pos_xy,vel_xy,heading,target_heading -
Per-creature targeting state:
target_x/target_y,target_player,force_target,ai_mode, plus link/orbit fields (link_index,target_offset_x/y,orbit_angle/radius) -
Per-creature timers:
attack_cooldown,collision_timer, hit flash timer - Per-creature “alive” state:
active+health(and a one-shot “death handled” latch) - Per-creature presentation state:
anim_phase+tint_rgba - Spawn-slot state (global arrays / pool), indexed via
link_indexfor slot-owning creatures
Referenced tables (renderer + SFX)¶
creature_update_all (and helpers) consult the creature type table
(docs/creatures/animations.md):
- Animation rate (advances
anim_phase) - Atlas base frame and corpse frame (render/death)
- Per-type SFX “banks” (damage/death/contact variants)
Per-tick order (Survival subset)¶
This is the “contract” part: the order matters because it defines which state is visible to subsystems in the same tick (e.g. collision uses post-move position; death should run after all damage sources have been applied).
Observed high-level structure inside creature_update_all (mirrors docs/crimsonland-exe/frame-loop.md):
1) AI target selection
- AI7 link-index timer behavior (flag 0x80) is ticked early and can force ai_mode = 7.
- Target selection computes:
- target_x/target_y, target_heading, force_target
- move_scale (local speed reduction in some modes)
- optional self-damage when a required link is dead (modes 4/5 apply creature_apply_damage(..., 1000.0, ...))
- Reference port:
src/crimson/creatures/ai.py(derived from decompilation).
2) Heading integration
- Move heading toward target_heading with a per-frame turn rate clamp (native eases rather
than snapping).
3) Velocity integration
- Compute velocity from heading and move_speed (scaled by move_scale from AI).
- Integrate position: pos += vel * dt.
4) Bounds clamp
- Clamp pos_xy to terrain bounds.
- The native clamp is size-aware; for Survival we can start with a simple [0, w] x [0, h]
clamp and refine when we port the exact margin rule.
5) Collision + contact damage
- Contact damage to players is driven by per-creature collision_flag +
collision_timer (see docs/creatures/struct.md).
- Contract:
- When in contact range, set/keep
collision_flagand decrementcollision_timerbydt. - When the timer crosses below zero, apply
player_take_damage(contact_damage)and reset the timer to its period (native adds0.5seconds). - When not in contact range, clear the flag and reset/relax the timer (native behavior TBD).
- When in contact range, set/keep
6) Ranged attacks
- If the creature is a ranged variant (creature_flags indicates ranged mode), decrement
attack_cooldown and, when it elapses, spawn a projectile and reset the cooldown.
-
Observed ranged-variant gates:
0x10: spawns projectile type9and adds1.0toattack_cooldown(playssfx_shock_fire).0x100: spawns a projectile whose type id is stored inorbit_radius(also plays a fire SFX).
-
Note: animation strip selection also uses
0x10as a render-strip offset; seedocs/creatures/animations.md.
7) Spawn-slot ticking
- Some spawner templates allocate a spawn slot and store the slot index in
creature_link_index (these creatures also use flag 0x4, which doubles as the short-strip
animation flag; see the HAS_SPAWN_SLOT alias in src/crimson/creatures/spawn.py).
-
Contract:
- For each active spawn slot, call
tick_spawn_slot(slot, dt_seconds). - If it returns a
child_template_id, callcreature_spawn_template(child_template_id, ...)(or the rewrite equivalent:build_spawn_plan(...)then materialize it).
- For each active spawn slot, call
-
The tested semantics (
tests/test_spawn_slots.py):- Timer always decrements:
timer -= dt - When
timer < 0, it is incremented byintervalexactly once (no loop). countincrements and a spawn triggers only ifcount < limit.- Even when at limit, the timer still resets by
interval.
- Timer always decrements:
“Death contract” (what happens when HP crosses <= 0)¶
When a creature transitions from alive to dead (health <= 0), the runtime must perform a
one-shot death handler.
In the native code this is handled by creature_handle_death (0x0041e910) and is invoked from
inside creature_update_all.
Minimum side effects needed by Survival:
1) Deactivate the creature (active = 0) and update creature counters:
- creature_kill_count (HUD and scoring)
- creature_active_count (recomputed each update pass in native)
2) Award XP to the primary player:
- creature_handle_death adds int(creature_reward_value) to player_experience
(player_health + 0x88).
-
If the “Bloody Mess / Quick Learner” perk is active (
perk_id_bloody_mess_quick_learner), it addsint(reward_value * 1.3)instead. -
If
bonus_double_xp_timer > 0, it adds the same amount again (effectively doubling XP).
3) Attempt bonus drop
- Native uses a per-kill gate (bonus_try_spawn_on_kill, 0x41f8d0), then selects a type using
bonus_pick_random_type (docs/re/static/reference/bonus-drop-rates.md).
- Forced bonus-on-death uses flag
0x400(BONUS_ON_DEATH) and callsbonus_spawn_at(pos, id, duration)usinglink_indexlow/high 16-bit fields.
4) Queue FX + SFX
- FX: blood, corpse decals, sprite bursts (see docs/structs/effects.md and terrain baking notes).
- SFX: per-type death sounds use the type table SFX banks (docs/creatures/animations.md).
5) Survival bookkeeping
- Survival tracks recent death positions and a handout gate via globals listed in
docs/creatures/struct.md (used by survival_update).
- The rewrite should carry equivalent state (even if the reward logic is initially stubbed).
Notes:
- Some flags add death behavior:
-
SPLIT_ON_DEATHspawns smaller child creatures (max_health scaling is documented indocs/creatures/struct.md). -
BONUS_ON_DEATH(0x400) forces a specific bonus id/duration (encoded vialink_indexin native).