package inkwell

import "git.teletypegames.org/engines/inkwell"

Package inkwell is a point-and-click adventure game framework for Go, built on top of Ebitengine (https://ebitengine.org/). A game is composed by registering plain struct literals into per-entity Manager registries hanging off a single Game root. The library handles input, rendering, dialog trees, cutscenes, the HUD, themes and lazy asset loading; the game only declares content.

inkwell is not a generic engine. It is a thin Go-level layer on top of Ebitengine that implements the fixed skeleton of the point-and-click genre as it existed in the LucasArts SCUMM era (Maniac Mansion, Monkey Island, Day of the Tentacle):

The generic infrastructure — registries, text in several languages, world state, the action and condition framework and the widget contract — lives in the inkbase package and its sub-packages, which a game that is not a point-and-click adventure can import alone. inkwell builds on inkbase and re-exports what a game needs, so an adventure imports this one package.

The design collapses to one organising pattern: every named entity is registered with Manager.Register(&Entity{Name: "..."}). No builders, no fluent chains, no scattered registries. The same mental model applies to items, scenes, characters, dialogs, scripts, assets, verbs, widgets and themes alike.

A complete example game ("Morning Coffee") lives in the separate inkwell-demo repository, ssh://git@git.teletypegames.org:2222/demos/inkwell-demo, and consumes this library as a plain Go module.

Quick start

The module needs the Go version named in its go.mod and a working OpenGL context; Ebitengine handles the window and the render loop.

go mod init my-game
go get git.teletypegames.org/engines/inkwell

If the host cannot be reached over HTTPS, point Go at the SSH endpoint:

git config --global \
    url."ssh://git@git.teletypegames.org:2222/".insteadOf "https://git.teletypegames.org/"
export GOPRIVATE=git.teletypegames.org/*

The minimum game:

package main

import (
	"log"

	"git.teletypegames.org/engines/inkwell"
)

func main() {
	player := &inkwell.Character{Name: "player", W: 28, H: 62}
	room := &inkwell.Scene{
		Name:   "room",
		Layers: []inkwell.SceneLayer{{Asset: &inkwell.Asset{Name: "bg/room", Path: "assets/bg/room.png"}, Z: -1}},
		Actors: []inkwell.SceneActor{{Character: player, At: inkwell.Point{X: 160, Y: 140}}},
		Hotspots: []inkwell.Hotspot{{
			Name:  "door",
			Area:  inkwell.Rect(260, 50, 40, 90),
			Label: inkwell.Text{{Lang: "en", Text: "door"}},
			OnLook: inkwell.Say(player, inkwell.Text{
				{Lang: "en", Text: "A wooden door."},
			}),
		}},
	}
	world := &inkwell.Stage{Name: "game", World: true}

	g := inkwell.New(inkwell.Game{
		Title:      "Sample",
		Width:      320,
		Height:     200,
		StartStage: world,
		StartScene: room,
		Player:     player,
	})
	g.CharacterManager.Register(player)
	g.SceneManager.Register(room)
	g.StageManager.Register(world)

	inkwell.RegisterDefaultUI(g) // optional: Run installs it when no widget is registered
	if err := inkwell.Run(g); err != nil {
		log.Fatal(err)
	}
}

The Manager pattern

Every name-addressable entity goes through one generic registry, Manager, over anything that is Named (it has GetName). Each entity kind has its own alias: ItemManager, SceneManager, CharacterManager, SpeakerManager, DialogManager, ScriptManager, AssetManager, VerbManager, WidgetManager, ThemeManager, StageManager and AchievementManager.

A Manager offers:

Register returns what it registered so that a registration can be an expression rather than a statement, which lets an entity file be a declaration instead of a procedure:

// the whole of a scene file
package scene

var Alley = SceneManager.Register(&inkwell.Scene{
	Name: "alley",
	…
})

A package-level variable is initialised in dependency order, so the manager is built before any file that writes into it, whichever file the compiler reads first. Registration order is the order the files are presented in, as it would be with init functions.

The result is the handle. Every manager holds pointers — Manager[*Scene], not Manager[Scene] — so the variable a file binds and the entry the engine looks up are one instance, and every field that refers to another entity is a pointer to it: Stage.Background is an *Asset, Exit.To a *Scene, SceneActor.Character a *Character. A reference is therefore an identifier, checked by the compiler and followed by an editor, and Name is left with the one job only a string can do: it is the key a save file, a State var and a command-line flag use to find the entity again.

The result is also safe to ignore, so the plain statement form still compiles, and a file that has to build something before registering it (a loop, or a value wired to itself) keeps its init function.

Most registerable types also implement TypeLabel, which the manager uses in its panic messages ("duplicate item …"). A duplicate registration is a construction-time bug, code that ran once on startup; crashing loudly surfaces it in development, where quietly accepting the second one would mask a shadowed entity at runtime. When an entry genuinely has to be rewritten — a derived field filled in after the fact, a hot-reloaded entity — Set is the deliberate overwrite, and it keeps the entry's place in the insertion order.

Building a game

Game is the root aggregate: the title and resolution, one manager per entity kind, what the game boots as, the domain-wide defaults and the runtime services (State, Inventory, AudioPlayer, Input). New builds one from a declared spec. Everything a game is rather than does is a field, so the whole of it is one literal at the call site:

game := inkwell.New(inkwell.Game{
	Title:       "Real World",
	Width:       640,
	Height:      380,
	WindowScale: 2,

	StartStage:  stage.Game,
	StartScene:  scene.Alley,
	StartScript: script.Opening,
	StartTheme:  theme.RealWorld93,

	Player:      character.Paul,
	Walkboxes:   scene.Floor,
	UseWithFail: script.UseWithFail,

	SceneManager:  scene.SceneManager,
	ScriptManager: script.ScriptManager,
	WidgetManager: widget.WidgetManager,
	ThemeManager:  theme.ThemeManager,
})

What the spec leaves out, New fills in: an empty manager for every kind the caller did not hand over, the runtime halves (State, Inventory, Audio, Input), the SCUMM verb set (look, use, talk, take) when no verbs were registered, and the four preset themes. Widgets are not registered automatically: call RegisterDefaultUI (or one of its siblings), or let Run install the default set when the WidgetManager is empty.

A manager handed over is used in place, not copied, so the engine and the game share one registry per kind. New adds the preset themes to whichever ThemeManager it ends up with, leaving alone any name the game has already taken, and a VerbManager handed over with verbs in it does not get the SCUMM four installed over it.

NewGame is New for a game built in steps: it declares nothing but the title and the window, and the caller assigns the rest before Run, which is where the engine wires up the parts that hold a registry directly.

g := inkwell.NewGame("Real World", 640, 380)
g.SceneManager = mySceneManager
g.AssetManager = myAssetManager

The fields that say what the game boots as:

Game.StartAt, Game.OnStart and Game.OnFinale are setters for StartScene, StartScript and FinaleScript for a game built in steps; they return the *Game so they chain. The opening and the finale are registered Script values rather than bare actions, so the opening a game plays is content like any other.

The domain-wide defaults:

Running a game

Game.Theme is the active theme and Game.UseTheme switches it; the next frame uses the new colours, and the theme is remembered by name, which is what the settings store.

The UI's transient state lives on the Game, written by actions and the engine and read by widgets: the selected verb (Game.SelectedVerb), the hover label and the composed "verb + target" hint (Game.HoverHint), the line being spoken (Game.SetSpeech) and the two-second status banner (Game.FlashLine), which the game loop expires whether or not a StatusLine shows it.

The chat log is a buffer of LogMessage values, each a LogKind: player commands (Game.LogAction), in-world replies (Game.LogResponse) and system lines (Game.LogSystem). The engine and Say push into it, Game.Messages reads it, and the ChatLog widget draws it.

The scene helpers:

The screen helpers: Game.CurrentStage and Game.PreviousStage, Game.OnWorldStage, Game.Started (does a session exist?), and Game.GoToStage, Game.Restart and Game.LoadLater, which all land at the top of the next frame, and Game.Quit.

The action queue

Game.Do queues an action. Queued actions run one at a time and in order: the engine starts the next one on the first frame the previous one is done, so a widget, a trigger and a hotspot click can all hand work in without any of them being dropped or racing the others. The engine's own hotspot and OnEnter actions go through the same queue. The action running holds the frame — the script slot — so no ordinary widget ticks and the scene takes no clicks until it is done; parallel work goes inside one action with Par. See git.teletypegames.org/engines/inkwell/act and git.teletypegames.org/engines/inkwell/inkbase/action for the action model.

Extensions and Steam

An Extension is machinery that lives beside the game for the whole run and is not a widget: a platform layer, which has to be up before the window opens, pumped every frame whether a script holds the frame or not, and shut down when the loop returns. Game.Extensions lists them.

Start runs at the top of Run, before the settings are read, so an extension may hand the game a Store of its own and may set Game.SystemLang. An error from Start ends Run. Tick runs first thing in every frame, on every Stage. Stop runs in reverse order once the loop has returned, whichever way it returned. An extension that also implements Unlocker is handed every Achievement the world earns; it is asked for by type assertion, so an extension with no notion of achievements implements nothing extra.

The one extension the library ships is the Steamworks layer in git.teletypegames.org/engines/inkwell/steam, which this package does not import — a game that never names it links none of it. It is one literal in the Game:

Extensions: []inkwell.Extension{
	&steam.Steam{
		AppID:     480,
		Languages: map[string]string{"english": "en", "hungarian": "hu"},
		Cloud:     []string{inkwell.SlotsKey},
	},
},

See that package for what it does at each step and what it needs at runtime.

Text and languages

Every string the player reads is a Text: the same string in as many languages as somebody has written it in, one Translation per language. The wording stays where the thing it names is — a hotspot's label in the hotspot, a line of dialog in the scene that speaks it — and nothing is lifted into a catalogue keyed by identifier, so a translator reading a scene file can see what the line is answering. One translation per line, stacked, reads as a table:

inkwell.Say(paul, inkwell.Text{
	{Lang: "en", Text: "Locked. And I'm not guessing four digits."},
	{Lang: "hu", Text: "Zárva. És nem fogok négy számjegyet találgatni."},
})

Adding a language is adding a line. The zero value says nothing in any language, and a Text with one language behaves like a plain string. Inside a []Text or a map[string]Text the element type is implied, which is what gofmt -s leaves behind.

String resolves a Text against the package variable Lang, falls back to Fallback, and falls back again to whichever translation was written first, so a line nobody has translated yet still reaches the player. In resolves against a named language. Set both variables once, before Run:

inkwell.Fallback = "en"
inkwell.Lang = *langFlag

They are package variables rather than Game fields because a Text resolves itself wherever it is read — in a widget's Draw, in the Say runner, in a hover label — and threading a *Game through all of those would add a parameter for a value there is only ever one of. AssetFS is a package variable for the same reason. Resolution happens at display time, so content built during initialisation is unaffected by when the language is set. Run picks the language in this order: whatever was set before Run (a -lang flag), the stored setting, Game.SystemLang, Fallback. UseLang switches it at runtime and saves the setting.

A sentence built around another Text has to come out in one language throughout, which is what Format is for: the frame and every Text argument resolve in the same language, and anything else is printed as it stands. It is not a join — English and Hungarian put the preposition in different places, and a frame per language is the only thing that survives contact with a second one:

var exitLook = inkwell.Text{
	{Lang: "en", Text: "That way: %s."},
	{Lang: "hu", Text: "Arra van: %s."},
}

g.ExitLook = func(e inkwell.Exit) inkwell.Action {
	return inkwell.Say(paul, inkwell.Format(exitLook, e.Label))
}

Map is the other composer: it rewrites every translation and keeps the languages, which is what a derived label wants.

Two things are keyed by something other than wording, because wording is not a stable identity. A Once dialog choice is recorded against its position in its node, so a used-up choice stays used up in another language and a translator can reword it without breaking a save. And a TopBar note is a State var whose value is identical in every language, with TopBar.Notes mapping it to what the player reads.

What the engine says itself

The handful of sentences the engine puts on screen without the game having written them are package variables in English and Hungarian, so a game in another language can teach them one by appending a Translation:

For example:

inkwell.MsgNothing = append(inkwell.MsgNothing, inkwell.Translation{
	Lang: "de",
	Text: "Nichts von Bedeutung.",
})

The four preset verbs carry both languages too; a game that wants a third replaces them through VerbManager.Set.

The font

A game in a second language needs a font that can draw it. The engine ships its own bitmap atlas, baked from bitmapfont by the internal/fontgen generator, covering ASCII, the Latin-1 supplement, Latin Extended-A (ő and ű, and most of what Polish, Czech, Croatian and Turkish need), dashes, curly quotes, the bullet and the ellipsis, the euro sign and the four arrows — 348 glyphs in under 8 KB. A rune outside the coverage draws a hollow box, so a gap is visible rather than silent.

Every glyph sits in a fixed GlyphW × GlyphH (6×16) cell, a white glyph over a black drop shadow, so Game.DrawText tints the glyph with the colour it is given and leaves the shadow black. TextWidth, WrapText (word wrap; a newline breaks where it stands) and ClipText (cut to width with an ellipsis) are what the built-in widgets lay out with, and every vertical measurement in a HUD should derive from GlyphH. They take a resolved string, not a Text: a Text reaches them through String.

To widen the coverage, edit the generator's ranges and run go generate in the module root (the generator is its own module, so the engine's dependency list stays short); it refuses any rune the 6-pixel cell cannot hold.

A ladder of lines

A Ladder is what somebody says the n-th time the player does the same fruitless thing. Answering a repeated failure with one line is the surest way to sound like a machine; a Ladder answers the first attempt with the first rung and keeps a character's patience visibly running out:

var refuse = inkwell.Ladder{
	Rungs: []inkwell.Text{
		{{Lang: "en", Text: "No. That's not going to work."}},
		{{Lang: "en", Text: "Tried that. It didn't improve."}},
	},
	Last: inkwell.Text{{Lang: "en", Text: "Leave it. It is not that kind of puzzle."}},
}

inkwell.Say(paul, refuse.At(state.Talked(key)))

Ladder.At counts the first attempt as 1. Past the last rung it picks one at random, unless it names a Last, which stands from then on. The counter is the game's: State.NoteTalked and State.Talked already count how many times a thing has been worried at, keyed however the game wants. Game.UseWithFail is the usual caller.

Geometry

Point is a position in game pixels. Shape is anything with Contains and Bounds; Rectangle (made with Rect) and Polygon (made with Poly, hit-tested by ray casting) both are, so either can be a Hotspot.Area. PolyRect is a rectangle as a four-corner polygon: a walkbox is a Polygon so that a floor can be any shape, but most floors start as a box.

Assets and audio

An Asset is a picture: a logical Name ("bg/kitchen") and a Path. Assets are lazy — registering one only stores the spec, and the file is opened on first use and cached. A missing or unreadable file falls back to a deterministic placeholder colour derived from the name, so the game runs without any art, which is useful for prototyping and CI; characters without art are drawn as a stylised figure instead. PNG and JPEG are decoded. Files are read through AssetFS — an embed.FS, or nil for the operating system's filesystem. The font is not an asset; it is baked in.

Sound is not an Asset. Three things are heard, and they are three types because they are played three ways:

The AudioPlayer (Game.Audio) creates the shared Ebitengine audio context lazily at 48 kHz, or reuses one a host process already made. Music streams from disk; voices and sounds are decoded once and cached. WAV, Ogg Vorbis and MP3 are decoded, picked by file extension. Every failure — a missing file, an unsupported codec, no audio device — degrades to a debug-log line and silence.

Scenes, exits and hotspots

A Scene is a place in the fiction: a picture in planes (Layers), the things in it a verb can reach (Hotspots), the ways out of it (Exits), the floor (Walkboxes), Triggers, the people standing in it (Actors), a Music track, and OnEnter and OnLeave actions.

Layers are full-screen images, each with transparency where the planes behind show through, at a depth Z; the cast stands at 0. The background is the layer at -1, which everybody walks in front of; a foreground — a fence along the bottom edge, a counter — is the layer at 1, which everybody walks behind. A layer's Asset need not be registered, a missing lowest layer is the usual placeholder, a missing layer above it draws nothing, and an empty Layers leaves the theme's backdrop showing.

A nil Actors means the usual cast, Game.Player at that character's Start; an empty non-nil slice is an empty scene. Walkboxes work the same way: nil falls back to Game.Walkboxes, an empty slice turns routing off. With walkboxes, a walk is routed by a breadth-first search over the polygons that share an edge, through the midpoint of each shared edge, and a destination outside every walkbox is clipped to the nearest boundary; without them, characters walk in a straight line.

Exit.To and GoTo hold the *Scene itself, and two package-level variables may not refer to each other in their initialisers, which two scenes that lead to each other would. So a scene file declares the scene first and its exits second, with Scene.Leads, which appends to Exits and hands the scene back:

var Alley = SceneManager.Register(&inkwell.Scene{Name: "alley", …})

var _ = Alley.Leads(
	inkwell.Exit{To: Street, Side: inkwell.ExitLeft},
	inkwell.Exit{
		Label:   inkwell.Text{{Lang: "en", Text: "the fire escape"}},
		Side:    inkwell.ExitBack,
		Needs:   "has_ladder",
		Blocked: inkwell.Say(paul, inkwell.Text{{Lang: "en", Text: "Can't reach it."}}),
	},
)

Scene.Derived hands back exits worked out from the rest of the game rather than written into the scene — the pins on a map screen, every location that says it leads there. Run asks each scene once, before validation, and appends the result to Exits, so everything downstream sees one kind of exit.

The engine turns each Exit into a Hotspot with the exit cursor, the exit's Label, and a use that travels: GoTo the target, or Back when To is nil. When Needs names a flag, the journey is wrapped in If(Flag(Needs), travel, Blocked) — the exit is visible either way, since a locked door still says it is a door. With no Area an exit is a strip along one edge of Game.SceneArea, chosen by its ExitSide. Look and take come from Game.ExitLook and Game.ExitTake unless the exit says otherwise, so that phrasing lives in one place. The generated hotspot is named ExitName of the target ("exit:noodle_house", or "exit:back"), so the location graph can be read back out of the registered scenes.

A Hotspot is a named, clickable Shape with a hover Label, a CursorKind and one action per verb: OnLook, OnUse, OnTalk, OnTake, OnGive, and OnVerb for custom verbs. Hotspots belong to their scene and have no manager. A click goes through Game.InvokeVerb:

Every resolved click logs a player-command line for the chat log. A right click drops the held item, or resets the verb to look.

A Trigger arms when its scene is entered. Every idle frame the engine samples its When condition, and on a rising edge (false to true) queues its Do; Once makes it fire at most once per arming. Triggers are not sampled while a script runs, but the edge is kept across the busy window, and loading a save re-arms them as entering the scene would.

Items and the inventory

An Item is a thing the player can carry, registered in the ItemManager. Give puts it in the Inventory (which remembers it by name) and marks it known; TakeAway removes it. Item.OnUseWith is an item used on another item: with one in hand, clicking another in the InventoryBar runs whichever of the two pairs them, and OnUseSelf answers an item used on its own — the held item clicked again. UseWithFail is the same shape as ExitLook and ExitTake: the pair the author never wrote is still content, and only the game knows what its characters say about it. The Inventory is pure runtime state owned by the Game, changed by actions and by the InventoryBar.

Characters and speakers

A Character is someone who stands in scenes: a Label for the UI, a Lore paragraph for a dossier screen, Poses, a walking Speed (60 pixels a second by default), a SpeechColor, a Delivery, a Start point and a size used by the placeholder figure (humanoid when narrower than tall, quadruped otherwise).

Delivery is where a character reaches the player, and Say obeys it: a DeliveryBubble character speaks in a bubble and the log, a DeliveryLog one in the log alone. A voice with no body — a radio, a tape, a narrator — is a Speaker, registered in the SpeakerManager: a name, a Label, a Delivery and a Color, and nothing about walking or being drawn. Both are a Talker, so Say, Reply and a DialogLine take either; Game.Speaker looks a name up in the characters first and the speakers second, and a name found in neither speaks in a bubble under its own name.

A Sprite is a picture or a sequence of them: a sheet and the frames cut from it, in order, at FrameTime seconds each, looping or holding the last. No frames means the whole sheet is the one frame, so a still item and a walking character are the same type. Character.Poses gives one Sprite per Pose, and the engine picks the pose: PoseWalk while moving, PoseTalk while the character's line is up, PoseWait after standing still for a while, PoseIdle otherwise — or whatever SetPose holds until the character next walks or speaks. A pose the art does not cover falls back to PoseIdle, and no PoseIdle draws the placeholder. Direction is not a second sheet: a Sprite says which way it Faces (Facing), the character remembers the way it last moved, and the frame is mirrored when the two differ; the facing survives a save.

Dialogs and scripts

A Dialog is a conversation: named DialogNode values, each a few DialogLine values spoken one at a time followed by the DialogChoice values the player picks from. RunDialog opens one and holds the script until it closes; the DialogBox widget shows the lines and the choices; a choice runs its Actions in sequence, and EndDialog and GotoNode close the conversation or move it. See git.teletypegames.org/engines/inkwell/dialog for the details.

A Script is a named composite action — an intro, a victory, a transition reused from several places. RunScript runs one.

Verbs

A Verb is a canonical name ("look"), a Label in every language, and a Default action run when a hotspot has no handler for it. New registers the four SCUMM verbs (look, use, talk, take) unless the game brought its own, and a game adds more:

g.VerbManager.Register(&inkwell.Verb{
	Name:    "push",
	Label:   inkwell.Text{{Lang: "en", Text: "Push"}, {Lang: "hu", Text: "Lökd"}},
	Default: inkwell.Say(player, inkwell.Text{{Lang: "en", Text: "It won't move."}}),
})

A hotspot answers a custom verb through OnVerb. The verb bar and the verb wheel read the VerbManager every frame, so a verb added at runtime appears at once.

Stages

A Stage is one full-screen thing the game can be showing, and exactly one is showing at a time. A main menu is a Stage, a settings screen is a Stage, a credits card is a Stage — and so is the game itself (World: true), which is the point: the world is not the thing every other screen floats over, it is one of the screens. A Scene is a place in the fiction and the engine always has one; a Stage is what owns the frame. Off the world Stage the scene is not drawn, characters do not walk, triggers do not fire and a click reaches no hotspot.

Widgets follow without a new field, because OnStage is a Condition like any other:

game := StageManager.Register(&inkwell.Stage{Name: "game", World: true})
mainMenu := StageManager.Register(&inkwell.Stage{Name: "main_menu"})

WidgetManager.Register(&inkwell.Menu{
	Name:   "main_menu",
	When:   inkwell.OnStage(mainMenu),
	Sticky: true,
	Root:   inkwell.MenuItem{ … },
})

GoToStage, Restart and LoadSlot all land at the top of the next frame rather than where they are called, so a menu row can say what it means in one sequence:

Do: inkwell.Seq(inkwell.Restart(), inkwell.GoToStage(game)),

A Stage may name how it gives way: a change out of it goes through its Transition, and nil is a cut. The whole pending change lands on the fully covered frame, a Restart or LoadSlot asked for at the same time included.

Hold shows a Stage for a number of seconds and then goes to Next, through the Stage's Transition, which is all a splash card or a title sequence is. The clock starts once OnEnter is done. Skippable lets Space, Enter or a click move on at once; the key or click is claimed, so nothing underneath reads it.

StageManager.Register(&inkwell.Stage{
	Name:       "splash_studio",
	Transition: inkwell.Fade(0.5),
	Hold:       2,
	Skippable:  true,
	Next:       splashTitle,
})

A game whose StartStage is the world starts a session at boot: the start scene is entered and StartScript is queued. A game that opens on a menu starts one when the player says so, which is all Restart is. Game.Started (and the Started condition) report whether a session exists, which is what a "Resume" or "Save" row is enabled by.

Achievements

An Achievement is something the player is recognised for, declared as content: a Name a platform knows it by (the Steam API name), a Title and a Text in every language the game speaks, and the Condition that earns it. It is registered in Game.AchievementManager. The engine evaluates every achievement on each frame the world is showing, a running script included, and hands the ones that have just come true to every Unlocker among the extensions. It keeps no record of its own beyond a per-process set; see git.teletypegames.org/engines/inkwell/achievement.

Actions

An Action is an immutable spec of work, safe to keep in a Hotspot field and run on every click; its Start makes a Runner, one in-flight execution that is ticked each frame with a Ctx and answers a Status: StatusRunning, StatusDone (a sequence moves on) or StatusFailed (a sequence stops here). The built-in constructors:

The Ctx a runner gets carries the frame's DT, the State, and the Game as an any; an adventure action type-asserts it to *Game:

bumpScore := inkwell.Custom(func(ctx *inkwell.Ctx) inkwell.Status {
	score, _ := ctx.State.Var("score").(int)
	ctx.State.SetVar("score", score+10)
	return inkwell.StatusDone
})

git.teletypegames.org/engines/inkwell/act documents the adventure actions and the host interfaces they reach the Game through; git.teletypegames.org/engines/inkwell/inkbase/action documents the generic ones and how to write an action of your own.

Conditions

A Condition is a stateless predicate evaluated lazily against a Ctx — in If, in DialogChoice.Show, in a widget's When, in a Trigger and in an Achievement. Not, And and Or combine; Flag, VarEq and Knows read the State; HasItem and SelectedItem the inventory; InScene, OnStage, OnWorld and Started where the game is. Given is a condition written as a literal — every field set on it has to hold — for the place a condition is read most, a dialog choice:

Show: inkwell.Given{
	Item:    mysteryTape,
	NotFlag: "dex_told",
},

See git.teletypegames.org/engines/inkwell/cond and git.teletypegames.org/engines/inkwell/inkbase/condition.

World state

State is the run's mutable record: flags (the workhorse of puzzle state, "cupboard_open"), vars of any type (scores, the clock, a note for the top bar), how many times each scene was visited, how many times each dialog choice or other counter was bumped (Talked and NoteTalked), and which Knowable entities the player has met. Known is kept for a dossier or codex screen: a character or speaker is known once it has said a line, an item once it has been in the inventory, and a script says Learn for the rest. Its value is the order of discovery, so a screen sorted by it reads as the player's own history; Knows is the same fact as a condition. An item and a character may share a name, because the kind is part of the key.

Widgets

The HUD is a set of Widget values registered in the WidgetManager. A widget handles its own input, hit testing, state and drawing; the library imposes no layout. Each declares a Layer (Layered) — the draw order runs from LayerScene up to LayerCursor, ticks run top layer down so the widget drawn on top gets the click first, and registration order decides inside one layer. A widget claims a click or a key through Input, and widgets below see it as consumed. Every built-in widget has a When condition (Gated): a widget whose condition is false neither ticks nor draws nor blocks a click, so a HUD hides for a cutscene by saying so on its widgets. A running script has the frame to itself except for the widgets that implement Awake — Menu and DialogBox — which tick before the script's runner. Whatever click survives the widgets reaches the scene, unless a visible widget claims the point as its own (see below).

Besides the inkbase layers, this package adds LayerDebug, between the dialog and the menus, for the HotspotDebug overlay. A wrapper widget that forwards to an inner one should return LayerOf the inner one from its own Layer, so that wrapping does not move it.

A widget that occupies an area of the screen implements ClickBlocker: BlocksClickAt(ctx *UICtx, p Point) bool, reporting whether p is its own. A left click there that the widget's Tick did not take never reaches the scene (no hotspot, no walk), and the RadialVerbs coin does not open over it. The built-ins that do are the verb bar, the inventory, the dialog box while a dialog is open, the top bar, the character panel while its character is in the scene, the chat log, a blocking panel, the hotspot panel, an open menu and the radial menu itself while it is visible. ClickBlocker is the inkbase widget contract of the same name, so a widget from the inkbase widget packages blocks the same way.

git.teletypegames.org/engines/inkwell/inkbase/widget documents the contract in full. A custom widget reaches the Game by type assertion:

type Minimap struct {
	Name   string
	Bounds inkwell.Rectangle
}

func (m *Minimap) GetName() string        { return m.Name }
func (m *Minimap) Tick(ctx *inkwell.UICtx) {}
func (m *Minimap) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) {
	g := ctx.Game.(*inkwell.Game)
	… // render a scene thumbnail, mark the cast
}

Built-in widgets

Fields left at their zero value fall back to a sensible default. The generic ones — Menu, Panel, TextCard, KeyBinding, Letterbox, EndCard, StatusLine, TopBar and ChatLog — are the inkbase widgets, re-exported: the Game is their host, and draws them with the active theme (MenuColors, ChatLogColors and the like are the theme's tokens handed to them). The rest need the adventure and live here.

RegisterDefaultUI installs the SCUMM layout (verb bar and inventory), RegisterRadialVerbUI a verb coin with a wider inventory, and RegisterRichUI a top bar, character panels (nil skips one), a chat log and a verb wheel.

Menus, the debug menu and the explorer

Menu is a nested list in a window: a page of rows, any of which can open a page of its own, as deep as the tree goes. A row and a page are the same type, MenuItem: Items is a page as authored, Expand a page read out of the game while it is open, Body a paragraph to read, and a row with none of them is a leaf whose Do is queued on Enter. FixedValue makes a Value column that does not depend on the game. The menu claims every key it uses; Sticky makes it the screen rather than a window over one, which is what a Stage is usually made of. Colours come from the active theme's dialog tokens. git.teletypegames.org/engines/inkwell/inkbase/menu describes the model, the keys and the mouse in full; this Menu is the same widget, wired to the Game.

DebugMenuRoot is the one use of the menu system the library ships: what the game is made of, and what is clickable in the current scene. It is data, not a widget, and nothing registers it — a game asks for it and gates it on its own debug switch:

g.WidgetManager.Register(&inkwell.Menu{
	Name:      "debug_menu",
	Root:      inkwell.DebugMenuRoot,
	When:      whenDebug,
	ToggleKey: ebiten.KeyD,
})

Its pages:

DEBUG
├── Scenes         every registered scene · Enter teleports
├── Items          give · take away · select · use on self · use with → target
├── Action points  the current scene's hotspots, exits included, in hit-test order
│   └── <hotspot>  the verbs it answers · use with → item
├── Exits          the current scene's exits · go · set or clear the flag it needs
└── EXPLORER       the Game itself, field by field

The rows name registry keys, not the prose a player reads, since a debug menu is read by whoever writes the content. Every verb goes through Game.InvokeVerb, the same path a click takes, so the chat log reads as if the player had done it; a use-with row hands the item over first. A game that wants sections of its own builds a root around it:

Root: inkwell.MenuItem{
	Label: inkwell.DebugMenuRoot.Label,
	Items: append(inkwell.DebugMenuRoot.Items, mySection),
},

ExplorerRoot, the last page of the debug menu, is the Game itself, field by field, worked out by reflection when a page opens: a manager opens onto its entities, an entity onto its literal, an action onto the actions it is made of, a Text onto its translations. The value column is what a row is worth now — a name, the current line of a Text, the truth of a Condition, the kind of an Action, a colour, a count. A pointer to a registered entity is a link to it. Unexported fields are read too, so the explorer can see inside a Say; it stops at functions, where the writing turns into code. Every action row opens with "run", so any authored sequence can be played from where it is written. Explore runs a game with nothing on screen but this, validated and wired exactly as Run wires it:

if explore {
	return inkwell.Explore(game)
}
return inkwell.Run(game)

Themes

A Theme is a named set of colour tokens every built-in widget draws with — panel, status, verbs, inventory, speech, dialog, overlays, top bar, chat log and character panel. New registers the four presets (RegisterPresetThemes: classic-scumm, sierra-coin, paper-notebook and terminal-green) and selects StartTheme, or classic-scumm. A custom theme is registered like anything else and selected with Game.UseTheme, or with the UseTheme action, which also saves the setting; widgets read the active theme every frame, so the change shows on the next one. See git.teletypegames.org/engines/inkwell/theme.

Input

Input (Game.Input) is polled once per frame: the pointer position, the two mouse buttons' presses, the wheel's travel and every key that went down. Clicks and keys are consume-on-use — whoever consumes first wins, and later checks see nothing — which is how one click is authoritative across the widget stack and the scene. KeyPressed is edge-triggered, and ClaimKey is KeyPressed plus ConsumeKey. git.teletypegames.org/engines/inkwell/inkbase/input documents the same model for inkbase games.

The game loop

Run (or Game.Run) resolves every Scene.Derived, starts the extensions, reads the settings and settles the language, validates the game, installs the default UI when no widget is registered, shows the StartStage — starting a session at once when it is the world — sizes the window to the resolution times WindowScale, and enters the Ebitengine loop.

Each frame's update, in order:

tick the extensions
apply the pending screen change          GoToStage, Restart, LoadSlot
if quitting: stop
count the flash down, poll input, advance the transition,
start the next queued action
if a transition is covering the picture: stop here
on the world Stage: evaluate the achievements
if an action is running:
    tick the Awake widgets, top layer down
    tick the action
    on the world Stage: move the characters
    stop here
on the world Stage: sample the scene's triggers
tick every visible widget, top layer down
on the world Stage: resolve the click on the scene, move the characters

Each frame's draw: the theme's backdrop, the scene layers at or below 0 by depth, the characters sorted by their feet, the layers above 0, every visible widget by layer and registration order, and the transition's cover on top.

A moving character steps along its waypoints at Speed pixels per second; Walk holds its script until the character arrives. A click on empty floor with WalkOnClick does not queue a Walk: a queued walk would be a script, and a script owns the frame, so the next click could not redirect it.

Scene transitions

A Transition is how one picture gives way to another, in two halves: the old picture is covered, the change lands on the frame it is fully hidden, and the new picture is uncovered. Fade and FadeTo are the ones the library ships; a Stage names its own, and a scene change uses a quarter second fade. At the midpoint of a scene change the old scene's OnLeave is queued, the current scene changes, the visit is counted, the actors are placed, the new scene's music plays and its OnEnter is queued; input is frozen while the old picture is going. The opening scene is entered without a transition. See git.teletypegames.org/engines/inkwell/inkbase/transition.

Validation

Game.Validate cross-checks what the types cannot. Run calls it after every Scene.Derived has been resolved, and a test can call it on its own. It checks that StartScene and StartStage are set and registered; that a Stage that holds has a registered Next; that every scene actor's character, and every exit's target, is registered (a nil target means "back the way you came"); that Player, when set, is registered; that every achievement has a When; and that the active theme is registered. Most of what such a validator would check is a type here: a scene, an item or a dialog is referred to by pointer, so an unknown one is a compile error. Duplicate names are caught earlier still, by Register's panic.

Save and load

A Game keeps what has to outlive the process through its Store: two documents, named by SettingsKey and SlotsKey. Game.App names the game to the machine (Game.AppName is App or "inkwell"), and the platform picks the place: natively a FileStore, one JSON file per document under the user's configuration directory (~/Library/Application Support/<app> on macOS, ~/.config/<app> on Linux, %AppData%\<app> on Windows), written to a temporary file and renamed so a crash leaves the previous document; in a browser, one localStorage entry per document. A game that wants neither hands in its own Store; a test hands in a MemoryStore. Read returns ErrNotStored for a key nothing has written.

Settings are read before the first frame: the stored language is used when the run has not chosen one, the stored theme when it is registered, the stored window scale when the game named none — so a -lang flag wins for one run without being written back. UseLang, UseTheme and SetVoice save the settings at once. Extra is the game's own: read and write it through Game.Settings, call Game.SaveSettings, and it round-trips beside the engine's fields.

Game.Save and Game.Load write and read one slot of the slots document; SaveSlot and LoadSlot are the actions. Game.Slots reads the header of slots 1 to n as SlotInfo values for a load or save screen; a missing or unreadable slot comes back as one that does not exist. Each slot keeps when and where it was saved in the clear, beside the session itself as a base64 JSON string, so listing slots decodes no session. A session holds the current and previous scene, the selected verb and the active theme, every character's position, target, movement and facing, the whole State, and the inventory with its selection. It does not hold the registrations — the game rebuilds those on every launch — nor the running action or open dialog (Load cancels both), the poses (re-derived on the next frame) or walk paths (a character still moving finishes in a straight line). A save is checked against the current registries before anything is touched, so a corrupt or outdated save returns an error without changing the running game, and a save from a newer format version is refused. Vars are stored as JSON, so numbers come back as float64.

Errors

The sentinel errors — ErrNoStartScene, ErrNoStartStage, ErrUnknownScene, ErrUnknownStage, ErrUnknownCharacter, ErrUnknownAsset, ErrUnknownScript and ErrNotStored — are wrapped with context and checked with errors.Is. Manager.Register and MustGet panic instead of returning an error, because they signal construction-time bugs that should crash loudly in development.

Testing and debugging

A game's cheapest CI gate is a test that builds the game and validates it; it opens no window and finishes in milliseconds:

func TestBuildValidates(t *testing.T) {
	g := domain.Build()
	if err := g.Validate(); err != nil {
		t.Fatalf("validate: %v", err)
	}
}

Runtime debug switches:

inkwell.DebugLog = true                                            // engine log to stderr
&inkwell.HotspotDebug{Enabled: true}                               // start with the F1 overlay on
&inkwell.Menu{Root: inkwell.DebugMenuRoot, ToggleKey: ebiten.KeyD} // the debug menu

Project layout

The adventure layer is this package plus a few sub-packages it re-exports, so a game imports this package alone:

The generic foundation is git.teletypegames.org/engines/inkwell/inkbase and its sub-packages: font, input, asset and transition (Ebitengine services), stage (the Stage), action and condition (the generic actions and conditions), widget (the widget contract) and the generic widgets — menu, panel, textcard, keybinding, letterbox, endcard, statusline, topbar and chatlog. Those widgets are the ones this package re-exports (Menu, ChatLog, TopBar, …): *Game implements each widget package's Host, drawing them with the active theme, so the same widget serves an adventure and a game built on inkbase alone.

The library is MIT licensed; see LICENSE.md.

Index

Constants

const (
	StatusRunning = inkbase.StatusRunning // not finished; tick it again next frame
	StatusDone    = inkbase.StatusDone    // finished
	StatusFailed  = inkbase.StatusFailed  // gave up; a sequence stops here
)

The outcomes of a tick.

const (
	GlyphW = 6
	GlyphH = 16
)

The built-in debug font is a fixed 6×16 cell. Every layout measurement in the library — and in a domain's own widgets — derives from these.

const (
	DeliveryBubble = inkbase.DeliveryBubble // a speech bubble over the scene, and the log
	DeliveryLog    = inkbase.DeliveryLog    // the message log alone
)

The ways a line can be delivered.

const (
	LogAction   = inkbase.LogAction   // a player command, "> look at door"
	LogResponse = inkbase.LogResponse // an in-world reply, a line someone said
	LogSystem   = inkbase.LogSystem   // a message from the game itself
)

The kinds of message-log entry; see inkbase.LogAction.

const (
	FacingRight = iwsprite.FacingRight // looking right
	FacingLeft  = iwsprite.FacingLeft  // looking left

	PoseIdle = iwsprite.PoseIdle // standing; the fallback for a pose with no sprite
	PoseWalk = iwsprite.PoseWalk // moving
	PoseTalk = iwsprite.PoseTalk // while its line is up
	PoseWait = iwsprite.PoseWait // after standing still a while
)

The facings and the poses a character is drawn in.

const (
	// SpeakerInline puts "DEX: " in front of every reply.
	SpeakerInline = chatlog.SpeakerInline
	// SpeakerHeader names only the latest speaker, once, above a rule.
	SpeakerHeader = chatlog.SpeakerHeader
	// SpeakerNone names nobody.
	SpeakerNone = chatlog.SpeakerNone
)
const (
	// MouseButtonLeft is the primary button.
	MouseButtonLeft = inkbase.MouseButtonLeft
	// MouseButtonRight is the secondary button.
	MouseButtonRight = inkbase.MouseButtonRight
)
const (
	// AlignLeft puts the line against the left edge.
	AlignLeft = inkbase.AlignLeft
	// AlignCenter centres the line.
	AlignCenter = inkbase.AlignCenter
	// AlignRight puts the line against the right edge.
	AlignRight = inkbase.AlignRight
)
const SettingsKey = "settings"

SettingsKey is the Store key the settings document lives under. It is exported beside SlotsKey so a game can name the documents it sends somewhere else — a platform's cloud, say — without knowing their spelling.

const SlotsKey = "slots"

SlotsKey is the Store key the slots document lives under — see SettingsKey.

Variables

var (
	// ErrUnknownAsset is a name no registered Asset has.
	ErrUnknownAsset = errors.New("inkwell: unknown asset")
	// ErrUnknownScene is a name no registered Scene has.
	ErrUnknownScene = errors.New("inkwell: unknown scene")
	// ErrUnknownCharacter is a name no registered Character has.
	ErrUnknownCharacter = errors.New("inkwell: unknown character")
	// ErrUnknownScript is a name no registered Script has.
	ErrUnknownScript = errors.New("inkwell: unknown script")
	// ErrUnknownStage is a name no registered Stage has.
	ErrUnknownStage = errors.New("inkwell: unknown stage")
	// ErrNoStartScene is a game whose StartScene is nil or not registered.
	ErrNoStartScene = errors.New("inkwell: StartScene not set or unknown scene")
	// ErrNoStartStage is a game whose StartStage is nil or not registered.
	ErrNoStartStage = errors.New("inkwell: StartStage not set or unknown stage")
	// ErrNotStored is what a Store's Read returns for a key nothing has
	// written yet.
	ErrNotStored = errors.New("inkwell: nothing stored under this key")
)

The errors the engine returns; compare with errors.Is, since most are wrapped with the name that was not found.

var (
	// MsgNothing is the answer to a verb nothing is bound to.
	MsgNothing = Text{
		{Lang: "en", Text: "Nothing worth mentioning."},
		{Lang: "hu", Text: "Semmi említésre méltó."},
	}

	// MsgNotThat is the fallback when an item is used on something nobody
	// paired it with and Game.UseWithFail is unset.
	MsgNotThat = Text{
		{Lang: "en", Text: "Not that."},
		{Lang: "hu", Text: "Nem ehhez."},
	}

	// MsgNeedItem is what RequireItem says. %s is the item. It starts as
	// [act.MsgNeedItem], and the engine hands it back to act every frame.
	MsgNeedItem = act.MsgNeedItem

	// HintVerb is the status line over a plain target: verb, then target.
	HintVerb = Text{
		{Lang: "en", Text: "%s %s"},
		{Lang: "hu", Text: "%s %s"},
	}

	// HintVerbOn is the status line while an item is held: verb, item,
	// target. The preposition is the reason this is a sentence and not a
	// join — English puts it between the two, Hungarian after them.
	HintVerbOn = Text{
		{Lang: "en", Text: "%s %s on %s"},
		{Lang: "hu", Text: "%s %s ezen: %s"},
	}

	// ScoreLabel prefixes the TopBar score. It starts as
	// [topbar.ScoreLabel], and the engine hands it back to topbar every
	// frame.
	ScoreLabel = topbar.ScoreLabel

	// MenuEmpty is what a Menu page with no rows says; it starts as
	// [menu.MenuEmpty], and the engine hands it back to menu every frame.
	MenuEmpty = menu.MenuEmpty

	// MenuKeys is the key legend along the foot of a Menu window; it starts
	// as [menu.MenuKeys], and the engine hands it back to menu every frame.
	MenuKeys = menu.MenuKeys

	// MenuKeysSticky is the same legend for a Menu that is the screen: it
	// cannot be closed, so it does not offer to be. It starts as
	// [menu.MenuKeysSticky], and the engine hands it back to menu every
	// frame.
	MenuKeysSticky = menu.MenuKeysSticky
)

The engine's own voice: the handful of sentences inkwell puts on screen without a game having authored them. A game's own wording lives in its own content files — these are the ones that belong to the machinery, and they are gathered here rather than left at their call sites because a translator adding a language wants one file for the chrome, not a grep.

The ones a sub-package prints itself — RequireItem's line, the TopBar's score label and the Menu's chrome — are written once, in that package; the variables here start as those and the engine copies them back every frame, so a game changes them here, like the rest.

They are variables, not constants, so a game that speaks a language the engine has not been taught can teach it one:

inkwell.MsgNothing = append(inkwell.MsgNothing, inkwell.Translation{
	Lang: "de",
	Text: "Nichts von Bedeutung.",
})
var AssetFS fs.FS

AssetFS is, when non-nil, the filesystem asset files (images, audio) are read from instead of the OS filesystem. Set it to an embed.FS before Run to ship assets inside the binary — required for js/wasm builds, where there is no OS filesystem and every asset would fall back to a placeholder:

//go:embed assets
var assetsFS embed.FS

func init() { inkwell.AssetFS = assetsFS }

Asset.Path values are used as-is, so they must be slash-separated and relative (e.g. "assets/bg/bedroom.png") — the same form that works with os.Open from the project root.

var DebugLog = false

DebugLog turns on the engine's debug trace: queued actions, audio that could not play, failed saves, written to standard error with an "inkwell" prefix. Off by default.

var DebugMenuRoot = MenuItem{
	Label: debugTitle,
	Items: []MenuItem{
		{
			Label:  debugScenes,
			Value:  debugTally(func(ctx *Ctx) int { return gameOf(ctx).SceneManager.Len() }),
			Expand: debugSceneRows,
		},
		{
			Label:  debugItems,
			Value:  debugTally(func(ctx *Ctx) int { return gameOf(ctx).ItemManager.Len() }),
			Expand: debugItemRows,
		},
		{
			Label:  debugActionPoints,
			Value:  debugTally(debugActionPointCount),
			Expand: debugActionPointRows,
		},
		{
			Label:  debugExits,
			Value:  debugTally(debugExitCount),
			Expand: debugExitRows,
		},
		{
			Label:  explorerTitle,
			Expand: exploreGame,
		},
	},
}

DebugMenuRoot is the page tree of the built-in debug menu: what the game is made of, and what is clickable in the scene the player is standing in. It is data rather than a widget — the menu system is the widget, and this is one root a game can hand it.

var ExplorerRoot = MenuItem{
	Label:  explorerTitle,
	Expand: exploreGame,
}

ExplorerRoot is the page tree of the built-in entity explorer: the Game itself, field by field. A manager opens onto its entities, an entity onto its literal, an action onto the actions it is made of, a Text onto its translations — as deep as the game was written. It is data rather than a widget, like DebugMenuRoot, which carries it as its last page; Explore runs a game with nothing but this on screen.

Every row is worked out by reflection when its page opens and rebuilt while the page is up, so a condition's row says what it evaluates to right now and a manager's count is the count as it stands. Unexported fields are read too — an action's speaker and line live in unexported fields of an unexported type, and a tool that could not see inside a Say would stop exactly where the writing starts.

var Fallback string

Fallback is the language used when a Text says nothing in Lang. Empty falls through to the first translation the Text lists, which is the language it was written in — so a line nobody has translated yet still reaches the player rather than vanishing.

var Format = inkbase.Format

Format composes a Text around other Texts, each part resolving in the language of the frame it is read in; see inkbase.Format.

var Lang string

Lang is the language the game speaks — whatever tag its content files spell, "en" and "hu" in the games this engine was written for. Empty means every Text falls through to Fallback, and then to the language it was authored in.

It is a package variable rather than a field on Game because a Text resolves itself wherever it is read: inside a widget's Draw, inside the Say runner, inside a hover label, inside a hotspot's generated exit. Threading a *Game through all of those would put a parameter on every one of them for a value there is only ever one of. AssetFS is a package variable for the same reason.

The engine syncs Lang and Fallback to inkbase at the top of every frame so sub-packages that call Text.String() resolve the same language.

var Poly = inkbase.Poly

Poly returns the Polygon through the given points; see inkbase.Poly.

var PolyRect = inkbase.PolyRect

PolyRect returns the rectangle x, y, w, h as a four-cornered Polygon, for a walkbox that is a plain box; see inkbase.PolyRect.

var Rect = inkbase.Rect

Rect returns the Rectangle at x, y of w by h; see inkbase.Rect.

Functions

func ClipText

func ClipText(s string, maxPx int) string

ClipText cuts s to maxPx pixels, ending in an ellipsis when it had to cut. A width with no room at all returns the empty string.

func ExitName

func ExitName(to *Scene) string

ExitName is the hotspot name an Exit gets: "exit:<scene>", or "exit:back".

func Explore

func Explore(g *Game) error

Explore runs a game with nothing on screen but the explorer: a Stage that is a Sticky Menu over the whole window, rooted at ExplorerRoot. The game is validated and wired exactly as Run wires it, so what the explorer shows is the game as it would have run.

func FixedValue

func FixedValue(t Text) func(*Ctx) Text

FixedValue is a row's value column when it does not depend on the game: a constant Text in the shape MenuItem.Value asks for; see menu.FixedValue.

func RegisterDefaultUI

func RegisterDefaultUI(g *Game)

RegisterDefaultUI installs the SCUMM-style HUD widgets into g.WidgetManager in the conventional Z-order (HotspotDebug at the back, Cursor on top). Domains that want a different layout call this and then either tweak the registered widgets in place or replace them entirely.

Layout assumes the default 320×200 internal resolution.

func RegisterPresetThemes

func RegisterPresetThemes(g *Game)

RegisterPresetThemes installs the four built-in themes, each of which is a file of its own. New calls it, so a game that never mentions a theme still has one — and it adds rather than replaces: a domain that hands over its own ThemeManager keeps everything already in it, and a preset whose name the domain has taken is left alone.

func RegisterRadialVerbUI

func RegisterRadialVerbUI(g *Game)

RegisterRadialVerbUI installs an alternative HUD that swaps the permanent verb-bar for a verb-coin (right-click radial menu). Inventory, dialog, speech and cursor stay the same.

func RegisterRichUI

func RegisterRichUI(g *Game, player, npc *Character)

RegisterRichUI installs a "story-rich" HUD that matches the layout of the screenshot used as the reference: top bar with title/score/time, floating character panels for the player and a single NPC, a chat-log strip at the bottom-left, a tiny inventory at the bottom-right and a permanent verb-wheel on the right edge.

The HUD assumes the default 320×200 internal resolution and that PlayerName / NPCName correspond to registered characters. Pass "" for NPCName to skip the NPC panel.

func Run

func Run(g *Game) error

Run validates the game, then enters the ebiten main loop. The window is sized to Game.WindowScale times the internal resolution, 4× by default.

func TextWidth

func TextWidth(s string) int

TextWidth is the pixel width of s in the built-in font.

func WidgetAwake

func WidgetAwake(w Widget) bool

WidgetAwake reports whether a widget takes part in the frames a script holds. Nothing but a menu should say yes.

func WidgetVisible

func WidgetVisible(ctx *Ctx, w Widget) bool

WidgetVisible reports whether a widget takes part in this frame at all. A widget switched off neither ticks nor draws, and does not block a click, so a HUD can be hidden for a cutscene without unregistering it.

func WrapText

func WrapText(s string, maxPx int) []string

WrapText splits s at word boundaries into lines no wider than maxPx. A newline in s breaks the line where it stands.

Types

type Achievement

type Achievement = achievement.Achievement

Achievement is something the player is recognised for, earned by a Condition the engine watches while the world shows; see achievement.Achievement.

type AchievementManager

type AchievementManager = achievement.Manager

AchievementManager is the registry of a game's Achievements, keyed by name; see achievement.Manager.

type Action

type Action = inkbase.Action

Action is an immutable spec of work, started fresh into a Runner every time it runs; see inkbase.Action.

func Back
func Back() Action

Back returns to the scene the player came from.

func ClearFlag
func ClearFlag(name string) Action

ClearFlag lowers a world-state flag.

func Custom
func Custom(fn func(*Ctx) Status) Action

Custom wraps a user function as an Action. Returns Done on first tick unless the function itself returns StatusRunning.

func EndDialog
func EndDialog() Action

EndDialog closes the open conversation, and so lets the RunDialog that opened it finish.

func Flash
func Flash(text Text) Action

Flash puts a line in the status strip for about two seconds.

func Give
func Give(item *Item) Action

Give puts an item in the inventory.

func GoBack
func GoBack() Action

GoBack is an Action that returns to the previous Stage, or does nothing when there is none; see Game.PreviousStage and ibaction.GoBack.

func GoTo
func GoTo(scene *Scene) Action

GoTo takes the player to another scene, through the scene fade.

func GoToStage
func GoToStage(stage *Stage) Action

GoToStage is an Action that shows stage, at the top of the next frame; see Game.GoToStage and ibaction.GoToStage. It is done at once.

func GotoNode
func GotoNode(node string) Action

GotoNode moves the open conversation to another of its nodes.

func If
func If(cond Condition, then Action, els ...Action) Action

If runs then when cond holds and els otherwise.

func Learn
func Learn(entity Knowable) Action

Learn marks an entity Known.

func LoadSlot
func LoadSlot(slot int) Action

LoadSlot is an Action that restores the session saved in slot, at the top of the next frame; see Game.LoadLater.

func Noting
func Noting(name string, value any, inner Action) Action

Noting writes a var for the duration of the inner action.

func Par
func Par(actions ...Action) Action

Par runs its actions in parallel and is done when all are.

func PlayMusic
func PlayMusic(music *Music) Action

PlayMusic is an Action that starts music in place of the current track; see AudioPlayer.PlayMusic. It is done at once.

func PlaySound
func PlaySound(sound *Sound) Action

PlaySound is an Action that plays sound once; see AudioPlayer.PlaySound. It is done at once, without waiting for the sound to end.

func Quit
func Quit() Action

Quit is an Action that ends the game: the main loop exits at the top of the next frame.

func Reply
func Reply(speaker Talker, answer func(state *State) Text) Action

Reply is Say with the line decided when it is spoken: answer is called with the State at that moment, and what it returns is what is said.

func RequireItem
func RequireItem(item *Item) Action

RequireItem fails the sequence, with a flash line, unless the item is held.

func Restart
func Restart() Action

Restart is an Action that begins a new session, at the top of the next frame; see Game.Restart.

func RunDialog
func RunDialog(dialog *Dialog) Action

RunDialog opens a dialog tree and holds the script until it closes.

func RunScript
func RunScript(script *Script) Action

RunScript is an Action that runs the script's Actions to the end and takes their Status; a nil script, or one with no Actions, fails. See ibaction.RunScript.

func SaveSlot
func SaveSlot(slot int) Action

SaveSlot is an Action that saves the session into slot at once; it fails when Game.Save returns an error, which goes to the debug log.

func Say
func Say(speaker Talker, text Text) Action

Say puts one line in a character's mouth. The line is a Text, so the same call carries every language it has been written in; which one reaches the player is decided when the runner starts, not when the content is authored.

func Seq
func Seq(actions ...Action) Action

Seq runs its actions one after another.

func SetFlag
func SetFlag(name string) Action

SetFlag raises a world-state flag.

func SetPose
func SetPose(character *Character, pose Pose) Action

SetPose holds a character in a pose until the character next walks or speaks.

func SetVar
func SetVar(name string, v any) Action

SetVar writes a world-state variable.

func SetVarIfEmpty
func SetVarIfEmpty(name string, v any) Action

SetVarIfEmpty writes a world-state variable only while nothing has written it yet.

func SetVoice
func SetVoice(on bool) Action

SetVoice switches recorded lines on or off and saves the settings.

func ShowEnd
func ShowEnd(text Text) Action

ShowEnd puts the end card up and never finishes: the game is over.

func StopMusic
func StopMusic() Action

StopMusic is an Action that stops the current track; see AudioPlayer.StopMusic.

func TakeAway
func TakeAway(item *Item) Action

TakeAway removes an item from the inventory.

func UseLang
func UseLang(lang string) Action

UseLang is an Action that sets Lang to lang and saves the settings; a failed save is ignored.

func UseTheme
func UseTheme(theme *Theme) Action

UseTheme is an Action that makes theme the active one and saves the settings; see Game.UseTheme. A nil or unregistered theme fails it; a failed save is ignored.

func Wait
func Wait(seconds float64) Action

Wait holds the frame for a number of seconds.

func Walk
func Walk(character *Character, to Point) Action

Walk sends a character to a point through the walkboxes and holds the script until it arrives.

type Align

type Align = inkbase.Align

Align is how a single line of text sits horizontally in the space a widget gives it; see inkbase.Align.

type Asset

type Asset = inkbase.Asset

Asset is a picture on the asset filesystem, a name and a Path; see inkbase.Asset.

type AssetManager

type AssetManager = Manager[*Asset]

AssetManager is the registry of a game's Assets, keyed by name; see Manager.

type Audible

type Audible = inkbase.Audible

Audible is anything the AudioPlayer can play, named by its Source path; see inkbase.Audible.

type AudioPlayer

type AudioPlayer struct {
	// contains filtered or unexported fields
}

AudioPlayer plays music, recorded lines and one-shot sound effects through Ebiten's audio subsystem. Music is streamed, looping when its Music says so; a Voice is decoded and played once, and can be stopped when the line is skipped; sound effects are decoded once, cached as raw PCM, and replayed via NewPlayerFromBytes for low-latency triggering.

AudioPlayer is fail-soft by design: missing files, decode errors and a missing audio device all degrade to no-ops with a debug log message — the game keeps running.

func NewAudioPlayer
func NewAudioPlayer() *AudioPlayer

NewAudioPlayer returns an AudioPlayer with nothing playing. The audio context is not created until the first thing is played.

func (*AudioPlayer) PlayMusic
func (a *AudioPlayer) PlayMusic(music *Music)

PlayMusic starts music in place of the current track, looping it when its Loop says so. The same track already playing carries on untouched; nil or a file that cannot be played stops the current track and plays nothing.

func (*AudioPlayer) PlaySound
func (a *AudioPlayer) PlaySound(sound *Sound)

PlaySound plays a sound once, on top of anything else playing. The file is decoded on first use and kept; nil or a file that cannot be played does nothing.

func (*AudioPlayer) PlayVoice
func (a *AudioPlayer) PlayVoice(voice *Voice) float64

PlayVoice plays a recorded line once and reports how long it lasts, in seconds, so the line can stay up exactly that long; 0 means nothing played. A Voice already playing is stopped first: one line at a time.

func (*AudioPlayer) StopMusic
func (a *AudioPlayer) StopMusic()

StopMusic stops the current track and forgets it, so the next PlayMusic starts afresh even with the same Music.

func (*AudioPlayer) StopVoice
func (a *AudioPlayer) StopVoice()

StopVoice cuts the recorded line short — a skipped line stops talking.

type Awake

type Awake = ibwidget.Awake

Awake is implemented by a widget that ticks even on the frames a running script holds — a debug menu or a pause menu, which has to answer a key press while a cutscene is playing, and has to be closable once it is open. Every other widget stands still until the script is done, which is what keeps a click from racing a cutscene that is already under way.

An awake widget ticks BEFORE the script's runner does, so a click it consumes is not also read as "skip this line".

type CharStat

type CharStat struct {
	Label  Text
	VarKey string
}

CharStat is one (Label, VarKey) row rendered inside a CharacterPanel. VarKey is read from g.State.Var; the value is fmt.Sprint'd verbatim.

type Character

type Character struct {
	Name string
	// Label is what the UI shows instead of Name — a code name, a title,
	// a shout. Empty means the Name is presentable as it stands.
	Label Text
	// Lore is what the player's record says about the character once they
	// are known — a bio for a dossier or a codex screen. Nil keeps the
	// character out of such a screen.
	Lore Text
	// Poses are the sprites the character is drawn with, one per Pose the
	// artwork covers. Nil, or a Pose with no entry, falls back to Idle; no
	// Idle draws the placeholder figure sized W by H.
	Poses map[Pose]*Sprite
	// Speed is how fast the character walks, in pixels per second.
	Speed float64
	// SpeechColor is the colour of the character's lines; nil, or a
	// fully transparent color.RGBA, leaves the theme's.
	SpeechColor color.Color
	// Delivery decides where Say puts this character's lines.
	Delivery Delivery
	// Start is where the character stands in a scene that lists no actors
	// of its own, and Game.Player names the one placed there.
	Start Point
	// Size hints used when the sprite is a placeholder rectangle.
	W, H float64
}

Character is someone with a body: drawn in a scene with its Poses, walking through the walkboxes at Speed, and speaking like a Speaker. Game.Player names the one the player steers.

func (Character) DisplayName
func (c Character) DisplayName() Text

DisplayName is the Label if there is one, the Name otherwise.

func (Character) GetName
func (c Character) GetName() string

GetName returns the character's Name, its key in the CharacterManager.

func (*Character) Reply
func (c *Character) Reply(answer func(state *State) Text) Action

Reply is the package-level Reply with this character filled in: the line is worked out from the State when the action runs.

func (*Character) Say
func (c *Character) Say(text Text) Action

Say is the package-level Say with this character filled in, so a script reads who, then what: paul.Say(line).

func (*Character) TalkerName
func (c *Character) TalkerName() string

TalkerName returns the character's Name, which is how a line names it.

func (Character) TypeLabel
func (c Character) TypeLabel() string

TypeLabel returns "character", the kind a Character is registered as.

type CharacterManager

type CharacterManager = Manager[*Character]

CharacterManager is the registry of a game's Characters, keyed by name; see Manager.

type CharacterPanel

type CharacterPanel struct {
	Name      string
	When      Condition
	Bounds    Rectangle
	Character *Character
	Title     Text // "PLAYER", "NPC", role badge
	Stats     []CharStat
}

CharacterPanel is a small floating panel that shows portrait + stats for one registered character. The panel auto-hides when the character isn't listed as an actor in the current scene, so the same panel registration works across scenes.

func (*CharacterPanel) BlocksClickAt
func (c *CharacterPanel) BlocksClickAt(ctx *UICtx, p Point) bool

BlocksClickAt reports whether p falls inside Bounds while the panel is shown, that is while its character is in the current scene.

func (*CharacterPanel) Draw
func (c *CharacterPanel) Draw(dst *ebiten.Image, ctx *UICtx)

Draw paints the panel in Bounds: the character's portrait on the left, then the Title, the character's display name and one "Label: value" line per stat. It draws nothing when Character is nil or not in the current scene.

func (*CharacterPanel) GetName
func (c *CharacterPanel) GetName() string

GetName returns the name the panel is registered under.

func (*CharacterPanel) Layer
func (c *CharacterPanel) Layer() Layer

Layer puts the panel on LayerHUD.

func (*CharacterPanel) Tick
func (c *CharacterPanel) Tick(ctx *UICtx)

Tick does nothing; the panel only displays.

func (*CharacterPanel) VisibleWhen
func (c *CharacterPanel) VisibleWhen() Condition

VisibleWhen returns the condition that switches the panel on; nil means always.

type ChatLog

type ChatLog = chatlog.ChatLog

ChatLog renders the in-game message log (player commands and in-world replies) as a scrolling box, in the active theme's chat log colours; see chatlog.ChatLog and Game.ChatLogColors.

type ClickBlocker

type ClickBlocker = ibwidget.ClickBlocker

ClickBlocker is the optional widget contract for "the screen under me is mine": a visible widget that reports p as blocked keeps a click there from reaching the scene behind it — no hotspot, no walk — and keeps the verb coin from opening over it. The widget's own Tick still gets the click first. VerbBar, InventoryBar, DialogBox (while a dialog is open), CharacterPanel (while its character is in the scene), HotspotPanel, Panel (with Blocks), TopBar, ChatLog, the open Menu and the open RadialVerbs implement it. See ibwidget.ClickBlocker.

type Condition

type Condition = inkbase.Condition

Condition is a question about the game as it stands: a flag, an item in hand, the scene the player is in. Actions branch on one, widgets are shown by one, and a dialog choice is offered by one, so the same eight little types answer for all three.

func And
func And(conditions ...Condition) Condition

And holds when all of its conditions hold.

func Flag
func Flag(name string) Condition

Flag holds while a named world-state flag is raised.

func HasItem
func HasItem(item *Item) Condition

HasItem is true while the item is in the inventory.

func InScene
func InScene(scene *Scene) Condition

InScene is true while the given scene is the current one.

func Knows
func Knows(entity Knowable) Condition

Knows is true once the player has come to know an entity.

func Not
func Not(c Condition) Condition

Not inverts a condition.

func OnStage
func OnStage(stage *Stage) Condition

OnStage is true while the given Stage is the one showing.

func OnWorld
func OnWorld() Condition

OnWorld is true while the Stage showing is the world.

func Or
func Or(conditions ...Condition) Condition

Or holds when at least one of its conditions holds.

func SelectedItem
func SelectedItem(item *Item) Condition

SelectedItem is true while the item is the one in hand.

func Started
func Started() Condition

Started is true once a session exists: the world has been begun or loaded.

func VarEq
func VarEq(name string, v any) Condition

VarEq holds while a world-state variable compares equal to a value.

type Ctx

type Ctx = inkbase.Ctx

Ctx is what a Runner is handed each tick: the frame's time step, the State, and the *Game; see inkbase.Ctx.

type Cursor

type Cursor struct {
	Name string
	When Condition
}

Cursor renders the mouse cursor. When the player has an inventory item selected, the item's sprite is drawn at the cursor instead of the default crosshair. It sits on LayerCursor, so it draws on top.

func (*Cursor) Draw
func (c *Cursor) Draw(dst *ebiten.Image, ctx *UICtx)

Draw paints the held item's sprite, 16 pixels square and centred on the pointer, or, with nothing held or no sprite, a small crosshair in Theme.CursorColor.

func (*Cursor) GetName
func (c *Cursor) GetName() string

GetName returns the name the cursor is registered under.

func (*Cursor) Layer
func (c *Cursor) Layer() Layer

Layer puts the cursor on LayerCursor, above everything else.

func (*Cursor) Tick
func (c *Cursor) Tick(ctx *UICtx)

Tick does nothing; the cursor follows the pointer when it draws.

func (*Cursor) VisibleWhen
func (c *Cursor) VisibleWhen() Condition

VisibleWhen returns the condition that switches the cursor on; nil means always.

type CursorKind

type CursorKind int

CursorKind is the pointer a Hotspot asks for while the mouse is over it.

const (
	CursorDefault CursorKind = iota // the ordinary arrow
	CursorLook                      // something to look at
	CursorUse                       // something to use
	CursorTalk                      // someone to talk to
	CursorTake                      // something to pick up
	CursorExit                      // a way out; every Exit gets it
)

The pointers a Hotspot can ask for.

type DebugKind

type DebugKind int

DebugKind is what a DebugTarget is, and decides the colour of the plate HotspotDebug paints over it. The kinds are the things a player can point at: a hotspot painted into the scene, a way out of it, a character standing in it, an item in the inventory, and a target mounted on a HotspotPanel.

const (
	// DebugHotspot is a hotspot painted into the scene.
	DebugHotspot DebugKind = iota
	// DebugExit is a way out of the scene.
	DebugExit
	// DebugCharacter is a character standing in the scene.
	DebugCharacter
	// DebugItem is an item in an inventory slot.
	DebugItem
	// DebugPanel is a hotspot mounted on a [HotspotPanel].
	DebugPanel
)

type DebugTarget

type DebugTarget struct {
	Kind DebugKind
	Name string
	Area Rectangle
}

DebugTarget is one thing on screen HotspotDebug should mark: its kind, the name the game knows it by, and the screen rectangle it occupies.

type Delivery

type Delivery = inkbase.Delivery

Delivery is where a Character's or a Speaker's lines reach the player; see inkbase.Delivery.

type Dialog

type Dialog = iwdialog.Dialog

Dialog is a conversation tree: named nodes and the node it opens on; see iwdialog.Dialog.

type DialogBox

type DialogBox struct {
	Name       string
	When       Condition
	Bounds     Rectangle
	LineHeight int
	Padding    int
}

DialogBox renders the active conversation: one line at a time (advanced on click) followed by the player's choices. The widget is dormant unless ctx.Game.dialogActive() — and while active it consumes input so nothing underneath reacts.

func (*DialogBox) BlocksClickAt
func (d *DialogBox) BlocksClickAt(ctx *UICtx, p Point) bool

BlocksClickAt reports whether p falls inside the box while a dialog is open; a zero Bounds is the strip Draw uses.

func (*DialogBox) Draw
func (d *DialogBox) Draw(dst *ebiten.Image, ctx *UICtx)

Draw paints the box while a dialog is active: the current line under its speaker's name, or, once the lines are spent, the choices the player can see, the one under the pointer highlighted. It records each choice's rectangle for Tick to hit-test. A zero Bounds is a 60-pixel strip from y 140 across the screen.

func (*DialogBox) GetName
func (d *DialogBox) GetName() string

GetName returns the name the dialog box is registered under.

func (*DialogBox) Layer
func (d *DialogBox) Layer() Layer

Layer puts the dialog box on LayerDialog.

func (*DialogBox) Tick
func (d *DialogBox) Tick(ctx *UICtx)

Tick does nothing unless a dialog is active. While one is, it consumes every click: a left click advances to the next line, or, once the lines are spent, picks the choice under the pointer, marks a Once choice as used and queues the choice's actions.

func (*DialogBox) TicksDuringScript
func (d *DialogBox) TicksDuringScript() bool

TicksDuringScript reports true: the conversation is the running script. RunDialog holds the frame for as long as the dialog is up, so the box has to tick inside it — otherwise its lines never advance, no choice is ever clicked, and the dialog can never reach the EndDialog that would let go.

func (*DialogBox) VisibleWhen
func (d *DialogBox) VisibleWhen() Condition

VisibleWhen returns the condition that switches the box on; nil means always.

type DialogChoice

type DialogChoice = iwdialog.DialogChoice

DialogChoice is an answer the player can pick in a DialogNode, with the Condition that offers it and the actions it runs; see iwdialog.DialogChoice.

type DialogLine

type DialogLine = iwdialog.DialogLine

DialogLine is one line of a DialogNode and the Talker who says it; see iwdialog.DialogLine.

type DialogManager

type DialogManager = iwdialog.Manager

DialogManager is the registry of a game's Dialogs, keyed by name; see iwdialog.Manager.

type DialogNode

type DialogNode = iwdialog.DialogNode

DialogNode is one step of a Dialog: the lines spoken on reaching it and the choices offered after them; see iwdialog.DialogNode.

type EndCard

type EndCard = endcard.EndCard

EndCard is a fullscreen overlay shown when the ShowEnd action fires. While active it consumes every click so nothing underneath reacts. It draws in the active theme's end card colours; see endcard.EndCard.

type Exit

type Exit struct {
	// To is where the exit leads. Nil means back the way the player came.
	// A scene that leads back to the one that leads here is written with
	// Scene.Leads, after both are declared.
	To *Scene

	Label Text

	// Side places the hotspot on an edge of the scene area when Area is nil.
	Side ExitSide

	Area Shape

	// Needs is a flag the journey requires; Blocked is what happens without
	// it. Nil Blocked simply does nothing.
	Needs   string
	Blocked Action

	OnLook Action
	OnTake Action
}

Exit is a way out of a scene. It becomes a Hotspot with the exit cursor whose use is the journey; look and take fall back to Game.ExitLook and Game.ExitTake when the Exit says nothing of its own.

type ExitSide

type ExitSide int

ExitSide says which edge of the picture an Exit sits on when it declares no Area of its own.

const (
	ExitLeft  ExitSide = iota // off the left edge
	ExitRight                 // off the right edge
	ExitBack                  // into the depth of the picture
	ExitNear                  // out towards the camera
)

The edges an Exit can sit on.

type Extension

type Extension interface {
	Start(game *Game) error
	Tick(ctx *Ctx)
	Stop()
}

Extension is a piece of machinery that lives beside the game for the whole run and is not a widget: a platform layer such as Steam, which has to be initialised before the window opens, pumped every frame whether a script holds the frame or not, and shut down when the loop returns. A widget stands still while a script runs and has no hook at either end, which is why this is its own contract.

Start runs at the top of Run, before the settings document is read, so an extension may hand the Game a Store of its own and set SystemLang. An error from Start ends Run. Tick runs first thing in every Update, on every Stage. Stop runs once the ebiten loop has returned.

type Facing

type Facing = iwsprite.Facing

Facing is which way a character looks, left or right; see iwsprite.Facing.

type FileStore

type FileStore struct {
	Dir string
}

FileStore keeps each key as <dir>/<key>.json. A write goes to a temporary file first and is renamed into place, so a crash mid-write leaves the previous document rather than half of the new one.

func NewFileStore
func NewFileStore(app string) *FileStore

NewFileStore is the store for an app on this machine: os.UserConfigDir with the app's name under it — ~/Library/Application Support/<app> on macOS, ~/.config/<app> on Linux, %AppData%\<app> on Windows. When the user's directory cannot be found it falls back to <app> under the working directory, so the game still saves somewhere.

func (*FileStore) Read
func (f *FileStore) Read(key string) ([]byte, error)
func (*FileStore) Write
func (f *FileStore) Write(key string, data []byte) error

type Game

type Game struct {
	Title         string
	Width, Height int

	ItemManager      *ItemManager
	SceneManager     *SceneManager
	CharacterManager *CharacterManager
	SpeakerManager   *SpeakerManager
	DialogManager    *DialogManager
	ScriptManager    *ScriptManager
	AssetManager     *AssetManager
	VerbManager      *VerbManager
	WidgetManager    *WidgetManager
	ThemeManager     *ThemeManager
	StageManager     *StageManager
	// AchievementManager holds what the player can be recognised for. Its
	// entries are evaluated on the world stage and reported through the
	// [Extension]s — see [Achievement] and [Extension].
	AchievementManager *AchievementManager

	// Extensions live beside the game for the whole run: started before the
	// settings are read, ticked first thing every frame, stopped when the
	// loop returns. A platform layer such as Steam is one. Nil is a game
	// with none.
	Extensions []Extension

	// SceneRect is the part of the window the picture occupies. Zero means
	// the whole window. A game with a HUD along the foot sets it, so that
	// generated exit strips (see [Exit]) land inside the painting.
	SceneRect Rectangle

	// ExitLook and ExitTake supply the default look and take responses for
	// every hotspot generated from Scene.Exits. Nil means no response.
	// An Exit can override either one.
	ExitLook func(Exit) Action
	ExitTake func(Exit) Action

	// UseWithFail supplies the response when the player uses an item on a
	// hotspot nobody paired it with. Nil falls back to a flash line.
	UseWithFail func(item *Item, h *Hotspot) Action

	// StartStage is the screen the game opens on — its main menu, or the
	// world itself for a run that goes straight in.
	StartStage *Stage
	// StartScene is the scene a session opens in. It is entered when the
	// world starts, which is at boot for a game whose StartStage is the
	// world, and on Restart for one that opens on a menu.
	StartScene *Scene
	// StartScript is queued after that scene's OnEnter, once.
	StartScript *Script
	// FinaleScript is the closing script, queued straight after
	// StartScript — which is what a "boot into the ending" flag wants.
	// Empty for an ordinary run.
	FinaleScript *Script
	// StartTheme is the theme selected at boot. Empty takes
	// "classic-scumm", so a game that never mentions a theme has one.
	StartTheme *Theme
	// Flags are set in State before the first frame. A game whose debug
	// build or command line switches something on says so here rather
	// than reaching into State after construction.
	Flags []string

	// SystemLang is the language the machine or the platform reports — the
	// Steam client's, an OS locale — and the one a first run speaks when
	// nothing stored says otherwise. Run's order is: the language set
	// before Run, then the stored setting, then this, then Fallback. An
	// Extension's Start is where a platform writes it.
	SystemLang string

	// Player names the character the player controls: the one a scene gets
	// when it lists no actors of its own, placed at that character's Start,
	// and the one WalkOnClick moves. Empty leaves such a scene unpeopled.
	Player *Character

	// WalkOnClick makes a left click on empty floor walk Player there,
	// routed through the walkboxes. Off by default: a game whose hotspots
	// answer from any distance has no reason for its character to move,
	// and one that does says so here.
	WalkOnClick bool

	// Walkboxes is the floor a scene walks on when it declares none of
	// its own. Nil means no routing in those scenes.
	Walkboxes []Polygon

	// WindowScale multiplies the internal resolution when Run sizes the
	// window. 0 means 4.
	WindowScale int

	State     *State
	Inventory *Inventory
	Audio     *AudioPlayer
	Input     *Input

	MaxLogLines int // 0 = unlimited (memory grows); set per-game.

	// App names the game to the machine it runs on: the directory its
	// settings and saves live in natively, the localStorage prefix in a
	// browser. Empty is "inkwell".
	App string
	// Store is where settings and saves are kept. Nil takes the platform's
	// default for App; a test hands in a MemoryStore.
	Store Store
	// contains filtered or unexported fields
}

Game is the root aggregate. It carries every Manager plus the runtime state. A domain package builds one via NewGame, registers entities, optionally calls RegisterDefaultUI / UseTheme, then hands it to Run.

func New
func New(spec Game) *Game

New builds a game from a declared one. Everything a game is rather than does — its size, its managers, where it opens, the responses it hands the engine — is a field, so the whole of it can be one literal at the call site instead of a page of assignments the reader has to hold in their head:

game := inkwell.New(inkwell.Game{
	Title:        "Real World",
	Width:        640,
	Height:       380,
	StartScene:   "alley",
	StartScript:  "opening",
	Player:       "paul",
	SceneManager: scene.SceneManager,
	…
})

What the spec leaves out, New fills in: an empty manager for every category the caller did not hand over, the runtime halves (State, Inventory, Audio, Input), the SCUMM verb set when no verbs were registered, and the preset themes — added to whatever the caller's ThemeManager already holds rather than replacing them. Widgets are NOT auto-registered; Run falls back to the SCUMM preset when none were.

A manager handed over here is used in place, not copied, so the engine and the domain share one registry per category instead of two to keep in step.

func NewGame
func NewGame(title string, w, h int) *Game

NewGame is New for a game that would rather be built in steps: it declares nothing but the window, and the caller assigns the rest.

func (*Game) AppName
func (g *Game) AppName() string

AppName is App, or "inkwell" for a game that named none: the word the platform's default Store files the documents under, exported so that a Store an extension builds files them under the same one.

func (*Game) ChangeScene
func (g *Game) ChangeScene(name string)

ChangeScene transitions to the named scene.

func (*Game) CharacterInScene
func (g *Game) CharacterInScene(character *Character) bool

CharacterInScene reports whether a registered character is listed as an actor in the current scene. Used by the CharacterPanel widget so it auto-hides when the character isn't in view.

func (*Game) CharacterMoving
func (g *Game) CharacterMoving(name string) bool

CharacterMoving reports whether a character is en route.

func (*Game) ChatLogColors
func (g *Game) ChatLogColors() chatlog.Colors

ChatLogColors is the palette a ChatLog draws with, from the active theme: the chat log background, prompt, response and system colours, and the character panel border for the frame and the header rule. It satisfies chatlog.Host.

func (*Game) ClearSpeech
func (g *Game) ClearSpeech()

ClearSpeech takes the line SetSpeech put up down again.

func (*Game) ClickBlockedAt
func (g *Game) ClickBlockedAt(ctx *UICtx, p Point, except Widget) bool

ClickBlockedAt reports whether a visible widget other than except claims p through ClickBlocker. except may be nil.

func (*Game) ConsumedClick
func (g *Game) ConsumedClick() bool

ConsumedClick reports (and consumes) a left click for skip-line.

func (*Game) CurrentScene
func (g *Game) CurrentScene() string

CurrentScene is the name of the scene the player is in, empty before the first one is entered.

func (*Game) CurrentStage
func (g *Game) CurrentStage() *Stage

CurrentStage is the screen showing, nil before the first frame.

func (*Game) CurrentStagePtr
func (g *Game) CurrentStagePtr() any

CurrentStagePtr satisfies cond.StageChecker. It returns the current Stage as an any so that cond/ can compare by pointer identity without importing the Stage type.

func (*Game) DeliveryOf
func (g *Game) DeliveryOf(name string) Delivery

DeliveryOf is how the voice behind name delivers its lines, by Game.Speaker; a name nobody answers to gets DeliveryBubble.

func (*Game) DialogActive
func (g *Game) DialogActive() bool

DialogActive reports whether a dialog is in progress.

func (*Game) Do
func (g *Game) Do(a Action)

Do queues an action. Queued actions run one at a time and in order — the engine starts the next one on the first frame the previous one is done, so a widget or a domain pump can hand work in at any moment.

func (*Game) DrawText
func (g *Game) DrawText(dst *ebiten.Image, s string, x, y int, c color.Color)

DrawText is a thin wrapper that lets widgets accept a Theme-supplied color today and switch to text/v2 later without changing call sites.

func (*Game) EndCardColors
func (g *Game) EndCardColors() endcard.Colors

EndCardColors is the palette an EndCard draws with: the active theme's EndCardBG and EndCardText. It satisfies endcard.Host.

func (*Game) EndCardText
func (g *Game) EndCardText() string

EndCardText is the text ShowEnd put up, or "" while the game is not over. It satisfies endcard.Host.

func (*Game) EndDialog
func (g *Game) EndDialog()

EndDialog closes the active dialog.

func (*Game) FlashLine
func (g *Game) FlashLine(text Text)

FlashLine puts up a status-line message for about two seconds of game time, replacing the one up now. The game loop counts it down every frame and clears it, so it expires whether or not a StatusLine is showing it; Game.FlashText reads it.

func (*Game) FlashText
func (g *Game) FlashText() string

FlashText is the flash message up now, or "" when there is none; see Game.FlashLine. It satisfies statusline.Host.

func (*Game) GoToStage
func (g *Game) GoToStage(stage *Stage)

GoToStage shows another screen, at the top of the next frame.

func (*Game) GotoDialogNode
func (g *Game) GotoDialogNode(name string)

GotoDialogNode moves to a node within the active dialog.

func (*Game) HotspotAt
func (g *Game) HotspotAt(p Point) *Hotspot

HotspotAt returns the topmost hotspot in the current scene whose Area contains p, or nil if none. Exposed so widgets (e.g., a verb coin that only opens over hotspots) can do their own hit-tests in their Tick — the engine's own resolution runs only after every widget had a chance, so widgets can't rely on it.

func (*Game) HoverHint
func (g *Game) HoverHint() string

HoverHint composes the "verb + target" string used by the StatusLine.

func (*Game) HoverLabel
func (g *Game) HoverLabel() string

HoverLabel returns the label of what the pointer is over, as the engine set it this frame, for a widget to show.

func (*Game) InventoryAdd
func (g *Game) InventoryAdd(name string)

InventoryAdd puts the named item in the player's Inventory; it satisfies act.InventoryHost.

func (*Game) InventoryHas
func (g *Game) InventoryHas(name string) bool

InventoryHas reports whether the player carries the named item; it satisfies act.InventoryHost.

func (*Game) InventoryRemove
func (g *Game) InventoryRemove(name string)

InventoryRemove takes the named item out of the player's Inventory; it satisfies act.InventoryHost.

func (*Game) InventorySelected
func (g *Game) InventorySelected() string

InventorySelected satisfies cond.InventoryChecker.

func (*Game) InvokeVerb
func (g *Game) InvokeVerb(h *Hotspot, verb string)

InvokeVerb runs the action bound to verb on hotspot h, honoring the SCUMM use-with-item flow when an inventory item is selected: the hotspot's own OnUseWith first, then the item's, then UseWithFail.

It is the one path from "the player picked a verb and a target" to an action on the queue, and it is exported because a target does not have to be in the picture. A HUD widget that owns something clickable — a device in the corner, a slot on a panel — hands its own Hotspot here and gets the same logging, the same use-with resolution and the same nothing-happens line as a painted hotspot, rather than re-deciding all of that in its Tick. HotspotPanel is that widget.

func (*Game) ItemLabel
func (g *Game) ItemLabel(name string) Text

ItemLabel is what the UI should print for an item: its Description if it carries one, its name otherwise. The status line and the chat log both name the item the player is holding, and a bare registry key is not a thing anybody wrote — least of all in the player's language.

func (*Game) Load
func (g *Game) Load(slot int) error

Load replaces the runtime state with the session in slot N. Managers (items, scenes, characters, …) are left intact. References in the save (scene name, theme, character names) are validated against the current managers before any state is touched.

func (*Game) LoadLater
func (g *Game) LoadLater(slot int)

LoadLater restores a saved session, at the top of the next frame. Load itself is immediate, and is what this ends up calling.

func (*Game) LogAction
func (g *Game) LogAction(text string)

LogAction appends a player command to the message log, trimming the oldest lines past MaxLogLines.

func (*Game) LogResponse
func (g *Game) LogResponse(speaker, text string)

LogResponse appends a line said by speaker to the message log, trimming the oldest lines past MaxLogLines.

func (*Game) LogSystem
func (g *Game) LogSystem(text string)

LogSystem appends a message from the game itself to the message log, trimming the oldest lines past MaxLogLines.

func (*Game) MakeCtx
func (g *Game) MakeCtx() *Ctx

MakeCtx returns a Ctx over the game's State for one frame, the kind a Condition or a Menu row's Value and Expand is evaluated with.

func (*Game) MenuColors
func (g *Game) MenuColors() menu.Colors

MenuColors is the palette a Menu draws with, from the active theme: the dialog background, border and text, the dialog choice colours for the row under the cursor, the dialog speaker colour for the title bar, the top bar's accent for the value column and the status text for the footer. It satisfies menu.Host.

func (*Game) Messages
func (g *Game) Messages() []LogMessage

Messages returns the message log, oldest first. The slice is the game's own buffer, not a copy, so a caller must not change it.

func (*Game) NoteSpoken
func (g *Game) NoteSpoken(name string)

NoteSpoken records that the voice behind a name has been heard.

func (*Game) OnFinale
func (g *Game) OnFinale(script *Script) *Game

OnFinale names the game's closing script, queued directly after the start script — which is what a "boot straight into the ending" debug flag wants. Leave it unset for an ordinary run.

func (*Game) OnStart
func (g *Game) OnStart(script *Script) *Game

OnStart names the script queued after the start scene's OnEnter, once, when the game boots.

func (*Game) OnWorldStage
func (g *Game) OnWorldStage() bool

OnWorldStage reports whether the screen showing is the game. Off it the scene is not painted, characters do not walk and triggers do not fire.

func (*Game) PanelColors
func (g *Game) PanelColors() panel.Colors

PanelColors is the fallback palette of a Panel that sets no colour of its own: the active theme's PanelBG and DialogBorder. It satisfies panel.Host.

func (*Game) PlayVoice
func (g *Game) PlayVoice(v *Voice) float64

PlayVoice plays a voice recording and returns its duration in seconds.

func (*Game) PreviousScene
func (g *Game) PreviousScene() string

PreviousScene is the name of the scene before the current one, which is where Back leads; empty if there was none.

func (*Game) PreviousStage
func (g *Game) PreviousStage() *Stage

PreviousStage is the screen shown before the current one, which is where a "back" row leads (GoBack); nil if there was none.

func (*Game) Quit
func (g *Game) Quit()

Quit closes the window at the end of the frame.

func (*Game) Restart
func (g *Game) Restart()

Restart begins a new session, at the top of the next frame.

func (*Game) Run
func (g *Game) Run() error

Run is Run called on g: it validates the game and enters the ebiten main loop, returning when the window closes or on a validation error.

func (*Game) Save
func (g *Game) Save(slot int) error

Save serialises the current session into slot N of the slots document. The in-flight script and dialog are dropped — saves capture state at idle/ready boundaries.

func (*Game) SaveSettings
func (g *Game) SaveSettings() error

SaveSettings writes the document with what the game is doing now: the language every Text resolves into, the active theme, the window scale. UseLang and UseTheme call it, so a choice on a settings screen is kept the moment it is made.

func (*Game) SceneArea
func (g *Game) SceneArea() Rectangle

SceneArea is SceneRect, or the whole window when it was never set.

func (*Game) SceneHotspots
func (g *Game) SceneHotspots(name string) []Hotspot

SceneHotspots is everything clickable in a scene: its own hotspots first, then one per Exit. Authored hotspots come first because the engine takes the first area that contains the click, so a painted thing beats the edge strip an exit sits on wherever the two overlap.

The expansion is cached, so the pointers HotspotAt hands out stay valid.

func (*Game) SceneTitle
func (g *Game) SceneTitle() Text

SceneTitle is what a bar calls the current scene: its Title, or its Name when it has none, and an empty Text before the first scene. It satisfies topbar.Host.

func (*Game) ScreenSize
func (g *Game) ScreenSize() (width, height int)

ScreenSize returns the logical screen, Width by Height; it is how a widget from the inkbase widget packages learns the screen it draws on (see ibwidget.Screen).

func (*Game) SelectedVerb
func (g *Game) SelectedVerb() string

SelectedVerb returns the name of the verb the player has picked, or "" for none.

func (*Game) SetCharacterPose
func (g *Game) SetCharacterPose(name string, pose Pose)

SetCharacterPose freezes a character's pose.

func (*Game) SetHoverLabel
func (g *Game) SetHoverLabel(s string)

SetHoverLabel sets the label HoverLabel returns.

func (*Game) SetQuitting
func (g *Game) SetQuitting()

SetQuitting satisfies inkbase/action.Quitter.

func (*Game) SetSelectedVerb
func (g *Game) SetSelectedVerb(s string)

SetSelectedVerb picks the verb with the name s; "" picks none.

func (*Game) SetSpeech
func (g *Game) SetSpeech(speaker, text string)

SetSpeech puts up text as the line speaker is saying; the Say action calls it, and the speech widgets draw whatever it holds.

func (*Game) SetTalking
func (g *Game) SetTalking(name string)

SetTalking sets the speaker whose line is up.

func (*Game) SetVoiceEnabled
func (g *Game) SetVoiceEnabled(on bool)

SetVoiceEnabled toggles voice recording playback.

func (*Game) Settings
func (g *Game) Settings() *Settings

Settings is the document as it stands: what was loaded at boot, updated by every SaveSettings since. A game reads and writes its Extra here.

func (*Game) ShowEndCard
func (g *Game) ShowEndCard(text string)

ShowEndCard puts up the end-of-game overlay.

func (*Game) Slots
func (g *Game) Slots(count int) []SlotInfo

Slots lists slots 1 to count from the document's metadata, without decoding a session. A slot nobody has written comes back as one that does not exist, because a screen listing saves is not the place to report a missing file.

func (*Game) Speaker
func (g *Game) Speaker(name string) (Speaker, bool)

Speaker resolves a name that has spoken — a log entry's, a bubble's — to the voice behind it: a character first, then a registered Speaker.

func (*Game) SpeakerLabel
func (g *Game) SpeakerLabel(name string) Text

SpeakerLabel is what the UI calls the voice behind name — its DisplayName — or the name itself when nobody answers to it.

func (*Game) SpeechColor
func (g *Game) SpeechColor(name string, fallback color.Color) color.Color

SpeechColor is the colour of the lines of the voice behind name, or fallback when it has none, its colour is a fully transparent color.RGBA, or nobody answers to the name.

func (*Game) StartAt
func (g *Game) StartAt(scene *Scene) *Game

StartAt sets StartScene, the scene a new session opens in, and hands the game back for chaining.

func (*Game) StartDialog
func (g *Game) StartDialog(d *Dialog)

StartDialog opens a dialog tree.

func (*Game) Started
func (g *Game) Started() bool

Started reports whether a session exists: the world has been begun or loaded. A "Resume" or "Save" row is off until it does.

func (*Game) StateVar
func (g *Game) StateVar(name string) any

StateVar reads the State variable name, nil when it was never set. It satisfies topbar.Host.

func (*Game) StatusLineColors
func (g *Game) StatusLineColors() statusline.Colors

StatusLineColors is the palette a StatusLine draws with: the active theme's FlashText for a flash and StatusText for the hover hint. It satisfies statusline.Host.

func (*Game) StopVoice
func (g *Game) StopVoice()

StopVoice stops the current voice playback.

func (*Game) Talking
func (g *Game) Talking() string

Talking is the name of the character or speaker whose line is showing.

func (*Game) TextCardColors
func (g *Game) TextCardColors() textcard.Colors

TextCardColors is the palette a TextCard draws with: the active theme's EndCardBG for the backdrop and EndCardText for the lines. It satisfies textcard.Host.

func (*Game) Theme
func (g *Game) Theme() *Theme

Theme returns the active Theme. When the active name is not registered — a settings file or a save written by another build of the game — it falls back to classic-scumm, then to the first registered theme. It panics only when the ThemeManager is empty, which Game.Validate rejects at start.

func (*Game) TopBarColors
func (g *Game) TopBarColors() topbar.Colors

TopBarColors is the palette a TopBar draws with, from the active theme's top bar tokens. It satisfies topbar.Host.

func (*Game) UseTheme
func (g *Game) UseTheme(theme *Theme) bool

UseTheme makes theme the active one, by its Name; it is not saved to the settings (the UseTheme action does that). A nil theme, or one not registered in the ThemeManager, leaves the active theme as it is and reports false.

func (*Game) UseThemeByName
func (g *Game) UseThemeByName(name string)

UseThemeByName satisfies inkbase/action.ThemeHost: it makes the theme registered under name the active one; an unknown name changes nothing.

func (*Game) Validate
func (g *Game) Validate() error

Validate cross-checks what the types cannot: that the game has somewhere to open, that a Stage which holds has somewhere to go, and that the active theme is registered. A reference to a scene, an item or a dialog is a pointer now, so an unknown name is a compile error rather than one of these.

func (*Game) VoiceOn
func (g *Game) VoiceOn() bool

VoiceOn reports whether recorded lines play. SetVoice is the action that flips it and saves the settings.

func (*Game) WalkCharacter
func (g *Game) WalkCharacter(name string, to Point)

WalkCharacter sends a character toward a point through the walkboxes.

func (*Game) WidgetInput
func (g *Game) WidgetInput() *Input

WidgetInput returns the frame's consume-on-use input, Game.Input, for the inkbase widgets that claim keys and clicks.

type Gated

type Gated = ibwidget.Gated

Gated is implemented by a widget that can be switched off by game state — every built-in is, through its When field. A widget that does not implement it, or whose condition is nil, is always live.

type Given

type Given = cond.Given

Given is a Condition written as a literal: every field that is set has to hold, and an empty Given holds always.

type Hotspot

type Hotspot struct {
	Name   string
	Area   Shape
	Label  Text
	Cursor CursorKind

	OnLook Action
	OnUse  Action
	OnTalk Action
	OnTake Action
	OnGive Action

	// OnUseWith answers for an item the player has selected when clicking
	// this hotspot, keyed by the item itself; it is looked up first.
	OnUseWith map[*Item]Action

	// OnUseWithAny answers for an item OnUseWith does not name — a thing
	// with one reply for whatever you put on it, which is what a slot, a
	// lock or a person usually is. It stands between the authored pairs
	// and Game.UseWithFail, so a hotspot that has something of its own to
	// say says it, and everything else still falls through to the game's
	// general refusal.
	OnUseWithAny Action

	// OnVerb holds the actions of custom verbs registered in the
	// VerbManager, keyed by the verb's Name.
	OnVerb map[string]Action
}

Hotspot is a thing in a scene a verb can reach: an Area to click, a Label to show on hover, and the Action each verb runs. A verb with no Action does nothing here.

type HotspotDebug

type HotspotDebug struct {
	Name    string
	When    Condition
	Enabled bool
	// ToggleKey, if non-zero, overrides the default F1 toggle.
	ToggleKey ebiten.Key
	// Colors overrides the plate colour of a kind; a kind left out keeps
	// its default. Alpha is honoured, so a plate can stay translucent.
	Colors map[DebugKind]color.Color
}

HotspotDebug paints a translucent plate over everything on screen a player could click or look at, one colour per kind: the current scene's hotspots and exits, the characters standing in it, and whatever every visible Targeted widget offers — the inventory's items, a panel's hotspots. The scene's own targets get the element's name written across the plate, because the picture shows them unlabelled; a widget's targets get the plate alone, because the widget already prints its own labels and a second copy would only cover them. F1 toggles it at runtime; the default is off.

func (*HotspotDebug) Draw
func (h *HotspotDebug) Draw(dst *ebiten.Image, ctx *UICtx)

Draw paints the plates while Enabled: the scene's targets with their names written across them, then the widgets' targets bare.

func (*HotspotDebug) GetName
func (h *HotspotDebug) GetName() string

GetName returns the name the overlay is registered under.

func (*HotspotDebug) Layer
func (h *HotspotDebug) Layer() Layer

Layer puts the overlay on LayerDebug.

func (*HotspotDebug) Tick
func (h *HotspotDebug) Tick(ctx *UICtx)

Tick flips Enabled when ToggleKey (F1 when zero) is pressed, claiming the key.

func (*HotspotDebug) VisibleWhen
func (h *HotspotDebug) VisibleWhen() Condition

VisibleWhen returns the condition that switches the overlay on; nil means always. Enabled decides whether it paints.

type HotspotPanel

type HotspotPanel struct {
	Name string
	When Condition
	// Bounds is the region the panel claims. A click inside it is the
	// panel's whether it lands on a hotspot or not, so the scene behind
	// never receives one.
	Bounds Rectangle
	// Hotspots is asked for the panel's targets, in screen coordinates,
	// on the frame it needs them. It is a function rather than a slice
	// because what is mounted on a panel changes as the game does — a
	// slot answers for whatever is in it — and a widget that asks each
	// time needs no invalidation.
	Hotspots func(g *Game) []Hotspot
}

HotspotPanel makes a region of the HUD answer the verbs, the way a region of the picture already does. It owns nothing but a hit-test: the hotspots it hands back are dispatched through Game.InvokeVerb, so a device mounted on the panel gets look, use, talk and take — and the use-with flow, and the log lines, and the nothing-happens fallback — out of the same code a painted hotspot does.

This is what keeps a game's own HUD from re-implementing the engine. Without it, a widget with two clickable slots on it has to read the selected verb, check for a held item, consume the click and answer every combination itself, in Go, in a switch — which is a hundred lines of engine, re-decided, with the game's own writing trapped inside it. With it, the slots are Hotspot literals like any other.

The panel draws nothing. A game that wants the slots painted embeds it and declares a Draw of its own:

type tapeSlots struct {
	inkwell.HotspotPanel
}

func (slots *tapeSlots) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) { … }
func (*HotspotPanel) BlocksClickAt
func (p *HotspotPanel) BlocksClickAt(ctx *UICtx, pt Point) bool

BlocksClickAt reports whether pt falls inside Bounds: a click there that misses every hotspot is still the panel's, and never reaches the scene.

func (*HotspotPanel) DebugTargets
func (p *HotspotPanel) DebugTargets(g *Game) []DebugTarget

DebugTargets is the panel's hotspots as the debug overlay sees them: one plate per target, named as the hotspot is.

func (*HotspotPanel) Draw
func (p *HotspotPanel) Draw(dst *ebiten.Image, ctx *UICtx)

Draw does nothing; a game that wants the slots painted embeds the panel and declares its own Draw.

func (*HotspotPanel) GetName
func (p *HotspotPanel) GetName() string

GetName returns the name the panel is registered under.

func (*HotspotPanel) Layer
func (p *HotspotPanel) Layer() Layer

Layer puts the panel on LayerHUD.

func (*HotspotPanel) Tick
func (p *HotspotPanel) Tick(ctx *UICtx)

Tick sets the hover label to the hotspot under the pointer and, on a left click, consumes it and invokes the selected verb on that hotspot through Game.InvokeVerb.

func (*HotspotPanel) VisibleWhen
func (p *HotspotPanel) VisibleWhen() Condition

VisibleWhen returns the condition that switches the panel on; nil means always.

type Input

type Input = input.Input

Input is the consume-on-use mouse and keyboard state the game polls once a frame, Game.Input; see input.Input.

type Inventory

type Inventory struct {
	// contains filtered or unexported fields
}

Inventory is what the player carries: item names in the order they were picked up, and the one selected for use on something, if any.

func NewInventory
func NewInventory() *Inventory

NewInventory returns an empty Inventory with nothing selected.

func (*Inventory) Add
func (i *Inventory) Add(name string)

Add puts the named item at the end of the inventory; an item already carried is not added twice.

func (*Inventory) Has
func (i *Inventory) Has(name string) bool

Has reports whether the named item is carried.

func (*Inventory) Items
func (i *Inventory) Items() []string

Items returns a copy of the carried item names, in pickup order.

func (*Inventory) Remove
func (i *Inventory) Remove(name string)

Remove takes the named item out of the inventory, and clears the selection if it was the selected one. An item not carried is ignored.

func (*Inventory) Select
func (i *Inventory) Select(name string)

Select makes the named item the one in hand. An empty name clears the selection; an item not carried leaves it as it was.

func (*Inventory) Selected
func (i *Inventory) Selected() string

Selected returns the name of the item in hand, or "" for none.

type InventoryBar

type InventoryBar struct {
	Name     string
	When     Condition
	Origin   Point
	Slots    int
	Cols     int
	SlotSize int
	Gap      int

	PanelBG bool
}

InventoryBar shows the player's items as clickable slots. Left-click on a slot under the "look" verb prints the item's description; under any other verb, it toggles selection (the cursor "picks up" the item). Clicking the item already held uses it on itself: its OnUseSelf runs when it has one, otherwise the click puts it back. A bar with no Slots shows and claims nothing.

func (*InventoryBar) BlocksClickAt
func (b *InventoryBar) BlocksClickAt(ctx *UICtx, p Point) bool

BlocksClickAt reports whether p falls inside the rectangle the slots span, empty ones included.

func (*InventoryBar) DebugTargets
func (b *InventoryBar) DebugTargets(g *Game) []DebugTarget

DebugTargets is one target per filled slot, named after the item in it, so the debug overlay can mark the inventory without knowing its layout.

func (*InventoryBar) Draw
func (b *InventoryBar) Draw(dst *ebiten.Image, ctx *UICtx)

Draw paints the PanelBG backdrop when asked, then every slot, the selected item's slot highlighted and each filled slot showing its item's sprite, or a filled square when it has none.

func (*InventoryBar) GetName
func (b *InventoryBar) GetName() string

GetName returns the name the bar is registered under.

func (*InventoryBar) Layer
func (b *InventoryBar) Layer() Layer

Layer puts the bar on LayerHUD.

func (*InventoryBar) Tick
func (b *InventoryBar) Tick(ctx *UICtx)

Tick sets the hover label to the item under the pointer and handles a left click on a filled slot, consuming it: under the look verb the player says the item's Description; with another item held, a pairing found in either item's OnUseWith is queued; with the same item held, its OnUseSelf is queued; otherwise the item is selected, or deselected when it already was.

func (*InventoryBar) VisibleWhen
func (b *InventoryBar) VisibleWhen() Condition

VisibleWhen returns the condition that switches the bar on; nil means always.

type Item

type Item = item.Item

Item is a thing the player can pick up and carry, and use on itself, on a hotspot or on another item; see item.Item.

type ItemManager

type ItemManager = item.Manager

ItemManager is the registry of a game's Items, keyed by name; see item.Manager.

type KeyBinding

type KeyBinding = keybinding.KeyBinding

KeyBinding is a key and what it does — the whole widget: it claims Key and hands Do to the game; see keybinding.KeyBinding.

type Knowable

type Knowable = inkbase.Knowable

Knowable is an entity the player can come to know — an Item, a Character, a Speaker — and so an entry a codex screen can list; see inkbase.Knowable.

type Ladder

type Ladder struct {
	// Rungs are the lines, in the order the player earns them.
	Rungs []Text
	// Last stands once the rungs run out. Empty picks a rung at random.
	Last Text
}

Ladder is what somebody says the n-th time the player does the same fruitless thing. A game that answers a repeated failure with one line sounds like a machine; a Ladder answers the first attempt with the first rung, the second with the second, and keeps a character's patience visibly running out.

Past the last rung a Ladder picks one at random, which keeps a stock refusal from turning into a stuck record — unless it names a Last, which stands from then on. A character who has given up has given up.

func (Ladder) At
func (l Ladder) At(n int) Text

At is the line for the n-th attempt, counting the first as 1. A Ladder with no rungs says nothing at all.

type Layer

type Layer = ibwidget.Layer

Layer is a widget's place in the paint order. A widget says where it belongs rather than depending on the order it happened to be registered in, which frees a domain to register its widgets one file at a time.

const (
	// LayerScene is over the picture and under the HUD: hotspot outlines,
	// cutscene bars, and the invisible widgets that only tick.
	LayerScene Layer = ibwidget.LayerScene
	// LayerPanel is the plate the HUD is drawn on.
	LayerPanel Layer = ibwidget.LayerPanel
	// LayerHUD is everything mounted on that plate: verbs, inventory,
	// status line, top bar.
	LayerHUD Layer = ibwidget.LayerHUD
	// LayerSpeech is speech bubbles, which float over the scene.
	LayerSpeech Layer = ibwidget.LayerSpeech
	// LayerDialog is the dialog box.
	LayerDialog Layer = ibwidget.LayerDialog
	// LayerDebug is the developer's overlay: over the scene, the HUD and
	// the dialog it annotates, under the menus a developer works from.
	LayerDebug Layer = 450
	// LayerMenu is what opens on top of the game: radial verbs, menus.
	LayerMenu Layer = ibwidget.LayerMenu
	// LayerCurtain is what covers the game: end cards, fades.
	LayerCurtain Layer = ibwidget.LayerCurtain
	// LayerCursor is the pointer, and nothing else belongs above it.
	LayerCursor Layer = ibwidget.LayerCursor
)
func LayerOf
func LayerOf(w Widget) Layer

LayerOf reports the layer a widget paints on. A wrapper widget that forwards to an inner one — a visibility gate, say — should return LayerOf(inner) so that wrapping does not move the widget.

type Layered

type Layered = ibwidget.Layered

Layered is implemented by a widget that declares its own layer. Every built-in widget does. A widget that does not sits on LayerHUD.

type Letterbox

type Letterbox = letterbox.Letterbox

Letterbox is the pair of bars that crop the picture top and bottom while a cutscene plays; see letterbox.Letterbox.

type LogKind

type LogKind = inkbase.LogKind

LogKind says what a message-log entry is, so a log widget can style it; see inkbase.LogKind.

type LogMessage

type LogMessage = inkbase.LogMessage

LogMessage is one line of the message log; see inkbase.LogMessage.

type Manager

type Manager[T Named] = inkbase.Manager[T]

Manager is the registry every entity kind is kept in, keyed by name and in registration order; see inkbase.Manager.

func NewManager
func NewManager[T Named]() *Manager[T]

NewManager returns an empty Manager for entities of type T; see inkbase.NewManager.

type MemoryStore

type MemoryStore struct {
	// contains filtered or unexported fields
}

MemoryStore keeps documents in a map, for a test or a run that must not touch the disk.

func NewMemoryStore
func NewMemoryStore() *MemoryStore

NewMemoryStore returns an empty MemoryStore.

func (*MemoryStore) Read
func (m *MemoryStore) Read(key string) ([]byte, error)

Read returns a copy of the document under key, or ErrNotStored if nothing has been written there.

func (*MemoryStore) Write
func (m *MemoryStore) Write(key string, data []byte) error

Write keeps a copy of data under key, replacing what was there. It never fails.

type Menu = menu.Menu

Menu is a nested list in a window — the whole of the menu system: a debug menu, a pause menu and an in-fiction device screen are this one widget with a different Root. It draws with the active theme's dialog colours; see menu.Menu and Game.MenuColors.

type MenuItem = menu.MenuItem

MenuItem is one row of a Menu and, when it has children, the page those children make up; see menu.MenuItem.

type MouseButton

type MouseButton = inkbase.MouseButton

MouseButton names the mouse button a widget answers to, such as RadialVerbs.Trigger; see inkbase.MouseButton.

type Music

type Music = inkbase.Music

Music is a track a Scene or a Stage plays while it shows, looped when its Loop is set; see inkbase.Music.

type Named

type Named = inkbase.Named

Named is anything that can be registered in a Manager: it answers its name; see inkbase.Named.

type Panel

type Panel = panel.Panel

Panel is a generic background widget — a coloured rectangle, optionally outlined and opaque to clicks; see panel.Panel. Its colours default to the active theme's, through Game.PanelColors.

type Point

type Point = inkbase.Point

Point is a position in screen pixels; see inkbase.Point.

type Polygon

type Polygon = inkbase.Polygon

Polygon is a closed outline through its Points, and a Shape — a hotspot area or a walkbox; see inkbase.Polygon.

type Pose

type Pose = iwsprite.Pose

Pose is what a character is doing as far as its sprite is concerned; see iwsprite.Pose.

type RadialVerbs

type RadialVerbs struct {
	Name    string
	When    Condition
	Trigger MouseButton
	Radius  float64

	// AlwaysVisible turns the widget into a permanent verb-wheel fixed at
	// Center. The trigger button is ignored in this mode; left-click picks
	// the verb slice under the cursor.
	AlwaysVisible bool
	Center        Point

	// HotspotOnly (trigger mode only) makes the wheel open only when the
	// trigger lands on a registered hotspot in the current scene. Right-
	// clicks on empty space fall through to handleSceneInput (so the
	// SCUMM "reset verb to look" behaviour still works there).
	HotspotOnly bool

	// Labels overrides the rendered verb label per verb Name. Useful when
	// the regular Verb.Label is too long to fit inside the wheel — e.g.
	// {"use": "Tedd", "take": "Vedd"}. Falls back to Verb.Label.
	Labels map[string]Text
	// contains filtered or unexported fields
}

RadialVerbs is the verb-coin alternative to VerbBar: secondary-click on the scene opens a radial menu of every registered verb, click on a slice (or close to its center) picks that verb.

To use it instead of a VerbBar, register RadialVerbs and skip VerbBar. Both can coexist if you really want.

func (*RadialVerbs) BlocksClickAt
func (r *RadialVerbs) BlocksClickAt(ctx *UICtx, p Point) bool

BlocksClickAt reports whether p falls within the wheel's reach, 1.4 times Radius from its centre, while it is open or AlwaysVisible.

func (*RadialVerbs) Draw
func (r *RadialVerbs) Draw(dst *ebiten.Image, ctx *UICtx)

Draw paints the open wheel: a translucent disc, one label per verb, a halo behind the selected verb and the one under the pointer, and a dot at the centre.

func (*RadialVerbs) GetName
func (r *RadialVerbs) GetName() string

GetName returns the name the wheel is registered under.

func (*RadialVerbs) Layer
func (r *RadialVerbs) Layer() Layer

Layer puts the wheel on LayerMenu, so it takes clicks before the HUD.

func (*RadialVerbs) Tick
func (r *RadialVerbs) Tick(ctx *UICtx)

Tick runs the wheel. It sets Radius to 40 when it is zero. With AlwaysVisible, a left click on a slice selects that verb. Otherwise the Trigger button opens the wheel at the pointer, unless HotspotOnly finds no hotspot there or a visible widget's BlocksClickAt claims the point; while it is open, a left click selects the slice under the pointer, invokes it on the remembered hotspot in HotspotOnly mode, and closes the wheel, and the Trigger button closes it without picking. Every click it acts on is consumed.

func (*RadialVerbs) VisibleWhen
func (r *RadialVerbs) VisibleWhen() Condition

VisibleWhen returns the condition that switches the wheel on; nil means always.

type Rectangle

type Rectangle = inkbase.Rectangle

Rectangle is an axis-aligned box at X, Y of W by H pixels, and a Shape; see inkbase.Rectangle.

type Runner

type Runner = inkbase.Runner

Runner is one running execution of an Action, ticked once a frame until it is done or failed; see inkbase.Runner.

type Scene

type Scene struct {
	Name string
	// Title is the human-readable name a TopBar shows; empty shows Name.
	Title Text
	// Layers is the picture, in planes: every one a full-screen image at a
	// depth Z, with the cast at 0. A negative Z is behind every character —
	// the background is -1 — and a positive Z is in front of every
	// character, a foreground the player walks behind. Nothing here leaves
	// the theme's backdrop showing — a scene that is all widgets.
	Layers []SceneLayer
	// Music starts when the scene is entered and keeps playing until a
	// scene or Stage with different music is. Nil changes nothing.
	Music     *Music
	Hotspots  []Hotspot
	Exits     []Exit
	Walkboxes []Polygon
	Triggers  []Trigger
	Actors    []SceneActor
	OnEnter   Action
	OnLeave   Action

	// Derived is asked once, by Run, for exits the scene computes rather
	// than writes — a map screen's pins, one per scene that leads back to
	// it. It is handed the scene itself, so it can ask "who leads to me"
	// without naming its own variable, which its own initialiser may not.
	// What comes back is appended to Exits before Validate.
	Derived func(g *Game, self *Scene) []Exit
}

Scene is a place in the fiction: a picture, the things in it a verb can reach, the ways out of it and the people standing in it. The engine always has one showing on the world Stage.

func (Scene) GetName
func (s Scene) GetName() string

GetName returns the scene's Name, its key in the SceneManager.

func (*Scene) Leads
func (s *Scene) Leads(exits ...Exit) *Scene

Leads appends exits to a scene after it is declared, and hands the scene back. Two scenes that lead to each other cannot each hold the other's pointer in their own literal — Go rejects the initialisation cycle — so a scene file declares the scene first and its exits second:

var Alley = SceneManager.Register(&Scene{Name: "alley", …})

var _ = Alley.Leads(
	Exit{To: Street, Side: ExitLeft},
)

The second declaration depends on both scenes and neither scene depends on it, so the graph may be as tangled as the city is.

func (Scene) TypeLabel
func (s Scene) TypeLabel() string

TypeLabel returns "scene", the kind a Scene is registered as.

type SceneActor

type SceneActor struct {
	Character *Character
	At        Point
}

SceneActor is a character standing in a scene when it is entered.

type SceneLayer

type SceneLayer = inkbase.SceneLayer

SceneLayer is one plane of a scene's picture at a depth Z, the cast standing at 0; see inkbase.SceneLayer.

type SceneManager

type SceneManager = Manager[*Scene]

SceneManager is the registry of a game's Scenes, keyed by name; see Manager.

type Script

type Script = inkbase.Script

Script is a named Action registered in the ScriptManager, so a game can start it by name or hand it to RunScript, Game.OnStart or Game.OnFinale; see inkbase.Script.

type ScriptManager

type ScriptManager = Manager[*Script]

ScriptManager is the registry of a game's Scripts, keyed by name; see Manager.

type Settings

type Settings struct {
	Version     int    `json:"version"`
	Lang        string `json:"lang,omitempty"`
	Theme       string `json:"theme,omitempty"`
	WindowScale int    `json:"window_scale,omitempty"`
	// Voice is whether recorded lines are played. On until switched off.
	Voice bool           `json:"voice"`
	Extra map[string]any `json:"extra,omitempty"`
}

Settings is what a player sets once and expects to find again: the language, the theme, the window scale. Extra is the game's own — anything a game wants kept beside these without the engine having to know it — and it round-trips untouched.

type Shape

type Shape = inkbase.Shape

Shape is an area a click can land in: a Rectangle or a Polygon; see inkbase.Shape.

type Size

type Size = inkbase.Size

Size is a width and height in screen pixels, such as the size of a VerbBar button; see inkbase.Size.

type SlotInfo

type SlotInfo struct {
	Slot    int
	Exists  bool
	Scene   string
	SavedAt time.Time
}

SlotInfo describes one slot on disk, for a load or save screen to list. An empty slot answers too — a screen showing four slots wants four rows, three of which say "empty".

type Sound

type Sound = inkbase.Sound

Sound is a one-shot noise, played by PlaySound; see inkbase.Sound.

type Speaker

type Speaker struct {
	Name string
	// Label is what a log or a dialog box calls the voice; empty falls back
	// to Name.
	Label    Text
	Delivery Delivery
	Color    color.Color
	// Lore is what the player's record says about the voice once it is
	// known — see Character.Lore.
	Lore Text
}

Speaker is a voice with no body: something that talks but never stands in a scene — a tape in a slot, a room-scale system, a narrator. Characters speak too, through the same fields, so anything that takes a Talker takes either.

func (Speaker) DisplayName
func (s Speaker) DisplayName() Text

DisplayName is the Label if there is one, the Name otherwise.

func (Speaker) GetName
func (s Speaker) GetName() string

GetName returns the speaker's Name, its key in the SpeakerManager.

func (*Speaker) Reply
func (s *Speaker) Reply(answer func(state *State) Text) Action

Reply is the package-level Reply with this speaker filled in: the line is worked out from the State when the action runs.

func (*Speaker) Say
func (s *Speaker) Say(text Text) Action

Say is the package-level Say with this speaker filled in, so a script reads who, then what: tape.Say(line).

func (*Speaker) TalkerName
func (s *Speaker) TalkerName() string

TalkerName returns the speaker's Name, which is how a line names it.

func (Speaker) TypeLabel
func (s Speaker) TypeLabel() string

TypeLabel returns "speaker", the kind a Speaker is registered as.

type SpeakerManager

type SpeakerManager = Manager[*Speaker]

SpeakerManager is the registry of a game's Speakers, keyed by name; see Manager.

type SpeakerStyle

type SpeakerStyle = chatlog.SpeakerStyle

SpeakerStyle says how a ChatLog names who is talking; see chatlog.SpeakerStyle.

type SpeechBubble

type SpeechBubble struct {
	Name      string
	When      Condition
	MaxWidth  int
	Padding   int
	OffsetY   int
	FallbackY int
}

SpeechBubble renders the line set by the Say action above the speaking character (or at FallbackY if the speaker has no on-screen position).

func (*SpeechBubble) Draw
func (s *SpeechBubble) Draw(dst *ebiten.Image, ctx *UICtx)

Draw paints the current line, wrapped to MaxWidth (200 when zero) on a backdrop with Padding (3 when zero), centred OffsetY pixels above the speaker's head and kept on screen, in the speaker's speech colour. A speaker not in the scene gets the bubble at the screen's centre with its bottom at FallbackY (14 when zero). It draws nothing when no line is being said.

func (*SpeechBubble) GetName
func (s *SpeechBubble) GetName() string

GetName returns the name the bubble is registered under.

func (*SpeechBubble) Layer
func (s *SpeechBubble) Layer() Layer

Layer puts the bubble on LayerSpeech, over the scene and the HUD.

func (*SpeechBubble) Tick
func (s *SpeechBubble) Tick(ctx *UICtx)

Tick does nothing; the game sets and clears the line, the bubble only shows it.

func (*SpeechBubble) VisibleWhen
func (s *SpeechBubble) VisibleWhen() Condition

VisibleWhen returns the condition that switches the bubble on; nil means always.

type Sprite

type Sprite = iwsprite.Sprite

Sprite is a picture or a sequence of them cut from one sheet; see iwsprite.Sprite.

type Stage

type Stage = ibstage.Stage

Stage is one full-screen thing the game can be showing — a main menu, a settings screen, a splash card, or the world itself; see ibstage.Stage.

That is the difference between a Stage and a Scene. A Scene is a place in the fiction, and the engine always has one; walking from a room to an alley is a Scene change inside the world. A Stage is what owns the frame. Off the world Stage the scene is not drawn, characters do not walk, triggers do not fire and hotspots answer nothing — which is what a game showing its main menu wants. Widgets need no new field to follow this: OnStage is a Condition like any other, so a HUD hides itself off the world the same way it hides itself for a cutscene.

type StageManager

type StageManager = Manager[*Stage]

StageManager is the registry of a game's Stages, keyed by name; see Manager.

type State

type State = inkbase.State

State is what the player has done so far: flags, variables, scenes visited, dialog nodes talked through and entities known. It is what a save slot stores; see inkbase.State.

func NewState
func NewState() *State

NewState returns an empty State; see inkbase.NewState.

type Status

type Status = inkbase.Status

Status is what one tick of a Runner says about it; see inkbase.Status.

type StatusLine

type StatusLine = statusline.StatusLine

StatusLine renders a single line: either the active flash message (set by Game.FlashLine) or the hover hint (verb + target under the cursor), in the active theme's FlashText and StatusText colours; see statusline.StatusLine.

type Store

type Store interface {
	Read(key string) ([]byte, error)
	Write(key string, data []byte) error
}

Store is where a game keeps what has to outlive the process: its settings and its save slots. A key is a short word — "settings", "slots" — and a value is the bytes of one document; the Store decides what that means on the platform it runs on. Natively a key is a JSON file in the user's configuration directory, under the game's App name; in a browser it is one localStorage entry. Read returns ErrNotStored for a key nothing has written yet, so a first run is not an error.

type Talker

type Talker = inkbase.Talker

Talker is whoever a line can be given to: a *Character or a *Speaker; see inkbase.Talker.

type Targeted

type Targeted interface {
	DebugTargets(g *Game) []DebugTarget
}

Targeted is implemented by a widget that has clickable things of its own to show under the debug overlay — the inventory's slots, a panel's hotspots. The engine's own widgets implement it, and a game widget that embeds one of them inherits it, so the overlay finds a game's slots without being told about them.

type Text

type Text = inkbase.Text

Text is a string in as many languages as somebody has written it in; it resolves against Lang and Fallback when read. See inkbase.Text.

type TextCard

type TextCard = textcard.TextCard

TextCard is a block of lines centred on the screen — a credits roll that does not roll, a title card, an apology — in the active theme's end card colours; see textcard.TextCard.

type Theme

type Theme = theme.Theme

Theme is a named set of colours every built-in widget reads; see theme.Theme.

type ThemeManager

type ThemeManager = theme.Manager

ThemeManager is the registry of a game's Themes, keyed by name; the active one is chosen with Game.UseTheme. See theme.Manager.

type TopBar

type TopBar = topbar.TopBar

TopBar is a thin strip across the top of the screen: the scene title on the left, a score in the centre and a time string on the right, in the active theme's top bar colours; see topbar.TopBar.

type Transition

type Transition = iwtransition.Transition

Transition is how one Stage gives way to the next, such as Fade; nil is a cut. See iwtransition.Transition.

func Fade
func Fade(seconds float64) Transition

Fade is a Transition through black: the old picture darkens over the given seconds, the swap lands on the black frame, and the new picture brightens over the same span. See iwtransition.Fade.

func FadeTo
func FadeTo(seconds float64, to color.Color) Transition

FadeTo is Fade through the colour to instead of black; see iwtransition.FadeTo.

type Translation

type Translation = inkbase.Translation

Translation is one language's wording of one string, with its Voice recording if there is one; see inkbase.Translation.

type Trigger

type Trigger struct {
	Name string
	When Condition
	Do   Action
	Once bool
}

Trigger fires Do when its When condition first becomes true after the trigger arms (on scene enter). Rising-edge semantics: the Do action is queued only on a false→true transition, so a condition that becomes true and stays true fires exactly once per arming. Once=true makes the trigger fire at most once per arming regardless of further toggles.

type UICtx

type UICtx = ibwidget.UICtx

UICtx is the per-tick context handed to widgets. Game is opaque; widgets type-assert it to their own host interface. DT is seconds since the previous frame.

type Unlocker

type Unlocker interface {
	Unlock(achievement *Achievement)
}

Unlocker is an Extension that can record an Achievement earned. It is asked for by type assertion, the way an action asks its host, so an extension with no notion of achievements implements nothing extra.

type Verb

type Verb = verb.Verb

Verb is an action word the player picks before clicking — Look, Use, Talk, Take; see verb.Verb.

type VerbBar

type VerbBar struct {
	Name   string
	When   Condition
	Origin Point
	Cols   int
	Button Size
	Gap    Point

	// PanelBG paints a Theme.PanelBG backdrop behind the buttons.
	PanelBG bool
}

VerbBar is the SCUMM-style permanent verb panel. It reads the VerbManager every frame so newly-registered verbs show up automatically.

func (*VerbBar) BlocksClickAt
func (v *VerbBar) BlocksClickAt(ctx *UICtx, p Point) bool

BlocksClickAt reports whether p falls inside the bar: the grid of its buttons, as many rows as the registered verbs fill.

func (*VerbBar) Draw
func (v *VerbBar) Draw(dst *ebiten.Image, ctx *UICtx)

Draw paints the PanelBG backdrop when asked, then one button per registered verb in a grid of Cols columns (2 when zero), the selected verb's button highlighted.

func (*VerbBar) GetName
func (v *VerbBar) GetName() string

GetName returns the name the bar is registered under.

func (*VerbBar) Layer
func (v *VerbBar) Layer() Layer

Layer puts the bar on LayerHUD.

func (*VerbBar) Tick
func (v *VerbBar) Tick(ctx *UICtx)

Tick selects the verb whose button is left-clicked and consumes the click.

func (*VerbBar) VisibleWhen
func (v *VerbBar) VisibleWhen() Condition

VisibleWhen returns the condition that switches the bar on; nil means always.

type VerbManager

type VerbManager = verb.Manager

VerbManager is the registry of a game's Verbs, in the order the verb widgets show them; see verb.Manager.

type Voice

type Voice = inkbase.Voice

Voice is the recording of one line in one language, carried by a Translation; see inkbase.Voice.

type Widget

type Widget = ibwidget.Widget

Widget is the minimal surface a HUD element must implement to participate in the per-frame loop. The library ships several built-in widgets (VerbBar, InventoryBar, DialogBox, etc.) and the domain can register arbitrary new ones — chat panels, minimaps, hotbars — as long as they satisfy this interface.

Lifecycle:

Registration order only decides ties inside one layer — see Layer.

type WidgetManager

type WidgetManager = Manager[Widget]

WidgetManager registers Widget instances. Same shape as every other manager.

Directories

achievement Package achievement holds achievements: what the player can be recognised for, declared as content.
act Package act holds the adventure actions: the ones that need more of the game than the inkbase.State in the Ctx — speech, the inventory, dialogs, scene changes, walking, poses, the voice setting and the end card.
cond Package cond holds the adventure-specific conditions: the ones that need more of the game than the inkbase.State in the Ctx.
dialog Package dialog holds dialog trees: the conversations a game opens with RunDialog and the DialogBox widget shows.
inkbase Package inkbase is the generic foundation of inkwell: the registries, text in several languages, world state, the action and condition framework and the data types any Ebitengine game can build on without pulling in adventure-game machinery — no scenes, characters, dialogs or verbs.
item Package item holds the Item type and its Manager: the things the player can carry.
sprite Package sprite holds the picture types characters and items are drawn with.
steam Package steam is inkwell's Steamworks layer: one inkwell.Extension that initialises the Steam API before the window opens, pumps its callbacks every frame, reports achievements, keeps chosen documents in Steam Cloud and offers the client's language as the game's SystemLang.
theme Package theme holds the Theme type, its Manager and the preset themes.
verb Package verb holds the Verb type, its Manager and the SCUMM verb set.