From 3890edc9467bc0d92001f55b93964ca099342923 Mon Sep 17 00:00:00 2001 From: Zsolt Tasnadi Date: Sun, 30 Aug 2026 22:27:51 +0200 Subject: [PATCH] widget manager refact --- CLAUDE.md | 52 ++++- README.md | 31 ++- go.mod | 2 +- go.sum | 4 + inc/boot.go | 3 +- inc/ui.manager.go | 199 ------------------ inc/widget.action_pump.go | 21 ++ inc/widget.cursor.go | 11 + inc/widget.dialog_box.go | 14 ++ inc/widget.end_card.go | 11 + inc/widget.hotspot_debug.go | 11 + inc/widget.hud_frame.go | 37 ++++ inc/widget.inventory_bar.go | 19 ++ inc/{ui.letterbox.go => widget.letterbox.go} | 10 +- inc/widget.manager.go | 65 ++++++ inc/widget.radial_verbs.go | 16 ++ inc/{ui.scene_nav.go => widget.scene_nav.go} | 7 + inc/widget.speech_bubble.go | 15 ++ inc/widget.status_line.go | 14 ++ ...tape_channel.go => widget.tape_channel.go} | 10 +- ...{ui.tape_slots.go => widget.tape_slots.go} | 10 +- inc/widget.top_bar.go | 27 +++ ...ewith_guard.go => widget.usewith_guard.go} | 7 + inc/widget.window_sizer.go | 35 +++ inc/world.action.go | 20 +- inc/world.manager.go | 4 - 26 files changed, 413 insertions(+), 242 deletions(-) delete mode 100644 inc/ui.manager.go create mode 100644 inc/widget.action_pump.go create mode 100644 inc/widget.cursor.go create mode 100644 inc/widget.dialog_box.go create mode 100644 inc/widget.end_card.go create mode 100644 inc/widget.hotspot_debug.go create mode 100644 inc/widget.hud_frame.go create mode 100644 inc/widget.inventory_bar.go rename inc/{ui.letterbox.go => widget.letterbox.go} (82%) create mode 100644 inc/widget.manager.go create mode 100644 inc/widget.radial_verbs.go rename inc/{ui.scene_nav.go => widget.scene_nav.go} (85%) create mode 100644 inc/widget.speech_bubble.go create mode 100644 inc/widget.status_line.go rename inc/{ui.tape_channel.go => widget.tape_channel.go} (89%) rename inc/{ui.tape_slots.go => widget.tape_slots.go} (92%) create mode 100644 inc/widget.top_bar.go rename inc/{ui.usewith_guard.go => widget.usewith_guard.go} (91%) create mode 100644 inc/widget.window_sizer.go diff --git a/CLAUDE.md b/CLAUDE.md index a6816af..020d2a9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,7 +49,7 @@ 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`. + `character`, `dialog`, `script`, `world`, `widget`, `ui`, `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 @@ -65,9 +65,10 @@ 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/world.action.go custom actions +inc/widget.manager.go the Widget alias, HUD layout, the visibility gate +inc/widget.*.go one widget per file, registering itself +inc/ui.text.go coloured text on a scratch image inc/background.*.go one image asset per scene inc/character.*.go the cast, tapes included inc/item.*.go inventory @@ -84,7 +85,7 @@ 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 seven are declared together, in `boot.go`, so the list of what the game +All eight 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 @@ -96,6 +97,7 @@ var ( SceneManager = inkwell.NewManager[Scene]() ScriptManager = inkwell.NewManager[Script]() ThemeManager = inkwell.NewManager[Theme]() + WidgetManager = inkwell.NewManager[Widget]() ) ``` @@ -146,6 +148,43 @@ into one. 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. `gate`, the wrapper +that hides a widget outside `ModePlay`, returns `inkwell.LayerOf` of the widget +it wraps, so wrapping never moves anything. + +Two widgets are worth knowing about because they are not decoration: `pump` +drives `World.PumpTick`, which is what runs every queued action, and +`usewith_guard` sits on `LayerScene` so that it ticks last and can answer a +use-with pair nobody authored. + ### Handing a category to the engine Nothing is copied. `inkwell.NewGame` builds its own empty managers, and `New` @@ -159,6 +198,7 @@ g.DialogueManager = DialogManager g.ItemManager = ItemManager g.SceneManager = SceneManager g.ScriptManager = ScriptManager +g.WidgetManager = WidgetManager ThemeManager.Each(g.ThemeManager.Register) ``` @@ -221,7 +261,7 @@ 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`, …), `registerUI`, the runners, +(`tapeSlots`, `letterbox`, `hudFrame`, …), `gated`, the runners, `prepareScene`, `sceneDefaults`, `fillSelectorPins`. The word is **scene**, the engine's own. The wiki and the concept-art deck count diff --git a/README.md b/README.md index 6802d32..3c67581 100644 --- a/README.md +++ b/README.md @@ -109,9 +109,9 @@ Every pixel it gives back is a pixel of art. InventoryBar TapeSlots TapeChannel ``` -Every vertical measurement derives from `ui.LineH` (glyph cell + leading) and -from `ui.ScreenH`, not from the wireframe deck's ratios, so the layout cannot -drift out of step with the font — or with the art — again; +Every vertical measurement derives from `LineH` (glyph cell + leading) and from +`ScreenH`, both in `widget.manager.go`, not from the wireframe deck's ratios, so +the layout cannot drift out of step with the font — or with the art — again; `TestLayoutFitsTheFont` guards it. The tape channel comes out at 4 rows of 38 columns, which is about one and a half of Dex's remarks on screen at once. @@ -161,8 +161,10 @@ 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/world.*.go unsaved runtime state and custom actions +inc/widget.manager.go HUD layout and the widget registration list +inc/widget.*.go one widget per file +inc/ui.text.go coloured text inc/..go one file per registered entity, by kind ``` @@ -195,7 +197,7 @@ 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. The seven of them are +`inkwell.Named` and need no help reading their own name. The eight of them are declared as one block in `boot.go`, and a category's manager file is left holding the alias: @@ -215,6 +217,17 @@ func init() { } ``` +Widgets go the same way, the engine's own included: `widget.cursor.go` registers +an `inkwell.Cursor` exactly as `background.street.go` registers an image. What +they cannot take from alphabetical order is their place in the stack, so each +widget names a **layer** — `LayerScene`, `LayerPanel`, `LayerHUD`, `LayerSpeech`, +`LayerDialog`, `LayerMenu`, `LayerCurtain`, `LayerCursor` — and inkwell draws +from the bottom layer up, ticks from the top layer down, and lets registration +order decide only ties inside one layer. That is why the HUD frame comes out +under the tape slots and the cursor over everything, whatever the file names +happen to be. The layer was added to inkwell for this; the alternative was a +registration list, which is the thing this package spent its life deleting. + So adding an entity is adding a file, and there is no second list to keep in step. The price is that registration order is file-name order: the arrow keys walk the scenes alphabetically rather than in the concept-art deck's order. @@ -307,8 +320,8 @@ has to shadow where the player is. *Fix:* move `drawText` to `text/v2` — no call site would change. 2. **`queueAction` is unexported**, so a domain widget cannot start an action. - *Workaround:* the pump in `world.action.go` drives its own `Runner` through - the exported `inkwell.Ctx`. *Caveat:* it runs alongside the engine's script + *Workaround:* the pump — `widget.action_pump.go` calling `World.PumpTick` — + drives its own `Runner` through the exported `inkwell.Ctx`. *Caveat:* it runs alongside the engine's script runner, not instead of it. 3. **The `"Nem ehhez."` flash is hardcoded** in `core.engine.go` for an @@ -319,7 +332,7 @@ has to shadow where the player is. 4. **Widgets have no `Visible` field and the `Manager` cannot unregister**, so the built-in HUD cannot be hidden during a cutscene. *Workaround:* the - `gate` wrapper in `ui/ui.go` forwards `Tick`/`Draw`/`BlocksClickAt` only + `gate` wrapper in `widget.manager.go` forwards `Tick`/`Draw`/`BlocksClickAt` only while the HUD is visible. 5. **`Run` hardcodes a 4× window** (`core.dsl.go`), which at 640×400 would be diff --git a/go.mod b/go.mod index 7fa8040..cf061d3 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.20260830092549-e26f7345c14d + git.teletypegames.org/engines/inkwell v0.1.1-0.20260830201917-2429ada0900f github.com/hajimehoshi/ebiten/v2 v2.9.9 ) diff --git a/go.sum b/go.sum index fb3f8f1..2ffc672 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,9 @@ git.teletypegames.org/engines/inkwell v0.1.1-0.20260830092549-e26f7345c14d h1:4AGtmrWwfnHkJvZ90on1+9bUmZOGSrcIG4KaJFiy+xo= git.teletypegames.org/engines/inkwell v0.1.1-0.20260830092549-e26f7345c14d/go.mod h1:/v6QtismTE8+e7kobbVR5B/CpTSrd4j2DYwJ8DvINhs= +git.teletypegames.org/engines/inkwell v0.1.1-0.20260830200555-53f4df04669c h1:GOKa6IbkfJXjB5LREr3eUw6YeeOeDICryfa0eKERWo0= +git.teletypegames.org/engines/inkwell v0.1.1-0.20260830200555-53f4df04669c/go.mod h1:/v6QtismTE8+e7kobbVR5B/CpTSrd4j2DYwJ8DvINhs= +git.teletypegames.org/engines/inkwell v0.1.1-0.20260830201917-2429ada0900f h1:WBIh7svmA2R7tbi1Xrafn5//yl8vnK2qbpBXDbbz6Aw= +git.teletypegames.org/engines/inkwell v0.1.1-0.20260830201917-2429ada0900f/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/boot.go b/inc/boot.go index c807b2f..5c65d81 100644 --- a/inc/boot.go +++ b/inc/boot.go @@ -12,6 +12,7 @@ var ( SceneManager = inkwell.NewManager[Scene]() ScriptManager = inkwell.NewManager[Script]() ThemeManager = inkwell.NewManager[Theme]() + WidgetManager = inkwell.NewManager[Widget]() ) type Opts struct { @@ -43,11 +44,11 @@ func New(o Opts) *inkwell.Game { g.ItemManager = ItemManager g.SceneManager = SceneManager g.ScriptManager = ScriptManager + g.WidgetManager = WidgetManager ThemeManager.Each(g.ThemeManager.Register) World.attach(g) g.UseTheme(RealWorld) - registerUI() g.StartAt(start) g.OnStart(ScriptOpening) diff --git a/inc/ui.manager.go b/inc/ui.manager.go deleted file mode 100644 index baa8d9d..0000000 --- a/inc/ui.manager.go +++ /dev/null @@ -1,199 +0,0 @@ -package inc - -import ( - inkwell "git.teletypegames.org/engines/inkwell" - "github.com/hajimehoshi/ebiten/v2" - "github.com/hajimehoshi/ebiten/v2/vector" -) - -const ( - ScreenW = 640 - ScreenH = 380 - - LineH = glyphH + 2 - Pad = 6 - - TopBarH = LineH + 2 - DividerX = 394 - - HUDH = Pad + LineH + 4 + invSlot*2 + invGap + Pad - HUDTop = ScreenH - HUDH - - statusY = HUDTop + Pad - slotsY = HUDTop + Pad + LineH - slotsH = LineH*2 + Pad*2 - invY = slotsY + 4 - invSlot = 40 - invGap = 4 -) - -func registerUI() { - g := World.G - - g.UIManager.Register(&useWithGuard{ - Name: "usewith_guard", - }) - g.UIManager.Register(&actionPump{ - Name: "pump", - }) - g.UIManager.Register(&sceneNav{ - Name: "scene_nav", - }) - g.UIManager.Register(&windowSizer{ - Name: "window", - Scale: 2, - }) - g.UIManager.Register(&inkwell.HotspotDebug{ - Name: "hotspot_debug", - }) - - top := &inkwell.TopBar{ - Name: "topbar", - Height: TopBarH, - TimeVar: VarArmillaStrip, - } - World.SetTopBar(top) - g.UIManager.Register(gated("topbar_gate", top)) - - g.UIManager.Register(&letterbox{ - Name: "letterbox", - Bar: 36, - }) - g.UIManager.Register(&hudFrame{ - Name: "hudframe", - }) - - g.UIManager.Register(gated("status_gate", &inkwell.StatusLine{ - Name: "status", - Y: statusY, - Align: inkwell.AlignLeft, - ScreenWidth: DividerX, - })) - g.UIManager.Register(gated("inventory_gate", &inkwell.InventoryBar{ - Name: "inventory", - Origin: inkwell.Point{ - X: Pad + 2, - Y: invY, - }, - Slots: 8, - Cols: 4, - SlotSize: invSlot, - Gap: invGap, - })) - g.UIManager.Register(&tapeSlots{ - Name: "tapes", - Bounds: inkwell.Rect(192, slotsY, 194, slotsH), - }) - g.UIManager.Register(&tapeChannel{ - Name: "channel", - Bounds: inkwell.Rect(DividerX+2, HUDTop+2, ScreenW-DividerX-4, ScreenH-HUDTop-4), - }) - - g.UIManager.Register(&inkwell.SpeechBubble{ - Name: "speech", - MaxWidth: 360, - Padding: Pad, - OffsetY: 8, - FallbackY: TopBarH + Pad, - }) - - g.UIManager.Register(&inkwell.DialogBox{ - Name: "dialog", - Bounds: inkwell.Rect(0, ScreenH-LineH*9, DividerX, LineH*9), - LineHeight: LineH, - Padding: Pad, - }) - - g.UIManager.Register(&inkwell.RadialVerbs{ - Name: "verbs", - Trigger: inkwell.MouseButtonRight, - Radius: 58, - Labels: map[string]string{ - "look": "Look", "use": "Use", "talk": "Talk", "take": "Take", - }, - }) - g.UIManager.Register(&inkwell.EndCard{ - Name: "endcard", - }) - g.UIManager.Register(&inkwell.Cursor{ - Name: "cursor", - }) -} - -type windowSizer struct { - Name string - Scale int - done bool -} - -func (s *windowSizer) GetName() string { return s.Name } -func (s *windowSizer) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) {} - -func (s *windowSizer) Tick(ctx *inkwell.UICtx) { - if s.done { - return - } - s.done = true - scale := s.Scale - if scale <= 0 { - scale = 2 - } - ebiten.SetWindowSize(ctx.Game.Width*scale, ctx.Game.Height*scale) -} - -type gate struct { - Name string - Inner inkwell.Widget -} - -func gated(name string, inner inkwell.Widget) inkwell.Widget { - return &gate{ - Name: name, - Inner: inner, - } -} - -func (n *gate) GetName() string { return n.Name } - -func (n *gate) Tick(ctx *inkwell.UICtx) { - if World.HUDVisible() { - n.Inner.Tick(ctx) - } -} - -func (n *gate) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) { - if World.HUDVisible() { - n.Inner.Draw(dst, ctx) - } -} - -func (n *gate) BlocksClickAt(p inkwell.Point) bool { - if !World.HUDVisible() { - return false - } - b, ok := n.Inner.(interface{ BlocksClickAt(inkwell.Point) bool }) - return ok && b.BlocksClickAt(p) -} - -type hudFrame struct { - Name string -} - -func (h *hudFrame) GetName() string { return h.Name } -func (h *hudFrame) Tick(ctx *inkwell.UICtx) {} - -func (h *hudFrame) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) { - if !World.HUDVisible() { - return - } - th := ctx.Game.Theme() - vector.DrawFilledRect(dst, 0, HUDTop+1, ScreenW, ScreenH-HUDTop-1, th.PanelBG, false) - - vector.StrokeLine(dst, 0, HUDTop, ScreenW, HUDTop, 1, th.CharacterPanelBorder, false) - - vector.StrokeLine(dst, DividerX, HUDTop+1, DividerX, ScreenH, 1, th.CharacterPanelBorder, false) -} - -func (h *hudFrame) BlocksClickAt(p inkwell.Point) bool { - return World.HUDVisible() && p.Y >= HUDTop -} diff --git a/inc/widget.action_pump.go b/inc/widget.action_pump.go new file mode 100644 index 0000000..a119093 --- /dev/null +++ b/inc/widget.action_pump.go @@ -0,0 +1,21 @@ +package inc + +import ( + inkwell "git.teletypegames.org/engines/inkwell" + "github.com/hajimehoshi/ebiten/v2" +) + +func init() { + WidgetManager.Register(&actionPump{ + Name: "pump", + }) +} + +type actionPump struct { + Name string +} + +func (p *actionPump) GetName() string { return p.Name } +func (p *actionPump) Layer() inkwell.Layer { return inkwell.LayerScene } +func (p *actionPump) Tick(ctx *inkwell.UICtx) { World.PumpTick(ctx.DT) } +func (p *actionPump) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) {} diff --git a/inc/widget.cursor.go b/inc/widget.cursor.go new file mode 100644 index 0000000..424a56b --- /dev/null +++ b/inc/widget.cursor.go @@ -0,0 +1,11 @@ +package inc + +import ( + inkwell "git.teletypegames.org/engines/inkwell" +) + +func init() { + WidgetManager.Register(&inkwell.Cursor{ + Name: "cursor", + }) +} diff --git a/inc/widget.dialog_box.go b/inc/widget.dialog_box.go new file mode 100644 index 0000000..931ceac --- /dev/null +++ b/inc/widget.dialog_box.go @@ -0,0 +1,14 @@ +package inc + +import ( + inkwell "git.teletypegames.org/engines/inkwell" +) + +func init() { + WidgetManager.Register(&inkwell.DialogBox{ + Name: "dialog", + Bounds: inkwell.Rect(0, ScreenH-LineH*9, DividerX, LineH*9), + LineHeight: LineH, + Padding: Pad, + }) +} diff --git a/inc/widget.end_card.go b/inc/widget.end_card.go new file mode 100644 index 0000000..181da2b --- /dev/null +++ b/inc/widget.end_card.go @@ -0,0 +1,11 @@ +package inc + +import ( + inkwell "git.teletypegames.org/engines/inkwell" +) + +func init() { + WidgetManager.Register(&inkwell.EndCard{ + Name: "endcard", + }) +} diff --git a/inc/widget.hotspot_debug.go b/inc/widget.hotspot_debug.go new file mode 100644 index 0000000..df3b934 --- /dev/null +++ b/inc/widget.hotspot_debug.go @@ -0,0 +1,11 @@ +package inc + +import ( + inkwell "git.teletypegames.org/engines/inkwell" +) + +func init() { + WidgetManager.Register(&inkwell.HotspotDebug{ + Name: "hotspot_debug", + }) +} diff --git a/inc/widget.hud_frame.go b/inc/widget.hud_frame.go new file mode 100644 index 0000000..ac32b5e --- /dev/null +++ b/inc/widget.hud_frame.go @@ -0,0 +1,37 @@ +package inc + +import ( + inkwell "git.teletypegames.org/engines/inkwell" + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/vector" +) + +func init() { + WidgetManager.Register(&hudFrame{ + Name: "hudframe", + }) +} + +type hudFrame struct { + Name string +} + +func (h *hudFrame) GetName() string { return h.Name } +func (h *hudFrame) Layer() inkwell.Layer { return inkwell.LayerPanel } +func (h *hudFrame) Tick(ctx *inkwell.UICtx) {} + +func (h *hudFrame) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) { + if !World.HUDVisible() { + return + } + th := ctx.Game.Theme() + vector.DrawFilledRect(dst, 0, HUDTop+1, ScreenW, ScreenH-HUDTop-1, th.PanelBG, false) + + vector.StrokeLine(dst, 0, HUDTop, ScreenW, HUDTop, 1, th.CharacterPanelBorder, false) + + vector.StrokeLine(dst, DividerX, HUDTop+1, DividerX, ScreenH, 1, th.CharacterPanelBorder, false) +} + +func (h *hudFrame) BlocksClickAt(p inkwell.Point) bool { + return World.HUDVisible() && p.Y >= HUDTop +} diff --git a/inc/widget.inventory_bar.go b/inc/widget.inventory_bar.go new file mode 100644 index 0000000..3f12cb8 --- /dev/null +++ b/inc/widget.inventory_bar.go @@ -0,0 +1,19 @@ +package inc + +import ( + inkwell "git.teletypegames.org/engines/inkwell" +) + +func init() { + WidgetManager.Register(gated("inventory", &inkwell.InventoryBar{ + Name: "inventory", + Origin: inkwell.Point{ + X: Pad + 2, + Y: invY, + }, + Slots: 8, + Cols: 4, + SlotSize: invSlot, + Gap: invGap, + })) +} diff --git a/inc/ui.letterbox.go b/inc/widget.letterbox.go similarity index 82% rename from inc/ui.letterbox.go rename to inc/widget.letterbox.go index 3dc4ba0..04a3dd0 100644 --- a/inc/ui.letterbox.go +++ b/inc/widget.letterbox.go @@ -7,12 +7,20 @@ import ( "github.com/hajimehoshi/ebiten/v2/vector" ) +func init() { + WidgetManager.Register(&letterbox{ + Name: "letterbox", + Bar: 36, + }) +} + type letterbox struct { Name string Bar float64 } -func (l *letterbox) GetName() string { return l.Name } +func (l *letterbox) GetName() string { return l.Name } +func (l *letterbox) Layer() inkwell.Layer { return inkwell.LayerScene } func (l *letterbox) Tick(ctx *inkwell.UICtx) { if World.Mode() != ModeCutscene || !World.TapeWaiting() { diff --git a/inc/widget.manager.go b/inc/widget.manager.go new file mode 100644 index 0000000..79d4a9e --- /dev/null +++ b/inc/widget.manager.go @@ -0,0 +1,65 @@ +package inc + +import ( + inkwell "git.teletypegames.org/engines/inkwell" + "github.com/hajimehoshi/ebiten/v2" +) + +type Widget = inkwell.Widget + +const ( + ScreenW = 640 + ScreenH = 380 + + LineH = glyphH + 2 + Pad = 6 + + TopBarH = LineH + 2 + DividerX = 394 + + HUDH = Pad + LineH + 4 + invSlot*2 + invGap + Pad + HUDTop = ScreenH - HUDH + + statusY = HUDTop + Pad + slotsY = HUDTop + Pad + LineH + slotsH = LineH*2 + Pad*2 + invY = slotsY + 4 + invSlot = 40 + invGap = 4 +) + +type gate struct { + Name string + Inner Widget +} + +func gated(name string, inner Widget) Widget { + return &gate{ + Name: name, + Inner: inner, + } +} + +func (n *gate) GetName() string { return n.Name } + +func (n *gate) Layer() inkwell.Layer { return inkwell.LayerOf(n.Inner) } + +func (n *gate) Tick(ctx *inkwell.UICtx) { + if World.HUDVisible() { + n.Inner.Tick(ctx) + } +} + +func (n *gate) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) { + if World.HUDVisible() { + n.Inner.Draw(dst, ctx) + } +} + +func (n *gate) BlocksClickAt(p inkwell.Point) bool { + if !World.HUDVisible() { + return false + } + b, ok := n.Inner.(interface{ BlocksClickAt(inkwell.Point) bool }) + return ok && b.BlocksClickAt(p) +} diff --git a/inc/widget.radial_verbs.go b/inc/widget.radial_verbs.go new file mode 100644 index 0000000..3a3ee7b --- /dev/null +++ b/inc/widget.radial_verbs.go @@ -0,0 +1,16 @@ +package inc + +import ( + inkwell "git.teletypegames.org/engines/inkwell" +) + +func init() { + WidgetManager.Register(&inkwell.RadialVerbs{ + Name: "verbs", + Trigger: inkwell.MouseButtonRight, + Radius: 58, + Labels: map[string]string{ + "look": "Look", "use": "Use", "talk": "Talk", "take": "Take", + }, + }) +} diff --git a/inc/ui.scene_nav.go b/inc/widget.scene_nav.go similarity index 85% rename from inc/ui.scene_nav.go rename to inc/widget.scene_nav.go index 9217872..98a9bbb 100644 --- a/inc/ui.scene_nav.go +++ b/inc/widget.scene_nav.go @@ -6,11 +6,18 @@ import ( "github.com/hajimehoshi/ebiten/v2/inpututil" ) +func init() { + WidgetManager.Register(&sceneNav{ + Name: "scene_nav", + }) +} + type sceneNav struct { Name string } func (n *sceneNav) GetName() string { return n.Name } +func (n *sceneNav) Layer() inkwell.Layer { return inkwell.LayerScene } func (n *sceneNav) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) {} func (n *sceneNav) Tick(ctx *inkwell.UICtx) { diff --git a/inc/widget.speech_bubble.go b/inc/widget.speech_bubble.go new file mode 100644 index 0000000..53585d7 --- /dev/null +++ b/inc/widget.speech_bubble.go @@ -0,0 +1,15 @@ +package inc + +import ( + inkwell "git.teletypegames.org/engines/inkwell" +) + +func init() { + WidgetManager.Register(&inkwell.SpeechBubble{ + Name: "speech", + MaxWidth: 360, + Padding: Pad, + OffsetY: 8, + FallbackY: TopBarH + Pad, + }) +} diff --git a/inc/widget.status_line.go b/inc/widget.status_line.go new file mode 100644 index 0000000..8832f57 --- /dev/null +++ b/inc/widget.status_line.go @@ -0,0 +1,14 @@ +package inc + +import ( + inkwell "git.teletypegames.org/engines/inkwell" +) + +func init() { + WidgetManager.Register(gated("status", &inkwell.StatusLine{ + Name: "status", + Y: statusY, + Align: inkwell.AlignLeft, + ScreenWidth: DividerX, + })) +} diff --git a/inc/ui.tape_channel.go b/inc/widget.tape_channel.go similarity index 89% rename from inc/ui.tape_channel.go rename to inc/widget.tape_channel.go index 6804b52..aab29a1 100644 --- a/inc/ui.tape_channel.go +++ b/inc/widget.tape_channel.go @@ -8,12 +8,20 @@ import ( "github.com/hajimehoshi/ebiten/v2/vector" ) +func init() { + WidgetManager.Register(&tapeChannel{ + Name: "channel", + Bounds: inkwell.Rect(DividerX+2, HUDTop+2, ScreenW-DividerX-4, ScreenH-HUDTop-4), + }) +} + type tapeChannel struct { Name string Bounds inkwell.Rectangle } -func (t *tapeChannel) GetName() string { return t.Name } +func (t *tapeChannel) GetName() string { return t.Name } +func (t *tapeChannel) Layer() inkwell.Layer { return inkwell.LayerHUD } func (t *tapeChannel) Tick(ctx *inkwell.UICtx) {} diff --git a/inc/ui.tape_slots.go b/inc/widget.tape_slots.go similarity index 92% rename from inc/ui.tape_slots.go rename to inc/widget.tape_slots.go index a7f0fce..3ad221b 100644 --- a/inc/ui.tape_slots.go +++ b/inc/widget.tape_slots.go @@ -6,12 +6,20 @@ import ( "github.com/hajimehoshi/ebiten/v2/vector" ) +func init() { + WidgetManager.Register(&tapeSlots{ + Name: "tapes", + Bounds: inkwell.Rect(192, slotsY, 194, slotsH), + }) +} + type tapeSlots struct { Name string Bounds inkwell.Rectangle } -func (t *tapeSlots) GetName() string { return t.Name } +func (t *tapeSlots) GetName() string { return t.Name } +func (t *tapeSlots) Layer() inkwell.Layer { return inkwell.LayerHUD } func (t *tapeSlots) BlocksClickAt(p inkwell.Point) bool { return World.HUDVisible() && t.Bounds.Contains(p) diff --git a/inc/widget.top_bar.go b/inc/widget.top_bar.go new file mode 100644 index 0000000..6c0ab48 --- /dev/null +++ b/inc/widget.top_bar.go @@ -0,0 +1,27 @@ +package inc + +import ( + inkwell "git.teletypegames.org/engines/inkwell" +) + +func init() { + WidgetManager.Register(gated("topbar", &titleBar{ + TopBar: &inkwell.TopBar{ + Name: "topbar", + Height: TopBarH, + TimeVar: VarArmillaStrip, + }, + })) +} + +type titleBar struct { + *inkwell.TopBar +} + +func (t *titleBar) Tick(ctx *inkwell.UICtx) { + t.LeftText = World.SceneTitle() + if World.Paused() { + t.LeftText += " — paused" + } + t.TopBar.Tick(ctx) +} diff --git a/inc/ui.usewith_guard.go b/inc/widget.usewith_guard.go similarity index 91% rename from inc/ui.usewith_guard.go rename to inc/widget.usewith_guard.go index 2db90e9..fcdcb3f 100644 --- a/inc/ui.usewith_guard.go +++ b/inc/widget.usewith_guard.go @@ -7,11 +7,18 @@ import ( "github.com/hajimehoshi/ebiten/v2" ) +func init() { + WidgetManager.Register(&useWithGuard{ + Name: "usewith_guard", + }) +} + type useWithGuard struct { Name string } func (u *useWithGuard) GetName() string { return u.Name } +func (u *useWithGuard) Layer() inkwell.Layer { return inkwell.LayerScene } func (u *useWithGuard) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) {} func (u *useWithGuard) Tick(ctx *inkwell.UICtx) { diff --git a/inc/widget.window_sizer.go b/inc/widget.window_sizer.go new file mode 100644 index 0000000..d5d611d --- /dev/null +++ b/inc/widget.window_sizer.go @@ -0,0 +1,35 @@ +package inc + +import ( + inkwell "git.teletypegames.org/engines/inkwell" + "github.com/hajimehoshi/ebiten/v2" +) + +func init() { + WidgetManager.Register(&windowSizer{ + Name: "window", + Scale: 2, + }) +} + +type windowSizer struct { + Name string + Scale int + done bool +} + +func (s *windowSizer) GetName() string { return s.Name } +func (s *windowSizer) Layer() inkwell.Layer { return inkwell.LayerScene } +func (s *windowSizer) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) {} + +func (s *windowSizer) Tick(ctx *inkwell.UICtx) { + if s.done { + return + } + s.done = true + scale := s.Scale + if scale <= 0 { + scale = 2 + } + ebiten.SetWindowSize(ctx.Game.Width*scale, ctx.Game.Height*scale) +} diff --git a/inc/world.action.go b/inc/world.action.go index 4dfc528..db4b72b 100644 --- a/inc/world.action.go +++ b/inc/world.action.go @@ -2,7 +2,6 @@ package inc import ( inkwell "git.teletypegames.org/engines/inkwell" - "github.com/hajimehoshi/ebiten/v2" ) func (w *world) Do(a inkwell.Action) { @@ -12,15 +11,6 @@ func (w *world) Do(a inkwell.Action) { } func (w *world) PumpTick(dt float64) { - - if w.top != nil { - title := w.sceneTitle() - if w.paused { - title += " — paused" - } - w.top.LeftText = title - } - if w.running == nil { if len(w.queue) == 0 { return @@ -40,7 +30,7 @@ func (w *world) PumpTick(dt float64) { } } -func (w *world) sceneTitle() string { +func (w *world) SceneTitle() string { s, ok := w.G.SceneManager.Get(w.G.CurrentScene()) if !ok { return "" @@ -51,14 +41,6 @@ func (w *world) sceneTitle() string { return s.Name } -type actionPump struct { - Name string -} - -func (p *actionPump) GetName() string { return p.Name } -func (p *actionPump) Tick(ctx *inkwell.UICtx) { World.PumpTick(ctx.DT) } -func (p *actionPump) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) {} - type fnAction struct{ fn func(*inkwell.Ctx) } func (a *fnAction) Start() inkwell.Runner { diff --git a/inc/world.manager.go b/inc/world.manager.go index 8a95486..f04783d 100644 --- a/inc/world.manager.go +++ b/inc/world.manager.go @@ -21,7 +21,6 @@ type world struct { pending string queue []inkwell.Action running inkwell.Runner - top *inkwell.TopBar tapeWaiting bool tapeLine inkwell.Action @@ -36,7 +35,6 @@ func (w *world) attach(g *inkwell.Game) { w.pending = "" w.queue = nil w.running = nil - w.top = nil w.tapeWaiting = false w.tapeLine = nil } @@ -49,8 +47,6 @@ func (w *world) HUDVisible() bool { return w.mode == ModePlay } func (w *world) Paused() bool { return w.paused } -func (w *world) SetTopBar(t *inkwell.TopBar) { w.top = t } - func (w *world) Slot2() string { if v, ok := w.G.State.Var(VarSlot2).(string); ok { return v