Skip to content

Creature spawning (creature_spawn_template / FUN_00430af0)

creature_spawn_template(template_id, pos_xy, heading) is the primary translation layer from "spawn ids" (quests, tutorial timelines, and other scripted spawners) to initialized creature_t entries.

It is not a static struct/table: it always performs base creature initialization, runs a large template switch, may allocate additional creatures and/or spawn-slot entries, and then applies shared tail modifiers (difficulty/hardcore, demo gating, etc).

For the pure, unit-testable model we use while porting templates, see: spawn_plan.md.

Inputs

  • template_id (aka param_1): spawn id used by quest tables and other spawners.
  • pos_xy (aka param_2): spawn position (two floats).
  • heading (aka param_3): radians; sentinel -100.0 means "randomize heading".
  • Globals consulted:
  • RNG stream: crt_rand() (MSVCRT rand()).
  • demo_mode_active: skips the spawn burst effect when nonzero.
  • terrain_texture_width/terrain_texture_height: bounds check for the burst effect.
  • config_blob.hardcore and DAT_00487194 (difficulty level): final stat modifiers.

Outputs and side effects

  • Returns a pointer into creature_pool (creature_t *). Some templates return the last creature allocated (for formations), not necessarily the base creature.

  • May allocate additional creature_pool entries (formation spawns, escorts).

  • May allocate and configure spawn-slot entries (deferred child spawns driven by creature_update_all).
  • May spawn a burst effect at the spawn position (skipped in demo mode or when out of bounds).

Algorithm sketch (high level)

1) Base init (always)

  • Allocates a creature slot (creature_alloc_slot()), then writes base fields:
  • ai_mode = 0, pos_xy, vel_xy = 0
  • active = 1, state_flag = 1
  • collision defaults (collision_flag = 0, collision_timer = 0)
  • hitbox_size = 16, attack_cooldown = 0
  • Seeds a transient random heading early: crt_rand() % 0x13a * 0.01.
  • If heading == -100.0, randomizes the final heading: crt_rand() % 0x274 * 0.01.
  • creature_alloc_slot() itself consumes RNG to seed per-creature defaults (notably phase_seed).

2) Template switch (template-specific)

Large switch/if-chain on template_id assigns template-specific constants and behavior:

  • Stats: type_id, flags, health, move_speed, reward_value, size, tint_rgba, ai_mode, and various AI/link fields.

  • Formation spawners: allocate N linked children and arrange them using circular offsets (cos/sin) and AI link modes (e.g. ai_mode = 3 with link_index = parent).

  • Spawn-slot spawners: allocate a slot (FUN_00430ad0()), store the slot index in link_index, and configure creature_spawn_slot_* arrays (timer/count/limit/interval/template/owner).

3) Tail modifiers (shared end-of-function)

Applied after the template switch to the returned creature:

  • If not in demo mode and inside terrain bounds: effect_spawn_burst(pos, 8).
  • max_health = health.
  • Note: difficulty/hardcore scaling applies to health after this assignment, so max_health retains the pre-scaled value.

  • Spider SP1 special case: when type_id == 3 and flags do not include 0x10 or 0x80, sets 0x80, clears link_index, and applies a move_speed *= 1.2 buff.

  • Template 0x38 special case: in hardcore, applies move_speed *= 0.7.

  • Overwrites heading with the final (possibly randomized) heading argument.
  • Difficulty / hardcore scaling:
  • Non-hardcore:

    • For flag 0x4 spawners: spawn_slot_interval += 0.2.
    • If DAT_00487194 > 0, scales reward/speed/contact/health, and for flag 0x4 spawners adds min(3.0, difficulty * 0.35) to spawn_slot_interval.
  • Hardcore:

    • Clears difficulty (DAT_00487194 = 0).
    • Buffs speed/contact/health.
    • For flag 0x4 spawners: spawn_slot_interval -= 0.2 clamped to >= 0.1.

Spawn id porting checklist (rewrite)

This tracks our creature_spawn_template rewrite coverage.

  • Ported: implemented in build_spawn_plan (pure plan builder).
  • Verified: covered by unit tests in tests/test_spawn_plan.py.
  • Legend: βœ… complete Β· 🚧 in progress Β· ⬜ not started
  • Note: spawn id 0x02 does not appear in the decompile extracts and is omitted.

Generated by uv run scripts/gen_spawn_templates.py.

