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 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