Files
realworld/README.md
T
mr.zero 45dac473fd
ci/woodpecker/push/ebitengine Pipeline was successful
game skeleton
2026-08-29 19:16:40 +02:00

219 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Real World
Point & click adventure — game two of the Norman Arc. Engine:
[inkwell](../../engines/inkwell).
The player is **Paul**, a junk dealer investigating what happened to Norman from
the outside. His companion is **Dex**, a Personal Tape riding in slot one of
Paul's **Armilla**. The world, the terminology and the entity catalogue live in
the wiki (`services/wiki-pages/pages/projects/realworld` and
`.../neumatronic-universe`); this file records the game-specific UI decisions.
## Running
inkwell is consumed as an ordinary Go module from the forge. Prerequisites:
Go 1.26+ and SSH access to `git.teletypegames.org`.
```bash
git clone ssh://git@git.teletypegames.org:2222/games/realworld
cd realworld
export GOPRIVATE=git.teletypegames.org # the Makefile sets this for its own targets
go mod tidy
go run .
```
```bash
go run . # the alley scene (beat 3)
go run . -finale # the finale — try the Nokia-punk theme switch
go run . -scene alley # pick a starting scene
```
To work against a local inkwell checkout, add a `replace` line temporarily —
and take it out before committing:
```bash
go mod edit -replace git.teletypegames.org/engines/inkwell=../../engines/inkwell
go mod edit -dropreplace git.teletypegames.org/engines/inkwell
```
## Building
```bash
make build # native binary into bin/
make test # headless: validates content and HUD layout
make wasm # dist/game.wasm + wasm_exec.js
make export VERSION=0.1 # zipped HTML/WASM bundle
make binaries VERSION=0.1 # win-x86, win-x64, linux-x64 zips
make clean
```
CI is Woodpecker; `.woodpecker.yaml` only names the platform, and the update
server serves the actual pipeline (`GET /build/config?platform=ebitengine`).
Release metadata lives in `metadata.json`.
| Control | |
|---|---|
| left click | run the selected verb |
| right click | verb coin (Look / Use / Talk / Take) |
| `F1` | toggle hotspot outlines |
| `SPACE` | during a cutscene: let the tape speak, if it offered |
Screenshots: `EBITEN_SCREENSHOT_KEY=q go run .`, then press `q` in the window.
## Layout
Internal resolution is **640×400** — the same 16:10 as the wiki's "320×200 VGA
look" brief and exactly 2× it, so art drawn at 320×200 upscales cleanly.
Integer scaling only; the window opens at 2×.
The doubling is forced by type, not taste. inkwell renders text through a fixed
**6×16** debug font that the domain cannot replace. At 320×200 a 16px glyph is
8% of the screen height: rows overlap, and a top bar sized from the wireframe
deck's proportions cannot hold a single line. At 640×400 the same glyph lands
at 4%, which is the proportion the design was drawn for.
```
0 394│396 639
┌────────────────────────────────────────────────────────────────────┐
0 │ ALLEY — paused SRP: 1 tape │ TopBar (20)
20 ├────────────────────────────────────────────────────────────────────┤
│ scene — hotspots, characters, SpeechBubble │ Scene (244)
264├─────────────────────────────────────────┬──────────────────────────┤
│ Use hook on: floor grate │ DEX │
│ ┌───┬───┬───┬───┐ ┌───────┐┌────────┐ │ "The hook is a hand │ HUD (136)
│ └───┴───┴───┴───┘ │ 1 DEX ││ 2 — │ │ shorter than the gap." │
399└─────────────────────────────────────────┴──────────────────────────┘
InventoryBar TapeSlots TapeChannel
```
Every vertical measurement derives from `ui.LineH` (glyph cell + leading), not
from the deck's ratios, so the layout cannot drift out of step with the font
again — `TestLayoutFitsTheFont` guards it. The tape channel comes out at 5 rows
of 38 columns, which is about two of Dex's remarks on screen at once.
The vertical divider at x=394 is the one proportion carried over from the
wireframe deck: 61.6% world, 38.4% tape channel. The deck's *vertical*
proportions are not carried over.
The dialogue box deliberately spans only the left region, so a tape can comment
*alongside* a conversation.
## The rule everything follows
Canon's central idea is the dual architecture: every machine has a digital side
and an ACP side, and the "agent-mediated focus" turns that into a UI paradigm —
one main text focus plus a side status panel, with the hand typing at the
digital side and the voice speaking to the ACP.
> **The hand holds the world. The voice speaks to the tape.**
>
> `WORLD` — mouse, the four verbs, inventory → Paul acts.
> `TAPE` — the `talk` verb and tape commentary → nothing happens in the world.
>
> There is always a visible, continuous separator between them, and the tape
> side never carries a button that reaches into the world.
Two consequences worth stating: there is **no text input anywhere** (the tape
channel is voice, opened with `talk`), and **colour carries the rule** — the
world and Paul speak in bone white, tapes in amber. If something is amber, a
tape said it.
There is also no "token" economy. The wireframe deck metered advice, but canon
has no such concept and the wiki describes Dex as commenting continuously. If
scarcity is ever wanted, the canon-native lever is the operator licence —
without one a Personal Tape runs in restricted mode, and Paul certainly has no
licence.
## Structure
```
main.go flags + inkwell.Run
internal/names/ entity names and world-state keys
internal/theme/ realworld-93 + nokia-punk
internal/world/ unsaved runtime state, custom actions, action pump
internal/ui/ HUD: layout, custom widgets, coloured text
internal/content/ one file per registered entity, by kind
internal/boot/ wiring
```
`world` deliberately knows nothing about the HUD or the content — both build on
it, so the reverse would be a cycle:
```
names ← theme ← world ← ui
↑ ↑
content ──┴── boot ← main
```
Content is one registered entity per file, grouped by kind:
```
internal/content/
asset/ alley_background.go
character/ paul.go dex.go mystery_tape.go support_tape.go
item/ noodle_letter.go black_market_armilla.go mystery_tape.go
dialog/ dex_talk.go mystery_tape_silent.go
script/ tape_insert.go awakening_finale.go
scene/ alley.go
```
Adding a scene means adding `scene/<name>.go` and one line in `scene/scene.go`.
Nothing else moves.
## Engine workarounds
Five inkwell limits turned up during implementation that the engine README does
not mention. All five are worked around on the domain side; each is a candidate
for a small engine change.
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 in `world/action.go` 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. **No exported `CurrentScene()`**, but the pump needs `Ctx.Scene`.
*Workaround:* `world.EnterScene` is the first step of every scene's
`OnEnter`; the domain tracks it.
5. **Widgets have no `Visible` field and the `Manager` cannot unregister**, so
the built-in HUD cannot be hidden during a cutscene. *Workaround:* the
`gate` wrapper in `ui/ui.go` forwards `Tick`/`Draw`/`BlocksClickAt` only
while the HUD is visible.
6. **`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.
Also: the inkwell README gives the module path as
`git.teletypegames.org/games/inkwell`; the real path per its `go.mod` is
`git.teletypegames.org/engines/inkwell`.
## Known gaps
- **No art.** Everything is an inkwell placeholder.
- **Spent dialogue choices are hidden, not struck through.** `DialogueChoice.Once`
hides them; the deck wanted them struck through but visible, so the list
becomes a memory of what you already tried. Needs a thin override over
`DialogBox`.
- **Only beat 3 exists.** The alley is a vertical slice; `boot.opening` sets the
police-tip flag that beat 2 will eventually set.
- **The render has only been inspected once, by the author of this repo.**
Geometry is covered by tests and a layout dump, but this environment cannot
take a screenshot (both synthetic keystrokes and screen capture are blocked by
macOS privacy permissions), so every visual judgement has to come from you.