Spawn id Creature Ported Verified
0x0 zombie βœ… βœ…
0x1 spider_sp2 βœ… βœ…
0x3 spider_sp1 βœ… βœ…
0x4 lizard βœ… βœ…
0x5 spider_sp2 βœ… βœ…
0x6 alien βœ… βœ…
0x7 alien βœ… βœ…
0x8 alien βœ… βœ…
0x9 alien βœ… βœ…
0xa alien βœ… βœ…
0xb alien βœ… βœ…
0xc alien βœ… βœ…
0xd alien βœ… βœ…
0xe alien βœ… βœ…
0xf alien βœ… βœ…
0x10 alien βœ… βœ…
0x11 lizard βœ… βœ…
0x12 alien βœ… βœ…
0x13 alien βœ… βœ…
0x14 alien βœ… βœ…
0x15 alien βœ… βœ…
0x16 lizard βœ… βœ…
0x17 spider_sp1 βœ… βœ…
0x18 alien βœ… βœ…
0x19 alien βœ… βœ…
0x1a alien βœ… βœ…
0x1b spider_sp1 βœ… βœ…
0x1c lizard βœ… βœ…
0x1d alien βœ… βœ…
0x1e alien βœ… βœ…
0x1f alien βœ… βœ…
0x20 alien βœ… βœ…
0x21 alien βœ… βœ…
0x22 alien βœ… βœ…
0x23 alien βœ… βœ…
0x24 alien βœ… βœ…
0x25 alien βœ… βœ…
0x26 alien βœ… βœ…
0x27 alien βœ… βœ…
0x28 alien βœ… βœ…
0x29 alien βœ… βœ…
0x2a alien βœ… βœ…
0x2b alien βœ… βœ…
0x2c alien βœ… βœ…
0x2d alien βœ… βœ…
0x2e lizard βœ… βœ…
0x2f lizard βœ… βœ…
0x30 lizard βœ… βœ…
0x31 lizard βœ… βœ…
0x32 spider_sp1 βœ… βœ…
0x33 spider_sp1 βœ… βœ…
0x34 spider_sp1 βœ… βœ…
0x35 spider_sp2 βœ… βœ…
0x36 alien βœ… βœ…
0x37 spider_sp2 βœ… βœ…
0x38 spider_sp1 βœ… βœ…
0x39 spider_sp1 βœ… βœ…
0x3a spider_sp1 βœ… βœ…
0x3b spider_sp1 βœ… βœ…
0x3c spider_sp1 βœ… βœ…
0x3d spider_sp1 βœ… βœ…
0x3e spider_sp1 βœ… βœ…
0x3f spider_sp1 βœ… βœ…
0x40 spider_sp1 βœ… βœ…
0x41 zombie βœ… βœ…
0x42 zombie βœ… βœ…
0x43 zombie βœ… βœ…

Spawn template ids (direct type/flags map)

The large template switch inside creature_spawn_template assigns type_id and sometimes flags. The table below lists only these direct assignments (useful for labeling).

It does not capture randomized parameters, formation spawns, spawn slots, or tail modifiers.

Generated by uv run scripts/gen_spawn_templates.py.

Spawn id (template_id) Type id Creature Flags (creature_flags) Anim note
0x0 0 zombie 0x44 long strip (0x40 overrides 0x4)
0x1 4 spider_sp2 0x8
0x3 3 spider_sp1 ``
0x4 1 lizard ``
0x5 4 spider_sp2 ``
0x6 2 alien ``
0x7 2 alien 0x4 short strip (ping-pong)
0x8 2 alien 0x4 short strip (ping-pong)
0x9 2 alien 0x4 short strip (ping-pong)
0xa 2 alien 0x4 short strip (ping-pong)
0xb 2 alien 0x4 short strip (ping-pong)
0xc 2 alien 0x4 short strip (ping-pong)
0xd 2 alien 0x4 short strip (ping-pong)
0xe 2 alien 0x4 short strip (ping-pong)
0xf 2 alien ``
0x10 2 alien 0x4 short strip (ping-pong)
0x11 1 lizard ``
0x12 2 alien ``
0x13 2 alien ``
0x14 2 alien ``
0x15 2 alien ``
0x16 1 lizard ``
0x17 3 spider_sp1 ``
0x18 2 alien ``
0x19 2 alien ``
0x1a 2 alien ``
0x1b 3 spider_sp1 ``
0x1c 1 lizard ``
0x1d 2 alien ``
0x1e 2 alien ``
0x1f 2 alien ``
0x20 2 alien ``
0x21 2 alien ``
0x22 2 alien ``
0x23 2 alien ``
0x24 2 alien ``
0x25 2 alien ``
0x26 2 alien ``
0x27 2 alien 0x400 bonus_id=WEAPON (3), duration_override=5 (packed in link_index)
0x28 2 alien ``
0x29 2 alien ``
0x2a 2 alien ``
0x2b 2 alien ``
0x2c 2 alien ``
0x2d 2 alien ``
0x2e 1 lizard ``
0x2f 1 lizard ``
0x30 1 lizard ``
0x31 1 lizard ``
0x32 3 spider_sp1 ``
0x33 3 spider_sp1 ``
0x34 3 spider_sp1 ``
0x35 4 spider_sp2 ``
0x36 2 alien ``
0x37 4 spider_sp2 0x100
0x38 3 spider_sp1 0x80
0x39 3 spider_sp1 0x80
0x3a 3 spider_sp1 0x10 projectile_type=9
0x3b 3 spider_sp1 ``
0x3c 3 spider_sp1 0x100 projectile_type=26 (packed in orbit_radius)
0x3d 3 spider_sp1 ``
0x3e 3 spider_sp1 ``
0x3f 3 spider_sp1 ``
0x40 3 spider_sp1 ``
0x41 0 zombie ``
0x42 0 zombie ``
0x43 0 zombie ``

