new file structure
This commit is contained in:
@@ -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
|
||||
```
|
||||
|
||||
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`,
|
||||
`ice_cream_shop`, `trinket_shop`, `small_restaurant`, `secret_club`,
|
||||
`bbs_terminal`, `street`, `hospital`, `server_farm`, `secret_lab`,
|
||||
@@ -151,18 +151,24 @@ licence.
|
||||
|
||||
## Structure
|
||||
|
||||
The game is one flat package, `inc`. There are no subdirectories: a file's
|
||||
name says where it belongs, in the form `[category].[name].go`. The category is
|
||||
always singular, and `[category].manager.go` is the file that ties that category
|
||||
together — its `register…`, its shared types, its list.
|
||||
|
||||
```
|
||||
main.go flags + inkwell.Run
|
||||
internal/names/ entity names and world-state keys
|
||||
internal/theme/ realworld-93 + nokia-punk
|
||||
internal/world/ unsaved runtime state, custom actions, action pump
|
||||
internal/ui/ HUD: layout, custom widgets, coloured text
|
||||
internal/content/ one file per registered entity, by kind
|
||||
internal/boot/ wiring
|
||||
inc/names.manager.go entity names and world-state keys
|
||||
inc/theme.manager.go realworld-93 + nokia-punk
|
||||
inc/world.*.go unsaved runtime state, custom actions, action pump
|
||||
inc/ui.*.go HUD: layout, custom widgets, coloured text
|
||||
inc/<kind>.<name>.go one file per registered entity, by kind
|
||||
inc/boot.manager.go wiring
|
||||
```
|
||||
|
||||
`world` deliberately knows nothing about the HUD or the content — both build on
|
||||
it, so the reverse would be a cycle:
|
||||
Nothing is enforced by the compiler any more, so the layering is a rule the
|
||||
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
|
||||
@@ -170,26 +176,30 @@ names ← theme ← world ← ui
|
||||
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/ one file per screen: paul_shop.go, noodle_house.go, …
|
||||
character/ paul.go dex.go mystery_tape.go support_tape.go
|
||||
item/ noodle_letter.go black_market_armilla.go mystery_tape.go
|
||||
dialog/ dex_talk.go mystery_tape_silent.go
|
||||
script/ tape_insert.go awakening_finale.go
|
||||
screen/ one file per screen: alley.go, trinket_shop.go, … + exit.go
|
||||
background.*.go one file per screen: background.paul_shop.go, …
|
||||
character.*.go character.paul.go character.dex.go character.mystery_tape.go
|
||||
item.*.go item.noodle_letter.go item.black_market_armilla.go …
|
||||
dialog.*.go dialog.dex_talk.go dialog.mystery_tape_silent.go
|
||||
script.*.go script.tape_insert.go script.awakening_finale.go
|
||||
screen.*.go one file per screen: screen.alley.go, … + screen.exit.go
|
||||
```
|
||||
|
||||
Adding a screen means adding `screen/<name>.go` and `background/<name>.go`, and
|
||||
one line in each package's list. Nothing else moves.
|
||||
Adding a screen means adding `screen.<name>.go` and `background.<name>.go`, and
|
||||
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
|
||||
type every file in `screen/` returns — but the wiki, the concept-art deck and
|
||||
the beat tables all count *screens*, so the package, the files and the flag say
|
||||
type every `screen.*.go` file returns — but the wiki, the concept-art deck and
|
||||
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
|
||||
(`world.EnterScene`, `SceneManager`).
|
||||
(`EnterScene`, `SceneManager`).
|
||||
|
||||
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.
|
||||
@@ -234,13 +244,13 @@ for a small engine change.
|
||||
1. **`drawText` discards colour.** In `asset.text.go` the colour argument is
|
||||
`_ = c` and rendering goes through `ebitenutil.DebugPrintAt`, which only
|
||||
draws white. Every text colour in `Theme` is therefore inert. *Workaround:*
|
||||
`ui/text.go` renders onto a scratch image and blits it tinted with
|
||||
`ui.text.go` renders onto a scratch image and blits it tinted with
|
||||
`ColorScale`. The custom widgets colour correctly; the built-ins
|
||||
(`StatusLine`, `DialogBox`, `TopBar`, `InventoryBar`) are still white.
|
||||
*Fix:* move `drawText` to `text/v2` — no call site would change.
|
||||
|
||||
2. **`queueAction` is unexported**, so a domain widget cannot start an action.
|
||||
*Workaround:* the pump in `world/action.go` drives its own `Runner` through
|
||||
*Workaround:* the pump in `world.action.go` drives its own `Runner` through
|
||||
the exported `inkwell.Ctx`. *Caveat:* it runs alongside the engine's script
|
||||
runner, not instead of it.
|
||||
|
||||
@@ -251,7 +261,7 @@ for a small engine change.
|
||||
character instead, escalating on repeats.
|
||||
|
||||
4. **No exported `CurrentScene()`**, but the pump needs `Ctx.Scene`.
|
||||
*Workaround:* `world.EnterScene` is the first step of every scene's
|
||||
*Workaround:* `EnterScene` is the first step of every scene's
|
||||
`OnEnter`; the domain tracks it.
|
||||
|
||||
5. **Widgets have no `Visible` field and the `Manager` cannot unregister**, so
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
package background
|
||||
package inc
|
||||
|
||||
import (
|
||||
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.
|
||||
//
|
||||
// Not painted yet: the file is missing on purpose, and inkwell draws its
|
||||
// placeholder instead — which is exactly the greybox convention.
|
||||
func alley() inkwell.Asset {
|
||||
func backgroundAlley() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgAlley,
|
||||
Name: BgAlley,
|
||||
Path: "assets/bg/alley.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
package background
|
||||
package inc
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// bbsTerminal — deck 11.
|
||||
func bbsTerminal() inkwell.Asset {
|
||||
func backgroundBbsTerminal() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgBBSTerminal,
|
||||
Name: BgBBSTerminal,
|
||||
Path: "assets/bg/bbs_terminal.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
package background
|
||||
package inc
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// bvkBranch — deck 18.
|
||||
func bvkBranch() inkwell.Asset {
|
||||
func backgroundBvkBranch() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgBVKBranch,
|
||||
Name: BgBVKBranch,
|
||||
Path: "assets/bg/bvk_branch.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
package background
|
||||
package inc
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// columbarium — deck 23.
|
||||
func columbarium() inkwell.Asset {
|
||||
func backgroundColumbarium() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgColumbarium,
|
||||
Name: BgColumbarium,
|
||||
Path: "assets/bg/columbarium.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
package background
|
||||
package inc
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// curatorShop — deck 19.
|
||||
func curatorShop() inkwell.Asset {
|
||||
func backgroundCuratorShop() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgCuratorShop,
|
||||
Name: BgCuratorShop,
|
||||
Path: "assets/bg/curator_shop.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
package background
|
||||
package inc
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// hackerspace — deck 06.
|
||||
func hackerspace() inkwell.Asset {
|
||||
func backgroundHackerspace() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgHackerspace,
|
||||
Name: BgHackerspace,
|
||||
Path: "assets/bg/hackerspace.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
package background
|
||||
package inc
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// hospital — deck 13.
|
||||
func hospital() inkwell.Asset {
|
||||
func backgroundHospital() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgHospital,
|
||||
Name: BgHospital,
|
||||
Path: "assets/bg/hospital.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
package background
|
||||
package inc
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// iceCreamShop — deck 07.
|
||||
func iceCreamShop() inkwell.Asset {
|
||||
func backgroundIceCreamShop() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgIceCreamShop,
|
||||
Name: BgIceCreamShop,
|
||||
Path: "assets/bg/ice_cream_shop.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
@@ -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 (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// noodleHouse — deck 02.
|
||||
func noodleHouse() inkwell.Asset {
|
||||
func backgroundNoodleHouse() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgNoodleHouse,
|
||||
Name: BgNoodleHouse,
|
||||
Path: "assets/bg/noodle_house.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
+3
-5
@@ -1,15 +1,13 @@
|
||||
package background
|
||||
package inc
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// normanApartment — deck 03.
|
||||
func normanApartment() inkwell.Asset {
|
||||
func backgroundNormanApartment() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgNormanApartment,
|
||||
Name: BgNormanApartment,
|
||||
Path: "assets/bg/norman_apartment.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
package background
|
||||
package inc
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// paulShop — deck 01.
|
||||
func paulShop() inkwell.Asset {
|
||||
func backgroundPaulShop() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgPaulShop,
|
||||
Name: BgPaulShop,
|
||||
Path: "assets/bg/paul_shop.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
package background
|
||||
package inc
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// policeStation — deck 04.
|
||||
func policeStation() inkwell.Asset {
|
||||
func backgroundPoliceStation() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgPoliceStation,
|
||||
Name: BgPoliceStation,
|
||||
Path: "assets/bg/police_station.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
package background
|
||||
package inc
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// publicBbs — deck 17.
|
||||
func publicBbs() inkwell.Asset {
|
||||
func backgroundPublicBBS() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgPublicBBS,
|
||||
Name: BgPublicBBS,
|
||||
Path: "assets/bg/public_bbs.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
package background
|
||||
package inc
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// rooftopHideout — deck 16.
|
||||
func rooftopHideout() inkwell.Asset {
|
||||
func backgroundRooftopHideout() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgRooftopHideout,
|
||||
Name: BgRooftopHideout,
|
||||
Path: "assets/bg/rooftop_hideout.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
package background
|
||||
package inc
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// samizdatPress — deck 21.
|
||||
func samizdatPress() inkwell.Asset {
|
||||
func backgroundSamizdatPress() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgSamizdatPress,
|
||||
Name: BgSamizdatPress,
|
||||
Path: "assets/bg/samizdat_press.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
package background
|
||||
package inc
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// scrapMarket — deck 20.
|
||||
func scrapMarket() inkwell.Asset {
|
||||
func backgroundScrapMarket() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgScrapMarket,
|
||||
Name: BgScrapMarket,
|
||||
Path: "assets/bg/scrap_market.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
package background
|
||||
package inc
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// secretClub — deck 10.
|
||||
func secretClub() inkwell.Asset {
|
||||
func backgroundSecretClub() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgSecretClub,
|
||||
Name: BgSecretClub,
|
||||
Path: "assets/bg/secret_club.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
package background
|
||||
package inc
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// secretLab — deck 15.
|
||||
func secretLab() inkwell.Asset {
|
||||
func backgroundSecretLab() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgSecretLab,
|
||||
Name: BgSecretLab,
|
||||
Path: "assets/bg/secret_lab.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
@@ -1,18 +1,16 @@
|
||||
package background
|
||||
package inc
|
||||
|
||||
import (
|
||||
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.
|
||||
//
|
||||
// Not painted yet: the file is missing on purpose, and inkwell draws its
|
||||
// placeholder instead — which is exactly the greybox convention.
|
||||
func selector() inkwell.Asset {
|
||||
func backgroundSelector() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgSelector,
|
||||
Name: BgSelector,
|
||||
Path: "assets/bg/selector.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
package background
|
||||
package inc
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// serverFarm — deck 14.
|
||||
func serverFarm() inkwell.Asset {
|
||||
func backgroundServerFarm() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgServerFarm,
|
||||
Name: BgServerFarm,
|
||||
Path: "assets/bg/server_farm.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
package background
|
||||
package inc
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// showroom — deck 22.
|
||||
func showroom() inkwell.Asset {
|
||||
func backgroundShowroom() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgShowroom,
|
||||
Name: BgShowroom,
|
||||
Path: "assets/bg/showroom.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
+3
-5
@@ -1,15 +1,13 @@
|
||||
package background
|
||||
package inc
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// smallRestaurant — deck 09.
|
||||
func smallRestaurant() inkwell.Asset {
|
||||
func backgroundSmallRestaurant() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgSmallRestaurant,
|
||||
Name: BgSmallRestaurant,
|
||||
Path: "assets/bg/small_restaurant.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
package background
|
||||
package inc
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// street — deck 12.
|
||||
func street() inkwell.Asset {
|
||||
func backgroundStreet() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgStreet,
|
||||
Name: BgStreet,
|
||||
Path: "assets/bg/street.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
package background
|
||||
package inc
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// trinketShop — deck 08.
|
||||
func trinketShop() inkwell.Asset {
|
||||
func backgroundTrinketShop() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgTrinketShop,
|
||||
Name: BgTrinketShop,
|
||||
Path: "assets/bg/trinket_shop.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
@@ -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 (
|
||||
"image/png"
|
||||
@@ -8,15 +8,11 @@ import (
|
||||
"testing"
|
||||
|
||||
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()
|
||||
g := boot.New(boot.Opts{})
|
||||
g := New(Opts{})
|
||||
if err := g.Validate(); err != nil {
|
||||
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
|
||||
// (RunDialogue and RunScript fail quietly), so check them here.
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
t.Errorf("missing item: %q", n)
|
||||
}
|
||||
}
|
||||
for n := range names.Tapes {
|
||||
for n := range Tapes {
|
||||
if !g.CharacterManager.Has(n) {
|
||||
t.Errorf("tape character not registered: %q", n)
|
||||
}
|
||||
}
|
||||
// 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) {
|
||||
t.Errorf("tape item not registered: %q", item)
|
||||
}
|
||||
if !g.CharacterManager.Has(char) {
|
||||
t.Errorf("tape character not registered: %q", char)
|
||||
}
|
||||
if !g.DialogueManager.Has(names.TapeDialogue(item)) {
|
||||
t.Errorf("tape %q has no dialogue: %q", item, names.TapeDialogue(item))
|
||||
if !g.DialogueManager.Has(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
|
||||
// readable.
|
||||
func TestTapeVoicesAreDistinct(t *testing.T) {
|
||||
g := build(t)
|
||||
paul, _ := g.CharacterManager.Get(names.Paul)
|
||||
g := newTestGame(t)
|
||||
paul, _ := g.CharacterManager.Get(Paul)
|
||||
seen := map[[4]uint32]string{}
|
||||
for n := range names.Tapes {
|
||||
for n := range Tapes {
|
||||
c, _ := g.CharacterManager.Get(n)
|
||||
if c.SpeechColor == nil {
|
||||
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
|
||||
// the HUD strip.
|
||||
func TestHUDGeometry(t *testing.T) {
|
||||
g := build(t)
|
||||
g := newTestGame(t)
|
||||
|
||||
ch, ok := g.UIManager.Get("channel")
|
||||
if !ok {
|
||||
t.Fatal("no 'channel' widget")
|
||||
}
|
||||
tc := ch.(*ui.TapeChannel)
|
||||
if tc.Bounds.X < ui.DividerX {
|
||||
t.Errorf("tape channel crosses into the world side: X=%v < %v", tc.Bounds.X, ui.DividerX)
|
||||
tc := ch.(*TapeChannel)
|
||||
if tc.Bounds.X < 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)
|
||||
}
|
||||
if tc.Bounds.Y < ui.HUDTop {
|
||||
t.Errorf("tape channel reaches into the scene: Y=%v < %v", tc.Bounds.Y, ui.HUDTop)
|
||||
if tc.Bounds.Y < HUDTop {
|
||||
t.Errorf("tape channel reaches into the scene: Y=%v < %v", tc.Bounds.Y, HUDTop)
|
||||
}
|
||||
|
||||
sl, ok := g.UIManager.Get("tapes")
|
||||
if !ok {
|
||||
t.Fatal("no 'tapes' widget")
|
||||
}
|
||||
if ts := sl.(*ui.TapeSlots); ts.Bounds.X+ts.Bounds.W > ui.DividerX {
|
||||
t.Errorf("tape slots cross the divider: %v > %v", 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, DividerX)
|
||||
}
|
||||
|
||||
// The dialogue box deliberately covers only the left region, so a tape can
|
||||
@@ -122,16 +118,16 @@ func TestHUDGeometry(t *testing.T) {
|
||||
if !ok {
|
||||
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",
|
||||
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 —
|
||||
// otherwise it would swallow HUD clicks.
|
||||
func TestWidgetOrder(t *testing.T) {
|
||||
g := build(t)
|
||||
g := newTestGame(t)
|
||||
n := g.UIManager.Names()
|
||||
if len(n) == 0 || n[0] != "usewith_guard" {
|
||||
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
|
||||
// the screen is 640×380 in the first place; this keeps the two in step.
|
||||
func TestSceneBackgroundsResolve(t *testing.T) {
|
||||
g := build(t)
|
||||
g := newTestGame(t)
|
||||
|
||||
// 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
|
||||
// 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() {
|
||||
s := g.SceneManager.MustGet(n)
|
||||
@@ -169,7 +165,7 @@ func TestSceneBackgroundsResolve(t *testing.T) {
|
||||
if undrawn[a.Name] {
|
||||
continue
|
||||
}
|
||||
f, err := os.Open(filepath.Join("..", "..", a.Path))
|
||||
f, err := os.Open(filepath.Join("..", a.Path))
|
||||
if err != nil {
|
||||
t.Errorf("scene %q: background file missing: %v", n, err)
|
||||
continue
|
||||
@@ -180,16 +176,16 @@ func TestSceneBackgroundsResolve(t *testing.T) {
|
||||
t.Errorf("scene %q: unreadable background: %v", n, err)
|
||||
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",
|
||||
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
|
||||
// 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 {
|
||||
var out []string
|
||||
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
|
||||
// into: the engine logs "changeScene: unknown" and nothing happens on screen.
|
||||
func TestExitsResolve(t *testing.T) {
|
||||
g := build(t)
|
||||
g := newTestGame(t)
|
||||
for _, n := range g.SceneManager.Names() {
|
||||
for _, to := range exits(g, n) {
|
||||
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
|
||||
// through the selector: that is what the wiki's location graph is centred on.
|
||||
func TestEveryScreenIsReachable(t *testing.T) {
|
||||
g := build(t)
|
||||
g := newTestGame(t)
|
||||
|
||||
seen := map[string]bool{names.ScreenPaulShop: true}
|
||||
queue := []string{names.ScreenPaulShop}
|
||||
seen := map[string]bool{ScreenPaulShop: true}
|
||||
queue := []string{ScreenPaulShop}
|
||||
for len(queue) > 0 {
|
||||
at := queue[0]
|
||||
queue = queue[1:]
|
||||
@@ -237,7 +233,7 @@ func TestEveryScreenIsReachable(t *testing.T) {
|
||||
|
||||
for _, n := range g.SceneManager.Names() {
|
||||
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
|
||||
// way on is the debug walk.
|
||||
func TestEveryScreenHasAWayOut(t *testing.T) {
|
||||
g := build(t)
|
||||
g := newTestGame(t)
|
||||
for _, n := range g.SceneManager.Names() {
|
||||
var ways int
|
||||
for _, h := range g.SceneManager.MustGet(n).Hotspots {
|
||||
@@ -1,17 +1,14 @@
|
||||
package character
|
||||
package inc
|
||||
|
||||
import (
|
||||
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
|
||||
// Paul's Armilla. Constant commentary, hint system, dialogue partner.
|
||||
func dex() inkwell.Character {
|
||||
func characterDex() inkwell.Character {
|
||||
return inkwell.Character{
|
||||
Name: names.Dex,
|
||||
SpeechColor: theme.Amber,
|
||||
Name: Dex,
|
||||
SpeechColor: Amber,
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
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
|
||||
@@ -12,9 +9,9 @@ import (
|
||||
//
|
||||
// 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.
|
||||
func mysteryTape() inkwell.Character {
|
||||
func characterMysteryTape() inkwell.Character {
|
||||
return inkwell.Character{
|
||||
Name: names.TapeMystery,
|
||||
SpeechColor: theme.RGB(0xB9A98A),
|
||||
Name: TapeMystery,
|
||||
SpeechColor: RGB(0xB9A98A),
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,20 @@
|
||||
package character
|
||||
package inc
|
||||
|
||||
import (
|
||||
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
|
||||
// competent, living on the edge of the city.
|
||||
func paul() inkwell.Character {
|
||||
func characterPaul() inkwell.Character {
|
||||
return inkwell.Character{
|
||||
Name: names.Paul,
|
||||
Name: Paul,
|
||||
Speed: 96,
|
||||
W: 28,
|
||||
H: 68,
|
||||
Start: inkwell.Point{X: 120, Y: 232},
|
||||
// Bone white: the world and Paul. If something is amber, a tape said
|
||||
// it — see the colour rule in README.
|
||||
SpeechColor: theme.Ink,
|
||||
SpeechColor: Ink,
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
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
|
||||
// story flags, so the same entry point stays useful as the game progresses.
|
||||
func dexTalk() inkwell.Dialogue {
|
||||
func dialogDexTalk() inkwell.Dialogue {
|
||||
return inkwell.Dialogue{
|
||||
Name: names.DlgDex,
|
||||
Name: DlgDex,
|
||||
Start: "root",
|
||||
Nodes: []inkwell.DialogueNode{{
|
||||
Name: "root",
|
||||
Lines: []inkwell.DialogueLine{{Speaker: names.Dex, Text: "Talk."}},
|
||||
Lines: []inkwell.DialogueLine{{Speaker: Dex, Text: "Talk."}},
|
||||
Choices: []inkwell.DialogueChoice{
|
||||
{
|
||||
Text: "What am I doing here, Dex?",
|
||||
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"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Text: "What do you know about Noodle?",
|
||||
Show: inkwell.Not(inkwell.Flag(names.FlagHasTape)),
|
||||
Show: inkwell.Not(inkwell.Flag(FlagHasTape)),
|
||||
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"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Text: "What do you make of this tape?",
|
||||
Show: inkwell.Flag(names.FlagHasTape),
|
||||
Show: inkwell.Flag(FlagHasTape),
|
||||
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"),
|
||||
},
|
||||
},
|
||||
@@ -43,7 +41,7 @@ func dexTalk() inkwell.Dialogue {
|
||||
Text: "I miss you.",
|
||||
Once: true,
|
||||
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"),
|
||||
},
|
||||
},
|
||||
@@ -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
|
||||
// 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
|
||||
// memory of what you already tried. That is still open — see README, "Known
|
||||
// gaps".
|
||||
package dialog
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/world"
|
||||
)
|
||||
|
||||
// Register adds every dialogue to the game.
|
||||
func Register(w *world.World) {
|
||||
// registerDialog adds every dialogue to the game.
|
||||
func registerDialog(w *World) {
|
||||
for _, d := range []inkwell.Dialogue{
|
||||
dexTalk(),
|
||||
mysteryTapeSilent(),
|
||||
dialogDexTalk(),
|
||||
dialogMysteryTapeSilent(),
|
||||
} {
|
||||
w.G.DialogueManager.Register(d)
|
||||
}
|
||||
@@ -1,26 +1,24 @@
|
||||
package dialog
|
||||
package inc
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// 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
|
||||
// tape itself says nothing.
|
||||
func mysteryTapeSilent() inkwell.Dialogue {
|
||||
func dialogMysteryTapeSilent() inkwell.Dialogue {
|
||||
return inkwell.Dialogue{
|
||||
Name: names.DlgMysteryTape,
|
||||
Name: DlgMysteryTape,
|
||||
Start: "root",
|
||||
Nodes: []inkwell.DialogueNode{{
|
||||
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{
|
||||
{
|
||||
Text: "Are you sure it works?",
|
||||
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"),
|
||||
},
|
||||
},
|
||||
@@ -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
|
||||
// go into slot 2 of Paul's Armilla, and loading one activates its effect.
|
||||
package item
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/world"
|
||||
)
|
||||
|
||||
// Register adds every item to the game.
|
||||
func Register(w *world.World) {
|
||||
// registerItem adds every item to the game.
|
||||
func registerItem(w *World) {
|
||||
for _, i := range []inkwell.Item{
|
||||
noodleLetter(),
|
||||
blackMarketArmilla(),
|
||||
mysteryTape(),
|
||||
itemNoodleLetter(),
|
||||
itemBlackMarketArmilla(),
|
||||
itemMysteryTape(),
|
||||
} {
|
||||
w.G.ItemManager.Register(i)
|
||||
}
|
||||
@@ -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."),
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
// (services/wiki-pages/pages/projects/realworld/entities) so that content and
|
||||
@@ -7,9 +9,8 @@
|
||||
// tape_behelyezes, sikator); the constants below are their English
|
||||
// equivalents — the mapping is one-to-one, in catalogue order.
|
||||
//
|
||||
// This package imports nothing from the project, so every layer can depend on
|
||||
// it without creating a cycle.
|
||||
package names
|
||||
// These constants are the vocabulary every other category speaks in; nothing
|
||||
// here depends on anything else in the game.
|
||||
|
||||
// Characters.
|
||||
const (
|
||||
@@ -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
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
package inc
|
||||
|
||||
// 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
|
||||
@@ -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,
|
||||
// Norman's machine at the flat, the old one on the roof — which is why its way
|
||||
// out is "back", and not a place.
|
||||
func bbsTerminal() screen {
|
||||
func screenBbsTerminal() screen {
|
||||
return screen{
|
||||
name: names.ScreenBBSTerminal,
|
||||
name: ScreenBBSTerminal,
|
||||
title: "TERMINAL — BBS ACCESS POINT",
|
||||
background: names.BgBBSTerminal,
|
||||
background: BgBBSTerminal,
|
||||
exits: []exit{
|
||||
{label: "step back from the terminal", side: sideNear},
|
||||
},
|
||||
@@ -1,16 +1,14 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
package inc
|
||||
|
||||
// bvkBranch is a 💡 suggestion: the local certification office, imprimatur
|
||||
// stamps and licence checks and numbered waiting rooms — everything a man with
|
||||
// a jailbroken two-slot Armilla should stay away from
|
||||
// (wiki: entities#potenciális-további-helyszínek).
|
||||
func bvkBranch() screen {
|
||||
func screenBvkBranch() screen {
|
||||
return screen{
|
||||
name: names.ScreenBVKBranch,
|
||||
name: ScreenBVKBranch,
|
||||
title: "BVK — SAN FRANCISCO BRANCH",
|
||||
background: names.BgBVKBranch,
|
||||
background: BgBVKBranch,
|
||||
onSelector: true,
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,14 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
package inc
|
||||
|
||||
// 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
|
||||
// the same thread as Norman and The AI
|
||||
// (wiki: entities#potenciális-további-helyszínek).
|
||||
func columbarium() screen {
|
||||
func screenColumbarium() screen {
|
||||
return screen{
|
||||
name: names.ScreenColumbarium,
|
||||
name: ScreenColumbarium,
|
||||
title: "COLUMBARIUM — DEX'S MEMORIAL",
|
||||
background: names.BgColumbarium,
|
||||
background: BgColumbarium,
|
||||
onSelector: true,
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
package inc
|
||||
|
||||
// curatorShop is a 💡 suggestion: the last workshop of the declining curators'
|
||||
// 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).
|
||||
func curatorShop() screen {
|
||||
func screenCuratorShop() screen {
|
||||
return screen{
|
||||
name: names.ScreenCuratorShop,
|
||||
name: ScreenCuratorShop,
|
||||
title: "CURATOR SERVICE — THE MASTER'S WORKSHOP",
|
||||
background: names.BgCuratorShop,
|
||||
background: BgCuratorShop,
|
||||
onSelector: true,
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package screen
|
||||
package inc
|
||||
|
||||
// Exits — the connections between screens.
|
||||
//
|
||||
@@ -25,9 +25,6 @@ package screen
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
"git.teletypegames.org/games/realworld/internal/world"
|
||||
)
|
||||
|
||||
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
|
||||
// wiki's Helyszínválasztó lists.
|
||||
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
|
||||
@@ -85,13 +82,13 @@ func exitName(to string) string {
|
||||
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)
|
||||
if e.to == "" {
|
||||
travel = world.Back(w)
|
||||
travel = Back(w)
|
||||
}
|
||||
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
|
||||
if area == nil {
|
||||
@@ -102,8 +99,8 @@ func (e exit) hotspot(w *world.World) inkwell.Hotspot {
|
||||
Label: e.label,
|
||||
Area: area,
|
||||
Cursor: inkwell.CursorExit,
|
||||
OnLook: inkwell.Say(names.Paul, "That way: "+e.label+"."),
|
||||
OnLook: inkwell.Say(Paul, "That way: "+e.label+"."),
|
||||
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
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
package inc
|
||||
|
||||
// hackerspace: the community workshop at the edge of the Mission, printers and
|
||||
// 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
|
||||
// selector like every other main location.
|
||||
func hackerspace() screen {
|
||||
func screenHackerspace() screen {
|
||||
return screen{
|
||||
name: names.ScreenHackerspace,
|
||||
name: ScreenHackerspace,
|
||||
title: "HACKERSPACE",
|
||||
background: names.BgHackerspace,
|
||||
background: BgHackerspace,
|
||||
onSelector: true,
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
package inc
|
||||
|
||||
// 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).
|
||||
func hospital() screen {
|
||||
func screenHospital() screen {
|
||||
return screen{
|
||||
name: names.ScreenHospital,
|
||||
name: ScreenHospital,
|
||||
title: "HOSPITAL — PSYCHIATRIC WING",
|
||||
background: names.BgHospital,
|
||||
background: BgHospital,
|
||||
onSelector: true,
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
package inc
|
||||
|
||||
// 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
|
||||
@@ -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 way there is the trinket shop's address, so this screen has no exit of
|
||||
// its own.
|
||||
func iceCreamShop() screen {
|
||||
func screenIceCreamShop() screen {
|
||||
return screen{
|
||||
name: names.ScreenIceCreamShop,
|
||||
name: ScreenIceCreamShop,
|
||||
title: "ICE CREAM SHOP — WHERE THE BAR WAS",
|
||||
background: names.BgIceCreamShop,
|
||||
background: BgIceCreamShop,
|
||||
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
|
||||
// and the beat tables all count screens. inkwell's word for the same thing is
|
||||
@@ -7,16 +9,12 @@
|
||||
// engine.
|
||||
//
|
||||
// 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
|
||||
// last part so far; the rest are greybox.
|
||||
package screen
|
||||
|
||||
import (
|
||||
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.
|
||||
@@ -25,7 +23,7 @@ type screen struct {
|
||||
title 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
|
||||
// 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
|
||||
@@ -40,68 +38,68 @@ type screen struct {
|
||||
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
|
||||
// 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
|
||||
// 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.
|
||||
var deck = []func() screen{
|
||||
paulShop, // 01
|
||||
noodleHouse, // 02
|
||||
alley, // 02, same catalogue row
|
||||
normanApartment, // 03
|
||||
policeStation, // 04
|
||||
hackerspace, // 06
|
||||
iceCreamShop, // 07
|
||||
trinketShop, // 08
|
||||
smallRestaurant, // 09
|
||||
secretClub, // 10
|
||||
bbsTerminal, // 11
|
||||
street, // 12
|
||||
hospital, // 13
|
||||
serverFarm, // 14
|
||||
secretLab, // 15
|
||||
rooftopHideout, // 16
|
||||
publicBBS, // 17
|
||||
bvkBranch, // 18
|
||||
curatorShop, // 19
|
||||
scrapMarket, // 20
|
||||
samizdatPress, // 21
|
||||
showroom, // 22
|
||||
columbarium, // 23
|
||||
var screenDeck = []func() screen{
|
||||
screenPaulShop, // 01
|
||||
screenNoodleHouse, // 02
|
||||
screenAlley, // 02, same catalogue row
|
||||
screenNormanApartment, // 03
|
||||
screenPoliceStation, // 04
|
||||
screenHackerspace, // 06
|
||||
screenIceCreamShop, // 07
|
||||
screenTrinketShop, // 08
|
||||
screenSmallRestaurant, // 09
|
||||
screenSecretClub, // 10
|
||||
screenBbsTerminal, // 11
|
||||
screenStreet, // 12
|
||||
screenHospital, // 13
|
||||
screenServerFarm, // 14
|
||||
screenSecretLab, // 15
|
||||
screenRooftopHideout, // 16
|
||||
screenPublicBBS, // 17
|
||||
screenBvkBranch, // 18
|
||||
screenCuratorShop, // 19
|
||||
screenScrapMarket, // 20
|
||||
screenSamizdatPress, // 21
|
||||
screenShowroom, // 22
|
||||
screenColumbarium, // 23
|
||||
}
|
||||
|
||||
// Register 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
|
||||
// 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 screenDeck rather than
|
||||
// listed in it.
|
||||
func Register(w *world.World) {
|
||||
screens := make([]screen, 0, len(deck)+1)
|
||||
for _, f := range deck {
|
||||
func registerScreen(w *World) {
|
||||
screens := make([]screen, 0, len(screenDeck)+1)
|
||||
for _, f := range screenDeck {
|
||||
screens = append(screens, f())
|
||||
}
|
||||
for _, s := range append([]screen{selector(screens)}, screens...) {
|
||||
w.G.SceneManager.Register(build(w, s))
|
||||
for _, s := range append([]screen{screenSelector(screens)}, screens...) {
|
||||
w.G.SceneManager.Register(screenBuild(w, s))
|
||||
}
|
||||
}
|
||||
|
||||
// The default floor: the bottom of the scene region, above the HUD divider.
|
||||
// A screen with a painted floor of its own overrides it.
|
||||
var floor = []inkwell.Polygon{
|
||||
var screenFloor = []inkwell.Polygon{
|
||||
inkwell.Poly(
|
||||
inkwell.Point{X: 16, Y: 196}, inkwell.Point{X: 624, Y: 196},
|
||||
inkwell.Point{X: 624, Y: 258}, inkwell.Point{X: 16, Y: 258},
|
||||
),
|
||||
}
|
||||
|
||||
// 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
|
||||
// first one whose area contains the click (HotspotAt): a painted thing beats
|
||||
// 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
|
||||
if s.onSelector {
|
||||
exits = append(exits, toSelector())
|
||||
@@ -115,14 +113,14 @@ func build(w *world.World, s screen) inkwell.Scene {
|
||||
actors := s.actors
|
||||
if actors == nil {
|
||||
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
|
||||
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 {
|
||||
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
|
||||
// screen does not clobber what the game has set in the meantime.
|
||||
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 {
|
||||
ctx.Game.State.SetVar(name, v)
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
package inc
|
||||
|
||||
// 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
|
||||
@@ -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
|
||||
// is reached from here, and from nowhere else.
|
||||
func noodleHouse() screen {
|
||||
func screenNoodleHouse() screen {
|
||||
return screen{
|
||||
name: names.ScreenNoodleHouse,
|
||||
name: ScreenNoodleHouse,
|
||||
title: "NOODLE'S HOUSE — SEALED",
|
||||
background: names.BgNoodleHouse,
|
||||
background: BgNoodleHouse,
|
||||
onSelector: true,
|
||||
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
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
package inc
|
||||
|
||||
// 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
|
||||
@@ -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
|
||||
// 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.
|
||||
func normanApartment() screen {
|
||||
func screenNormanApartment() screen {
|
||||
return screen{
|
||||
name: names.ScreenNormanApartment,
|
||||
name: ScreenNormanApartment,
|
||||
title: "NORMAN'S APARTMENT",
|
||||
background: names.BgNormanApartment,
|
||||
background: BgNormanApartment,
|
||||
onSelector: true,
|
||||
exits: []exit{
|
||||
{to: names.ScreenRooftopHideout, label: "the stairs up to the roof", side: sideBack},
|
||||
{to: names.ScreenBBSTerminal, label: "Norman's terminal", side: sideRight},
|
||||
{to: ScreenRooftopHideout, label: "the stairs up to the roof", side: sideBack},
|
||||
{to: ScreenBBSTerminal, label: "Norman's terminal", side: sideRight},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
package inc
|
||||
|
||||
// paulShop is where the game starts: the junk shop and garage Paul lives
|
||||
// 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
|
||||
// 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.
|
||||
func paulShop() screen {
|
||||
func screenPaulShop() screen {
|
||||
return screen{
|
||||
name: names.ScreenPaulShop,
|
||||
name: ScreenPaulShop,
|
||||
title: "PAUL'S SHOP — JUNK AND GARAGE",
|
||||
background: names.BgPaulShop,
|
||||
background: BgPaulShop,
|
||||
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
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
package inc
|
||||
|
||||
// policeStation is where Noodle is held for dealing in cracked Armillas, and
|
||||
// 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
|
||||
// room and the interview window are hotspots inside this one, not screens of
|
||||
// their own.
|
||||
func policeStation() screen {
|
||||
func screenPoliceStation() screen {
|
||||
return screen{
|
||||
name: names.ScreenPoliceStation,
|
||||
name: ScreenPoliceStation,
|
||||
title: "SFPD — STATION AND HOLDING",
|
||||
background: names.BgPoliceStation,
|
||||
background: BgPoliceStation,
|
||||
onSelector: true,
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,14 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
package inc
|
||||
|
||||
// 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
|
||||
// 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).
|
||||
func publicBBS() screen {
|
||||
func screenPublicBBS() screen {
|
||||
return screen{
|
||||
name: names.ScreenPublicBBS,
|
||||
name: ScreenPublicBBS,
|
||||
title: "PUBLIC BBS TERMINAL",
|
||||
background: names.BgPublicBBS,
|
||||
background: BgPublicBBS,
|
||||
onSelector: true,
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
package inc
|
||||
|
||||
// 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
|
||||
@@ -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
|
||||
// puts a terminal.
|
||||
func rooftopHideout() screen {
|
||||
func screenRooftopHideout() screen {
|
||||
return screen{
|
||||
name: names.ScreenRooftopHideout,
|
||||
name: ScreenRooftopHideout,
|
||||
title: "NORMAN'S ROOFTOP HIDEOUT",
|
||||
background: names.BgRooftopHideout,
|
||||
background: BgRooftopHideout,
|
||||
exits: []exit{
|
||||
{to: names.ScreenNormanApartment, label: "back down into the flat", side: sideNear},
|
||||
{to: names.ScreenBBSTerminal, label: "the old terminal", side: sideRight},
|
||||
{to: ScreenNormanApartment, label: "back down into the flat", side: sideNear},
|
||||
{to: ScreenBBSTerminal, label: "the old terminal", side: sideRight},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,14 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
package inc
|
||||
|
||||
// samizdatPress is a 💡 suggestion: the Resistance's physical node — parody
|
||||
// BVK databases, underground publications, a P2P node humming in the cellar.
|
||||
// The samizdat intellectuals are the first to write down that the world is
|
||||
// slipping (wiki: entities#potenciális-további-helyszínek).
|
||||
func samizdatPress() screen {
|
||||
func screenSamizdatPress() screen {
|
||||
return screen{
|
||||
name: names.ScreenSamizdatPress,
|
||||
name: ScreenSamizdatPress,
|
||||
title: "SAMIZDAT PRINTING HOUSE",
|
||||
background: names.BgSamizdatPress,
|
||||
background: BgSamizdatPress,
|
||||
onSelector: true,
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
package inc
|
||||
|
||||
// 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
|
||||
// wants to live on" (wiki: entities#potenciális-további-helyszínek).
|
||||
func scrapMarket() screen {
|
||||
func screenScrapMarket() screen {
|
||||
return screen{
|
||||
name: names.ScreenScrapMarket,
|
||||
name: ScreenScrapMarket,
|
||||
title: "SCRAP MARKET",
|
||||
background: names.BgScrapMarket,
|
||||
background: BgScrapMarket,
|
||||
onSelector: true,
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
package inc
|
||||
|
||||
// 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
|
||||
@@ -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
|
||||
// running under the standard LECTOR surface. Reading it is where the mystery
|
||||
// tape finally speaks.
|
||||
func secretClub() screen {
|
||||
func screenSecretClub() screen {
|
||||
return screen{
|
||||
name: names.ScreenSecretClub,
|
||||
name: ScreenSecretClub,
|
||||
title: "SECRET CLUB — THE UNDERWATER SUN",
|
||||
background: names.BgSecretClub,
|
||||
background: BgSecretClub,
|
||||
exits: []exit{
|
||||
{to: names.ScreenBBSTerminal, label: "the terminal in the corner", side: sideBack},
|
||||
{to: names.ScreenTrinketShop, label: "back out through the shop", side: sideNear},
|
||||
{to: ScreenBBSTerminal, label: "the terminal in the corner", side: sideBack},
|
||||
{to: ScreenTrinketShop, label: "back out through the shop", side: sideNear},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
package inc
|
||||
|
||||
// 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
|
||||
@@ -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
|
||||
// it as one screen, so the door at the foot of the bridge, the observation room
|
||||
// and the glass wall are hotspots in here.
|
||||
func secretLab() screen {
|
||||
func screenSecretLab() screen {
|
||||
return screen{
|
||||
name: names.ScreenSecretLab,
|
||||
name: ScreenSecretLab,
|
||||
title: "SECRET RESEARCH LABORATORY",
|
||||
background: names.BgSecretLab,
|
||||
background: BgSecretLab,
|
||||
onSelector: true,
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
package screen
|
||||
package inc
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
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
|
||||
@@ -17,9 +15,9 @@ import (
|
||||
// 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
|
||||
// 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.
|
||||
func selector(rest []screen) screen {
|
||||
func screenSelector(rest []screen) screen {
|
||||
var exits []exit
|
||||
for _, s := range rest {
|
||||
if !s.onSelector {
|
||||
@@ -32,9 +30,9 @@ func selector(rest []screen) screen {
|
||||
})
|
||||
}
|
||||
return screen{
|
||||
name: names.ScreenSelector,
|
||||
name: ScreenSelector,
|
||||
title: "SAN FRANCISCO",
|
||||
background: names.BgSelector,
|
||||
background: BgSelector,
|
||||
exits: exits,
|
||||
// Nobody stands on a map.
|
||||
actors: []inkwell.SceneActor{},
|
||||
@@ -1,15 +1,13 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
package inc
|
||||
|
||||
// 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
|
||||
// from the hackerspace or from Noodle (wiki: entities, timeline 24).
|
||||
func serverFarm() screen {
|
||||
func screenServerFarm() screen {
|
||||
return screen{
|
||||
name: names.ScreenServerFarm,
|
||||
name: ScreenServerFarm,
|
||||
title: "SERVER FARM",
|
||||
background: names.BgServerFarm,
|
||||
background: BgServerFarm,
|
||||
onSelector: true,
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,14 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
package inc
|
||||
|
||||
// showroom is a 💡 suggestion: the legitimate, glittering side — Armilla
|
||||
// cabinets, licensed operators, matte-black Nokia-punk under showroom lights.
|
||||
// The contrast with Paul's edge of the world
|
||||
// (wiki: entities#potenciális-további-helyszínek).
|
||||
func showroom() screen {
|
||||
func screenShowroom() screen {
|
||||
return screen{
|
||||
name: names.ScreenShowroom,
|
||||
name: ScreenShowroom,
|
||||
title: "NEUMATRONIC SHOWROOM",
|
||||
background: names.BgShowroom,
|
||||
background: BgShowroom,
|
||||
onSelector: true,
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
package inc
|
||||
|
||||
// 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
|
||||
@@ -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
|
||||
// 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.
|
||||
func smallRestaurant() screen {
|
||||
func screenSmallRestaurant() screen {
|
||||
return screen{
|
||||
name: names.ScreenSmallRestaurant,
|
||||
name: ScreenSmallRestaurant,
|
||||
title: "SMALL RESTAURANT — NEXT DOOR",
|
||||
background: names.BgSmallRestaurant,
|
||||
background: BgSmallRestaurant,
|
||||
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
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
package inc
|
||||
|
||||
// street is the city itself: pavements, a square, a metro entrance, and the
|
||||
// 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
|
||||
// it (story#norman-lakása); that is a beat for this screen, not a screen of its
|
||||
// own.
|
||||
func street() screen {
|
||||
func screenStreet() screen {
|
||||
return screen{
|
||||
name: names.ScreenStreet,
|
||||
name: ScreenStreet,
|
||||
title: "STREET",
|
||||
background: names.BgStreet,
|
||||
background: BgStreet,
|
||||
onSelector: true,
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
package inc
|
||||
|
||||
// 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
|
||||
@@ -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
|
||||
// place next door, and Bolt →|beengedés| Klub, which stays shut until the
|
||||
// shopkeeper has been shown the underwater sun.
|
||||
func trinketShop() screen {
|
||||
func screenTrinketShop() screen {
|
||||
return screen{
|
||||
name: names.ScreenTrinketShop,
|
||||
name: ScreenTrinketShop,
|
||||
title: "CHINATOWN — TRINKET SHOP",
|
||||
background: names.BgTrinketShop,
|
||||
background: BgTrinketShop,
|
||||
onSelector: true,
|
||||
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,
|
||||
needs: names.FlagClubEntry,
|
||||
to: ScreenSecretClub, label: "the way in at the back", side: sideBack,
|
||||
needs: FlagClubEntry,
|
||||
blocked: "Just a shop, as far as the man behind the counter is concerned.",
|
||||
},
|
||||
},
|
||||
@@ -1,11 +1,7 @@
|
||||
package script
|
||||
package inc
|
||||
|
||||
import (
|
||||
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
|
||||
@@ -14,18 +10,18 @@ import (
|
||||
// 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
|
||||
// it long before the surrounding scene exists.
|
||||
func awakeningFinale(w *world.World) inkwell.Script {
|
||||
func scriptAwakeningFinale(w *World) inkwell.Script {
|
||||
return inkwell.Script{
|
||||
Name: names.ScriptFinale,
|
||||
Name: ScriptFinale,
|
||||
Actions: inkwell.Seq(
|
||||
world.SetMode(w, world.ModeCutscene),
|
||||
SetMode(w, ModeCutscene),
|
||||
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),
|
||||
world.UseTheme(theme.NokiaPunk),
|
||||
UseTheme(NokiaPunk),
|
||||
inkwell.Say("norman", "No — this isn't what you were supposed to do!"),
|
||||
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.ShowEnd("REAL WORLD — end of game two"),
|
||||
),
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
// 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
|
||||
// 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
|
||||
// Paul speak in bone white. If something is amber, a tape said it.
|
||||
package theme
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
@@ -147,8 +148,8 @@ func nokiaPunkTheme() inkwell.Theme {
|
||||
}
|
||||
}
|
||||
|
||||
// Register adds both themes to the game.
|
||||
func Register(g *inkwell.Game) {
|
||||
// registerTheme adds both themes to the game.
|
||||
func registerTheme(g *inkwell.Game) {
|
||||
g.ThemeManager.Register(realWorldTheme())
|
||||
g.ThemeManager.Register(nokiaPunkTheme())
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package ui
|
||||
package inc
|
||||
|
||||
// Letterbox — the cutscene frame.
|
||||
//
|
||||
@@ -12,22 +12,18 @@ import (
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||
"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 {
|
||||
Name string
|
||||
W *world.World
|
||||
W *World
|
||||
Bar float64 // height of the black bar, top and bottom
|
||||
}
|
||||
|
||||
func (l *Letterbox) GetName() string { return l.Name }
|
||||
|
||||
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
|
||||
}
|
||||
// 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) {
|
||||
if l.W.Mode() != world.ModeCutscene {
|
||||
if l.W.Mode() != ModeCutscene {
|
||||
return
|
||||
}
|
||||
g := ctx.Game
|
||||
@@ -47,12 +43,12 @@ func (l *Letterbox) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) {
|
||||
bar = 18
|
||||
}
|
||||
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, h-float32(bar), w, float32(bar), black, false)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package ui
|
||||
package inc
|
||||
|
||||
// HUD layout.
|
||||
//
|
||||
@@ -37,9 +37,6 @@ import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"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.
|
||||
@@ -75,16 +72,16 @@ const (
|
||||
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.
|
||||
func Register(w *world.World) {
|
||||
func registerUI(w *World) {
|
||||
g := w.G
|
||||
|
||||
// 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
|
||||
// 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(&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(&windowSizer{Name: "window", Scale: 2})
|
||||
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
|
||||
// it for the Armilla's single-line phosphor strip: in canon the device
|
||||
// display is one line of text, not a phone screen.
|
||||
TimeVar: names.VarArmillaStrip,
|
||||
TimeVar: VarArmillaStrip,
|
||||
}
|
||||
w.SetTopBar(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.
|
||||
type gate struct {
|
||||
Name string
|
||||
W *world.World
|
||||
W *World
|
||||
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}
|
||||
}
|
||||
|
||||
@@ -212,7 +209,7 @@ func (n *gate) BlocksClickAt(p inkwell.Point) bool {
|
||||
// two channels always have a visible, continuous separator between them.
|
||||
type hudFrame struct {
|
||||
Name string
|
||||
W *world.World
|
||||
W *World
|
||||
}
|
||||
|
||||
func (h *hudFrame) GetName() string { return h.Name }
|
||||
@@ -1,9 +1,9 @@
|
||||
package ui
|
||||
package inc
|
||||
|
||||
// screenNav — walking the deck with the arrow keys.
|
||||
//
|
||||
// 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
|
||||
// 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
|
||||
@@ -17,13 +17,11 @@ import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/world"
|
||||
)
|
||||
|
||||
type screenNav struct {
|
||||
Name string
|
||||
W *world.World
|
||||
W *World
|
||||
}
|
||||
|
||||
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.
|
||||
// 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 {
|
||||
deck := g.SceneManager.Names()
|
||||
if len(deck) < 2 {
|
||||
@@ -1,11 +1,9 @@
|
||||
package ui
|
||||
package inc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
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 —
|
||||
@@ -16,9 +14,9 @@ func TestScreenNavWrapsBothWays(t *testing.T) {
|
||||
for _, n := range []string{"a", "b", "c"} {
|
||||
g.SceneManager.Register(inkwell.Scene{Name: n, Background: "bg"})
|
||||
}
|
||||
w := world.New(g)
|
||||
w := newWorld(g)
|
||||
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 {
|
||||
from string
|
||||
@@ -1,4 +1,4 @@
|
||||
package ui
|
||||
package inc
|
||||
|
||||
// TapeChannel — the tape channel down the right-hand side.
|
||||
//
|
||||
@@ -19,14 +19,11 @@ import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
"git.teletypegames.org/games/realworld/internal/world"
|
||||
)
|
||||
|
||||
type TapeChannel struct {
|
||||
Name string
|
||||
W *world.World
|
||||
W *World
|
||||
Bounds inkwell.Rectangle
|
||||
}
|
||||
|
||||
@@ -76,7 +73,7 @@ func (t *TapeChannel) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) {
|
||||
// joke book readable.
|
||||
col, txt = th.ChatLogPrompt, m.Text
|
||||
case inkwell.LogResponse:
|
||||
if !names.IsTape(m.Speaker) {
|
||||
if !IsTape(m.Speaker) {
|
||||
continue // Paul and the NPCs speak in the scene, not here
|
||||
}
|
||||
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()
|
||||
for i := len(msgs) - 1; i >= 0; i-- {
|
||||
m := msgs[i]
|
||||
if m.Kind == inkwell.LogResponse && names.IsTape(m.Speaker) {
|
||||
return names.TapeDisplayName(m.Speaker), speechColor(g, m.Speaker, th.ChatLogResponse)
|
||||
if m.Kind == inkwell.LogResponse && IsTape(m.Speaker) {
|
||||
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.
|
||||
@@ -1,4 +1,4 @@
|
||||
package ui
|
||||
package inc
|
||||
|
||||
// TapeSlots — the two slots of Paul's Armilla.
|
||||
//
|
||||
@@ -19,14 +19,11 @@ import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
"git.teletypegames.org/games/realworld/internal/world"
|
||||
)
|
||||
|
||||
type TapeSlots struct {
|
||||
Name string
|
||||
W *world.World
|
||||
W *World
|
||||
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.
|
||||
switch {
|
||||
case r1.Contains(mp):
|
||||
g.SetHoverLabel(names.TapeDisplayName(names.Dex))
|
||||
g.SetHoverLabel(TapeDisplayName(Dex))
|
||||
case r2.Contains(mp):
|
||||
if s := t.W.Slot2(); s != "" {
|
||||
g.SetHoverLabel(itemLabel(g, s))
|
||||
@@ -77,41 +74,41 @@ func (t *TapeSlots) Tick(ctx *inkwell.UICtx) {
|
||||
// Loading a tape into slot 2.
|
||||
if slot2 && sel != "" {
|
||||
g.Inventory.Select("")
|
||||
if names.IsTapeItem(sel) {
|
||||
if IsTapeItem(sel) {
|
||||
t.W.SetPending(sel)
|
||||
t.W.Do(inkwell.RunScript(names.ScriptTapeInsert))
|
||||
t.W.Do(inkwell.RunScript(ScriptTapeInsert))
|
||||
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
|
||||
}
|
||||
if !slot2 && sel != "" {
|
||||
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
|
||||
}
|
||||
|
||||
switch g.SelectedVerb() {
|
||||
case "talk":
|
||||
if !slot2 {
|
||||
t.W.Do(world.Paused(t.W, inkwell.RunDialogue(names.DlgDex)))
|
||||
t.W.Do(Paused(t.W, inkwell.RunDialogue(DlgDex)))
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
drawSlot(r1, "SLOT 1", names.TapeDisplayName(names.Dex), true)
|
||||
drawSlot(r1, "SLOT 1", TapeDisplayName(Dex), true)
|
||||
if s := t.W.Slot2(); s != "" {
|
||||
drawSlot(r2, "SLOT 2", itemLabel(g, s), true)
|
||||
} else {
|
||||
@@ -1,8 +1,6 @@
|
||||
// Package ui is the game's HUD: the layout, the custom widgets and coloured
|
||||
// text rendering.
|
||||
package ui
|
||||
package inc
|
||||
|
||||
// Coloured text rendering.
|
||||
// The HUD's coloured text rendering.
|
||||
//
|
||||
// inkwell's drawText (asset.text.go) discards its colour argument (`_ = c`) and
|
||||
// always renders white through the ebitenutil debug font. The two-channel
|
||||
@@ -1,4 +1,4 @@
|
||||
package ui
|
||||
package inc
|
||||
|
||||
import "testing"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package ui
|
||||
package inc
|
||||
|
||||
// UseWithGuard — enforcing authored failure.
|
||||
//
|
||||
@@ -22,14 +22,11 @@ import (
|
||||
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
"git.teletypegames.org/games/realworld/internal/world"
|
||||
)
|
||||
|
||||
type UseWithGuard struct {
|
||||
Name string
|
||||
W *world.World
|
||||
W *World
|
||||
}
|
||||
|
||||
func (u *UseWithGuard) GetName() string { return u.Name }
|
||||
@@ -64,8 +61,8 @@ func (u *UseWithGuard) Tick(ctx *inkwell.UICtx) {
|
||||
n := g.State.Talked(key)
|
||||
|
||||
u.W.Do(inkwell.Seq(
|
||||
inkwell.Say(names.Paul, pick(paulFails, n)),
|
||||
world.TapeSay(names.Dex, dexFail(n)),
|
||||
inkwell.Say(Paul, pick(paulFails, n)),
|
||||
TapeSay(Dex, dexFail(n)),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package world
|
||||
package inc
|
||||
|
||||
// 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
|
||||
// actions and the action pump.
|
||||
//
|
||||
// This package knows nothing about the HUD or the content, so ui and content
|
||||
// can both build on it without creating an import cycle.
|
||||
package world
|
||||
// The world knows nothing about the HUD or the content: the ui and content
|
||||
// categories both build on it, never the other way round.
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// Mode is the HUD mode.
|
||||
@@ -48,7 +47,7 @@ type World struct {
|
||||
}
|
||||
|
||||
// New wraps an existing inkwell game.
|
||||
func New(g *inkwell.Game) *World {
|
||||
func newWorld(g *inkwell.Game) *World {
|
||||
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
|
||||
// saved.
|
||||
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 ""
|
||||
}
|
||||
|
||||
// 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
|
||||
// performs the actual load; the widget only marks.
|
||||
@@ -1,42 +1,39 @@
|
||||
package world_test
|
||||
package inc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
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 in-flight script, so a save taken mid-cutscene would otherwise reload
|
||||
// into a letterboxed, HUD-less game with nothing running.
|
||||
func TestModeIsNotSavedState(t *testing.T) {
|
||||
w := world.New(inkwell.NewGame("t", 320, 200))
|
||||
w := newWorld(inkwell.NewGame("t", 320, 200))
|
||||
|
||||
w.SetMode(world.ModeCutscene)
|
||||
for _, k := range []string{"ui.mode", "mode"} {
|
||||
w.SetMode(ModeCutscene)
|
||||
for _, k := range []string{"mode", "mode"} {
|
||||
if v := w.G.State.Var(k); v != nil {
|
||||
t.Errorf("mode leaked into State.Vars (%q = %v)", k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// Slot 2, by contrast, is real game state and must be saved.
|
||||
w.SetSlot2(names.ItemMysteryTape)
|
||||
if w.G.State.Var(names.VarSlot2) != names.ItemMysteryTape {
|
||||
w.SetSlot2(ItemMysteryTape)
|
||||
if w.G.State.Var(VarSlot2) != ItemMysteryTape {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// The widget marks a tape as pending; the script consumes it — once.
|
||||
func TestPendingIsConsumedOnce(t *testing.T) {
|
||||
w := world.New(inkwell.NewGame("t", 320, 200))
|
||||
w.SetPending(names.ItemMysteryTape)
|
||||
if got := w.TakePending(); got != names.ItemMysteryTape {
|
||||
w := newWorld(inkwell.NewGame("t", 320, 200))
|
||||
w.SetPending(ItemMysteryTape)
|
||||
if got := w.TakePending(); got != ItemMysteryTape {
|
||||
t.Fatalf("TakePending() = %q", got)
|
||||
}
|
||||
if got := w.TakePending(); got != "" {
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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."),
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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."),
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -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."),
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -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."),
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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."),
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
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
|
||||
@@ -21,13 +21,13 @@ var assets embed.FS
|
||||
func init() { inkwell.AssetFS = assets }
|
||||
|
||||
func main() {
|
||||
var o boot.Opts
|
||||
var o inc.Opts
|
||||
flag.StringVar(&o.Screen, "screen", "", "starting screen (empty: the alley)")
|
||||
flag.BoolVar(&o.Finale, "finale", false,
|
||||
"start with the finale, to try the Nokia-punk theme switch")
|
||||
flag.Parse()
|
||||
|
||||
if err := inkwell.Run(boot.New(o)); err != nil {
|
||||
if err := inkwell.Run(inc.New(o)); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user