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
46 lines
1.4 KiB
Python
46 lines
1.4 KiB
Python
"""UNWRITTEN - modal input dispatch. Owner: Agent A.
|
|
|
|
One InputStack per scene owns scene.on_key and dispatches to a stack of
|
|
handlers, top-most first. A handler is fn(key, state) -> bool; returning True
|
|
consumes the event (lower handlers do not see it). Overworld pushes its
|
|
movement handler; a DialogueBox / menu pushes itself while open and pops on
|
|
close, freezing everything beneath it.
|
|
"""
|
|
|
|
|
|
class InputStack:
|
|
def __init__(self, scene):
|
|
self.scene = scene
|
|
self.stack = [] # list of (name, handler)
|
|
scene.on_key = self._dispatch
|
|
|
|
def push(self, handler, name=""):
|
|
self.stack.append((name, handler))
|
|
|
|
def pop(self, name=""):
|
|
"""Pop the top handler, or the top-most handler matching name."""
|
|
if name:
|
|
for i in range(len(self.stack) - 1, -1, -1):
|
|
if self.stack[i][0] == name:
|
|
return self.stack.pop(i)
|
|
return None
|
|
if self.stack:
|
|
return self.stack.pop()
|
|
return None
|
|
|
|
def replace(self, handler, name=""):
|
|
"""Replace the top handler with a new one."""
|
|
self.pop()
|
|
self.push(handler, name)
|
|
|
|
def clear(self):
|
|
self.stack = []
|
|
|
|
def has(self, name):
|
|
return any(n == name for n, _ in self.stack)
|
|
|
|
def _dispatch(self, key, state):
|
|
for i in range(len(self.stack) - 1, -1, -1):
|
|
handler = self.stack[i][1]
|
|
if handler(key, state):
|
|
return
|