Notes:

  • src/crimson/creatures/spawn.py contains both the spawn-id labeling index (direct type/flags) and the pure plan builder (formations/spawn slots/tail modifiers).

Spawn id sources (call sites)

template_id is supplied by a mix of scripted spawners and data tables:

  • demo_setup_variant_0 (FUN_00402ed0), demo_setup_variant_2 (FUN_00402fe0), demo_setup_variant_1 (FUN_004030f0), demo_setup_variant_3 (FUN_00403250): mode setup helpers called from demo_mode_start (FUN_00403390) (hard‑coded spawn ids like 0x34, 0x35, 0x38, 0x41, 0x24, 0x25).

  • survival_update (FUN_00407cd0): milestone spawns using 0x12, 0x2b, 0x2c, 0x35, 0x38, 0x3a, 0x3c, and 1. Regular enemy waves are spawned via survival_spawn_creature (FUN_00407510), which selects type/stats based on player_experience (not a spawn id). Python models: advance_survival_spawn_stage, tick_survival_wave_spawns, build_survival_spawn_creature.

  • Rush mode (rush_mode_update, FUN_004072b0): spawns edge waves via creature_spawn (type ids 2/3), not creature_spawn_template. Python models: tick_rush_mode_spawns, build_rush_mode_spawn_creature.

  • Tutorial timeline (tutorial_timeline_update, FUN_00408990): scripted spawns using 0x24, 0x26, 0x27, 0x28, 0x40. Python models: build_tutorial_stage3_fire_spawns, build_tutorial_stage4_clear_spawns, build_tutorial_stage5_repeat_spawns, build_tutorial_stage6_perks_done_spawns.

  • Quest/timeline spawner (quest_spawn_timeline_update, FUN_00434250): pulls spawn ids from the table at DAT_004857a8 (pfVar4[3]) with counts in pfVar4[5]. Python model: crimson.quests.timeline.tick_quest_spawn_timeline (see also tick_quest_mode_spawns for the quest_mode_update gating).

  • AI subspawns (creature_update_all): periodic spawns using &DAT_00484fe4 + iVar6 * 0x18, which is seeded for some template ids inside creature_spawn_template (FUN_00430af0).

Quest spawn table (DAT_004857a8)

Quests populate a fixed table at DAT_004857a8 (entry size 0x18, count in DAT_00482b08). quest_spawn_timeline_update (FUN_00434250) walks the table and spawns entries whose trigger time has elapsed.

Entry layout (dwords):

Offset Field Notes
0x00 x base spawn X; if offscreen, the group spreads along Y instead of X.
0x04 y base spawn Y.
0x08 heading passed as heading to creature_spawn_template.
0x0c spawn id cast to int and passed as template_id to creature_spawn_template.
0x10 trigger time compared against DAT_00486fd0 (quest clock).
0x14 count number of spawns in the group; decremented to 0 after firing.

Notes:

  • quest_start_selected (FUN_0043a790) chooses a quest builder from the table at DAT_00484730. The function pointer lives at &DAT_0048474c; when null, it falls back to quest_build_fallback (FUN_004343e0) (two entries with spawn id 0x40, counts 10/0x14, trigger times 500/5000).

  • quest_build_zombie_time (0x00437d70), quest_build_lizard_raze (0x00438840), and quest_build_surrounded_by_reptiles (0x00438940) are examples of quest builders that write multiple DAT_004857a8 entries with varying spawn ids and timings.

Repo references

  • Decompile extracts (used for reconciliation):
  • artifacts/creature_spawn_template/ghidra.c
  • artifacts/creature_spawn_template/ida.c
  • artifacts/creature_spawn_template/binja-hlil.txt
  • Creature pool + spawn-slot fields: docs/creatures/struct.md
  • Rewrite model (pure plan builder): src/crimson/creatures/spawn.py
  • Survival mode (pure models): src/crimson/creatures/spawn.py
  • advance_survival_spawn_stage, tick_survival_wave_spawns, build_survival_spawn_creature
  • Tests: tests/test_survival_milestones.py, tests/test_survival_wave.py, tests/test_survival_spawn.py
  • Rush mode (pure models): src/crimson/creatures/spawn.py
  • tick_rush_mode_spawns, build_rush_mode_spawn_creature
  • Tests: tests/test_rush_mode_spawn.py
  • Tutorial timeline (pure models): src/crimson/creatures/spawn.py
  • build_tutorial_stage3_fire_spawns, build_tutorial_stage4_clear_spawns, build_tutorial_stage5_repeat_spawns, build_tutorial_stage6_perks_done_spawns

  • Tests: tests/test_tutorial_timeline_spawns.py

  • Quest timeline (pure model): src/crimson/quests/timeline.py
  • tick_quest_spawn_timeline, tick_quest_mode_spawns, quest_spawn_table_empty
  • Tests: tests/test_quest_spawn_timeline.py, tests/test_quest_mode_spawns.py
  • MSVCRT-compatible RNG for deterministic replays: src/grim/rand.py (Crand)