diff --git a/CLAUDE.md b/CLAUDE.md index 4b45870..77e7b18 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,25 +49,25 @@ 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`, `dialog`, `script`, `world`, `ui`, `theme`, `names`, `boot`, - `content`. + `character`, `dialog`, `script`, `world`, `ui`, `theme`, `names`. - `[category].manager.go` is the file that ties a category together — its - manager, its entity type, whatever the category shares. + 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.manager.go wiring; New(Opts) builds the game +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 manager and the colour helpers +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 and the action pump inc/ui.manager.go HUD layout and widget registration inc/ui.*.go custom widgets, coloured text -inc/content.manager.go composition root; calls every register… inc/background.*.go one image asset per scene inc/character.*.go the cast, tapes included inc/item.*.go inventory @@ -84,18 +84,30 @@ 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. -The entity types are aliases of engine structs, so they already carry -`GetName()` and satisfy `inkwell.Named`; the manager needs nothing else: +All seven 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]() + ThemeManager = inkwell.NewManager[Theme]() +) +``` + +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 - -var CharacterManager = inkwell.NewManager[Character]() ``` -That is the whole of a category's manager file — an alias and one line. There -are seven managers: `BackgroundManager`, `CharacterManager`, `DialogManager`, -`ItemManager`, `ScriptManager`, `SceneManager`, `ThemeManager`. +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 @@ -126,8 +138,9 @@ func init() { ``` Adding an entity is adding a file. Deleting one is deleting a file. Package-level -variables are initialised before any `init()` runs, so the managers exist by the -time the first file registers into one. +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, @@ -135,23 +148,30 @@ which is simply the order the files sit in. ### Handing a category to the engine -Once the game exists, `Each` walks a manager and gives every entity to the -engine's own manager. `registerContent` is the whole of it: +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 -func registerContent() { - BackgroundManager.Each(World.G.AssetManager.Register) - CharacterManager.Each(World.G.CharacterManager.Register) - ItemManager.Each(World.G.ItemManager.Register) - DialogManager.Each(World.G.DialogueManager.Register) - ScriptManager.Each(World.G.ScriptManager.Register) - registerScene() -} +g.AssetManager = BackgroundManager +g.CharacterManager = CharacterManager +g.DialogueManager = DialogManager +g.ItemManager = ItemManager +g.SceneManager = SceneManager +g.ScriptManager = ScriptManager +ThemeManager.Each(g.ThemeManager.Register) ``` -`registerScene` is the one that needs its own function: it fills in the defaults -a scene may leave out (Paul's starting position, the floor walkbox), and it -derives the selector's pins by reading the exit graph backwards. +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. + +Two consequences of not copying. `prepareScene` has to run **before** the +hand-off, because there is no longer a copy pass to fill in the defaults a +scene leaves out — it writes them back into `SceneManager` with `Set`, and +derives the selector's pins by reading the exit graph backwards. And 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. ## The world @@ -171,7 +191,7 @@ One package means one namespace, so an entity's constructor carries its category as a prefix: ```go -sceneFloor sceneDefaults registerScene fillSelectorPins +sceneFloor sceneDefaults prepareScene fillSelectorPins ``` Entities themselves need no name at all — they are anonymous literals inside @@ -183,8 +203,8 @@ constants in `names.manager.go`, the colour tokens, and the action constructors (`TapeSay`, `Paused`, `SetMode`, `EnterScene`, `Back`, `Fn`). Everything else is machinery and stays unexported: the HUD widgets -(`tapeSlots`, `letterbox`, `hudFrame`, …), the `register…` functions, the -runners, `sceneDefaults`, `fillSelectorPins`. +(`tapeSlots`, `letterbox`, `hudFrame`, …), `registerUI`, the runners, +`prepareScene`, `sceneDefaults`, `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 diff --git a/README.md b/README.md index cc72224..35afaf8 100644 --- a/README.md +++ b/README.md @@ -153,16 +153,17 @@ licence. The game is one flat package, `inc`. There are no subdirectories: a file's name says where it belongs, in the form `[category].[name].go`. The category is always singular, and `[category].manager.go` is the file that ties that category -together — its manager, its shared types, its hand-off to the engine. +together — its entity type and whatever else the category shares. `boot.go` is +the one file with no category: it declares every manager and builds the game. ``` main.go flags + inkwell.Run +inc/boot.go the managers, and New(Opts) inc/names.manager.go entity names and world-state keys inc/theme.*.go realworld-93 + nokia-punk inc/world.*.go unsaved runtime state, custom actions, action pump inc/ui.*.go HUD: layout, custom widgets, coloured text inc/..go one file per registered entity, by kind -inc/boot.manager.go wiring ``` Nothing is enforced by the compiler any more, so the layering is a rule the @@ -194,13 +195,12 @@ Every category owns a manager, and they are all the engine's own `inkwell.Manager[T]` — the same registry the `*Game` hangs its content off, kept in registration order and addressed by name. There is no second registry type here: the entity types are aliases of engine structs, so they already satisfy -`inkwell.Named` and need no help reading their own name. A category's manager -file is an alias and one line: +`inkwell.Named` and need no help reading their own name. The seven of them are +declared as one block in `boot.go`, and a category's manager file is left +holding the alias: ```go type Character = inkwell.Character - -var CharacterManager = inkwell.NewManager[Character]() ``` An entity file is a literal that hands itself over in an `init()`: @@ -223,9 +223,18 @@ step with the files by hand, and the deck is a thing to look at, not a thing to play through. The game's own catalogue is readable without going through the engine: -`SceneManager.Get("alley")` answers before a single scene has been handed over. -`registerContent` is where the hand-off happens, one `Each` call per category — -the game's manager iterated straight into the engine manager's `Register`. +`SceneManager.Get("alley")` answers long before there is a `*Game` to ask. And +when the game is built, nothing is copied into it — `New` assigns our managers +onto the `*Game` in place of the empty ones `NewGame` made, so engine and game +share one registry per category instead of keeping two of them in step. Themes +are the exception: `NewGame` seeds its theme manager with four presets, so ours +are added to that set rather than replacing it. + +The price of not copying is that the defaults a scene may leave out — Paul's +starting position, the floor walkbox — have to be written back into +`SceneManager` before the hand-off rather than filled in on the way past. +`prepareScene` does that with `Set`, and derives the selector's pins in the same +pass by reading the exit graph backwards. There is one world, and it is a package-level singleton: `World`. Nothing takes a `*world` parameter and no widget holds a back-reference, which is what lets a diff --git a/go.mod b/go.mod index df806e4..72c9829 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module git.teletypegames.org/games/realworld go 1.26.3 require ( - git.teletypegames.org/engines/inkwell v0.1.1-0.20260830075028-08f15a7e3d11 + git.teletypegames.org/engines/inkwell v0.1.1-0.20260830083613-39af65f3c98f github.com/hajimehoshi/ebiten/v2 v2.9.9 ) diff --git a/go.sum b/go.sum index 7591bee..53c423c 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -git.teletypegames.org/engines/inkwell v0.1.1-0.20260830075028-08f15a7e3d11 h1:DPN74Xy0xcyelfh5xLuv6XBj6wrdzKVME1a1HyACsj4= -git.teletypegames.org/engines/inkwell v0.1.1-0.20260830075028-08f15a7e3d11/go.mod h1:/v6QtismTE8+e7kobbVR5B/CpTSrd4j2DYwJ8DvINhs= +git.teletypegames.org/engines/inkwell v0.1.1-0.20260830083613-39af65f3c98f h1:Qw/sUeJeCBEFvdiqwGUn919hsmoTILJyKSPlkKvNgh0= +git.teletypegames.org/engines/inkwell v0.1.1-0.20260830083613-39af65f3c98f/go.mod h1:/v6QtismTE8+e7kobbVR5B/CpTSrd4j2DYwJ8DvINhs= github.com/ebitengine/gomobile v0.0.0-20250923094054-ea854a63cce1 h1:+kz5iTT3L7uU+VhlMfTb8hHcxLO3TlaELlX8wa4XjA0= github.com/ebitengine/gomobile v0.0.0-20250923094054-ea854a63cce1/go.mod h1:lKJoeixeJwnFmYsBny4vvCJGVFc3aYDalhuDsfZzWHI= github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj1yReDqE= diff --git a/inc/background.manager.go b/inc/background.manager.go index e870845..0750b98 100644 --- a/inc/background.manager.go +++ b/inc/background.manager.go @@ -5,5 +5,3 @@ import ( ) type Background = inkwell.Asset - -var BackgroundManager = inkwell.NewManager[Background]() diff --git a/inc/boot.manager.go b/inc/boot.go similarity index 64% rename from inc/boot.manager.go rename to inc/boot.go index 6b623e2..3355a1e 100644 --- a/inc/boot.manager.go +++ b/inc/boot.go @@ -4,6 +4,16 @@ import ( inkwell "git.teletypegames.org/engines/inkwell" ) +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]() + ThemeManager = inkwell.NewManager[Theme]() +) + type Opts struct { Scene string Finale bool @@ -15,6 +25,8 @@ func New(o Opts) *inkwell.Game { start = SceneAlley } + prepareScene() + g := inkwell.NewGame("Real World", ScreenW, ScreenH) g.MaxLogLines = 64 g.SceneRect = inkwell.Rect(0, TopBarH, ScreenW, HUDTop-TopBarH) @@ -25,10 +37,15 @@ func New(o Opts) *inkwell.Game { return inkwell.Say(Paul, "It's a way out, not a thing.") } - World.attach(g) - registerTheme() - registerContent() + g.AssetManager = BackgroundManager + g.CharacterManager = CharacterManager + g.DialogueManager = DialogManager + g.ItemManager = ItemManager + g.SceneManager = SceneManager + g.ScriptManager = ScriptManager + ThemeManager.Each(g.ThemeManager.Register) + World.attach(g) g.UseTheme(RealWorld) registerUI() diff --git a/inc/character.manager.go b/inc/character.manager.go index 0e519b8..5130b94 100644 --- a/inc/character.manager.go +++ b/inc/character.manager.go @@ -5,5 +5,3 @@ import ( ) type Character = inkwell.Character - -var CharacterManager = inkwell.NewManager[Character]() diff --git a/inc/content.manager.go b/inc/content.manager.go deleted file mode 100644 index bfddfb7..0000000 --- a/inc/content.manager.go +++ /dev/null @@ -1,10 +0,0 @@ -package inc - -func registerContent() { - BackgroundManager.Each(World.G.AssetManager.Register) - CharacterManager.Each(World.G.CharacterManager.Register) - ItemManager.Each(World.G.ItemManager.Register) - DialogManager.Each(World.G.DialogueManager.Register) - ScriptManager.Each(World.G.ScriptManager.Register) - registerScene() -} diff --git a/inc/dialog.manager.go b/inc/dialog.manager.go index 664addb..a7038f8 100644 --- a/inc/dialog.manager.go +++ b/inc/dialog.manager.go @@ -5,5 +5,3 @@ import ( ) type Dialog = inkwell.Dialogue - -var DialogManager = inkwell.NewManager[Dialog]() diff --git a/inc/item.manager.go b/inc/item.manager.go index 50bf3b4..8fa2c89 100644 --- a/inc/item.manager.go +++ b/inc/item.manager.go @@ -5,5 +5,3 @@ import ( ) type Item = inkwell.Item - -var ItemManager = inkwell.NewManager[Item]() diff --git a/inc/scene.manager.go b/inc/scene.manager.go index 8ecaa53..2bf0983 100644 --- a/inc/scene.manager.go +++ b/inc/scene.manager.go @@ -6,13 +6,11 @@ import ( type Scene = inkwell.Scene -var SceneManager = inkwell.NewManager[Scene]() - -func registerScene() { +func prepareScene() { fillSelectorPins() - SceneManager.Each(func(entity Scene) { - World.G.SceneManager.Register(sceneDefaults(entity)) - }) + for _, entity := range SceneManager.All() { + SceneManager.Set(sceneDefaults(entity)) + } } var sceneFloor = []inkwell.Polygon{ diff --git a/inc/script.manager.go b/inc/script.manager.go index e353e1d..2fc6af8 100644 --- a/inc/script.manager.go +++ b/inc/script.manager.go @@ -5,5 +5,3 @@ import ( ) type Script = inkwell.Script - -var ScriptManager = inkwell.NewManager[Script]() diff --git a/inc/theme.manager.go b/inc/theme.manager.go index f0d023c..9958316 100644 --- a/inc/theme.manager.go +++ b/inc/theme.manager.go @@ -8,8 +8,6 @@ import ( type Theme = inkwell.Theme -var ThemeManager = inkwell.NewManager[Theme]() - const ( RealWorld = "realworld-93" NokiaPunk = "nokia-punk" @@ -32,7 +30,3 @@ func RGBA(hex uint32, a uint8) color.Color { A: a, } } - -func registerTheme() { - ThemeManager.Each(World.G.ThemeManager.Register) -}