McRogueFace/games/unwritten/data/enemies.py
John McCardle 80908f771c UNWRITTEN: complete short-form RPG at games/unwritten/
A 30-45 minute turn-based RPG built overnight as an engine showcase:
six recruitable characters (party of three), four recurring NPCs,
choice-driven dialogue with visible-but-locked skill checks, shops,
loot, levels, two bosses with in-battle Talk options, the reverted
grey-town beat, and an epilogue assembled from the player's actual
choices. 33 dialogue scenes / 171 nodes. Terrain is ColorLayers only;
kenney_tinydungeon supplies characters, items, and props.

Creative direction, story, dialogue, maps, and systems design by
Fable; implementation by four Opus subagents (framework, battle,
overworld, integration). Verified by tests/playthrough.py: a scripted
headless run of all three acts, 20/20 beats passing, plus per-system
unit tests (script integrity, UI kit, 225-battle balance sim,
overworld).

Run: cd build && ./mcrogueface --exec ../games/unwritten/main.py

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014vxWJ6GY384SmZiMVXgrF4
2026-07-03 10:11:12 -04:00

101 lines
4.6 KiB
Python

"""UNWRITTEN - enemy tables and encounter packs. Owner: Agent B.
Numbers canon from SYSTEMS section 4; boss behavior from BIBLE section 4.
Area/variant textures (Pale Echo, Chime-Imp, Grey Echo) are hue/sat-shifted
copies of the shared atlas, built ONCE here at import via TEX.hsl_shift.
ENEMIES : id -> record used by systems.battle to build combatants.
PACKS : pack_id -> list of enemy ids (order = left-to-right on screen).
ASCII only. "Fail early": unknown ids raise.
"""
from core.assets import TEX
# ------------------------------------------------- variant textures (built once)
# Pale Echo: sprite 121 hue -40 deg (hsl_shift takes [0,360) so -40 -> 320).
PALE_ECHO_TEX = TEX.hsl_shift(320.0, 0.0, 0.0)
# Chime-Imp: sprite 110 hue +160 deg (teal).
CHIME_TEX = TEX.hsl_shift(160.0, 0.0, 0.0)
# Grey Echo (Custodian summon): starter sprite 85, drained of color.
GREY_ECHO_TEX = TEX.hsl_shift(0.0, -0.85, 0.02)
def _e(name, sprite, hp, atk, mag, dfn, spd, xp, gold, ai="basic",
labels=None, tex=None, scale=4.0, drop=None):
return {
"name": name, "sprite": sprite,
"HP": hp, "ATK": atk, "MAG": mag, "DEF": dfn, "SPD": spd,
"XP": xp, "gold": gold, "ai": ai,
"labels": set(labels or ()), "tex": tex, "scale": scale, "drop": drop,
}
ENEMIES = {
"gloom_bat": _e("Gloom Bat", 120, 14, 5, 0, 1, 8, 6, 4, ai="bat"),
"hedge_slime": _e("Hedge Slime", 108, 20, 4, 0, 3, 2, 7, 5, ai="slime"),
"slimelet": _e("Slimelet", 108, 8, 3, 0, 1, 3, 0, 0, ai="basic",
scale=3.0),
"loom_spider": _e("Loom Spider", 122, 16, 6, 0, 2, 6, 8, 6, ai="spider"),
"waxwing_scarab": _e("Waxwing Scarab", 123, 24, 6, 0, 5, 3, 10, 8,
ai="scarab"),
"unwoken_skeleton": _e("Unwoken Skeleton", 86, 26, 8, 0, 3, 5, 13, 10,
ai="skeleton", labels=("undead",)),
"pale_echo": _e("Pale Echo", 121, 18, 0, 8, 1, 7, 12, 0, ai="echo",
tex=PALE_ECHO_TEX,
drop=("chalk", 0.40)),
"chime_imp": _e("Chime-Imp", 110, 22, 7, 4, 3, 7, 14, 12, ai="chime",
labels=("construct",), tex=CHIME_TEX),
"mimic": _e("Mimic", 92, 45, 10, 0, 6, 1, 30, 60, ai="mimic"),
# ---------------------------------------------------------------- bosses
"gatekeeper": _e("The Gatekeeper", 20, 150, 11, 6, 8, 3, 80, 100,
ai="gatekeeper", labels=("construct",), scale=7.0),
"custodian": _e("The Custodian", 41, 210, 9, 12, 7, 6, 0, 0,
ai="custodian", labels=("construct",), scale=8.0),
# -------------------------------------------------------- boss summons/misc
# Bell: Chime-Imp stats but the base (red) sprite, unshifted.
"bell": _e("Bell", 110, 22, 7, 4, 3, 7, 0, 0, ai="chime",
labels=("construct",)),
# Grey Echo: Pale Echo stats +50%, desaturated starter sprite.
"grey_echo": _e("Grey Echo", 85, 27, 0, 12, 2, 7, 0, 0, ai="echo",
labels=("construct",), tex=GREY_ECHO_TEX),
}
# ------------------------------------------------------------------------ packs
PACKS = {
# Gearwood (Act 1): soft encounters. gw_slimes is size 2 so slimes split.
"gw_bats": ["gloom_bat", "gloom_bat", "gloom_bat"],
"gw_mixed": ["gloom_bat", "hedge_slime", "loom_spider"],
"gw_mixed2": ["loom_spider", "gloom_bat", "hedge_slime"],
"gw_scarabs": ["waxwing_scarab", "waxwing_scarab"],
"gw_slimes": ["hedge_slime", "hedge_slime"],
# Undercroft (Act 2): proper dungeon. uc_mimic is solo.
"uc_bones": ["unwoken_skeleton", "unwoken_skeleton"],
"uc_bones2": ["unwoken_skeleton", "unwoken_skeleton", "waxwing_scarab"],
"uc_chimes": ["chime_imp", "chime_imp"],
"uc_echoes": ["pale_echo", "pale_echo", "unwoken_skeleton"],
"uc_mixed": ["unwoken_skeleton", "pale_echo", "waxwing_scarab"],
"uc_mimic": ["mimic"],
# Act 3 Study approach: pale echoes only (the world is running out).
"st_echoes": ["pale_echo", "pale_echo"],
"st_echoes2": ["pale_echo", "pale_echo", "pale_echo"],
# Bosses (BIBLE section 4). Gatekeeper flanked by two Chime-Imps.
"boss_gatekeeper": ["chime_imp", "gatekeeper", "chime_imp"],
"boss_custodian": ["custodian"],
}
def pack(pack_id):
if pack_id not in PACKS:
raise KeyError("unknown battle pack %r in data.enemies.PACKS" % (pack_id,))
return list(PACKS[pack_id])
def enemy(enemy_id):
if enemy_id not in ENEMIES:
raise KeyError("unknown enemy id %r in data.enemies.ENEMIES" % (enemy_id,))
return ENEMIES[enemy_id]