From 25ffbfc5648cbe701c9b6c6e625027af33517eeb Mon Sep 17 00:00:00 2001 From: nathan Date: Sat, 16 May 2026 02:18:46 -0700 Subject: [PATCH] Add Ground Zero natural environment pass --- AGRARIAN_DEVELOPMENT_ROADMAP.md | 2 +- .../Maps/L_GroundZeroTerrain_Test.umap | 4 +- .../Materials/M_AGR_GZ_Fiber_Resource.uasset | 3 + .../Materials/M_AGR_GZ_FreshWater.uasset | 3 + .../M_AGR_GZ_Grass_DryCoastal.uasset | 3 + .../M_AGR_GZ_Shrub_CoyoteBrush.uasset | 3 + .../Materials/M_AGR_GZ_Stone_Sandstone.uasset | 3 + .../M_AGR_GZ_Terrain_CoastalScrub.uasset | 3 + .../Materials/M_AGR_GZ_Tree_CoastalOak.uasset | 3 + .../Materials/M_AGR_GZ_Wood_Resource.uasset | 3 + Docs/Terrain/GroundZeroFoliagePass.md | 7 +- .../GroundZeroNaturalEnvironmentPass.md | 39 +++++ Scripts/setup_ground_zero_demo_map.py | 150 +++++++++++++++++- ...fy_ground_zero_natural_environment_pass.py | 141 ++++++++++++++++ 14 files changed, 355 insertions(+), 12 deletions(-) create mode 100644 Content/Agrarian/Materials/M_AGR_GZ_Fiber_Resource.uasset create mode 100644 Content/Agrarian/Materials/M_AGR_GZ_FreshWater.uasset create mode 100644 Content/Agrarian/Materials/M_AGR_GZ_Grass_DryCoastal.uasset create mode 100644 Content/Agrarian/Materials/M_AGR_GZ_Shrub_CoyoteBrush.uasset create mode 100644 Content/Agrarian/Materials/M_AGR_GZ_Stone_Sandstone.uasset create mode 100644 Content/Agrarian/Materials/M_AGR_GZ_Terrain_CoastalScrub.uasset create mode 100644 Content/Agrarian/Materials/M_AGR_GZ_Tree_CoastalOak.uasset create mode 100644 Content/Agrarian/Materials/M_AGR_GZ_Wood_Resource.uasset create mode 100644 Docs/Terrain/GroundZeroNaturalEnvironmentPass.md create mode 100644 Scripts/verify_ground_zero_natural_environment_pass.py diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index 8694aef..b380dfa 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -456,7 +456,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [~] Add resource nodes. - [x] Add biome-appropriate natural resources based on Ground Zero. - [x] Add water source. -- [ ] Replace grey-box environment presentation with an MVP natural environment pass: terrain material, grass, shrubs, bushes, trees, water-source visuals, and clearer Ground Zero biome dressing. +- [x] Replace grey-box environment presentation with an MVP natural environment pass: terrain material, grass, shrubs, bushes, trees, water-source visuals, and clearer Ground Zero biome dressing. Added repeatable Ground Zero environment material generation, applied terrain/foliage/resource/water materials in the map setup, regenerated the demo map, documented the pass, and added verification for material assets plus map assignments. - [ ] Add first-pass environment asset variation so trees, bushes, grass, resource nodes, and water do not read as repeated placeholders. - [ ] Replace `LevelPrototyping` cube/cylinder mesh dependencies in Agrarian setup scripts and prototype Blueprints with Agrarian-native placeholder environment meshes. - [ ] Add weather exposure zones if needed. diff --git a/Content/Agrarian/Maps/L_GroundZeroTerrain_Test.umap b/Content/Agrarian/Maps/L_GroundZeroTerrain_Test.umap index 23cca6e..8bbbe58 100644 --- a/Content/Agrarian/Maps/L_GroundZeroTerrain_Test.umap +++ b/Content/Agrarian/Maps/L_GroundZeroTerrain_Test.umap @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b543e7a7ab079497a2c122d91710cf9883795eb97a5228a2aad0d15958f1e9a6 -size 7484993 +oid sha256:6e99e4071b76ad17941a4988e6ec06201a31aa3efb9503ae28c26672ea4dc588 +size 7452338 diff --git a/Content/Agrarian/Materials/M_AGR_GZ_Fiber_Resource.uasset b/Content/Agrarian/Materials/M_AGR_GZ_Fiber_Resource.uasset new file mode 100644 index 0000000..291c7bf --- /dev/null +++ b/Content/Agrarian/Materials/M_AGR_GZ_Fiber_Resource.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b976718b020df0851743392c76855670336644bb2dab337e99d8c5d764b14041 +size 5453 diff --git a/Content/Agrarian/Materials/M_AGR_GZ_FreshWater.uasset b/Content/Agrarian/Materials/M_AGR_GZ_FreshWater.uasset new file mode 100644 index 0000000..3ce4856 --- /dev/null +++ b/Content/Agrarian/Materials/M_AGR_GZ_FreshWater.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ea4675d9b82a3a79256d1ab68cd0e07cccf0f94fe4f5069ebbe08caf2c165e97 +size 5429 diff --git a/Content/Agrarian/Materials/M_AGR_GZ_Grass_DryCoastal.uasset b/Content/Agrarian/Materials/M_AGR_GZ_Grass_DryCoastal.uasset new file mode 100644 index 0000000..68ab36a --- /dev/null +++ b/Content/Agrarian/Materials/M_AGR_GZ_Grass_DryCoastal.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:319c13875c6229171a95026874c28c55e8e6e67c1f03df98f67a17ccb472204a +size 5465 diff --git a/Content/Agrarian/Materials/M_AGR_GZ_Shrub_CoyoteBrush.uasset b/Content/Agrarian/Materials/M_AGR_GZ_Shrub_CoyoteBrush.uasset new file mode 100644 index 0000000..4643980 --- /dev/null +++ b/Content/Agrarian/Materials/M_AGR_GZ_Shrub_CoyoteBrush.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d72c2711649942c1616deb9f3a148dc8b1220451061b279b56d6869e88622979 +size 5471 diff --git a/Content/Agrarian/Materials/M_AGR_GZ_Stone_Sandstone.uasset b/Content/Agrarian/Materials/M_AGR_GZ_Stone_Sandstone.uasset new file mode 100644 index 0000000..e492ebb --- /dev/null +++ b/Content/Agrarian/Materials/M_AGR_GZ_Stone_Sandstone.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53025de49292334da46b46146c836ef30df2f08149afde687dd6dd04552c1bcc +size 5459 diff --git a/Content/Agrarian/Materials/M_AGR_GZ_Terrain_CoastalScrub.uasset b/Content/Agrarian/Materials/M_AGR_GZ_Terrain_CoastalScrub.uasset new file mode 100644 index 0000000..ad96c67 --- /dev/null +++ b/Content/Agrarian/Materials/M_AGR_GZ_Terrain_CoastalScrub.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:afa93e2eb85f2b2e69da0a72aa0aff75c626539bac72d15e4f9418ebf19bdc65 +size 5489 diff --git a/Content/Agrarian/Materials/M_AGR_GZ_Tree_CoastalOak.uasset b/Content/Agrarian/Materials/M_AGR_GZ_Tree_CoastalOak.uasset new file mode 100644 index 0000000..1f62a46 --- /dev/null +++ b/Content/Agrarian/Materials/M_AGR_GZ_Tree_CoastalOak.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:830f96bb928091a0edad65cfe567eafe2b173816351fb3aa47a7773596a5337e +size 5459 diff --git a/Content/Agrarian/Materials/M_AGR_GZ_Wood_Resource.uasset b/Content/Agrarian/Materials/M_AGR_GZ_Wood_Resource.uasset new file mode 100644 index 0000000..00a2682 --- /dev/null +++ b/Content/Agrarian/Materials/M_AGR_GZ_Wood_Resource.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a1964bca35649776ba24a0e226897c9db6d03ad5460134028667c89b8937b0e +size 5447 diff --git a/Docs/Terrain/GroundZeroFoliagePass.md b/Docs/Terrain/GroundZeroFoliagePass.md index 94337ac..889ec3a 100644 --- a/Docs/Terrain/GroundZeroFoliagePass.md +++ b/Docs/Terrain/GroundZeroFoliagePass.md @@ -35,6 +35,7 @@ and wildlife spawn area so the initial investor path remains readable. ## Follow-Up This is a visual and performance-safe placeholder pass, not final ecology. The -next biome/resource passes should replace prototype meshes with real coastal -California scrub, grassland, woodland, and resource-specific assets, then tie -spawn density to land-cover and hydrography data. +current prototype geometry now uses dedicated Ground Zero tree, shrub, and dry +grass materials. The next biome/resource passes should replace prototype meshes +with real coastal California scrub, grassland, woodland, and resource-specific +assets, then tie spawn density to land-cover and hydrography data. diff --git a/Docs/Terrain/GroundZeroNaturalEnvironmentPass.md b/Docs/Terrain/GroundZeroNaturalEnvironmentPass.md new file mode 100644 index 0000000..2db3771 --- /dev/null +++ b/Docs/Terrain/GroundZeroNaturalEnvironmentPass.md @@ -0,0 +1,39 @@ +# Ground Zero MVP Natural Environment Pass + +The Ground Zero map now has a first MVP natural environment pass so the investor +demo reads as coastal California scrub/woodland instead of a plain grey-box test +space. + +## Scope + +- Terrain receives a warm coastal scrub ground material. +- Foliage patch instances keep the current prototype meshes but use distinct + tree, shrub, and dry grass materials. +- Wood, fiber, stone, and freshwater actors receive distinct first-pass + materials. +- The setup remains repeatable through `Scripts/setup_ground_zero_demo_map.py`. + +## Material Assets + +- `/Game/Agrarian/Materials/M_AGR_GZ_Terrain_CoastalScrub` +- `/Game/Agrarian/Materials/M_AGR_GZ_Tree_CoastalOak` +- `/Game/Agrarian/Materials/M_AGR_GZ_Shrub_CoyoteBrush` +- `/Game/Agrarian/Materials/M_AGR_GZ_Grass_DryCoastal` +- `/Game/Agrarian/Materials/M_AGR_GZ_Wood_Resource` +- `/Game/Agrarian/Materials/M_AGR_GZ_Fiber_Resource` +- `/Game/Agrarian/Materials/M_AGR_GZ_Stone_Sandstone` +- `/Game/Agrarian/Materials/M_AGR_GZ_FreshWater` + +## Validation + +`Scripts/verify_ground_zero_natural_environment_pass.py` checks that the +materials exist, the landscape uses the terrain material, the foliage actor has +the expected instance counts and material assignments, and resource/water actors +are visually dressed. + +## Follow-Up + +This pass deliberately keeps the current prototype geometry so it stays small +and stable. The next environment work should add first-pass asset variation so +trees, bushes, grass, resource nodes, and water no longer read as repeated +placeholder shapes. diff --git a/Scripts/setup_ground_zero_demo_map.py b/Scripts/setup_ground_zero_demo_map.py index 845fbe2..062b091 100644 --- a/Scripts/setup_ground_zero_demo_map.py +++ b/Scripts/setup_ground_zero_demo_map.py @@ -22,6 +22,57 @@ FOLIAGE_MESHES = { "shrub": "/Game/LevelPrototyping/Meshes/SM_Cube", "grass": "/Game/LevelPrototyping/Meshes/SM_Cylinder", } +MATERIAL_FOLDER = "/Game/Agrarian/Materials" +ENVIRONMENT_MATERIALS = { + "terrain": { + "path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Terrain_CoastalScrub", + "color": unreal.LinearColor(0.28, 0.24, 0.16, 1.0), + "roughness": 0.92, + }, + "tree": { + "path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Tree_CoastalOak", + "color": unreal.LinearColor(0.18, 0.31, 0.16, 1.0), + "roughness": 0.88, + }, + "shrub": { + "path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Shrub_CoyoteBrush", + "color": unreal.LinearColor(0.31, 0.39, 0.20, 1.0), + "roughness": 0.9, + }, + "grass": { + "path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Grass_DryCoastal", + "color": unreal.LinearColor(0.47, 0.42, 0.23, 1.0), + "roughness": 0.95, + }, + "wood_resource": { + "path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Wood_Resource", + "color": unreal.LinearColor(0.26, 0.16, 0.08, 1.0), + "roughness": 0.86, + }, + "fiber_resource": { + "path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Fiber_Resource", + "color": unreal.LinearColor(0.55, 0.49, 0.28, 1.0), + "roughness": 0.93, + }, + "stone_resource": { + "path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Stone_Sandstone", + "color": unreal.LinearColor(0.43, 0.40, 0.35, 1.0), + "roughness": 0.97, + }, + "fresh_water": { + "path": f"{MATERIAL_FOLDER}/M_AGR_GZ_FreshWater", + "color": unreal.LinearColor(0.08, 0.28, 0.38, 1.0), + "roughness": 0.35, + }, +} +RESOURCE_MATERIAL_BY_LABEL_PREFIX = { + "AGR_GZ_Wood": "wood_resource", + "AGR_GZ_Fiber": "fiber_resource", + "AGR_GZ_Stone": "stone_resource", + "AGR_DemoWoodResource": "wood_resource", + "AGR_DemoFiberResource": "fiber_resource", + "AGR_GZ_FreshWaterSource": "fresh_water", +} DEMO_ACTORS = [ @@ -245,6 +296,75 @@ def load_required_asset(path): return asset +def ensure_environment_materials(): + if not unreal.EditorAssetLibrary.does_directory_exist(MATERIAL_FOLDER): + unreal.EditorAssetLibrary.make_directory(MATERIAL_FOLDER) + + asset_tools = unreal.AssetToolsHelpers.get_asset_tools() + created_or_loaded = {} + for key, spec in ENVIRONMENT_MATERIALS.items(): + material = unreal.EditorAssetLibrary.load_asset(spec["path"]) + if not material: + asset_name = spec["path"].rsplit("/", 1)[-1] + material = asset_tools.create_asset(asset_name, MATERIAL_FOLDER, unreal.Material, unreal.MaterialFactoryNew()) + if not material: + raise RuntimeError(f"Could not create material: {spec['path']}") + + base_color = unreal.MaterialEditingLibrary.create_material_expression( + material, unreal.MaterialExpressionConstant3Vector, -420, -120 + ) + base_color.set_editor_property("constant", spec["color"]) + unreal.MaterialEditingLibrary.connect_material_property( + base_color, "", unreal.MaterialProperty.MP_BASE_COLOR + ) + + roughness = unreal.MaterialEditingLibrary.create_material_expression( + material, unreal.MaterialExpressionConstant, -420, 80 + ) + roughness.set_editor_property("r", spec["roughness"]) + unreal.MaterialEditingLibrary.connect_material_property( + roughness, "", unreal.MaterialProperty.MP_ROUGHNESS + ) + + unreal.MaterialEditingLibrary.recompile_material(material) + unreal.EditorAssetLibrary.save_asset(spec["path"]) + unreal.log(f"Created Ground Zero environment material: {spec['path']}") + created_or_loaded[key] = material + + return created_or_loaded + + +def apply_material_to_actor_meshes(actor, material): + applied_count = 0 + for component in actor.get_components_by_class(unreal.StaticMeshComponent): + slot_count = component.get_num_materials() + if slot_count == 0: + component.set_material(0, material) + applied_count += 1 + continue + for slot_index in range(slot_count): + component.set_material(slot_index, material) + applied_count += 1 + return applied_count + + +def material_key_for_actor_label(label): + for prefix, material_key in RESOURCE_MATERIAL_BY_LABEL_PREFIX.items(): + if label.startswith(prefix): + return material_key + return None + + +def apply_landscape_material(material): + applied = 0 + for actor in unreal.EditorLevelLibrary.get_all_level_actors(): + if isinstance(actor, unreal.Landscape): + actor.set_editor_property("landscape_material", material) + applied += 1 + unreal.log(f"Applied Ground Zero terrain material to {applied} landscape actor(s).") + return applied + + def load_heightmap(): raw = HEIGHTMAP_PATH.read_bytes() expected_bytes = LANDSCAPE_SIZE * LANDSCAPE_SIZE * 2 @@ -295,6 +415,18 @@ def configure_foliage_meshes(foliage_actor): component.set_editor_property("static_mesh", mesh) +def apply_foliage_materials(foliage_actor, materials): + component_materials = { + "tree_instances": materials["tree"], + "shrub_instances": materials["shrub"], + "grass_instances": materials["grass"], + } + + for property_name, material in component_materials.items(): + component = foliage_actor.get_editor_property(property_name) + component.set_material(0, material) + + def choose_foliage_points(height_values, zone, reserved_points, existing_points): rng = random.Random(FOLIAGE_RANDOM_SEED + len(existing_points) + int(zone["count"])) chosen = [] @@ -325,7 +457,7 @@ def choose_foliage_points(height_values, zone, reserved_points, existing_points) return chosen -def spawn_foliage_actor(height_values): +def spawn_foliage_actor(height_values, materials): reserved_points = [ spec["location_xy"] for spec in DEMO_ACTORS @@ -343,6 +475,7 @@ def spawn_foliage_actor(height_values): set_actor_label(foliage_actor, FOLIAGE_LABEL) configure_foliage_meshes(foliage_actor) + apply_foliage_materials(foliage_actor, materials) foliage_actor.clear_foliage() existing_points = [] @@ -375,7 +508,7 @@ def spawn_foliage_actor(height_values): return foliage_actor -def spawn_demo_actor(spec, height_values): +def spawn_demo_actor(spec, height_values, materials): location_xy = spec["location_xy"] z = spec.get("fixed_z") if z is None: @@ -395,6 +528,9 @@ def spawn_demo_actor(spec, height_values): raise RuntimeError(f"Could not spawn {spec['label']}") set_actor_label(actor, spec["label"]) + material_key = material_key_for_actor_label(spec["label"]) + if material_key: + apply_material_to_actor_meshes(actor, materials[material_key]) unreal.log(f"Placed {spec['label']} at {actor.get_actor_location()}") return actor @@ -410,14 +546,16 @@ def main(): labels.add(FOLIAGE_LABEL) remove_existing_demo_actors(labels) + materials = ensure_environment_materials() + apply_landscape_material(materials["terrain"]) height_values = load_heightmap() - spawn_foliage_actor(height_values) + spawn_foliage_actor(height_values, materials) for spec in BIOME_RESOURCE_ACTORS: - spawn_demo_actor(spec, height_values) + spawn_demo_actor(spec, height_values, materials) for spec in WATER_SOURCE_ACTORS: - spawn_demo_actor(spec, height_values) + spawn_demo_actor(spec, height_values, materials) for spec in DEMO_ACTORS: - spawn_demo_actor(spec, height_values) + spawn_demo_actor(spec, height_values, materials) unreal.EditorLevelLibrary.save_current_level() unreal.log("Ground Zero demo map setup complete.") diff --git a/Scripts/verify_ground_zero_natural_environment_pass.py b/Scripts/verify_ground_zero_natural_environment_pass.py new file mode 100644 index 0000000..f4bc8fa --- /dev/null +++ b/Scripts/verify_ground_zero_natural_environment_pass.py @@ -0,0 +1,141 @@ +import unreal + + +MAP_PATH = "/Game/Agrarian/Maps/L_GroundZeroTerrain_Test" +FOLIAGE_LABEL = "AGR_GroundZeroFoliage_FirstPass" +MATERIALS = { + "terrain": "/Game/Agrarian/Materials/M_AGR_GZ_Terrain_CoastalScrub", + "tree": "/Game/Agrarian/Materials/M_AGR_GZ_Tree_CoastalOak", + "shrub": "/Game/Agrarian/Materials/M_AGR_GZ_Shrub_CoyoteBrush", + "grass": "/Game/Agrarian/Materials/M_AGR_GZ_Grass_DryCoastal", + "wood_resource": "/Game/Agrarian/Materials/M_AGR_GZ_Wood_Resource", + "fiber_resource": "/Game/Agrarian/Materials/M_AGR_GZ_Fiber_Resource", + "stone_resource": "/Game/Agrarian/Materials/M_AGR_GZ_Stone_Sandstone", + "fresh_water": "/Game/Agrarian/Materials/M_AGR_GZ_FreshWater", +} +EXPECTED_FOLIAGE_COUNTS = { + "trees": 42, + "shrubs": 96, + "grass": 180, +} +RESOURCE_MATERIALS = { + "AGR_GZ_Wood": "wood_resource", + "AGR_GZ_Fiber": "fiber_resource", + "AGR_GZ_Stone": "stone_resource", + "AGR_DemoWoodResource": "wood_resource", + "AGR_DemoFiberResource": "fiber_resource", + "AGR_GZ_FreshWaterSource": "fresh_water", +} + + +def get_actor_label(actor): + try: + return actor.get_actor_label() + except Exception: + return actor.get_name() + + +def material_path(material): + if not material: + return "" + return material.get_path_name().split(".", 1)[0] + + +def material_key_for_label(label): + for prefix, material_key in RESOURCE_MATERIALS.items(): + if label.startswith(prefix): + return material_key + return None + + +def assert_asset(path): + asset = unreal.EditorAssetLibrary.load_asset(path) + if not asset: + raise RuntimeError(f"Missing required environment material: {path}") + return asset + + +def main(): + if not unreal.EditorLevelLibrary.load_level(MAP_PATH): + raise RuntimeError(f"Could not load map: {MAP_PATH}") + + materials = {key: assert_asset(path) for key, path in MATERIALS.items()} + actors = unreal.EditorLevelLibrary.get_all_level_actors() + failures = [] + + landscapes = [actor for actor in actors if isinstance(actor, unreal.Landscape)] + if not landscapes: + failures.append("no landscape actor found") + for landscape in landscapes: + assigned = material_path(landscape.get_editor_property("landscape_material")) + expected = MATERIALS["terrain"] + if assigned != expected: + failures.append(f"landscape material expected {expected}, got {assigned}") + + foliage_actors = [actor for actor in actors if get_actor_label(actor) == FOLIAGE_LABEL] + if len(foliage_actors) != 1: + failures.append(f"expected one {FOLIAGE_LABEL}, found {len(foliage_actors)}") + else: + foliage = foliage_actors[0] + counts = { + "trees": foliage.get_tree_instance_count(), + "shrubs": foliage.get_shrub_instance_count(), + "grass": foliage.get_grass_instance_count(), + } + for key, expected_count in EXPECTED_FOLIAGE_COUNTS.items(): + if counts[key] != expected_count: + failures.append(f"{key} expected {expected_count}, got {counts[key]}") + + component_expectations = { + "tree_instances": "tree", + "shrub_instances": "shrub", + "grass_instances": "grass", + } + for property_name, material_key in component_expectations.items(): + component = foliage.get_editor_property(property_name) + assigned = material_path(component.get_material(0)) + expected = MATERIALS[material_key] + if assigned != expected: + failures.append(f"{property_name} material expected {expected}, got {assigned}") + + checked_resource_actors = 0 + for actor in actors: + label = get_actor_label(actor) + material_key = material_key_for_label(label) + if not material_key: + continue + + checked_resource_actors += 1 + expected = MATERIALS[material_key] + mesh_components = actor.get_components_by_class(unreal.StaticMeshComponent) + if not mesh_components: + failures.append(f"{label} has no static mesh component for material assignment") + continue + assigned_any = any(material_path(component.get_material(0)) == expected for component in mesh_components) + if not assigned_any: + failures.append(f"{label} did not use expected material {expected}") + + if checked_resource_actors < 10: + failures.append(f"expected at least 10 dressed resource/water actors, checked {checked_resource_actors}") + + docs = unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_dir()) + "Docs/Terrain/GroundZeroNaturalEnvironmentPass.md" + roadmap = unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_dir()) + "AGRARIAN_DEVELOPMENT_ROADMAP.md" + for path, snippet in [ + (docs, "MVP natural environment pass"), + (roadmap, "[x] Replace grey-box environment presentation with an MVP natural environment pass"), + ]: + with open(path, "r", encoding="utf-8") as handle: + text = handle.read() + if snippet not in text: + failures.append(f"{path} missing {snippet}") + + if failures: + raise RuntimeError("Ground Zero natural environment verification failed: " + "; ".join(failures)) + + unreal.log( + "Ground Zero natural environment verification complete: " + f"{len(materials)} materials, {len(landscapes)} landscape(s), {checked_resource_actors} dressed resource/water actor(s)." + ) + + +main()