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>
168 lines
7.1 KiB
GDScript
168 lines
7.1 KiB
GDScript
class_name LuneMap
|
|
extends RefCounted
|
|
## Map data in ground coordinates Vector2(x, z): Blau (team 0) in the south-west,
|
|
## Rot (team 1) in the north-east. Rot is the mirror image of Blau across x = z
|
|
## (mirror(v) = Vector2(v.y, v.x)), Bottom is Top mirrored across the Mid line.
|
|
## Both hold by construction, so strecken, towers and spawns are functionally equal.
|
|
|
|
const LANES: Array[String] = ["top", "mid", "bot"]
|
|
const LANE_NAMES: Dictionary = {"top": "Top", "mid": "Mid", "bot": "Bottom"}
|
|
# First halves from the Blau end to the centre line; the Rot half is mirrored.
|
|
const TOP_HALF: Array[Vector2] = [Vector2(-24, 22), Vector2(-26, 6), Vector2(-26.5, -12), Vector2(-25, -25)]
|
|
const MID_HALF: Array[Vector2] = [Vector2(-21, 21), Vector2(-11, 10), Vector2(0, 0)]
|
|
const CORE_BLUE := Vector2(-30, 30)
|
|
const BASE_TOWERS_BLUE: Array[Vector2] = [Vector2(-28.8, 24.6), Vector2(-24.6, 28.8)]
|
|
## Neutral boss grotto, north-west of Mid and on the symmetry axis (M2).
|
|
const GROTTO := Vector2(-11.5, -11.5)
|
|
const HALF_EXTENT: float = 36.0
|
|
|
|
var balance: LuneBalance
|
|
var lanes: Dictionary = {}
|
|
var buildings: Array[Dictionary] = []
|
|
|
|
class LanePath:
|
|
extends RefCounted
|
|
var points: PackedVector2Array
|
|
var cumulative: PackedFloat32Array
|
|
var length: float = 0.0
|
|
|
|
func _init(source: Array[Vector2]) -> void:
|
|
points = PackedVector2Array(source)
|
|
cumulative.append(0.0)
|
|
for i: int in range(1, points.size()):
|
|
length += points[i].distance_to(points[i - 1])
|
|
cumulative.append(length)
|
|
|
|
func point_at(s: float) -> Vector2:
|
|
var d: float = clampf(s, 0.0, length)
|
|
for i: int in range(1, points.size()):
|
|
if d <= cumulative[i] or i == points.size() - 1:
|
|
var span: float = maxf(0.0001, cumulative[i] - cumulative[i - 1])
|
|
return points[i - 1].lerp(points[i], clampf((d - cumulative[i - 1]) / span, 0.0, 1.0))
|
|
return points[points.size() - 1]
|
|
|
|
func tangent_at(s: float) -> Vector2:
|
|
var d: float = clampf(s, 0.0, length)
|
|
for i: int in range(1, points.size()):
|
|
if d <= cumulative[i] or i == points.size() - 1:
|
|
return (points[i] - points[i - 1]).normalized()
|
|
return Vector2.RIGHT
|
|
|
|
## Point and unit tangent at s in one pass.
|
|
func sample(s: float) -> Array:
|
|
var d: float = clampf(s, 0.0, length)
|
|
for i: int in range(1, points.size()):
|
|
if d <= cumulative[i] or i == points.size() - 1:
|
|
var a: Vector2 = points[i - 1]
|
|
var b: Vector2 = points[i]
|
|
var span: float = maxf(0.0001, cumulative[i] - cumulative[i - 1])
|
|
return [a.lerp(b, clampf((d - cumulative[i - 1]) / span, 0.0, 1.0)), (b - a) / span]
|
|
return [points[points.size() - 1], Vector2.RIGHT]
|
|
|
|
func project(position: Vector2) -> float:
|
|
var best: float = INF
|
|
var best_s: float = 0.0
|
|
for i: int in range(1, points.size()):
|
|
var a: Vector2 = points[i - 1]
|
|
var b: Vector2 = points[i]
|
|
var ab: Vector2 = b - a
|
|
var t: float = clampf((position - a).dot(ab) / maxf(0.0001, ab.length_squared()), 0.0, 1.0)
|
|
var d: float = position.distance_squared_to(a + ab * t)
|
|
if d < best:
|
|
best = d
|
|
best_s = cumulative[i - 1] + ab.length() * t
|
|
return best_s
|
|
|
|
static func mirror(v: Vector2) -> Vector2:
|
|
return Vector2(v.y, v.x)
|
|
|
|
static func mirror_mid(v: Vector2) -> Vector2:
|
|
return Vector2(-v.y, -v.x)
|
|
|
|
static func full_lane(half: Array[Vector2]) -> Array[Vector2]:
|
|
var result: Array[Vector2] = half.duplicate()
|
|
for i: int in range(half.size() - 2, -1, -1):
|
|
result.append(mirror(half[i]))
|
|
return result
|
|
|
|
func _init(config: LuneBalance = null) -> void:
|
|
balance = config if config != null else LuneBalance.default_balance()
|
|
var bot_half: Array[Vector2] = []
|
|
for point: Vector2 in TOP_HALF:
|
|
bot_half.append(mirror_mid(point))
|
|
lanes["top"] = LanePath.new(full_lane(TOP_HALF))
|
|
lanes["mid"] = LanePath.new(full_lane(MID_HALF))
|
|
lanes["bot"] = LanePath.new(full_lane(bot_half))
|
|
_build_buildings()
|
|
|
|
## Lane coordinate s for a team's own marching progress p (0 at its own base).
|
|
func s_for(team: int, lane: String, progress: float) -> float:
|
|
var path: LanePath = lanes[lane]
|
|
return progress if team == 0 else path.length - progress
|
|
|
|
func progress_for(team: int, lane: String, s: float) -> float:
|
|
var path: LanePath = lanes[lane]
|
|
return s if team == 0 else path.length - s
|
|
|
|
## Blau frame point, mirrored for Rot so both sides are exact images.
|
|
func lane_point(team: int, lane: String, progress: float, lateral: float = 0.0) -> Vector2:
|
|
var path: LanePath = lanes[lane]
|
|
var sampled: Array = path.sample(progress)
|
|
var tangent: Vector2 = sampled[1]
|
|
var blue: Vector2 = sampled[0] + Vector2(-tangent.y, tangent.x) * lateral
|
|
return blue if team == 0 else Vector2(blue.y, blue.x)
|
|
|
|
func lane_forward(team: int, lane: String, progress: float) -> Vector2:
|
|
var path: LanePath = lanes[lane]
|
|
var tangent: Vector2 = path.tangent_at(clampf(progress, 0.0, path.length))
|
|
return tangent if team == 0 else mirror(tangent)
|
|
|
|
func core_position(team: int) -> Vector2:
|
|
return CORE_BLUE if team == 0 else mirror(CORE_BLUE)
|
|
|
|
func _build_buildings() -> void:
|
|
buildings.clear()
|
|
for team: int in range(2):
|
|
for lane: String in LANES:
|
|
var path: LanePath = lanes[lane]
|
|
for tier: String in ["inner", "outer"]:
|
|
var fraction: float = balance.inner_tower_fraction if tier == "inner" else balance.outer_tower_fraction
|
|
var progress: float = path.length * fraction
|
|
buildings.append({"id": "%s_%s_%s" % ["blau" if team == 0 else "rot", lane, tier], "team": team, "lane": lane,
|
|
"tier": tier, "progress": progress, "position": lane_point(team, lane, progress, balance.tower_side_offset),
|
|
"max_hp": balance.tower_inner_hp if tier == "inner" else balance.tower_outer_hp,
|
|
"damage": balance.tower_damage, "range": balance.tower_range,
|
|
"bounty": balance.bounty_inner_tower if tier == "inner" else balance.bounty_outer_tower})
|
|
for index: int in range(2):
|
|
var point: Vector2 = BASE_TOWERS_BLUE[index]
|
|
buildings.append({"id": "%s_base_%d" % ["blau" if team == 0 else "rot", index + 1], "team": team, "lane": "base",
|
|
"tier": "base", "progress": -1.0, "position": point if team == 0 else mirror(point),
|
|
"max_hp": balance.base_tower_hp, "damage": balance.base_tower_damage, "range": balance.tower_range,
|
|
"bounty": balance.bounty_base_tower})
|
|
buildings.append({"id": "%s_ancient" % ("blau" if team == 0 else "rot"), "team": team, "lane": "base", "tier": "core",
|
|
"progress": -1.0, "position": core_position(team), "max_hp": balance.core_hp, "damage": 0.0, "range": 0.0, "bounty": 0})
|
|
|
|
var _index: Dictionary = {}
|
|
|
|
func building_def(id: String) -> Dictionary:
|
|
if _index.is_empty():
|
|
for entry: Dictionary in buildings:
|
|
_index[entry.id] = entry
|
|
return _index.get(id, {})
|
|
|
|
## Gate rule shared by every damage source. alive maps building id -> bool.
|
|
static func is_attackable(definition: Dictionary, alive: Dictionary) -> bool:
|
|
var prefix: String = "blau" if int(definition.team) == 0 else "rot"
|
|
match str(definition.tier):
|
|
"outer":
|
|
return true
|
|
"inner":
|
|
return not alive.get("%s_%s_outer" % [prefix, definition.lane], false)
|
|
"base":
|
|
for lane: String in LANES:
|
|
if not alive.get("%s_%s_outer" % [prefix, lane], false) and not alive.get("%s_%s_inner" % [prefix, lane], false):
|
|
return true
|
|
return false
|
|
"core":
|
|
return not alive.get(prefix + "_base_1", false) and not alive.get(prefix + "_base_2", false)
|
|
return false
|