saumwacht-godot/scripts/luneraria/core/lune_unit.gd
Martin Stickelbroeck c27a0097c3 M1: spielbarer Drei-Lane-Kern mit Karte, HUD, Menü und Abnahme
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>
2026-09-27 21:37:23 +02:00

163 lines
5.5 KiB
GDScript

class_name LuneUnit
extends RefCounted
## One combat instance. Persistent ownership stays in LuneMatch; this object lives for
## one combat phase. Damage, heals and effects are queued and applied by LuneCombat.
const DAMAGE_MATRIX = [[1.30, 0.75, 1.00], [1.00, 1.30, 0.75], [0.75, 1.00, 1.30]]
const MOBILE_KINDS: Array[String] = ["troop", "hero", "monster", "champion", "boss"]
var id: int = 0
## Spawn index within the own team; mirrored units share it (used for fair staggering).
var team_index: int = 0
var kind: String = "troop"
var team: int = 0
var owner_slot: int = -1
var persistent_uid: String = ""
var lane: String = "mid"
var zone: String = "lane"
var def: Dictionary = {}
var building_id: String = ""
var position: Vector2 = Vector2.ZERO
var previous_position: Vector2 = Vector2.ZERO
var facing: float = 0.0
var hp: float = 1.0
var max_hp: float = 1.0
var alive: bool = true
var target: LuneUnit = null
var progress: float = 0.0
var lateral: float = 0.0
var attack_progress: float = 0.0
var hit_count: int = 0
var combat_time: float = 0.0
var shield: float = 0.0
var shield_remaining: float = INF
var effects: Dictionary = {}
var dots: Dictionary = {}
var ability_ready: float = 6.0
var reached_base: bool = false
# Presentation-only signals; never read by the rules.
var attack_serial: int = 0
var ability_serial: int = 0
var hit_flash: float = 0.0
var in_range: bool = false
var death_time: float = -1.0
func is_building() -> bool:
return kind == "tower" or kind == "core"
func is_mobile() -> bool:
return kind in MOBILE_KINDS
func attack_range() -> float:
return float(def.get("attack_range", 1.0))
func speed() -> float:
return float(def.get("speed", 2.0))
func armor_type() -> int:
return clampi(int(def.get("armor_type", 1 if is_building() else 0)), 0, 2)
func ability() -> String:
return str(def.get("ability", ""))
func is_boss() -> bool:
return kind == "boss" or bool(def.get("is_boss", false))
func mitigated(raw: float, attack_type: int) -> float:
var value: float = maxf(0.0, raw) * float(DAMAGE_MATRIX[clampi(attack_type, 0, 2)][armor_type()])
return value * (1.0 - damage_reduction())
## Applies an already mitigated amount through the shield.
func take(amount: float) -> void:
if not alive or amount <= 0.0:
return
var absorbed: float = minf(shield, amount)
shield -= absorbed
hp = maxf(0.0, hp - (amount - absorbed))
hit_flash = .18
func heal(amount: float) -> void:
if alive and not is_building():
hp = minf(max_hp, hp + maxf(0.0, amount) * (1.0 - strongest_effect("healing_reduction")))
func grant_shield(amount: float, duration: float = INF) -> void:
if alive and amount > shield:
shield = amount
shield_remaining = duration
func apply_effect(family: String, effect_kind: String, amount: float, duration: float) -> void:
# Buildings ignore crowd control and buffs; their gates are handled elsewhere.
if not alive or is_building():
return
if effects.has(family):
effects[family].amount = maxf(float(effects[family].amount), amount)
effects[family].remaining = maxf(float(effects[family].remaining), duration)
else:
effects[family] = {"kind": effect_kind, "amount": amount, "remaining": duration}
func apply_dot(family: String, damage: float, duration: float, attack_type: int) -> void:
if not alive or is_building():
return
if dots.has(family):
var effect: Dictionary = dots[family]
if damage > float(effect.damage):
effect.damage = damage
effect.attack_type = attack_type
effect.remaining = maxf(float(effect.remaining), duration)
else:
dots[family] = {"damage": damage, "remaining": duration, "next_tick": 1.0, "attack_type": attack_type}
func strongest_effect(effect_kind: String) -> float:
var amount: float = 0.0
for effect: Dictionary in effects.values():
if str(effect.kind) == effect_kind:
amount = maxf(amount, float(effect.amount))
return amount
func damage_bonus() -> float:
var result: float = 0.0
for effect: Dictionary in effects.values():
if str(effect.kind) == "damage":
result += float(effect.amount)
return minf(0.30, result) + float(def.get("damage_bonus", 0.0))
func damage_reduction() -> float:
var result: float = strongest_effect("reduction")
match ability():
"g03":
if combat_time < 8.0 - 0.000001:
result = maxf(result, 0.15)
"g04":
result = maxf(result, 0.25 if combat_time < 10.0 - 0.000001 else 0.10)
return minf(0.35, result + float(def.get("reduction_bonus", 0.0)))
func attack_speed_factor() -> float:
var slowing: float = strongest_effect("attack_slow") * (0.5 if is_boss() else 1.0)
return 1.0 + clampf(strongest_effect("attack_speed") - slowing, -0.25, 0.25)
func movement_factor() -> float:
return 1.0 - minf(0.35, strongest_effect("movement_slow") * (0.5 if is_boss() else 1.0))
## Status timers; DoT damage is returned so the caller can queue it with all other damage.
func advance_status(delta: float) -> float:
combat_time += delta
hit_flash = maxf(0.0, hit_flash - delta)
for family: String in effects.keys():
effects[family].remaining -= delta
if float(effects[family].remaining) <= 0.000001:
effects.erase(family)
if shield_remaining != INF:
shield_remaining -= delta
if shield_remaining <= 0.000001:
shield = 0.0
var dot_damage: float = 0.0
for family: String in dots.keys():
var effect: Dictionary = dots[family]
effect.next_tick -= minf(delta, maxf(0.0, float(effect.remaining)))
while float(effect.next_tick) <= 0.000001:
dot_damage += mitigated(float(effect.damage), int(effect.attack_type))
effect.next_tick += 1.0
effect.remaining -= delta
if float(effect.remaining) <= 0.000001:
dots.erase(family)
return dot_damage