@@ -23,11 +23,19 @@ go run .
|
||||
```
|
||||
|
||||
```bash
|
||||
go run . # the alley scene (beat 3)
|
||||
go run . -finale # the finale — try the Nokia-punk theme switch
|
||||
go run . -scene alley # pick a starting scene
|
||||
go run . # the alley (beat 3)
|
||||
go run . -finale # the finale — try the Nokia-punk theme switch
|
||||
go run . -screen selector # the location selector: the map the city hangs off
|
||||
go run . -screen hospital # any screen in the deck, e.g. to look at the art
|
||||
```
|
||||
|
||||
Screen names are the constants in `internal/names` (`selector`, `paul_shop`,
|
||||
`noodle_house`, `alley`, `norman_apartment`, `police_station`, `hackerspace`,
|
||||
`ice_cream_shop`, `trinket_shop`, `small_restaurant`, `secret_club`,
|
||||
`bbs_terminal`, `street`, `hospital`, `server_farm`, `secret_lab`,
|
||||
`rooftop_hideout`, `public_bbs`, `bvk_branch`, `curator_shop`, `scrap_market`,
|
||||
`samizdat_press`, `showroom`, `columbarium`).
|
||||
|
||||
To work against a local inkwell checkout, add a `replace` line temporarily —
|
||||
and take it out before committing:
|
||||
|
||||
@@ -55,41 +63,58 @@ Release metadata lives in `metadata.json`.
|
||||
|---|---|
|
||||
| left click | run the selected verb |
|
||||
| right click | verb coin (Look / Use / Talk / Take) |
|
||||
| `←` `→` | previous / next screen, in concept-art order, wrapping |
|
||||
| `F1` | toggle hotspot outlines |
|
||||
| `SPACE` | during a cutscene: let the tape speak, if it offered |
|
||||
|
||||
The screens are connected to each other (see **The map** below); the arrow keys
|
||||
are a reviewing tool on top of that, walking the deck in concept-art order. The
|
||||
walk is inert during a cutscene, a menu, or a stopped world, so it can never cut
|
||||
across an authored beat.
|
||||
|
||||
Screenshots: `EBITEN_SCREENSHOT_KEY=q go run .`, then press `q` in the window.
|
||||
|
||||
## Layout
|
||||
|
||||
Internal resolution is **640×400** — the same 16:10 as the wiki's "320×200 VGA
|
||||
look" brief and exactly 2× it, so art drawn at 320×200 upscales cleanly.
|
||||
Internal resolution is **640×380**: the size of one painted background.
|
||||
Integer scaling only; the window opens at 2×.
|
||||
|
||||
The doubling is forced by type, not taste. inkwell renders text through a fixed
|
||||
**6×16** debug font that the domain cannot replace. At 320×200 a 16px glyph is
|
||||
8% of the screen height: rows overlap, and a top bar sized from the wireframe
|
||||
deck's proportions cannot hold a single line. At 640×400 the same glyph lands
|
||||
at 4%, which is the proportion the design was drawn for.
|
||||
That size is not a taste call. inkwell scales a scene background over the
|
||||
*whole* screen (`core.engine.go`), not over the region the HUD leaves free — so
|
||||
at any screen size other than the art's own, every painting is squashed to fit.
|
||||
The deck is drawn at 640×380, so the screen is 640×380 and the art lands pixel
|
||||
for pixel. `TestSceneBackgroundsResolve` keeps the two in step: a background of
|
||||
any other size fails the build.
|
||||
|
||||
It still gives the wiki's "320×200 VGA look" brief what it was after — a low,
|
||||
wide VGA frame at an exact 2× of a 320-wide canvas — and it still keeps the
|
||||
engine's fixed **6×16** debug font readable, at 4% of the screen height. (At
|
||||
320×190 it would be 8%: rows overlap and the top bar cannot hold a line.)
|
||||
|
||||
The HUD is therefore an opaque strip laid over the foot of the painting, and it
|
||||
is measured from the bottom and kept as short as its contents allow — a status
|
||||
row, then the two rows of inventory slots, which are the tallest thing in it.
|
||||
Every pixel it gives back is a pixel of art.
|
||||
|
||||
```
|
||||
0 394│396 639
|
||||
┌────────────────────────────────────────────────────────────────────┐
|
||||
0 │ ALLEY — paused SRP: 1 tape │ TopBar (20)
|
||||
20 ├────────────────────────────────────────────────────────────────────┤
|
||||
│ scene — hotspots, characters, SpeechBubble │ Scene (244)
|
||||
264├─────────────────────────────────────────┬──────────────────────────┤
|
||||
│ scene — hotspots, characters, SpeechBubble │ Scene (242)
|
||||
262├─────────────────────────────────────────┬──────────────────────────┤
|
||||
│ Use hook on: floor grate │ DEX │
|
||||
│ ┌───┬───┬───┬───┐ ┌───────┐┌────────┐ │ "The hook is a hand │ HUD (136)
|
||||
│ ┌───┬───┬───┬───┐ ┌───────┐┌────────┐ │ "The hook is a hand │ HUD (118)
|
||||
│ └───┴───┴───┴───┘ │ 1 DEX ││ 2 — │ │ shorter than the gap." │
|
||||
399└─────────────────────────────────────────┴──────────────────────────┘
|
||||
379└─────────────────────────────────────────┴──────────────────────────┘
|
||||
InventoryBar TapeSlots TapeChannel
|
||||
```
|
||||
|
||||
Every vertical measurement derives from `ui.LineH` (glyph cell + leading), not
|
||||
from the deck's ratios, so the layout cannot drift out of step with the font
|
||||
again — `TestLayoutFitsTheFont` guards it. The tape channel comes out at 5 rows
|
||||
of 38 columns, which is about two of Dex's remarks on screen at once.
|
||||
Every vertical measurement derives from `ui.LineH` (glyph cell + leading) and
|
||||
from `ui.ScreenH`, not from the wireframe deck's ratios, so the layout cannot
|
||||
drift out of step with the font — or with the art — again;
|
||||
`TestLayoutFitsTheFont` guards it. The tape channel comes out at 4 rows of 38
|
||||
columns, which is about one and a half of Dex's remarks on screen at once.
|
||||
|
||||
The vertical divider at x=394 is the one proportion carried over from the
|
||||
wireframe deck: 61.6% world, 38.4% tape channel. The deck's *vertical*
|
||||
@@ -149,16 +174,56 @@ Content is one registered entity per file, grouped by kind:
|
||||
|
||||
```
|
||||
internal/content/
|
||||
asset/ alley_background.go
|
||||
background/ one file per screen: paul_shop.go, noodle_house.go, …
|
||||
character/ paul.go dex.go mystery_tape.go support_tape.go
|
||||
item/ noodle_letter.go black_market_armilla.go mystery_tape.go
|
||||
dialog/ dex_talk.go mystery_tape_silent.go
|
||||
script/ tape_insert.go awakening_finale.go
|
||||
scene/ alley.go
|
||||
screen/ one file per screen: alley.go, trinket_shop.go, … + exit.go
|
||||
```
|
||||
|
||||
Adding a scene means adding `scene/<name>.go` and one line in `scene/scene.go`.
|
||||
Nothing else moves.
|
||||
Adding a screen means adding `screen/<name>.go` and `background/<name>.go`, and
|
||||
one line in each package's list. Nothing else moves.
|
||||
|
||||
**On the word "screen".** inkwell's entity is called a `Scene`, and that is the
|
||||
type every file in `screen/` returns — but the wiki, the concept-art deck and
|
||||
the beat tables all count *screens*, so the package, the files and the flag say
|
||||
screen. "Scene" survives only where the code is talking to the engine
|
||||
(`world.EnterScene`, `SceneManager`).
|
||||
|
||||
Art lives in `assets/bg/` and is embedded into the binary (`main.go`), because
|
||||
js/wasm has no OS filesystem and `make binaries` packages the executable alone.
|
||||
|
||||
## The map
|
||||
|
||||
Where a screen leads is recorded **in that screen**, in its own file, and it
|
||||
comes from the wiki's location graph
|
||||
(`projects/realworld/story#helyszín-gráf-vázlat`). That graph has two kinds of
|
||||
edge and so does the code:
|
||||
|
||||
- **Physical adjacency** — the alley is behind Noodle's house, the eating place
|
||||
is next door to the trinket shop, the club is through its back room, the roof
|
||||
is up Norman's stairs. Declared with `to:` in the screen's `exits`.
|
||||
- **The selector** — the wiki centres its map on a *Helyszínválasztó*, "nem
|
||||
valódi helyszín, hanem a menü-képernyő": a screen every main location connects
|
||||
to both ways. A screen marks itself `onSelector: true`; `screen/selector.go`
|
||||
builds the other half of each of those edges, so the list of locations on the
|
||||
map is never written down twice.
|
||||
|
||||
The wiki's conditional arrows survive too: `needs:` holds the flag an exit waits
|
||||
for, which is how *Bolt →|beengedés| Klub* is expressed — the way into the club
|
||||
is visible and named from the first visit, and stays shut until the shopkeeper
|
||||
has been shown the underwater sun.
|
||||
|
||||
Until the doors are measured on the paintings, an exit is a strip along one edge
|
||||
of the picture (`sideLeft`, `sideRight`, `sideBack`, `sideNear`) — the
|
||||
convention every 1990s point & click used, and one field to re-aim later. A
|
||||
screen reached from several rooms has no fixed way out: the BBS terminal is the
|
||||
same terminal from the club, the flat or the roof, so its exit is `back`.
|
||||
|
||||
Three tests keep the map honest: every exit names a registered screen, every
|
||||
screen has a way out, and every screen can be walked to from Paul's shop through
|
||||
the exits alone — the arrow keys do not count.
|
||||
|
||||
## Engine workarounds
|
||||
|
||||
@@ -205,7 +270,19 @@ Also: the inkwell README gives the module path as
|
||||
|
||||
## Known gaps
|
||||
|
||||
- **No art.** Everything is an inkwell placeholder.
|
||||
- **The HUD still covers the foot of every painting.** The art is no longer
|
||||
stretched, but the strip is opaque and 118px of the 380 sit on top of the
|
||||
picture. Either the deck gets recut so nothing that matters lives in its
|
||||
bottom third, or the HUD gets a translucent panel treatment.
|
||||
- **The screens are empty.** 22 backgrounds are in and they are wired to each
|
||||
other, but only the alley has hotspots, NPCs and dialogue; everywhere else
|
||||
there is nothing to do but leave again.
|
||||
- **The selector has no art.** The wiki's Helyszínválasztó is the centre of the
|
||||
map and the deck does not include it, so it renders as a placeholder with one
|
||||
labelled pin per location on a plain grid.
|
||||
- **Two screens are missing from the deck.** The alley (the one authored beat)
|
||||
and deck 05, Norman's workplace — which is in the wiki's catalogue but has no
|
||||
screen file yet, so the workplace thread of the story has nowhere to happen.
|
||||
- **Spent dialogue choices are hidden, not struck through.** `DialogueChoice.Once`
|
||||
hides them; the deck wanted them struck through but visible, so the list
|
||||
becomes a memory of what you already tried. Needs a thin override over
|
||||
|
||||
|
After Width: | Height: | Size: 341 KiB |
|
After Width: | Height: | Size: 399 KiB |
|
After Width: | Height: | Size: 423 KiB |
|
After Width: | Height: | Size: 445 KiB |
|
After Width: | Height: | Size: 391 KiB |
|
After Width: | Height: | Size: 293 KiB |
|
After Width: | Height: | Size: 341 KiB |
|
After Width: | Height: | Size: 367 KiB |
|
After Width: | Height: | Size: 420 KiB |
|
After Width: | Height: | Size: 416 KiB |
|
After Width: | Height: | Size: 309 KiB |
|
After Width: | Height: | Size: 329 KiB |
|
After Width: | Height: | Size: 349 KiB |
|
After Width: | Height: | Size: 341 KiB |
|
After Width: | Height: | Size: 422 KiB |
|
After Width: | Height: | Size: 313 KiB |
|
After Width: | Height: | Size: 352 KiB |
|
After Width: | Height: | Size: 366 KiB |
|
After Width: | Height: | Size: 318 KiB |
|
After Width: | Height: | Size: 295 KiB |
|
After Width: | Height: | Size: 349 KiB |
|
After Width: | Height: | Size: 408 KiB |
@@ -14,15 +14,15 @@ import (
|
||||
|
||||
// Opts are the run parameters.
|
||||
type Opts struct {
|
||||
Scene string // starting scene; empty means the alley
|
||||
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 {
|
||||
scene := o.Scene
|
||||
if scene == "" {
|
||||
scene = names.SceneAlley
|
||||
screen := o.Screen
|
||||
if screen == "" {
|
||||
screen = names.ScreenAlley
|
||||
}
|
||||
|
||||
g := inkwell.NewGame("Real World", ui.ScreenW, ui.ScreenH)
|
||||
@@ -35,16 +35,16 @@ func New(o Opts) *inkwell.Game {
|
||||
g.UseTheme(theme.RealWorld)
|
||||
ui.Register(w)
|
||||
|
||||
g.StartAt(scene)
|
||||
g.OnStart(inkwell.Seq(opening(w, scene, o.Finale)...))
|
||||
g.StartAt(screen)
|
||||
g.OnStart(inkwell.Seq(opening(w, screen, o.Finale)...))
|
||||
return g
|
||||
}
|
||||
|
||||
func opening(w *world.World, scene string, finale bool) []inkwell.Action {
|
||||
func opening(w *world.World, screen string, finale bool) []inkwell.Action {
|
||||
acts := []inkwell.Action{
|
||||
world.EnterScene(w, scene),
|
||||
// Beat 2 (the police station) will set the tip flag. Until that scene
|
||||
// exists we set it here so the alley slice is playable end to end —
|
||||
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."),
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package boot_test
|
||||
|
||||
import (
|
||||
"image/png"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
@@ -136,3 +140,121 @@ func TestWidgetOrder(t *testing.T) {
|
||||
t.Errorf("cursor is not registered last: %v", n)
|
||||
}
|
||||
}
|
||||
|
||||
// Every screen needs a background, the drawn ones need the file to be where the
|
||||
// asset says it is, and the file has to be exactly screen-sized.
|
||||
//
|
||||
// The path check: a renamed or mistyped path shows up in the running game only
|
||||
// as an inkwell placeholder, which is exactly what a still-greyboxed screen
|
||||
// looks like — invisible as a bug.
|
||||
//
|
||||
// The size check: the engine scales a background over the WHOLE screen, so a
|
||||
// painting that is not ScreenW×ScreenH is silently squashed to fit. That is why
|
||||
// the screen is 640×380 in the first place; this keeps the two in step.
|
||||
func TestSceneBackgroundsResolve(t *testing.T) {
|
||||
g := build(t)
|
||||
|
||||
// Not drawn yet: these render as placeholders on purpose. The alley is
|
||||
// missing from the concept-art deck; the selector is the wiki's menu
|
||||
// screen, which the deck does not cover at all.
|
||||
undrawn := map[string]bool{names.BgAlley: true, names.BgSelector: true}
|
||||
|
||||
for _, n := range g.SceneManager.Names() {
|
||||
s := g.SceneManager.MustGet(n)
|
||||
a, ok := g.AssetManager.Get(s.Background)
|
||||
if !ok {
|
||||
t.Errorf("scene %q: unknown background %q", n, s.Background)
|
||||
continue
|
||||
}
|
||||
if undrawn[a.Name] {
|
||||
continue
|
||||
}
|
||||
f, err := os.Open(filepath.Join("..", "..", a.Path))
|
||||
if err != nil {
|
||||
t.Errorf("scene %q: background file missing: %v", n, err)
|
||||
continue
|
||||
}
|
||||
cfg, err := png.DecodeConfig(f)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
t.Errorf("scene %q: unreadable background: %v", n, err)
|
||||
continue
|
||||
}
|
||||
if cfg.Width != ui.ScreenW || cfg.Height != ui.ScreenH {
|
||||
t.Errorf("scene %q: background is %d×%d, want %d×%d — it will be stretched",
|
||||
n, cfg.Width, cfg.Height, ui.ScreenW, ui.ScreenH)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// exits reads the connection graph back out of the registered screens. The
|
||||
// exit hotspots are named "exit:<target>", which is what keeps the graph
|
||||
// machine-readable — see content/screen/exit.go.
|
||||
func exits(g *inkwell.Game, screen string) []string {
|
||||
var out []string
|
||||
for _, h := range g.SceneManager.MustGet(screen).Hotspots {
|
||||
if to, ok := strings.CutPrefix(h.Name, "exit:"); ok && to != "back" {
|
||||
out = append(out, to)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// An exit that names a screen nobody registered is a dead end the player walks
|
||||
// into: the engine logs "changeScene: unknown" and nothing happens on screen.
|
||||
func TestExitsResolve(t *testing.T) {
|
||||
g := build(t)
|
||||
for _, n := range g.SceneManager.Names() {
|
||||
for _, to := range exits(g, n) {
|
||||
if !g.SceneManager.Has(to) {
|
||||
t.Errorf("screen %q: exit to unknown screen %q", n, to)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Every screen has to be walkable to from the start of the game, through the
|
||||
// exits alone — the arrow keys are a browsing tool, not the map. A screen that
|
||||
// only the arrow keys can reach is content no player would ever see.
|
||||
//
|
||||
// The walk starts at Paul's shop, where the letter arrives, and mostly runs
|
||||
// through the selector: that is what the wiki's location graph is centred on.
|
||||
func TestEveryScreenIsReachable(t *testing.T) {
|
||||
g := build(t)
|
||||
|
||||
seen := map[string]bool{names.ScreenPaulShop: true}
|
||||
queue := []string{names.ScreenPaulShop}
|
||||
for len(queue) > 0 {
|
||||
at := queue[0]
|
||||
queue = queue[1:]
|
||||
for _, to := range exits(g, at) {
|
||||
if !seen[to] && g.SceneManager.Has(to) {
|
||||
seen[to] = true
|
||||
queue = append(queue, to)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, n := range g.SceneManager.Names() {
|
||||
if !seen[n] {
|
||||
t.Errorf("screen %q cannot be reached from %q by its exits", n, names.ScreenPaulShop)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A screen with no way out at all is a trap: the player walks in and the only
|
||||
// way on is the debug walk.
|
||||
func TestEveryScreenHasAWayOut(t *testing.T) {
|
||||
g := build(t)
|
||||
for _, n := range g.SceneManager.Names() {
|
||||
var ways int
|
||||
for _, h := range g.SceneManager.MustGet(n).Hotspots {
|
||||
if strings.HasPrefix(h.Name, "exit:") {
|
||||
ways++
|
||||
}
|
||||
}
|
||||
if ways == 0 {
|
||||
t.Errorf("screen %q has no exit", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
package asset
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// alleyBackground is the alley behind Noodle's house (beat 3).
|
||||
func alleyBackground() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.AssetAlleyBG,
|
||||
Path: "assets/bg/alley.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// Package asset registers the game's assets.
|
||||
//
|
||||
// During the greybox phase no asset file exists on disk: inkwell draws a
|
||||
// placeholder when a sprite is missing, which is exactly the wireframe deck's
|
||||
// dashed-outline convention, for free. Only the registration has to exist —
|
||||
// Game.Validate requires every scene background to name a registered asset.
|
||||
package asset
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/world"
|
||||
)
|
||||
|
||||
// Register adds every asset to the game.
|
||||
func Register(w *world.World) {
|
||||
for _, a := range []inkwell.Asset{
|
||||
alleyBackground(),
|
||||
} {
|
||||
w.G.AssetManager.Register(a)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package background
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// alley — part of the catalogue's "Noodle háza & a sikátor" row — not in the deck.
|
||||
//
|
||||
// Not painted yet: the file is missing on purpose, and inkwell draws its
|
||||
// placeholder instead — which is exactly the greybox convention.
|
||||
func alley() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgAlley,
|
||||
Path: "assets/bg/alley.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package background
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// bbsTerminal — deck 11.
|
||||
func bbsTerminal() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgBBSTerminal,
|
||||
Path: "assets/bg/bbs_terminal.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package background
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// bvkBranch — deck 18.
|
||||
func bvkBranch() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgBVKBranch,
|
||||
Path: "assets/bg/bvk_branch.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package background
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// columbarium — deck 23.
|
||||
func columbarium() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgColumbarium,
|
||||
Path: "assets/bg/columbarium.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package background
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// curatorShop — deck 19.
|
||||
func curatorShop() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgCuratorShop,
|
||||
Path: "assets/bg/curator_shop.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package background
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// hackerspace — deck 06.
|
||||
func hackerspace() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgHackerspace,
|
||||
Path: "assets/bg/hackerspace.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package background
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// hospital — deck 13.
|
||||
func hospital() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgHospital,
|
||||
Path: "assets/bg/hospital.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package background
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// iceCreamShop — deck 07.
|
||||
func iceCreamShop() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgIceCreamShop,
|
||||
Path: "assets/bg/ice_cream_shop.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package background
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// noodleHouse — deck 02.
|
||||
func noodleHouse() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgNoodleHouse,
|
||||
Path: "assets/bg/noodle_house.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package background
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// normanApartment — deck 03.
|
||||
func normanApartment() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgNormanApartment,
|
||||
Path: "assets/bg/norman_apartment.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package background
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// paulShop — deck 01.
|
||||
func paulShop() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgPaulShop,
|
||||
Path: "assets/bg/paul_shop.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package background
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// policeStation — deck 04.
|
||||
func policeStation() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgPoliceStation,
|
||||
Path: "assets/bg/police_station.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package background
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// publicBbs — deck 17.
|
||||
func publicBbs() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgPublicBBS,
|
||||
Path: "assets/bg/public_bbs.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package background
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// rooftopHideout — deck 16.
|
||||
func rooftopHideout() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgRooftopHideout,
|
||||
Path: "assets/bg/rooftop_hideout.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package background
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// samizdatPress — deck 21.
|
||||
func samizdatPress() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgSamizdatPress,
|
||||
Path: "assets/bg/samizdat_press.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package background
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// scrapMarket — deck 20.
|
||||
func scrapMarket() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgScrapMarket,
|
||||
Path: "assets/bg/scrap_market.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package background
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// secretClub — deck 10.
|
||||
func secretClub() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgSecretClub,
|
||||
Path: "assets/bg/secret_club.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package background
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// secretLab — deck 15.
|
||||
func secretLab() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgSecretLab,
|
||||
Path: "assets/bg/secret_lab.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package background
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// selector — the wiki's Helyszínválasztó — no deck number, and not drawn.
|
||||
//
|
||||
// Not painted yet: the file is missing on purpose, and inkwell draws its
|
||||
// placeholder instead — which is exactly the greybox convention.
|
||||
func selector() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgSelector,
|
||||
Path: "assets/bg/selector.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package background
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// serverFarm — deck 14.
|
||||
func serverFarm() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgServerFarm,
|
||||
Path: "assets/bg/server_farm.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package background
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// showroom — deck 22.
|
||||
func showroom() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgShowroom,
|
||||
Path: "assets/bg/showroom.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package background
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// smallRestaurant — deck 09.
|
||||
func smallRestaurant() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgSmallRestaurant,
|
||||
Path: "assets/bg/small_restaurant.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package background
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// street — deck 12.
|
||||
func street() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgStreet,
|
||||
Path: "assets/bg/street.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package background
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// trinketShop — deck 08.
|
||||
func trinketShop() inkwell.Asset {
|
||||
return inkwell.Asset{
|
||||
Name: names.BgTrinketShop,
|
||||
Path: "assets/bg/trinket_shop.png",
|
||||
Kind: inkwell.AssetImage,
|
||||
}
|
||||
}
|
||||
@@ -2,27 +2,27 @@
|
||||
// assets, characters, items, dialogues, scripts and scenes.
|
||||
//
|
||||
// Each registered entity lives in its own file, under a subpackage named after
|
||||
// its kind. Adding a scene means adding scene/<name>.go and one line in
|
||||
// scene/scene.go — nothing else moves.
|
||||
// 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/asset"
|
||||
"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/scene"
|
||||
"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 scenes
|
||||
// reference assets and characters, which Game.Validate cross-checks.
|
||||
// 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) {
|
||||
asset.Register(w)
|
||||
background.Register(w)
|
||||
character.Register(w)
|
||||
item.Register(w)
|
||||
dialog.Register(w)
|
||||
script.Register(w)
|
||||
scene.Register(w)
|
||||
screen.Register(w)
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
// Package scene registers the game's scenes.
|
||||
package scene
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/world"
|
||||
)
|
||||
|
||||
// Register adds every scene to the game.
|
||||
func Register(w *world.World) {
|
||||
for _, s := range []inkwell.Scene{
|
||||
alley(w),
|
||||
} {
|
||||
w.G.SceneManager.Register(s)
|
||||
}
|
||||
}
|
||||
|
||||
// setVarIfEmpty only writes when the variable is still unset, so re-entering a
|
||||
// scene does not clobber what the game has set in the meantime.
|
||||
func setVarIfEmpty(name string, v any) inkwell.Action {
|
||||
return world.Fn(func(ctx *inkwell.Ctx) {
|
||||
if ctx.Game.State.Var(name) == nil {
|
||||
ctx.Game.State.SetVar(name, v)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package scene
|
||||
package screen
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
@@ -7,33 +7,28 @@ import (
|
||||
"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
|
||||
// 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.
|
||||
func alley(w *world.World) inkwell.Scene {
|
||||
return inkwell.Scene{
|
||||
Name: names.SceneAlley,
|
||||
Title: "ALLEY — BEHIND NOODLE'S HOUSE",
|
||||
Background: names.AssetAlleyBG,
|
||||
Actors: []inkwell.SceneActor{
|
||||
//
|
||||
// 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}},
|
||||
},
|
||||
Walkboxes: []inkwell.Polygon{
|
||||
inkwell.Poly(
|
||||
inkwell.Point{X: 16, Y: 196}, inkwell.Point{X: 624, Y: 196},
|
||||
inkwell.Point{X: 624, Y: 258}, inkwell.Point{X: 16, Y: 258},
|
||||
),
|
||||
},
|
||||
OnEnter: inkwell.Seq(
|
||||
world.EnterScene(w, names.SceneAlley),
|
||||
setVarIfEmpty(names.VarArmillaStrip, "SRP: 1 tape"),
|
||||
),
|
||||
Hotspots: []inkwell.Hotspot{
|
||||
hidingPlace(),
|
||||
backDoor(),
|
||||
bin(),
|
||||
fireEscape(),
|
||||
},
|
||||
onEnter: setVarIfEmpty(names.VarArmillaStrip, "SRP: 1 tape"),
|
||||
hotspots: []inkwell.Hotspot{hidingPlace(), backDoor(), bin(), fireEscape()},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
|
||||
// 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
|
||||
// opens the SPOKE — BBS surface (wiki: entities#helyszinek).
|
||||
//
|
||||
// It is the same screen wherever the terminal stands — the club's ECHO box,
|
||||
// Norman's machine at the flat, the old one on the roof — which is why its way
|
||||
// out is "back", and not a place.
|
||||
func bbsTerminal() screen {
|
||||
return screen{
|
||||
name: names.ScreenBBSTerminal,
|
||||
title: "TERMINAL — BBS ACCESS POINT",
|
||||
background: names.BgBBSTerminal,
|
||||
exits: []exit{
|
||||
{label: "step back from the terminal", side: sideNear},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
|
||||
// bvkBranch is a 💡 suggestion: the local certification office, imprimatur
|
||||
// stamps and licence checks and numbered waiting rooms — everything a man with
|
||||
// a jailbroken two-slot Armilla should stay away from
|
||||
// (wiki: entities#potenciális-további-helyszínek).
|
||||
func bvkBranch() screen {
|
||||
return screen{
|
||||
name: names.ScreenBVKBranch,
|
||||
title: "BVK — SAN FRANCISCO BRANCH",
|
||||
background: names.BgBVKBranch,
|
||||
onSelector: true,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
|
||||
// columbarium is a 💡 suggestion: where Dex is buried. A quiet place, away
|
||||
// from the noise of the terminals — the loss-and-letting-go thread, which is
|
||||
// the same thread as Norman and The AI
|
||||
// (wiki: entities#potenciális-további-helyszínek).
|
||||
func columbarium() screen {
|
||||
return screen{
|
||||
name: names.ScreenColumbarium,
|
||||
title: "COLUMBARIUM — DEX'S MEMORIAL",
|
||||
background: names.BgColumbarium,
|
||||
onSelector: true,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
|
||||
// curatorShop is a 💡 suggestion: the last workshop of the declining curators'
|
||||
// guild — quartz tuning, tape repair, craft handed down for generations. The
|
||||
// man who built Paul's second slot (wiki: entities#potenciális-további-helyszínek).
|
||||
func curatorShop() screen {
|
||||
return screen{
|
||||
name: names.ScreenCuratorShop,
|
||||
title: "CURATOR SERVICE — THE MASTER'S WORKSHOP",
|
||||
background: names.BgCuratorShop,
|
||||
onSelector: true,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package screen
|
||||
|
||||
// Exits — the connections between screens.
|
||||
//
|
||||
// Where a screen leads is recorded in that screen's own file, and it comes from
|
||||
// the wiki's location graph (projects/realworld/story#helyszín-gráf-vázlat).
|
||||
// That graph has two kinds of edge, and so does this file:
|
||||
//
|
||||
// - Physical adjacency. The alley is behind Noodle's house; the club is
|
||||
// through the back of the trinket shop; the noodle place is next door to
|
||||
// it. These are the arrows the wiki draws between locations, and they are
|
||||
// declared with `to`.
|
||||
// - The selector. The wiki centres its map on a "Helyszínválasztó" — not a
|
||||
// real location but the menu screen every main location connects to both
|
||||
// ways. A screen marks itself with onSelector; selector.go builds the other
|
||||
// half of each of those edges.
|
||||
//
|
||||
// Some of the wiki's arrows are one-way with a condition on them ("beengedés",
|
||||
// "NG+ kód", "Sumphor liftkódja"). `needs` carries that: the exit is there, and
|
||||
// says so, but will not open until the flag is set.
|
||||
//
|
||||
// Until the doors are measured on the paintings, an exit is a strip along one
|
||||
// edge of the picture — the convention every 1990s point & click used, and a
|
||||
// one-field change to re-aim at a real door later.
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
"git.teletypegames.org/games/realworld/internal/world"
|
||||
)
|
||||
|
||||
type side int
|
||||
|
||||
const (
|
||||
sideLeft side = iota // off the left edge
|
||||
sideRight // off the right edge
|
||||
sideBack // into the depth of the picture
|
||||
sideNear // out towards the camera
|
||||
)
|
||||
|
||||
// area is the strip of picture the exit occupies, inside the scene region
|
||||
// (below the top bar at y=20, above the HUD divider at y=262).
|
||||
func (s side) area() inkwell.Shape {
|
||||
switch s {
|
||||
case sideLeft:
|
||||
return inkwell.Rect(0, 40, 56, 212)
|
||||
case sideRight:
|
||||
return inkwell.Rect(584, 40, 56, 212)
|
||||
case sideBack:
|
||||
return inkwell.Rect(232, 28, 176, 84)
|
||||
default: // sideNear
|
||||
return inkwell.Rect(232, 214, 176, 48)
|
||||
}
|
||||
}
|
||||
|
||||
// exit is one way out of a screen.
|
||||
type exit struct {
|
||||
to string // the screen it leads to; empty means "back the way you came"
|
||||
label string // what the status line calls it
|
||||
side side
|
||||
|
||||
// area overrides the edge strip. The selector's map pins use it; nothing
|
||||
// else should need to until the art arrives.
|
||||
area inkwell.Shape
|
||||
|
||||
// needs is a flag that has to be set before the exit opens, and blocked is
|
||||
// what Paul says while it is not.
|
||||
needs string
|
||||
blocked string
|
||||
}
|
||||
|
||||
// toSelector is the way back out to the rest of the city, on every screen the
|
||||
// wiki's Helyszínválasztó lists.
|
||||
func toSelector() exit {
|
||||
return exit{to: names.ScreenSelector, label: "the rest of the city", side: sideNear}
|
||||
}
|
||||
|
||||
// exitName is how a connection stays machine-readable: the graph can be read
|
||||
// straight back out of the registered scenes, so a test can walk it.
|
||||
func exitName(to string) string {
|
||||
if to == "" {
|
||||
return "exit:back"
|
||||
}
|
||||
return "exit:" + to
|
||||
}
|
||||
|
||||
func (e exit) hotspot(w *world.World) inkwell.Hotspot {
|
||||
var travel inkwell.Action = inkwell.GoTo(e.to)
|
||||
if e.to == "" {
|
||||
travel = world.Back(w)
|
||||
}
|
||||
if e.needs != "" {
|
||||
travel = inkwell.If(inkwell.Flag(e.needs), travel, inkwell.Say(names.Paul, e.blocked))
|
||||
}
|
||||
area := e.area
|
||||
if area == nil {
|
||||
area = e.side.area()
|
||||
}
|
||||
return inkwell.Hotspot{
|
||||
Name: exitName(e.to),
|
||||
Label: e.label,
|
||||
Area: area,
|
||||
Cursor: inkwell.CursorExit,
|
||||
OnLook: inkwell.Say(names.Paul, "That way: "+e.label+"."),
|
||||
OnUse: travel,
|
||||
OnTake: inkwell.Say(names.Paul, "It's a way out, not a thing."),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
|
||||
// hackerspace: the community workshop at the edge of the Mission, printers and
|
||||
// stripped servers and paranoid amateur cryptographers. Where the server-farm
|
||||
// access tape can be had (wiki: entities#helyszinek, timeline 24).
|
||||
//
|
||||
// The wiki's graph gives it no physical neighbour, so it hangs off the
|
||||
// selector like every other main location.
|
||||
func hackerspace() screen {
|
||||
return screen{
|
||||
name: names.ScreenHackerspace,
|
||||
title: "HACKERSPACE",
|
||||
background: names.BgHackerspace,
|
||||
onSelector: true,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
|
||||
// hospital is the clinical dead end: Dr. Maren looked for a medical reason for
|
||||
// Norman's state and did not find one (wiki: entities, timeline 21).
|
||||
func hospital() screen {
|
||||
return screen{
|
||||
name: names.ScreenHospital,
|
||||
title: "HOSPITAL — PSYCHIATRIC WING",
|
||||
background: names.BgHospital,
|
||||
onSelector: true,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
|
||||
// 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
|
||||
// candy-crush guy will not give up (wiki: story, the Fagyizó beat).
|
||||
//
|
||||
// The search he eventually allows is what names the club — a lead, not a door:
|
||||
// the way there is the trinket shop's address, so this screen has no exit of
|
||||
// its own.
|
||||
func iceCreamShop() screen {
|
||||
return screen{
|
||||
name: names.ScreenIceCreamShop,
|
||||
title: "ICE CREAM SHOP — WHERE THE BAR WAS",
|
||||
background: names.BgIceCreamShop,
|
||||
onSelector: true,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
|
||||
// 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
|
||||
// uniform stands in front of the tape (wiki: story#noodle-szál).
|
||||
//
|
||||
// The wiki's graph runs NoodleUtcafront → Sikátor: the alley behind the house
|
||||
// is reached from here, and from nowhere else.
|
||||
func noodleHouse() screen {
|
||||
return screen{
|
||||
name: names.ScreenNoodleHouse,
|
||||
title: "NOODLE'S HOUSE — SEALED",
|
||||
background: names.BgNoodleHouse,
|
||||
onSelector: true,
|
||||
exits: []exit{
|
||||
{to: names.ScreenAlley, label: "the alley behind the house", side: sideRight},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
|
||||
// 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
|
||||
// correspondence and the diary (wiki: story#norman-lakása).
|
||||
//
|
||||
// Two ways on from here. His terminal is one of the BBS access points the
|
||||
// catalogue lists ("a titkos klub ECHO-terminálja; később Norman lakása /
|
||||
// tetőterasz"), and the roof hideout is up his own stairs.
|
||||
func normanApartment() screen {
|
||||
return screen{
|
||||
name: names.ScreenNormanApartment,
|
||||
title: "NORMAN'S APARTMENT",
|
||||
background: names.BgNormanApartment,
|
||||
onSelector: true,
|
||||
exits: []exit{
|
||||
{to: names.ScreenRooftopHideout, label: "the stairs up to the roof", side: sideBack},
|
||||
{to: names.ScreenBBSTerminal, label: "Norman's terminal", side: sideRight},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
|
||||
// paulShop is where the game starts: the junk shop and garage Paul lives
|
||||
// behind (wiki: entities#helyszinek). It is the only screen outside San
|
||||
// Francisco — Noodle's letter arrives here and puts him on a bus
|
||||
// (wiki: entities, Paul's timeline 1–2).
|
||||
//
|
||||
// The bus is a screen the deck does not have, so the exit is the journey
|
||||
// itself: one way, with no coming back to the shop mid-story. That is also why
|
||||
// the shop is not on the selector — it is not one of the city's locations.
|
||||
func paulShop() screen {
|
||||
return screen{
|
||||
name: names.ScreenPaulShop,
|
||||
title: "PAUL'S SHOP — JUNK AND GARAGE",
|
||||
background: names.BgPaulShop,
|
||||
exits: []exit{
|
||||
{to: names.ScreenNoodleHouse, label: "the bus to San Francisco", side: sideNear},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
|
||||
// policeStation is where Noodle is held for dealing in cracked Armillas, and
|
||||
// where a clerk quietly tips Paul off about the hiding place
|
||||
// (wiki: entities, Paul's timeline 4 and 15).
|
||||
//
|
||||
// The wiki's graph splits it three ways — utcafront → recepció → kihallgató —
|
||||
// but the deck paints it as one screen, so the reception desk, the waiting
|
||||
// room and the interview window are hotspots inside this one, not screens of
|
||||
// their own.
|
||||
func policeStation() screen {
|
||||
return screen{
|
||||
name: names.ScreenPoliceStation,
|
||||
title: "SFPD — STATION AND HOLDING",
|
||||
background: names.BgPoliceStation,
|
||||
onSelector: true,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
|
||||
// publicBBS is one of the wiki's 💡 suggestions: the canon has BBS terminals
|
||||
// standing in cafés, post offices and bus stops — live infrastructure, not
|
||||
// relics. An alternative way to read the boards, in a place where somebody can
|
||||
// read over your shoulder (wiki: entities#potenciális-további-helyszínek).
|
||||
func publicBBS() screen {
|
||||
return screen{
|
||||
name: names.ScreenPublicBBS,
|
||||
title: "PUBLIC BBS TERMINAL",
|
||||
background: names.BgPublicBBS,
|
||||
onSelector: true,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
|
||||
// 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
|
||||
// of the investigation (wiki: entities#helyszinek, timeline 23).
|
||||
//
|
||||
// It is up the stairs from his flat, and it is one of the places the catalogue
|
||||
// puts a terminal.
|
||||
func rooftopHideout() screen {
|
||||
return screen{
|
||||
name: names.ScreenRooftopHideout,
|
||||
title: "NORMAN'S ROOFTOP HIDEOUT",
|
||||
background: names.BgRooftopHideout,
|
||||
exits: []exit{
|
||||
{to: names.ScreenNormanApartment, label: "back down into the flat", side: sideNear},
|
||||
{to: names.ScreenBBSTerminal, label: "the old terminal", side: sideRight},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
|
||||
// samizdatPress is a 💡 suggestion: the Resistance's physical node — parody
|
||||
// BVK databases, underground publications, a P2P node humming in the cellar.
|
||||
// The samizdat intellectuals are the first to write down that the world is
|
||||
// slipping (wiki: entities#potenciális-további-helyszínek).
|
||||
func samizdatPress() screen {
|
||||
return screen{
|
||||
name: names.ScreenSamizdatPress,
|
||||
title: "SAMIZDAT PRINTING HOUSE",
|
||||
background: names.BgSamizdatPress,
|
||||
onSelector: true,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
|
||||
// scrapMarket is a 💡 suggestion: the black market in condemned ACPs and bare,
|
||||
// unstamped tape. Canon says the leak of scrap is not an accident — "something
|
||||
// wants to live on" (wiki: entities#potenciális-további-helyszínek).
|
||||
func scrapMarket() screen {
|
||||
return screen{
|
||||
name: names.ScreenScrapMarket,
|
||||
title: "SCRAP MARKET",
|
||||
background: names.BgScrapMarket,
|
||||
onSelector: true,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
// Package screen registers the game's screens — one file per screen.
|
||||
//
|
||||
// "Screen" is this project's word: the wiki's catalogue, the concept-art deck
|
||||
// and the beat tables all count screens. inkwell's word for the same thing is
|
||||
// Scene, which is the type every file here returns — a screen IS the engine's
|
||||
// scene, and the rest of the code says "scene" only where it is talking to the
|
||||
// engine.
|
||||
//
|
||||
// A file here holds everything that is true of one screen: its picture, the
|
||||
// name on the top bar, where you can get to from it (exit.go), and — once its
|
||||
// beat is written — its hotspots, NPCs and dialogue. Only the alley has that
|
||||
// last part so far; the rest are greybox.
|
||||
package screen
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
"git.teletypegames.org/games/realworld/internal/world"
|
||||
)
|
||||
|
||||
// screen is what each file in this package fills in.
|
||||
type screen struct {
|
||||
name string
|
||||
title string
|
||||
background string
|
||||
|
||||
// exits: where you can get to from here. See exit.go.
|
||||
exits []exit
|
||||
// onSelector marks a main location — one of those the wiki's
|
||||
// Helyszínválasztó lists (story#helyszín-gráf-vázlat). It gets a way back
|
||||
// to the selector for free, and the selector gets a way here.
|
||||
onSelector bool
|
||||
|
||||
// What an authored beat adds on top. Zero values are fine: Paul lands in
|
||||
// the middle of the default floor strip and nothing else happens.
|
||||
hotspots []inkwell.Hotspot
|
||||
actors []inkwell.SceneActor
|
||||
walkboxes []inkwell.Polygon
|
||||
onEnter inkwell.Action
|
||||
}
|
||||
|
||||
// deck is every screen in the game, in the order of the wiki's location
|
||||
// catalogue (entities#helyszinek) — which the concept-art deck follows number
|
||||
// for number. Registration order is this order, and it is also the order the
|
||||
// arrow keys walk (ui/screen_nav.go).
|
||||
//
|
||||
// Deck 05, Norman's workplace, is in the catalogue but has not been drawn, so
|
||||
// it has no file yet. The alley has no deck number: the catalogue keeps it in
|
||||
// the same row as Noodle's house, and that is where it sits here too.
|
||||
var deck = []func() screen{
|
||||
paulShop, // 01
|
||||
noodleHouse, // 02
|
||||
alley, // 02, same catalogue row
|
||||
normanApartment, // 03
|
||||
policeStation, // 04
|
||||
hackerspace, // 06
|
||||
iceCreamShop, // 07
|
||||
trinketShop, // 08
|
||||
smallRestaurant, // 09
|
||||
secretClub, // 10
|
||||
bbsTerminal, // 11
|
||||
street, // 12
|
||||
hospital, // 13
|
||||
serverFarm, // 14
|
||||
secretLab, // 15
|
||||
rooftopHideout, // 16
|
||||
publicBBS, // 17
|
||||
bvkBranch, // 18
|
||||
curatorShop, // 19
|
||||
scrapMarket, // 20
|
||||
samizdatPress, // 21
|
||||
showroom, // 22
|
||||
columbarium, // 23
|
||||
}
|
||||
|
||||
// Register adds every screen to the game, the selector first: it is the centre
|
||||
// of the wiki's location graph, and it is built from the deck rather than
|
||||
// listed in it.
|
||||
func Register(w *world.World) {
|
||||
screens := make([]screen, 0, len(deck)+1)
|
||||
for _, f := range deck {
|
||||
screens = append(screens, f())
|
||||
}
|
||||
for _, s := range append([]screen{selector(screens)}, screens...) {
|
||||
w.G.SceneManager.Register(build(w, s))
|
||||
}
|
||||
}
|
||||
|
||||
// The default floor: the bottom of the scene region, above the HUD divider.
|
||||
// A screen with a painted floor of its own overrides it.
|
||||
var floor = []inkwell.Polygon{
|
||||
inkwell.Poly(
|
||||
inkwell.Point{X: 16, Y: 196}, inkwell.Point{X: 624, Y: 196},
|
||||
inkwell.Point{X: 624, Y: 258}, inkwell.Point{X: 16, Y: 258},
|
||||
),
|
||||
}
|
||||
|
||||
// build turns a screen into the engine's scene.
|
||||
//
|
||||
// The authored hotspots go in front of the exits, because the engine takes the
|
||||
// first one whose area contains the click (HotspotAt): a painted thing beats
|
||||
// the edge strip an exit sits on, wherever the two overlap.
|
||||
func build(w *world.World, s screen) inkwell.Scene {
|
||||
exits := s.exits
|
||||
if s.onSelector {
|
||||
exits = append(exits, toSelector())
|
||||
}
|
||||
hotspots := make([]inkwell.Hotspot, 0, len(exits)+len(s.hotspots))
|
||||
hotspots = append(hotspots, s.hotspots...)
|
||||
for _, e := range exits {
|
||||
hotspots = append(hotspots, e.hotspot(w))
|
||||
}
|
||||
|
||||
actors := s.actors
|
||||
if actors == nil {
|
||||
actors = []inkwell.SceneActor{
|
||||
{CharacterName: names.Paul, At: inkwell.Point{X: 320, Y: 232}},
|
||||
}
|
||||
}
|
||||
walkboxes := s.walkboxes
|
||||
if walkboxes == nil {
|
||||
walkboxes = floor
|
||||
}
|
||||
onEnter := inkwell.Action(world.EnterScene(w, s.name))
|
||||
if s.onEnter != nil {
|
||||
onEnter = inkwell.Seq(onEnter, s.onEnter)
|
||||
}
|
||||
|
||||
return inkwell.Scene{
|
||||
Name: s.name,
|
||||
Title: s.title,
|
||||
Background: s.background,
|
||||
Actors: actors,
|
||||
Walkboxes: walkboxes,
|
||||
Hotspots: hotspots,
|
||||
OnEnter: onEnter,
|
||||
}
|
||||
}
|
||||
|
||||
// setVarIfEmpty only writes when the variable is still unset, so re-entering a
|
||||
// screen does not clobber what the game has set in the meantime.
|
||||
func setVarIfEmpty(name string, v any) inkwell.Action {
|
||||
return world.Fn(func(ctx *inkwell.Ctx) {
|
||||
if ctx.Game.State.Var(name) == nil {
|
||||
ctx.Game.State.SetVar(name, v)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
|
||||
// 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
|
||||
// people's minds and nothing about the key or the tape
|
||||
// (wiki: story, the club beat).
|
||||
//
|
||||
// The terminal in here is the one that matters: not under BVK control, ECHO
|
||||
// running under the standard LECTOR surface. Reading it is where the mystery
|
||||
// tape finally speaks.
|
||||
func secretClub() screen {
|
||||
return screen{
|
||||
name: names.ScreenSecretClub,
|
||||
title: "SECRET CLUB — THE UNDERWATER SUN",
|
||||
background: names.BgSecretClub,
|
||||
exits: []exit{
|
||||
{to: names.ScreenBBSTerminal, label: "the terminal in the corner", side: sideBack},
|
||||
{to: names.ScreenTrinketShop, label: "back out through the shop", side: sideNear},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
|
||||
// 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
|
||||
// screen the finale plays on (wiki: entities#helyszinek, timeline 22 and 25).
|
||||
//
|
||||
// The wiki's graph runs Híd → security szoba → Norman szimulációs szobája, and
|
||||
// the story puts the whole thing inside the Golden Gate Bridge. The deck paints
|
||||
// it as one screen, so the door at the foot of the bridge, the observation room
|
||||
// and the glass wall are hotspots in here.
|
||||
func secretLab() screen {
|
||||
return screen{
|
||||
name: names.ScreenSecretLab,
|
||||
title: "SECRET RESEARCH LABORATORY",
|
||||
background: names.BgSecretLab,
|
||||
onSelector: true,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package screen
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/names"
|
||||
)
|
||||
|
||||
// selector is the wiki's Helyszínválasztó: "nem valódi helyszín, hanem a
|
||||
// menü-képernyő, ahonnan az összes fő helyszín elérhető"
|
||||
// (story#helyszínválasztó). The location graph is centred on it — every main
|
||||
// location connects to it both ways, and physical adjacency takes over from
|
||||
// there inwards — so without it the city is a handful of disconnected rooms.
|
||||
//
|
||||
// It is not in the concept-art deck and has no painting, so it renders as an
|
||||
// inkwell placeholder with one labelled pin per destination, laid out on a
|
||||
// grid. Whatever the map ends up looking like, the connections it carries are
|
||||
// the ones the screens declare, not a list kept here: selector() is built from
|
||||
// whichever screens marked themselves onSelector.
|
||||
func selector(rest []screen) screen {
|
||||
var exits []exit
|
||||
for _, s := range rest {
|
||||
if !s.onSelector {
|
||||
continue
|
||||
}
|
||||
exits = append(exits, exit{
|
||||
to: s.name,
|
||||
label: pinLabel(s.title),
|
||||
area: pin(len(exits)),
|
||||
})
|
||||
}
|
||||
return screen{
|
||||
name: names.ScreenSelector,
|
||||
title: "SAN FRANCISCO",
|
||||
background: names.BgSelector,
|
||||
exits: exits,
|
||||
// Nobody stands on a map.
|
||||
actors: []inkwell.SceneActor{},
|
||||
walkboxes: []inkwell.Polygon{},
|
||||
}
|
||||
}
|
||||
|
||||
// The pin grid: three columns down the scene region, filled top to bottom.
|
||||
const (
|
||||
pinCols = 3
|
||||
pinW = 192.0
|
||||
pinH = 32.0
|
||||
pinX = 16.0
|
||||
pinY = 28.0
|
||||
pinGapX = 204.0
|
||||
pinGapY = 38.0
|
||||
)
|
||||
|
||||
func pin(i int) inkwell.Shape {
|
||||
col, row := i%pinCols, i/pinCols
|
||||
return inkwell.Rect(pinX+float64(col)*pinGapX, pinY+float64(row)*pinGapY, pinW, pinH)
|
||||
}
|
||||
|
||||
// pinLabel is the screen's title without the em-dash subtitle: on a map a
|
||||
// location wants its name, not its description.
|
||||
func pinLabel(title string) string {
|
||||
name, _, _ := strings.Cut(title, " — ")
|
||||
return name
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
|
||||
// serverFarm is the cooled, humming corridors where The AI's physical
|
||||
// infrastructure can be seen for what it is. Getting in needs the access tape
|
||||
// from the hackerspace or from Noodle (wiki: entities, timeline 24).
|
||||
func serverFarm() screen {
|
||||
return screen{
|
||||
name: names.ScreenServerFarm,
|
||||
title: "SERVER FARM",
|
||||
background: names.BgServerFarm,
|
||||
onSelector: true,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
|
||||
// showroom is a 💡 suggestion: the legitimate, glittering side — Armilla
|
||||
// cabinets, licensed operators, matte-black Nokia-punk under showroom lights.
|
||||
// The contrast with Paul's edge of the world
|
||||
// (wiki: entities#potenciális-további-helyszínek).
|
||||
func showroom() screen {
|
||||
return screen{
|
||||
name: names.ScreenShowroom,
|
||||
title: "NEUMATRONIC SHOWROOM",
|
||||
background: names.BgShowroom,
|
||||
onSelector: true,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
|
||||
// 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
|
||||
// behind it (wiki: story, the Kajálda beat).
|
||||
//
|
||||
// The wiki's graph runs Kajálda → étterem rész → WC; the deck paints all three
|
||||
// as one screen, so the mirror is a hotspot here. The only way out is back to
|
||||
// the shop — this is an inner room, not a location on the selector.
|
||||
func smallRestaurant() screen {
|
||||
return screen{
|
||||
name: names.ScreenSmallRestaurant,
|
||||
title: "SMALL RESTAURANT — NEXT DOOR",
|
||||
background: names.BgSmallRestaurant,
|
||||
exits: []exit{
|
||||
{to: names.ScreenTrinketShop, label: "back to the trinket shop", side: sideLeft},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
|
||||
// street is the city itself: pavements, a square, a metro entrance, and the
|
||||
// environmental decay that tracks The AI's spread — glitching systems, odd
|
||||
// news, infrastructure that slips (wiki: entities#helyszinek).
|
||||
//
|
||||
// The wiki's own street front is the one with the hotdog stand and Sumphor on
|
||||
// it (story#norman-lakása); that is a beat for this screen, not a screen of its
|
||||
// own.
|
||||
func street() screen {
|
||||
return screen{
|
||||
name: names.ScreenStreet,
|
||||
title: "STREET",
|
||||
background: names.BgStreet,
|
||||
onSelector: true,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package screen
|
||||
|
||||
import "git.teletypegames.org/games/realworld/internal/names"
|
||||
|
||||
// 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
|
||||
// (wiki: story#csecsebecse-bolt--kajálda-szál).
|
||||
//
|
||||
// Both of the wiki's arrows out of it are here: Bolt → Kajálda, the eating
|
||||
// place next door, and Bolt →|beengedés| Klub, which stays shut until the
|
||||
// shopkeeper has been shown the underwater sun.
|
||||
func trinketShop() screen {
|
||||
return screen{
|
||||
name: names.ScreenTrinketShop,
|
||||
title: "CHINATOWN — TRINKET SHOP",
|
||||
background: names.BgTrinketShop,
|
||||
onSelector: true,
|
||||
exits: []exit{
|
||||
{to: names.ScreenSmallRestaurant, label: "the eating place next door", side: sideRight},
|
||||
{
|
||||
to: names.ScreenSecretClub, label: "the way in at the back", side: sideBack,
|
||||
needs: names.FlagClubEntry,
|
||||
blocked: "Just a shop, as far as the man behind the counter is concerned.",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -46,20 +46,78 @@ const (
|
||||
FlagPoliceTip = "police_tip" // wiki: hatosagi_tipp
|
||||
FlagHasTape = "has_mystery_tape" // wiki: titokzatos_tape_megvan
|
||||
FlagTapeSpoke = "tape_spoke" // wiki: tape_megszolalt
|
||||
FlagClubEntry = "club_entry" // wiki: klub_bejutas — the shopkeeper let Paul through
|
||||
|
||||
VarSlot2 = "armilla.slot2"
|
||||
VarArmillaStrip = "armilla.strip"
|
||||
VarDecay = "decay_level" // wiki: romlas_szint
|
||||
)
|
||||
|
||||
// Scenes.
|
||||
// Screens.
|
||||
//
|
||||
// One constant per screen in the wiki's location catalogue
|
||||
// (entities#helyszinek), whose order the concept-art deck follows number for
|
||||
// number (games/realworld_screen_assets); the trailing comment is the wiki's
|
||||
// Hungarian name. The last seven are the catalogue's "javaslat" locations —
|
||||
// drawn, so they are wired up, but no beat lands on them yet.
|
||||
//
|
||||
// Two screens have no deck number. The alley shares the catalogue's "Noodle
|
||||
// háza & a sikátor" row with the house; the selector is the wiki's
|
||||
// Helyszínválasztó (story#helyszínválasztó), the menu screen the location
|
||||
// graph is centred on.
|
||||
const (
|
||||
SceneAlley = "alley" // wiki: sikator
|
||||
ScreenSelector = "selector" // wiki: Helyszínválasztó
|
||||
ScreenPaulShop = "paul_shop" // wiki: Paul boltja / garázsa
|
||||
ScreenNoodleHouse = "noodle_house" // wiki: Noodle háza
|
||||
ScreenNormanApartment = "norman_apartment" // wiki: Norman lakása / műhelye
|
||||
ScreenPoliceStation = "police_station" // wiki: rendőrség / fogda
|
||||
ScreenAlley = "alley" // wiki: sikátor (Noodle háza mögött)
|
||||
ScreenHackerspace = "hackerspace" // wiki: hackerspace
|
||||
ScreenIceCreamShop = "ice_cream_shop" // wiki: fagyizó
|
||||
ScreenTrinketShop = "trinket_shop" // wiki: chinatowni csecsebecse-bolt
|
||||
ScreenSmallRestaurant = "small_restaurant" // wiki: kajálda
|
||||
ScreenSecretClub = "secret_club" // wiki: titkos klub („vízalatti nap")
|
||||
ScreenBBSTerminal = "bbs_terminal" // wiki: a terminál / BBS-hozzáférési pont
|
||||
ScreenStreet = "street" // wiki: köztér / utca
|
||||
ScreenHospital = "hospital" // wiki: kórház / pszichiátria
|
||||
ScreenServerFarm = "server_farm" // wiki: szerverfarm / adatközpont
|
||||
ScreenSecretLab = "secret_lab" // wiki: titkos kutatólabor
|
||||
ScreenRooftopHideout = "rooftop_hideout" // wiki: Norman tetőterasz-rejteke
|
||||
ScreenPublicBBS = "public_bbs" // wiki 💡: nyilvános BBS-terminál
|
||||
ScreenBVKBranch = "bvk_branch" // wiki 💡: BVK-kirendeltség (San Francisco)
|
||||
ScreenCuratorShop = "curator_shop" // wiki 💡: kurátor-szerviz („a Mester")
|
||||
ScreenScrapMarket = "scrap_market" // wiki 💡: selejt-piac
|
||||
ScreenSamizdatPress = "samizdat_press" // wiki 💡: samizdat-nyomda / P2P-csomópont
|
||||
ScreenShowroom = "showroom" // wiki 💡: Neumatronic márkakereskedés
|
||||
ScreenColumbarium = "columbarium" // wiki 💡: kolumbárium / Dex emlékhelye
|
||||
)
|
||||
|
||||
// Assets.
|
||||
// Backgrounds — one per screen, same order.
|
||||
const (
|
||||
AssetAlleyBG = "bg_alley"
|
||||
BgSelector = "bg_selector"
|
||||
BgPaulShop = "bg_paul_shop"
|
||||
BgNoodleHouse = "bg_noodle_house"
|
||||
BgNormanApartment = "bg_norman_apartment"
|
||||
BgPoliceStation = "bg_police_station"
|
||||
BgAlley = "bg_alley"
|
||||
BgHackerspace = "bg_hackerspace"
|
||||
BgIceCreamShop = "bg_ice_cream_shop"
|
||||
BgTrinketShop = "bg_trinket_shop"
|
||||
BgSmallRestaurant = "bg_small_restaurant"
|
||||
BgSecretClub = "bg_secret_club"
|
||||
BgBBSTerminal = "bg_bbs_terminal"
|
||||
BgStreet = "bg_street"
|
||||
BgHospital = "bg_hospital"
|
||||
BgServerFarm = "bg_server_farm"
|
||||
BgSecretLab = "bg_secret_lab"
|
||||
BgRooftopHideout = "bg_rooftop_hideout"
|
||||
BgPublicBBS = "bg_public_bbs"
|
||||
BgBVKBranch = "bg_bvk_branch"
|
||||
BgCuratorShop = "bg_curator_shop"
|
||||
BgScrapMarket = "bg_scrap_market"
|
||||
BgSamizdatPress = "bg_samizdat_press"
|
||||
BgShowroom = "bg_showroom"
|
||||
BgColumbarium = "bg_columbarium"
|
||||
)
|
||||
|
||||
// Tapes maps a character name to the label shown in the tape channel header.
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package ui
|
||||
|
||||
// screenNav — walking the deck with the arrow keys.
|
||||
//
|
||||
// The screens are connected — each one records where you can get to from it
|
||||
// (content/screen/exit.go) — but walking the map is the player's job, and a
|
||||
// screen whose beat is not written yet has nothing in it to walk towards. LEFT
|
||||
// and RIGHT step to the previous and next screen so the deck can be reviewed as
|
||||
// a deck: in concept-art order, wrapping at both ends. It is a tool for looking
|
||||
// at the game, not a way through it.
|
||||
//
|
||||
// It is deliberately inert whenever the game is actually doing something: no
|
||||
// stepping out of a cutscene, a menu, or a stopped world during dialogue. So
|
||||
// once a beat owns a screen, this cannot cut across it.
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/world"
|
||||
)
|
||||
|
||||
type screenNav struct {
|
||||
Name string
|
||||
W *world.World
|
||||
}
|
||||
|
||||
func (n *screenNav) GetName() string { return n.Name }
|
||||
func (n *screenNav) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) {}
|
||||
|
||||
func (n *screenNav) Tick(ctx *inkwell.UICtx) {
|
||||
if !n.W.HUDVisible() || n.W.Paused() {
|
||||
return
|
||||
}
|
||||
step := 0
|
||||
// inkwell's Input only covers the mouse; read the keys directly.
|
||||
switch {
|
||||
case inpututil.IsKeyJustPressed(ebiten.KeyArrowRight):
|
||||
step = 1
|
||||
case inpututil.IsKeyJustPressed(ebiten.KeyArrowLeft):
|
||||
step = -1
|
||||
default:
|
||||
return
|
||||
}
|
||||
if next := n.neighbour(ctx.Game, step); next != "" {
|
||||
n.W.Do(inkwell.GoTo(next))
|
||||
}
|
||||
}
|
||||
|
||||
// neighbour returns the screen step places from the current one, wrapping.
|
||||
// Registration order is the deck order — see content/screen/screen.go.
|
||||
func (n *screenNav) neighbour(g *inkwell.Game, step int) string {
|
||||
deck := g.SceneManager.Names()
|
||||
if len(deck) < 2 {
|
||||
return ""
|
||||
}
|
||||
for i, name := range deck {
|
||||
if name == n.W.Scene() {
|
||||
return deck[((i+step)%len(deck)+len(deck))%len(deck)]
|
||||
}
|
||||
}
|
||||
return deck[0]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/world"
|
||||
)
|
||||
|
||||
// The arrow keys walk the deck in registration order and wrap at both ends —
|
||||
// until the beats grow doors, that walk is the only way through most screens,
|
||||
// so a dead end at either end would strand the player on a greybox screen.
|
||||
func TestScreenNavWrapsBothWays(t *testing.T) {
|
||||
g := inkwell.NewGame("t", ScreenW, ScreenH)
|
||||
for _, n := range []string{"a", "b", "c"} {
|
||||
g.SceneManager.Register(inkwell.Scene{Name: n, Background: "bg"})
|
||||
}
|
||||
w := world.New(g)
|
||||
nav := &screenNav{Name: "screen_nav", W: w}
|
||||
at := func(name string) { world.EnterScene(w, name).Start().Tick(&inkwell.Ctx{Game: g}) }
|
||||
|
||||
for _, tc := range []struct {
|
||||
from string
|
||||
step int
|
||||
want string
|
||||
}{
|
||||
{"a", 1, "b"}, {"b", 1, "c"}, {"c", 1, "a"},
|
||||
{"c", -1, "b"}, {"b", -1, "a"}, {"a", -1, "c"},
|
||||
} {
|
||||
at(tc.from)
|
||||
if got := nav.neighbour(g, tc.step); got != tc.want {
|
||||
t.Errorf("from %q step %+d = %q, want %q", tc.from, tc.step, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,26 +7,31 @@ package ui
|
||||
// 0 │ ALLEY — paused SRP: 1 tape │ TopBar (20)
|
||||
// 20 ├────────────────────────────────────────────────────────────────────┤
|
||||
// │ │
|
||||
// │ scene — hotspots, characters, SpeechBubble │ Scene (244)
|
||||
// │ scene — hotspots, characters, SpeechBubble │ Scene (242)
|
||||
// │ │
|
||||
// 264├─────────────────────────────────────────┬──────────────────────────┤
|
||||
// 262├─────────────────────────────────────────┬──────────────────────────┤
|
||||
// │ Use hook on: floor grate │ DEX │
|
||||
// │ ┌───┬───┬───┬───┐ ┌───────┐┌────────┐ │ "The hook is a hand │ HUD (135)
|
||||
// │ ┌───┬───┬───┬───┐ ┌───────┐┌────────┐ │ "The hook is a hand │ HUD (118)
|
||||
// │ ├───┼───┼───┼───┤ │ 1 DEX ││ 2 — │ │ shorter than the gap." │
|
||||
// │ └───┴───┴───┴───┘ └───────┘└────────┘ │ │
|
||||
// 399└─────────────────────────────────────────┴──────────────────────────┘
|
||||
// 379└─────────────────────────────────────────┴──────────────────────────┘
|
||||
// InventoryBar TapeSlots TapeChannel
|
||||
//
|
||||
// On the resolution: the wiki's art brief asks for a "320×200 VGA look", and
|
||||
// inkwell's widget defaults are authored for exactly that. We render at 640×400
|
||||
// — the same 16:10, exactly 2× the brief, so art drawn at 320×200 upscales
|
||||
// cleanly — because the engine's text is a fixed 6×16 debug font. At 320×200 a
|
||||
// 16px glyph is 8% of the screen height and nothing fits; at 640×400 it lands
|
||||
// at 4%, which is the proportion the design was drawn for.
|
||||
// On the resolution: the screen is exactly one painted background. inkwell
|
||||
// stretches a scene background over the WHOLE screen (core.engine.go), not over
|
||||
// the region left free by the HUD, so any screen size other than the art's own
|
||||
// squashes every painting — and the deck is drawn at 640×380
|
||||
// (games/realworld_screen_assets). Hence 640×380: the art lands pixel for
|
||||
// pixel, and the HUD is an opaque strip laid over its foot.
|
||||
//
|
||||
// Everything vertical is derived from LineH below rather than from the
|
||||
// wireframe deck's ratios, so the layout cannot drift out of step with the font
|
||||
// again.
|
||||
// That still satisfies what the wiki's "320×200 VGA look" brief was after — a
|
||||
// low, wide VGA frame at an exact 2× of a 320-wide canvas — and it still keeps
|
||||
// the engine's fixed 6×16 debug font readable: a 16px glyph is 4% of 380, the
|
||||
// proportion the design was drawn for. (At 320×190 it would be 8%, rows would
|
||||
// overlap and the top bar could not hold a line.)
|
||||
//
|
||||
// Everything vertical is derived from LineH and from ScreenH below, never from
|
||||
// the wireframe deck's ratios, so the layout cannot drift out of step with the
|
||||
// font — or with the art — again.
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
@@ -39,8 +44,11 @@ import (
|
||||
|
||||
// Screen and layout, in internal (unscaled) pixels.
|
||||
const (
|
||||
// The screen is the size of one painted background, because the engine
|
||||
// scales a background to the full screen: at any other size the art is
|
||||
// squashed. See the note above.
|
||||
ScreenW = 640
|
||||
ScreenH = 400
|
||||
ScreenH = 380
|
||||
|
||||
// LineH is one text row: the glyph cell plus leading. Every text-bearing
|
||||
// box is sized as a multiple of this, so rows can never overlap.
|
||||
@@ -48,14 +56,21 @@ const (
|
||||
Pad = 6 // inner padding for every panel
|
||||
|
||||
TopBarH = LineH + 2 // 20 — one row plus a hairline of breathing room
|
||||
HUDTop = 264 // the scene | HUD divider
|
||||
DividerX = 394 // world | tape channel (61.6% of the width)
|
||||
|
||||
// The HUD strip is as short as its own contents allow, and is measured
|
||||
// from the bottom of the screen — every pixel it takes is a pixel of the
|
||||
// painting it covers. It has to hold the status row and, under it, the two
|
||||
// rows of inventory slots (the tallest thing in the strip), padded above
|
||||
// and below.
|
||||
HUDH = Pad + LineH + 4 + invSlot*2 + invGap + Pad // 118
|
||||
HUDTop = ScreenH - HUDH // 262 — the scene | HUD divider
|
||||
|
||||
// Derived HUD rows.
|
||||
statusY = HUDTop + Pad // 270
|
||||
slotsY = HUDTop + Pad + LineH // 288
|
||||
statusY = HUDTop + Pad // 268
|
||||
slotsY = HUDTop + Pad + LineH // 286
|
||||
slotsH = LineH*2 + Pad*2 // 48 — two rows plus padding
|
||||
invY = slotsY + 4 // 292
|
||||
invY = slotsY + 4 // 290
|
||||
invSlot = 40
|
||||
invGap = 4
|
||||
)
|
||||
@@ -70,6 +85,7 @@ func Register(w *world.World) {
|
||||
// it does, and it only ever catches clicks that land on the scene.
|
||||
g.UIManager.Register(&UseWithGuard{Name: "usewith_guard", W: w})
|
||||
g.UIManager.Register(&world.ActionPump{Name: "pump", W: w})
|
||||
g.UIManager.Register(&screenNav{Name: "screen_nav", W: w})
|
||||
g.UIManager.Register(&windowSizer{Name: "window", Scale: 2})
|
||||
g.UIManager.Register(&inkwell.HotspotDebug{Name: "hotspot_debug"})
|
||||
|
||||
@@ -129,8 +145,8 @@ func Register(w *world.World) {
|
||||
|
||||
// windowSizer resizes the window once, on the first frame.
|
||||
//
|
||||
// inkwell.Run hardcodes a 4× window (core.dsl.go), which at 640×400 would be
|
||||
// 2560×1600 — larger than most laptop screens. Run sets the size before
|
||||
// inkwell.Run hardcodes a 4× window (core.dsl.go), which at 640×380 would be
|
||||
// 2560×1520 — larger than most laptop screens. Run sets the size before
|
||||
// entering the loop, so the only place to override it is the first tick.
|
||||
type windowSizer struct {
|
||||
Name string
|
||||
|
||||
@@ -104,7 +104,27 @@ func SetMode(w *World, m Mode) inkwell.Action {
|
||||
// EnterScene tells the domain which scene we are in. inkwell exposes no
|
||||
// CurrentScene accessor, and the pump needs Ctx.Scene.
|
||||
func EnterScene(w *World, name string) inkwell.Action {
|
||||
return Fn(func(*inkwell.Ctx) { w.scene = name })
|
||||
return Fn(func(*inkwell.Ctx) {
|
||||
if name != w.scene {
|
||||
w.prev = w.scene
|
||||
}
|
||||
w.scene = name
|
||||
})
|
||||
}
|
||||
|
||||
// Back returns to the screen we came from.
|
||||
//
|
||||
// Most exits name their destination, because a door leads where it leads. A
|
||||
// screen that is reached from several different rooms cannot: the BBS terminal
|
||||
// is the same terminal whether you walked to it from the club, from Norman's
|
||||
// flat or from the roof, so its way out is "back", not a place.
|
||||
func Back(w *World) inkwell.Action {
|
||||
return Fn(func(ctx *inkwell.Ctx) {
|
||||
if w.prev == "" {
|
||||
return
|
||||
}
|
||||
inkwell.GoTo(w.prev).Start().Tick(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
// ----- TapeSay ----------------------------------------------------------
|
||||
|
||||
@@ -34,6 +34,7 @@ type World struct {
|
||||
mode Mode
|
||||
|
||||
scene string // current scene — inkwell exposes no accessor for it
|
||||
prev string // the one before it, for screens with no fixed way out
|
||||
paused bool // is the world stopped (during dialogue), for the TopBar suffix
|
||||
pending string // the tape currently being loaded into slot 2
|
||||
queue []inkwell.Action // HUD-originated actions waiting to run
|
||||
@@ -63,6 +64,9 @@ func (w *World) HUDVisible() bool { return w.mode == ModePlay }
|
||||
// Scene returns the name of the current scene.
|
||||
func (w *World) Scene() string { return w.scene }
|
||||
|
||||
// Previous returns the screen we came from, empty at the start of the game.
|
||||
func (w *World) Previous() string { return w.prev }
|
||||
|
||||
// Paused reports whether the world is stopped (during dialogue).
|
||||
func (w *World) Paused() bool { return w.paused }
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"flag"
|
||||
"log"
|
||||
|
||||
@@ -9,9 +10,19 @@ import (
|
||||
"git.teletypegames.org/games/realworld/internal/boot"
|
||||
)
|
||||
|
||||
// The art ships inside the binary. js/wasm has no OS filesystem, so without
|
||||
// this every background in the web build falls back to a placeholder; and
|
||||
// `make binaries` packages the executable alone, with no assets directory
|
||||
// beside it.
|
||||
//
|
||||
//go:embed assets
|
||||
var assets embed.FS
|
||||
|
||||
func init() { inkwell.AssetFS = assets }
|
||||
|
||||
func main() {
|
||||
var o boot.Opts
|
||||
flag.StringVar(&o.Scene, "scene", "", "starting scene (empty: the alley)")
|
||||
flag.StringVar(&o.Screen, "screen", "", "starting screen (empty: the alley)")
|
||||
flag.BoolVar(&o.Finale, "finale", false,
|
||||
"start with the finale, to try the Nokia-punk theme switch")
|
||||
flag.Parse()
|
||||
|
||||