602 lines
18 KiB
Go
602 lines
18 KiB
Go
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
|
|
} |