Hauptszene scenes/luneraria.tscn, Saumwacht bleibt über --legacy und die alten QA-Schalter erreichbar. Karte aus Bestandsassets plus neuem Turm, HUD mit Planung, Lanes und Minimap, Menü umbenannt, Einstellungen werden aus dem alten Benutzerordner übernommen. Simulation 6,6x schneller (Raster, Caches, faire Staffelung) und exakt spiegelsymmetrisch. Regeln 91/91, echte Anwendung 38/38, Legacy unverändert grün. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
303 lines
11 KiB
GDScript
303 lines
11 KiB
GDScript
extends Node3D
|
|
## 3D presentation of LuneMap built from existing assets: Kluft terrain shaders, the
|
|
## Saumkern models as Ancients, compare-pack rocks and trees, and the new lane tower.
|
|
## Purely visual; the simulation never reads anything from this node.
|
|
|
|
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 TOWER := "res://assets/luneraria/tower.glb"
|
|
const ROCK := "res://assets/compare/compare-rock.glb"
|
|
const TREE := "res://assets/compare/compare-tree.glb"
|
|
const TEAM_COLORS: Array[Color] = [Color("5aa8ff"), Color("ff6a55")]
|
|
const TEAM_NAMES: Array[String] = ["Blau", "Rot"]
|
|
|
|
var map: LuneMap
|
|
var building_nodes: Dictionary = {}
|
|
var _terrain: Dictionary = {}
|
|
var _crystal_materials: Array[StandardMaterial3D] = []
|
|
|
|
func build(match_map: LuneMap) -> void:
|
|
map = match_map
|
|
name = "LuneWorld"
|
|
_environment()
|
|
_ground()
|
|
_river()
|
|
for lane: String in LuneMap.LANES:
|
|
_lane_ribbon(map.lanes[lane])
|
|
for team: int in range(2):
|
|
_base_plaza(team)
|
|
_decoration()
|
|
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)
|
|
|
|
func terrain(key: String, color: Color) -> ShaderMaterial:
|
|
if not _terrain.has(key):
|
|
var source := StandardMaterial3D.new()
|
|
source.resource_name = key
|
|
source.albedo_color = color
|
|
_terrain[key] = Sanctuary.terrain_material(source)
|
|
return _terrain[key]
|
|
|
|
func to3(v: Vector2, y: float = 0.0) -> Vector3:
|
|
return Vector3(v.x, y, v.y)
|
|
|
|
func _environment() -> void:
|
|
var environment := WorldEnvironment.new()
|
|
var settings := Environment.new()
|
|
Lighting.configure(settings)
|
|
# The overview camera sits ~80 m away; distance fog would wash out the whole board.
|
|
settings.fog_enabled = false
|
|
environment.environment = settings
|
|
add_child(environment)
|
|
var 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_max_distance = 90.0
|
|
sun.shadow_blur = 1.8
|
|
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 ground := MeshInstance3D.new()
|
|
var plane := PlaneMesh.new()
|
|
plane.size = Vector2(LuneMap.HALF_EXTENT * 2.0 + 30.0, LuneMap.HALF_EXTENT * 2.0 + 30.0)
|
|
plane.subdivide_width = 32
|
|
plane.subdivide_depth = 32
|
|
ground.mesh = plane
|
|
ground.material_override = terrain("ENV_soil", Color(.10, .15, .065))
|
|
ground.name = "Ground"
|
|
add_child(ground)
|
|
|
|
func _river() -> void:
|
|
# The river follows the symmetry axis x = z and passes under all three lanes.
|
|
var tool := SurfaceTool.new()
|
|
tool.begin(Mesh.PRIMITIVE_TRIANGLES)
|
|
var a := Vector2(-40, -40)
|
|
var b := Vector2(40, 40)
|
|
var side := Vector2(1, -1).normalized() * 3.2
|
|
var steps: int = 40
|
|
for i: int in range(steps):
|
|
# Godot front faces wind clockwise seen from above.
|
|
for quad: Array in [[i, -1], [i + 1, 1], [i + 1, -1], [i, -1], [i, 1], [i + 1, 1]]:
|
|
var t: float = float(quad[0]) / steps
|
|
var wobble: float = sin(t * 18.0) * .6
|
|
var p: Vector2 = a.lerp(b, t) + side * (float(quad[1]) + wobble * .15)
|
|
tool.set_normal(Vector3.UP)
|
|
tool.add_vertex(to3(p, .012))
|
|
var river := MeshInstance3D.new()
|
|
river.mesh = tool.commit()
|
|
var water := ShaderMaterial.new()
|
|
water.shader = RiverShader
|
|
river.material_override = water
|
|
river.name = "River"
|
|
add_child(river)
|
|
|
|
func _lane_ribbon(path: LuneMap.LanePath) -> void:
|
|
var tool := SurfaceTool.new()
|
|
tool.begin(Mesh.PRIMITIVE_TRIANGLES)
|
|
var steps: int = int(ceil(path.length / .8))
|
|
var half: float = map.balance.lane_half_width + .6
|
|
var previous_left := Vector3.ZERO
|
|
var previous_right := Vector3.ZERO
|
|
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 + .5)) - path.point_at(maxf(0.0, s - .5))).normalized()
|
|
var normal := Vector2(-tangent.y, tangent.x)
|
|
var left: Vector3 = to3(point + normal * half, .03)
|
|
var right: Vector3 = to3(point - normal * half, .03)
|
|
if i > 0:
|
|
for vertex: Vector3 in [previous_left, right, left, previous_left, previous_right, right]:
|
|
tool.set_normal(Vector3.UP)
|
|
tool.set_uv(Vector2(vertex.x, vertex.z) * .25)
|
|
tool.add_vertex(vertex)
|
|
previous_left = left
|
|
previous_right = right
|
|
var ribbon := MeshInstance3D.new()
|
|
ribbon.mesh = tool.commit()
|
|
ribbon.material_override = terrain("ENV_paving", Color(.19, .185, .15))
|
|
ribbon.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
|
add_child(ribbon)
|
|
|
|
func _base_plaza(team: int) -> void:
|
|
var plaza := MeshInstance3D.new()
|
|
var disc := CylinderMesh.new()
|
|
disc.top_radius = 9.5
|
|
disc.bottom_radius = 9.8
|
|
disc.height = .12
|
|
disc.radial_segments = 48
|
|
plaza.mesh = disc
|
|
plaza.material_override = terrain("ENV_limestone", Color(.25, .225, .17))
|
|
plaza.position = to3(map.core_position(team), .02)
|
|
add_child(plaza)
|
|
var rim := MeshInstance3D.new()
|
|
var ring := TorusMesh.new()
|
|
ring.inner_radius = 9.35
|
|
ring.outer_radius = 9.6
|
|
ring.rings = 64
|
|
rim.mesh = ring
|
|
var glow := StandardMaterial3D.new()
|
|
glow.albedo_color = TEAM_COLORS[team]
|
|
glow.emission_enabled = true
|
|
glow.emission = TEAM_COLORS[team]
|
|
glow.emission_energy_multiplier = 1.2
|
|
rim.material_override = glow
|
|
rim.position = to3(map.core_position(team), .1)
|
|
rim.scale = Vector3(1, .3, 1)
|
|
add_child(rim)
|
|
|
|
func _far_from_lanes(point: Vector2, margin: float) -> bool:
|
|
for lane: String in LuneMap.LANES:
|
|
var path: LuneMap.LanePath = map.lanes[lane]
|
|
if path.point_at(path.project(point)).distance_to(point) < margin:
|
|
return false
|
|
for team: int in range(2):
|
|
if map.core_position(team).distance_to(point) < 12.0:
|
|
return false
|
|
if absf(point.x - point.y) < 5.0:
|
|
return false
|
|
return true
|
|
|
|
func _decoration() -> void:
|
|
# Scatter one half deterministically and mirror it, so decoration never favours a team.
|
|
var rng := RandomNumberGenerator.new()
|
|
rng.seed = 90210
|
|
var tree: PackedScene = load(TREE)
|
|
var rock: PackedScene = load(ROCK)
|
|
var placed: Array[Vector2] = []
|
|
var tries: int = 0
|
|
while placed.size() < 70 and tries < 4000:
|
|
tries += 1
|
|
var point := Vector2(rng.randf_range(-LuneMap.HALF_EXTENT - 4, LuneMap.HALF_EXTENT + 4), rng.randf_range(-LuneMap.HALF_EXTENT - 4, LuneMap.HALF_EXTENT + 4))
|
|
if point.y < point.x or not _far_from_lanes(point, 5.2):
|
|
continue
|
|
var clear: bool = true
|
|
for other: Vector2 in placed:
|
|
if other.distance_to(point) < 2.6:
|
|
clear = false
|
|
break
|
|
if not clear:
|
|
continue
|
|
placed.append(point)
|
|
var is_tree: bool = rng.randf() < .62
|
|
var size: float = rng.randf_range(1.1, 1.9) if is_tree else rng.randf_range(.8, 1.6)
|
|
var yaw: float = rng.randf() * TAU
|
|
for mirrored: bool in [false, true]:
|
|
var at: Vector2 = LuneMap.mirror(point) if mirrored else point
|
|
var holder := Node3D.new()
|
|
holder.position = to3(at)
|
|
holder.rotation.y = yaw if not mirrored else -yaw
|
|
holder.scale = Vector3.ONE * size
|
|
var prop: Node3D = (tree if is_tree else rock).instantiate()
|
|
# The compare-pack props were exported with their height along -Z; stand them up.
|
|
prop.rotation.x = PI * .5
|
|
_dress(prop)
|
|
holder.add_child(prop)
|
|
add_child(holder)
|
|
|
|
var _vertex_colored: StandardMaterial3D
|
|
|
|
func _dress(node: Node) -> void:
|
|
if node is MeshInstance3D:
|
|
for surface: int in range(node.mesh.get_surface_count()):
|
|
var source := node.mesh.surface_get_material(surface) as StandardMaterial3D
|
|
if source != null and source.resource_name.begins_with("ENV_") and not source.resource_name.contains("brass"):
|
|
node.set_surface_override_material(surface, terrain(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(surface, _vertex_colored)
|
|
for child: Node in node.get_children():
|
|
_dress(child)
|
|
|
|
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)
|
|
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 surface: int in range(node.mesh.get_surface_count()):
|
|
var source: Material = node.mesh.surface_get_material(surface)
|
|
if source != null and source.resource_name == "TW_Crystal":
|
|
node.set_surface_override_material(surface, 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
|