Compare commits

17 Commits
Author SHA1 Message Date
mr.zero c5c0c02c03 dirs
ci/woodpecker/push/ebitengine Pipeline was successful
2026-08-30 23:21:06 +02:00
mr.zero 28b1662195 more refact 2026-08-30 23:01:48 +02:00
mr.zero 3890edc946 widget manager refact 2026-08-30 22:27:51 +02:00
mr.zeroandClaude Opus 5 258cbf85c0 The opening is a script now, not a slice in the wiring
ci/woodpecker/push/ebitengine Pipeline was successful
bootOpening built its actions in boot.go and appended the finale by hand,
which made the one thing a player sees first the only content not
authored as content. inkwell's OnStart takes a script name now, so the
opening moves to script.opening.go and boot names it.

The -finale flag becomes OnFinale, the engine's hook for a closing script
queued after the start one, so the branch is a hook rather than a slice
append. bootOpening's start parameter had been dead for a while; it goes
with the function.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 11:26:47 +02:00
mr.zeroandClaude Opus 5 220985ec57 Declare the managers in boot.go, hand them to the engine whole
ci/woodpecker/push/ebitengine Pipeline was successful
boot.manager.go becomes boot.go and carries all seven managers as one
block, so what the game holds reads in one place instead of a line hidden
in each category file. The category files keep their entity alias.

The hand-off stops copying. Our managers are the same *inkwell.Manager[T]
the Game holds, so New assigns them onto it and engine and game share one
registry per category. content.manager.go is gone with the copy loop.

Themes stay additive — NewGame seeds four presets we would otherwise
throw away — and registerScene becomes prepareScene, which now has to run
before the hand-off: with no copy pass left, the scene defaults are
written back into SceneManager with Set.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 10:37:02 +02:00
mr.zeroandClaude Opus 5 a70dff8771 Write down where inkwell and its wiki live
ci/woodpecker/push/ebitengine Pipeline was successful
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 10:27:18 +02:00
mr.zeroandClaude Opus 5 07312f0f29 Drop the duplicate Manager, use inkwell's
ci/woodpecker/push/ebitengine Pipeline was successful
The engine already had the registry the game was reimplementing:
inkwell.Manager[T Named], and every entity type here is an alias of an
engine struct, so they all carry GetName() already. manager.manager.go
and manager.interface.go are gone; a category manager is now one call to
inkwell.NewManager.

