class_name RaidState extends RefCounted ## Authoritative server state for a single raid instance. ## Owns all player states, boss state, encounter timeline, and fight phase. ## This is the single source of truth on the server. Clients send inputs; ## this validates and broadcasts results. enum FightPhase { LOBBY, ACTIVE, WIPED, COMPLETE } ## Represents one player's combat state within the raid. class PlayerState: var player_id: int = -1 var name: String = "" var class_id: String = "" # "warden", "luminary", "blade", "arcanist" var role: String = "" # "tank", "healer", "dps" var level: int = 1 var hp: float = 0.0 var max_hp: float = 0.0 var is_dead: bool = false var is_disconnected: bool = false var weakness_timer: float = 0.0 # Seconds remaining on post-revive debuff var position: Vector3 = Vector3.ZERO # Combat systems (per-player): var gcd: GCDTimer = null var combo: ComboState = null var gauges: GimmickGauges = null var abilities: AbilityResolver = null func _init(player_id: int, name: String, class_id: String, role: String, level: int): self.player_id = player_id self.name = name self.class_id = class_id self.role = role self.level = level ## The raid instance state. var fight_phase: int = FightPhase.LOBBY var end_reason: String = "" # Set when the fight ends: "boss_dead", "enrage", "all_players_dead" var current_encounter_id: String = "" # e.g., "t1e1" var players: Array[PlayerState] = [] var boss_hp: float = 0.0 var boss_max_hp: float = 0.0 var boss_phase: int = 1 var enrage_timer: float = 0.0 # Seconds remaining (hard enrage) or -1 (soft enrage uses ramp) var soft_enrage_active: bool = false var soft_enrage_ramp: float = 0.0 # Current ramp level (increases over time) var fight_time: float = 0.0 # Total elapsed time in the active fight ## Initializes a new raid instance with an encounter and set of players. func _init(encounter_id: String, player_data: Array[Dictionary]): current_encounter_id = encounter_id fight_phase = FightPhase.LOBBY for pd in player_data: create_player( int(pd["player_id"]), str(pd["name"]), str(pd["class_id"]), str(pd["role"]), int(pd["level"]) ) ## Creates a player and appends them to the raid. Returns the new PlayerState. ## (RaidServer uses this for dynamic joins; _init uses it for pre-seeded rosters.) func create_player(player_id: int, name: String, class_id: String, role: String, level: int) -> PlayerState: var ps := PlayerState.new(player_id, name, class_id, role, level) # Initialize combat systems (combo/abilities are wired up by RaidServer # from data/classes.json once the player's class is known). ps.gcd = GCDTimer.new(2.5) ps.gauges = GimmickGauges.new() players.append(ps) return ps ## Starts the fight (transition from lobby to active). func start_fight(boss_max_hp_value: float, enrage_duration: float) -> void: fight_phase = FightPhase.ACTIVE end_reason = "" boss_max_hp = boss_max_hp_value boss_hp = boss_max_hp_value boss_phase = 1 enrage_timer = enrage_duration soft_enrage_active = false soft_enrage_ramp = 0.0 fight_time = 0.0 for p in players: p.is_dead = false p.is_disconnected = false p.weakness_timer = 0.0 ## Ticks the raid state forward by delta_time. Call once per server tick. func tick(delta_time: float) -> void: if fight_phase != FightPhase.ACTIVE: return fight_time += delta_time # Tick enrage timer (hard enrage) if enrage_timer > 0.0: enrage_timer -= delta_time if enrage_timer <= 0.0: _wipe("enrage") # Tick soft enrage ramp if soft_enrage_active: soft_enrage_ramp += delta_time # Ramp increases over time # Tick all players for p in players: if p.is_dead or p.is_disconnected: continue p.gcd.tick(delta_time) if p.weakness_timer > 0.0: p.weakness_timer = max(p.weakness_timer - delta_time, 0.0) ## Applies damage to the boss. Returns true if boss died. func deal_damage_to_boss(amount: float) -> bool: if fight_phase != FightPhase.ACTIVE: return false boss_hp -= amount if boss_hp <= 0.0: boss_hp = 0.0 fight_phase = FightPhase.COMPLETE end_reason = "boss_dead" return true # Phase transitions are encounter-specific and come from the timeline # (hp_threshold events), applied by FightSim — not hardcoded here. return false ## Applies damage to a player. Handles death and wipe check. func deal_damage_to_player(player_id: int, amount: float) -> void: if fight_phase != FightPhase.ACTIVE: return for p in players: if p.player_id == player_id: p.hp -= amount if p.hp <= 0.0: p.hp = 0.0 p.is_dead = true _check_wipe() break ## Revives a dead player (called when Revive Potion is used on their corpse). func revive_player(player_id: int) -> bool: for p in players: if p.player_id == player_id and p.is_dead: p.is_dead = false p.hp = p.max_hp * 0.3 # Spawn at 30% HP p.weakness_timer = 90.0 # 90s weakness debuff return true return false ## Marks a player as disconnected. func set_disconnected(player_id: int, disconnected: bool) -> void: for p in players: if p.player_id == player_id: p.is_disconnected = disconnected if not disconnected: # Rejoining: spawn at 50% HP with brief vulnerability p.hp = p.max_hp * 0.5 p.is_dead = false break ## Checks if all players are dead (wipe condition). func _check_wipe() -> void: for p in players: if not p.is_dead and not p.is_disconnected: return # Someone is still alive # All players dead or disconnected — WIPE _wipe("all_players_dead") ## Triggers a wipe. Resets the encounter. func _wipe(reason: String) -> void: fight_phase = FightPhase.WIPED end_reason = reason # In a full implementation, this would signal clients to reset positions, # reset boss HP, etc. For now we just mark the phase. # The server will re-initialize on next start_fight() call. ## Returns the number of alive (non-dead, non-disconnected) players. func get_alive_player_count() -> int: var count := 0 for p in players: if not p.is_dead and not p.is_disconnected: count += 1 return count