Files
realworld/internal/boot/boot_test.go
T
2026-08-29 19:07:32 +02:00

139 lines
3.9 KiB
Go

package boot_test
import (
"testing"
inkwell "git.teletypegames.org/engines/inkwell"
"realworld/internal/boot"
"realworld/internal/names"
"realworld/internal/ui"
)
func build(t *testing.T) *inkwell.Game {
t.Helper()
g := boot.New(boot.Opts{})
if err := g.Validate(); err != nil {
t.Fatalf("Validate: %v", err)
}
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 := build(t)
for _, n := range []string{names.DlgDex, names.DlgMysteryTape} {
if !g.DialogueManager.Has(n) {
t.Errorf("missing dialogue: %q", n)
}
}
for _, n := range []string{names.ScriptTapeInsert, names.ScriptFinale} {
if !g.ScriptManager.Has(n) {
t.Errorf("missing script: %q", n)
}
}
for _, n := range []string{names.ItemNoodleLetter, names.ItemMysteryTape, names.ItemBlackArmilla} {
if !g.ItemManager.Has(n) {
t.Errorf("missing item: %q", n)
}
}
for n := range names.Tapes {
if !g.CharacterManager.Has(n) {
t.Errorf("tape character not registered: %q", n)
}
}
// Every loadable tape needs an item, a character and a dialogue.
for item, char := range names.TapeItems {
if !g.ItemManager.Has(item) {
t.Errorf("tape item not registered: %q", item)
}
if !g.CharacterManager.Has(char) {
t.Errorf("tape character not registered: %q", char)
}
if !g.DialogueManager.Has(names.TapeDialogue(item)) {
t.Errorf("tape %q has no dialogue: %q", item, names.TapeDialogue(item))
}
}
}
// 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 := build(t)
paul, _ := g.CharacterManager.Get(names.Paul)
seen := map[[4]uint32]string{}
for n := range names.Tapes {
c, _ := g.CharacterManager.Get(n)
if c.SpeechColor == nil {
t.Errorf("%s: no SpeechColor", n)
continue
}
if c.SpeechColor == paul.SpeechColor {
t.Errorf("%s speaks in the same colour as Paul", n)
}
r, gr, b, a := c.SpeechColor.RGBA()
key := [4]uint32{r, gr, b, a}
if other, dup := seen[key]; dup {
t.Errorf("%s and %s share a voice colour", n, other)
}
seen[key] = n
}
}
// HUD geometry: the two channels must not overlap, and both must stay inside
// the HUD strip.
func TestHUDGeometry(t *testing.T) {
g := build(t)
ch, ok := g.UIManager.Get("channel")
if !ok {
t.Fatal("no 'channel' widget")
}
tc := ch.(*ui.TapeChannel)
if tc.Bounds.X < ui.DividerX {
t.Errorf("tape channel crosses into the world side: X=%v < %v", tc.Bounds.X, ui.DividerX)
}
if tc.Bounds.X+tc.Bounds.W > ui.ScreenW {
t.Errorf("tape channel runs off screen: %v", tc.Bounds.X+tc.Bounds.W)
}
if tc.Bounds.Y < ui.HUDTop {
t.Errorf("tape channel reaches into the scene: Y=%v < %v", tc.Bounds.Y, ui.HUDTop)
}
sl, ok := g.UIManager.Get("tapes")
if !ok {
t.Fatal("no 'tapes' widget")
}
if ts := sl.(*ui.TapeSlots); ts.Bounds.X+ts.Bounds.W > ui.DividerX {
t.Errorf("tape slots cross the divider: %v > %v", ts.Bounds.X+ts.Bounds.W, ui.DividerX)
}
// 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")
}
if box := db.(*inkwell.DialogBox); box.Bounds.X+box.Bounds.W > ui.DividerX {
t.Errorf("dialogue box would cover the tape channel: %v > %v",
box.Bounds.X+box.Bounds.W, ui.DividerX)
}
}
// The guard must be registered FIRST so it ticks LAST among the widgets —
// otherwise it would swallow HUD clicks.
func TestWidgetOrder(t *testing.T) {
g := build(t)
n := g.UIManager.Names()
if len(n) == 0 || n[0] != "usewith_guard" {
t.Errorf("usewith_guard is not registered first: %v", n)
}
if n[len(n)-1] != "cursor" {
t.Errorf("cursor is not registered last: %v", n)
}
}