# 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 ```bash 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`](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) 1. **Bedroom** — the `intro` script runs two `Say` lines from the `player`. 2. Click the **nightstand** with the "Take" verb → you receive `key`. 3. Click the **door** → you switch to the kitchen. 4. **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. 5. Click the **shelf** with "Take" → you receive `mug`. 6. 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 — `OnUseWith` does the same thing. 7. Click the **coffee machine** with `beans` → beans loaded. 8. Click the coffee machine with `mug` → mug in place. 9. Once both flags are set, the coffee machine fires the `victory` script: 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 to `Talk`, 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: ```go 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: ```go 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`) ```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..go` with a `define(g *p.Game)` function that calls `g.SceneManager.Register(...)`. Add a call in `Build()`. If any hotspot uses `GoTo("")`, `Validate()` will cross-check the link for you. - **New item**: same pattern, `item..go`, register into `g.ItemManager`. Map keys inside `OnUseWith` must exactly match the target hotspot's `Name` field. - **New dialog**: `dialog..go` — `DialogueChoice.Show` accepts anything that implements `Condition` (`Flag`, `HasItem`, `Not(...)`, etc.). - **New cutscene**: `script..go`, register into `g.ScriptManager`, then fire it anywhere with `p.RunScript("")`. ## UI customisation The demo calls `p.RegisterDefaultUI(g)` for the classic SCUMM HUD, but the whole UI is a tree of `Widget`s in `g.UIManager` — swap pieces or add your own: ```go // 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..go`): `Panel`, `StatusLine`, `VerbBar`, `RadialVerbs`, `InventoryBar`, `SpeechBubble`, `DialogBox`, `EndCard`, `Cursor`, `HotspotDebug`. `Widget` is a three-method interface (`GetName`, `Tick`, `Draw`); see [`UIPLAN.md`](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/.png` or `assets/spr/.png`, the library switches to it immediately — no code change required. Prompts and positioning tips for an image AI: [`GFX.md`](GFX.md). ## Back to the library For the framework structure and the Manager pattern itself, see [`README.md`](README.md) and [`PLAN.md`](PLAN.md). The widget-based UI design is in [`UIPLAN.md`](UIPLAN.md).