registerAll goes with it — Each does the hand-off — and the selector
writes its derived pins back with Set, which keeps the scene in place in
the registration order.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 09:51:17 +02:00
mr.zero da1410cb4a screen to scene
ci/woodpecker/push/ebitengine Pipeline was successful
2026-08-30 00:15:26 +02:00
mr.zero 94649a38fe register logic 2026-08-29 23:59:56 +02:00
mr.zero ddea3b0893 remove test files 2026-08-29 23:36:20 +02:00
mr.zero 59decf54ef CLAUDE.md 2026-08-29 23:33:38 +02:00
mr.zero 1fa19f0b2e format fixes 2026-08-29 23:33:29 +02:00
mr.zero 954bd762f7 remove comments 2026-08-29 23:27:15 +02:00
mr.zero ec48cd6b37 new file structure 2026-08-29 23:25:36 +02:00
mr.zero bbc3faf3d7 tmp backgrounds
ci/woodpecker/push/ebitengine Pipeline was successful
2026-08-29 22:30:22 +02:00
mr.zero 45dac473fd game skeleton
ci/woodpecker/push/ebitengine Pipeline was successful
2026-08-29 19:16:40 +02:00
mr.zero 0d8918e6f5 game skeleton 2026-08-29 19:07:32 +02:00
125 changed files with 3361 additions and 2 deletions
+6
View File
@@ -0,0 +1,6 @@
bin
dist
*.zip
.version
saves
screenshot*.png
+2
View File
@@ -0,0 +1,2 @@
# The CI pipeline is served by the update server: GET /build/config?platform=ebitengine
platform: ebitengine
+402
View File
@@ -0,0 +1,402 @@
# Real World
A point & click adventure built on the inkwell engine (Ebitengine).
## Conventions
### No comments
Go source files carry no comments. Not doc comments, not inline comments, not
section headers. If a piece of code needs explaining, rename it or restructure
it until it does not, and put the reasoning in `README.md` instead.
The single exception is compiler directives (`//go:embed`, `//go:build`), which
are instructions to the toolchain rather than prose.
### English only
Everything written in the repository is in English: identifiers, string
literals, commit messages, `README.md`, this file. The design wiki is in
Hungarian; when a concept crosses over, translate it and keep the mapping
one-to-one.
### No tests
Do not write tests and do not add test files. Correctness is checked by running
the game.
### Struct literals are always multi-line
Every field of a struct instance goes on its own line, with a trailing comma —
never on the same line as the brace, never two fields on one line.
```go
return inkwell.Asset{
Name: BgStreet,
Path: "assets/bg/street.png",
Kind: inkwell.AssetImage,
}
```
Not `inkwell.Asset{Name: BgStreet, Path: "...", Kind: inkwell.AssetImage}`.
This holds for nested literals too, including small ones such as
`inkwell.Point`. Slice and map literals are not covered by the rule.
## Layout
One category, one package, one directory. A file keeps the category in its own
name anyway — `[category].[name].go` inside `inc/[category]/` — because the
prefix is what makes a file findable in a list of editor tabs, a grep, or a
diff, where the directory has already fallen off.
- The category is always **singular**: `item`, `scene`, `background`,
`character`, `tape`, `dialog`, `script`, `world`, `widget`, `theme`.
- `[category].manager.go` is the file that ties a package together — its entity
type, its `Manager`, and whatever else the category shares.
- Every other file in a package holds exactly one entity: one scene, one
background, one item. The file registers it itself, in an `init()`.
- Two files have no category. `inc/constants.go` is the vocabulary every
package imports, and `inc/boot/boot.go` builds the game.
```
main.go flags + inkwell.Run
inc/constants.go entity names and world-state keys
inc/boot/boot.go New(Opts) builds the game
inc/theme/theme.manager.go the Theme alias, its Manager, RGB/RGBA
inc/theme/theme.realworld.go realworld-93
inc/theme/theme.nokia_punk.go nokia-punk
inc/world/world.manager.go unsaved runtime state
inc/world/world.action.go custom actions
inc/tape/tape.manager.go the Tape entity and the lookups over it
inc/tape/tape.*.go one tape per file
inc/widget/widget.manager.go the Widget alias, HUD layout, the conditions
inc/widget/widget.*.go one widget per file, registering itself
inc/background/background.*.go one image asset per scene
inc/character/character.*.go the cast, tapes included
inc/item/item.*.go inventory
inc/dialog/dialog.*.go dialogue trees
inc/script/script.*.go named action sequences
inc/scene/scene.manager.go the Scene alias, its Manager, the floor
inc/scene/scene.selector.go the map screen: pins derived from the graph
inc/scene/scene.*.go one file per scene
```
`inc` is constants and nothing else, so every package can import it and it can
import none of them. That is also why `New` sits in `inc/boot` rather than in
`inc`: a package cannot be imported by what it imports, and the assembly is the
one thing that has to name every package at once.
## Managers
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.
Each one is declared in its own package's manager file, beside the alias it
holds, and it needs no prefix because the package already is one:
```go
// inc/character/character.manager.go
package character
type Character = inkwell.Character
var Manager = inkwell.NewManager[Character]()
```
Read from `inc/boot` the nine of them still line up as a list —
`background.Manager`, `character.Manager`, `dialog.Manager`, `item.Manager`,
`scene.Manager`, `script.Manager`, `tape.Manager`, `theme.Manager`,
`widget.Manager` — only now the list is a consequence of the imports rather
than a block someone has to keep in step.
Aliases of engine structs already carry `GetName()` and satisfy
`inkwell.Named`, which is the whole of what a manager asks of them.
The methods the game uses are `Register`, `Set`, `Get`, `All` and `Each`.
`Register` panics on a duplicate name — a second registration is a
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.
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 — `tape.Is`, `tape.Of`, `tape.IsItem`,
`tape.Dialogue`. 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
An entity file is a literal and nothing else. No constructor function, no list
somewhere else to keep in step — the file hands itself to its manager in an
`init()`:
```go
package background
func init() {
Manager.Register(Background{
Name: inc.BgServerFarm,
Path: "assets/bg/server_farm.png",
Kind: inkwell.AssetImage,
})
}
```
Adding an entity is adding a file. Deleting one is deleting a file. Package-level
variables are initialised before any `init()` runs, whatever file each sits in,
so a package's `Manager` exists by the time its first entity file registers into
one.
`init()` order is file-name order, so **registration order is alphabetical**.
Nothing may depend on it — including the order the arrow keys walk the scenes,
which is simply the order the files sit in.
An `init()` only runs if something imports the package. `inc/boot` names eight
of the nine for their `Manager`, which is import enough; `tape` is the one
nobody names, so it is there as a blank import. That list is **per package, not
per entity** — a new scene file still touches nothing but itself.
### Widgets name their layer instead of their order
A widget is a file like any other entity — `widget.<name>.go`, one widget,
registering itself in an `init()` — and that includes the engine's own widgets,
which the game registers as literals the same way it registers a background:
```go
func init() {
Manager.Register(&inkwell.Cursor{
Name: "cursor",
})
}
```
What a widget cannot take from alphabetical registration is its place in the
stack: the frame has to be drawn under the slots that sit on it, the cursor over
everything, and the use-with guard has to see a click after the HUD has had it.
So a widget declares a layer rather than inheriting one from the order it was
registered in — `inkwell.LayerScene`, `LayerPanel`, `LayerHUD`, `LayerSpeech`,
`LayerDialog`, `LayerMenu`, `LayerCurtain`, `LayerCursor`. The engine draws from
the bottom layer up and ticks from the top layer down, and registration order
only decides ties inside one layer:
```go
func (h *hudFrame) Layer() inkwell.Layer { return inkwell.LayerPanel }
```
A widget that says nothing sits on `LayerHUD`; ours all say it, because the
layer is the one thing about a widget the file cannot show.
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
Manager.Register(&inkwell.StatusLine{
Name: "status",
When: whenPlaying,
Y: statusY,
})
```
`whenPlaying`, `whenCutscene` and `whenUnpaused` are in `widget.manager.go`, and
they read `inc.VarMode` and `inc.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
Nothing is copied. `inkwell.NewGame` builds its own empty managers, and `New`
hands ours over in their place — the types are identical, so the engine and the
game end up sharing one registry per category rather than two in step:
```go
g.AssetManager = background.Manager
g.CharacterManager = character.Manager
g.DialogueManager = dialog.Manager
g.ItemManager = item.Manager
g.SceneManager = scene.Manager
g.ScriptManager = script.Manager
g.WidgetManager = widget.Manager
theme.Manager.Each(g.ThemeManager.Register)
```
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.
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
`New` names two scripts and nothing else — the opening a player sees is content
like every other script, in `script/script.opening.go`, not a slice built in the
wiring:
```go
g.StartAt(start)
g.OnStart(inc.ScriptOpening)
if o.Finale {
g.OnFinale(inc.ScriptFinale)
}
```
`OnFinale` is the engine's hook for a closing script, queued straight after the
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 = inc.Paul
g.Walkboxes = scene.Floor
g.UseWithFail = script.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/script.usewith_fail.go`.
## The world
There is exactly one game, so there is exactly one world — and since there is
exactly one, the **package is the singleton**. There is no `World` value to pass
around and no back-reference for a widget to hold: `world.Do(…)`,
`world.Slot2()`, `world.SetPending(…)` are reachable from anywhere that imports
`world`. `New` calls `world.Attach(g)`, which binds the engine and resets the
run 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 (`inc.VarMode`, `inc.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 package
variables is the two things the engine has no notion of: the tape waiting to
speak, and the cassette on its way into slot 2. Only `world.Attach` resets them.
This is what lets an entity file be a literal: the tape-insert script closes
over the `world` package, not over a parameter it would have had to be handed.
## Naming
The package is the namespace now, so an identifier never repeats what the
package already says. It is `scene.Floor`, not `scene.SceneFloor`;
`tape.Dialogue`, not `tape.TapeDialogue`; `widget.Manager`, not
`widget.WidgetManager`; `world.Do`, not `world.WorldDo`. Read the call site, not
the declaration, when choosing a name:
```go
scene.FillSelectorPins() tape.Is(speaker) world.Do(a) widget.ScreenW
```
Entities themselves need no name at all — they are anonymous literals inside
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:
`boot.New` and `boot.Opts`, the constants in `inc/constants.go`, each package's
`Manager` and entity type, the colour tokens, the tape lookups (`tape.Is`,
`tape.Of`, `tape.IsItem`, `tape.Dialogue`), the world (`world.Do`,
`world.Slot2`, …) and the action constructors (`world.Paused`, `world.SetMode`,
`world.UseTheme`, `world.TapeOffer`, `world.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 stays unexported, and now the compiler holds the line: the HUD
widgets (`tapeSlots`, `letterbox`, `hudFrame`, …), the conditions
(`whenPlaying`, `whenCutscene`, `whenUnpaused`), the runners, `setMode`,
`setNote`, `setVarIfEmpty`.
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
scene did not now lives in inkwell. "Screen" survives only where it means the
display: `ScreenW`, `ScreenH`.
## Layering
The layering is the import graph, so the compiler keeps it:
```
inc ← theme ← world ← tape ← widget ← boot ← main
↑ ↑ ↑
└── content ───────────────────-┘
```
`inc` imports nothing and everything imports it. `world` knows nothing about the
HUD or the content; both build on it, never the other way round — and an arrow
pointing back is now an import cycle, not a review comment. The one edge worth
knowing is `widget → tape`: the tape slots ask which cassette a tape is in, so
the tape concept sits below the HUD rather than beside it.
## Adding a scene
1. Constants in `inc/constants.go`: `Scene<Name>`, `Bg<Name>`
2. `inc/scene/scene.<name>.go` — an `init()` registering a `Scene`
3. `inc/background/background.<name>.go` — an `init()` registering a `Background`
4. A 640×380 PNG in `assets/bg/`
Nothing else moves. There is no list to update.
## The engine next door
inkwell is checked out beside this repository, and so is the wiki that
documents it. Paths are relative to the game's root:
```
../../engines/inkwell the engine source
../../services/wiki-pages/pages/engines/inkwell/default.en.md the manual, English
../../services/wiki-pages/pages/engines/inkwell/default.hu.md the manual, Hungarian
```
The engine's module path is `git.teletypegames.org/engines/inkwell` and `go.mod`
pins a pseudo-version of it, so a local edit is invisible here until it is
pushed:
```
git -C ../../engines/inkwell commit … && git -C ../../engines/inkwell push
go get git.teletypegames.org/engines/inkwell@master
```
A `replace` directive is fine while trying something out, but it never survives
into a commit — drop it and bump the pin instead.
Three things move together when the engine changes: the code, `README.md` in
the inkwell checkout, and **both** wiki pages. The two pages are a translation
pair, section for section — an edit to one is an edit to the other. The
no-comment rule stops at the module boundary: inkwell's own source is commented,
and edits there follow its style, not this one's.
## Commands
```
make build native binary into bin/
make wasm js/wasm build into dist/
make watch rebuild on change
go run . -scene <name> start on a given scene
go run . -finale start with the finale
```
Art is embedded into the binary (`//go:embed assets` in `main.go`), because
js/wasm has no OS filesystem and `make binaries` ships the executable alone.
The screen is 640×380 because the engine stretches a background over the whole
window: any other size squashes the paintings.
+8
View File
@@ -0,0 +1,8 @@
The MIT License (MIT)
Copyright © 2026 Teletype Games
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+91
View File
@@ -0,0 +1,91 @@
# -----------------------------------------
# Makefile Ebitengine project builder
# -----------------------------------------
PROJECT = realworld
# Our own modules live on the forge, not on proxy.golang.org: fetch them
# directly and skip the public checksum database.
export GOPRIVATE = git.teletypegames.org
BIN_DIR = bin
DIST_DIR = dist
WASM_NAME = game.wasm
OUTPUT_BIN = $(BIN_DIR)/$(PROJECT)
OUTPUT_WASM = $(DIST_DIR)/$(WASM_NAME)
OUTPUT_JS = $(DIST_DIR)/wasm_exec.js
OUTPUT_ZIP = $(PROJECT)-$(VERSION).html.zip
INDEX_HTML_URL = https://git.teletypegames.org/tools/ebitengine-tools/raw/branch/master/web/index.html
all: build
build:
@mkdir -p $(BIN_DIR)
go build -o $(OUTPUT_BIN) .
# Headless: composes the game, validates every cross-manager name reference
# and checks the HUD layout. No window opens.
test:
go test ./...
wasm:
@mkdir -p $(DIST_DIR)
GOOS=js GOARCH=wasm go build -o $(OUTPUT_WASM) .
cp "$$(go env GOROOT)/lib/wasm/wasm_exec.js" $(OUTPUT_JS)
export: wasm
@if [ -z "$(VERSION)" ]; then \
echo "ERROR: VERSION not set!"; exit 1; \
fi
@echo "==> Downloading index.html"
curl -sSL $(INDEX_HTML_URL) -o $(DIST_DIR)/index.html
@echo "==> Packaging HTML/WASM for $(VERSION)"
zip -r $(OUTPUT_ZIP) -j $(DIST_DIR)/$(WASM_NAME) $(DIST_DIR)/wasm_exec.js $(DIST_DIR)/index.html
@echo "==> Cleaning temporary files"
rm -f $(DIST_DIR)/$(WASM_NAME) $(DIST_DIR)/wasm_exec.js $(DIST_DIR)/index.html
# --- native binaries (lásd: teletypegames/NOTES_BINARY_PLAN.md) -------------
# win-x86 / win-x64: pure Go cross-compile (Windowson nem kell cgo)
# linux-x64: cgo build, linux/amd64 hoston fut (builder image, X11/GL dev libekkel)
BINARY_TARGETS = win-x86 win-x64 linux-x64
binaries:
@if [ -z "$(VERSION)" ]; then \
echo "ERROR: VERSION not set!"; exit 1; \
fi
@for target in $(BINARY_TARGETS); do \
$(MAKE) binary-$$target VERSION=$(VERSION) || exit 1; \
done
binary-win-x86:
@$(MAKE) binary-build VERSION=$(VERSION) B_GOOS=windows B_GOARCH=386 B_CGO=0 B_EXT=.exe B_TARGET=win-x86
binary-win-x64:
@$(MAKE) binary-build VERSION=$(VERSION) B_GOOS=windows B_GOARCH=amd64 B_CGO=0 B_EXT=.exe B_TARGET=win-x64
binary-linux-x64:
@$(MAKE) binary-build VERSION=$(VERSION) B_GOOS=linux B_GOARCH=amd64 B_CGO=1 B_EXT= B_TARGET=linux-x64
# belső helper: egy target buildje + zip egyetlen gyökérmappával
# (a zipet unixos zip készíti, így a végrehajtási bit megmarad)
binary-build:
@set -e; \
PKG_DIR="$(PROJECT)-$(VERSION)-$(B_TARGET)"; \
echo "==> Building $$PKG_DIR"; \
rm -rf "$$PKG_DIR" "$$PKG_DIR.zip"; \
mkdir -p "$$PKG_DIR"; \
CGO_ENABLED=$(B_CGO) GOOS=$(B_GOOS) GOARCH=$(B_GOARCH) go build -o "$$PKG_DIR/$(PROJECT)$(B_EXT)" .; \
if [ -f LICENSE.md ]; then cp LICENSE.md "$$PKG_DIR/"; fi; \
if [ -f README.md ]; then cp README.md "$$PKG_DIR/"; fi; \
zip -r "$$PKG_DIR.zip" "$$PKG_DIR" >/dev/null; \
rm -rf "$$PKG_DIR"; \
echo "==> $$PKG_DIR.zip kesz"
watch:
fswatch -o . | while read; do make build; done
clean:
rm -rf $(BIN_DIR) $(DIST_DIR)
.PHONY: all build test wasm export watch clean binaries binary-win-x86 binary-win-x64 binary-linux-x64 binary-build
+372
View File
@@ -0,0 +1,372 @@
# 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 (beat 3)
go run . -finale # the finale — try the Nokia-punk theme switch
go run . -scene selector # the location selector: the map the city hangs off
go run . -scene hospital # any scene in the deck, e.g. to look at the art
```
Scene names are the constants in `names.manager.go` (`selector`, `paul_shop`,
`noodle_house`, `alley`, `norman_apartment`, `police_station`, `hackerspace`,
`ice_cream_shop`, `trinket_shop`, `small_restaurant`, `secret_club`,
`bbs_terminal`, `street`, `hospital`, `server_farm`, `secret_lab`,
`rooftop_hideout`, `public_bbs`, `bvk_branch`, `curator_shop`, `scrap_market`,
`samizdat_press`, `showroom`, `columbarium`).
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 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) |
| `←` `→` | previous / next scene, in file order, wrapping |
| `F1` | toggle hotspot outlines |
| `SPACE` | during a cutscene: let the tape speak, if it offered |
The scenes are connected to each other (see **The map** below); the arrow keys
are a reviewing tool on top of that, walking every scene in turn. The walk is
inert during a cutscene, a menu, or a stopped world, so it can never cut
across an authored beat.
Screenshots: `EBITEN_SCREENSHOT_KEY=q go run .`, then press `q` in the window.
## Layout
Internal resolution is **640×380**: the size of one painted background.
Integer scaling only; the window opens at 2×.
That size is not a taste call. inkwell scales a scene background over the
*whole* screen (`core.engine.go`), not over the region the HUD leaves free — so
at any screen size other than the art's own, every painting is squashed to fit.
The deck is drawn at 640×380, so the screen is 640×380 and the art lands pixel
for pixel. `TestSceneBackgroundsResolve` keeps the two in step: a background of
any other size fails the build.
It still gives the wiki's "320×200 VGA look" brief what it was after — a low,
wide VGA frame at an exact 2× of a 320-wide canvas — and it still keeps the
engine's fixed **6×16** debug font readable, at 4% of the screen height. (At
320×190 it would be 8%: rows overlap and the top bar cannot hold a line.)
The HUD is therefore an opaque strip laid over the foot of the painting, and it
is measured from the bottom and kept as short as its contents allow — a status
row, then the two rows of inventory slots, which are the tallest thing in it.
Every pixel it gives back is a pixel of art.
```
0 394│396 639
┌────────────────────────────────────────────────────────────────────┐
0 │ ALLEY — paused SRP: 1 tape │ TopBar (20)
20 ├────────────────────────────────────────────────────────────────────┤
│ scene — hotspots, characters, SpeechBubble │ Scene (242)
262├─────────────────────────────────────────┬──────────────────────────┤
│ Use hook on: floor grate │ DEX │
│ ┌───┬───┬───┬───┐ ┌───────┐┌────────┐ │ "The hook is a hand │ HUD (118)
│ └───┴───┴───┴───┘ │ 1 DEX ││ 2 — │ │ shorter than the gap." │
379└─────────────────────────────────────────┴──────────────────────────┘
InventoryBar TapeSlots TapeChannel
```
Every vertical measurement derives from `LineH` (glyph cell + leading) and from
`ScreenH`, both in `widget/widget.manager.go`, not from the wireframe deck's ratios, so
the layout cannot drift out of step with the font — or with the art — again;
`TestLayoutFitsTheFont` guards it. The tape channel comes out at 4 rows of 38
columns, which is about one and a half of Dex's remarks on screen at once.
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
One category, one package, one directory. A file still says which entity it
holds in its own name — `[category].[name].go`, the prefix repeated inside the
directory that already carries it, because a file called `alley.go` tells you
nothing in a list of open editor tabs.
```
main.go flags + inkwell.Run
inc/constants.go every name and state key; imports nothing
inc/boot/ New(Opts): the hand-off to the engine
inc/theme/ the Theme alias, RGB, realworld-93, nokia-punk
inc/world/ the run state and the custom actions
inc/tape/ the Tape entity and the lookups over it
inc/widget/ HUD layout, the visibility conditions, 14 widgets
inc/scene/ inc/background/ one file per location, twice
inc/item/ inc/character/ inc/dialog/ inc/script/
```
`inc` itself holds nothing but constants, which is why every package can import
it and it can import none of them. The consequence is that the assembly moved
down rather than up: `New` lives in `inc/boot`, because a package cannot be
imported by what it imports.
The layering is no longer a rule kept by hand — it is the import graph, and the
compiler rejects the arrow that points the wrong way:
```
inc ← theme ← world ← tape ← widget ← boot ← main
↑ ↑ ↑
└── content ───────────────────-┘
```
Adding a scene means adding `inc/scene/scene.<name>.go` and
`inc/background/background.<name>.go`. Nothing else moves.
Every category owns a manager, and they are all the engine's own
`inkwell.Manager[T]` — the same registry the `*Game` hangs its content off, kept
in registration order and addressed by name. There is no second registry type
here: the entity types are aliases of engine structs, so they already satisfy
`inkwell.Named` and need no help reading their own name. Each one lives in its
own package's manager file, next to the alias it holds, and it needs no prefix
because the package already is one:
```go
// inc/character/character.manager.go
type Character = inkwell.Character
var Manager = inkwell.NewManager[Character]()
```
An entity file is a literal that hands itself over in an `init()`:
```go
package background
func init() {
Manager.Register(Background{
Name: inc.BgServerFarm,
Path: "assets/bg/server_farm.png",
Kind: inkwell.AssetImage,
})
}
```
The `init()` only runs if something imports the package, so a package whose
entities nobody names by symbol needs a blank import in `inc/boot`. Exactly one
does — `tape`, everything else is imported for its `Manager` — and the list is
per package, not per entity, so adding a file is still adding a file.
Widgets go the same way, the engine's own included: `widget/widget.cursor.go` registers
an `inkwell.Cursor` exactly as `background/background.street.go` registers an image. What
they cannot take from alphabetical order is their place in the stack, so each
widget names a **layer**`LayerScene`, `LayerPanel`, `LayerHUD`, `LayerSpeech`,
`LayerDialog`, `LayerMenu`, `LayerCurtain`, `LayerCursor` — and inkwell draws
from the bottom layer up, ticks from the top layer down, and lets registration
order decide only ties inside one layer. That is why the HUD frame comes out
under the tape slots and the cursor over everything, whatever the file names
happen to be. The layer was added to inkwell for this; the alternative was a
registration list, which is the thing this package spent its life deleting.
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.
That was a deliberate trade — the deck order was a list that had to be kept in
step with the files by hand, and the deck is a thing to look at, not a thing to
play through.
The game's own catalogue is readable without going through the engine:
`scene.Manager.Get("alley")` answers long before there is a `*Game` to ask. And
when the game is built, nothing is copied into it — `New` assigns our managers
onto the `*Game` in place of the empty ones `NewGame` made, so engine and game
share one registry per category instead of keeping two of them in step. Themes
are the exception: `NewGame` seeds its theme manager with four presets, so ours
are added to that set rather than replacing it.
The 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:
`scene.FillSelectorPins` reads the exit graph backwards and `Set`s the selector.
There is one world, and now the package *is* the singleton: `world.Do(…)`,
`world.Slot2()`, `world.SetPending(…)`. Nothing takes a world parameter and no
widget holds a back-reference, which is what lets a script file be a literal —
the tape-insert script closes over the `world` package rather than over a
parameter someone would have had to thread to it. What little it still keeps is
in package-level variables that only `world.Attach` may reset; the mode and the
top bar's note are not among them, because those are `State` vars the engine
itself can see.
**On the word "scene".** The wiki, the concept-art deck and the beat tables all
count *screens*, and this code used to as well: it carried its own `Screen`
struct, because inkwell's `Scene` could not hold the things a screen needs —
its exits above all. Those went into the engine instead (inkwell `Exit`,
`Game.SceneRect`, `CurrentScene`/`PreviousScene`, `Back()`), and with the gap
closed there was nothing left for a second type to carry. The code says scene,
because that is what the thing is. "Screen" survives only where it means the
display: `ScreenW`, `ScreenH`.
Art lives in `assets/bg/` and is embedded into the binary (`main.go`), because
js/wasm has no OS filesystem and `make binaries` packages the executable alone.
## The map
Where a scene leads is recorded **in that scene**, in its own file, and it
comes from the wiki's location graph
(`projects/realworld/story#helyszín-gráf-vázlat`). That graph has two kinds of
edge and so does the code:
- **Physical adjacency** — the alley is behind Noodle's house, the eating place
is next door to the trinket shop, the club is through its back room, the roof
is up Norman's stairs. Declared with `To:` in the scene's `Exits`.
- **The selector** — the wiki centres its map on a *Helyszínválasztó*, "nem
valódi helyszín, hanem a menü-képernyő": a scene every main location connects
to both ways. A main location lists `exitToSelector` among its exits, and
`scene/scene.selector.go` derives the other half of each edge by reading the graph
backwards — every scene with an exit to the selector gets a pin on it. The
list of locations on the map is never written down twice.
The wiki's conditional arrows survive too: `Needs:` holds the flag an exit waits
for, which is how *Bolt →|beengedés| Klub* is expressed — the way into the club
is visible and named from the first visit, and stays shut until the shopkeeper
has been shown the underwater sun.
Until the doors are measured on the paintings, an exit is a strip along one edge
of the picture (`inkwell.ExitLeft`, `ExitRight`, `ExitBack`, `ExitNear`), sized
from `Game.SceneRect` so it lands inside the painting rather than under the HUD
— the convention every 1990s point & click used, and one field to re-aim later.
A scene reached from several rooms has no fixed way out: the BBS terminal is the
same terminal from the club, the flat or the roof, so its exit leaves `To` empty
and the engine binds it to `Back()`.
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.
## What this game pushed into the engine
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:
| 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
`git.teletypegames.org/engines/inkwell`.
## Known gaps
- **The HUD still covers the foot of every painting.** The art is no longer
stretched, but the strip is opaque and 118px of the 380 sit on top of the
picture. Either the deck gets recut so nothing that matters lives in its
bottom third, or the HUD gets a translucent panel treatment.
- **The scenes are empty.** 22 backgrounds are in and they are wired to each
other, but only the alley has hotspots, NPCs and dialogue; everywhere else
there is nothing to do but leave again.
- **The selector has no art.** The wiki's Helyszínválasztó is the centre of the
map and the deck does not include it, so it renders as a placeholder with one
labelled pin per location on a plain grid.
- **Two scenes are missing from the deck.** The alley (the one authored beat)
and deck 05, Norman's workplace — which is in the wiki's catalogue but has no
scene file yet, so the workplace thread of the story has nowhere to happen.
- **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; the opening script 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 derived from one font cell and one screen size, 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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 KiB

+20 -2
View File
@@ -1,3 +1,21 @@
module realworld
module git.teletypegames.org/games/realworld
go 1.26.5
go 1.26.3
require (
git.teletypegames.org/engines/inkwell v0.1.1-0.20260830205716-92fc36bdf5b8
github.com/hajimehoshi/ebiten/v2 v2.9.9
)
require (
github.com/ebitengine/gomobile v0.0.0-20250923094054-ea854a63cce1 // indirect
github.com/ebitengine/hideconsole v1.0.0 // indirect
github.com/ebitengine/oto/v3 v3.4.0 // indirect
github.com/ebitengine/purego v0.9.0 // indirect
github.com/hajimehoshi/go-mp3 v0.3.4 // indirect
github.com/jezek/xgb v1.1.1 // indirect
github.com/jfreymuth/oggvorbis v1.0.5 // indirect
github.com/jfreymuth/vorbis v1.0.2 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
)
+34
View File
@@ -0,0 +1,34 @@
git.teletypegames.org/engines/inkwell v0.1.1-0.20260830092549-e26f7345c14d h1:4AGtmrWwfnHkJvZ90on1+9bUmZOGSrcIG4KaJFiy+xo=
git.teletypegames.org/engines/inkwell v0.1.1-0.20260830092549-e26f7345c14d/go.mod h1:/v6QtismTE8+e7kobbVR5B/CpTSrd4j2DYwJ8DvINhs=
git.teletypegames.org/engines/inkwell v0.1.1-0.20260830200555-53f4df04669c h1:GOKa6IbkfJXjB5LREr3eUw6YeeOeDICryfa0eKERWo0=
git.teletypegames.org/engines/inkwell v0.1.1-0.20260830200555-53f4df04669c/go.mod h1:/v6QtismTE8+e7kobbVR5B/CpTSrd4j2DYwJ8DvINhs=
git.teletypegames.org/engines/inkwell v0.1.1-0.20260830201917-2429ada0900f h1:WBIh7svmA2R7tbi1Xrafn5//yl8vnK2qbpBXDbbz6Aw=
git.teletypegames.org/engines/inkwell v0.1.1-0.20260830201917-2429ada0900f/go.mod h1:/v6QtismTE8+e7kobbVR5B/CpTSrd4j2DYwJ8DvINhs=
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=
github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A=
github.com/ebitengine/oto/v3 v3.4.0 h1:br0PgASsEWaoWn38b2Goe7m1GKFYfNgnsjSd5Gg+/bQ=
github.com/ebitengine/oto/v3 v3.4.0/go.mod h1:IOleLVD0m+CMak3mRVwsYY8vTctQgOM0iiL6S7Ar7eI=
github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k=
github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/hajimehoshi/ebiten/v2 v2.9.9 h1:JdDag6Ndj12iD4lxQGG8kbsrh7ssj4Sbzth6r929H/M=
github.com/hajimehoshi/ebiten/v2 v2.9.9/go.mod h1:DAt4tnkYYpCvu3x9i1X/nK/vOruNXIlYq/tBXxnhrXM=
github.com/hajimehoshi/go-mp3 v0.3.4 h1:NUP7pBYH8OguP4diaTZ9wJbUbk3tC0KlfzsEpWmYj68=
github.com/hajimehoshi/go-mp3 v0.3.4/go.mod h1:fRtZraRFcWb0pu7ok0LqyFhCUrPeMsGRSVop0eemFmo=
github.com/hajimehoshi/oto/v2 v2.3.1/go.mod h1:seWLbgHH7AyUMYKfKYT9pg7PhUu9/SisyJvNTT+ASQo=
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
github.com/jfreymuth/oggvorbis v1.0.5 h1:u+Ck+R0eLSRhgq8WTmffYnrVtSztJcYrl588DM4e3kQ=
github.com/jfreymuth/oggvorbis v1.0.5/go.mod h1:1U4pqWmghcoVsCJJ4fRBKv9peUJMBHixthRlBeD6uII=
github.com/jfreymuth/vorbis v1.0.2 h1:m1xH6+ZI4thH927pgKD8JOH4eaGRm18rEE9/0WKjvNE=
github.com/jfreymuth/vorbis v1.0.2/go.mod h1:DoftRo4AznKnShRl1GxiTFCseHr4zR9BN3TWXyuzrqQ=
golang.org/x/image v0.31.0 h1:mLChjE2MV6g1S7oqbXC0/UcKijjm5fnJLUYKIYrLESA=
golang.org/x/image v0.31.0/go.mod h1:R9ec5Lcp96v9FTF+ajwaH3uGxPH4fKfHHAVbUILxghA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+14
View File
@@ -0,0 +1,14 @@
package background
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Background{
Name: inc.BgAlley,
Path: "assets/bg/alley.png",
Kind: inkwell.AssetImage,
})
}
+14
View File
@@ -0,0 +1,14 @@
package background
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Background{
Name: inc.BgBBSTerminal,
Path: "assets/bg/bbs_terminal.png",
Kind: inkwell.AssetImage,
})
}
+14
View File
@@ -0,0 +1,14 @@
package background
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Background{
Name: inc.BgBVKBranch,
Path: "assets/bg/bvk_branch.png",
Kind: inkwell.AssetImage,
})
}
+14
View File
@@ -0,0 +1,14 @@
package background
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Background{
Name: inc.BgColumbarium,
Path: "assets/bg/columbarium.png",
Kind: inkwell.AssetImage,
})
}
+14
View File
@@ -0,0 +1,14 @@
package background
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Background{
Name: inc.BgCuratorShop,
Path: "assets/bg/curator_shop.png",
Kind: inkwell.AssetImage,
})
}
+14
View File
@@ -0,0 +1,14 @@
package background
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Background{
Name: inc.BgHackerspace,
Path: "assets/bg/hackerspace.png",
Kind: inkwell.AssetImage,
})
}
+14
View File
@@ -0,0 +1,14 @@
package background
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Background{
Name: inc.BgHospital,
Path: "assets/bg/hospital.png",
Kind: inkwell.AssetImage,
})
}
@@ -0,0 +1,14 @@
package background
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Background{
Name: inc.BgIceCreamShop,
Path: "assets/bg/ice_cream_shop.png",
Kind: inkwell.AssetImage,
})
}
+9
View File
@@ -0,0 +1,9 @@
package background
import (
inkwell "git.teletypegames.org/engines/inkwell"
)
type Background = inkwell.Asset
var Manager = inkwell.NewManager[Background]()
+14
View File
@@ -0,0 +1,14 @@
package background
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Background{
Name: inc.BgNoodleHouse,
Path: "assets/bg/noodle_house.png",
Kind: inkwell.AssetImage,
})
}
@@ -0,0 +1,14 @@
package background
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Background{
Name: inc.BgNormanApartment,
Path: "assets/bg/norman_apartment.png",
Kind: inkwell.AssetImage,
})
}
+14
View File
@@ -0,0 +1,14 @@
package background
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Background{
Name: inc.BgPaulShop,
Path: "assets/bg/paul_shop.png",
Kind: inkwell.AssetImage,
})
}
@@ -0,0 +1,14 @@
package background
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Background{
Name: inc.BgPoliceStation,
Path: "assets/bg/police_station.png",
Kind: inkwell.AssetImage,
})
}
+14
View File
@@ -0,0 +1,14 @@
package background
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Background{
Name: inc.BgPublicBBS,
Path: "assets/bg/public_bbs.png",
Kind: inkwell.AssetImage,
})
}
@@ -0,0 +1,14 @@
package background
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Background{
Name: inc.BgRooftopHideout,
Path: "assets/bg/rooftop_hideout.png",
Kind: inkwell.AssetImage,
})
}
@@ -0,0 +1,14 @@
package background
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Background{
Name: inc.BgSamizdatPress,
Path: "assets/bg/samizdat_press.png",
Kind: inkwell.AssetImage,
})
}
+14
View File
@@ -0,0 +1,14 @@
package background
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Background{
Name: inc.BgScrapMarket,
Path: "assets/bg/scrap_market.png",
Kind: inkwell.AssetImage,
})
}
+14
View File
@@ -0,0 +1,14 @@
package background
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Background{
Name: inc.BgSecretClub,
Path: "assets/bg/secret_club.png",
Kind: inkwell.AssetImage,
})
}
+14
View File
@@ -0,0 +1,14 @@
package background
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Background{
Name: inc.BgSecretLab,
Path: "assets/bg/secret_lab.png",
Kind: inkwell.AssetImage,
})
}
+14
View File
@@ -0,0 +1,14 @@
package background
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Background{
Name: inc.BgSelector,
Path: "assets/bg/selector.png",
Kind: inkwell.AssetImage,
})
}
+14
View File
@@ -0,0 +1,14 @@
package background
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Background{
Name: inc.BgServerFarm,
Path: "assets/bg/server_farm.png",
Kind: inkwell.AssetImage,
})
}
+14
View File
@@ -0,0 +1,14 @@
package background
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Background{
Name: inc.BgShowroom,
Path: "assets/bg/showroom.png",
Kind: inkwell.AssetImage,
})
}
@@ -0,0 +1,14 @@
package background
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Background{
Name: inc.BgSmallRestaurant,
Path: "assets/bg/small_restaurant.png",
Kind: inkwell.AssetImage,
})
}
+14
View File
@@ -0,0 +1,14 @@
package background
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Background{
Name: inc.BgStreet,
Path: "assets/bg/street.png",
Kind: inkwell.AssetImage,
})
}
+14
View File
@@ -0,0 +1,14 @@
package background
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Background{
Name: inc.BgTrinketShop,
Path: "assets/bg/trinket_shop.png",
Kind: inkwell.AssetImage,
})
}
+63
View File
@@ -0,0 +1,63 @@
package boot
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
"git.teletypegames.org/games/realworld/inc/background"
"git.teletypegames.org/games/realworld/inc/character"
"git.teletypegames.org/games/realworld/inc/dialog"
"git.teletypegames.org/games/realworld/inc/item"
"git.teletypegames.org/games/realworld/inc/scene"
"git.teletypegames.org/games/realworld/inc/script"
_ "git.teletypegames.org/games/realworld/inc/tape"
"git.teletypegames.org/games/realworld/inc/theme"
"git.teletypegames.org/games/realworld/inc/widget"
"git.teletypegames.org/games/realworld/inc/world"
)
type Opts struct {
Scene string
Finale bool
}
func New(o Opts) *inkwell.Game {
start := o.Scene
if start == "" {
start = inc.SceneAlley
}
scene.FillSelectorPins()
g := inkwell.NewGame("Real World", widget.ScreenW, widget.ScreenH)
g.MaxLogLines = 64
g.WindowScale = 2
g.SceneRect = inkwell.Rect(0, widget.TopBarH, widget.ScreenW, widget.HUDTop-widget.TopBarH)
g.Player = inc.Paul
g.Walkboxes = scene.Floor
g.ExitLook = func(e inkwell.Exit) inkwell.Action {
return inkwell.Say(inc.Paul, "That way: "+e.Label+".")
}
g.ExitTake = func(inkwell.Exit) inkwell.Action {
return inkwell.Say(inc.Paul, "It's a way out, not a thing.")
}
g.UseWithFail = script.UseWithFail
g.AssetManager = background.Manager
g.CharacterManager = character.Manager
g.DialogueManager = dialog.Manager
g.ItemManager = item.Manager
g.SceneManager = scene.Manager
g.ScriptManager = script.Manager
g.WidgetManager = widget.Manager
theme.Manager.Each(g.ThemeManager.Register)
world.Attach(g)
g.UseTheme(inc.RealWorld)
g.StartAt(start)
g.OnStart(inc.ScriptOpening)
if o.Finale {
g.OnFinale(inc.ScriptFinale)
}
return g
}
+16
View File
@@ -0,0 +1,16 @@
package character
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
"git.teletypegames.org/games/realworld/inc/theme"
)
func init() {
Manager.Register(Character{
Name: inc.Dex,
Label: "DEX",
Voice: inkwell.VoiceLog,
SpeechColor: theme.Amber,
})
}
+9
View File
@@ -0,0 +1,9 @@
package character
import (
inkwell "git.teletypegames.org/engines/inkwell"
)
type Character = inkwell.Character
var Manager = inkwell.NewManager[Character]()
+16
View File
@@ -0,0 +1,16 @@
package character
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
"git.teletypegames.org/games/realworld/inc/theme"
)
func init() {
Manager.Register(Character{
Name: inc.TapeMystery,
Label: "UNKNOWN TAPE",
Voice: inkwell.VoiceLog,
SpeechColor: theme.RGB(0xB9A98A),
})
}
+21
View File
@@ -0,0 +1,21 @@
package character
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
"git.teletypegames.org/games/realworld/inc/theme"
)
func init() {
Manager.Register(Character{
Name: inc.Paul,
Speed: 96,
W: 28,
H: 68,
Start: inkwell.Point{
X: 320,
Y: 232,
},
SpeechColor: theme.Ink,
})
}
+16
View File
@@ -0,0 +1,16 @@
package character
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
"git.teletypegames.org/games/realworld/inc/theme"
)
func init() {
Manager.Register(Character{
Name: inc.TapeSupport,
Label: "TECH SUPPORT",
Voice: inkwell.VoiceLog,
SpeechColor: theme.RGB(0xE8C86A),
})
}
+100
View File
@@ -0,0 +1,100 @@
package inc
const (
Paul = "paul"
Dex = "dex"
TapeMystery = "tape_mystery"
TapeSupport = "tape_support"
)
const (
ItemNoodleLetter = "noodle_letter"
ItemMysteryTape = "mystery_tape"
ItemBlackArmilla = "black_market_armilla"
)
const (
RealWorld = "realworld-93"
NokiaPunk = "nokia-punk"
)
const (
DlgDex = "dex_talk"
DlgMysteryTape = "mystery_tape_silent"
)
const (
ScriptOpening = "opening"
ScriptTapeInsert = "tape_insert"
ScriptFinale = "awakening_finale"
)
const (
FlagPoliceTip = "police_tip"
FlagHasTape = "has_mystery_tape"
FlagTapeSpoke = "tape_spoke"
FlagClubEntry = "club_entry"
VarSlot2 = "armilla.slot2"
VarArmillaStrip = "armilla.strip"
VarDecay = "decay_level"
VarMode = "mode"
VarNote = "note"
NotePaused = "paused"
)
const (
SceneSelector = "selector"
ScenePaulShop = "paul_shop"
SceneNoodleHouse = "noodle_house"
SceneNormanApartment = "norman_apartment"
ScenePoliceStation = "police_station"
SceneAlley = "alley"
SceneHackerspace = "hackerspace"
SceneIceCreamShop = "ice_cream_shop"
SceneTrinketShop = "trinket_shop"
SceneSmallRestaurant = "small_restaurant"
SceneSecretClub = "secret_club"
SceneBBSTerminal = "bbs_terminal"
SceneStreet = "street"
SceneHospital = "hospital"
SceneServerFarm = "server_farm"
SceneSecretLab = "secret_lab"
SceneRooftopHideout = "rooftop_hideout"
ScenePublicBBS = "public_bbs"
SceneBVKBranch = "bvk_branch"
SceneCuratorShop = "curator_shop"
SceneScrapMarket = "scrap_market"
SceneSamizdatPress = "samizdat_press"
SceneShowroom = "showroom"
SceneColumbarium = "columbarium"
)
const (
BgSelector = "bg_selector"
BgPaulShop = "bg_paul_shop"
BgNoodleHouse = "bg_noodle_house"
BgNormanApartment = "bg_norman_apartment"
BgPoliceStation = "bg_police_station"
BgAlley = "bg_alley"
BgHackerspace = "bg_hackerspace"
BgIceCreamShop = "bg_ice_cream_shop"
BgTrinketShop = "bg_trinket_shop"
BgSmallRestaurant = "bg_small_restaurant"
BgSecretClub = "bg_secret_club"
BgBBSTerminal = "bg_bbs_terminal"
BgStreet = "bg_street"
BgHospital = "bg_hospital"
BgServerFarm = "bg_server_farm"
BgSecretLab = "bg_secret_lab"
BgRooftopHideout = "bg_rooftop_hideout"
BgPublicBBS = "bg_public_bbs"
BgBVKBranch = "bg_bvk_branch"
BgCuratorShop = "bg_curator_shop"
BgScrapMarket = "bg_scrap_market"
BgSamizdatPress = "bg_samizdat_press"
BgShowroom = "bg_showroom"
BgColumbarium = "bg_columbarium"
)
+57
View File
@@ -0,0 +1,57 @@
package dialog
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Dialog{
Name: inc.DlgDex,
Start: "root",
Nodes: []inkwell.DialogueNode{{
Name: "root",
Lines: []inkwell.DialogueLine{{
Speaker: inc.Dex,
Text: "Talk.",
}},
Choices: []inkwell.DialogueChoice{
{
Text: "What am I doing here, Dex?",
Actions: []inkwell.Action{
inkwell.Say(inc.Dex, "You got a letter from a man you hadn't seen in twenty years. And you came. That says more about you than it does about him."),
inkwell.GotoNode("root"),
},
},
{
Text: "What do you know about Noodle?",
Show: inkwell.Not(inkwell.Flag(inc.FlagHasTape)),
Actions: []inkwell.Action{
inkwell.Say(inc.Dex, "He traded cracked Armillas. That isn't charity, Paul, that's a business. The kind they take you in for."),
inkwell.GotoNode("root"),
},
},
{
Text: "What do you make of this tape?",
Show: inkwell.Flag(inc.FlagHasTape),
Actions: []inkwell.Action{
inkwell.Say(inc.Dex, "I make of it that you found a password-locked tape in a gap between two bins, on a tip from a government office. Count how many times that has ended well."),
inkwell.GotoNode("root"),
},
},
{
Text: "I miss you.",
Once: true,
Actions: []inkwell.Action{
inkwell.Say(inc.Dex, "I'm four kilobytes of a dead man. Don't do this to yourself."),
inkwell.GotoNode("root"),
},
},
{
Text: "Nothing. Forget it.",
Actions: []inkwell.Action{inkwell.EndDialogue()},
},
},
}},
})
}
+9
View File
@@ -0,0 +1,9 @@
package dialog
import (
inkwell "git.teletypegames.org/engines/inkwell"
)
type Dialog = inkwell.Dialogue
var Manager = inkwell.NewManager[Dialog]()
+33
View File
@@ -0,0 +1,33 @@
package dialog
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Dialog{
Name: inc.DlgMysteryTape,
Start: "root",
Nodes: []inkwell.DialogueNode{{
Name: "root",
Lines: []inkwell.DialogueLine{{
Speaker: inc.Dex,
Text: "Nothing. Warm, spinning, and silent.",
}},
Choices: []inkwell.DialogueChoice{
{
Text: "Are you sure it works?",
Actions: []inkwell.Action{
inkwell.Say(inc.Dex, "It works. It just isn't talking to you. That is not the same as silence."),
inkwell.GotoNode("root"),
},
},
{
Text: "Fine. It'll speak when it speaks.",
Actions: []inkwell.Action{inkwell.EndDialogue()},
},
},
}},
})
}
+23
View File
@@ -0,0 +1,23 @@
package item
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Item{
Name: inc.ItemBlackArmilla,
Description: "black-market Armilla",
OnUseSelf: inkwell.Seq(
inkwell.Say(inc.Paul, "Jailbroken. Pushes ads, but it was cheap."),
inkwell.Say(inc.Dex, "Two slots, and both of them lie about the temperature. Don't trade me in for it."),
),
OnUseWith: map[string]inkwell.Action{
inc.ItemNoodleLetter: inkwell.Seq(
inkwell.Say(inc.Paul, "A letter and a bracelet. Brilliant."),
inkwell.Say(inc.Dex, "Nothing. Which is what I'd charge for it."),
),
},
})
}
+9
View File
@@ -0,0 +1,9 @@
package item
import (
inkwell "git.teletypegames.org/engines/inkwell"
)
type Item = inkwell.Item
var Manager = inkwell.NewManager[Item]()
+17
View File
@@ -0,0 +1,17 @@
package item
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Item{
Name: inc.ItemMysteryTape,
Description: "unmarked Personal Tape",
OnUseSelf: inkwell.Seq(
inkwell.Say(inc.Paul, "Password-locked. Of course."),
inkwell.Say(inc.Dex, "Put it in the second slot if you care that much. I did warn you."),
),
})
}
+17
View File
@@ -0,0 +1,17 @@
package item
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Item{
Name: inc.ItemNoodleLetter,
Description: "Noodle's letter",
OnUseSelf: inkwell.Seq(
inkwell.Say(inc.Paul, "“Come out, Paul. Not a phone conversation.” That's all it says."),
inkwell.Say(inc.Dex, "And now you're here. And he isn't."),
),
})
}
+101
View File
@@ -0,0 +1,101 @@
package scene
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Scene{
Name: inc.SceneAlley,
Title: "ALLEY — BEHIND NOODLE'S HOUSE",
Background: inc.BgAlley,
Exits: []inkwell.Exit{
{
To: inc.SceneNoodleHouse,
Label: "back out to the street",
Side: inkwell.ExitLeft,
},
},
Actors: []inkwell.SceneActor{
{
CharacterName: inc.Paul,
At: inkwell.Point{
X: 120,
Y: 232,
},
},
},
OnEnter: setVarIfEmpty(inc.VarArmillaStrip, "SRP: 1 tape"),
Hotspots: []inkwell.Hotspot{hidingPlace(), backDoor(), bin(), fireEscape()},
})
}
func hidingPlace() inkwell.Hotspot {
return inkwell.Hotspot{
Name: "hiding_place",
Label: "gap at the foot of the wall",
Area: inkwell.Rect(416, 150, 68, 52),
OnLook: inkwell.If(inkwell.Flag(inc.FlagHasTape),
inkwell.Say(inc.Paul, "Empty. Whatever was in there is on me now."),
inkwell.Say(inc.Paul, "Loose brick. There's a gap behind it."),
),
OnUse: inkwell.If(inkwell.Flag(inc.FlagPoliceTip),
inkwell.If(inkwell.Flag(inc.FlagHasTape),
inkwell.Say(inc.Paul, "There's nothing else in there."),
inkwell.Seq(
inkwell.Say(inc.Paul, "“Behind the brick.” All right then."),
inkwell.Give(inc.ItemMysteryTape),
inkwell.SetFlag(inc.FlagHasTape),
inkwell.Say(inc.Paul, "A Personal Tape. No label, no seal."),
inkwell.Say(inc.Dex, "Password-locked. And somebody very much did not want it found."),
),
),
inkwell.Say(inc.Paul, "One loose brick. I'm not taking the alley apart over it."),
),
OnTake: inkwell.Say(inc.Paul, "I'm not carrying a wall."),
}
}
func backDoor() inkwell.Hotspot {
return inkwell.Hotspot{
Name: "back_door",
Label: "locked back door",
Area: inkwell.Rect(72, 106, 80, 124),
OnLook: inkwell.Say(inc.Paul, "Keypad. Four digits, worn keys."),
OnUse: inkwell.Seq(
inkwell.Say(inc.Paul, "Locked. And I'm not guessing four digits."),
inkwell.Say(inc.Dex, "The wear is heaviest on the two and the seven. That isn't a code. It's fewer options."),
),
OnTalk: inkwell.Say(inc.Paul, "To the door? I'm not there yet."),
OnUseWith: map[string]inkwell.Action{
inc.ItemBlackArmilla: inkwell.Seq(
inkwell.Say(inc.Paul, "A black-market bracelet doesn't open a keypad."),
inkwell.Say(inc.Dex, "But you do get an advert out of it."),
),
},
}
}
func bin() inkwell.Hotspot {
return inkwell.Hotspot{
Name: "bin",
Label: "toppled bin",
Area: inkwell.Rect(240, 170, 88, 60),
OnLook: inkwell.Say(inc.Paul, "Somebody's been through it. Thoroughly."),
OnUse: inkwell.Seq(
inkwell.Say(inc.Paul, "Already turned out. I'm not going to be the second one."),
inkwell.Say(inc.Dex, "The police don't go through bins. So who did?"),
),
}
}
func fireEscape() inkwell.Hotspot {
return inkwell.Hotspot{
Name: "fire_escape",
Label: "fire escape",
Area: inkwell.Rect(528, 44, 88, 160),
OnLook: inkwell.Say(inc.Paul, "Runs all the way to the roof. Bottom rung is two metres over my head."),
OnUse: inkwell.Say(inc.Paul, "Can't reach it. I'd need something to stand on."),
}
}
+20
View File
@@ -0,0 +1,20 @@
package scene
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Scene{
Name: inc.SceneBBSTerminal,
Title: "TERMINAL — BBS ACCESS POINT",
Background: inc.BgBBSTerminal,
Exits: []inkwell.Exit{
{
Label: "step back from the terminal",
Side: inkwell.ExitNear,
},
},
})
}
+17
View File
@@ -0,0 +1,17 @@
package scene
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Scene{
Name: inc.SceneBVKBranch,
Title: "BVK — SAN FRANCISCO BRANCH",
Background: inc.BgBVKBranch,
Exits: []inkwell.Exit{
exitToSelector,
},
})
}
+17
View File
@@ -0,0 +1,17 @@
package scene
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Scene{
Name: inc.SceneColumbarium,
Title: "COLUMBARIUM — DEX'S MEMORIAL",
Background: inc.BgColumbarium,
Exits: []inkwell.Exit{
exitToSelector,
},
})
}
+17
View File
@@ -0,0 +1,17 @@
package scene
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Scene{
Name: inc.SceneCuratorShop,
Title: "CURATOR SERVICE — THE MASTER'S WORKSHOP",
Background: inc.BgCuratorShop,
Exits: []inkwell.Exit{
exitToSelector,
},
})
}
+17
View File
@@ -0,0 +1,17 @@
package scene
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Scene{
Name: inc.SceneHackerspace,
Title: "HACKERSPACE",
Background: inc.BgHackerspace,
Exits: []inkwell.Exit{
exitToSelector,
},
})
}
+17
View File
@@ -0,0 +1,17 @@
package scene
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Scene{
Name: inc.SceneHospital,
Title: "HOSPITAL — PSYCHIATRIC WING",
Background: inc.BgHospital,
Exits: []inkwell.Exit{
exitToSelector,
},
})
}
+17
View File
@@ -0,0 +1,17 @@
package scene
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Scene{
Name: inc.SceneIceCreamShop,
Title: "ICE CREAM SHOP — WHERE THE BAR WAS",
Background: inc.BgIceCreamShop,
Exits: []inkwell.Exit{
exitToSelector,
},
})
}
+37
View File
@@ -0,0 +1,37 @@
package scene
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc/world"
)
type Scene = inkwell.Scene
var Manager = inkwell.NewManager[Scene]()
var Floor = []inkwell.Polygon{
inkwell.Poly(
inkwell.Point{
X: 16,
Y: 196,
}, inkwell.Point{
X: 624,
Y: 196,
},
inkwell.Point{
X: 624,
Y: 258,
}, inkwell.Point{
X: 16,
Y: 258,
},
),
}
func setVarIfEmpty(name string, v any) inkwell.Action {
return world.Fn(func(ctx *inkwell.Ctx) {
if ctx.Game.State.Var(name) == nil {
ctx.Game.State.SetVar(name, v)
}
})
}
+22
View File
@@ -0,0 +1,22 @@
package scene
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Scene{
Name: inc.SceneNoodleHouse,
Title: "NOODLE'S HOUSE — SEALED",
Background: inc.BgNoodleHouse,
Exits: []inkwell.Exit{
exitToSelector,
{
To: inc.SceneAlley,
Label: "the alley behind the house",
Side: inkwell.ExitRight,
},
},
})
}
+27
View File
@@ -0,0 +1,27 @@
package scene
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Scene{
Name: inc.SceneNormanApartment,
Title: "NORMAN'S APARTMENT",
Background: inc.BgNormanApartment,
Exits: []inkwell.Exit{
exitToSelector,
{
To: inc.SceneRooftopHideout,
Label: "the stairs up to the roof",
Side: inkwell.ExitBack,
},
{
To: inc.SceneBBSTerminal,
Label: "Norman's terminal",
Side: inkwell.ExitRight,
},
},
})
}
+21
View File
@@ -0,0 +1,21 @@
package scene
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Scene{
Name: inc.ScenePaulShop,
Title: "PAUL'S SHOP — JUNK AND GARAGE",
Background: inc.BgPaulShop,
Exits: []inkwell.Exit{
{
To: inc.SceneNoodleHouse,
Label: "the bus to San Francisco",
Side: inkwell.ExitNear,
},
},
})
}
+17
View File
@@ -0,0 +1,17 @@
package scene
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Scene{
Name: inc.ScenePoliceStation,
Title: "SFPD — STATION AND HOLDING",
Background: inc.BgPoliceStation,
Exits: []inkwell.Exit{
exitToSelector,
},
})
}
+17
View File
@@ -0,0 +1,17 @@
package scene
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Scene{
Name: inc.ScenePublicBBS,
Title: "PUBLIC BBS TERMINAL",
Background: inc.BgPublicBBS,
Exits: []inkwell.Exit{
exitToSelector,
},
})
}
+26
View File
@@ -0,0 +1,26 @@
package scene
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Scene{
Name: inc.SceneRooftopHideout,
Title: "NORMAN'S ROOFTOP HIDEOUT",
Background: inc.BgRooftopHideout,
Exits: []inkwell.Exit{
{
To: inc.SceneNormanApartment,
Label: "back down into the flat",
Side: inkwell.ExitNear,
},
{
To: inc.SceneBBSTerminal,
Label: "the old terminal",
Side: inkwell.ExitRight,
},
},
})
}
+17
View File
@@ -0,0 +1,17 @@
package scene
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Scene{
Name: inc.SceneSamizdatPress,
Title: "SAMIZDAT PRINTING HOUSE",
Background: inc.BgSamizdatPress,
Exits: []inkwell.Exit{
exitToSelector,
},
})
}
+17
View File
@@ -0,0 +1,17 @@
package scene
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Scene{
Name: inc.SceneScrapMarket,
Title: "SCRAP MARKET",
Background: inc.BgScrapMarket,
Exits: []inkwell.Exit{
exitToSelector,
},
})
}
+26
View File
@@ -0,0 +1,26 @@
package scene
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Scene{
Name: inc.SceneSecretClub,
Title: "SECRET CLUB — THE UNDERWATER SUN",
Background: inc.BgSecretClub,
Exits: []inkwell.Exit{
{
To: inc.SceneBBSTerminal,
Label: "the terminal in the corner",
Side: inkwell.ExitBack,
},
{
To: inc.SceneTrinketShop,
Label: "back out through the shop",
Side: inkwell.ExitNear,
},
},
})
}
+17
View File
@@ -0,0 +1,17 @@
package scene
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Scene{
Name: inc.SceneSecretLab,
Title: "SECRET RESEARCH LABORATORY",
Background: inc.BgSecretLab,
Exits: []inkwell.Exit{
exitToSelector,
},
})
}
+73
View File
@@ -0,0 +1,73 @@
package scene
import (
"strings"
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
var exitToSelector = inkwell.Exit{
To: inc.SceneSelector,
Label: "the rest of the city",
Side: inkwell.ExitNear,
}
func init() {
Manager.Register(Scene{
Name: inc.SceneSelector,
Title: "SAN FRANCISCO",
Background: inc.BgSelector,
Actors: []inkwell.SceneActor{},
Walkboxes: []inkwell.Polygon{},
})
}
func FillSelectorPins() {
selector, ok := Manager.Get(inc.SceneSelector)
if !ok {
return
}
var exits []inkwell.Exit
for _, entity := range Manager.All() {
if !leadsToSelector(entity) {
continue
}
exits = append(exits, inkwell.Exit{
To: entity.Name,
Label: pinLabel(entity.Title),
Area: pin(len(exits)),
})
}
selector.Exits = exits
Manager.Set(selector)
}
func leadsToSelector(s Scene) bool {
for _, e := range s.Exits {
if e.To == inc.SceneSelector {
return true
}
}
return false
}
const (
pinCols = 3
pinW = 192.0
pinH = 32.0
pinX = 16.0
pinY = 28.0
pinGapX = 204.0
pinGapY = 38.0
)
func pin(i int) inkwell.Shape {
col, row := i%pinCols, i/pinCols
return inkwell.Rect(pinX+float64(col)*pinGapX, pinY+float64(row)*pinGapY, pinW, pinH)
}
func pinLabel(title string) string {
name, _, _ := strings.Cut(title, " — ")
return name
}
+17
View File
@@ -0,0 +1,17 @@
package scene
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Scene{
Name: inc.SceneServerFarm,
Title: "SERVER FARM",
Background: inc.BgServerFarm,
Exits: []inkwell.Exit{
exitToSelector,
},
})
}
+17
View File
@@ -0,0 +1,17 @@
package scene
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Scene{
Name: inc.SceneShowroom,
Title: "NEUMATRONIC SHOWROOM",
Background: inc.BgShowroom,
Exits: []inkwell.Exit{
exitToSelector,
},
})
}
+21
View File
@@ -0,0 +1,21 @@
package scene
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Scene{
Name: inc.SceneSmallRestaurant,
Title: "SMALL RESTAURANT — NEXT DOOR",
Background: inc.BgSmallRestaurant,
Exits: []inkwell.Exit{
{
To: inc.SceneTrinketShop,
Label: "back to the trinket shop",
Side: inkwell.ExitLeft,
},
},
})
}
+17
View File
@@ -0,0 +1,17 @@
package scene
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Scene{
Name: inc.SceneStreet,
Title: "STREET",
Background: inc.BgStreet,
Exits: []inkwell.Exit{
exitToSelector,
},
})
}
+29
View File
@@ -0,0 +1,29 @@
package scene
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Scene{
Name: inc.SceneTrinketShop,
Title: "CHINATOWN — TRINKET SHOP",
Background: inc.BgTrinketShop,
Exits: []inkwell.Exit{
exitToSelector,
{
To: inc.SceneSmallRestaurant,
Label: "the eating place next door",
Side: inkwell.ExitRight,
},
{
To: inc.SceneSecretClub,
Label: "the way in at the back",
Side: inkwell.ExitBack,
Needs: inc.FlagClubEntry,
Blocked: inkwell.Say(inc.Paul, "Just a shop, as far as the man behind the counter is concerned."),
},
},
})
}
+25
View File
@@ -0,0 +1,25 @@
package script
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
"git.teletypegames.org/games/realworld/inc/world"
)
func init() {
Manager.Register(Script{
Name: inc.ScriptFinale,
Actions: inkwell.Seq(
world.SetMode(world.ModeCutscene),
inkwell.Wait(0.6),
inkwell.Say(inc.Paul, "I'm putting it in. That's all this is."),
inkwell.Wait(0.4),
world.UseTheme(inc.NokiaPunk),
inkwell.Say("norman", "No — this isn't what you were supposed to do!"),
inkwell.Wait(0.6),
inkwell.Say(inc.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"),
),
})
}
+9
View File
@@ -0,0 +1,9 @@
package script
import (
inkwell "git.teletypegames.org/engines/inkwell"
)
type Script = inkwell.Script
var Manager = inkwell.NewManager[Script]()
+17
View File
@@ -0,0 +1,17 @@
package script
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Script{
Name: inc.ScriptOpening,
Actions: inkwell.Seq(
inkwell.SetFlag(inc.FlagPoliceTip),
inkwell.Say(inc.Paul, "Back alley. Just like the clerk said."),
inkwell.Say(inc.Dex, "A government office tipped you off to remove something from a scene. That doesn't strike you as odd?"),
),
})
}
+27
View File
@@ -0,0 +1,27 @@
package script
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
"git.teletypegames.org/games/realworld/inc/world"
)
func init() {
Manager.Register(Script{
Name: inc.ScriptTapeInsert,
Actions: inkwell.Seq(
world.Fn(func(ctx *inkwell.Ctx) {
item := world.TakePending()
if item == "" {
return
}
world.SetSlot2(item)
ctx.Game.Inventory.Remove(item)
ctx.Game.State.SetVar(inc.VarArmillaStrip, "SLOT2: READING")
}),
inkwell.Say(inc.Paul, "Right. Let's see who you are."),
inkwell.Say(inc.Dex, "Clicked in. Spinning. And now: nothing."),
inkwell.Say(inc.Dex, "A stranger's tape in your own Armilla, Paul. I hope you know what you're doing."),
),
})
}
+61
View File
@@ -0,0 +1,61 @@
package script
import (
"math/rand"
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/inc"
"git.teletypegames.org/games/realworld/inc/world"
)
func UseWithFail(item string, h *inkwell.Hotspot) inkwell.Action {
key := "fail." + item + "." + h.Name
world.Game().State.NoteTalked(key)
n := world.Game().State.Talked(key)
return inkwell.Seq(
inkwell.Say(inc.Paul, pick(paulFails, n)),
inkwell.Say(inc.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))]
}
}
+12
View File
@@ -0,0 +1,12 @@
package tape
import (
"git.teletypegames.org/games/realworld/inc"
)
func init() {
Manager.Register(Tape{
Name: inc.Dex,
Dialogue: inc.DlgDex,
})
}

Some files were not shown because too many files have changed in this diff Show More