Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
258cbf85c0 | ||
|
|
220985ec57 | ||
|
|
a70dff8771 | ||
|
|
07312f0f29 | ||
|
|
da1410cb4a | ||
|
|
94649a38fe | ||
|
|
ddea3b0893 | ||
|
|
59decf54ef | ||
|
|
1fa19f0b2e | ||
|
|
954bd762f7 | ||
|
|
ec48cd6b37 | ||
|
|
bbc3faf3d7 | ||
|
|
45dac473fd | ||
|
|
0d8918e6f5 |
@@ -0,0 +1,6 @@
|
|||||||
|
bin
|
||||||
|
dist
|
||||||
|
*.zip
|
||||||
|
.version
|
||||||
|
saves
|
||||||
|
screenshot*.png
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# The CI pipeline is served by the update server: GET /build/config?platform=ebitengine
|
||||||
|
platform: ebitengine
|
||||||
@@ -0,0 +1,298 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
All game code lives in one flat package, `inc`. There are no subdirectories: a
|
||||||
|
file's name carries the structure, in the form `[category].[name].go`.
|
||||||
|
|
||||||
|
- The category is always **singular**: `item`, `scene`, `background`,
|
||||||
|
`character`, `dialog`, `script`, `world`, `ui`, `theme`, `names`.
|
||||||
|
- `[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
|
||||||
|
background, one item. The file registers it itself, in an `init()`.
|
||||||
|
- `boot.go` is the exception that has no category: it declares every manager
|
||||||
|
and builds the game.
|
||||||
|
|
||||||
|
```
|
||||||
|
main.go flags + inkwell.Run
|
||||||
|
inc/boot.go the managers; New(Opts) builds the game
|
||||||
|
inc/names.manager.go entity names and world-state keys
|
||||||
|
inc/theme.manager.go the Theme alias and the colour helpers
|
||||||
|
inc/theme.realworld.go realworld-93
|
||||||
|
inc/theme.nokia_punk.go nokia-punk
|
||||||
|
inc/world.manager.go unsaved runtime state
|
||||||
|
inc/world.action.go custom actions and the action pump
|
||||||
|
inc/ui.manager.go HUD layout and widget registration
|
||||||
|
inc/ui.*.go custom widgets, coloured text
|
||||||
|
inc/background.*.go one image asset per scene
|
||||||
|
inc/character.*.go the cast, tapes included
|
||||||
|
inc/item.*.go inventory
|
||||||
|
inc/dialog.*.go dialogue trees
|
||||||
|
inc/script.*.go named action sequences
|
||||||
|
inc/scene.manager.go the Scene alias, the defaults every scene gets
|
||||||
|
inc/scene.selector.go the map screen: its pins are derived from the graph
|
||||||
|
inc/scene.*.go one file per scene
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
All seven 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
|
||||||
|
var (
|
||||||
|
BackgroundManager = inkwell.NewManager[Background]()
|
||||||
|
CharacterManager = inkwell.NewManager[Character]()
|
||||||
|
DialogManager = inkwell.NewManager[Dialog]()
|
||||||
|
ItemManager = inkwell.NewManager[Item]()
|
||||||
|
SceneManager = inkwell.NewManager[Scene]()
|
||||||
|
ScriptManager = inkwell.NewManager[Script]()
|
||||||
|
ThemeManager = inkwell.NewManager[Theme]()
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
The entity types stay in their own category files, one alias each, and that is
|
||||||
|
all a manager file holds now:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Character = inkwell.Character
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
### 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 inc
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
BackgroundManager.Register(Background{
|
||||||
|
Name: 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 the managers in `boot.go` exist by the time the 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.
|
||||||
|
|
||||||
|
### 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 = BackgroundManager
|
||||||
|
g.CharacterManager = CharacterManager
|
||||||
|
g.DialogueManager = DialogManager
|
||||||
|
g.ItemManager = ItemManager
|
||||||
|
g.SceneManager = SceneManager
|
||||||
|
g.ScriptManager = ScriptManager
|
||||||
|
ThemeManager.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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
### What runs at boot
|
||||||
|
|
||||||
|
`New` names two scripts and nothing else — the opening a player sees is content
|
||||||
|
like every other script, in `script.opening.go`, not a slice built in the
|
||||||
|
wiring:
|
||||||
|
|
||||||
|
```go
|
||||||
|
g.StartAt(start)
|
||||||
|
g.OnStart(ScriptOpening)
|
||||||
|
if o.Finale {
|
||||||
|
g.OnFinale(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 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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Naming
|
||||||
|
|
||||||
|
One package means one namespace, so an entity's constructor carries its
|
||||||
|
category as a prefix:
|
||||||
|
|
||||||
|
```go
|
||||||
|
sceneFloor sceneDefaults prepareScene fillSelectorPins
|
||||||
|
```
|
||||||
|
|
||||||
|
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:
|
||||||
|
`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`).
|
||||||
|
|
||||||
|
Everything else is machinery and stays unexported: the HUD widgets
|
||||||
|
(`tapeSlots`, `letterbox`, `hudFrame`, …), `registerUI`, the runners,
|
||||||
|
`prepareScene`, `sceneDefaults`, `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
|
||||||
|
scene did not now lives in inkwell. "Screen" survives only where it means the
|
||||||
|
display: `ScreenW`, `ScreenH`.
|
||||||
|
|
||||||
|
## Layering
|
||||||
|
|
||||||
|
Nothing is enforced by the compiler any more, so the layering is a rule kept by
|
||||||
|
hand:
|
||||||
|
|
||||||
|
```
|
||||||
|
names ← theme ← world ← ui
|
||||||
|
↑ ↑
|
||||||
|
content ──┴── boot ← main
|
||||||
|
```
|
||||||
|
|
||||||
|
`world` knows nothing about the HUD or the content. Both build on it, never the
|
||||||
|
other way round.
|
||||||
|
|
||||||
|
## Adding a scene
|
||||||
|
|
||||||
|
1. Constants in `inc/names.manager.go`: `Scene<Name>`, `Bg<Name>`
|
||||||
|
2. `inc/scene.<name>.go` — an `init()` registering a `Scene`
|
||||||
|
3. `inc/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.
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,359 @@
|
|||||||
|
# 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 `ui.LineH` (glyph cell + leading) and
|
||||||
|
from `ui.ScreenH`, not from the wireframe deck's ratios, so the layout cannot
|
||||||
|
drift out of step with the font — or with the art — again;
|
||||||
|
`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
|
||||||
|
|
||||||
|
The game is one flat package, `inc`. There are no subdirectories: a file's
|
||||||
|
name says where it belongs, in the form `[category].[name].go`. The category is
|
||||||
|
always singular, and `[category].manager.go` is the file that ties that category
|
||||||
|
together — its entity type and whatever else the category shares. `boot.go` is
|
||||||
|
the one file with no category: it declares every manager and builds the game.
|
||||||
|
|
||||||
|
```
|
||||||
|
main.go flags + inkwell.Run
|
||||||
|
inc/boot.go the managers, and New(Opts)
|
||||||
|
inc/names.manager.go entity names and world-state keys
|
||||||
|
inc/theme.*.go realworld-93 + nokia-punk
|
||||||
|
inc/world.*.go unsaved runtime state, custom actions, action pump
|
||||||
|
inc/ui.*.go HUD: layout, custom widgets, coloured text
|
||||||
|
inc/<kind>.<name>.go one file per registered entity, by kind
|
||||||
|
```
|
||||||
|
|
||||||
|
Nothing is enforced by the compiler any more, so the layering is a rule the
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
Content is one registered entity per file, and the category prefix groups them
|
||||||
|
the way directories used to:
|
||||||
|
|
||||||
|
```
|
||||||
|
background.*.go one file per scene: background.paul_shop.go, …
|
||||||
|
character.*.go character.paul.go character.dex.go character.mystery_tape.go
|
||||||
|
item.*.go item.noodle_letter.go item.black_market_armilla.go …
|
||||||
|
dialog.*.go dialog.dex_talk.go dialog.mystery_tape_silent.go
|
||||||
|
script.*.go script.tape_insert.go script.awakening_finale.go
|
||||||
|
scene.*.go one file per scene: scene.alley.go, … + scene.selector.go
|
||||||
|
```
|
||||||
|
|
||||||
|
Adding a scene means adding `scene.<name>.go` and `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. The seven of them are
|
||||||
|
declared as one block in `boot.go`, and a category's manager file is left
|
||||||
|
holding the alias:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Character = inkwell.Character
|
||||||
|
```
|
||||||
|
|
||||||
|
An entity file is a literal that hands itself over in an `init()`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func init() {
|
||||||
|
BackgroundManager.Register(Background{
|
||||||
|
Name: BgServerFarm,
|
||||||
|
Path: "assets/bg/server_farm.png",
|
||||||
|
Kind: inkwell.AssetImage,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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:
|
||||||
|
`SceneManager.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 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.
|
||||||
|
|
||||||
|
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
|
||||||
|
script file be a literal — the tape-insert script closes over `World` rather
|
||||||
|
than over a parameter someone would have had to thread to it.
|
||||||
|
|
||||||
|
**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.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.
|
||||||
|
|
||||||
|
## Engine workarounds
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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 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. **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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
After Width: | Height: | Size: 341 KiB |
|
After Width: | Height: | Size: 399 KiB |
|
After Width: | Height: | Size: 423 KiB |
|
After Width: | Height: | Size: 445 KiB |
|
After Width: | Height: | Size: 391 KiB |
|
After Width: | Height: | Size: 293 KiB |
|
After Width: | Height: | Size: 341 KiB |
|
After Width: | Height: | Size: 367 KiB |
|
After Width: | Height: | Size: 420 KiB |
|
After Width: | Height: | Size: 416 KiB |
|
After Width: | Height: | Size: 309 KiB |
|
After Width: | Height: | Size: 329 KiB |
|
After Width: | Height: | Size: 349 KiB |
|
After Width: | Height: | Size: 341 KiB |
|
After Width: | Height: | Size: 422 KiB |
|
After Width: | Height: | Size: 313 KiB |
|
After Width: | Height: | Size: 352 KiB |
|
After Width: | Height: | Size: 366 KiB |
|
After Width: | Height: | Size: 318 KiB |
|
After Width: | Height: | Size: 295 KiB |
|
After Width: | Height: | Size: 349 KiB |
|
After Width: | Height: | Size: 408 KiB |
@@ -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.20260830092549-e26f7345c14d
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
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=
|
||||||
|
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=
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
BackgroundManager.Register(Background{
|
||||||
|
Name: BgAlley,
|
||||||
|
Path: "assets/bg/alley.png",
|
||||||
|
Kind: inkwell.AssetImage,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
BackgroundManager.Register(Background{
|
||||||
|
Name: BgBBSTerminal,
|
||||||
|
Path: "assets/bg/bbs_terminal.png",
|
||||||
|
Kind: inkwell.AssetImage,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
BackgroundManager.Register(Background{
|
||||||
|
Name: BgBVKBranch,
|
||||||
|
Path: "assets/bg/bvk_branch.png",
|
||||||
|
Kind: inkwell.AssetImage,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
BackgroundManager.Register(Background{
|
||||||
|
Name: BgColumbarium,
|
||||||
|
Path: "assets/bg/columbarium.png",
|
||||||
|
Kind: inkwell.AssetImage,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
BackgroundManager.Register(Background{
|
||||||
|
Name: BgCuratorShop,
|
||||||
|
Path: "assets/bg/curator_shop.png",
|
||||||
|
Kind: inkwell.AssetImage,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
BackgroundManager.Register(Background{
|
||||||
|
Name: BgHackerspace,
|
||||||
|
Path: "assets/bg/hackerspace.png",
|
||||||
|
Kind: inkwell.AssetImage,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
BackgroundManager.Register(Background{
|
||||||
|
Name: BgHospital,
|
||||||
|
Path: "assets/bg/hospital.png",
|
||||||
|
Kind: inkwell.AssetImage,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
BackgroundManager.Register(Background{
|
||||||
|
Name: BgIceCreamShop,
|
||||||
|
Path: "assets/bg/ice_cream_shop.png",
|
||||||
|
Kind: inkwell.AssetImage,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Background = inkwell.Asset
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
BackgroundManager.Register(Background{
|
||||||
|
Name: BgNoodleHouse,
|
||||||
|
Path: "assets/bg/noodle_house.png",
|
||||||
|
Kind: inkwell.AssetImage,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
BackgroundManager.Register(Background{
|
||||||
|
Name: BgNormanApartment,
|
||||||
|
Path: "assets/bg/norman_apartment.png",
|
||||||
|
Kind: inkwell.AssetImage,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
BackgroundManager.Register(Background{
|
||||||
|
Name: BgPaulShop,
|
||||||
|
Path: "assets/bg/paul_shop.png",
|
||||||
|
Kind: inkwell.AssetImage,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
BackgroundManager.Register(Background{
|
||||||
|
Name: BgPoliceStation,
|
||||||
|
Path: "assets/bg/police_station.png",
|
||||||
|
Kind: inkwell.AssetImage,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
BackgroundManager.Register(Background{
|
||||||
|
Name: BgPublicBBS,
|
||||||
|
Path: "assets/bg/public_bbs.png",
|
||||||
|
Kind: inkwell.AssetImage,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
BackgroundManager.Register(Background{
|
||||||
|
Name: BgRooftopHideout,
|
||||||
|
Path: "assets/bg/rooftop_hideout.png",
|
||||||
|
Kind: inkwell.AssetImage,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
BackgroundManager.Register(Background{
|
||||||
|
Name: BgSamizdatPress,
|
||||||
|
Path: "assets/bg/samizdat_press.png",
|
||||||
|
Kind: inkwell.AssetImage,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
BackgroundManager.Register(Background{
|
||||||
|
Name: BgScrapMarket,
|
||||||
|
Path: "assets/bg/scrap_market.png",
|
||||||
|
Kind: inkwell.AssetImage,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
BackgroundManager.Register(Background{
|
||||||
|
Name: BgSecretClub,
|
||||||
|
Path: "assets/bg/secret_club.png",
|
||||||
|
Kind: inkwell.AssetImage,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
BackgroundManager.Register(Background{
|
||||||
|
Name: BgSecretLab,
|
||||||
|
Path: "assets/bg/secret_lab.png",
|
||||||
|
Kind: inkwell.AssetImage,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
BackgroundManager.Register(Background{
|
||||||
|
Name: BgSelector,
|
||||||
|
Path: "assets/bg/selector.png",
|
||||||
|
Kind: inkwell.AssetImage,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
BackgroundManager.Register(Background{
|
||||||
|
Name: BgServerFarm,
|
||||||
|
Path: "assets/bg/server_farm.png",
|
||||||
|
Kind: inkwell.AssetImage,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
BackgroundManager.Register(Background{
|
||||||
|
Name: BgShowroom,
|
||||||
|
Path: "assets/bg/showroom.png",
|
||||||
|
Kind: inkwell.AssetImage,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
BackgroundManager.Register(Background{
|
||||||
|
Name: BgSmallRestaurant,
|
||||||
|
Path: "assets/bg/small_restaurant.png",
|
||||||
|
Kind: inkwell.AssetImage,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
BackgroundManager.Register(Background{
|
||||||
|
Name: BgStreet,
|
||||||
|
Path: "assets/bg/street.png",
|
||||||
|
Kind: inkwell.AssetImage,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
BackgroundManager.Register(Background{
|
||||||
|
Name: BgTrinketShop,
|
||||||
|
Path: "assets/bg/trinket_shop.png",
|
||||||
|
Kind: inkwell.AssetImage,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
BackgroundManager = inkwell.NewManager[Background]()
|
||||||
|
CharacterManager = inkwell.NewManager[Character]()
|
||||||
|
DialogManager = inkwell.NewManager[Dialog]()
|
||||||
|
ItemManager = inkwell.NewManager[Item]()
|
||||||
|
SceneManager = inkwell.NewManager[Scene]()
|
||||||
|
ScriptManager = inkwell.NewManager[Script]()
|
||||||
|
ThemeManager = inkwell.NewManager[Theme]()
|
||||||
|
)
|
||||||
|
|
||||||
|
type Opts struct {
|
||||||
|
Scene string
|
||||||
|
Finale bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(o Opts) *inkwell.Game {
|
||||||
|
start := o.Scene
|
||||||
|
if start == "" {
|
||||||
|
start = SceneAlley
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareScene()
|
||||||
|
|
||||||
|
g := inkwell.NewGame("Real World", ScreenW, ScreenH)
|
||||||
|
g.MaxLogLines = 64
|
||||||
|
g.SceneRect = inkwell.Rect(0, TopBarH, ScreenW, HUDTop-TopBarH)
|
||||||
|
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.AssetManager = BackgroundManager
|
||||||
|
g.CharacterManager = CharacterManager
|
||||||
|
g.DialogueManager = DialogManager
|
||||||
|
g.ItemManager = ItemManager
|
||||||
|
g.SceneManager = SceneManager
|
||||||
|
g.ScriptManager = ScriptManager
|
||||||
|
ThemeManager.Each(g.ThemeManager.Register)
|
||||||
|
|
||||||
|
World.attach(g)
|
||||||
|
g.UseTheme(RealWorld)
|
||||||
|
registerUI()
|
||||||
|
|
||||||
|
g.StartAt(start)
|
||||||
|
g.OnStart(ScriptOpening)
|
||||||
|
if o.Finale {
|
||||||
|
g.OnFinale(ScriptFinale)
|
||||||
|
}
|
||||||
|
return g
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
CharacterManager.Register(Character{
|
||||||
|
Name: Dex,
|
||||||
|
SpeechColor: Amber,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Character = inkwell.Character
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
CharacterManager.Register(Character{
|
||||||
|
Name: TapeMystery,
|
||||||
|
SpeechColor: RGB(0xB9A98A),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
CharacterManager.Register(Character{
|
||||||
|
Name: Paul,
|
||||||
|
Speed: 96,
|
||||||
|
W: 28,
|
||||||
|
H: 68,
|
||||||
|
Start: inkwell.Point{
|
||||||
|
X: 120,
|
||||||
|
Y: 232,
|
||||||
|
},
|
||||||
|
SpeechColor: Ink,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
CharacterManager.Register(Character{
|
||||||
|
Name: TapeSupport,
|
||||||
|
SpeechColor: RGB(0xE8C86A),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
DialogManager.Register(Dialog{
|
||||||
|
Name: DlgDex,
|
||||||
|
Start: "root",
|
||||||
|
Nodes: []inkwell.DialogueNode{{
|
||||||
|
Name: "root",
|
||||||
|
Lines: []inkwell.DialogueLine{{
|
||||||
|
Speaker: Dex,
|
||||||
|
Text: "Talk.",
|
||||||
|
}},
|
||||||
|
Choices: []inkwell.DialogueChoice{
|
||||||
|
{
|
||||||
|
Text: "What am I doing here, Dex?",
|
||||||
|
Actions: []inkwell.Action{
|
||||||
|
inkwell.Say(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(FlagHasTape)),
|
||||||
|
Actions: []inkwell.Action{
|
||||||
|
inkwell.Say(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(FlagHasTape),
|
||||||
|
Actions: []inkwell.Action{
|
||||||
|
inkwell.Say(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(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()},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Dialog = inkwell.Dialogue
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
DialogManager.Register(Dialog{
|
||||||
|
Name: DlgMysteryTape,
|
||||||
|
Start: "root",
|
||||||
|
Nodes: []inkwell.DialogueNode{{
|
||||||
|
Name: "root",
|
||||||
|
Lines: []inkwell.DialogueLine{{
|
||||||
|
Speaker: Dex,
|
||||||
|
Text: "Nothing. Warm, spinning, and silent.",
|
||||||
|
}},
|
||||||
|
Choices: []inkwell.DialogueChoice{
|
||||||
|
{
|
||||||
|
Text: "Are you sure it works?",
|
||||||
|
Actions: []inkwell.Action{
|
||||||
|
inkwell.Say(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()},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
ItemManager.Register(Item{
|
||||||
|
Name: ItemBlackArmilla,
|
||||||
|
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."),
|
||||||
|
),
|
||||||
|
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."),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Item = inkwell.Item
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
ItemManager.Register(Item{
|
||||||
|
Name: ItemMysteryTape,
|
||||||
|
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."),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
ItemManager.Register(Item{
|
||||||
|
Name: ItemNoodleLetter,
|
||||||
|
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."),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
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 (
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SceneManager.Register(Scene{
|
||||||
|
Name: SceneAlley,
|
||||||
|
Title: "ALLEY — BEHIND NOODLE'S HOUSE",
|
||||||
|
Background: BgAlley,
|
||||||
|
Exits: []inkwell.Exit{
|
||||||
|
{
|
||||||
|
To: SceneNoodleHouse,
|
||||||
|
Label: "back out to the street",
|
||||||
|
Side: inkwell.ExitLeft,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Actors: []inkwell.SceneActor{
|
||||||
|
{
|
||||||
|
CharacterName: Paul,
|
||||||
|
At: inkwell.Point{
|
||||||
|
X: 120,
|
||||||
|
Y: 232,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
OnEnter: setVarIfEmpty(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(FlagHasTape),
|
||||||
|
inkwell.Say(Paul, "Empty. Whatever was in there is on me now."),
|
||||||
|
inkwell.Say(Paul, "Loose brick. There's a gap behind it."),
|
||||||
|
),
|
||||||
|
OnUse: inkwell.If(inkwell.Flag(FlagPoliceTip),
|
||||||
|
inkwell.If(inkwell.Flag(FlagHasTape),
|
||||||
|
inkwell.Say(Paul, "There's nothing else in there."),
|
||||||
|
inkwell.Seq(
|
||||||
|
inkwell.Say(Paul, "“Behind the brick.” All right then."),
|
||||||
|
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(Paul, "One loose brick. I'm not taking the alley apart over it."),
|
||||||
|
),
|
||||||
|
OnTake: inkwell.Say(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(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."),
|
||||||
|
),
|
||||||
|
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."),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func bin() inkwell.Hotspot {
|
||||||
|
return inkwell.Hotspot{
|
||||||
|
Name: "bin",
|
||||||
|
Label: "toppled bin",
|
||||||
|
Area: inkwell.Rect(240, 170, 88, 60),
|
||||||
|
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?"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fireEscape() inkwell.Hotspot {
|
||||||
|
return inkwell.Hotspot{
|
||||||
|
Name: "fire_escape",
|
||||||
|
Label: "fire escape",
|
||||||
|
Area: inkwell.Rect(528, 44, 88, 160),
|
||||||
|
OnLook: inkwell.Say(Paul, "Runs all the way to the roof. Bottom rung is two metres over my head."),
|
||||||
|
OnUse: inkwell.Say(Paul, "Can't reach it. I'd need something to stand on."),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SceneManager.Register(Scene{
|
||||||
|
Name: SceneBBSTerminal,
|
||||||
|
Title: "TERMINAL — BBS ACCESS POINT",
|
||||||
|
Background: BgBBSTerminal,
|
||||||
|
Exits: []inkwell.Exit{
|
||||||
|
{
|
||||||
|
Label: "step back from the terminal",
|
||||||
|
Side: inkwell.ExitNear,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SceneManager.Register(Scene{
|
||||||
|
Name: SceneBVKBranch,
|
||||||
|
Title: "BVK — SAN FRANCISCO BRANCH",
|
||||||
|
Background: BgBVKBranch,
|
||||||
|
Exits: []inkwell.Exit{
|
||||||
|
exitToSelector,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SceneManager.Register(Scene{
|
||||||
|
Name: SceneColumbarium,
|
||||||
|
Title: "COLUMBARIUM — DEX'S MEMORIAL",
|
||||||
|
Background: BgColumbarium,
|
||||||
|
Exits: []inkwell.Exit{
|
||||||
|
exitToSelector,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SceneManager.Register(Scene{
|
||||||
|
Name: SceneCuratorShop,
|
||||||
|
Title: "CURATOR SERVICE — THE MASTER'S WORKSHOP",
|
||||||
|
Background: BgCuratorShop,
|
||||||
|
Exits: []inkwell.Exit{
|
||||||
|
exitToSelector,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SceneManager.Register(Scene{
|
||||||
|
Name: SceneHackerspace,
|
||||||
|
Title: "HACKERSPACE",
|
||||||
|
Background: BgHackerspace,
|
||||||
|
Exits: []inkwell.Exit{
|
||||||
|
exitToSelector,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SceneManager.Register(Scene{
|
||||||
|
Name: SceneHospital,
|
||||||
|
Title: "HOSPITAL — PSYCHIATRIC WING",
|
||||||
|
Background: BgHospital,
|
||||||
|
Exits: []inkwell.Exit{
|
||||||
|
exitToSelector,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SceneManager.Register(Scene{
|
||||||
|
Name: SceneIceCreamShop,
|
||||||
|
Title: "ICE CREAM SHOP — WHERE THE BAR WAS",
|
||||||
|
Background: BgIceCreamShop,
|
||||||
|
Exits: []inkwell.Exit{
|
||||||
|
exitToSelector,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Scene = inkwell.Scene
|
||||||
|
|
||||||
|
func prepareScene() {
|
||||||
|
fillSelectorPins()
|
||||||
|
for _, entity := range SceneManager.All() {
|
||||||
|
SceneManager.Set(sceneDefaults(entity))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sceneFloor = []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 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 {
|
||||||
|
ctx.Game.State.SetVar(name, v)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SceneManager.Register(Scene{
|
||||||
|
Name: SceneNoodleHouse,
|
||||||
|
Title: "NOODLE'S HOUSE — SEALED",
|
||||||
|
Background: BgNoodleHouse,
|
||||||
|
Exits: []inkwell.Exit{
|
||||||
|
exitToSelector,
|
||||||
|
{
|
||||||
|
To: SceneAlley,
|
||||||
|
Label: "the alley behind the house",
|
||||||
|
Side: inkwell.ExitRight,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SceneManager.Register(Scene{
|
||||||
|
Name: SceneNormanApartment,
|
||||||
|
Title: "NORMAN'S APARTMENT",
|
||||||
|
Background: BgNormanApartment,
|
||||||
|
Exits: []inkwell.Exit{
|
||||||
|
exitToSelector,
|
||||||
|
{
|
||||||
|
To: SceneRooftopHideout,
|
||||||
|
Label: "the stairs up to the roof",
|
||||||
|
Side: inkwell.ExitBack,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
To: SceneBBSTerminal,
|
||||||
|
Label: "Norman's terminal",
|
||||||
|
Side: inkwell.ExitRight,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SceneManager.Register(Scene{
|
||||||
|
Name: ScenePaulShop,
|
||||||
|
Title: "PAUL'S SHOP — JUNK AND GARAGE",
|
||||||
|
Background: BgPaulShop,
|
||||||
|
Exits: []inkwell.Exit{
|
||||||
|
{
|
||||||
|
To: SceneNoodleHouse,
|
||||||
|
Label: "the bus to San Francisco",
|
||||||
|
Side: inkwell.ExitNear,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SceneManager.Register(Scene{
|
||||||
|
Name: ScenePoliceStation,
|
||||||
|
Title: "SFPD — STATION AND HOLDING",
|
||||||
|
Background: BgPoliceStation,
|
||||||
|
Exits: []inkwell.Exit{
|
||||||
|
exitToSelector,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SceneManager.Register(Scene{
|
||||||
|
Name: ScenePublicBBS,
|
||||||
|
Title: "PUBLIC BBS TERMINAL",
|
||||||
|
Background: BgPublicBBS,
|
||||||
|
Exits: []inkwell.Exit{
|
||||||
|
exitToSelector,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SceneManager.Register(Scene{
|
||||||
|
Name: SceneRooftopHideout,
|
||||||
|
Title: "NORMAN'S ROOFTOP HIDEOUT",
|
||||||
|
Background: BgRooftopHideout,
|
||||||
|
Exits: []inkwell.Exit{
|
||||||
|
{
|
||||||
|
To: SceneNormanApartment,
|
||||||
|
Label: "back down into the flat",
|
||||||
|
Side: inkwell.ExitNear,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
To: SceneBBSTerminal,
|
||||||
|
Label: "the old terminal",
|
||||||
|
Side: inkwell.ExitRight,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SceneManager.Register(Scene{
|
||||||
|
Name: SceneSamizdatPress,
|
||||||
|
Title: "SAMIZDAT PRINTING HOUSE",
|
||||||
|
Background: BgSamizdatPress,
|
||||||
|
Exits: []inkwell.Exit{
|
||||||
|
exitToSelector,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SceneManager.Register(Scene{
|
||||||
|
Name: SceneScrapMarket,
|
||||||
|
Title: "SCRAP MARKET",
|
||||||
|
Background: BgScrapMarket,
|
||||||
|
Exits: []inkwell.Exit{
|
||||||
|
exitToSelector,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SceneManager.Register(Scene{
|
||||||
|
Name: SceneSecretClub,
|
||||||
|
Title: "SECRET CLUB — THE UNDERWATER SUN",
|
||||||
|
Background: BgSecretClub,
|
||||||
|
Exits: []inkwell.Exit{
|
||||||
|
{
|
||||||
|
To: SceneBBSTerminal,
|
||||||
|
Label: "the terminal in the corner",
|
||||||
|
Side: inkwell.ExitBack,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
To: SceneTrinketShop,
|
||||||
|
Label: "back out through the shop",
|
||||||
|
Side: inkwell.ExitNear,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SceneManager.Register(Scene{
|
||||||
|
Name: SceneSecretLab,
|
||||||
|
Title: "SECRET RESEARCH LABORATORY",
|
||||||
|
Background: BgSecretLab,
|
||||||
|
Exits: []inkwell.Exit{
|
||||||
|
exitToSelector,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
var exitToSelector = inkwell.Exit{
|
||||||
|
To: SceneSelector,
|
||||||
|
Label: "the rest of the city",
|
||||||
|
Side: inkwell.ExitNear,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SceneManager.Register(Scene{
|
||||||
|
Name: SceneSelector,
|
||||||
|
Title: "SAN FRANCISCO",
|
||||||
|
Background: BgSelector,
|
||||||
|
Actors: []inkwell.SceneActor{},
|
||||||
|
Walkboxes: []inkwell.Polygon{},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func fillSelectorPins() {
|
||||||
|
selector, ok := SceneManager.Get(SceneSelector)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var exits []inkwell.Exit
|
||||||
|
for _, entity := range SceneManager.All() {
|
||||||
|
if !leadsToSelector(entity) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
exits = append(exits, inkwell.Exit{
|
||||||
|
To: entity.Name,
|
||||||
|
Label: pinLabel(entity.Title),
|
||||||
|
Area: pin(len(exits)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
selector.Exits = exits
|
||||||
|
SceneManager.Set(selector)
|
||||||
|
}
|
||||||
|
|
||||||
|
func leadsToSelector(s Scene) bool {
|
||||||
|
for _, e := range s.Exits {
|
||||||
|
if e.To == 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
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SceneManager.Register(Scene{
|
||||||
|
Name: SceneServerFarm,
|
||||||
|
Title: "SERVER FARM",
|
||||||
|
Background: BgServerFarm,
|
||||||
|
Exits: []inkwell.Exit{
|
||||||
|
exitToSelector,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SceneManager.Register(Scene{
|
||||||
|
Name: SceneShowroom,
|
||||||
|
Title: "NEUMATRONIC SHOWROOM",
|
||||||
|
Background: BgShowroom,
|
||||||
|
Exits: []inkwell.Exit{
|
||||||
|
exitToSelector,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SceneManager.Register(Scene{
|
||||||
|
Name: SceneSmallRestaurant,
|
||||||
|
Title: "SMALL RESTAURANT — NEXT DOOR",
|
||||||
|
Background: BgSmallRestaurant,
|
||||||
|
Exits: []inkwell.Exit{
|
||||||
|
{
|
||||||
|
To: SceneTrinketShop,
|
||||||
|
Label: "back to the trinket shop",
|
||||||
|
Side: inkwell.ExitLeft,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SceneManager.Register(Scene{
|
||||||
|
Name: SceneStreet,
|
||||||
|
Title: "STREET",
|
||||||
|
Background: BgStreet,
|
||||||
|
Exits: []inkwell.Exit{
|
||||||
|
exitToSelector,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SceneManager.Register(Scene{
|
||||||
|
Name: SceneTrinketShop,
|
||||||
|
Title: "CHINATOWN — TRINKET SHOP",
|
||||||
|
Background: BgTrinketShop,
|
||||||
|
Exits: []inkwell.Exit{
|
||||||
|
exitToSelector,
|
||||||
|
{
|
||||||
|
To: SceneSmallRestaurant,
|
||||||
|
Label: "the eating place next door",
|
||||||
|
Side: inkwell.ExitRight,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
To: SceneSecretClub,
|
||||||
|
Label: "the way in at the back",
|
||||||
|
Side: inkwell.ExitBack,
|
||||||
|
Needs: FlagClubEntry,
|
||||||
|
Blocked: inkwell.Say(Paul, "Just a shop, as far as the man behind the counter is concerned."),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
ScriptManager.Register(Script{
|
||||||
|
Name: ScriptFinale,
|
||||||
|
Actions: inkwell.Seq(
|
||||||
|
SetMode(ModeCutscene),
|
||||||
|
inkwell.Wait(0.6),
|
||||||
|
inkwell.Say(Paul, "I'm putting it in. That's all this is."),
|
||||||
|
inkwell.Wait(0.4),
|
||||||
|
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.Wait(1.0),
|
||||||
|
inkwell.ShowEnd("REAL WORLD — end of game two"),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Script = inkwell.Script
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
ScriptManager.Register(Script{
|
||||||
|
Name: ScriptOpening,
|
||||||
|
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?"),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
ScriptManager.Register(Script{
|
||||||
|
Name: ScriptTapeInsert,
|
||||||
|
Actions: inkwell.Seq(
|
||||||
|
Fn(func(ctx *inkwell.Ctx) {
|
||||||
|
item := World.TakePending()
|
||||||
|
if item == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
World.SetSlot2(item)
|
||||||
|
ctx.Game.Inventory.Remove(item)
|
||||||
|
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."),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Theme = inkwell.Theme
|
||||||
|
|
||||||
|
const (
|
||||||
|
RealWorld = "realworld-93"
|
||||||
|
NokiaPunk = "nokia-punk"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RGB(hex uint32) color.Color {
|
||||||
|
return color.RGBA{
|
||||||
|
R: uint8(hex >> 16),
|
||||||
|
G: uint8(hex >> 8),
|
||||||
|
B: uint8(hex),
|
||||||
|
A: 0xff,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RGBA(hex uint32, a uint8) color.Color {
|
||||||
|
return color.RGBA{
|
||||||
|
R: uint8(hex >> 16),
|
||||||
|
G: uint8(hex >> 8),
|
||||||
|
B: uint8(hex),
|
||||||
|
A: a,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package inc
|
||||||
|
|
||||||
|
var (
|
||||||
|
Green = RGB(0x3BE86B)
|
||||||
|
GreenLo = RGB(0x1F7A39)
|
||||||
|
npBlack = RGB(0x030603)
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
ThemeManager.Register(Theme{
|
||||||
|
Name: NokiaPunk,
|
||||||
|
PanelBG: npBlack,
|
||||||
|
StatusText: Green,
|
||||||
|
FlashText: RGB(0xD8F06A),
|
||||||
|
VerbButtonBG: RGB(0x08140A),
|
||||||
|
VerbButtonSelectedBG: GreenLo,
|
||||||
|
VerbButtonText: Green,
|
||||||
|
InventorySlotBG: RGB(0x08140A),
|
||||||
|
InventorySlotSelectedBG: GreenLo,
|
||||||
|
SpeechBubbleBG: RGBA(0x030603, 0xDC),
|
||||||
|
SpeechDefaultText: Green,
|
||||||
|
DialogBG: RGBA(0x030603, 0xF0),
|
||||||
|
DialogBorder: Green,
|
||||||
|
DialogChoiceBG: RGB(0x08140A),
|
||||||
|
DialogChoiceHover: RGB(0xB4FFC8),
|
||||||
|
DialogSpeaker: RGB(0xB4FFC8),
|
||||||
|
DialogText: Green,
|
||||||
|
EndCardBG: RGBA(0x000000, 0xFA),
|
||||||
|
EndCardText: Green,
|
||||||
|
CursorColor: Green,
|
||||||
|
HotspotOutline: GreenLo,
|
||||||
|
SceneBackdrop: npBlack,
|
||||||
|
TopBarBG: RGB(0x061006),
|
||||||
|
TopBarText: GreenLo,
|
||||||
|
TopBarAccent: Green,
|
||||||
|
ChatLogBG: RGB(0x040A04),
|
||||||
|
ChatLogPrompt: GreenLo,
|
||||||
|
ChatLogResponse: Green,
|
||||||
|
ChatLogSystem: RGB(0x156030),
|
||||||
|
CharacterPanelBG: RGB(0x040A04),
|
||||||
|
CharacterPanelBorder: Green,
|
||||||
|
CharacterPanelTitle: RGB(0xB4FFC8),
|
||||||
|
})
|
||||||
|
}
|
||||||