remove comments
This commit is contained in:
@@ -4,10 +4,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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 backgroundAlley() inkwell.Asset {
|
func backgroundAlley() inkwell.Asset {
|
||||||
return inkwell.Asset{
|
return inkwell.Asset{
|
||||||
Name: BgAlley,
|
Name: BgAlley,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// bbsTerminal — deck 11.
|
|
||||||
func backgroundBbsTerminal() inkwell.Asset {
|
func backgroundBbsTerminal() inkwell.Asset {
|
||||||
return inkwell.Asset{
|
return inkwell.Asset{
|
||||||
Name: BgBBSTerminal,
|
Name: BgBBSTerminal,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// bvkBranch — deck 18.
|
|
||||||
func backgroundBvkBranch() inkwell.Asset {
|
func backgroundBvkBranch() inkwell.Asset {
|
||||||
return inkwell.Asset{
|
return inkwell.Asset{
|
||||||
Name: BgBVKBranch,
|
Name: BgBVKBranch,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// columbarium — deck 23.
|
|
||||||
func backgroundColumbarium() inkwell.Asset {
|
func backgroundColumbarium() inkwell.Asset {
|
||||||
return inkwell.Asset{
|
return inkwell.Asset{
|
||||||
Name: BgColumbarium,
|
Name: BgColumbarium,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// curatorShop — deck 19.
|
|
||||||
func backgroundCuratorShop() inkwell.Asset {
|
func backgroundCuratorShop() inkwell.Asset {
|
||||||
return inkwell.Asset{
|
return inkwell.Asset{
|
||||||
Name: BgCuratorShop,
|
Name: BgCuratorShop,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// hackerspace — deck 06.
|
|
||||||
func backgroundHackerspace() inkwell.Asset {
|
func backgroundHackerspace() inkwell.Asset {
|
||||||
return inkwell.Asset{
|
return inkwell.Asset{
|
||||||
Name: BgHackerspace,
|
Name: BgHackerspace,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// hospital — deck 13.
|
|
||||||
func backgroundHospital() inkwell.Asset {
|
func backgroundHospital() inkwell.Asset {
|
||||||
return inkwell.Asset{
|
return inkwell.Asset{
|
||||||
Name: BgHospital,
|
Name: BgHospital,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// iceCreamShop — deck 07.
|
|
||||||
func backgroundIceCreamShop() inkwell.Asset {
|
func backgroundIceCreamShop() inkwell.Asset {
|
||||||
return inkwell.Asset{
|
return inkwell.Asset{
|
||||||
Name: BgIceCreamShop,
|
Name: BgIceCreamShop,
|
||||||
|
|||||||
@@ -1,20 +1,9 @@
|
|||||||
package inc
|
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 (
|
import (
|
||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// registerBackground adds every background to the game.
|
|
||||||
func registerBackground(w *World) {
|
func registerBackground(w *World) {
|
||||||
for _, a := range []inkwell.Asset{
|
for _, a := range []inkwell.Asset{
|
||||||
backgroundSelector(),
|
backgroundSelector(),
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// noodleHouse — deck 02.
|
|
||||||
func backgroundNoodleHouse() inkwell.Asset {
|
func backgroundNoodleHouse() inkwell.Asset {
|
||||||
return inkwell.Asset{
|
return inkwell.Asset{
|
||||||
Name: BgNoodleHouse,
|
Name: BgNoodleHouse,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// normanApartment — deck 03.
|
|
||||||
func backgroundNormanApartment() inkwell.Asset {
|
func backgroundNormanApartment() inkwell.Asset {
|
||||||
return inkwell.Asset{
|
return inkwell.Asset{
|
||||||
Name: BgNormanApartment,
|
Name: BgNormanApartment,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// paulShop — deck 01.
|
|
||||||
func backgroundPaulShop() inkwell.Asset {
|
func backgroundPaulShop() inkwell.Asset {
|
||||||
return inkwell.Asset{
|
return inkwell.Asset{
|
||||||
Name: BgPaulShop,
|
Name: BgPaulShop,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// policeStation — deck 04.
|
|
||||||
func backgroundPoliceStation() inkwell.Asset {
|
func backgroundPoliceStation() inkwell.Asset {
|
||||||
return inkwell.Asset{
|
return inkwell.Asset{
|
||||||
Name: BgPoliceStation,
|
Name: BgPoliceStation,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// publicBbs — deck 17.
|
|
||||||
func backgroundPublicBBS() inkwell.Asset {
|
func backgroundPublicBBS() inkwell.Asset {
|
||||||
return inkwell.Asset{
|
return inkwell.Asset{
|
||||||
Name: BgPublicBBS,
|
Name: BgPublicBBS,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// rooftopHideout — deck 16.
|
|
||||||
func backgroundRooftopHideout() inkwell.Asset {
|
func backgroundRooftopHideout() inkwell.Asset {
|
||||||
return inkwell.Asset{
|
return inkwell.Asset{
|
||||||
Name: BgRooftopHideout,
|
Name: BgRooftopHideout,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// samizdatPress — deck 21.
|
|
||||||
func backgroundSamizdatPress() inkwell.Asset {
|
func backgroundSamizdatPress() inkwell.Asset {
|
||||||
return inkwell.Asset{
|
return inkwell.Asset{
|
||||||
Name: BgSamizdatPress,
|
Name: BgSamizdatPress,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// scrapMarket — deck 20.
|
|
||||||
func backgroundScrapMarket() inkwell.Asset {
|
func backgroundScrapMarket() inkwell.Asset {
|
||||||
return inkwell.Asset{
|
return inkwell.Asset{
|
||||||
Name: BgScrapMarket,
|
Name: BgScrapMarket,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// secretClub — deck 10.
|
|
||||||
func backgroundSecretClub() inkwell.Asset {
|
func backgroundSecretClub() inkwell.Asset {
|
||||||
return inkwell.Asset{
|
return inkwell.Asset{
|
||||||
Name: BgSecretClub,
|
Name: BgSecretClub,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// secretLab — deck 15.
|
|
||||||
func backgroundSecretLab() inkwell.Asset {
|
func backgroundSecretLab() inkwell.Asset {
|
||||||
return inkwell.Asset{
|
return inkwell.Asset{
|
||||||
Name: BgSecretLab,
|
Name: BgSecretLab,
|
||||||
|
|||||||
@@ -4,10 +4,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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 backgroundSelector() inkwell.Asset {
|
func backgroundSelector() inkwell.Asset {
|
||||||
return inkwell.Asset{
|
return inkwell.Asset{
|
||||||
Name: BgSelector,
|
Name: BgSelector,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// serverFarm — deck 14.
|
|
||||||
func backgroundServerFarm() inkwell.Asset {
|
func backgroundServerFarm() inkwell.Asset {
|
||||||
return inkwell.Asset{
|
return inkwell.Asset{
|
||||||
Name: BgServerFarm,
|
Name: BgServerFarm,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// showroom — deck 22.
|
|
||||||
func backgroundShowroom() inkwell.Asset {
|
func backgroundShowroom() inkwell.Asset {
|
||||||
return inkwell.Asset{
|
return inkwell.Asset{
|
||||||
Name: BgShowroom,
|
Name: BgShowroom,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// smallRestaurant — deck 09.
|
|
||||||
func backgroundSmallRestaurant() inkwell.Asset {
|
func backgroundSmallRestaurant() inkwell.Asset {
|
||||||
return inkwell.Asset{
|
return inkwell.Asset{
|
||||||
Name: BgSmallRestaurant,
|
Name: BgSmallRestaurant,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// street — deck 12.
|
|
||||||
func backgroundStreet() inkwell.Asset {
|
func backgroundStreet() inkwell.Asset {
|
||||||
return inkwell.Asset{
|
return inkwell.Asset{
|
||||||
Name: BgStreet,
|
Name: BgStreet,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// trinketShop — deck 08.
|
|
||||||
func backgroundTrinketShop() inkwell.Asset {
|
func backgroundTrinketShop() inkwell.Asset {
|
||||||
return inkwell.Asset{
|
return inkwell.Asset{
|
||||||
Name: BgTrinketShop,
|
Name: BgTrinketShop,
|
||||||
|
|||||||
+3
-17
@@ -1,26 +1,14 @@
|
|||||||
// 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
|
package inc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Opts are the run parameters.
|
|
||||||
type Opts struct {
|
type Opts struct {
|
||||||
Screen string // starting screen; empty means the alley
|
Screen string
|
||||||
Finale bool // start with the finale, to try the Nokia-punk theme switch
|
Finale bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// New builds a ready-to-run game.
|
|
||||||
func New(o Opts) *inkwell.Game {
|
func New(o Opts) *inkwell.Game {
|
||||||
start := o.Screen
|
start := o.Screen
|
||||||
if start == "" {
|
if start == "" {
|
||||||
@@ -45,9 +33,7 @@ func New(o Opts) *inkwell.Game {
|
|||||||
func bootOpening(w *World, start string, finale bool) []inkwell.Action {
|
func bootOpening(w *World, start string, finale bool) []inkwell.Action {
|
||||||
acts := []inkwell.Action{
|
acts := []inkwell.Action{
|
||||||
EnterScene(w, start),
|
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.SetFlag(FlagPoliceTip),
|
||||||
inkwell.Say(Paul, "Back alley. Just like the clerk said."),
|
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?"),
|
TapeSay(Dex, "A government office tipped you off to remove something from a scene. That doesn't strike you as odd?"),
|
||||||
|
|||||||
@@ -19,9 +19,6 @@ func newTestGame(t *testing.T) *inkwell.Game {
|
|||||||
return g
|
return g
|
||||||
}
|
}
|
||||||
|
|
||||||
// The engine's Validate only checks scene→asset/character references. A typo in
|
|
||||||
// a dialogue, script or item name would surface at runtime as a silent no-op
|
|
||||||
// (RunDialogue and RunScript fail quietly), so check them here.
|
|
||||||
func TestReferencesResolve(t *testing.T) {
|
func TestReferencesResolve(t *testing.T) {
|
||||||
g := newTestGame(t)
|
g := newTestGame(t)
|
||||||
|
|
||||||
@@ -45,7 +42,7 @@ func TestReferencesResolve(t *testing.T) {
|
|||||||
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.
|
|
||||||
for item, char := range 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)
|
||||||
@@ -59,9 +56,6 @@ func TestReferencesResolve(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The two-channel rule: every tape needs its own voice colour, distinct from
|
|
||||||
// the world's. Without that, "if something is amber, a tape said it" is not
|
|
||||||
// readable.
|
|
||||||
func TestTapeVoicesAreDistinct(t *testing.T) {
|
func TestTapeVoicesAreDistinct(t *testing.T) {
|
||||||
g := newTestGame(t)
|
g := newTestGame(t)
|
||||||
paul, _ := g.CharacterManager.Get(Paul)
|
paul, _ := g.CharacterManager.Get(Paul)
|
||||||
@@ -84,8 +78,6 @@ func TestTapeVoicesAreDistinct(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HUD geometry: the two channels must not overlap, and both must stay inside
|
|
||||||
// the HUD strip.
|
|
||||||
func TestHUDGeometry(t *testing.T) {
|
func TestHUDGeometry(t *testing.T) {
|
||||||
g := newTestGame(t)
|
g := newTestGame(t)
|
||||||
|
|
||||||
@@ -112,8 +104,6 @@ func TestHUDGeometry(t *testing.T) {
|
|||||||
t.Errorf("tape slots cross the divider: %v > %v", ts.Bounds.X+ts.Bounds.W, DividerX)
|
t.Errorf("tape slots cross the divider: %v > %v", ts.Bounds.X+ts.Bounds.W, DividerX)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The dialogue box deliberately covers only the left region, so a tape can
|
|
||||||
// comment alongside the conversation.
|
|
||||||
db, ok := g.UIManager.Get("dialog")
|
db, ok := g.UIManager.Get("dialog")
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("no 'dialog' widget")
|
t.Fatal("no 'dialog' widget")
|
||||||
@@ -124,8 +114,6 @@ func TestHUDGeometry(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The guard must be registered FIRST so it ticks LAST among the widgets —
|
|
||||||
// otherwise it would swallow HUD clicks.
|
|
||||||
func TestWidgetOrder(t *testing.T) {
|
func TestWidgetOrder(t *testing.T) {
|
||||||
g := newTestGame(t)
|
g := newTestGame(t)
|
||||||
n := g.UIManager.Names()
|
n := g.UIManager.Names()
|
||||||
@@ -137,22 +125,9 @@ func TestWidgetOrder(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
func TestSceneBackgroundsResolve(t *testing.T) {
|
||||||
g := newTestGame(t)
|
g := newTestGame(t)
|
||||||
|
|
||||||
// Not drawn yet: these render as placeholders on purpose. The alley is
|
|
||||||
// missing from the concept-art deck; the selector is the wiki's menu
|
|
||||||
// screen, which the deck does not cover at all.
|
|
||||||
undrawn := map[string]bool{BgAlley: true, BgSelector: true}
|
undrawn := map[string]bool{BgAlley: true, BgSelector: true}
|
||||||
|
|
||||||
for _, n := range g.SceneManager.Names() {
|
for _, n := range g.SceneManager.Names() {
|
||||||
@@ -183,9 +158,6 @@ func TestSceneBackgroundsResolve(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 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 {
|
||||||
@@ -196,8 +168,6 @@ func exits(g *inkwell.Game, screen string) []string {
|
|||||||
return out
|
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) {
|
func TestExitsResolve(t *testing.T) {
|
||||||
g := newTestGame(t)
|
g := newTestGame(t)
|
||||||
for _, n := range g.SceneManager.Names() {
|
for _, n := range g.SceneManager.Names() {
|
||||||
@@ -209,12 +179,6 @@ func TestExitsResolve(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
func TestEveryScreenIsReachable(t *testing.T) {
|
||||||
g := newTestGame(t)
|
g := newTestGame(t)
|
||||||
|
|
||||||
@@ -238,8 +202,6 @@ func TestEveryScreenIsReachable(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A screen with no way out at all is a trap: the player walks in and the only
|
|
||||||
// way on is the debug walk.
|
|
||||||
func TestEveryScreenHasAWayOut(t *testing.T) {
|
func TestEveryScreenHasAWayOut(t *testing.T) {
|
||||||
g := newTestGame(t)
|
g := newTestGame(t)
|
||||||
for _, n := range g.SceneManager.Names() {
|
for _, n := range g.SceneManager.Names() {
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// dex is the personality imprint of a dead friend, permanently in slot 1 of
|
|
||||||
// Paul's Armilla. Constant commentary, hint system, dialogue partner.
|
|
||||||
func characterDex() inkwell.Character {
|
func characterDex() inkwell.Character {
|
||||||
return inkwell.Character{
|
return inkwell.Character{
|
||||||
Name: Dex,
|
Name: Dex,
|
||||||
|
|||||||
@@ -1,16 +1,9 @@
|
|||||||
package inc
|
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 (
|
import (
|
||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// registerCharacter adds every character to the game.
|
|
||||||
func registerCharacter(w *World) {
|
func registerCharacter(w *World) {
|
||||||
for _, c := range []inkwell.Character{
|
for _, c := range []inkwell.Character{
|
||||||
characterPaul(),
|
characterPaul(),
|
||||||
|
|||||||
@@ -4,11 +4,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// mysteryTape is the tape found in the alley — in truth Norman's Personal
|
|
||||||
// Tape, but the player only learns that in the finale.
|
|
||||||
//
|
|
||||||
// Its voice is a colder, greyer amber than Dex's. That pays off in reverse:
|
|
||||||
// it never once spoke in the same voice as the friend.
|
|
||||||
func characterMysteryTape() inkwell.Character {
|
func characterMysteryTape() inkwell.Character {
|
||||||
return inkwell.Character{
|
return inkwell.Character{
|
||||||
Name: TapeMystery,
|
Name: TapeMystery,
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// paul is the player character: a junk dealer and street survivor, technically
|
|
||||||
// competent, living on the edge of the city.
|
|
||||||
func characterPaul() inkwell.Character {
|
func characterPaul() inkwell.Character {
|
||||||
return inkwell.Character{
|
return inkwell.Character{
|
||||||
Name: Paul,
|
Name: Paul,
|
||||||
@@ -13,8 +11,7 @@ func characterPaul() inkwell.Character {
|
|||||||
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
|
|
||||||
// it — see the colour rule in README.
|
|
||||||
SpeechColor: Ink,
|
SpeechColor: Ink,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
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 {
|
func characterSupportTape() inkwell.Character {
|
||||||
return inkwell.Character{
|
return inkwell.Character{
|
||||||
Name: TapeSupport,
|
Name: TapeSupport,
|
||||||
|
|||||||
@@ -1,14 +1,5 @@
|
|||||||
package inc
|
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) {
|
func registerContent(w *World) {
|
||||||
registerBackground(w)
|
registerBackground(w)
|
||||||
registerCharacter(w)
|
registerCharacter(w)
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// dexTalk is the standing conversation with Dex. Its branches are gated on
|
|
||||||
// story flags, so the same entry point stays useful as the game progresses.
|
|
||||||
func dialogDexTalk() inkwell.Dialogue {
|
func dialogDexTalk() inkwell.Dialogue {
|
||||||
return inkwell.Dialogue{
|
return inkwell.Dialogue{
|
||||||
Name: DlgDex,
|
Name: DlgDex,
|
||||||
|
|||||||
@@ -1,20 +1,9 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// Dialogues — the game's dialogue trees, one file each.
|
|
||||||
//
|
|
||||||
// Talking to a tape carries the same weight as talking to a human NPC — the
|
|
||||||
// wiki states this explicitly. Tapes are dialogue partners, not menus.
|
|
||||||
//
|
|
||||||
// A note on DialogueChoice.Once: inkwell HIDES a spent choice. The wireframe
|
|
||||||
// deck wanted it struck through but still visible, so the list becomes a
|
|
||||||
// memory of what you already tried. That is still open — see README, "Known
|
|
||||||
// gaps".
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// registerDialog adds every dialogue to the game.
|
|
||||||
func registerDialog(w *World) {
|
func registerDialog(w *World) {
|
||||||
for _, d := range []inkwell.Dialogue{
|
for _, d := range []inkwell.Dialogue{
|
||||||
dialogDexTalk(),
|
dialogDexTalk(),
|
||||||
|
|||||||
@@ -4,9 +4,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// mysteryTapeSilent covers the stretch before the club trigger (beat 6), while
|
|
||||||
// the tape is still mute. Talking to it is really Dex talking about it — the
|
|
||||||
// tape itself says nothing.
|
|
||||||
func dialogMysteryTapeSilent() inkwell.Dialogue {
|
func dialogMysteryTapeSilent() inkwell.Dialogue {
|
||||||
return inkwell.Dialogue{
|
return inkwell.Dialogue{
|
||||||
Name: DlgMysteryTape,
|
Name: DlgMysteryTape,
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
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 {
|
func itemBlackMarketArmilla() inkwell.Item {
|
||||||
return inkwell.Item{
|
return inkwell.Item{
|
||||||
Name: ItemBlackArmilla,
|
Name: ItemBlackArmilla,
|
||||||
@@ -15,8 +13,7 @@ func itemBlackMarketArmilla() inkwell.Item {
|
|||||||
TapeSay(Dex, "Two slots, and both of them lie about the temperature. Don't trade me in for it."),
|
TapeSay(Dex, "Two slots, and both of them lie about the temperature. Don't trade me in for it."),
|
||||||
),
|
),
|
||||||
OnUseWith: map[string]inkwell.Action{
|
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(
|
ItemNoodleLetter: inkwell.Seq(
|
||||||
inkwell.Say(Paul, "A letter and a bracelet. Brilliant."),
|
inkwell.Say(Paul, "A letter and a bracelet. Brilliant."),
|
||||||
TapeSay(Dex, "Nothing. Which is what I'd charge for it."),
|
TapeSay(Dex, "Nothing. Which is what I'd charge for it."),
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// Items — the game's inventory, one file each.
|
|
||||||
//
|
|
||||||
// Personal Tapes are inventory items and quest triggers at the same time: they
|
|
||||||
// go into slot 2 of Paul's Armilla, and loading one activates its effect.
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// registerItem adds every item to the game.
|
|
||||||
func registerItem(w *World) {
|
func registerItem(w *World) {
|
||||||
for _, i := range []inkwell.Item{
|
for _, i := range []inkwell.Item{
|
||||||
itemNoodleLetter(),
|
itemNoodleLetter(),
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
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 {
|
func itemMysteryTape() inkwell.Item {
|
||||||
return inkwell.Item{
|
return inkwell.Item{
|
||||||
Name: ItemMysteryTape,
|
Name: ItemMysteryTape,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// noodleLetter starts the story: it is what brings Paul to the city.
|
|
||||||
func itemNoodleLetter() inkwell.Item {
|
func itemNoodleLetter() inkwell.Item {
|
||||||
return inkwell.Item{
|
return inkwell.Item{
|
||||||
Name: ItemNoodleLetter,
|
Name: ItemNoodleLetter,
|
||||||
|
|||||||
+32
-73
@@ -1,99 +1,67 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// Names — the single source of entity names and world-state keys.
|
|
||||||
//
|
|
||||||
// The names follow the wiki entity catalogue
|
|
||||||
// (services/wiki-pages/pages/projects/realworld/entities) so that content and
|
|
||||||
// design share one vocabulary. Note that the wiki's world-state table spells
|
|
||||||
// its keys in Hungarian (hatosagi_tipp, titokzatos_tape_megvan, romlas_szint,
|
|
||||||
// tape_behelyezes, sikator); the constants below are their English
|
|
||||||
// equivalents — the mapping is one-to-one, in catalogue order.
|
|
||||||
//
|
|
||||||
// These constants are the vocabulary every other category speaks in; nothing
|
|
||||||
// here depends on anything else in the game.
|
|
||||||
|
|
||||||
// Characters.
|
|
||||||
const (
|
const (
|
||||||
Paul = "paul"
|
Paul = "paul"
|
||||||
Dex = "dex" // Personal Tape, permanently in slot 1 of Paul's Armilla
|
Dex = "dex"
|
||||||
|
|
||||||
// Tapes (Personal Tapes) are characters, not props: the wiki is explicit
|
TapeMystery = "tape_mystery"
|
||||||
// that they have their own personality, motivation and voice.
|
TapeSupport = "tape_support"
|
||||||
TapeMystery = "tape_mystery" // = The AI, but that only surfaces in the finale
|
|
||||||
TapeSupport = "tape_support" // the "Indian tech support" tape
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Inventory items.
|
|
||||||
const (
|
const (
|
||||||
ItemNoodleLetter = "noodle_letter"
|
ItemNoodleLetter = "noodle_letter"
|
||||||
ItemMysteryTape = "mystery_tape"
|
ItemMysteryTape = "mystery_tape"
|
||||||
ItemBlackArmilla = "black_market_armilla"
|
ItemBlackArmilla = "black_market_armilla"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Dialogues.
|
|
||||||
const (
|
const (
|
||||||
DlgDex = "dex_talk"
|
DlgDex = "dex_talk"
|
||||||
DlgMysteryTape = "mystery_tape_silent"
|
DlgMysteryTape = "mystery_tape_silent"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Scripts.
|
|
||||||
const (
|
const (
|
||||||
ScriptTapeInsert = "tape_insert"
|
ScriptTapeInsert = "tape_insert"
|
||||||
ScriptFinale = "awakening_finale"
|
ScriptFinale = "awakening_finale"
|
||||||
)
|
)
|
||||||
|
|
||||||
// World state — flags and variables.
|
|
||||||
const (
|
const (
|
||||||
FlagPoliceTip = "police_tip" // wiki: hatosagi_tipp
|
FlagPoliceTip = "police_tip"
|
||||||
FlagHasTape = "has_mystery_tape" // wiki: titokzatos_tape_megvan
|
FlagHasTape = "has_mystery_tape"
|
||||||
FlagTapeSpoke = "tape_spoke" // wiki: tape_megszolalt
|
FlagTapeSpoke = "tape_spoke"
|
||||||
FlagClubEntry = "club_entry" // wiki: klub_bejutas — the shopkeeper let Paul through
|
FlagClubEntry = "club_entry"
|
||||||
|
|
||||||
VarSlot2 = "armilla.slot2"
|
VarSlot2 = "armilla.slot2"
|
||||||
VarArmillaStrip = "armilla.strip"
|
VarArmillaStrip = "armilla.strip"
|
||||||
VarDecay = "decay_level" // wiki: romlas_szint
|
VarDecay = "decay_level"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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 (
|
const (
|
||||||
ScreenSelector = "selector" // wiki: Helyszínválasztó
|
ScreenSelector = "selector"
|
||||||
ScreenPaulShop = "paul_shop" // wiki: Paul boltja / garázsa
|
ScreenPaulShop = "paul_shop"
|
||||||
ScreenNoodleHouse = "noodle_house" // wiki: Noodle háza
|
ScreenNoodleHouse = "noodle_house"
|
||||||
ScreenNormanApartment = "norman_apartment" // wiki: Norman lakása / műhelye
|
ScreenNormanApartment = "norman_apartment"
|
||||||
ScreenPoliceStation = "police_station" // wiki: rendőrség / fogda
|
ScreenPoliceStation = "police_station"
|
||||||
ScreenAlley = "alley" // wiki: sikátor (Noodle háza mögött)
|
ScreenAlley = "alley"
|
||||||
ScreenHackerspace = "hackerspace" // wiki: hackerspace
|
ScreenHackerspace = "hackerspace"
|
||||||
ScreenIceCreamShop = "ice_cream_shop" // wiki: fagyizó
|
ScreenIceCreamShop = "ice_cream_shop"
|
||||||
ScreenTrinketShop = "trinket_shop" // wiki: chinatowni csecsebecse-bolt
|
ScreenTrinketShop = "trinket_shop"
|
||||||
ScreenSmallRestaurant = "small_restaurant" // wiki: kajálda
|
ScreenSmallRestaurant = "small_restaurant"
|
||||||
ScreenSecretClub = "secret_club" // wiki: titkos klub („vízalatti nap")
|
ScreenSecretClub = "secret_club"
|
||||||
ScreenBBSTerminal = "bbs_terminal" // wiki: a terminál / BBS-hozzáférési pont
|
ScreenBBSTerminal = "bbs_terminal"
|
||||||
ScreenStreet = "street" // wiki: köztér / utca
|
ScreenStreet = "street"
|
||||||
ScreenHospital = "hospital" // wiki: kórház / pszichiátria
|
ScreenHospital = "hospital"
|
||||||
ScreenServerFarm = "server_farm" // wiki: szerverfarm / adatközpont
|
ScreenServerFarm = "server_farm"
|
||||||
ScreenSecretLab = "secret_lab" // wiki: titkos kutatólabor
|
ScreenSecretLab = "secret_lab"
|
||||||
ScreenRooftopHideout = "rooftop_hideout" // wiki: Norman tetőterasz-rejteke
|
ScreenRooftopHideout = "rooftop_hideout"
|
||||||
ScreenPublicBBS = "public_bbs" // wiki 💡: nyilvános BBS-terminál
|
ScreenPublicBBS = "public_bbs"
|
||||||
ScreenBVKBranch = "bvk_branch" // wiki 💡: BVK-kirendeltség (San Francisco)
|
ScreenBVKBranch = "bvk_branch"
|
||||||
ScreenCuratorShop = "curator_shop" // wiki 💡: kurátor-szerviz („a Mester")
|
ScreenCuratorShop = "curator_shop"
|
||||||
ScreenScrapMarket = "scrap_market" // wiki 💡: selejt-piac
|
ScreenScrapMarket = "scrap_market"
|
||||||
ScreenSamizdatPress = "samizdat_press" // wiki 💡: samizdat-nyomda / P2P-csomópont
|
ScreenSamizdatPress = "samizdat_press"
|
||||||
ScreenShowroom = "showroom" // wiki 💡: Neumatronic márkakereskedés
|
ScreenShowroom = "showroom"
|
||||||
ScreenColumbarium = "columbarium" // wiki 💡: kolumbárium / Dex emlékhelye
|
ScreenColumbarium = "columbarium"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Backgrounds — one per screen, same order.
|
|
||||||
const (
|
const (
|
||||||
BgSelector = "bg_selector"
|
BgSelector = "bg_selector"
|
||||||
BgPaulShop = "bg_paul_shop"
|
BgPaulShop = "bg_paul_shop"
|
||||||
@@ -121,19 +89,14 @@ const (
|
|||||||
BgColumbarium = "bg_columbarium"
|
BgColumbarium = "bg_columbarium"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Tapes maps a character name to the label shown in the tape channel header.
|
|
||||||
// TapeChannel uses this to decide whether a line belongs in the tape channel
|
|
||||||
// or stays in the scene.
|
|
||||||
var Tapes = map[string]string{
|
var Tapes = map[string]string{
|
||||||
Dex: "DEX",
|
Dex: "DEX",
|
||||||
TapeMystery: "UNKNOWN TAPE",
|
TapeMystery: "UNKNOWN TAPE",
|
||||||
TapeSupport: "TECH SUPPORT",
|
TapeSupport: "TECH SUPPORT",
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsTape reports whether a character is a tape.
|
|
||||||
func IsTape(name string) bool { _, ok := Tapes[name]; return ok }
|
func IsTape(name string) bool { _, ok := Tapes[name]; return ok }
|
||||||
|
|
||||||
// TapeDisplayName is the label shown in the tape channel header.
|
|
||||||
func TapeDisplayName(name string) string {
|
func TapeDisplayName(name string) string {
|
||||||
if d, ok := Tapes[name]; ok {
|
if d, ok := Tapes[name]; ok {
|
||||||
return d
|
return d
|
||||||
@@ -141,16 +104,12 @@ func TapeDisplayName(name string) string {
|
|||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
// TapeItems maps an inventory item that can go into slot 2 to the character
|
|
||||||
// living on it.
|
|
||||||
var TapeItems = map[string]string{
|
var TapeItems = map[string]string{
|
||||||
ItemMysteryTape: TapeMystery,
|
ItemMysteryTape: TapeMystery,
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsTapeItem reports whether an item can be loaded into slot 2.
|
|
||||||
func IsTapeItem(item string) bool { _, ok := TapeItems[item]; return ok }
|
func IsTapeItem(item string) bool { _, ok := TapeItems[item]; return ok }
|
||||||
|
|
||||||
// TapeDialogue is the dialogue belonging to a tape in slot 2.
|
|
||||||
func TapeDialogue(item string) string {
|
func TapeDialogue(item string) string {
|
||||||
switch item {
|
switch item {
|
||||||
case ItemMysteryTape:
|
case ItemMysteryTape:
|
||||||
|
|||||||
+2
-14
@@ -4,21 +4,13 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
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 {
|
func screenAlley() screen {
|
||||||
return screen{
|
return screen{
|
||||||
name: ScreenAlley,
|
name: ScreenAlley,
|
||||||
title: "ALLEY — BEHIND NOODLE'S HOUSE",
|
title: "ALLEY — BEHIND NOODLE'S HOUSE",
|
||||||
background: BgAlley,
|
background: BgAlley,
|
||||||
exits: []exit{
|
exits: []exit{
|
||||||
// On the left: the back door hotspot below owns the other end.
|
|
||||||
{to: ScreenNoodleHouse, label: "back out to the street", side: sideLeft},
|
{to: ScreenNoodleHouse, label: "back out to the street", side: sideLeft},
|
||||||
},
|
},
|
||||||
actors: []inkwell.SceneActor{
|
actors: []inkwell.SceneActor{
|
||||||
@@ -29,8 +21,6 @@ func screenAlley() screen {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
func hidingPlace() inkwell.Hotspot {
|
||||||
return inkwell.Hotspot{
|
return inkwell.Hotspot{
|
||||||
Name: "hiding_place",
|
Name: "hiding_place",
|
||||||
@@ -57,8 +47,6 @@ func hidingPlace() inkwell.Hotspot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
func backDoor() inkwell.Hotspot {
|
||||||
return inkwell.Hotspot{
|
return inkwell.Hotspot{
|
||||||
Name: "back_door",
|
Name: "back_door",
|
||||||
@@ -71,7 +59,7 @@ func backDoor() inkwell.Hotspot {
|
|||||||
),
|
),
|
||||||
OnTalk: inkwell.Say(Paul, "To the door? I'm not there yet."),
|
OnTalk: inkwell.Say(Paul, "To the door? I'm not there yet."),
|
||||||
OnUseWith: map[string]inkwell.Action{
|
OnUseWith: map[string]inkwell.Action{
|
||||||
// An AUTHORED failure — not the engine's hardcoded flash.
|
|
||||||
ItemBlackArmilla: inkwell.Seq(
|
ItemBlackArmilla: inkwell.Seq(
|
||||||
inkwell.Say(Paul, "A black-market bracelet doesn't open a keypad."),
|
inkwell.Say(Paul, "A black-market bracelet doesn't open a keypad."),
|
||||||
TapeSay(Dex, "But you do get an advert out of it."),
|
TapeSay(Dex, "But you do get an advert out of it."),
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// bbsTerminal is the screen you get when Paul sits down at a terminal: the
|
|
||||||
// catalogue calls it a "Tárgy/portál", a physical object in the world that
|
|
||||||
// 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 screenBbsTerminal() screen {
|
func screenBbsTerminal() screen {
|
||||||
return screen{
|
return screen{
|
||||||
name: ScreenBBSTerminal,
|
name: ScreenBBSTerminal,
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// bvkBranch is a 💡 suggestion: the local certification office, imprimatur
|
|
||||||
// stamps and licence checks and numbered waiting rooms — everything a man with
|
|
||||||
// a jailbroken two-slot Armilla should stay away from
|
|
||||||
// (wiki: entities#potenciális-további-helyszínek).
|
|
||||||
func screenBvkBranch() screen {
|
func screenBvkBranch() screen {
|
||||||
return screen{
|
return screen{
|
||||||
name: ScreenBVKBranch,
|
name: ScreenBVKBranch,
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// columbarium is a 💡 suggestion: where Dex is buried. A quiet place, away
|
|
||||||
// from the noise of the terminals — the loss-and-letting-go thread, which is
|
|
||||||
// the same thread as Norman and The AI
|
|
||||||
// (wiki: entities#potenciális-további-helyszínek).
|
|
||||||
func screenColumbarium() screen {
|
func screenColumbarium() screen {
|
||||||
return screen{
|
return screen{
|
||||||
name: ScreenColumbarium,
|
name: ScreenColumbarium,
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// curatorShop is a 💡 suggestion: the last workshop of the declining curators'
|
|
||||||
// guild — quartz tuning, tape repair, craft handed down for generations. The
|
|
||||||
// man who built Paul's second slot (wiki: entities#potenciális-további-helyszínek).
|
|
||||||
func screenCuratorShop() screen {
|
func screenCuratorShop() screen {
|
||||||
return screen{
|
return screen{
|
||||||
name: ScreenCuratorShop,
|
name: ScreenCuratorShop,
|
||||||
|
|||||||
+7
-41
@@ -1,28 +1,5 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// 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 (
|
import (
|
||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
@@ -30,14 +7,12 @@ import (
|
|||||||
type side int
|
type side int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
sideLeft side = iota // off the left edge
|
sideLeft side = iota
|
||||||
sideRight // off the right edge
|
sideRight
|
||||||
sideBack // into the depth of the picture
|
sideBack
|
||||||
sideNear // out towards the camera
|
sideNear
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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 {
|
func (s side) area() inkwell.Shape {
|
||||||
switch s {
|
switch s {
|
||||||
case sideLeft:
|
case sideLeft:
|
||||||
@@ -46,35 +21,26 @@ func (s side) area() inkwell.Shape {
|
|||||||
return inkwell.Rect(584, 40, 56, 212)
|
return inkwell.Rect(584, 40, 56, 212)
|
||||||
case sideBack:
|
case sideBack:
|
||||||
return inkwell.Rect(232, 28, 176, 84)
|
return inkwell.Rect(232, 28, 176, 84)
|
||||||
default: // sideNear
|
default:
|
||||||
return inkwell.Rect(232, 214, 176, 48)
|
return inkwell.Rect(232, 214, 176, 48)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// exit is one way out of a screen.
|
|
||||||
type exit struct {
|
type exit struct {
|
||||||
to string // the screen it leads to; empty means "back the way you came"
|
to string
|
||||||
label string // what the status line calls it
|
label string
|
||||||
side side
|
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
|
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
|
needs string
|
||||||
blocked 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 {
|
func toSelector() exit {
|
||||||
return exit{to: 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
|
|
||||||
// straight back out of the registered scenes, so a test can walk it.
|
|
||||||
func exitName(to string) string {
|
func exitName(to string) string {
|
||||||
if to == "" {
|
if to == "" {
|
||||||
return "exit:back"
|
return "exit:back"
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// 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 screenHackerspace() screen {
|
func screenHackerspace() screen {
|
||||||
return screen{
|
return screen{
|
||||||
name: ScreenHackerspace,
|
name: ScreenHackerspace,
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// hospital is the clinical dead end: Dr. Maren looked for a medical reason for
|
|
||||||
// Norman's state and did not find one (wiki: entities, timeline 21).
|
|
||||||
func screenHospital() screen {
|
func screenHospital() screen {
|
||||||
return screen{
|
return screen{
|
||||||
name: ScreenHospital,
|
name: ScreenHospital,
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// iceCreamShop stands where Paul's old bar used to be, and they insist there
|
|
||||||
// never was a bar. The BVK-registered terminal in the corner is the one the
|
|
||||||
// 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 screenIceCreamShop() screen {
|
func screenIceCreamShop() screen {
|
||||||
return screen{
|
return screen{
|
||||||
name: ScreenIceCreamShop,
|
name: ScreenIceCreamShop,
|
||||||
|
|||||||
+24
-63
@@ -1,80 +1,50 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// Screens — one file per screen.
|
|
||||||
//
|
|
||||||
// "Screen" is this project's word: the wiki's catalogue, the concept-art deck
|
|
||||||
// and the beat tables all count screens. inkwell's word for the same thing is
|
|
||||||
// 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 (screen.exit.go), and — once its
|
|
||||||
// beat is written — its hotspots, NPCs and dialogue. Only the alley has that
|
|
||||||
// last part so far; the rest are greybox.
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// screen is what each file in this package fills in.
|
|
||||||
type screen struct {
|
type screen struct {
|
||||||
name string
|
name string
|
||||||
title string
|
title string
|
||||||
background string
|
background string
|
||||||
|
|
||||||
// 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
|
|
||||||
// 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
|
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
|
hotspots []inkwell.Hotspot
|
||||||
actors []inkwell.SceneActor
|
actors []inkwell.SceneActor
|
||||||
walkboxes []inkwell.Polygon
|
walkboxes []inkwell.Polygon
|
||||||
onEnter inkwell.Action
|
onEnter inkwell.Action
|
||||||
}
|
}
|
||||||
|
|
||||||
// screenDeck is every screen in the game, in the order of the wiki's location
|
|
||||||
// catalogue (entities#helyszinek) — which the concept-art deck follows number
|
|
||||||
// for number. Registration order is this order, and it is also the order the
|
|
||||||
// arrow keys walk (ui.screen_nav.go).
|
|
||||||
//
|
|
||||||
// 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 screenDeck = []func() screen{
|
var screenDeck = []func() screen{
|
||||||
screenPaulShop, // 01
|
screenPaulShop,
|
||||||
screenNoodleHouse, // 02
|
screenNoodleHouse,
|
||||||
screenAlley, // 02, same catalogue row
|
screenAlley,
|
||||||
screenNormanApartment, // 03
|
screenNormanApartment,
|
||||||
screenPoliceStation, // 04
|
screenPoliceStation,
|
||||||
screenHackerspace, // 06
|
screenHackerspace,
|
||||||
screenIceCreamShop, // 07
|
screenIceCreamShop,
|
||||||
screenTrinketShop, // 08
|
screenTrinketShop,
|
||||||
screenSmallRestaurant, // 09
|
screenSmallRestaurant,
|
||||||
screenSecretClub, // 10
|
screenSecretClub,
|
||||||
screenBbsTerminal, // 11
|
screenBbsTerminal,
|
||||||
screenStreet, // 12
|
screenStreet,
|
||||||
screenHospital, // 13
|
screenHospital,
|
||||||
screenServerFarm, // 14
|
screenServerFarm,
|
||||||
screenSecretLab, // 15
|
screenSecretLab,
|
||||||
screenRooftopHideout, // 16
|
screenRooftopHideout,
|
||||||
screenPublicBBS, // 17
|
screenPublicBBS,
|
||||||
screenBvkBranch, // 18
|
screenBvkBranch,
|
||||||
screenCuratorShop, // 19
|
screenCuratorShop,
|
||||||
screenScrapMarket, // 20
|
screenScrapMarket,
|
||||||
screenSamizdatPress, // 21
|
screenSamizdatPress,
|
||||||
screenShowroom, // 22
|
screenShowroom,
|
||||||
screenColumbarium, // 23
|
screenColumbarium,
|
||||||
}
|
}
|
||||||
|
|
||||||
// registerScreen adds every screen to the game, the selector first: it is the centre
|
|
||||||
// of the wiki's location graph, and it is built from the screenDeck rather than
|
|
||||||
// listed in it.
|
|
||||||
func registerScreen(w *World) {
|
func registerScreen(w *World) {
|
||||||
screens := make([]screen, 0, len(screenDeck)+1)
|
screens := make([]screen, 0, len(screenDeck)+1)
|
||||||
for _, f := range screenDeck {
|
for _, f := range screenDeck {
|
||||||
@@ -85,8 +55,6 @@ func registerScreen(w *World) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 screenFloor = []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},
|
||||||
@@ -94,11 +62,6 @@ var screenFloor = []inkwell.Polygon{
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
// screenBuild turns a screen into the engine's scene.
|
|
||||||
//
|
|
||||||
// The authored hotspots go in front of the exits, because the engine takes the
|
|
||||||
// first one whose area contains the click (HotspotAt): a painted thing beats
|
|
||||||
// the edge strip an exit sits on, wherever the two overlap.
|
|
||||||
func screenBuild(w *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 {
|
||||||
@@ -136,8 +99,6 @@ func screenBuild(w *World, s screen) inkwell.Scene {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// setVarIfEmpty only writes when the variable is still unset, so re-entering a
|
|
||||||
// screen does not clobber what the game has set in the meantime.
|
|
||||||
func setVarIfEmpty(name string, v any) inkwell.Action {
|
func setVarIfEmpty(name string, v any) inkwell.Action {
|
||||||
return 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 {
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// noodleHouse is the street front outside Noodle's place. By the time Paul
|
|
||||||
// gets there the police have sealed the house and taken Noodle in, and a
|
|
||||||
// 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 screenNoodleHouse() screen {
|
func screenNoodleHouse() screen {
|
||||||
return screen{
|
return screen{
|
||||||
name: ScreenNoodleHouse,
|
name: ScreenNoodleHouse,
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// normanApartment is the flat Norman was taken from: his Armilla left behind
|
|
||||||
// on the side, his Personal Tape missing from it, his terminal full of the
|
|
||||||
// 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 screenNormanApartment() screen {
|
func screenNormanApartment() screen {
|
||||||
return screen{
|
return screen{
|
||||||
name: ScreenNormanApartment,
|
name: ScreenNormanApartment,
|
||||||
|
|||||||
@@ -1,13 +1,5 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// paulShop is where the game starts: the junk shop and garage Paul lives
|
|
||||||
// behind (wiki: entities#helyszinek). It is the only screen outside San
|
|
||||||
// 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 screenPaulShop() screen {
|
func screenPaulShop() screen {
|
||||||
return screen{
|
return screen{
|
||||||
name: ScreenPaulShop,
|
name: ScreenPaulShop,
|
||||||
|
|||||||
@@ -1,13 +1,5 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// policeStation is where Noodle is held for dealing in cracked Armillas, and
|
|
||||||
// where a clerk quietly tips Paul off about the hiding place
|
|
||||||
// (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 screenPoliceStation() screen {
|
func screenPoliceStation() screen {
|
||||||
return screen{
|
return screen{
|
||||||
name: ScreenPoliceStation,
|
name: ScreenPoliceStation,
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// publicBBS is one of the wiki's 💡 suggestions: the canon has BBS terminals
|
|
||||||
// standing in cafés, post offices and bus stops — live infrastructure, not
|
|
||||||
// relics. An alternative way to read the boards, in a place where somebody can
|
|
||||||
// read over your shoulder (wiki: entities#potenciális-további-helyszínek).
|
|
||||||
func screenPublicBBS() screen {
|
func screenPublicBBS() screen {
|
||||||
return screen{
|
return screen{
|
||||||
name: ScreenPublicBBS,
|
name: ScreenPublicBBS,
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// rooftopHideout is Norman's hiding place: an abandoned roof terrace with his
|
|
||||||
// personal things, his notes and an old BBS terminal — the emotional high point
|
|
||||||
// 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 screenRooftopHideout() screen {
|
func screenRooftopHideout() screen {
|
||||||
return screen{
|
return screen{
|
||||||
name: ScreenRooftopHideout,
|
name: ScreenRooftopHideout,
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// samizdatPress is a 💡 suggestion: the Resistance's physical node — parody
|
|
||||||
// BVK databases, underground publications, a P2P node humming in the cellar.
|
|
||||||
// The samizdat intellectuals are the first to write down that the world is
|
|
||||||
// slipping (wiki: entities#potenciális-további-helyszínek).
|
|
||||||
func screenSamizdatPress() screen {
|
func screenSamizdatPress() screen {
|
||||||
return screen{
|
return screen{
|
||||||
name: ScreenSamizdatPress,
|
name: ScreenSamizdatPress,
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// scrapMarket is a 💡 suggestion: the black market in condemned ACPs and bare,
|
|
||||||
// unstamped tape. Canon says the leak of scrap is not an accident — "something
|
|
||||||
// wants to live on" (wiki: entities#potenciális-további-helyszínek).
|
|
||||||
func screenScrapMarket() screen {
|
func screenScrapMarket() screen {
|
||||||
return screen{
|
return screen{
|
||||||
name: ScreenScrapMarket,
|
name: ScreenScrapMarket,
|
||||||
|
|||||||
@@ -1,13 +1,5 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// secretClub is the empty room behind the shop where the "underwater sun"
|
|
||||||
// company gave their talk, with a secretary who knows about the BVK's plans for
|
|
||||||
// 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 screenSecretClub() screen {
|
func screenSecretClub() screen {
|
||||||
return screen{
|
return screen{
|
||||||
name: ScreenSecretClub,
|
name: ScreenSecretClub,
|
||||||
|
|||||||
@@ -1,13 +1,5 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// secretLab is the lab the BVK's secret section runs, where Norman lies wired
|
|
||||||
// into the simulator: the two-thirds point where Paul finds him, and the
|
|
||||||
// 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 screenSecretLab() screen {
|
func screenSecretLab() screen {
|
||||||
return screen{
|
return screen{
|
||||||
name: ScreenSecretLab,
|
name: ScreenSecretLab,
|
||||||
|
|||||||
+1
-15
@@ -6,17 +6,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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: screenSelector() is built from
|
|
||||||
// whichever screens marked themselves onSelector.
|
|
||||||
func screenSelector(rest []screen) screen {
|
func screenSelector(rest []screen) screen {
|
||||||
var exits []exit
|
var exits []exit
|
||||||
for _, s := range rest {
|
for _, s := range rest {
|
||||||
@@ -34,13 +23,12 @@ func screenSelector(rest []screen) screen {
|
|||||||
title: "SAN FRANCISCO",
|
title: "SAN FRANCISCO",
|
||||||
background: BgSelector,
|
background: BgSelector,
|
||||||
exits: exits,
|
exits: exits,
|
||||||
// Nobody stands on a map.
|
|
||||||
actors: []inkwell.SceneActor{},
|
actors: []inkwell.SceneActor{},
|
||||||
walkboxes: []inkwell.Polygon{},
|
walkboxes: []inkwell.Polygon{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The pin grid: three columns down the scene region, filled top to bottom.
|
|
||||||
const (
|
const (
|
||||||
pinCols = 3
|
pinCols = 3
|
||||||
pinW = 192.0
|
pinW = 192.0
|
||||||
@@ -56,8 +44,6 @@ func pin(i int) inkwell.Shape {
|
|||||||
return inkwell.Rect(pinX+float64(col)*pinGapX, pinY+float64(row)*pinGapY, pinW, pinH)
|
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 {
|
func pinLabel(title string) string {
|
||||||
name, _, _ := strings.Cut(title, " — ")
|
name, _, _ := strings.Cut(title, " — ")
|
||||||
return name
|
return name
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// serverFarm is the cooled, humming corridors where The AI's physical
|
|
||||||
// infrastructure can be seen for what it is. Getting in needs the access tape
|
|
||||||
// from the hackerspace or from Noodle (wiki: entities, timeline 24).
|
|
||||||
func screenServerFarm() screen {
|
func screenServerFarm() screen {
|
||||||
return screen{
|
return screen{
|
||||||
name: ScreenServerFarm,
|
name: ScreenServerFarm,
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// showroom is a 💡 suggestion: the legitimate, glittering side — Armilla
|
|
||||||
// cabinets, licensed operators, matte-black Nokia-punk under showroom lights.
|
|
||||||
// The contrast with Paul's edge of the world
|
|
||||||
// (wiki: entities#potenciális-további-helyszínek).
|
|
||||||
func screenShowroom() screen {
|
func screenShowroom() screen {
|
||||||
return screen{
|
return screen{
|
||||||
name: ScreenShowroom,
|
name: ScreenShowroom,
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// smallRestaurant is the little Chinese place beside the trinket shop, and the
|
|
||||||
// lit mirror in its toilet is the puzzle: it flickers, and something shows
|
|
||||||
// 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 screenSmallRestaurant() screen {
|
func screenSmallRestaurant() screen {
|
||||||
return screen{
|
return screen{
|
||||||
name: ScreenSmallRestaurant,
|
name: ScreenSmallRestaurant,
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// street is the city itself: pavements, a square, a metro entrance, and the
|
|
||||||
// environmental decay that tracks The AI's spread — glitching systems, odd
|
|
||||||
// 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 screenStreet() screen {
|
func screenStreet() screen {
|
||||||
return screen{
|
return screen{
|
||||||
name: ScreenStreet,
|
name: ScreenStreet,
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// trinketShop is the Chinatown shop at the club's address, whose owner sells
|
|
||||||
// Paul junk he does not need and is, in fact, the club's doorman
|
|
||||||
// (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 screenTrinketShop() screen {
|
func screenTrinketShop() screen {
|
||||||
return screen{
|
return screen{
|
||||||
name: ScreenTrinketShop,
|
name: ScreenTrinketShop,
|
||||||
|
|||||||
@@ -4,12 +4,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// awakeningFinale is the end of game two. The point of it, mechanically, is
|
|
||||||
// the theme switch: this is where the Nokia-punk texture breaks in.
|
|
||||||
//
|
|
||||||
// Run it early and often during development (`go run . -finale`) — it is the
|
|
||||||
// single most important visual beat in the game, so you want to be looking at
|
|
||||||
// it long before the surrounding scene exists.
|
|
||||||
func scriptAwakeningFinale(w *World) inkwell.Script {
|
func scriptAwakeningFinale(w *World) inkwell.Script {
|
||||||
return inkwell.Script{
|
return inkwell.Script{
|
||||||
Name: ScriptFinale,
|
Name: ScriptFinale,
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// Scripts — the game's named, reusable action sequences:
|
|
||||||
// cutscenes and recurring logic, built from the inkwell action set.
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// registerScript adds every script to the game.
|
|
||||||
func registerScript(w *World) {
|
func registerScript(w *World) {
|
||||||
for _, s := range []inkwell.Script{
|
for _, s := range []inkwell.Script{
|
||||||
scriptTapeInsert(w),
|
scriptTapeInsert(w),
|
||||||
|
|||||||
@@ -4,9 +4,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
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 {
|
func scriptTapeInsert(w *World) inkwell.Script {
|
||||||
return inkwell.Script{
|
return inkwell.Script{
|
||||||
Name: ScriptTapeInsert,
|
Name: ScriptTapeInsert,
|
||||||
|
|||||||
+4
-26
@@ -1,45 +1,28 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// Themes — the game's two colour themes and its colour tokens.
|
|
||||||
//
|
|
||||||
// In canon the machines of this world are matte black with a green or amber
|
|
||||||
// character display, and in the Real World finale "the Nokia-punk texture
|
|
||||||
// breaks in" — with inkwell's runtime theme switching that beat is a single
|
|
||||||
// line (see UseTheme and scriptAwakeningFinale).
|
|
||||||
//
|
|
||||||
// The colour rule: tapes speak in amber (green in Nokia-punk), the world and
|
|
||||||
// Paul speak in bone white. If something is amber, a tape said it.
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"image/color"
|
"image/color"
|
||||||
|
|
||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The two theme names. Runtime switching (inkwell UseTheme) refers to these.
|
|
||||||
const (
|
const (
|
||||||
RealWorld = "realworld-93"
|
RealWorld = "realworld-93"
|
||||||
NokiaPunk = "nokia-punk"
|
NokiaPunk = "nokia-punk"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RGB builds an opaque colour from a hex literal.
|
|
||||||
func RGB(hex uint32) color.Color {
|
func RGB(hex uint32) color.Color {
|
||||||
return color.RGBA{R: uint8(hex >> 16), G: uint8(hex >> 8), B: uint8(hex), A: 0xff}
|
return color.RGBA{R: uint8(hex >> 16), G: uint8(hex >> 8), B: uint8(hex), A: 0xff}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RGBA builds a colour with the given alpha from a hex literal.
|
|
||||||
func RGBA(hex uint32, a uint8) color.Color {
|
func RGBA(hex uint32, a uint8) color.Color {
|
||||||
return color.RGBA{R: uint8(hex >> 16), G: uint8(hex >> 8), B: uint8(hex), A: a}
|
return color.RGBA{R: uint8(hex >> 16), G: uint8(hex >> 8), B: uint8(hex), A: a}
|
||||||
}
|
}
|
||||||
|
|
||||||
// realworld-93 — 95% of the game. Muted earth tones, black HUD, amber tape
|
|
||||||
// channel. The Neumatronic layer is not visible yet.
|
|
||||||
// The realworld-93 palette. Ink and Amber are the two sides of the two-channel
|
|
||||||
// colour rule: the world speaks in bone white, tapes speak in amber.
|
|
||||||
var (
|
var (
|
||||||
Ink = RGB(0xDCD5C4) // bone white — the voice of the world and of Paul
|
Ink = RGB(0xDCD5C4)
|
||||||
InkDim = RGB(0x8A806B)
|
InkDim = RGB(0x8A806B)
|
||||||
Amber = RGB(0xE0A33E) // tape voice
|
Amber = RGB(0xE0A33E)
|
||||||
AmberLo = RGB(0xA8701A)
|
AmberLo = RGB(0xA8701A)
|
||||||
black = RGB(0x14120E)
|
black = RGB(0x14120E)
|
||||||
panel = RGB(0x0D0B08)
|
panel = RGB(0x0D0B08)
|
||||||
@@ -82,8 +65,8 @@ func realWorldTheme() inkwell.Theme {
|
|||||||
TopBarAccent: Amber,
|
TopBarAccent: Amber,
|
||||||
|
|
||||||
ChatLogBG: panel,
|
ChatLogBG: panel,
|
||||||
ChatLogPrompt: InkDim, // your own actions — quiet
|
ChatLogPrompt: InkDim,
|
||||||
ChatLogResponse: Amber, // tape voice
|
ChatLogResponse: Amber,
|
||||||
ChatLogSystem: RGB(0x6B6353),
|
ChatLogSystem: RGB(0x6B6353),
|
||||||
|
|
||||||
CharacterPanelBG: panel,
|
CharacterPanelBG: panel,
|
||||||
@@ -92,10 +75,6 @@ func realWorldTheme() inkwell.Theme {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// nokia-punk — at peak decay, on BBS/ECHO terminals, and in the finale.
|
|
||||||
// Matte black and phosphor green: a tuned variant of the engine's
|
|
||||||
// terminal-green preset.
|
|
||||||
// The nokia-punk palette — phosphor green on black glass.
|
|
||||||
var (
|
var (
|
||||||
Green = RGB(0x3BE86B)
|
Green = RGB(0x3BE86B)
|
||||||
GreenLo = RGB(0x1F7A39)
|
GreenLo = RGB(0x1F7A39)
|
||||||
@@ -148,7 +127,6 @@ func nokiaPunkTheme() inkwell.Theme {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// registerTheme adds both themes to the game.
|
|
||||||
func registerTheme(g *inkwell.Game) {
|
func registerTheme(g *inkwell.Game) {
|
||||||
g.ThemeManager.Register(realWorldTheme())
|
g.ThemeManager.Register(realWorldTheme())
|
||||||
g.ThemeManager.Register(nokiaPunkTheme())
|
g.ThemeManager.Register(nokiaPunkTheme())
|
||||||
|
|||||||
+2
-9
@@ -1,12 +1,5 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// Letterbox — the cutscene frame.
|
|
||||||
//
|
|
||||||
// The one exception to the fixed layout. In cutscene mode the HUD strip and
|
|
||||||
// the TopBar are hidden, the scene gets a black bar top and bottom, and a line
|
|
||||||
// at the foot of the screen says a tape would like to speak — but it does not
|
|
||||||
// speak on its own. The floor is the player's: a tape signals, never interrupts.
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
"github.com/hajimehoshi/ebiten/v2"
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
@@ -17,7 +10,7 @@ import (
|
|||||||
type Letterbox struct {
|
type Letterbox struct {
|
||||||
Name string
|
Name string
|
||||||
W *World
|
W *World
|
||||||
Bar float64 // height of the black bar, top and bottom
|
Bar float64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Letterbox) GetName() string { return l.Name }
|
func (l *Letterbox) GetName() string { return l.Name }
|
||||||
@@ -26,7 +19,7 @@ func (l *Letterbox) Tick(ctx *inkwell.UICtx) {
|
|||||||
if l.W.Mode() != 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.
|
|
||||||
if inpututil.IsKeyJustPressed(ebiten.KeySpace) {
|
if inpututil.IsKeyJustPressed(ebiten.KeySpace) {
|
||||||
l.W.TakeTapeOffer()
|
l.W.TakeTapeOffer()
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-86
@@ -1,85 +1,35 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// HUD layout.
|
|
||||||
//
|
|
||||||
// 0 394│396 639
|
|
||||||
// ┌────────────────────────────────────────────────────────────────────┐
|
|
||||||
// 0 │ ALLEY — paused SRP: 1 tape │ TopBar (20)
|
|
||||||
// 20 ├────────────────────────────────────────────────────────────────────┤
|
|
||||||
// │ │
|
|
||||||
// │ scene — hotspots, characters, SpeechBubble │ Scene (242)
|
|
||||||
// │ │
|
|
||||||
// 262├─────────────────────────────────────────┬──────────────────────────┤
|
|
||||||
// │ Use hook on: floor grate │ DEX │
|
|
||||||
// │ ┌───┬───┬───┬───┐ ┌───────┐┌────────┐ │ "The hook is a hand │ HUD (118)
|
|
||||||
// │ ├───┼───┼───┼───┤ │ 1 DEX ││ 2 — │ │ shorter than the gap." │
|
|
||||||
// 379└─────────────────────────────────────────┴──────────────────────────┘
|
|
||||||
// InventoryBar TapeSlots TapeChannel
|
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
//
|
|
||||||
// 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 (
|
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"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Screen and layout, in internal (unscaled) pixels.
|
|
||||||
const (
|
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
|
ScreenW = 640
|
||||||
ScreenH = 380
|
ScreenH = 380
|
||||||
|
|
||||||
// LineH is one text row: the glyph cell plus leading. Every text-bearing
|
LineH = glyphH + 2
|
||||||
// box is sized as a multiple of this, so rows can never overlap.
|
Pad = 6
|
||||||
LineH = glyphH + 2 // 18
|
|
||||||
Pad = 6 // inner padding for every panel
|
|
||||||
|
|
||||||
TopBarH = LineH + 2 // 20 — one row plus a hairline of breathing room
|
TopBarH = LineH + 2
|
||||||
DividerX = 394 // world | tape channel (61.6% of the width)
|
DividerX = 394
|
||||||
|
|
||||||
// The HUD strip is as short as its own contents allow, and is measured
|
HUDH = Pad + LineH + 4 + invSlot*2 + invGap + Pad
|
||||||
// from the bottom of the screen — every pixel it takes is a pixel of the
|
HUDTop = ScreenH - HUDH
|
||||||
// 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
|
||||||
statusY = HUDTop + Pad // 268
|
slotsY = HUDTop + Pad + LineH
|
||||||
slotsY = HUDTop + Pad + LineH // 286
|
slotsH = LineH*2 + Pad*2
|
||||||
slotsH = LineH*2 + Pad*2 // 48 — two rows plus padding
|
invY = slotsY + 4
|
||||||
invY = slotsY + 4 // 290
|
|
||||||
invSlot = 40
|
invSlot = 40
|
||||||
invGap = 4
|
invGap = 4
|
||||||
)
|
)
|
||||||
|
|
||||||
// registerUI assembles the whole HUD in the conventional Z-order: the debug
|
|
||||||
// overlay at the back, the cursor in front.
|
|
||||||
func registerUI(w *World) {
|
func registerUI(w *World) {
|
||||||
g := w.G
|
g := w.G
|
||||||
|
|
||||||
// The guard goes FIRST so it ticks LAST among the widgets — widgets tick
|
|
||||||
// in reverse registration order, so every HUD widget gets the click before
|
|
||||||
// it does, and it only ever catches clicks that land on the scene.
|
|
||||||
g.UIManager.Register(&UseWithGuard{Name: "usewith_guard", W: w})
|
g.UIManager.Register(&UseWithGuard{Name: "usewith_guard", W: w})
|
||||||
g.UIManager.Register(&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})
|
||||||
@@ -89,9 +39,7 @@ func registerUI(w *World) {
|
|||||||
top := &inkwell.TopBar{
|
top := &inkwell.TopBar{
|
||||||
Name: "topbar",
|
Name: "topbar",
|
||||||
Height: TopBarH,
|
Height: TopBarH,
|
||||||
// The right section prints State.Var(TimeVar). We deliberately reuse
|
|
||||||
// it for the Armilla's single-line phosphor strip: in canon the device
|
|
||||||
// display is one line of text, not a phone screen.
|
|
||||||
TimeVar: VarArmillaStrip,
|
TimeVar: VarArmillaStrip,
|
||||||
}
|
}
|
||||||
w.SetTopBar(top)
|
w.SetTopBar(top)
|
||||||
@@ -119,15 +67,12 @@ func registerUI(w *World) {
|
|||||||
g.UIManager.Register(&inkwell.SpeechBubble{
|
g.UIManager.Register(&inkwell.SpeechBubble{
|
||||||
Name: "speech", MaxWidth: 360, Padding: Pad, OffsetY: 8, FallbackY: TopBarH + Pad,
|
Name: "speech", MaxWidth: 360, Padding: Pad, OffsetY: 8, FallbackY: TopBarH + Pad,
|
||||||
})
|
})
|
||||||
// The dialogue box deliberately covers only the left region, so a tape can
|
|
||||||
// comment ALONGSIDE the conversation. It consumes every click while active,
|
|
||||||
// so the tape channel stays visible but inert — which is what we want.
|
|
||||||
g.UIManager.Register(&inkwell.DialogBox{
|
g.UIManager.Register(&inkwell.DialogBox{
|
||||||
Name: "dialog", Bounds: inkwell.Rect(0, ScreenH-LineH*9, DividerX, LineH*9),
|
Name: "dialog", Bounds: inkwell.Rect(0, ScreenH-LineH*9, DividerX, LineH*9),
|
||||||
LineHeight: LineH, Padding: Pad,
|
LineHeight: LineH, Padding: Pad,
|
||||||
})
|
})
|
||||||
// A verb coin, not a verb bar: a permanent VerbBar would eat the left
|
|
||||||
// region, leaving room for neither the inventory nor the tape slots.
|
|
||||||
g.UIManager.Register(&inkwell.RadialVerbs{
|
g.UIManager.Register(&inkwell.RadialVerbs{
|
||||||
Name: "verbs", Trigger: inkwell.MouseButtonRight, Radius: 58,
|
Name: "verbs", Trigger: inkwell.MouseButtonRight, Radius: 58,
|
||||||
Labels: map[string]string{
|
Labels: map[string]string{
|
||||||
@@ -138,13 +83,6 @@ func registerUI(w *World) {
|
|||||||
g.UIManager.Register(&inkwell.Cursor{Name: "cursor"})
|
g.UIManager.Register(&inkwell.Cursor{Name: "cursor"})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- windowSizer ------------------------------------------------------
|
|
||||||
|
|
||||||
// windowSizer resizes the window once, on the first frame.
|
|
||||||
//
|
|
||||||
// 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 {
|
type windowSizer struct {
|
||||||
Name string
|
Name string
|
||||||
Scale int
|
Scale int
|
||||||
@@ -166,11 +104,6 @@ func (s *windowSizer) Tick(ctx *inkwell.UICtx) {
|
|||||||
ebiten.SetWindowSize(ctx.Game.Width*scale, ctx.Game.Height*scale)
|
ebiten.SetWindowSize(ctx.Game.Width*scale, ctx.Game.Height*scale)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- gate -------------------------------------------------------------
|
|
||||||
|
|
||||||
// gate switches built-in widgets off in cutscene and menu mode. inkwell's
|
|
||||||
// widgets have no Visible field and the Manager cannot unregister, so we wrap.
|
|
||||||
// BlocksClickAt is forwarded, otherwise the verb coin would open over the HUD.
|
|
||||||
type gate struct {
|
type gate struct {
|
||||||
Name string
|
Name string
|
||||||
W *World
|
W *World
|
||||||
@@ -203,10 +136,6 @@ func (n *gate) BlocksClickAt(p inkwell.Point) bool {
|
|||||||
return ok && b.BlocksClickAt(p)
|
return ok && b.BlocksClickAt(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- hudFrame ---------------------------------------------------------
|
|
||||||
|
|
||||||
// hudFrame paints the HUD strip's backdrop and the two dividers. The rule: the
|
|
||||||
// two channels always have a visible, continuous separator between them.
|
|
||||||
type hudFrame struct {
|
type hudFrame struct {
|
||||||
Name string
|
Name string
|
||||||
W *World
|
W *World
|
||||||
@@ -221,9 +150,9 @@ func (h *hudFrame) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) {
|
|||||||
}
|
}
|
||||||
th := ctx.Game.Theme()
|
th := ctx.Game.Theme()
|
||||||
vector.DrawFilledRect(dst, 0, HUDTop+1, ScreenW, ScreenH-HUDTop-1, th.PanelBG, false)
|
vector.DrawFilledRect(dst, 0, HUDTop+1, ScreenW, ScreenH-HUDTop-1, th.PanelBG, false)
|
||||||
// horizontal: scene | HUD
|
|
||||||
vector.StrokeLine(dst, 0, HUDTop, ScreenW, HUDTop, 1, th.CharacterPanelBorder, false)
|
vector.StrokeLine(dst, 0, HUDTop, ScreenW, HUDTop, 1, th.CharacterPanelBorder, false)
|
||||||
// vertical: world | tape
|
|
||||||
vector.StrokeLine(dst, DividerX, HUDTop+1, DividerX, ScreenH, 1, th.CharacterPanelBorder, false)
|
vector.StrokeLine(dst, DividerX, HUDTop+1, DividerX, ScreenH, 1, th.CharacterPanelBorder, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-16
@@ -1,18 +1,5 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// screenNav — walking the deck with the arrow keys.
|
|
||||||
//
|
|
||||||
// The screens are connected — each one records where you can get to from it
|
|
||||||
// (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 (
|
import (
|
||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
"github.com/hajimehoshi/ebiten/v2"
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
@@ -32,7 +19,7 @@ func (n *screenNav) Tick(ctx *inkwell.UICtx) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
step := 0
|
step := 0
|
||||||
// inkwell's Input only covers the mouse; read the keys directly.
|
|
||||||
switch {
|
switch {
|
||||||
case inpututil.IsKeyJustPressed(ebiten.KeyArrowRight):
|
case inpututil.IsKeyJustPressed(ebiten.KeyArrowRight):
|
||||||
step = 1
|
step = 1
|
||||||
@@ -46,8 +33,6 @@ func (n *screenNav) Tick(ctx *inkwell.UICtx) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// neighbour returns the screen step places from the current one, wrapping.
|
|
||||||
// Registration order is the deck order — see 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 {
|
||||||
|
|||||||
@@ -6,9 +6,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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) {
|
func TestScreenNavWrapsBothWays(t *testing.T) {
|
||||||
g := inkwell.NewGame("t", ScreenW, ScreenH)
|
g := inkwell.NewGame("t", ScreenW, ScreenH)
|
||||||
for _, n := range []string{"a", "b", "c"} {
|
for _, n := range []string{"a", "b", "c"} {
|
||||||
|
|||||||
+2
-22
@@ -1,18 +1,5 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// TapeChannel — the tape channel down the right-hand side.
|
|
||||||
//
|
|
||||||
// Not assistant UI: this is the voice of CHARACTERS. The wiki is explicit that
|
|
||||||
// ACPs and Personal Tapes are not props but characters with their own voice.
|
|
||||||
// That is why we do not use the built-in ChatLog — it paints every LogResponse
|
|
||||||
// in one Theme.ChatLogResponse colour, whereas here three or four tapes can
|
|
||||||
// speak into the same strip (Dex, the mystery tape, the tech support tape) and
|
|
||||||
// the colour comes per speaker from Character.SpeechColor.
|
|
||||||
//
|
|
||||||
// Paul and the NPCs never appear here: they speak in the scene, through the
|
|
||||||
// SpeechBubble. The channel is what the tapes said, plus — quietly — what you
|
|
||||||
// did to prompt it. That gives the "joke book, not a scoreboard" log for free.
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"image/color"
|
"image/color"
|
||||||
|
|
||||||
@@ -31,8 +18,6 @@ func (t *TapeChannel) GetName() string { return t.Name }
|
|||||||
|
|
||||||
func (t *TapeChannel) Tick(ctx *inkwell.UICtx) {}
|
func (t *TapeChannel) Tick(ctx *inkwell.UICtx) {}
|
||||||
|
|
||||||
// BlocksClickAt — the channel is not interactive, but it is a solid input
|
|
||||||
// zone: the verb coin must not open over it.
|
|
||||||
func (t *TapeChannel) BlocksClickAt(p inkwell.Point) bool {
|
func (t *TapeChannel) BlocksClickAt(p inkwell.Point) bool {
|
||||||
return t.W.HUDVisible() && t.Bounds.Contains(p)
|
return t.W.HUDVisible() && t.Bounds.Contains(p)
|
||||||
}
|
}
|
||||||
@@ -51,7 +36,6 @@ func (t *TapeChannel) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) {
|
|||||||
|
|
||||||
vector.DrawFilledRect(dst, float32(b.X), float32(b.Y), float32(b.W), float32(b.H), th.ChatLogBG, false)
|
vector.DrawFilledRect(dst, float32(b.X), float32(b.Y), float32(b.W), float32(b.H), th.ChatLogBG, false)
|
||||||
|
|
||||||
// Header: the name of whichever tape spoke last — not "console".
|
|
||||||
speaker, speakerCol := t.lastSpeaker(g, th)
|
speaker, speakerCol := t.lastSpeaker(g, th)
|
||||||
drawTextC(dst, speaker, int(b.X)+pad, int(b.Y)+pad, speakerCol)
|
drawTextC(dst, speaker, int(b.X)+pad, int(b.Y)+pad, speakerCol)
|
||||||
hy := float32(b.Y+pad+lh) + 2
|
hy := float32(b.Y+pad+lh) + 2
|
||||||
@@ -69,12 +53,11 @@ func (t *TapeChannel) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) {
|
|||||||
var txt string
|
var txt string
|
||||||
switch m.Kind {
|
switch m.Kind {
|
||||||
case inkwell.LogAction:
|
case inkwell.LogAction:
|
||||||
// Your own actions — quiet, but visible: that is what makes the
|
|
||||||
// joke book readable.
|
|
||||||
col, txt = th.ChatLogPrompt, m.Text
|
col, txt = th.ChatLogPrompt, m.Text
|
||||||
case inkwell.LogResponse:
|
case inkwell.LogResponse:
|
||||||
if !IsTape(m.Speaker) {
|
if !IsTape(m.Speaker) {
|
||||||
continue // Paul and the NPCs speak in the scene, not here
|
continue
|
||||||
}
|
}
|
||||||
col, txt = speechColor(g, m.Speaker, th.ChatLogResponse), m.Text
|
col, txt = speechColor(g, m.Speaker, th.ChatLogResponse), m.Text
|
||||||
default:
|
default:
|
||||||
@@ -98,8 +81,6 @@ func (t *TapeChannel) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// lastSpeaker returns the display name and colour of whichever tape spoke
|
|
||||||
// last, defaulting to the occupant of slot 1.
|
|
||||||
func (t *TapeChannel) lastSpeaker(g *inkwell.Game, th inkwell.Theme) (string, color.Color) {
|
func (t *TapeChannel) lastSpeaker(g *inkwell.Game, th inkwell.Theme) (string, color.Color) {
|
||||||
msgs := g.Messages()
|
msgs := g.Messages()
|
||||||
for i := len(msgs) - 1; i >= 0; i-- {
|
for i := len(msgs) - 1; i >= 0; i-- {
|
||||||
@@ -111,7 +92,6 @@ func (t *TapeChannel) lastSpeaker(g *inkwell.Game, th inkwell.Theme) (string, co
|
|||||||
return TapeDisplayName(Dex), speechColor(g, Dex, th.ChatLogResponse)
|
return TapeDisplayName(Dex), speechColor(g, Dex, th.ChatLogResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
// speechColor is the character's voice colour, falling back to the theme's.
|
|
||||||
func speechColor(g *inkwell.Game, name string, fallback color.Color) color.Color {
|
func speechColor(g *inkwell.Game, name string, fallback color.Color) color.Color {
|
||||||
if c, ok := g.CharacterManager.Get(name); ok && c.SpeechColor != nil {
|
if c, ok := g.CharacterManager.Get(name); ok && c.SpeechColor != nil {
|
||||||
return c.SpeechColor
|
return c.SpeechColor
|
||||||
|
|||||||
+1
-22
@@ -1,20 +1,5 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// TapeSlots — the two slots of Paul's Armilla.
|
|
||||||
//
|
|
||||||
// In canon Paul's Armilla has TWO slots where the standard has one: Dex sits
|
|
||||||
// permanently in the first, and tapes picked up during the game go into the
|
|
||||||
// second. The wireframe deck does not know about this at all — this is the one
|
|
||||||
// HUD element that comes purely from the wiki.
|
|
||||||
//
|
|
||||||
// Interaction follows the selected verb, like everything else:
|
|
||||||
//
|
|
||||||
// talk → converse with whoever occupies the slot
|
|
||||||
// look → describe the slot
|
|
||||||
// use + a selected tape → load it into slot 2
|
|
||||||
//
|
|
||||||
// Nothing here touches the world: this is the Vox side of the two channels.
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
"github.com/hajimehoshi/ebiten/v2"
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
@@ -33,7 +18,6 @@ func (t *TapeSlots) BlocksClickAt(p inkwell.Point) bool {
|
|||||||
return t.W.HUDVisible() && t.Bounds.Contains(p)
|
return t.W.HUDVisible() && t.Bounds.Contains(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
// slotRects splits the widget bounds into the two slot rectangles.
|
|
||||||
func (t *TapeSlots) slotRects() (inkwell.Rectangle, inkwell.Rectangle) {
|
func (t *TapeSlots) slotRects() (inkwell.Rectangle, inkwell.Rectangle) {
|
||||||
b := t.Bounds
|
b := t.Bounds
|
||||||
w := (b.W - Pad) / 2
|
w := (b.W - Pad) / 2
|
||||||
@@ -48,7 +32,6 @@ func (t *TapeSlots) Tick(ctx *inkwell.UICtx) {
|
|||||||
mp := g.Input.Point()
|
mp := g.Input.Point()
|
||||||
r1, r2 := t.slotRects()
|
r1, r2 := t.slotRects()
|
||||||
|
|
||||||
// Hover label, so the StatusLine spells out what is about to happen.
|
|
||||||
switch {
|
switch {
|
||||||
case r1.Contains(mp):
|
case r1.Contains(mp):
|
||||||
g.SetHoverLabel(TapeDisplayName(Dex))
|
g.SetHoverLabel(TapeDisplayName(Dex))
|
||||||
@@ -71,7 +54,6 @@ func (t *TapeSlots) Tick(ctx *inkwell.UICtx) {
|
|||||||
slot2 := r2.Contains(mp)
|
slot2 := r2.Contains(mp)
|
||||||
sel := g.Inventory.Selected()
|
sel := g.Inventory.Selected()
|
||||||
|
|
||||||
// Loading a tape into slot 2.
|
|
||||||
if slot2 && sel != "" {
|
if slot2 && sel != "" {
|
||||||
g.Inventory.Select("")
|
g.Inventory.Select("")
|
||||||
if IsTapeItem(sel) {
|
if IsTapeItem(sel) {
|
||||||
@@ -99,7 +81,7 @@ func (t *TapeSlots) Tick(ctx *inkwell.UICtx) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
t.W.Do(TapeSay(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:
|
||||||
if !slot2 {
|
if !slot2 {
|
||||||
t.W.Do(TapeSay(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
|
||||||
@@ -120,8 +102,6 @@ func (t *TapeSlots) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) {
|
|||||||
th := g.Theme()
|
th := g.Theme()
|
||||||
r1, r2 := t.slotRects()
|
r1, r2 := t.slotRects()
|
||||||
|
|
||||||
// Two text rows per slot: the slot number on top, its occupant below.
|
|
||||||
// Both rows are LineH tall, so nothing can overlap the border.
|
|
||||||
drawSlot := func(r inkwell.Rectangle, label, body string, filled bool) {
|
drawSlot := func(r inkwell.Rectangle, label, body string, filled bool) {
|
||||||
bg, border, col := th.InventorySlotBG, th.ChatLogSystem, th.ChatLogSystem
|
bg, border, col := th.InventorySlotBG, th.ChatLogSystem, th.ChatLogSystem
|
||||||
if filled {
|
if filled {
|
||||||
@@ -143,7 +123,6 @@ func (t *TapeSlots) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// itemLabel is a short display label for an inventory item.
|
|
||||||
func itemLabel(g *inkwell.Game, name string) string {
|
func itemLabel(g *inkwell.Game, name string) string {
|
||||||
if it, ok := g.ItemManager.Get(name); ok && it.Description != "" {
|
if it, ok := g.ItemManager.Get(name); ok && it.Description != "" {
|
||||||
return it.Description
|
return it.Description
|
||||||
|
|||||||
@@ -1,15 +1,5 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// The HUD's coloured text rendering.
|
|
||||||
//
|
|
||||||
// inkwell's drawText (asset.text.go) discards its colour argument (`_ = c`) and
|
|
||||||
// always renders white through the ebitenutil debug font. The two-channel
|
|
||||||
// colour rule — "if something is amber, a tape said it" — cannot work through
|
|
||||||
// it, so the custom widgets do not call Game.DrawText: they render glyphs onto
|
|
||||||
// a scratch image and blit it tinted with ColorScale. The built-in widgets
|
|
||||||
// (StatusLine, DialogBox, TopBar) still render white. See README, "Engine
|
|
||||||
// workarounds".
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
@@ -19,9 +9,6 @@ import (
|
|||||||
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
// glyphW and glyphH are the ebitenutil debug font's cell. The engine's own
|
|
||||||
// textWidth uses the same 6px but counts bytes, which mismeasures any
|
|
||||||
// non-ASCII text; we count runes.
|
|
||||||
const (
|
const (
|
||||||
glyphW = 6
|
glyphW = 6
|
||||||
glyphH = 16
|
glyphH = 16
|
||||||
@@ -29,10 +16,8 @@ const (
|
|||||||
|
|
||||||
var scratch *ebiten.Image
|
var scratch *ebiten.Image
|
||||||
|
|
||||||
// textW is the pixel width of a string, counted in runes.
|
|
||||||
func textW(s string) int { return utf8.RuneCountInString(s) * glyphW }
|
func textW(s string) int { return utf8.RuneCountInString(s) * glyphW }
|
||||||
|
|
||||||
// drawTextC draws one line in colour c, with the same origin as DebugPrintAt.
|
|
||||||
func drawTextC(dst *ebiten.Image, s string, x, y int, c color.Color) {
|
func drawTextC(dst *ebiten.Image, s string, x, y int, c color.Color) {
|
||||||
if s == "" {
|
if s == "" {
|
||||||
return
|
return
|
||||||
@@ -60,8 +45,6 @@ func drawTextC(dst *ebiten.Image, s string, x, y int, c color.Color) {
|
|||||||
dst.DrawImage(scratch.SubImage(image.Rect(0, 0, w, h)).(*ebiten.Image), op)
|
dst.DrawImage(scratch.SubImage(image.Rect(0, 0, w, h)).(*ebiten.Image), op)
|
||||||
}
|
}
|
||||||
|
|
||||||
// wrap breaks s at word boundaries into lines no wider than maxPx. The engine's
|
|
||||||
// wrapText is unexported and byte-based, hence our own.
|
|
||||||
func wrap(s string, maxPx int) []string {
|
func wrap(s string, maxPx int) []string {
|
||||||
if maxPx <= glyphW || textW(s) <= maxPx {
|
if maxPx <= glyphW || textW(s) <= maxPx {
|
||||||
return []string{s}
|
return []string{s}
|
||||||
@@ -108,7 +91,6 @@ func wrap(s string, maxPx int) []string {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// clip truncates a string so it fits into maxPx.
|
|
||||||
func clip(s string, maxPx int) string {
|
func clip(s string, maxPx int) string {
|
||||||
r := []rune(s)
|
r := []rune(s)
|
||||||
max := maxPx / glyphW
|
max := maxPx / glyphW
|
||||||
|
|||||||
+2
-12
@@ -2,8 +2,6 @@ package inc
|
|||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
// The engine's textWidth counts bytes, which mismeasures any non-ASCII string.
|
|
||||||
// Ours counts runes — pin that down, because wrapping depends on it.
|
|
||||||
func TestTextWidthCountsRunes(t *testing.T) {
|
func TestTextWidthCountsRunes(t *testing.T) {
|
||||||
if got, want := textW("naïve"), 5*glyphW; got != want {
|
if got, want := textW("naïve"), 5*glyphW; got != want {
|
||||||
t.Errorf("textW(naïve) = %d, want %d", got, want)
|
t.Errorf("textW(naïve) = %d, want %d", got, want)
|
||||||
@@ -35,11 +33,6 @@ func TestClipFits(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regression guard for the bug that made the first build unreadable: the
|
|
||||||
// layout was derived from the wireframe deck's ratios, where text is 2.8% of
|
|
||||||
// the screen height, while the engine's debug font is a fixed 6×16 cell. At
|
|
||||||
// 320×200 that is 8% — so rows overlapped and the top bar overflowed into the
|
|
||||||
// scene. Every text row must be at least as tall as the glyph cell.
|
|
||||||
func TestLayoutFitsTheFont(t *testing.T) {
|
func TestLayoutFitsTheFont(t *testing.T) {
|
||||||
if LineH < glyphH {
|
if LineH < glyphH {
|
||||||
t.Errorf("LineH (%d) is shorter than the glyph cell (%d): rows will overlap", LineH, glyphH)
|
t.Errorf("LineH (%d) is shorter than the glyph cell (%d): rows will overlap", LineH, glyphH)
|
||||||
@@ -47,21 +40,18 @@ func TestLayoutFitsTheFont(t *testing.T) {
|
|||||||
if TopBarH < glyphH {
|
if TopBarH < glyphH {
|
||||||
t.Errorf("TopBarH (%d) cannot hold a %dpx glyph", TopBarH, glyphH)
|
t.Errorf("TopBarH (%d) cannot hold a %dpx glyph", TopBarH, glyphH)
|
||||||
}
|
}
|
||||||
// The tape channel must fit a header, a rule and at least four body rows —
|
|
||||||
// below that the dialogue is unreadable.
|
|
||||||
const channelH = ScreenH - HUDTop - 4
|
const channelH = ScreenH - HUDTop - 4
|
||||||
body := (channelH - Pad*2 - LineH - 6) / LineH
|
body := (channelH - Pad*2 - LineH - 6) / LineH
|
||||||
if body < 4 {
|
if body < 4 {
|
||||||
t.Errorf("tape channel only fits %d body rows, want >= 4", body)
|
t.Errorf("tape channel only fits %d body rows, want >= 4", body)
|
||||||
}
|
}
|
||||||
// The two tape slots must fit their two rows inside their border.
|
|
||||||
if slotsH < LineH*2+Pad {
|
if slotsH < LineH*2+Pad {
|
||||||
t.Errorf("slotsH (%d) cannot hold two %dpx rows", slotsH, LineH)
|
t.Errorf("slotsH (%d) cannot hold two %dpx rows", slotsH, LineH)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The HUD must not reach outside the screen, and the scene must keep a usable
|
|
||||||
// aspect ratio for a point & click background.
|
|
||||||
func TestSceneProportions(t *testing.T) {
|
func TestSceneProportions(t *testing.T) {
|
||||||
if invY+invSlot*2+invGap > ScreenH {
|
if invY+invSlot*2+invGap > ScreenH {
|
||||||
t.Errorf("inventory runs off the bottom: %d > %d", invY+invSlot*2+invGap, ScreenH)
|
t.Errorf("inventory runs off the bottom: %d > %d", invY+invSlot*2+invGap, ScreenH)
|
||||||
|
|||||||
+1
-22
@@ -1,22 +1,5 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// UseWithGuard — enforcing authored failure.
|
|
||||||
//
|
|
||||||
// The rule: a failed interaction fails IN CHARACTER, never with an error
|
|
||||||
// message. But inkwell hardcodes a "Nem ehhez." flash in core.engine.go when an
|
|
||||||
// item/hotspot pair has no OnUseWith handler, and that string cannot be
|
|
||||||
// replaced from the domain.
|
|
||||||
//
|
|
||||||
// The fix, without touching the engine: widgets tick BEFORE the engine's
|
|
||||||
// handleSceneInput and may consume the click. So this widget catches the
|
|
||||||
// "selected item + hotspot with no authored pair" case and fails in Paul's
|
|
||||||
// voice with a remark from Dex — costing nothing, since nothing is consumed and
|
|
||||||
// nothing breaks.
|
|
||||||
//
|
|
||||||
// IMPORTANT: it must be registered FIRST so that it ticks LAST among the
|
|
||||||
// widgets. That way every HUD widget (inventory, tape slots, dialogue) sees the
|
|
||||||
// click first, and this only ever catches clicks that land on the scene.
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
|
||||||
@@ -43,7 +26,7 @@ func (u *UseWithGuard) Tick(ctx *inkwell.UICtx) {
|
|||||||
}
|
}
|
||||||
h := g.HotspotAt(g.Input.Point())
|
h := g.HotspotAt(g.Input.Point())
|
||||||
if h == nil || hasAuthoredPair(g, h, sel) {
|
if h == nil || hasAuthoredPair(g, h, sel) {
|
||||||
return // leave authored pairs to the engine
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
g.Input.ConsumeLeft()
|
g.Input.ConsumeLeft()
|
||||||
@@ -55,7 +38,6 @@ func (u *UseWithGuard) Tick(ctx *inkwell.UICtx) {
|
|||||||
}
|
}
|
||||||
g.LogAction("> Use " + itemLabel(g, sel) + " on: " + label)
|
g.LogAction("> Use " + itemLabel(g, sel) + " on: " + label)
|
||||||
|
|
||||||
// The tone escalates on repeats: dry, then teasing, then a real nudge.
|
|
||||||
key := "fail." + sel + "." + h.Name
|
key := "fail." + sel + "." + h.Name
|
||||||
g.State.NoteTalked(key)
|
g.State.NoteTalked(key)
|
||||||
n := g.State.Talked(key)
|
n := g.State.Talked(key)
|
||||||
@@ -66,7 +48,6 @@ func (u *UseWithGuard) Tick(ctx *inkwell.UICtx) {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
// hasAuthoredPair mirrors the engine's lookup order in invokeHotspotVerb.
|
|
||||||
func hasAuthoredPair(g *inkwell.Game, h *inkwell.Hotspot, item string) bool {
|
func hasAuthoredPair(g *inkwell.Game, h *inkwell.Hotspot, item string) bool {
|
||||||
if h.OnUseWith != nil {
|
if h.OnUseWith != nil {
|
||||||
if a, ok := h.OnUseWith[item]; ok && a != nil {
|
if a, ok := h.OnUseWith[item]; ok && a != nil {
|
||||||
@@ -98,8 +79,6 @@ var dexTease = []string{
|
|||||||
"Do it a few more times, maybe physics reconsiders.",
|
"Do it a few more times, maybe physics reconsiders.",
|
||||||
}
|
}
|
||||||
|
|
||||||
// dexFail escalates: dry, then teasing, then an actual hint — so the joke
|
|
||||||
// never becomes the wall.
|
|
||||||
func dexFail(n int) string {
|
func dexFail(n int) string {
|
||||||
switch {
|
switch {
|
||||||
case n <= 1:
|
case n <= 1:
|
||||||
|
|||||||
+2
-52
@@ -1,32 +1,18 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// The game's own actions, and the domain action pump.
|
|
||||||
//
|
|
||||||
// inkwell's queueAction is unexported, so domain widgets (TapeSlots) cannot put
|
|
||||||
// an action on the engine's queue — the built-in widgets only manage it because
|
|
||||||
// they live inside the package. inkwell.Ctx, however, is exported with exported
|
|
||||||
// fields, so the domain can drive a Runner itself. That is what the pump does:
|
|
||||||
// it runs HUD-originated actions (starting a dialogue, a tape line) alongside
|
|
||||||
// the engine's own script runner. See README, "Engine workarounds".
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
"github.com/hajimehoshi/ebiten/v2"
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ----- pump -------------------------------------------------------------
|
|
||||||
|
|
||||||
// Do queues a HUD-originated action.
|
|
||||||
func (w *World) Do(a inkwell.Action) {
|
func (w *World) Do(a inkwell.Action) {
|
||||||
if a != nil {
|
if a != nil {
|
||||||
w.queue = append(w.queue, a)
|
w.queue = append(w.queue, a)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PumpTick advances the pump. The ActionPump widget calls it every frame.
|
|
||||||
func (w *World) PumpTick(dt float64) {
|
func (w *World) PumpTick(dt float64) {
|
||||||
// The TopBar's left section: location plus a single word for a stopped
|
|
||||||
// world. No pause overlay, no blur — one word says time has stopped.
|
|
||||||
if w.top != nil {
|
if w.top != nil {
|
||||||
title := w.sceneTitle()
|
title := w.sceneTitle()
|
||||||
if w.paused {
|
if w.paused {
|
||||||
@@ -62,8 +48,6 @@ func (w *World) sceneTitle() string {
|
|||||||
return s.Name
|
return s.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
// ActionPump is an invisible widget that advances the pump. The HUD registers
|
|
||||||
// it; the world itself knows nothing about the widget tree.
|
|
||||||
type ActionPump struct {
|
type ActionPump struct {
|
||||||
Name string
|
Name string
|
||||||
W *World
|
W *World
|
||||||
@@ -73,8 +57,6 @@ func (p *ActionPump) GetName() string { return p.Name
|
|||||||
func (p *ActionPump) Tick(ctx *inkwell.UICtx) { p.W.PumpTick(ctx.DT) }
|
func (p *ActionPump) Tick(ctx *inkwell.UICtx) { p.W.PumpTick(ctx.DT) }
|
||||||
func (p *ActionPump) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) {}
|
func (p *ActionPump) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) {}
|
||||||
|
|
||||||
// ----- helpers ----------------------------------------------------------
|
|
||||||
|
|
||||||
type fnAction struct{ fn func(*inkwell.Ctx) }
|
type fnAction struct{ fn func(*inkwell.Ctx) }
|
||||||
|
|
||||||
func (a *fnAction) Start() inkwell.Runner { return &fnRunner{spec: a} }
|
func (a *fnAction) Start() inkwell.Runner { return &fnRunner{spec: a} }
|
||||||
@@ -86,23 +68,16 @@ func (r *fnRunner) Tick(ctx *inkwell.Ctx) inkwell.Status {
|
|||||||
return inkwell.StatusDone
|
return inkwell.StatusDone
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fn turns a function into a one-shot action that completes immediately.
|
|
||||||
func Fn(f func(*inkwell.Ctx)) inkwell.Action { return &fnAction{fn: f} }
|
func Fn(f func(*inkwell.Ctx)) inkwell.Action { return &fnAction{fn: f} }
|
||||||
|
|
||||||
// UseTheme switches the theme at runtime. This is the single line that makes
|
|
||||||
// the Nokia-punk texture break in during the finale. inkwell saves ActiveTheme,
|
|
||||||
// so the switch survives a save without needing a flag of its own.
|
|
||||||
func UseTheme(name string) inkwell.Action {
|
func UseTheme(name string) inkwell.Action {
|
||||||
return Fn(func(ctx *inkwell.Ctx) { ctx.Game.UseTheme(name) })
|
return Fn(func(ctx *inkwell.Ctx) { ctx.Game.UseTheme(name) })
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetMode switches the HUD mode (play / cutscene / menu).
|
|
||||||
func SetMode(w *World, m Mode) inkwell.Action {
|
func SetMode(w *World, m Mode) inkwell.Action {
|
||||||
return Fn(func(*inkwell.Ctx) { w.SetMode(m) })
|
return Fn(func(*inkwell.Ctx) { w.SetMode(m) })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
func EnterScene(w *World, name string) inkwell.Action {
|
||||||
return Fn(func(*inkwell.Ctx) {
|
return Fn(func(*inkwell.Ctx) {
|
||||||
if name != w.scene {
|
if name != w.scene {
|
||||||
@@ -112,12 +87,6 @@ func EnterScene(w *World, name string) inkwell.Action {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
func Back(w *World) inkwell.Action {
|
||||||
return Fn(func(ctx *inkwell.Ctx) {
|
return Fn(func(ctx *inkwell.Ctx) {
|
||||||
if w.prev == "" {
|
if w.prev == "" {
|
||||||
@@ -127,14 +96,6 @@ func Back(w *World) inkwell.Action {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- TapeSay ----------------------------------------------------------
|
|
||||||
|
|
||||||
// TapeSay is a line spoken by a tape.
|
|
||||||
//
|
|
||||||
// It is deliberately not inkwell.Say: Say puts a SpeechBubble above the
|
|
||||||
// speaker's head, but tapes have no body in the scene — they speak into your
|
|
||||||
// ear. This is the two-channel rule enforced in code: a tape can only reach the
|
|
||||||
// tape channel, never the scene.
|
|
||||||
func TapeSay(speaker, text string) inkwell.Action {
|
func TapeSay(speaker, text string) inkwell.Action {
|
||||||
return &tapeSayAction{speaker: speaker, text: text}
|
return &tapeSayAction{speaker: speaker, text: text}
|
||||||
}
|
}
|
||||||
@@ -153,7 +114,7 @@ type tapeSayRunner struct {
|
|||||||
func (r *tapeSayRunner) Tick(ctx *inkwell.Ctx) inkwell.Status {
|
func (r *tapeSayRunner) Tick(ctx *inkwell.Ctx) inkwell.Status {
|
||||||
if !r.started {
|
if !r.started {
|
||||||
r.started = true
|
r.started = true
|
||||||
// Same pacing as Say, so the two breathe together inside a Seq.
|
|
||||||
r.duration = 1.2 + float64(len([]rune(r.spec.text)))*0.05
|
r.duration = 1.2 + float64(len([]rune(r.spec.text)))*0.05
|
||||||
ctx.Game.LogResponse(r.spec.speaker, r.spec.text)
|
ctx.Game.LogResponse(r.spec.speaker, r.spec.text)
|
||||||
}
|
}
|
||||||
@@ -164,11 +125,6 @@ func (r *tapeSayRunner) Tick(ctx *inkwell.Ctx) inkwell.Status {
|
|||||||
return inkwell.StatusRunning
|
return inkwell.StatusRunning
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- TapeOffer --------------------------------------------------------
|
|
||||||
|
|
||||||
// TapeOffer offers a tape line during a cutscene: the Letterbox shows that a
|
|
||||||
// tape would speak, but it only speaks if the player asks. A tape never
|
|
||||||
// interrupts — the floor is the player's.
|
|
||||||
func TapeOffer(w *World, line inkwell.Action) inkwell.Action {
|
func TapeOffer(w *World, line inkwell.Action) inkwell.Action {
|
||||||
return Fn(func(*inkwell.Ctx) {
|
return Fn(func(*inkwell.Ctx) {
|
||||||
w.tapeLine = line
|
w.tapeLine = line
|
||||||
@@ -176,7 +132,6 @@ func TapeOffer(w *World, line inkwell.Action) inkwell.Action {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TakeTapeOffer queues the offered line and clears the prompt.
|
|
||||||
func (w *World) TakeTapeOffer() {
|
func (w *World) TakeTapeOffer() {
|
||||||
if !w.tapeWaiting {
|
if !w.tapeWaiting {
|
||||||
return
|
return
|
||||||
@@ -187,11 +142,6 @@ func (w *World) TakeTapeOffer() {
|
|||||||
w.Do(line)
|
w.Do(line)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- Paused -----------------------------------------------------------
|
|
||||||
|
|
||||||
// Paused marks the world as stopped for the duration of the wrapped action.
|
|
||||||
// inkwell exposes no dialogueActive accessor, so we mark it on the content
|
|
||||||
// side: every dialogue start is wrapped in this.
|
|
||||||
func Paused(w *World, inner inkwell.Action) inkwell.Action {
|
func Paused(w *World, inner inkwell.Action) inkwell.Action {
|
||||||
return &pausedAction{w: w, inner: inner}
|
return &pausedAction{w: w, inner: inner}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-43
@@ -1,81 +1,52 @@
|
|||||||
package inc
|
package inc
|
||||||
|
|
||||||
// World — the domain aggregate: a thin layer around the inkwell Game
|
|
||||||
// holding the runtime state that must NOT be saved, plus the game's own
|
|
||||||
// actions and the action pump.
|
|
||||||
//
|
|
||||||
// The world knows nothing about the HUD or the content: the ui and content
|
|
||||||
// categories both build on it, never the other way round.
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Mode is the HUD mode.
|
|
||||||
//
|
|
||||||
// IMPORTANT: this is deliberately NOT a State.Var. inkwell's state.save.go
|
|
||||||
// serialises State.Vars but drops the in-flight script ("saves capture state at
|
|
||||||
// idle/ready boundaries"), so a save taken mid-cutscene would reload into a
|
|
||||||
// permanently letterboxed, HUD-less game with no script running. The mode is
|
|
||||||
// therefore an unsaved runtime field.
|
|
||||||
type Mode string
|
type Mode string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ModePlay Mode = "play" // full HUD
|
ModePlay Mode = "play"
|
||||||
ModeCutscene Mode = "cutscene" // letterbox, HUD hidden
|
ModeCutscene Mode = "cutscene"
|
||||||
ModeMenu Mode = "menu" // main menu, HUD hidden
|
ModeMenu Mode = "menu"
|
||||||
)
|
)
|
||||||
|
|
||||||
// World holds the runtime state that does not belong in a save file.
|
|
||||||
type World struct {
|
type World struct {
|
||||||
G *inkwell.Game
|
G *inkwell.Game
|
||||||
|
|
||||||
mode Mode
|
mode Mode
|
||||||
|
|
||||||
scene string // current scene — inkwell exposes no accessor for it
|
scene string
|
||||||
prev string // the one before it, for screens with no fixed way out
|
prev string
|
||||||
paused bool // is the world stopped (during dialogue), for the TopBar suffix
|
paused bool
|
||||||
pending string // the tape currently being loaded into slot 2
|
pending string
|
||||||
queue []inkwell.Action // HUD-originated actions waiting to run
|
queue []inkwell.Action
|
||||||
running inkwell.Runner
|
running inkwell.Runner
|
||||||
top *inkwell.TopBar // the location bar, so the pump can write its suffix
|
top *inkwell.TopBar
|
||||||
|
|
||||||
// During a cutscene a tape signals but never interrupts: the line waits
|
|
||||||
// here until the player asks for it with SPACE.
|
|
||||||
tapeWaiting bool
|
tapeWaiting bool
|
||||||
tapeLine inkwell.Action
|
tapeLine inkwell.Action
|
||||||
}
|
}
|
||||||
|
|
||||||
// New wraps an existing inkwell game.
|
|
||||||
func newWorld(g *inkwell.Game) *World {
|
func newWorld(g *inkwell.Game) *World {
|
||||||
return &World{G: g, mode: ModePlay}
|
return &World{G: g, mode: ModePlay}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mode returns the current HUD mode.
|
|
||||||
func (w *World) Mode() Mode { return w.mode }
|
func (w *World) Mode() Mode { return w.mode }
|
||||||
|
|
||||||
// SetMode switches the HUD mode.
|
|
||||||
func (w *World) SetMode(m Mode) { w.mode = m }
|
func (w *World) SetMode(m Mode) { w.mode = m }
|
||||||
|
|
||||||
// HUDVisible reports whether the HUD should draw: only in play mode.
|
|
||||||
func (w *World) HUDVisible() bool { return w.mode == ModePlay }
|
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 }
|
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 }
|
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 }
|
func (w *World) Paused() bool { return w.paused }
|
||||||
|
|
||||||
// SetTopBar hands over the location bar so the pump can write its suffix.
|
|
||||||
func (w *World) SetTopBar(t *inkwell.TopBar) { w.top = t }
|
func (w *World) SetTopBar(t *inkwell.TopBar) { w.top = t }
|
||||||
|
|
||||||
// Slot2 returns the item name in the second tape slot, empty if there is none.
|
|
||||||
//
|
|
||||||
// Unlike Mode, this is REAL game state and lives in State.Var so that it is
|
|
||||||
// saved.
|
|
||||||
func (w *World) Slot2() string {
|
func (w *World) Slot2() string {
|
||||||
if v, ok := w.G.State.Var(VarSlot2).(string); ok {
|
if v, ok := w.G.State.Var(VarSlot2).(string); ok {
|
||||||
return v
|
return v
|
||||||
@@ -83,19 +54,14 @@ func (w *World) Slot2() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetSlot2 sets the contents of the second tape slot.
|
|
||||||
func (w *World) SetSlot2(item string) { w.G.State.SetVar(VarSlot2, item) }
|
func (w *World) SetSlot2(item string) { w.G.State.SetVar(VarSlot2, item) }
|
||||||
|
|
||||||
// SetPending marks which tape is going into slot 2. The tape-insert script
|
|
||||||
// performs the actual load; the widget only marks.
|
|
||||||
func (w *World) SetPending(item string) { w.pending = item }
|
func (w *World) SetPending(item string) { w.pending = item }
|
||||||
|
|
||||||
// TakePending returns and clears the tape waiting to be loaded.
|
|
||||||
func (w *World) TakePending() string {
|
func (w *World) TakePending() string {
|
||||||
item := w.pending
|
item := w.pending
|
||||||
w.pending = ""
|
w.pending = ""
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
// TapeWaiting reports whether a tape line has been offered but not yet spoken.
|
|
||||||
func (w *World) TapeWaiting() bool { return w.tapeWaiting }
|
func (w *World) TapeWaiting() bool { return w.tapeWaiting }
|
||||||
|
|||||||
@@ -6,9 +6,6 @@ import (
|
|||||||
inkwell "git.teletypegames.org/engines/inkwell"
|
inkwell "git.teletypegames.org/engines/inkwell"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The mode must not leak into saved state: inkwell saves State.Vars but drops
|
|
||||||
// the in-flight script, so a save taken mid-cutscene would otherwise reload
|
|
||||||
// into a letterboxed, HUD-less game with nothing running.
|
|
||||||
func TestModeIsNotSavedState(t *testing.T) {
|
func TestModeIsNotSavedState(t *testing.T) {
|
||||||
w := newWorld(inkwell.NewGame("t", 320, 200))
|
w := newWorld(inkwell.NewGame("t", 320, 200))
|
||||||
|
|
||||||
@@ -19,7 +16,6 @@ func TestModeIsNotSavedState(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Slot 2, by contrast, is real game state and must be saved.
|
|
||||||
w.SetSlot2(ItemMysteryTape)
|
w.SetSlot2(ItemMysteryTape)
|
||||||
if w.G.State.Var(VarSlot2) != 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")
|
||||||
@@ -29,7 +25,6 @@ func TestModeIsNotSavedState(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The widget marks a tape as pending; the script consumes it — once.
|
|
||||||
func TestPendingIsConsumedOnce(t *testing.T) {
|
func TestPendingIsConsumedOnce(t *testing.T) {
|
||||||
w := newWorld(inkwell.NewGame("t", 320, 200))
|
w := newWorld(inkwell.NewGame("t", 320, 200))
|
||||||
w.SetPending(ItemMysteryTape)
|
w.SetPending(ItemMysteryTape)
|
||||||
|
|||||||
@@ -10,11 +10,6 @@ import (
|
|||||||
"git.teletypegames.org/games/realworld/inc"
|
"git.teletypegames.org/games/realworld/inc"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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
|
//go:embed assets
|
||||||
var assets embed.FS
|
var assets embed.FS
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user