eq2go/internal/quests/types.go

602 lines
17 KiB
Go

package quests
import (
"sync"
"time"
)
// 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
}