class_name DiveSiteDefinition extends Resource const SiteTerrainData = preload("res://game/domain/sites/site_terrain_data.gd") const TerrainHeightField = preload("res://game/domain/sites/terrain_height_field.gd") const SitePlacement = preload("res://game/domain/sites/site_placement.gd") const SiteRoute = preload("res://game/domain/sites/site_route.gd") @export var site_id: StringName @export var display_name: String = "" @export var terrain: SiteTerrainData ## Metres, world XZ: position = (x_min, z_min), size = (width along +X, depth along +Z). @export var playable_bounds_m: Rect2 = Rect2() @export var spawn_position_m: Vector3 = Vector3.ZERO @export var spawn_yaw_radians: float = 0.0 @export var placements: Array[SitePlacement] = [] @export var routes: Array[SiteRoute] = [] func height_field() -> TerrainHeightField: return TerrainHeightField.from_data(terrain) func spawn_transform() -> Transform3D: return Transform3D(Basis(Vector3.UP, spawn_yaw_radians), spawn_position_m) func placement(placement_id: StringName) -> SitePlacement: for candidate: SitePlacement in placements: if candidate != null and candidate.placement_id == placement_id: return candidate return null func placements_of_kind(kind: SitePlacement.Kind) -> Array[SitePlacement]: var result: Array[SitePlacement] = [] for candidate: SitePlacement in placements: if candidate != null and candidate.kind == kind: result.append(candidate) return result func route(route_id: StringName) -> SiteRoute: for candidate: SiteRoute in routes: if candidate != null and candidate.route_id == route_id: return candidate return null func validation_error() -> String: if site_id.is_empty(): return "site id is empty" if terrain == null: return "site terrain is missing" var terrain_error := terrain.validation_error() if not terrain_error.is_empty(): return terrain_error var field := TerrainHeightField.from_data(terrain) if playable_bounds_m.size.x <= 0.0 or playable_bounds_m.size.y <= 0.0: return "playable bounds must have a positive size" if not field.bounds_m().encloses(playable_bounds_m): return "playable bounds must lie inside the terrain" if not _inside_bounds(spawn_position_m): return "spawn position must lie inside the playable bounds" if not is_finite(spawn_yaw_radians): return "spawn yaw must be finite" var seen_ids: Dictionary = {} for candidate: SitePlacement in placements: if candidate == null: return "placement entry is null" var placement_error := candidate.validation_error() if not placement_error.is_empty(): return placement_error if seen_ids.has(candidate.placement_id): return "placement id %s is duplicated" % candidate.placement_id seen_ids[candidate.placement_id] = true if not _inside_bounds(candidate.position_m): return "placement %s lies outside the playable bounds" % candidate.placement_id var seen_routes: Dictionary = {} for candidate_route: SiteRoute in routes: if candidate_route == null: return "route entry is null" var route_error := candidate_route.validation_error() if not route_error.is_empty(): return route_error if seen_routes.has(candidate_route.route_id): return "route id %s is duplicated" % candidate_route.route_id seen_routes[candidate_route.route_id] = true for waypoint: Vector3 in candidate_route.waypoints_m: if not _inside_bounds(waypoint): return "route %s leaves the playable bounds" % candidate_route.route_id return "" func _inside_bounds(position: Vector3) -> bool: return playable_bounds_m.has_point(Vector2(position.x, position.z))