perf: harden runtime for weaker hardware
This commit is contained in:
@@ -130,6 +130,10 @@ func _run() -> void:
|
||||
)
|
||||
var reloaded_goat := load("res://world/animals/goat/cozy_goat.tscn").instantiate() as AnimalNode
|
||||
animals_parent.add_child(reloaded_goat)
|
||||
# Freeze presentation movement while proving that binding itself is a pure
|
||||
# reload. A physics step may otherwise land between add_child() and the next
|
||||
# process frame and legitimately advance the authoritative route.
|
||||
reloaded_goat.set_physics_process(false)
|
||||
await process_frame
|
||||
goat = reloaded_goat
|
||||
goat.move_speed = 4.0
|
||||
@@ -141,6 +145,7 @@ func _run() -> void:
|
||||
),
|
||||
"Reloading the self-contained goat scene should bind the same state without resetting it"
|
||||
)
|
||||
goat.set_physics_process(true)
|
||||
for _frame in 3:
|
||||
await physics_frame
|
||||
_check(
|
||||
|
||||
@@ -67,8 +67,23 @@ func _test_player_downing() -> void:
|
||||
for _tick in 22:
|
||||
manager.simulate_tick()
|
||||
_check(not player_state.is_downed(), "The downed player should recover and stand again")
|
||||
var player_combatant: CombatantStateRecord = manager.get_combatant(&"player_combatant")
|
||||
_check(
|
||||
player_state.get_health() > 0.0, "Recovery should restore the player to a fighting baseline"
|
||||
(
|
||||
player_state.get_health() > 0.0
|
||||
and player_combatant != null
|
||||
and player_combatant.is_alive()
|
||||
and is_equal_approx(player_combatant.get_health(), player_state.get_health())
|
||||
),
|
||||
"Recovery should revive both authoritative player records at matching health"
|
||||
)
|
||||
manager.update_player_combatant(Vector3(100.0, 0.0, 100.0))
|
||||
manager.spawn_wolf(Vector3(100.0, 0.0, 100.0))
|
||||
var recovered_health := player_state.get_health()
|
||||
manager.simulate_tick()
|
||||
_check(
|
||||
player_state.get_health() < recovered_health,
|
||||
"A recovered player should become targetable and vulnerable again"
|
||||
)
|
||||
manager.free()
|
||||
|
||||
|
||||
@@ -10,8 +10,11 @@ func _initialize() -> void:
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@@ -98,15 +101,101 @@ func _test_war_motivation() -> void:
|
||||
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 wolf_id: StringName = manager.spawn_wolf(Vector3(0.0, 0.0, 0.0))
|
||||
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(
|
||||
@@ -118,6 +207,8 @@ func _test_player_combat_and_restore() -> void:
|
||||
"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(
|
||||
@@ -129,13 +220,42 @@ func _test_player_combat_and_restore() -> void:
|
||||
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 combatant health, factions, and checksum"
|
||||
"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
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
extends SceneTree
|
||||
|
||||
var failures: Array[String] = []
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var creature := CreatureVisual.new()
|
||||
root.add_child(creature)
|
||||
creature.global_position = Vector3.ZERO
|
||||
creature.set_sim_position(Vector3.ZERO)
|
||||
for _frame in 12:
|
||||
await physics_frame
|
||||
_check(
|
||||
creature.path_query_count == 0,
|
||||
"A creature already at its authoritative position should not request a path"
|
||||
)
|
||||
|
||||
creature.set_sim_position(Vector3(12.0, 0.0, 0.0))
|
||||
for _frame in 6:
|
||||
await physics_frame
|
||||
var first_query_count := creature.path_query_count
|
||||
_check(first_query_count == 1, "A true retarget should request one navigation path")
|
||||
for _frame in 20:
|
||||
await physics_frame
|
||||
_check(
|
||||
creature.path_query_count == first_query_count,
|
||||
"An unreachable target should not be queried again every physics frame"
|
||||
)
|
||||
for _frame in 120:
|
||||
await physics_frame
|
||||
_check(
|
||||
creature.path_query_count <= 4,
|
||||
"Unreachable navigation retries should follow bounded exponential backoff"
|
||||
)
|
||||
|
||||
var before_retarget := creature.path_query_count
|
||||
creature.set_sim_position(Vector3(24.0, 0.0, 0.0))
|
||||
for _frame in 4:
|
||||
await physics_frame
|
||||
_check(
|
||||
creature.path_query_count == before_retarget + 1,
|
||||
"Moving the authoritative target should bypass stale retry delay once"
|
||||
)
|
||||
|
||||
creature.free()
|
||||
if failures.is_empty():
|
||||
print("[TEST] Creature path retry passed: stationary -> backoff -> retarget")
|
||||
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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://db7vm7rdi5blm
|
||||
@@ -47,19 +47,33 @@ func _run() -> void:
|
||||
)
|
||||
|
||||
var wolf_visual: Node3D = combat_view.hostiles[wolf.get_combatant_id()]
|
||||
var sword_definition := SimulationItems.get_weapon(SimulationIds.ITEM_SWORD)
|
||||
_check(
|
||||
is_equal_approx(controller.attack_range, sword_definition.reach),
|
||||
"Player targeting and simulation authority should share the sword definition reach"
|
||||
)
|
||||
player.global_position = wolf_visual.global_position + Vector3(-2.0, 0.0, 0.0)
|
||||
player.look_at(wolf_visual.global_position, Vector3.UP)
|
||||
player.rotation.x = 0.0
|
||||
simulation_manager.update_player_combatant(player.global_position)
|
||||
var wolf_health: float = wolf.get_health()
|
||||
controller.call("_perform_attack")
|
||||
_check(
|
||||
wolf.get_health() < wolf_health,
|
||||
"A player sword strike should damage the hostile wolf through the simulation"
|
||||
)
|
||||
var hit_tween: Tween = wolf_visual.get("_flash_tween") as Tween
|
||||
wolf.set_position(wolf.get_position() + Vector3(0.1, 0.0, 0.0))
|
||||
_check(
|
||||
wolf_visual.get("_flash_tween") == hit_tween,
|
||||
"An injured hostile should not replay its hit flash for a position-only state update"
|
||||
)
|
||||
var struck := 0
|
||||
for _attempt in 6:
|
||||
if not wolf.is_alive():
|
||||
break
|
||||
controller.call("_advance_timers", sword_definition.attack_cooldown)
|
||||
simulation_manager.advance_player_combat_time(sword_definition.attack_cooldown)
|
||||
controller.call("_perform_attack")
|
||||
struck += 1
|
||||
await process_frame
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
extends SceneTree
|
||||
|
||||
var failures: Array[String] = []
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var camera := Camera3D.new()
|
||||
camera.current = true
|
||||
root.add_child(camera)
|
||||
var world: JajceWorld = load("res://world/jajce/JajceWorld.tscn").instantiate()
|
||||
root.add_child(world)
|
||||
await process_frame
|
||||
await physics_frame
|
||||
|
||||
var viewport := root as Viewport
|
||||
var environment: Environment = (
|
||||
(world.get_node("WorldEnvironment") as WorldEnvironment).environment
|
||||
)
|
||||
var light := world.get_node("DirectionalLight3D") as DirectionalLight3D
|
||||
var grass := world.get_node("TerrainRoot/Terrain3D/CozyGrassField") as Node3D
|
||||
var grass_controller := world.get_node("TerrainRoot/GrassInteractionController") as Node
|
||||
var world_grass_material := grass.get("mesh_material_override") as ShaderMaterial
|
||||
var world_process_material := grass.get("process_material") as ShaderMaterial
|
||||
var world_sky: Sky = environment.sky
|
||||
var world_sky_material: Material = world_sky.sky_material
|
||||
var balanced_particle_count := int(grass.get("particle_count"))
|
||||
var balanced_render_scale := viewport.scaling_3d_scale
|
||||
|
||||
_check(
|
||||
world.get_active_presentation_quality() == JajceWorld.PresentationQuality.BALANCED,
|
||||
"Balanced should be the default presentation tier"
|
||||
)
|
||||
_check(
|
||||
balanced_render_scale <= JajceWorld.BALANCED_RENDER_SCALE + 0.001,
|
||||
(
|
||||
"Balanced should reduce 3D render resolution while leaving UI resolution untouched"
|
||||
+ " (found %.2f)" % balanced_render_scale
|
||||
)
|
||||
)
|
||||
_check(
|
||||
(
|
||||
light.directional_shadow_max_distance <= JajceWorld.BALANCED_SHADOW_DISTANCE
|
||||
and light.directional_shadow_mode == DirectionalLight3D.SHADOW_PARALLEL_2_SPLITS
|
||||
and environment.fog_enabled
|
||||
and not environment.volumetric_fog_enabled
|
||||
),
|
||||
"Balanced should bound shadows and use cheap valley fog instead of volumetric fog"
|
||||
)
|
||||
_check(
|
||||
world_grass_material == grass_controller.get("grass_material"),
|
||||
"Grass rendering and interaction should share this world's isolated material"
|
||||
)
|
||||
|
||||
_check(
|
||||
world.apply_presentation_quality(JajceWorld.PresentationQuality.HIGH),
|
||||
"High presentation tier should be selectable"
|
||||
)
|
||||
await physics_frame
|
||||
var high_particle_count := int(grass.get("particle_count"))
|
||||
var high_render_scale := viewport.scaling_3d_scale
|
||||
var high_shadow_distance := light.directional_shadow_max_distance
|
||||
_check(
|
||||
high_particle_count in range(9000, 15001),
|
||||
"High should restore the authored grass population"
|
||||
)
|
||||
_check(
|
||||
balanced_particle_count < high_particle_count,
|
||||
"Balanced should materially reduce grass density relative to High"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
environment.glow_enabled
|
||||
and (environment.volumetric_fog_enabled == world.call("_supports_volumetric_fog"))
|
||||
and environment.adjustment_enabled
|
||||
),
|
||||
"High should preserve supported authored effects without enabling unsupported fog"
|
||||
)
|
||||
_check(
|
||||
is_equal_approx(high_shadow_distance, 180.0),
|
||||
"High should preserve the authored 180 metre shadow range"
|
||||
)
|
||||
_check(_all_grass_particles_emitting(grass), "High should render every authored grass cell")
|
||||
|
||||
_check(
|
||||
world.apply_presentation_quality(JajceWorld.PresentationQuality.LOW),
|
||||
"Low presentation tier should be selectable"
|
||||
)
|
||||
await physics_frame
|
||||
_check(
|
||||
viewport.scaling_3d_scale <= JajceWorld.LOW_RENDER_SCALE + 0.001,
|
||||
"Low should reduce 3D render resolution"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
environment.fog_enabled
|
||||
and not environment.glow_enabled
|
||||
and not environment.volumetric_fog_enabled
|
||||
and not environment.adjustment_enabled
|
||||
),
|
||||
"Low should retain cheap valley fog while disabling costly post-processing"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
light.directional_shadow_max_distance <= JajceWorld.LOW_SHADOW_DISTANCE
|
||||
and light.directional_shadow_mode == DirectionalLight3D.SHADOW_ORTHOGONAL
|
||||
),
|
||||
"Low should shorten shadows and use one orthogonal shadow region"
|
||||
)
|
||||
_check(
|
||||
int(grass.get("particle_count")) <= high_particle_count / 4,
|
||||
"Low should reduce the latent grass population by at least 75 percent"
|
||||
)
|
||||
_check(
|
||||
not grass.visible and not grass.is_physics_processing(),
|
||||
"Low should hide grass and stop its camera-grid physics work"
|
||||
)
|
||||
_check(not grass_controller.is_processing(), "Low should stop grass interaction scans")
|
||||
_check(_not_any_grass_particle_emitting(grass), "Low should stop every grass particle emitter")
|
||||
_check(
|
||||
(
|
||||
world_grass_material != null
|
||||
and int(world_grass_material.get_shader_parameter("interactor_count")) == 0
|
||||
),
|
||||
"Low should clear grass shader interactors"
|
||||
)
|
||||
|
||||
_check(
|
||||
world.apply_presentation_quality(JajceWorld.PresentationQuality.HIGH),
|
||||
"High should be restorable after Low"
|
||||
)
|
||||
await physics_frame
|
||||
_check(
|
||||
(
|
||||
is_equal_approx(viewport.scaling_3d_scale, high_render_scale)
|
||||
and environment.glow_enabled
|
||||
and (environment.volumetric_fog_enabled == world.call("_supports_volumetric_fog"))
|
||||
and environment.adjustment_enabled
|
||||
and is_equal_approx(light.directional_shadow_max_distance, high_shadow_distance)
|
||||
),
|
||||
"Returning to High should restore the authored environment contract"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
grass.visible
|
||||
and grass.is_physics_processing()
|
||||
and grass_controller.is_processing()
|
||||
and int(grass.get("particle_count")) == high_particle_count
|
||||
and _all_grass_particles_emitting(grass)
|
||||
),
|
||||
"Returning to High should restore grass rendering and processing"
|
||||
)
|
||||
|
||||
var world_base_scale := float(world.get("_authored_render_scale"))
|
||||
world.free()
|
||||
_check(
|
||||
is_equal_approx(viewport.scaling_3d_scale, world_base_scale),
|
||||
"An exiting world should restore the viewport scale it originally owned"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
world_grass_material.resource_local_to_scene
|
||||
and world_process_material.resource_local_to_scene
|
||||
and environment.resource_local_to_scene
|
||||
and world_sky.resource_local_to_scene
|
||||
and world_sky_material.resource_local_to_scene
|
||||
),
|
||||
"Every mutable environment, sky, and grass resource should be local to one world"
|
||||
)
|
||||
var fresh_world: JajceWorld = load("res://world/jajce/JajceWorld.tscn").instantiate()
|
||||
root.add_child(fresh_world)
|
||||
await process_frame
|
||||
await physics_frame
|
||||
var fresh_environment: Environment = (
|
||||
(fresh_world.get_node("WorldEnvironment") as WorldEnvironment).environment
|
||||
)
|
||||
var fresh_grass := fresh_world.get_node("TerrainRoot/Terrain3D/CozyGrassField") as Node3D
|
||||
var fresh_grass_material := fresh_grass.get("mesh_material_override") as ShaderMaterial
|
||||
var fresh_process_material := fresh_grass.get("process_material") as ShaderMaterial
|
||||
_check(
|
||||
(
|
||||
fresh_world.get_active_presentation_quality() == JajceWorld.PresentationQuality.BALANCED
|
||||
and fresh_environment.fog_enabled
|
||||
and not fresh_environment.volumetric_fog_enabled
|
||||
and fresh_grass.visible
|
||||
and fresh_grass.is_physics_processing()
|
||||
and int(fresh_grass.get("particle_count")) == balanced_particle_count
|
||||
),
|
||||
"A fresh world after Low should start from an isolated Balanced presentation"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
environment != fresh_environment
|
||||
and environment.sky != fresh_environment.sky
|
||||
and environment.sky.sky_material != fresh_environment.sky.sky_material
|
||||
and world_grass_material != fresh_grass_material
|
||||
and world_process_material != fresh_process_material
|
||||
and _all_grass_particles_use_process_material(fresh_grass, fresh_process_material)
|
||||
),
|
||||
"Sequential Jajce worlds should not share mutable environment, sky, or grass resources"
|
||||
)
|
||||
var original_scale := float(fresh_world.get("_authored_render_scale"))
|
||||
fresh_world.free()
|
||||
_check(
|
||||
is_equal_approx(viewport.scaling_3d_scale, original_scale),
|
||||
"A sequential world should release its viewport-scale ownership"
|
||||
)
|
||||
|
||||
var older_owner := _create_presentation_only_world(JajceWorld.PresentationQuality.LOW)
|
||||
root.add_child(older_owner)
|
||||
await process_frame
|
||||
var newer_owner := _create_presentation_only_world(JajceWorld.PresentationQuality.BALANCED)
|
||||
root.add_child(newer_owner)
|
||||
await process_frame
|
||||
_check(
|
||||
is_equal_approx(float(newer_owner.get("_authored_render_scale")), original_scale),
|
||||
"Concurrent owners should inherit the viewport base, not another owner's reduced scale"
|
||||
)
|
||||
older_owner.free()
|
||||
_check(
|
||||
is_equal_approx(viewport.scaling_3d_scale, JajceWorld.BALANCED_RENDER_SCALE),
|
||||
"A stale owner exiting should not overwrite the active owner's scale"
|
||||
)
|
||||
newer_owner.free()
|
||||
_check(
|
||||
is_equal_approx(viewport.scaling_3d_scale, original_scale),
|
||||
"The final viewport owner should restore the original scale on exit"
|
||||
)
|
||||
|
||||
var main_scene: Node = load("res://main.tscn").instantiate()
|
||||
var time_dial := main_scene.get_node("UI/TimeDial") as Control
|
||||
_check(
|
||||
(
|
||||
is_equal_approx(time_dial.anchor_left, 0.5)
|
||||
and is_equal_approx(time_dial.anchor_right, 0.5)
|
||||
and is_equal_approx(time_dial.offset_left, -30.0)
|
||||
and is_equal_approx(time_dial.offset_right, 30.0)
|
||||
),
|
||||
"Time dial should remain horizontally centered at weaker-PC window sizes"
|
||||
)
|
||||
main_scene.free()
|
||||
|
||||
camera.free()
|
||||
if failures.is_empty():
|
||||
print("Jajce presentation quality checks passed")
|
||||
quit(0)
|
||||
else:
|
||||
for failure in failures:
|
||||
push_error(failure)
|
||||
quit(1)
|
||||
|
||||
|
||||
func _all_grass_particles_emitting(grass: Node3D) -> bool:
|
||||
var particles: Array = grass.get("particle_nodes")
|
||||
if particles.is_empty():
|
||||
return false
|
||||
for value in particles:
|
||||
var particle := value as GPUParticles3D
|
||||
if particle == null or not particle.emitting:
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _create_presentation_only_world(quality: int) -> JajceWorld:
|
||||
var world: JajceWorld = load("res://world/jajce/JajceWorld.tscn").instantiate()
|
||||
# Stable simulation-facing IDs deliberately allow only one loaded authority
|
||||
# world. The ownership test needs only the presentation subtree.
|
||||
world.get_node("WorldObjects").free()
|
||||
world.presentation_quality = quality
|
||||
return world
|
||||
|
||||
|
||||
func _not_any_grass_particle_emitting(grass: Node3D) -> bool:
|
||||
var particles: Array = grass.get("particle_nodes")
|
||||
for value in particles:
|
||||
var particle := value as GPUParticles3D
|
||||
if particle != null and particle.emitting:
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _all_grass_particles_use_process_material(grass: Node3D, material: ShaderMaterial) -> bool:
|
||||
var particles: Array = grass.get("particle_nodes")
|
||||
if particles.is_empty():
|
||||
return false
|
||||
for value in particles:
|
||||
var particle := value as GPUParticles3D
|
||||
if particle == null or particle.process_material != material:
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _check(condition: bool, message: String) -> void:
|
||||
if not condition:
|
||||
failures.append(message)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bs7y460mcireu
|
||||
@@ -18,8 +18,8 @@ func _run() -> void:
|
||||
simulation_manager.set_process(false)
|
||||
var saved_clock_ticks: int = simulation_manager.clock.elapsed_ticks
|
||||
simulation_manager.clock.elapsed_ticks = 0
|
||||
await process_frame
|
||||
await process_frame
|
||||
var day_night_cycle := main_scene.get_node("JajceWorld/DayNightCycle")
|
||||
day_night_cycle.call("_process", 0.11)
|
||||
var environment: Environment = (
|
||||
(main_scene.get_node("JajceWorld/WorldEnvironment") as WorldEnvironment).environment
|
||||
)
|
||||
|
||||
@@ -16,11 +16,20 @@ func _run() -> void:
|
||||
var configured_assets: Terrain3DAssets = load("res://terrain/jajce/assets.tres")
|
||||
var configured_texture_count := configured_assets.get_texture_count()
|
||||
|
||||
var world: Node3D = load("res://world/jajce/JajceWorld.tscn").instantiate()
|
||||
var world: JajceWorld = load("res://world/jajce/JajceWorld.tscn").instantiate()
|
||||
root.add_child(world)
|
||||
await process_frame
|
||||
for _frame in 10:
|
||||
await physics_frame
|
||||
_check(
|
||||
world.get_active_presentation_quality() == JajceWorld.PresentationQuality.BALANCED,
|
||||
"JajceWorld should default to the balanced presentation budget"
|
||||
)
|
||||
_check(
|
||||
world.apply_presentation_quality(JajceWorld.PresentationQuality.HIGH),
|
||||
"Scaffold validation should be able to select the authored high presentation"
|
||||
)
|
||||
await physics_frame
|
||||
|
||||
var terrain = world.get_node("TerrainRoot/Terrain3D")
|
||||
_check(terrain is Terrain3D, "JajceWorld should own a Terrain3D node")
|
||||
|
||||
@@ -51,11 +51,23 @@ func _test_exact_bounded_discovery(benchmark: RefCounted, fixture: Dictionary) -
|
||||
"Ordinary local queries should inspect a bounded subset of 180 loaded resources",
|
||||
)
|
||||
var adapter: ActiveWorldAdapter = fixture["adapter"]
|
||||
var manager: Node = fixture["manager"]
|
||||
var stats := adapter.get_resource_index_stats()
|
||||
_check(
|
||||
int(stats["candidate_count"]) == 180 and int(stats["occupied_cell_count"]) > 1,
|
||||
"The adapter should index every loaded anchor across multiple horizontal cells",
|
||||
)
|
||||
var player_origin := ResourceNode.get_by_id(&"benchmark_resource_0001").global_position
|
||||
var player_target: ResourceNode = manager.find_resource_node_for_player(player_origin, 12.0)
|
||||
var player_query: Dictionary = manager.last_player_resource_query_stats
|
||||
_check(player_target != null, "Player discovery should find a nearby usable resource")
|
||||
_check(
|
||||
(
|
||||
StringName(player_query.get("mode", "")) == &"spatial"
|
||||
and int(player_query.get("candidate_count", 180)) < 12
|
||||
),
|
||||
"Player discovery should inspect a bounded spatial subset, not all 180 loaded resources",
|
||||
)
|
||||
|
||||
|
||||
func _test_far_priority_is_not_pruned(benchmark: RefCounted, fixture: Dictionary) -> void:
|
||||
|
||||
@@ -3,6 +3,23 @@ extends SceneTree
|
||||
const SimulationManagerScript := preload("res://simulation/SimulationManager.gd")
|
||||
const BenchmarkScript := preload("res://simulation/benchmark/SimulationScalingBenchmark.gd")
|
||||
|
||||
|
||||
class CoverageDroppingManager:
|
||||
extends "res://simulation/SimulationManager.gd"
|
||||
|
||||
var drop_after_simulation_ticks := -1
|
||||
var simulation_ticks_seen := 0
|
||||
var removed_combatant_id: StringName = &""
|
||||
|
||||
func simulate_tick() -> void:
|
||||
super()
|
||||
simulation_ticks_seen += 1
|
||||
if simulation_ticks_seen != drop_after_simulation_ticks or npcs.is_empty():
|
||||
return
|
||||
removed_combatant_id = SimulationIds.npc_combatant_id(npcs[-1].id)
|
||||
conflict_system.combatants.erase(removed_combatant_id)
|
||||
|
||||
|
||||
var failures: Array[String] = []
|
||||
|
||||
|
||||
@@ -21,9 +38,22 @@ func _run() -> void:
|
||||
_check(first["fixture_valid"], "The prepared benchmark state should pass schema validation")
|
||||
_check(
|
||||
(
|
||||
int(first["schema_version"]) == SimulationScalingBenchmark.SCHEMA_VERSION
|
||||
and StringName(first["workload_id"]) == SimulationScalingBenchmark.WORKLOAD_ID
|
||||
int(first["schema_version"]) == 2
|
||||
and SimulationScalingBenchmark.SCHEMA_VERSION == 2
|
||||
and (
|
||||
int(first["simulation_state_schema_version"])
|
||||
== SimulationStateRecord.SCHEMA_VERSION
|
||||
)
|
||||
and StringName(first["workload_id"]) == &"full_fidelity_combatant_headless_arrival_v2"
|
||||
and (
|
||||
SimulationScalingBenchmark.WORKLOAD_ID
|
||||
== &"full_fidelity_combatant_headless_arrival_v2"
|
||||
)
|
||||
and int(first["population"]) == 12
|
||||
and int(first["npc_combatant_count"]) == 12
|
||||
and bool(first["npc_combatant_coverage_valid"])
|
||||
and int(first["fixture_npc_combatant_count"]) == 12
|
||||
and bool(first["fixture_combatants_valid"])
|
||||
and int(first["history_seed_events"]) == 24
|
||||
and int(first["measured_ticks"]) == 8
|
||||
and int(first["npc_updates"]) == 96
|
||||
@@ -59,6 +89,8 @@ func _run() -> void:
|
||||
"arrivals_processed",
|
||||
"warmup_arrivals",
|
||||
"tick_interval",
|
||||
"npc_combatant_count",
|
||||
"npc_combatant_coverage_valid",
|
||||
]:
|
||||
_check(
|
||||
first[deterministic_key] == repeated[deterministic_key],
|
||||
@@ -68,6 +100,14 @@ func _run() -> void:
|
||||
first["final_checksum"] != different_seed["final_checksum"],
|
||||
"A different fixture seed should produce a different deterministic checksum"
|
||||
)
|
||||
_check(
|
||||
_coverage_loss_is_rejected(2, 3, 2),
|
||||
"Coverage lost on the final warmup tick should reject the benchmark result"
|
||||
)
|
||||
_check(
|
||||
_coverage_loss_is_rejected(2, 3, 5),
|
||||
"Coverage lost on the final measured tick should reject the benchmark result"
|
||||
)
|
||||
_finish()
|
||||
|
||||
|
||||
@@ -82,12 +122,57 @@ func _run_case(seed_value: int) -> Dictionary:
|
||||
manager.free()
|
||||
return {}
|
||||
var fixture_valid := SimulationStateRecord.from_json(manager.serialize_state()) != null
|
||||
var fixture_npc_combatant_count := 0
|
||||
var fixture_combatants_valid := true
|
||||
for npc in manager.npcs:
|
||||
var combatant: CombatantStateRecord = manager.conflict_system.get_combatant(
|
||||
SimulationIds.npc_combatant_id(npc.id)
|
||||
)
|
||||
if (
|
||||
combatant == null
|
||||
or combatant.get_npc_id() != npc.id
|
||||
or combatant.get_display_name() != npc.npc_name
|
||||
or not combatant.get_position().is_equal_approx(npc.position)
|
||||
):
|
||||
fixture_combatants_valid = false
|
||||
else:
|
||||
fixture_npc_combatant_count += 1
|
||||
var result: Dictionary = benchmark.measure_manager(manager, 12, 24, 2, 8)
|
||||
result["fixture_valid"] = fixture_valid
|
||||
result["fixture_npc_combatant_count"] = fixture_npc_combatant_count
|
||||
result["fixture_combatants_valid"] = fixture_combatants_valid
|
||||
manager.free()
|
||||
return result
|
||||
|
||||
|
||||
func _coverage_loss_is_rejected(
|
||||
warmup_ticks: int, measured_ticks: int, drop_after_simulation_ticks: int
|
||||
) -> bool:
|
||||
var manager := CoverageDroppingManager.new()
|
||||
manager.simulation_seed = 9010
|
||||
manager.debug_logs = false
|
||||
manager.set_process(false)
|
||||
root.add_child(manager)
|
||||
var benchmark := BenchmarkScript.new()
|
||||
var prepared := benchmark.prepare_manager(manager, 4, 0, manager.simulation_seed)
|
||||
if not prepared:
|
||||
manager.free()
|
||||
return false
|
||||
var target_id := SimulationIds.npc_combatant_id(manager.npcs[-1].id)
|
||||
var target_existed := manager.conflict_system.get_combatant(target_id) != null
|
||||
manager.drop_after_simulation_ticks = drop_after_simulation_ticks
|
||||
var result: Dictionary = benchmark.measure_manager(manager, 4, 0, warmup_ticks, measured_ticks)
|
||||
var rejected_after_exact_drop := (
|
||||
target_existed
|
||||
and manager.simulation_ticks_seen == drop_after_simulation_ticks
|
||||
and manager.removed_combatant_id == target_id
|
||||
and manager.conflict_system.get_combatant(target_id) == null
|
||||
and result.is_empty()
|
||||
)
|
||||
manager.free()
|
||||
return rejected_after_exact_drop
|
||||
|
||||
|
||||
func _check(condition: bool, message: String) -> void:
|
||||
if not condition:
|
||||
failures.append(message)
|
||||
|
||||
@@ -7,6 +7,90 @@ const EventKnowledgeSystemScript := preload("res://simulation/knowledge/EventKno
|
||||
const SimulationManagerScript := preload("res://simulation/SimulationManager.gd")
|
||||
|
||||
|
||||
func test_simulation_clock_caps_frame_work_without_discarding_backlog() -> void:
|
||||
var clock := SimulationClock.new(1.0)
|
||||
clock.cycle_duration_seconds = 100.0
|
||||
|
||||
assert_eq(clock.advance(10.0, 3), 3)
|
||||
assert_eq(clock.elapsed_ticks, 3)
|
||||
assert_eq(clock.accumulator, 7.0)
|
||||
assert_true(is_equal_approx(clock.time_of_day(), 0.03))
|
||||
assert_eq(clock.advance(0.0, 3), 3)
|
||||
assert_eq(clock.elapsed_ticks, 6)
|
||||
assert_eq(clock.accumulator, 4.0)
|
||||
assert_true(is_equal_approx(clock.time_of_day(), 0.06))
|
||||
|
||||
var fractional_clock := SimulationClock.new(1.0)
|
||||
fractional_clock.cycle_duration_seconds = 100.0
|
||||
assert_eq(fractional_clock.advance(0.25, 3), 0)
|
||||
assert_true(is_equal_approx(fractional_clock.time_of_day(), 0.0025))
|
||||
|
||||
|
||||
func test_player_attack_uses_definition_reach_and_unscaled_realtime_cooldown() -> void:
|
||||
var conflict := ConflictSystem.new()
|
||||
conflict.configure(null, 0.1)
|
||||
conflict.initialize_factions()
|
||||
conflict.set_player_combatant_position(Vector3.ZERO)
|
||||
var sword := SimulationItems.get_weapon(SimulationIds.ITEM_SWORD)
|
||||
assert_not_null(sword)
|
||||
var wolf_id := conflict.spawn_wolf(Vector3(sword.reach + 0.01, 8.0, 0.0))
|
||||
var wolf := conflict.get_combatant(wolf_id)
|
||||
|
||||
assert_eq(conflict.player_attack(wolf_id), 0.0)
|
||||
wolf.set_position(Vector3(sword.reach - 0.01, 8.0, 0.0))
|
||||
assert_eq(conflict.player_attack(wolf_id), sword.damage)
|
||||
assert_false(conflict.is_player_attack_ready())
|
||||
conflict.advance(1)
|
||||
assert_false(conflict.is_player_attack_ready())
|
||||
conflict.advance_realtime(sword.attack_cooldown - 0.01)
|
||||
assert_false(conflict.is_player_attack_ready())
|
||||
conflict.advance_realtime(0.02)
|
||||
assert_true(conflict.is_player_attack_ready())
|
||||
|
||||
|
||||
func test_player_dash_duration_and_cooldown_are_enforced_separately() -> void:
|
||||
var controller := PlayerCombatController.new()
|
||||
var player := CharacterBody3D.new()
|
||||
controller.player = player
|
||||
player.add_child(controller)
|
||||
add_child_autofree(player)
|
||||
var sword := SimulationItems.get_weapon(SimulationIds.ITEM_SWORD)
|
||||
|
||||
assert_true(is_equal_approx(controller.attack_range, sword.reach))
|
||||
controller.call("_perform_dash")
|
||||
assert_true(controller.is_dashing())
|
||||
assert_true(
|
||||
is_equal_approx(controller.get_dash_cooldown_remaining(), controller.dash_cooldown_seconds)
|
||||
)
|
||||
controller.call("_advance_timers", controller.dash_duration + 0.01)
|
||||
assert_false(controller.is_dashing())
|
||||
controller.call("_perform_dash")
|
||||
assert_false(controller.is_dashing())
|
||||
controller.call("_advance_timers", controller.get_dash_cooldown_remaining())
|
||||
controller.call("_perform_dash")
|
||||
assert_true(controller.is_dashing())
|
||||
|
||||
|
||||
func test_combat_cooldown_uses_the_configured_simulation_tick() -> void:
|
||||
var combatant := CombatantStateRecord.create(
|
||||
&"cooldown_test",
|
||||
SimulationIds.COMBATANT_KIND_PLAYER,
|
||||
SimulationIds.FACTION_VILLAGE,
|
||||
"Cooldown Tester",
|
||||
Vector3.ZERO,
|
||||
100.0,
|
||||
SimulationIds.ITEM_SWORD
|
||||
)
|
||||
|
||||
combatant.mark_attacked(0.3)
|
||||
assert_eq(combatant.get_attack_cooldown(), 2)
|
||||
combatant.tick_cooldown()
|
||||
combatant.tick_cooldown()
|
||||
assert_true(combatant.is_attack_ready())
|
||||
combatant.mark_attacked(1.2)
|
||||
assert_eq(combatant.get_attack_cooldown(), 1)
|
||||
|
||||
|
||||
func test_storage_never_accepts_more_than_its_capacity() -> void:
|
||||
var storage := StorageStateRecord.create(&"test_storage", {"food": 4.5}, 5.0)
|
||||
|
||||
@@ -30,6 +114,22 @@ func test_economy_moves_inventory_into_authoritative_storage() -> void:
|
||||
assert_eq(village.food, 5.0)
|
||||
|
||||
|
||||
func test_failed_food_consumption_preserves_fractional_inventory() -> void:
|
||||
var village := SimVillage.new()
|
||||
var economy := VillageEconomyScript.new()
|
||||
economy.configure(village, false)
|
||||
var npc := SimNPC.new(8, "Fractional", SimulationIds.PROFESSION_FARMER, 5.0, 5.0)
|
||||
npc.hunger = 50.0
|
||||
npc.add_inventory(SimulationIds.RESOURCE_FOOD, 0.5)
|
||||
var event_count := [0]
|
||||
economy.economic_event_requested.connect(func(_a, _b, _c, _d, _e, _f): event_count[0] += 1)
|
||||
|
||||
assert_false(economy.consume_npc_food(npc))
|
||||
assert_eq(npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 0.5)
|
||||
assert_eq(npc.hunger, 55.0)
|
||||
assert_eq(event_count[0], 0)
|
||||
|
||||
|
||||
func test_event_log_preserves_order_and_derives_consumption_rate() -> void:
|
||||
var event_log := SimulationEventLogScript.new()
|
||||
event_log.record_narrative(10, SimulationIds.EVENT_TASK_STARTED, 4, &"", "Study")
|
||||
|
||||
Reference in New Issue
Block a user