more refact
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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/<kind>.<name>.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:<target>`, 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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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=
|
||||
|
||||
+6
-1
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ func init() {
|
||||
W: 28,
|
||||
H: 68,
|
||||
Start: inkwell.Point{
|
||||
X: 120,
|
||||
X: 320,
|
||||
Y: 232,
|
||||
},
|
||||
SpeechColor: Ink,
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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."),
|
||||
),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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."),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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."),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
+4
-29
@@ -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
|
||||
}
|
||||
|
||||
+4
-4
@@ -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?"),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"),
|
||||
),
|
||||
|
||||
@@ -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?"),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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."),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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))]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package inc
|
||||
|
||||
func init() {
|
||||
TapeManager.Register(Tape{
|
||||
Name: Dex,
|
||||
Dialogue: DlgDex,
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package inc
|
||||
|
||||
func init() {
|
||||
TapeManager.Register(Tape{
|
||||
Name: TapeMystery,
|
||||
Item: ItemMysteryTape,
|
||||
Dialogue: DlgMysteryTape,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package inc
|
||||
|
||||
func init() {
|
||||
TapeManager.Register(Tape{
|
||||
Name: TapeSupport,
|
||||
})
|
||||
}
|
||||
-107
@@ -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]) + "…"
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
@@ -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 }
|
||||
|
||||
@@ -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,
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+6
-37
@@ -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)))
|
||||
)
|
||||
|
||||
+2
-43
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+14
-21
@@ -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 {
|
||||
|
||||
+7
-19
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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))]
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
+3
-74
@@ -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
|
||||
}
|
||||
|
||||
+11
-13
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user