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