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
101 lines
4.6 KiB
Python
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]
|