The entry-point coordinator read as an internal name ("GameFlow" buried in
scenes/flow/), so it wasn't obvious THIS is the scene that boots first — the
title screen looked like the entry point. Renamed to the Godot-conventional
Main (scenes/Main.tscn + scripts/main.gd) so the boot scene is self-evident.
TitleScreen is unchanged and still shown BY Main as its first screen.
Also drops the hand-written gd_scene uid (now matches sibling scenes) —
closes the Minor review finding. Full suite 327/327 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
72 lines
2.8 KiB
GDScript
72 lines
2.8 KiB
GDScript
extends "res://addons/gut/test.gd"
|
|
|
|
const SCENE := "res://scenes/Main.tscn"
|
|
const ContentDB = preload("res://scripts/content/content_db.gd")
|
|
const FakeTransport = preload("res://tests/doubles/fake_transport.gd")
|
|
const DmResponse = preload("res://scripts/net/dm_response.gd")
|
|
|
|
|
|
func _flow() -> Node:
|
|
# Inject fakes BEFORE _ready (add_child): a real world + a network-free service,
|
|
# so the shell's initial narrate never touches HTTP.
|
|
var node = load(SCENE).instantiate()
|
|
var world = ContentDB.new()
|
|
world.load_from(ContentDB.default_content_root())
|
|
node.world = world
|
|
node.origin = ContentDB.load_json(ContentDB.origin_path("deserter"))
|
|
var transport = FakeTransport.new()
|
|
transport.set_response(DmResponse.ok(200, {"prose": "Rain on the cobbles."}))
|
|
node.service = DmService.new(transport, FallbackLibrary.new())
|
|
add_child_autofree(node)
|
|
return node
|
|
|
|
|
|
func _creation() -> Dictionary:
|
|
return {
|
|
"name": "Dagnet", "race_id": "human", "calling_id": "sellsword",
|
|
"seed": 8675309, "spend": {},
|
|
"skills": ["athletics", "endurance"], "bonus_skill": "perception",
|
|
}
|
|
|
|
|
|
func test_boots_into_the_title():
|
|
var flow := _flow()
|
|
assert_true(flow._current is TitleScreen, "Main opens on the title")
|
|
|
|
|
|
func test_new_game_shows_creation():
|
|
var flow := _flow()
|
|
flow._current.emit_signal("menu_activated", &"new_game")
|
|
assert_true(flow._current is CharacterCreation, "new game routes to creation")
|
|
|
|
|
|
func test_confirming_creation_reaches_the_shell_as_the_created_character():
|
|
var flow := _flow()
|
|
flow._current.emit_signal("menu_activated", &"new_game")
|
|
flow._current.emit_signal("creation_confirmed", _creation())
|
|
assert_true(flow._current is MainWindowShell, "creation routes to the shell")
|
|
assert_eq(flow._current._log.player.name, "Dagnet",
|
|
"the CREATED character flowed through — not a seed, not a swap alone")
|
|
|
|
|
|
func _assert_stays_on_creation(bad: Dictionary, why: String) -> void:
|
|
var flow := _flow()
|
|
flow._current.emit_signal("menu_activated", &"new_game")
|
|
# Drive the REAL signal with an invalid dict. The CTA gates this out via the UI,
|
|
# but §2 forbids Main trusting the caller and §13 forbids surfacing an error.
|
|
flow._current.emit_signal("creation_confirmed", bad)
|
|
assert_false(flow._current is MainWindowShell, "a failed construct never reaches the shell (§13): %s" % why)
|
|
assert_true(flow._current is CharacterCreation, "the player stays on the valid creation screen: %s" % why)
|
|
|
|
|
|
func test_construct_failure_empty_name_stays_on_creation():
|
|
var bad := _creation()
|
|
bad["name"] = "" # empty name fails NewGame.validate
|
|
_assert_stays_on_creation(bad, "empty name")
|
|
|
|
|
|
func test_construct_failure_bad_calling_stays_on_creation():
|
|
var bad := _creation()
|
|
bad["calling_id"] = "bard" # NPC-only / not a real calling — fails NewGame.validate
|
|
_assert_stays_on_creation(bad, "unknown calling")
|