314 lines
10 KiB
GDScript
314 lines
10 KiB
GDScript
extends SceneTree
|
|
|
|
var failures: Array[String] = []
|
|
var war_outcomes: Array[StringName] = []
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
_test_combat_and_wolf()
|
|
_test_wolf_closes_distance_before_striking()
|
|
_test_npc_death_signals_observe_authoritative_death()
|
|
_test_war_motivation()
|
|
_test_player_combat_and_restore()
|
|
_test_raid_plunder_uses_authoritative_pantry()
|
|
_finish()
|
|
|
|
|
|
func _test_combat_and_wolf() -> void:
|
|
var manager := _create_manager(981)
|
|
manager.update_player_combatant(Vector3(100.0, 0.0, 100.0))
|
|
var npc: SimNPC = manager.npcs[0]
|
|
npc.position = Vector3.ZERO
|
|
var npc_combatant: CombatantStateRecord = manager.get_combatant(
|
|
SimulationIds.npc_combatant_id(npc.id)
|
|
)
|
|
_check(
|
|
(
|
|
npc_combatant != null
|
|
and is_equal_approx(npc_combatant.get_health(), 100.0)
|
|
and npc_combatant.get_faction_id() == SimulationIds.FACTION_VILLAGE
|
|
),
|
|
"Each living villager should gain a village combatant record"
|
|
)
|
|
var wolf_id: StringName = manager.spawn_wolf(Vector3(1.0, 0.0, 0.0))
|
|
_check(wolf_id != &"", "A hungry wolf should spawn as a hostile combatant")
|
|
var wolf: CombatantStateRecord = manager.get_combatant(wolf_id)
|
|
_check(wolf.is_hostile(), "A spawned wolf should be hostile")
|
|
manager.simulate_tick()
|
|
_check(
|
|
wolf.get_health() == 40.0 and npc_combatant.get_health() < 100.0,
|
|
"A nearby wolf should wound the villager with its claw"
|
|
)
|
|
var hurt_found := _has_event(manager, SimulationIds.EVENT_COMBATANT_HURT)
|
|
_check(hurt_found, "The wolf's attack should record a combatant_hurt fact")
|
|
for _tick in 40:
|
|
manager.simulate_tick()
|
|
_check(
|
|
npc.is_dead or not wolf.is_alive(),
|
|
"A prolonged wolf fight should resolve with a death on one side"
|
|
)
|
|
manager.free()
|
|
|
|
|
|
func _test_war_motivation() -> void:
|
|
var strong_manager := _create_manager(982)
|
|
_set_all_strength(strong_manager, 9.0)
|
|
strong_manager.war_resolved.connect(_on_war_resolved)
|
|
for _tick in 400:
|
|
strong_manager.simulate_tick()
|
|
var strong_plan: StringName = strong_manager.get_tribe_war_plan()
|
|
var strong_raiders := _count_raiders(strong_manager)
|
|
_check(
|
|
(
|
|
strong_plan in [SimulationIds.WAR_PLAN_PLANNED, SimulationIds.WAR_PLAN_NONE]
|
|
and strong_raiders == 0
|
|
),
|
|
"A strong village should make the hungry tribe plan but never dare to raid"
|
|
)
|
|
strong_manager.free()
|
|
|
|
var weak_manager := _create_manager(983)
|
|
_set_all_strength(weak_manager, 1.0)
|
|
var weak_outcomes: Array[StringName] = []
|
|
weak_manager.war_resolved.connect(func(outcome): weak_outcomes.append(outcome))
|
|
for _tick in 200:
|
|
weak_manager.simulate_tick()
|
|
_check(
|
|
weak_manager.get_tribe_war_plan() == SimulationIds.WAR_PLAN_PLANNED,
|
|
"A hungry tribe should first watch a weak food-rich village before striking"
|
|
)
|
|
for _tick in 200:
|
|
weak_manager.simulate_tick()
|
|
_check(
|
|
weak_manager.get_tribe_war_plan() == SimulationIds.WAR_PLAN_RAIDING,
|
|
"A weak village with plenty of food should be raided once the tribe commits"
|
|
)
|
|
_check(_count_raiders(weak_manager) > 0, "The raid should spawn hostile raiders")
|
|
var resolved := false
|
|
for _tick in 600:
|
|
weak_manager.simulate_tick()
|
|
if weak_manager.get_tribe_war_plan() != SimulationIds.WAR_PLAN_RAIDING:
|
|
resolved = true
|
|
break
|
|
_check(
|
|
resolved and not weak_outcomes.is_empty(),
|
|
"The raid should resolve into a war outcome with recorded consequences"
|
|
)
|
|
weak_manager.free()
|
|
|
|
|
|
func _test_wolf_closes_distance_before_striking() -> void:
|
|
var manager := _create_manager(986)
|
|
var target: SimNPC = manager.npcs[0]
|
|
target.position = Vector3.ZERO
|
|
for index in range(1, manager.npcs.size()):
|
|
manager.npcs[index].position = Vector3(80.0 + index, 0.0, 80.0)
|
|
manager.update_player_combatant(Vector3(120.0, 0.0, 120.0))
|
|
var target_combatant: CombatantStateRecord = manager.get_combatant(
|
|
SimulationIds.npc_combatant_id(target.id)
|
|
)
|
|
var wolf_id: StringName = manager.spawn_wolf(Vector3(10.0, 0.0, 0.0))
|
|
var wolf: CombatantStateRecord = manager.get_combatant(wolf_id)
|
|
var health_before := target_combatant.get_health()
|
|
var distance_before := wolf.get_position().distance_to(target.position)
|
|
|
|
manager.simulate_tick()
|
|
|
|
_check(
|
|
is_equal_approx(target_combatant.get_health(), health_before),
|
|
"The 18 metre wolf hunt radius should not become a remote melee strike"
|
|
)
|
|
_check(
|
|
wolf.get_position().distance_to(target.position) < distance_before,
|
|
"An alerted wolf should move its authoritative position toward its defender"
|
|
)
|
|
manager.free()
|
|
|
|
|
|
func _test_npc_death_signals_observe_authoritative_death() -> void:
|
|
var manager := _create_manager(987)
|
|
var npc: SimNPC = manager.npcs[0]
|
|
npc.current_task = SimulationIds.ACTION_GATHER_FOOD
|
|
npc.task_state = SimNPC.TASK_STATE_TRAVELING
|
|
npc.target_id = &"missing_test_resource"
|
|
npc.has_travel_target = true
|
|
var death_observed := [false]
|
|
var task_change_observed := [false]
|
|
manager.npc_died.connect(
|
|
func(dead_npc: SimNPC):
|
|
death_observed[0] = (
|
|
dead_npc == npc
|
|
and dead_npc.is_dead
|
|
and dead_npc.current_task == SimulationIds.ACTION_DEAD
|
|
and dead_npc.target_id == &""
|
|
and not dead_npc.has_travel_target
|
|
)
|
|
)
|
|
manager.npc_task_changed.connect(
|
|
func(changed_npc: SimNPC, old_task: StringName, new_task: StringName):
|
|
task_change_observed[0] = (
|
|
changed_npc == npc
|
|
and changed_npc.is_dead
|
|
and old_task == SimulationIds.ACTION_GATHER_FOOD
|
|
and new_task == SimulationIds.ACTION_DEAD
|
|
)
|
|
)
|
|
var combatant: CombatantStateRecord = manager.get_combatant(
|
|
SimulationIds.npc_combatant_id(npc.id)
|
|
)
|
|
manager.conflict_system.call(
|
|
"_apply_damage", combatant, combatant.get_health(), SimulationIds.PLAYER_ACTOR_ID
|
|
)
|
|
_check(
|
|
death_observed[0] and task_change_observed[0],
|
|
"NPC death and task listeners should observe an already-dead, released simulation record"
|
|
)
|
|
manager.free()
|
|
|
|
|
|
func _test_player_combat_and_restore() -> void:
|
|
var manager := _create_manager(984)
|
|
var friendly: SimNPC = manager.npcs[0]
|
|
_check(
|
|
is_equal_approx(manager.player_attack(SimulationIds.npc_combatant_id(friendly.id)), 0.0),
|
|
"Simulation authority should reject a player attack against a friendly villager"
|
|
)
|
|
var distant_wolf_id: StringName = manager.spawn_wolf(Vector3(20.0, 0.0, 0.0))
|
|
_check(
|
|
is_equal_approx(manager.player_attack(distant_wolf_id), 0.0),
|
|
"Simulation authority should reject an out-of-range sword strike"
|
|
)
|
|
var wolf_id := distant_wolf_id
|
|
var wolf: CombatantStateRecord = manager.get_combatant(wolf_id)
|
|
wolf.set_position(Vector3.ZERO)
|
|
_check(
|
|
is_equal_approx(manager.player_attack(wolf_id), 24.0),
|
|
"The player's sword strike should deal its configured damage"
|
|
)
|
|
_check(is_equal_approx(wolf.get_health(), 16.0), "The wolf should survive one sword strike")
|
|
_check(
|
|
is_equal_approx(manager.player_attack(wolf_id), 0.0),
|
|
"Simulation authority should enforce the configured player attack cooldown"
|
|
)
|
|
var sword := SimulationItems.get_weapon(SimulationIds.ITEM_SWORD)
|
|
manager.advance_player_combat_time(sword.attack_cooldown)
|
|
manager.player_attack(wolf_id)
|
|
_check(not wolf.is_alive(), "A second sword strike should kill the wolf")
|
|
_check(
|
|
_has_event(manager, SimulationIds.EVENT_COMBATANT_KILLED),
|
|
"The player's kill should record a combatant_killed fact"
|
|
)
|
|
_check(
|
|
manager.get_living_hostile_combatants().is_empty(),
|
|
"The dead wolf should leave the living-hostile set"
|
|
)
|
|
|
|
manager.get_player_state().take_damage(35.0)
|
|
manager.call("_sync_player_combatant")
|
|
var saved_json: String = manager.serialize_state()
|
|
var restored := _create_manager(985)
|
|
_check(
|
|
restored.restore_state_from_json(saved_json),
|
|
"Combatants and factions should survive schema-v14 save and restore"
|
|
)
|
|
_check(
|
|
(
|
|
restored.get_state_checksum() == manager.get_state_checksum()
|
|
and restored.get_faction(SimulationIds.FACTION_TRIBE) != null
|
|
and not restored.get_combatant(wolf_id).is_alive()
|
|
and is_equal_approx(
|
|
restored.get_combatant(&"player_combatant").get_health(),
|
|
restored.get_player_state().get_health()
|
|
)
|
|
),
|
|
"Restore should preserve hostile state, factions, checksum, and player health parity"
|
|
)
|
|
restored.free()
|
|
manager.free()
|
|
|
|
|
|
func _test_raid_plunder_uses_authoritative_pantry() -> void:
|
|
var manager := _create_manager(988)
|
|
var pantry: StorageStateRecord = manager.get_pantry()
|
|
pantry.withdraw(SimulationIds.RESOURCE_FOOD, pantry.get_amount(SimulationIds.RESOURCE_FOOD))
|
|
pantry.deposit(SimulationIds.RESOURCE_FOOD, 10.0)
|
|
manager.economy.sync_resource(SimulationIds.RESOURCE_FOOD)
|
|
var village_faction: FactionStateRecord = manager.get_faction(SimulationIds.FACTION_VILLAGE)
|
|
var tribe: FactionStateRecord = manager.get_faction(SimulationIds.FACTION_TRIBE)
|
|
village_faction.set_food(10.0)
|
|
var tribe_food_before := tribe.get_food()
|
|
|
|
manager.conflict_system.call("_finish_war", SimulationIds.WAR_PLAN_ABORTED, &"village_lost")
|
|
|
|
_check(
|
|
(
|
|
is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), 4.0)
|
|
and is_equal_approx(manager.village.food, 4.0)
|
|
and is_equal_approx(village_faction.get_food(), 4.0)
|
|
and is_equal_approx(tribe.get_food(), tribe_food_before + 6.0)
|
|
),
|
|
"Raid plunder should transfer real pantry food instead of mutating only faction display state"
|
|
)
|
|
manager.free()
|
|
|
|
|
|
func _set_all_strength(manager: Node, strength: float) -> void:
|
|
for npc in manager.npcs:
|
|
npc.strength = strength
|
|
|
|
|
|
func _count_raiders(manager: Node) -> int:
|
|
var count := 0
|
|
for combatant in manager.conflict_system.get_all_combatants():
|
|
if combatant.get_kind() == SimulationIds.COMBATANT_KIND_RAIDER:
|
|
count += 1
|
|
return count
|
|
|
|
|
|
func _has_event(manager: Node, event_type: StringName) -> bool:
|
|
for event in manager.economic_events:
|
|
if StringName(event.data["event_type"]) == event_type:
|
|
return true
|
|
return false
|
|
|
|
|
|
func _on_war_resolved(_outcome: StringName) -> void:
|
|
war_outcomes.append(_outcome)
|
|
|
|
|
|
func _create_manager(seed_value: int = 981) -> Node:
|
|
var manager: Node = load("res://simulation/SimulationManager.gd").new()
|
|
manager.simulation_seed = seed_value
|
|
manager.debug_logs = false
|
|
var home_positions: Array[Vector3] = [
|
|
Vector3(0.0, 0.0, 0.0),
|
|
Vector3(2.0, 0.0, 0.0),
|
|
Vector3(10.0, 0.0, 0.0),
|
|
Vector3(12.0, 0.0, 0.0),
|
|
Vector3(30.0, 0.0, 30.0),
|
|
Vector3(40.0, 0.0, 40.0),
|
|
]
|
|
manager.home_positions = home_positions
|
|
root.add_child(manager)
|
|
manager.set_process(false)
|
|
return manager
|
|
|
|
|
|
func _finish() -> void:
|
|
if failures.is_empty():
|
|
print("[TEST] Conflict and war passed: combat, wolves, motivation, raids")
|
|
quit(0)
|
|
return
|
|
for failure in failures:
|
|
push_error("[TEST] " + failure)
|
|
quit(1)
|
|
|
|
|
|
func _check(condition: bool, message: String) -> void:
|
|
if not condition:
|
|
failures.append(message)
|