# Real World A point & click adventure built on the inkwell engine (Ebitengine). ## Conventions ### No comments Go source files carry no comments. Not doc comments, not inline comments, not section headers. If a piece of code needs explaining, rename it or restructure it until it does not, and put the reasoning in `README.md` instead. The single exception is compiler directives (`//go:embed`, `//go:build`), which are instructions to the toolchain rather than prose. ### English only Everything written in the repository is in English: identifiers, string literals, commit messages, `README.md`, this file. The design wiki is in Hungarian; when a concept crosses over, translate it and keep the mapping one-to-one. ### No tests Do not write tests and do not add test files. Correctness is checked by running the game. ### Struct literals are always multi-line Every field of a struct instance goes on its own line, with a trailing comma — never on the same line as the brace, never two fields on one line. ```go return inkwell.Asset{ Name: BgStreet, Path: "assets/bg/street.png", Kind: inkwell.AssetImage, } ``` Not `inkwell.Asset{Name: BgStreet, Path: "...", Kind: inkwell.AssetImage}`. This holds for nested literals too, including small ones such as `inkwell.Point`. Slice and map literals are not covered by the rule. ## Layout All game code lives in one flat package, `inc`. There are no subdirectories: a file's name carries the structure, in the form `[category].[name].go`. - The category is always **singular**: `item`, `scene`, `background`, `character`, `tape`, `dialog`, `script`, `world`, `widget`, `theme`, `names`. - `[category].manager.go` is the file that ties a category together — its entity type and whatever else the category shares. - Every other file in a category holds exactly one entity: one scene, one background, one item. The file registers it itself, in an `init()`. - `boot.go` is the exception that has no category: it declares every manager and builds the game. ``` main.go flags + inkwell.Run inc/boot.go the managers; New(Opts) builds the game inc/names.manager.go entity names and world-state keys inc/theme.manager.go the Theme alias and the colour helpers inc/theme.realworld.go realworld-93 inc/theme.nokia_punk.go nokia-punk inc/world.manager.go unsaved runtime state inc/world.action.go custom actions inc/widget.manager.go the Widget alias, HUD layout, the shared conditions inc/widget.*.go one widget per file, registering itself inc/background.*.go one image asset per scene inc/character.*.go the cast, tapes included inc/tape.manager.go the Tape entity and the lookups over it inc/tape.*.go one tape per file inc/item.*.go inventory inc/dialog.*.go dialogue trees inc/script.*.go named action sequences inc/scene.manager.go the Scene alias, the defaults every scene gets inc/scene.selector.go the map screen: its pins are derived from the graph inc/scene.*.go one file per scene ``` ## Managers Every category that owns a collection of entities has a manager, and they are all the engine's own `inkwell.Manager[T]` — the same registry type the `*Game` hangs its content off. The game defines no registry of its own. All nine are declared together, in `boot.go`, so the list of what the game holds is one block rather than a line hidden in each category file: ```go var ( BackgroundManager = inkwell.NewManager[Background]() CharacterManager = inkwell.NewManager[Character]() DialogManager = inkwell.NewManager[Dialog]() ItemManager = inkwell.NewManager[Item]() SceneManager = inkwell.NewManager[Scene]() ScriptManager = inkwell.NewManager[Script]() TapeManager = inkwell.NewManager[Tape]() ThemeManager = inkwell.NewManager[Theme]() WidgetManager = inkwell.NewManager[Widget]() ) ``` The entity types stay in their own category files, one alias each, and that is all a manager file holds now: ```go type Character = inkwell.Character ``` Aliases of engine structs already carry `GetName()` and satisfy `inkwell.Named`, which is the whole of what a manager asks of them. The methods the game uses are `Register`, `Set`, `Get`, `All` and `Each`. `Register` panics on a duplicate name — a second registration is a construction-time bug, not an update — so rewriting an entity that is already in the registry goes through `Set`, which keeps its position in the order. `All` and `Each` both hand back the entities in registration order. Nearly every entity type is an alias, `Scene` included. It was once a struct of our own, because inkwell's `Scene` could not carry exits; that gap was closed in the engine, so there is nothing left for a second type to hold. `Tape` is the exception, and it is a real one: a tape is a cassette that speaks, a thing inkwell has no notion of. It names the character whose voice it is, the item that carries it and the dialogue it plays, and `tape.manager.go` holds the four lookups over the registry — `IsTape`, `TapeOf`, `IsTapeItem`, `TapeDialogue`. What a tape *sounds* like is not in it: the display name and the log-only voice are `Character.Label` and `Character.Voice`, because those are facts about a speaker, not about a cassette. ### Entities register themselves An entity file is a literal and nothing else. No constructor function, no list somewhere else to keep in step — the file hands itself to its manager in an `init()`: ```go package inc func init() { BackgroundManager.Register(Background{ Name: BgServerFarm, Path: "assets/bg/server_farm.png", Kind: inkwell.AssetImage, }) } ``` Adding an entity is adding a file. Deleting one is deleting a file. Package-level variables are initialised before any `init()` runs, whatever file each sits in, so the managers in `boot.go` exist by the time the first entity file registers into one. `init()` order is file-name order, so **registration order is alphabetical**. Nothing may depend on it — including the order the arrow keys walk the scenes, which is simply the order the files sit in. ### Widgets name their layer instead of their order A widget is a file like any other entity — `widget..go`, one widget, registering itself in an `init()` — and that includes the engine's own widgets, which the game registers as literals the same way it registers a background: ```go func init() { WidgetManager.Register(&inkwell.Cursor{ Name: "cursor", }) } ``` What a widget cannot take from alphabetical registration is its place in the stack: the frame has to be drawn under the slots that sit on it, the cursor over everything, and the use-with guard has to see a click after the HUD has had it. So a widget declares a layer rather than inheriting one from the order it was registered in — `inkwell.LayerScene`, `LayerPanel`, `LayerHUD`, `LayerSpeech`, `LayerDialog`, `LayerMenu`, `LayerCurtain`, `LayerCursor`. The engine draws from the bottom layer up and ticks from the top layer down, and registration order only decides ties inside one layer: ```go func (h *hudFrame) Layer() inkwell.Layer { return inkwell.LayerPanel } ``` A widget that says nothing sits on `LayerHUD`; ours all say it, because the layer is the one thing about a widget the file cannot show. The other thing a widget declares is **when it is there at all**. The HUD is hidden for a cutscene, and that is a condition over game state, not a wrapper and not an `if` at the top of every `Draw`: ```go WidgetManager.Register(&inkwell.StatusLine{ Name: "status", When: whenPlaying, Y: statusY, }) ``` `whenPlaying`, `whenCutscene` and `whenUnpaused` are in `widget.manager.go`, and they read `VarMode` and `VarNote` out of the engine's own `State`. A widget of ours says the same thing with a method — `VisibleWhen() inkwell.Condition` — because it has no literal to put a field in. A widget that is switched off neither ticks nor draws nor blocks a click, so nothing else has to ask. ### Handing a category to the engine Nothing is copied. `inkwell.NewGame` builds its own empty managers, and `New` hands ours over in their place — the types are identical, so the engine and the game end up sharing one registry per category rather than two in step: ```go g.AssetManager = BackgroundManager g.CharacterManager = CharacterManager g.DialogueManager = DialogManager g.ItemManager = ItemManager g.SceneManager = SceneManager g.ScriptManager = ScriptManager g.WidgetManager = WidgetManager ThemeManager.Each(g.ThemeManager.Register) ``` Themes are the odd one out and stay a copy: `NewGame` puts four preset themes into its `ThemeManager`, and replacing it would throw them away. Ours are added to that set instead. One consequence of not copying: a manager swapped in this way must be in place before `inkwell.Run`, which is where the engine wires up the parts that hold a registry directly. `fillSelectorPins` runs before the hand-off for the same reason — it writes the derived pins back into `SceneManager` with `Set`. The defaults a scene leaves out are no longer written into it at all. `g.Player` names the character a scene with no actors of its own gets, placed at his `Start`; `g.Walkboxes` is the floor a scene walks on when it declares none. Both are fields on the `*Game`, so a scene file that says nothing about either is saying "the usual", and the selector opts out by declaring both empty. ### What runs at boot `New` names two scripts and nothing else — the opening a player sees is content like every other script, in `script.opening.go`, not a slice built in the wiring: ```go g.StartAt(start) g.OnStart(ScriptOpening) if o.Finale { g.OnFinale(ScriptFinale) } ``` `OnFinale` is the engine's hook for a closing script, queued straight after the opening. Here it is what `-finale` uses to drop into the ending, which is why it is set from `Opts` rather than always. The rest of `New` is fields on the `*Game`, and every one of them replaces something this package used to carry itself: ```go g.WindowScale = 2 g.Player = Paul g.Walkboxes = sceneFloor g.UseWithFail = useWithFail ``` `UseWithFail` is the response to a use-with pair nobody authored — the engine asks for it the same way it asks for `ExitLook` and `ExitTake`, and the lines live with the rest of the writing, in `script.usewith_fail.go`. ## The world There is exactly one game, so there is exactly one world: `World`, a package-level singleton in `world.manager.go`. Nothing takes a `*world` parameter and no widget holds a back-reference — `World.Do(…)`, `World.Mode()`, `World.Slot2()` are reachable from anywhere in the package. `New` calls `World.attach(g)`, which binds the engine and resets the runtime state, so building the game twice is clean. `World` holds as little as it can get away with. The mode and the top bar's note are `State` vars (`VarMode`, `VarNote`), because state the engine can see is state a `Condition` can read — that is what makes a widget's `When` possible and what puts "— paused" in the top bar without anyone pushing it there. `World.Do` hands its action to `g.Do`, the engine's queue, so the game runs one action at a time without a pump of its own. What is left in the struct is the two things the engine has no notion of: the tape waiting to speak, and the cassette on its way into slot 2. This is what lets an entity file be a literal: the tape-insert script closes over `World`, not over a parameter it would have had to be handed. ## Naming One package means one namespace, so an entity's constructor carries its category as a prefix: ```go sceneFloor fillSelectorPins tapeSlots useWithFail ``` Entities themselves need no name at all — they are anonymous literals inside their file's `init()`, and the file name says which one it is. Exported names are the authoring vocabulary — what a content file spells out: `New` and `Opts`, `World`, the managers and the entity types they hold, the constants in `names.manager.go`, the colour tokens, the tape lookups (`IsTape`, `TapeOf`, `IsTapeItem`, `TapeDialogue`) and the action constructors (`Paused`, `SetMode`, `UseTheme`, `TapeOffer`, `Fn`). A tape's line is `inkwell.Say` like anyone else's — the tape voice is on the character, not on a second spelling of Say. Everything else is machinery and stays unexported: the HUD widgets (`tapeSlots`, `letterbox`, `hudFrame`, …), the conditions (`whenPlaying`, `whenCutscene`, `whenUnpaused`), the runners, `useWithFail`, `fillSelectorPins`. The word is **scene**, the engine's own. The wiki and the concept-art deck count *screens*, and this code used to as well, but everything a screen had that a scene did not now lives in inkwell. "Screen" survives only where it means the display: `ScreenW`, `ScreenH`. ## Layering Nothing is enforced by the compiler any more, so the layering is a rule kept by hand: ``` names ← theme ← world ← widget ↑ ↑ content ───┴── boot ← main ``` `world` knows nothing about the HUD or the content. Both build on it, never the other way round. ## Adding a scene 1. Constants in `inc/names.manager.go`: `Scene`, `Bg` 2. `inc/scene..go` — an `init()` registering a `Scene` 3. `inc/background..go` — an `init()` registering a `Background` 4. A 640×380 PNG in `assets/bg/` Nothing else moves. There is no list to update. ## The engine next door inkwell is checked out beside this repository, and so is the wiki that documents it. Paths are relative to the game's root: ``` ../../engines/inkwell the engine source ../../services/wiki-pages/pages/engines/inkwell/default.en.md the manual, English ../../services/wiki-pages/pages/engines/inkwell/default.hu.md the manual, Hungarian ``` The engine's module path is `git.teletypegames.org/engines/inkwell` and `go.mod` pins a pseudo-version of it, so a local edit is invisible here until it is pushed: ``` git -C ../../engines/inkwell commit … && git -C ../../engines/inkwell push go get git.teletypegames.org/engines/inkwell@master ``` A `replace` directive is fine while trying something out, but it never survives into a commit — drop it and bump the pin instead. Three things move together when the engine changes: the code, `README.md` in the inkwell checkout, and **both** wiki pages. The two pages are a translation pair, section for section — an edit to one is an edit to the other. The no-comment rule stops at the module boundary: inkwell's own source is commented, and edits there follow its style, not this one's. ## Commands ``` make build native binary into bin/ make wasm js/wasm build into dist/ make watch rebuild on change go run . -scene start on a given scene go run . -finale start with the finale ``` Art is embedded into the binary (`//go:embed assets` in `main.go`), because js/wasm has no OS filesystem and `make binaries` ships the executable alone. The screen is 640×380 because the engine stretches a background over the whole window: any other size squashes the paintings.