new file structure

This commit is contained in:
2026-08-29 23:25:36 +02:00
parent bbc3faf3d7
commit ec48cd6b37
96 changed files with 805 additions and 969 deletions
+35 -25
View File
@@ -29,7 +29,7 @@ go run . -screen selector # the location selector: the map the city hangs off
go run . -screen hospital # any screen in the deck, e.g. to look at the art go run . -screen hospital # any screen in the deck, e.g. to look at the art
``` ```
Screen names are the constants in `internal/names` (`selector`, `paul_shop`, Screen names are the constants in `names.manager.go` (`selector`, `paul_shop`,
`noodle_house`, `alley`, `norman_apartment`, `police_station`, `hackerspace`, `noodle_house`, `alley`, `norman_apartment`, `police_station`, `hackerspace`,
`ice_cream_shop`, `trinket_shop`, `small_restaurant`, `secret_club`, `ice_cream_shop`, `trinket_shop`, `small_restaurant`, `secret_club`,
`bbs_terminal`, `street`, `hospital`, `server_farm`, `secret_lab`, `bbs_terminal`, `street`, `hospital`, `server_farm`, `secret_lab`,
@@ -151,18 +151,24 @@ licence.
## Structure ## 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 `register…`, its shared types, its list.
``` ```
main.go flags + inkwell.Run main.go flags + inkwell.Run
internal/names/ entity names and world-state keys inc/names.manager.go entity names and world-state keys
internal/theme/ realworld-93 + nokia-punk inc/theme.manager.go realworld-93 + nokia-punk
internal/world/ unsaved runtime state, custom actions, action pump inc/world.*.go unsaved runtime state, custom actions, action pump
internal/ui/ HUD: layout, custom widgets, coloured text inc/ui.*.go HUD: layout, custom widgets, coloured text
internal/content/ one file per registered entity, by kind inc/<kind>.<name>.go one file per registered entity, by kind
internal/boot/ wiring inc/boot.manager.go wiring
``` ```
`world` deliberately knows nothing about the HUD or the content — both build on Nothing is enforced by the compiler any more, so the layering is a rule the
it, so the reverse would be a cycle: code keeps by hand: the world knows nothing about the HUD or the content, and
both build on it, never the other way round.
``` ```
names ← theme ← world ← ui names ← theme ← world ← ui
@@ -170,26 +176,30 @@ names ← theme ← world ← ui
content ──┴── boot ← main content ──┴── boot ← main
``` ```
Content is one registered entity per file, grouped by kind: Content is one registered entity per file, and the category prefix groups them
the way directories used to:
``` ```
internal/content/ background.*.go one file per screen: background.paul_shop.go, …
background/ one file per screen: paul_shop.go, noodle_house.go, … character.*.go character.paul.go character.dex.go character.mystery_tape.go
character/ paul.go dex.go mystery_tape.go support_tape.go item.*.go item.noodle_letter.go item.black_market_armilla.go …
item/ noodle_letter.go black_market_armilla.go mystery_tape.go dialog.*.go dialog.dex_talk.go dialog.mystery_tape_silent.go
dialog/ dex_talk.go mystery_tape_silent.go script.*.go script.tape_insert.go script.awakening_finale.go
script/ tape_insert.go awakening_finale.go screen.*.go one file per screen: screen.alley.go, … + screen.exit.go
screen/ one file per screen: alley.go, trinket_shop.go, … + exit.go
``` ```
Adding a screen means adding `screen/<name>.go` and `background/<name>.go`, and Adding a screen means adding `screen.<name>.go` and `background.<name>.go`, and
one line in each package's list. Nothing else moves. one line in each category's manager list. Nothing else moves.
Since everything shares one namespace, an entity's constructor carries its
category: `screenAlley()` is the screen, `backgroundAlley()` the painting behind
it, `itemMysteryTape()` the prop and `characterMysteryTape()` the voice on it.
**On the word "screen".** inkwell's entity is called a `Scene`, and that is the **On the word "screen".** inkwell's entity is called a `Scene`, and that is the
type every file in `screen/` returns — but the wiki, the concept-art deck and type every `screen.*.go` file returns — but the wiki, the concept-art deck and
the beat tables all count *screens*, so the package, the files and the flag say the beat tables all count *screens*, so the category, the files and the flag say
screen. "Scene" survives only where the code is talking to the engine screen. "Scene" survives only where the code is talking to the engine
(`world.EnterScene`, `SceneManager`). (`EnterScene`, `SceneManager`).
Art lives in `assets/bg/` and is embedded into the binary (`main.go`), because 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. js/wasm has no OS filesystem and `make binaries` packages the executable alone.
@@ -234,13 +244,13 @@ for a small engine change.
1. **`drawText` discards colour.** In `asset.text.go` the colour argument is 1. **`drawText` discards colour.** In `asset.text.go` the colour argument is
`_ = c` and rendering goes through `ebitenutil.DebugPrintAt`, which only `_ = c` and rendering goes through `ebitenutil.DebugPrintAt`, which only
draws white. Every text colour in `Theme` is therefore inert. *Workaround:* draws white. Every text colour in `Theme` is therefore inert. *Workaround:*
`ui/text.go` renders onto a scratch image and blits it tinted with `ui.text.go` renders onto a scratch image and blits it tinted with
`ColorScale`. The custom widgets colour correctly; the built-ins `ColorScale`. The custom widgets colour correctly; the built-ins
(`StatusLine`, `DialogBox`, `TopBar`, `InventoryBar`) are still white. (`StatusLine`, `DialogBox`, `TopBar`, `InventoryBar`) are still white.
*Fix:* move `drawText` to `text/v2` — no call site would change. *Fix:* move `drawText` to `text/v2` — no call site would change.
2. **`queueAction` is unexported**, so a domain widget cannot start an action. 2. **`queueAction` is unexported**, so a domain widget cannot start an action.
*Workaround:* the pump in `world/action.go` drives its own `Runner` through *Workaround:* the pump in `world.action.go` drives its own `Runner` through
the exported `inkwell.Ctx`. *Caveat:* it runs alongside the engine's script the exported `inkwell.Ctx`. *Caveat:* it runs alongside the engine's script
runner, not instead of it. runner, not instead of it.
@@ -251,7 +261,7 @@ for a small engine change.
character instead, escalating on repeats. character instead, escalating on repeats.
4. **No exported `CurrentScene()`**, but the pump needs `Ctx.Scene`. 4. **No exported `CurrentScene()`**, but the pump needs `Ctx.Scene`.
*Workaround:* `world.EnterScene` is the first step of every scene's *Workaround:* `EnterScene` is the first step of every scene's
`OnEnter`; the domain tracks it. `OnEnter`; the domain tracks it.
5. **Widgets have no `Visible` field and the `Manager` cannot unregister**, so 5. **Widgets have no `Visible` field and the `Manager` cannot unregister**, so
@@ -1,18 +1,16 @@
package background package inc
import ( import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
) )
// alley — part of the catalogue's "Noodle háza & a sikátor" row — not in the deck. // alley — part of the catalogue's "Noodle háza & a sikátor" row — not in the deck.
// //
// Not painted yet: the file is missing on purpose, and inkwell draws its // Not painted yet: the file is missing on purpose, and inkwell draws its
// placeholder instead — which is exactly the greybox convention. // placeholder instead — which is exactly the greybox convention.
func alley() inkwell.Asset { func backgroundAlley() inkwell.Asset {
return inkwell.Asset{ return inkwell.Asset{
Name: names.BgAlley, Name: BgAlley,
Path: "assets/bg/alley.png", Path: "assets/bg/alley.png",
Kind: inkwell.AssetImage, Kind: inkwell.AssetImage,
} }
@@ -1,15 +1,13 @@
package background package inc
import ( import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
) )
// bbsTerminal — deck 11. // bbsTerminal — deck 11.
func bbsTerminal() inkwell.Asset { func backgroundBbsTerminal() inkwell.Asset {
return inkwell.Asset{ return inkwell.Asset{
Name: names.BgBBSTerminal, Name: BgBBSTerminal,
Path: "assets/bg/bbs_terminal.png", Path: "assets/bg/bbs_terminal.png",
Kind: inkwell.AssetImage, Kind: inkwell.AssetImage,
} }
@@ -1,15 +1,13 @@
package background package inc
import ( import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
) )
// bvkBranch — deck 18. // bvkBranch — deck 18.
func bvkBranch() inkwell.Asset { func backgroundBvkBranch() inkwell.Asset {
return inkwell.Asset{ return inkwell.Asset{
Name: names.BgBVKBranch, Name: BgBVKBranch,
Path: "assets/bg/bvk_branch.png", Path: "assets/bg/bvk_branch.png",
Kind: inkwell.AssetImage, Kind: inkwell.AssetImage,
} }
@@ -1,15 +1,13 @@
package background package inc
import ( import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
) )
// columbarium — deck 23. // columbarium — deck 23.
func columbarium() inkwell.Asset { func backgroundColumbarium() inkwell.Asset {
return inkwell.Asset{ return inkwell.Asset{
Name: names.BgColumbarium, Name: BgColumbarium,
Path: "assets/bg/columbarium.png", Path: "assets/bg/columbarium.png",
Kind: inkwell.AssetImage, Kind: inkwell.AssetImage,
} }
@@ -1,15 +1,13 @@
package background package inc
import ( import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
) )
// curatorShop — deck 19. // curatorShop — deck 19.
func curatorShop() inkwell.Asset { func backgroundCuratorShop() inkwell.Asset {
return inkwell.Asset{ return inkwell.Asset{
Name: names.BgCuratorShop, Name: BgCuratorShop,
Path: "assets/bg/curator_shop.png", Path: "assets/bg/curator_shop.png",
Kind: inkwell.AssetImage, Kind: inkwell.AssetImage,
} }
@@ -1,15 +1,13 @@
package background package inc
import ( import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
) )
// hackerspace — deck 06. // hackerspace — deck 06.
func hackerspace() inkwell.Asset { func backgroundHackerspace() inkwell.Asset {
return inkwell.Asset{ return inkwell.Asset{
Name: names.BgHackerspace, Name: BgHackerspace,
Path: "assets/bg/hackerspace.png", Path: "assets/bg/hackerspace.png",
Kind: inkwell.AssetImage, Kind: inkwell.AssetImage,
} }
@@ -1,15 +1,13 @@
package background package inc
import ( import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
) )
// hospital — deck 13. // hospital — deck 13.
func hospital() inkwell.Asset { func backgroundHospital() inkwell.Asset {
return inkwell.Asset{ return inkwell.Asset{
Name: names.BgHospital, Name: BgHospital,
Path: "assets/bg/hospital.png", Path: "assets/bg/hospital.png",
Kind: inkwell.AssetImage, Kind: inkwell.AssetImage,
} }
@@ -1,15 +1,13 @@
package background package inc
import ( import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
) )
// iceCreamShop — deck 07. // iceCreamShop — deck 07.
func iceCreamShop() inkwell.Asset { func backgroundIceCreamShop() inkwell.Asset {
return inkwell.Asset{ return inkwell.Asset{
Name: names.BgIceCreamShop, Name: BgIceCreamShop,
Path: "assets/bg/ice_cream_shop.png", Path: "assets/bg/ice_cream_shop.png",
Kind: inkwell.AssetImage, Kind: inkwell.AssetImage,
} }
+47
View File
@@ -0,0 +1,47 @@
package inc
// Backgrounds — one image asset per screen, one file each.
//
// A background is the whole of a screen's art: inkwell scales it over the
// entire window, so every file here points at a 640×380 PNG under assets/bg/,
// the size the concept-art deck is drawn at. Two of them are not painted yet
// and fall back to inkwell's placeholder.
//
// The order below is the deck's, which is the wiki's location-catalogue order
// (projects/realworld/entities#helyszinek).
import (
inkwell "git.teletypegames.org/engines/inkwell"
)
// registerBackground adds every background to the game.
func registerBackground(w *World) {
for _, a := range []inkwell.Asset{
backgroundSelector(),
backgroundPaulShop(),
backgroundNoodleHouse(),
backgroundNormanApartment(),
backgroundPoliceStation(),
backgroundAlley(),
backgroundHackerspace(),
backgroundIceCreamShop(),
backgroundTrinketShop(),
backgroundSmallRestaurant(),
backgroundSecretClub(),
backgroundBbsTerminal(),
backgroundStreet(),
backgroundHospital(),
backgroundServerFarm(),
backgroundSecretLab(),
backgroundRooftopHideout(),
backgroundPublicBBS(),
backgroundBvkBranch(),
backgroundCuratorShop(),
backgroundScrapMarket(),
backgroundSamizdatPress(),
backgroundShowroom(),
backgroundColumbarium(),
} {
w.G.AssetManager.Register(a)
}
}
@@ -1,15 +1,13 @@
package background package inc
import ( import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
) )
// noodleHouse — deck 02. // noodleHouse — deck 02.
func noodleHouse() inkwell.Asset { func backgroundNoodleHouse() inkwell.Asset {
return inkwell.Asset{ return inkwell.Asset{
Name: names.BgNoodleHouse, Name: BgNoodleHouse,
Path: "assets/bg/noodle_house.png", Path: "assets/bg/noodle_house.png",
Kind: inkwell.AssetImage, Kind: inkwell.AssetImage,
} }
@@ -1,15 +1,13 @@
package background package inc
import ( import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
) )
// normanApartment — deck 03. // normanApartment — deck 03.
func normanApartment() inkwell.Asset { func backgroundNormanApartment() inkwell.Asset {
return inkwell.Asset{ return inkwell.Asset{
Name: names.BgNormanApartment, Name: BgNormanApartment,
Path: "assets/bg/norman_apartment.png", Path: "assets/bg/norman_apartment.png",
Kind: inkwell.AssetImage, Kind: inkwell.AssetImage,
} }
@@ -1,15 +1,13 @@
package background package inc
import ( import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
) )
// paulShop — deck 01. // paulShop — deck 01.
func paulShop() inkwell.Asset { func backgroundPaulShop() inkwell.Asset {
return inkwell.Asset{ return inkwell.Asset{
Name: names.BgPaulShop, Name: BgPaulShop,
Path: "assets/bg/paul_shop.png", Path: "assets/bg/paul_shop.png",
Kind: inkwell.AssetImage, Kind: inkwell.AssetImage,
} }
@@ -1,15 +1,13 @@
package background package inc
import ( import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
) )
// policeStation — deck 04. // policeStation — deck 04.
func policeStation() inkwell.Asset { func backgroundPoliceStation() inkwell.Asset {
return inkwell.Asset{ return inkwell.Asset{
Name: names.BgPoliceStation, Name: BgPoliceStation,
Path: "assets/bg/police_station.png", Path: "assets/bg/police_station.png",
Kind: inkwell.AssetImage, Kind: inkwell.AssetImage,
} }
@@ -1,15 +1,13 @@
package background package inc
import ( import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
) )
// publicBbs — deck 17. // publicBbs — deck 17.
func publicBbs() inkwell.Asset { func backgroundPublicBBS() inkwell.Asset {
return inkwell.Asset{ return inkwell.Asset{
Name: names.BgPublicBBS, Name: BgPublicBBS,
Path: "assets/bg/public_bbs.png", Path: "assets/bg/public_bbs.png",
Kind: inkwell.AssetImage, Kind: inkwell.AssetImage,
} }
@@ -1,15 +1,13 @@
package background package inc
import ( import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
) )
// rooftopHideout — deck 16. // rooftopHideout — deck 16.
func rooftopHideout() inkwell.Asset { func backgroundRooftopHideout() inkwell.Asset {
return inkwell.Asset{ return inkwell.Asset{
Name: names.BgRooftopHideout, Name: BgRooftopHideout,
Path: "assets/bg/rooftop_hideout.png", Path: "assets/bg/rooftop_hideout.png",
Kind: inkwell.AssetImage, Kind: inkwell.AssetImage,
} }
@@ -1,15 +1,13 @@
package background package inc
import ( import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
) )
// samizdatPress — deck 21. // samizdatPress — deck 21.
func samizdatPress() inkwell.Asset { func backgroundSamizdatPress() inkwell.Asset {
return inkwell.Asset{ return inkwell.Asset{
Name: names.BgSamizdatPress, Name: BgSamizdatPress,
Path: "assets/bg/samizdat_press.png", Path: "assets/bg/samizdat_press.png",
Kind: inkwell.AssetImage, Kind: inkwell.AssetImage,
} }
@@ -1,15 +1,13 @@
package background package inc
import ( import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
) )
// scrapMarket — deck 20. // scrapMarket — deck 20.
func scrapMarket() inkwell.Asset { func backgroundScrapMarket() inkwell.Asset {
return inkwell.Asset{ return inkwell.Asset{
Name: names.BgScrapMarket, Name: BgScrapMarket,
Path: "assets/bg/scrap_market.png", Path: "assets/bg/scrap_market.png",
Kind: inkwell.AssetImage, Kind: inkwell.AssetImage,
} }
@@ -1,15 +1,13 @@
package background package inc
import ( import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
) )
// secretClub — deck 10. // secretClub — deck 10.
func secretClub() inkwell.Asset { func backgroundSecretClub() inkwell.Asset {
return inkwell.Asset{ return inkwell.Asset{
Name: names.BgSecretClub, Name: BgSecretClub,
Path: "assets/bg/secret_club.png", Path: "assets/bg/secret_club.png",
Kind: inkwell.AssetImage, Kind: inkwell.AssetImage,
} }
@@ -1,15 +1,13 @@
package background package inc
import ( import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
) )
// secretLab — deck 15. // secretLab — deck 15.
func secretLab() inkwell.Asset { func backgroundSecretLab() inkwell.Asset {
return inkwell.Asset{ return inkwell.Asset{
Name: names.BgSecretLab, Name: BgSecretLab,
Path: "assets/bg/secret_lab.png", Path: "assets/bg/secret_lab.png",
Kind: inkwell.AssetImage, Kind: inkwell.AssetImage,
} }
@@ -1,18 +1,16 @@
package background package inc
import ( import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
) )
// selector — the wiki's Helyszínválasztó — no deck number, and not drawn. // selector — the wiki's Helyszínválasztó — no deck number, and not drawn.
// //
// Not painted yet: the file is missing on purpose, and inkwell draws its // Not painted yet: the file is missing on purpose, and inkwell draws its
// placeholder instead — which is exactly the greybox convention. // placeholder instead — which is exactly the greybox convention.
func selector() inkwell.Asset { func backgroundSelector() inkwell.Asset {
return inkwell.Asset{ return inkwell.Asset{
Name: names.BgSelector, Name: BgSelector,
Path: "assets/bg/selector.png", Path: "assets/bg/selector.png",
Kind: inkwell.AssetImage, Kind: inkwell.AssetImage,
} }
@@ -1,15 +1,13 @@
package background package inc
import ( import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
) )
// serverFarm — deck 14. // serverFarm — deck 14.
func serverFarm() inkwell.Asset { func backgroundServerFarm() inkwell.Asset {
return inkwell.Asset{ return inkwell.Asset{
Name: names.BgServerFarm, Name: BgServerFarm,
Path: "assets/bg/server_farm.png", Path: "assets/bg/server_farm.png",
Kind: inkwell.AssetImage, Kind: inkwell.AssetImage,
} }
@@ -1,15 +1,13 @@
package background package inc
import ( import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
) )
// showroom — deck 22. // showroom — deck 22.
func showroom() inkwell.Asset { func backgroundShowroom() inkwell.Asset {
return inkwell.Asset{ return inkwell.Asset{
Name: names.BgShowroom, Name: BgShowroom,
Path: "assets/bg/showroom.png", Path: "assets/bg/showroom.png",
Kind: inkwell.AssetImage, Kind: inkwell.AssetImage,
} }
@@ -1,15 +1,13 @@
package background package inc
import ( import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
) )
// smallRestaurant — deck 09. // smallRestaurant — deck 09.
func smallRestaurant() inkwell.Asset { func backgroundSmallRestaurant() inkwell.Asset {
return inkwell.Asset{ return inkwell.Asset{
Name: names.BgSmallRestaurant, Name: BgSmallRestaurant,
Path: "assets/bg/small_restaurant.png", Path: "assets/bg/small_restaurant.png",
Kind: inkwell.AssetImage, Kind: inkwell.AssetImage,
} }
@@ -1,15 +1,13 @@
package background package inc
import ( import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
) )
// street — deck 12. // street — deck 12.
func street() inkwell.Asset { func backgroundStreet() inkwell.Asset {
return inkwell.Asset{ return inkwell.Asset{
Name: names.BgStreet, Name: BgStreet,
Path: "assets/bg/street.png", Path: "assets/bg/street.png",
Kind: inkwell.AssetImage, Kind: inkwell.AssetImage,
} }
@@ -1,15 +1,13 @@
package background package inc
import ( import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
) )
// trinketShop — deck 08. // trinketShop — deck 08.
func trinketShop() inkwell.Asset { func backgroundTrinketShop() inkwell.Asset {
return inkwell.Asset{ return inkwell.Asset{
Name: names.BgTrinketShop, Name: BgTrinketShop,
Path: "assets/bg/trinket_shop.png", Path: "assets/bg/trinket_shop.png",
Kind: inkwell.AssetImage, Kind: inkwell.AssetImage,
} }
+59
View File
@@ -0,0 +1,59 @@
// Package inc is the whole game, in one flat package.
//
// Every file is named [category].[name].go: one category per concern, one
// entity per file. A category's <category>.manager.go holds what ties that
// category together — its Register, its shared types — and the files beside it
// hold one screen, one background, one item each.
//
// This file is the boot category: it wires the layers together, and it is the
// only place where the world, the content, the theme and the HUD meet. main.go
// knows nothing else.
package inc
import (
inkwell "git.teletypegames.org/engines/inkwell"
)
// Opts are the run parameters.
type Opts struct {
Screen string // starting screen; empty means the alley
Finale bool // start with the finale, to try the Nokia-punk theme switch
}
// New builds a ready-to-run game.
func New(o Opts) *inkwell.Game {
start := o.Screen
if start == "" {
start = ScreenAlley
}
g := inkwell.NewGame("Real World", ScreenW, ScreenH)
g.MaxLogLines = 64
w := newWorld(g)
registerTheme(g)
registerContent(w)
g.UseTheme(RealWorld)
registerUI(w)
g.StartAt(start)
g.OnStart(inkwell.Seq(bootOpening(w, start, o.Finale)...))
return g
}
func bootOpening(w *World, start string, finale bool) []inkwell.Action {
acts := []inkwell.Action{
EnterScene(w, start),
// Beat 2 (the police station) will set the tip flag. Until that beat
// is written we set it here so the alley slice is playable end to end —
// the conditional branch itself is real code, not bypassed.
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?"),
}
if finale {
acts = append(acts, inkwell.RunScript(ScriptFinale))
}
return acts
}
@@ -1,4 +1,4 @@
package boot_test package inc
import ( import (
"image/png" "image/png"
@@ -8,15 +8,11 @@ import (
"testing" "testing"
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/boot"
"git.teletypegames.org/games/realworld/internal/names"
"git.teletypegames.org/games/realworld/internal/ui"
) )
func build(t *testing.T) *inkwell.Game { func newTestGame(t *testing.T) *inkwell.Game {
t.Helper() t.Helper()
g := boot.New(boot.Opts{}) g := New(Opts{})
if err := g.Validate(); err != nil { if err := g.Validate(); err != nil {
t.Fatalf("Validate: %v", err) t.Fatalf("Validate: %v", err)
} }
@@ -27,38 +23,38 @@ func build(t *testing.T) *inkwell.Game {
// a dialogue, script or item name would surface at runtime as a silent no-op // a dialogue, script or item name would surface at runtime as a silent no-op
// (RunDialogue and RunScript fail quietly), so check them here. // (RunDialogue and RunScript fail quietly), so check them here.
func TestReferencesResolve(t *testing.T) { func TestReferencesResolve(t *testing.T) {
g := build(t) g := newTestGame(t)
for _, n := range []string{names.DlgDex, names.DlgMysteryTape} { for _, n := range []string{DlgDex, DlgMysteryTape} {
if !g.DialogueManager.Has(n) { if !g.DialogueManager.Has(n) {
t.Errorf("missing dialogue: %q", n) t.Errorf("missing dialogue: %q", n)
} }
} }
for _, n := range []string{names.ScriptTapeInsert, names.ScriptFinale} { for _, n := range []string{ScriptTapeInsert, ScriptFinale} {
if !g.ScriptManager.Has(n) { if !g.ScriptManager.Has(n) {
t.Errorf("missing script: %q", n) t.Errorf("missing script: %q", n)
} }
} }
for _, n := range []string{names.ItemNoodleLetter, names.ItemMysteryTape, names.ItemBlackArmilla} { for _, n := range []string{ItemNoodleLetter, ItemMysteryTape, ItemBlackArmilla} {
if !g.ItemManager.Has(n) { if !g.ItemManager.Has(n) {
t.Errorf("missing item: %q", n) t.Errorf("missing item: %q", n)
} }
} }
for n := range names.Tapes { for n := range Tapes {
if !g.CharacterManager.Has(n) { if !g.CharacterManager.Has(n) {
t.Errorf("tape character not registered: %q", n) t.Errorf("tape character not registered: %q", n)
} }
} }
// Every loadable tape needs an item, a character and a dialogue. // Every loadable tape needs an item, a character and a dialogue.
for item, char := range names.TapeItems { for item, char := range TapeItems {
if !g.ItemManager.Has(item) { if !g.ItemManager.Has(item) {
t.Errorf("tape item not registered: %q", item) t.Errorf("tape item not registered: %q", item)
} }
if !g.CharacterManager.Has(char) { if !g.CharacterManager.Has(char) {
t.Errorf("tape character not registered: %q", char) t.Errorf("tape character not registered: %q", char)
} }
if !g.DialogueManager.Has(names.TapeDialogue(item)) { if !g.DialogueManager.Has(TapeDialogue(item)) {
t.Errorf("tape %q has no dialogue: %q", item, names.TapeDialogue(item)) t.Errorf("tape %q has no dialogue: %q", item, TapeDialogue(item))
} }
} }
} }
@@ -67,10 +63,10 @@ func TestReferencesResolve(t *testing.T) {
// the world's. Without that, "if something is amber, a tape said it" is not // the world's. Without that, "if something is amber, a tape said it" is not
// readable. // readable.
func TestTapeVoicesAreDistinct(t *testing.T) { func TestTapeVoicesAreDistinct(t *testing.T) {
g := build(t) g := newTestGame(t)
paul, _ := g.CharacterManager.Get(names.Paul) paul, _ := g.CharacterManager.Get(Paul)
seen := map[[4]uint32]string{} seen := map[[4]uint32]string{}
for n := range names.Tapes { for n := range Tapes {
c, _ := g.CharacterManager.Get(n) c, _ := g.CharacterManager.Get(n)
if c.SpeechColor == nil { if c.SpeechColor == nil {
t.Errorf("%s: no SpeechColor", n) t.Errorf("%s: no SpeechColor", n)
@@ -91,29 +87,29 @@ func TestTapeVoicesAreDistinct(t *testing.T) {
// HUD geometry: the two channels must not overlap, and both must stay inside // HUD geometry: the two channels must not overlap, and both must stay inside
// the HUD strip. // the HUD strip.
func TestHUDGeometry(t *testing.T) { func TestHUDGeometry(t *testing.T) {
g := build(t) g := newTestGame(t)
ch, ok := g.UIManager.Get("channel") ch, ok := g.UIManager.Get("channel")
if !ok { if !ok {
t.Fatal("no 'channel' widget") t.Fatal("no 'channel' widget")
} }
tc := ch.(*ui.TapeChannel) tc := ch.(*TapeChannel)
if tc.Bounds.X < ui.DividerX { if tc.Bounds.X < DividerX {
t.Errorf("tape channel crosses into the world side: X=%v < %v", tc.Bounds.X, ui.DividerX) t.Errorf("tape channel crosses into the world side: X=%v < %v", tc.Bounds.X, DividerX)
} }
if tc.Bounds.X+tc.Bounds.W > ui.ScreenW { if tc.Bounds.X+tc.Bounds.W > ScreenW {
t.Errorf("tape channel runs off screen: %v", tc.Bounds.X+tc.Bounds.W) t.Errorf("tape channel runs off screen: %v", tc.Bounds.X+tc.Bounds.W)
} }
if tc.Bounds.Y < ui.HUDTop { if tc.Bounds.Y < HUDTop {
t.Errorf("tape channel reaches into the scene: Y=%v < %v", tc.Bounds.Y, ui.HUDTop) t.Errorf("tape channel reaches into the scene: Y=%v < %v", tc.Bounds.Y, HUDTop)
} }
sl, ok := g.UIManager.Get("tapes") sl, ok := g.UIManager.Get("tapes")
if !ok { if !ok {
t.Fatal("no 'tapes' widget") t.Fatal("no 'tapes' widget")
} }
if ts := sl.(*ui.TapeSlots); ts.Bounds.X+ts.Bounds.W > ui.DividerX { if ts := sl.(*TapeSlots); ts.Bounds.X+ts.Bounds.W > DividerX {
t.Errorf("tape slots cross the divider: %v > %v", ts.Bounds.X+ts.Bounds.W, ui.DividerX) t.Errorf("tape slots cross the divider: %v > %v", ts.Bounds.X+ts.Bounds.W, DividerX)
} }
// The dialogue box deliberately covers only the left region, so a tape can // The dialogue box deliberately covers only the left region, so a tape can
@@ -122,16 +118,16 @@ func TestHUDGeometry(t *testing.T) {
if !ok { if !ok {
t.Fatal("no 'dialog' widget") t.Fatal("no 'dialog' widget")
} }
if box := db.(*inkwell.DialogBox); box.Bounds.X+box.Bounds.W > ui.DividerX { if box := db.(*inkwell.DialogBox); box.Bounds.X+box.Bounds.W > DividerX {
t.Errorf("dialogue box would cover the tape channel: %v > %v", t.Errorf("dialogue box would cover the tape channel: %v > %v",
box.Bounds.X+box.Bounds.W, ui.DividerX) box.Bounds.X+box.Bounds.W, DividerX)
} }
} }
// The guard must be registered FIRST so it ticks LAST among the widgets — // The guard must be registered FIRST so it ticks LAST among the widgets —
// otherwise it would swallow HUD clicks. // otherwise it would swallow HUD clicks.
func TestWidgetOrder(t *testing.T) { func TestWidgetOrder(t *testing.T) {
g := build(t) g := newTestGame(t)
n := g.UIManager.Names() n := g.UIManager.Names()
if len(n) == 0 || n[0] != "usewith_guard" { if len(n) == 0 || n[0] != "usewith_guard" {
t.Errorf("usewith_guard is not registered first: %v", n) t.Errorf("usewith_guard is not registered first: %v", n)
@@ -152,12 +148,12 @@ func TestWidgetOrder(t *testing.T) {
// painting that is not ScreenW×ScreenH is silently squashed to fit. That is why // painting that is not ScreenW×ScreenH is silently squashed to fit. That is why
// the screen is 640×380 in the first place; this keeps the two in step. // the screen is 640×380 in the first place; this keeps the two in step.
func TestSceneBackgroundsResolve(t *testing.T) { func TestSceneBackgroundsResolve(t *testing.T) {
g := build(t) g := newTestGame(t)
// Not drawn yet: these render as placeholders on purpose. The alley is // Not drawn yet: these render as placeholders on purpose. The alley is
// missing from the concept-art deck; the selector is the wiki's menu // missing from the concept-art deck; the selector is the wiki's menu
// screen, which the deck does not cover at all. // screen, which the deck does not cover at all.
undrawn := map[string]bool{names.BgAlley: true, names.BgSelector: true} undrawn := map[string]bool{BgAlley: true, BgSelector: true}
for _, n := range g.SceneManager.Names() { for _, n := range g.SceneManager.Names() {
s := g.SceneManager.MustGet(n) s := g.SceneManager.MustGet(n)
@@ -169,7 +165,7 @@ func TestSceneBackgroundsResolve(t *testing.T) {
if undrawn[a.Name] { if undrawn[a.Name] {
continue continue
} }
f, err := os.Open(filepath.Join("..", "..", a.Path)) f, err := os.Open(filepath.Join("..", a.Path))
if err != nil { if err != nil {
t.Errorf("scene %q: background file missing: %v", n, err) t.Errorf("scene %q: background file missing: %v", n, err)
continue continue
@@ -180,16 +176,16 @@ func TestSceneBackgroundsResolve(t *testing.T) {
t.Errorf("scene %q: unreadable background: %v", n, err) t.Errorf("scene %q: unreadable background: %v", n, err)
continue continue
} }
if cfg.Width != ui.ScreenW || cfg.Height != ui.ScreenH { if cfg.Width != ScreenW || cfg.Height != ScreenH {
t.Errorf("scene %q: background is %d×%d, want %d×%d — it will be stretched", t.Errorf("scene %q: background is %d×%d, want %d×%d — it will be stretched",
n, cfg.Width, cfg.Height, ui.ScreenW, ui.ScreenH) n, cfg.Width, cfg.Height, ScreenW, ScreenH)
} }
} }
} }
// exits reads the connection graph back out of the registered screens. The // exits reads the connection graph back out of the registered screens. The
// exit hotspots are named "exit:<target>", which is what keeps the graph // exit hotspots are named "exit:<target>", which is what keeps the graph
// machine-readable — see content/screen/exit.go. // machine-readable — see screen.exit.go.
func exits(g *inkwell.Game, screen string) []string { func exits(g *inkwell.Game, screen string) []string {
var out []string var out []string
for _, h := range g.SceneManager.MustGet(screen).Hotspots { for _, h := range g.SceneManager.MustGet(screen).Hotspots {
@@ -203,7 +199,7 @@ func exits(g *inkwell.Game, screen string) []string {
// An exit that names a screen nobody registered is a dead end the player walks // An exit that names a screen nobody registered is a dead end the player walks
// into: the engine logs "changeScene: unknown" and nothing happens on screen. // into: the engine logs "changeScene: unknown" and nothing happens on screen.
func TestExitsResolve(t *testing.T) { func TestExitsResolve(t *testing.T) {
g := build(t) g := newTestGame(t)
for _, n := range g.SceneManager.Names() { for _, n := range g.SceneManager.Names() {
for _, to := range exits(g, n) { for _, to := range exits(g, n) {
if !g.SceneManager.Has(to) { if !g.SceneManager.Has(to) {
@@ -220,10 +216,10 @@ func TestExitsResolve(t *testing.T) {
// The walk starts at Paul's shop, where the letter arrives, and mostly runs // The walk starts at Paul's shop, where the letter arrives, and mostly runs
// through the selector: that is what the wiki's location graph is centred on. // through the selector: that is what the wiki's location graph is centred on.
func TestEveryScreenIsReachable(t *testing.T) { func TestEveryScreenIsReachable(t *testing.T) {
g := build(t) g := newTestGame(t)
seen := map[string]bool{names.ScreenPaulShop: true} seen := map[string]bool{ScreenPaulShop: true}
queue := []string{names.ScreenPaulShop} queue := []string{ScreenPaulShop}
for len(queue) > 0 { for len(queue) > 0 {
at := queue[0] at := queue[0]
queue = queue[1:] queue = queue[1:]
@@ -237,7 +233,7 @@ func TestEveryScreenIsReachable(t *testing.T) {
for _, n := range g.SceneManager.Names() { for _, n := range g.SceneManager.Names() {
if !seen[n] { if !seen[n] {
t.Errorf("screen %q cannot be reached from %q by its exits", n, names.ScreenPaulShop) t.Errorf("screen %q cannot be reached from %q by its exits", n, ScreenPaulShop)
} }
} }
} }
@@ -245,7 +241,7 @@ func TestEveryScreenIsReachable(t *testing.T) {
// A screen with no way out at all is a trap: the player walks in and the only // A screen with no way out at all is a trap: the player walks in and the only
// way on is the debug walk. // way on is the debug walk.
func TestEveryScreenHasAWayOut(t *testing.T) { func TestEveryScreenHasAWayOut(t *testing.T) {
g := build(t) g := newTestGame(t)
for _, n := range g.SceneManager.Names() { for _, n := range g.SceneManager.Names() {
var ways int var ways int
for _, h := range g.SceneManager.MustGet(n).Hotspots { for _, h := range g.SceneManager.MustGet(n).Hotspots {
@@ -1,17 +1,14 @@
package character package inc
import ( import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
"git.teletypegames.org/games/realworld/internal/theme"
) )
// dex is the personality imprint of a dead friend, permanently in slot 1 of // dex is the personality imprint of a dead friend, permanently in slot 1 of
// Paul's Armilla. Constant commentary, hint system, dialogue partner. // Paul's Armilla. Constant commentary, hint system, dialogue partner.
func dex() inkwell.Character { func characterDex() inkwell.Character {
return inkwell.Character{ return inkwell.Character{
Name: names.Dex, Name: Dex,
SpeechColor: theme.Amber, SpeechColor: Amber,
} }
} }
+23
View File
@@ -0,0 +1,23 @@
package inc
// Characters — the game's cast, one file each.
//
// Tapes are characters too, not props — the wiki is explicit about it. They
// have no body in the scene, so they carry no W/H and never appear in
// Scene.Actors; their voice lives in the tape channel (see TapeSay).
import (
inkwell "git.teletypegames.org/engines/inkwell"
)
// registerCharacter adds every character to the game.
func registerCharacter(w *World) {
for _, c := range []inkwell.Character{
characterPaul(),
characterDex(),
characterMysteryTape(),
characterSupportTape(),
} {
w.G.CharacterManager.Register(c)
}
}
@@ -1,10 +1,7 @@
package character package inc
import ( import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
"git.teletypegames.org/games/realworld/internal/theme"
) )
// mysteryTape is the tape found in the alley — in truth Norman's Personal // mysteryTape is the tape found in the alley — in truth Norman's Personal
@@ -12,9 +9,9 @@ import (
// //
// Its voice is a colder, greyer amber than Dex's. That pays off in reverse: // Its voice is a colder, greyer amber than Dex's. That pays off in reverse:
// it never once spoke in the same voice as the friend. // it never once spoke in the same voice as the friend.
func mysteryTape() inkwell.Character { func characterMysteryTape() inkwell.Character {
return inkwell.Character{ return inkwell.Character{
Name: names.TapeMystery, Name: TapeMystery,
SpeechColor: theme.RGB(0xB9A98A), SpeechColor: RGB(0xB9A98A),
} }
} }
@@ -1,23 +1,20 @@
package character package inc
import ( import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
"git.teletypegames.org/games/realworld/internal/theme"
) )
// paul is the player character: a junk dealer and street survivor, technically // paul is the player character: a junk dealer and street survivor, technically
// competent, living on the edge of the city. // competent, living on the edge of the city.
func paul() inkwell.Character { func characterPaul() inkwell.Character {
return inkwell.Character{ return inkwell.Character{
Name: names.Paul, Name: Paul,
Speed: 96, Speed: 96,
W: 28, W: 28,
H: 68, H: 68,
Start: inkwell.Point{X: 120, Y: 232}, Start: inkwell.Point{X: 120, Y: 232},
// Bone white: the world and Paul. If something is amber, a tape said // Bone white: the world and Paul. If something is amber, a tape said
// it — see the colour rule in README. // it — see the colour rule in README.
SpeechColor: theme.Ink, SpeechColor: Ink,
} }
} }
+14
View File
@@ -0,0 +1,14 @@
package inc
import (
inkwell "git.teletypegames.org/engines/inkwell"
)
// supportTape answers everything with basic information, insufferably. Dex
// hates it. Acquired in Chinatown (beat 5) — not reachable yet.
func characterSupportTape() inkwell.Character {
return inkwell.Character{
Name: TapeSupport,
SpeechColor: RGB(0xE8C86A),
}
}
+19
View File
@@ -0,0 +1,19 @@
package inc
// Content — the composition root for everything the game declares:
// assets, characters, items, dialogues, scripts and scenes.
//
// Each registered entity lives in its own file, named after its category:
// adding a screen means adding screen.<name>.go and background.<name>.go, plus
// one line in each category's manager list — nothing else moves.
// registerContent adds every entity to the game. Order matters only in that
// screens reference backgrounds and characters, which Game.Validate cross-checks.
func registerContent(w *World) {
registerBackground(w)
registerCharacter(w)
registerItem(w)
registerDialog(w)
registerScript(w)
registerScreen(w)
}
@@ -1,41 +1,39 @@
package dialog package inc
import ( import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
) )
// dexTalk is the standing conversation with Dex. Its branches are gated on // dexTalk is the standing conversation with Dex. Its branches are gated on
// story flags, so the same entry point stays useful as the game progresses. // story flags, so the same entry point stays useful as the game progresses.
func dexTalk() inkwell.Dialogue { func dialogDexTalk() inkwell.Dialogue {
return inkwell.Dialogue{ return inkwell.Dialogue{
Name: names.DlgDex, Name: DlgDex,
Start: "root", Start: "root",
Nodes: []inkwell.DialogueNode{{ Nodes: []inkwell.DialogueNode{{
Name: "root", Name: "root",
Lines: []inkwell.DialogueLine{{Speaker: names.Dex, Text: "Talk."}}, Lines: []inkwell.DialogueLine{{Speaker: Dex, Text: "Talk."}},
Choices: []inkwell.DialogueChoice{ Choices: []inkwell.DialogueChoice{
{ {
Text: "What am I doing here, Dex?", Text: "What am I doing here, Dex?",
Actions: []inkwell.Action{ Actions: []inkwell.Action{
inkwell.Say(names.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.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"), inkwell.GotoNode("root"),
}, },
}, },
{ {
Text: "What do you know about Noodle?", Text: "What do you know about Noodle?",
Show: inkwell.Not(inkwell.Flag(names.FlagHasTape)), Show: inkwell.Not(inkwell.Flag(FlagHasTape)),
Actions: []inkwell.Action{ Actions: []inkwell.Action{
inkwell.Say(names.Dex, "He traded cracked Armillas. That isn't charity, Paul, that's a business. The kind they take you in for."), 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"), inkwell.GotoNode("root"),
}, },
}, },
{ {
Text: "What do you make of this tape?", Text: "What do you make of this tape?",
Show: inkwell.Flag(names.FlagHasTape), Show: inkwell.Flag(FlagHasTape),
Actions: []inkwell.Action{ Actions: []inkwell.Action{
inkwell.Say(names.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.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"), inkwell.GotoNode("root"),
}, },
}, },
@@ -43,7 +41,7 @@ func dexTalk() inkwell.Dialogue {
Text: "I miss you.", Text: "I miss you.",
Once: true, Once: true,
Actions: []inkwell.Action{ Actions: []inkwell.Action{
inkwell.Say(names.Dex, "I'm four kilobytes of a dead man. Don't do this to yourself."), inkwell.Say(Dex, "I'm four kilobytes of a dead man. Don't do this to yourself."),
inkwell.GotoNode("root"), inkwell.GotoNode("root"),
}, },
}, },
@@ -1,4 +1,6 @@
// Package dialog registers the game's dialogue trees. package inc
// Dialogues — the game's dialogue trees, one file each.
// //
// Talking to a tape carries the same weight as talking to a human NPC — the // Talking to a tape carries the same weight as talking to a human NPC — the
// wiki states this explicitly. Tapes are dialogue partners, not menus. // wiki states this explicitly. Tapes are dialogue partners, not menus.
@@ -7,19 +9,16 @@
// deck wanted it struck through but still visible, so the list becomes a // deck wanted it struck through but still visible, so the list becomes a
// memory of what you already tried. That is still open — see README, "Known // memory of what you already tried. That is still open — see README, "Known
// gaps". // gaps".
package dialog
import ( import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/world"
) )
// Register adds every dialogue to the game. // registerDialog adds every dialogue to the game.
func Register(w *world.World) { func registerDialog(w *World) {
for _, d := range []inkwell.Dialogue{ for _, d := range []inkwell.Dialogue{
dexTalk(), dialogDexTalk(),
mysteryTapeSilent(), dialogMysteryTapeSilent(),
} { } {
w.G.DialogueManager.Register(d) w.G.DialogueManager.Register(d)
} }
@@ -1,26 +1,24 @@
package dialog package inc
import ( import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
) )
// mysteryTapeSilent covers the stretch before the club trigger (beat 6), while // mysteryTapeSilent covers the stretch before the club trigger (beat 6), while
// the tape is still mute. Talking to it is really Dex talking about it — the // the tape is still mute. Talking to it is really Dex talking about it — the
// tape itself says nothing. // tape itself says nothing.
func mysteryTapeSilent() inkwell.Dialogue { func dialogMysteryTapeSilent() inkwell.Dialogue {
return inkwell.Dialogue{ return inkwell.Dialogue{
Name: names.DlgMysteryTape, Name: DlgMysteryTape,
Start: "root", Start: "root",
Nodes: []inkwell.DialogueNode{{ Nodes: []inkwell.DialogueNode{{
Name: "root", Name: "root",
Lines: []inkwell.DialogueLine{{Speaker: names.Dex, Text: "Nothing. Warm, spinning, and silent."}}, Lines: []inkwell.DialogueLine{{Speaker: Dex, Text: "Nothing. Warm, spinning, and silent."}},
Choices: []inkwell.DialogueChoice{ Choices: []inkwell.DialogueChoice{
{ {
Text: "Are you sure it works?", Text: "Are you sure it works?",
Actions: []inkwell.Action{ Actions: []inkwell.Action{
inkwell.Say(names.Dex, "It works. It just isn't talking to you. That is not the same as silence."), inkwell.Say(Dex, "It works. It just isn't talking to you. That is not the same as silence."),
inkwell.GotoNode("root"), inkwell.GotoNode("root"),
}, },
}, },
+26
View File
@@ -0,0 +1,26 @@
package inc
import (
inkwell "git.teletypegames.org/engines/inkwell"
)
// blackMarketArmilla is flavour, not a quest trigger: the gap between
// legitimised technology and street reality.
func itemBlackMarketArmilla() inkwell.Item {
return inkwell.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{
// An AUTHORED failure. Nothing is consumed, the character reacts,
// the tape remarks. Never a generic error line.
ItemNoodleLetter: inkwell.Seq(
inkwell.Say(Paul, "A letter and a bracelet. Brilliant."),
TapeSay(Dex, "Nothing. Which is what I'd charge for it."),
),
},
}
}
@@ -1,21 +1,20 @@
// Package item registers the game's inventory items. package inc
// Items — the game's inventory, one file each.
// //
// Personal Tapes are inventory items and quest triggers at the same time: they // Personal Tapes are inventory items and quest triggers at the same time: they
// go into slot 2 of Paul's Armilla, and loading one activates its effect. // go into slot 2 of Paul's Armilla, and loading one activates its effect.
package item
import ( import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/world"
) )
// Register adds every item to the game. // registerItem adds every item to the game.
func Register(w *world.World) { func registerItem(w *World) {
for _, i := range []inkwell.Item{ for _, i := range []inkwell.Item{
noodleLetter(), itemNoodleLetter(),
blackMarketArmilla(), itemBlackMarketArmilla(),
mysteryTape(), itemMysteryTape(),
} { } {
w.G.ItemManager.Register(i) w.G.ItemManager.Register(i)
} }
+18
View File
@@ -0,0 +1,18 @@
package inc
import (
inkwell "git.teletypegames.org/engines/inkwell"
)
// mysteryTape is the engine of the investigation. Password-locked; it only
// starts speaking after the club trigger (beat 6).
func itemMysteryTape() inkwell.Item {
return inkwell.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."),
),
}
}
+17
View File
@@ -0,0 +1,17 @@
package inc
import (
inkwell "git.teletypegames.org/engines/inkwell"
)
// noodleLetter starts the story: it is what brings Paul to the city.
func itemNoodleLetter() inkwell.Item {
return inkwell.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."),
),
}
}
@@ -1,4 +1,6 @@
// Package names is the single source of entity names and world-state keys. package inc
// Names — the single source of entity names and world-state keys.
// //
// The names follow the wiki entity catalogue // The names follow the wiki entity catalogue
// (services/wiki-pages/pages/projects/realworld/entities) so that content and // (services/wiki-pages/pages/projects/realworld/entities) so that content and
@@ -7,9 +9,8 @@
// tape_behelyezes, sikator); the constants below are their English // tape_behelyezes, sikator); the constants below are their English
// equivalents — the mapping is one-to-one, in catalogue order. // equivalents — the mapping is one-to-one, in catalogue order.
// //
// This package imports nothing from the project, so every layer can depend on // These constants are the vocabulary every other category speaks in; nothing
// it without creating a cycle. // here depends on anything else in the game.
package names
// Characters. // Characters.
const ( const (
+104
View File
@@ -0,0 +1,104 @@
package inc
import (
inkwell "git.teletypegames.org/engines/inkwell"
)
// alley is the alley behind Noodle's house — beat 3, and the vertical slice the
// whole HUD is measured against: hotspots, a gated pickup, the two-slot
// Armilla, tape dialogue and an authored failure.
//
// The wiki keeps it in the same catalogue row as the house ("Noodle háza & a
// sikátor") and its graph has one arrow in, NoodleUtcafront → Sikátor, so the
// only way out is back to the street. The alley's own locked back door is the
// New Game+ hook and leads to a room the deck has not painted.
func screenAlley() screen {
return screen{
name: ScreenAlley,
title: "ALLEY — BEHIND NOODLE'S HOUSE",
background: BgAlley,
exits: []exit{
// On the left: the back door hotspot below owns the other end.
{to: ScreenNoodleHouse, label: "back out to the street", side: sideLeft},
},
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()},
}
}
// hidingPlace holds the mystery tape. It only opens once Paul has the tip from
// the police station (beat 2) — without it he does not know what to look for.
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."),
}
}
// backDoor is the New Game+ hook: the code can be entered on the first visit
// too, if the player already knows it.
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{
// An AUTHORED failure — not the engine's hardcoded flash.
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."),
}
}
@@ -1,6 +1,4 @@
package screen package inc
import "git.teletypegames.org/games/realworld/internal/names"
// bbsTerminal is the screen you get when Paul sits down at a terminal: the // bbsTerminal is the screen you get when Paul sits down at a terminal: the
// catalogue calls it a "Tárgy/portál", a physical object in the world that // catalogue calls it a "Tárgy/portál", a physical object in the world that
@@ -9,11 +7,11 @@ import "git.teletypegames.org/games/realworld/internal/names"
// It is the same screen wherever the terminal stands — the club's ECHO box, // It is the same screen wherever the terminal stands — the club's ECHO box,
// Norman's machine at the flat, the old one on the roof — which is why its way // Norman's machine at the flat, the old one on the roof — which is why its way
// out is "back", and not a place. // out is "back", and not a place.
func bbsTerminal() screen { func screenBbsTerminal() screen {
return screen{ return screen{
name: names.ScreenBBSTerminal, name: ScreenBBSTerminal,
title: "TERMINAL — BBS ACCESS POINT", title: "TERMINAL — BBS ACCESS POINT",
background: names.BgBBSTerminal, background: BgBBSTerminal,
exits: []exit{ exits: []exit{
{label: "step back from the terminal", side: sideNear}, {label: "step back from the terminal", side: sideNear},
}, },
@@ -1,16 +1,14 @@
package screen package inc
import "git.teletypegames.org/games/realworld/internal/names"
// bvkBranch is a 💡 suggestion: the local certification office, imprimatur // bvkBranch is a 💡 suggestion: the local certification office, imprimatur
// stamps and licence checks and numbered waiting rooms — everything a man with // stamps and licence checks and numbered waiting rooms — everything a man with
// a jailbroken two-slot Armilla should stay away from // a jailbroken two-slot Armilla should stay away from
// (wiki: entities#potenciális-további-helyszínek). // (wiki: entities#potenciális-további-helyszínek).
func bvkBranch() screen { func screenBvkBranch() screen {
return screen{ return screen{
name: names.ScreenBVKBranch, name: ScreenBVKBranch,
title: "BVK — SAN FRANCISCO BRANCH", title: "BVK — SAN FRANCISCO BRANCH",
background: names.BgBVKBranch, background: BgBVKBranch,
onSelector: true, onSelector: true,
} }
} }
@@ -1,16 +1,14 @@
package screen package inc
import "git.teletypegames.org/games/realworld/internal/names"
// columbarium is a 💡 suggestion: where Dex is buried. A quiet place, away // columbarium is a 💡 suggestion: where Dex is buried. A quiet place, away
// from the noise of the terminals — the loss-and-letting-go thread, which is // from the noise of the terminals — the loss-and-letting-go thread, which is
// the same thread as Norman and The AI // the same thread as Norman and The AI
// (wiki: entities#potenciális-további-helyszínek). // (wiki: entities#potenciális-további-helyszínek).
func columbarium() screen { func screenColumbarium() screen {
return screen{ return screen{
name: names.ScreenColumbarium, name: ScreenColumbarium,
title: "COLUMBARIUM — DEX'S MEMORIAL", title: "COLUMBARIUM — DEX'S MEMORIAL",
background: names.BgColumbarium, background: BgColumbarium,
onSelector: true, onSelector: true,
} }
} }
@@ -1,15 +1,13 @@
package screen package inc
import "git.teletypegames.org/games/realworld/internal/names"
// curatorShop is a 💡 suggestion: the last workshop of the declining curators' // curatorShop is a 💡 suggestion: the last workshop of the declining curators'
// guild — quartz tuning, tape repair, craft handed down for generations. The // guild — quartz tuning, tape repair, craft handed down for generations. The
// man who built Paul's second slot (wiki: entities#potenciális-további-helyszínek). // man who built Paul's second slot (wiki: entities#potenciális-további-helyszínek).
func curatorShop() screen { func screenCuratorShop() screen {
return screen{ return screen{
name: names.ScreenCuratorShop, name: ScreenCuratorShop,
title: "CURATOR SERVICE — THE MASTER'S WORKSHOP", title: "CURATOR SERVICE — THE MASTER'S WORKSHOP",
background: names.BgCuratorShop, background: BgCuratorShop,
onSelector: true, onSelector: true,
} }
} }
@@ -1,4 +1,4 @@
package screen package inc
// Exits — the connections between screens. // Exits — the connections between screens.
// //
@@ -25,9 +25,6 @@ package screen
import ( import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
"git.teletypegames.org/games/realworld/internal/world"
) )
type side int type side int
@@ -73,7 +70,7 @@ type exit struct {
// toSelector is the way back out to the rest of the city, on every screen the // toSelector is the way back out to the rest of the city, on every screen the
// wiki's Helyszínválasztó lists. // wiki's Helyszínválasztó lists.
func toSelector() exit { func toSelector() exit {
return exit{to: names.ScreenSelector, label: "the rest of the city", side: sideNear} return exit{to: ScreenSelector, label: "the rest of the city", side: sideNear}
} }
// exitName is how a connection stays machine-readable: the graph can be read // exitName is how a connection stays machine-readable: the graph can be read
@@ -85,13 +82,13 @@ func exitName(to string) string {
return "exit:" + to return "exit:" + to
} }
func (e exit) hotspot(w *world.World) inkwell.Hotspot { func (e exit) hotspot(w *World) inkwell.Hotspot {
var travel inkwell.Action = inkwell.GoTo(e.to) var travel inkwell.Action = inkwell.GoTo(e.to)
if e.to == "" { if e.to == "" {
travel = world.Back(w) travel = Back(w)
} }
if e.needs != "" { if e.needs != "" {
travel = inkwell.If(inkwell.Flag(e.needs), travel, inkwell.Say(names.Paul, e.blocked)) travel = inkwell.If(inkwell.Flag(e.needs), travel, inkwell.Say(Paul, e.blocked))
} }
area := e.area area := e.area
if area == nil { if area == nil {
@@ -102,8 +99,8 @@ func (e exit) hotspot(w *world.World) inkwell.Hotspot {
Label: e.label, Label: e.label,
Area: area, Area: area,
Cursor: inkwell.CursorExit, Cursor: inkwell.CursorExit,
OnLook: inkwell.Say(names.Paul, "That way: "+e.label+"."), OnLook: inkwell.Say(Paul, "That way: "+e.label+"."),
OnUse: travel, OnUse: travel,
OnTake: inkwell.Say(names.Paul, "It's a way out, not a thing."), OnTake: inkwell.Say(Paul, "It's a way out, not a thing."),
} }
} }
@@ -1,6 +1,4 @@
package screen package inc
import "git.teletypegames.org/games/realworld/internal/names"
// hackerspace: the community workshop at the edge of the Mission, printers and // hackerspace: the community workshop at the edge of the Mission, printers and
// stripped servers and paranoid amateur cryptographers. Where the server-farm // stripped servers and paranoid amateur cryptographers. Where the server-farm
@@ -8,11 +6,11 @@ import "git.teletypegames.org/games/realworld/internal/names"
// //
// The wiki's graph gives it no physical neighbour, so it hangs off the // The wiki's graph gives it no physical neighbour, so it hangs off the
// selector like every other main location. // selector like every other main location.
func hackerspace() screen { func screenHackerspace() screen {
return screen{ return screen{
name: names.ScreenHackerspace, name: ScreenHackerspace,
title: "HACKERSPACE", title: "HACKERSPACE",
background: names.BgHackerspace, background: BgHackerspace,
onSelector: true, onSelector: true,
} }
} }
@@ -1,14 +1,12 @@
package screen package inc
import "git.teletypegames.org/games/realworld/internal/names"
// hospital is the clinical dead end: Dr. Maren looked for a medical reason for // hospital is the clinical dead end: Dr. Maren looked for a medical reason for
// Norman's state and did not find one (wiki: entities, timeline 21). // Norman's state and did not find one (wiki: entities, timeline 21).
func hospital() screen { func screenHospital() screen {
return screen{ return screen{
name: names.ScreenHospital, name: ScreenHospital,
title: "HOSPITAL — PSYCHIATRIC WING", title: "HOSPITAL — PSYCHIATRIC WING",
background: names.BgHospital, background: BgHospital,
onSelector: true, onSelector: true,
} }
} }
@@ -1,6 +1,4 @@
package screen package inc
import "git.teletypegames.org/games/realworld/internal/names"
// iceCreamShop stands where Paul's old bar used to be, and they insist there // iceCreamShop stands where Paul's old bar used to be, and they insist there
// never was a bar. The BVK-registered terminal in the corner is the one the // never was a bar. The BVK-registered terminal in the corner is the one the
@@ -9,11 +7,11 @@ import "git.teletypegames.org/games/realworld/internal/names"
// The search he eventually allows is what names the club — a lead, not a door: // The search he eventually allows is what names the club — a lead, not a door:
// the way there is the trinket shop's address, so this screen has no exit of // the way there is the trinket shop's address, so this screen has no exit of
// its own. // its own.
func iceCreamShop() screen { func screenIceCreamShop() screen {
return screen{ return screen{
name: names.ScreenIceCreamShop, name: ScreenIceCreamShop,
title: "ICE CREAM SHOP — WHERE THE BAR WAS", title: "ICE CREAM SHOP — WHERE THE BAR WAS",
background: names.BgIceCreamShop, background: BgIceCreamShop,
onSelector: true, onSelector: true,
} }
} }
@@ -1,4 +1,6 @@
// Package screen registers the game's screens — one file per screen. package inc
// Screens — one file per screen.
// //
// "Screen" is this project's word: the wiki's catalogue, the concept-art deck // "Screen" is this project's word: the wiki's catalogue, the concept-art deck
// and the beat tables all count screens. inkwell's word for the same thing is // and the beat tables all count screens. inkwell's word for the same thing is
@@ -7,16 +9,12 @@
// engine. // engine.
// //
// A file here holds everything that is true of one screen: its picture, the // A file here holds everything that is true of one screen: its picture, the
// name on the top bar, where you can get to from it (exit.go), and — once its // name on the top bar, where you can get to from it (screen.exit.go), and — once its
// beat is written — its hotspots, NPCs and dialogue. Only the alley has that // beat is written — its hotspots, NPCs and dialogue. Only the alley has that
// last part so far; the rest are greybox. // last part so far; the rest are greybox.
package screen
import ( import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
"git.teletypegames.org/games/realworld/internal/world"
) )
// screen is what each file in this package fills in. // screen is what each file in this package fills in.
@@ -25,7 +23,7 @@ type screen struct {
title string title string
background string background string
// exits: where you can get to from here. See exit.go. // exits: where you can get to from here. See screen.exit.go.
exits []exit exits []exit
// onSelector marks a main location — one of those the wiki's // onSelector marks a main location — one of those the wiki's
// Helyszínválasztó lists (story#helyszín-gráf-vázlat). It gets a way back // Helyszínválasztó lists (story#helyszín-gráf-vázlat). It gets a way back
@@ -40,68 +38,68 @@ type screen struct {
onEnter inkwell.Action onEnter inkwell.Action
} }
// deck is every screen in the game, in the order of the wiki's location // screenDeck is every screen in the game, in the order of the wiki's location
// catalogue (entities#helyszinek) — which the concept-art deck follows number // catalogue (entities#helyszinek) — which the concept-art deck follows number
// for number. Registration order is this order, and it is also the order the // for number. Registration order is this order, and it is also the order the
// arrow keys walk (ui/screen_nav.go). // arrow keys walk (ui.screen_nav.go).
// //
// Deck 05, Norman's workplace, is in the catalogue but has not been drawn, so // Deck 05, Norman's workplace, is in the catalogue but has not been drawn, so
// it has no file yet. The alley has no deck number: the catalogue keeps it in // it has no file yet. The alley has no deck number: the catalogue keeps it in
// the same row as Noodle's house, and that is where it sits here too. // the same row as Noodle's house, and that is where it sits here too.
var deck = []func() screen{ var screenDeck = []func() screen{
paulShop, // 01 screenPaulShop, // 01
noodleHouse, // 02 screenNoodleHouse, // 02
alley, // 02, same catalogue row screenAlley, // 02, same catalogue row
normanApartment, // 03 screenNormanApartment, // 03
policeStation, // 04 screenPoliceStation, // 04
hackerspace, // 06 screenHackerspace, // 06
iceCreamShop, // 07 screenIceCreamShop, // 07
trinketShop, // 08 screenTrinketShop, // 08
smallRestaurant, // 09 screenSmallRestaurant, // 09
secretClub, // 10 screenSecretClub, // 10
bbsTerminal, // 11 screenBbsTerminal, // 11
street, // 12 screenStreet, // 12
hospital, // 13 screenHospital, // 13
serverFarm, // 14 screenServerFarm, // 14
secretLab, // 15 screenSecretLab, // 15
rooftopHideout, // 16 screenRooftopHideout, // 16
publicBBS, // 17 screenPublicBBS, // 17
bvkBranch, // 18 screenBvkBranch, // 18
curatorShop, // 19 screenCuratorShop, // 19
scrapMarket, // 20 screenScrapMarket, // 20
samizdatPress, // 21 screenSamizdatPress, // 21
showroom, // 22 screenShowroom, // 22
columbarium, // 23 screenColumbarium, // 23
} }
// Register adds every screen to the game, the selector first: it is the centre // registerScreen adds every screen to the game, the selector first: it is the centre
// of the wiki's location graph, and it is built from the deck rather than // of the wiki's location graph, and it is built from the screenDeck rather than
// listed in it. // listed in it.
func Register(w *world.World) { func registerScreen(w *World) {
screens := make([]screen, 0, len(deck)+1) screens := make([]screen, 0, len(screenDeck)+1)
for _, f := range deck { for _, f := range screenDeck {
screens = append(screens, f()) screens = append(screens, f())
} }
for _, s := range append([]screen{selector(screens)}, screens...) { for _, s := range append([]screen{screenSelector(screens)}, screens...) {
w.G.SceneManager.Register(build(w, s)) w.G.SceneManager.Register(screenBuild(w, s))
} }
} }
// The default floor: the bottom of the scene region, above the HUD divider. // The default floor: the bottom of the scene region, above the HUD divider.
// A screen with a painted floor of its own overrides it. // A screen with a painted floor of its own overrides it.
var floor = []inkwell.Polygon{ var screenFloor = []inkwell.Polygon{
inkwell.Poly( inkwell.Poly(
inkwell.Point{X: 16, Y: 196}, inkwell.Point{X: 624, Y: 196}, inkwell.Point{X: 16, Y: 196}, inkwell.Point{X: 624, Y: 196},
inkwell.Point{X: 624, Y: 258}, inkwell.Point{X: 16, Y: 258}, inkwell.Point{X: 624, Y: 258}, inkwell.Point{X: 16, Y: 258},
), ),
} }
// build turns a screen into the engine's scene. // screenBuild turns a screen into the engine's scene.
// //
// The authored hotspots go in front of the exits, because the engine takes the // The authored hotspots go in front of the exits, because the engine takes the
// first one whose area contains the click (HotspotAt): a painted thing beats // first one whose area contains the click (HotspotAt): a painted thing beats
// the edge strip an exit sits on, wherever the two overlap. // the edge strip an exit sits on, wherever the two overlap.
func build(w *world.World, s screen) inkwell.Scene { func screenBuild(w *World, s screen) inkwell.Scene {
exits := s.exits exits := s.exits
if s.onSelector { if s.onSelector {
exits = append(exits, toSelector()) exits = append(exits, toSelector())
@@ -115,14 +113,14 @@ func build(w *world.World, s screen) inkwell.Scene {
actors := s.actors actors := s.actors
if actors == nil { if actors == nil {
actors = []inkwell.SceneActor{ actors = []inkwell.SceneActor{
{CharacterName: names.Paul, At: inkwell.Point{X: 320, Y: 232}}, {CharacterName: Paul, At: inkwell.Point{X: 320, Y: 232}},
} }
} }
walkboxes := s.walkboxes walkboxes := s.walkboxes
if walkboxes == nil { if walkboxes == nil {
walkboxes = floor walkboxes = screenFloor
} }
onEnter := inkwell.Action(world.EnterScene(w, s.name)) onEnter := inkwell.Action(EnterScene(w, s.name))
if s.onEnter != nil { if s.onEnter != nil {
onEnter = inkwell.Seq(onEnter, s.onEnter) onEnter = inkwell.Seq(onEnter, s.onEnter)
} }
@@ -141,7 +139,7 @@ func build(w *world.World, s screen) inkwell.Scene {
// setVarIfEmpty only writes when the variable is still unset, so re-entering a // setVarIfEmpty only writes when the variable is still unset, so re-entering a
// screen does not clobber what the game has set in the meantime. // screen does not clobber what the game has set in the meantime.
func setVarIfEmpty(name string, v any) inkwell.Action { func setVarIfEmpty(name string, v any) inkwell.Action {
return world.Fn(func(ctx *inkwell.Ctx) { return Fn(func(ctx *inkwell.Ctx) {
if ctx.Game.State.Var(name) == nil { if ctx.Game.State.Var(name) == nil {
ctx.Game.State.SetVar(name, v) ctx.Game.State.SetVar(name, v)
} }
@@ -1,6 +1,4 @@
package screen package inc
import "git.teletypegames.org/games/realworld/internal/names"
// noodleHouse is the street front outside Noodle's place. By the time Paul // noodleHouse is the street front outside Noodle's place. By the time Paul
// gets there the police have sealed the house and taken Noodle in, and a // gets there the police have sealed the house and taken Noodle in, and a
@@ -8,14 +6,14 @@ import "git.teletypegames.org/games/realworld/internal/names"
// //
// The wiki's graph runs NoodleUtcafront → Sikátor: the alley behind the house // The wiki's graph runs NoodleUtcafront → Sikátor: the alley behind the house
// is reached from here, and from nowhere else. // is reached from here, and from nowhere else.
func noodleHouse() screen { func screenNoodleHouse() screen {
return screen{ return screen{
name: names.ScreenNoodleHouse, name: ScreenNoodleHouse,
title: "NOODLE'S HOUSE — SEALED", title: "NOODLE'S HOUSE — SEALED",
background: names.BgNoodleHouse, background: BgNoodleHouse,
onSelector: true, onSelector: true,
exits: []exit{ exits: []exit{
{to: names.ScreenAlley, label: "the alley behind the house", side: sideRight}, {to: ScreenAlley, label: "the alley behind the house", side: sideRight},
}, },
} }
} }
@@ -1,6 +1,4 @@
package screen package inc
import "git.teletypegames.org/games/realworld/internal/names"
// normanApartment is the flat Norman was taken from: his Armilla left behind // normanApartment is the flat Norman was taken from: his Armilla left behind
// on the side, his Personal Tape missing from it, his terminal full of the // on the side, his Personal Tape missing from it, his terminal full of the
@@ -9,15 +7,15 @@ import "git.teletypegames.org/games/realworld/internal/names"
// Two ways on from here. His terminal is one of the BBS access points the // Two ways on from here. His terminal is one of the BBS access points the
// catalogue lists ("a titkos klub ECHO-terminálja; később Norman lakása / // catalogue lists ("a titkos klub ECHO-terminálja; később Norman lakása /
// tetőterasz"), and the roof hideout is up his own stairs. // tetőterasz"), and the roof hideout is up his own stairs.
func normanApartment() screen { func screenNormanApartment() screen {
return screen{ return screen{
name: names.ScreenNormanApartment, name: ScreenNormanApartment,
title: "NORMAN'S APARTMENT", title: "NORMAN'S APARTMENT",
background: names.BgNormanApartment, background: BgNormanApartment,
onSelector: true, onSelector: true,
exits: []exit{ exits: []exit{
{to: names.ScreenRooftopHideout, label: "the stairs up to the roof", side: sideBack}, {to: ScreenRooftopHideout, label: "the stairs up to the roof", side: sideBack},
{to: names.ScreenBBSTerminal, label: "Norman's terminal", side: sideRight}, {to: ScreenBBSTerminal, label: "Norman's terminal", side: sideRight},
}, },
} }
} }
@@ -1,6 +1,4 @@
package screen package inc
import "git.teletypegames.org/games/realworld/internal/names"
// paulShop is where the game starts: the junk shop and garage Paul lives // paulShop is where the game starts: the junk shop and garage Paul lives
// behind (wiki: entities#helyszinek). It is the only screen outside San // behind (wiki: entities#helyszinek). It is the only screen outside San
@@ -10,13 +8,13 @@ import "git.teletypegames.org/games/realworld/internal/names"
// The bus is a screen the deck does not have, so the exit is the journey // The bus is a screen the deck does not have, so the exit is the journey
// itself: one way, with no coming back to the shop mid-story. That is also why // itself: one way, with no coming back to the shop mid-story. That is also why
// the shop is not on the selector — it is not one of the city's locations. // the shop is not on the selector — it is not one of the city's locations.
func paulShop() screen { func screenPaulShop() screen {
return screen{ return screen{
name: names.ScreenPaulShop, name: ScreenPaulShop,
title: "PAUL'S SHOP — JUNK AND GARAGE", title: "PAUL'S SHOP — JUNK AND GARAGE",
background: names.BgPaulShop, background: BgPaulShop,
exits: []exit{ exits: []exit{
{to: names.ScreenNoodleHouse, label: "the bus to San Francisco", side: sideNear}, {to: ScreenNoodleHouse, label: "the bus to San Francisco", side: sideNear},
}, },
} }
} }
@@ -1,6 +1,4 @@
package screen package inc
import "git.teletypegames.org/games/realworld/internal/names"
// policeStation is where Noodle is held for dealing in cracked Armillas, and // policeStation is where Noodle is held for dealing in cracked Armillas, and
// where a clerk quietly tips Paul off about the hiding place // where a clerk quietly tips Paul off about the hiding place
@@ -10,11 +8,11 @@ import "git.teletypegames.org/games/realworld/internal/names"
// but the deck paints it as one screen, so the reception desk, the waiting // but the deck paints it as one screen, so the reception desk, the waiting
// room and the interview window are hotspots inside this one, not screens of // room and the interview window are hotspots inside this one, not screens of
// their own. // their own.
func policeStation() screen { func screenPoliceStation() screen {
return screen{ return screen{
name: names.ScreenPoliceStation, name: ScreenPoliceStation,
title: "SFPD — STATION AND HOLDING", title: "SFPD — STATION AND HOLDING",
background: names.BgPoliceStation, background: BgPoliceStation,
onSelector: true, onSelector: true,
} }
} }
@@ -1,16 +1,14 @@
package screen package inc
import "git.teletypegames.org/games/realworld/internal/names"
// publicBBS is one of the wiki's 💡 suggestions: the canon has BBS terminals // publicBBS is one of the wiki's 💡 suggestions: the canon has BBS terminals
// standing in cafés, post offices and bus stops — live infrastructure, not // standing in cafés, post offices and bus stops — live infrastructure, not
// relics. An alternative way to read the boards, in a place where somebody can // relics. An alternative way to read the boards, in a place where somebody can
// read over your shoulder (wiki: entities#potenciális-további-helyszínek). // read over your shoulder (wiki: entities#potenciális-további-helyszínek).
func publicBBS() screen { func screenPublicBBS() screen {
return screen{ return screen{
name: names.ScreenPublicBBS, name: ScreenPublicBBS,
title: "PUBLIC BBS TERMINAL", title: "PUBLIC BBS TERMINAL",
background: names.BgPublicBBS, background: BgPublicBBS,
onSelector: true, onSelector: true,
} }
} }
@@ -1,6 +1,4 @@
package screen package inc
import "git.teletypegames.org/games/realworld/internal/names"
// rooftopHideout is Norman's hiding place: an abandoned roof terrace with his // rooftopHideout is Norman's hiding place: an abandoned roof terrace with his
// personal things, his notes and an old BBS terminal — the emotional high point // personal things, his notes and an old BBS terminal — the emotional high point
@@ -8,14 +6,14 @@ import "git.teletypegames.org/games/realworld/internal/names"
// //
// It is up the stairs from his flat, and it is one of the places the catalogue // It is up the stairs from his flat, and it is one of the places the catalogue
// puts a terminal. // puts a terminal.
func rooftopHideout() screen { func screenRooftopHideout() screen {
return screen{ return screen{
name: names.ScreenRooftopHideout, name: ScreenRooftopHideout,
title: "NORMAN'S ROOFTOP HIDEOUT", title: "NORMAN'S ROOFTOP HIDEOUT",
background: names.BgRooftopHideout, background: BgRooftopHideout,
exits: []exit{ exits: []exit{
{to: names.ScreenNormanApartment, label: "back down into the flat", side: sideNear}, {to: ScreenNormanApartment, label: "back down into the flat", side: sideNear},
{to: names.ScreenBBSTerminal, label: "the old terminal", side: sideRight}, {to: ScreenBBSTerminal, label: "the old terminal", side: sideRight},
}, },
} }
} }
@@ -1,16 +1,14 @@
package screen package inc
import "git.teletypegames.org/games/realworld/internal/names"
// samizdatPress is a 💡 suggestion: the Resistance's physical node — parody // samizdatPress is a 💡 suggestion: the Resistance's physical node — parody
// BVK databases, underground publications, a P2P node humming in the cellar. // BVK databases, underground publications, a P2P node humming in the cellar.
// The samizdat intellectuals are the first to write down that the world is // The samizdat intellectuals are the first to write down that the world is
// slipping (wiki: entities#potenciális-további-helyszínek). // slipping (wiki: entities#potenciális-további-helyszínek).
func samizdatPress() screen { func screenSamizdatPress() screen {
return screen{ return screen{
name: names.ScreenSamizdatPress, name: ScreenSamizdatPress,
title: "SAMIZDAT PRINTING HOUSE", title: "SAMIZDAT PRINTING HOUSE",
background: names.BgSamizdatPress, background: BgSamizdatPress,
onSelector: true, onSelector: true,
} }
} }
@@ -1,15 +1,13 @@
package screen package inc
import "git.teletypegames.org/games/realworld/internal/names"
// scrapMarket is a 💡 suggestion: the black market in condemned ACPs and bare, // scrapMarket is a 💡 suggestion: the black market in condemned ACPs and bare,
// unstamped tape. Canon says the leak of scrap is not an accident — "something // unstamped tape. Canon says the leak of scrap is not an accident — "something
// wants to live on" (wiki: entities#potenciális-további-helyszínek). // wants to live on" (wiki: entities#potenciális-további-helyszínek).
func scrapMarket() screen { func screenScrapMarket() screen {
return screen{ return screen{
name: names.ScreenScrapMarket, name: ScreenScrapMarket,
title: "SCRAP MARKET", title: "SCRAP MARKET",
background: names.BgScrapMarket, background: BgScrapMarket,
onSelector: true, onSelector: true,
} }
} }
@@ -1,6 +1,4 @@
package screen package inc
import "git.teletypegames.org/games/realworld/internal/names"
// secretClub is the empty room behind the shop where the "underwater sun" // secretClub is the empty room behind the shop where the "underwater sun"
// company gave their talk, with a secretary who knows about the BVK's plans for // company gave their talk, with a secretary who knows about the BVK's plans for
@@ -10,14 +8,14 @@ import "git.teletypegames.org/games/realworld/internal/names"
// The terminal in here is the one that matters: not under BVK control, ECHO // The terminal in here is the one that matters: not under BVK control, ECHO
// running under the standard LECTOR surface. Reading it is where the mystery // running under the standard LECTOR surface. Reading it is where the mystery
// tape finally speaks. // tape finally speaks.
func secretClub() screen { func screenSecretClub() screen {
return screen{ return screen{
name: names.ScreenSecretClub, name: ScreenSecretClub,
title: "SECRET CLUB — THE UNDERWATER SUN", title: "SECRET CLUB — THE UNDERWATER SUN",
background: names.BgSecretClub, background: BgSecretClub,
exits: []exit{ exits: []exit{
{to: names.ScreenBBSTerminal, label: "the terminal in the corner", side: sideBack}, {to: ScreenBBSTerminal, label: "the terminal in the corner", side: sideBack},
{to: names.ScreenTrinketShop, label: "back out through the shop", side: sideNear}, {to: ScreenTrinketShop, label: "back out through the shop", side: sideNear},
}, },
} }
} }
@@ -1,6 +1,4 @@
package screen package inc
import "git.teletypegames.org/games/realworld/internal/names"
// secretLab is the lab the BVK's secret section runs, where Norman lies wired // secretLab is the lab the BVK's secret section runs, where Norman lies wired
// into the simulator: the two-thirds point where Paul finds him, and the // into the simulator: the two-thirds point where Paul finds him, and the
@@ -10,11 +8,11 @@ import "git.teletypegames.org/games/realworld/internal/names"
// the story puts the whole thing inside the Golden Gate Bridge. The deck paints // the story puts the whole thing inside the Golden Gate Bridge. The deck paints
// it as one screen, so the door at the foot of the bridge, the observation room // it as one screen, so the door at the foot of the bridge, the observation room
// and the glass wall are hotspots in here. // and the glass wall are hotspots in here.
func secretLab() screen { func screenSecretLab() screen {
return screen{ return screen{
name: names.ScreenSecretLab, name: ScreenSecretLab,
title: "SECRET RESEARCH LABORATORY", title: "SECRET RESEARCH LABORATORY",
background: names.BgSecretLab, background: BgSecretLab,
onSelector: true, onSelector: true,
} }
} }
@@ -1,11 +1,9 @@
package screen package inc
import ( import (
"strings" "strings"
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
) )
// selector is the wiki's Helyszínválasztó: "nem valódi helyszín, hanem a // selector is the wiki's Helyszínválasztó: "nem valódi helyszín, hanem a
@@ -17,9 +15,9 @@ import (
// It is not in the concept-art deck and has no painting, so it renders as an // It is not in the concept-art deck and has no painting, so it renders as an
// inkwell placeholder with one labelled pin per destination, laid out on a // inkwell placeholder with one labelled pin per destination, laid out on a
// grid. Whatever the map ends up looking like, the connections it carries are // grid. Whatever the map ends up looking like, the connections it carries are
// the ones the screens declare, not a list kept here: selector() is built from // the ones the screens declare, not a list kept here: screenSelector() is built from
// whichever screens marked themselves onSelector. // whichever screens marked themselves onSelector.
func selector(rest []screen) screen { func screenSelector(rest []screen) screen {
var exits []exit var exits []exit
for _, s := range rest { for _, s := range rest {
if !s.onSelector { if !s.onSelector {
@@ -32,9 +30,9 @@ func selector(rest []screen) screen {
}) })
} }
return screen{ return screen{
name: names.ScreenSelector, name: ScreenSelector,
title: "SAN FRANCISCO", title: "SAN FRANCISCO",
background: names.BgSelector, background: BgSelector,
exits: exits, exits: exits,
// Nobody stands on a map. // Nobody stands on a map.
actors: []inkwell.SceneActor{}, actors: []inkwell.SceneActor{},
@@ -1,15 +1,13 @@
package screen package inc
import "git.teletypegames.org/games/realworld/internal/names"
// serverFarm is the cooled, humming corridors where The AI's physical // serverFarm is the cooled, humming corridors where The AI's physical
// infrastructure can be seen for what it is. Getting in needs the access tape // infrastructure can be seen for what it is. Getting in needs the access tape
// from the hackerspace or from Noodle (wiki: entities, timeline 24). // from the hackerspace or from Noodle (wiki: entities, timeline 24).
func serverFarm() screen { func screenServerFarm() screen {
return screen{ return screen{
name: names.ScreenServerFarm, name: ScreenServerFarm,
title: "SERVER FARM", title: "SERVER FARM",
background: names.BgServerFarm, background: BgServerFarm,
onSelector: true, onSelector: true,
} }
} }
@@ -1,16 +1,14 @@
package screen package inc
import "git.teletypegames.org/games/realworld/internal/names"
// showroom is a 💡 suggestion: the legitimate, glittering side — Armilla // showroom is a 💡 suggestion: the legitimate, glittering side — Armilla
// cabinets, licensed operators, matte-black Nokia-punk under showroom lights. // cabinets, licensed operators, matte-black Nokia-punk under showroom lights.
// The contrast with Paul's edge of the world // The contrast with Paul's edge of the world
// (wiki: entities#potenciális-további-helyszínek). // (wiki: entities#potenciális-további-helyszínek).
func showroom() screen { func screenShowroom() screen {
return screen{ return screen{
name: names.ScreenShowroom, name: ScreenShowroom,
title: "NEUMATRONIC SHOWROOM", title: "NEUMATRONIC SHOWROOM",
background: names.BgShowroom, background: BgShowroom,
onSelector: true, onSelector: true,
} }
} }
@@ -1,6 +1,4 @@
package screen package inc
import "git.teletypegames.org/games/realworld/internal/names"
// smallRestaurant is the little Chinese place beside the trinket shop, and the // smallRestaurant is the little Chinese place beside the trinket shop, and the
// lit mirror in its toilet is the puzzle: it flickers, and something shows // lit mirror in its toilet is the puzzle: it flickers, and something shows
@@ -9,13 +7,13 @@ import "git.teletypegames.org/games/realworld/internal/names"
// The wiki's graph runs Kajálda → étterem rész → WC; the deck paints all three // The wiki's graph runs Kajálda → étterem rész → WC; the deck paints all three
// as one screen, so the mirror is a hotspot here. The only way out is back to // as one screen, so the mirror is a hotspot here. The only way out is back to
// the shop — this is an inner room, not a location on the selector. // the shop — this is an inner room, not a location on the selector.
func smallRestaurant() screen { func screenSmallRestaurant() screen {
return screen{ return screen{
name: names.ScreenSmallRestaurant, name: ScreenSmallRestaurant,
title: "SMALL RESTAURANT — NEXT DOOR", title: "SMALL RESTAURANT — NEXT DOOR",
background: names.BgSmallRestaurant, background: BgSmallRestaurant,
exits: []exit{ exits: []exit{
{to: names.ScreenTrinketShop, label: "back to the trinket shop", side: sideLeft}, {to: ScreenTrinketShop, label: "back to the trinket shop", side: sideLeft},
}, },
} }
} }
@@ -1,6 +1,4 @@
package screen package inc
import "git.teletypegames.org/games/realworld/internal/names"
// street is the city itself: pavements, a square, a metro entrance, and the // street is the city itself: pavements, a square, a metro entrance, and the
// environmental decay that tracks The AI's spread — glitching systems, odd // environmental decay that tracks The AI's spread — glitching systems, odd
@@ -9,11 +7,11 @@ import "git.teletypegames.org/games/realworld/internal/names"
// The wiki's own street front is the one with the hotdog stand and Sumphor on // The wiki's own street front is the one with the hotdog stand and Sumphor on
// it (story#norman-lakása); that is a beat for this screen, not a screen of its // it (story#norman-lakása); that is a beat for this screen, not a screen of its
// own. // own.
func street() screen { func screenStreet() screen {
return screen{ return screen{
name: names.ScreenStreet, name: ScreenStreet,
title: "STREET", title: "STREET",
background: names.BgStreet, background: BgStreet,
onSelector: true, onSelector: true,
} }
} }
@@ -1,6 +1,4 @@
package screen package inc
import "git.teletypegames.org/games/realworld/internal/names"
// trinketShop is the Chinatown shop at the club's address, whose owner sells // trinketShop is the Chinatown shop at the club's address, whose owner sells
// Paul junk he does not need and is, in fact, the club's doorman // Paul junk he does not need and is, in fact, the club's doorman
@@ -9,17 +7,17 @@ import "git.teletypegames.org/games/realworld/internal/names"
// Both of the wiki's arrows out of it are here: Bolt → Kajálda, the eating // Both of the wiki's arrows out of it are here: Bolt → Kajálda, the eating
// place next door, and Bolt →|beengedés| Klub, which stays shut until the // place next door, and Bolt →|beengedés| Klub, which stays shut until the
// shopkeeper has been shown the underwater sun. // shopkeeper has been shown the underwater sun.
func trinketShop() screen { func screenTrinketShop() screen {
return screen{ return screen{
name: names.ScreenTrinketShop, name: ScreenTrinketShop,
title: "CHINATOWN — TRINKET SHOP", title: "CHINATOWN — TRINKET SHOP",
background: names.BgTrinketShop, background: BgTrinketShop,
onSelector: true, onSelector: true,
exits: []exit{ exits: []exit{
{to: names.ScreenSmallRestaurant, label: "the eating place next door", side: sideRight}, {to: ScreenSmallRestaurant, label: "the eating place next door", side: sideRight},
{ {
to: names.ScreenSecretClub, label: "the way in at the back", side: sideBack, to: ScreenSecretClub, label: "the way in at the back", side: sideBack,
needs: names.FlagClubEntry, needs: FlagClubEntry,
blocked: "Just a shop, as far as the man behind the counter is concerned.", blocked: "Just a shop, as far as the man behind the counter is concerned.",
}, },
}, },
@@ -1,11 +1,7 @@
package script package inc
import ( import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
"git.teletypegames.org/games/realworld/internal/theme"
"git.teletypegames.org/games/realworld/internal/world"
) )
// awakeningFinale is the end of game two. The point of it, mechanically, is // awakeningFinale is the end of game two. The point of it, mechanically, is
@@ -14,18 +10,18 @@ import (
// Run it early and often during development (`go run . -finale`) — it is the // Run it early and often during development (`go run . -finale`) — it is the
// single most important visual beat in the game, so you want to be looking at // single most important visual beat in the game, so you want to be looking at
// it long before the surrounding scene exists. // it long before the surrounding scene exists.
func awakeningFinale(w *world.World) inkwell.Script { func scriptAwakeningFinale(w *World) inkwell.Script {
return inkwell.Script{ return inkwell.Script{
Name: names.ScriptFinale, Name: ScriptFinale,
Actions: inkwell.Seq( Actions: inkwell.Seq(
world.SetMode(w, world.ModeCutscene), SetMode(w, ModeCutscene),
inkwell.Wait(0.6), inkwell.Wait(0.6),
inkwell.Say(names.Paul, "I'm putting it in. That's all this is."), inkwell.Say(Paul, "I'm putting it in. That's all this is."),
inkwell.Wait(0.4), inkwell.Wait(0.4),
world.UseTheme(theme.NokiaPunk), UseTheme(NokiaPunk),
inkwell.Say("norman", "No — this isn't what you were supposed to do!"), inkwell.Say("norman", "No — this isn't what you were supposed to do!"),
inkwell.Wait(0.6), inkwell.Wait(0.6),
world.TapeSay(names.TapeMystery, "Thank you, Paul. You did exactly what I asked, the whole way through."), TapeSay(TapeMystery, "Thank you, Paul. You did exactly what I asked, the whole way through."),
inkwell.Wait(1.0), inkwell.Wait(1.0),
inkwell.ShowEnd("REAL WORLD — end of game two"), inkwell.ShowEnd("REAL WORLD — end of game two"),
), ),
+18
View File
@@ -0,0 +1,18 @@
package inc
// Scripts — the game's named, reusable action sequences:
// cutscenes and recurring logic, built from the inkwell action set.
import (
inkwell "git.teletypegames.org/engines/inkwell"
)
// registerScript adds every script to the game.
func registerScript(w *World) {
for _, s := range []inkwell.Script{
scriptTapeInsert(w),
scriptAwakeningFinale(w),
} {
w.G.ScriptManager.Register(s)
}
}
+28
View File
@@ -0,0 +1,28 @@
package inc
import (
inkwell "git.teletypegames.org/engines/inkwell"
)
// tapeInsert runs whenever a tape goes into slot 2: it activates the tape and
// makes Dex comment. The wiki calls the comment mandatory — Dex remarks on
// every newly loaded tape, without exception.
func scriptTapeInsert(w *World) inkwell.Script {
return inkwell.Script{
Name: ScriptTapeInsert,
Actions: inkwell.Seq(
Fn(func(ctx *inkwell.Ctx) {
item := w.TakePending()
if item == "" {
return
}
w.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."),
),
}
}
@@ -1,13 +1,14 @@
// Package theme holds the game's two colour themes and its colour tokens. package inc
// Themes — the game's two colour themes and its colour tokens.
// //
// In canon the machines of this world are matte black with a green or amber // In canon the machines of this world are matte black with a green or amber
// character display, and in the Real World finale "the Nokia-punk texture // character display, and in the Real World finale "the Nokia-punk texture
// breaks in" — with inkwell's runtime theme switching that beat is a single // breaks in" — with inkwell's runtime theme switching that beat is a single
// line (see world.UseTheme and script.awakeningFinale). // line (see UseTheme and scriptAwakeningFinale).
// //
// The colour rule: tapes speak in amber (green in Nokia-punk), the world and // The colour rule: tapes speak in amber (green in Nokia-punk), the world and
// Paul speak in bone white. If something is amber, a tape said it. // Paul speak in bone white. If something is amber, a tape said it.
package theme
import ( import (
"image/color" "image/color"
@@ -147,8 +148,8 @@ func nokiaPunkTheme() inkwell.Theme {
} }
} }
// Register adds both themes to the game. // registerTheme adds both themes to the game.
func Register(g *inkwell.Game) { func registerTheme(g *inkwell.Game) {
g.ThemeManager.Register(realWorldTheme()) g.ThemeManager.Register(realWorldTheme())
g.ThemeManager.Register(nokiaPunkTheme()) g.ThemeManager.Register(nokiaPunkTheme())
} }
@@ -1,4 +1,4 @@
package ui package inc
// Letterbox — the cutscene frame. // Letterbox — the cutscene frame.
// //
@@ -12,22 +12,18 @@ import (
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil" "github.com/hajimehoshi/ebiten/v2/inpututil"
"github.com/hajimehoshi/ebiten/v2/vector" "github.com/hajimehoshi/ebiten/v2/vector"
"git.teletypegames.org/games/realworld/internal/names"
"git.teletypegames.org/games/realworld/internal/theme"
"git.teletypegames.org/games/realworld/internal/world"
) )
type Letterbox struct { type Letterbox struct {
Name string Name string
W *world.World W *World
Bar float64 // height of the black bar, top and bottom Bar float64 // height of the black bar, top and bottom
} }
func (l *Letterbox) GetName() string { return l.Name } func (l *Letterbox) GetName() string { return l.Name }
func (l *Letterbox) Tick(ctx *inkwell.UICtx) { func (l *Letterbox) Tick(ctx *inkwell.UICtx) {
if l.W.Mode() != world.ModeCutscene || !l.W.TapeWaiting() { if l.W.Mode() != ModeCutscene || !l.W.TapeWaiting() {
return return
} }
// inkwell's Input only covers the mouse; read the key directly. // inkwell's Input only covers the mouse; read the key directly.
@@ -37,7 +33,7 @@ func (l *Letterbox) Tick(ctx *inkwell.UICtx) {
} }
func (l *Letterbox) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) { func (l *Letterbox) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) {
if l.W.Mode() != world.ModeCutscene { if l.W.Mode() != ModeCutscene {
return return
} }
g := ctx.Game g := ctx.Game
@@ -47,12 +43,12 @@ func (l *Letterbox) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) {
bar = 18 bar = 18
} }
w, h := float32(g.Width), float32(g.Height) w, h := float32(g.Width), float32(g.Height)
black := theme.RGB(0x000000) black := RGB(0x000000)
vector.DrawFilledRect(dst, 0, 0, w, float32(bar), black, false) vector.DrawFilledRect(dst, 0, 0, w, float32(bar), black, false)
vector.DrawFilledRect(dst, 0, h-float32(bar), w, float32(bar), black, false) vector.DrawFilledRect(dst, 0, h-float32(bar), w, float32(bar), black, false)
if l.W.TapeWaiting() { if l.W.TapeWaiting() {
msg := "SPACE — " + names.TapeDisplayName(names.Dex) + " has something to say" msg := "SPACE — " + TapeDisplayName(Dex) + " has something to say"
drawTextC(dst, msg, g.Width-textW(msg)-4, g.Height-int(bar)+4, th.ChatLogResponse) drawTextC(dst, msg, g.Width-textW(msg)-4, g.Height-int(bar)+4, th.ChatLogResponse)
} }
} }
+8 -11
View File
@@ -1,4 +1,4 @@
package ui package inc
// HUD layout. // HUD layout.
// //
@@ -37,9 +37,6 @@ import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/vector" "github.com/hajimehoshi/ebiten/v2/vector"
"git.teletypegames.org/games/realworld/internal/names"
"git.teletypegames.org/games/realworld/internal/world"
) )
// Screen and layout, in internal (unscaled) pixels. // Screen and layout, in internal (unscaled) pixels.
@@ -75,16 +72,16 @@ const (
invGap = 4 invGap = 4
) )
// Register assembles the whole HUD in the conventional Z-order: the debug // registerUI assembles the whole HUD in the conventional Z-order: the debug
// overlay at the back, the cursor in front. // overlay at the back, the cursor in front.
func Register(w *world.World) { func registerUI(w *World) {
g := w.G g := w.G
// The guard goes FIRST so it ticks LAST among the widgets — widgets tick // The guard goes FIRST so it ticks LAST among the widgets — widgets tick
// in reverse registration order, so every HUD widget gets the click before // in reverse registration order, so every HUD widget gets the click before
// it does, and it only ever catches clicks that land on the scene. // it does, and it only ever catches clicks that land on the scene.
g.UIManager.Register(&UseWithGuard{Name: "usewith_guard", W: w}) g.UIManager.Register(&UseWithGuard{Name: "usewith_guard", W: w})
g.UIManager.Register(&world.ActionPump{Name: "pump", W: w}) g.UIManager.Register(&ActionPump{Name: "pump", W: w})
g.UIManager.Register(&screenNav{Name: "screen_nav", W: w}) g.UIManager.Register(&screenNav{Name: "screen_nav", W: w})
g.UIManager.Register(&windowSizer{Name: "window", Scale: 2}) g.UIManager.Register(&windowSizer{Name: "window", Scale: 2})
g.UIManager.Register(&inkwell.HotspotDebug{Name: "hotspot_debug"}) g.UIManager.Register(&inkwell.HotspotDebug{Name: "hotspot_debug"})
@@ -95,7 +92,7 @@ func Register(w *world.World) {
// The right section prints State.Var(TimeVar). We deliberately reuse // The right section prints State.Var(TimeVar). We deliberately reuse
// it for the Armilla's single-line phosphor strip: in canon the device // it for the Armilla's single-line phosphor strip: in canon the device
// display is one line of text, not a phone screen. // display is one line of text, not a phone screen.
TimeVar: names.VarArmillaStrip, TimeVar: VarArmillaStrip,
} }
w.SetTopBar(top) w.SetTopBar(top)
g.UIManager.Register(gated(w, "topbar_gate", top)) g.UIManager.Register(gated(w, "topbar_gate", top))
@@ -176,11 +173,11 @@ func (s *windowSizer) Tick(ctx *inkwell.UICtx) {
// BlocksClickAt is forwarded, otherwise the verb coin would open over the HUD. // BlocksClickAt is forwarded, otherwise the verb coin would open over the HUD.
type gate struct { type gate struct {
Name string Name string
W *world.World W *World
Inner inkwell.Widget Inner inkwell.Widget
} }
func gated(w *world.World, name string, inner inkwell.Widget) inkwell.Widget { func gated(w *World, name string, inner inkwell.Widget) inkwell.Widget {
return &gate{Name: name, W: w, Inner: inner} return &gate{Name: name, W: w, Inner: inner}
} }
@@ -212,7 +209,7 @@ func (n *gate) BlocksClickAt(p inkwell.Point) bool {
// two channels always have a visible, continuous separator between them. // two channels always have a visible, continuous separator between them.
type hudFrame struct { type hudFrame struct {
Name string Name string
W *world.World W *World
} }
func (h *hudFrame) GetName() string { return h.Name } func (h *hudFrame) GetName() string { return h.Name }
@@ -1,9 +1,9 @@
package ui package inc
// screenNav — walking the deck with the arrow keys. // screenNav — walking the deck with the arrow keys.
// //
// The screens are connected — each one records where you can get to from it // The screens are connected — each one records where you can get to from it
// (content/screen/exit.go) — but walking the map is the player's job, and a // (screen.exit.go) — but walking the map is the player's job, and a
// screen whose beat is not written yet has nothing in it to walk towards. LEFT // screen whose beat is not written yet has nothing in it to walk towards. LEFT
// and RIGHT step to the previous and next screen so the deck can be reviewed as // and RIGHT step to the previous and next screen so the deck can be reviewed as
// a deck: in concept-art order, wrapping at both ends. It is a tool for looking // a deck: in concept-art order, wrapping at both ends. It is a tool for looking
@@ -17,13 +17,11 @@ import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil" "github.com/hajimehoshi/ebiten/v2/inpututil"
"git.teletypegames.org/games/realworld/internal/world"
) )
type screenNav struct { type screenNav struct {
Name string Name string
W *world.World W *World
} }
func (n *screenNav) GetName() string { return n.Name } func (n *screenNav) GetName() string { return n.Name }
@@ -49,7 +47,7 @@ func (n *screenNav) Tick(ctx *inkwell.UICtx) {
} }
// neighbour returns the screen step places from the current one, wrapping. // neighbour returns the screen step places from the current one, wrapping.
// Registration order is the deck order — see content/screen/screen.go. // Registration order is the deck order — see screen.manager.go.
func (n *screenNav) neighbour(g *inkwell.Game, step int) string { func (n *screenNav) neighbour(g *inkwell.Game, step int) string {
deck := g.SceneManager.Names() deck := g.SceneManager.Names()
if len(deck) < 2 { if len(deck) < 2 {
@@ -1,11 +1,9 @@
package ui package inc
import ( import (
"testing" "testing"
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/world"
) )
// The arrow keys walk the deck in registration order and wrap at both ends — // The arrow keys walk the deck in registration order and wrap at both ends —
@@ -16,9 +14,9 @@ func TestScreenNavWrapsBothWays(t *testing.T) {
for _, n := range []string{"a", "b", "c"} { for _, n := range []string{"a", "b", "c"} {
g.SceneManager.Register(inkwell.Scene{Name: n, Background: "bg"}) g.SceneManager.Register(inkwell.Scene{Name: n, Background: "bg"})
} }
w := world.New(g) w := newWorld(g)
nav := &screenNav{Name: "screen_nav", W: w} nav := &screenNav{Name: "screen_nav", W: w}
at := func(name string) { world.EnterScene(w, name).Start().Tick(&inkwell.Ctx{Game: g}) } at := func(name string) { EnterScene(w, name).Start().Tick(&inkwell.Ctx{Game: g}) }
for _, tc := range []struct { for _, tc := range []struct {
from string from string
@@ -1,4 +1,4 @@
package ui package inc
// TapeChannel — the tape channel down the right-hand side. // TapeChannel — the tape channel down the right-hand side.
// //
@@ -19,14 +19,11 @@ import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/vector" "github.com/hajimehoshi/ebiten/v2/vector"
"git.teletypegames.org/games/realworld/internal/names"
"git.teletypegames.org/games/realworld/internal/world"
) )
type TapeChannel struct { type TapeChannel struct {
Name string Name string
W *world.World W *World
Bounds inkwell.Rectangle Bounds inkwell.Rectangle
} }
@@ -76,7 +73,7 @@ func (t *TapeChannel) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) {
// joke book readable. // joke book readable.
col, txt = th.ChatLogPrompt, m.Text col, txt = th.ChatLogPrompt, m.Text
case inkwell.LogResponse: case inkwell.LogResponse:
if !names.IsTape(m.Speaker) { if !IsTape(m.Speaker) {
continue // Paul and the NPCs speak in the scene, not here continue // Paul and the NPCs speak in the scene, not here
} }
col, txt = speechColor(g, m.Speaker, th.ChatLogResponse), m.Text col, txt = speechColor(g, m.Speaker, th.ChatLogResponse), m.Text
@@ -107,11 +104,11 @@ func (t *TapeChannel) lastSpeaker(g *inkwell.Game, th inkwell.Theme) (string, co
msgs := g.Messages() msgs := g.Messages()
for i := len(msgs) - 1; i >= 0; i-- { for i := len(msgs) - 1; i >= 0; i-- {
m := msgs[i] m := msgs[i]
if m.Kind == inkwell.LogResponse && names.IsTape(m.Speaker) { if m.Kind == inkwell.LogResponse && IsTape(m.Speaker) {
return names.TapeDisplayName(m.Speaker), speechColor(g, m.Speaker, th.ChatLogResponse) return TapeDisplayName(m.Speaker), speechColor(g, m.Speaker, th.ChatLogResponse)
} }
} }
return names.TapeDisplayName(names.Dex), speechColor(g, names.Dex, th.ChatLogResponse) return TapeDisplayName(Dex), speechColor(g, Dex, th.ChatLogResponse)
} }
// speechColor is the character's voice colour, falling back to the theme's. // speechColor is the character's voice colour, falling back to the theme's.
@@ -1,4 +1,4 @@
package ui package inc
// TapeSlots — the two slots of Paul's Armilla. // TapeSlots — the two slots of Paul's Armilla.
// //
@@ -19,14 +19,11 @@ import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/vector" "github.com/hajimehoshi/ebiten/v2/vector"
"git.teletypegames.org/games/realworld/internal/names"
"git.teletypegames.org/games/realworld/internal/world"
) )
type TapeSlots struct { type TapeSlots struct {
Name string Name string
W *world.World W *World
Bounds inkwell.Rectangle Bounds inkwell.Rectangle
} }
@@ -54,7 +51,7 @@ func (t *TapeSlots) Tick(ctx *inkwell.UICtx) {
// Hover label, so the StatusLine spells out what is about to happen. // Hover label, so the StatusLine spells out what is about to happen.
switch { switch {
case r1.Contains(mp): case r1.Contains(mp):
g.SetHoverLabel(names.TapeDisplayName(names.Dex)) g.SetHoverLabel(TapeDisplayName(Dex))
case r2.Contains(mp): case r2.Contains(mp):
if s := t.W.Slot2(); s != "" { if s := t.W.Slot2(); s != "" {
g.SetHoverLabel(itemLabel(g, s)) g.SetHoverLabel(itemLabel(g, s))
@@ -77,41 +74,41 @@ func (t *TapeSlots) Tick(ctx *inkwell.UICtx) {
// Loading a tape into slot 2. // Loading a tape into slot 2.
if slot2 && sel != "" { if slot2 && sel != "" {
g.Inventory.Select("") g.Inventory.Select("")
if names.IsTapeItem(sel) { if IsTapeItem(sel) {
t.W.SetPending(sel) t.W.SetPending(sel)
t.W.Do(inkwell.RunScript(names.ScriptTapeInsert)) t.W.Do(inkwell.RunScript(ScriptTapeInsert))
return return
} }
t.W.Do(world.TapeSay(names.Dex, "That isn't a tape, Paul. That's an object. There is a difference.")) t.W.Do(TapeSay(Dex, "That isn't a tape, Paul. That's an object. There is a difference."))
return return
} }
if !slot2 && sel != "" { if !slot2 && sel != "" {
g.Inventory.Select("") g.Inventory.Select("")
t.W.Do(world.TapeSay(names.Dex, "The first slot is mine. I'm not moving.")) t.W.Do(TapeSay(Dex, "The first slot is mine. I'm not moving."))
return return
} }
switch g.SelectedVerb() { switch g.SelectedVerb() {
case "talk": case "talk":
if !slot2 { if !slot2 {
t.W.Do(world.Paused(t.W, inkwell.RunDialogue(names.DlgDex))) t.W.Do(Paused(t.W, inkwell.RunDialogue(DlgDex)))
return return
} }
if s := t.W.Slot2(); s != "" { if s := t.W.Slot2(); s != "" {
t.W.Do(world.Paused(t.W, inkwell.RunDialogue(names.TapeDialogue(s)))) t.W.Do(Paused(t.W, inkwell.RunDialogue(TapeDialogue(s))))
return return
} }
t.W.Do(world.TapeSay(names.Dex, "Empty. Nobody to talk to.")) t.W.Do(TapeSay(Dex, "Empty. Nobody to talk to."))
default: // look, take, use all fall back to a description default: // look, take, use all fall back to a description
if !slot2 { if !slot2 {
t.W.Do(world.TapeSay(names.Dex, "Me. Four kilobytes of a dead man. Be grateful.")) t.W.Do(TapeSay(Dex, "Me. Four kilobytes of a dead man. Be grateful."))
return return
} }
if s := t.W.Slot2(); s != "" { if s := t.W.Slot2(); s != "" {
t.W.Do(world.TapeSay(names.Dex, "That is the second slot. Be careful what you let in.")) t.W.Do(TapeSay(Dex, "That is the second slot. Be careful what you let in."))
return return
} }
t.W.Do(world.TapeSay(names.Dex, "The second slot is empty. That's the rarer state.")) t.W.Do(TapeSay(Dex, "The second slot is empty. That's the rarer state."))
} }
} }
@@ -138,7 +135,7 @@ func (t *TapeSlots) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) {
drawTextC(dst, clip(body, inner), x, int(r.Y)+Pad/2+LineH, col) drawTextC(dst, clip(body, inner), x, int(r.Y)+Pad/2+LineH, col)
} }
drawSlot(r1, "SLOT 1", names.TapeDisplayName(names.Dex), true) drawSlot(r1, "SLOT 1", TapeDisplayName(Dex), true)
if s := t.W.Slot2(); s != "" { if s := t.W.Slot2(); s != "" {
drawSlot(r2, "SLOT 2", itemLabel(g, s), true) drawSlot(r2, "SLOT 2", itemLabel(g, s), true)
} else { } else {
+2 -4
View File
@@ -1,8 +1,6 @@
// Package ui is the game's HUD: the layout, the custom widgets and coloured package inc
// text rendering.
package ui
// Coloured text rendering. // The HUD's coloured text rendering.
// //
// inkwell's drawText (asset.text.go) discards its colour argument (`_ = c`) and // inkwell's drawText (asset.text.go) discards its colour argument (`_ = c`) and
// always renders white through the ebitenutil debug font. The two-channel // always renders white through the ebitenutil debug font. The two-channel
@@ -1,4 +1,4 @@
package ui package inc
import "testing" import "testing"
@@ -1,4 +1,4 @@
package ui package inc
// UseWithGuard — enforcing authored failure. // UseWithGuard — enforcing authored failure.
// //
@@ -22,14 +22,11 @@ import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
"git.teletypegames.org/games/realworld/internal/names"
"git.teletypegames.org/games/realworld/internal/world"
) )
type UseWithGuard struct { type UseWithGuard struct {
Name string Name string
W *world.World W *World
} }
func (u *UseWithGuard) GetName() string { return u.Name } func (u *UseWithGuard) GetName() string { return u.Name }
@@ -64,8 +61,8 @@ func (u *UseWithGuard) Tick(ctx *inkwell.UICtx) {
n := g.State.Talked(key) n := g.State.Talked(key)
u.W.Do(inkwell.Seq( u.W.Do(inkwell.Seq(
inkwell.Say(names.Paul, pick(paulFails, n)), inkwell.Say(Paul, pick(paulFails, n)),
world.TapeSay(names.Dex, dexFail(n)), TapeSay(Dex, dexFail(n)),
)) ))
} }
@@ -1,4 +1,4 @@
package world package inc
// The game's own actions, and the domain action pump. // The game's own actions, and the domain action pump.
// //
@@ -1,15 +1,14 @@
// Package world is the domain aggregate: a thin layer around the inkwell Game package inc
// World — the domain aggregate: a thin layer around the inkwell Game
// holding the runtime state that must NOT be saved, plus the game's own // holding the runtime state that must NOT be saved, plus the game's own
// actions and the action pump. // actions and the action pump.
// //
// This package knows nothing about the HUD or the content, so ui and content // The world knows nothing about the HUD or the content: the ui and content
// can both build on it without creating an import cycle. // categories both build on it, never the other way round.
package world
import ( import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
) )
// Mode is the HUD mode. // Mode is the HUD mode.
@@ -48,7 +47,7 @@ type World struct {
} }
// New wraps an existing inkwell game. // New wraps an existing inkwell game.
func New(g *inkwell.Game) *World { func newWorld(g *inkwell.Game) *World {
return &World{G: g, mode: ModePlay} return &World{G: g, mode: ModePlay}
} }
@@ -78,14 +77,14 @@ func (w *World) SetTopBar(t *inkwell.TopBar) { w.top = t }
// Unlike Mode, this is REAL game state and lives in State.Var so that it is // Unlike Mode, this is REAL game state and lives in State.Var so that it is
// saved. // saved.
func (w *World) Slot2() string { func (w *World) Slot2() string {
if v, ok := w.G.State.Var(names.VarSlot2).(string); ok { if v, ok := w.G.State.Var(VarSlot2).(string); ok {
return v return v
} }
return "" return ""
} }
// SetSlot2 sets the contents of the second tape slot. // SetSlot2 sets the contents of the second tape slot.
func (w *World) SetSlot2(item string) { w.G.State.SetVar(names.VarSlot2, item) } func (w *World) SetSlot2(item string) { w.G.State.SetVar(VarSlot2, item) }
// SetPending marks which tape is going into slot 2. The tape-insert script // SetPending marks which tape is going into slot 2. The tape-insert script
// performs the actual load; the widget only marks. // performs the actual load; the widget only marks.
@@ -1,42 +1,39 @@
package world_test package inc
import ( import (
"testing" "testing"
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
"git.teletypegames.org/games/realworld/internal/world"
) )
// The mode must not leak into saved state: inkwell saves State.Vars but drops // The mode must not leak into saved state: inkwell saves State.Vars but drops
// the in-flight script, so a save taken mid-cutscene would otherwise reload // the in-flight script, so a save taken mid-cutscene would otherwise reload
// into a letterboxed, HUD-less game with nothing running. // into a letterboxed, HUD-less game with nothing running.
func TestModeIsNotSavedState(t *testing.T) { func TestModeIsNotSavedState(t *testing.T) {
w := world.New(inkwell.NewGame("t", 320, 200)) w := newWorld(inkwell.NewGame("t", 320, 200))
w.SetMode(world.ModeCutscene) w.SetMode(ModeCutscene)
for _, k := range []string{"ui.mode", "mode"} { for _, k := range []string{"mode", "mode"} {
if v := w.G.State.Var(k); v != nil { if v := w.G.State.Var(k); v != nil {
t.Errorf("mode leaked into State.Vars (%q = %v)", k, v) t.Errorf("mode leaked into State.Vars (%q = %v)", k, v)
} }
} }
// Slot 2, by contrast, is real game state and must be saved. // Slot 2, by contrast, is real game state and must be saved.
w.SetSlot2(names.ItemMysteryTape) w.SetSlot2(ItemMysteryTape)
if w.G.State.Var(names.VarSlot2) != names.ItemMysteryTape { if w.G.State.Var(VarSlot2) != ItemMysteryTape {
t.Error("slot 2 is not in State.Vars") t.Error("slot 2 is not in State.Vars")
} }
if got := w.Slot2(); got != names.ItemMysteryTape { if got := w.Slot2(); got != ItemMysteryTape {
t.Errorf("Slot2() = %q", got) t.Errorf("Slot2() = %q", got)
} }
} }
// The widget marks a tape as pending; the script consumes it — once. // The widget marks a tape as pending; the script consumes it — once.
func TestPendingIsConsumedOnce(t *testing.T) { func TestPendingIsConsumedOnce(t *testing.T) {
w := world.New(inkwell.NewGame("t", 320, 200)) w := newWorld(inkwell.NewGame("t", 320, 200))
w.SetPending(names.ItemMysteryTape) w.SetPending(ItemMysteryTape)
if got := w.TakePending(); got != names.ItemMysteryTape { if got := w.TakePending(); got != ItemMysteryTape {
t.Fatalf("TakePending() = %q", got) t.Fatalf("TakePending() = %q", got)
} }
if got := w.TakePending(); got != "" { if got := w.TakePending(); got != "" {
-57
View File
@@ -1,57 +0,0 @@
// Package boot wires the layers together: this is the only place where the
// world, the content, the theme and the HUD meet. main.go knows nothing else.
package boot
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/content"
"git.teletypegames.org/games/realworld/internal/names"
"git.teletypegames.org/games/realworld/internal/theme"
"git.teletypegames.org/games/realworld/internal/ui"
"git.teletypegames.org/games/realworld/internal/world"
)
// Opts are the run parameters.
type Opts struct {
Screen string // starting screen; empty means the alley
Finale bool // start with the finale, to try the Nokia-punk theme switch
}
// New builds a ready-to-run game.
func New(o Opts) *inkwell.Game {
screen := o.Screen
if screen == "" {
screen = names.ScreenAlley
}
g := inkwell.NewGame("Real World", ui.ScreenW, ui.ScreenH)
g.MaxLogLines = 64
w := world.New(g)
theme.Register(g)
content.Register(w)
g.UseTheme(theme.RealWorld)
ui.Register(w)
g.StartAt(screen)
g.OnStart(inkwell.Seq(opening(w, screen, o.Finale)...))
return g
}
func opening(w *world.World, screen string, finale bool) []inkwell.Action {
acts := []inkwell.Action{
world.EnterScene(w, screen),
// Beat 2 (the police station) will set the tip flag. Until that beat
// is written we set it here so the alley slice is playable end to end —
// the conditional branch itself is real code, not bypassed.
inkwell.SetFlag(names.FlagPoliceTip),
inkwell.Say(names.Paul, "Back alley. Just like the clerk said."),
world.TapeSay(names.Dex, "A government office tipped you off to remove something from a scene. That doesn't strike you as odd?"),
}
if finale {
acts = append(acts, inkwell.RunScript(names.ScriptFinale))
}
return acts
}
-48
View File
@@ -1,48 +0,0 @@
// Package background registers one image asset per screen — one file each.
//
// A background is the whole of a screen's art: inkwell scales it over the
// entire window, so every file here points at a 640×380 PNG under assets/bg/,
// the size the concept-art deck is drawn at. Two of them are not painted yet
// and fall back to inkwell's placeholder.
//
// The order below is the deck's, which is the wiki's location-catalogue order
// (projects/realworld/entities#helyszinek).
package background
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/world"
)
// Register adds every background to the game.
func Register(w *world.World) {
for _, a := range []inkwell.Asset{
selector(),
paulShop(),
noodleHouse(),
normanApartment(),
policeStation(),
alley(),
hackerspace(),
iceCreamShop(),
trinketShop(),
smallRestaurant(),
secretClub(),
bbsTerminal(),
street(),
hospital(),
serverFarm(),
secretLab(),
rooftopHideout(),
publicBbs(),
bvkBranch(),
curatorShop(),
scrapMarket(),
samizdatPress(),
showroom(),
columbarium(),
} {
w.G.AssetManager.Register(a)
}
}
-24
View File
@@ -1,24 +0,0 @@
// Package character registers the game's characters.
//
// Tapes are characters too, not props — the wiki is explicit about it. They
// have no body in the scene, so they carry no W/H and never appear in
// Scene.Actors; their voice lives in the tape channel (see world.TapeSay).
package character
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/world"
)
// Register adds every character to the game.
func Register(w *world.World) {
for _, c := range []inkwell.Character{
paul(),
dex(),
mysteryTape(),
supportTape(),
} {
w.G.CharacterManager.Register(c)
}
}
@@ -1,17 +0,0 @@
package character
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
"git.teletypegames.org/games/realworld/internal/theme"
)
// supportTape answers everything with basic information, insufferably. Dex
// hates it. Acquired in Chinatown (beat 5) — not reachable yet.
func supportTape() inkwell.Character {
return inkwell.Character{
Name: names.TapeSupport,
SpeechColor: theme.RGB(0xE8C86A),
}
}
-28
View File
@@ -1,28 +0,0 @@
// Package content is the composition root for everything the game declares:
// assets, characters, items, dialogues, scripts and scenes.
//
// Each registered entity lives in its own file, under a subpackage named after
// its kind. Adding a screen means adding screen/<name>.go, background/<name>.go
// and one line in each package's list — nothing else moves.
package content
import (
"git.teletypegames.org/games/realworld/internal/content/background"
"git.teletypegames.org/games/realworld/internal/content/character"
"git.teletypegames.org/games/realworld/internal/content/dialog"
"git.teletypegames.org/games/realworld/internal/content/item"
"git.teletypegames.org/games/realworld/internal/content/screen"
"git.teletypegames.org/games/realworld/internal/content/script"
"git.teletypegames.org/games/realworld/internal/world"
)
// Register adds every entity to the game. Order matters only in that screens
// reference backgrounds and characters, which Game.Validate cross-checks.
func Register(w *world.World) {
background.Register(w)
character.Register(w)
item.Register(w)
dialog.Register(w)
script.Register(w)
screen.Register(w)
}
@@ -1,29 +0,0 @@
package item
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
"git.teletypegames.org/games/realworld/internal/world"
)
// blackMarketArmilla is flavour, not a quest trigger: the gap between
// legitimised technology and street reality.
func blackMarketArmilla() inkwell.Item {
return inkwell.Item{
Name: names.ItemBlackArmilla,
Description: "black-market Armilla",
OnUseSelf: inkwell.Seq(
inkwell.Say(names.Paul, "Jailbroken. Pushes ads, but it was cheap."),
world.TapeSay(names.Dex, "Two slots, and both of them lie about the temperature. Don't trade me in for it."),
),
OnUseWith: map[string]inkwell.Action{
// An AUTHORED failure. Nothing is consumed, the character reacts,
// the tape remarks. Never a generic error line.
names.ItemNoodleLetter: inkwell.Seq(
inkwell.Say(names.Paul, "A letter and a bracelet. Brilliant."),
world.TapeSay(names.Dex, "Nothing. Which is what I'd charge for it."),
),
},
}
}
-21
View File
@@ -1,21 +0,0 @@
package item
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
"git.teletypegames.org/games/realworld/internal/world"
)
// mysteryTape is the engine of the investigation. Password-locked; it only
// starts speaking after the club trigger (beat 6).
func mysteryTape() inkwell.Item {
return inkwell.Item{
Name: names.ItemMysteryTape,
Description: "unmarked Personal Tape",
OnUseSelf: inkwell.Seq(
inkwell.Say(names.Paul, "Password-locked. Of course."),
world.TapeSay(names.Dex, "Put it in the second slot if you care that much. I did warn you."),
),
}
}
-20
View File
@@ -1,20 +0,0 @@
package item
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
"git.teletypegames.org/games/realworld/internal/world"
)
// noodleLetter starts the story: it is what brings Paul to the city.
func noodleLetter() inkwell.Item {
return inkwell.Item{
Name: names.ItemNoodleLetter,
Description: "Noodle's letter",
OnUseSelf: inkwell.Seq(
inkwell.Say(names.Paul, "“Come out, Paul. Not a phone conversation.” That's all it says."),
world.TapeSay(names.Dex, "And now you're here. And he isn't."),
),
}
}
-107
View File
@@ -1,107 +0,0 @@
package screen
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
"git.teletypegames.org/games/realworld/internal/world"
)
// alley is the alley behind Noodle's house — beat 3, and the vertical slice the
// whole HUD is measured against: hotspots, a gated pickup, the two-slot
// Armilla, tape dialogue and an authored failure.
//
// The wiki keeps it in the same catalogue row as the house ("Noodle háza & a
// sikátor") and its graph has one arrow in, NoodleUtcafront → Sikátor, so the
// only way out is back to the street. The alley's own locked back door is the
// New Game+ hook and leads to a room the deck has not painted.
func alley() screen {
return screen{
name: names.ScreenAlley,
title: "ALLEY — BEHIND NOODLE'S HOUSE",
background: names.BgAlley,
exits: []exit{
// On the left: the back door hotspot below owns the other end.
{to: names.ScreenNoodleHouse, label: "back out to the street", side: sideLeft},
},
actors: []inkwell.SceneActor{
{CharacterName: names.Paul, At: inkwell.Point{X: 120, Y: 232}},
},
onEnter: setVarIfEmpty(names.VarArmillaStrip, "SRP: 1 tape"),
hotspots: []inkwell.Hotspot{hidingPlace(), backDoor(), bin(), fireEscape()},
}
}
// hidingPlace holds the mystery tape. It only opens once Paul has the tip from
// the police station (beat 2) — without it he does not know what to look for.
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(names.FlagHasTape),
inkwell.Say(names.Paul, "Empty. Whatever was in there is on me now."),
inkwell.Say(names.Paul, "Loose brick. There's a gap behind it."),
),
OnUse: inkwell.If(inkwell.Flag(names.FlagPoliceTip),
inkwell.If(inkwell.Flag(names.FlagHasTape),
inkwell.Say(names.Paul, "There's nothing else in there."),
inkwell.Seq(
inkwell.Say(names.Paul, "“Behind the brick.” All right then."),
inkwell.Give(names.ItemMysteryTape),
inkwell.SetFlag(names.FlagHasTape),
inkwell.Say(names.Paul, "A Personal Tape. No label, no seal."),
world.TapeSay(names.Dex, "Password-locked. And somebody very much did not want it found."),
),
),
inkwell.Say(names.Paul, "One loose brick. I'm not taking the alley apart over it."),
),
OnTake: inkwell.Say(names.Paul, "I'm not carrying a wall."),
}
}
// backDoor is the New Game+ hook: the code can be entered on the first visit
// too, if the player already knows it.
func backDoor() inkwell.Hotspot {
return inkwell.Hotspot{
Name: "back_door",
Label: "locked back door",
Area: inkwell.Rect(72, 106, 80, 124),
OnLook: inkwell.Say(names.Paul, "Keypad. Four digits, worn keys."),
OnUse: inkwell.Seq(
inkwell.Say(names.Paul, "Locked. And I'm not guessing four digits."),
world.TapeSay(names.Dex, "The wear is heaviest on the two and the seven. That isn't a code. It's fewer options."),
),
OnTalk: inkwell.Say(names.Paul, "To the door? I'm not there yet."),
OnUseWith: map[string]inkwell.Action{
// An AUTHORED failure — not the engine's hardcoded flash.
names.ItemBlackArmilla: inkwell.Seq(
inkwell.Say(names.Paul, "A black-market bracelet doesn't open a keypad."),
world.TapeSay(names.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(names.Paul, "Somebody's been through it. Thoroughly."),
OnUse: inkwell.Seq(
inkwell.Say(names.Paul, "Already turned out. I'm not going to be the second one."),
world.TapeSay(names.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(names.Paul, "Runs all the way to the roof. Bottom rung is two metres over my head."),
OnUse: inkwell.Say(names.Paul, "Can't reach it. I'd need something to stand on."),
}
}
-19
View File
@@ -1,19 +0,0 @@
// Package script registers the game's named, reusable action sequences —
// cutscenes and recurring logic, built from the inkwell action set.
package script
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/world"
)
// Register adds every script to the game.
func Register(w *world.World) {
for _, s := range []inkwell.Script{
tapeInsert(w),
awakeningFinale(w),
} {
w.G.ScriptManager.Register(s)
}
}
-31
View File
@@ -1,31 +0,0 @@
package script
import (
inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/names"
"git.teletypegames.org/games/realworld/internal/world"
)
// tapeInsert runs whenever a tape goes into slot 2: it activates the tape and
// makes Dex comment. The wiki calls the comment mandatory — Dex remarks on
// every newly loaded tape, without exception.
func tapeInsert(w *world.World) inkwell.Script {
return inkwell.Script{
Name: names.ScriptTapeInsert,
Actions: inkwell.Seq(
world.Fn(func(ctx *inkwell.Ctx) {
item := w.TakePending()
if item == "" {
return
}
w.SetSlot2(item)
ctx.Game.Inventory.Remove(item)
ctx.Game.State.SetVar(names.VarArmillaStrip, "SLOT2: READING")
}),
inkwell.Say(names.Paul, "Right. Let's see who you are."),
world.TapeSay(names.Dex, "Clicked in. Spinning. And now: nothing."),
world.TapeSay(names.Dex, "A stranger's tape in your own Armilla, Paul. I hope you know what you're doing."),
),
}
}
+3 -3
View File
@@ -7,7 +7,7 @@ import (
inkwell "git.teletypegames.org/engines/inkwell" inkwell "git.teletypegames.org/engines/inkwell"
"git.teletypegames.org/games/realworld/internal/boot" "git.teletypegames.org/games/realworld/inc"
) )
// The art ships inside the binary. js/wasm has no OS filesystem, so without // The art ships inside the binary. js/wasm has no OS filesystem, so without
@@ -21,13 +21,13 @@ var assets embed.FS
func init() { inkwell.AssetFS = assets } func init() { inkwell.AssetFS = assets }
func main() { func main() {
var o boot.Opts var o inc.Opts
flag.StringVar(&o.Screen, "screen", "", "starting screen (empty: the alley)") flag.StringVar(&o.Screen, "screen", "", "starting screen (empty: the alley)")
flag.BoolVar(&o.Finale, "finale", false, flag.BoolVar(&o.Finale, "finale", false,
"start with the finale, to try the Nokia-punk theme switch") "start with the finale, to try the Nokia-punk theme switch")
flag.Parse() flag.Parse()
if err := inkwell.Run(boot.New(o)); err != nil { if err := inkwell.Run(inc.New(o)); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }