Part of the
make-game skill. The skill links here when your agent needs the detail.2d-rpg.md: enemies, NPCs and dialogue, quests,
inventory, loot, perks, status effects, and save/load. Everything here talks
to the rest of the game only through EventBus and the autoloads.
Enemies (data-driven, five archetypes)
EnemyData resource defines stats + behavior; enemy_base.gd implements all five behaviors and reads the data. No per-enemy scenes.- CHASE (slime): direct pursuit, melee lunge, low knockback resistance.
- RANGED (skeleton archer): holds preferred distance, fires projectiles, strafes at range, backs off when crowded.
- SWARM (bat): fast, random jitter movement, contact damage.
- TANK (golem): slow, high HP, knockback_resistance 0.8, ground slam with screen shake.
- BOSS (dark mage): cycling pattern (single shot -> fan of 3 -> charge),
phase 2 at 40% HP (faster everything + aura), can summon minions. Emits
boss_health_changedfor the HUD boss bar.
db.get_enemy("slime"), db.get_enemies_for_wave(5) returns a
wave-appropriate mix. Built-ins: slime, skeleton_archer, bat, golem,
dark_mage.
Contract: every enemy is in the “enemies” group and exposes
take_hit(damage: int, source_position: Vector2, is_crit: bool = false).
Weapons and projectiles call it directly. Death drops XP + gold orbs via
_drop_loot() and emits EventBus.enemy_killed(enemy, xp_value).
EnemySpawner is wave-based: wave_interval 12s, enemies_per_wave_base 3,
enemies_per_wave_growth 1, max_enemies 20, difficulty_scale 1.08
(HP/damage multiplier per wave). spawn_boss("dark_mage", pos) for arenas.
NPCs and dialogue
DialogueData is a resource with a speaker name and a lines array. Each line is a dictionary supporting optional keys:start_quest:<id>, give_item:<resource_path>, give_gold:<n>,
heal. Conditions: quest_complete:<id>, quest_active:<id>,
has_item:<id>. This tiny string DSL covers most RPG dialogue needs -
resist building a graph editor.
Interaction flow (the pause dance):
add_child it, THEN set script properties (npc_name, dialogue,
quest_to_give) - properties set before the script enters the tree can be
stomped by _ready.
DialogueBox: typewriter at 40 chars/sec, click skips to full text, click
again advances, choices render as buttons. Must be process_mode ALWAYS (tree
is paused during dialogue).
Quests
QuestData: id, title, description, objectives array ({"id": "kill_slimes", "description": "Kill 3 slimes", "target": 3}),
xp_reward, gold_reward, prerequisite_quest_id.
QuestManager (autoload) API: start_quest(data),
progress_objective(quest_id, objective_id, amount = 1),
is_quest_active/complete(id), get_objective_progress(id, obj_id). When
all objectives hit target the quest auto-completes, grants rewards, and
emits quest_completed.
Wiring kills to quests happens in the LEVEL script, not the enemy:
Inventory and items
InventoryManager (autoload): 20 item slots, 3 equipment slots (weapon, armor, accessory), gold.add_item() returns false when full;
use_item() only works on consumables; equip_item() auto-routes by the
item’s slot; get_total_stat_bonus("attack") sums equipment bonuses for the
stats pipeline; clear() on restart.
ItemData: ItemType (CONSUMABLE/WEAPON/ARMOR/ACCESSORY/KEY_ITEM), Rarity
(COMMON/UNCOMMON/RARE/EPIC with white/green/blue/purple colors),
stat_bonuses: Dictionary (“attack” -> 5), heal_amount, buy/sell price.
Loot
- LootTable: guaranteed gold range + guaranteed XP, then
drop_chanceroll over weighted entries ({"item", "weight", "min_count", "max_count"}).table.roll()returns the drops. - PickupOrb: XP/gold orbs from dead enemies, magnet-pull within 100px, accelerating toward the player.
- TreasureChest: interactable static body - guaranteed item + gold orbs + a loot table roll on open.
- ItemDrop: dropped items render with a rarity glow.
Perks (level-up progression)
PerkData: id, name, description (supports a{value} token), max_level 3,
stat_modifiers: Dictionary, special_effect: String + effect_value for
non-stat perks. Built-in pool:
PerkPool:
roll_selection(3) (unowned or upgradable only),
acquire_perk(), get_stat_bonus("attack"), has_effect("lifesteal") +
get_effect_value(), reset() per run.
Level-up flow: stats.add_xp() true -> emit player_leveled_up then
level_up_ready -> PerkSelectionUI shows 3 cards (LEVEL_UP state pauses
tree) -> pick emits perk_selected -> player acquires and applies stat
modifiers via stats.add_modifier -> resume. Special effects (lifesteal,
thorns, multishot) are checked at their point of use via
pool.has_effect(), not applied as stats.
Status effects
StatusEffect base (Node): effect_id, duration 3s, tick_interval 1s, max_stacks 3. Overridetick(), on_expire(), on_max_stacks(),
get_speed_multiplier(). add_stack() refreshes duration.
EffectManager attaches to any entity: apply_effect(BurnEffect, entity)
handles stacking, get_speed_multiplier() returns the product of all active
effects (feed it into movement), remove_effect/remove_all.
Built-ins and their max-stack payoffs (the interesting design bit -
stacking to cap triggers a burst):
- Burn: 3 dmg per 0.5s, 5 stacks; at cap, a 15x damage burst.
- Freeze: 30% slow per stack, 3 stacks; at cap, full 2s stun.
- Poison: 2 dmg/s, 3 stacks; each new stack extends duration 2s; supports lifesteal back to the source.
Juice quick reference
register_camera is never called, shake silently does nothing - a
classic “why is there no juice” bug.
HUD and notifications
HUD subscribes to EventBus only: health/energy/XP bars, level, gold, active quest list, and a notification label (fade in, 2s hold, fade out) driven byEventBus.notification_requested.emit("Quest complete!", Color.GOLD).
HUD also owns pause and inventory toggles.
Audio
AudioManager:play_music(path) crossfades (pass false for hard cut),
play_sfx(stream, pitch_variation), play_sfx_at(stream, pos, variation)
positional. Auto-creates Music and SFX buses if missing. Town theme in the
hub, battle music in combat areas, switch on scene_ready.
Save/load
GameManager saves touser://savegame.cfg (ConfigFile): current level path
- spawn position, player stats (level, XP, health, attack, defense),
inventory + equipment as resource paths, gold, quest state (active with
progress, completed ids).
save_game()from the pause menu,load_game() -> boolfrom Continue,has_save()gates the button.restart_game()clears inventory, resets perks, returns to town.
Order of content work
- Enemy roster (CHASE first, then RANGED; TANK/SWARM/BOSS after combat feels right) + spawner in the forest level.
- Loot orbs + level-up perks (progression pull).
- One NPC with dialogue in town, one kill quest wired through the level.
- Inventory + a treasure chest + equipment stat bonuses.
- Status effects on weapons/perks (burning_strikes, frost_touch).
- Boss + dungeon kill gate, then save/load last - it serializes everything above, so build it when the shapes are stable.
Source:
library/skills/make-game/references/2d-rpg-content.md, rendered from summer-engine@4.3.0 on npm.
