# PLAN.md — `pncdsl` point-and-click adventure library ## Cél Egy Go nyelvű, [Ebitengine](https://ebitengine.org/) tetejére épülő könyvtár, amellyel klasszikus point-and-click kalandjátékok (Lucasarts/Sierra stílus) gyorsan, **deklaratív struct literal**-ekkel összerakhatók. A library a `pncdsl` modulban él, a konkrét játék pedig a `domain` package-ben a regisztráló hívásokon keresztül definiálódik. A `main.go` mindössze behúzza a domaint, példányosítja a `pncdsl.Game`-et és elindítja. **Vezérlő minta — Manager-regisztráció.** Minden névvel hivatkozható entitás (item, scene, character, dialogue, script, asset, verb) egy-egy globális (Game-en lógó) **`XxxManager`**-be kerül a **`Register(...)`** metódussal, és a `Get(name)` adja vissza. Például `g.ItemManager.Register(pncdsl.Item{Name: "key", ...})` és onnantól `g.ItemManager.Get("key")`. Mindegyik manager pontosan ugyanúgy viselkedik — egységes konvenció, nincs entitásonkénti egyedi API. ## Felhasználói élmény (DSL ízelítő) A `domain/` minden építőkövét **külön fájlba** tesszük, a fájlnév az adott entitás típusát és azonosítóját tükrözi (`téma.azonosító.go`). A `domain/game.go` csak behúzza az építő függvényeket egy `Build()`-be: ```go // domain/game.go package domain import p "pncdsl/pncdsl" func Build() *p.Game { g := p.NewGame("Reggeli Kávé", 320, 200) // assetek (a domain.asset.go-ban felsorolva) registerAssets(g) // karakterek definePlayer(g) defineCat(g) // itemek defineKey(g) defineBeans(g) defineMug(g) // dialógusok defineCatMorning(g) // scenek defineBedroom(g) defineKitchen(g) // scriptek / cutscene-ek defineIntroScript(g) defineVictoryScript(g) g.StartAt("bedroom").OnStart(p.RunScript("intro")) return g } ``` ```go // domain/scene.kitchen.go package domain import p "pncdsl/pncdsl" func defineKitchen(g *p.Game) { g.SceneManager.Register(p.Scene{ Name: "kitchen", Background: "bg/kitchen", Music: "mus/calm", Hotspots: []p.Hotspot{ { Name: "cupboard", Area: p.Rect(40, 60, 80, 120), Label: "szekrény", OnLook: p.Say("player", "Egy konyhaszekrény. Zárva."), OnUse: p.If(p.Flag("cupboard_open"), p.Say("player", "Már nyitva van."), p.Seq( p.RequireItem("key"), p.Say("player", "Klikk."), p.SetFlag("cupboard_open"), p.Give("beans"), p.TakeAway("key"), ), ), }, { Name: "cat", Area: p.Rect(200, 130, 30, 30), Label: "Cirmos", OnTalk: p.RunDialogue("cat_morning"), }, { Name: "door", Area: p.Rect(280, 60, 30, 120), OnUse: p.GoTo("bedroom"), }, }, }) } ``` ```go // domain/item.key.go 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", }) } ``` Az építőkövek tehát **plain struct literal**-ek, amiket a megfelelő `*Manager`-be regisztrálunk. Nincs fluent builder, nincs `.Done()` — a struct mezői önmagukban deklaratívak, az IDE autocomplete pedig kvázi-DSL-szerű élményt ad. ```go // main.go package main import ( "pncdsl/domain" "pncdsl/pncdsl" ) func main() { pncdsl.Run(domain.Build()) } ``` ## Mappa- és fájlstruktúra **Konvenció**: minden Go-fájl neve `téma.azonosító.go`. A pont szeparátor szándékos — így `ls`-ben a fájlok témánként szépen csoportosulnak. A library oldalon a téma a komponens funkcióját jelöli (`core`, `scene`, `actor`, `dialog`, `action`, `state`, `ui`, `input`, `asset`, `util`); a domain oldalon a téma az entitás típusa (`scene`, `item`, `character`, `dialog`, `script`), az azonosító pedig pontosan az a string, amivel a DSL-ben hivatkozunk rá (`scene.kitchen.go` ↔ `g.DefineScene("kitchen")`). ``` pncdsl/ ├── go.mod ├── PLAN.md ├── main.go # belépési pont ├── assets/ # képek, hangok, fontok (futásidőben olvasva) │ ├── bg/ spr/ snd/ mus/ fnt/ │ ├── pncdsl/ # maga a library │ ├── core.doc.go # package docs, design rationale │ ├── core.game.go # Game (root aggregátum), Run() │ ├── core.engine.go # Ebiten adapter: Update/Draw/Layout │ ├── core.manager.go # Manager[T Named] — KÖZPONTI minta │ ├── core.dsl.go # toplevel helperek (Rect, Seq, Par, Say...) │ ├── core.errors.go # tipizált hibák, sentinel értékek │ │ │ ├── scene.def.go # Scene struct (Name + mezők) │ ├── scene.manager.go # SceneManager (alias: Manager[Scene]) │ ├── scene.hotspot.go # Hotspot struct (Scene gyermeke) │ ├── scene.trigger.go # Trigger (Scene gyermeke) │ ├── scene.transition.go # Fade, Cut, Wipe a scenek között │ ├── scene.camera.go # Camera (pan/follow/shake) │ │ │ ├── item.def.go # Item struct (Name + mezők) │ ├── item.manager.go # ItemManager (alias: Manager[Item]) │ ├── item.inventory.go # Inventory, slotok, drag&drop │ │ │ ├── actor.def.go # Character struct (Name + mezők) │ ├── actor.manager.go # CharacterManager (alias: Manager[Character]) │ ├── actor.animation.go # SpriteSheet, AnimationClip, Animator │ │ │ ├── dialog.def.go # Dialogue, DialogueNode, Choice │ ├── dialog.manager.go # DialogueManager (alias: Manager[Dialogue]) │ ├── dialog.box.go # DialogBox UI a Dialogue megjelenítéshez │ │ │ ├── action.def.go # Action interface + beépített konstruktorok │ ├── action.condition.go # Condition interface + beépítettek │ ├── action.script.go # Script struct (Name + Actions) │ ├── action.manager.go # ScriptManager (alias: Manager[Script]) │ │ │ ├── state.def.go # World state, Flags, Vars, History │ ├── state.save.go # SaveSlot, serialize/restore │ │ │ ├── ui.def.go # UI root, layout │ ├── ui.verb.go # Verb struct + VerbBar │ ├── ui.verb_manager.go # VerbManager (alias: Manager[Verb]) │ ├── ui.cursor.go # Cursor sprite-ek és állapotok │ ├── ui.speech.go # SpeechBubble renderelés │ │ │ ├── input.def.go # Egér, billentyű, hover/click resolve │ │ │ ├── asset.def.go # Asset struct (Name, Path, Kind) │ ├── asset.manager.go # AssetManager (alias: Manager[Asset]) + lazy load │ ├── asset.audio.go # AudioPlayer, Music, Sfx mixing │ ├── asset.text.go # FontLoader, TextRenderer, Wrapper │ │ │ ├── util.geometry.go # Point, Rect, Polygon, Path │ ├── util.timer.go # Tickek, Tween, Delay │ └── util.log.go # opcionális debug overlay │ └── domain/ # a konkrét játék tartalma — lásd Demo szakasz ├── game.go # Build() — sorban hívja a define*() függvényeket ├── domain.asset.go # registerAssets(g) — minden asset egy helyen ├── domain.flag.go # flag/var konstansok (string típusos enum) │ ├── scene.bedroom.go # defineBedroom(g) ├── scene.kitchen.go # defineKitchen(g) │ ├── item.key.go # defineKey(g) ├── item.beans.go # defineBeans(g) ├── item.mug.go # defineMug(g) │ ├── character.player.go # definePlayer(g) ├── character.cat.go # defineCat(g) │ ├── dialog.cat_morning.go # defineCatMorning(g) │ ├── script.intro.go # defineIntroScript(g) └── script.victory.go # defineVictoryScript(g) ``` A domain-konvenció előnye: amikor új scene-t/itemet teszek hozzá, **csak egy új fájlt hozok létre + egy sort a `Build()`-ben**. Nincs egyetlen ezer soros `items.go`. Ha a jövőben többszáz entitás lesz, a fájllista önmagában dokumentáció: `ls domain/scene.*` megmutatja az összes helyszínt. ## Központi struktúrák ### `Manager[T]` — a regisztráló minta (`pncdsl/core.manager.go`) Minden névvel hivatkozható entitást egyetlen generikus `Manager` kezel. A library egyetlen helyen definiálja, és típus-aliasokkal kap nevet entitásonként — így minden manager egységesen viselkedik (`Register`, `Get`, `MustGet`, `Has`, `Names`, `Each`). ```go // pncdsl/core.manager.go package pncdsl type Named interface { GetName() string } type Manager[T Named] struct { items map[string]T } func NewManager[T Named]() *Manager[T] { return &Manager[T]{items: make(map[string]T)} } func (m *Manager[T]) Register(v T) { name := v.GetName() if name == "" { panic("pncdsl: Register: empty Name") } if _, dup := m.items[name]; dup { panic("pncdsl: Register: duplicate name: " + name) } m.items[name] = v } func (m *Manager[T]) Get(name string) (T, bool) { v, ok := m.items[name] return v, ok } func (m *Manager[T]) MustGet(name string) T { v, ok := m.items[name] if !ok { panic("pncdsl: MustGet: unknown name: " + name) } return v } func (m *Manager[T]) Has(name string) bool { _, ok := m.items[name]; return ok } func (m *Manager[T]) Names() []string { /* sorted keys */ } func (m *Manager[T]) Each(fn func(T)) { /* iterate */ } ``` Minden entitás implementálja a `Named` interfészt egyetlen kis metódussal: ```go type Item struct { Name string Sprite string Description string // ... handlerek } func (i Item) GetName() string { return i.Name } ``` Entitásonkénti **nevesített manager-típus** (generic alias, Go 1.24+): ```go // pncdsl/item.manager.go type ItemManager = Manager[Item] // pncdsl/scene.manager.go type SceneManager = Manager[Scene] // pncdsl/actor.manager.go type CharacterManager = Manager[Character] // pncdsl/dialog.manager.go type DialogueManager = Manager[Dialogue] // pncdsl/action.manager.go type ScriptManager = Manager[Script] // pncdsl/asset.manager.go type AssetManager = Manager[Asset] // pncdsl/ui.verb_manager.go type VerbManager = Manager[Verb] ``` A használat ezért minden entitáson **pontosan ugyanígy néz ki**: ```go g.ItemManager.Register(p.Item{Name: "key", Sprite: "spr/key"}) g.ItemManager.Get("key") // (Item, bool) g.ItemManager.MustGet("key") // Item, panic ha nincs g.SceneManager.Register(p.Scene{Name: "kitchen", ...}) g.AssetManager.Register(p.Asset{Name: "bg/kitchen", Path: "assets/bg/kitchen.png"}) ``` **Manager-listája:** `ItemManager`, `SceneManager`, `CharacterManager`, `DialogueManager`, `ScriptManager`, `AssetManager`, `VerbManager`. (A `Hotspot`, `Trigger`, `Walkbox`, `DialogueNode` nem kap saját managert — ezek mindig egy parent struct mezőiként élnek, scoped a Scene-hez ill. a Dialogue-hoz.) ### `Game` — a gyökér aggregátum (`pncdsl/core.game.go`) A `Game` az összes managert mezőként hordozza, plusz a futásidejű állapotot: ```go type Game struct { Title string Width, Height int // regisztrálható entitások — minden mező egyforma alakú ItemManager *ItemManager SceneManager *SceneManager CharacterManager *CharacterManager DialogueManager *DialogueManager ScriptManager *ScriptManager AssetManager *AssetManager VerbManager *VerbManager // futásidejű állapot State *State Inventory *Inventory UI *UI Audio *AudioPlayer Camera *Camera Cursor *Cursor current *Scene startID string onStart Action } func NewGame(title string, w, h int) *Game { return &Game{ Title: title, Width: w, Height: h, ItemManager: NewManager[Item](), SceneManager: NewManager[Scene](), CharacterManager: NewManager[Character](), DialogueManager: NewManager[Dialogue](), ScriptManager: NewManager[Script](), AssetManager: NewManager[Asset](), VerbManager: NewManager[Verb](), State: NewState(), Inventory: NewInventory(), // ... } } ``` A `Game` többi metódusa minimális — nem builder, csak konfig: - `func NewGame(title string, w, h int) *Game` - `func (g *Game) StartAt(sceneName string) *Game` - `func (g *Game) OnStart(a Action) *Game` - `func (g *Game) Validate() error` - `func (g *Game) Run() error` - `func (g *Game) Save(slot int) error` / `func (g *Game) Load(slot int) error` A toplevel `pncdsl.Run(g *Game) error` annyi, hogy `g.Validate()` után belép az Ebiten ciklusba. ### `Asset` (`pncdsl/asset.def.go`) ```go type Asset struct { Name string // pl. "bg/kitchen" Path string // pl. "assets/bg/kitchen.png" Kind AssetKind // Image | Audio | Font } func (a Asset) GetName() string { return a.Name } ``` Az `AssetManager` lazy: `Register` csak metaadatot tárol, az első tényleges hozzáférésnél tölti be és cache-eli (a betöltött resource az `AssetManager` belső térképében, nem az `Asset` structban — utóbbi marad immutable konfig). ### `Scene` (`pncdsl/scene.def.go`) ```go type Scene struct { Name string Background string // Asset.Name Music string // Asset.Name Hotspots []Hotspot Walkboxes []Polygon Triggers []Trigger Actors []SceneActor // { CharacterName string; At Point } OnEnter Action OnLeave Action } func (s Scene) GetName() string { return s.Name } ``` Hotspot / Trigger / Walkbox értékek inline, ugyanabban a struct literalban — nem kell külön regisztrálni őket; egy Hotspot eleve scene-scoped. ### `Hotspot` (`pncdsl/scene.hotspot.go`) ```go type Hotspot struct { Name string Area Shape // Rect | Polygon Label string // a kurzor melletti felirat hover-kor Cursor CursorKind // verbenkénti handler — egyszerű mezők, nem map OnLook Action OnUse Action OnTalk Action OnTake Action OnGive Action // egyéni verbek (`g.VerbManager.Register(Verb{Name: "push"})`-hoz) OnVerb map[string]Action } ``` ### `Item` (`pncdsl/item.def.go`) ```go type Item struct { Name string Sprite string // Asset.Name Description string OnUseSelf Action // pl. „nem tudom mit kezdjek vele" OnUseWith map[string]Action // targetName -> action (hotspot vagy másik item) } func (i Item) GetName() string { return i.Name } ``` ### `Inventory` (`pncdsl/item.inventory.go`) ```go type Inventory struct { items []string capacity int selected string } func (i *Inventory) Add(name string) func (i *Inventory) Remove(name string) func (i *Inventory) Has(name string) bool func (i *Inventory) Select(name string) func (i *Inventory) Selected() string ``` (Az `Inventory` nem manager — futásidejű állapot, nem regisztráció.) ### `Character` (`pncdsl/actor.def.go`) ```go type Character struct { Name string Sprite string // Asset.Name Animations map[string]AnimationClip // idle, walk_left, walk_right, talk Speed float64 SpeechColor color.Color Start Point // alappozíció (scene-en belül felülírhatja Actor) } func (c Character) GetName() string { return c.Name } ``` Futásidejű mozgás-API a `Game`-en keresztül érhető el (nem a Character struct módosításán át, hiszen az regisztrált konfig): `g.WalkCharacter("player", Point{...})`, `g.FaceCharacter("player", dir)`. ### `Dialogue` (`pncdsl/dialog.def.go`) ```go type Dialogue struct { Name string Start string // kezdő node Name Nodes []DialogueNode // mezőként, nem manager — Dialogue-scoped } func (d Dialogue) GetName() string { return d.Name } type DialogueNode struct { Name string Lines []DialogueLine // ki mit mond a node belépésén Choices []DialogueChoice } type DialogueLine struct { Speaker string // Character.Name Text string } type DialogueChoice struct { Text string Show Condition // nil = mindig látszik Once bool Actions []Action } ``` ### `Script` (`pncdsl/action.script.go`) ```go type Script struct { Name string Actions Action // tipikusan Seq(...) } func (s Script) GetName() string { return s.Name } ``` ### `Verb` (`pncdsl/ui.verb.go`) ```go type Verb struct { Name string // "look", "use", "talk", "take", ... Label string // megjelenő szöveg a VerbBar-on Default Action // ha a hotspot nem definiál erre handlert } func (v Verb) GetName() string { return v.Name } ``` ### `Action` interface (`pncdsl/action.def.go`) ```go type Action interface { Run(ctx *Ctx) Status // Running | Done | Failed } type Ctx struct { Game *Game Scene *Scene Hotspot *Hotspot Item *Item dt float64 } ``` Beépített actionök (sima konstruktorfüggvények, hogy DSL-ből szépen olvasódjon): - `Say(speakerID, text string)` - `Wait(seconds float64)` - `GoTo(sceneID string)` - `Walk(charID string, to Point)` - `Give(itemID string)` / `TakeAway(itemID string)` - `SetFlag(name string)` / `ClearFlag(name string)` / `SetVar(name string, v any)` - `PlaySound(assetID string)` / `PlayMusic(assetID string)` / `StopMusic()` - `RunDialogue(dialogueID string)` / `EndDialogue()` / `GotoNode(nodeID string)` - `RunScript(scriptID string)` - `Seq(a ...Action)` — sorban (default builder-szemantika) - `Par(a ...Action)` — párhuzamosan - `If(c Condition, then Action, else_ ...Action)` - `RequireItem(itemID string)` — short-circuit ha nincs nála (visszadob egy "nem tudom használni"-t) - `Custom(fn func(*Ctx) Status)` — escape hatch a domainnek ### `Condition` (`pncdsl/action.condition.go`) ```go type Condition interface{ Eval(*Ctx) bool } ``` Beépítettek: `HasItem(name)`, `Flag(name)`, `Not(c)`, `And(c...)`, `Or(c...)`, `VarEq(name, v)`, `InScene(name)`, `SelectedItem(name)`. ### `State` (`pncdsl/state.def.go`) ```go type State struct { flags map[string]bool vars map[string]any visited map[string]int // scene name -> hányszor lépett be talked map[string]int // dialogue node -> hányszor futott le (a `Once` choice-okhoz) } ``` Metódusok: `Flag/SetFlag/ClearFlag`, `Var/SetVar`, `Visited(name)`, `TalkedTo(name)`, `Snapshot()`, `Restore(snap)`. (Ez sem manager — runtime állapot.) ### `UI` (`pncdsl/ui.def.go`, `pncdsl/dialog.box.go`) ```go type UI struct { verbBar *VerbBar invBar *InventoryBar speech *SpeechBubble dialogBox *DialogBox hoverLabel string } ``` Konfigurálható: layout, font, színek. Default a SCUMM-szerű alsó verbsáv + jobboldali inventory; ki is kapcsolható ha valaki modern verb-coin variánst akar. ### `Engine` — Ebiten adapter (`pncdsl/core.engine.go`) ```go type engine struct { g *Game } func (e *engine) Update() error func (e *engine) Draw(screen *ebiten.Image) func (e *engine) Layout(outW, outH int) (int, int) ``` Az `Update` belerajzol: 1. input → resolve (melyik hotspoton van a kurzor, milyen verb aktív) 2. aktív cutscene/dialogue lépése 3. karakterek mozgása, animation tick 4. trigger feltételek 5. UI állapot frissítése A `Draw` rétegei alulról felfele: háttér → walkbox debug (csak ha bekapcsolva) → karakterek (y-sorted) → speech bubble-k → UI → cursor → tranzíció overlay. ## Asset és audio kezelés Az `AssetManager` egy hagyományos `Manager[Asset]` — a `Register` csak az útvonalat és típust tárolja, az első `Load(name)` hívásra olvas be a diszkről és cache-el (a betöltött `*ebiten.Image` / audio buffer egy belső térképben él, az `Asset` struct maga immutable konfig marad). ```go g.AssetManager.Register(p.Asset{Name: "bg/kitchen", Path: "assets/bg/kitchen.png", Kind: p.AssetImage}) g.AssetManager.Register(p.Asset{Name: "mus/calm", Path: "assets/mus/calm.ogg", Kind: p.AssetAudio}) ``` `AudioPlayer` egy zenecsatornát + N sfx csatornát kezel, fade-in/out támogatással a `PlayMusic` action-höz. ## Save / Load `SaveSlot` JSON-be sorosít: - aktuális scene `Name`-je - karakterek pozíciói - `State` (flags, vars, visited, talked) - inventory tartalom (item `Name`-ek listája) - futó script PC (ha cutscene közben menteni szabad — opcionálisan tilthatóan) A managerek **tartalmát nem mentjük** — azok a `Build()`-ből minden indításkor újra előállnak. Csak a runtime-állapotot persistáljuk. API: `Game.Save(slot int) error`, `Game.Load(slot int) error`. ## Hibakezelés és validáció `Game.Validate() error` a `Run()` előtt fut. Ellenőrzi a managerek közötti név-hivatkozások konzisztenciáját: - minden `Scene.Background`/`Music` és `Character.Sprite` szerepel-e az `AssetManager`-ben, - minden `GoTo(name)` célja él-e a `SceneManager`-ben, - `RunDialogue(name)` és `Dialogue` `Choice.Actions`-ben szereplő `GotoNode` ill. `RunScript(name)` célok léteznek-e, - minden `Hotspot.OnUseWith` map kulcs (item/hotspot név) feloldható, - `StartAt(name)` scene létezik, - nincs két azonos nevű regisztráció (ezt amúgy a `Register` panic-cal megfogja). Hibák tipizáltak: `ErrUnknownAsset`, `ErrUnknownScene`, `ErrUnknownDialogue`, `ErrUnknownDialogueNode`, `ErrUnknownScript`, `ErrUnknownItem`, `ErrDuplicateName`. ## Tesztelési stratégia - A `pncdsl/*.go` minden nem-render részére táblavezérelt unit teszt (`action_test.go`, `dialogue_test.go`, `state_test.go`). - A `domain/`-hoz egy headless smoke test, ami `Build()` után `Validate()`-et hív — így a játék statikus integritását CI-ben is le lehet csekkolni Ebiten ablak nélkül. - A renderhez `ebiten/ebitenutil` segítségével offscreen image diff — opcionális, csak ha sok regressziónk lenne. ## Demo játék: „Reggeli Kávé" A library kipróbálására egy szándékosan apró, ~5 perces játék, ami a fontos mechanikákat **egyenként** gyakorolja anélkül, hogy a domain túlnőne a libraryn. ### Sztori egy mondatban Frissen ébredsz, és muszáj kávét főznöd, mielőtt a macskád szétszedné a konyhát. ### Helyszínek | ID | Háttér | Zene | Tartalom | |------------|---------------------|-------------|------------------------------------------------------------| | `bedroom` | `bg/bedroom.png` | `mus/wakeup`| ágy, éjjeliszekrény, ajtó | | `kitchen` | `bg/kitchen.png` | `mus/calm` | szekrény, polc, kávéfőző, macska, ajtó | ### Itemek | ID | Sprite | Honnan szerezhető | Mire jó | |---------|-----------------|------------------------------------------|-------------------------------------------| | `key` | `spr/key.png` | `bedroom.nightstand` → Take | `kitchen.cupboard` kinyitása | | `beans` | `spr/beans.png` | `kitchen.cupboard` (kinyitás után) | `kitchen.coffee_machine`-be tölteni | | `mug` | `spr/mug.png` | `kitchen.shelf` → Take | `kitchen.coffee_machine`-re tenni | ### Karakterek - **`player`** — az általunk irányított PC. `WalkTo` célokra mozog, beszél (`Say`). - **`cat`** — az NPC, a konyhában ül. Csak `Talk` interakció, dialog-fa. ### Dialógus (`dialog.cat_morning.go`) ``` [start] cat: Mióóóóóóóóu. Késő van. > „Mindjárt megy a kávé." → SetFlag("promised_coffee") → end > „Te etted meg a babot?" → goto cat_beans > „Hagyj békén." → end [cat_beans] (csak ha NEM flag `cupboard_open`) cat: A szekrényben van. Mindig ott szokott lenni. > „Köszi." → end ``` ### Hotspot-tábla **`scene.bedroom.go`** | Hotspot | Look | Use | |--------------|-----------------------------------|------------------------------------------------------------------| | `bed` | „A párnám még meleg." | `Say("player", "Most keltem fel.")` | | `nightstand` | „Az éjjeliszekrényemen egy kulcs." | egyszer: `Give("key")` + `SetFlag("key_taken")`; utána: „Üres." | | `door` | „Az ajtó a konyhába vezet." | `GoTo("kitchen")` | **`scene.kitchen.go`** | Hotspot | Look | Use | |------------------|-------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------| | `cupboard` | „Konyhaszekrény. Zárva." | ha `cupboard_open`: „Már nyitva."; különben `RequireItem("key")` → `SetFlag("cupboard_open")` + `Give("beans")` + `TakeAway("key")` | | `shelf` | „A polc tele bögrékkel." | egyszer: `Give("mug")`; utána: „Tiszta már mind." | | `coffee_machine` | „A jó öreg kávéfőző." | progresszív: 0/2 → kell `beans` és `mug`; 1/2 → kell a másik; 2/2 → `RunScript("victory")` | | `cat` (NPC) | „Cirmos, a házikedvencem." | (Talk) `RunDialogue("cat_morning")` | | `door` | „Vissza a hálóba." | `GoTo("bedroom")` | A „progresszív" használat egy kis state-trükkel: a kávéfőző két flaget figyel (`coffee_has_beans`, `coffee_has_mug`); a `Use` ág `If` action-ökkel szétágazik aszerint, hogy melyik item van kiválasztva az inventory-ban (`SelectedItem` condition), és csak a megfelelőt fogadja el. Konkrétan így néz ki a hotspot regisztrációja: ```go // domain/scene.kitchen.go (részlet) { Name: "coffee_machine", Area: p.Rect(140, 70, 50, 60), Label: "kávéfőző", OnLook: p.Say("player", "A jó öreg kávéfőző."), OnUse: p.Seq( p.If(p.And(p.SelectedItem("beans"), p.Not(p.Flag("coffee_has_beans"))), p.Seq(p.SetFlag("coffee_has_beans"), p.TakeAway("beans"), p.Say("player", "Bab betöltve."))), p.If(p.And(p.SelectedItem("mug"), p.Not(p.Flag("coffee_has_mug"))), p.Seq(p.SetFlag("coffee_has_mug"), p.TakeAway("mug"), p.Say("player", "Bögre a helyén."))), p.If(p.And(p.Flag("coffee_has_beans"), p.Flag("coffee_has_mug")), p.RunScript("victory")), ), } ``` És így néz ki egy item regisztrációja, ami egy másik item-re vagy hotspotra reagál: ```go // domain/item.key.go 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.SetFlag("cupboard_open"), p.Give("beans"), p.TakeAway("key"), p.Say("player", "Klikk.")), }, }) } ``` ### Scriptek **`script.intro.go`** — az `OnStart`-ra kötve, `bedroom` scene-ben: ``` Wait(0.5) → fade in → Say("player", "Brr, hideg van.") → Say("player", "Egy kávé kéne...") → Walk("player", Point{160, 140}) ``` **`script.victory.go`** — amikor a `coffee_machine` mindkét flaget látja: ``` Say("player", "Készen is van!") → PlaySound("snd/coffee_done") → Wait(0.8) → Say("cat", "Mióóóóu. *boldog macska*") → Say("player", "Reggeli kávé: megmentve.") → fade out → ShowEnd("Vége — kösz hogy játszottál!") ``` ### Flag/var táblázat (`domain.flag.go`) ```go const ( FlagKeyTaken = "key_taken" FlagCupboardOpen = "cupboard_open" FlagCoffeeHasBeans = "coffee_has_beans" FlagCoffeeHasMug = "coffee_has_mug" FlagPromisedCoffee = "promised_coffee" ) ``` ### Mit gyakorol a demo | Library képesség | Hol jelenik meg a demoban | |--------------------------|------------------------------------------------------------| | `Scene` + `GoTo` | bedroom ↔ kitchen ajtó | | `Hotspot` + verbek | minden helyszínen több hotspot, Look/Use/Talk | | `Inventory` + `Give` | key, beans, mug felvétele | | `RequireItem` | szekrény nyitása csak kulccsal | | Use-with-item | kávéfőző két különböző itemet kér, progresszíven | | `Flag` + `If` | szekrény „már nyitva", éjjeliszekrény „már üres" | | `Dialogue` + `Choice` | macska dialógusfája feltételes ággal | | `Script` / cutscene | intro (mozgás+beszéd) és victory (több actor) | | `Walk` + walkbox | player A* a kattintott pontig | | `PlayMusic` / `PlaySound`| szobánkénti zene, victory hangeffekt | | `Save` / `Load` | mid-game mentés, pl. a szekrény kinyitása után | | `Validate()` | CI smoke test a `Build()` outputra | Ami **nincs benne** szándékosan: több aktor egyszerre mozogva, kamera-pan, inventory-tárgyak kombinálása (item+item). Ezeket egy esetleges 2. demo (pl. "A pajta") gyakorolná, de a libraryban már első körben benne van a kapacitás. ## Implementációs ütemterv 1. **Csontváz** — `Game`, `Scene`, `Hotspot`, `AssetRegistry`, Ebiten `engine`, egér + háttér + egy darab Look kattintás. Egy szoba, három hotspot, működő `Say`. 2. **Verbek és inventory** — `VerbBar`, `Inventory`, `Item`, `UseOn`. SCUMM-szerű flow. 3. **Karakterek és walkboxok** — `Character`, `Walkbox`, A* az útkereséshez. 4. **Dialógus rendszer** — `Dialogue`, `DialogBox`, `DialogueChoice`, `Once`. 5. **Cutscene / Script** — `Action` interface, `Seq`/`Par`/`If`, `RunScript`. 6. **State, Flags, Save/Load** — perzisztencia. 7. **Polish** — tranzíciók, audio fade, font wrapping, debug overlay, validáció. Minden lépés végén egy minimális `domain/` példajáték validálja, hogy a DSL valóban kényelmes-e arra, amire ki van találva.