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
104 lines
3.5 KiB
Python
104 lines
3.5 KiB
Python
"""UNWRITTEN - UI kit headless test. Owner: Agent A.
|
|
|
|
Builds a scene, exercises every widget (DialogueBox with a real node from
|
|
script_act1 'intro', a MenuList with a disabled row, a Bar at 30%, a Toast, a
|
|
TitleBanner), drives mcrfpy.step to advance the typewriter, and screenshots two
|
|
meaningful states.
|
|
|
|
Run:
|
|
cd build && ./mcrogueface --headless --exec ../games/unwritten/tests/test_ui_kit.py
|
|
"""
|
|
import os
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
import mcrfpy
|
|
from core import palette, ui
|
|
from data import script_act1
|
|
|
|
SHOTS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "shots")
|
|
|
|
|
|
def step_n(n):
|
|
for _ in range(n):
|
|
mcrfpy.step(1.0 / 60.0)
|
|
|
|
|
|
def main():
|
|
os.makedirs(SHOTS, exist_ok=True)
|
|
scene = mcrfpy.Scene("uikit")
|
|
mcrfpy.current_scene = scene
|
|
ch = scene.children
|
|
|
|
# background
|
|
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=palette.INK)
|
|
ch.append(bg)
|
|
|
|
# --- MenuList with one disabled row (top-left) ---
|
|
menu = ui.MenuList(
|
|
ch, (60, 150), 300,
|
|
items=[
|
|
("Attack", "attack", True),
|
|
("Skill", "skill", True),
|
|
("Item", "item", False, "no items"),
|
|
("Guard", "guard", True),
|
|
],
|
|
title="COMMAND")
|
|
|
|
# --- Bar at 30% (right) ---
|
|
bar_panel = ui.Panel(ch, (600, 150), (340, 70), outline_color=palette.OUTLINE)
|
|
ui.Label(bar_panel.children, "HP", (16, 14), color=palette.PARCH,
|
|
size=palette.SMALL_SIZE)
|
|
hp_bar = ui.Bar(bar_panel.children, (16, 34), (300, 22), palette.BLOOD,
|
|
cur=30, maxv=100)
|
|
|
|
# --- Toast (top-right) ---
|
|
ui.Toast(ch, "+1 Story Point", color=palette.GOLD)
|
|
|
|
# --- TitleBanner (top center) ---
|
|
ui.TitleBanner(ch, "THE GEARWOOD", cx=512, cy=70)
|
|
|
|
# --- DialogueBox: a real NARRATOR node from 'intro', mid-typewriter ---
|
|
box = ui.DialogueBox(ch)
|
|
intro_n1 = script_act1.SCENES["intro"]["nodes"]["n1"]
|
|
box.show_node("NARRATOR", intro_n1["text"], on_advance=lambda: None)
|
|
|
|
# let the entrance settle and reveal part of the body
|
|
step_n(16)
|
|
ok1 = mcrfpy.automation.screenshot(os.path.join(SHOTS, "uikit_1.png"))
|
|
assert ok1, "screenshot 1 failed"
|
|
partial = box.body.text
|
|
assert 0 < len(partial) < len(box._full), \
|
|
"typewriter should be mid-reveal (%d/%d)" % (len(partial), len(box._full))
|
|
|
|
# --- DialogueBox choice mode: a real node with a disabled [ALCHEMY] choice ---
|
|
disp = script_act1.SCENES["shop_dispute"]["nodes"]["n3"]
|
|
# empty active party -> the [ALCHEMY] tag choice is locked (disabled), the
|
|
# two plain choices are enabled. This is the design pillar: shown but DIM.
|
|
choices = []
|
|
for c in disp["choices"]:
|
|
enabled = c.get("req") is None
|
|
choices.append((c["label"], enabled))
|
|
box.show_node("VERA", disp["text"], choices=choices,
|
|
on_choice=lambda i: None)
|
|
|
|
# fully reveal the prompt so the choice list appears
|
|
step_n(180)
|
|
disabled_count = sum(1 for _lbl, en in choices if not en)
|
|
assert disabled_count == 1, "expected exactly one disabled choice"
|
|
assert len(box.choice_caps) == len(choices), "all choices should render"
|
|
ok2 = mcrfpy.automation.screenshot(os.path.join(SHOTS, "uikit_2.png"))
|
|
assert ok2, "screenshot 2 failed"
|
|
|
|
box.destroy()
|
|
menu.destroy()
|
|
|
|
print("uikit_1.png:", os.path.join(SHOTS, "uikit_1.png"), flush=True)
|
|
print("uikit_2.png:", os.path.join(SHOTS, "uikit_2.png"), flush=True)
|
|
print("PASS test_ui_kit", flush=True)
|
|
sys.exit(0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|