saumwacht-godot/scripts/luneraria/core/lune_unit.gd
Martin Stickelbroeck f53859a298 M2: Helden, Items, Talente, Angriffschampions und Bossgrotte
Drei Helden mit Nähe-/Teilnahme-XP, Talenten und drei Itemplätzen,
drei Angriffschampions mit Buchung n -> n+1, Bossgrotte mit Streitfall,
ruhendem Boss, idempotenter Eroberung, einmaliger Entsendung für das
Siegerteam, hohem Kopfgeld und Sperre bis Sieg+4. HUD-Reiter, Porträt
aus echtem Modell, Grotten-Asset, Bots für alle Systeme. Regeln M2 73/73,
M1 91/91, App M2 15/15 und M1 38/38, Bossszenarien dokumentiert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 22:33:11 +02:00

173 lines
5.8 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
## Grotto boss waits while two expeditions still fight each other.
var dormant: bool = false
var aura_ready: float = 4.0
# 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)
var bonus: float = float(def.get("attack_speed_bonus", 0.0))
return 1.0 + clampf(strongest_effect("attack_speed") - slowing + bonus, -0.25, 0.5)
func has_talent(id: String) -> bool:
return (def.get("talents", []) as Array).has(id)
func role() -> String:
return str(def.get("role", ""))
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