package quests import ( "sync" "time" "eq2emu/internal/common" ) // Location represents a 3D location in a zone for quest steps type Location struct { ID int32 `json:"id"` X float32 `json:"x"` Y float32 `json:"y"` Z float32 `json:"z"` ZoneID int32 `json:"zone_id"` } // NewLocation creates a new location func NewLocation(id int32, x, y, z float32, zoneID int32) *Location { return &Location{ ID: id, X: x, Y: y, Z: z, ZoneID: zoneID, } } // QuestFactionPrereq represents faction requirements for a quest type QuestFactionPrereq struct { FactionID int32 `json:"faction_id"` Min int32 `json:"min"` Max int32 `json:"max"` } // NewQuestFactionPrereq creates a new faction prerequisite func NewQuestFactionPrereq(factionID, min, max int32) *QuestFactionPrereq { return &QuestFactionPrereq{ FactionID: factionID, Min: min, Max: max, } } // QuestStep represents a single step in a quest type QuestStep struct { // Basic step data ID int32 `json:"step_id"` Type int8 `json:"type"` Description string `json:"description"` TaskGroup string `json:"task_group"` Quantity int32 `json:"quantity"` StepProgress int32 `json:"step_progress"` Icon int16 `json:"icon"` MaxVariation float32 `json:"max_variation"` Percentage float32 `json:"percentage"` UsableItemID int32 `json:"usable_item_id"` // Tracking data UpdateName string `json:"update_name"` UpdateTargetName string `json:"update_target_name"` Updated bool `json:"updated"` // Step data (one of these will be populated based on type) IDs map[int32]bool `json:"ids,omitempty"` // For kill, chat, obtain item, etc. Locations []*Location `json:"locations,omitempty"` // For location steps // Thread safety mutex sync.RWMutex } // NewQuestStep creates a new quest step func NewQuestStep(id int32, stepType int8, description string, ids []int32, quantity int32, taskGroup string, locations []*Location, maxVariation, percentage float32, usableItemID int32) *QuestStep { step := &QuestStep{ ID: id, Type: stepType, Description: description, TaskGroup: taskGroup, Quantity: quantity, StepProgress: 0, Icon: DefaultIcon, MaxVariation: maxVariation, Percentage: percentage, UsableItemID: usableItemID, Updated: false, } // Initialize IDs map for non-location steps if stepType != StepTypeLocation && len(ids) > 0 { step.IDs = make(map[int32]bool) for _, id := range ids { step.IDs[id] = true } } // Initialize locations for location steps if stepType == StepTypeLocation && len(locations) > 0 { step.Locations = make([]*Location, len(locations)) copy(step.Locations, locations) } return step } // Copy creates a copy of a quest step func (qs *QuestStep) Copy() *QuestStep { qs.mutex.RLock() defer qs.mutex.RUnlock() newStep := &QuestStep{ ID: qs.ID, Type: qs.Type, Description: qs.Description, TaskGroup: qs.TaskGroup, Quantity: qs.Quantity, StepProgress: 0, // Reset progress for new quest copy Icon: qs.Icon, MaxVariation: qs.MaxVariation, Percentage: qs.Percentage, UsableItemID: qs.UsableItemID, UpdateName: qs.UpdateName, UpdateTargetName: qs.UpdateTargetName, Updated: false, } // Copy IDs map if qs.IDs != nil { newStep.IDs = make(map[int32]bool) for id, value := range qs.IDs { newStep.IDs[id] = value } } // Copy locations if qs.Locations != nil { newStep.Locations = make([]*Location, len(qs.Locations)) for i, loc := range qs.Locations { newStep.Locations[i] = &Location{ ID: loc.ID, X: loc.X, Y: loc.Y, Z: loc.Z, ZoneID: loc.ZoneID, } } } return newStep } // Complete checks if the step is complete func (qs *QuestStep) Complete() bool { qs.mutex.RLock() defer qs.mutex.RUnlock() return qs.StepProgress >= qs.Quantity } // SetComplete marks the step as complete func (qs *QuestStep) SetComplete() { qs.mutex.Lock() defer qs.mutex.Unlock() qs.StepProgress = qs.Quantity qs.Updated = true } // AddStepProgress adds progress to the step and returns the actual amount added func (qs *QuestStep) AddStepProgress(val int32) int32 { qs.mutex.Lock() defer qs.mutex.Unlock() qs.Updated = true remaining := qs.Quantity - qs.StepProgress if val > remaining { qs.StepProgress = qs.Quantity return remaining } qs.StepProgress += val return val } // SetStepProgress sets the progress directly func (qs *QuestStep) SetStepProgress(val int32) { qs.mutex.Lock() defer qs.mutex.Unlock() qs.StepProgress = val } // GetStepProgress returns current progress func (qs *QuestStep) GetStepProgress() int32 { qs.mutex.RLock() defer qs.mutex.RUnlock() return qs.StepProgress } // CheckStepReferencedID checks if an ID is referenced by this step func (qs *QuestStep) CheckStepReferencedID(id int32) bool { qs.mutex.RLock() defer qs.mutex.RUnlock() if qs.IDs != nil { _, exists := qs.IDs[id] return exists } return false } // CheckStepLocationUpdate checks if character location matches step requirements func (qs *QuestStep) CheckStepLocationUpdate(charX, charY, charZ float32, zoneID int32) bool { qs.mutex.RLock() defer qs.mutex.RUnlock() if qs.Locations == nil { return false } for _, loc := range qs.Locations { if loc.ZoneID > 0 && loc.ZoneID != zoneID { continue } // Calculate distance within max variation diffX := loc.X - charX if diffX < 0 { diffX = -diffX } if diffX <= qs.MaxVariation { diffZ := loc.Z - charZ if diffZ < 0 { diffZ = -diffZ } if diffZ <= qs.MaxVariation { totalDiff := diffX + diffZ if totalDiff <= qs.MaxVariation { diffY := loc.Y - charY if diffY < 0 { diffY = -diffY } if diffY <= qs.MaxVariation { totalDiff += diffY if totalDiff <= qs.MaxVariation { return true } } } } } } return false } // WasUpdated returns if step was updated func (qs *QuestStep) WasUpdated() bool { qs.mutex.RLock() defer qs.mutex.RUnlock() return qs.Updated } // SetWasUpdated sets the updated flag func (qs *QuestStep) SetWasUpdated(val bool) { qs.mutex.Lock() defer qs.mutex.Unlock() qs.Updated = val } // GetCurrentQuantity returns current progress as int16 func (qs *QuestStep) GetCurrentQuantity() int16 { qs.mutex.RLock() defer qs.mutex.RUnlock() return int16(qs.StepProgress) } // GetNeededQuantity returns required quantity as int16 func (qs *QuestStep) GetNeededQuantity() int16 { qs.mutex.RLock() defer qs.mutex.RUnlock() return int16(qs.Quantity) } // ResetTaskGroup clears the task group func (qs *QuestStep) ResetTaskGroup() { qs.mutex.Lock() defer qs.mutex.Unlock() qs.TaskGroup = "" } // SetTaskGroup sets the task group func (qs *QuestStep) SetTaskGroup(taskGroup string) { qs.mutex.Lock() defer qs.mutex.Unlock() qs.TaskGroup = taskGroup } // SetDescription sets the step description func (qs *QuestStep) SetDescription(description string) { qs.mutex.Lock() defer qs.mutex.Unlock() qs.Description = description } // SetUpdateName sets the update name func (qs *QuestStep) SetUpdateName(name string) { qs.mutex.Lock() defer qs.mutex.Unlock() qs.UpdateName = name } // SetUpdateTargetName sets the update target name func (qs *QuestStep) SetUpdateTargetName(name string) { qs.mutex.Lock() defer qs.mutex.Unlock() qs.UpdateTargetName = name } // SetIcon sets the step icon func (qs *QuestStep) SetIcon(icon int16) { qs.mutex.Lock() defer qs.mutex.Unlock() qs.Icon = icon } // Quest represents a complete quest with all its steps and requirements type Quest struct { // Basic quest information ID int32 `json:"quest_id"` Name string `json:"name"` Type string `json:"type"` Zone string `json:"zone"` Level int8 `json:"level"` EncounterLevel int8 `json:"encounter_level"` Description string `json:"description"` CompletedDesc string `json:"completed_description"` // Quest giver and return NPC QuestGiver int32 `json:"quest_giver"` ReturnID int32 `json:"return_id"` // Prerequisites PrereqLevel int8 `json:"prereq_level"` PrereqTSLevel int8 `json:"prereq_ts_level"` PrereqMaxLevel int8 `json:"prereq_max_level"` PrereqMaxTSLevel int8 `json:"prereq_max_ts_level"` PrereqFactions []*QuestFactionPrereq `json:"prereq_factions"` PrereqRaces []int8 `json:"prereq_races"` PrereqModelTypes []int16 `json:"prereq_model_types"` PrereqClasses []int8 `json:"prereq_classes"` PrereqTSClasses []int8 `json:"prereq_ts_classes"` PrereqQuests []int32 `json:"prereq_quests"` // Rewards RewardCoins int64 `json:"reward_coins"` RewardCoinsMax int64 `json:"reward_coins_max"` RewardFactions map[int32]int32 `json:"reward_factions"` RewardStatus int32 `json:"reward_status"` RewardComment string `json:"reward_comment"` RewardExp int32 `json:"reward_exp"` RewardTSExp int32 `json:"reward_ts_exp"` GeneratedCoin int64 `json:"generated_coin"` // Temporary rewards TmpRewardStatus int32 `json:"tmp_reward_status"` TmpRewardCoins int64 `json:"tmp_reward_coins"` // Steps and task groups QuestSteps []*QuestStep `json:"quest_steps"` QuestStepMap map[int32]*QuestStep `json:"-"` // For quick lookup QuestStepReverseMap map[*QuestStep]int32 `json:"-"` // Reverse lookup StepUpdates []*QuestStep `json:"-"` // Steps that were updated StepFailures []*QuestStep `json:"-"` // Steps that failed TaskGroupOrder map[int16]string `json:"task_group_order"` TaskGroup map[string][]*QuestStep `json:"-"` // Grouped steps TaskGroupNum int16 `json:"task_group_num"` // Actions CompleteActions map[int32]string `json:"complete_actions"` ProgressActions map[int32]string `json:"progress_actions"` FailedActions map[int32]string `json:"failed_actions"` CompleteAction string `json:"complete_action"` // State tracking Deleted bool `json:"deleted"` TurnedIn bool `json:"turned_in"` UpdateNeeded bool `json:"update_needed"` HasSentLastUpdate bool `json:"has_sent_last_update"` NeedsSave bool `json:"needs_save"` Visible int8 `json:"visible"` // Date tracking Day int8 `json:"day"` Month int8 `json:"month"` Year int8 `json:"year"` // Quest flags and settings FeatherColor int8 `json:"feather_color"` Repeatable bool `json:"repeatable"` Tracked bool `json:"tracked"` CompletedFlag bool `json:"completed_flag"` YellowName bool `json:"yellow_name"` QuestFlags int32 `json:"quest_flags"` Hidden bool `json:"hidden"` Status int32 `json:"status"` // Timer and completion tracking Timestamp int32 `json:"timestamp"` TimerStep int32 `json:"timer_step"` CompleteCount int16 `json:"complete_count"` // Temporary state QuestStateTemporary bool `json:"quest_state_temporary"` QuestTempDescription string `json:"quest_temp_description"` QuestShareableFlag int32 `json:"quest_shareable_flag"` CanDeleteQuest bool `json:"can_delete_quest"` StatusToEarnMin int32 `json:"status_to_earn_min"` StatusToEarnMax int32 `json:"status_to_earn_max"` HideReward bool `json:"hide_reward"` // Thread safety stepsMutex sync.RWMutex completeActionsMutex sync.RWMutex progressActionsMutex sync.RWMutex failedActionsMutex sync.RWMutex } // NewQuest creates a new quest with the given ID func NewQuest(id int32) *Quest { now := time.Now() quest := &Quest{ ID: id, PrereqLevel: DefaultPrereqLevel, PrereqTSLevel: 0, PrereqMaxLevel: 0, PrereqMaxTSLevel: 0, RewardCoins: 0, RewardCoinsMax: 0, CompletedFlag: false, HasSentLastUpdate: false, EncounterLevel: 0, RewardExp: 0, RewardTSExp: 0, FeatherColor: 0, Repeatable: false, YellowName: false, Hidden: false, GeneratedCoin: 0, QuestFlags: 0, Timestamp: 0, CompleteCount: 0, QuestStateTemporary: false, TmpRewardStatus: 0, TmpRewardCoins: 0, CompletedDesc: "", QuestTempDescription: "", QuestShareableFlag: 0, CanDeleteQuest: false, Status: 0, StatusToEarnMin: 0, StatusToEarnMax: 0, HideReward: false, Deleted: false, TurnedIn: false, UpdateNeeded: true, NeedsSave: false, TaskGroupNum: DefaultTaskGroupNum, Visible: DefaultVisible, Day: int8(now.Day()), Month: int8(now.Month()), Year: int8(now.Year() - 2000), // EQ2 uses 2-digit years // Initialize maps and slices QuestStepMap: make(map[int32]*QuestStep), QuestStepReverseMap: make(map[*QuestStep]int32), TaskGroupOrder: make(map[int16]string), TaskGroup: make(map[string][]*QuestStep), CompleteActions: make(map[int32]string), ProgressActions: make(map[int32]string), FailedActions: make(map[int32]string), RewardFactions: make(map[int32]int32), } return quest } // Copy creates a complete copy of a quest (used when giving quest to player) func (q *Quest) Copy() *Quest { q.stepsMutex.RLock() defer q.stepsMutex.RUnlock() newQuest := NewQuest(q.ID) // Copy basic information newQuest.Name = q.Name newQuest.Type = q.Type newQuest.Zone = q.Zone newQuest.Level = q.Level newQuest.EncounterLevel = q.EncounterLevel newQuest.Description = q.Description newQuest.CompletedDesc = q.CompletedDesc newQuest.QuestGiver = q.QuestGiver newQuest.ReturnID = q.ReturnID // Copy prerequisites newQuest.PrereqLevel = q.PrereqLevel newQuest.PrereqTSLevel = q.PrereqTSLevel newQuest.PrereqMaxLevel = q.PrereqMaxLevel newQuest.PrereqMaxTSLevel = q.PrereqMaxTSLevel // Copy prerequisite slices - create new slices newQuest.PrereqRaces = make([]int8, len(q.PrereqRaces)) copy(newQuest.PrereqRaces, q.PrereqRaces) newQuest.PrereqModelTypes = make([]int16, len(q.PrereqModelTypes)) copy(newQuest.PrereqModelTypes, q.PrereqModelTypes) newQuest.PrereqClasses = make([]int8, len(q.PrereqClasses)) copy(newQuest.PrereqClasses, q.PrereqClasses) newQuest.PrereqTSClasses = make([]int8, len(q.PrereqTSClasses)) copy(newQuest.PrereqTSClasses, q.PrereqTSClasses) newQuest.PrereqQuests = make([]int32, len(q.PrereqQuests)) copy(newQuest.PrereqQuests, q.PrereqQuests) // Copy faction prerequisites newQuest.PrereqFactions = make([]*QuestFactionPrereq, len(q.PrereqFactions)) for i, faction := range q.PrereqFactions { newQuest.PrereqFactions[i] = &QuestFactionPrereq{ FactionID: faction.FactionID, Min: faction.Min, Max: faction.Max, } } // Copy rewards newQuest.RewardCoins = q.RewardCoins newQuest.RewardCoinsMax = q.RewardCoinsMax newQuest.RewardStatus = q.RewardStatus newQuest.RewardComment = q.RewardComment newQuest.RewardExp = q.RewardExp newQuest.RewardTSExp = q.RewardTSExp newQuest.GeneratedCoin = q.GeneratedCoin // Copy reward factions map for factionID, amount := range q.RewardFactions { newQuest.RewardFactions[factionID] = amount } // Copy quest steps for _, step := range q.QuestSteps { newQuest.AddQuestStep(step.Copy()) } // Copy actions maps q.completeActionsMutex.RLock() for stepID, action := range q.CompleteActions { newQuest.CompleteActions[stepID] = action } q.completeActionsMutex.RUnlock() q.progressActionsMutex.RLock() for stepID, action := range q.ProgressActions { newQuest.ProgressActions[stepID] = action } q.progressActionsMutex.RUnlock() q.failedActionsMutex.RLock() for stepID, action := range q.FailedActions { newQuest.FailedActions[stepID] = action } q.failedActionsMutex.RUnlock() // Copy other properties newQuest.CompleteAction = q.CompleteAction newQuest.FeatherColor = q.FeatherColor newQuest.Repeatable = q.Repeatable newQuest.Hidden = q.Hidden newQuest.QuestFlags = q.QuestFlags newQuest.Status = q.Status newQuest.CompleteCount = q.CompleteCount newQuest.QuestShareableFlag = q.QuestShareableFlag newQuest.CanDeleteQuest = q.CanDeleteQuest newQuest.StatusToEarnMin = q.StatusToEarnMin newQuest.StatusToEarnMax = q.StatusToEarnMax newQuest.HideReward = q.HideReward newQuest.CompletedFlag = q.CompletedFlag newQuest.HasSentLastUpdate = q.HasSentLastUpdate newQuest.YellowName = q.YellowName // Reset state for new quest copy newQuest.StepUpdates = make([]*QuestStep, 0) newQuest.StepFailures = make([]*QuestStep, 0) newQuest.Deleted = false newQuest.UpdateNeeded = true newQuest.TurnedIn = false newQuest.QuestStateTemporary = false newQuest.TmpRewardStatus = 0 newQuest.TmpRewardCoins = 0 newQuest.QuestTempDescription = "" return newQuest }