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>
163 lines
5.5 KiB
GDScript
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
|