189 lines
8.9 KiB
GDScript
189 lines
8.9 KiB
GDScript
extends SceneTree
|
|
|
|
var _checks := 0
|
|
var _failures := 0
|
|
var _clock: Variant
|
|
var _state: Variant
|
|
var _runtime: Variant
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
func _run() -> void:
|
|
print("S1: runtime world and construction integration")
|
|
_reset_runtime()
|
|
_test_world_bootstrap()
|
|
_test_rank_expansion()
|
|
_test_build_to_completion()
|
|
_test_overlap_is_atomic()
|
|
_test_mid_build_snapshot()
|
|
_test_cancel_and_demolish()
|
|
_cleanup()
|
|
if _failures == 0:
|
|
print("PASS: %d checks" % _checks)
|
|
else:
|
|
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
|
|
quit(_failures)
|
|
|
|
func _reset_runtime() -> void:
|
|
_cleanup()
|
|
_clock = load("res://autoload/game_clock.gd").new()
|
|
_state = load("res://autoload/game_state.gd").new()
|
|
_runtime = load("res://autoload/simulation_runtime.gd").new()
|
|
_clock.reset(1, 6, 0)
|
|
_state.reset()
|
|
_runtime.initialize(_clock, _state, 8080)
|
|
_clock.set_paused(true)
|
|
|
|
func _cleanup() -> void:
|
|
if _runtime != null:
|
|
_runtime.free()
|
|
if _clock != null:
|
|
_clock.free()
|
|
if _state != null:
|
|
_state.free()
|
|
_runtime = null
|
|
_clock = null
|
|
_state = null
|
|
|
|
func _test_world_bootstrap() -> void:
|
|
var bounds: Dictionary = _runtime.world_service.get_bounds()
|
|
_expect(bounds == {&"min_x": -12, &"max_x": 12, &"min_y": 0, &"max_y": 6}, "Rank 1 world uses canonical map bounds")
|
|
_expect(_runtime.world_service.is_walkable(Vector3i(-12, 0, WorldService.LAYER_FRONT)), "starter FRONT boundary corridor is walkable")
|
|
_expect(_runtime.world_service.is_support_anchor(Vector3i(0, 0, WorldService.LAYER_FRONT)), "starter boundary corridor is structurally anchored")
|
|
_expect(_runtime.path_service.has_connector(&"starter.staff_door"), "starter staff door explicitly connects FRONT and BACK")
|
|
|
|
func _test_rank_expansion() -> void:
|
|
var reputation = _runtime.submit_command(
|
|
&"update_progression_metrics",
|
|
{&"metrics": {&"reputation": 25}},
|
|
&"player", &"", 100, -1, &"s1.rank.reputation"
|
|
)
|
|
var capital = _runtime.submit_command(
|
|
&"change_resources",
|
|
{&"deltas": {&"gold": 5_000}},
|
|
&"player", &"", 100, -1, &"s1.rank.capital"
|
|
)
|
|
_expect(reputation.is_success() and capital.is_success(), "Rank 2 eligibility facts are recorded before promotion")
|
|
var result = _runtime.submit_command(
|
|
&"set_world_tree_rank",
|
|
{&"rank": 2},
|
|
&"player", &"", 100, -1, &"s1.rank.two"
|
|
)
|
|
_expect(result.is_success() and _state.world_tree_rank == 2, "rank command expands progression exactly once")
|
|
_expect(_runtime.world_service.get_bounds() == {&"min_x": -18, &"max_x": 18, &"min_y": -4, &"max_y": 10}, "Rank 2 uses canonical expanded bounds")
|
|
_expect(_runtime.world_service.is_walkable(Vector3i(18, 0, WorldService.LAYER_FRONT)), "new boundary corridor cells become walkable")
|
|
_expect(not _runtime.world_service.is_excavated(Vector3i(0, -1, WorldService.LAYER_FRONT)), "new underground cells remain unexcavated")
|
|
var normalize_capital = _runtime.submit_command(
|
|
&"change_resources",
|
|
{&"deltas": {&"gold": -5_000}},
|
|
&"player", &"", 100, -1, &"s1.rank.capital.normalize"
|
|
)
|
|
_expect(normalize_capital.is_success() and _state.get_resource(&"gold") == 5_000, "Rank fixture restores the construction budget")
|
|
var rejected = _runtime.submit_command(
|
|
&"set_world_tree_rank", {&"rank": 1}, &"player", &"", 100, -1, &"s1.rank.shrink"
|
|
)
|
|
_expect(not rejected.is_success(), "world rank and bounds cannot shrink")
|
|
|
|
func _test_build_to_completion() -> void:
|
|
var result = _runtime.submit_command(
|
|
&"build_facility",
|
|
{&"definition_id": &"reception", &"origin": Vector3i(-6, 0, WorldService.LAYER_FRONT), &"rotation": 0},
|
|
&"player", &"", 50, -1, &"s1.build.reception"
|
|
)
|
|
var build_label := "valid build command creates a construction job"
|
|
if not result.is_success():
|
|
build_label += " (%s: %s; %s)" % [CommandResult.Code.keys()[result.code], result.message, result.details]
|
|
_expect(result.is_success(), build_label)
|
|
var job_id := StringName(result.details.get(&"job_id", &""))
|
|
var job = _runtime.construction_service.get_job(job_id)
|
|
_expect(job != null and job.state == ConstructionJob.State.PLANNED, "new facility begins as PLANNED")
|
|
_expect(_state.get_resource(&"gold") == 4500 and _state.get_resource(&"materials") == 42, "construction cost is paid once")
|
|
_expect(not job_id.is_empty() and _runtime.world_service.get_occupant(Vector3i(-6, 0, 0)) == job_id, "construction job reserves its footprint")
|
|
_expect(_runtime.latest_digest.length() == 64, "build command leaves a serializable runtime snapshot")
|
|
if job == null:
|
|
return
|
|
_clock.advance_minutes(119)
|
|
_expect(job.state == ConstructionJob.State.BUILDING and job.progress < ConstructionJob.MAX_PROGRESS, "job remains BUILDING before its final minute")
|
|
_clock.advance_one_minute()
|
|
_expect(job.state == ConstructionJob.State.COMPLETED, "job completes at its configured build time")
|
|
var facilities: Array[Dictionary] = _runtime.construction_service.get_facilities()
|
|
_expect(facilities.size() == 1, "completion creates one facility instance")
|
|
var instance_id := StringName(facilities[0][&"instance_id"])
|
|
_expect(_runtime.world_service.get_occupant(Vector3i(-6, 0, 0)) == instance_id, "completion transfers world occupancy to the facility")
|
|
var saw_completion := false
|
|
for event in _runtime.domain_events.recent_events():
|
|
if event.event_type == &"FACILITY_COMPLETED" and instance_id in event.entity_ids:
|
|
saw_completion = true
|
|
_expect(saw_completion, "facility completion is published as a domain event")
|
|
|
|
func _test_overlap_is_atomic() -> void:
|
|
var before_resources: Dictionary = _state.create_snapshot()
|
|
var before_job_count: int = _runtime.construction_service.get_jobs().size()
|
|
var result = _runtime.submit_command(
|
|
&"build_facility",
|
|
{&"definition_id": &"reception", &"origin": [-6, 0, 0], &"rotation": 0},
|
|
&"player", &"", 50, -1, &"s1.build.overlap"
|
|
)
|
|
_expect(not result.is_success(), "overlapping construction command is rejected")
|
|
_expect(_state.create_snapshot() == before_resources, "rejected placement spends no resource")
|
|
_expect(_runtime.construction_service.get_jobs().size() == before_job_count, "rejected placement creates no job")
|
|
|
|
func _test_mid_build_snapshot() -> void:
|
|
var result = _runtime.submit_command(
|
|
&"build_facility",
|
|
{&"definition_id": &"warehouse", &"origin": Vector3i(2, 0, WorldService.LAYER_BACK), &"rotation": 0},
|
|
&"player", &"", 40, -1, &"s1.build.warehouse"
|
|
)
|
|
_expect(result.is_success(), "BACK-layer warehouse construction starts")
|
|
var job_id := StringName(result.details[&"job_id"])
|
|
_clock.advance_minutes(90)
|
|
var snapshot: Dictionary = _runtime.create_snapshot()
|
|
var expected_digest := SimulationDigest.compute(snapshot)
|
|
var expected_progress: int = _runtime.construction_service.get_job(job_id).progress
|
|
_clock.advance_minutes(90)
|
|
_expect(_runtime.construction_service.get_job(job_id).state == ConstructionJob.State.COMPLETED, "warehouse can complete after the snapshot")
|
|
_expect(_runtime.restore_snapshot(snapshot), "mid-build world and construction snapshot restores")
|
|
_expect(_runtime.latest_digest == expected_digest, "mid-build restore reproduces the exact digest")
|
|
_expect(_runtime.construction_service.get_job(job_id).progress == expected_progress, "mid-build integer progress restores exactly")
|
|
_clock.advance_minutes(90)
|
|
_expect(_runtime.construction_service.get_job(job_id).state == ConstructionJob.State.COMPLETED, "restored build completes once")
|
|
|
|
func _test_cancel_and_demolish() -> void:
|
|
var materials_before_cancel: int = _state.get_resource(&"materials")
|
|
var start = _runtime.submit_command(
|
|
&"build_facility",
|
|
{&"definition_id": &"cafeteria", &"origin": Vector3i(7, 0, WorldService.LAYER_FRONT), &"rotation": 90},
|
|
&"player", &"", 30, -1, &"s1.build.cancel_target"
|
|
)
|
|
_expect(start.is_success(), "cancellable construction starts")
|
|
var job_id := StringName(start.details[&"job_id"])
|
|
var cancel = _runtime.submit_command(
|
|
&"cancel_construction",
|
|
{&"job_id": job_id},
|
|
&"player", &"", 30, -1, &"s1.cancel.target"
|
|
)
|
|
_expect(cancel.is_success(), "zero-progress construction cancels")
|
|
_expect(_state.get_resource(&"materials") == materials_before_cancel, "zero-progress cancellation returns all materials")
|
|
_expect(_runtime.world_service.get_entity_cells(job_id).is_empty(), "cancellation releases reserved world cells")
|
|
|
|
var facilities: Array[Dictionary] = _runtime.construction_service.get_facilities()
|
|
var reception_id := StringName(facilities[0][&"instance_id"])
|
|
var materials_before_demolition: int = _state.get_resource(&"materials")
|
|
var demolition = _runtime.submit_command(
|
|
&"demolish_facility",
|
|
{&"instance_id": reception_id},
|
|
&"player", &"", 30, -1, &"s1.demolish.reception"
|
|
)
|
|
_expect(demolition.is_success(), "completed facility can be demolished through a command")
|
|
_expect(_state.get_resource(&"materials") == materials_before_demolition + 2, "demolition returns 25 percent of reception materials")
|
|
_expect(_runtime.world_service.get_entity_cells(reception_id).is_empty(), "demolition releases facility occupancy")
|
|
|
|
func _expect(condition: bool, label: String) -> void:
|
|
_checks += 1
|
|
if condition:
|
|
print(" ✓ %s" % label)
|
|
return
|
|
_failures += 1
|
|
push_error(" ✗ %s" % label)
|