class_name SyncProtocol extends RefCounted ## Message protocol between the authoritative server and clients. ## All messages are Dictionaries with a "type" field, encoded as JSON strings. ## The transport layer (ENet / MultiplayerPeer) is a separate concern — it ## feeds decoded dicts into RaidServer.handle_client_message() and drains ## RaidServer.get_outgoing(). This file is pure data: no sockets, no nodes. ## ## Client -> Server: ## join { name, class_id, role, level } ## start_fight {} (host) ## input_ability { ability_id, positional } ("combo_attack" = combo hit) ## input_move { position: [x, y, z] } ## input_consumable { consumable_id, target_id } ## ## Server -> Client: ## joined { player_id, players: [...] } ## fight_started { encounter_id, boss_max_hp } ## snapshot { fight: {...}, players: [...] } (periodic full state) ## event_damage { target: "boss"|"player", source_id, amount } ## event_mechanic { event_type, params, time } (telegraph data) ## event_dialog { line } ## event_phase { phase } ## event_enrage { kind: "soft" } ## event_ability_used { player_id, ability_id, positional_bonus } ## event_combo_broken { player_id } ## event_player_down { player_id } ## event_player_up { player_id, hp_pct } (revive / reconnect) ## input_rejected { player_id, reason } ## fight_ended { result: "wiped"|"complete", reason } static func encode(msg: Dictionary) -> String: return JSON.stringify(msg) static func decode(data: String) -> Dictionary: var parsed = JSON.parse_string(data) if typeof(parsed) != TYPE_DICTIONARY: push_error("SyncProtocol: invalid message payload") return {} return parsed as Dictionary ## Builds a full state snapshot from a live FightSim. Sent periodically so ## clients can render (and resync after packet loss). Clients never derive ## rules from this — it is presentation data only. static func build_snapshot(sim: FightSim) -> Dictionary: var players: Array[Dictionary] = [] for p in sim.raid_state.players: players.append({ "id": p.player_id, "hp": p.hp, "max_hp": p.max_hp, "dead": p.is_dead, "disconnected": p.is_disconnected, "weakness": p.weakness_timer, "gcd_progress": 1.0 if p.gcd == null else p.gcd.get_progress(), "combo_hit": -1 if p.combo == null else p.combo.current_hit, "position": [p.position.x, p.position.y, p.position.z] }) return { "type": "snapshot", "fight": { "phase": sim.raid_state.fight_phase, "time": sim.raid_state.fight_time, "boss_hp": sim.raid_state.boss_hp, "boss_max_hp": sim.raid_state.boss_max_hp, "boss_phase": sim.raid_state.boss_phase, "enrage_timer": sim.raid_state.enrage_timer, "soft_enrage": sim.raid_state.soft_enrage_active }, "players": players }