diff --git a/inc/background.alley.go b/inc/background.alley.go index 137f0bf..7cfac2a 100644 --- a/inc/background.alley.go +++ b/inc/background.alley.go @@ -4,10 +4,6 @@ import ( 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 { return inkwell.Asset{ Name: BgAlley, diff --git a/inc/background.bbs_terminal.go b/inc/background.bbs_terminal.go index 507fcb3..8de2a22 100644 --- a/inc/background.bbs_terminal.go +++ b/inc/background.bbs_terminal.go @@ -4,7 +4,6 @@ import ( inkwell "git.teletypegames.org/engines/inkwell" ) -// bbsTerminal — deck 11. func backgroundBbsTerminal() inkwell.Asset { return inkwell.Asset{ Name: BgBBSTerminal, diff --git a/inc/background.bvk_branch.go b/inc/background.bvk_branch.go index 4e45269..60b857f 100644 --- a/inc/background.bvk_branch.go +++ b/inc/background.bvk_branch.go @@ -4,7 +4,6 @@ import ( inkwell "git.teletypegames.org/engines/inkwell" ) -// bvkBranch — deck 18. func backgroundBvkBranch() inkwell.Asset { return inkwell.Asset{ Name: BgBVKBranch, diff --git a/inc/background.columbarium.go b/inc/background.columbarium.go index f43a1b6..abdaf9f 100644 --- a/inc/background.columbarium.go +++ b/inc/background.columbarium.go @@ -4,7 +4,6 @@ import ( inkwell "git.teletypegames.org/engines/inkwell" ) -// columbarium — deck 23. func backgroundColumbarium() inkwell.Asset { return inkwell.Asset{ Name: BgColumbarium, diff --git a/inc/background.curator_shop.go b/inc/background.curator_shop.go index e77d8e4..853c7d3 100644 --- a/inc/background.curator_shop.go +++ b/inc/background.curator_shop.go @@ -4,7 +4,6 @@ import ( inkwell "git.teletypegames.org/engines/inkwell" ) -// curatorShop — deck 19. func backgroundCuratorShop() inkwell.Asset { return inkwell.Asset{ Name: BgCuratorShop, diff --git a/inc/background.hackerspace.go b/inc/background.hackerspace.go index 216a061..0a0c67d 100644 --- a/inc/background.hackerspace.go +++ b/inc/background.hackerspace.go @@ -4,7 +4,6 @@ import ( inkwell "git.teletypegames.org/engines/inkwell" ) -// hackerspace — deck 06. func backgroundHackerspace() inkwell.Asset { return inkwell.Asset{ Name: BgHackerspace, diff --git a/inc/background.hospital.go b/inc/background.hospital.go index 0631751..7089809 100644 --- a/inc/background.hospital.go +++ b/inc/background.hospital.go @@ -4,7 +4,6 @@ import ( inkwell "git.teletypegames.org/engines/inkwell" ) -// hospital — deck 13. func backgroundHospital() inkwell.Asset { return inkwell.Asset{ Name: BgHospital, diff --git a/inc/background.ice_cream_shop.go b/inc/background.ice_cream_shop.go index 62ad27a..00a8d76 100644 --- a/inc/background.ice_cream_shop.go +++ b/inc/background.ice_cream_shop.go @@ -4,7 +4,6 @@ import ( inkwell "git.teletypegames.org/engines/inkwell" ) -// iceCreamShop — deck 07. func backgroundIceCreamShop() inkwell.Asset { return inkwell.Asset{ Name: BgIceCreamShop, diff --git a/inc/background.manager.go b/inc/background.manager.go index aa1d454..7b9deb5 100644 --- a/inc/background.manager.go +++ b/inc/background.manager.go @@ -1,20 +1,9 @@ package inc -// Backgrounds — one image asset per screen, one file each. -// -// A background is the whole of a screen's art: inkwell scales it over the -// entire window, so every file here points at a 640×380 PNG under assets/bg/, -// the size the concept-art deck is drawn at. Two of them are not painted yet -// and fall back to inkwell's placeholder. -// -// The order below is the deck's, which is the wiki's location-catalogue order -// (projects/realworld/entities#helyszinek). - import ( inkwell "git.teletypegames.org/engines/inkwell" ) -// registerBackground adds every background to the game. func registerBackground(w *World) { for _, a := range []inkwell.Asset{ backgroundSelector(), diff --git a/inc/background.noodle_house.go b/inc/background.noodle_house.go index c72bacd..f893a04 100644 --- a/inc/background.noodle_house.go +++ b/inc/background.noodle_house.go @@ -4,7 +4,6 @@ import ( inkwell "git.teletypegames.org/engines/inkwell" ) -// noodleHouse — deck 02. func backgroundNoodleHouse() inkwell.Asset { return inkwell.Asset{ Name: BgNoodleHouse, diff --git a/inc/background.norman_apartment.go b/inc/background.norman_apartment.go index 5083482..29b494b 100644 --- a/inc/background.norman_apartment.go +++ b/inc/background.norman_apartment.go @@ -4,7 +4,6 @@ import ( inkwell "git.teletypegames.org/engines/inkwell" ) -// normanApartment — deck 03. func backgroundNormanApartment() inkwell.Asset { return inkwell.Asset{ Name: BgNormanApartment, diff --git a/inc/background.paul_shop.go b/inc/background.paul_shop.go index 2e18d15..2355799 100644 --- a/inc/background.paul_shop.go +++ b/inc/background.paul_shop.go @@ -4,7 +4,6 @@ import ( inkwell "git.teletypegames.org/engines/inkwell" ) -// paulShop — deck 01. func backgroundPaulShop() inkwell.Asset { return inkwell.Asset{ Name: BgPaulShop, diff --git a/inc/background.police_station.go b/inc/background.police_station.go index 8cbf219..98b855b 100644 --- a/inc/background.police_station.go +++ b/inc/background.police_station.go @@ -4,7 +4,6 @@ import ( inkwell "git.teletypegames.org/engines/inkwell" ) -// policeStation — deck 04. func backgroundPoliceStation() inkwell.Asset { return inkwell.Asset{ Name: BgPoliceStation, diff --git a/inc/background.public_bbs.go b/inc/background.public_bbs.go index 4bc3c5e..2669421 100644 --- a/inc/background.public_bbs.go +++ b/inc/background.public_bbs.go @@ -4,7 +4,6 @@ import ( inkwell "git.teletypegames.org/engines/inkwell" ) -// publicBbs — deck 17. func backgroundPublicBBS() inkwell.Asset { return inkwell.Asset{ Name: BgPublicBBS, diff --git a/inc/background.rooftop_hideout.go b/inc/background.rooftop_hideout.go index 31e2f67..c0f21b9 100644 --- a/inc/background.rooftop_hideout.go +++ b/inc/background.rooftop_hideout.go @@ -4,7 +4,6 @@ import ( inkwell "git.teletypegames.org/engines/inkwell" ) -// rooftopHideout — deck 16. func backgroundRooftopHideout() inkwell.Asset { return inkwell.Asset{ Name: BgRooftopHideout, diff --git a/inc/background.samizdat_press.go b/inc/background.samizdat_press.go index 79f0946..78f274d 100644 --- a/inc/background.samizdat_press.go +++ b/inc/background.samizdat_press.go @@ -4,7 +4,6 @@ import ( inkwell "git.teletypegames.org/engines/inkwell" ) -// samizdatPress — deck 21. func backgroundSamizdatPress() inkwell.Asset { return inkwell.Asset{ Name: BgSamizdatPress, diff --git a/inc/background.scrap_market.go b/inc/background.scrap_market.go index 7fa5092..8a21792 100644 --- a/inc/background.scrap_market.go +++ b/inc/background.scrap_market.go @@ -4,7 +4,6 @@ import ( inkwell "git.teletypegames.org/engines/inkwell" ) -// scrapMarket — deck 20. func backgroundScrapMarket() inkwell.Asset { return inkwell.Asset{ Name: BgScrapMarket, diff --git a/inc/background.secret_club.go b/inc/background.secret_club.go index de951e4..71a990d 100644 --- a/inc/background.secret_club.go +++ b/inc/background.secret_club.go @@ -4,7 +4,6 @@ import ( inkwell "git.teletypegames.org/engines/inkwell" ) -// secretClub — deck 10. func backgroundSecretClub() inkwell.Asset { return inkwell.Asset{ Name: BgSecretClub, diff --git a/inc/background.secret_lab.go b/inc/background.secret_lab.go index f67cb70..769df52 100644 --- a/inc/background.secret_lab.go +++ b/inc/background.secret_lab.go @@ -4,7 +4,6 @@ import ( inkwell "git.teletypegames.org/engines/inkwell" ) -// secretLab — deck 15. func backgroundSecretLab() inkwell.Asset { return inkwell.Asset{ Name: BgSecretLab, diff --git a/inc/background.selector.go b/inc/background.selector.go index c77cc51..6bc220f 100644 --- a/inc/background.selector.go +++ b/inc/background.selector.go @@ -4,10 +4,6 @@ import ( 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 { return inkwell.Asset{ Name: BgSelector, diff --git a/inc/background.server_farm.go b/inc/background.server_farm.go index e319d6f..352f927 100644 --- a/inc/background.server_farm.go +++ b/inc/background.server_farm.go @@ -4,7 +4,6 @@ import ( inkwell "git.teletypegames.org/engines/inkwell" ) -// serverFarm — deck 14. func backgroundServerFarm() inkwell.Asset { return inkwell.Asset{ Name: BgServerFarm, diff --git a/inc/background.showroom.go b/inc/background.showroom.go index de7a241..fa349d6 100644 --- a/inc/background.showroom.go +++ b/inc/background.showroom.go @@ -4,7 +4,6 @@ import ( inkwell "git.teletypegames.org/engines/inkwell" ) -// showroom — deck 22. func backgroundShowroom() inkwell.Asset { return inkwell.Asset{ Name: BgShowroom, diff --git a/inc/background.small_restaurant.go b/inc/background.small_restaurant.go index b9925d8..fb5937f 100644 --- a/inc/background.small_restaurant.go +++ b/inc/background.small_restaurant.go @@ -4,7 +4,6 @@ import ( inkwell "git.teletypegames.org/engines/inkwell" ) -// smallRestaurant — deck 09. func backgroundSmallRestaurant() inkwell.Asset { return inkwell.Asset{ Name: BgSmallRestaurant, diff --git a/inc/background.street.go b/inc/background.street.go index 7c8b751..e59659c 100644 --- a/inc/background.street.go +++ b/inc/background.street.go @@ -4,7 +4,6 @@ import ( inkwell "git.teletypegames.org/engines/inkwell" ) -// street — deck 12. func backgroundStreet() inkwell.Asset { return inkwell.Asset{ Name: BgStreet, diff --git a/inc/background.trinket_shop.go b/inc/background.trinket_shop.go index cc2ccf9..5f2fea5 100644 --- a/inc/background.trinket_shop.go +++ b/inc/background.trinket_shop.go @@ -4,7 +4,6 @@ import ( inkwell "git.teletypegames.org/engines/inkwell" ) -// trinketShop — deck 08. func backgroundTrinketShop() inkwell.Asset { return inkwell.Asset{ Name: BgTrinketShop, diff --git a/inc/boot.manager.go b/inc/boot.manager.go index 3cc5156..58df517 100644 --- a/inc/boot.manager.go +++ b/inc/boot.manager.go @@ -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 .manager.go holds what ties that -// category together — its Register, its shared types — and the files beside it -// hold one screen, one background, one item each. -// -// This file is the boot category: it wires the layers together, and it is the -// only place where the world, the content, the theme and the HUD meet. main.go -// knows nothing else. package inc import ( inkwell "git.teletypegames.org/engines/inkwell" ) -// Opts are the run parameters. type Opts struct { - Screen string // starting screen; empty means the alley - Finale bool // start with the finale, to try the Nokia-punk theme switch + Screen string + Finale bool } -// New builds a ready-to-run game. func New(o Opts) *inkwell.Game { start := o.Screen if start == "" { @@ -45,9 +33,7 @@ func New(o Opts) *inkwell.Game { func bootOpening(w *World, start string, finale bool) []inkwell.Action { acts := []inkwell.Action{ EnterScene(w, start), - // Beat 2 (the police station) will set the tip flag. Until that beat - // is written we set it here so the alley slice is playable end to end — - // the conditional branch itself is real code, not bypassed. + inkwell.SetFlag(FlagPoliceTip), inkwell.Say(Paul, "Back alley. Just like the clerk said."), TapeSay(Dex, "A government office tipped you off to remove something from a scene. That doesn't strike you as odd?"), diff --git a/inc/boot.manager_test.go b/inc/boot.manager_test.go index d836c44..e5d3637 100644 --- a/inc/boot.manager_test.go +++ b/inc/boot.manager_test.go @@ -19,9 +19,6 @@ func newTestGame(t *testing.T) *inkwell.Game { 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) { g := newTestGame(t) @@ -45,7 +42,7 @@ func TestReferencesResolve(t *testing.T) { t.Errorf("tape character not registered: %q", n) } } - // Every loadable tape needs an item, a character and a dialogue. + for item, char := range TapeItems { if !g.ItemManager.Has(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) { g := newTestGame(t) 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) { 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) } - // The dialogue box deliberately covers only the left region, so a tape can - // comment alongside the conversation. db, ok := g.UIManager.Get("dialog") if !ok { 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) { g := newTestGame(t) 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) { 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} 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:", which is what keeps the graph -// machine-readable — see screen.exit.go. func exits(g *inkwell.Game, screen string) []string { var out []string for _, h := range g.SceneManager.MustGet(screen).Hotspots { @@ -196,8 +168,6 @@ func exits(g *inkwell.Game, screen string) []string { return out } -// An exit that names a screen nobody registered is a dead end the player walks -// into: the engine logs "changeScene: unknown" and nothing happens on screen. func TestExitsResolve(t *testing.T) { g := newTestGame(t) 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) { 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) { g := newTestGame(t) for _, n := range g.SceneManager.Names() { diff --git a/inc/character.dex.go b/inc/character.dex.go index 1da5be9..fd5abab 100644 --- a/inc/character.dex.go +++ b/inc/character.dex.go @@ -4,8 +4,6 @@ import ( 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 { return inkwell.Character{ Name: Dex, diff --git a/inc/character.manager.go b/inc/character.manager.go index dd31b57..8992f11 100644 --- a/inc/character.manager.go +++ b/inc/character.manager.go @@ -1,16 +1,9 @@ package inc -// Characters — the game's cast, one file each. -// -// Tapes are characters too, not props — the wiki is explicit about it. They -// have no body in the scene, so they carry no W/H and never appear in -// Scene.Actors; their voice lives in the tape channel (see TapeSay). - import ( inkwell "git.teletypegames.org/engines/inkwell" ) -// registerCharacter adds every character to the game. func registerCharacter(w *World) { for _, c := range []inkwell.Character{ characterPaul(), diff --git a/inc/character.mystery_tape.go b/inc/character.mystery_tape.go index fc3a472..de1b5ab 100644 --- a/inc/character.mystery_tape.go +++ b/inc/character.mystery_tape.go @@ -4,11 +4,6 @@ import ( 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 { return inkwell.Character{ Name: TapeMystery, diff --git a/inc/character.paul.go b/inc/character.paul.go index 4ec7d8b..5a751de 100644 --- a/inc/character.paul.go +++ b/inc/character.paul.go @@ -4,8 +4,6 @@ import ( 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 { return inkwell.Character{ Name: Paul, @@ -13,8 +11,7 @@ func characterPaul() inkwell.Character { W: 28, H: 68, Start: inkwell.Point{X: 120, Y: 232}, - // Bone white: the world and Paul. If something is amber, a tape said - // it — see the colour rule in README. + SpeechColor: Ink, } } diff --git a/inc/character.support_tape.go b/inc/character.support_tape.go index 730eb43..74bb607 100644 --- a/inc/character.support_tape.go +++ b/inc/character.support_tape.go @@ -4,8 +4,6 @@ import ( inkwell "git.teletypegames.org/engines/inkwell" ) -// supportTape answers everything with basic information, insufferably. Dex -// hates it. Acquired in Chinatown (beat 5) — not reachable yet. func characterSupportTape() inkwell.Character { return inkwell.Character{ Name: TapeSupport, diff --git a/inc/content.manager.go b/inc/content.manager.go index e1b3335..1761a64 100644 --- a/inc/content.manager.go +++ b/inc/content.manager.go @@ -1,14 +1,5 @@ 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..go and background..go, plus -// one line in each category's manager list — nothing else moves. - -// registerContent adds every entity to the game. Order matters only in that -// screens reference backgrounds and characters, which Game.Validate cross-checks. func registerContent(w *World) { registerBackground(w) registerCharacter(w) diff --git a/inc/dialog.dex_talk.go b/inc/dialog.dex_talk.go index c67c381..963747e 100644 --- a/inc/dialog.dex_talk.go +++ b/inc/dialog.dex_talk.go @@ -4,8 +4,6 @@ import ( 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 { return inkwell.Dialogue{ Name: DlgDex, diff --git a/inc/dialog.manager.go b/inc/dialog.manager.go index e8404e9..5b2a3c6 100644 --- a/inc/dialog.manager.go +++ b/inc/dialog.manager.go @@ -1,20 +1,9 @@ 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 ( inkwell "git.teletypegames.org/engines/inkwell" ) -// registerDialog adds every dialogue to the game. func registerDialog(w *World) { for _, d := range []inkwell.Dialogue{ dialogDexTalk(), diff --git a/inc/dialog.mystery_tape_silent.go b/inc/dialog.mystery_tape_silent.go index 8882a17..8b5ec5d 100644 --- a/inc/dialog.mystery_tape_silent.go +++ b/inc/dialog.mystery_tape_silent.go @@ -4,9 +4,6 @@ import ( 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 { return inkwell.Dialogue{ Name: DlgMysteryTape, diff --git a/inc/item.black_market_armilla.go b/inc/item.black_market_armilla.go index 8684764..774d35e 100644 --- a/inc/item.black_market_armilla.go +++ b/inc/item.black_market_armilla.go @@ -4,8 +4,6 @@ import ( inkwell "git.teletypegames.org/engines/inkwell" ) -// blackMarketArmilla is flavour, not a quest trigger: the gap between -// legitimised technology and street reality. func itemBlackMarketArmilla() inkwell.Item { return inkwell.Item{ Name: ItemBlackArmilla, @@ -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."), ), OnUseWith: map[string]inkwell.Action{ - // An AUTHORED failure. Nothing is consumed, the character reacts, - // the tape remarks. Never a generic error line. + ItemNoodleLetter: inkwell.Seq( inkwell.Say(Paul, "A letter and a bracelet. Brilliant."), TapeSay(Dex, "Nothing. Which is what I'd charge for it."), diff --git a/inc/item.manager.go b/inc/item.manager.go index bd9cbad..2260861 100644 --- a/inc/item.manager.go +++ b/inc/item.manager.go @@ -1,15 +1,9 @@ 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 ( inkwell "git.teletypegames.org/engines/inkwell" ) -// registerItem adds every item to the game. func registerItem(w *World) { for _, i := range []inkwell.Item{ itemNoodleLetter(), diff --git a/inc/item.mystery_tape.go b/inc/item.mystery_tape.go index 7a69e8f..bd08f7f 100644 --- a/inc/item.mystery_tape.go +++ b/inc/item.mystery_tape.go @@ -4,8 +4,6 @@ import ( inkwell "git.teletypegames.org/engines/inkwell" ) -// mysteryTape is the engine of the investigation. Password-locked; it only -// starts speaking after the club trigger (beat 6). func itemMysteryTape() inkwell.Item { return inkwell.Item{ Name: ItemMysteryTape, diff --git a/inc/item.noodle_letter.go b/inc/item.noodle_letter.go index 84e6695..5f50dca 100644 --- a/inc/item.noodle_letter.go +++ b/inc/item.noodle_letter.go @@ -4,7 +4,6 @@ import ( inkwell "git.teletypegames.org/engines/inkwell" ) -// noodleLetter starts the story: it is what brings Paul to the city. func itemNoodleLetter() inkwell.Item { return inkwell.Item{ Name: ItemNoodleLetter, diff --git a/inc/names.manager.go b/inc/names.manager.go index ff1c850..37f723e 100644 --- a/inc/names.manager.go +++ b/inc/names.manager.go @@ -1,99 +1,67 @@ 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 ( 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 - // that they have their own personality, motivation and voice. - TapeMystery = "tape_mystery" // = The AI, but that only surfaces in the finale - TapeSupport = "tape_support" // the "Indian tech support" tape + TapeMystery = "tape_mystery" + TapeSupport = "tape_support" ) -// Inventory items. const ( ItemNoodleLetter = "noodle_letter" ItemMysteryTape = "mystery_tape" ItemBlackArmilla = "black_market_armilla" ) -// Dialogues. const ( DlgDex = "dex_talk" DlgMysteryTape = "mystery_tape_silent" ) -// Scripts. const ( ScriptTapeInsert = "tape_insert" ScriptFinale = "awakening_finale" ) -// World state — flags and variables. const ( - FlagPoliceTip = "police_tip" // wiki: hatosagi_tipp - FlagHasTape = "has_mystery_tape" // wiki: titokzatos_tape_megvan - FlagTapeSpoke = "tape_spoke" // wiki: tape_megszolalt - FlagClubEntry = "club_entry" // wiki: klub_bejutas — the shopkeeper let Paul through + FlagPoliceTip = "police_tip" + FlagHasTape = "has_mystery_tape" + FlagTapeSpoke = "tape_spoke" + FlagClubEntry = "club_entry" VarSlot2 = "armilla.slot2" 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 ( - ScreenSelector = "selector" // wiki: Helyszínválasztó - ScreenPaulShop = "paul_shop" // wiki: Paul boltja / garázsa - ScreenNoodleHouse = "noodle_house" // wiki: Noodle háza - ScreenNormanApartment = "norman_apartment" // wiki: Norman lakása / műhelye - ScreenPoliceStation = "police_station" // wiki: rendőrség / fogda - ScreenAlley = "alley" // wiki: sikátor (Noodle háza mögött) - ScreenHackerspace = "hackerspace" // wiki: hackerspace - ScreenIceCreamShop = "ice_cream_shop" // wiki: fagyizó - ScreenTrinketShop = "trinket_shop" // wiki: chinatowni csecsebecse-bolt - ScreenSmallRestaurant = "small_restaurant" // wiki: kajálda - ScreenSecretClub = "secret_club" // wiki: titkos klub („vízalatti nap") - ScreenBBSTerminal = "bbs_terminal" // wiki: a terminál / BBS-hozzáférési pont - ScreenStreet = "street" // wiki: köztér / utca - ScreenHospital = "hospital" // wiki: kórház / pszichiátria - ScreenServerFarm = "server_farm" // wiki: szerverfarm / adatközpont - ScreenSecretLab = "secret_lab" // wiki: titkos kutatólabor - ScreenRooftopHideout = "rooftop_hideout" // wiki: Norman tetőterasz-rejteke - ScreenPublicBBS = "public_bbs" // wiki 💡: nyilvános BBS-terminál - ScreenBVKBranch = "bvk_branch" // wiki 💡: BVK-kirendeltség (San Francisco) - ScreenCuratorShop = "curator_shop" // wiki 💡: kurátor-szerviz („a Mester") - ScreenScrapMarket = "scrap_market" // wiki 💡: selejt-piac - ScreenSamizdatPress = "samizdat_press" // wiki 💡: samizdat-nyomda / P2P-csomópont - ScreenShowroom = "showroom" // wiki 💡: Neumatronic márkakereskedés - ScreenColumbarium = "columbarium" // wiki 💡: kolumbárium / Dex emlékhelye + ScreenSelector = "selector" + ScreenPaulShop = "paul_shop" + ScreenNoodleHouse = "noodle_house" + ScreenNormanApartment = "norman_apartment" + ScreenPoliceStation = "police_station" + ScreenAlley = "alley" + ScreenHackerspace = "hackerspace" + ScreenIceCreamShop = "ice_cream_shop" + ScreenTrinketShop = "trinket_shop" + ScreenSmallRestaurant = "small_restaurant" + ScreenSecretClub = "secret_club" + ScreenBBSTerminal = "bbs_terminal" + ScreenStreet = "street" + ScreenHospital = "hospital" + ScreenServerFarm = "server_farm" + ScreenSecretLab = "secret_lab" + ScreenRooftopHideout = "rooftop_hideout" + ScreenPublicBBS = "public_bbs" + ScreenBVKBranch = "bvk_branch" + ScreenCuratorShop = "curator_shop" + ScreenScrapMarket = "scrap_market" + ScreenSamizdatPress = "samizdat_press" + ScreenShowroom = "showroom" + ScreenColumbarium = "columbarium" ) -// Backgrounds — one per screen, same order. const ( BgSelector = "bg_selector" BgPaulShop = "bg_paul_shop" @@ -121,19 +89,14 @@ const ( 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{ Dex: "DEX", TapeMystery: "UNKNOWN TAPE", TapeSupport: "TECH SUPPORT", } -// IsTape reports whether a character is a tape. 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 { if d, ok := Tapes[name]; ok { return d @@ -141,16 +104,12 @@ func TapeDisplayName(name string) string { return name } -// TapeItems maps an inventory item that can go into slot 2 to the character -// living on it. var TapeItems = map[string]string{ ItemMysteryTape: TapeMystery, } -// IsTapeItem reports whether an item can be loaded into slot 2. 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 { switch item { case ItemMysteryTape: diff --git a/inc/screen.alley.go b/inc/screen.alley.go index e77afba..be9a49c 100644 --- a/inc/screen.alley.go +++ b/inc/screen.alley.go @@ -4,21 +4,13 @@ import ( inkwell "git.teletypegames.org/engines/inkwell" ) -// alley is the alley behind Noodle's house — beat 3, and the vertical slice the -// whole HUD is measured against: hotspots, a gated pickup, the two-slot -// Armilla, tape dialogue and an authored failure. -// -// The wiki keeps it in the same catalogue row as the house ("Noodle háza & a -// sikátor") and its graph has one arrow in, NoodleUtcafront → Sikátor, so the -// only way out is back to the street. The alley's own locked back door is the -// New Game+ hook and leads to a room the deck has not painted. func screenAlley() screen { return screen{ name: ScreenAlley, title: "ALLEY — BEHIND NOODLE'S HOUSE", background: BgAlley, exits: []exit{ - // On the left: the back door hotspot below owns the other end. + {to: ScreenNoodleHouse, label: "back out to the street", side: sideLeft}, }, actors: []inkwell.SceneActor{ @@ -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 { return inkwell.Hotspot{ 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 { return inkwell.Hotspot{ Name: "back_door", @@ -71,7 +59,7 @@ func backDoor() inkwell.Hotspot { ), OnTalk: inkwell.Say(Paul, "To the door? I'm not there yet."), OnUseWith: map[string]inkwell.Action{ - // An AUTHORED failure — not the engine's hardcoded flash. + ItemBlackArmilla: inkwell.Seq( inkwell.Say(Paul, "A black-market bracelet doesn't open a keypad."), TapeSay(Dex, "But you do get an advert out of it."), diff --git a/inc/screen.bbs_terminal.go b/inc/screen.bbs_terminal.go index 8b0f7c5..a44e67d 100644 --- a/inc/screen.bbs_terminal.go +++ b/inc/screen.bbs_terminal.go @@ -1,12 +1,5 @@ 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 { return screen{ name: ScreenBBSTerminal, diff --git a/inc/screen.bvk_branch.go b/inc/screen.bvk_branch.go index 8165a4c..8a8ead2 100644 --- a/inc/screen.bvk_branch.go +++ b/inc/screen.bvk_branch.go @@ -1,9 +1,5 @@ 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 { return screen{ name: ScreenBVKBranch, diff --git a/inc/screen.columbarium.go b/inc/screen.columbarium.go index 6d0631f..7c5b0bb 100644 --- a/inc/screen.columbarium.go +++ b/inc/screen.columbarium.go @@ -1,9 +1,5 @@ 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 { return screen{ name: ScreenColumbarium, diff --git a/inc/screen.curator_shop.go b/inc/screen.curator_shop.go index 37d9beb..ab62c00 100644 --- a/inc/screen.curator_shop.go +++ b/inc/screen.curator_shop.go @@ -1,8 +1,5 @@ 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 { return screen{ name: ScreenCuratorShop, diff --git a/inc/screen.exit.go b/inc/screen.exit.go index 6f9df7d..76ac800 100644 --- a/inc/screen.exit.go +++ b/inc/screen.exit.go @@ -1,28 +1,5 @@ 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 ( inkwell "git.teletypegames.org/engines/inkwell" ) @@ -30,14 +7,12 @@ import ( type side int const ( - sideLeft side = iota // off the left edge - sideRight // off the right edge - sideBack // into the depth of the picture - sideNear // out towards the camera + sideLeft side = iota + sideRight + sideBack + 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 { switch s { case sideLeft: @@ -46,35 +21,26 @@ func (s side) area() inkwell.Shape { return inkwell.Rect(584, 40, 56, 212) case sideBack: return inkwell.Rect(232, 28, 176, 84) - default: // sideNear + default: return inkwell.Rect(232, 214, 176, 48) } } -// exit is one way out of a screen. type exit struct { - to string // the screen it leads to; empty means "back the way you came" - label string // what the status line calls it + to string + label string side side - // area overrides the edge strip. The selector's map pins use it; nothing - // else should need to until the art arrives. area inkwell.Shape - // needs is a flag that has to be set before the exit opens, and blocked is - // what Paul says while it is not. needs string blocked string } -// toSelector is the way back out to the rest of the city, on every screen the -// wiki's Helyszínválasztó lists. func toSelector() exit { return exit{to: ScreenSelector, label: "the rest of the city", side: sideNear} } -// exitName is how a connection stays machine-readable: the graph can be read -// straight back out of the registered scenes, so a test can walk it. func exitName(to string) string { if to == "" { return "exit:back" diff --git a/inc/screen.hackerspace.go b/inc/screen.hackerspace.go index 1d9791a..0631d4c 100644 --- a/inc/screen.hackerspace.go +++ b/inc/screen.hackerspace.go @@ -1,11 +1,5 @@ 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 { return screen{ name: ScreenHackerspace, diff --git a/inc/screen.hospital.go b/inc/screen.hospital.go index ed6980e..b0d6fc2 100644 --- a/inc/screen.hospital.go +++ b/inc/screen.hospital.go @@ -1,7 +1,5 @@ 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 { return screen{ name: ScreenHospital, diff --git a/inc/screen.ice_cream_shop.go b/inc/screen.ice_cream_shop.go index df87219..18b7731 100644 --- a/inc/screen.ice_cream_shop.go +++ b/inc/screen.ice_cream_shop.go @@ -1,12 +1,5 @@ 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 { return screen{ name: ScreenIceCreamShop, diff --git a/inc/screen.manager.go b/inc/screen.manager.go index 8d79e3c..5e9e2f0 100644 --- a/inc/screen.manager.go +++ b/inc/screen.manager.go @@ -1,80 +1,50 @@ 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 ( inkwell "git.teletypegames.org/engines/inkwell" ) -// screen is what each file in this package fills in. type screen struct { name string title string background string - // exits: where you can get to from here. See screen.exit.go. exits []exit - // onSelector marks a main location — one of those the wiki's - // Helyszínválasztó lists (story#helyszín-gráf-vázlat). It gets a way back - // to the selector for free, and the selector gets a way here. + onSelector bool - // What an authored beat adds on top. Zero values are fine: Paul lands in - // the middle of the default floor strip and nothing else happens. hotspots []inkwell.Hotspot actors []inkwell.SceneActor walkboxes []inkwell.Polygon onEnter inkwell.Action } -// 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{ - screenPaulShop, // 01 - screenNoodleHouse, // 02 - screenAlley, // 02, same catalogue row - screenNormanApartment, // 03 - screenPoliceStation, // 04 - screenHackerspace, // 06 - screenIceCreamShop, // 07 - screenTrinketShop, // 08 - screenSmallRestaurant, // 09 - screenSecretClub, // 10 - screenBbsTerminal, // 11 - screenStreet, // 12 - screenHospital, // 13 - screenServerFarm, // 14 - screenSecretLab, // 15 - screenRooftopHideout, // 16 - screenPublicBBS, // 17 - screenBvkBranch, // 18 - screenCuratorShop, // 19 - screenScrapMarket, // 20 - screenSamizdatPress, // 21 - screenShowroom, // 22 - screenColumbarium, // 23 + screenPaulShop, + screenNoodleHouse, + screenAlley, + screenNormanApartment, + screenPoliceStation, + screenHackerspace, + screenIceCreamShop, + screenTrinketShop, + screenSmallRestaurant, + screenSecretClub, + screenBbsTerminal, + screenStreet, + screenHospital, + screenServerFarm, + screenSecretLab, + screenRooftopHideout, + screenPublicBBS, + screenBvkBranch, + screenCuratorShop, + screenScrapMarket, + screenSamizdatPress, + screenShowroom, + 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) { screens := make([]screen, 0, len(screenDeck)+1) 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{ inkwell.Poly( 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 { exits := s.exits 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 { return Fn(func(ctx *inkwell.Ctx) { if ctx.Game.State.Var(name) == nil { diff --git a/inc/screen.noodle_house.go b/inc/screen.noodle_house.go index 8bce985..1207ba2 100644 --- a/inc/screen.noodle_house.go +++ b/inc/screen.noodle_house.go @@ -1,11 +1,5 @@ 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 { return screen{ name: ScreenNoodleHouse, diff --git a/inc/screen.norman_apartment.go b/inc/screen.norman_apartment.go index 45f8622..d715609 100644 --- a/inc/screen.norman_apartment.go +++ b/inc/screen.norman_apartment.go @@ -1,12 +1,5 @@ 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 { return screen{ name: ScreenNormanApartment, diff --git a/inc/screen.paul_shop.go b/inc/screen.paul_shop.go index ecb1c16..01bb8cf 100644 --- a/inc/screen.paul_shop.go +++ b/inc/screen.paul_shop.go @@ -1,13 +1,5 @@ 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 { return screen{ name: ScreenPaulShop, diff --git a/inc/screen.police_station.go b/inc/screen.police_station.go index 898293e..6bfca63 100644 --- a/inc/screen.police_station.go +++ b/inc/screen.police_station.go @@ -1,13 +1,5 @@ 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 { return screen{ name: ScreenPoliceStation, diff --git a/inc/screen.public_bbs.go b/inc/screen.public_bbs.go index cc050f8..a509c43 100644 --- a/inc/screen.public_bbs.go +++ b/inc/screen.public_bbs.go @@ -1,9 +1,5 @@ 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 { return screen{ name: ScreenPublicBBS, diff --git a/inc/screen.rooftop_hideout.go b/inc/screen.rooftop_hideout.go index 8a52781..4903b3f 100644 --- a/inc/screen.rooftop_hideout.go +++ b/inc/screen.rooftop_hideout.go @@ -1,11 +1,5 @@ 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 { return screen{ name: ScreenRooftopHideout, diff --git a/inc/screen.samizdat_press.go b/inc/screen.samizdat_press.go index ccb09fc..cde9adf 100644 --- a/inc/screen.samizdat_press.go +++ b/inc/screen.samizdat_press.go @@ -1,9 +1,5 @@ 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 { return screen{ name: ScreenSamizdatPress, diff --git a/inc/screen.scrap_market.go b/inc/screen.scrap_market.go index ceb5629..521a2d2 100644 --- a/inc/screen.scrap_market.go +++ b/inc/screen.scrap_market.go @@ -1,8 +1,5 @@ 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 { return screen{ name: ScreenScrapMarket, diff --git a/inc/screen.secret_club.go b/inc/screen.secret_club.go index 781cd47..9a8dc74 100644 --- a/inc/screen.secret_club.go +++ b/inc/screen.secret_club.go @@ -1,13 +1,5 @@ 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 { return screen{ name: ScreenSecretClub, diff --git a/inc/screen.secret_lab.go b/inc/screen.secret_lab.go index 5545226..cb9f371 100644 --- a/inc/screen.secret_lab.go +++ b/inc/screen.secret_lab.go @@ -1,13 +1,5 @@ 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 { return screen{ name: ScreenSecretLab, diff --git a/inc/screen.selector.go b/inc/screen.selector.go index 4c43fb4..64a3b9c 100644 --- a/inc/screen.selector.go +++ b/inc/screen.selector.go @@ -6,17 +6,6 @@ import ( 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 { var exits []exit for _, s := range rest { @@ -34,13 +23,12 @@ func screenSelector(rest []screen) screen { title: "SAN FRANCISCO", background: BgSelector, exits: exits, - // Nobody stands on a map. + actors: []inkwell.SceneActor{}, walkboxes: []inkwell.Polygon{}, } } -// The pin grid: three columns down the scene region, filled top to bottom. const ( pinCols = 3 pinW = 192.0 @@ -56,8 +44,6 @@ func pin(i int) inkwell.Shape { return inkwell.Rect(pinX+float64(col)*pinGapX, pinY+float64(row)*pinGapY, pinW, pinH) } -// pinLabel is the screen's title without the em-dash subtitle: on a map a -// location wants its name, not its description. func pinLabel(title string) string { name, _, _ := strings.Cut(title, " — ") return name diff --git a/inc/screen.server_farm.go b/inc/screen.server_farm.go index 9e659b8..29f8a44 100644 --- a/inc/screen.server_farm.go +++ b/inc/screen.server_farm.go @@ -1,8 +1,5 @@ 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 { return screen{ name: ScreenServerFarm, diff --git a/inc/screen.showroom.go b/inc/screen.showroom.go index 4485585..7d05ada 100644 --- a/inc/screen.showroom.go +++ b/inc/screen.showroom.go @@ -1,9 +1,5 @@ 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 { return screen{ name: ScreenShowroom, diff --git a/inc/screen.small_restaurant.go b/inc/screen.small_restaurant.go index 0c2901c..8a84727 100644 --- a/inc/screen.small_restaurant.go +++ b/inc/screen.small_restaurant.go @@ -1,12 +1,5 @@ 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 { return screen{ name: ScreenSmallRestaurant, diff --git a/inc/screen.street.go b/inc/screen.street.go index 1d3f838..bf4cb7d 100644 --- a/inc/screen.street.go +++ b/inc/screen.street.go @@ -1,12 +1,5 @@ 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 { return screen{ name: ScreenStreet, diff --git a/inc/screen.trinket_shop.go b/inc/screen.trinket_shop.go index 25846a3..695aaec 100644 --- a/inc/screen.trinket_shop.go +++ b/inc/screen.trinket_shop.go @@ -1,12 +1,5 @@ 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 { return screen{ name: ScreenTrinketShop, diff --git a/inc/script.awakening_finale.go b/inc/script.awakening_finale.go index 7e012b0..cfc9f75 100644 --- a/inc/script.awakening_finale.go +++ b/inc/script.awakening_finale.go @@ -4,12 +4,6 @@ import ( 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 { return inkwell.Script{ Name: ScriptFinale, diff --git a/inc/script.manager.go b/inc/script.manager.go index 605c23a..c96a85a 100644 --- a/inc/script.manager.go +++ b/inc/script.manager.go @@ -1,13 +1,9 @@ package inc -// Scripts — the game's named, reusable action sequences: -// cutscenes and recurring logic, built from the inkwell action set. - import ( inkwell "git.teletypegames.org/engines/inkwell" ) -// registerScript adds every script to the game. func registerScript(w *World) { for _, s := range []inkwell.Script{ scriptTapeInsert(w), diff --git a/inc/script.tape_insert.go b/inc/script.tape_insert.go index 264488b..a5453f1 100644 --- a/inc/script.tape_insert.go +++ b/inc/script.tape_insert.go @@ -4,9 +4,6 @@ import ( inkwell "git.teletypegames.org/engines/inkwell" ) -// tapeInsert runs whenever a tape goes into slot 2: it activates the tape and -// makes Dex comment. The wiki calls the comment mandatory — Dex remarks on -// every newly loaded tape, without exception. func scriptTapeInsert(w *World) inkwell.Script { return inkwell.Script{ Name: ScriptTapeInsert, diff --git a/inc/theme.manager.go b/inc/theme.manager.go index fde0336..ad823a3 100644 --- a/inc/theme.manager.go +++ b/inc/theme.manager.go @@ -1,45 +1,28 @@ 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 ( "image/color" inkwell "git.teletypegames.org/engines/inkwell" ) -// The two theme names. Runtime switching (inkwell UseTheme) refers to these. const ( RealWorld = "realworld-93" NokiaPunk = "nokia-punk" ) -// RGB builds an opaque colour from a hex literal. func RGB(hex uint32) color.Color { 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 { 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 ( - Ink = RGB(0xDCD5C4) // bone white — the voice of the world and of Paul + Ink = RGB(0xDCD5C4) InkDim = RGB(0x8A806B) - Amber = RGB(0xE0A33E) // tape voice + Amber = RGB(0xE0A33E) AmberLo = RGB(0xA8701A) black = RGB(0x14120E) panel = RGB(0x0D0B08) @@ -82,8 +65,8 @@ func realWorldTheme() inkwell.Theme { TopBarAccent: Amber, ChatLogBG: panel, - ChatLogPrompt: InkDim, // your own actions — quiet - ChatLogResponse: Amber, // tape voice + ChatLogPrompt: InkDim, + ChatLogResponse: Amber, ChatLogSystem: RGB(0x6B6353), 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 ( Green = RGB(0x3BE86B) GreenLo = RGB(0x1F7A39) @@ -148,7 +127,6 @@ func nokiaPunkTheme() inkwell.Theme { } } -// registerTheme adds both themes to the game. func registerTheme(g *inkwell.Game) { g.ThemeManager.Register(realWorldTheme()) g.ThemeManager.Register(nokiaPunkTheme()) diff --git a/inc/ui.letterbox.go b/inc/ui.letterbox.go index 8474d65..82bb645 100644 --- a/inc/ui.letterbox.go +++ b/inc/ui.letterbox.go @@ -1,12 +1,5 @@ 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 ( inkwell "git.teletypegames.org/engines/inkwell" "github.com/hajimehoshi/ebiten/v2" @@ -17,7 +10,7 @@ import ( type Letterbox struct { Name string W *World - Bar float64 // height of the black bar, top and bottom + Bar float64 } 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() { return } - // inkwell's Input only covers the mouse; read the key directly. + if inpututil.IsKeyJustPressed(ebiten.KeySpace) { l.W.TakeTapeOffer() } diff --git a/inc/ui.manager.go b/inc/ui.manager.go index ec2399e..5e0e95c 100644 --- a/inc/ui.manager.go +++ b/inc/ui.manager.go @@ -1,85 +1,35 @@ 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 ( inkwell "git.teletypegames.org/engines/inkwell" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/vector" ) -// Screen and layout, in internal (unscaled) pixels. const ( - // The screen is the size of one painted background, because the engine - // scales a background to the full screen: at any other size the art is - // squashed. See the note above. ScreenW = 640 ScreenH = 380 - // LineH is one text row: the glyph cell plus leading. Every text-bearing - // box is sized as a multiple of this, so rows can never overlap. - LineH = glyphH + 2 // 18 - Pad = 6 // inner padding for every panel + LineH = glyphH + 2 + Pad = 6 - TopBarH = LineH + 2 // 20 — one row plus a hairline of breathing room - DividerX = 394 // world | tape channel (61.6% of the width) + TopBarH = LineH + 2 + DividerX = 394 - // The HUD strip is as short as its own contents allow, and is measured - // from the bottom of the screen — every pixel it takes is a pixel of the - // painting it covers. It has to hold the status row and, under it, the two - // rows of inventory slots (the tallest thing in the strip), padded above - // and below. - HUDH = Pad + LineH + 4 + invSlot*2 + invGap + Pad // 118 - HUDTop = ScreenH - HUDH // 262 — the scene | HUD divider + HUDH = Pad + LineH + 4 + invSlot*2 + invGap + Pad + HUDTop = ScreenH - HUDH - // Derived HUD rows. - statusY = HUDTop + Pad // 268 - slotsY = HUDTop + Pad + LineH // 286 - slotsH = LineH*2 + Pad*2 // 48 — two rows plus padding - invY = slotsY + 4 // 290 + statusY = HUDTop + Pad + slotsY = HUDTop + Pad + LineH + slotsH = LineH*2 + Pad*2 + invY = slotsY + 4 invSlot = 40 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) { 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(&ActionPump{Name: "pump", W: w}) g.UIManager.Register(&screenNav{Name: "screen_nav", W: w}) @@ -89,9 +39,7 @@ func registerUI(w *World) { top := &inkwell.TopBar{ Name: "topbar", 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, } w.SetTopBar(top) @@ -119,15 +67,12 @@ func registerUI(w *World) { g.UIManager.Register(&inkwell.SpeechBubble{ 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{ Name: "dialog", Bounds: inkwell.Rect(0, ScreenH-LineH*9, DividerX, LineH*9), 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{ Name: "verbs", Trigger: inkwell.MouseButtonRight, Radius: 58, Labels: map[string]string{ @@ -138,13 +83,6 @@ func registerUI(w *World) { 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 { Name string Scale int @@ -166,11 +104,6 @@ func (s *windowSizer) Tick(ctx *inkwell.UICtx) { 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 { Name string W *World @@ -203,10 +136,6 @@ func (n *gate) BlocksClickAt(p inkwell.Point) bool { 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 { Name string W *World @@ -221,9 +150,9 @@ func (h *hudFrame) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) { } th := ctx.Game.Theme() 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) - // vertical: world | tape + vector.StrokeLine(dst, DividerX, HUDTop+1, DividerX, ScreenH, 1, th.CharacterPanelBorder, false) } diff --git a/inc/ui.screen_nav.go b/inc/ui.screen_nav.go index d9a26ad..59d6542 100644 --- a/inc/ui.screen_nav.go +++ b/inc/ui.screen_nav.go @@ -1,18 +1,5 @@ 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 ( inkwell "git.teletypegames.org/engines/inkwell" "github.com/hajimehoshi/ebiten/v2" @@ -32,7 +19,7 @@ func (n *screenNav) Tick(ctx *inkwell.UICtx) { return } step := 0 - // inkwell's Input only covers the mouse; read the keys directly. + switch { case inpututil.IsKeyJustPressed(ebiten.KeyArrowRight): 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 { deck := g.SceneManager.Names() if len(deck) < 2 { diff --git a/inc/ui.screen_nav_test.go b/inc/ui.screen_nav_test.go index cd847ed..a37d603 100644 --- a/inc/ui.screen_nav_test.go +++ b/inc/ui.screen_nav_test.go @@ -6,9 +6,6 @@ import ( 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) { g := inkwell.NewGame("t", ScreenW, ScreenH) for _, n := range []string{"a", "b", "c"} { diff --git a/inc/ui.tape_channel.go b/inc/ui.tape_channel.go index c58b54f..1e17e61 100644 --- a/inc/ui.tape_channel.go +++ b/inc/ui.tape_channel.go @@ -1,18 +1,5 @@ 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 ( "image/color" @@ -31,8 +18,6 @@ func (t *TapeChannel) GetName() string { return t.Name } 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 { 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) - // Header: the name of whichever tape spoke last — not "console". speaker, speakerCol := t.lastSpeaker(g, th) drawTextC(dst, speaker, int(b.X)+pad, int(b.Y)+pad, speakerCol) hy := float32(b.Y+pad+lh) + 2 @@ -69,12 +53,11 @@ func (t *TapeChannel) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) { var txt string switch m.Kind { case inkwell.LogAction: - // Your own actions — quiet, but visible: that is what makes the - // joke book readable. + col, txt = th.ChatLogPrompt, m.Text case inkwell.LogResponse: 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 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) { msgs := g.Messages() 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) } -// 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 { if c, ok := g.CharacterManager.Get(name); ok && c.SpeechColor != nil { return c.SpeechColor diff --git a/inc/ui.tape_slots.go b/inc/ui.tape_slots.go index de268d6..3ac51b5 100644 --- a/inc/ui.tape_slots.go +++ b/inc/ui.tape_slots.go @@ -1,20 +1,5 @@ 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 ( inkwell "git.teletypegames.org/engines/inkwell" "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) } -// slotRects splits the widget bounds into the two slot rectangles. func (t *TapeSlots) slotRects() (inkwell.Rectangle, inkwell.Rectangle) { b := t.Bounds w := (b.W - Pad) / 2 @@ -48,7 +32,6 @@ func (t *TapeSlots) Tick(ctx *inkwell.UICtx) { mp := g.Input.Point() r1, r2 := t.slotRects() - // Hover label, so the StatusLine spells out what is about to happen. switch { case r1.Contains(mp): g.SetHoverLabel(TapeDisplayName(Dex)) @@ -71,7 +54,6 @@ func (t *TapeSlots) Tick(ctx *inkwell.UICtx) { slot2 := r2.Contains(mp) sel := g.Inventory.Selected() - // Loading a tape into slot 2. if slot2 && sel != "" { g.Inventory.Select("") if IsTapeItem(sel) { @@ -99,7 +81,7 @@ func (t *TapeSlots) Tick(ctx *inkwell.UICtx) { return } t.W.Do(TapeSay(Dex, "Empty. Nobody to talk to.")) - default: // look, take, use all fall back to a description + default: if !slot2 { t.W.Do(TapeSay(Dex, "Me. Four kilobytes of a dead man. Be grateful.")) return @@ -120,8 +102,6 @@ func (t *TapeSlots) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) { th := g.Theme() 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) { bg, border, col := th.InventorySlotBG, th.ChatLogSystem, th.ChatLogSystem 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 { if it, ok := g.ItemManager.Get(name); ok && it.Description != "" { return it.Description diff --git a/inc/ui.text.go b/inc/ui.text.go index 91f10f3..a62c6c7 100644 --- a/inc/ui.text.go +++ b/inc/ui.text.go @@ -1,15 +1,5 @@ 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 ( "image" "image/color" @@ -19,9 +9,6 @@ import ( "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 ( glyphW = 6 glyphH = 16 @@ -29,10 +16,8 @@ const ( 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 } -// 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) { if s == "" { 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) } -// 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 { if maxPx <= glyphW || textW(s) <= maxPx { return []string{s} @@ -108,7 +91,6 @@ func wrap(s string, maxPx int) []string { return out } -// clip truncates a string so it fits into maxPx. func clip(s string, maxPx int) string { r := []rune(s) max := maxPx / glyphW diff --git a/inc/ui.text_test.go b/inc/ui.text_test.go index e79d654..e59cb08 100644 --- a/inc/ui.text_test.go +++ b/inc/ui.text_test.go @@ -2,8 +2,6 @@ package inc 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) { if got, want := textW("naïve"), 5*glyphW; 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) { if 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 { 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 body := (channelH - Pad*2 - LineH - 6) / LineH if body < 4 { 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 { 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) { if invY+invSlot*2+invGap > ScreenH { t.Errorf("inventory runs off the bottom: %d > %d", invY+invSlot*2+invGap, ScreenH) diff --git a/inc/ui.usewith_guard.go b/inc/ui.usewith_guard.go index df6e558..c2b0bff 100644 --- a/inc/ui.usewith_guard.go +++ b/inc/ui.usewith_guard.go @@ -1,22 +1,5 @@ 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 ( "math/rand" @@ -43,7 +26,7 @@ func (u *UseWithGuard) Tick(ctx *inkwell.UICtx) { } h := g.HotspotAt(g.Input.Point()) if h == nil || hasAuthoredPair(g, h, sel) { - return // leave authored pairs to the engine + return } g.Input.ConsumeLeft() @@ -55,7 +38,6 @@ func (u *UseWithGuard) Tick(ctx *inkwell.UICtx) { } 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 g.State.NoteTalked(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 { if h.OnUseWith != 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.", } -// dexFail escalates: dry, then teasing, then an actual hint — so the joke -// never becomes the wall. func dexFail(n int) string { switch { case n <= 1: diff --git a/inc/world.action.go b/inc/world.action.go index 2dd5f7a..68b652f 100644 --- a/inc/world.action.go +++ b/inc/world.action.go @@ -1,32 +1,18 @@ 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 ( inkwell "git.teletypegames.org/engines/inkwell" "github.com/hajimehoshi/ebiten/v2" ) -// ----- pump ------------------------------------------------------------- - -// Do queues a HUD-originated action. func (w *World) Do(a inkwell.Action) { if a != nil { w.queue = append(w.queue, a) } } -// PumpTick advances the pump. The ActionPump widget calls it every frame. 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 { title := w.sceneTitle() if w.paused { @@ -62,8 +48,6 @@ func (w *World) sceneTitle() string { 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 { Name string 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) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) {} -// ----- helpers ---------------------------------------------------------- - type fnAction struct{ fn func(*inkwell.Ctx) } 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 } -// Fn turns a function into a one-shot action that completes immediately. 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 { 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 { 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 { return Fn(func(*inkwell.Ctx) { 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 { return Fn(func(ctx *inkwell.Ctx) { 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 { return &tapeSayAction{speaker: speaker, text: text} } @@ -153,7 +114,7 @@ type tapeSayRunner struct { func (r *tapeSayRunner) Tick(ctx *inkwell.Ctx) inkwell.Status { if !r.started { 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 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 } -// ----- 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 { return Fn(func(*inkwell.Ctx) { 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() { if !w.tapeWaiting { return @@ -187,11 +142,6 @@ func (w *World) TakeTapeOffer() { 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 { return &pausedAction{w: w, inner: inner} } diff --git a/inc/world.manager.go b/inc/world.manager.go index ce45b94..495b842 100644 --- a/inc/world.manager.go +++ b/inc/world.manager.go @@ -1,81 +1,52 @@ 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 ( 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 const ( - ModePlay Mode = "play" // full HUD - ModeCutscene Mode = "cutscene" // letterbox, HUD hidden - ModeMenu Mode = "menu" // main menu, HUD hidden + ModePlay Mode = "play" + ModeCutscene Mode = "cutscene" + ModeMenu Mode = "menu" ) -// World holds the runtime state that does not belong in a save file. type World struct { G *inkwell.Game mode Mode - scene string // current scene — inkwell exposes no accessor for it - prev string // the one before it, for screens with no fixed way out - paused bool // is the world stopped (during dialogue), for the TopBar suffix - pending string // the tape currently being loaded into slot 2 - queue []inkwell.Action // HUD-originated actions waiting to run + scene string + prev string + paused bool + pending string + queue []inkwell.Action 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 tapeLine inkwell.Action } -// New wraps an existing inkwell game. func newWorld(g *inkwell.Game) *World { return &World{G: g, mode: ModePlay} } -// Mode returns the current HUD mode. func (w *World) Mode() Mode { return w.mode } -// SetMode switches the HUD mode. 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 } -// Scene returns the name of the current scene. func (w *World) Scene() string { return w.scene } -// Previous returns the screen we came from, empty at the start of the game. func (w *World) Previous() string { return w.prev } -// Paused reports whether the world is stopped (during dialogue). func (w *World) Paused() bool { return w.paused } -// SetTopBar hands over the location bar so the pump can write its suffix. 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 { if v, ok := w.G.State.Var(VarSlot2).(string); ok { return v @@ -83,19 +54,14 @@ func (w *World) Slot2() string { return "" } -// SetSlot2 sets the contents of the second tape slot. 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 } -// TakePending returns and clears the tape waiting to be loaded. func (w *World) TakePending() string { item := w.pending w.pending = "" return item } -// TapeWaiting reports whether a tape line has been offered but not yet spoken. func (w *World) TapeWaiting() bool { return w.tapeWaiting } diff --git a/inc/world.manager_test.go b/inc/world.manager_test.go index e03cb47..1eb9893 100644 --- a/inc/world.manager_test.go +++ b/inc/world.manager_test.go @@ -6,9 +6,6 @@ import ( 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) { 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) if w.G.State.Var(VarSlot2) != ItemMysteryTape { 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) { w := newWorld(inkwell.NewGame("t", 320, 200)) w.SetPending(ItemMysteryTape) diff --git a/main.go b/main.go index da2bbc5..c8d02fd 100644 --- a/main.go +++ b/main.go @@ -10,11 +10,6 @@ import ( "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 var assets embed.FS