32 KiB
PLAN.md — pncdsl point-and-click adventure library
Cél
Egy Go nyelvű, Ebitengine 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:
// 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
}
// 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"),
},
},
})
}
// 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.
// 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).
// 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:
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+):
// 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:
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:
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) *Gamefunc (g *Game) StartAt(sceneName string) *Gamefunc (g *Game) OnStart(a Action) *Gamefunc (g *Game) Validate() errorfunc (g *Game) Run() errorfunc (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)
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)
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)
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)
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)
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)
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)
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)
type Script struct {
Name string
Actions Action // tipikusan Seq(...)
}
func (s Script) GetName() string { return s.Name }
Verb (pncdsl/ui.verb.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)
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árhuzamosanIf(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)
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)
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)
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)
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:
- input → resolve (melyik hotspoton van a kurzor, milyen verb aktív)
- aktív cutscene/dialogue lépése
- karakterek mozgása, animation tick
- trigger feltételek
- 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).
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ésCharacter.Spriteszerepel-e azAssetManager-ben, - minden
GoTo(name)célja él-e aSceneManager-ben, RunDialogue(name)ésDialogueChoice.Actions-ben szereplőGotoNodeill.RunScript(name)célok léteznek-e,- minden
Hotspot.OnUseWithmap kulcs (item/hotspot név) feloldható, StartAt(name)scene létezik,- nincs két azonos nevű regisztráció (ezt amúgy a
Registerpanic-cal megfogja).
Hibák tipizáltak: ErrUnknownAsset, ErrUnknownScene, ErrUnknownDialogue,
ErrUnknownDialogueNode, ErrUnknownScript, ErrUnknownItem, ErrDuplicateName.
Tesztelési stratégia
- A
pncdsl/*.gominden 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, amiBuild()utánValidate()-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/ebitenutilsegí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.WalkTocélokra mozog, beszél (Say).cat— az NPC, a konyhában ül. CsakTalkinterakció, 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:
// 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:
// 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)
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
- Csontváz —
Game,Scene,Hotspot,AssetRegistry, Ebitenengine, egér + háttér + egy darab Look kattintás. Egy szoba, három hotspot, működőSay. - Verbek és inventory —
VerbBar,Inventory,Item,UseOn. SCUMM-szerű flow. - Karakterek és walkboxok —
Character,Walkbox, A* az útkereséshez. - Dialógus rendszer —
Dialogue,DialogBox,DialogueChoice,Once. - Cutscene / Script —
Actioninterface,Seq/Par/If,RunScript. - State, Flags, Save/Load — perzisztencia.
- 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.