eq2go/internal/quests/quest.go

737 lines
17 KiB
Go

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
}