package model import ( "encoding/json" "time" ) // WSEnvelope is the wire format for all WebSocket messages (both directions). type WSEnvelope struct { Type string `json:"type"` Payload json.RawMessage `json:"payload"` } // NewWSEnvelope creates an envelope by marshaling the payload to JSON. // If marshaling fails, payload is set to "{}". func NewWSEnvelope(msgType string, payload any) WSEnvelope { raw, err := json.Marshal(payload) if err != nil { raw = []byte("{}") } return WSEnvelope{Type: msgType, Payload: raw} } // ClientMessage is a parsed inbound message from a WebSocket client, // tagged with the hero ID of the sending connection. type ClientMessage struct { HeroID int64 Type string Payload json.RawMessage } // --- Server -> Client payload types --- // HeroMovePayload is sent on the configured movement cadence while the hero is walking. type HeroMovePayload struct { X float64 `json:"x"` Y float64 `json:"y"` TargetX float64 `json:"targetX"` TargetY float64 `json:"targetY"` Speed float64 `json:"speed"` Heading float64 `json:"heading"` // radians } // PositionSyncPayload is sent on the configured sync cadence as drift correction. type PositionSyncPayload struct { X float64 `json:"x"` Y float64 `json:"y"` WaypointIndex int `json:"waypointIndex"` WaypointFraction float64 `json:"waypointFraction"` State string `json:"state"` } // RouteAssignedPayload is sent when the hero starts walking a new road segment. type RouteAssignedPayload struct { RoadID int64 `json:"roadId"` Waypoints []PointXY `json:"waypoints"` DestinationTownID int64 `json:"destinationTownId"` Speed float64 `json:"speed"` } // PointXY is a 2D coordinate used in route payloads. type PointXY struct { X float64 `json:"x"` Y float64 `json:"y"` } // CombatStartPayload is sent when combat begins. type CombatStartPayload struct { Enemy CombatEnemyInfo `json:"enemy"` } // CombatEnemyInfo is the enemy snapshot sent to the client on combat_start. // Type is the unique template slug (enemies.type) for rendering; Archetype is the family label. type CombatEnemyInfo struct { Name string `json:"name"` Type string `json:"type"` // slug — visual key Archetype string `json:"archetype,omitempty"` Biome string `json:"biome,omitempty"` Level int `json:"level,omitempty"` HP int `json:"hp"` MaxHP int `json:"maxHp"` Attack int `json:"attack"` Defense int `json:"defense"` Speed float64 `json:"speed"` IsElite bool `json:"isElite"` } // AttackPayload is sent on each swing during combat. type AttackPayload struct { Source string `json:"source"` // "hero", "enemy", "potion", "dot" (DoT tick), "summon" (minion) Damage int `json:"damage"` IsCrit bool `json:"isCrit,omitempty"` Outcome string `json:"outcome,omitempty"` // "hit", "dodge", "block", "stun" HeroHP int `json:"heroHp"` EnemyHP int `json:"enemyHp"` DebuffApplied string `json:"debuffApplied,omitempty"` } // EnemyRegenPayload is sent when an enemy regenerates HP during combat. type EnemyRegenPayload struct { Amount int `json:"amount"` EnemyHP int `json:"enemyHp"` } // CombatEndPayload is sent when the hero wins a fight. type CombatEndPayload struct { XPGained int64 `json:"xpGained"` GoldGained int64 `json:"goldGained"` Loot []LootItem `json:"loot,omitempty"` LeveledUp bool `json:"leveledUp"` NewLevel int `json:"newLevel,omitempty"` } // LootItem describes a single piece of loot in the combat_end payload. type LootItem struct { ItemType string `json:"itemType"` Name string `json:"name"` Rarity string `json:"rarity"` } // HeroDiedPayload is sent when the hero's HP reaches 0. type HeroDiedPayload struct { KilledBy string `json:"killedBy"` } // HeroRevivedPayload is sent after a revive. type HeroRevivedPayload struct { HP int `json:"hp"` } // TownNPCInfo describes an NPC in a town (town_enter payload). type TownNPCInfo struct { ID int64 `json:"id"` Name string `json:"name"` NameKey string `json:"nameKey,omitempty"` Type string `json:"type"` BuildingID *int64 `json:"buildingId,omitempty"` WorldX float64 `json:"worldX"` WorldY float64 `json:"worldY"` } // TownBuildingInfo describes a building in a town (town_enter payload). type TownBuildingInfo struct { ID int64 `json:"id"` BuildingType string `json:"buildingType"` WorldX float64 `json:"worldX"` WorldY float64 `json:"worldY"` Facing string `json:"facing"` FootprintW float64 `json:"footprintW"` FootprintH float64 `json:"footprintH"` } // TownEnterPayload is sent when a hero arrives at a town. type TownEnterPayload struct { TownID int64 `json:"townId"` TownName string `json:"townName"` TownNameKey string `json:"townNameKey,omitempty"` Biome string `json:"biome"` NPCs []TownNPCInfo `json:"npcs"` Buildings []TownBuildingInfo `json:"buildings"` RestDurationMs int64 `json:"restDurationMs"` } // TownNPCVisitPayload is sent when the hero finishes walking to an NPC visit (quest/shop/healer). // WorldX/WorldY are the hero's stand position (near the NPC), not the NPC tile center. type TownNPCVisitPayload struct { NPCID int64 `json:"npcId"` Name string `json:"name"` NameKey string `json:"nameKey,omitempty"` Type string `json:"type"` TownID int64 `json:"townId"` TownNameKey string `json:"townNameKey,omitempty"` WorldX float64 `json:"worldX"` WorldY float64 `json:"worldY"` } // TownTourPhasePayload is sent when the in-town tour phase or NPC context changes (ExcursionKindTown). type TownTourPhasePayload struct { Phase string `json:"phase"` TownID int64 `json:"townId"` TownNameKey string `json:"townNameKey,omitempty"` NpcID int64 `json:"npcId,omitempty"` NpcName string `json:"npcName,omitempty"` NpcNameKey string `json:"npcNameKey,omitempty"` NpcType string `json:"npcType,omitempty"` WorldX float64 `json:"worldX,omitempty"` WorldY float64 `json:"worldY,omitempty"` ExitPending bool `json:"exitPending,omitempty"` } // TownTourServiceEndPayload is sent when the NPC service phase ends by server timeout. type TownTourServiceEndPayload struct { Reason string `json:"reason"` // "timeout" } // AdventureLogLinePayload is sent when a new line is appended to the hero's adventure log. type AdventureLogLinePayload = AdventureLogLine // TownExitPayload is sent when the hero leaves a town. type TownExitPayload struct{} // MerchantLootPayload is sent after a successful wandering merchant trade (WS or REST when online). type MerchantLootPayload struct { GoldSpent int64 `json:"goldSpent"` ItemType string `json:"itemType"` // "potion", equipment slot key, etc. ItemName string `json:"itemName,omitempty"` Rarity string `json:"rarity,omitempty"` GoldAmount int64 `json:"goldAmount,omitempty"` // auto-sell gold } // NPCEncounterPayload is sent when the hero meets a wandering NPC on the road (e.g. merchant). type NPCEncounterPayload struct { NPCID int64 `json:"npcId"` NPCName string `json:"npcName"` NPCNameKey string `json:"npcNameKey,omitempty"` Role string `json:"role"` DialogueKey string `json:"dialogueKey,omitempty"` Cost int64 `json:"cost"` } // NPCEncounterEndPayload is sent when the wandering merchant prompt ends (e.g. timeout). type NPCEncounterEndPayload struct { Reason string `json:"reason"` // "timeout" } // LevelUpPayload is sent on level-up. type LevelUpPayload struct { NewLevel int `json:"newLevel"` } // BuffAppliedPayload is sent when a buff is activated. type BuffAppliedPayload struct { BuffType string `json:"buffType"` Duration float64 `json:"duration"` // seconds Magnitude float64 `json:"magnitude"` } // DebuffAppliedPayload is sent when a debuff is applied to the hero. type DebuffAppliedPayload struct { DebuffType string `json:"debuffType"` DurationMs int64 `json:"durationMs"` Magnitude float64 `json:"magnitude,omitempty"` ExpiresAt time.Time `json:"expiresAt"` } // ErrorPayload is sent when a client command fails validation. type ErrorPayload struct { Code string `json:"code"` Message string `json:"message"` } // --- Client -> Server payload types --- // ActivateBuffPayload is the payload for the activate_buff command. type ActivateBuffPayload struct { BuffType string `json:"buffType"` } // AcceptQuestPayload is the payload for the accept_quest command. type AcceptQuestPayload struct { QuestID int64 `json:"questId"` } // ClaimQuestPayload is the payload for the claim_quest command. type ClaimQuestPayload struct { QuestID int64 `json:"questId"` } // NPCInteractPayload is the payload for the npc_interact command. type NPCInteractPayload struct { NPCID int64 `json:"npcId"` } // ExcursionStartPayload is sent when a mini-adventure begins. type ExcursionStartPayload struct { DepthWorldUnits float64 `json:"depthWorldUnits"` } // ExcursionPhasePayload is sent when the excursion transitions between phases. type ExcursionPhasePayload struct { Phase string `json:"phase"` } // ExcursionEndPayload is sent when the mini-adventure completes. type ExcursionEndPayload struct{} // HeroMeetPartnerSnapshot is the other hero for UI (render + name). type HeroMeetPartnerSnapshot struct { ID int64 `json:"id"` Name string `json:"name"` Level int `json:"level"` ModelVariant int `json:"modelVariant"` PositionX float64 `json:"positionX"` PositionY float64 `json:"positionY"` } // HeroMeetStartPayload begins a paired meet session (server → client). // MeetPhase is "out" while walking to the stand, "meet" when dialogue is active (may be sent twice). type HeroMeetStartPayload struct { Partner HeroMeetPartnerSnapshot `json:"partner"` AnySideOnline bool `json:"anySideOnline"` PromptEndsAt *time.Time `json:"promptEndsAt,omitempty"` PartnerLingerMs int64 `json:"partnerLingerMs,omitempty"` MeetPhase string `json:"meetPhase,omitempty"` // "out" | "meet" } // HeroMeetLinePayload is one spoken line (player or scripted auto). type HeroMeetLinePayload struct { FromHeroID int64 `json:"fromHeroId"` Kind string `json:"kind"` // "player" | "scripted" Text string `json:"text,omitempty"` LineKey string `json:"lineKey,omitempty"` } // HeroMeetEndPayload ends the meet; client resumes walking and may linger partner visually. type HeroMeetEndPayload struct { Reason string `json:"reason"` // offline_timer | user_end | admin | partner_gone PartnerLingerMs int64 `json:"partnerLingerMs,omitempty"` } // HeroMeetSendMessagePayload is the client → server chat line (max 140 runes). type HeroMeetSendMessagePayload struct { Text string `json:"text"` }