Files

209 lines
8.5 KiB
GDScript

class_name RelayArena
extends Node3D
var obstacles: Array[AABB] = []
var nav := AStarGrid2D.new()
var spawn_points: Array[Vector3] = []
var materials := {}
var building_origins: Array[Vector3] = []
var lifts: Array[Dictionary] = []
var vehicle_entries: Array[Vector3] = []
var vending_points: Array[Vector3] = []
var collision_only := false
var map_id := "quarry"
var map_size := Vector2(90, 128)
var navigation: ArenaNavigation
var patrol_points: Array[Vector3] = [Vector3(-21, 0.1, -29), Vector3(21, 0.1, 29), Vector3(0, 0.1, 0), Vector3(-11, 7.4, 22.7), Vector3(11, 7.4, -22.6)]
var objective_positions: Array[Vector3] = [Vector3(-21, 0.1, -12), Vector3(0, 0.1, 0), Vector3(21, 0.1, 12)]
const HALF_WIDTH := 45
const HALF_LENGTH := 64
const SPAWN_Z := 45
const ROAD = Color("39474d")
const CONCRETE = Color("b2b7ad")
const TEAL = Color("337f80")
const OCHRE = Color("c59857")
func _ready() -> void:
build()
build_navigation()
func material(color: Color) -> StandardMaterial3D:
if not materials.has(color):
materials[color] = UrbanMaterials.get_surface("concrete", color.lightened(0.25))
return materials[color]
func box(pos: Vector3, size: Vector3, color: Color, solid: bool = true) -> Node3D:
var mesh := MeshInstance3D.new()
var cube := BoxMesh.new()
cube.size = size
mesh.mesh = cube
mesh.material_override = material(color)
if size.y < 0.7 and size.x > 50:
mesh.material_override = UrbanMaterials.get_surface("sand", Color("b6a28a"))
if color == ROAD:
mesh.material_override = UrbanMaterials.get_surface("asphalt", Color("a8aaa7"))
if absf(pos.x) == 39:
mesh.material_override = UrbanMaterials.get_surface("sand", Color("c8b194"))
if size.y == 4.4:
mesh.material_override = UrbanMaterials.get_surface("rock", Color("b1b0a5"))
if not solid and size.y < 0.05 and color != ROAD:
mesh.material_override = UrbanMaterials.get_surface("paint", color)
mesh.visible = not collision_only
mesh.position = pos
add_child(mesh)
if solid:
var body := StaticBody3D.new()
body.collision_layer = 1
body.collision_mask = 0
var shape := CollisionShape3D.new()
var bounds := BoxShape3D.new()
bounds.size = size
shape.shape = bounds
body.add_child(shape)
mesh.add_child(body)
if pos.y + size.y / 2.0 > 0.45:
obstacles.append(AABB(pos - size / 2.0, size))
return mesh
func sign_text(words: String, pos: Vector3, yaw: float, tint: Color = Color.WHITE, font_size: int = 70) -> void:
var label := Label3D.new()
label.text = words
label.font_size = font_size
label.pixel_size = 0.009
label.modulate = tint
label.outline_size = 0
label.position = pos
label.rotation.y = yaw
add_child(label)
func building(x: float, z: float, tint: Color, title: String) -> void:
building_origins.append(Vector3(x, 0, z))
collision_only = true
# Two opposite doors plus a side door: a compact interior is a cross-route.
box(Vector3(x, 0.05, z), Vector3(9, 0.1, 11), CONCRETE)
for end in [-1, 1]:
for side in [-1, 1]:
box(Vector3(x + side * 3.05, 1.7, z + end * 5.5), Vector3(2.9, 3.4, 0.35), tint)
box(Vector3(x, 3.05, z + end * 5.5), Vector3(3.2, 0.7, 0.35), tint)
for side in [-1, 1]:
for end in [-1, 1]:
box(Vector3(x + side * 4.5, 1.7, z + end * 3.8), Vector3(0.35, 3.4, 3.4), tint)
box(Vector3(x + side * 4.5, 3.05, z), Vector3(0.35, 0.7, 4.4), tint)
box(Vector3(x - 2, 0.65, z), Vector3(1.7, 1.3, 2.4), Color("6e7773"))
collision_only = false
if DisplayServer.get_name() != "headless":
add_child(UrbanArchitecture.build(x, z, tint, title))
UrbanFloors.build(self, x, z, tint)
func car(x: float, z: float, tint: Color, long_vehicle: bool = false) -> void:
collision_only = true
var length := 6.5 if long_vehicle else 4.6
box(Vector3(x, 0.65, z), Vector3(2.3, 1.1, length), tint)
box(Vector3(x, 1.45, z - 0.3), Vector3(2.05, 0.9, length * 0.56), Color("2b414b"))
collision_only = false
if DisplayServer.get_name() != "headless":
var model := UrbanVehicles.build(tint, long_vehicle)
model.position = Vector3(x, 0, z)
add_child(model)
func build() -> void:
box(Vector3(0, -0.3, 0), Vector3(HALF_WIDTH * 2 + 2, 0.6, HALF_LENGTH * 2 + 2), Color("7d8a7a"))
for x in [-22, 0, 22]:
box(Vector3(x, 0.012, 0), Vector3(10, 0.025, HALF_LENGTH * 2 - 2), ROAD, false)
for z in [-55, -39, -31, 0, 31, 39, 55]:
box(Vector3(0, 0.018, z), Vector3(HALF_WIDTH * 2 - 2, 0.03, 7), ROAD, false)
for z in range(-43, 45, 5):
box(Vector3(0, 0.04, z), Vector3(0.14, 0.025, 2.4), Color("cebb8b"), false)
for x in [-HALF_WIDTH, HALF_WIDTH]:
box(Vector3(x, 2.2, 0), Vector3(1, 4.4, HALF_LENGTH * 2), Color("718780"))
for z in [-HALF_LENGTH, HALF_LENGTH]:
box(Vector3(0, 2.2, z), Vector3(HALF_WIDTH * 2, 4.4, 1), Color("718780"))
building(-11, -18, TEAL, "RELAY / 01")
building(11, 18, OCHRE, "ATELIER / 04")
building(11, -18, Color("abb8b1"), "NORTH MARKET")
building(-11, 18, Color("bd9d85"), "MOTOR WORKS")
car(-2.5, -6, OCHRE, true)
car(2.8, 11, Color("b8c4bd"))
car(-22, -18, Color("373c37"))
car(23, 19, Color("423d35"))
for pos in [Vector3(-19, 0.7, 12), Vector3(-19, 0.7, -3), Vector3(20, 0.7, -10), Vector3(27, 0.7, 13), Vector3(3, 0.7, -22), Vector3(-3, 0.7, 24)]:
box(pos, Vector3(3.2, 1.4, 1.5), CONCRETE).visible = false
if DisplayServer.get_name() != "headless":
add_child(UrbanCover.build(pos, Vector3(3.2, 1.4, 1.5)))
# Dogleg spawn shields prevent a single straight line across both bases.
for z in [-35, 35]:
box(Vector3(0, 1.9, z), Vector3(13, 3.8, 0.65), TEAL if z > 0 else OCHRE)
for x in [-26, -13, 0, 13, 26]:
spawn_points.append(Vector3(x, 0.15, sign(z) * SPAWN_Z))
spawn_shelter(x, sign(z))
for x in [-14, 14]:
box(Vector3(x, 1.3, z + sign(z) * 2), Vector3(0.55, 2.6, 4.2), CONCRETE)
# Added side courtyards remain connected to the existing lanes.
for x in [-32, 32]:
for z in [-19, 3, 23]:
var pos := Vector3(x, 0.7, z)
box(pos, Vector3(3.2, 1.4, 1.5), CONCRETE).visible = false
if DisplayServer.get_name() != "headless":
add_child(UrbanCover.build(pos, Vector3(3.2, 1.4, 1.5)))
for x in [-23, 23]:
# Reachable platform with two stair approaches, broken side sightlines.
box(Vector3(x, 1.1, 0), Vector3(4, 2.2, 5), Color("657b7c"))
for end in [-1, 1]:
for step in range(11):
var h := (step + 1) * 0.2
box(Vector3(x, h / 2, end * (7.75 - step * 0.5)), Vector3(3, h, 0.5), CONCRETE)
box(Vector3(x + sign(x) * 1.8, 2.75, 0), Vector3(0.35, 1.1, 5), TEAL)
box(Vector3(x - sign(x) * 1.8, 2.6, 0), Vector3(0.35, 0.8, 2), CONCRETE)
WarDistrict.build(self)
QuarryLandscape.build_routes(self)
if DisplayServer.get_name() != "headless":
add_child(UrbanDetails.build())
DepotDetails.build(self)
QuarryLandscape.build(self)
sign_text("RELAY / QUARRY 07", Vector3(0, 3, -HALF_LENGTH + 0.6), 0, Color("f2d8a4"), 110)
sign_text("BLOCKLINE / SOUTH", Vector3(0, 3, HALF_LENGTH - 0.6), PI, Color("bce4de"), 85)
UrbanLighting.build(self)
QuarryFabrication.build(self)
QuarryRealism.apply(self)
func spawn_shelter(x: float, side: float) -> void:
var z := side * SPAWN_Z
# Front baffle + canopy stop elevated fire; both side exits remain open.
box(Vector3(x, 3.3, z), Vector3(9.6, 0.3, 6.6), CONCRETE)
box(Vector3(x, 1.65, z - side * 2.9), Vector3(9.4, 3.3, 0.3), TEAL if side > 0 else OCHRE)
for dx in [-4.55, 4.55]:
box(Vector3(x + dx, 1.65, z - side * 1.65), Vector3(0.3, 3.3, 2.7), CONCRETE)
box(Vector3(x + dx, 1.65, z + side * 2.8), Vector3(0.25, 3.3, 0.25), CONCRETE)
func blocked(pos: Vector3) -> bool:
var clearance := AABB(pos + Vector3(-0.48, -0.95, -0.48), Vector3(0.96, 1.8, 0.96))
for bounds in obstacles:
if bounds.intersects(clearance):
return true
return false
func load_navigation(resource_path: String) -> void:
navigation = ArenaNavigation.new()
add_child(navigation)
navigation.setup(resource_path)
func build_navigation() -> void:
if ResourceLoader.exists("res://assets/navigation/quarry.res"):
load_navigation("res://assets/navigation/quarry.res")
nav.region = Rect2i(-HALF_WIDTH + 1, -HALF_LENGTH + 2, HALF_WIDTH * 2 - 1, HALF_LENGTH * 2 - 3)
nav.cell_size = Vector2.ONE
nav.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_ONLY_IF_NO_OBSTACLES
nav.update()
for x in range(-HALF_WIDTH + 1, HALF_WIDTH):
for z in range(-HALF_LENGTH + 2, HALF_LENGTH - 1):
if blocked(Vector3(x, 1, z)):
nav.set_point_solid(Vector2i(x, z))
func path(from: Vector3, to: Vector3) -> PackedVector2Array:
var a := Vector2i(roundi(from.x), roundi(from.z))
var b := Vector2i(clampi(roundi(to.x), -HALF_WIDTH + 2, HALF_WIDTH - 2), clampi(roundi(to.z), -HALF_LENGTH + 3, HALF_LENGTH - 3))
if not nav.is_in_boundsv(a) or nav.is_point_solid(a) or nav.is_point_solid(b):
return PackedVector2Array()
return nav.get_point_path(a, b)