97 lines
3.5 KiB
GDScript
97 lines
3.5 KiB
GDScript
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))
|