extends SceneTree
## GATE — ghost creatures (demo sprint §5), on the REAL wiring: game.tscn as /root/Game,
## its own change_map, and creatures as what they are now — BOTS with a creature body
## (#114 phase 5: the creature spawner and its listener are retired). The ghosts were
## creatures drawn frozen in a map they no longer belonged to, or drawn twice.
## C1 a map-bound creature bot's body is inside the current map
## C2 the authority draws each creature ONCE (its simulated body), not a twin
## C3 a declared row from an earlier session is REVIVED (simulated), not drawn frozen
## C4 after change_map no creature body from the old map is left anywhere
## C5 an authority that LOSES authority stops simulating but still draws each
## creature once, in its map; TAKING it back simulates them again
## A1 the elected authority writes bot registry rows whatever its Keet role; a peer
## that is NOT the authority still needs mod
## B1 a map-bound bot row HELD for map A (it arrived before A existed) is dropped by
## the map change, not revived into map B
##
## Run: CI=true HYPERTPS_DAEMON_TCP=127.0.0.1:1 "$HYPER_TPS_GODOT" --headless --path . --script res://scenes/test/gate_ghost_creatures.gd
const MAP_A := "res://scenes/maps/map_flat.tscn"
const MAP_B := "res://scenes/maps/map_test.tscn"
const LIVE := "ghost-hopper"
const STALE := "ghost-stale"
var ok := true
var checks := 0
var _frame := 0
var _game: Node
var _br: Node
var _gs: Node
func _p(c: bool, m: String, got: Variant = "") -> void:
checks += 1
print(("ok " if c else "FAIL: ") + m + ("" if str(got) == "" else " (" + str(got) + ")"))
if not c:
ok = false
func _creature_nodes() -> Array:
var out: Array = []
for n in root.find_children("*", "", true, false):
var s: Script = n.get_script()
if s != null and ["GroundCrawler", "FlyingSerpent", "Hopper"].has(String(s.get_global_name())) \
and not n.is_queued_for_deletion():
out.append(n)
return out
func _named(id: String) -> Array:
var out: Array = []
for n in _creature_nodes():
if String(n.name) == "Bot_" + id or String(n.name).begins_with("Bot_" + id + "@"):
out.append(n)
return out
func _process(_d: float) -> bool:
_frame += 1
match _frame:
1:
_game = (load("res://scenes/game/game.tscn") as PackedScene).instantiate()
_game.name = "Game"
root.add_child(_game)
30:
_game.call("change_map", MAP_A, Vector3.ZERO)
45:
_stage_a()
47:
_stage_c5()
50:
# B1 setup: what BotRegistry._revive does when the row's map is not there yet.
(_br.get("_pending_rows") as Dictionary)["gate_held_bot"] = {"bindings": ["map"],
"row": {"body": "position", "brain": {"kind": "none"}}}
_br.set_process(true) # as _revive does when it holds a row
_game.call("change_map", MAP_B, Vector3.ZERO)
65:
_stage_b()
print("\n%d checks — %s" % [checks, "PASS" if ok else "FAIL"])
quit(0 if ok else 1)
return true
return false
func _stage_a() -> void:
_br = root.get_node("/root/BotRegistry")
_gs = root.get_node("/root/GameState")
var map: Node = _game.get("current_map")
# The registry is mod-gated; offline the gate has no role, and a refused write is
# silent — every check below would pass vacuously. Grant it, as other gates do.
var keet: Node = root.get_node_or_null("/root/KeetService")
if keet != null:
keet.current_member_id = "gate-author"
_gs.route("world:bots:registry:*", 1, "peer")
_p(map != null and String(map.scene_file_path) == MAP_A, "on map A", map.scene_file_path if map != null else "none")
var r: Dictionary = _br.register(LIVE, ["map"], "hopper", {"kind": "none"}, Vector3(4, 0, 4))
_p(bool(r.get("ok", false)), "a creature bot (hopper) is registered on map A", r)
# ---- C1 ----
var body: Node3D = _br.body_of(LIVE)
_p(body != null and map.is_ancestor_of(body), "C1 its body is inside the current map",
str(body.get_path()) if body != null else "none")
# ---- C2 ----
_p(_named(LIVE).size() == 1, "C2 the authority draws it once, not a twin", _named(LIVE).size())
# ---- C3: an earlier session's declaration arrives with the room's state ----
_gs.set_state(_br.KEY_MAP_REGISTRY + STALE, {"body": "crawler", "brain": {"kind": "none"}, "pos": [6.0, 0.0, 6.0]})
_p(_br.is_registered(STALE), "C3 a declared row is revived — simulated by the authority")
_p(_named(STALE).size() == 1, "C3 and drawn once (no frozen passive copy)", _named(STALE).size())
_stage_authority()
## C5: the election moves away and back (menu_main._elect_authority calls
## BotRegistry.on_authority_changed after set_multiplayer_authority).
func _stage_c5() -> void:
var map: Node = _game.get("current_map")
_game.set_multiplayer_authority(999)
_br.on_authority_changed()
var lost_body := _named(LIVE)
_p(not _br.is_registered(LIVE), "C5 lost authority: it no longer simulates the bot")
_p(lost_body.size() == 1 and map.is_ancestor_of(lost_body[0]), "C5 but still draws it once, in its map",
lost_body.size())
_game.set_multiplayer_authority(1)
_br.on_authority_changed()
_p(_br.is_registered(LIVE) and _br.is_registered(STALE), "C5 taking it back: it simulates them again")
_p(_named(LIVE).size() == 1 and _named(STALE).size() == 1, "C5 still drawn once each",
[_named(LIVE).size(), _named(STALE).size()])
## A1: the route table as shipped (registry = mod + authority) — the gate's grant undone.
func _stage_authority() -> void:
var keet: Node = root.get_node_or_null("/root/KeetService")
var role_was: Variant = keet.get("current_role") if keet != null else null
_gs.install_routes()
if keet != null:
keet.set("current_role", "peer")
var key := "world:bots:registry:bot_a1"
_p(_gs.can_write(key), "A1 the authority (role peer) may write a bot registry row")
_game.set_multiplayer_authority(999) # this peer is no longer the authority
_p(not _game.is_multiplayer_authority() and not _gs.can_write(key),
"A1 a non-authority peer (role peer) may not")
if keet != null:
keet.set("current_role", "mod")
_p(_gs.can_write(key), "A1 a non-authority mod still may (the role path is unchanged)")
_p(not _gs.can_write("world:settings:current_map") or String(keet.get("current_role")) == "admin",
"A1 the exception is bot rows only (world:settings stays admin)")
_game.set_multiplayer_authority(1)
if keet != null:
keet.set("current_role", role_was if role_was != null else "")
_gs.route("world:bots:registry:*", 1, "peer")
func _stage_b() -> void:
var map: Node = _game.get("current_map")
_p(map != null and String(map.scene_file_path) == MAP_B, "on map B", map.scene_file_path if map != null else "none")
var left := _creature_nodes()
var where: Array = []
for n in left:
where.append(str((n as Node).get_path()))
_p(left.is_empty(), "C4 no creature from map A is left anywhere", where)
var revived: bool = (_br.get("_bots") as Dictionary).has("gate_held_bot")
_p(not revived and not (_br.get("_pending_rows") as Dictionary).has("gate_held_bot"),
"B1 a row held for map A is dropped by the map change, not revived in map B",
"revived" if revived else "still held" if (_br.get("_pending_rows") as Dictionary).has("gate_held_bot") else "")