class_name ComboState extends RefCounted ## Combo attack state machine. Server-authoritative. ## Tracks the current hit in a player's combo rotation, validates positionals, ## and handles combo breaks. enum Positional { NONE, FRONT, BACK, LEFT, RIGHT } var current_hit: int = 0 # Index into the combo hits array (0-based) var is_broken: bool = false # True briefly after a break for VFX/audio trigger # Combo definition: array of hit data var _hits: Array[Dictionary] = [] # Each hit dict: { "name": String, "positional": Positional, "damage_mult": float } ## Initializes the combo state with a set of hits. ## hits: Array of dictionaries, each with keys: ## "name" (String), "positional" (Positional enum int), "damage_mult" (float) func _init(hits: Array[Dictionary]): _hits = hits current_hit = 0 is_broken = false ## Returns the total number of hits in this combo. func get_combo_length() -> int: return _hits.size() ## Returns the data for the current hit. func get_current_hit_data() -> Dictionary: if current_hit < 0 or current_hit >= _hits.size(): return {} return _hits[current_hit] ## Returns the positional required for the current hit. ## Positional.NONE means no positional is required. func get_required_positional() -> int: var data := get_current_hit_data() if data.is_empty(): return Positional.NONE return data.get("positional", Positional.NONE) ## Returns the damage multiplier for the current hit (relative to base combo damage). func get_damage_multiplier() -> float: var data := get_current_hit_data() if data.is_empty(): return 1.0 return data.get("damage_mult", 1.0) ## Attempts to advance the combo by one hit. ## player_positional: The player's current positional relative to target. ## Returns a result dictionary: ## { "success": bool, "broke": bool, "hit_data": Dictionary, "advanced_to": int } ## On success: advances to next hit (or wraps to 0). ## On break: resets to hit 0, no damage dealt. GCD is still consumed by caller. func advance(player_positional: int) -> Dictionary: var required := get_required_positional() # Check positional requirement if required != Positional.NONE and player_positional != required: # COMBO BREAK current_hit = 0 is_broken = true return { "success": false, "broke": true, "hit_data": {}, "advanced_to": 0 } # Valid hit — return current hit data, then advance var hit_data := get_current_hit_data() var next_hit := (current_hit + 1) % _hits.size() current_hit = next_hit is_broken = false return { "success": true, "broke": false, "hit_data": hit_data, "advanced_to": next_hit } ## Clears the broken flag (called after VFX/audio has played). func clear_broken() -> void: is_broken = false ## Resets the combo to hit 0 (e.g., between fights or on target change if designed that way). func reset() -> void: current_hit = 0 is_broken = false