Files
gamedev-the-steward/tests/jajce_world_scaffold_test.gd
admin c61d286005 feat: wood inventory, carrying, and woodpile storage
Wood now follows the same gather-carry-deposit pattern as food. NPCs put harvested wood into inventory, walk to the VillageWoodpile StorageNode, and deposit it. Village wood syncs from storage. Player harvesting also routes wood to the woodpile. ActiveWorldAdapter exposes woodpile routing for DEPOSIT_WOOD action targeting. WorldViewManager recognizes wood inventory changes. Tests verify woodpile node existence, storage ID, navigation reachability, and adapter routing.
2026-07-08 18:55:51 +02:00

376 lines
14 KiB
GDScript

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 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()
root.add_child(world)
await process_frame
for _frame in 10:
await physics_frame
var terrain = world.get_node("TerrainRoot/Terrain3D")
_check(terrain is Terrain3D, "JajceWorld should own a Terrain3D node")
_check(
terrain.data_directory == "res://terrain/jajce/data",
"Terrain3D should use dedicated Jajce data"
)
_check(terrain.collision_mode != 0, "Terrain3D collision should be enabled for gameplay")
_check(
not world.has_node("NavigationRegion3D/GreyboxGround"),
"JajceWorld should not keep the temporary greybox navigation ground"
)
_check(
configured_texture_count == 6,
(
"Jajce terrain should define the bounded six-material palette (found %s)"
% configured_texture_count
)
)
var texture_contract_valid := true
for texture_index in configured_texture_count:
var texture_asset: Terrain3DTextureAsset = configured_assets.get_texture(texture_index)
texture_contract_valid = (
texture_contract_valid
and texture_asset.albedo_texture != null
and texture_asset.normal_texture != null
and texture_asset.albedo_texture.get_image().has_mipmaps()
and texture_asset.normal_texture.get_image().has_mipmaps()
)
_check(
texture_contract_valid,
"All Terrain3D texture assets should provide matching mipmapped maps"
)
_check(
absf(terrain.data.get_height(Vector3.ZERO)) < 0.1,
"Central gameplay area should remain level for current navigation"
)
_check(
terrain.data.get_height(Vector3(-72, 0, 18)) > 10.0,
"Authored terrain should raise the western fortress ridge"
)
_check(world.has_node("WaterRoot"), "JajceWorld should expose WaterRoot")
_check(world.has_node("VillageRoot"), "JajceWorld should expose VillageRoot")
_check(world.has_node("FoliageRoot"), "JajceWorld should expose FoliageRoot")
_check(
world.get_node("FoliageRoot").get_child_count() >= 10,
"JajceWorld should include restrained authored foliage clusters"
)
_check(world.has_node("FortressBlockout"), "JajceWorld should include the fortress landmark")
_check(world.has_node("WaterRoot/WaterfallBody"), "WaterRoot should include a waterfall drop")
_check(
world.has_node("WaterRoot/WaterfallMist"),
"WaterRoot should include waterfall mist particles"
)
var waterfall_mist: GPUParticles3D = world.get_node("WaterRoot/WaterfallMist")
_check(
waterfall_mist.draw_pass_1 != null,
"Waterfall mist particles should have visible billboard geometry"
)
_check(world.has_node("WaterRoot/RiverSurface"), "WaterRoot should include a river surface")
var environment: Environment = (
(world.get_node("WorldEnvironment") as WorldEnvironment).environment
)
_check(
environment.fog_enabled and environment.sky != null,
"Jajce environment should provide sky and restrained valley fog"
)
_check(
environment.glow_enabled
and environment.volumetric_fog_enabled
and environment.adjustment_enabled,
"WorldEnvironment should provide bounded glow, depth haze, and color grading"
)
_check(
world.has_node("VillageRoot/Mill") and world.has_node("VillageRoot/Bridge"),
"VillageRoot should include the mill and bridge blockouts"
)
var smoke_count := 0
for suffix in ["House_01", "House_02", "House_03"]:
if world.has_node("VillageRoot/%s/Smoke" % suffix):
var smoke: GPUParticles3D = world.get_node("VillageRoot/%s/Smoke" % suffix)
if smoke.draw_pass_1 == null:
continue
smoke_count += 1
_check(
smoke_count >= 2,
"At least two houses should have chimney smoke particles (found %s)" % smoke_count
)
var lookdev: Node3D = load("res://world/jajce/JajceLookdev.tscn").instantiate()
_check(lookdev.has_node("JajceWorld"), "Lookdev should instance JajceWorld")
_check(lookdev.has_node("BeautyCamera"), "Lookdev should provide a beauty camera")
lookdev.free()
var resources := world.get_node("WorldObjects/ResourceNodes").get_children()
var ids: Array[String] = []
for resource in resources:
ids.append(String((resource as ResourceNode).node_id))
ids.sort()
_check(
ids
== [
"animal_camp_01",
"animal_camp_river_01",
"berry_bush_01",
"berry_bush_02",
"berry_bush_river_02",
"berry_patch_mill_01",
"berry_patch_river_01",
"berry_patch_south_01",
"farm_crop_01",
"tree_01",
"tree_02",
"tree_forest_grove_01",
"tree_forest_grove_02",
"tree_north_outskirts_01",
"tree_river_resource_01",
"tree_south_edge_01",
"wood_pile_mill_01",
"wood_pile_village_01"
],
"Jajce resources should preserve all stable and discoverable IDs"
)
var metadata_valid := true
var food_count := 0
var wood_count := 0
var animal_context_count := 0
var berry_context_count := 0
var village_context_count := 0
var outskirt_context_count := 0
var farming_context_count := 0
for resource in resources:
var node := resource as ResourceNode
var id := String(node.node_id)
if node.resource_id == SimulationIds.RESOURCE_FOOD:
food_count += 1
if node.resource_id == SimulationIds.RESOURCE_WOOD:
wood_count += 1
if id.begins_with("animal_camp"):
animal_context_count += 1
if id.begins_with("berry_"):
berry_context_count += 1
if id.contains("_village_"):
village_context_count += 1
if id.contains("_outskirts_") or id.contains("_edge_") or id.contains("_river_"):
outskirt_context_count += 1
if id.begins_with("farm_"):
farming_context_count += 1
metadata_valid = (
metadata_valid
and node.comfort_distance > 0.0
and node.safety_risk >= 0.0
and node.safety_risk <= 1.0
)
_check(metadata_valid, "Jajce resources should define discovery metadata")
_check(food_count >= 9, "Jajce should expose at least nine finite food resources")
_check(wood_count >= 9, "Jajce should expose at least nine finite wood resources")
_check(animal_context_count >= 2, "Jajce food discovery should include animal-camp contexts")
_check(berry_context_count >= 6, "Jajce food discovery should include berry contexts")
_check(village_context_count >= 1, "Jajce discovery should include at least one village stockpile")
_check(outskirt_context_count >= 4, "Jajce discovery should include outskirts/river contexts")
_check(farming_context_count >= 1, "Jajce food discovery should include farming contexts")
_check(world.has_node("WorldObjects/StorageSites"), "JajceWorld should expose StorageSites")
var pantry := world.get_node("WorldObjects/StorageSites/VillagePantry") as StorageNode
_check(pantry != null, "JajceWorld should include a typed VillagePantry StorageNode")
_check(
pantry.storage_id == SimulationIds.STORAGE_VILLAGE_PANTRY,
"VillagePantry should preserve the stable storage ID"
)
var woodpile := world.get_node("WorldObjects/StorageSites/VillageWoodpile") as StorageNode
_check(woodpile != null, "JajceWorld should include a typed VillageWoodpile StorageNode")
_check(
woodpile.storage_id == SimulationIds.STORAGE_VILLAGE_WOODPILE,
"VillageWoodpile should preserve the stable storage ID"
)
_check(
not world.has_node("LegacyActivityMarkers/PantryMarker"),
"Pantry should no longer be a legacy activity marker"
)
_check(
not world.has_node("LegacyActivityMarkers"),
"JajceWorld should no longer expose legacy activity markers"
)
var activity_root := world.get_node("WorldObjects/ActivitySites")
var activity_ids: Array[String] = []
for site in activity_root.get_children():
activity_ids.append(String((site as ActivitySite).site_id))
activity_ids.sort()
_check(
activity_ids == ["guard_post", "rest_bench", "study_desk"],
"Jajce activity sites should preserve stable semantic IDs"
)
var navigation_region := world.get_node("NavigationRegion3D") as NavigationRegion3D
var navigation_map: RID = NavigationServer3D.region_get_map(navigation_region.get_rid())
_check(navigation_map.is_valid(), "Jajce navigation region should be assigned to a map")
_check(
navigation_region.navigation_mesh.get_polygon_count() > 0,
"Jajce navigation mesh should contain baked polygons"
)
_check(
navigation_region.navigation_mesh.resource_path == "res://world/jajce/JajceNavigationMesh.tres",
"Jajce navigation should use the Terrain3D-derived baked mesh resource"
)
await _wait_for_navigation_map(navigation_map)
NavigationServer3D.map_force_update(navigation_map)
var origins := [Vector3(-6, 0, -4), Vector3(0, 0, 0), Vector3(6, 0, 4)]
var destinations: Array[Vector3] = []
for resource in resources:
destinations.append((resource as ResourceNode).interaction_point.global_position)
destinations.append(pantry.get_interaction_position())
destinations.append(woodpile.get_interaction_position())
for site in activity_root.get_children():
destinations.append((site as ActivitySite).get_interaction_position())
for origin in origins:
for destination in destinations:
var path := NavigationServer3D.map_get_path(navigation_map, origin, destination, true)
_check(
not path.is_empty(),
"Jajce navigation path should exist from %s to %s" % [origin, destination]
)
_check_path_tracks_terrain(
terrain, path, "Jajce navigation path should track Terrain3D height"
)
_check_path_reaches_destination(
path, destination, "Jajce navigation path should reach its requested target"
)
var adapter := ActiveWorldAdapter.new()
adapter.pantry_storage = pantry
adapter.woodpile_storage = woodpile
root.add_child(adapter)
var food_candidates := adapter.get_resource_candidates(SimulationIds.ACTION_GATHER_FOOD)
_check(food_candidates.size() >= 9, "Adapter should expose all authored food candidates")
var candidate_metadata_valid := true
for candidate in food_candidates:
candidate_metadata_valid = (
candidate_metadata_valid
and candidate.has_all(
[
"target_id",
"position",
"resource_id",
"safety_risk",
"comfort_distance",
"discovery_priority"
]
)
)
_check(candidate_metadata_valid, "Adapter resource facts should include discovery metadata")
var manager: Node = load("res://simulation/SimulationManager.gd").new()
manager.debug_logs = false
manager.active_world_adapter = adapter
root.add_child(manager)
manager.set_process(false)
await process_frame
var npc: SimNPC = manager.npcs[0]
npc.set_task(SimulationIds.ACTION_GATHER_FOOD)
_check(
manager.resolve_npc_target(npc.id, Vector3.ZERO),
"Target resolver should find Jajce resources without a parent path"
)
_check(
npc.target_id != &"animal_camp_river_01",
"Target resolver should not choose the far risky animal camp while safer food exists"
)
var selected := ResourceNode.get_by_id(npc.target_id)
if selected != null:
var selected_state: ResourceStateRecord = manager.get_resource_state(selected.node_id)
_check(
selected_state.get_reserved_by() == npc.id,
"Selected Jajce resource should be authoritatively reserved"
)
manager.release_resource(selected.node_id, npc.id)
var storage_target := adapter.get_activity_target(SimulationIds.ACTION_WITHDRAW_FOOD)
_check(
storage_target.get("target_id", "") == String(SimulationIds.STORAGE_VILLAGE_PANTRY),
"Storage actions should target the typed pantry's stable ID"
)
var storage_target_position: Vector3 = storage_target.get("position", Vector3(9999, 9999, 9999))
_check(
storage_target_position.distance_to(pantry.get_interaction_position()) < 0.01,
"Storage actions should use the pantry StorageNode interaction point"
)
var wood_target := adapter.get_activity_target(SimulationIds.ACTION_DEPOSIT_WOOD)
_check(
wood_target.get("target_id", "") == String(SimulationIds.STORAGE_VILLAGE_WOODPILE),
"Wood deposit should target the typed woodpile's stable ID"
)
var rest_target := adapter.get_activity_target(SimulationIds.ACTION_REST)
_check(
rest_target.get("target_id", "") == "rest_bench",
"Rest should target the typed RestBench ActivitySite"
)
if failures.is_empty():
print(
"[TEST] Jajce scaffold passed: Terrain3D, scattered resources, typed sites"
)
quit(0)
return
for failure in failures:
push_error("[TEST] " + failure)
quit(1)
func _wait_for_navigation_map(navigation_map: RID) -> void:
for attempt in 30:
if (
NavigationServer3D.map_get_iteration_id(navigation_map) > 0
and not NavigationServer3D.map_get_regions(navigation_map).is_empty()
):
await physics_frame
return
await physics_frame
_check(false, "Jajce navigation map did not synchronize within 30 physics frames")
func _check(condition: bool, message: String) -> void:
if not condition:
failures.append(message)
func _check_path_tracks_terrain(terrain: Terrain3D, path: PackedVector3Array, message: String) -> void:
if path.is_empty():
return
for point in path:
var terrain_height: float = terrain.data.get_height(point)
if is_nan(terrain_height):
failures.append("%s: missing terrain height at %s" % [message, point])
return
if absf(point.y - terrain_height) > 0.85:
failures.append(
"%s: path point %s is too far from terrain height %.2f"
% [message, point, terrain_height]
)
return
func _check_path_reaches_destination(path: PackedVector3Array, destination: Vector3, message: String) -> void:
if path.is_empty():
return
var final_point := path[path.size() - 1]
var final_xz := Vector2(final_point.x, final_point.z)
var destination_xz := Vector2(destination.x, destination.z)
if final_xz.distance_to(destination_xz) > 1.5:
failures.append(
"%s: final path point %s is too far from %s"
% [message, final_point, destination]
)