TITLE=Inkwell ROUTE=/engines/inkwell LOCALE=hu MODIFIED=1792417593 URL=https://wiki.teletypegames.org/hu/engines/inkwell L=1 https://ebitengine.org/ L=2 ssh://git@git.teletypegames.org:2222/demos/inkwell-demo q│ Az Inkwell point-and-click kalandjáték-keretrendszer Go-hoz, 1Ebitengine q│ alapokon. Sima struct literal-okat regisztrálsz be manager-ekbe; a library q│ kezeli az inputot, a renderelést, a dialog tree-ket, a cutscene-eket, a q│ HUD-ot, a theme-eket és a lazy asset-betöltést. A kódod csak tartalmat q│ deklarál. 1Amit kapsz 1══════════ • bSCUMM-korszakbeli váz0 — jelenetek hotspot-okkal és walkbox-okkal, verb-alapú interakció, inventory és item-kombók, dialog tree-k, cutscene-ek. • bEgy pattern0 — minden elnevezett entitás egy struct literal, amit a cManager.Register0-nek adsz át. • bKomponálható action-ök0 — cSeq0, cPar0, cIf0, cWait0, cSay0, cWalk0, … lazy condition-ökkel flag-ekre, változókra és az inventory-ra. • bWidget-fa alapú HUD0 — verb bar, verb coin, inventory, dialog box, speech bubble-ök, top bar, character panel-ek, chat log. Bármi, amit az Ebitengine ki tud rajzolni, érvényes widget. • bTheme-ek és placeholder-ek0 — négy színtéma és determinisztikus placeholder grafika, így a játék nulla asset mellett is elindul. • bSave, load, validate0 — JSON save slot-ok a futásidejű állapotnak, plusz induláskor validálja az összes névhivatkozást. bModulútvonal:0 cgit.teletypegames.org/engines/inkwell A „Morning Coffee" demójáték az 2cinkwell-demo0 repóban él, és sima Go-modulként használja a könyvtárat. r────────────────────────────────────────────────────────────────────────────── 1Referenciakézikönyv 1═══════════════════ 2Tartalomjegyzék 2─────────────── 1. Gyorsindítás 2. A Manager-minta 3. A Game aggregátum • Bővítmények és a Steam-réteg 4. Szöveg és nyelvek 5. Geometria 6. Entitásreferencia • Achievement 7. Az akciórendszer 8. Feltételek 9. Világállapot 10. A widgetrendszer • A menürendszer • A debug menü 11. Témák 12. Asset-pipeline 13. Hang 14. Bevitel 15. A játékciklus 16. Jelenetváltások 17. Validáció 18. Mentés és betöltés 19. Hibák 20. Tesztelés 21. Projektszerkezet r────────────────────────────────────────────────────────────────────────────── 2Gyorsindítás 2──────────── Go 1.24+ (generikus típusaliasok) és OpenGL-kontextus kell hozzá. p go mod init my-game p go get git.teletypegames.org/engines/inkwell Ha a HTTPS nem megy a privát hoszthoz, használj SSH-t: p git config --global \ p url."ssh://git@git.teletypegames.org:2222/".insteadOf "https://git.telet» p ypegames.org/" p export GOPRIVATE=git.teletypegames.org/* Egy minimális játék: p package main p p import ( p "log" p "git.teletypegames.org/engines/inkwell" p ) p p func main() { p player := &inkwell.Character{Name: "player", W: 28, H: 62} p room := &inkwell.Scene{ p Name: "room", p Layers: []inkwell.SceneLayer{{Asset: &inkwell.Asset{Name: "bg/ro» p om", Path: "assets/bg/room.png"}, Z: -1}}, p Actors: []inkwell.SceneActor{{Character: player, At: inkwell.Poi» p nt{X: 160, Y: 140}}}, p Hotspots: []inkwell.Hotspot{{ p Name: "door", p Area: inkwell.Rect(260, 50, 40, 90), p Label: inkwell.Text{{Lang: "en", Text: "door"}}, p OnLook: inkwell.Say(player, inkwell.Text{ p {Lang: "en", Text: "A wooden door."}, p }), p }}, p } p world := &inkwell.Stage{Name: "game", World: true} p p g := inkwell.New(inkwell.Game{ p Title: "Sample", p Width: 320, p Height: 200, p StartStage: world, p StartScene: room, p Player: player, p }) p g.CharacterManager.Register(player) p g.SceneManager.Register(room) p g.StageManager.Register(world) p p inkwell.RegisterDefaultUI(g) // optional; auto-called by Run if no widge» p ts registered p if err := inkwell.Run(g); err != nil { p log.Fatal(err) p } p } r────────────────────────────────────────────────────────────────────────────── 2A Manager-minta 2─────────────── Minden név szerint címezhető entitás egyetlen generikus típuson megy át: p // core.manager.go p p type Named interface { p GetName() string p } p p type Manager[T Named] struct { /* ... */ } p p type ItemManager = Manager[*Item] p type SceneManager = Manager[*Scene] p type CharacterManager = Manager[*Character] p type SpeakerManager = Manager[*Speaker] p type DialogManager = Manager[*Dialog] p type ScriptManager = Manager[*Script] p type AssetManager = Manager[*Asset] p type VerbManager = Manager[*Verb] p type WidgetManager = Manager[Widget] p type ThemeManager = Manager[*Theme] t┌─────────────────────┬──────────────────────────────────────────────────────┐ t│h Metódus t│h Viselkedés t│ t├─────────────────────┼──────────────────────────────────────────────────────┤ t│0 cRegister(v T) T0 t│0 Hozzáadja a cv0 elemet, és vissza is adja. Üres vagy t│ t│0 t│0 duplikált cName0 esetén pánikol. t│ t├─────────────────────┼──────────────────────────────────────────────────────┤ t│0 cSet(v T)0 t│0 Helyben cseréli a bejegyzést, megtartva a helyét; új t│ t│0 t│0 név esetén regisztrálja. t│ t├─────────────────────┼──────────────────────────────────────────────────────┤ t│0 cGet(name) (T, bool)0 t│0 Keresés; cfalse0, ha nincs. t│ t├─────────────────────┼──────────────────────────────────────────────────────┤ t│0 cMustGet(name) T0 t│0 Ugyanaz, de hiányzó névnél pánikol. t│ t├─────────────────────┼──────────────────────────────────────────────────────┤ t│0 cHas(name) bool0 t│0 Igaz, ha regisztrálva van. t│ t├─────────────────────┼──────────────────────────────────────────────────────┤ t│0 cLen() int0 t│0 A bejegyzések száma. t│ t├─────────────────────┼──────────────────────────────────────────────────────┤ t│0 cNames() []string0 t│0 Nevek bbeszúrási sorrendben0 (a widgetek Z-sorrendje t│ t│0 t│0 ezt használja). t│ t├─────────────────────┼──────────────────────────────────────────────────────┤ t│0 cAll() []T0 t│0 Minden bejegyzés bbeszúrási sorrendben0. t│ t├─────────────────────┼──────────────────────────────────────────────────────┤ t│0 cEach(fn func(T))0 t│0 Bejárás beszúrási sorrendben. t│ t└─────────────────────┴──────────────────────────────────────────────────────┘ A cRegister0 visszaadja azt, amit regisztrált, hogy a regisztráció bkifejezés lehessen, ne utasítás — ez teszi lehetővé, hogy egy entitásfájl deklaráció legyen, ne eljárás —, és hogy a változó, amihez köti, maga legyen a fogantyú. Minden manager bpointereket0 tart, cManager[*Scene]0-t, nem cManager[Scene]0-t, így a változó és a bejegyzés, amit a motor kikeres, egy és ugyanaz a példány, és minden mező, amely másik entitásra hivatkozik, pointer rá: cStage.Background c*Asset0, cExit.To *Scene0, cSceneActor.Character *Character0, cSay(speaker Talker, c…)0. A hivatkozás azonosító, amit a fordító ellenőriz és a szerkesztő követ; a cName0-nek az az egy dolga marad, amit csak string tud: kulcs, amellyel a mentésfájl, egy cState0-változó és egy parancssori kapcsoló újra megtalálja az entitást. p // scene.alley.go — a teljes fájl p package scene p p var Alley = SceneManager.Register(&Scene{ p Name: "alley", p … p }) Ez nem pusztán rövidebb, mint a hívást cinit()0-be csomagolni. A csomagszintű változók bfüggőségi sorrendben0 inicializálódnak, tehát a manager előbb épül fel, mint bármelyik fájl, amelyik beleír — mindegy, melyiket olvassa a fordító elsőnek; az cinit()0-es forma ehelyett arra a külön szabályra támaszkodik, hogy minden csomagszintű változó kész, mielőtt bármelyik cinit()0 lefutna. A regisztrációs sorrend mindkét formában ugyanaz. A visszatérési érték eldobható, tehát a sima utasításos alak is fordul, és az a fájl, amelyiknek iössze kell szerelnie0 valamit a regisztráció előtt, megtartja az cinit()0-jét — és jól teszi. Minden regisztrálható struct megvalósítja a cGetName()0 metódust. Egy opcionális cTypeLabel()0 érthetőbbé teszi a pánikszövegeket: p func (i Item) GetName() string { return i.Name } p func (i Item) TypeLabel() string { return "item" } A duplikátumok szándékosan pánikolnak: a duplikált regisztráció összeállítás-idejű hiba. Ha csendben elfogadnánk, futásidőben elárnyékolna egy entitást. Amikor egy regisztrációt valóban felül kell írni — egy utólag kitöltött származtatott mező, egy hot-reloadolt entitás —, a cSet0 a szándékos felülírás. Megtartja a bejegyzés helyét a beszúrási sorrendben, így a widgetek Z-sorrendje és minden más sorrendérzékeny bejárás túléli a felülírást. r────────────────────────────────────────────────────────────────────────────── 2A Game aggregátum 2───────────────── p // core.game.go p p type Game struct { p Title string p Width, Height int p p ItemManager *ItemManager p SceneManager *SceneManager p CharacterManager *CharacterManager p SpeakerManager *SpeakerManager p DialogManager *DialogManager p ScriptManager *ScriptManager p AssetManager *AssetManager p VerbManager *VerbManager p WidgetManager *WidgetManager p ThemeManager *ThemeManager p StageManager *StageManager p p State *State p Inventory *Inventory p Audio *AudioPlayer p Input *Input p p SceneRect Rectangle // az ablak azon része, amit a kép elfoglal p ExitLook func(Exit) Action // alap nézés-válasz a generált kijáratokho» p z p ExitTake func(Exit) Action // alap elvétel-válasz a generált kijáratok» p hoz p p UseWithFail func(item *Item, h *Hotspot) Action // szerzőtlen use-with» p páros p Player *Character // akit egy szereplőtl» p en jelenet kap; akit a WalkOnClick mozgat p WalkOnClick bool // az üres padlóra kat» p tintás odaviszi a Player-t (alapból ki) p Walkboxes []Polygon // a padló, ha a jelen» p et nem hoz sajátot p WindowScale int // ablak = felbontás ×» p ennyi (0 = 4) p p StartStage *Stage // a képernyő, amelyen a játék nyit p StartScene *Scene // a jelenet, amelyben egy munkamenet kezd p StartScript *Script // e jelenet OnEnter-je után áll sorba, egyszer p FinaleScript *Script // zárószkript, közvetlenül a StartScript után p StartTheme *Theme // induláskor választott téma (nil -> classic-scu» p mm) p Flags []string // State-jelzők, még az első képkocka előtt p SystemLang string // a gép vagy a platform által jelentett nyelv; a» p tárolt után, a Fallback előtt p p MaxLogLines int p p App string // a játék neve a gép felé: a konfigurációs könyvtára, a » p localStorage-előtagja p Store Store // nil: a platform alapértelmezése az App-hoz p p AchievementManager *AchievementManager // amiért a játékos elismerést » p kap — lásd Achievement p Extensions []Extension // platformrétegek: indítva, ti» p kkelve, leállítva — lásd alább p } A cNew(spec Game) *Game0 egy leírt játékból épít játékot. Minden, ami a játék ivan0, nem pedig amit itesz0, mező — így az egész egyetlen literál a hívási helyen, nem egy oldal hozzárendelés, amit az olvasónak fejben kell tartania: p game := inkwell.New(inkwell.Game{ p Title: "Underwater Sun Rising", p Width: 640, p Height: 380, p WindowScale: 2, p p StartScene: "alley", p StartScript: "opening", p StartTheme: "realworld-93", p p Player: "paul", p Walkboxes: scene.Floor, p UseWithFail: script.UseWithFail, p p SceneManager: scene.SceneManager, p ScriptManager: script.ScriptManager, p WidgetManager: widget.WidgetManager, p ThemeManager: theme.ThemeManager, p }) Amit a leírás kihagy, azt a cNew0 kitölti: üres menedzsert minden kategóriához, amit a hívó nem adott át, a futásidejű feleket (cState0, cInventory0, cAudio0, cInput0), a négy SCUMM-igét (clook0, cuse0, ctalk0, ctake0), ha egyetlen ige sem volt regisztrálva, és a négy előre elkészített témát. A widgetek bnem regisztrálódnak automatikusan — hívd meg a cRegisterDefaultUI(g)0 függvényt, vagy hagyd, hogy a cRun0 tegye meg, ha a cWidgetManager0 üres. Az itt átadott menedzser ba helyén használódik, nem másolódik0, tehát a motor és a domain egyetlen regisztert osztanak kategóriánként, nem kettőt kell szinkronban tartani — és az átadás már nem is kerül semmibe. A cThemeManager régen a presetekkel érkezett, tehát a cseréje eldobta őket; a cNew0 most ihozzáadja0 a preseteket ahhoz a cThemeManager0-hez, amivel végül dolgozik, és békén hagyja azt a nevet, amit a domain már elfoglalt. A cVerbManager0 ugyanígy működik: adj át egy telit, és a SCUMM-négyes nem íródik rá. A cNewGame(title, w, h)0 az a cNew0, amelyik lépésenként épülő játékhoz kell — semmit nem ír le az ablakon kívül, a többit a hívó rendeli hozzá: p g := inkwell.NewGame("Underwater Sun Rising", 640, 380) p g.SceneManager = mySceneManager p g.AssetManager = myAssetManager A cRun0 előtt kell megtenni — ott köti be a motor azokat a részeket, amelyek közvetlenül tartják a regisztert. p func (g *Game) StartAt(scene *Scene) *Game // entry scene p func (g *Game) OnStart(script *Script) *Game // script run after the scene» p 's OnEnter p func (g *Game) OnFinale(script *Script) *Game // closing script, queued afte» p r OnStart p func (g *Game) Validate() error // cross-check what the types » p cannot p func (g *Game) Run() error // same as inkwell.Run(g) p p func (g *Game) CurrentScene() string // ahol a játékos van p func (g *Game) PreviousScene() string // ahonnan jött; ide vezet a B» p ack() p func (g *Game) SceneArea() Rectangle // SceneRect, vagy a teljes ab» p lak p func (g *Game) SceneHotspots(name string) []Hotspot p p func (g *Game) Theme() *Theme p func (g *Game) UseTheme(theme *Theme) // runtime switch, next frame » p uses it A cStartScript0 és a cFinaleScript0 regisztrált scriptet nevez meg, nem akciót vár, így a játék nyitánya is ugyanolyan tartalom, mint bármi más — egy cScript a cScriptManager0-ben, a huzalozás érintése nélkül szerkeszthető. Mindkettő pointer, tehát nem létező scriptet nem lehet megnevezni. A cFinaleScript közvetlenül a cStartScript0 után áll sorba, ami épp az, amit egy „indulj rögtön a befejezésbe” debug-kapcsoló akar. A cStartAt0, cOnStart0 és cOnFinale0 e három mező beállítói, annak a hívónak, aki lépésenként épít játékot, nem pedig leírja; c*Game0 értéket adnak vissza, így láncolhatók. Az a játék, amelyik literált ad a cNew0-nak, a mezőket nevezi meg, és egyikre sem szorul: p game := inkwell.New(inkwell.Game{ p … p StartScene: start, p StartScript: ScriptOpening, p FinaleScript: finaleScript, // "" egy szokásos futáshoz p }) A cSceneHotspots0 a jelenet saját hotspotjai, utánuk kijáratonként egy, jelenetenként gyorsítótárazva; a kézzel megírt hotspotok jönnek előre, tehát a megfestett dolog megelőzi azt az élsávot, amin a kijárat ül. A cCurrentScene0 és a cPreviousScene0 is túléli a mentést. A felület futásidejű állapota a cGame0 objektumon él: az akciók és a motor írják, a widgetek olvassák. p func (g *Game) SelectedVerb() string p func (g *Game) SetSelectedVerb(s string) p p func (g *Game) HoverLabel() string // raw target label p func (g *Game) SetHoverLabel(s string) p func (g *Game) HoverHint() string // composed "verb + target" p p func (g *Game) SetSpeech(speaker, text string) p func (g *Game) ClearSpeech() p p func (g *Game) FlashLine(text Text) // ~2s status banner p p func (g *Game) InvokeVerb(h *Hotspot, verb string) // ige + cél -> sorba t» p ett akció p p func (g *Game) CurrentStage() *Stage // a látszó képernyő p func (g *Game) PreviousStage() *Stage // az előtte lévő p func (g *Game) OnWorldStage() bool // maga a játék látszik? p func (g *Game) Started() bool // van megkezdett munkamenet? p func (g *Game) GoToStage(stage *Stage) // másik képernyő, a következő» p képkockán p func (g *Game) Restart() // új munkamenet, a következő » p képkockán p func (g *Game) LoadLater(slot int) // visszatöltés, a következő k» p épkockán p func (g *Game) Quit() // az ablak bezárása p func (g *Game) CharacterInScene(character *Character) bool // CharacterPan» p el auto-hide p func (g *Game) DrawText(dst *ebiten.Image, s string, x, y int, c color.Color» p ) p func (g *Game) Do(a Action) // akciót sorba állít; sorrend» p ben fut p p const GlyphW, GlyphH = 6, 16 // a beépített betű cellája p func TextWidth(s string) int p func WrapText(s string, maxPx int) []string p func ClipText(s string, maxPx int) string A cDrawText0 abban a színben rajzol, amit kap, glifánként a motor saját atlaszából: minden cella egy fehér glifa egy fekete vetett árnyék fölött, így a blit színnel való skálázása a glifát színezi, az árnyékot feketén hagyja, és a cTheme0 minden szövegszíne él. Ez a négy feloldott cstring0-et kap, nem cText0-et — ezek a nyelő, és egy cText0 a cString()0-en át jut el hozzájuk. A cTextWidth0, a cWrapText0 és a cClipText0 az, amivel a beépített widgetek tördelnek, egy HUD függőleges méreteit pedig a cGlyphH0 értékből érdemes származtatni. A cDo0 sorba állít egy akciót. A sorba állított akciók egyesével és sorrendben futnak — a motor akkor indítja a következőt, amikor az előző elkészült —, így egy widget, egy trigger és egy hotspot-kattintás is beadhat munkát anélkül, hogy bármelyik elveszne. A motor saját hotspot- és cOnEnter0-akciói ugyanezen a soron mennek. A csevegésnapló gyűrűpuffer; a cGame.MaxLogLines0 értékkel korlátozható (0 = korlátlan). p type LogKind int p const ( p LogAction // "> look at door" — player commands p LogResponse // in-world replies, Say output p LogSystem // meta p ) p p type LogMessage struct { p Speaker string p Text string p Kind LogKind p } p p func (g *Game) LogAction(text string) p func (g *Game) LogResponse(speaker, text string) p func (g *Game) LogSystem(text string) p func (g *Game) Messages() []LogMessage r────────────────────────────────────────────────────────────────────────────── 3Bővítmények és a Steam-réteg p // inkwell/extension.def.go p p type Extension interface { p Start(game *Game) error // a Run eleje, a beállítások beolvasása előtt p Tick(ctx *Ctx) // minden Update első dolga, minden Stage-en p Stop() // amikor az ebiten-ciklus visszatért p } p p type Unlocker interface { // opcionális: olyan Extension, amely achievem» p entet jegyez fel p Unlock(achievement *Achievement) p } Egy cExtension0 olyan gépezet, amely a teljes futás alatt a játék mellett él, és nem widget: egy platformréteg, amelynek még az ablak megnyílása előtt fel kell állnia, minden képkockán pumpálni kell, akár tart egy script a képkockát, akár nem, és le kell állítani, amikor a ciklus visszatér. Egy widget áll, amíg script fut, és egyik végén sincs kampója — ezért külön szerződés ez. A cStart0 a cloadSettings0 előtt fut, így egy bővítmény saját cStore0-t adhat a játéknak — a beállítások aztán abból olvasódnak —, és beállíthatja a cGame.SystemLang0-ot, a gép vagy a platform által jelentett nyelvet. A cRun0 ebben a sorrendben dönt a nyelvről: ami a cRun0 előtt be volt állítva (egy c-lang kapcsoló), a tárolt beállítás, a cSystemLang0, a cFallback0. A cStart0 hibája véget vet a cRun0-nak. A cStop0 fordított sorrendben fut, cdefer0-en át, tehát akárhogy tért vissza a ciklus, lefut. Az cUnlocker0-t megvalósító bővítmény minden cAchievement0-et kézhez kap, amit a világ megszerez — lásd Achievement. Típusállítással kérdezzük, ahogy egy akció a hostját, így egy bővítmény, amelynek fogalma sincs achievementről, semmi pluszt nem valósít meg. A csteam/0 az egyetlen bővítmény, amit a motor szállít, saját csomagban, cgit.teletypegames.org/engines/inkwell/steam0, amit a gyökércsomag nem importál — az a játék, amely sosem nevezi meg, semmit nem linkel belőle, és semmit a cgithub.com/bogdannbv/go-steamworks0-ből, az alatta lévő purego-kötésből. Egy literál a cGame0-ben: p Extensions: []inkwell.Extension{ p &steam.Steam{ p AppID: 480, p Languages: map[string]string{ p "english": "en", p "hungarian": "hu", p }, p Cloud: []string{inkwell.SlotsKey}, p }, p }, A cStart0 betölti a redistributable könyvtárat, a Steamen át újraindítja a binárist, ha azon kívülről indult (cRestartAppIfNecessary0 az cAppID0-val), inicializálja az API-t, aztán átadja a játéknak, amit megtudott: a kliens nyelvét a cLanguages0-en át, amely a Steam neveit (c"english"0, c"hungarian"0) képezi a játék kódjaira, és a cCloud0-ban megnevezett cStore0-kulcsokhoz egy Steam Cloud mögötti cStore0-t, dokumentumonként egy fájllal. Minden más kulcs a platform alapértelmezett tárolójánál marad, és a felhőtároló a lemez felé enged: a semmit nem találó felhőolvasás a helyi dokumentumra esik vissza — így jut el a betöltőképernyőre a mentés, amely még a Steam előtt készült, és a következő íráskor feltöltődik —, a sikertelen felhőírás pedig helyben íródik ki, naplózva. A cTick0 a cRunCallbacks0, a cStop0 a cShutdown0, az cUnlock0 a cSetAchievement0 plusz a cStoreStats0. Ha a könyvtár nem tölthető be, vagy az API nem inicializálódik, a bővítmény naplózza, miért, és hátralép: a játék Steam nélkül fut, nem pedig sehogy. A cRunning()0 mondja meg, melyik történt. Futásidőben a Steam-kliensnek futnia kell, és a redistributable-nek — csteam_api64.dll0, clibsteam_api.so0, clibsteam_api.dylib0 — a munkakönyvtárban kell lennie, ahonnan a Steam a játékot indítja; a cSTEAMWORKS_LIB_PATH0 környezeti változó más helyet nevez meg, a cSTEAM_APPID0 pedig kiíratja a kötéssel a csteam_appid.txt0-t, amire egy Steamen kívüli futásnak szüksége van. A kötés csak 64 bites: Windows, Linux és macOS. r────────────────────────────────────────────────────────────────────────────── 2Szöveg és nyelvek 2───────────────── Minden szöveg, amit a játékos elolvas, egy cText0 — ugyanaz a szöveg annyi nyelven, ahányon valaki megírta. p // i18n.text.go p p type Translation struct { p Lang string p Text string p Voice *Voice // the recording of this line in this language, if any p } p p type Text []Translation p p var Lang string // a nyelv, amit a játék beszél p var Fallback string // ezt használja, ha a Lang-hoz nincs fordítás p p func (t Text) String() string p func (t Text) In(lang string) string p func (t Text) Empty() bool p func (t Text) Map(fn func(string) string) Text p func Format(format Text, args ...any) Text A szöveg ott marad, ahol az van, amit megnevez: egy hotspot felirata a hotspot-ban, egy párbeszédsor abban a jelenetben, amelyik kimondja. Semmi nem kerül ki egy azonosítóval kulcsolt katalógusba — egy azonosító még egy kitalálandó név és még egy dolog, amit szinkronban kell tartani, egy fordító pedig, aki a jelenetfájlt olvassa, látja, mire válasz az adott sor. Fordításonként egy sor, egymás alá, hogy a nyelvek táblázatként olvassanak: p Title: inkwell.Text{ p {Lang: "en", Text: "ALLEY — BEHIND NOODLE'S HOUSE"}, p {Lang: "hu", Text: "SIKÁTOR — NOODLE HÁZA MÖGÖTT"}, p }, p p inkwell.Say(paul, inkwell.Text{ p {Lang: "en", Text: "Locked. And I'm not guessing four digits."}, p {Lang: "hu", Text: "Zárva. És nem fogok négy számjegyet találgatni."}, p }) Egy nyelv hozzáadása egy sor hozzáadása, és nem nyúl semmihez körülötte. A nulla érték egyetlen nyelven sem mond semmit, tehát egy le nem fordított mező pontosan úgy viselkedik, mint a korábbi c""0, egy egynyelvű cText0 pedig úgy, mint egy sima string. Egy c[]Text0-en vagy egy cmap[string]Text0-en belül az elem típusa magától értetődik, és ezt hagyja maga után a cgofmt -s0 is — c"look": c{{Lang: "en", …}}0. A cString()0 a csomagszintű cLang0-ot olvassa, onnan a cFallback0-re esik vissza, végül arra a fordításra, amelyik elsőként íródott — így egy még le nem fordított sor is eljut a játékoshoz ahelyett, hogy eltűnne. Mindkettőt egyszer kell beállítani, a cRun0 előtt: p inkwell.Fallback = "en" p inkwell.Lang = *langFlag Csomagszintű változók, nem cGame0-mezők, mert egy cText0 ott oldja fel magát, ahol kiolvassák: egy widget cDraw0-jában, a cSay0 runner-ében, egy hover-feliratban, abban a hotspot-ban, amit egy cExit0 generál. Ha egy c*Game0-et kellene átfűzni mindegyiken, az mindegyikre egy paramétert tenne egy olyan értékért, amiből mindig pontosan egy van; az cAssetFS0 ugyanezért csomagszintű változó. A feloldás a megjelenítés pillanatában történik, sosem a létrehozáskor, így az cinit()0-ben felépített tartalmat nem érdekli, mikor állítod be a nyelvet. 3Komponálás Egy mondatnak, amit egy másik cText0 köré építesz, végig egy nyelven kell kijönnie. A cFormat0 a keretet és minden cText0 argumentumot ugyanazon a nyelven old fel; ami nem cText0, az úgy kerül bele, ahogy van. p var exitLook = inkwell.Text{ p {Lang: "en", Text: "That way: %s."}, p {Lang: "hu", Text: "Arra van: %s."}, p } p p g.ExitLook = func(e inkwell.Exit) inkwell.Action { p return inkwell.Say(paul, inkwell.Format(exitLook, e.Label)) p } Ez nem összefűzés, és pont ez a lényeg: az angol az elöljárót az ige és a tárgya közé teszi, a magyar utánuk. Nyelvenként egy keret az egyetlen, ami túléli a második nyelvvel való találkozást. A másik komponáló a cMap0 — minden fordítást átír, és megtartja a nyelveket, ami épp az, amit egy származtatott felirat kíván: p pin := scene.Title.Map(func(s string) string { p name, _, _ := strings.Cut(s, " — ") p return name p }) 3Amit a motor magától mond Az a néhány mondat, amit az inkwell a képernyőre tesz anélkül, hogy egy játék megírta volna, az ci18n.strings.go0-ban él, angolul és magyarul: cMsgNothing (olyan verb, amihez semmi nincs kötve), cMsgNotThat0 (párosítatlan use-with, ha az cUseWithFail0 nil), cMsgNeedItem0 (cRequireItem0 olyan tárgyra, ami nincs a játékosnál), cHintVerb0 és cHintVerbOn0 (az állapotsor), cScoreLabel0 (a cTopBar pontszám-előtagja). Változók, tehát egy játék, amely a motor számára ismeretlen nyelvet beszél, megtaníthatja rá: p inkwell.MsgNothing = append(inkwell.MsgNothing, inkwell.Translation{ p Lang: "de", p Text: "Nichts von Bedeutung.", p }) A négy alapértelmezett verb is mindkét nyelvet hordozza, és ha egy játéknak harmadik kell, a cVerbManager.Set0-tel cseréli le őket. 3Nyelvfüggetlen állapot Két dolog, amit korábban a szöveg kulcsolt, már nem azt teszi, mert a szöveg többé nem stabil azonosság. Egy cOnce0 párbeszédválasztás a csomóponton belüli bhelye0 szerint kerül feljegyzésre, nem a szövege szerint — egy elhasznált választás elhasznált marad akkor is, ha a játékot másik nyelven játsszák, a fordító pedig átfogalmazhat egy sort anélkül, hogy bárki mentését újranyitná. A cTopBar.NoteVar0 egy cState0 változót olvas, és egy cCondition0 ehhez az értékhez hasonlít, tehát magának az értéknek minden nyelven azonosnak kell maradnia. Amit a játékos elolvas, azt a cTopBar.Notes0 képezi le: p &inkwell.TopBar{ p NoteVar: "note", p Notes: map[string]inkwell.Text{ p "paused": { p {Lang: "en", Text: "paused"}, p {Lang: "hu", Text: "szünet"}, p }, p }, p } 3A betűkészlet A cText0 csak a fele: egy játéknak, amelyik második nyelvet beszél, olyan betűkészlet kell, ami ki tudja rajzolni. A motor korábban az cebitenutil.DebugPrintAt0-en át rajzolt, amelynek az atlasza 256 cella — U+0000-tól U+00FF-ig, Latin-1 és semmi tovább. Ez angolra elég, magyarra egy karakterrel kevés: az cő0 és az cű0 a Latin Extended-A-ban van, és ott van a legtöbb, ami a lengyelnek, a csehnek, a horvátnak és a töröknek kell. Bármi U+00FF fölött üres résként rajzolódott ki, beleértve azokat a gondolatjeleket és hármaspontokat, amiket a motor addig is kiírt. Az inkwell mostantól a saját atlaszát szállítja, a cfont.png0-t, amit az cinternal/fontgen0 süt ki a cbitmapfont/v40-ből: t┌───────────────┬────────────────────────────────────────────────────────────┐ t│h Tartomány t│h Mi az t│ t├───────────────┼────────────────────────────────────────────────────────────┤ t│0 U+0020–U+007E t│0 ASCII t│ t├───────────────┼────────────────────────────────────────────────────────────┤ t│0 U+00A0–U+00FF t│0 Latin-1 kiegészítés t│ t├───────────────┼────────────────────────────────────────────────────────────┤ t│0 U+0100–U+017F t│0 Latin Extended-A t│ t├───────────────┼────────────────────────────────────────────────────────────┤ t│0 U+2010–U+2027 t│0 gondolatjelek, idézőjelek, kereszt, felsorolásjel, t│ t│0 t│0 hármaspont t│ t├───────────────┼────────────────────────────────────────────────────────────┤ t│0 U+20AC t│0 € t│ t├───────────────┼────────────────────────────────────────────────────────────┤ t│0 U+2190–U+2193 t│0 nyilak t│ t└───────────────┴────────────────────────────────────────────────────────────┘ 348 glifa, 8 KB alatt — annak a betűkészletnek a latin része, amelynek a teljes alakja 2,5 MB beágyazott CJK, és ez az egész, amit egy európai point & click valaha a képernyőre tesz. A lefedettségen kívüli karakter üres keretet rajzol, így a hiány látszik, nem néma. A cGlyphW0 és a cGlyphH0 változatlanul 6×16, minden cella ugyanazt a besütött vetett árnyékot hordozza, mint a debug betűkészlet, és a sor továbbra is egy pixellel az cx0-től jobbra indul — így minden elrendezés, amit a régi renderelőhöz mértek, ugyanúgy mér. A lefedettség bővítéséhez írd át a cranges0-t az cinternal/fontgen/main.go0-ban, és futtasd; újragenerálja a cfont.png0-t és az ci18n.glyphs.go0-t is, és visszautasít minden karaktert, amit a fix 6 pixeles cella nem tud befogadni. p go -C internal/fontgen run . Az cinternal/fontgen0 saját modul, így a motor saját függőséglistája egyetlen bejegyzés marad. 3Sorok létrája p // i18n.ladder.go p p type Ladder struct { p Rungs []Text // egy sor próbálkozásonként, sorrendben p Last Text // ez áll, ha a fokok elfogytak; üresen véletlent válas» p zt p } p p func (l Ladder) At(n int) Text A cLadder0 az, amit valaki n-edszerre mond, amikor a játékos ugyanazt a hiábavaló dolgot teszi. Egy ismételt kudarcra egyetlen sorral válaszolni a legbiztosabb módja annak, hogy a játék gépnek hangozzék; a cLadder0 az első próbálkozásra az első fokkal válaszol, és láthatóan fogyni hagyja a karakter türelmét. Az cAt0 az első próbálkozást 1-nek számolja. Az utolsó fok után a cLadder véletlenszerűen választ, ami megóvja a bevett elutasítást attól, hogy beragadt lemezzé váljon — kivéve, ha megnevez egy cLast0 értéket, ami onnantól áll, mert aki feladta, az feladta. A számláló a játéké: a cState.NoteTalked0 / cState.Talked0 már számolja, hányszor rágódtak egy dolgon, olyan kulccsal, amilyennel a játék akarja — tárgyanként, hotspotonként, páronként. A szokásos hívó a cGame.UseWithFail0. r────────────────────────────────────────────────────────────────────────────── 2Geometria 2───────── p // util.geometry.go p p type Point struct{ X, Y float64 } p p func (p Point) Add(q Point) Point p func (p Point) Dist(q Point) float64 p p type Shape interface { p Contains(p Point) bool p Bounds() Rectangle p } p p type Rectangle struct{ X, Y, W, H float64 } p p func Rect(x, y, w, h float64) Rectangle p func (r Rectangle) Contains(p Point) bool p func (r Rectangle) Bounds() Rectangle p p type Polygon struct{ Points []Point } p p func Poly(pts ...Point) Polygon p func PolyRect(x, y, w, h float64) Polygon // a téglalap négy sarokpontta» p l p func (g Polygon) Contains(pt Point) bool // ray-casting p func (g Polygon) Bounds() Rectangle // axis-aligned bbox Mindkét alakzat használható cHotspot.Area0 értékként. A hotspotokat minden képkockában a cShape.Contains0 metódussal teszteljük. A cPolyRect0 azért van, mert a járófelület cPolygon0, hogy a padló bármilyen alakú lehessen — de a legtöbb padló téglalapként indul, és sarkonként kiírni nem mond semmit, amit a számok ne mondtak volna el. r────────────────────────────────────────────────────────────────────────────── 2Entitásreferencia 2───────────────── 3Asset p // asset.def.go p p type Asset struct { p Name string // logical id, "bg/kitchen" p Path string // "assets/bg/kitchen.png" p } p p // audio.def.go — sound is not an Asset p p type Audible interface{ Source() string } // Voice, Music and Sound p p type Voice struct{ Path string } // a recorded line, on a Trans» p lation p type Music struct{ Name, Path string; Loop bool } // a track a Scene or Stag» p e plays p type Sound struct{ Name, Path string } // a noise, played once by Pla» p ySound p p kitchen := g.AssetManager.Register(&inkwell.Asset{ p Name: "bg/kitchen", Path: "assets/bg/kitchen.png", p }) A cRegister0 csak a leírást tárolja el. A fájl az első használatkor nyílik meg, a hiányzó fájlból pedig determinisztikus, színes helyőrző lesz — így a játék grafika nélkül is fut. 3Scene p // scene.def.go p p type Scene struct { p Name string p Title Text // human-readable, read by TopBar p Layers []SceneLayer // a kép síkokban, mindegyik egy Z mélységnél;» p üresen a téma háttere látszik p Music *Music // starts on enter; nil changes nothing p Hotspots []Hotspot p Exits []Exit // kapcsolatok más jelenetekhez p Walkboxes []Polygon p Triggers []Trigger p Actors []SceneActor p OnEnter Action p OnLeave Action p Derived func(g *Game, self *Scene) []Exit p } p p func (s *Scene) Leads(exits ...Exit) *Scene // kijáratok a deklaráció után p p type SceneActor struct { p Character *Character p At Point p } p p type SceneLayer struct { p Asset *Asset p Z int // a szereplők a 0-n: negatív minden karakter mögött, poz» p itív előttük p } • Az cOnEnter0 akkor kerül sorba, amikor a jelenet aktuálissá válik (a beúszás után), az cOnLeave0 pedig kifelé menet. • Az cActors0 mondja meg, mely regisztrált karakterek állnak a jelenetben, és hol kezdődik a lábuk. • A cWalkboxes0 korlátozza a mozgást. Járófelületekkel a cWalk0 szélességi kereséssel útvonalaz a poligonszomszédsági gráfon (a közös éllel rendelkező poligonok szomszédok, a közös él felezőpontja útpont), a minden dobozon kívüli célpontok pedig a legközelebbi határra vágódnak. Járófelületek nélkül a karakter egyenesen megy. • A cLayers0 maga a kép, síkokban. Minden réteg egy teljes képernyős kép, átlátszó ott, ahol a mögötte lévő síkok átlátszanak, egy cZ0 mélységnél, a szereplők pedig a 0-n állnak: a negatív cZ0 minden karakter mögött van, a pozitív cZ0 minden karakter előtt, és minél távolabb a 0-tól, annál távolabb a szereplőktől. A háttér a -1-nél lévő réteg, az előtér — egy kerítés az alsó szél mentén, egy pult — az 1-nél lévő. Két azonos cZ0-nél lévő réteg megtartja a leírásuk sorrendjét. • Egy réteg cAsset0-jét nem kell sehová regisztrálni: egy jelenet beágyazhatja a literált, vagy egy játék a regisztráció után jelenetenként származtathat egyet, ahogy az a játék teszi, amelyiknek a hátterei a jelenetükről kapják a nevüket. Az üres cLayers0 a téma hátterét hagyja látszani. A hiányzó fájl a legalsó rétegnél a szokásos, névvel ellátott helyőrző, egy fölötte lévő rétegnél pedig semmi — egy hiányzó darab átlátszóság nem lemez, amit a jelenetre kellene festeni. Minden sík az egész vászon, így a festő ugyanabból a fájlból exportálja őket, mint a hátteret, rétegenként egy PNG-be, és nincs eltolás, amit karban kellene tartani. • bMásik jelenet megnevezése.0 Az cExit.To0 és a cGoTo0 magát a c*Scene0-t tartja, és két csomagszintű változó nem hivatkozhat egymásra az inicializálójában — két egymásba vezető jelenet pedig pontosan ezt tenné. Ezért a jelenetfájl előbb a jelenetet deklarálja, aztán a kijáratait, a cLeads0-szel, amely az cExits0-hez fűz és a jelenetet adja vissza: cvar _ = Alley.Leads(inkwell.Exit{To: Street, cSide: inkwell.ExitLeft})0. A második deklaráció mindkét jelenettől függ, és tőle egyik sem, így a gráf olyan kusza lehet, mint a város. • Az cExits0 a jelenet kapcsolatai a többi jelenethez, adatként leírva. A motor mindegyiket hotspottá alakítja — lásd alább. • A cDerived0 olyan kijáratokat ad vissza, amelyek a játék többi részéből következnek, nem ebbe a jelenetbe vannak beírva — egy térképképernyő gombostűi, vagyis minden helyszín, amely azt mondja, hogy ide vezet. A cRun egyszer kérdezi meg minden jelenetet, a cValidate0 előtt, és amit visszakap, az cExits0-hez fűzi, így a validáció, a hotspot-kibontás és a debug menü mind egyfajta kijáratot lát, a jelenetfájl pedig literál marad, nem pedig valami, amit a bekötés a regisztráció után megpatkol. 3Exit p // scene.exit.go p p type Exit struct { p To *Scene // ahová vezet; nil: "vissza, amerről jöttél" p Label Text // ahogy az állapotsor nevezi p Side ExitSide // hol helyezkedik el, ha az Area nil p Area Shape // felülírja a Side adta élsávot p Needs string // zászló, aminek állnia kell, mielőtt kinyílik p Blocked Action // ami addig történik, amíg nem áll p OnLook Action // felülírja a Game.ExitLook-ot erre a kijáratra p OnTake Action // felülírja a Game.ExitTake-et erre a kijáratra p } p p type ExitSide int p const ( p ExitLeft ExitSide = iota // a bal szélen ki p ExitRight // a jobb szélen ki p ExitBack // a kép mélyébe p ExitNear // a kamera felé kifelé p ) p p func ExitName(to *Scene) string A kapcsolat ahhoz a jelenethez tartozik, amelyikből kivezet, ezért annak a jelenetnek a saját fájljában áll, közvetlenül a jelenet után, a cLeads0-szel — a literál után és nem benne, mert a jelenet, ahová vezet, visszavezethet: p var Alley = SceneManager.Register(&Scene{Name: "alley", …}) p p var _ = Alley.Leads( p Exit{To: NoodleHouse, Label: "vissza az utcára", Side: ExitLeft}, p Exit{To: nil, Label: "a tűzlétra", Side: ExitBack, p Needs: "has_ladder", Blocked: Say(paul, "Nem érem el.")}, p ) A motor minden kijáratot cHotspot0-tá bont ki: cCursor: CursorExit0, a kijárat cLabel0-je, és az cOnUse0 a cGoTo(To)0-ra kötve — vagy a cBack()0-re, ha a cTo0 nil. Ha a cNeeds0 be van állítva, az egész utazás cIf(Flag(Needs), travel, Blocked)0-be kerül, tehát a kijárat így is, úgy is látszik: egy zárt ajtó is megmondja, hogy ajtó. • bElhelyezés.0 cArea0 nélkül a kijárat egy sáv a kép egyik széle mentén, a cGame.SceneArea()0 arányában méretezve — ez az a konvenció, amit minden 1990-es évekbeli point & click használt, és egy mezőnyi változtatás valódi ajtóra irányítani, ha a rajz már ki van mérve. Az a játék, amelynek a HUD-ja eltakarja az ablak alját, beállítja a cGame.SceneRect0-et, így a sávok a képen belülre esnek, nem a HUD alá. • bA megfogalmazás egy helyen.0 A cGame.ExitLook0 és a cGame.ExitTake0 adja a nézés- és elvétel-választ minden generált kijárathoz, így ezek nem ismétlődnek kijáratonként. • bA gráf olvasható marad.0 A generált hotspot neve cExitName(To)0 — c"exit:noodle_house"0, illetve c"exit:back"0 —, tehát a helyszíngráf visszaolvasható a regisztrált jelenetekből, a cValidate0 pedig elutasítja azt a kijáratot, ami nem regisztrált jelenetet nevez meg. 3Hotspot p // scene.hotspot.go p p type Hotspot struct { p Name string p Area Shape // Rectangle or Polygon p Label Text // hover hint p Cursor CursorKind p p OnLook Action p OnUse Action p OnTalk Action p OnTake Action p OnGive Action p p OnUseWith map[*Item]Action // item -> action p OnUseWithAny Action // egy válasz minden tárgyra, amit az » p OnUseWith nem nevez meg p OnVerb map[string]Action // custom verb Name -> action p } p p type CursorKind int p const ( p CursorDefault CursorKind = iota p CursorLook p CursorUse p CursorTalk p CursorTake p CursorExit p ) A hotspotok a jelenethez tartoznak, nem menedzserhez. Egy kattintás a chotspot.handler(verbName)0 hívással oldódik fel: a négy beépített ige az cOn* mezőkre képződik le, minden más az cOnVerb[verbName]0 értékre esik vissza. Az cOnUseWithAny0 felel minden olyan tárgyra, amelyet az cOnUseWith0 nem nevez meg. Egy foglalatnak, egy zárnak vagy egy embernek általában egyetlen válasza van arra, amit rátesznek, és ezt a tárgykatalógus felsorolásával kimondani olyan lista, amit egy másikkal kell szinkronban tartani. A megírt párok és a cGame.UseWithFail0 között áll, tehát amelyik hotspotnak van saját mondanivalója, az elmondja, a többi pedig továbbesik a játék általános elutasításába. Egy hotspotnak nem kell a képen lennie. Egy HUD-widget birtokolhat egyet, és átadhatja a cGame.InvokeVerb0 hívásnak — lásd a cHotspotPanel0 elemet a widgetrendszerben. 3Trigger p // scene.trigger.go p p type Trigger struct { p Name string p When Condition p Do Action p Once bool p } A triggerek a jelenetbe lépéskor élesednek. Minden tétlen képkockában (amikor a szkripthely szabad) a motor mintát vesz a cWhen0 feltételből, és bfelfutó élnél (hamis → igaz) sorba állítja a cDo0 akciót. Az cOnce: true0 élesedésenként legfeljebb egyszer sül el. A cnil0 értékű cWhen0 vagy cDo0 kimarad. Átvezető vagy párbeszéd futása közben nem veszünk mintát a triggerekből. Az él túléli ezt az elfoglalt ablakot, és a következő tétlen képkockában észleljük. A betöltés „frissen élesített" állapotba állítja vissza a triggereket, ahogy a jelenetbe való újbóli belépés is. 3Item p // item.def.go p p type Item struct { p Name string p Sprite *Sprite // a picture, or frames on a sheet;» p see Character p Description Text p Lore Text // a paragraph for a dossier screen» p , once the item is known p p OnUseSelf Action p OnUseWith map[*Item]Action // this item used on that item p } A cGive(key)0 a tárgylistához fűzi a tárgyat, amely a cName0 alapján tartja számon, és a tárgy ismertté válik (lásd cState.Known0). Ha kiválasztott tárggyal kattintasz egy hotspotra, a feloldás sorrendje: 1. chotspot.OnUseWith[item] 2. chotspot.OnUseWithAny 3. cGame.UseWithFail(item, hotspot) 4. különben felvillan a „Nem ehhez." üzenet, és megszűnik a kijelölés Az cItem.OnUseWith0 tárgy itárgyon0: kézben tartott tárggyal egy másikra kattintva az cInventoryBar0-ban az fut le, amelyik a kettőt párba állítja. 3Inventory p // item.inventory.go p p func NewInventory() *Inventory p func (i *Inventory) Add(name string) p func (i *Inventory) Remove(name string) p func (i *Inventory) Has(name string) bool p func (i *Inventory) Select(name string) // "" clears p func (i *Inventory) Selected() string p func (i *Inventory) Items() []string // copy Futásidejű állapot, amely a cGame0 tulajdona, nem menedzseré. A cGive0 / cTakeAway és az cInventoryBar0 kattintásai módosítják. 3Character p // character.def.go p p type Character struct { p Name string p Label Text // what the UI shows; empty = N» p ame p Lore Text // a bio for a dossier screen, » p once the character is known p Poses map[Pose]*Sprite // one Sprite per Pose the art » p covers; none draws the placeholder p Speed float64 // px/sec, default 60 p SpeechColor color.Color p Delivery Delivery // DeliveryBubble (default) or » p DeliveryLog p Start Point // where Game.Player stands by » p default p W, H float64 // placeholder size hint p } p p type Delivery int p const ( p DeliveryBubble Delivery = iota // speech bubble over the scene, plus th» p e log p DeliveryLog // the log alone p ) p p func (c Character) DisplayName() Text // Label, or Name p p type Speaker struct { // a voice with no body: a tape, a » p room, a narrator p Name string p Label Text p Delivery Delivery p Color color.Color p Lore Text // a bio for a dossier screen, once» p the voice is known p } p p type Talker interface{ /* *Character or *Speaker */ } p p func (c *Character) Say(text Text) Action // Say(c, text» p ): who, then what p func (c *Character) Reply(answer func(*State) Text) Action p func (s *Speaker) Say(text Text) Action p func (s *Speaker) Reply(answer func(*State) Text) Action p func (g *Game) Speaker(name string) (Speaker, bool) // a character's voice,» p or a bodiless one p func (g *Game) SpeakerLabel(name string) Text p func (g *Game) SpeechColor(name string, fallback color.Color) color.Color p func (g *Game) ItemLabel(name string) Text // Description, or Name p func (g *Game) DeliveryOf(name string) Delivery p p type Sprite struct { // a picture, or a sequence of them p Sheet *Asset p Frames []Rectangle // cut from the sheet, in order; em» p pty = the whole sheet p FrameTime float64 // seconds per frame p Loop bool p Faces Facing // the way the artwork looks; mirro» p red for the other p } p p type Pose int p const ( p PoseIdle Pose = iota p PoseWalk p PoseTalk p PoseWait p ) p p type Facing int p const ( p FacingRight Facing = iota p FacingLeft p ) • A cDelivery0 az, ahogyan egy szereplő eléri a játékost, és a cSay0 ehhez tartja magát: egy cDeliveryLog0 szereplő buborék nélkül, csak a naplóba kerül. Ehhez egyetlen domainnek sem kell a cSay0 második kiadása. (cVoice0 volt a neve, amíg a voice felvételt nem kezdett jelenteni; lásd Hang.) • Az a hang, amelyiknek egyáltalán nincs teste, nem szereplő. Rádió, kazetta, narrátor — bármi, ami beszél, de soha nem áll egy jelenetben — cSpeaker0 a cSpeakerManager0-ben: cName0, cLabel0, cDelivery0, cColor0, és semmi a járásról vagy a kirajzolásról. A cSay0, a párbeszéddoboz, a csevegésnapló és a buborék a cGame.Speaker(name)0 hívással oldja fel a nevet, először a szereplők, aztán a beszélők között, így egy sornak nem kell tudnia, melyik fajta mondta; a cGame.DeliveryOf0, a cGame.SpeakerLabel0 és a cGame.SpeechColor0 az a három válasz, amit egy widget kér. Az a név, amelyik egyik nyilvántartásban sincs, buborékban beszél, a saját nevén. • A cLabel0 az, amit a HUD kiír: fedőnév, cím, kiáltás. A cStart0 az a pont, ahol a cGame.Player0 áll egy olyan jelenetben, amelyik nem sorol fel szereplőket. A cGame.WalkOnClick0 bekapcsolásával a cGame.Player0 egyben az is, aki jár: az üres padlóra tett bal kattintás oda küldi ezt a karaktert, a jelenet walkboxain át vezetve. Alapból ki van kapcsolva, tehát az a játék mondja ki, amelyik akarja. • Ha nincs sprite a lemezen → stilizált helyőrző: humanoid, ha cW < H0, különben négylábú. Egy cRGBA0 típusú cSpeechColor0 a helyőrző testét és a buborék szövegét is színezi. • bSprite-ok és pózok.0 A cSprite0 egy kép vagy képek sorozata: egy cSheet0 és a belőle kivágott cFrames0, sorrendben, cFrameTime0 másodpercenként, ismételve vagy az utolsón megállva; üres cFrames0 esetén a lap maga az egy képkocka, így egy álló tárgy és egy sétáló karakter ugyanaz a típus. A cPoses0 egy karakternek egy-egy cSprite0-ot ad mindenre, amit csinálni látható, és a pózt a motor választja: cPoseWalk0, amíg mozog, cPoseTalk0, amíg a sora fent van, cPoseWait0, ha nyolc másodpercig egy helyben állt, különben cPoseIdle0 — vagy amit a cSetPose0 tart, amíg a karakter legközelebb el nem indul vagy meg nem szólal. A rajz által nem fedett póz cPoseIdle0-re esik vissza; cPoseIdle0 nélkül a helyőrző rajzolódik. A képkockákat cSubImage0 hívással másoljuk, és cW×H méretre skálázzuk. • Az irány nem második lap. A cSprite0 megmondja, merre néz (cFaces0), a karakter megjegyzi, merre mozgott utoljára, és ha a kettő eltér, a képkocka tükrözve rajzolódik; az irány a mentést is túléli. 3Dialog p // dialog.def.go p p type Dialog struct { p Name string p Start string // initial node; "" = first in slice p Nodes []DialogNode p } p p type DialogNode struct { p Name string p Lines []DialogLine // one per click p Choices []DialogChoice p } p p type DialogLine struct { p Speaker Talker // a *Character or a *Speaker p Text Text p } p p type DialogChoice struct { p Text Text p Show Condition // nil = always visible p Once bool // hide after the first pick p Actions []Action p } p p func (d Dialog) Node(name string) (DialogNode, bool) A folyamat: a cRunDialog(dialog)0 blokkol, amíg a párbeszéd be nem zárul → a cDialogBox0 kattintásonként egy sort mutat → az utolsó sor után megjelennek a látható cChoices0 elemek → az egyik kiválasztása lefuttatja a cSeq(choice.Actions...)0 sorozatot, és cOnce0 esetén rögzíti a cState.NoteTalked hívással. A cGotoNode("other")0 a párbeszéden belül ugrik, az cEndDialog() bezárja. 3Script p // script.def.go p p type Script struct { p Name string p Actions Action // usually Seq(...) p } Elnevezett összetett akció, egy átvezető több hívási helyről való újrafelhasználásához. A cRunScript(script)0 futtatja. 3Verb p // verb.def.go p p type Verb struct { p Name string // canonical id, "look" p Label Text // display label, "Look at" / "Nézd" p Default Action // runs when the hotspot has no hand» p ler p } p p g.VerbManager.Register(inkwell.Verb{ p Name: "push", Label: "Lökd", p Default: inkwell.Say(player, inkwell.Text{{Lang: "hu", Text: "Nem mozdul» p ."}}), p }) Az igesáv és az igekerék minden képkockában a cVerbManager.Names()0 listát olvassa, így a futásidőben hozzáadott ige azonnal megjelenik. r────────────────────────────────────────────────────────────────────────────── 3Stage p // stage.def.go p p type Stage struct { p Name string p Title Text p World bool // ez a Stage maga a játék p Background *Asset // teljes képernyős kép egy nem-világ Stage-hez p Transition Transition // ahogy ez a Stage átadja a helyét; nil: vágás p Hold float64 // hány másodpercig látszik, mielőtt Next-re lép p Next *Stage p Skippable bool // Space, Enter vagy kattintás lezárja a Hold-ot p OnEnter Action p OnLeave Action p } A cStage0 egy teljes képernyős dolog, amit a játék mutathat, és mindig pontosan egy látszik belőlük. A főmenü Stage, a beállítóképernyő Stage, a készítők listája Stage — és maga a játék is, és épp ez a lényeg: a világ nem az a dolog, ami fölött minden más képernyő lebeg, hanem egy a képernyők közül. Ez a különbség a Stage és a Scene között. A Scene egy hely a fikcióban, és a motornak mindig van egy; egy szobából a sikátorba átmenni Scene-váltás a világon ibelül0. A Stage az, ami birtokolja a képkockát. A világ-Stage-en kívül a jelenet nem rajzolódik, a karakterek nem sétálnak, a triggerek nem ütnek, és a kattintás egyetlen hotspotot sem ér el — ez az, amit egy főmenüt mutató játék akar, és amit nem kaphatott meg addig, amíg a világ megállításának egyetlen módja egy soha be nem fejeződő script volt. A widgetekhez nem kell új mező, mert az cOnStage(stage)0 ugyanolyan cCondition0, mint bármelyik másik: p game := StageManager.Register(&inkwell.Stage{Name: "game", World: true}) p mainMenu := StageManager.Register(&inkwell.Stage{Name: "main_menu"}) p p WidgetManager.Register(&inkwell.Menu{ p Name: "main_menu", p When: inkwell.OnStage(mainMenu), p Sticky: true, p Root: inkwell.MenuItem{ … }, p }) A HUD ugyanúgy rejti el magát a világon kívül, ahogy átvágóképnél már eddig is — egyetlen feltétellel, csomagoló nélkül, cif0 nélkül minden cDraw0 tetején. A cGoToStage0, a cRestart0 és a cLoadSlot0 mind a bkövetkező0 képkocka elején hajtódik végre, nem ott, ahol meghívták. A cState0, a tárgylista és minden karakter kicserélése az alatt az akció alatt, amelyik kérte, nem olyasmi, amit annak az akciónak túl kellene élnie — és az egyetlen halasztott pont teszi lehetővé, hogy egy menüsor egyetlen cSeq0-ben mondja el a teljes szándékát: p Do: inkwell.Seq(inkwell.Restart(), inkwell.GoToStage(game)), bKilépés egy Transition-ön át.0 Egy cStage0 megnevezheti, hogyan adja át a helyét: a cTransition0 egy cinkwell.Transition0, és a Stage-ből kifelé tartó váltás — egy cGoToStage0, vagy a cHold0 lejárta — ezen keresztül megy. A cFade(seconds)0 az, amit a motor ad (cFadeTo0 a feketétől eltérő színhez); a nil vágás, ami egy menünek és az alképernyőinek kell. A teljes függő váltás a letakart képkockán ér földet, az ugyanabban a lélegzetben kért cRestart0 vagy cLoadSlot0 is, így az áttűnés alatt még látszó képernyő ugyanaz a világ fölött van, amelyik fölött volt. A jelenetváltások ugyanezen a gépezeten mennek át, saját negyed másodperces áttűnéssel. bEgy kártya, amelyik továbblép.0 A cHold0 néhány másodpercig mutatja a Stage-et, majd a cNext0-re lép, a Stage cTransition0-jén át — ennyi egy splash-kártya vagy egy címsorozat: p StageManager.Register(&inkwell.Stage{ p Name: "splash_studio", p Transition: inkwell.Fade(0.5), p Hold: 2, p Skippable: true, p Next: splashTitle, p }) Az óra akkor indul, amikor az cOnEnter0 végzett: a tartás egyetlen action ugyanazon a soron, mint az cOnEnter0, így úgy ér földet, mint bármelyik másik váltás, a képkocka elején. A cSkippable0 engedi, hogy a játékos lerövidítse — Space, Enter vagy egy kattintás azonnal a cNext0-re lép, ugyanazon a cTransition0-ön át, és a billentyűt vagy a kattintást a tartás elviszi, hogy alatta semmi ne olvassa a magáénak. Pont ezért egyetlen action, és nem egy cWait0 meg egy cGoToStage0: egy átugrás után még sorban álló cGoToStage0 azon a Stage-en sülne el, amelyik addigra látszik. A cValidate0 elutasítja a senki által nem regisztrált cNext0-et és a cNext0 nélküli cHold0-ot. Az a játék, amelynek a cStartStage0-e a világ, induláskor kezd munkamenetet: belép a kezdőjelenetbe, és sorba teszi a cStartScript0-et. Az, amelyik menüvel nyit, akkor kezd, amikor a játékos mondja — ennyi a cRestart0. A cStarted()0 azt mondja meg, van-e munkamenet, és ettől engedélyezett egy „Folytatás" vagy „Mentés" sor. 3Achievement p // inkwell/achievement/achievement.go p p type Achievement struct { p Name string // a kulcs, amin egy platform ismeri — a Steam API-név p Title Text // amit egy képernyő mutat róla p Text Text p When Condition // ami megszerzi, minden képkockán kiértékelve a világ» p -Stage-en p } Egy cAchievement0 az, amiért a játékos elismerést kap, tartalomként deklarálva: egy név, egy cím és egy sor a játék minden nyelvén, és a cCondition0, amely megszerzi — egy felhúzott jelző, egy kézben tartott tárgy, egy belépett jelenet. Úgy regisztrálódik, mint bármelyik másik entitás, és cGame.AchievementManager0-ként kerül átadásra; a cValidate0 elutasítja azt, amelyiknek nincs cWhen0-je. A motor minden achievementet kiértékel minden képkockán, amelyen a világ látszik, futó scriptet is beleértve, és az éppen igazra váltakat átadja az cExtensions0 minden cUnlocker0-ének (lásd Bővítmények). Saját nyilvántartást nem vezet, egy folyamatonkénti halmazon túl, amely megakadályozza, hogy egy igaz maradó feltétel minden képkockán elsüljön: a platform, amelynek van nyilvántartása, a Steam, idempotensen válaszol az ismételt cUnlock0-ra, a platform nélküli játéknak pedig nincs mit megőriznie. A már teljesült feltételt találó cLoad0 ezért a folyamat következő futásán újra jelenti, és ez az, ami az offline megszerzett achievementet eljuttatja a platformig. A cTitle0 és a cText0 az, amit egy platform saját listája megismétel — a Steamworks partneroldalra begépelt szavak ezek —, és amit a játék saját képernyője mutatna; a motorban még semmi nem rajzolja őket. r────────────────────────────────────────────────────────────────────────────── 2Az akciórendszer 2──────────────── p // action.def.go p p type Action interface { p Start() Runner p } p p type Runner interface { p Tick(ctx *Ctx) Status p } p p type Status int p const ( p StatusRunning Status = iota p StatusDone p StatusFailed p ) p p type Ctx struct { p Game *Game p DT float64 // seconds since last tick p Scene *Scene // current scene, may be nil p Hotspot *Hotspot p Item *Item p } Az cAction0 bmegváltoztathatatlan leírás0 — nyugodtan tárolható egy cHotspot.OnLook mezőben, és minden kattintásnál újrahasználható. A cRunner0 egyetlen végrehajtás a saját állapotával; a motor mindig frisset kér. Egyszerre egy runner aktív (a „szkripthely"). A futás közben sorba állított akciók eldobódnak és naplózódnak. Párhuzamos munkához használj cPar(...)0 hívást egyetlen akción belül. A cStatusDone0 lépteti a cSeq0 sorozatot. A cStatusFailed0 rövidre zárja az egész ágat — így szakítja meg a cRequireItem0 a sorozatot. 3Beépített akciók t┌─────────────────────────────────────┬──────────────────────────────────────┐ t│h Konstruktor t│h Viselkedés t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cSeq(a ...Action)0 t│0 A gyerekek sorban. A beágyazott cSeq0 t│ t│0 t│0 kilapul, a cnil0 kimarad. t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cPar(a ...Action)0 t│0 A gyerekek együtt; akkor kész, ha t│ t│0 t│0 mind kész. t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cIf(c Condition, then Action, els0 t│0 cthen0, ha a cc0 igaz, különben t│ t│0 c...Action)0 t│0 cSeq(els...)0. t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cWait(seconds float64)0 t│0 Blokkol cseconds0 másodpercig. t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cSay(speaker Talker, text Text)0 t│0 Sor a beszélő felett + t│ t│0 t│0 csevegésnapló-bejegyzés. t│ t│0 t│0 Kattintással átugorható. Az t│ t│0 t│0 időtartam a feloldott szöveg t│ t│0 t│0 hosszával skálázódik, 1,2 mp az alsó t│ t│0 t│0 határ. t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cReply(speaker Talker, answer0 t│0 cSay0, amelyiknek a sora akkor dől el, t│ t│0 cfunc(*State) Text)0 t│0 amikor elhangzik: az canswer0 megkapja t│ t│0 t│0 a cState0-et, és a pillanat cText0-jét t│ t│0 t│0 adja vissza, így egy beszélő abból t│ t│0 t│0 válaszol, amit a játékos tett, t│ t│0 t│0 párbeszédcsomópont nélkül minden t│ t│0 t│0 esetre. t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cGoTo(scene *Scene)0 t│0 Jelenetváltás áttűnéssel. t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cBack()0 t│0 Vissza a cPreviousScene()0-re. Ha t│ t│0 t│0 nincs hova visszamenni, nem csinál t│ t│0 t│0 semmit. t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cWalk(character *Character, to0 t│0 Mozgás cCharacter.Speed0 sebességgel; t│ t│0 cPoint)0 t│0 megérkezéskor kész. t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cSetPose(character *Character, pose0 t│0 Pózban tart egy karaktert, amíg t│ t│0 cPose)0 t│0 legközelebb el nem indul vagy meg t│ t│0 t│0 nem szólal. t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cGive(item *Item)0 t│0 Hozzáadás a tárgylistához, és a t│ t│0 t│0 tárgy ismertté tétele. t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cLearn(entity Knowable)0 t│0 Egy Item, Character vagy Speaker t│ t│0 t│0 ismertté tétele — lásd cState.Known0. t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cTakeAway(item *Item)0 t│0 Eltávolítás a tárgylistából. t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cRequireItem(item *Item)0 t│0 Hiányzó tárgy → felvillan a t│ t│0 t│0 cMsgNeedItem0, és elbukik. t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cSetFlag(name)0 / cClearFlag(name)0 t│0 cState.SetFlag0 / cState.ClearFlag0. t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cSetVar(name string, v any)0 t│0 cState.SetVar0. t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cSetVarIfEmpty(name string, v any)0 t│0 cState.SetVar0, de csak amíg semmi nem t│ t│0 t│0 írta még — ezt akarja egy jelenet t│ t│0 t│0 cOnEnter0 mezője az első odaérkezéskor t│ t│0 t│0 mutatott értékhez. t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cNoting(name string, v any, inner0 t│0 Beír egy változót pontosan addig, t│ t│0 cAction)0 t│0 amíg az cinner0 fut, utána törli. t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cUseTheme(theme *Theme)0 t│0 Témaváltás scripten belülről, és a t│ t│0 t│0 beállítások mentése. t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cUseLang(lang string)0 t│0 Nyelvváltás: minden cText0 ebbe t│ t│0 t│0 oldódik fel a következő képkockától, t│ t│0 t│0 és a beállítások mentése. t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cGoToStage(stage *Stage)0 t│0 Másik Stage megjelenítése, a t│ t│0 t│0 következő képkocka elején. t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cGoBack()0 t│0 Az előzőleg látszó Stage t│ t│0 t│0 megjelenítése. Ha nincs, nem csinál t│ t│0 t│0 semmit. t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cRestart()0 t│0 Új munkamenet, a következő képkocka t│ t│0 t│0 elején. t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cLoadSlot(slot int)0 t│0 Mentett munkamenet visszatöltése, a t│ t│0 t│0 következő képkocka elején. t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cSaveSlot(slot int)0 t│0 A munkamenet slotba írása, ott ahol t│ t│0 t│0 meghívták. t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cQuit()0 t│0 Az ablak bezárása a képkocka végén. t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cFlash(text Text)0 t│0 Sor a státuszsávba ~2 másodpercre — t│ t│0 t│0 a cGame.FlashLine0 akcióként. t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cPlayMusic(music *Music)0 / t│0 cAudioPlayer0-hívások; a szám akkor t│ t│0 cStopMusic()0 / cPlaySound(sound0 t│0 ismétlődik, ha a cMusic.Loop0 mondja. t│ t│0 c*Sound)0 t│0 t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cSetVoice(on bool)0 t│0 A felvett sorok ki- vagy t│ t│0 t│0 bekapcsolása, és a beállítások t│ t│0 t│0 mentése. t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cRunDialog(dialog *Dialog)0 t│0 Elindít egy párbeszédet; blokkol, t│ t│0 t│0 amíg be nem zárul. t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cEndDialog()0 t│0 Bezárja az aktív párbeszédet. t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cGotoNode(node string)0 t│0 Ugrás az aktív párbeszéden belül. t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cRunScript(script *Script)0 t│0 Lefuttat egy regisztrált cScript0 t│ t│0 t│0 elemet. t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cShowEnd(text Text)0 t│0 Zárókártya-overlay; soha nem t│ t│0 t│0 fejeződik be. t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cCustom(fn func(*Ctx) Status)0 t│0 Vészkijárat tetszőleges t│ t│0 t│0 visszahíváshoz. t│ t└─────────────────────────────────────┴──────────────────────────────────────┘ 3Egyedi akciók Hosszan futó állapothoz (animáció, tween, lekérdezés) valósítsd meg mindkét interfészt: p type fadeAction struct{ target string; duration float64 } p p func (a *fadeAction) Start() Runner { return &fadeRunner{spec: a} } p p type fadeRunner struct { p spec *fadeAction p elapsed float64 p } p p func (r *fadeRunner) Tick(ctx *Ctx) Status { p r.elapsed += ctx.DT p if r.elapsed >= r.spec.duration { p return StatusDone p } p return StatusRunning p } Egyszeri módosításhoz a cCustom0 rövidebb: p bumpScore := inkwell.Custom(func(ctx *inkwell.Ctx) inkwell.Status { p cur := ctx.Game.State.Var("score").(int) p ctx.Game.State.SetVar("score", cur+10) p return inkwell.StatusDone p }) r────────────────────────────────────────────────────────────────────────────── 2Feltételek 2────────── p // condition.def.go p p type Condition interface { p Eval(ctx *Ctx) bool p } t┌─────────────────────────────────────┬──────────────────────────────────────┐ t│h Konstruktor t│h Jelentés t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cNot(c)0 t│0 c!c0 t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cAnd(cs ...)0 t│0 mind igaz t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cOr(cs ...)0 t│0 legalább egy igaz t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cFlag(name)0 t│0 cState.Flag(name)0 t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cHasItem(item)0 t│0 cInventory.Has(name)0 t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cSelectedItem(item)0 t│0 cInventory.Selected() == name0 t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cInScene(scene *Scene)0 t│0 az aktuális cCtx.Scene0 ez t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cOnStage(stage)0 t│0 a megnevezett Stage az, amelyik t│ t│0 t│0 látszik t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cOnWorld()0 t│0 a látszó Stage a világ, akármelyik t│ t│0 t│0 is az t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cStarted()0 t│0 van megkezdett munkamenet — indított t│ t│0 t│0 vagy betöltött t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cKnows(entity Knowable)0 t│0 cState.Known(entity) > 00 t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cVarEq(name, v)0 t│0 cState.Var(name) == v0, Go-egyenlőség t│ t├─────────────────────────────────────┼──────────────────────────────────────┤ t│0 cGiven{Flag, NotFlag, Item, NotItem,0 t│0 literál: minden kitöltött mezőnek t│ t│0 cVar, Is}0 t│0 teljesülnie kell; a cGiven{}0 mindig t│ t│0 t│0 igaz t│ t└─────────────────────────────────────┴──────────────────────────────────────┘ A feltételek állapotmentesek, és lustán értékelődnek ki, az cIf0 és a cDialogChoice.Show0 belsejében is. A cGiven0 az egyetlen, amelyik struct, nem konstruktor, és ott van értelme, ahol egy feltételt a legtöbbször olvasnak: egy párbeszéd-választásban, ahol a „csak ha a játékosnál van a kazetta, és még nem mondták el neki" két egymás alá írt mező — cGiven{Item: mysteryTape, NotFlag: "dex_told"}0 — egy hívásfa helyett. Minden kitöltött mezőnek teljesülnie kell, tehát egy cGiven0 a mezőinek cAnd0-je, és az alternatívák továbbra is cOr(Given{…}, Given{…})0 alakban olvashatók. r────────────────────────────────────────────────────────────────────────────── 2Világállapot 2──────────── p // state.def.go p p func NewState() *State p p func (s *State) Flag(name string) bool p func (s *State) SetFlag(name string) p func (s *State) ClearFlag(name string) p p func (s *State) Var(name string) any p func (s *State) SetVar(name string, v any) p p func (s *State) Visited(name string) int p func (s *State) NoteVisit(name string) // engine bumps on scene » p enter p p type Knowable interface { Named; TypeLabel() string } // an Item, a Charact» p er, a Speaker p p func (s *State) Known(entity Knowable) int // 1, 2, 3… in order of d» p iscovery; 0 for not yet p func (s *State) NoteKnown(entity Knowable) // Say notes the talker, » p Give the item, Learn anything p p func (s *State) Talked(node string) int p func (s *State) NoteTalked(node string) // DialogBox bumps on a O» p nce pick A jelzők hordozzák a fejtörők állapotát (c"cupboard_open"0). A cSetVar0 bármilyen értéket elfogad — sztringeket, pontszámokat, struct-hivatkozásokat. A cTopBar név szerint olvassa a változókat a pontszámhoz és az időhöz. A cVisited0 és a cTalked0 áll a „Once" szemantika mögött. A cKnown0 a játékos nyilvántartása arról, kivel és mivel találkozott már — egy dosszié- vagy kódex-képernyőnek. A motor magától tölti két szabály szerint — egy karakter vagy egy beszélő ismert, amint mondott egy sort, egy tárgy, amint a tárgylistában volt —, a többit egy script mondja ki cLearn(entity)0-vel: egy listáról leolvasott név, egy üvegen át látott arc. Az érték a felfedezés sorrendje, így az eszerint rendezett képernyő a játékos saját történeteként olvasható; a cKnows(entity)0 ugyanez a tény feltételként. Egy tárgy és egy karakter viselhet azonos cName0-et, mert a fajta a kulcs része. r────────────────────────────────────────────────────────────────────────────── 2A widgetrendszer 2──────────────── A HUD a cg.WidgetManager0 menedzserbe regisztrált widgetek fája. A beépítettek lefedik a klasszikus kalandjáték-felületet; a sajátjaid a könyvtár érintése nélkül illeszkednek be. p // widget.def.go p p type Widget interface { p Named p Tick(ctx *UICtx) p Draw(dst *ebiten.Image, ctx *UICtx) p } p p type Layer int p const ( p LayerScene Layer = 0 // a kép fölött, a HUD alatt p LayerPanel Layer = 100 // a lap, amin a HUD ül p LayerHUD Layer = 200 // igék, tárgylista, státuszsor, felső sáv p LayerSpeech Layer = 300 // szövegbuborékok p LayerDialog Layer = 400 // a párbeszéddoboz p LayerDebug Layer = 450 // a fejlesztői fedőréteg p LayerMenu Layer = 500 // körigék, menük p LayerCurtain Layer = 600 // zárókártyák, áttűnések p LayerCursor Layer = 700 // az egérmutató p ) p p type Layered interface { p Layer() Layer p } p p func LayerOf(w Widget) Layer // Layered, különben LayerHUD p p type Gated interface { p VisibleWhen() Condition p } p p func WidgetVisible(ctx *Ctx, w Widget) bool // hamis feltételű Gated = kika» p pcsolva p p type Awake interface { p TicksDuringScript() bool p } p p func WidgetAwake(w Widget) bool // azokon a képkockákon is fut, amiket egy » p script birtokol p p type UICtx struct { p Game *Game p DT float64 p } p p type MouseButton int p const ( p MouseButtonLeft MouseButton = iota p MouseButtonRight p ) p p type Size struct{ W, H int } p p type Align int p const ( p AlignLeft Align = iota p AlignCenter p AlignRight p ) Egy widget maga kezeli a bevitelét, a találatvizsgálatát, az állapotát és a rajzolását. Elrendezési réteg nincs — a widgetek maguk viszik a cBounds értéküket (vagy kiszámolják, mint a cRadialVerbs0). 3Z-sorrend és bevitel • bA cDrawb alulról felfelé, rétegenként fut0, tehát a magasabb rétegen ülő widget kerül felülre. Egy rétegen belül a regisztrációs sorrend dönt. • bA cTickb felülről lefelé, rétegenként fut0, így a legfelső widget kapja meg elsőként a kattintást. Az eseményt a cctx.Game.Input.ConsumeLeft()0 / cConsumeRight()0 hívással veszi birtokba; az alatta lévő widgetek ekkor már cLeftClicked() == false0 értéket látnak. • bA widget maga mondja meg a rétegét0; nem a regisztráció sorrendjéből örökli. Minden beépített widget megvalósítja a cLayered0 interfészt; amelyik nem, az a cLayerHUD0 rétegre kerül. A cCursor0 a cLayerCursor0 rétegen ül: mindig felül van, soha nem veszi birtokba a kattintást, bármikor is regisztrálták. • A rétegek teszik lehetővé, hogy egy domain bfájlonként egyesével0 regisztrálja a widgetjeit — widgetenként egy cinit()0, olyan sorrendben, ahogy a fájlnevek éppen esnek —, és a HUD mégse keveredjen össze. • Az a burkoló widget, amelyik egy belsőnek adja tovább a hívásokat, a saját cLayer0 metódusából a cLayerOf(inner)0 értéket adja vissza, hogy a burkolás ne mozdítsa el a widgetet. • bMinden beépített widget visz egy cWhen Conditionb mezőt is.0 Az a widget, amelyiknek a feltétele hamis, nem fut, nem rajzol és kattintást sem fog, így egy HUD elrejtése egy átvezetőre magukon a widgeteken mondható ki — cWhen: cinkwell.VarEq("mode", "play")0 —, nem pedig leregisztrálással vagy burkolással. Egy domain-widget a cVisibleWhen() Condition0 metódussal csatlakozik; amelyik nem valósítja meg, az mindig él. • bA futó scripté az egész képkocka0 — egyetlen widget sem fut, amíg be nem fejeződik, és ez az, ami megóvja a már elindult átvezetőt egy versenyző kattintástól. Az a widget, amelyik megvalósítja az cAwake0 interfészt, kilép ez alól: ezeken a képkockákon is fut, ba script futtatója előtt0, hogy egy általa felemésztett kattintás vagy leütés ne számítson egyben „ugorjuk át ezt a sort” utasításnak is. Két beépített widget mond erre igent: a cMenu0, amelyiket egy elindult átvezető után nem lehetne bezárni, és a cDialogBox0, mert egy beszélgetés bmaga a futó script0 — a cRunDialog0 addig birtokolja a képkockát, amíg a párbeszéd áll, tehát a doboznak azon belül kell futnia, különben a sorai soha nem lépnek tovább. • Miután minden widget lefutott, a kattintást a chandleSceneInput0 kapja meg: hotspot-interakciók, és a cGame.WalkOnClick0 mellett az a kattintás, amelyik nem hotspotra esik, odasétáltatja a cGame.Player0-t. Ez közvetlenül a cwalkCharacter0-t hívja, nem egy cWalk0 akciót sorol be, mert a besorolt séta script lenne, és a következő kattintás nem tudná átirányítani. Egy már felhasznált kattintás esetén ez nem csinál semmit. 3Beépített widgetek Mindegyik saját fájlban, cwidget..go0 néven. A nulla értékű mezők ésszerű alapértékekre esnek vissza. p // widget.panel.go — colored rectangle, optional border; backdrop for other » p widgets p type Panel struct { p Name string p Bounds Rectangle p BG color.Color // nil -> Theme.PanelBG p Border int // 0 = none p BorderColor color.Color // nil -> Theme.DialogBorder p Blocks bool // kattintásra átlátszatlan p } p p // widget.status.go — one line: the active flash message, else the hover hin» p t p type StatusLine struct { p Name string p Y int p Align Align // default AlignCenter p ScreenWidth int // 0 -> Game.Width p } p p // widget.verb_bar.go — SCUMM verb panel, Cols x ceil(N/Cols) grid from Verb» p Manager.Names() p type VerbBar struct { p Name string p Origin Point p Cols int // default 2 p Button Size p Gap Point p PanelBG bool p } p p // widget.verb_radial.go — verb coin or permanent wheel p type RadialVerbs struct { p Name string p Trigger MouseButton // default MouseButtonRight p Radius float64 // default 40 p p AlwaysVisible bool // permanent wheel at Center; Trigger ignored p Center Point p p Labels map[string]Text // rövidebb felirat, hogy elférjen a korongon p } p p // widget.inventory.go — item slots; hover sets the description, click toggl» p es selection p type InventoryBar struct { p Name string p Origin Point p Slots int p Cols int // 0 -> Slots (single row) p SlotSize int p Gap int p PanelBG bool p } p p // widget.speech.go — draws whatever Game.SetSpeech last set, above the spea» p ker's head p type SpeechBubble struct { p Name string p MaxWidth int // wrap width, default 200 p Padding int // default 3 p OffsetY int // lift above head p FallbackY int // anchor when the speaker is unknown p } p p // widget.dialog_box.go — dormant unless a dialog runs; then consumes every » p click; p // Awake, because RunDialog is the script holding the frame p type DialogBox struct { p Name string p Bounds Rectangle // default Rect(0, 140, Width, 60) p LineHeight int // default 14 p Padding int // default 6 p } p p // widget.end_card.go — fullscreen overlay from ShowEnd; consumes every clic» p k p type EndCard struct{ Name string } p p // widget.cursor.go — crosshair, or the selected item's sprite when it has o» p ne p type Cursor struct{ Name string } p p // widget.hotspot_debug.go — színes plakett és név minden kattinthatón: hots» p pot, kijárat, szereplő, tárgy, panel-cél p type HotspotDebug struct { p Name string p When Condition p Enabled bool p ToggleKey ebiten.Key // 0 -> ebiten.KeyF1 p Colors map[DebugKind]color.Color // fajtánkénti felülírás; az alfa é» p rvényes p } p // Targeted — egy widget, amelynek saját kattinthatói vannak, DebugTargets(g» p ) []DebugTarget-tel adja őket; az InventoryBar és a HotspotPanel megvalósítj» p a p p // widget.top_bar.go — left: LeftText or Scene.Title (+ NoteVar) | center: s» p core | right: time p type TopBar struct { p Name string p When Condition p Height int // default 12 p LeftText Text p ScoreVar string // State.Var key; "" = no score p ScoreMax int // >0 -> "Score: X/MAX" p TimeVar string // State.Var key; "" = no time p NoteVar string // State.Var appended after an em dash: "» p ALLEY — paused" p } p p // widget.menu.go — egymásba ágyazott lista egy ablakban; lásd „A menürendsz» p er” p type Menu struct { p Name string p When Condition p Root MenuItem // az első oldal; a Label adja az ablak c» p ímét p Bounds Rectangle // nulla = a képernyő jobb fele p ToggleKey ebiten.Key // nulla = nincs billentyű, csak Toggle() p Sticky bool // a menü MAGA a képernyő; nem zárható be p LineHeight int // alapértelmezés: GlyphH + 2 p Padding int // alapértelmezés: 6 p } p p // widget.character_panel.go — portrait, role badge, stat rows; p // auto-hides when the character is not an actor in the current scene p type CharStat struct { p Label Text p VarKey string // State.Var(VarKey), fmt-printed p } p p type CharacterPanel struct { p Name string p Bounds Rectangle p Character string p Title Text // "PLAYER", "NPC", "GUIDE", … p Stats []CharStat p } p p // widget.chat_log.go — Game.Messages() with per-kind colors, newest at the » p bottom p type ChatLog struct { p Name string p Bounds Rectangle p LineHeight int // default 10 p Padding int // default 3 p ShowBorder bool p p Speaker SpeakerStyle // SpeakerInline | SpeakerHeader | Speake» p rNone p Deliveries []Delivery // csak az így kézbesített válaszok; nil » p = mind p Resting string // a fejlécben álló karakter, míg senki n» p em szólalt meg p } p p // widget.key_binding.go — egy billentyű és amit csinál; semmit nem rajzol p type KeyBinding struct { p Name string p When Condition p Key ebiten.Key p Do Action p DuringScript bool // felel a futó script által tartott képkockákon is p } p p // widget.text_card.go — a képernyő közepére igazított sorblokk p type TextCard struct { p Name string p When Condition p Lines []Text // az üres Text egy üres sor p Align Align p LineHeight int // 0 -> GlyphH + 2 p Backdrop bool // kitölti a képernyőt a blokk mögött p } p p // widget.letterbox.go — a sávok, amelyek átvágóképnél levágják a képet p type Letterbox struct { p Name string p When Condition p Bar float64 // egy sáv magassága; 0 -> 18 p Color color.Color // nil -> fekete p } p p // widget.hotspot_panel.go — a HUD egy régiója, amely felel az igékre p type HotspotPanel struct { p Name string p When Condition p Bounds Rectangle // a régió, amelyet a panel magána» p k követel p Hotspots func(g *Game) []Hotspot // a céljai, képernyő-koordinátákb» p an p } Megjegyzések: • A cSpeechBubble0 szövegszíne: cCharacter.SpeechColor0, ennek híján cTheme.SpeechDefaultText0. • A cDialogBox0 rámutatási színe a cTheme.DialogChoiceHover0; a cOnce0 választások az első kiválasztás után eltűnnek. • A cHotspotDebug0 futásidőben az F1 billentyűvel kapcsolható; fajtánként más színű plakettet fest minden kattinthatóra, a nevet csak a jelenet saját céljaira írja rá (egy widget a saját feliratát már kiírja), a cLayerDebug rétegen, a menük alatt. • A cTopBar0 kihagyja az üres mezőket, így két- vagy egyszakaszos elrendezésre esik vissza. • A cCharacterPanel0 és a cChatLog0 a helyőrző portrékat és a téma csevegésnapló-színeit használja újra. Egy cLogResponse0 sor a beszélő saját cCharacter.SpeechColor0 értékét kapja, ha a karakter hordoz ilyet, így a több hangot tartalmazó napló több hangként is olvasható marad. • Kis cChatLog0 esetén állítsd a cGame.MaxLogLines0 értékét nagyjából 64-re. • Egy cChatLog0 a beszélgetés egy részére is átadható, nem csak az egészre. A cDeliveries0 csak az általa felsorolt módon kézbesített válaszokat tartja meg — így ad egy rádiót, kazettát vagy narrátort használó játék saját panelt annak a hangnak, anélkül, hogy a widgetnek tudnia kellene, mi az a dolog. A motor már tudja, hogyan ér el egy karakter a játékoshoz (cCharacter.Delivery0), tehát a szűrő ez a tény, nem egy második, amit szinkronban kell tartani vele. Ilyen panelhez a cSpeakerHeader0 illik: a név fejlécként olvasható, nem a margón lefelé ismételt szóként. • A cLetterbox0 a cLayerScene0 rétegen ül, tehát a kép fölé és a HUD alá rajzol. Az a játék, amely átvágóképnél a HUD-ot is elrejti, két, egy feltételen osztozó regisztrációból kapja meg az egész konvenciót. • A cHotspotPanel0 a HUD egy régióját teszi képessé arra, hogy feleljen az igékre, ahogy a kép egy régiója már felel. Semmi mást nem birtokol, csak egy találattesztet: a visszaadott hotspotok a cGame.InvokeVerb0 hívásán mennek át, tehát a panelre szerelt eszköz megkapja a nézd, húzd, szólj és vedd igéket — és a use-with folyamatot, a naplósorokat és a nem-történik-semmi visszaesést — ugyanabból a kódból, amiből egy festett hotspot. Ez tartja vissza a játék saját HUD-ját a motor újraimplementálásától: nélküle egy két kattintható foglalattal rendelkező widgetnek magának kell kiolvasnia a kiválasztott igét, ellenőriznie a kézben tartott tárgyat, elfogyasztania a kattintást és megválaszolnia minden kombinációt, egy switchben, a játék saját szövegével bezárva oda. A cHotspots0 inkább függvény, mint szelet, mert ami egy panelre fel van szerelve, az a játékkal együtt változik, és az a widget, amely minden alkalommal megkérdezi, nem igényel érvénytelenítést. A panel semmit nem rajzol — ágyazd be, és írj saját cDraw0 metódust. 3A clickBlocker szerződés Azok a widgetek, amelyeknek fix területük van, ne engedjék, hogy egy cRadialVerbs0 menü felugorjon föléjük: p type clickBlocker interface { p BlocksClickAt(p Point) bool p } Megvalósítja a cVerbBar0, az cInventoryBar0, a cDialogBox0, a cTopBar0, a cCharacterPanel0, a cChatLog0, és a cRadialVerbs0 maga is, amíg látható, és a cMenu0 — az ablaka, amíg nyitva van. Valósítsd meg a saját widgetjeiden, hogy tömör beviteli zónaként jelöld meg őket. 3Regisztrációs segédek p // widget.preset_scumm.go, widget.preset_radial.go, widget.preset_rich.go p p func RegisterDefaultUI(g *Game) // verb bar + inv» p entory p func RegisterRadialVerbUI(g *Game) // verb coin + w» p ider inventory p func RegisterRichUI(g *Game, player, npc *Character) // top bar + pane» p ls + chat log + wheel Adj át c""0 karakternevet, ha ki akarod hagyni az adott cCharacterPanel0 elemet. Mindhárom a szokásos Z-sorrendben regisztrál: a debug-overlay hátul, a kurzor elöl. 3Egyedi widgetek Bármi működik, ami megvalósítja a cWidget0 interfészt. Olvasd a cctx.Game objektumot, vedd birtokba a bevitelt a cctx.Game.Input0 hívásokkal, és rajzolj Ebitengine-nel. p type Minimap struct { p Name string p Bounds inkwell.Rectangle p } p p func (m *Minimap) GetName() string { return m.Name } p func (m *Minimap) Tick(ctx *inkwell.UICtx) {} p func (m *Minimap) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) { p // render scene thumbnail, mark NPCs, … p } p p g.WidgetManager.Register(&Minimap{Name: "minimap", Bounds: inkwell.Rect(220,» p 4, 96, 56)}) 3A menürendszer A cMenu0 egy egymásba ágyazott lista egy ablakban: egy oldal sorokból, amelyek bármelyike megnyithatja a saját oldalát, olyan mélyen, ameddig a fa tart. Widget, mint bármi más — a cLayerMenu0 rétegen, a cWhen0 mezővel kapuzva, a saját fájljából regisztrálva — és ez a menürendszer egésze. Egy debug menü, egy szüneteltető menü és egy fikción belüli készülék kijelzője ugyanez az egy widget, más cRoot0 mezővel. Egy sor és egy oldal bugyanaz a típus0. A cMenuItem.Label0 nevezi meg a sort a szülő oldalán, és ugyanez a felirat adja az általa megnyitott oldal címét — ettől jön ki magától a címsorban futó útvonal: p // widget.menu.go p p type MenuItem struct { p Label Text p Value func(*Ctx) Text // a jobb oldali oszlop: szám, cél, „nál» p am” p Items []MenuItem // az oldal, amit ez a sor megnyit, megí» p rva p Expand func(*Ctx) []MenuItem // az oldal, amit ez a sor megnyit, a já» p tékból kiolvasva p Body Text // az oldal, amit ez a sor megnyit, olva» p snivaló bekezdésként p Do Action // amit egy levélen az Enter beállít a s» p orba p Enabled Condition // hamis: tompítja a sort, és nem engedi» p az Entert p KeepOpen bool // a Do után is nyitva hagyja a menüt p } p p func FixedValue(t Text) func(*Ctx) Text // olyan Value, ami nem függ a ját» p éktól Az a sor, amelyiknek se cItems0, se cExpand0, se cBody0 mezője nincs, levél: az Enter a cDo0 mezőjét adja át a cGame.Do0 hívásnak. A két oldal-mező egyetlen döntés: baz cItemsb az, amit egy szerző beírt, az cExpandb az, amit a játék tud. Egy cExpand0 oldal újraépül, amíg nyitva van, tehát a jelenlegi jelenetben kattintható dolgok listája átíródik, amikor a játékos elhagyja a jelenetet; egy cItems0 oldal képkockánként semmibe nem kerül. A cValue0, az cExpand0 és az cEnabled0 ugyanazt a c*Ctx0 értéket kapja, mint egy akció vagy egy feltétel, tehát a cctx.Scene0 kérés nélkül ott van. A cBody0 az általa megnyitott oldalt sorok helyett bekezdéssé teszi: a cText0 az ablakhoz tördelve, a játékos nyelvén, ugyanazokkal a billentyűkkel görgetve, választani való nélkül. Egy kódex-bejegyzés, egy levél, egy életrajz. A cBody0-s sor ugyanazt a c→0-t mutatja, mint egy oldalt nyitó sor, és a cDo0 mezője sosem fut le. A menü arra válaszol, amire egy lista mindig is válaszolt, és bminden általa bhasznált billentyűt birtokba vesz0, hogy semmi ne lássa alatta: t┌─────────────────────────────────┬──────────────────────────────────────────┐ t│h t│h t│ t├─────────────────────────────────┼──────────────────────────────────────────┤ t│0 fel / le, egérgörgő t│0 járja a sorokat, körbe t│ t├─────────────────────────────────┼──────────────────────────────────────────┤ t│0 PageUp / PageDown / Home / End t│0 ugrás t│ t├─────────────────────────────────┼──────────────────────────────────────────┤ t│0 Enter, jobbra t│0 megnyitja a sort, vagy lefuttatja t│ t├─────────────────────────────────┼──────────────────────────────────────────┤ t│0 Backspace, balra, jobb egérgomb t│0 egy szinttel vissza; a gyökéren bezár t│ t├─────────────────────────────────┼──────────────────────────────────────────┤ t│0 Escape, a c×0 a címsorban t│0 bezár t│ t├─────────────────────────────────┼──────────────────────────────────────────┤ t│0 ráhúzás t│0 mozgatja a kurzort — csak ha maga a t│ t│0 t│0 mutató elmozdult, hogy ne küzdjön a t│ t│0 t│0 billentyűkkel t│ t├─────────────────────────────────┼──────────────────────────────────────────┤ t│0 bal egérgomb t│0 megnyitja a mutató alatti sort t│ t├─────────────────────────────────┼──────────────────────────────────────────┤ t│0 c←0 a címsorban t│0 egy szinttel vissza t│ t└─────────────────────────────────┴──────────────────────────────────────────┘ Csak az ablakon belüli kattintás emésztődik fel, tehát egy menü a képernyő egyik oldalán játszhatóan hagyja a másikat. A cToggleKey0 nyitja és zárja; a nulla nem köt billentyűt, és ezt az cOpen0, a cClose0 és a cToggle0 hívásra hagyja — így vezérelné egy HUD-gomb. A cSticky0 a menüt magává a képernyővé teszi, nem egy fölötte lévő ablakká: nyitva van, amikor látható, az cEscape0 és a gyökéren a vissza nem csinál semmit, a cToggleKey0 figyelmen kívül marad, és a bezáró doboz nem rajzolódik ki. Egy induló főmenü mögött nincs semmi, amire be lehetne csukni, és az a menü, amit üres képkockába lehet elbocsátani, nem a kilépés módja, hanem a játék elvesztéséé. Egy Stage rendszerint ilyen menüből áll, és a billentyűlegendája ezt mondja is — nem kínál ESC-et. A színek az aktív cTheme0 dialógus-tokenjeiből jönnek, tehát egy menü téma-munka nélkül úgy néz ki, mint a játék többi része; a letiltott sor nem külön tokenből, hanem elhalványítva rajzolódik. A cMenuEmpty0 (üres oldal) és a cMenuKeys0 (a lábon futó billentyű-legenda) motorszövegek, az ci18n.strings.go fájlban. 3A debug menü A cDebugMenuRoot0 a menürendszer egyetlen felhasználása, amit a könyvtár szállít: amiből a játék áll, és ami kattintható abban a jelenetben, ahol a játékos áll. bAdat, nem widget0 — a widget maga a menü: p g.WidgetManager.Register(&inkwell.Menu{ p Name: "debug_menu", p Root: inkwell.DebugMenuRoot, p When: whenDebug, p ToggleKey: ebiten.KeyD, p }) p DEBUG p ├── Scenes minden regisztrált jelenet, regisztrációs sorrendben p │ └── érték: „itt”, vagy ×N látogatás · az Enter odavis» p z p ├── Items minden regisztrált tárgy p │ └── érték: „nálam” / „kézben” p │ ├── add ide · vedd el · válaszd ki p │ ├── használd magában csak ha a tárgynak van OnUseSelf mezője p │ └── használd ezzel → a tárgy saját OnUseWith párjai p ├── Action points a jelenlegi jelenet hotspotjai, a kijáratokkal, találatvi» p zsgálati sorrendben p │ └── érték: a CursorKind értéke p │ ├── csak azok az igék, amikre a hotspot válaszol p │ └── használd ezzel → a hotspot OnUseWith párjai p ├── Exits a jelenlegi jelenet kijáratai, ahogy a gráf látja őket p │ └── exit: érték: melyik élen ül p │ ├── menj p │ └── kell hozzá: érték: be / ki p │ └── állítsd be · töröld p └── EXPLORER maga a Game, mezőnként — lásd alább A sorok bregistry-kulcsokat0 neveznek meg, nem a prózát, amit a játékos lát: a debug menüt az olvassa, aki a tartalmat írja, és a cnoodle_house0 ott többet mond, mint a c"NOODLE HÁZA — KONYHA"0, bármely nyelven. Minden verb az cinvokeHotspotVerb0 hívásán megy át, ugyanazon az úton, amit egy kattintás bejár, tehát a chat-napló és az állapotsor úgy olvasódik, mintha a játékos tette volna. Egy „használd ezzel” sor előbb átadja a tárgyat — a cél az, hogy lássuk a párosítást lefutni, nem az, hogy emlékeztetve legyünk rá: a játékos soha nem vette fel azt a dolgot. Semmi nem regisztrálja, a cRegisterDefaultUI0 sem: a debug menüt a játék kéri el, és arra kapuzza, amit a debug buildjei beállítanak. Amelyik játék saját szekciókat akar, körülépít egy gyökeret: p Root: inkwell.MenuItem{ p Label: inkwell.DebugMenuRoot.Label, p Items: append(inkwell.DebugMenuRoot.Items, mySection), p }, 3Az explorer Az cExplorerRoot0 a másik lapfa, amit a könyvtár szállít, és a cDebugMenuRoot utolsó lapja: maga a cGame0, mezőnként, reflectionnel kiszámolva, amikor egy lap megnyílik. Egy manager az entitásaira nyílik, egy entitás a literáljára, egy akció az akciókra, amikből áll, egy cText0 a fordításaira — olyan mélyen, amilyen mélyen a játékot megírták, és semmit nem kell kétszer leírni. p EXPLORER p ├── Title · Width · Height · … érték: maga a skalár p ├── SceneManager érték: hány darab p │ └── érték: a Name mezője p │ ├── Title → en · hu érték: a sor, aztán fordításonként p │ ├── Layers → → Asset · Z p │ └── Hotspots → p │ └── OnLook érték: Seq p │ ├── futtasd az Enter sorba állítja az akciót p │ └── children → [0] érték: Say p │ ├── futtasd p │ ├── speaker érték: paul p │ └── text érték: a sor p ├── ItemManager · CharacterManager · … p └── State · Inventory · … a futás, ahogy áll • Az érték-oszlop az, amit a sor most ér: név annak, aminek van, az aktuális nyelv sora egy cText0-nek, cGiven = true0 egy cCondition0-nek, a játék jelen állása szerint kiértékelve, az akció fajtája — cSay0, cSeq0, cIf0 — egy cAction0-nek, c#RRGGBBAA0 egy színnek, darabszám egy gyűjteménynek. • Egy regisztrált entitásra mutató pointer link: az cItem = key0 magára a tárgyra nyílik, bárhol is találkoztunk vele. Egy függvény cfunc0, és semmire nem nyílik; a cReply0, a cCustom0 és a cDerived0 closure, és az explorer ott áll meg, ahol az írás kóddá válik. • A nem exportált mezőket is olvassa, egy friss pointeren át ugyanarra a memóriára: egy akció beszélője és sora egy nem exportált típus nem exportált mezőiben él, és az az explorer, amelyik nem látna bele egy cSay0-be, pontosan ott állna meg, ahol az írás kezdődik. Minden akció-sor a cfuttasd0-dal nyílik, így bármely megírt szekvencia lejátszható onnan, ahol írták. • Az cExplore(game)0 úgy futtatja a játékot, hogy a képernyőn semmi más nincs, csak ez — egy Stage, amely egy cSticky0 cMenu0 az egész ablakon, az cExplorerRoot0-ban gyökerezve —, ugyanúgy validálva és bekötve, ahogy a cRun köti be, tehát azt mutatja, ami a játék lett volna. r────────────────────────────────────────────────────────────────────────────── 2Témák 2───── p // theme.def.go p p type Theme struct { p Name string p p PanelBG color.Color p StatusText color.Color p FlashText color.Color p p VerbButtonBG color.Color p VerbButtonSelectedBG color.Color p VerbButtonText color.Color p p InventorySlotBG color.Color p InventorySlotSelectedBG color.Color p p SpeechBubbleBG color.Color p SpeechDefaultText color.Color p p DialogBG color.Color p DialogBorder color.Color p DialogChoiceBG color.Color p DialogChoiceHover color.Color p DialogSpeaker color.Color p DialogText color.Color p p EndCardBG color.Color p EndCardText color.Color p CursorColor color.Color p HotspotOutline color.Color p SceneBackdrop color.Color p p TopBarBG color.Color p TopBarText color.Color p TopBarAccent color.Color p p ChatLogBG color.Color p ChatLogPrompt color.Color p ChatLogResponse color.Color p ChatLogSystem color.Color p p CharacterPanelBG color.Color p CharacterPanelBorder color.Color p CharacterPanelTitle color.Color p } A cNew0 regisztrálja az előre elkészített témákat, és a cStartTheme0 témát választja, vagy a cclassic-scumm0-ot, ha a játék egyet sem nevez meg. Hozzáad, nem cserél, tehát az a domain, amelyik saját cThemeManager0-t ad át, megtartja mindazt, ami már benne van: t┌────────────────┬───────────────────────────────────────────────────────────┐ t│h Név t│h Hangulat t│ t├────────────────┼───────────────────────────────────────────────────────────┤ t│0 cclassic-scumm0 t│0 SCUMM-korabeli sötétkék meleg borostyánsárga t│ t│0 t│0 kiemelésekkel (alapértelmezett). t│ t├────────────────┼───────────────────────────────────────────────────────────┤ t│0 csierra-coin0 t│0 Sötétebb lila és narancs, az igeérméhez tervezve. t│ t├────────────────┼───────────────────────────────────────────────────────────┤ t│0 cpaper-notebook0 t│0 Krémszínű papír, tinta, egy kis szépia. t│ t├────────────────┼───────────────────────────────────────────────────────────┤ t│0 cterminal-green0 t│0 Retró foszforzöld feketén. t│ t└────────────────┴───────────────────────────────────────────────────────────┘ A saját témád csak egy újabb regisztráció: p g.ThemeManager.Register(inkwell.Theme{ p Name: "midnight-noir", p PanelBG: color.RGBA{6, 6, 12, 255}, p StatusText: color.White, p // … the remaining color fields … p }) p g.UseTheme("midnight-noir") A widgetek minden képkockában olvassák az aktív témát, így egy futásidejű váltás a következő rajzoláskor látszik. r────────────────────────────────────────────────────────────────────────────── 2Asset-pipeline 2────────────── Az cAssetManager.Register0 csak a leírást tárolja. Az első képkérésnél: 1. megnyitja az cAsset.Path0 fájlt, 2. hiányzó vagy olvashatatlan fájl esetén determinisztikus helyőrző színt generál az cfnv32a(name)0 értékből, és megjelöli a bejegyzést, 3. gyorsítótárazza a dekódolt c*ebiten.Image0 objektumot. p // internal p func (la *loadedAssets) image(asset *Asset) *ebiten.Image p func (la *loadedAssets) isPlaceholder(asset *Asset) bool A widgetek az cisPlaceholder0 alapján döntenek a stilizált helyőrző és a valódi sprite között. Képformátumok: PNG és JPEG. A hang egyáltalán nem cAsset0: a cVoice0, a cMusic0 és a cSound0 elemeket az cAudioPlayer0 dekódolja igény szerint. A motor saját betűtípusa nem asset: az a cfont.png0-be sütött atlasz. r────────────────────────────────────────────────────────────────────────────── 2Hang 2──── p // audio.def.go p p type Audible interface{ Source() string } p p type Voice struct{ Path string } // felvett sor p type Music struct{ Name, Path string; Loop bool } // zeneszám p type Sound struct{ Name, Path string } // zaj p p // asset.audio.go p p func NewAudioPlayer() *AudioPlayer p func (a *AudioPlayer) PlayMusic(music *Music) p func (a *AudioPlayer) StopMusic() p func (a *AudioPlayer) PlayVoice(voice *Voice) float64 // hány másodpercig » p tart; 0, ha semmi nem szólt p func (a *AudioPlayer) StopVoice() p func (a *AudioPlayer) PlaySound(sound *Sound) Három dolog hallható, és három típus, mert háromféleképp szól: • cVoice0 egy felvett sor. A cTranslation0-ön ül, mert az angol és a magyar felvétel két fájl, és a cSay0 azt játssza, amelyet a feloldott nyelv hordoz — pontosan addig, amíg a felvétel tart, a szöveghossz-időzítő helyett. Az átugrott sor elhallgat. Egy cVoice0 egyetlen soré, ezért nem kell neki név és regisztráció: c{Lang: "hu", Text: "Katt.", Voice: &Voice{Path: c"assets/voice/alley/click.hu.ogg"}}0. A játékos a beállításokban kikapcsolhatja (cSettings.Voice0, cSetVoice0), és a cSay0 visszaáll az időzítőre. • cMusic0 egy szám, amelyet egy cScene0 vagy egy cStage0 játszik, amíg látszik, és ismétlődik, ha a cLoop0 mondja. Olyan Scene-be vagy Stage-be lépni, amelynek a cMusic0-ja már szól, nem változtat semmin; a nil cMusic0 meghagyja, ami szól. A cPlayMusic0 ugyanez akcióként. • cSound0 egy zaj — ajtó, retesz, helyére kattanó kazetta —, amelyet a cPlaySound egyszer játszik le, a zene és egy másik cSound0 fölött. Az Ebiten caudio0 csomagjára épül. A közös kontextus lustán, 48 kHz-en jön létre az első lejátszáskor; egy már létező caudio.CurrentContext0 újrahasznosul. A zene lemezről streamel; a hangok és zajok egyszer dekódolódnak PCM-be, cSource szerint gyorsítótárazódnak, és a cNewPlayerFromBytes0 játssza le újra. A befejezett zajlejátszók a következő cPlaySound0 hívásnál takarodnak ki. Formátumok kiterjesztés szerint: WAV, OGG Vorbis, MP3. Bármi más dekódolása elbukik, a forrás megjelölődik, így a későbbi lejátszások nem csinálnak semmit. Minden hiba (hiányzó fájl, nem támogatott kodek, nincs hangeszköz) egyetlen debug-naplósor — a játék hang nélkül fut tovább. r────────────────────────────────────────────────────────────────────────────── 2Bevitel 2─────── p // input.def.go p p func (i *Input) Pos() (int, int) p func (i *Input) Point() Point p func (i *Input) LeftClicked() bool // épp lenyomott ÉS még nem felemés» p ztett p func (i *Input) RightClicked() bool p func (i *Input) ConsumeLeft() p func (i *Input) ConsumeRight() p p func (i *Input) Wheel() (float64, float64) // a képkocka elmozdulása» p ; semmi nem emészti fel p p func (i *Input) KeyPressed(key ebiten.Key) bool // épp lenyomott ÉS még n» p em felemésztett p func (i *Input) ConsumeKey(key ebiten.Key) p func (i *Input) ClaimKey(key ebiten.Key) bool // KeyPressed + ConsumeKe» p y A motor képkockánként egyszer kérdez le, és pillanatképet készít a kurzorról, mindkét éppen lenyomott állapotról, a görgő elmozdulásáról és minden lenyomott billentyűről. Használatkor felemésztjük: egy kattintás mérvadó, és az nyer, aki elsőként veszi birtokba. A billentyűk ugyanígy működnek, és a cClaimKey0 az az alak, amit egy féltucat billentyűt lekötő widget akar: egy listát a nyilakkal járó menü birtokba veszi őket, így azon a képkockán más nem olvassa ki. A cKeyPressed0 a cLeftClicked mintájára él-vezérelt — egy lenyomva tartott billentyű egyszer válaszol igazzal — és az a widget, amelyik nyomvatartást akar, maga olvassa az cebiten.IsKeyPressed0 hívást. r────────────────────────────────────────────────────────────────────────────── 2A játékciklus 2───────────── p // core.run.go p p func Run(g *Game) error // same as g.Run() A cRun0 sorrendben: 1. cg.Audio.attach(g.AssetManager)0 — bekötni a hanglejátszót abba az asset-regiszterbe, amelyet a játék addigra tart. 2. cg.Validate()0. 3. cRegisterDefaultUI(g)0, ha a cWidgetManager0 üres. 4. Elhelyezi a kezdőjelenetet átmenet nélkül, növeli a cState.NoteVisit értékét, pozicionálja a szereplőket, elindítja a zenét. 5. Sorba állítja a cSeq(scene.OnEnter, OnStart script, OnFinale script) sorozatot első akcióként. 6. cebiten.SetWindowSize(Width*WindowScale, Height*WindowScale)0 (beállítatlanul 4×), cebiten.SetWindowTitle(g.Title)0, cebiten.RunGame(&engine{g})0. p engine.Update: p poll input p update transition p if fading out -> return p p if scriptRunner != nil: p tick the Awake widgets, top layer down # a menu, so it stays clo» p sable p tick runner; tick characters; return p p clear hoverLabel p for w in WidgetManager, top layer down, WidgetVisible only: w.Tick(uictx» p ) p handleSceneInput() # hotspots, right-click re» p set p tick characters p p engine.Draw: p fill Theme.SceneBackdrop, or black p for layer in scene layers with Z <= 0, by Z: drawFullScreen(layer) p for c in characters sorted by Y: drawCharacter(c) p for layer in scene layers with Z > 0, by Z: drawFullScreen(layer) p for w in WidgetManager (by layer, then registration order), WidgetVisibl» p e only: w.Draw(screen, uictx) p draw transition overlay A chandleSceneInput0 azután fut, hogy minden widget megkapta az esélyét: 1. Beállítja a rámutatási feliratot a kurzor alatti hotspotból. 2. Jobb kattintás: megszünteti a fogott tárgy kijelölését, vagy a cselectedVerb értéket c"look"0 értékre állítja vissza. 3. Bal kattintás tárggyal: chotspot.OnUseWith[item]0, majd citem.OnUseWith[hotspot]0, majd a cGame.UseWithFail0, különben felvillan a „Nem ehhez.". 4. Bal kattintás tárgy nélkül: chotspot.handler(selectedVerb)0, majd az ige cDefault0 akciója, különben felvillan a „Semmi említésre méltó.". 5. Minden sikeres kattintás betol egy cLogAction0 sort. Karaktermozgás: a ctickCharacters0 az útpontsor elejét lépteti cSpeed0 px/mp sebességgel, megérkezéskor kiveszi, és a karakter tétlenné válik, amikor a sor kiürül. A sort a cWalk0 építi, amely cStatusRunning0 értéket ad vissza, amíg a karakter meg nem áll. A ctickPose0 képkockánként kiválasztja a cPose0-t — séta, beszéd, várakozás, tétlenség, vagy amit a cSetPose0 tart —, és annak a sprite-nak az aktuális képkockáját másolja ki az iránynak megfelelően tükrözve, vagy visszaesik a helyőrzőre. r────────────────────────────────────────────────────────────────────────────── 2Jelenetváltások 2─────────────── p // transition.def.go p p type Transition interface { p Seconds() float64 p Draw(dst *ebiten.Image, width, height int, cover float64) p } p p func Fade(seconds float64) Transition p func FadeTo(seconds float64, to color.Color) Transition A cTransition0 az, ahogy egy kép átadja a helyét a másiknak, két félben: a régi kép letakarodik, a csere azon a képkockán ér földet, amelyiken teljesen el van rejtve, és az új kép kitakarodik. A cSeconds0 egy fél hossza; a cDraw0 a takarást festi, miközben a ccover0 0-ról 1-re fut, aztán vissza. A cFade0 az, amit a motor ad, egy törlés vagy egy írisz ugyanennek a két metódusnak egy másik megvalósítása. Egy Stage a sajátját a cStage.Transition0-ben nevezi meg; egy jelenetváltás a cFade(0.25)0-öt használja. A cGame.changeScene(name)0 ezt a negyed másodperces áttűnést indítja. A felezőpontban: 1. sorba állítja a cprevScene.OnLeave0 akciót, 2. beállítja az új ccurrentScene0 értéket, 3. cState.NoteVisit(name)0, 4. elhelyezi a szereplőket a cSceneActor.At0 pozícióikban, 5. lejátssza az új cMusic0 elemet, 6. sorba állítja a cnewScene.OnEnter0 akciót. Aztán jön a visszaúszás. A kiúszás felében a bevitel be van fagyasztva. A cStartScene0 átmenet bnélkül0 áll be, így az introszkript azonnal lefut. r────────────────────────────────────────────────────────────────────────────── 2Validáció 2───────── p func (g *Game) Validate() error A cRun0 hívja meg — miután minden cScene.Derived0 feloldódott, tehát egy származtatott kijárat ugyanúgy ellenőrződik, mint egy megírt —, de tesztből is használható. Azt ellenőrzi, hogy: • a cStartScene0 és a cStartStage0 be van állítva, és regisztrált, • egy cStage.Hold0-nak van cNext0-je, amire továbblép, és az a cNext0 regisztrált, • minden cSceneActor.Character0 és minden cExit.To0 regisztrált (a nil cTo megengedett — az azt jelenti, "vissza, amerről jöttél"), • a cPlayer0, ha be van állítva, regisztrált karakter, • van kiválasztott és regisztrált aktív téma. Amit ez korábban ellenőrzött, annak nagy része ma típus: egy jelenetre, tárgyra vagy dialogra pointerrel hivatkozunk, tehát egy soha nem regisztrált név fordítási hiba, nem ezek egyike. Az első hibát adja vissza, egy cErr…0 őrértékbe csomagolva az cerrors.Is hívásokhoz. A duplikált neveket korábban, a cRegister0 pánikja kapja el. r────────────────────────────────────────────────────────────────────────────── 2Mentés és betöltés 2────────────────── p // state.save.go p p func (g *Game) Save(slot int) error p func (g *Game) Load(slot int) error p func (g *Game) Slots(count int) []SlotInfo p p type SlotInfo struct { p Slot int p Exists bool p Scene string p SavedAt time.Time p } A cSlots0 az 1-től ccount0-ig minden slot fejlécét beolvassa — ezt listázza egy betöltő- vagy mentőképernyő. A hiányzó vagy olvashatatlan slot nem létezőként jön vissza — egy mentéseket listázó képernyő nem az a hely, ahol elemzési hibát kell jelenteni —, tehát a négy slotot mutató játék négy sort kap, amiből három azt mondja, „üres". Az írás ideje benne van a fájlban (csaved_at0, 2-es sémaverzió), így a betöltőképernyő meg tudja mondani, mikor. p // inkwell/state.store.go p p type Store interface { p Read(key string) ([]byte, error) // ErrNotStored for a key nothing has» p written p Write(key string, data []byte) error p } p p func NewFileStore(app string) *FileStore // native: os.UserConfigDir()//.json p func NewMemoryStore() *MemoryStore // a map, for a test p p // inkwell/state.settings.go p p type Settings struct { p Version int p Lang string p Theme string p WindowScale int p Voice bool // recorded lines on or off; on until switc» p hed p Extra map[string]any // the game's own, kept untouched p } p p func (g *Game) Settings() *Settings p func (g *Game) SaveSettings() error bHová kerül mindez.0 Egy cGame0 a cStore0-ján át tartja meg, aminek túl kell élnie a processzt: két dokumentumot, a csettings0-et és a cslots0-ot. A cGame.App0 a játék neve a gép felé, a helyet a platform választja. Natívan ez egy könyvtár a felhasználó konfigurációs könyvtárában — macOS-en c~/Library/Application cSupport/0, Linuxon c~/.config/0, Windowson c%AppData%\0 —, dokumentumonként egy JSON-fájl, amely ideiglenes fájlba íródik és onnan kerül a helyére átnevezéssel, így egy írás közepén történt összeomlás az előző dokumentumot hagyja meg. Böngészőben (cGOOS=js0) a clocalStorage0, dokumentumonként egy bejegyzés, az alkalmazásnévvel előtagolva. Az a játék, amelyik egyiket sem akarja, saját cStore0-t ad; egy teszt cMemoryStore0-t. bBeállítások.0 A cRun0 az első képkocka előtt beolvassa a csettings0 dokumentumot: a tárolt nyelvet akkor használja, ha a futás nem választott (az cinkwell.Lang üresen maradt), a tárolt témát, ha regisztrált, a tárolt ablakméretet, ha a játék nem nevezett meg egyet — így a c-lang0 kapcsoló erre a futásra nyer, visszaírás nélkül. A cUseLang0 és a cUseTheme0 meghívja a cSaveSettings0-et, tehát egy beállítóképernyőn tett választás abban a pillanatban megmarad. Az cExtra0 a játéké: a cSettings()0-en át olvasható és írható, egy cSaveSettings0, és a motor mezői mellett kerül körbe. bDokumentumok név szerint.0 A két kulcs cSettingsKey0 és cSlotsKey0 néven exportált, így egy játék megnevezheti azokat a dokumentumokat, amelyeket máshová küld, mint az alapértelmezett tároló — a csteam.Steam{Cloud: c[]string{inkwell.SlotsKey}}0 a mentéseket a Steam Cloudban tartja, a beállításokat a gépen —, anélkül, hogy ismerné a betűzésüket. A cGame.AppName() az cApp0, vagy c"inkwell"0, a könyvtár, amely alá az alapértelmezett tároló teszi őket, egy bővítmény által mellé épített cStore0 számára. p {"version": 1, "lang": "hu", "theme": "realworld-93", "window_scale": 2, "vo» p ice": true} bSlotok.0 Minden mentési hely az egyetlen cslots0 dokumentumban él, cslot0 kulcs alatt. Amit egy betöltőképernyő listáz — mikor, hol —, tisztán áll a munkamenet mellett, amely a mentés JSON-ja base64 stringként, így négy slot listázása egyetlen munkamenetet sem dekódol, és a dokumentum slotok táblájaként olvasható, nem négy játékként. A belső munkamenet saját sémaverziót visel (2), a dokumentumétól (1) függetlenül. p { p "version": 1, p "slots": { p "slot1": {"saved_at": "…", "scene": "alley", "title": "Underwater Sun Ri» p sing", "state": "eyJ2ZXJzaW9uIjoy…"} p } p } Elmentve: az aktuális és az előző jelenet, az aktív ige, az aktív téma, minden karakter pozíciója/célpontja/mozgásjelzője, a teljes cState0 (jelzők, változók, meglátogatott helyek, elmondott párbeszédek), valamint a tárgylista a kiválasztott hellyel együtt. A statikus regisztrációk bnincsenek0 elmentve — a cdomain.Build()0 minden indításkor újra létrehozza őket. A mentésben lévő hivatkozásokat az aktuális menedzserekkel ellenőrizzük, mielőtt bármihez hozzányúlnánk, így egy elavult mentés hibát ad ahelyett, hogy megrontaná az élő c*Game0 objektumot. Nincs elmentve: • a futó cRunner0 és az esetleges aktív párbeszéd — a cLoad0 mindkettőt megszakítja, tehát a mentés gyakorlatilag tétlen határon készül; • a póz és a képkocka-óra — a következő képkocka újraszármaztatja a pózt; az irány mentődik; • a járófelület-útvonalsorok — az éppen mozgó karakter egylépéses egyenes útvonalat kap a cpos0 pontból a ctarget0 pontba. A cVar0 értékek úgy mennek át az cencoding/json0 csomagon, ahogy vannak: a számok cfloat640 típusként jönnek vissza, a sztringek sztringként. A formátum verziózott, tehát egy újabb verzióból származó fájl hibát ad ahelyett, hogy némán elveszítene mezőket. r────────────────────────────────────────────────────────────────────────────── 2Hibák 2───── p // core.errors.go p p var ( p ErrUnknownAsset = errors.New("inkwell: unknown asset") p ErrUnknownScene = errors.New("inkwell: unknown scene") p ErrUnknownCharacter = errors.New("inkwell: unknown character") p ErrUnknownScript = errors.New("inkwell: unknown script") p ErrNoStartScene = errors.New("inkwell: StartScene not set or unknown» p scene") p ErrUnknownStage = errors.New("inkwell: unknown stage") p ErrNoStartStage = errors.New("inkwell: StartStage not set or unknown» p stage") p ErrNotStored = errors.New("inkwell: nothing stored under this key» p ") p ) A cRegister0 és a cMustGet0 ezek visszaadása helyett pánikol: összeállítás-idejű hibákat jeleznek, amelyeknek hangosan kell összeomlaniuk. A listában minden őrszem olyan, amit a cGame.Validate0 vissza is ad — egy hiba, amit a könyvtár sosem ad vissza, nem őrszem, csak még egy név, amit szinkronban kell tartani. r────────────────────────────────────────────────────────────────────────────── 2Tesztelés 2───────── A könyvtár egyetlen csomag, egyelőre fán belüli egységtesztek nélkül. Az integrációs felületet egy headless füstpróba fedi le — másold be bármelyik projektbe, amely a könyvtárat használja. Nem nyit ablakot, és ezredmásodpercek alatt lefut, tehát olcsó CI-kapu. A könyvtár saját CI-ja (c.woodpecker.yaml0) egyetlen ctest0 lépés: cgo build ./... és cgo vet ./...0. A cgit.teletypegames.org/build/ebitengine-ttg0 image-ben fut — ez az Ebitengine-builder, ami a cgo-s X11/GL/ALSA fejlécek miatt kell, előtöltve a TTG összes Go-projektjének moduljaival, így a lépés semmit nem tölt le —, és minden futáskor pullolja, mert a tag mozog. p func TestBuildValidates(t *testing.T) { p g := domain.Build() p if err := g.Validate(); err != nil { p t.Fatalf("validate: %v", err) p } p } Debug-kapcsolók: p inkwell.DebugLog = true // script queue, audio, scene change» p -> stderr p &inkwell.HotspotDebug{Enabled: true} // start with the F1 overlay on r────────────────────────────────────────────────────────────────────────────── 2Projektszerkezet 2──────────────── Minden forrás a repó gyökerében ül, ckategória.azonosító.go0 névvel, így az cls cwidget.*0 vagy az cls action.*0 egy kategóriát csoportosít — a kategória pedig az a szó, amit maga az entitás használ: a ccharacter.*0 a cCharacter0-t tartja, a cscript.*0 a cScript0-et, a cdialog.*0 a cDialog0-ot. cui.*0 csoport nincs: az „UI” nem olyan entitás, amit bárki regisztrál, egy widget, egy téma és egy ige pedig három kategória, aminek annyi a közös vonása, hogy ki van rajzolva. A csoport közös szerződését tartó kategóriafájl alatt minden fájl begy0 dolgot tart: egy akciót, egy kondíciót, egy előre gyártott témát, egy widgetet. Egy beépített akció hozzáadása egy fájl hozzáadása, ezért a hosszú sorozataik lentebb globként szerepelnek, nem egyesével felsorolva. p inkwell/ # module git.teletypegames.org/engines/inkwe» p ll p ├── core.doc.go # package docs p ├── core.manager.go # Manager[T Named], Named, TypeLabel p ├── core.game.go # Game aggregate, its managers and accessors p ├── core.transition.go # the one Transition in flight, scene or Sta» p ge p ├── core.validate.go # Game.Validate p ├── core.log.go # LogKind, LogMessage, the chat-log buffer p ├── core.engine.go # ebiten.Game adapter (Update/Draw/Layout) p ├── core.run.go # Run() p ├── core.errors.go # sentinel errors p │ p ├── util.geometry.go # Point, Rectangle, Polygon, Shape p ├── util.log.go # DebugLog + logf p │ p ├── asset.def.go # Asset: a picture p ├── audio.def.go # Audible: Voice, Music, Sound p ├── asset.manager.go # AssetManager alias + lazy loader p ├── asset.audio.go # AudioPlayer (WAV/OGG/MP3): music, voices, » p sounds p ├── asset.text.go # drawText / wrapText helpers p ├── asset.fs.go # AssetFS: an embed.FS or the OS filesystem p │ p ├── i18n.text.go # Translation, Text, Format, Lang p ├── i18n.strings.go # the engine's own wording, per language p ├── i18n.font.go # the glyph atlas and its lookup p ├── i18n.glyphs.go # generated: the atlas coverage p ├── font.png # generated: the atlas itself p ├── internal/fontgen/ # the generator (its own module) p │ p ├── scene.def.go # Scene, SceneActor p ├── scene.manager.go # SceneManager alias p ├── scene.hotspot.go # Hotspot, CursorKind p ├── scene.exit.go # Exit, ExitSide + edge-strip geometry p ├── scene.trigger.go # Trigger + rising-edge engine sweep p ├── scene.path.go # walkbox routing (BFS over polygon adjacenc» p y) p ├── scene.change.go # changeScene: the fade, and what happens mi» p d-fade p │ p ├── item.def.go # Item p ├── item.manager.go # ItemManager alias p ├── item.inventory.go # Inventory p │ p ├── character.def.go # Character p ├── character.manager.go # CharacterManager alias p ├── character.pose.go # tickPose: which Pose, which frame p ├── sprite.def.go # Sprite, Pose, Facing, drawSprite p ├── character.runtime.go # runtimeChar: placing, walking, ticking the» p cast p ├── speaker.def.go # Speaker: a voice with no body, and how a n» p ame resolves to one p ├── speaker.manager.go # SpeakerManager alias p ├── character.draw.go # the cast on screen, placeholder shapes inc» p luded p │ p ├── dialog.def.go # Dialog, DialogNode, DialogChoice p ├── dialog.manager.go # DialogManager alias p ├── dialog.runtime.go # the conversation in flight p │ p ├── action.def.go # Action, Runner, Ctx, Status: the contract p ├── action.queue.go # Game.Do and the one-at-a-time queue p ├── action.custom.go # Custom p ├── action.*.go # one built-in action per file: say, goto, w» p ait, walk, … p │ p ├── condition.def.go # Condition p ├── condition.*.go # one condition per file: flag, has_item, an» p d, or, … p │ p ├── script.def.go # Script p ├── script.manager.go # ScriptManager alias p │ p ├── state.def.go # State (flags, vars, visited, talked) p ├── state.store.go # Store, MemoryStore: where settings and sav» p es live p ├── state.store_native.go # FileStore under os.UserConfigDir (!js) p ├── state.store_js.go # LocalStore over window.localStorage (js) p ├── state.settings.go # Settings: lang, theme, window scale, the g» p ame's Extra p ├── state.save.go # Save/Load: the slots document, one base64 » p session per slot p │ p ├── input.def.go # Input (consume-on-use) p │ p ├── theme.def.go # Theme p ├── theme.manager.go # ThemeManager alias p ├── theme.presets.go # RegisterPresetThemes p ├── theme.*.go # one preset theme per file p │ p ├── verb.def.go # Verb p ├── verb.manager.go # VerbManager alias p ├── verb.defaults.go # the built-in SCUMM verb set p │ p ├── widget.def.go # Widget interface, Layer, Gated, Awake, UIC» p tx, Size, Align p ├── widget.manager.go # WidgetManager alias + layer-ordered iterat» p ors p ├── widget.preset_scumm.go # RegisterDefaultUI p ├── widget.preset_radial.go # RegisterRadialVerbUI p ├── widget.preset_rich.go # RegisterRichUI p ├── widget.verb_bar.go # VerbBar p ├── widget.verb_radial.go # RadialVerbs p ├── widget.inventory.go # InventoryBar p ├── widget.status.go # StatusLine p ├── widget.speech.go # SpeechBubble p ├── widget.dialog_box.go # DialogBox p ├── widget.end_card.go # EndCard p ├── widget.cursor.go # Cursor p ├── widget.hotspot_debug.go # HotspotDebug p ├── widget.menu.go # Menu + MenuItem: the menu system p ├── widget.debug_menu.go # DebugMenuRoot: the menu system's one built» p -in use p ├── widget.panel.go # Panel p ├── widget.top_bar.go # TopBar p ├── widget.character_panel.go # CharacterPanel + CharStat p ├── widget.chat_log.go # ChatLog + SpeakerStyle p ├── widget.letterbox.go # Letterbox p ├── widget.key_binding.go # KeyBinding p ├── widget.text_card.go # TextCard p ├── stage.def.go # Stage p ├── stage.manager.go # StageManager p │ p ├── achievement.def.go # Achievement → alias to achievement/ p ├── achievement.manager.go # AchievementManager alias p ├── achievement.runtime.go # tickAchievements: the rising edge, handed » p to the Unlockers p ├── extension.def.go # Extension, Unlocker; start, tick, stop p ├── achievement/ # Achievement and its Manager p ├── steam/ # the Steamworks Extension: init, callbacks,» p achievements, cloud saves p │ p ├── transition.def.go # Transition p ├── transition.fade.go # Fade, FadeTo p ├── widget.hotspot_panel.go # HotspotPanel p │ p ├── LICENSE.md # MIT p └── README.md MIT licenc alatt. Copyright © 2026 Teletype Games.