package quests import ( "fmt" "math/rand" "strings" "time" ) // RegisterQuest sets the basic quest information func (q *Quest) RegisterQuest(name, questType, zone string, level int8, description string) { q.Name = name q.Type = questType q.Zone = zone q.Level = level q.Description = description q.SetSaveNeeded(true) } // AddQuestStep adds a step to the quest func (q *Quest) AddQuestStep(step *QuestStep) bool { q.stepsMutex.Lock() defer q.stepsMutex.Unlock() // Check if step ID already exists if _, exists := q.QuestStepMap[step.ID]; exists { return false } // Add to all tracking structures q.QuestSteps = append(q.QuestSteps, step) q.QuestStepMap[step.ID] = step q.QuestStepReverseMap[step] = step.ID // Handle task groups taskGroup := step.TaskGroup if taskGroup == "" { taskGroup = step.Description } if taskGroup != "" { // Add to task group order if new if _, exists := q.getTaskGroupByName(taskGroup); !exists { q.TaskGroupOrder[q.TaskGroupNum] = taskGroup q.TaskGroupNum++ } // Add step to task group q.TaskGroup[taskGroup] = append(q.TaskGroup[taskGroup], step) } q.SetSaveNeeded(true) return true } // CreateQuestStep creates and adds a new quest step func (q *Quest) CreateQuestStep(id int32, stepType int8, description string, ids []int32, quantity int32, taskGroup string, locations []*Location, maxVariation, percentage float32, usableItemID int32) *QuestStep { step := NewQuestStep(id, stepType, description, ids, quantity, taskGroup, locations, maxVariation, percentage, usableItemID) if q.AddQuestStep(step) { return step } return nil } // RemoveQuestStep removes a step from the quest func (q *Quest) RemoveQuestStep(stepID int32) bool { q.stepsMutex.Lock() defer q.stepsMutex.Unlock() step, exists := q.QuestStepMap[stepID] if !exists { return false } // Remove from maps delete(q.QuestStepMap, stepID) delete(q.QuestStepReverseMap, step) // Remove from slice for i, questStep := range q.QuestSteps { if questStep == step { q.QuestSteps = append(q.QuestSteps[:i], q.QuestSteps[i+1:]...) break } } // Remove from task groups taskGroup := step.TaskGroup if taskGroup != "" { if steps, exists := q.TaskGroup[taskGroup]; exists { for i, taskStep := range steps { if taskStep == step { q.TaskGroup[taskGroup] = append(steps[:i], steps[i+1:]...) break } } // If task group is now empty, remove it if len(q.TaskGroup[taskGroup]) == 0 { delete(q.TaskGroup, taskGroup) // Find and remove from task group order for orderNum, groupName := range q.TaskGroupOrder { if groupName == taskGroup { delete(q.TaskGroupOrder, orderNum) q.TaskGroupNum-- break } } } } } // Remove from actions q.completeActionsMutex.Lock() delete(q.CompleteActions, stepID) q.completeActionsMutex.Unlock() q.progressActionsMutex.Lock() delete(q.ProgressActions, stepID) q.progressActionsMutex.Unlock() q.failedActionsMutex.Lock() delete(q.FailedActions, stepID) q.failedActionsMutex.Unlock() q.SetSaveNeeded(true) return true } // GetQuestStep returns a quest step by ID func (q *Quest) GetQuestStep(stepID int32) *QuestStep { q.stepsMutex.RLock() defer q.stepsMutex.RUnlock() return q.QuestStepMap[stepID] } // SetStepComplete marks a step as complete func (q *Quest) SetStepComplete(stepID int32) bool { q.stepsMutex.Lock() defer q.stepsMutex.Unlock() step, exists := q.QuestStepMap[stepID] if !exists || step.Complete() { return false } step.SetComplete() q.StepUpdates = append(q.StepUpdates, step) q.SetSaveNeeded(true) return true } // AddStepProgress adds progress to a step func (q *Quest) AddStepProgress(stepID int32, progress int32) bool { q.stepsMutex.Lock() defer q.stepsMutex.Unlock() step, exists := q.QuestStepMap[stepID] if !exists { return false } // Check percentage chance for success if step.Percentage < MaxPercentage && step.Percentage > 0 { if step.Percentage <= rand.Float32()*MaxPercentage { q.StepFailures = append(q.StepFailures, step) return false } } actualProgress := step.AddStepProgress(progress) if actualProgress > 0 { q.StepUpdates = append(q.StepUpdates, step) q.SetSaveNeeded(true) // TODO: Call progress action if exists q.progressActionsMutex.RLock() if action, exists := q.ProgressActions[stepID]; exists && action != "" { // TODO: Execute Lua script with action _ = action // Placeholder for Lua execution } q.progressActionsMutex.RUnlock() return true } return false } // GetStepProgress returns the current progress of a step func (q *Quest) GetStepProgress(stepID int32) int32 { step := q.GetQuestStep(stepID) if step != nil { return step.GetStepProgress() } return 0 } // GetQuestStepCompleted checks if a step is completed func (q *Quest) GetQuestStepCompleted(stepID int32) bool { step := q.GetQuestStep(stepID) return step != nil && step.Complete() } // GetQuestStep returns the first incomplete step ID func (q *Quest) GetCurrentQuestStep() int16 { q.stepsMutex.RLock() defer q.stepsMutex.RUnlock() for _, step := range q.QuestSteps { if !step.Complete() { return int16(step.ID) } } return 0 } // QuestStepIsActive checks if a step is active (not completed) func (q *Quest) QuestStepIsActive(stepID int16) bool { step := q.GetQuestStep(int32(stepID)) return step != nil && !step.Complete() } // GetTaskGroupStep returns the current task group step func (q *Quest) GetTaskGroupStep() int16 { q.stepsMutex.RLock() defer q.stepsMutex.RUnlock() ret := int16(len(q.TaskGroupOrder)) for orderNum, taskGroupName := range q.TaskGroupOrder { if steps, exists := q.TaskGroup[taskGroupName]; exists { complete := true for _, step := range steps { if !step.Complete() { complete = false break } } if !complete && orderNum < ret { ret = orderNum } } } return ret } // CheckQuestReferencedSpawns checks if a spawn is referenced by any quest step func (q *Quest) CheckQuestReferencedSpawns(spawnID int32) bool { q.stepsMutex.RLock() defer q.stepsMutex.RUnlock() for _, step := range q.QuestSteps { if step.Complete() { continue } switch step.Type { case StepTypeKill, StepTypeNormal: if step.CheckStepReferencedID(spawnID) { return true } case StepTypeKillRaceReq: // TODO: Implement race requirement checking // This would require spawn race information } } return false } // CheckQuestKillUpdate checks and updates kill quest steps func (q *Quest) CheckQuestKillUpdate(spawnID int32, update bool) bool { q.stepsMutex.Lock() defer q.stepsMutex.Unlock() hasUpdate := false for _, step := range q.QuestSteps { if step.Complete() { continue } shouldUpdate := false switch step.Type { case StepTypeKill: shouldUpdate = step.CheckStepReferencedID(spawnID) case StepTypeKillRaceReq: // TODO: Implement race requirement checking // shouldUpdate = step.CheckStepKillRaceReqUpdate(spawn) } if shouldUpdate { if update { // Check percentage chance passed := true if step.Percentage < MaxPercentage { passed = step.Percentage > rand.Float32()*MaxPercentage } if passed { actualProgress := step.AddStepProgress(1) if actualProgress > 0 { q.StepUpdates = append(q.StepUpdates, step) // TODO: Call progress action q.progressActionsMutex.RLock() if action, exists := q.ProgressActions[step.ID]; exists && action != "" { // TODO: Execute Lua script _ = action } q.progressActionsMutex.RUnlock() hasUpdate = true } } else { q.StepFailures = append(q.StepFailures, step) } } else { hasUpdate = true } } } if hasUpdate && update { q.SetSaveNeeded(true) } return hasUpdate } // CheckQuestChatUpdate checks and updates chat quest steps func (q *Quest) CheckQuestChatUpdate(npcID int32, update bool) bool { q.stepsMutex.Lock() defer q.stepsMutex.Unlock() hasUpdate := false for _, step := range q.QuestSteps { if step.Complete() || step.Type != StepTypeChat { continue } if step.CheckStepReferencedID(npcID) { if update { actualProgress := step.AddStepProgress(1) if actualProgress > 0 { q.StepUpdates = append(q.StepUpdates, step) // TODO: Call progress action q.progressActionsMutex.RLock() if action, exists := q.ProgressActions[step.ID]; exists && action != "" { // TODO: Execute Lua script _ = action } q.progressActionsMutex.RUnlock() } } hasUpdate = true } } if hasUpdate && update { q.SetSaveNeeded(true) } return hasUpdate } // CheckQuestItemUpdate checks and updates item quest steps func (q *Quest) CheckQuestItemUpdate(itemID int32, quantity int8) bool { q.stepsMutex.Lock() defer q.stepsMutex.Unlock() hasUpdate := false for _, step := range q.QuestSteps { if step.Complete() || step.Type != StepTypeObtainItem { continue } if step.CheckStepReferencedID(itemID) { // Check percentage chance passed := true if step.Percentage < MaxPercentage { passed = step.Percentage > rand.Float32()*MaxPercentage } if passed { actualProgress := step.AddStepProgress(int32(quantity)) if actualProgress > 0 { q.StepUpdates = append(q.StepUpdates, step) // TODO: Call progress action q.progressActionsMutex.RLock() if action, exists := q.ProgressActions[step.ID]; exists && action != "" { // TODO: Execute Lua script _ = action } q.progressActionsMutex.RUnlock() } hasUpdate = true } else { q.StepFailures = append(q.StepFailures, step) } } } if hasUpdate { q.SetSaveNeeded(true) } return hasUpdate } // CheckQuestLocationUpdate checks and updates location quest steps func (q *Quest) CheckQuestLocationUpdate(charX, charY, charZ float32, zoneID int32) bool { q.stepsMutex.Lock() defer q.stepsMutex.Unlock() hasUpdate := false for _, step := range q.QuestSteps { if step.Complete() || step.Type != StepTypeLocation { continue } if step.CheckStepLocationUpdate(charX, charY, charZ, zoneID) { actualProgress := step.AddStepProgress(1) if actualProgress > 0 { q.StepUpdates = append(q.StepUpdates, step) // TODO: Call progress action q.progressActionsMutex.RLock() if action, exists := q.ProgressActions[step.ID]; exists && action != "" { // TODO: Execute Lua script _ = action } q.progressActionsMutex.RUnlock() } hasUpdate = true } } if hasUpdate { q.SetSaveNeeded(true) } return hasUpdate } // CheckQuestSpellUpdate checks and updates spell quest steps func (q *Quest) CheckQuestSpellUpdate(spellID int32) bool { q.stepsMutex.Lock() defer q.stepsMutex.Unlock() hasUpdate := false for _, step := range q.QuestSteps { if step.Complete() || step.Type != StepTypeSpell { continue } if step.CheckStepReferencedID(spellID) { // Check percentage chance passed := true if step.Percentage < MaxPercentage { passed = step.Percentage > rand.Float32()*MaxPercentage } if passed { actualProgress := step.AddStepProgress(1) if actualProgress > 0 { q.StepUpdates = append(q.StepUpdates, step) // TODO: Call progress action q.progressActionsMutex.RLock() if action, exists := q.ProgressActions[step.ID]; exists && action != "" { // TODO: Execute Lua script _ = action } q.progressActionsMutex.RUnlock() } hasUpdate = true } else { q.StepFailures = append(q.StepFailures, step) } } } if hasUpdate { q.SetSaveNeeded(true) } return hasUpdate } // CheckQuestRefIDUpdate checks and updates reference ID quest steps (craft/harvest) func (q *Quest) CheckQuestRefIDUpdate(refID int32, quantity int32) bool { q.stepsMutex.Lock() defer q.stepsMutex.Unlock() hasUpdate := false for _, step := range q.QuestSteps { if step.Complete() { continue } if step.Type == StepTypeHarvest || step.Type == StepTypeCraft { if step.CheckStepReferencedID(refID) { // Check percentage chance passed := true if step.Percentage < MaxPercentage { passed = step.Percentage > rand.Float32()*MaxPercentage } if passed { actualProgress := step.AddStepProgress(quantity) if actualProgress > 0 { q.StepUpdates = append(q.StepUpdates, step) // TODO: Call progress action q.progressActionsMutex.RLock() if action, exists := q.ProgressActions[step.ID]; exists && action != "" { // TODO: Execute Lua script _ = action } q.progressActionsMutex.RUnlock() } hasUpdate = true } else { q.StepFailures = append(q.StepFailures, step) } } } } if hasUpdate { q.SetSaveNeeded(true) } return hasUpdate } // GetCompleted checks if the quest is complete func (q *Quest) GetCompleted() bool { q.stepsMutex.RLock() defer q.stepsMutex.RUnlock() for _, step := range q.QuestSteps { if !step.Complete() { return false } } return true } // CheckCategoryYellow checks if the quest category should be displayed in yellow func (q *Quest) CheckCategoryYellow() bool { category := q.Type yellowCategories := []string{ "Signature", "Heritage", "Hallmark", "Deity", "Miscellaneous", "Language", "Lore and Legend", "World Event", "Tradeskill", } for _, yellowCat := range yellowCategories { if strings.EqualFold(category, yellowCat) { return true } } return false } // SetStepTimer sets a timer for a quest step func (q *Quest) SetStepTimer(duration int32) { if duration == 0 { q.Timestamp = 0 } else { q.Timestamp = int32(time.Now().Unix()) + duration } q.SetSaveNeeded(true) } // StepFailed handles when a step fails func (q *Quest) StepFailed(stepID int32) { q.failedActionsMutex.RLock() action, exists := q.FailedActions[stepID] q.failedActionsMutex.RUnlock() if exists && action != "" { // TODO: Execute Lua script for failed action _ = action } } // Helper methods // getTaskGroupByName finds a task group by name func (q *Quest) getTaskGroupByName(name string) ([]*QuestStep, bool) { steps, exists := q.TaskGroup[name] return steps, exists } // SetSaveNeeded sets the save needed flag func (q *Quest) SetSaveNeeded(needed bool) { q.NeedsSave = needed } // SetQuestTemporaryState sets the quest temporary state func (q *Quest) SetQuestTemporaryState(tempState bool, customDescription string) { if !tempState { q.TmpRewardCoins = 0 q.TmpRewardStatus = 0 // TODO: Clear temporary reward items } q.QuestStateTemporary = tempState q.QuestTempDescription = customDescription q.SetSaveNeeded(true) } // CanShareQuestCriteria checks if the quest meets sharing criteria func (q *Quest) CanShareQuestCriteria(hasQuest, hasCompleted bool, currentStep int16) bool { shareableFlag := q.QuestShareableFlag // Check if quest can be shared at all if shareableFlag == ShareableNone { return false } // Check completed sharing if (shareableFlag&ShareableCompleted) == 0 && hasCompleted { return false } // Check if can only share when completed if shareableFlag == ShareableCompleted && !hasCompleted { return false } // Check during quest sharing if (shareableFlag&ShareableDuring) == 0 && hasQuest && currentStep > 1 { return false } // Check active quest sharing if (shareableFlag&ShareableActive) == 0 && hasQuest { return false } // Check if has quest for sharing if (shareableFlag&ShareableCompleted) == 0 && !hasQuest { return false } return true } // Validation methods // ValidateQuest performs basic quest validation func (q *Quest) ValidateQuest() error { if q.ID <= 0 { return fmt.Errorf("quest ID must be positive") } if q.Name == "" { return fmt.Errorf("quest name cannot be empty") } if len(q.Name) > MaxQuestNameLength { return fmt.Errorf("quest name too long (max %d)", MaxQuestNameLength) } if len(q.Description) > MaxQuestDescriptionLength { return fmt.Errorf("quest description too long (max %d)", MaxQuestDescriptionLength) } if q.Level < 1 || q.Level > 100 { return fmt.Errorf("quest level must be between 1 and 100") } if len(q.QuestSteps) == 0 { return fmt.Errorf("quest must have at least one step") } // Validate steps for _, step := range q.QuestSteps { if err := q.validateStep(step); err != nil { return fmt.Errorf("step %d validation failed: %w", step.ID, err) } } return nil } // validateStep validates a single quest step func (q *Quest) validateStep(step *QuestStep) error { if step.ID <= 0 { return fmt.Errorf("step ID must be positive") } if step.Type < StepTypeKill || step.Type > StepTypeKillRaceReq { return fmt.Errorf("invalid step type: %d", step.Type) } if len(step.Description) > MaxStepDescriptionLength { return fmt.Errorf("step description too long (max %d)", MaxStepDescriptionLength) } if step.Quantity <= 0 { return fmt.Errorf("step quantity must be positive") } if step.Percentage < MinPercentage || step.Percentage > MaxPercentage { return fmt.Errorf("step percentage must be between %.1f and %.1f", MinPercentage, MaxPercentage) } // Type-specific validation switch step.Type { case StepTypeLocation: if len(step.Locations) == 0 { return fmt.Errorf("location step must have at least one location") } if step.MaxVariation < MinLocationVariation || step.MaxVariation > MaxLocationVariation { return fmt.Errorf("location max variation must be between %.1f and %.1f", MinLocationVariation, MaxLocationVariation) } default: if len(step.IDs) == 0 { return fmt.Errorf("non-location step must have at least one referenced ID") } } return nil }