class_name AbilityResolver extends RefCounted ## Resolves ability activation: checks cooldowns, GCD state, positionals, ## and applies effects. Server-authoritative. enum AbilityType { ON_GCD, OFF_GCD } ## Represents a single ability definition (data-driven). class AbilityDef: var id: String = "" var name: String = "" var type: int = AbilityType.OFF_GCD # AbilityType enum var cooldown: float = 0.0 # Seconds (off-GCD abilities only) var positional_bonus: int = -1 # Positional enum for bonus effect (-1 = none) var positional_bonus_mult: float = 1.0 # Damage multiplier if in correct position var description: String = "" func _init(id: String, name: String, type: int, cooldown: float = 0.0): self.id = id self.name = name self.type = type self.cooldown = cooldown ## Tracks the runtime state of a single ability for one player. class AbilityState: var def: AbilityDef var _cooldown_remaining: float = 0.0 func _init(def: AbilityDef): self.def = def _cooldown_remaining = 0.0 func is_ready() -> bool: return _cooldown_remaining <= 0.0 func tick(delta_time: float) -> void: if _cooldown_remaining > 0.0: _cooldown_remaining = max(_cooldown_remaining - delta_time, 0.0) func activate() -> void: _cooldown_remaining = def.cooldown func get_cooldown_progress() -> float: if def.cooldown <= 0.0: return 1.0 return 1.0 - (_cooldown_remaining / def.cooldown) ## The resolver manages all abilities for one player. var _abilities: Dictionary = {} # id -> AbilityState var gcd: GCDTimer ## Initializes the resolver with a set of ability definitions and the ## player's shared GCD timer (the same instance used by combo attacks, so ## on-GCD abilities and combos compete for one sweep). func _init(ability_defs: Array[AbilityDef], gcd_timer: GCDTimer): gcd = gcd_timer for def in ability_defs: _abilities[def.id] = AbilityState.new(def) ## Ticks all cooldowns and the GCD. Call once per server tick. func tick(delta_time: float) -> void: gcd.tick(delta_time) for id in _abilities: _abilities[id].tick(delta_time) ## Attempts to use an ability by ID. ## player_positional: Current positional relative to target (for bonus checks). ## Returns a result dictionary: ## { "success": bool, "reason": String, "positional_bonus_applied": bool } func try_use_ability(ability_id: String, player_positional: int = -1) -> Dictionary: if not _abilities.has(ability_id): return {"success": false, "reason": "unknown_ability", "positional_bonus_applied": false} var state: AbilityState = _abilities[ability_id] # Check ability's own cooldown (off-GCD abilities) if state.def.type == AbilityType.OFF_GCD and not state.is_ready(): return {"success": false, "reason": "on_cooldown", "positional_bonus_applied": false} # Check GCD (on-GCD abilities) if state.def.type == AbilityType.ON_GCD: if not gcd.is_ready(): if gcd.is_in_queue_window(): # Queue it — will fire next tick. For now, report as queued. return {"success": false, "reason": "gcd_queued", "positional_bonus_applied": false} return {"success": false, "reason": "gcd_active", "positional_bonus_applied": false} gcd.consume() # Activate the ability (start its cooldown) state.activate() # Check positional bonus var bonus_applied := false if state.def.positional_bonus != -1 and player_positional == state.def.positional_bonus: bonus_applied = true return {"success": true, "reason": "ok", "positional_bonus_applied": bonus_applied} ## Returns the cooldown progress (0.0 to 1.0) for a given ability. For UI. func get_ability_cooldown_progress(ability_id: String) -> float: if not _abilities.has(ability_id): return 1.0 return _abilities[ability_id].get_cooldown_progress() ## Returns the GCD progress (0.0 to 1.0). For UI. func get_gcd_progress() -> float: return gcd.get_progress() ## Resets all abilities and GCD (between fights). func reset() -> void: gcd.reset() for id in _abilities: _abilities[id]._cooldown_remaining = 0.0