Skip to main content
Part of the make-game skill. The skill links here when your agent needs the detail.
The systems layer on top of 2d-survivors.md: enemy roster, wave-driven spawning, elites, level-up upgrades, the between-wave shop, weapon evolution, and characters. Build these only after the core loop (move + auto-fire + chase) runs.

Enemy roster (one script, many behaviors)

All types share enemy_base.gd (Area2D, “enemies” group), configured through setup(type, wave) - no per-type scenes. HP/speed scale with wave number: Behavior dispatch is a match behavior: in _physics_process. Give every enemy a randomized wobble (_wobble_phase = randf() * TAU, per-enemy speed and amplitude) so hordes look organic instead of converging in a straight line. Charger is a 4-state machine: WALK -> TELEGRAPH (0.45s, within 320px trigger range) -> CHARGE (0.4s at 550px/s in the locked direction) -> RECOVER (0.3s), then 3.5s cooldown. The telegraph is what makes it fair - flash/scale the sprite during it. Knockback: on hit, set _knockback_vel = (pos - source).normalized() * 180, decay with move_toward(Vector2.ZERO, 900 * delta) and add to movement.

Hit feedback and death (non-negotiable)

take_damage(amount, source_pos, is_crit) must: flash the sprite, spawn a floating damage number (bigger + colored on crit), spawn directional particles, apply knockback, show the health bar. Death: GameManager.add_kill(), add_xp(xp_value), drop gold (rare health orb), particle burst, then disable before animating out:
The set_deferred pair matters: freeing an Area2D mid-physics-callback without disabling monitoring first throws errors.

Spawner and wave data

The spawner reads a per-wave config and spawns groups on an interval, always off-screen (650-900px from the player, clamped to arena bounds):
Each WaveConfig: enemy_weights: Dictionary, spawn_interval, group_size, max_alive, elite_chance, boss: bool, hazards: int, duration. Type selection is weighted random over the dictionary. Hand-tune 20 waves; the ramp that matters: Do NOT scale difficulty by HP alone. The ramp is spawn rate + group size + max alive + type diversity + elite chance + new mechanics at fixed waves. Bosses spawn 3s into their combat phase via a scene-tree timer. Arena hazards from wave 3+: spike traps (static) and fire pools (drift at 15-35px/s). Tick damage every 0.6s (6 + wave*1.5) to enemies in radius, half damage to the player - positioning pressure for both sides.

Elites

elite_system.gd is a static RefCounted helper applied at spawn time with elite_chance probability. One of four modifiers, 3x XP:
  • FAST: speed x1.8, HP x1.3, lightened color
  • TANKY: HP x3, scale x1.3, contact damage x1.5, darkened
  • SPLITTING: HP x1.5, green; on death spawns 2 walker copies at 0.6x scale, 0.3x HP (use call_deferred("add_child", copy) - you are inside a physics callback)
  • VAMPIRIC: HP x2; heals 8% max HP every 3s
Give elites a visible pulse tween so players can prioritize them.

Two-track progression

Track 1 - mid-wave level-ups: on level-up the tree pauses, 3 random stat upgrades from the pool are offered, each stackable to level 3. Also vacuum all pickups (attract() on the “pickups” group) as a reward beat. get_random_choices(3) filters out maxed upgrades, shuffles, takes 3. apply_upgrade bumps level and writes the stat straight onto GameManager. Armor applies as maxi(1, amount - player_armor) - always at least 1 damage. Track 2 - between-wave shop: 4 random items, gold-priced, 4 tiers (Common / Uncommon / Rare / Legendary). Tier odds shift with wave - by wave 16+: 15% Legendary / 30% Rare / 35% Uncommon / 20% Common; waves 1-5 have no Legendary at all. Prices scale cost_base * (1.0 + wave * 0.12); reroll costs 2 + reroll_count + wave / 5 (escalates within a single shop visit). Items are either stat dictionaries applied to GameManager ({"player_armor": 2}) or weapon grants that call player.add_or_upgrade_weapon(id). The interesting ones trade off:
Do not skip the shop. Without a between-wave decision point the game is just “survive longer”; trade-off items are what create builds.

Weapon evolution

A base weapon evolves when the player owns it AND a specific catalyst upgrade is maxed (level 3). Check after every upgrade pick and shop purchase; emit evolution_available once per recipe: Evolving replaces the node: queue_free() the old weapon, erase from the player’s _weapons dict, instantiate the evolved script, screen flash + zoom pulse. DOT zones (Supernova) are a Polygon2D + tween loop: N tick callbacks at 0.5s intervals doing a group distance scan, then fade and free. Guard every tick with is_instance_valid - the zone can outlive enemies and vice versa.

Characters

Four picks before the run, each a starting weapon + stat override dictionary:
  • Sentinel: projectile, no overrides (baseline)
  • Berserker: projectile, damage_mult 1.4, speed 240, max HP 80
  • Warden: orbital, max HP 150, armor 3, attack_speed 0.85
  • Arcanist: nova, attack_speed 1.25, damage_mult 1.15, max HP 70
GameManager.reset_run() resets all stats to base, then applies selected_character.stat_overrides via set(key, value) and fills health to max. Character select is just 4 cards that set selected_character and load the arena.

UI stack (all procedural, all CanvasLayer, all process_mode ALWAYS)

  • HUD: health + dash bars top-left, wave counter + timer top-center, gold/kills/level top-right, full-width 6px XP bar at the bottom.
  • LevelUpUI (layer 20): dark overlay, 3 cards, gamepad focus.
  • ShopUI (layer 25): stats panel left, 4 tier-colored item cards, reroll + next-wave buttons, “SOLD” placeholders, live stat refresh after purchase.
  • PauseMenu / GameOverUI (layer 40): game over shows victory or death plus time / kills / level / wave, retry + menu.
Retry = get_tree().paused = false THEN reload_current_scene(). Forgetting the unpause is the classic frozen-retry bug.

Audio integration points

Pooled AudioManager (16 global + 16 positional players, round-robin; all play methods null-safe so missing streams never crash). Pitch variance 5% global / 10% positional prevents machine-gun repetition. Hook: enemy hit + death (positional), pickup collect, level-up, shop purchase, weapon fire (-6dB, it fires constantly), music on combat start with fade.
Source: library/skills/make-game/references/2d-survivors-progression.md, rendered from summer-engine@4.3.0 on npm.