10 KiB
DEMO.md — "Morning Coffee"
The domain/ folder contains a 5-minute mini-game that exercises every
important pncdsl feature in one sitting — without the demo itself
outgrowing the library. The code is a good place to learn from, because
each mechanic appears in exactly one small file.
Story
You've just woken up in your bedroom. The cat (Whiskers) is already meowing in the kitchen, and you'd better make coffee before she tears the whole apartment apart.
Running it
go run .
A window opens at 1280×800 (internal resolution 320×200, 4× scale). No
asset files are needed — every graphic is a colored placeholder /
stylized humanoid until you replace it with real art (see
GFX.md).
Controls:
- left click on a hotspot/UI button: run the active verb
- left click on an inventory item: select it (the cursor picks it up)
- left click on a hotspot while an item is selected: use-with
- right click: deselect the held item, or reset the verb to "Look"
- click during dialog: advance
- F1: toggle the hotspot debug overlay (yellow rectangles + names on every clickable region in the current scene)
Walkthrough (spoilers)
- Bedroom — the
introscript runs twoSaylines from theplayer. - Click the nightstand with the "Take" verb → you receive
key. - Click the door → you switch to the kitchen.
- Kitchen — Whiskers is sitting in the corner. Try "Talk" on her — a conditional dialog opens; one of the branches is only available while the cupboard hasn't been opened yet.
- Click the shelf with "Take" → you receive
mug. - Click the cupboard with "Use" (key in inventory) → it opens, you
automatically receive
beans, and the key is consumed. Or select the key in the inventory and click the cupboard —OnUseWithdoes the same thing. - Click the coffee machine with
beans→ beans loaded. - Click the coffee machine with
mug→ mug in place. - Once both flags are set, the coffee machine fires the
victoryscript: two characters talk, a sound plays (stubbed), end card → done.
Scenes
| ID | Background | Music | Contents |
|---|---|---|---|
bedroom |
bg/bedroom |
mus/wakeup |
bed, nightstand (key), door |
kitchen |
bg/kitchen |
mus/calm |
cupboard, shelf, coffee machine, cat (NPC), door |
Items
| ID | Acquired from | Used for |
|---|---|---|
key |
bedroom.nightstand → Take |
Use on kitchen.cupboard, or as a selected item via OnUseWith |
beans |
kitchen.cupboard (after unlocking) |
Selected, used on kitchen.coffee_machine |
mug |
kitchen.shelf → Take |
Selected, used on kitchen.coffee_machine |
Characters
player— the player character. Walks (Walk), talks (Say). Size 28×62 (W×H), roughly the SCUMM-style 30% screen height.cat— Whiskers, the household pet. Present in the kitchen, only responds toTalk, has a dialog tree.
Until you provide real sprites, they're rendered as a stylized humanoid (skin-tone head + colored torso + dark trousers + feet) and quadruped (side-view with ears, head, eye, tail) placeholder shape.
Dialog (dialog.cat_morning.go)
[start]
cat: Meeeoooow. It's late.
> "Coffee's coming right up." → SetFlag(promised_coffee), end
> "Did you eat the beans?" (only while NOT cupboard_open) → goto beans
> "Leave me alone." → end
[beans]
cat: They're in the cupboard. They're always in the cupboard.
> "Thanks." → end
The Show: p.Not(p.Flag(FlagCupboardOpen)) condition makes the "Did
you eat the beans?" choice disappear once you've opened the cupboard
— this demonstrates conditional choice visibility
(DialogueChoice.Show).
Cutscenes
script.intro.go — wired up to OnStart, runs in the bedroom:
p.Seq(
p.Wait(0.4),
p.Say("player", "Brr, it's cold."),
p.Say("player", "I need coffee before my head explodes."),
p.Walk("player", p.Point{X: 200, Y: 145}),
)
script.victory.go — fired when the coffee machine sees both flags:
p.Seq(
p.Say("player", "All done!"),
p.PlaySound("snd/coffee_done"),
p.Wait(0.6),
p.Say("cat", "Mrrrrow. *content cat noises*"),
p.Say("player", "Morning coffee: saved."),
p.ShowEnd("The End — thanks for playing!"),
)
Flag/var table (domain.flag.go)
const (
FlagKeyTaken = "key_taken"
FlagCupboardOpen = "cupboard_open"
FlagMugTaken = "mug_taken"
FlagCoffeeHasBeans = "coffee_has_beans"
FlagCoffeeHasMug = "coffee_has_mug"
FlagPromisedCoffee = "promised_coffee"
)
Naming the flags as constants is worth it because the type system then
catches typos — pncdsl.Flag("cupbord_open") would otherwise silently
return false forever.
What the demo exercises
| Library feature | Where it shows up |
|---|---|
Scene + GoTo |
bedroom ↔ kitchen door, with fade transition |
Hotspot with every verb |
each scene has multiple hotspots: Look / Use / Talk / Take |
Inventory + Give / TakeAway |
picking up and consuming key, beans, mug |
RequireItem semantics |
cupboard checks HasItem("key") |
OnUseWith (from the hotspot) |
cupboard + key |
OnUseWith (from the item) |
coffee machine + beans / mug |
Flag + If |
cupboard "already open", shelf "already took one" |
Dialogue + Choice.Show |
cat's dialog tree, vanishing branch driven by a flag |
Script / cutscene |
intro (walk+say) + victory (multi-actor, sound, end card) |
Walk with straight-line stepping |
every interaction is preceded by a walk target |
ShowEnd |
last action of the victory script |
Validate() |
domain/build_test.go CI-friendly smoke test |
RegisterDefaultUI + widget tree |
game.go registers the SCUMM-style HUD widgets |
Theme system (classic-scumm) |
every color comes from the active theme; runtime-switchable |
HotspotDebug widget |
F1 in-game toggles a yellow outline over every clickable area |
Files in domain/
domain/
├── game.go # Build() — calls every define*() in order
├── domain.asset.go # registerAssets(g) — all assets in one place
├── domain.flag.go # flag-name constants
│
├── character.player.go # definePlayer(g)
├── character.cat.go # defineCat(g)
│
├── item.key.go # defineKey(g) — OnUseWith: {"cupboard": ...}
├── item.beans.go # defineBeans(g)
├── item.mug.go # defineMug(g)
│
├── dialog.cat_morning.go # defineCatMorning(g) — 2 nodes, 3+1 choices
│
├── scene.bedroom.go # defineBedroom(g)
├── scene.kitchen.go # defineKitchen(g)
│
├── script.intro.go # defineIntroScript(g)
├── script.victory.go # defineVictoryScript(g)
│
└── build_test.go # Build() + Validate() smoke test
Adding a new scene/item: one new file + one line in Build() inside
game.go. There's no central registry list to edit.
How to extend it
- New scene: create
scene.<name>.gowith adefine<Name>(g *p.Game)function that callsg.SceneManager.Register(...). Add a call inBuild(). If any hotspot usesGoTo("<name>"),Validate()will cross-check the link for you. - New item: same pattern,
item.<name>.go, register intog.ItemManager. Map keys insideOnUseWithmust exactly match the target hotspot'sNamefield. - New dialog:
dialog.<name>.go—DialogueChoice.Showaccepts anything that implementsCondition(Flag,HasItem,Not(...), etc.). - New cutscene:
script.<name>.go, register intog.ScriptManager, then fire it anywhere withp.RunScript("<name>").
UI customisation
The demo calls p.RegisterDefaultUI(g) for the classic SCUMM HUD, but
the whole UI is a tree of Widgets in g.UIManager — swap pieces or
add your own:
// 1. Verb coin instead of verb bar
p.RegisterRadialVerbUI(g) // alternative preset
// 2. Or: start from the SCUMM default, then customise
p.RegisterDefaultUI(g)
g.UIManager.Remove("verbs") // drop the verb bar
g.UIManager.Register(&p.RadialVerbs{Name: "verbs", Radius: 40})
// 3. Add a custom widget — anything implementing pncdsl.Widget plugs in
g.UIManager.Register(&ChatPanel{Name: "chat", Bounds: p.Rect(220, 4, 96, 130)})
// 4. Change the theme at any point
g.UseTheme("terminal-green") // classic-scumm | sierra-coin | paper-notebook | terminal-green
The built-in widget catalogue (each in its own pncdsl/ui.<name>.go):
Panel, StatusLine, VerbBar, RadialVerbs, InventoryBar,
SpeechBubble, DialogBox, EndCard, Cursor, HotspotDebug.
Widget is a three-method interface (GetName, Tick, Draw); see
UIPLAN.md for the design rationale and a chat-panel
example.
Graphics
The library substitutes a deterministic colored rectangle for every
missing image and renders characters as a stylized humanoid/quadruped
shape. The game therefore runs without any asset on disk. When you drop
a generated image into assets/bg/<name>.png or
assets/spr/<name>.png, the library switches to it immediately — no
code change required.
Prompts and positioning tips for an image AI: GFX.md.
Back to the library
For the framework structure and the Manager pattern itself, see
README.md and PLAN.md. The widget-based UI
design is in UIPLAN.md.