Validate Ground Zero safe spawn area
This commit is contained in:
@@ -93,6 +93,25 @@ RESOURCE_MATERIAL_BY_LABEL_PREFIX = {
|
||||
"AGR_GZ_FreshWaterSource": "fresh_water",
|
||||
}
|
||||
|
||||
SAFE_SPAWN_CONFIG = {
|
||||
"player_start_label": "AGR_DemoPlayerStart",
|
||||
"safe_z_offset": 220.0,
|
||||
"min_elevation_m": 2.0,
|
||||
"max_slope_degrees": 8.0,
|
||||
"slope_sample_distance_cm": 500.0,
|
||||
"min_water_distance_cm": 10000.0,
|
||||
"min_resource_distance_cm": 5000.0,
|
||||
"fallback_location_xy": unreal.Vector(-22000.0, -3500.0, 0.0),
|
||||
"candidate_locations_xy": [
|
||||
unreal.Vector(-22000.0, -3500.0, 0.0),
|
||||
unreal.Vector(-25000.0, -4000.0, 0.0),
|
||||
unreal.Vector(-20500.0, -10500.0, 0.0),
|
||||
unreal.Vector(-28000.0, -9000.0, 0.0),
|
||||
unreal.Vector(-30000.0, -12000.0, 0.0),
|
||||
unreal.Vector(-35000.0, 0.0, 0.0),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
DEMO_ACTORS = [
|
||||
{
|
||||
@@ -597,6 +616,18 @@ def terrain_elevation_m(height_values, x_cm, y_cm):
|
||||
return terrain_z_cm(height_values, x_cm, y_cm) / 100.0
|
||||
|
||||
|
||||
def terrain_slope_degrees(height_values, x_cm, y_cm, sample_distance_cm):
|
||||
dz_dx_m = (
|
||||
terrain_elevation_m(height_values, x_cm + sample_distance_cm, y_cm)
|
||||
- terrain_elevation_m(height_values, x_cm - sample_distance_cm, y_cm)
|
||||
) / ((sample_distance_cm * 2.0) / 100.0)
|
||||
dz_dy_m = (
|
||||
terrain_elevation_m(height_values, x_cm, y_cm + sample_distance_cm)
|
||||
- terrain_elevation_m(height_values, x_cm, y_cm - sample_distance_cm)
|
||||
) / ((sample_distance_cm * 2.0) / 100.0)
|
||||
return math.degrees(math.atan(math.sqrt((dz_dx_m * dz_dx_m) + (dz_dy_m * dz_dy_m))))
|
||||
|
||||
|
||||
def remove_existing_demo_actors(labels):
|
||||
for actor in unreal.EditorLevelLibrary.get_all_level_actors():
|
||||
if get_actor_label(actor) in labels:
|
||||
@@ -607,6 +638,60 @@ def distance_2d(a, b):
|
||||
return math.hypot(a.x - b.x, a.y - b.y)
|
||||
|
||||
|
||||
def validate_safe_spawn_location(height_values, location_xy, resource_points, water_points):
|
||||
failures = []
|
||||
elevation_m = terrain_elevation_m(height_values, location_xy.x, location_xy.y)
|
||||
slope_degrees = terrain_slope_degrees(
|
||||
height_values,
|
||||
location_xy.x,
|
||||
location_xy.y,
|
||||
SAFE_SPAWN_CONFIG["slope_sample_distance_cm"],
|
||||
)
|
||||
min_resource_distance_cm = min(distance_2d(location_xy, point) for point in resource_points)
|
||||
min_water_distance_cm = min(distance_2d(location_xy, point) for point in water_points)
|
||||
|
||||
if elevation_m < SAFE_SPAWN_CONFIG["min_elevation_m"]:
|
||||
failures.append(f"elevation {elevation_m:.2f}m below minimum")
|
||||
if slope_degrees > SAFE_SPAWN_CONFIG["max_slope_degrees"]:
|
||||
failures.append(f"slope {slope_degrees:.2f}deg above maximum")
|
||||
if min_resource_distance_cm < SAFE_SPAWN_CONFIG["min_resource_distance_cm"]:
|
||||
failures.append(f"resource distance {min_resource_distance_cm:.0f}cm below minimum")
|
||||
if min_water_distance_cm < SAFE_SPAWN_CONFIG["min_water_distance_cm"]:
|
||||
failures.append(f"water distance {min_water_distance_cm:.0f}cm below minimum")
|
||||
|
||||
return failures
|
||||
|
||||
|
||||
def select_safe_spawn_location(height_values):
|
||||
resource_points = [spec["location_xy"] for spec in BIOME_RESOURCE_ACTORS]
|
||||
resource_points.extend(
|
||||
spec["location_xy"]
|
||||
for spec in DEMO_ACTORS
|
||||
if spec["label"] in {"AGR_DemoWoodResource_01", "AGR_DemoFiberResource_01"}
|
||||
)
|
||||
water_points = [spec["location_xy"] for spec in WATER_SOURCE_ACTORS]
|
||||
|
||||
candidates = list(SAFE_SPAWN_CONFIG["candidate_locations_xy"])
|
||||
fallback = SAFE_SPAWN_CONFIG["fallback_location_xy"]
|
||||
if all(distance_2d(fallback, candidate) > 1.0 for candidate in candidates):
|
||||
candidates.append(fallback)
|
||||
|
||||
rejected = []
|
||||
for candidate in candidates:
|
||||
failures = validate_safe_spawn_location(height_values, candidate, resource_points, water_points)
|
||||
if not failures:
|
||||
unreal.log(
|
||||
"Selected safe Ground Zero spawn at "
|
||||
f"({candidate.x:.0f}, {candidate.y:.0f}) "
|
||||
f"elevation {terrain_elevation_m(height_values, candidate.x, candidate.y):.2f}m "
|
||||
f"slope {terrain_slope_degrees(height_values, candidate.x, candidate.y, SAFE_SPAWN_CONFIG['slope_sample_distance_cm']):.2f}deg"
|
||||
)
|
||||
return candidate
|
||||
rejected.append(f"({candidate.x:.0f}, {candidate.y:.0f}): {', '.join(failures)}")
|
||||
|
||||
raise RuntimeError("No safe Ground Zero spawn candidate found. Rejected: " + "; ".join(rejected))
|
||||
|
||||
|
||||
def make_foliage_transform(height_values, x, y, yaw, z_offset, scale_xy, scale_z):
|
||||
return unreal.Transform(
|
||||
location=unreal.Vector(x, y, terrain_z_cm(height_values, x, y) + z_offset),
|
||||
@@ -720,8 +805,11 @@ def spawn_foliage_actor(height_values, materials):
|
||||
return foliage_actor
|
||||
|
||||
|
||||
def spawn_demo_actor(spec, height_values, materials):
|
||||
def spawn_demo_actor(spec, height_values, materials, safe_spawn_location_xy=None):
|
||||
location_xy = spec["location_xy"]
|
||||
if spec["label"] == SAFE_SPAWN_CONFIG["player_start_label"] and safe_spawn_location_xy is not None:
|
||||
location_xy = safe_spawn_location_xy
|
||||
|
||||
z = spec.get("fixed_z")
|
||||
if z is None:
|
||||
z = terrain_z_cm(height_values, location_xy.x, location_xy.y) + spec.get("z_offset", 0.0)
|
||||
@@ -811,6 +899,7 @@ def main():
|
||||
materials = ensure_environment_materials()
|
||||
apply_landscape_material(materials["terrain"])
|
||||
height_values = load_heightmap()
|
||||
safe_spawn_location_xy = select_safe_spawn_location(height_values)
|
||||
spawn_foliage_actor(height_values, materials)
|
||||
for spec in BIOME_RESOURCE_ACTORS:
|
||||
spawn_demo_actor(spec, height_values, materials)
|
||||
@@ -823,7 +912,7 @@ def main():
|
||||
for spec in RUIN_PLACEHOLDER_ACTORS:
|
||||
spawn_environment_variation_actor(spec, height_values, materials)
|
||||
for spec in DEMO_ACTORS:
|
||||
spawn_demo_actor(spec, height_values, materials)
|
||||
spawn_demo_actor(spec, height_values, materials, safe_spawn_location_xy)
|
||||
|
||||
unreal.EditorLevelLibrary.save_current_level()
|
||||
unreal.log("Ground Zero demo map setup complete.")
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import math
|
||||
import struct
|
||||
from pathlib import Path
|
||||
|
||||
import unreal
|
||||
|
||||
|
||||
MAP_PATH = "/Game/Agrarian/Maps/L_GroundZeroTerrain_Test"
|
||||
PROJECT_ROOT = Path(r"Y:\AgrarianGameBulid")
|
||||
TILE_ID = "gz_us_ca_pacifica_utm10n_e544_n4160"
|
||||
HEIGHTMAP_PATH = PROJECT_ROOT / "Data" / "Terrain" / "Unreal" / TILE_ID / f"{TILE_ID}_unreal_1009.r16"
|
||||
LANDSCAPE_SIZE = 1009
|
||||
XY_SCALE_CM = 100000.0 / (LANDSCAPE_SIZE - 1)
|
||||
Z_SCALE_CM = 100.0
|
||||
LANDSCAPE_MIN_XY = -50000.0
|
||||
|
||||
PLAYER_START_LABEL = "AGR_DemoPlayerStart"
|
||||
SAFE_Z_OFFSET_CM = 220.0
|
||||
MIN_ELEVATION_M = 2.0
|
||||
MAX_SLOPE_DEGREES = 8.0
|
||||
SLOPE_SAMPLE_DISTANCE_CM = 500.0
|
||||
MIN_WATER_DISTANCE_CM = 10000.0
|
||||
MIN_RESOURCE_DISTANCE_CM = 5000.0
|
||||
KNOWN_SAFE_FALLBACK_XY = (-22000.0, -3500.0)
|
||||
RESOURCE_PREFIXES = (
|
||||
"AGR_GZ_Wood",
|
||||
"AGR_GZ_Fiber",
|
||||
"AGR_GZ_Stone",
|
||||
"AGR_DemoWoodResource",
|
||||
"AGR_DemoFiberResource",
|
||||
)
|
||||
WATER_LABELS = {
|
||||
"AGR_GZ_FreshWaterSource_01",
|
||||
"AGR_GZ_EnvVar_Water_Surface_01",
|
||||
}
|
||||
|
||||
|
||||
def get_actor_label(actor):
|
||||
try:
|
||||
return actor.get_actor_label()
|
||||
except Exception:
|
||||
return actor.get_name()
|
||||
|
||||
|
||||
def load_heightmap():
|
||||
raw = HEIGHTMAP_PATH.read_bytes()
|
||||
expected_bytes = LANDSCAPE_SIZE * LANDSCAPE_SIZE * 2
|
||||
if len(raw) != expected_bytes:
|
||||
raise RuntimeError(f"Unexpected heightmap size: {len(raw)} != {expected_bytes}")
|
||||
return struct.unpack(f"<{LANDSCAPE_SIZE * LANDSCAPE_SIZE}H", raw)
|
||||
|
||||
|
||||
def terrain_elevation_m(height_values, x_cm, y_cm):
|
||||
sample_x = max(0, min(LANDSCAPE_SIZE - 1, round((x_cm - LANDSCAPE_MIN_XY) / XY_SCALE_CM)))
|
||||
sample_y = max(0, min(LANDSCAPE_SIZE - 1, round((y_cm - LANDSCAPE_MIN_XY) / XY_SCALE_CM)))
|
||||
height_value = height_values[int(sample_y) * LANDSCAPE_SIZE + int(sample_x)]
|
||||
return (float(height_value) - 32768.0) * Z_SCALE_CM / (100.0 * 128.0)
|
||||
|
||||
|
||||
def terrain_slope_degrees(height_values, x_cm, y_cm):
|
||||
dz_dx_m = (
|
||||
terrain_elevation_m(height_values, x_cm + SLOPE_SAMPLE_DISTANCE_CM, y_cm)
|
||||
- terrain_elevation_m(height_values, x_cm - SLOPE_SAMPLE_DISTANCE_CM, y_cm)
|
||||
) / ((SLOPE_SAMPLE_DISTANCE_CM * 2.0) / 100.0)
|
||||
dz_dy_m = (
|
||||
terrain_elevation_m(height_values, x_cm, y_cm + SLOPE_SAMPLE_DISTANCE_CM)
|
||||
- terrain_elevation_m(height_values, x_cm, y_cm - SLOPE_SAMPLE_DISTANCE_CM)
|
||||
) / ((SLOPE_SAMPLE_DISTANCE_CM * 2.0) / 100.0)
|
||||
return math.degrees(math.atan(math.sqrt((dz_dx_m * dz_dx_m) + (dz_dy_m * dz_dy_m))))
|
||||
|
||||
|
||||
def distance_2d(a, b):
|
||||
return math.hypot(a.x - b.x, a.y - b.y)
|
||||
|
||||
|
||||
def main():
|
||||
if not unreal.EditorLevelLibrary.load_level(MAP_PATH):
|
||||
raise RuntimeError(f"Could not load map: {MAP_PATH}")
|
||||
|
||||
failures = []
|
||||
actors = unreal.EditorLevelLibrary.get_all_level_actors()
|
||||
starts = [actor for actor in actors if get_actor_label(actor) == PLAYER_START_LABEL]
|
||||
if len(starts) != 1:
|
||||
raise RuntimeError(f"Expected exactly one {PLAYER_START_LABEL}, found {len(starts)}")
|
||||
|
||||
player_start = starts[0]
|
||||
location = player_start.get_actor_location()
|
||||
height_values = load_heightmap()
|
||||
terrain_elevation = terrain_elevation_m(height_values, location.x, location.y)
|
||||
terrain_z_cm = terrain_elevation * 100.0
|
||||
slope_degrees = terrain_slope_degrees(height_values, location.x, location.y)
|
||||
actual_z_offset_cm = location.z - terrain_z_cm
|
||||
|
||||
resource_points = [
|
||||
actor.get_actor_location()
|
||||
for actor in actors
|
||||
if get_actor_label(actor).startswith(RESOURCE_PREFIXES)
|
||||
]
|
||||
water_points = [
|
||||
actor.get_actor_location()
|
||||
for actor in actors
|
||||
if get_actor_label(actor) in WATER_LABELS
|
||||
]
|
||||
|
||||
if terrain_elevation < MIN_ELEVATION_M:
|
||||
failures.append(f"spawn elevation {terrain_elevation:.2f}m below {MIN_ELEVATION_M:.2f}m")
|
||||
if actual_z_offset_cm < SAFE_Z_OFFSET_CM - 1.0:
|
||||
failures.append(f"spawn z offset {actual_z_offset_cm:.1f}cm below {SAFE_Z_OFFSET_CM:.1f}cm")
|
||||
if slope_degrees > MAX_SLOPE_DEGREES:
|
||||
failures.append(f"spawn slope {slope_degrees:.2f}deg exceeds {MAX_SLOPE_DEGREES:.2f}deg")
|
||||
if not resource_points:
|
||||
failures.append("no resource actors found for spacing validation")
|
||||
elif min(distance_2d(location, point) for point in resource_points) < MIN_RESOURCE_DISTANCE_CM:
|
||||
failures.append("spawn is too close to a resource cluster")
|
||||
if not water_points:
|
||||
failures.append("no water actors found for spacing validation")
|
||||
elif min(distance_2d(location, point) for point in water_points) < MIN_WATER_DISTANCE_CM:
|
||||
failures.append("spawn is too close to water")
|
||||
|
||||
fallback_point = unreal.Vector(KNOWN_SAFE_FALLBACK_XY[0], KNOWN_SAFE_FALLBACK_XY[1], 0.0)
|
||||
if distance_2d(location, fallback_point) > 1.0:
|
||||
failures.append(
|
||||
"spawn is not using the known safe fallback coordinate "
|
||||
f"{KNOWN_SAFE_FALLBACK_XY}, got ({location.x:.0f}, {location.y:.0f})"
|
||||
)
|
||||
|
||||
roadmap = unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_dir()) + "AGRARIAN_DEVELOPMENT_ROADMAP.md"
|
||||
technical_design = unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_dir()) + "Docs/TechnicalDesignDocument.md"
|
||||
for path, snippet in [
|
||||
(roadmap, "[x] Add spawn area with validation"),
|
||||
(technical_design, "safe Ground Zero spawn"),
|
||||
]:
|
||||
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 safe spawn verification failed: " + "; ".join(failures))
|
||||
|
||||
unreal.log(
|
||||
"Ground Zero safe spawn verification complete: "
|
||||
f"location ({location.x:.0f}, {location.y:.0f}, {location.z:.0f}), "
|
||||
f"terrain {terrain_elevation:.2f}m, slope {slope_degrees:.2f}deg, z offset {actual_z_offset_cm:.0f}cm."
|
||||
)
|
||||
|
||||
|
||||
main()
|
||||
Reference in New Issue
Block a user