remove demo game

This commit is contained in:
2026-05-25 21:28:17 +02:00
parent b1ea3447a8
commit 76f910ab36
75 changed files with 115 additions and 815 deletions

252
DEMO.md
View File

@@ -1,252 +0,0 @@
# 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.<name>.go` with a `define<Name>(g *p.Game)`
function that calls `g.SceneManager.Register(...)`. Add a call in
`Build()`. If any hotspot uses `GoTo("<name>")`, `Validate()` will
cross-check the link for you.
- **New item**: same pattern, `item.<name>.go`, register into
`g.ItemManager`. Map keys inside `OnUseWith` must exactly match the
target hotspot's `Name` field.
- **New dialog**: `dialog.<name>.go``DialogueChoice.Show` accepts
anything that implements `Condition` (`Flag`, `HasItem`, `Not(...)`,
etc.).
- **New cutscene**: `script.<name>.go`, register into
`g.ScriptManager`, then fire it anywhere with
`p.RunScript("<name>")`.
## 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.<name>.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/<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`](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).

206
README.md
View File

@@ -7,8 +7,16 @@ single `*Game` root. The library handles input, rendering, dialog trees,
cutscenes, HUD, themes and lazy asset loading; the domain only declares cutscenes, HUD, themes and lazy asset loading; the domain only declares
content. content.
This document is the full library reference. For a 5-minute tour of the This document is the full library reference. The companion `pncdsl-demo`
shipped demo, see [`DEMO.md`](DEMO.md). repo at <ssh://git@git.teletype.hu:2222/games/pncdsl-demo> is a small
end-to-end example ("Morning Coffee") that consumes this library as a
plain Go module.
**Module path:** `git.teletype.hu/games/pncdsl`
```go
import "git.teletype.hu/games/pncdsl"
```
--- ---
@@ -81,8 +89,9 @@ fluent chains, no scattered registries. The same mental model applies to
items, scenes, characters, dialogues, scripts, assets, verbs, widgets and items, scenes, characters, dialogues, scripts, assets, verbs, widgets and
themes alike. themes alike.
A complete example game ("Morning Coffee") lives in `domain/` and is A complete example game ("Morning Coffee") lives in the separate
described in [`DEMO.md`](DEMO.md). [`pncdsl-demo`](ssh://git@git.teletype.hu:2222/games/pncdsl-demo) repo
and consumes this library as a regular Go module.
--- ---
@@ -91,18 +100,21 @@ described in [`DEMO.md`](DEMO.md).
**Prerequisites.** Go 1.24+ (for generic type aliases) and a working OpenGL **Prerequisites.** Go 1.24+ (for generic type aliases) and a working OpenGL
context. Ebitengine handles windowing and the render loop. context. Ebitengine handles windowing and the render loop.
**Run the bundled demo:** **Use the library in your own project:**
```bash ```bash
git clone <repo> go mod init my-game
cd pncdsl go get git.teletype.hu/games/pncdsl
go run .
``` ```
A window opens at 1280×800 (the library uses a 320×200 internal resolution If the private host can't be reached over HTTPS, point Go at the SSH
and Ebitengine scales it 4×). No asset files are required — placeholders endpoint:
are generated for anything missing under `assets/`. Press **F1** at any
time to toggle the hotspot debug overlay. ```bash
git config --global \
url."ssh://git@git.teletype.hu:2222/".insteadOf "https://git.teletype.hu/"
export GOPRIVATE=git.teletype.hu/*
```
**Minimum game:** **Minimum game:**
@@ -111,7 +123,7 @@ package main
import ( import (
"log" "log"
"pncdsl/pncdsl" "git.teletype.hu/games/pncdsl"
) )
func main() { func main() {
@@ -1519,10 +1531,13 @@ should crash loudly in development.
## 20. Testing ## 20. Testing
```bash ```bash
go test ./... go build ./...
``` ```
`domain/build_test.go` is the canonical headless smoke test: The library is single-package and has no in-tree unit tests at this
milestone — the manager + action primitives are intentionally small
enough that the `pncdsl-demo` repo's `build_test.go` (a headless
`Build() + Validate()` smoke test) covers the integration surface:
```go ```go
func TestBuildValidates(t *testing.T) { func TestBuildValidates(t *testing.T) {
@@ -1530,16 +1545,14 @@ func TestBuildValidates(t *testing.T) {
if err := g.Validate(); err != nil { if err := g.Validate(); err != nil {
t.Fatalf("validate: %v", err) t.Fatalf("validate: %v", err)
} }
// ... assert specific managers contain expected entries ...
} }
``` ```
It calls `Build()` (which registers every entity) and `Validate()` (which Drop a similar test into any project that consumes the library — it
cross-checks every name reference). No Ebiten window is opened — perfect opens no window and finishes in milliseconds, so it's a cheap CI gate.
for CI.
Library-internal unit tests are not shipped yet; the manager + action Library-internal unit tests are not shipped yet; the building blocks are
primitives are intentionally small enough to verify through smoke runs small enough to verify through smoke runs
of the demo for the time being. of the demo for the time being.
### 20.1 Useful runtime debug switches ### 20.1 Useful runtime debug switches
@@ -1553,87 +1566,88 @@ pncdsl.DebugLog = true // logf output to stderr (script queue, a
## 21. Project layout ## 21. Project layout
All library source files live at the repo root; no nested package
directory. The naming convention is `theme.identifier.go``ls core.*`
or `ls ui.*` instantly groups related sources.
``` ```
pncdsl/ pncdsl/ # module git.teletype.hu/games/pncdsl
├── main.go # entry point: pncdsl.Run(domain.Build()) ├── go.mod
├── pncdsl/ # the library — theme-prefixed file names ├── go.sum
│ ├── core.doc.go # package docs
│ ├── core.manager.go # Manager[T Named], Named, TypeLabel
│ ├── core.game.go # Game aggregate, runtime state, helpers
│ ├── core.engine.go # ebiten.Game adapter (Update/Draw/Layout)
│ ├── core.dsl.go # Run()
│ ├── core.errors.go # sentinel errors
│ │
│ ├── util.geometry.go # Point, Rectangle, Polygon, Shape
│ ├── util.timer.go # Timer helper
│ ├── util.log.go # DebugLog + logf
│ │
│ ├── asset.def.go # Asset, AssetKind
│ ├── asset.manager.go # AssetManager alias + lazy loader
│ ├── asset.audio.go # AudioPlayer (stub)
│ ├── asset.text.go # drawText / wrapText helpers
│ │
│ ├── scene.def.go # Scene, SceneActor
│ ├── scene.manager.go # SceneManager alias
│ ├── scene.hotspot.go # Hotspot, CursorKind
│ ├── scene.trigger.go # Trigger (stub)
│ ├── scene.transition.go # fade-to-black overlay (internal)
│ ├── scene.camera.go # Camera (stub identity transform)
│ │
│ ├── item.def.go # Item
│ ├── item.manager.go # ItemManager alias
│ ├── item.inventory.go # Inventory
│ │
│ ├── actor.def.go # Character
│ ├── actor.manager.go # CharacterManager alias
│ ├── actor.animation.go # AnimationClip (stub)
│ │
│ ├── dialog.def.go # Dialogue, DialogueNode, DialogueChoice
│ ├── dialog.manager.go # DialogueManager alias
│ │
│ ├── action.def.go # Action, Runner, Ctx, Status + every built-in action
│ ├── action.condition.go # Condition interface + combinators
│ ├── action.script.go # Script entity
│ ├── action.manager.go # ScriptManager alias
│ │
│ ├── state.def.go # State (flags, vars, visited, talked)
│ ├── state.save.go # Save/Load (stub)
│ │
│ ├── input.def.go # Input (consume-on-use)
│ │
│ ├── ui.widget.go # Widget interface, UICtx, Size, Align
│ ├── ui.manager.go # UIManager alias + reversed/ordered iterators
│ ├── ui.theme.go # Theme + ThemeManager
│ ├── ui.theme_presets.go # 4 preset themes
│ ├── ui.defaults.go # RegisterDefaultUI/RadialVerbUI/RichUI
│ ├── ui.verb.go # Verb + VerbManager
│ ├── ui.verb_bar.go # VerbBar widget
│ ├── ui.verb_radial.go # RadialVerbs widget
│ ├── ui.inventory.go # InventoryBar widget
│ ├── ui.status.go # StatusLine widget
│ ├── ui.speech.go # SpeechBubble widget
│ ├── ui.dialog_box.go # DialogBox widget
│ ├── ui.end_card.go # EndCard widget
│ ├── ui.cursor.go # Cursor widget
│ ├── ui.hotspot_debug.go # HotspotDebug widget
│ ├── ui.panel.go # Panel widget
│ ├── ui.top_bar.go # TopBar widget
│ ├── ui.character_panel.go # CharacterPanel widget + CharStat
│ └── ui.chat_log.go # ChatLog widget
├── domain/ # the concrete game — see DEMO.md ├── core.doc.go # package docs
├── assets/ # optional image / audio files ├── core.manager.go # Manager[T Named], Named, TypeLabel
├── PLAN.md # original design document ├── core.game.go # Game aggregate, runtime state, helpers
├── UIPLAN.md # widget-system design ├── core.engine.go # ebiten.Game adapter (Update/Draw/Layout)
├── DEMO.md # walkthrough of the bundled game ├── core.dsl.go # Run()
├── GFX.md # asset prompts for image AIs ├── core.errors.go # sentinel errors
├── util.geometry.go # Point, Rectangle, Polygon, Shape
├── util.timer.go # Timer helper
├── util.log.go # DebugLog + logf
├── asset.def.go # Asset, AssetKind
├── asset.manager.go # AssetManager alias + lazy loader
├── asset.audio.go # AudioPlayer (stub)
├── asset.text.go # drawText / wrapText helpers
├── scene.def.go # Scene, SceneActor
├── scene.manager.go # SceneManager alias
├── scene.hotspot.go # Hotspot, CursorKind
├── scene.trigger.go # Trigger (stub)
├── scene.transition.go # fade-to-black overlay (internal)
├── scene.camera.go # Camera (stub identity transform)
├── item.def.go # Item
├── item.manager.go # ItemManager alias
├── item.inventory.go # Inventory
├── actor.def.go # Character
├── actor.manager.go # CharacterManager alias
├── actor.animation.go # AnimationClip (stub)
├── dialog.def.go # Dialogue, DialogueNode, DialogueChoice
├── dialog.manager.go # DialogueManager alias
├── action.def.go # Action, Runner, Ctx, Status + every built-in action
├── action.condition.go # Condition interface + combinators
├── action.script.go # Script entity
├── action.manager.go # ScriptManager alias
├── state.def.go # State (flags, vars, visited, talked)
├── state.save.go # Save/Load (stub)
├── input.def.go # Input (consume-on-use)
├── ui.widget.go # Widget interface, UICtx, Size, Align
├── ui.manager.go # UIManager alias + reversed/ordered iterators
├── ui.theme.go # Theme + ThemeManager
├── ui.theme_presets.go # 4 preset themes
├── ui.defaults.go # RegisterDefaultUI/RadialVerbUI/RichUI
├── ui.verb.go # Verb + VerbManager
├── ui.verb_bar.go # VerbBar widget
├── ui.verb_radial.go # RadialVerbs widget
├── ui.inventory.go # InventoryBar widget
├── ui.status.go # StatusLine widget
├── ui.speech.go # SpeechBubble widget
├── ui.dialog_box.go # DialogBox widget
├── ui.end_card.go # EndCard widget
├── ui.cursor.go # Cursor widget
├── ui.hotspot_debug.go # HotspotDebug widget
├── ui.panel.go # Panel widget
├── ui.top_bar.go # TopBar widget
├── ui.character_panel.go # CharacterPanel widget + CharStat
├── ui.chat_log.go # ChatLog widget
├── UIPLAN.md # widget-system design rationale
├── LICENSE.md # MIT ├── LICENSE.md # MIT
└── README.md # this file └── README.md # this file
``` ```
Files under `domain/` follow `theme.identifier.go` (e.g. The companion demo project (`pncdsl-demo`) lives in its own repo at
`scene.kitchen.go`, `item.key.go`) — `ls domain/scene.*` instantly lists <ssh://git@git.teletype.hu:2222/games/pncdsl-demo>. It consumes this
every location. package via `require git.teletype.hu/games/pncdsl …` in its `go.mod`
no in-tree coupling.
--- ---

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 MiB

View File

@@ -1,29 +0,0 @@
package domain
import "testing"
// TestBuildValidates is the headless smoke test described in PLAN.md:
// it composes the full game and asks the library to cross-check every
// name reference between managers. No window opens.
func TestBuildValidates(t *testing.T) {
g := Build()
if err := g.Validate(); err != nil {
t.Fatalf("validate: %v", err)
}
for _, name := range []string{"bedroom", "kitchen"} {
if !g.SceneManager.Has(name) {
t.Errorf("missing scene %q", name)
}
}
for _, name := range []string{"key", "beans", "mug"} {
if !g.ItemManager.Has(name) {
t.Errorf("missing item %q", name)
}
}
if !g.DialogueManager.Has("cat_morning") {
t.Errorf("missing dialogue cat_morning")
}
if !g.ScriptManager.Has("intro") || !g.ScriptManager.Has("victory") {
t.Errorf("missing intro/victory script")
}
}

View File

@@ -1,18 +0,0 @@
package domain
import (
"image/color"
p "pncdsl/pncdsl"
)
func defineCat(g *p.Game) {
g.CharacterManager.Register(p.Character{
Name: "cat",
Sprite: "spr/cat",
Speed: 40,
SpeechColor: color.RGBA{200, 200, 255, 255},
W: 34,
H: 20,
})
}

View File

@@ -1,18 +0,0 @@
package domain
import (
"image/color"
p "pncdsl/pncdsl"
)
func definePlayer(g *p.Game) {
g.CharacterManager.Register(p.Character{
Name: "player",
Sprite: "spr/player",
Speed: 80,
SpeechColor: color.RGBA{255, 230, 160, 255},
W: 28,
H: 62,
})
}

View File

@@ -1,45 +0,0 @@
package domain
import p "pncdsl/pncdsl"
func defineCatMorning(g *p.Game) {
g.DialogueManager.Register(p.Dialogue{
Name: "cat_morning",
Start: "start",
Nodes: []p.DialogueNode{
{
Name: "start",
Lines: []p.DialogueLine{
{Speaker: "cat", Text: "Mióóóóóóóóu. Késő van."},
},
Choices: []p.DialogueChoice{
{
Text: "Mindjárt megy a kávé.",
Actions: []p.Action{p.SetFlag(FlagPromisedCoffee), p.EndDialogue()},
},
{
Text: "Te etted meg a babot?",
Show: p.Not(p.Flag(FlagCupboardOpen)),
Actions: []p.Action{p.GotoNode("beans")},
},
{
Text: "Hagyj békén.",
Actions: []p.Action{p.EndDialogue()},
},
},
},
{
Name: "beans",
Lines: []p.DialogueLine{
{Speaker: "cat", Text: "A szekrényben van. Mindig ott szokott lenni."},
},
Choices: []p.DialogueChoice{
{
Text: "Köszi.",
Actions: []p.Action{p.EndDialogue()},
},
},
},
},
})
}

View File

@@ -1,31 +0,0 @@
package domain
import p "pncdsl/pncdsl"
// registerAssets declares every asset the game references. Files don't
// need to exist on disk — the library will substitute deterministic
// colored placeholders for anything missing.
func registerAssets(g *p.Game) {
imgs := []string{
"bg/bedroom", "bg/kitchen",
"spr/key", "spr/beans", "spr/mug",
"spr/player", "spr/cat",
}
for _, name := range imgs {
g.AssetManager.Register(p.Asset{
Name: name,
Path: "assets/" + name + ".png",
Kind: p.AssetImage,
})
}
audios := []string{
"mus/wakeup", "mus/calm", "snd/coffee_done",
}
for _, name := range audios {
g.AssetManager.Register(p.Asset{
Name: name,
Path: "assets/" + name + ".ogg",
Kind: p.AssetAudio,
})
}
}

View File

@@ -1,10 +0,0 @@
package domain
const (
FlagKeyTaken = "key_taken"
FlagCupboardOpen = "cupboard_open"
FlagMugTaken = "mug_taken"
FlagCoffeeHasBeans = "coffee_has_beans"
FlagCoffeeHasMug = "coffee_has_mug"
FlagPromisedCoffee = "promised_coffee"
)

View File

@@ -1,46 +0,0 @@
package domain
import p "pncdsl/pncdsl"
// Build assembles the full Reggeli Kávé game by registering every entity
// into its matching manager on a fresh *Game. The order matters only for
// readability — Validate() runs after Build to cross-check references.
func Build() *p.Game {
g := p.NewGame("Reggeli Kávé", 320, 200)
registerAssets(g)
definePlayer(g)
defineCat(g)
defineKey(g)
defineBeans(g)
defineMug(g)
defineCatMorning(g)
defineIntroScript(g)
defineVictoryScript(g)
defineBedroom(g)
defineKitchen(g)
// Story-rich HUD: top bar + NPC character panel + chat log + permanent
// verb wheel + small inventory. The player panel is skipped — the
// player avatar is always visible in the scene, so the floating card
// in the corner just gets in the way. Pass "player" as the second
// arg to put it back.
p.RegisterRichUI(g, "", "cat")
g.MaxLogLines = 64
// initial HUD vars
g.State.SetVar("score", 0)
g.State.SetVar("time", "Nap 1 - 07:23")
g.State.SetVar("player.state", "Ébren")
g.State.SetVar("player.mood", "Álmos")
g.State.SetVar("cat.state", "Várakozik")
g.State.SetVar("cat.mood", "Türelmetlen")
g.StartAt("bedroom").OnStart(p.RunScript("intro"))
return g
}

View File

@@ -1,11 +0,0 @@
package domain
import p "pncdsl/pncdsl"
func defineBeans(g *p.Game) {
g.ItemManager.Register(p.Item{
Name: "beans",
Sprite: "spr/beans",
Description: "őrölt kávébab",
})
}

View File

@@ -1,20 +0,0 @@
package domain
import p "pncdsl/pncdsl"
func defineKey(g *p.Game) {
g.ItemManager.Register(p.Item{
Name: "key",
Sprite: "spr/key",
Description: "rozsdás kulcs",
OnUseWith: map[string]p.Action{
"cupboard": p.Seq(
p.Say("player", "Klikk."),
p.SetFlag(FlagCupboardOpen),
p.Give("beans"),
p.TakeAway("key"),
p.Say("player", "Bab van benne. Pont, ami kell."),
),
},
})
}

View File

@@ -1,11 +0,0 @@
package domain
import p "pncdsl/pncdsl"
func defineMug(g *p.Game) {
g.ItemManager.Register(p.Item{
Name: "mug",
Sprite: "spr/mug",
Description: "kedvenc bögréje",
})
}

View File

@@ -1,61 +0,0 @@
package domain
import p "pncdsl/pncdsl"
func defineBedroom(g *p.Game) {
g.SceneManager.Register(p.Scene{
Name: "bedroom",
Title: "Hálószoba",
Background: "bg/bedroom",
Music: "mus/wakeup",
Actors: []p.SceneActor{
{CharacterName: "player", At: p.Point{X: 160, Y: 145}},
},
Hotspots: []p.Hotspot{
{
Name: "bed",
Area: p.Rect(20, 70, 90, 60),
Label: "ágy",
OnLook: p.Say("player", "A párnám még meleg."),
OnUse: p.Say("player", "Most keltem fel. Nem alszom vissza."),
},
{
Name: "nightstand",
Area: p.Rect(115, 80, 35, 50),
Label: "éjjeliszekrény",
OnLook: p.If(p.Flag(FlagKeyTaken),
p.Say("player", "Üres a tetején."),
p.Say("player", "Egy rozsdás kulcs az éjjeliszekrényemen."),
),
OnTake: p.If(p.Flag(FlagKeyTaken),
p.Say("player", "Már elvittem."),
p.Seq(
p.Walk("player", p.Point{X: 130, Y: 145}),
p.Give("key"),
p.SetFlag(FlagKeyTaken),
p.Say("player", "Pont jól jöhet."),
),
),
OnUse: p.If(p.Flag(FlagKeyTaken),
p.Say("player", "Már elvittem a kulcsot."),
p.Seq(
p.Walk("player", p.Point{X: 130, Y: 145}),
p.Give("key"),
p.SetFlag(FlagKeyTaken),
p.Say("player", "Megvan a kulcs."),
),
),
},
{
Name: "door",
Area: p.Rect(260, 50, 40, 90),
Label: "ajtó",
OnLook: p.Say("player", "Az ajtó a konyhába vezet."),
OnUse: p.Seq(
p.Walk("player", p.Point{X: 260, Y: 145}),
p.GoTo("kitchen"),
),
},
},
})
}

View File

@@ -1,115 +0,0 @@
package domain
import p "pncdsl/pncdsl"
func defineKitchen(g *p.Game) {
g.SceneManager.Register(p.Scene{
Name: "kitchen",
Title: "Konyha",
Background: "bg/kitchen",
Music: "mus/calm",
Actors: []p.SceneActor{
{CharacterName: "player", At: p.Point{X: 60, Y: 145}},
{CharacterName: "cat", At: p.Point{X: 215, Y: 142}},
},
Hotspots: []p.Hotspot{
{
Name: "cupboard",
Area: p.Rect(30, 40, 80, 80),
Label: "szekrény",
OnLook: p.Say("player", "Konyhaszekrény. Zárva."),
OnUse: p.If(p.Flag(FlagCupboardOpen),
p.Say("player", "Már nyitva van."),
p.If(p.HasItem("key"),
p.Seq(
p.Walk("player", p.Point{X: 70, Y: 145}),
p.Say("player", "Klikk."),
p.SetFlag(FlagCupboardOpen),
p.Give("beans"),
p.TakeAway("key"),
),
p.Say("player", "Zárva. Kéne egy kulcs."),
),
),
OnUseWith: map[string]p.Action{
"key": p.Seq(
p.Walk("player", p.Point{X: 70, Y: 145}),
p.Say("player", "Klikk."),
p.SetFlag(FlagCupboardOpen),
p.Give("beans"),
p.TakeAway("key"),
),
},
},
{
Name: "shelf",
Area: p.Rect(115, 45, 35, 50),
Label: "polc",
OnLook: p.Say("player", "A polc tele bögrékkel."),
OnTake: p.If(p.Flag(FlagMugTaken),
p.Say("player", "Tiszta már mind elfogyott."),
p.Seq(
p.Walk("player", p.Point{X: 125, Y: 145}),
p.Give("mug"),
p.SetFlag(FlagMugTaken),
p.Say("player", "Egy bögre, kérem."),
),
),
OnUse: p.If(p.Flag(FlagMugTaken),
p.Say("player", "Már elvettem egyet."),
p.Seq(
p.Walk("player", p.Point{X: 125, Y: 145}),
p.Give("mug"),
p.SetFlag(FlagMugTaken),
p.Say("player", "Egy bögre, kérem."),
),
),
},
{
Name: "coffee_machine",
Area: p.Rect(160, 75, 50, 55),
Label: "kávéfőző",
OnLook: p.Say("player", "A jó öreg kávéfőző."),
OnUse: p.If(p.And(p.Flag(FlagCoffeeHasBeans), p.Flag(FlagCoffeeHasMug)),
p.RunScript("victory"),
p.Say("player", "Még hiányzik valami. Bab? Bögre?"),
),
OnUseWith: map[string]p.Action{
"beans": p.Seq(
p.Walk("player", p.Point{X: 180, Y: 145}),
p.SetFlag(FlagCoffeeHasBeans),
p.TakeAway("beans"),
p.Say("player", "Bab a helyén."),
p.If(p.And(p.Flag(FlagCoffeeHasBeans), p.Flag(FlagCoffeeHasMug)),
p.RunScript("victory")),
),
"mug": p.Seq(
p.Walk("player", p.Point{X: 180, Y: 145}),
p.SetFlag(FlagCoffeeHasMug),
p.TakeAway("mug"),
p.Say("player", "Bögre a helyén."),
p.If(p.And(p.Flag(FlagCoffeeHasBeans), p.Flag(FlagCoffeeHasMug)),
p.RunScript("victory")),
),
},
},
{
Name: "cat",
Area: p.Rect(195, 118, 35, 22),
Label: "Cirmos",
OnLook: p.Say("player", "Cirmos, a házikedvencem."),
OnTalk: p.RunDialogue("cat_morning"),
},
{
Name: "door",
Area: p.Rect(0, 40, 22, 90),
Label: "ajtó",
OnLook: p.Say("player", "Vissza a hálóba."),
OnUse: p.Seq(
p.Walk("player", p.Point{X: 20, Y: 145}),
p.GoTo("bedroom"),
),
},
},
})
}

View File

@@ -1,16 +0,0 @@
package domain
import p "pncdsl/pncdsl"
func defineIntroScript(g *p.Game) {
g.ScriptManager.Register(p.Script{
Name: "intro",
Actions: p.Seq(
p.Wait(0.4),
p.Say("player", "Brr, hideg van."),
p.Say("player", "Egy kávé kéne, mielőtt szétszakad a fejem."),
p.Walk("player", p.Point{X: 200, Y: 145}),
p.SetVar("player.mood", "Eltökélt"),
),
})
}

View File

@@ -1,20 +0,0 @@
package domain
import p "pncdsl/pncdsl"
func defineVictoryScript(g *p.Game) {
g.ScriptManager.Register(p.Script{
Name: "victory",
Actions: p.Seq(
p.Say("player", "Készen is van!"),
p.PlaySound("snd/coffee_done"),
p.Wait(0.6),
p.Say("cat", "Mióóóóu. *boldog macska*"),
p.Say("player", "Reggeli kávé: megmentve."),
p.SetVar("score", 100),
p.SetVar("player.mood", "Boldog"),
p.SetVar("cat.mood", "Boldog"),
p.ShowEnd("Vége — kösz, hogy játszottál!"),
),
})
}

5
go.mod
View File

@@ -1,12 +1,13 @@
module pncdsl module git.teletype.hu/games/pncdsl
go 1.26.3 go 1.26.3
require github.com/hajimehoshi/ebiten/v2 v2.9.9
require ( require (
github.com/ebitengine/gomobile v0.0.0-20250923094054-ea854a63cce1 // indirect github.com/ebitengine/gomobile v0.0.0-20250923094054-ea854a63cce1 // indirect
github.com/ebitengine/hideconsole v1.0.0 // indirect github.com/ebitengine/hideconsole v1.0.0 // indirect
github.com/ebitengine/purego v0.9.0 // indirect github.com/ebitengine/purego v0.9.0 // indirect
github.com/hajimehoshi/ebiten/v2 v2.9.9 // indirect
github.com/jezek/xgb v1.1.1 // indirect github.com/jezek/xgb v1.1.1 // indirect
golang.org/x/sync v0.17.0 // indirect golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect golang.org/x/sys v0.36.0 // indirect

2
go.sum
View File

@@ -8,6 +8,8 @@ github.com/hajimehoshi/ebiten/v2 v2.9.9 h1:JdDag6Ndj12iD4lxQGG8kbsrh7ssj4Sbzth6r
github.com/hajimehoshi/ebiten/v2 v2.9.9/go.mod h1:DAt4tnkYYpCvu3x9i1X/nK/vOruNXIlYq/tBXxnhrXM= github.com/hajimehoshi/ebiten/v2 v2.9.9/go.mod h1:DAt4tnkYYpCvu3x9i1X/nK/vOruNXIlYq/tBXxnhrXM=
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4= github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
golang.org/x/image v0.31.0 h1:mLChjE2MV6g1S7oqbXC0/UcKijjm5fnJLUYKIYrLESA=
golang.org/x/image v0.31.0/go.mod h1:R9ec5Lcp96v9FTF+ajwaH3uGxPH4fKfHHAVbUILxghA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=

14
main.go
View File

@@ -1,14 +0,0 @@
package main
import (
"log"
"pncdsl/domain"
"pncdsl/pncdsl"
)
func main() {
if err := pncdsl.Run(domain.Build()); err != nil {
log.Fatal(err)
}
}