139 lines
4.0 KiB
Go
139 lines
4.0 KiB
Go
package boot_test
|
|
|
|
import (
|
|
"testing"
|
|
|
|
inkwell "git.teletypegames.org/engines/inkwell"
|
|
|
|
"git.teletypegames.org/games/realworld/internal/boot"
|
|
"git.teletypegames.org/games/realworld/internal/names"
|
|
"git.teletypegames.org/games/realworld/internal/ui"
|
|
)
|
|
|
|
func build(t *testing.T) *inkwell.Game {
|
|
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)
|
|
}
|
|
}
|