Top windet sich über das Hochland, Mid quert die Kluft über die Mittelplattform, Bottom führt durch das Tiefland über die Deltabrücke; Top und Bottom sind exakt gleich lang, alles bleibt spiegelsymmetrisch. LuneTerrain liefert ein exakt gespiegeltes Höhenfeld, auf dem Figuren stehen. Bossgrotte als Terrasse auf der Achse zwischen Top und Mid, Aufmarschbereiche je Lane, perspektivische Kamera mit Sprungtasten (F2–F4, B, G, H, O, R, Q/E). Bestandsassets wiederverwendet, neue Formen als Blockout für G2/G4. Geprüft: verify_g1 443/443, Regeln 91/91, M2 73/73, verify_m1 38/38, verify_m2 14/14, Netz 43/43, Loopback 12/12, Balance 40 Partien (Seitentausch 20/20), Legacy 209/463. Vorher/Nachher-Aufnahmen und Messungen in G1-KARTE.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
907 lines
39 KiB
GDScript
907 lines
39 KiB
GDScript
extends Node3D
|
||
## 3D presentation of LuneMap (G1 layout). The ground is LuneTerrain's height field:
|
||
## the north-western highland carries Top, the Kluft river runs on the symmetry axis
|
||
## from the grotto spring under the central Mid platform into the south-eastern delta
|
||
## that Bottom crosses on a bridge. Existing assets are reused: the lane tower, the
|
||
## Saumkern Ancients, the grotto rock arc, map props (trees, rocks, curbs, bridge,
|
||
## banners) and the Kluft surface shader. New shapes are simple blockout primitives
|
||
## that G2/G4 replace. Purely visual; the simulation never reads anything from here.
|
||
|
||
const Lighting = preload("res://scripts/presentation/lighting.gd")
|
||
const Sanctuary = preload("res://scripts/presentation/sanctuary.gd")
|
||
const RiverShader = preload("res://scripts/presentation/river.gdshader")
|
||
const GroundShader = preload("res://scripts/luneraria/view/lune_terrain.gdshader")
|
||
const PadShader = preload("res://scripts/luneraria/view/lune_pad.gdshader")
|
||
const Stone = preload("res://assets/materials/phase10/limestone-albedo.png")
|
||
const TOWER := "res://assets/luneraria/tower.glb"
|
||
const GROTTO_SCENE := "res://assets/luneraria/grotto.glb"
|
||
const PROPS := "res://assets/luneraria/map_props.glb"
|
||
const TEAM_COLORS: Array[Color] = [Color("5aa8ff"), Color("ff6a55")]
|
||
const TEAM_NAMES: Array[String] = ["Blau", "Rot"]
|
||
const GROTTO_COLOR := Color("b37cff")
|
||
## Visible paving half width per lane; Mid is the broad monumental route.
|
||
const PAVING: Dictionary = {"top": 3.0, "mid": 3.9, "bot": 3.0}
|
||
## Lane identity: old grey flagstones on Top, pale limestone on Mid, packed earth on Bottom.
|
||
const PAVING_MATERIAL: Dictionary = {"top": ["ENV_paving_top", Color(.33, .31, .27)], "mid": ["ENV_paving_mid", Color(.42, .39, .33)],
|
||
"bot": ["ENV_soil_bot", Color(.31, .25, .17)]}
|
||
|
||
var map: LuneMap
|
||
var terrain: LuneTerrain
|
||
var sun: DirectionalLight3D
|
||
var building_nodes: Dictionary = {}
|
||
var grotto_light: OmniLight3D
|
||
var _grotto_runes: StandardMaterial3D
|
||
var _materials: Dictionary = {}
|
||
var _crystal_materials: Array[StandardMaterial3D] = []
|
||
var _props: Dictionary = {}
|
||
## "team:lane" -> deployment pad, team -> grotto entrance ring
|
||
var _pads: Dictionary = {}
|
||
var _grotto_pads: Dictionary = {}
|
||
|
||
func build(match_map: LuneMap) -> void:
|
||
map = match_map
|
||
name = "LuneWorld"
|
||
terrain = LuneTerrain.for_map(map)
|
||
_environment()
|
||
_ground()
|
||
_water()
|
||
_load_props()
|
||
for lane: String in LuneMap.LANES:
|
||
_paving(lane)
|
||
_central_platform()
|
||
_delta_bridge()
|
||
_curbs()
|
||
for team: int in range(2):
|
||
_base(team)
|
||
_landmarks()
|
||
_vegetation()
|
||
for team: int in range(2):
|
||
var crystal := StandardMaterial3D.new()
|
||
crystal.albedo_color = TEAM_COLORS[team]
|
||
crystal.emission_enabled = true
|
||
crystal.emission = TEAM_COLORS[team]
|
||
crystal.emission_energy_multiplier = 2.2
|
||
crystal.roughness = .15
|
||
_crystal_materials.append(crystal)
|
||
for definition: Dictionary in map.buildings:
|
||
_building(definition)
|
||
_grotto()
|
||
_deployment_pads()
|
||
|
||
# ---------------------------------------------------------------- helpers
|
||
## Height a unit or building stands on (lane decks instead of the river bed).
|
||
func height_at(p: Vector2) -> float:
|
||
return terrain.walk_height(p)
|
||
|
||
func to3(v: Vector2, lift: float = 0.0) -> Vector3:
|
||
return Vector3(v.x, terrain.walk_height(v) + lift, v.y)
|
||
|
||
func _ground3(v: Vector2, lift: float = 0.0) -> Vector3:
|
||
return Vector3(v.x, terrain.height_at(v) + lift, v.y)
|
||
|
||
## Kluft surface material for ENV_* keys (flags come from the key: paving, soil, leaf, bark).
|
||
func surface(key: String, color: Color) -> ShaderMaterial:
|
||
if not _materials.has(key):
|
||
var source := StandardMaterial3D.new()
|
||
source.resource_name = key
|
||
source.albedo_color = color
|
||
_materials[key] = Sanctuary.terrain_material(source)
|
||
return _materials[key]
|
||
|
||
static func _glow(color: Color, energy: float) -> StandardMaterial3D:
|
||
var material := StandardMaterial3D.new()
|
||
material.albedo_color = color
|
||
material.emission_enabled = true
|
||
material.emission = color
|
||
material.emission_energy_multiplier = energy
|
||
material.roughness = .3
|
||
return material
|
||
|
||
func _multi(mesh: Mesh, material: Material, transforms: Array[Transform3D], label: String, shadows: bool = true) -> MultiMeshInstance3D:
|
||
var multimesh := MultiMesh.new()
|
||
multimesh.transform_format = MultiMesh.TRANSFORM_3D
|
||
multimesh.mesh = mesh
|
||
multimesh.instance_count = transforms.size()
|
||
for index: int in range(transforms.size()):
|
||
multimesh.set_instance_transform(index, transforms[index])
|
||
var instance := MultiMeshInstance3D.new()
|
||
instance.name = label
|
||
instance.multimesh = multimesh
|
||
if material != null:
|
||
instance.material_override = material
|
||
instance.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_ON if shadows else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||
add_child(instance)
|
||
return instance
|
||
|
||
func _prop_multi(prop: String, transforms: Array[Transform3D], shadows: bool = true) -> void:
|
||
if not transforms.is_empty():
|
||
_multi(_props[prop], null, transforms, "Props_" + prop, shadows)
|
||
|
||
static func _yaw_basis(yaw: float, scale: Vector3 = Vector3.ONE) -> Basis:
|
||
return Basis(Vector3.UP, yaw) * Basis.from_scale(scale)
|
||
|
||
## Yaw that turns a model's +Z towards the given ground direction.
|
||
static func _facing(direction: Vector2) -> float:
|
||
return atan2(direction.x, direction.y)
|
||
|
||
# ---------------------------------------------------------------- environment and ground
|
||
func _environment() -> void:
|
||
var environment := WorldEnvironment.new()
|
||
var settings := Environment.new()
|
||
Lighting.configure(settings)
|
||
# The camera sits 40–170 m away; distance fog would wash out the board (G5 revisits).
|
||
settings.fog_enabled = false
|
||
environment.environment = settings
|
||
add_child(environment)
|
||
sun = DirectionalLight3D.new()
|
||
sun.rotation_degrees = Vector3(-55, -30, 0)
|
||
sun.light_color = Color("ffe2bc")
|
||
sun.light_energy = 1.7
|
||
sun.shadow_enabled = true
|
||
sun.directional_shadow_mode = DirectionalLight3D.SHADOW_PARALLEL_4_SPLITS
|
||
sun.directional_shadow_max_distance = 190.0
|
||
sun.shadow_blur = 1.6
|
||
add_child(sun)
|
||
var fill := DirectionalLight3D.new()
|
||
fill.rotation_degrees = Vector3(-35, 150, 0)
|
||
fill.light_color = Color("a8c4e8")
|
||
fill.light_energy = .45
|
||
add_child(fill)
|
||
|
||
func _ground() -> void:
|
||
var material := ShaderMaterial.new()
|
||
material.shader = GroundShader
|
||
material.set_shader_parameter("stone_map", Stone)
|
||
var ground := MeshInstance3D.new()
|
||
ground.name = "Ground"
|
||
ground.mesh = terrain.inner_mesh()
|
||
ground.material_override = material
|
||
add_child(ground)
|
||
var outer := MeshInstance3D.new()
|
||
outer.name = "Mountains"
|
||
outer.mesh = terrain.outer_mesh()
|
||
outer.material_override = material
|
||
# The distant ring lies outside the board; its shadows would only cost shadow-map time.
|
||
outer.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||
add_child(outer)
|
||
|
||
func _water() -> void:
|
||
var water_material := ShaderMaterial.new()
|
||
water_material.shader = RiverShader
|
||
var water := MeshInstance3D.new()
|
||
water.name = "Water"
|
||
var plane := PlaneMesh.new()
|
||
plane.size = Vector2(LuneTerrain.OUTER_EXTENT * 2.0, LuneTerrain.OUTER_EXTENT * 2.0)
|
||
water.mesh = plane
|
||
water.position.y = LuneTerrain.WATER_LEVEL
|
||
water.material_override = water_material
|
||
water.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||
add_child(water)
|
||
# The spring leaves the grotto terrace between the two stairs as a short waterfall.
|
||
var top: Vector2 = LuneMap.GROTTO + LuneMap.AXIS * (LuneTerrain.ARENA_RADIUS - 0.5)
|
||
var bottom: Vector2 = LuneMap.GROTTO + LuneMap.AXIS * (LuneTerrain.ARENA_RADIUS + 1.7)
|
||
var across: Vector2 = LuneMap.BLUE_SIDE * .9
|
||
var corners: Array[Vector3] = [Vector3(top.x + across.x, LuneTerrain.ARENA_HEIGHT - 0.05, top.y + across.y),
|
||
Vector3(top.x - across.x, LuneTerrain.ARENA_HEIGHT - 0.05, top.y - across.y),
|
||
Vector3(bottom.x - across.x, LuneTerrain.WATER_LEVEL + .02, bottom.y - across.y),
|
||
Vector3(bottom.x + across.x, LuneTerrain.WATER_LEVEL + .02, bottom.y + across.y)]
|
||
var tool := SurfaceTool.new()
|
||
tool.begin(Mesh.PRIMITIVE_TRIANGLES)
|
||
# Both windings, so the fall is visible from every camera angle.
|
||
for index: int in [0, 1, 2, 0, 2, 3, 0, 2, 1, 0, 3, 2]:
|
||
tool.add_vertex(corners[index])
|
||
tool.generate_normals()
|
||
var fall := MeshInstance3D.new()
|
||
fall.name = "SpringFall"
|
||
fall.mesh = tool.commit()
|
||
var foam := StandardMaterial3D.new()
|
||
foam.albedo_color = Color(.72, .86, .9, .82)
|
||
foam.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||
foam.emission_enabled = true
|
||
foam.emission = Color(.35, .5, .55)
|
||
foam.emission_energy_multiplier = .35
|
||
foam.roughness = .2
|
||
fall.material_override = foam
|
||
fall.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||
add_child(fall)
|
||
|
||
## Shadows cover what the camera shows: close views keep the shadow map dense and cheap,
|
||
## the overview reaches the whole board.
|
||
func fit_shadows(camera_distance: float) -> void:
|
||
sun.directional_shadow_max_distance = clampf(camera_distance * 1.3 + 25.0, 70.0, 220.0)
|
||
|
||
# ---------------------------------------------------------------- lanes
|
||
## Lane sections that a deck replaces: the central platform on Mid, the delta bridge on Bottom.
|
||
func _deck_gap(lane: String, point: Vector2) -> bool:
|
||
if lane == "mid":
|
||
return point.length() < LuneTerrain.PLATFORM_RADIUS - 0.4
|
||
if lane == "bot":
|
||
return point.distance_to(delta_point()) < 7.2
|
||
return false
|
||
|
||
## Where Bottom crosses the Kluft delta (its midpoint, on the symmetry axis).
|
||
func delta_point() -> Vector2:
|
||
var path: LuneMap.LanePath = map.lanes["bot"]
|
||
return path.point_at(path.length * 0.5)
|
||
|
||
func _paving(lane: String) -> void:
|
||
var path: LuneMap.LanePath = map.lanes[lane]
|
||
var half: float = PAVING[lane]
|
||
var tool := SurfaceTool.new()
|
||
tool.begin(Mesh.PRIMITIVE_TRIANGLES)
|
||
var steps: int = int(ceil(path.length / 0.8))
|
||
var previous: Array[Vector3] = []
|
||
var previous_gap: bool = true
|
||
for i: int in range(steps + 1):
|
||
var s: float = path.length * float(i) / steps
|
||
var point: Vector2 = path.point_at(s)
|
||
var tangent: Vector2 = (path.point_at(minf(path.length, s + .6)) - path.point_at(maxf(0.0, s - .6))).normalized()
|
||
var normal := Vector2(-tangent.y, tangent.x)
|
||
var y: float = terrain.lane_height(lane, s) + 0.05
|
||
# Outer shoulder, paving edge, paving edge, outer shoulder (a bevel into the ground).
|
||
var section: Array[Vector3] = []
|
||
for offset: Array in [[half + .45, -.17], [half, 0.0], [-half, 0.0], [-half - .45, -.17]]:
|
||
var at: Vector2 = point + normal * float(offset[0])
|
||
section.append(Vector3(at.x, y + float(offset[1]), at.y))
|
||
var gap: bool = _deck_gap(lane, point)
|
||
if i > 0 and not gap and not previous_gap:
|
||
for k: int in range(3):
|
||
for vertex: Vector3 in [previous[k], section[k + 1], section[k], previous[k], previous[k + 1], section[k + 1]]:
|
||
tool.add_vertex(vertex)
|
||
previous = section
|
||
previous_gap = gap
|
||
tool.generate_normals()
|
||
var ribbon := MeshInstance3D.new()
|
||
ribbon.name = "Paving_" + lane
|
||
ribbon.mesh = tool.commit()
|
||
var spec: Array = PAVING_MATERIAL[lane]
|
||
ribbon.material_override = surface(spec[0], spec[1])
|
||
ribbon.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||
add_child(ribbon)
|
||
|
||
## Mid crosses the Kluft on a round platform: pier, parapet, rune circle, four standing stones.
|
||
func _central_platform() -> void:
|
||
var y: float = LuneTerrain.PLATFORM_HEIGHT
|
||
var pier := MeshInstance3D.new()
|
||
pier.name = "CentralPlatform"
|
||
var cylinder := CylinderMesh.new()
|
||
cylinder.top_radius = LuneTerrain.PLATFORM_RADIUS
|
||
cylinder.bottom_radius = LuneTerrain.PLATFORM_RADIUS + .7
|
||
cylinder.height = 2.6
|
||
cylinder.radial_segments = 56
|
||
pier.mesh = cylinder
|
||
pier.position = Vector3(0, y + .05 - 1.3, 0)
|
||
pier.material_override = surface("ENV_limestone_platform", Color(.56, .52, .46))
|
||
add_child(pier)
|
||
var inlay := MeshInstance3D.new()
|
||
var disc := CylinderMesh.new()
|
||
disc.top_radius = LuneTerrain.PLATFORM_RADIUS - 1.0
|
||
disc.bottom_radius = disc.top_radius
|
||
disc.height = .04
|
||
disc.radial_segments = 56
|
||
inlay.mesh = disc
|
||
inlay.position = Vector3(0, y + .07, 0)
|
||
inlay.material_override = surface("ENV_paving_plaza", Color(.6, .56, .49))
|
||
add_child(inlay)
|
||
var ring := MeshInstance3D.new()
|
||
var torus := TorusMesh.new()
|
||
torus.inner_radius = 3.25
|
||
torus.outer_radius = 3.5
|
||
torus.rings = 64
|
||
ring.mesh = torus
|
||
ring.scale = Vector3(1, .12, 1)
|
||
ring.position = Vector3(0, y + .1, 0)
|
||
ring.material_override = _glow(Color("d9ccff"), .9)
|
||
ring.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||
add_child(ring)
|
||
var parapet := MeshInstance3D.new()
|
||
var rim := TorusMesh.new()
|
||
rim.inner_radius = LuneTerrain.PLATFORM_RADIUS - .35
|
||
rim.outer_radius = LuneTerrain.PLATFORM_RADIUS + .05
|
||
rim.rings = 64
|
||
parapet.mesh = rim
|
||
parapet.scale = Vector3(1, .9, 1)
|
||
parapet.position = Vector3(0, y + .15, 0)
|
||
parapet.material_override = surface("ENV_limestone_parapet", Color(.5, .47, .41))
|
||
add_child(parapet)
|
||
var stones: Array[Transform3D] = []
|
||
for direction: Vector2 in [Vector2(1, 0), Vector2(0, 1), Vector2(-1, 0), Vector2(0, -1)]:
|
||
var at: Vector2 = direction * 5.3
|
||
var lean: float = .05 if direction.x + direction.y > 0.0 else -.05
|
||
stones.append(Transform3D(_yaw_basis(_facing(-direction)) * Basis(Vector3.RIGHT, lean), Vector3(at.x, y + 1.75, at.y)))
|
||
var slab := BoxMesh.new()
|
||
slab.size = Vector3(1.0, 3.6, .62)
|
||
_multi(slab, surface("ENV_cliff_runestone", Color(.44, .42, .45)), stones, "RuneStones")
|
||
var runes: Array[Transform3D] = []
|
||
for stone: Transform3D in stones:
|
||
runes.append(Transform3D(stone.basis, stone.origin + stone.basis.z * .32 + Vector3(0, .5, 0)))
|
||
var rune := BoxMesh.new()
|
||
rune.size = Vector3(.34, 1.5, .04)
|
||
_multi(rune, _glow(Color("cdb8ff"), 1.3), runes, "RuneGlyphs", false)
|
||
|
||
## Bottom crosses the delta on the existing bridge prop, stretched to span the wider water.
|
||
func _delta_bridge() -> void:
|
||
var at: Vector2 = delta_point()
|
||
var path: LuneMap.LanePath = map.lanes["bot"]
|
||
var deck: float = terrain.lane_height("bot", path.length * 0.5) + 0.05
|
||
var bridge := MeshInstance3D.new()
|
||
bridge.name = "DeltaBridge"
|
||
bridge.mesh = _props["Bridge"]
|
||
# The prop's deck surface lies 0.10 m above its origin.
|
||
bridge.transform = Transform3D(_yaw_basis(PI * .25, Vector3(1.34, 1.0, 1.0)), Vector3(at.x, deck - 0.1, at.y))
|
||
add_child(bridge)
|
||
|
||
## Curb stones along Top and Mid, skipped on decks, at bases and around towers.
|
||
func _curbs() -> void:
|
||
var transforms: Array[Transform3D] = []
|
||
for lane: String in ["top", "mid"]:
|
||
var path: LuneMap.LanePath = map.lanes[lane]
|
||
var offset: float = float(PAVING[lane]) + .45
|
||
var s: float = 0.0
|
||
while s < path.length:
|
||
var sampled: Array = path.sample(s)
|
||
var tangent: Vector2 = sampled[1]
|
||
var normal := Vector2(-tangent.y, tangent.x)
|
||
for side: float in [-1.0, 1.0]:
|
||
var at: Vector2 = sampled[0] + normal * offset * side
|
||
var clear: bool = not _deck_gap(lane, at) and at.length() > LuneTerrain.PLATFORM_RADIUS + .6
|
||
for team: int in range(2):
|
||
clear = clear and map.core_position(team).distance_to(at) > LuneTerrain.BASE_RADIUS + 0.8
|
||
for definition: Dictionary in map.buildings:
|
||
clear = clear and Vector2(definition.position).distance_to(at) > 2.3
|
||
if clear:
|
||
transforms.append(Transform3D(_yaw_basis(atan2(-tangent.y, tangent.x)), Vector3(at.x, terrain.lane_height(lane, s) - .06, at.y)))
|
||
s += 1.55
|
||
_prop_multi("Curb", transforms, false)
|
||
|
||
# ---------------------------------------------------------------- bases
|
||
## Plateau plaza with the team rim, a ring wall with three lane gates and gate crystals.
|
||
func _base(team: int) -> void:
|
||
var core: Vector2 = map.core_position(team)
|
||
var y: float = LuneTerrain.BASE_HEIGHT
|
||
var plaza := MeshInstance3D.new()
|
||
plaza.name = "BasePlaza_%d" % team
|
||
var disc := CylinderMesh.new()
|
||
disc.top_radius = 8.4
|
||
disc.bottom_radius = 8.7
|
||
disc.height = .12
|
||
disc.radial_segments = 56
|
||
plaza.mesh = disc
|
||
plaza.position = Vector3(core.x, y + .02, core.y)
|
||
plaza.material_override = surface("ENV_limestone_plaza", Color(.52, .48, .4))
|
||
add_child(plaza)
|
||
var rim := MeshInstance3D.new()
|
||
var ring := TorusMesh.new()
|
||
ring.inner_radius = 8.25
|
||
ring.outer_radius = 8.5
|
||
ring.rings = 64
|
||
rim.mesh = ring
|
||
rim.material_override = _glow(TEAM_COLORS[team], 1.2)
|
||
rim.position = Vector3(core.x, y + .1, core.y)
|
||
rim.scale = Vector3(1, .3, 1)
|
||
rim.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||
add_child(rim)
|
||
var gates: Array[float] = []
|
||
for lane: String in LuneMap.LANES:
|
||
var start: Vector2 = map.lane_point(team, lane, 0.0)
|
||
gates.append(atan2(start.y - core.y, start.x - core.x))
|
||
var walls: Array[Transform3D] = []
|
||
var pillars: Array[Transform3D] = []
|
||
var gems: Array[Transform3D] = []
|
||
var radius: float = LuneTerrain.BASE_RADIUS - .2
|
||
var segments: int = 40
|
||
for index: int in range(segments):
|
||
var angle: float = TAU * index / segments
|
||
var near_gate: float = PI
|
||
for gate: float in gates:
|
||
near_gate = minf(near_gate, absf(wrapf(angle - gate, -PI, PI)))
|
||
var at: Vector2 = core + Vector2(cos(angle), sin(angle)) * radius
|
||
if near_gate < deg_to_rad(15.0):
|
||
continue
|
||
if near_gate < deg_to_rad(24.0):
|
||
pillars.append(Transform3D(Basis.IDENTITY, Vector3(at.x, y + 1.7, at.y)))
|
||
gems.append(Transform3D(Basis(Vector3.UP, angle), Vector3(at.x, y + 3.75, at.y)))
|
||
continue
|
||
walls.append(Transform3D(_yaw_basis(-angle + PI * .5), Vector3(at.x, y + .5, at.y)))
|
||
var wall := BoxMesh.new()
|
||
wall.size = Vector3(1.72, 1.2, .7)
|
||
_multi(wall, surface("ENV_limestone_wall", Color(.5, .47, .42)), walls, "BaseWall_%d" % team)
|
||
var pillar := CylinderMesh.new()
|
||
pillar.top_radius = .42
|
||
pillar.bottom_radius = .55
|
||
pillar.height = 3.4
|
||
pillar.radial_segments = 10
|
||
_multi(pillar, surface("ENV_limestone_gate", Color(.55, .51, .45)), pillars, "BaseGates_%d" % team)
|
||
var gem := SphereMesh.new()
|
||
gem.radius = .45
|
||
gem.height = 1.1
|
||
gem.radial_segments = 6
|
||
gem.rings = 2
|
||
_multi(gem, _glow(TEAM_COLORS[team], 2.4), gems, "GateCrystals_%d" % team, false)
|
||
|
||
# ---------------------------------------------------------------- landmarks (blockout)
|
||
## Top: a ruined lookout on the highland corner, an old gate and broken watch walls.
|
||
## Bottom: overgrown wall stubs in the lowland. Every piece is mirrored for Rot.
|
||
func _landmarks() -> void:
|
||
var walls: Array[Transform3D] = []
|
||
var columns: Array[Transform3D] = []
|
||
for team: int in range(2):
|
||
# Old gate where Top reaches the highland.
|
||
for side: float in [-1.0, 1.0]:
|
||
var post: Vector2 = map.lane_point(team, "top", 36.0, side * 4.7)
|
||
columns.append(Transform3D(Basis.from_scale(Vector3(1, 1.25, 1)), _ground3(post, 1.4)))
|
||
# Broken watch walls on the outer edge of the highland path.
|
||
for spec: Vector3 in [Vector3(29.0, -6.2, 3.4), Vector3(41.5, -6.4, 2.6), Vector3(45.0, 5.8, 2.0)]:
|
||
var at: Vector2 = map.lane_point(team, "top", spec.x, spec.y)
|
||
var along: Vector2 = map.lane_forward(team, "top", spec.x)
|
||
walls.append(Transform3D(_yaw_basis(atan2(-along.y, along.x), Vector3(spec.z, 1.0, 1.0)), _ground3(at, .45)))
|
||
# Overgrown stubs beside Bottom in the lowland.
|
||
for spec: Vector3 in [Vector3(24.0, 6.4, 2.2), Vector3(39.0, -6.3, 1.6)]:
|
||
var at: Vector2 = map.lane_point(team, "bot", spec.x, spec.y)
|
||
var along: Vector2 = map.lane_forward(team, "bot", spec.x)
|
||
walls.append(Transform3D(_yaw_basis(atan2(-along.y, along.x) + .3, Vector3(spec.z, .7, 1.0)), _ground3(at, .2)))
|
||
var block := BoxMesh.new()
|
||
block.size = Vector3(1.0, 1.1, .7)
|
||
_multi(block, surface("ENV_limestone_ruin", Color(.5, .48, .44)), walls, "RuinWalls")
|
||
var column := CylinderMesh.new()
|
||
column.top_radius = .38
|
||
column.bottom_radius = .48
|
||
column.height = 2.4
|
||
column.radial_segments = 10
|
||
_multi(column, surface("ENV_limestone_ruin", Color(.5, .48, .44)), columns, "RuinColumns")
|
||
# The lookout on the Top corner hill, on the symmetry axis.
|
||
var lookout := Vector2(-31.0, -31.0)
|
||
var tower := MeshInstance3D.new()
|
||
tower.name = "Lookout"
|
||
var shaft := CylinderMesh.new()
|
||
shaft.top_radius = 2.0
|
||
shaft.bottom_radius = 2.6
|
||
shaft.height = 4.6
|
||
shaft.radial_segments = 14
|
||
tower.mesh = shaft
|
||
tower.position = _ground3(lookout, 2.1)
|
||
tower.material_override = surface("ENV_limestone_ruin", Color(.5, .48, .44))
|
||
add_child(tower)
|
||
var crown := MeshInstance3D.new()
|
||
var broken := CylinderMesh.new()
|
||
broken.top_radius = 1.6
|
||
broken.bottom_radius = 2.0
|
||
broken.height = 1.4
|
||
broken.radial_segments = 7
|
||
crown.mesh = broken
|
||
crown.position = _ground3(lookout, 5.0)
|
||
crown.rotation = Vector3(.12, .4, -.12)
|
||
crown.material_override = tower.material_override
|
||
add_child(crown)
|
||
|
||
# ---------------------------------------------------------------- vegetation
|
||
## Clusters, not scatter: a noise field decides where forests stand; dense cores carry
|
||
## the big trees, edges the small ones and shrubs, clearings stay open. The species
|
||
## follow the zone (highland pines, valley mixed wood, lowland oaks and reeds, mountain
|
||
## pines). The Blau side is generated and mirrored, so decoration never favours a team.
|
||
func _vegetation() -> void:
|
||
var noise := FastNoiseLite.new()
|
||
noise.seed = 1203
|
||
noise.frequency = .055
|
||
noise.fractal_octaves = 2
|
||
var rng := RandomNumberGenerator.new()
|
||
rng.seed = 90210
|
||
var buckets: Dictionary = {}
|
||
var add := func(kind: String, p: Vector2, size: float, yaw: float, squash: Vector3 = Vector3.ONE) -> void:
|
||
if not buckets.has(kind):
|
||
buckets[kind] = [] as Array[Transform3D]
|
||
for mirrored: bool in [false, true]:
|
||
var at: Vector2 = LuneMap.mirror(p) if mirrored else p
|
||
var basis: Basis = _yaw_basis(-yaw if mirrored else yaw, squash * size)
|
||
(buckets[kind] as Array[Transform3D]).append(Transform3D(basis, _ground3(at, -.06)))
|
||
var spacing: float = 2.25
|
||
var x: float = -50.0
|
||
while x <= 50.0:
|
||
var z: float = -50.0
|
||
while z <= 50.0:
|
||
var p := Vector2(x + rng.randf_range(-.95, .95), z + rng.randf_range(-.95, .95))
|
||
var roll: float = rng.randf()
|
||
var yaw: float = rng.randf() * TAU
|
||
var jitter: float = rng.randf_range(.85, 1.2)
|
||
z += spacing
|
||
if p.y <= p.x or not _plantable(p):
|
||
continue
|
||
var density: float = noise.get_noise_2d(p.x, p.y) * .5 + .5
|
||
var u: float = LuneTerrain.axis_u(p)
|
||
var a: float = LuneTerrain.axis_a(p)
|
||
var edge: float = terrain.escarpment(a)
|
||
var border: float = pow(pow(absf(p.x), 5.0) + pow(absf(p.y), 5.0), .2)
|
||
if border > 39.5:
|
||
if density > .56 and roll < .55:
|
||
add.call(["Pine_0", "Pine_1", "Pine_2"][int(roll * 6.0) % 3], p, jitter * 1.15, yaw)
|
||
elif roll > .93:
|
||
add.call("Rock_1", p, jitter * 1.6, yaw)
|
||
continue
|
||
if absf(u - edge) < 1.9:
|
||
if roll < .35:
|
||
add.call(["Rock_0", "Rock_1", "Rock_2"][int(roll * 9.0) % 3], p, jitter * 1.35, yaw)
|
||
continue
|
||
if density < .43:
|
||
if roll > .965:
|
||
add.call("Rock_2", p, jitter, yaw)
|
||
continue
|
||
var core: bool = density > .62
|
||
if u < edge:
|
||
if core or roll < .6:
|
||
add.call(["Pine_0", "Pine_1", "Pine_2"][int(roll * 7.0) % 3], p, jitter * (1.25 if core else .95), yaw)
|
||
elif roll < .82:
|
||
add.call("Rock_0", p, jitter, yaw)
|
||
elif u < 20.0:
|
||
if core:
|
||
add.call(["Oak_0", "Oak_1", "Pine_1"][int(roll * 7.0) % 3], p, jitter * 1.2, yaw)
|
||
elif roll < .45:
|
||
add.call(["Oak_2", "Pine_2"][int(roll * 5.0) % 2], p, jitter * .9, yaw)
|
||
elif roll < .8:
|
||
add.call(["Shrub_0", "Shrub_1"][int(roll * 5.0) % 2], p, jitter, yaw)
|
||
else:
|
||
if core:
|
||
add.call(["Oak_0", "Oak_1", "Oak_2"][int(roll * 7.0) % 3], p, jitter * 1.1, yaw)
|
||
elif roll < .55:
|
||
add.call(["Shrub_0", "Shrub_1"][int(roll * 5.0) % 2], p, jitter * 1.1, yaw)
|
||
elif roll < .75:
|
||
add.call("Shrub_0", p, jitter * .8, yaw, Vector3(.5, 1.9, .5))
|
||
x += spacing
|
||
# River banks: stones along the Kluft, reeds in the lowland.
|
||
var u_bank: float = -8.0
|
||
while u_bank < 44.0:
|
||
var a_bank: float = terrain.river_half_width(u_bank) + .5
|
||
var p: Vector2 = LuneMap.AXIS * u_bank + LuneMap.BLUE_SIDE * a_bank
|
||
var roll: float = rng.randf()
|
||
if _bank_clear(p):
|
||
if u_bank > 18.0 and roll < .6:
|
||
add.call("Shrub_0", p, rng.randf_range(.7, 1.0), rng.randf() * TAU, Vector3(.45, 2.0, .45))
|
||
elif roll < .7:
|
||
add.call(["Rock_0", "Rock_2"][int(roll * 4.0) % 2], p, rng.randf_range(.7, 1.05), rng.randf() * TAU)
|
||
u_bank += rng.randf_range(2.0, 3.4)
|
||
for kind: String in buckets:
|
||
_prop_multi(kind, buckets[kind], not kind.begins_with("Shrub"))
|
||
|
||
## Keeps lanes, bases, towers, the grotto, the platform, the river and open water clear.
|
||
func _plantable(p: Vector2) -> bool:
|
||
if maxf(absf(p.x), absf(p.y)) > 50.0:
|
||
return false
|
||
var lane: Array = terrain.nearest_lane(p)
|
||
if float(lane[0]) < float(LuneTerrain.CORRIDOR.get(lane[1], 3.6)) + 1.9:
|
||
return false
|
||
for team: int in range(2):
|
||
if p.distance_to(map.core_position(team)) < LuneTerrain.BASE_RADIUS + 1.8:
|
||
return false
|
||
if p.distance_to(LuneMap.GROTTO) < LuneTerrain.ARENA_RADIUS + 4.6 or p.length() < LuneTerrain.PLATFORM_RADIUS + 3.0:
|
||
return false
|
||
if p.distance_to(Vector2(-31.0, -31.0)) < 4.2:
|
||
return false
|
||
var u: float = LuneTerrain.axis_u(p)
|
||
if LuneTerrain.axis_a(p) < terrain.river_half_width(u) + 1.6:
|
||
return false
|
||
if terrain.height_at(p) < LuneTerrain.WATER_LEVEL + .12:
|
||
return false
|
||
for definition: Dictionary in map.buildings:
|
||
if Vector2(definition.position).distance_to(p) < 4.2:
|
||
return false
|
||
return true
|
||
|
||
func _bank_clear(p: Vector2) -> bool:
|
||
if p.length() < LuneTerrain.PLATFORM_RADIUS + 1.5 or p.distance_to(delta_point()) < 8.5:
|
||
return false
|
||
var lane: Array = terrain.nearest_lane(p)
|
||
return float(lane[0]) > float(LuneTerrain.CORRIDOR.get(lane[1], 3.6)) + .8
|
||
|
||
# ---------------------------------------------------------------- props
|
||
var _vertex_colored: StandardMaterial3D
|
||
|
||
## Prop meshes from the Blender generator, with the Kluft shaders applied per surface.
|
||
func _load_props() -> void:
|
||
var scene: Node3D = (load(PROPS) as PackedScene).instantiate()
|
||
for child: Node in scene.get_children():
|
||
if child is MeshInstance3D:
|
||
var mesh: ArrayMesh = (child as MeshInstance3D).mesh.duplicate()
|
||
for index: int in range(mesh.get_surface_count()):
|
||
var source := mesh.surface_get_material(index) as StandardMaterial3D
|
||
if source != null and source.resource_name.begins_with("ENV_") and not source.resource_name.contains("brass"):
|
||
mesh.surface_set_material(index, surface(source.resource_name, source.albedo_color))
|
||
_props[str(child.name)] = mesh
|
||
scene.free()
|
||
|
||
func _dress(node: Node) -> void:
|
||
if node is MeshInstance3D:
|
||
for index: int in range(node.mesh.get_surface_count()):
|
||
var source := node.mesh.surface_get_material(index) as StandardMaterial3D
|
||
if source != null and source.resource_name.begins_with("ENV_") and not source.resource_name.contains("brass"):
|
||
node.set_surface_override_material(index, surface(source.resource_name, source.albedo_color))
|
||
elif source != null and source.resource_name.begins_with("CV_"):
|
||
# Compare-pack props carry their colours as vertex colours.
|
||
if _vertex_colored == null:
|
||
_vertex_colored = StandardMaterial3D.new()
|
||
_vertex_colored.vertex_color_use_as_albedo = true
|
||
_vertex_colored.roughness = .86
|
||
node.set_surface_override_material(index, _vertex_colored)
|
||
for child: Node in node.get_children():
|
||
_dress(child)
|
||
|
||
# ---------------------------------------------------------------- grotto
|
||
## Blockout of the boss grotto: the terrace arena in front of the escarpment, the rock
|
||
## arc as cave mouth, a ring of pillars around the back, cliffs for the silhouette,
|
||
## one stair per team up to its entrance and the spring falling towards Mid.
|
||
func _grotto() -> void:
|
||
var center: Vector2 = LuneMap.GROTTO
|
||
var y: float = LuneTerrain.ARENA_HEIGHT
|
||
var arena := MeshInstance3D.new()
|
||
arena.name = "GrottoArena"
|
||
var disc := CylinderMesh.new()
|
||
disc.top_radius = LuneTerrain.ARENA_RADIUS - .25
|
||
disc.bottom_radius = LuneTerrain.ARENA_RADIUS
|
||
disc.height = .1
|
||
disc.radial_segments = 56
|
||
arena.mesh = disc
|
||
arena.position = Vector3(center.x, y + .02, center.y)
|
||
arena.material_override = surface("ENV_paving_arena", Color(.4, .37, .42))
|
||
add_child(arena)
|
||
_grotto_runes = _glow(GROTTO_COLOR, 1.0)
|
||
var runes := MeshInstance3D.new()
|
||
var ring := TorusMesh.new()
|
||
ring.inner_radius = 4.3
|
||
ring.outer_radius = 4.6
|
||
ring.rings = 64
|
||
runes.mesh = ring
|
||
runes.scale = Vector3(1, .12, 1)
|
||
runes.position = Vector3(center.x, y + .09, center.y)
|
||
runes.material_override = _grotto_runes
|
||
runes.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||
add_child(runes)
|
||
var cave := MeshInstance3D.new()
|
||
cave.name = "BossGrotto"
|
||
cave.mesh = _cave_mesh()
|
||
cave.scale = Vector3.ONE * 1.05
|
||
cave.position = Vector3(center.x, y - .15, center.y) - Vector3(LuneMap.AXIS.x, 0, LuneMap.AXIS.y) * 1.2
|
||
_dress(cave)
|
||
add_child(cave)
|
||
var pillars: Array[Transform3D] = []
|
||
var caps: Array[Transform3D] = []
|
||
# Pillars around the back half; the front towards Mid stays open for stairs and spring.
|
||
var back: float = atan2(-LuneMap.AXIS.y, -LuneMap.AXIS.x)
|
||
for index: int in range(11):
|
||
var angle: float = back + deg_to_rad(-120.0 + 24.0 * index)
|
||
var at: Vector2 = center + Vector2(cos(angle), sin(angle)) * (LuneTerrain.ARENA_RADIUS - .15)
|
||
pillars.append(Transform3D(Basis.IDENTITY, Vector3(at.x, y + 1.6, at.y)))
|
||
caps.append(Transform3D(Basis(Vector3.UP, angle), Vector3(at.x, y + 3.45, at.y)))
|
||
var pillar := CylinderMesh.new()
|
||
pillar.top_radius = .36
|
||
pillar.bottom_radius = .46
|
||
pillar.height = 3.2
|
||
pillar.radial_segments = 10
|
||
_multi(pillar, surface("ENV_limestone_grotto", Color(.46, .43, .47)), pillars, "GrottoPillars")
|
||
var cap := SphereMesh.new()
|
||
cap.radius = .34
|
||
cap.height = .9
|
||
cap.radial_segments = 6
|
||
cap.rings = 2
|
||
_multi(cap, _grotto_runes, caps, "GrottoCrystals", false)
|
||
# Cliffs behind the arena give the grotto its silhouette from the game camera; any
|
||
# piece that would reach into a lane corridor is left out.
|
||
var cliffs: Array[Transform3D] = []
|
||
for step: int in range(-1, 2):
|
||
var angle: float = back + step * deg_to_rad(38.0)
|
||
var at: Vector2 = center + Vector2(cos(angle), sin(angle)) * 9.6
|
||
var inward: Vector2 = (center - at).normalized()
|
||
if _clear_of_lanes(at, 5.2):
|
||
cliffs.append(Transform3D(_yaw_basis(_facing(inward), Vector3(.62, 2.2 - absf(step) * .35, 1.1)), _ground3(at, -1.3)))
|
||
_prop_multi("EdgeCliff", cliffs)
|
||
var rocks: Array[Transform3D] = []
|
||
for spec: Vector3 in [Vector3(-9.0, 5.8, 2.2), Vector3(-10.8, 0.0, 2.6), Vector3(-9.0, -5.8, 2.2), Vector3(-4.5, 8.8, 1.6), Vector3(-4.5, -8.8, 1.6)]:
|
||
var at: Vector2 = center + LuneMap.AXIS * spec.x + LuneMap.BLUE_SIDE * spec.y
|
||
if _clear_of_lanes(at, spec.z * 1.4):
|
||
rocks.append(Transform3D(_yaw_basis(spec.y * .4, Vector3.ONE * spec.z), _ground3(at, -.3)))
|
||
_prop_multi("Rock_1", rocks)
|
||
# One stair per team from the valley up to its entrance.
|
||
var steps: Array[Transform3D] = []
|
||
for team: int in range(2):
|
||
var entry: Vector2 = map.grotto_entry(team)
|
||
var outward: Vector2 = (entry - center).normalized()
|
||
for k: int in range(5):
|
||
var at: Vector2 = center + outward * (LuneTerrain.ARENA_RADIUS + .15 + k * .72)
|
||
steps.append(Transform3D(_yaw_basis(_facing(outward)), _ground3(at, -.08)))
|
||
var stair := BoxMesh.new()
|
||
stair.size = Vector3(3.0, .34, .78)
|
||
_multi(stair, surface("ENV_limestone_stair", Color(.5, .47, .44)), steps, "GrottoStairs")
|
||
grotto_light = OmniLight3D.new()
|
||
grotto_light.position = Vector3(center.x, y + 2.6, center.y) - Vector3(LuneMap.AXIS.x, 0, LuneMap.AXIS.y) * 3.0
|
||
grotto_light.light_color = GROTTO_COLOR
|
||
grotto_light.light_energy = 2.4
|
||
grotto_light.omni_range = 12.0
|
||
add_child(grotto_light)
|
||
|
||
## True if a prop of the given radius at p stays outside every lane corridor.
|
||
func _clear_of_lanes(p: Vector2, radius: float) -> bool:
|
||
var lane: Array = terrain.nearest_lane(p)
|
||
return float(lane[0]) > float(LuneTerrain.CORRIDOR.get(lane[1], 3.6)) + radius
|
||
|
||
## The M2 grotto model without its floor disc and loose boulders (the G1 arena has its
|
||
## own floor): only the rock arc, the dark cave and the crystals remain.
|
||
func _cave_mesh() -> ArrayMesh:
|
||
var scene: Node3D = (load(GROTTO_SCENE) as PackedScene).instantiate()
|
||
var source: Mesh = (scene.get_node("BossGrotto") as MeshInstance3D).mesh
|
||
var mesh := ArrayMesh.new()
|
||
for index: int in range(source.get_surface_count()):
|
||
var material: Material = source.surface_get_material(index)
|
||
var key: String = material.resource_name if material != null else ""
|
||
if key in ["ENV_soil", "ENV_limestone"]:
|
||
continue
|
||
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, source.surface_get_arrays(index))
|
||
mesh.surface_set_material(mesh.get_surface_count() - 1, material)
|
||
scene.free()
|
||
return mesh
|
||
|
||
## Grotto glow: violet when open, dim when locked, team colour while a claim is pending.
|
||
func sync_grotto(available: bool, claim_team: int) -> void:
|
||
if grotto_light == null:
|
||
return
|
||
var color: Color = TEAM_COLORS[claim_team] if claim_team >= 0 else GROTTO_COLOR
|
||
var lit: bool = available or claim_team >= 0
|
||
grotto_light.light_color = color
|
||
grotto_light.light_energy = 2.4 if lit else .5
|
||
_grotto_runes.emission = color
|
||
_grotto_runes.albedo_color = color
|
||
_grotto_runes.emission_energy_multiplier = 1.2 if lit else .15
|
||
|
||
# ---------------------------------------------------------------- deployment areas
|
||
## One pad per team and lane where the army forms up (rows of four, front row ahead),
|
||
## and one ring per team at its grotto entrance. Shown during planning only.
|
||
func _deployment_pads() -> void:
|
||
var span: Dictionary = map.deployment_span()
|
||
var start: float = maxf(0.0, float(span.from) - .4)
|
||
var finish: float = float(span.to) + .4
|
||
var half: float = float(span.half_width)
|
||
for team: int in range(2):
|
||
for lane: String in LuneMap.LANES:
|
||
var tool := SurfaceTool.new()
|
||
tool.begin(Mesh.PRIMITIVE_TRIANGLES)
|
||
var previous: Array = []
|
||
var progress: float = start
|
||
while progress <= finish + 0.001:
|
||
var left: Vector2 = map.lane_point(team, lane, progress, half)
|
||
var right: Vector2 = map.lane_point(team, lane, progress, -half)
|
||
var y: float = terrain.walk_height(map.lane_point(team, lane, progress)) + .09
|
||
var section: Array = [[Vector3(left.x, y, left.y), Vector2(1.0, progress - start)], [Vector3(right.x, y, right.y), Vector2(-1.0, progress - start)]]
|
||
if not previous.is_empty():
|
||
for corner: Array in [previous[0], section[1], section[0], previous[0], previous[1], section[1]]:
|
||
tool.set_uv(corner[1])
|
||
tool.add_vertex(corner[0])
|
||
previous = section
|
||
progress += .5
|
||
var pad := MeshInstance3D.new()
|
||
pad.name = "Deployment_%d_%s" % [team, lane]
|
||
pad.mesh = tool.commit()
|
||
var material := ShaderMaterial.new()
|
||
material.shader = PadShader
|
||
material.set_shader_parameter("tint", TEAM_COLORS[team])
|
||
material.set_shader_parameter("pad_length", finish - start)
|
||
material.set_shader_parameter("row_start", float(span.to) - start)
|
||
pad.material_override = material
|
||
pad.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||
pad.visible = false
|
||
add_child(pad)
|
||
_pads["%d:%s" % [team, lane]] = pad
|
||
var ring := MeshInstance3D.new()
|
||
ring.name = "GrottoEntry_%d" % team
|
||
var torus := TorusMesh.new()
|
||
torus.inner_radius = 1.9
|
||
torus.outer_radius = 2.15
|
||
torus.rings = 40
|
||
ring.mesh = torus
|
||
ring.scale = Vector3(1, .15, 1)
|
||
var entry: Vector2 = map.grotto_entry(team)
|
||
ring.position = Vector3(entry.x, LuneTerrain.ARENA_HEIGHT + .12, entry.y)
|
||
ring.material_override = _glow(TEAM_COLORS[team], 1.6)
|
||
ring.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||
ring.visible = false
|
||
add_child(ring)
|
||
_grotto_pads[team] = ring
|
||
|
||
## Planning shows the own team's formation areas; lanes with own units are highlighted.
|
||
func show_deployment(visible_now: bool, team: int, used: Dictionary, grotto_open: bool) -> void:
|
||
for key: String in _pads:
|
||
var pad: MeshInstance3D = _pads[key]
|
||
var parts: PackedStringArray = key.split(":")
|
||
pad.visible = visible_now and int(parts[0]) == team
|
||
if pad.visible:
|
||
(pad.material_override as ShaderMaterial).set_shader_parameter("highlight", 1.0 if int(used.get(parts[1], 0)) > 0 else 0.0)
|
||
for side: int in _grotto_pads:
|
||
(_grotto_pads[side] as MeshInstance3D).visible = visible_now and grotto_open and side == team
|
||
|
||
# ---------------------------------------------------------------- buildings
|
||
func _building(definition: Dictionary) -> void:
|
||
var root := Node3D.new()
|
||
root.name = str(definition.id)
|
||
root.position = to3(definition.position)
|
||
add_child(root)
|
||
var team: int = int(definition.team)
|
||
if definition.tier == "core":
|
||
var core: Node3D = AssetLibrary.instantiate_fighter("core_team%d" % team)
|
||
if core != null:
|
||
core.scale = Vector3.ONE * 1.35
|
||
root.add_child(core)
|
||
if core.has_method("trigger_activate"):
|
||
core.trigger_activate()
|
||
root.set_meta("core", core)
|
||
else:
|
||
var tower: Node3D = (load(TOWER) as PackedScene).instantiate()
|
||
tower.scale = Vector3.ONE * (1.18 if definition.tier == "base" else 1.0)
|
||
_dress(tower)
|
||
_tint_crystal(tower, _crystal_materials[team])
|
||
# Towers face the lane they guard.
|
||
var toward: Vector2 = map.core_position(1 - team) - Vector2(definition.position)
|
||
tower.rotation.y = atan2(toward.x, toward.y)
|
||
root.add_child(tower)
|
||
root.set_meta("tower", tower)
|
||
if definition.tier == "core":
|
||
for side: float in [-1.0, 1.0]:
|
||
# Team banners flank each Ancient.
|
||
var banner := MeshInstance3D.new()
|
||
banner.mesh = _props["Banner"]
|
||
var cloth := StandardMaterial3D.new()
|
||
cloth.albedo_color = TEAM_COLORS[team]
|
||
cloth.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||
cloth.roughness = .8
|
||
for index: int in range(banner.mesh.get_surface_count()):
|
||
var source: Material = banner.mesh.surface_get_material(index)
|
||
if source != null and source.resource_name == "PR_Banner":
|
||
banner.set_surface_override_material(index, cloth)
|
||
var toward: Vector2 = (map.core_position(1 - team) - map.core_position(team)).normalized()
|
||
var across := Vector2(-toward.y, toward.x) * side * 3.6
|
||
banner.position = Vector3(across.x, 0, across.y) + Vector3(toward.x, 0, toward.y) * 2.4
|
||
banner.rotation.y = atan2(toward.x, toward.y)
|
||
root.add_child(banner)
|
||
var light := OmniLight3D.new()
|
||
light.position = Vector3(0, 5.2 if definition.tier != "core" else 2.4, 0)
|
||
light.light_color = TEAM_COLORS[team]
|
||
light.light_energy = 1.2
|
||
light.omni_range = 6.0
|
||
root.add_child(light)
|
||
root.set_meta("light", light)
|
||
var bar := Label3D.new()
|
||
bar.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
||
bar.no_depth_test = true
|
||
bar.font_size = 34
|
||
bar.outline_size = 8
|
||
bar.pixel_size = .01
|
||
bar.modulate = TEAM_COLORS[team].lightened(.25)
|
||
bar.position = Vector3(0, 7.4 if definition.tier != "core" else 4.4, 0)
|
||
root.add_child(bar)
|
||
root.set_meta("bar", bar)
|
||
building_nodes[definition.id] = root
|
||
|
||
func _tint_crystal(node: Node, crystal: StandardMaterial3D) -> void:
|
||
if node is MeshInstance3D:
|
||
for index: int in range(node.mesh.get_surface_count()):
|
||
var source: Material = node.mesh.surface_get_material(index)
|
||
if source != null and source.resource_name == "TW_Crystal":
|
||
node.set_surface_override_material(index, crystal)
|
||
for child: Node in node.get_children():
|
||
_tint_crystal(child, crystal)
|
||
|
||
## Health text and destroyed state from the authoritative building HP.
|
||
func sync_buildings(hp: Dictionary, max_hp: Dictionary, time: float, delta: float) -> void:
|
||
for id: String in building_nodes:
|
||
var root: Node3D = building_nodes[id]
|
||
var value: float = float(hp.get(id, 0.0))
|
||
var bar: Label3D = root.get_meta("bar")
|
||
var alive: bool = value > 0.0
|
||
bar.visible = alive and value < float(max_hp[id]) - .5
|
||
bar.text = "%d%%" % ceili(value / float(max_hp[id]) * 100.0)
|
||
(root.get_meta("light") as OmniLight3D).visible = alive
|
||
if root.has_meta("tower"):
|
||
var tower: Node3D = root.get_meta("tower")
|
||
tower.position.y = move_toward(tower.position.y, 0.0 if alive else -3.2, delta * 2.0)
|
||
tower.rotation.z = move_toward(tower.rotation.z, 0.0 if alive else .22, delta * .3)
|
||
elif root.has_meta("core"):
|
||
var core: Node3D = root.get_meta("core")
|
||
if core.has_method("set_core_health"):
|
||
core.set_core_health(value / float(max_hp[id]))
|
||
if core.has_method("pose"):
|
||
core.pose(time, false, 0, 0, 0, delta)
|
||
core.visible = alive
|