diff --git a/CLAUDE.md b/CLAUDE.md index 020d2a9..bb75925 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`, `widget`, `ui`, `theme`, `names`. + `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 @@ -66,11 +66,12 @@ 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 visibility gate +inc/widget.manager.go the Widget alias, HUD layout, the shared conditions 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/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 @@ -85,7 +86,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 eight are declared together, in `boot.go`, so the list of what the game +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 @@ -96,6 +97,7 @@ var ( ItemManager = inkwell.NewManager[Item]() SceneManager = inkwell.NewManager[Scene]() ScriptManager = inkwell.NewManager[Script]() + TapeManager = inkwell.NewManager[Tape]() ThemeManager = inkwell.NewManager[Theme]() WidgetManager = inkwell.NewManager[Widget]() ) @@ -117,9 +119,17 @@ 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. -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. +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 @@ -176,14 +186,25 @@ 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. +layer is the one thing about a widget the file cannot show. -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. +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 @@ -206,12 +227,16 @@ 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. +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 @@ -231,14 +256,37 @@ if o.Finale { 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.HUDVisible()`, `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. +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. @@ -249,7 +297,7 @@ One package means one namespace, so an entity's constructor carries its category as a prefix: ```go -sceneFloor sceneDefaults prepareScene fillSelectorPins +sceneFloor fillSelectorPins tapeSlots useWithFail ``` Entities themselves need no name at all — they are anonymous literals inside @@ -257,12 +305,16 @@ 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, and the action constructors -(`TapeSay`, `Paused`, `SetMode`, `EnterScene`, `Back`, `Fn`). +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`, …), `gated`, the runners, -`prepareScene`, `sceneDefaults`, `fillSelectorPins`. +(`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 @@ -275,9 +327,9 @@ Nothing is enforced by the compiler any more, so the layering is a rule kept by hand: ``` -names ← theme ← world ← ui - ↑ ↑ - content ──┴── boot ← main +names ← theme ← world ← widget + ↑ ↑ + content ───┴── boot ← main ``` `world` knows nothing about the HUD or the content. Both build on it, never the diff --git a/README.md b/README.md index 3c67581..21d6dcc 100644 --- a/README.md +++ b/README.md @@ -162,9 +162,8 @@ 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 and custom actions -inc/widget.manager.go HUD layout and the widget registration list +inc/widget.manager.go HUD layout and the shared visibility conditions inc/widget.*.go one widget per file -inc/ui.text.go coloured text inc/..go one file per registered entity, by kind ``` @@ -173,9 +172,9 @@ code keeps by hand: the world holds no opinion about the HUD or the content, and both build on it, never the other way round. ``` -names ← theme ← world ← ui - ↑ ↑ - content ──┴── boot ← main +names ← theme ← world ← widget + ↑ ↑ + content ───┴── boot ← main ``` Content is one registered entity per file, and the category prefix groups them @@ -228,6 +227,19 @@ 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. +The second thing a widget declares is when it is on screen at all: `When` is an +`inkwell.Condition` over game state, and the HUD's is `whenPlaying`, which reads +`VarMode` out of the engine's `State`. Hiding the HUD for a cutscene is +therefore one word per widget rather than a wrapper around each of them and an +`if` at the top of every `Draw`. + +Tapes are the one entity type this game invents. `Tape` names the character +whose voice it is, the cassette that carries it and the dialogue it plays, one +file each — what a tape *sounds* like stays on the character, as +`Character.Label` and `Character.Voice`, because that is a fact about a speaker. +A tape's line is `inkwell.Say` like anybody else's; the engine sends it to the +log instead of a speech bubble because the character says so. + 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. @@ -243,11 +255,13 @@ 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. +The defaults a scene may leave out are not written into it at all any more: +`g.Player` names the character a scene with no actors of its own receives, at +his `Start`, and `g.Walkboxes` is the floor a scene without one walks on. Both +are fields on the `*Game`, so a scene file that says nothing about either means +"the usual", and the selector opts out by declaring both empty. What is still +written back before the hand-off is the derived half of the map: +`fillSelectorPins` reads the exit graph backwards and `Set`s the selector. 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 @@ -300,45 +314,25 @@ The graph stays machine-readable: exit hotspots are named `exit:`, so the connections can be read straight back out of the registered scenes — and `Validate` rejects an exit that names a scene nobody registered. -## Engine workarounds +## What this game pushed into the engine -Four inkwell limits turned up during implementation that the engine README does -not mention. All four are worked around on the domain side; each is a candidate -for a small engine change. +Everything below started as a workaround in this package and ended up in +inkwell, because in each case the thing being worked around was a fact an +entity should have carried in the first place. The domain side of each is now a +field in a literal: -Two others have been fixed in the engine since: exits are now -[`inkwell.Exit`](https://git.teletypegames.org/engines/inkwell) on the scene -itself, and `Game.CurrentScene()` / `PreviousScene()` mean the domain no longer -has to shadow where the player is. - -1. **`drawText` discards colour.** In `asset.text.go` the colour argument is - `_ = c` and rendering goes through `ebitenutil.DebugPrintAt`, which only - draws white. Every text colour in `Theme` is therefore inert. *Workaround:* - `ui.text.go` renders onto a scratch image and blits it tinted with - `ColorScale`. The custom widgets colour correctly; the built-ins - (`StatusLine`, `DialogBox`, `TopBar`, `InventoryBar`) are still white. - *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 — `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 - item/hotspot pair with no `OnUseWith`, and cannot be replaced from the - domain. *Workaround:* `UseWithGuard` — widgets tick before the engine's - `handleSceneInput` and can consume the click, so unauthored pairs fail in - character instead, escalating on repeats. - -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 `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 - 2560×1600 — bigger than most laptop screens. *Workaround:* the `windowSizer` - widget resizes once on the first tick, since Run sets the size before - entering the loop. +| was worked around here | is now | +|---|---| +| a scratch-image text blitter, because `drawText` dropped the colour | `Game.DrawText` renders in colour; `inkwell.TextWidth`, `WrapText`, `ClipText`, `GlyphW/H` are the library's | +| a pump widget driving its own `Runner`, because `queueAction` was unexported | `g.Do(action)` — one queue, in order, engine-side | +| `TapeSay`, a second spelling of `Say` that skipped the speech bubble | `Character.Voice` — `VoiceLog` sends a character's lines to the log | +| a `Tapes` map of display names | `Character.Label` | +| a `gate` wrapper and `if !World.HUDVisible()` at the top of every `Draw` | `Widget.When`, an `inkwell.Condition` the engine evaluates | +| a `titleBar` widget that pushed the scene title into the top bar | `TopBar` already discovers the title; `NoteVar` adds the "— paused" | +| a `UseWithGuard` widget stealing clicks to answer unauthored use-with pairs | `Game.UseWithFail`, beside `ExitLook` and `ExitTake` | +| a `windowSizer` widget resizing the window on its first tick | `Game.WindowScale` | +| a `sceneNav` widget walking the scene catalogue | `inkwell.SceneNav`, a built-in dev widget | +| `sceneDefaults` rewriting every scene with Paul and a floor | `Game.Player` and `Game.Walkboxes` | Also: the inkwell README gives the module path as `git.teletypegames.org/games/inkwell`; the real path per its `go.mod` is diff --git a/go.mod b/go.mod index cf061d3..8f94187 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.20260830201917-2429ada0900f + git.teletypegames.org/engines/inkwell v0.1.1-0.20260830205716-92fc36bdf5b8 github.com/hajimehoshi/ebiten/v2 v2.9.9 ) diff --git a/go.sum b/go.sum index 2ffc672..b12ab43 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ git.teletypegames.org/engines/inkwell v0.1.1-0.20260830200555-53f4df04669c h1:GO 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= +git.teletypegames.org/engines/inkwell v0.1.1-0.20260830205716-92fc36bdf5b8 h1:t1xAbtOTCUeIbqIHUpRXQMiYDvp8KOt+m39iEZ5Q6Sg= +git.teletypegames.org/engines/inkwell v0.1.1-0.20260830205716-92fc36bdf5b8/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 5c65d81..c51f820 100644 --- a/inc/boot.go +++ b/inc/boot.go @@ -11,6 +11,7 @@ var ( ItemManager = inkwell.NewManager[Item]() SceneManager = inkwell.NewManager[Scene]() ScriptManager = inkwell.NewManager[Script]() + TapeManager = inkwell.NewManager[Tape]() ThemeManager = inkwell.NewManager[Theme]() WidgetManager = inkwell.NewManager[Widget]() ) @@ -26,17 +27,21 @@ func New(o Opts) *inkwell.Game { start = SceneAlley } - prepareScene() + fillSelectorPins() g := inkwell.NewGame("Real World", ScreenW, ScreenH) g.MaxLogLines = 64 + g.WindowScale = 2 g.SceneRect = inkwell.Rect(0, TopBarH, ScreenW, HUDTop-TopBarH) + g.Player = Paul + g.Walkboxes = sceneFloor g.ExitLook = func(e inkwell.Exit) inkwell.Action { return inkwell.Say(Paul, "That way: "+e.Label+".") } g.ExitTake = func(inkwell.Exit) inkwell.Action { return inkwell.Say(Paul, "It's a way out, not a thing.") } + g.UseWithFail = useWithFail g.AssetManager = BackgroundManager g.CharacterManager = CharacterManager diff --git a/inc/character.dex.go b/inc/character.dex.go index a28a15a..d8ac78e 100644 --- a/inc/character.dex.go +++ b/inc/character.dex.go @@ -1,8 +1,14 @@ package inc +import ( + inkwell "git.teletypegames.org/engines/inkwell" +) + func init() { CharacterManager.Register(Character{ Name: Dex, + Label: "DEX", + Voice: inkwell.VoiceLog, SpeechColor: Amber, }) } diff --git a/inc/character.mystery_tape.go b/inc/character.mystery_tape.go index b2f9978..b10f417 100644 --- a/inc/character.mystery_tape.go +++ b/inc/character.mystery_tape.go @@ -1,8 +1,14 @@ package inc +import ( + inkwell "git.teletypegames.org/engines/inkwell" +) + func init() { CharacterManager.Register(Character{ Name: TapeMystery, + Label: "UNKNOWN TAPE", + Voice: inkwell.VoiceLog, SpeechColor: RGB(0xB9A98A), }) } diff --git a/inc/character.paul.go b/inc/character.paul.go index 7aa281b..34c3e77 100644 --- a/inc/character.paul.go +++ b/inc/character.paul.go @@ -11,7 +11,7 @@ func init() { W: 28, H: 68, Start: inkwell.Point{ - X: 120, + X: 320, Y: 232, }, SpeechColor: Ink, diff --git a/inc/character.support_tape.go b/inc/character.support_tape.go index 6d135ff..02e0fef 100644 --- a/inc/character.support_tape.go +++ b/inc/character.support_tape.go @@ -1,8 +1,14 @@ package inc +import ( + inkwell "git.teletypegames.org/engines/inkwell" +) + func init() { CharacterManager.Register(Character{ Name: TapeSupport, + Label: "TECH SUPPORT", + Voice: inkwell.VoiceLog, SpeechColor: RGB(0xE8C86A), }) } diff --git a/inc/item.black_market_armilla.go b/inc/item.black_market_armilla.go index afc9b98..8fa23b2 100644 --- a/inc/item.black_market_armilla.go +++ b/inc/item.black_market_armilla.go @@ -10,12 +10,12 @@ func init() { Description: "black-market Armilla", OnUseSelf: inkwell.Seq( inkwell.Say(Paul, "Jailbroken. Pushes ads, but it was cheap."), - TapeSay(Dex, "Two slots, and both of them lie about the temperature. Don't trade me in for it."), + inkwell.Say(Dex, "Two slots, and both of them lie about the temperature. Don't trade me in for it."), ), OnUseWith: map[string]inkwell.Action{ ItemNoodleLetter: inkwell.Seq( inkwell.Say(Paul, "A letter and a bracelet. Brilliant."), - TapeSay(Dex, "Nothing. Which is what I'd charge for it."), + inkwell.Say(Dex, "Nothing. Which is what I'd charge for it."), ), }, }) diff --git a/inc/item.mystery_tape.go b/inc/item.mystery_tape.go index 1c0bb7d..b61608d 100644 --- a/inc/item.mystery_tape.go +++ b/inc/item.mystery_tape.go @@ -10,7 +10,7 @@ func init() { Description: "unmarked Personal Tape", OnUseSelf: inkwell.Seq( inkwell.Say(Paul, "Password-locked. Of course."), - TapeSay(Dex, "Put it in the second slot if you care that much. I did warn you."), + inkwell.Say(Dex, "Put it in the second slot if you care that much. I did warn you."), ), }) } diff --git a/inc/item.noodle_letter.go b/inc/item.noodle_letter.go index 307ef5f..b6f2365 100644 --- a/inc/item.noodle_letter.go +++ b/inc/item.noodle_letter.go @@ -10,7 +10,7 @@ func init() { Description: "Noodle's letter", OnUseSelf: inkwell.Seq( inkwell.Say(Paul, "“Come out, Paul. Not a phone conversation.” That's all it says."), - TapeSay(Dex, "And now you're here. And he isn't."), + inkwell.Say(Dex, "And now you're here. And he isn't."), ), }) } diff --git a/inc/names.manager.go b/inc/names.manager.go index 82f8cd9..1dcc42d 100644 --- a/inc/names.manager.go +++ b/inc/names.manager.go @@ -34,6 +34,10 @@ const ( VarSlot2 = "armilla.slot2" VarArmillaStrip = "armilla.strip" VarDecay = "decay_level" + VarMode = "mode" + VarNote = "note" + + NotePaused = "paused" ) const ( @@ -89,32 +93,3 @@ const ( BgShowroom = "bg_showroom" BgColumbarium = "bg_columbarium" ) - -var Tapes = map[string]string{ - Dex: "DEX", - TapeMystery: "UNKNOWN TAPE", - TapeSupport: "TECH SUPPORT", -} - -func IsTape(name string) bool { _, ok := Tapes[name]; return ok } - -func TapeDisplayName(name string) string { - if d, ok := Tapes[name]; ok { - return d - } - return name -} - -var TapeItems = map[string]string{ - ItemMysteryTape: TapeMystery, -} - -func IsTapeItem(item string) bool { _, ok := TapeItems[item]; return ok } - -func TapeDialogue(item string) string { - switch item { - case ItemMysteryTape: - return DlgMysteryTape - } - return DlgDex -} diff --git a/inc/scene.alley.go b/inc/scene.alley.go index ab893a5..0afe6de 100644 --- a/inc/scene.alley.go +++ b/inc/scene.alley.go @@ -47,7 +47,7 @@ func hidingPlace() inkwell.Hotspot { inkwell.Give(ItemMysteryTape), inkwell.SetFlag(FlagHasTape), inkwell.Say(Paul, "A Personal Tape. No label, no seal."), - TapeSay(Dex, "Password-locked. And somebody very much did not want it found."), + inkwell.Say(Dex, "Password-locked. And somebody very much did not want it found."), ), ), inkwell.Say(Paul, "One loose brick. I'm not taking the alley apart over it."), @@ -64,13 +64,13 @@ func backDoor() inkwell.Hotspot { OnLook: inkwell.Say(Paul, "Keypad. Four digits, worn keys."), OnUse: inkwell.Seq( inkwell.Say(Paul, "Locked. And I'm not guessing four digits."), - TapeSay(Dex, "The wear is heaviest on the two and the seven. That isn't a code. It's fewer options."), + inkwell.Say(Dex, "The wear is heaviest on the two and the seven. That isn't a code. It's fewer options."), ), OnTalk: inkwell.Say(Paul, "To the door? I'm not there yet."), OnUseWith: map[string]inkwell.Action{ ItemBlackArmilla: inkwell.Seq( inkwell.Say(Paul, "A black-market bracelet doesn't open a keypad."), - TapeSay(Dex, "But you do get an advert out of it."), + inkwell.Say(Dex, "But you do get an advert out of it."), ), }, } @@ -84,7 +84,7 @@ func bin() inkwell.Hotspot { OnLook: inkwell.Say(Paul, "Somebody's been through it. Thoroughly."), OnUse: inkwell.Seq( inkwell.Say(Paul, "Already turned out. I'm not going to be the second one."), - TapeSay(Dex, "The police don't go through bins. So who did?"), + inkwell.Say(Dex, "The police don't go through bins. So who did?"), ), } } diff --git a/inc/scene.manager.go b/inc/scene.manager.go index 2bf0983..fa4a9da 100644 --- a/inc/scene.manager.go +++ b/inc/scene.manager.go @@ -6,13 +6,6 @@ import ( type Scene = inkwell.Scene -func prepareScene() { - fillSelectorPins() - for _, entity := range SceneManager.All() { - SceneManager.Set(sceneDefaults(entity)) - } -} - var sceneFloor = []inkwell.Polygon{ inkwell.Poly( inkwell.Point{ @@ -32,24 +25,6 @@ var sceneFloor = []inkwell.Polygon{ ), } -func sceneDefaults(s Scene) Scene { - if s.Actors == nil { - s.Actors = []inkwell.SceneActor{ - { - CharacterName: Paul, - At: inkwell.Point{ - X: 320, - Y: 232, - }, - }, - } - } - if s.Walkboxes == nil { - s.Walkboxes = sceneFloor - } - return s -} - func setVarIfEmpty(name string, v any) inkwell.Action { return Fn(func(ctx *inkwell.Ctx) { if ctx.Game.State.Var(name) == nil { diff --git a/inc/script.awakening_finale.go b/inc/script.awakening_finale.go index 442d344..c6cebee 100644 --- a/inc/script.awakening_finale.go +++ b/inc/script.awakening_finale.go @@ -15,7 +15,7 @@ func init() { UseTheme(NokiaPunk), inkwell.Say("norman", "No — this isn't what you were supposed to do!"), inkwell.Wait(0.6), - TapeSay(TapeMystery, "Thank you, Paul. You did exactly what I asked, the whole way through."), + inkwell.Say(TapeMystery, "Thank you, Paul. You did exactly what I asked, the whole way through."), inkwell.Wait(1.0), inkwell.ShowEnd("REAL WORLD — end of game two"), ), diff --git a/inc/script.opening.go b/inc/script.opening.go index c5cb33d..c1b420a 100644 --- a/inc/script.opening.go +++ b/inc/script.opening.go @@ -10,7 +10,7 @@ func init() { Actions: inkwell.Seq( inkwell.SetFlag(FlagPoliceTip), inkwell.Say(Paul, "Back alley. Just like the clerk said."), - TapeSay(Dex, "A government office tipped you off to remove something from a scene. That doesn't strike you as odd?"), + inkwell.Say(Dex, "A government office tipped you off to remove something from a scene. That doesn't strike you as odd?"), ), }) } diff --git a/inc/script.tape_insert.go b/inc/script.tape_insert.go index 0dbbb40..5b7c843 100644 --- a/inc/script.tape_insert.go +++ b/inc/script.tape_insert.go @@ -18,8 +18,8 @@ func init() { ctx.Game.State.SetVar(VarArmillaStrip, "SLOT2: READING") }), inkwell.Say(Paul, "Right. Let's see who you are."), - TapeSay(Dex, "Clicked in. Spinning. And now: nothing."), - TapeSay(Dex, "A stranger's tape in your own Armilla, Paul. I hope you know what you're doing."), + inkwell.Say(Dex, "Clicked in. Spinning. And now: nothing."), + inkwell.Say(Dex, "A stranger's tape in your own Armilla, Paul. I hope you know what you're doing."), ), }) } diff --git a/inc/script.usewith_fail.go b/inc/script.usewith_fail.go new file mode 100644 index 0000000..64281db --- /dev/null +++ b/inc/script.usewith_fail.go @@ -0,0 +1,59 @@ +package inc + +import ( + "math/rand" + + inkwell "git.teletypegames.org/engines/inkwell" +) + +func useWithFail(item string, h *inkwell.Hotspot) inkwell.Action { + key := "fail." + item + "." + h.Name + World.G.State.NoteTalked(key) + n := World.G.State.Talked(key) + + return inkwell.Seq( + inkwell.Say(Paul, pick(paulFails, n)), + inkwell.Say(Dex, dexFail(n)), + ) +} + +var paulFails = []string{ + "No. That's not going to work.", + "Tried that. It didn't improve.", + "All right, I know that one doesn't work.", + "Now it's personal.", +} + +var dexDry = []string{ + "No. Not like that.", + "I heard it. Nothing happened.", +} + +var dexTease = []string{ + "Twice the same. The second one rarely goes better.", + "Do it a few more times, maybe physics reconsiders.", +} + +func dexFail(n int) string { + switch { + case n <= 1: + return pick(dexDry, n) + case n == 2: + return pick(dexTease, n) + default: + return "Paul. Leave it. Look again at what you're carrying — that's where it is." + } +} + +func pick(s []string, n int) string { + switch { + case len(s) == 0: + return "" + case n <= 0: + return s[0] + case n-1 < len(s): + return s[n-1] + default: + return s[rand.Intn(len(s))] + } +} diff --git a/inc/tape.dex.go b/inc/tape.dex.go new file mode 100644 index 0000000..ae0dbc6 --- /dev/null +++ b/inc/tape.dex.go @@ -0,0 +1,8 @@ +package inc + +func init() { + TapeManager.Register(Tape{ + Name: Dex, + Dialogue: DlgDex, + }) +} diff --git a/inc/tape.manager.go b/inc/tape.manager.go new file mode 100644 index 0000000..50e1a92 --- /dev/null +++ b/inc/tape.manager.go @@ -0,0 +1,32 @@ +package inc + +type Tape struct { + Name string + Item string + Dialogue string +} + +func (t Tape) GetName() string { return t.Name } + +func IsTape(name string) bool { return TapeManager.Has(name) } + +func TapeOf(item string) (Tape, bool) { + for _, t := range TapeManager.All() { + if t.Item != "" && t.Item == item { + return t, true + } + } + return Tape{}, false +} + +func IsTapeItem(item string) bool { + _, ok := TapeOf(item) + return ok +} + +func TapeDialogue(item string) string { + if t, ok := TapeOf(item); ok && t.Dialogue != "" { + return t.Dialogue + } + return DlgDex +} diff --git a/inc/tape.mystery.go b/inc/tape.mystery.go new file mode 100644 index 0000000..3ebf404 --- /dev/null +++ b/inc/tape.mystery.go @@ -0,0 +1,9 @@ +package inc + +func init() { + TapeManager.Register(Tape{ + Name: TapeMystery, + Item: ItemMysteryTape, + Dialogue: DlgMysteryTape, + }) +} diff --git a/inc/tape.support.go b/inc/tape.support.go new file mode 100644 index 0000000..1c575e2 --- /dev/null +++ b/inc/tape.support.go @@ -0,0 +1,7 @@ +package inc + +func init() { + TapeManager.Register(Tape{ + Name: TapeSupport, + }) +} diff --git a/inc/ui.text.go b/inc/ui.text.go deleted file mode 100644 index a62c6c7..0000000 --- a/inc/ui.text.go +++ /dev/null @@ -1,107 +0,0 @@ -package inc - -import ( - "image" - "image/color" - "unicode/utf8" - - "github.com/hajimehoshi/ebiten/v2" - "github.com/hajimehoshi/ebiten/v2/ebitenutil" -) - -const ( - glyphW = 6 - glyphH = 16 -) - -var scratch *ebiten.Image - -func textW(s string) int { return utf8.RuneCountInString(s) * glyphW } - -func drawTextC(dst *ebiten.Image, s string, x, y int, c color.Color) { - if s == "" { - return - } - w, h := textW(s)+glyphW, glyphH - if scratch == nil || scratch.Bounds().Dx() < w || scratch.Bounds().Dy() < h { - nw, nh := w, h - if nw < 320 { - nw = 320 - } - scratch = ebiten.NewImage(nw, nh) - } - scratch.Clear() - ebitenutil.DebugPrintAt(scratch, s, 0, 0) - - if c == nil { - c = color.White - } - r, g, b, a := c.RGBA() - op := &ebiten.DrawImageOptions{} - op.GeoM.Translate(float64(x), float64(y)) - op.ColorScale.Scale( - float32(r)/0xffff, float32(g)/0xffff, float32(b)/0xffff, float32(a)/0xffff, - ) - dst.DrawImage(scratch.SubImage(image.Rect(0, 0, w, h)).(*ebiten.Image), op) -} - -func wrap(s string, maxPx int) []string { - if maxPx <= glyphW || textW(s) <= maxPx { - return []string{s} - } - var out []string - line, word := "", "" - flush := func() { - if line != "" { - out = append(out, line) - line = "" - } - } - emit := func() { - if word == "" { - return - } - switch { - case line == "": - line = word - case textW(line+" "+word) <= maxPx: - line += " " + word - default: - flush() - line = word - } - word = "" - } - for _, r := range s { - switch r { - case ' ': - emit() - case '\n': - emit() - flush() - default: - word += string(r) - } - } - emit() - flush() - if len(out) == 0 { - return []string{s} - } - return out -} - -func clip(s string, maxPx int) string { - r := []rune(s) - max := maxPx / glyphW - switch { - case max <= 0: - return "" - case len(r) <= max: - return s - case max == 1: - return string(r[:1]) - default: - return string(r[:max-1]) + "…" - } -} diff --git a/inc/widget.action_pump.go b/inc/widget.action_pump.go deleted file mode 100644 index a119093..0000000 --- a/inc/widget.action_pump.go +++ /dev/null @@ -1,21 +0,0 @@ -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.hud_frame.go b/inc/widget.hud_frame.go index ac32b5e..0eda796 100644 --- a/inc/widget.hud_frame.go +++ b/inc/widget.hud_frame.go @@ -16,14 +16,12 @@ 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) GetName() string { return h.Name } +func (h *hudFrame) VisibleWhen() inkwell.Condition { return whenPlaying } +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) @@ -32,6 +30,4 @@ func (h *hudFrame) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) { 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 -} +func (h *hudFrame) BlocksClickAt(p inkwell.Point) bool { return p.Y >= HUDTop } diff --git a/inc/widget.inventory_bar.go b/inc/widget.inventory_bar.go index 3f12cb8..104f4ba 100644 --- a/inc/widget.inventory_bar.go +++ b/inc/widget.inventory_bar.go @@ -5,8 +5,9 @@ import ( ) func init() { - WidgetManager.Register(gated("inventory", &inkwell.InventoryBar{ + WidgetManager.Register(&inkwell.InventoryBar{ Name: "inventory", + When: whenPlaying, Origin: inkwell.Point{ X: Pad + 2, Y: invY, @@ -15,5 +16,5 @@ func init() { Cols: 4, SlotSize: invSlot, Gap: invGap, - })) + }) } diff --git a/inc/widget.letterbox.go b/inc/widget.letterbox.go index 04a3dd0..90478c8 100644 --- a/inc/widget.letterbox.go +++ b/inc/widget.letterbox.go @@ -19,23 +19,20 @@ type letterbox struct { Bar float64 } -func (l *letterbox) GetName() string { return l.Name } -func (l *letterbox) Layer() inkwell.Layer { return inkwell.LayerScene } +func (l *letterbox) GetName() string { return l.Name } +func (l *letterbox) VisibleWhen() inkwell.Condition { return whenCutscene } +func (l *letterbox) Layer() inkwell.Layer { return inkwell.LayerScene } func (l *letterbox) Tick(ctx *inkwell.UICtx) { - if World.Mode() != ModeCutscene || !World.TapeWaiting() { + if !World.TapeWaiting() { return } - if inpututil.IsKeyJustPressed(ebiten.KeySpace) { World.TakeTapeOffer() } } func (l *letterbox) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) { - if World.Mode() != ModeCutscene { - return - } g := ctx.Game th := g.Theme() bar := l.Bar @@ -48,7 +45,7 @@ func (l *letterbox) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) { vector.DrawFilledRect(dst, 0, h-float32(bar), w, float32(bar), black, false) if World.TapeWaiting() { - msg := "SPACE — " + TapeDisplayName(Dex) + " has something to say" - drawTextC(dst, msg, g.Width-textW(msg)-4, g.Height-int(bar)+4, th.ChatLogResponse) + msg := "SPACE — " + g.CharacterLabel(Dex) + " has something to say" + g.DrawText(dst, msg, g.Width-inkwell.TextWidth(msg)-4, g.Height-int(bar)+4, th.ChatLogResponse) } } diff --git a/inc/widget.manager.go b/inc/widget.manager.go index 79d4a9e..562c209 100644 --- a/inc/widget.manager.go +++ b/inc/widget.manager.go @@ -2,7 +2,6 @@ package inc import ( inkwell "git.teletypegames.org/engines/inkwell" - "github.com/hajimehoshi/ebiten/v2" ) type Widget = inkwell.Widget @@ -11,7 +10,7 @@ const ( ScreenW = 640 ScreenH = 380 - LineH = glyphH + 2 + LineH = inkwell.GlyphH + 2 Pad = 6 TopBarH = LineH + 2 @@ -28,38 +27,8 @@ const ( 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) -} +var ( + whenPlaying = inkwell.VarEq(VarMode, string(ModePlay)) + whenCutscene = inkwell.VarEq(VarMode, string(ModeCutscene)) + whenUnpaused = inkwell.And(whenPlaying, inkwell.Not(inkwell.VarEq(VarNote, NotePaused))) +) diff --git a/inc/widget.scene_nav.go b/inc/widget.scene_nav.go index 98a9bbb..3e6a81f 100644 --- a/inc/widget.scene_nav.go +++ b/inc/widget.scene_nav.go @@ -2,52 +2,11 @@ package inc import ( inkwell "git.teletypegames.org/engines/inkwell" - "github.com/hajimehoshi/ebiten/v2" - "github.com/hajimehoshi/ebiten/v2/inpututil" ) func init() { - WidgetManager.Register(&sceneNav{ + WidgetManager.Register(&inkwell.SceneNav{ Name: "scene_nav", + When: whenUnpaused, }) } - -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) { - if !World.HUDVisible() || World.Paused() { - return - } - step := 0 - - switch { - case inpututil.IsKeyJustPressed(ebiten.KeyArrowRight): - step = 1 - case inpututil.IsKeyJustPressed(ebiten.KeyArrowLeft): - step = -1 - default: - return - } - if next := n.neighbour(ctx.Game, step); next != "" { - World.Do(inkwell.GoTo(next)) - } -} - -func (n *sceneNav) neighbour(g *inkwell.Game, step int) string { - deck := g.SceneManager.Names() - if len(deck) < 2 { - return "" - } - for i, name := range deck { - if name == World.G.CurrentScene() { - return deck[((i+step)%len(deck)+len(deck))%len(deck)] - } - } - return deck[0] -} diff --git a/inc/widget.status_line.go b/inc/widget.status_line.go index 8832f57..664f590 100644 --- a/inc/widget.status_line.go +++ b/inc/widget.status_line.go @@ -5,10 +5,11 @@ import ( ) func init() { - WidgetManager.Register(gated("status", &inkwell.StatusLine{ + WidgetManager.Register(&inkwell.StatusLine{ Name: "status", + When: whenPlaying, Y: statusY, Align: inkwell.AlignLeft, ScreenWidth: DividerX, - })) + }) } diff --git a/inc/widget.tape_channel.go b/inc/widget.tape_channel.go index aab29a1..e82a728 100644 --- a/inc/widget.tape_channel.go +++ b/inc/widget.tape_channel.go @@ -20,19 +20,15 @@ type tapeChannel struct { Bounds inkwell.Rectangle } -func (t *tapeChannel) GetName() string { return t.Name } -func (t *tapeChannel) Layer() inkwell.Layer { return inkwell.LayerHUD } +func (t *tapeChannel) GetName() string { return t.Name } +func (t *tapeChannel) VisibleWhen() inkwell.Condition { return whenPlaying } +func (t *tapeChannel) Layer() inkwell.Layer { return inkwell.LayerHUD } func (t *tapeChannel) Tick(ctx *inkwell.UICtx) {} -func (t *tapeChannel) BlocksClickAt(p inkwell.Point) bool { - return World.HUDVisible() && t.Bounds.Contains(p) -} +func (t *tapeChannel) BlocksClickAt(p inkwell.Point) bool { return t.Bounds.Contains(p) } func (t *tapeChannel) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) { - if !World.HUDVisible() { - return - } g := ctx.Game th := g.Theme() b := t.Bounds @@ -44,7 +40,7 @@ func (t *tapeChannel) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) { vector.DrawFilledRect(dst, float32(b.X), float32(b.Y), float32(b.W), float32(b.H), th.ChatLogBG, false) speaker, speakerCol := t.lastSpeaker(g, th) - drawTextC(dst, speaker, int(b.X)+pad, int(b.Y)+pad, speakerCol) + g.DrawText(dst, speaker, int(b.X)+pad, int(b.Y)+pad, speakerCol) hy := float32(b.Y+pad+lh) + 2 vector.StrokeLine(dst, float32(b.X)+pad, hy, float32(b.X+b.W)-pad, hy, 1, th.CharacterPanelBorder, false) @@ -70,7 +66,7 @@ func (t *tapeChannel) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) { default: col, txt = th.ChatLogSystem, m.Text } - for _, wl := range wrap(txt, wrapW) { + for _, wl := range inkwell.WrapText(txt, wrapW) { rendered = append(rendered, line{ text: wl, col: col, @@ -87,7 +83,7 @@ func (t *tapeChannel) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) { rendered = rendered[start:] } for i, ln := range rendered { - drawTextC(dst, ln.text, int(b.X)+pad, top+i*lh, ln.col) + g.DrawText(dst, ln.text, int(b.X)+pad, top+i*lh, ln.col) } } @@ -96,10 +92,10 @@ func (t *tapeChannel) lastSpeaker(g *inkwell.Game, th inkwell.Theme) (string, co for i := len(msgs) - 1; i >= 0; i-- { m := msgs[i] if m.Kind == inkwell.LogResponse && IsTape(m.Speaker) { - return TapeDisplayName(m.Speaker), speechColor(g, m.Speaker, th.ChatLogResponse) + return g.CharacterLabel(m.Speaker), speechColor(g, m.Speaker, th.ChatLogResponse) } } - return TapeDisplayName(Dex), speechColor(g, Dex, th.ChatLogResponse) + return g.CharacterLabel(Dex), speechColor(g, Dex, th.ChatLogResponse) } func speechColor(g *inkwell.Game, name string, fallback color.Color) color.Color { diff --git a/inc/widget.tape_slots.go b/inc/widget.tape_slots.go index 3ad221b..8b3db74 100644 --- a/inc/widget.tape_slots.go +++ b/inc/widget.tape_slots.go @@ -18,12 +18,11 @@ type tapeSlots struct { Bounds inkwell.Rectangle } -func (t *tapeSlots) GetName() string { return t.Name } -func (t *tapeSlots) Layer() inkwell.Layer { return inkwell.LayerHUD } +func (t *tapeSlots) GetName() string { return t.Name } +func (t *tapeSlots) VisibleWhen() inkwell.Condition { return whenPlaying } +func (t *tapeSlots) Layer() inkwell.Layer { return inkwell.LayerHUD } -func (t *tapeSlots) BlocksClickAt(p inkwell.Point) bool { - return World.HUDVisible() && t.Bounds.Contains(p) -} +func (t *tapeSlots) BlocksClickAt(p inkwell.Point) bool { return t.Bounds.Contains(p) } func (t *tapeSlots) slotRects() (inkwell.Rectangle, inkwell.Rectangle) { b := t.Bounds @@ -32,16 +31,13 @@ func (t *tapeSlots) slotRects() (inkwell.Rectangle, inkwell.Rectangle) { } func (t *tapeSlots) Tick(ctx *inkwell.UICtx) { - if !World.HUDVisible() { - return - } g := ctx.Game mp := g.Input.Point() r1, r2 := t.slotRects() switch { case r1.Contains(mp): - g.SetHoverLabel(TapeDisplayName(Dex)) + g.SetHoverLabel(g.CharacterLabel(Dex)) case r2.Contains(mp): if s := World.Slot2(); s != "" { g.SetHoverLabel(itemLabel(g, s)) @@ -68,12 +64,12 @@ func (t *tapeSlots) Tick(ctx *inkwell.UICtx) { World.Do(inkwell.RunScript(ScriptTapeInsert)) return } - World.Do(TapeSay(Dex, "That isn't a tape, Paul. That's an object. There is a difference.")) + World.Do(inkwell.Say(Dex, "That isn't a tape, Paul. That's an object. There is a difference.")) return } if !slot2 && sel != "" { g.Inventory.Select("") - World.Do(TapeSay(Dex, "The first slot is mine. I'm not moving.")) + World.Do(inkwell.Say(Dex, "The first slot is mine. I'm not moving.")) return } @@ -87,24 +83,21 @@ func (t *tapeSlots) Tick(ctx *inkwell.UICtx) { World.Do(Paused(inkwell.RunDialogue(TapeDialogue(s)))) return } - World.Do(TapeSay(Dex, "Empty. Nobody to talk to.")) + World.Do(inkwell.Say(Dex, "Empty. Nobody to talk to.")) default: if !slot2 { - World.Do(TapeSay(Dex, "Me. Four kilobytes of a dead man. Be grateful.")) + World.Do(inkwell.Say(Dex, "Me. Four kilobytes of a dead man. Be grateful.")) return } if s := World.Slot2(); s != "" { - World.Do(TapeSay(Dex, "That is the second slot. Be careful what you let in.")) + World.Do(inkwell.Say(Dex, "That is the second slot. Be careful what you let in.")) return } - World.Do(TapeSay(Dex, "The second slot is empty. That's the rarer state.")) + World.Do(inkwell.Say(Dex, "The second slot is empty. That's the rarer state.")) } } func (t *tapeSlots) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) { - if !World.HUDVisible() { - return - } g := ctx.Game th := g.Theme() r1, r2 := t.slotRects() @@ -118,11 +111,11 @@ func (t *tapeSlots) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) { vector.StrokeRect(dst, float32(r.X), float32(r.Y), float32(r.W), float32(r.H), 1, border, false) x, inner := int(r.X)+Pad, int(r.W)-Pad*2 - drawTextC(dst, label, x, int(r.Y)+Pad/2, th.ChatLogSystem) - drawTextC(dst, clip(body, inner), x, int(r.Y)+Pad/2+LineH, col) + g.DrawText(dst, label, x, int(r.Y)+Pad/2, th.ChatLogSystem) + g.DrawText(dst, inkwell.ClipText(body, inner), x, int(r.Y)+Pad/2+LineH, col) } - drawSlot(r1, "SLOT 1", TapeDisplayName(Dex), true) + drawSlot(r1, "SLOT 1", g.CharacterLabel(Dex), true) if s := World.Slot2(); s != "" { drawSlot(r2, "SLOT 2", itemLabel(g, s), true) } else { diff --git a/inc/widget.top_bar.go b/inc/widget.top_bar.go index 6c0ab48..ccd79ab 100644 --- a/inc/widget.top_bar.go +++ b/inc/widget.top_bar.go @@ -5,23 +5,11 @@ import ( ) 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) + WidgetManager.Register(&inkwell.TopBar{ + Name: "topbar", + When: whenPlaying, + Height: TopBarH, + TimeVar: VarArmillaStrip, + NoteVar: VarNote, + }) } diff --git a/inc/widget.usewith_guard.go b/inc/widget.usewith_guard.go deleted file mode 100644 index fcdcb3f..0000000 --- a/inc/widget.usewith_guard.go +++ /dev/null @@ -1,110 +0,0 @@ -package inc - -import ( - "math/rand" - - inkwell "git.teletypegames.org/engines/inkwell" - "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) { - g := ctx.Game - if !World.HUDVisible() || !g.Input.LeftClicked() { - return - } - sel := g.Inventory.Selected() - if sel == "" { - return - } - h := g.HotspotAt(g.Input.Point()) - if h == nil || hasAuthoredPair(g, h, sel) { - return - } - - g.Input.ConsumeLeft() - g.Inventory.Select("") - - label := h.Label - if label == "" { - label = h.Name - } - g.LogAction("> Use " + itemLabel(g, sel) + " on: " + label) - - key := "fail." + sel + "." + h.Name - g.State.NoteTalked(key) - n := g.State.Talked(key) - - World.Do(inkwell.Seq( - inkwell.Say(Paul, pick(paulFails, n)), - TapeSay(Dex, dexFail(n)), - )) -} - -func hasAuthoredPair(g *inkwell.Game, h *inkwell.Hotspot, item string) bool { - if h.OnUseWith != nil { - if a, ok := h.OnUseWith[item]; ok && a != nil { - return true - } - } - if it, ok := g.ItemManager.Get(item); ok && it.OnUseWith != nil { - if a, ok := it.OnUseWith[h.Name]; ok && a != nil { - return true - } - } - return false -} - -var paulFails = []string{ - "No. That's not going to work.", - "Tried that. It didn't improve.", - "All right, I know that one doesn't work.", - "Now it's personal.", -} - -var dexDry = []string{ - "No. Not like that.", - "I heard it. Nothing happened.", -} - -var dexTease = []string{ - "Twice the same. The second one rarely goes better.", - "Do it a few more times, maybe physics reconsiders.", -} - -func dexFail(n int) string { - switch { - case n <= 1: - return pick(dexDry, n) - case n == 2: - return pick(dexTease, n) - default: - return "Paul. Leave it. Look again at what you're carrying — that's where it is." - } -} - -func pick(s []string, n int) string { - switch { - case len(s) == 0: - return "" - case n <= 0: - return s[0] - case n-1 < len(s): - return s[n-1] - default: - return s[rand.Intn(len(s))] - } -} diff --git a/inc/widget.window_sizer.go b/inc/widget.window_sizer.go deleted file mode 100644 index d5d611d..0000000 --- a/inc/widget.window_sizer.go +++ /dev/null @@ -1,35 +0,0 @@ -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 db4b72b..50368d9 100644 --- a/inc/world.action.go +++ b/inc/world.action.go @@ -4,42 +4,7 @@ import ( inkwell "git.teletypegames.org/engines/inkwell" ) -func (w *world) Do(a inkwell.Action) { - if a != nil { - w.queue = append(w.queue, a) - } -} - -func (w *world) PumpTick(dt float64) { - if w.running == nil { - if len(w.queue) == 0 { - return - } - w.running = w.queue[0].Start() - w.queue = w.queue[1:] - } - ctx := &inkwell.Ctx{ - Game: w.G, - DT: dt, - } - if s, ok := w.G.SceneManager.Get(w.G.CurrentScene()); ok { - ctx.Scene = &s - } - if w.running.Tick(ctx) != inkwell.StatusRunning { - w.running = nil - } -} - -func (w *world) SceneTitle() string { - s, ok := w.G.SceneManager.Get(w.G.CurrentScene()) - if !ok { - return "" - } - if s.Title != "" { - return s.Title - } - return s.Name -} +func (w *world) Do(a inkwell.Action) { w.G.Do(a) } type fnAction struct{ fn func(*inkwell.Ctx) } @@ -70,42 +35,6 @@ func SetMode(m Mode) inkwell.Action { return Fn(func(*inkwell.Ctx) { World.SetMode(m) }) } -func TapeSay(speaker, text string) inkwell.Action { - return &tapeSayAction{ - speaker: speaker, - text: text, - } -} - -type tapeSayAction struct{ speaker, text string } - -func (a *tapeSayAction) Start() inkwell.Runner { - return &tapeSayRunner{ - spec: a, - } -} - -type tapeSayRunner struct { - spec *tapeSayAction - started bool - elapsed float64 - duration float64 -} - -func (r *tapeSayRunner) Tick(ctx *inkwell.Ctx) inkwell.Status { - if !r.started { - r.started = true - - r.duration = 1.2 + float64(len([]rune(r.spec.text)))*0.05 - ctx.Game.LogResponse(r.spec.speaker, r.spec.text) - } - r.elapsed += ctx.DT - if r.elapsed >= r.duration { - return inkwell.StatusDone - } - return inkwell.StatusRunning -} - func TapeOffer(line inkwell.Action) inkwell.Action { return Fn(func(*inkwell.Ctx) { World.tapeLine = line @@ -149,11 +78,11 @@ type pausedRunner struct { func (r *pausedRunner) Tick(ctx *inkwell.Ctx) inkwell.Status { if !r.started { r.started = true - World.paused = true + World.SetNote(NotePaused) } s := r.inner.Tick(ctx) if s != inkwell.StatusRunning { - World.paused = false + World.SetNote("") } return s } diff --git a/inc/world.manager.go b/inc/world.manager.go index f04783d..e382b6f 100644 --- a/inc/world.manager.go +++ b/inc/world.manager.go @@ -15,12 +15,7 @@ const ( type world struct { G *inkwell.Game - mode Mode - - paused bool pending string - queue []inkwell.Action - running inkwell.Runner tapeWaiting bool tapeLine inkwell.Action @@ -30,22 +25,25 @@ var World = &world{} func (w *world) attach(g *inkwell.Game) { w.G = g - w.mode = ModePlay - w.paused = false w.pending = "" - w.queue = nil - w.running = nil w.tapeWaiting = false w.tapeLine = nil + w.SetMode(ModePlay) + w.SetNote("") } -func (w *world) Mode() Mode { return w.mode } +func (w *world) Mode() Mode { + if v, ok := w.G.State.Var(VarMode).(string); ok { + return Mode(v) + } + return ModePlay +} -func (w *world) SetMode(m Mode) { w.mode = m } +func (w *world) SetMode(m Mode) { w.G.State.SetVar(VarMode, string(m)) } -func (w *world) HUDVisible() bool { return w.mode == ModePlay } +func (w *world) SetNote(text string) { w.G.State.SetVar(VarNote, text) } -func (w *world) Paused() bool { return w.paused } +func (w *world) Paused() bool { return w.G.State.Var(VarNote) == NotePaused } func (w *world) Slot2() string { if v, ok := w.G.State.Var(VarSlot2).(string); ok {