class_name DamageCalculator extends RefCounted ## Damage calculation system. Pure math, no Node dependencies. ## Handles base damage, mitigation stacking (sequential, highest-first), ## crits, and various modifiers. ## Represents a single mitigation source. class Mitigation: var percentage: float # 0.0 to 1.0 (e.g., 0.6 for 60%) var absorb_flat: float = 0.0 # Flat damage absorbed (applied after all %) var source_name: String = "" func _init(pct: float, flat: float = 0.0, name: String = ""): percentage = pct absorb_flat = flat source_name = name ## Calculates final damage after all mitigations are applied. ## base_damage: The raw damage before any mitigation. ## mitigations: Array of Mitigation objects active on the target. ## Returns a result dictionary: ## { "final_damage": float, "mitigation_log": Array[String] } static func calculate(base_damage: float, mitigations: Array) -> Dictionary: if base_damage <= 0.0: return {"final_damage": 0.0, "mitigation_log": []} var log: Array[String] = [] var remaining := base_damage # Separate % mitigations from absorb shields var pct_mits: Array[Mitigation] = [] var total_absorb: float = 0.0 for mit in mitigations: if mit.percentage > 0.0: pct_mits.append(mit) if mit.absorb_flat > 0.0: total_absorb += mit.absorb_flat # Sort % mitigations highest first pct_mits.sort_custom(func(a, b): return a.percentage > b.percentage) # Apply each % mitigation sequentially to the remaining damage for mit in pct_mits: var reduced := remaining * (1.0 - mit.percentage) log.append("%s: %.1f -> %.1f (-%.0f%%)" % [mit.source_name, remaining, reduced, mit.percentage * 100.0]) remaining = reduced # Apply absorb shields after all % mitigations if total_absorb > 0.0: var before_absorb := remaining remaining = max(remaining - total_absorb, 0.0) log.append("Absorb: %.1f -> %.1f (-%.1f flat)" % [before_absorb, remaining, before_absorb - remaining]) return { "final_damage": max(remaining, 0.0), "mitigation_log": log } ## Calculates damage with a crit check. ## base_damage: Raw damage before mitigation. ## crit_chance: 0.0 to 1.0 probability of critting. ## crit_multiplier: Damage multiplier on crit (default 1.5 = 150%). ## rng: Optional RandomNumberGenerator for testability. ## Returns { "damage": float, "critted": bool } static func roll_damage(base_damage: float, crit_chance: float, crit_multiplier: float = 1.5, rng: RandomNumberGenerator = null) -> Dictionary: if rng == null: rng = RandomNumberGenerator.new() var critted := rng.randf() < crit_chance var final := base_damage * (crit_multiplier if critted else 1.0) return {"damage": final, "critted": critted} ## Applies a vulnerability modifier to damage. ## vulnerability: 0.0 to 1.0 (e.g., 0.15 = +15% damage taken) static func apply_vulnerability(damage: float, vulnerability: float) -> float: return damage * (1.0 + vulnerability) ## Applies a weakness modifier (post-revive debuff). ## reduction: 0.0 to 1.0 (e.g., 0.25 = -25% damage dealt) static func apply_weakness(damage: float, reduction: float) -> float: return damage * (1.0 - reduction)