convert more internals

This commit is contained in:
Sky Johnson 2025-07-30 16:34:08 -05:00
parent 1082b47942
commit a4f2ad4156
27 changed files with 18960 additions and 668 deletions

View File

@ -91,6 +91,7 @@ go run golang.org/x/vuln/cmd/govulncheck@latest ./...
- `appearances/` - Appearance management system with client version compatibility and efficient ID-based lookups
- `factions/` - Faction reputation system with player standings, consideration levels, and inter-faction relationships
- `ground_spawn/` - Harvestable resource node system with skill-based harvesting, rare item generation, and multi-attempt mechanics
- `languages/` - Multilingual character communication system with master language registry and player-specific language knowledge
### Network Protocol
EverQuest II UDP protocol with reliability layer, RC4 encryption, CRC validation, connection management, packet combining.
@ -227,6 +228,36 @@ XML-driven packet definitions with version-specific formats, conditional fields,
- `internal/ground_spawn/interfaces.go`: Integration interfaces with database, players, items, skills, and event handling systems
- `internal/ground_spawn/manager.go`: High-level ground spawn management with respawn scheduling, statistics, and command processing
**Languages System:**
- `internal/languages/constants.go`: Language ID constants, validation limits, and system configuration values
- `internal/languages/types.go`: Core Language struct, master language list, player language list, and statistics tracking
- `internal/languages/manager.go`: High-level language management with database integration, statistics, and command processing
- `internal/languages/interfaces.go`: Integration interfaces with database, players, chat processing, and event handling systems
**Quests System:**
- `internal/quests/constants.go`: Quest step types, display status flags, sharing constants, and validation limits
- `internal/quests/types.go`: Core Quest and QuestStep structures with complete quest data management and thread-safe operations
- `internal/quests/quest.go`: Core quest functionality including step management, progress tracking, update checking, and validation
- `internal/quests/prerequisites.go`: Quest prerequisite management with class, race, level, faction, and quest requirements
- `internal/quests/rewards.go`: Quest reward system with coins, experience, status, faction rewards, and level-based calculations
- `internal/quests/actions.go`: Quest action system for Lua script execution on completion, progress, and failure events
- `internal/quests/manager.go`: MasterQuestList and QuestManager for system-wide quest management and player quest tracking
- `internal/quests/interfaces.go`: Integration interfaces with player, client, spawn, item systems and QuestSystemAdapter for complete quest lifecycle management
**NPC System:**
- `internal/npc/constants.go`: AI strategy constants, randomization flags, pet types, cast types, and system limits
- `internal/npc/types.go`: Core NPC struct extending Entity, NPCSpell configurations, skill bonuses, movement locations, brain system
- `internal/npc/npc.go`: Complete NPC functionality with AI, combat, spell casting, movement, appearance randomization, and validation
- `internal/npc/manager.go`: High-level NPC management with zone indexing, appearance tracking, combat processing, and statistics
- `internal/npc/interfaces.go`: Integration interfaces with database, spell/skill/appearance systems, combat, movement, and entity adapters
**NPC AI System:**
- `internal/npc/ai/constants.go`: AI timing constants, combat ranges, hate limits, brain types, and decision parameters
- `internal/npc/ai/types.go`: HateList and EncounterList management, BrainState tracking, performance statistics, and thread-safe operations
- `internal/npc/ai/brain.go`: BaseBrain with complete AI logic including hate management, encounter tracking, spell/melee processing, and combat decisions
- `internal/npc/ai/variants.go`: Specialized brain types (CombatPet, NonCombatPet, Blank, Lua, DumbFire) with unique behaviors and factory functions
- `internal/npc/ai/interfaces.go`: Integration interfaces with NPC/Entity systems, AIManager for brain lifecycle, adapters, and debugging utilities
**Packet Definitions:**
- `internal/packets/xml/`: XML packet structure definitions
- `internal/packets/PARSER.md`: Packet definition language documentation
@ -307,6 +338,14 @@ Command-line flags override JSON configuration.
**Ground Spawn System**: Harvestable resource node system extending spawn functionality for gathering, mining, fishing, trapping, and foresting. Features skill-based table selection, probabilistic harvest outcomes (1/3/5/10 items + rare/imbue types), multi-attempt harvesting sessions, skill progression mechanics, and automatic respawn scheduling. Implements complex C++ harvest logic with table filtering by skill/level requirements, random item selection from filtered pools, grid-based location restrictions, and comprehensive item reward processing. Supports collection vs. harvesting skill differentiation, harvest message generation, spell integration for casting animations, and statistics tracking. Includes PlayerGroundSpawnAdapter and HarvestEventAdapter for seamless integration with player and event systems. Thread-safe operations with separate mutexes for harvest processing and usage handling.
**Languages System**: Multilingual character communication system managing language learning and chat processing. Features master language registry with ID-based and name-based lookups, individual player language collections with thread-safe operations, language validation and persistence, and comprehensive multilingual chat processing. Supports all EverQuest II racial languages (Common, Elvish, Dwarven, Halfling, Gnomish, Iksar, Trollish, Ogrish, Fae, Arasai, Sarnak, Froglok), language learning/forgetting mechanics, primary language selection, and message scrambling for unknown languages. Includes PlayerLanguageAdapter for seamless player integration, ChatLanguageProcessor for multilingual communication, and statistics tracking for language usage patterns. Thread-safe operations with efficient hash-based lookups and comprehensive validation systems.
**Quests System**: Complete quest management with quest definitions, step system, and real-time progress tracking. Features quest system with multiple step types (kill, chat, obtain item, location, spell, normal, craft, harvest, kill race requirements), comprehensive prerequisite system (level, class, race, faction, quest dependencies), flexible reward system (coins, experience, status points, faction reputation, items), step-based progress tracking with percentage-based success chances, task group organization for complex quests, Lua action system for completion/progress/failure events, quest sharing system with configurable sharing rules, repeatable quest support, player quest management with active quest tracking, master quest list with categorization and search, validation system for quest integrity, and thread-safe operations with proper mutex usage. Includes QuestSystemAdapter for complete quest lifecycle management and integration with player, client, spawn, and item systems.
**NPC System**: Non-player character system extending Entity with complete AI, combat, and spell casting capabilities. Features NPC struct with brain system, spell management (cast-on-spawn/aggro triggers), skill bonuses, movement with runback mechanics, appearance randomization (33+ flags for race/gender/colors/features), AI strategies (balanced/offensive/defensive), and combat state management. Includes NPCSpell configurations with HP ratio requirements, skill bonus system with spell-based modifications, movement locations with navigation pathfinding, timer system for pause/movement control, and comprehensive appearance randomization covering all EQ2 races and visual elements. Manager provides zone-based indexing, appearance tracking, combat processing, AI processing, statistics collection, and command interface. Integration interfaces support database persistence, spell/skill/appearance systems, combat management, movement control, and entity adapters for seamless system integration. Thread-safe operations with proper mutex usage and atomic flags for state management.
**NPC AI System**: Comprehensive artificial intelligence system for NPCs with hate management, encounter tracking, and specialized brain types. Features BaseBrain with complete AI logic including target selection, spell/melee processing, combat decisions, movement control, and runback mechanics. HateList provides thread-safe hate value tracking with percentage calculations and most-hated selection. EncounterList manages player/group participation for loot rights and rewards with character ID mapping. Specialized brain variants include CombatPetBrain (follows owner, assists in combat), NonCombatPetBrain (cosmetic pet following), BlankBrain (minimal processing), LuaBrain (script-controlled AI), and DumbFirePetBrain (temporary combat pets with expiration). BrainState tracks timing, spell recovery, active status, and debug levels. AIManager provides centralized brain lifecycle management with type-based creation, active brain processing, and performance statistics. Integration interfaces support NPC/Entity systems, Lua scripting, zone operations, and debugging utilities. Thread-safe operations with proper mutex usage and performance tracking for all AI operations.
All systems are converted from C++ with TODO comments marking areas for future implementation (LUA integration, advanced mechanics, etc.).
**Testing**: Focus testing on the UDP protocol layer and packet parsing, as these are critical for client compatibility.

View File

@ -1,582 +0,0 @@
/*
EQ2Emulator: Everquest II Server Emulator
Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net)
This file is part of EQ2Emulator.
EQ2Emulator is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
EQ2Emulator is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with EQ2Emulator. If not, see <http://www.gnu.org/licenses/>.
*/
#include "GroundSpawn.h"
#include "World.h"
#include "Spells.h"
#include "Rules/Rules.h"
#include "../common/MiscFunctions.h"
#include "../common/Log.h"
extern ConfigReader configReader;
extern MasterSpellList master_spell_list;
extern World world;
extern RuleManager rule_manager;
GroundSpawn::GroundSpawn(){
packet_num = 0;
appearance.difficulty = 0;
spawn_type = 2;
appearance.pos.state = 129;
number_harvests = 0;
num_attempts_per_harvest = 0;
groundspawn_id = 0;
MHarvest.SetName("GroundSpawn::MHarvest");
MHarvestUse.SetName("GroundSpawn::MHarvestUse");
randomize_heading = true; // we by default randomize heading of groundspawns DB overrides
}
GroundSpawn::~GroundSpawn(){
}
EQ2Packet* GroundSpawn::serialize(Player* player, int16 version){
return spawn_serialize(player, version);
}
int8 GroundSpawn::GetNumberHarvests(){
return number_harvests;
}
void GroundSpawn::SetNumberHarvests(int8 val){
number_harvests = val;
}
int8 GroundSpawn::GetAttemptsPerHarvest(){
return num_attempts_per_harvest;
}
void GroundSpawn::SetAttemptsPerHarvest(int8 val){
num_attempts_per_harvest = val;
}
int32 GroundSpawn::GetGroundSpawnEntryID(){
return groundspawn_id;
}
void GroundSpawn::SetGroundSpawnEntryID(int32 val){
groundspawn_id = val;
}
void GroundSpawn::SetCollectionSkill(const char* val){
if(val)
collection_skill = string(val);
}
const char* GroundSpawn::GetCollectionSkill(){
return collection_skill.c_str();
}
void GroundSpawn::ProcessHarvest(Client* client) {
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Process harvesting for player '%s' (%u)", client->GetPlayer()->GetName(), client->GetPlayer()->GetID());
MHarvest.lock();
vector<GroundSpawnEntry*>* groundspawn_entries = GetZone()->GetGroundSpawnEntries(groundspawn_id);
vector<GroundSpawnEntryItem*>* groundspawn_items = GetZone()->GetGroundSpawnEntryItems(groundspawn_id);
Item* master_item = 0;
Item* master_rare = 0;
Item* item = 0;
Item* item_rare = 0;
int16 lowest_skill_level = 0;
int16 table_choice = 0;
int32 item_choice = 0;
int32 rare_choice = 0;
int8 harvest_type = 0;
int32 item_harvested = 0;
int8 reward_total = 1;
int32 rare_harvested = 0;
int8 rare_item = 0;
bool is_collection = false;
if (!groundspawn_entries || !groundspawn_items) {
LogWrite(GROUNDSPAWN__ERROR, 3, "GSpawn", "No groundspawn entries or items assigned to groundspawn id: %u", groundspawn_id);
client->Message(CHANNEL_COLOR_RED, "Error: There are no groundspawn entries or items assigned to groundspawn id: %u", groundspawn_id);
MHarvest.unlock();
return;
}
if (number_harvests == 0) {
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Total harvests depleated for groundspawn id: %u", groundspawn_id);
client->SimpleMessage(CHANNEL_COLOR_RED, "Error: This spawn has nothing more to harvest!");
MHarvest.unlock();
return;
}
Skill* skill = 0;
if (collection_skill == "Collecting") {
skill = client->GetPlayer()->GetSkillByName("Gathering");
is_collection = true;
}
else
skill = client->GetPlayer()->GetSkillByName(collection_skill.c_str()); // Fix: #576 - don't skill up yet with GetSkillByName(skill, true), we might be trying to harvest low level
if (!skill) {
LogWrite(GROUNDSPAWN__WARNING, 3, "GSpawn", "Player '%s' lacks the skill: '%s'", client->GetPlayer()->GetName(), collection_skill.c_str());
client->Message(CHANNEL_COLOR_RED, "Error: You do not have the '%s' skill!", collection_skill.c_str());
MHarvest.unlock();
return;
}
int16 totalSkill = skill->current_val;
int32 skillID = master_item_list.GetItemStatIDByName(collection_skill);
int16 max_skill_req_groundspawn = rule_manager.GetZoneRule(client->GetCurrentZoneID(), R_Player, MinSkillMultiplierValue)->GetInt16();
if(max_skill_req_groundspawn < 1) // can't be 0
max_skill_req_groundspawn = 1;
if(skillID != 0xFFFFFFFF)
{
((Entity*)client->GetPlayer())->MStats.lock();
totalSkill += ((Entity*)client->GetPlayer())->stats[skillID];
((Entity*)client->GetPlayer())->MStats.unlock();
}
for (int8 i = 0; i < num_attempts_per_harvest; i++) {
vector<GroundSpawnEntry*> mod_groundspawn_entries;
if (groundspawn_entries) {
vector<GroundSpawnEntry*> highest_match;
vector<GroundSpawnEntry*>::iterator itr;
GroundSpawnEntry* entry = 0; // current data
GroundSpawnEntry* selected_table = 0; // selected table data
// first, iterate through groundspawn_entries, discard tables player cannot use
for (itr = groundspawn_entries->begin(); itr != groundspawn_entries->end(); itr++) {
entry = *itr;
if(entry->min_skill_level > max_skill_req_groundspawn)
max_skill_req_groundspawn = entry->min_skill_level;
// if player lacks skill, skip table
if (entry->min_skill_level > totalSkill)
continue;
// if bonus, but player lacks level, skip table
if (entry->bonus_table && (client->GetPlayer()->GetLevel() < entry->min_adventure_level))
continue;
// build modified entries table
mod_groundspawn_entries.push_back(entry);
LogWrite(GROUNDSPAWN__DEBUG, 5, "GSpawn", "Keeping groundspawn_entry: %i", entry->min_skill_level);
}
// if anything remains, find lowest min_skill_level in remaining set(s)
if (mod_groundspawn_entries.size() > 0) {
vector<GroundSpawnEntry*>::iterator itr;
GroundSpawnEntry* entry = 0;
for (itr = mod_groundspawn_entries.begin(); itr != mod_groundspawn_entries.end(); itr++) {
entry = *itr;
// find the low range of available tables for random roll
if (lowest_skill_level > entry->min_skill_level || lowest_skill_level == 0)
lowest_skill_level = entry->min_skill_level;
}
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Lowest Skill Level: %i", lowest_skill_level);
}
else {
// if no tables chosen, you must lack the skills
// TODO: move this check to LUA when harvest command is first selected
client->Message(CHANNEL_COLOR_RED, "You lack the skills to harvest this node!");
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "All groundspawn_entry tables tossed! No Skills? Something broke?");
MHarvest.unlock();
return;
}
// now roll to see which table to use
table_choice = MakeRandomInt(lowest_skill_level, totalSkill);
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Random INT for Table by skill level: %i", table_choice);
int16 highest_score = 0;
for (itr = mod_groundspawn_entries.begin(); itr != mod_groundspawn_entries.end(); itr++) {
entry = *itr;
// determines the highest min_skill_level in the current set of tables (if multiple tables)
if (table_choice >= entry->min_skill_level && (highest_score == 0 || highest_score < table_choice)) {
// removes old highest for the new one
highest_match.clear();
highest_score = entry->min_skill_level;
}
// if the score = level, push into highest_match set
if (highest_score == entry->min_skill_level)
highest_match.push_back(entry);
}
// if there is STILL more than 1 table player qualifies for, rand() and pick one
if (highest_match.size() > 1) {
int16 rand_index = rand() % highest_match.size();
selected_table = highest_match.at(rand_index);
}
else if (highest_match.size() > 0)
selected_table = highest_match.at(0);
// by this point, we should have 1 table who's min skill matches the score (selected_table)
if (selected_table) {
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Using Table: %i, %i, %i, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %i",
selected_table->min_skill_level,
selected_table->min_adventure_level,
selected_table->bonus_table,
selected_table->harvest1,
selected_table->harvest3,
selected_table->harvest5,
selected_table->harvest_imbue,
selected_table->harvest_rare,
selected_table->harvest10,
selected_table->harvest_coin);
// roll 1-100 for chance-to-harvest percentage
float chance = MakeRandomFloat(0, 100);
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Random FLOAT for harvest percentages: %.2f", chance);
// starting with the lowest %, select a harvest type + reward qty
if (chance <= selected_table->harvest10 && is_collection == false) {
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Harvest 10 items + Rare Item from table : %i", selected_table->min_skill_level);
harvest_type = 6;
reward_total = 10;
}
else if (chance <= selected_table->harvest_rare && is_collection == false) {
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Harvest Rare Item from table : %i", selected_table->min_skill_level);
harvest_type = 5;
}
else if (chance <= selected_table->harvest_imbue && is_collection == false) {
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Harvest Imbue Item from table : %i", selected_table->min_skill_level);
harvest_type = 4;
}
else if (chance <= selected_table->harvest5 && is_collection == false) {
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Harvest 5 Items from table : %i", selected_table->min_skill_level);
harvest_type = 3;
reward_total = 5;
}
else if (chance <= selected_table->harvest3 && is_collection == false) {
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Harvest 3 Items from table : %i", selected_table->min_skill_level);
harvest_type = 2;
reward_total = 3;
}
else if (chance <= selected_table->harvest1 || totalSkill >= skill->max_val || is_collection) {
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Harvest 1 Item from table : %i", selected_table->min_skill_level);
harvest_type = 1;
}
else
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Harvest nothing...");
float node_maxskill_multiplier = rule_manager.GetZoneRule(client->GetCurrentZoneID(), R_Player, HarvestSkillUpMultiplier)->GetFloat();
if(node_maxskill_multiplier <= 0.0f) {
node_maxskill_multiplier = 1.0f;
}
int16 skillup_max_skill_allowed = (int16)((float)max_skill_req_groundspawn*node_maxskill_multiplier);
if (!is_collection && skill && skill->current_val < skillup_max_skill_allowed) {
skill = client->GetPlayer()->GetSkillByName(collection_skill.c_str(), true); // Fix: #576 - skill up after min skill and adv level checks
}
}
// once you know how many and what type of item to harvest, pick an item from the list
if (harvest_type) {
vector<GroundSpawnEntryItem*> mod_groundspawn_items;
vector<GroundSpawnEntryItem*> mod_groundspawn_rares;
vector<GroundSpawnEntryItem*> mod_groundspawn_imbue;
vector<GroundSpawnEntryItem*>::iterator itr;
GroundSpawnEntryItem* entry = 0;
// iterate through groundspawn_items, discard items player cannot roll for
for (itr = groundspawn_items->begin(); itr != groundspawn_items->end(); itr++) {
entry = *itr;
// if this is a Rare, or an Imbue, but is_rare flag is 0, skip item
if ((harvest_type == 5 || harvest_type == 4) && entry->is_rare == 0)
continue;
// if it is a 1, 3, or 5 and is_rare = 1, skip
else if (harvest_type < 4 && entry->is_rare == 1)
continue;
// if the grid_id on the item matches player grid, or is 0, keep the item
if (!entry->grid_id || (entry->grid_id == client->GetPlayer()->GetLocation())) {
// build modified entries table
if ((entry->is_rare == 1 && harvest_type == 5) || (entry->is_rare == 1 && harvest_type == 6)) {
// if the matching item is rare, or harvest10 push to mod rares
mod_groundspawn_rares.push_back(entry);
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Keeping groundspawn_rare_item: %u", entry->item_id);
}
if (entry->is_rare == 0 && harvest_type != 4 && harvest_type != 5) {
// if the matching item is normal,or harvest 10 push to mod items
mod_groundspawn_items.push_back(entry);
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Keeping groundspawn_common_item: %u", entry->item_id);
}
if (entry->is_rare == 2 && harvest_type == 4) {
// if the matching item is imbue item, push to mod imbue
mod_groundspawn_imbue.push_back(entry);
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Keeping groundspawn_imbue_item: %u", entry->item_id);
}
}
}
// if any items remain in the list, random to see which one gets awarded
if (mod_groundspawn_items.size() > 0) {
// roll to see which item index to use
item_choice = rand() % mod_groundspawn_items.size();
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Random INT for which item to award: %i", item_choice);
// set item_id to be awarded
item_harvested = mod_groundspawn_items[item_choice]->item_id;
// if reward is rare, set flag
rare_item = mod_groundspawn_items[item_choice]->is_rare;
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Item ID to award: %u, Rare = %i", item_harvested, item_rare);
// if 10+rare, handle additional "rare" reward
if (harvest_type == 6) {
// make sure there is a rare table to choose from!
if (mod_groundspawn_rares.size() > 0) {
// roll to see which rare index to use
rare_choice = rand() % mod_groundspawn_rares.size();
// set (rare) item_id to be awarded
rare_harvested = mod_groundspawn_rares[rare_choice]->item_id;
// we're picking a rare here, so obviously this is true ;)
rare_item = 1;
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "RARE Item ID to award: %u", rare_harvested);
}
else {
// all rare entries were eliminated above, or none are assigned. Either way, shouldn't be here!
LogWrite(GROUNDSPAWN__ERROR, 3, "GSpawn", "Groundspawn Entry for '%s' (%i) has no RARE items!", GetName(), GetID());
}
}
}
else if (mod_groundspawn_rares.size() > 0) {
// roll to see which rare index to use
item_choice = rand() % mod_groundspawn_rares.size();
// set (rare) item_id to be awarded
item_harvested = mod_groundspawn_rares[item_choice]->item_id;
// we're picking a rare here, so obviously this is true ;)
rare_item = 1;
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "RARE Item ID to award: %u", rare_harvested);
}
else if (mod_groundspawn_imbue.size() > 0) {
// roll to see which rare index to use
item_choice = rand() % mod_groundspawn_imbue.size();
// set (rare) item_id to be awarded
item_harvested = mod_groundspawn_imbue[item_choice]->item_id;
// we're picking a rare here, so obviously this is true ;)
rare_item = 0;
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "imbue Item ID to award: %u", rare_harvested);
}
else {
// all item entries were eliminated above, or none are assigned. Either way, shouldn't be here!
LogWrite(GROUNDSPAWN__ERROR, 0, "GSpawn", "Groundspawn Entry for '%s' (%i) has no items!", GetName(), GetID());
}
// if an item was harvested, send updates to client, add item to inventory
if (item_harvested) {
char tmp[200] = { 0 };
// set Normal item harvested
master_item = master_item_list.GetItem(item_harvested);
if (master_item) {
// set details of Normal item
item = new Item(master_item);
// set how many of this item the player receives
item->details.count = reward_total;
// chat box update for normal item (todo: verify output text)
client->Message(CHANNEL_HARVESTING, "You %s %i %s from the %s.", GetHarvestMessageName(true).c_str(), item->details.count, item->CreateItemLink(client->GetVersion(), true).c_str(), GetName());
// add Normal item to player inventory
bool itemDeleted = false;
client->AddItem(item, &itemDeleted);
if(!itemDeleted) {
//Check if the player has a harvesting quest for this
client->GetPlayer()->CheckQuestsHarvestUpdate(item, reward_total);
// if this is a 10+rare, handle sepErately
if (harvest_type == 6 && rare_item == 1) {
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Item ID %u is Normal. Qty %i", item_harvested, item->details.count);
// send Normal harvest message to client
sprintf(tmp, "\\#64FFFFYou have %s:\12\\#C8FFFF%i %s", GetHarvestMessageName().c_str(), item->details.count, item->name.c_str());
client->SendPopupMessage(10, tmp, "ui_harvested_normal", 2.25, 0xFF, 0xFF, 0xFF);
client->GetPlayer()->UpdatePlayerStatistic(STAT_PLAYER_ITEMS_HARVESTED, item->details.count);
// set Rare item harvested
master_rare = master_item_list.GetItem(rare_harvested);
if (master_rare) {
// set details of Rare item
item_rare = new Item(master_rare);
// count of Rare is always 1
item_rare->details.count = 1;
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Item ID %u is RARE!", rare_harvested);
// send Rare harvest message to client
sprintf(tmp, "\\#FFFF6ERare item found!\12%s: \\#C8FFFF%i %s", GetHarvestMessageName().c_str(), item_rare->details.count, item_rare->name.c_str());
client->Message(CHANNEL_HARVESTING, "You have found a rare item!");
client->SendPopupMessage(11, tmp, "ui_harvested_rare", 2.25, 0xFF, 0xFF, 0xFF);
client->GetPlayer()->UpdatePlayerStatistic(STAT_PLAYER_RARES_HARVESTED, item_rare->details.count);
// chat box update for rare item (todo: verify output text)
client->Message(CHANNEL_HARVESTING, "You %s %i %s from the %s.", GetHarvestMessageName(true).c_str(), item_rare->details.count, item->CreateItemLink(client->GetVersion(), true).c_str(), GetName());
// add Rare item to player inventory
client->AddItem(item_rare);
//Check if the player has a harvesting quest for this
client->GetPlayer()->CheckQuestsHarvestUpdate(item_rare, 1);
}
}
else if (rare_item == 1) {
// if harvest signaled rare or imbue type
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Item ID %u is RARE! Qty: %i", item_harvested, item->details.count);
// send Rare harvest message to client
sprintf(tmp, "\\#FFFF6ERare item found!\12%s: \\#C8FFFF%i %s", GetHarvestMessageName().c_str(), item->details.count, item->name.c_str());
client->Message(CHANNEL_HARVESTING, "You have found a rare item!");
client->SendPopupMessage(11, tmp, "ui_harvested_rare", 2.25, 0xFF, 0xFF, 0xFF);
client->GetPlayer()->UpdatePlayerStatistic(STAT_PLAYER_RARES_HARVESTED, item->details.count);
}
else {
// send Normal harvest message to client
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "Item ID %u is Normal. Qty %i", item_harvested, item->details.count);
sprintf(tmp, "\\#64FFFFYou have %s:\12\\#C8FFFF%i %s", GetHarvestMessageName().c_str(), item->details.count, item->name.c_str());
client->SendPopupMessage(10, tmp, "ui_harvested_normal", 2.25, 0xFF, 0xFF, 0xFF);
client->GetPlayer()->UpdatePlayerStatistic(STAT_PLAYER_ITEMS_HARVESTED, item->details.count);
}
}
}
else {
// error!
LogWrite(GROUNDSPAWN__ERROR, 0, "GSpawn", "Error: Item ID Not Found - %u", item_harvested);
client->Message(CHANNEL_COLOR_RED, "Error: Unable to find item id %u", item_harvested);
}
// decrement # of pulls on this node before it despawns
number_harvests--;
}
else {
// if no item harvested
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "No item_harvested");
client->Message(CHANNEL_HARVESTING, "You failed to %s anything from %s.", GetHarvestMessageName(true, true).c_str(), GetName());
}
}
else {
// if no harvest type
LogWrite(GROUNDSPAWN__DEBUG, 3, "GSpawn", "No harvest_type");
client->Message(CHANNEL_HARVESTING, "You failed to %s anything from %s.", GetHarvestMessageName(true, true).c_str(), GetName());
}
}
} // cycle through num_attempts_per_harvest
MHarvest.unlock();
LogWrite(GROUNDSPAWN__DEBUG, 0, "GSpawn", "Process harvest complete for player '%s' (%u)", client->GetPlayer()->GetName(), client->GetPlayer()->GetID());
}
string GroundSpawn::GetHarvestMessageName(bool present_tense, bool failure){
string ret = "";
if((collection_skill == "Gathering" ||collection_skill == "Collecting") && !present_tense)
ret = "gathered";
else if(collection_skill == "Gathering" || collection_skill == "Collecting")
ret = "gather";
else if(collection_skill == "Mining" && !present_tense)
ret = "mined";
else if(collection_skill == "Mining")
ret = "mine";
else if (collection_skill == "Fishing" && !present_tense)
ret = "fished";
else if(collection_skill == "Fishing")
ret = "fish";
else if(collection_skill == "Trapping" && !present_tense && !failure)
ret = "acquired";
else if(collection_skill == "Trapping" && failure)
ret = "trap";
else if(collection_skill == "Trapping")
ret = "acquire";
else if(collection_skill == "Foresting" && !present_tense)
ret = "forested";
else if(collection_skill == "Foresting")
ret = "forest";
else if (collection_skill == "Collecting")
ret = "collect";
return ret;
}
string GroundSpawn::GetHarvestSpellType(){
string ret = "";
if(collection_skill == "Gathering" || collection_skill == "Collecting")
ret = "gather";
else if(collection_skill == "Mining")
ret = "mine";
else if(collection_skill == "Trapping")
ret = "trap";
else if(collection_skill == "Foresting")
ret = "chop";
else if(collection_skill == "Fishing")
ret = "fish";
return ret;
}
string GroundSpawn::GetHarvestSpellName() {
string ret = "";
if (collection_skill == "Collecting")
ret = "Gathering";
else
ret = collection_skill;
return ret;
}
void GroundSpawn::HandleUse(Client* client, string type){
if(!client || (client->GetVersion() > 561 && type.length() == 0)) // older clients do not send the type
return;
//The following check disables the use of the groundspawn if spawn access is not granted
if (client) {
bool meets_quest_reqs = MeetsSpawnAccessRequirements(client->GetPlayer());
if (!meets_quest_reqs && (GetQuestsRequiredOverride() & 2) == 0)
return;
else if (meets_quest_reqs && appearance.show_command_icon != 1)
return;
}
MHarvestUse.lock();
std::string typeLwr = ToLower(type);
if(client->GetVersion() <= 561 && (typeLwr == "" || typeLwr == "collect" || typeLwr == "gather" || typeLwr == "chop" || typeLwr == "mine"))
type = GetHarvestSpellType();
if (type == GetHarvestSpellType() && MeetsSpawnAccessRequirements(client->GetPlayer())) {
Spell* spell = master_spell_list.GetSpellByName(GetHarvestSpellName().c_str());
if (spell)
client->GetCurrentZone()->ProcessSpell(spell, client->GetPlayer(), client->GetPlayer()->GetTarget(), true, true);
}
else if (appearance.show_command_icon == 1 && MeetsSpawnAccessRequirements(client->GetPlayer())) {
EntityCommand* entity_command = FindEntityCommand(type);
if (entity_command)
client->GetCurrentZone()->ProcessEntityCommand(entity_command, client->GetPlayer(), client->GetPlayer()->GetTarget());
}
MHarvestUse.unlock();
}

View File

@ -1,86 +0,0 @@
/*
EQ2Emulator: Everquest II Server Emulator
Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net)
This file is part of EQ2Emulator.
EQ2Emulator is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
EQ2Emulator is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with EQ2Emulator. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef __EQ2_GroundSpawn__
#define __EQ2_GroundSpawn__
#include "Spawn.h"
#include "client.h"
#include "../common/Mutex.h"
class GroundSpawn : public Spawn {
public:
GroundSpawn();
virtual ~GroundSpawn();
GroundSpawn* Copy(){
GroundSpawn* new_spawn = new GroundSpawn();
new_spawn->size = size;
new_spawn->SetPrimaryCommands(&primary_command_list);
new_spawn->SetSecondaryCommands(&secondary_command_list);
new_spawn->database_id = database_id;
new_spawn->primary_command_list_id = primary_command_list_id;
new_spawn->secondary_command_list_id = secondary_command_list_id;
memcpy(&new_spawn->appearance, &appearance, sizeof(AppearanceData));
new_spawn->faction_id = faction_id;
new_spawn->target = 0;
new_spawn->SetTotalHP(GetTotalHP());
new_spawn->SetTotalPower(GetTotalPower());
new_spawn->SetHP(GetHP());
new_spawn->SetPower(GetPower());
new_spawn->SetNumberHarvests(number_harvests);
new_spawn->SetAttemptsPerHarvest(num_attempts_per_harvest);
new_spawn->SetGroundSpawnEntryID(groundspawn_id);
new_spawn->SetCollectionSkill(collection_skill.c_str());
SetQuestsRequired(new_spawn);
new_spawn->forceMapCheck = forceMapCheck;
new_spawn->SetOmittedByDBFlag(IsOmittedByDBFlag());
new_spawn->SetLootTier(GetLootTier());
new_spawn->SetLootDropType(GetLootDropType());
new_spawn->SetRandomizeHeading(GetRandomizeHeading());
return new_spawn;
}
bool IsGroundSpawn(){ return true; }
EQ2Packet* serialize(Player* player, int16 version);
int8 GetNumberHarvests();
void SetNumberHarvests(int8 val);
int8 GetAttemptsPerHarvest();
void SetAttemptsPerHarvest(int8 val);
int32 GetGroundSpawnEntryID();
void SetGroundSpawnEntryID(int32 val);
void ProcessHarvest(Client* client);
void SetCollectionSkill(const char* val);
const char* GetCollectionSkill();
string GetHarvestMessageName(bool present_tense = false, bool failure = false);
string GetHarvestSpellType();
string GetHarvestSpellName();
void HandleUse(Client* client, string type);
void SetRandomizeHeading(bool val) { randomize_heading = val; }
bool GetRandomizeHeading() { return randomize_heading; }
private:
int8 number_harvests;
int8 num_attempts_per_harvest;
int32 groundspawn_id;
string collection_skill;
Mutex MHarvest;
Mutex MHarvestUse;
bool randomize_heading;
};
#endif

7945
internal/Player.cpp Normal file

File diff suppressed because it is too large Load Diff

1276
internal/Player.h Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,39 @@
package languages
// Language system constants
const (
// Maximum language name length
MaxLanguageNameLength = 50
// Special language IDs (common in EQ2)
LanguageIDCommon = 0 // Common tongue (default)
LanguageIDElvish = 1 // Elvish
LanguageIDDwarven = 2 // Dwarven
LanguageIDHalfling = 3 // Halfling
LanguageIDGnomish = 4 // Gnomish
LanguageIDIksar = 5 // Iksar
LanguageIDTrollish = 6 // Trollish
LanguageIDOgrish = 7 // Ogrish
LanguageIDFae = 8 // Fae
LanguageIDArasai = 9 // Arasai
LanguageIDSarnak = 10 // Sarnak
LanguageIDFroglok = 11 // Froglok
)
// Language validation constants
const (
MinLanguageID = 0
MaxLanguageID = 999999 // Reasonable upper bound
)
// Database operation constants
const (
SaveStatusUnchanged = false
SaveStatusNeeded = true
)
// System limits
const (
MaxLanguagesPerPlayer = 100 // Reasonable limit to prevent abuse
MaxTotalLanguages = 1000 // System-wide language limit
)

View File

@ -0,0 +1,396 @@
package languages
// Database interface for language persistence
type Database interface {
LoadAllLanguages() ([]*Language, error)
SaveLanguage(language *Language) error
DeleteLanguage(languageID int32) error
LoadPlayerLanguages(playerID int32) ([]*Language, error)
SavePlayerLanguage(playerID int32, languageID int32) error
DeletePlayerLanguage(playerID int32, languageID int32) error
}
// Logger interface for language logging
type Logger interface {
LogInfo(message string, args ...interface{})
LogError(message string, args ...interface{})
LogDebug(message string, args ...interface{})
LogWarning(message string, args ...interface{})
}
// Player interface for language-related player operations
type Player interface {
GetCharacterID() int32
GetName() string
GetLanguages() *PlayerLanguagesList
KnowsLanguage(languageID int32) bool
LearnLanguage(languageID int32) error
ForgetLanguage(languageID int32) error
SendMessage(message string)
}
// Client interface for language-related client operations
type Client interface {
GetPlayer() *Player
GetVersion() int16
SendLanguageUpdate(languageData []byte) error
}
// LanguageProvider interface for systems that provide language functionality
type LanguageProvider interface {
GetMasterLanguagesList() *MasterLanguagesList
GetLanguage(languageID int32) *Language
GetLanguageByName(name string) *Language
CreatePlayerLanguagesList() *PlayerLanguagesList
}
// LanguageAware interface for entities that can understand languages
type LanguageAware interface {
GetKnownLanguages() *PlayerLanguagesList
KnowsLanguage(languageID int32) bool
GetPrimaryLanguage() int32
SetPrimaryLanguage(languageID int32)
CanUnderstand(languageID int32) bool
}
// LanguageHandler interface for handling language events
type LanguageHandler interface {
OnLanguageLearned(player *Player, languageID int32) error
OnLanguageForgotten(player *Player, languageID int32) error
OnLanguageUsed(player *Player, languageID int32, message string) error
}
// ChatProcessor interface for processing multilingual chat
type ChatProcessor interface {
ProcessMessage(speaker *Player, message string, languageID int32) (string, error)
FilterMessage(listener *Player, message string, languageID int32) string
GetLanguageSkramble(message string, comprehension float32) string
}
// PlayerLanguageAdapter provides language functionality for players
type PlayerLanguageAdapter struct {
player *Player
languages *PlayerLanguagesList
primaryLang int32
manager *Manager
logger Logger
}
// NewPlayerLanguageAdapter creates a new player language adapter
func NewPlayerLanguageAdapter(player *Player, manager *Manager, logger Logger) *PlayerLanguageAdapter {
return &PlayerLanguageAdapter{
player: player,
languages: manager.CreatePlayerLanguagesList(),
primaryLang: LanguageIDCommon, // Default to common
manager: manager,
logger: logger,
}
}
// GetKnownLanguages returns the player's known languages
func (pla *PlayerLanguageAdapter) GetKnownLanguages() *PlayerLanguagesList {
return pla.languages
}
// KnowsLanguage checks if the player knows a specific language
func (pla *PlayerLanguageAdapter) KnowsLanguage(languageID int32) bool {
return pla.languages.HasLanguage(languageID)
}
// GetPrimaryLanguage returns the player's primary language
func (pla *PlayerLanguageAdapter) GetPrimaryLanguage() int32 {
return pla.primaryLang
}
// SetPrimaryLanguage sets the player's primary language
func (pla *PlayerLanguageAdapter) SetPrimaryLanguage(languageID int32) {
// Only allow setting to a known language
if pla.languages.HasLanguage(languageID) {
pla.primaryLang = languageID
if pla.logger != nil {
lang := pla.manager.GetLanguage(languageID)
langName := "Unknown"
if lang != nil {
langName = lang.GetName()
}
pla.logger.LogDebug("Player %s set primary language to %s (%d)",
pla.player.GetName(), langName, languageID)
}
}
}
// CanUnderstand checks if the player can understand a language
func (pla *PlayerLanguageAdapter) CanUnderstand(languageID int32) bool {
// Common language is always understood
if languageID == LanguageIDCommon {
return true
}
// Check if player knows the language
return pla.languages.HasLanguage(languageID)
}
// LearnLanguage teaches the player a new language
func (pla *PlayerLanguageAdapter) LearnLanguage(languageID int32) error {
// Get the language from master list
language := pla.manager.GetLanguage(languageID)
if language == nil {
return fmt.Errorf("language with ID %d does not exist", languageID)
}
// Check if already known
if pla.languages.HasLanguage(languageID) {
return fmt.Errorf("player already knows language %s", language.GetName())
}
// Create a copy for the player
playerLang := language.Copy()
playerLang.SetSaveNeeded(true)
// Add to player's languages
if err := pla.languages.Add(playerLang); err != nil {
return fmt.Errorf("failed to add language to player: %w", err)
}
// Record usage statistics
pla.manager.RecordLanguageUsage(languageID)
if pla.logger != nil {
pla.logger.LogInfo("Player %s learned language %s (%d)",
pla.player.GetName(), language.GetName(), languageID)
}
return nil
}
// ForgetLanguage makes the player forget a language
func (pla *PlayerLanguageAdapter) ForgetLanguage(languageID int32) error {
// Cannot forget common language
if languageID == LanguageIDCommon {
return fmt.Errorf("cannot forget common language")
}
// Check if player knows the language
if !pla.languages.HasLanguage(languageID) {
return fmt.Errorf("player does not know language %d", languageID)
}
// Get language name for logging
language := pla.manager.GetLanguage(languageID)
langName := "Unknown"
if language != nil {
langName = language.GetName()
}
// Remove from player's languages
if !pla.languages.RemoveLanguage(languageID) {
return fmt.Errorf("failed to remove language from player")
}
// Reset primary language if this was it
if pla.primaryLang == languageID {
pla.primaryLang = LanguageIDCommon
}
if pla.logger != nil {
pla.logger.LogInfo("Player %s forgot language %s (%d)",
pla.player.GetName(), langName, languageID)
}
return nil
}
// LoadPlayerLanguages loads the player's languages from database
func (pla *PlayerLanguageAdapter) LoadPlayerLanguages(database Database) error {
if database == nil {
return fmt.Errorf("database is nil")
}
playerID := pla.player.GetCharacterID()
languages, err := database.LoadPlayerLanguages(playerID)
if err != nil {
return fmt.Errorf("failed to load player languages: %w", err)
}
// Clear current languages
pla.languages.Clear()
// Add loaded languages
for _, lang := range languages {
if err := pla.languages.Add(lang); err != nil && pla.logger != nil {
pla.logger.LogWarning("Failed to add loaded language %d to player %s: %v",
lang.GetID(), pla.player.GetName(), err)
}
}
// Ensure player knows common language
if !pla.languages.HasLanguage(LanguageIDCommon) {
commonLang := pla.manager.GetLanguage(LanguageIDCommon)
if commonLang != nil {
playerCommon := commonLang.Copy()
pla.languages.Add(playerCommon)
}
}
if pla.logger != nil {
pla.logger.LogDebug("Loaded %d languages for player %s",
len(languages), pla.player.GetName())
}
return nil
}
// SavePlayerLanguages saves the player's languages to database
func (pla *PlayerLanguageAdapter) SavePlayerLanguages(database Database) error {
if database == nil {
return fmt.Errorf("database is nil")
}
playerID := pla.player.GetCharacterID()
languages := pla.languages.GetAllLanguages()
// Save each language that needs saving
for _, lang := range languages {
if lang.GetSaveNeeded() {
if err := database.SavePlayerLanguage(playerID, lang.GetID()); err != nil {
return fmt.Errorf("failed to save player language %d: %w", lang.GetID(), err)
}
lang.SetSaveNeeded(false)
}
}
if pla.logger != nil {
pla.logger.LogDebug("Saved languages for player %s", pla.player.GetName())
}
return nil
}
// ChatLanguageProcessor handles multilingual chat processing
type ChatLanguageProcessor struct {
manager *Manager
logger Logger
}
// NewChatLanguageProcessor creates a new chat language processor
func NewChatLanguageProcessor(manager *Manager, logger Logger) *ChatLanguageProcessor {
return &ChatLanguageProcessor{
manager: manager,
logger: logger,
}
}
// ProcessMessage processes a chat message in a specific language
func (clp *ChatLanguageProcessor) ProcessMessage(speaker *Player, message string, languageID int32) (string, error) {
if speaker == nil {
return "", fmt.Errorf("speaker cannot be nil")
}
// Validate language exists
language := clp.manager.GetLanguage(languageID)
if language == nil {
return "", fmt.Errorf("language %d does not exist", languageID)
}
// Check if speaker knows the language
if !speaker.KnowsLanguage(languageID) {
return "", fmt.Errorf("speaker does not know language %s", language.GetName())
}
// Record language usage
clp.manager.RecordLanguageUsage(languageID)
return message, nil
}
// FilterMessage filters a message for a listener based on language comprehension
func (clp *ChatLanguageProcessor) FilterMessage(listener *Player, message string, languageID int32) string {
if listener == nil {
return message
}
// Common language is always understood
if languageID == LanguageIDCommon {
return message
}
// Check if listener knows the language
if listener.KnowsLanguage(languageID) {
return message
}
// Scramble the message for unknown languages
return clp.GetLanguageSkramble(message, 0.0)
}
// GetLanguageSkramble scrambles a message based on comprehension level
func (clp *ChatLanguageProcessor) GetLanguageSkramble(message string, comprehension float32) string {
if comprehension >= 1.0 {
return message
}
if comprehension <= 0.0 {
// Complete scramble - replace with gibberish
runes := []rune(message)
scrambled := make([]rune, len(runes))
for i, r := range runes {
if r == ' ' {
scrambled[i] = ' '
} else if r >= 'a' && r <= 'z' {
scrambled[i] = 'a' + rune((int(r-'a')+7)%26)
} else if r >= 'A' && r <= 'Z' {
scrambled[i] = 'A' + rune((int(r-'A')+7)%26)
} else {
scrambled[i] = r
}
}
return string(scrambled)
}
// Partial comprehension - scramble some words
// This is a simplified implementation
return message
}
// LanguageEventAdapter handles language-related events
type LanguageEventAdapter struct {
handler LanguageHandler
logger Logger
}
// NewLanguageEventAdapter creates a new language event adapter
func NewLanguageEventAdapter(handler LanguageHandler, logger Logger) *LanguageEventAdapter {
return &LanguageEventAdapter{
handler: handler,
logger: logger,
}
}
// ProcessLanguageEvent processes a language-related event
func (lea *LanguageEventAdapter) ProcessLanguageEvent(eventType string, player *Player, languageID int32, data interface{}) {
if lea.handler == nil {
return
}
switch eventType {
case "language_learned":
if err := lea.handler.OnLanguageLearned(player, languageID); err != nil && lea.logger != nil {
lea.logger.LogError("Language learned handler failed: %v", err)
}
case "language_forgotten":
if err := lea.handler.OnLanguageForgotten(player, languageID); err != nil && lea.logger != nil {
lea.logger.LogError("Language forgotten handler failed: %v", err)
}
case "language_used":
if message, ok := data.(string); ok {
if err := lea.handler.OnLanguageUsed(player, languageID, message); err != nil && lea.logger != nil {
lea.logger.LogError("Language used handler failed: %v", err)
}
}
}
}

View File

@ -0,0 +1,525 @@
package languages
import (
"fmt"
"sync"
)
// Manager provides high-level management of the language system
type Manager struct {
masterLanguagesList *MasterLanguagesList
database Database
logger Logger
mutex sync.RWMutex
// Statistics
languageLookups int64
playersWithLanguages int64
languageUsageCount map[int32]int64 // Language ID -> usage count
}
// NewManager creates a new language manager
func NewManager(database Database, logger Logger) *Manager {
return &Manager{
masterLanguagesList: NewMasterLanguagesList(),
database: database,
logger: logger,
languageUsageCount: make(map[int32]int64),
}
}
// Initialize loads languages from database
func (m *Manager) Initialize() error {
if m.logger != nil {
m.logger.LogInfo("Initializing language manager...")
}
if m.database == nil {
if m.logger != nil {
m.logger.LogWarning("No database provided, starting with empty language list")
}
return nil
}
// Load languages from database
languages, err := m.database.LoadAllLanguages()
if err != nil {
return fmt.Errorf("failed to load languages from database: %w", err)
}
for _, language := range languages {
if err := m.masterLanguagesList.AddLanguage(language); err != nil {
if m.logger != nil {
m.logger.LogError("Failed to add language %d (%s): %v", language.GetID(), language.GetName(), err)
}
}
}
if m.logger != nil {
m.logger.LogInfo("Loaded %d languages from database", len(languages))
}
return nil
}
// GetMasterLanguagesList returns the master language list
func (m *Manager) GetMasterLanguagesList() *MasterLanguagesList {
return m.masterLanguagesList
}
// CreatePlayerLanguagesList creates a new player language list
func (m *Manager) CreatePlayerLanguagesList() *PlayerLanguagesList {
m.mutex.Lock()
m.playersWithLanguages++
m.mutex.Unlock()
return NewPlayerLanguagesList()
}
// GetLanguage returns a language by ID
func (m *Manager) GetLanguage(id int32) *Language {
m.mutex.Lock()
m.languageLookups++
m.mutex.Unlock()
return m.masterLanguagesList.GetLanguage(id)
}
// GetLanguageByName returns a language by name
func (m *Manager) GetLanguageByName(name string) *Language {
m.mutex.Lock()
m.languageLookups++
m.mutex.Unlock()
return m.masterLanguagesList.GetLanguageByName(name)
}
// AddLanguage adds a new language to the system
func (m *Manager) AddLanguage(language *Language) error {
if language == nil {
return fmt.Errorf("language cannot be nil")
}
// Add to master list
if err := m.masterLanguagesList.AddLanguage(language); err != nil {
return fmt.Errorf("failed to add language to master list: %w", err)
}
// Save to database if available
if m.database != nil {
if err := m.database.SaveLanguage(language); err != nil {
// Remove from master list if database save failed
m.masterLanguagesList.RemoveLanguage(language.GetID())
return fmt.Errorf("failed to save language to database: %w", err)
}
}
if m.logger != nil {
m.logger.LogInfo("Added language %d: %s", language.GetID(), language.GetName())
}
return nil
}
// UpdateLanguage updates an existing language
func (m *Manager) UpdateLanguage(language *Language) error {
if language == nil {
return fmt.Errorf("language cannot be nil")
}
// Update in master list
if err := m.masterLanguagesList.UpdateLanguage(language); err != nil {
return fmt.Errorf("failed to update language in master list: %w", err)
}
// Save to database if available
if m.database != nil {
if err := m.database.SaveLanguage(language); err != nil {
return fmt.Errorf("failed to save language to database: %w", err)
}
}
if m.logger != nil {
m.logger.LogInfo("Updated language %d: %s", language.GetID(), language.GetName())
}
return nil
}
// RemoveLanguage removes a language from the system
func (m *Manager) RemoveLanguage(id int32) error {
// Check if language exists
if !m.masterLanguagesList.HasLanguage(id) {
return fmt.Errorf("language with ID %d does not exist", id)
}
// Remove from database first if available
if m.database != nil {
if err := m.database.DeleteLanguage(id); err != nil {
return fmt.Errorf("failed to delete language from database: %w", err)
}
}
// Remove from master list
if !m.masterLanguagesList.RemoveLanguage(id) {
return fmt.Errorf("failed to remove language from master list")
}
if m.logger != nil {
m.logger.LogInfo("Removed language %d", id)
}
return nil
}
// RecordLanguageUsage records language usage for statistics
func (m *Manager) RecordLanguageUsage(languageID int32) {
m.mutex.Lock()
defer m.mutex.Unlock()
m.languageUsageCount[languageID]++
}
// GetStatistics returns language system statistics
func (m *Manager) GetStatistics() *LanguageStatistics {
m.mutex.RLock()
defer m.mutex.RUnlock()
// Create language name mapping
languagesByName := make(map[string]int32)
allLanguages := m.masterLanguagesList.GetAllLanguages()
for _, lang := range allLanguages {
languagesByName[lang.GetName()] = lang.GetID()
}
// Copy usage count
usageCount := make(map[int32]int64)
for id, count := range m.languageUsageCount {
usageCount[id] = count
}
return &LanguageStatistics{
TotalLanguages: len(allLanguages),
PlayersWithLanguages: int(m.playersWithLanguages),
LanguageUsageCount: usageCount,
LanguageLookups: m.languageLookups,
LanguagesByName: languagesByName,
}
}
// ResetStatistics resets all statistics
func (m *Manager) ResetStatistics() {
m.mutex.Lock()
defer m.mutex.Unlock()
m.languageLookups = 0
m.playersWithLanguages = 0
m.languageUsageCount = make(map[int32]int64)
}
// ValidateAllLanguages validates all languages in the system
func (m *Manager) ValidateAllLanguages() []string {
allLanguages := m.masterLanguagesList.GetAllLanguages()
var issues []string
for _, lang := range allLanguages {
if !lang.IsValid() {
issues = append(issues, fmt.Sprintf("Language %d (%s) is invalid", lang.GetID(), lang.GetName()))
}
}
return issues
}
// ReloadFromDatabase reloads all languages from database
func (m *Manager) ReloadFromDatabase() error {
if m.database == nil {
return fmt.Errorf("no database available")
}
// Clear current languages
m.masterLanguagesList.Clear()
// Reload from database
return m.Initialize()
}
// GetLanguageCount returns the total number of languages
func (m *Manager) GetLanguageCount() int32 {
return m.masterLanguagesList.Size()
}
// ProcessCommand handles language-related commands
func (m *Manager) ProcessCommand(command string, args []string) (string, error) {
switch command {
case "stats":
return m.handleStatsCommand(args)
case "validate":
return m.handleValidateCommand(args)
case "list":
return m.handleListCommand(args)
case "info":
return m.handleInfoCommand(args)
case "reload":
return m.handleReloadCommand(args)
case "add":
return m.handleAddCommand(args)
case "remove":
return m.handleRemoveCommand(args)
case "search":
return m.handleSearchCommand(args)
default:
return "", fmt.Errorf("unknown language command: %s", command)
}
}
// handleStatsCommand shows language system statistics
func (m *Manager) handleStatsCommand(args []string) (string, error) {
stats := m.GetStatistics()
result := "Language System Statistics:\n"
result += fmt.Sprintf("Total Languages: %d\n", stats.TotalLanguages)
result += fmt.Sprintf("Players with Languages: %d\n", stats.PlayersWithLanguages)
result += fmt.Sprintf("Language Lookups: %d\n", stats.LanguageLookups)
if len(stats.LanguageUsageCount) > 0 {
result += "\nMost Used Languages:\n"
// Show top 5 most used languages
count := 0
for langID, usage := range stats.LanguageUsageCount {
if count >= 5 {
break
}
lang := m.GetLanguage(langID)
name := "Unknown"
if lang != nil {
name = lang.GetName()
}
result += fmt.Sprintf(" %s (ID: %d): %d uses\n", name, langID, usage)
count++
}
}
return result, nil
}
// handleValidateCommand validates all languages
func (m *Manager) handleValidateCommand(args []string) (string, error) {
issues := m.ValidateAllLanguages()
if len(issues) == 0 {
return "All languages are valid.", nil
}
result := fmt.Sprintf("Found %d issues with languages:\n", len(issues))
for i, issue := range issues {
if i >= 10 { // Limit output
result += "... (and more)\n"
break
}
result += fmt.Sprintf("%d. %s\n", i+1, issue)
}
return result, nil
}
// handleListCommand lists languages
func (m *Manager) handleListCommand(args []string) (string, error) {
languages := m.masterLanguagesList.GetAllLanguages()
if len(languages) == 0 {
return "No languages loaded.", nil
}
result := fmt.Sprintf("Languages (%d):\n", len(languages))
count := 0
for _, language := range languages {
if count >= 20 { // Limit output
result += "... (and more)\n"
break
}
result += fmt.Sprintf(" %d: %s\n", language.GetID(), language.GetName())
count++
}
return result, nil
}
// handleInfoCommand shows information about a specific language
func (m *Manager) handleInfoCommand(args []string) (string, error) {
if len(args) == 0 {
return "", fmt.Errorf("language ID or name required")
}
var language *Language
// Try to parse as ID first
var languageID int32
if _, err := fmt.Sscanf(args[0], "%d", &languageID); err == nil {
language = m.GetLanguage(languageID)
} else {
// Try as name
language = m.GetLanguageByName(args[0])
}
if language == nil {
return fmt.Sprintf("Language '%s' not found.", args[0]), nil
}
result := fmt.Sprintf("Language Information:\n")
result += fmt.Sprintf("ID: %d\n", language.GetID())
result += fmt.Sprintf("Name: %s\n", language.GetName())
result += fmt.Sprintf("Save Needed: %v\n", language.GetSaveNeeded())
// Show usage statistics if available
m.mutex.RLock()
if usage, exists := m.languageUsageCount[language.GetID()]; exists {
result += fmt.Sprintf("Usage Count: %d\n", usage)
} else {
result += "Usage Count: 0\n"
}
m.mutex.RUnlock()
return result, nil
}
// handleReloadCommand reloads languages from database
func (m *Manager) handleReloadCommand(args []string) (string, error) {
if err := m.ReloadFromDatabase(); err != nil {
return "", fmt.Errorf("failed to reload languages: %w", err)
}
count := m.GetLanguageCount()
return fmt.Sprintf("Successfully reloaded %d languages from database.", count), nil
}
// handleAddCommand adds a new language
func (m *Manager) handleAddCommand(args []string) (string, error) {
if len(args) < 2 {
return "", fmt.Errorf("usage: add <id> <name>")
}
var id int32
if _, err := fmt.Sscanf(args[0], "%d", &id); err != nil {
return "", fmt.Errorf("invalid language ID: %s", args[0])
}
name := args[1]
language := NewLanguage()
language.SetID(id)
language.SetName(name)
language.SetSaveNeeded(true)
if err := m.AddLanguage(language); err != nil {
return "", fmt.Errorf("failed to add language: %w", err)
}
return fmt.Sprintf("Successfully added language %d: %s", id, name), nil
}
// handleRemoveCommand removes a language
func (m *Manager) handleRemoveCommand(args []string) (string, error) {
if len(args) == 0 {
return "", fmt.Errorf("language ID required")
}
var id int32
if _, err := fmt.Sscanf(args[0], "%d", &id); err != nil {
return "", fmt.Errorf("invalid language ID: %s", args[0])
}
if err := m.RemoveLanguage(id); err != nil {
return "", fmt.Errorf("failed to remove language: %w", err)
}
return fmt.Sprintf("Successfully removed language %d", id), nil
}
// handleSearchCommand searches for languages by name
func (m *Manager) handleSearchCommand(args []string) (string, error) {
if len(args) == 0 {
return "", fmt.Errorf("search term required")
}
searchTerm := args[0]
languages := m.masterLanguagesList.GetAllLanguages()
var results []*Language
// Search by name (case-insensitive partial match)
for _, language := range languages {
if contains(language.GetName(), searchTerm) {
results = append(results, language)
}
}
if len(results) == 0 {
return fmt.Sprintf("No languages found matching '%s'.", searchTerm), nil
}
result := fmt.Sprintf("Found %d languages matching '%s':\n", len(results), searchTerm)
for i, language := range results {
if i >= 20 { // Limit output
result += "... (and more)\n"
break
}
result += fmt.Sprintf(" %d: %s\n", language.GetID(), language.GetName())
}
return result, nil
}
// Shutdown gracefully shuts down the manager
func (m *Manager) Shutdown() {
if m.logger != nil {
m.logger.LogInfo("Shutting down language manager...")
}
// Clear languages
m.masterLanguagesList.Clear()
}
// contains checks if a string contains a substring (case-insensitive)
func contains(str, substr string) bool {
if len(substr) == 0 {
return true
}
if len(str) < len(substr) {
return false
}
// Convert to lowercase for case-insensitive comparison
strLower := make([]byte, len(str))
substrLower := make([]byte, len(substr))
for i := 0; i < len(str); i++ {
if str[i] >= 'A' && str[i] <= 'Z' {
strLower[i] = str[i] + 32
} else {
strLower[i] = str[i]
}
}
for i := 0; i < len(substr); i++ {
if substr[i] >= 'A' && substr[i] <= 'Z' {
substrLower[i] = substr[i] + 32
} else {
substrLower[i] = substr[i]
}
}
for i := 0; i <= len(strLower)-len(substrLower); i++ {
match := true
for j := 0; j < len(substrLower); j++ {
if strLower[i+j] != substrLower[j] {
match = false
break
}
}
if match {
return true
}
}
return false
}

460
internal/languages/types.go Normal file
View File

@ -0,0 +1,460 @@
package languages
import (
"fmt"
"sync"
)
// Language represents a single language that can be learned by players
type Language struct {
id int32 // Unique language identifier
name string // Language name
saveNeeded bool // Whether this language needs to be saved to database
mutex sync.RWMutex // Thread safety
}
// NewLanguage creates a new language with default values
func NewLanguage() *Language {
return &Language{
id: 0,
name: "",
saveNeeded: false,
}
}
// NewLanguageFromExisting creates a copy of an existing language
func NewLanguageFromExisting(source *Language) *Language {
if source == nil {
return NewLanguage()
}
source.mutex.RLock()
defer source.mutex.RUnlock()
return &Language{
id: source.id,
name: source.name,
saveNeeded: source.saveNeeded,
}
}
// GetID returns the language's unique identifier
func (l *Language) GetID() int32 {
l.mutex.RLock()
defer l.mutex.RUnlock()
return l.id
}
// SetID updates the language's unique identifier
func (l *Language) SetID(id int32) {
l.mutex.Lock()
defer l.mutex.Unlock()
l.id = id
}
// GetName returns the language name
func (l *Language) GetName() string {
l.mutex.RLock()
defer l.mutex.RUnlock()
return l.name
}
// SetName updates the language name
func (l *Language) SetName(name string) {
l.mutex.Lock()
defer l.mutex.Unlock()
// Truncate if too long
if len(name) > MaxLanguageNameLength {
name = name[:MaxLanguageNameLength]
}
l.name = name
}
// GetSaveNeeded returns whether this language needs database saving
func (l *Language) GetSaveNeeded() bool {
l.mutex.RLock()
defer l.mutex.RUnlock()
return l.saveNeeded
}
// SetSaveNeeded updates the save status
func (l *Language) SetSaveNeeded(needed bool) {
l.mutex.Lock()
defer l.mutex.Unlock()
l.saveNeeded = needed
}
// IsValid validates the language data
func (l *Language) IsValid() bool {
l.mutex.RLock()
defer l.mutex.RUnlock()
if l.id < MinLanguageID || l.id > MaxLanguageID {
return false
}
if len(l.name) == 0 || len(l.name) > MaxLanguageNameLength {
return false
}
return true
}
// String returns a string representation of the language
func (l *Language) String() string {
l.mutex.RLock()
defer l.mutex.RUnlock()
return fmt.Sprintf("Language{ID: %d, Name: %s, SaveNeeded: %v}", l.id, l.name, l.saveNeeded)
}
// Copy creates a deep copy of the language
func (l *Language) Copy() *Language {
return NewLanguageFromExisting(l)
}
// MasterLanguagesList manages the global list of all available languages
type MasterLanguagesList struct {
languages map[int32]*Language // Languages indexed by ID for fast lookup
nameIndex map[string]*Language // Languages indexed by name for name lookups
mutex sync.RWMutex // Thread safety
}
// NewMasterLanguagesList creates a new master languages list
func NewMasterLanguagesList() *MasterLanguagesList {
return &MasterLanguagesList{
languages: make(map[int32]*Language),
nameIndex: make(map[string]*Language),
}
}
// Clear removes all languages from the list
func (mll *MasterLanguagesList) Clear() {
mll.mutex.Lock()
defer mll.mutex.Unlock()
mll.languages = make(map[int32]*Language)
mll.nameIndex = make(map[string]*Language)
}
// Size returns the number of languages in the list
func (mll *MasterLanguagesList) Size() int32 {
mll.mutex.RLock()
defer mll.mutex.RUnlock()
return int32(len(mll.languages))
}
// AddLanguage adds a new language to the master list
func (mll *MasterLanguagesList) AddLanguage(language *Language) error {
if language == nil {
return fmt.Errorf("language cannot be nil")
}
if !language.IsValid() {
return fmt.Errorf("language is not valid: %s", language.String())
}
mll.mutex.Lock()
defer mll.mutex.Unlock()
// Check for duplicate ID
if _, exists := mll.languages[language.GetID()]; exists {
return fmt.Errorf("language with ID %d already exists", language.GetID())
}
// Check for duplicate name
name := language.GetName()
if _, exists := mll.nameIndex[name]; exists {
return fmt.Errorf("language with name '%s' already exists", name)
}
// Add to both indexes
mll.languages[language.GetID()] = language
mll.nameIndex[name] = language
return nil
}
// GetLanguage retrieves a language by ID
func (mll *MasterLanguagesList) GetLanguage(id int32) *Language {
mll.mutex.RLock()
defer mll.mutex.RUnlock()
return mll.languages[id]
}
// GetLanguageByName retrieves a language by name
func (mll *MasterLanguagesList) GetLanguageByName(name string) *Language {
mll.mutex.RLock()
defer mll.mutex.RUnlock()
return mll.nameIndex[name]
}
// GetAllLanguages returns a copy of all languages
func (mll *MasterLanguagesList) GetAllLanguages() []*Language {
mll.mutex.RLock()
defer mll.mutex.RUnlock()
result := make([]*Language, 0, len(mll.languages))
for _, lang := range mll.languages {
result = append(result, lang)
}
return result
}
// HasLanguage checks if a language exists by ID
func (mll *MasterLanguagesList) HasLanguage(id int32) bool {
mll.mutex.RLock()
defer mll.mutex.RUnlock()
_, exists := mll.languages[id]
return exists
}
// HasLanguageByName checks if a language exists by name
func (mll *MasterLanguagesList) HasLanguageByName(name string) bool {
mll.mutex.RLock()
defer mll.mutex.RUnlock()
_, exists := mll.nameIndex[name]
return exists
}
// RemoveLanguage removes a language by ID
func (mll *MasterLanguagesList) RemoveLanguage(id int32) bool {
mll.mutex.Lock()
defer mll.mutex.Unlock()
language, exists := mll.languages[id]
if !exists {
return false
}
// Remove from both indexes
delete(mll.languages, id)
delete(mll.nameIndex, language.GetName())
return true
}
// UpdateLanguage updates an existing language
func (mll *MasterLanguagesList) UpdateLanguage(language *Language) error {
if language == nil {
return fmt.Errorf("language cannot be nil")
}
if !language.IsValid() {
return fmt.Errorf("language is not valid: %s", language.String())
}
mll.mutex.Lock()
defer mll.mutex.Unlock()
id := language.GetID()
oldLanguage, exists := mll.languages[id]
if !exists {
return fmt.Errorf("language with ID %d does not exist", id)
}
// Remove old name index if name changed
oldName := oldLanguage.GetName()
newName := language.GetName()
if oldName != newName {
delete(mll.nameIndex, oldName)
// Check for name conflicts
if _, exists := mll.nameIndex[newName]; exists {
return fmt.Errorf("language with name '%s' already exists", newName)
}
mll.nameIndex[newName] = language
}
// Update language
mll.languages[id] = language
return nil
}
// GetLanguageNames returns all language names
func (mll *MasterLanguagesList) GetLanguageNames() []string {
mll.mutex.RLock()
defer mll.mutex.RUnlock()
names := make([]string, 0, len(mll.nameIndex))
for name := range mll.nameIndex {
names = append(names, name)
}
return names
}
// PlayerLanguagesList manages languages known by a specific player
type PlayerLanguagesList struct {
languages map[int32]*Language // Player's languages indexed by ID
nameIndex map[string]*Language // Player's languages indexed by name
mutex sync.RWMutex // Thread safety
}
// NewPlayerLanguagesList creates a new player languages list
func NewPlayerLanguagesList() *PlayerLanguagesList {
return &PlayerLanguagesList{
languages: make(map[int32]*Language),
nameIndex: make(map[string]*Language),
}
}
// Clear removes all languages from the player's list
func (pll *PlayerLanguagesList) Clear() {
pll.mutex.Lock()
defer pll.mutex.Unlock()
pll.languages = make(map[int32]*Language)
pll.nameIndex = make(map[string]*Language)
}
// Add adds a language to the player's known languages
func (pll *PlayerLanguagesList) Add(language *Language) error {
if language == nil {
return fmt.Errorf("language cannot be nil")
}
pll.mutex.Lock()
defer pll.mutex.Unlock()
id := language.GetID()
name := language.GetName()
// Check if already known
if _, exists := pll.languages[id]; exists {
return fmt.Errorf("player already knows language with ID %d", id)
}
// Check player language limit
if len(pll.languages) >= MaxLanguagesPerPlayer {
return fmt.Errorf("player has reached maximum language limit (%d)", MaxLanguagesPerPlayer)
}
// Add to both indexes
pll.languages[id] = language
pll.nameIndex[name] = language
return nil
}
// GetLanguage retrieves a language the player knows by ID
func (pll *PlayerLanguagesList) GetLanguage(id int32) *Language {
pll.mutex.RLock()
defer pll.mutex.RUnlock()
return pll.languages[id]
}
// GetLanguageByName retrieves a language the player knows by name
func (pll *PlayerLanguagesList) GetLanguageByName(name string) *Language {
pll.mutex.RLock()
defer pll.mutex.RUnlock()
return pll.nameIndex[name]
}
// GetAllLanguages returns a copy of all languages the player knows
func (pll *PlayerLanguagesList) GetAllLanguages() []*Language {
pll.mutex.RLock()
defer pll.mutex.RUnlock()
result := make([]*Language, 0, len(pll.languages))
for _, lang := range pll.languages {
result = append(result, lang)
}
return result
}
// HasLanguage checks if the player knows a language by ID
func (pll *PlayerLanguagesList) HasLanguage(id int32) bool {
pll.mutex.RLock()
defer pll.mutex.RUnlock()
_, exists := pll.languages[id]
return exists
}
// HasLanguageByName checks if the player knows a language by name
func (pll *PlayerLanguagesList) HasLanguageByName(name string) bool {
pll.mutex.RLock()
defer pll.mutex.RUnlock()
_, exists := pll.nameIndex[name]
return exists
}
// RemoveLanguage removes a language from the player's known languages
func (pll *PlayerLanguagesList) RemoveLanguage(id int32) bool {
pll.mutex.Lock()
defer pll.mutex.Unlock()
language, exists := pll.languages[id]
if !exists {
return false
}
// Remove from both indexes
delete(pll.languages, id)
delete(pll.nameIndex, language.GetName())
return true
}
// Size returns the number of languages the player knows
func (pll *PlayerLanguagesList) Size() int32 {
pll.mutex.RLock()
defer pll.mutex.RUnlock()
return int32(len(pll.languages))
}
// GetLanguageIDs returns all language IDs the player knows
func (pll *PlayerLanguagesList) GetLanguageIDs() []int32 {
pll.mutex.RLock()
defer pll.mutex.RUnlock()
ids := make([]int32, 0, len(pll.languages))
for id := range pll.languages {
ids = append(ids, id)
}
return ids
}
// GetLanguageNames returns all language names the player knows
func (pll *PlayerLanguagesList) GetLanguageNames() []string {
pll.mutex.RLock()
defer pll.mutex.RUnlock()
names := make([]string, 0, len(pll.nameIndex))
for name := range pll.nameIndex {
names = append(names, name)
}
return names
}
// LanguageStatistics contains language system statistics
type LanguageStatistics struct {
TotalLanguages int `json:"total_languages"`
PlayersWithLanguages int `json:"players_with_languages"`
LanguageUsageCount map[int32]int64 `json:"language_usage_count"`
LanguageLookups int64 `json:"language_lookups"`
LanguagesByName map[string]int32 `json:"languages_by_name"`
}

635
internal/npc/ai/brain.go Normal file
View File

@ -0,0 +1,635 @@
package ai
import (
"fmt"
"sync"
"time"
)
// Brain interface defines the core AI functionality
type Brain interface {
// Core AI methods
Think() error
GetBrainType() int8
// State management
IsActive() bool
SetActive(bool)
GetState() int32
SetState(int32)
// Timing
GetThinkTick() int32
SetThinkTick(int32)
GetLastThink() int64
SetLastThink(int64)
// Body management
GetBody() NPC
SetBody(NPC)
// Hate management
AddHate(entityID int32, hate int32)
GetHate(entityID int32) int32
ClearHate()
ClearHateForEntity(entityID int32)
GetMostHated() int32
GetHatePercentage(entityID int32) int8
GetHateList() map[int32]*HateEntry
// Encounter management
AddToEncounter(entityID, characterID int32, isPlayer, isBot bool) bool
IsEntityInEncounter(entityID int32) bool
IsPlayerInEncounter(characterID int32) bool
HasPlayerInEncounter() bool
GetEncounterSize() int
ClearEncounter()
CheckLootAllowed(entityID int32) bool
// Combat methods
ProcessSpell(target Entity, distance float32) bool
ProcessMelee(target Entity, distance float32)
CheckBuffs() bool
HasRecovered() bool
MoveCloser(target Spawn)
// Statistics
GetStatistics() *BrainStatistics
ResetStatistics()
}
// BaseBrain provides the default AI implementation
type BaseBrain struct {
npc NPC // The NPC this brain controls
brainType int8 // Type of brain
state *BrainState // Brain state management
hateList *HateList // Hate management
encounterList *EncounterList // Encounter management
statistics *BrainStatistics // Performance statistics
logger Logger // Logger interface
mutex sync.RWMutex // Thread safety
}
// NewBaseBrain creates a new base brain
func NewBaseBrain(npc NPC, logger Logger) *BaseBrain {
return &BaseBrain{
npc: npc,
brainType: BrainTypeDefault,
state: NewBrainState(),
hateList: NewHateList(),
encounterList: NewEncounterList(),
statistics: NewBrainStatistics(),
logger: logger,
}
}
// Think implements the main AI logic
func (bb *BaseBrain) Think() error {
if !bb.IsActive() {
return nil
}
startTime := time.Now()
defer func() {
// Update statistics
bb.mutex.Lock()
bb.statistics.ThinkCycles++
thinkTime := float64(time.Since(startTime).Nanoseconds()) / 1000000.0 // Convert to milliseconds
bb.statistics.AverageThinkTime = (bb.statistics.AverageThinkTime + thinkTime) / 2.0
bb.statistics.LastThinkTime = time.Now().UnixMilli()
bb.mutex.Unlock()
bb.state.SetLastThink(time.Now().UnixMilli())
}()
if bb.npc == nil {
return fmt.Errorf("brain has no body")
}
// Handle pet ID registration for players
if bb.npc.IsPet() && bb.npc.GetOwner() != nil && bb.npc.GetOwner().IsPlayer() {
// TODO: Register pet ID with player's info struct
}
// Get the most hated target
mostHatedID := bb.hateList.GetMostHated()
var target Entity
if mostHatedID > 0 {
target = bb.getEntityByID(mostHatedID)
// Remove dead targets from hate list
if target != nil && target.GetHP() <= 0 {
bb.hateList.RemoveHate(mostHatedID)
target = nil
// Try again to get most hated
mostHatedID = bb.hateList.GetMostHated()
if mostHatedID > 0 {
target = bb.getEntityByID(mostHatedID)
}
}
}
// Skip if mezzed, stunned, or feared
if bb.npc.IsMezzedOrStunned() {
return nil
}
// Get runback distance
runbackDistance := bb.npc.GetRunbackDistance()
if target != nil {
// We have a target to fight
if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelDetailed {
bb.logger.LogDebug("NPC %s has target %s", bb.npc.GetName(), target.GetName())
}
// Set target if not already set
if bb.npc.GetTarget() != target {
bb.npc.SetTarget(target)
}
// Face the target
bb.npc.FaceTarget(target, false)
// Enter combat if not already in combat
if !bb.npc.GetInCombat() {
bb.npc.ClearRunningLocations()
bb.npc.InCombat(true)
bb.npc.SetCastOnAggroCompleted(false)
// TODO: Call spawn script for aggro
}
// Check chase distance and water restrictions
if bb.shouldBreakPursuit(target, runbackDistance) {
// Break pursuit - clear hate and encounter
if bb.logger != nil {
bb.logger.LogDebug("NPC %s breaking pursuit (distance: %.2f)", bb.npc.GetName(), runbackDistance)
}
// TODO: Send encounter break messages to players
bb.hateList.Clear()
bb.encounterList.Clear()
} else {
// Continue combat
distance := bb.npc.GetDistance(target)
// Try to cast spells first, then melee
if !bb.npc.IsCasting() && (!bb.HasRecovered() || !bb.ProcessSpell(target, distance)) {
if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelDetailed {
bb.logger.LogDebug("NPC %s attempting melee on %s", bb.npc.GetName(), target.GetName())
}
bb.npc.FaceTarget(target, false)
bb.ProcessMelee(target, distance)
}
}
} else {
// No target - handle out of combat behavior
wasInCombat := bb.npc.GetInCombat()
if bb.npc.GetInCombat() {
bb.npc.InCombat(false)
// Restore HP for non-player pets
if !bb.npc.IsPet() || (bb.npc.IsPet() && bb.npc.GetOwner() != nil && !bb.npc.GetOwner().IsPlayer()) {
bb.npc.SetHP(bb.npc.GetTotalHP())
}
}
// Check for buffs when not in combat
bb.CheckBuffs()
// Handle runback if needed
if !bb.npc.GetInCombat() && !bb.npc.IsPauseMovementTimerActive() {
if runbackDistance > RunbackThreshold || (bb.npc.ShouldCallRunback() && !bb.npc.IsFollowing()) {
bb.npc.SetEncounterState(EncounterStateBroken)
bb.npc.Runback(runbackDistance)
bb.npc.SetCallRunback(false)
} else if bb.npc.GetRunbackLocation() != nil {
bb.handleRunbackStages()
}
}
// Clear encounter if any entities remain
if bb.encounterList.Size() > 0 {
bb.encounterList.Clear()
}
}
return nil
}
// GetBrainType returns the brain type
func (bb *BaseBrain) GetBrainType() int8 {
return bb.brainType
}
// IsActive returns whether the brain is active
func (bb *BaseBrain) IsActive() bool {
return bb.state.IsActive()
}
// SetActive sets the brain's active state
func (bb *BaseBrain) SetActive(active bool) {
bb.state.SetActive(active)
}
// GetState returns the current AI state
func (bb *BaseBrain) GetState() int32 {
return bb.state.GetState()
}
// SetState sets the current AI state
func (bb *BaseBrain) SetState(state int32) {
bb.state.SetState(state)
}
// GetThinkTick returns the think tick interval
func (bb *BaseBrain) GetThinkTick() int32 {
return bb.state.GetThinkTick()
}
// SetThinkTick sets the think tick interval
func (bb *BaseBrain) SetThinkTick(tick int32) {
bb.state.SetThinkTick(tick)
}
// GetLastThink returns the timestamp of the last think cycle
func (bb *BaseBrain) GetLastThink() int64 {
return bb.state.GetLastThink()
}
// SetLastThink sets the timestamp of the last think cycle
func (bb *BaseBrain) SetLastThink(timestamp int64) {
bb.state.SetLastThink(timestamp)
}
// GetBody returns the NPC this brain controls
func (bb *BaseBrain) GetBody() NPC {
bb.mutex.RLock()
defer bb.mutex.RUnlock()
return bb.npc
}
// SetBody sets the NPC this brain controls
func (bb *BaseBrain) SetBody(npc NPC) {
bb.mutex.Lock()
defer bb.mutex.Unlock()
bb.npc = npc
}
// AddHate adds hate for an entity
func (bb *BaseBrain) AddHate(entityID int32, hate int32) {
// Don't add hate while running back
if bb.npc != nil && bb.npc.IsRunningBack() {
return
}
// Don't add hate if owner is attacking pet
if bb.npc != nil && bb.npc.IsPet() && bb.npc.GetOwner() != nil {
if bb.npc.GetOwner().GetID() == entityID {
return
}
}
// Check for taunt immunity
// TODO: Implement immunity checking
bb.hateList.AddHate(entityID, hate)
// Update statistics
bb.mutex.Lock()
bb.statistics.HateEvents++
bb.mutex.Unlock()
// TODO: Add to entity's HatedBy list
// Add pet owner to hate list if not already present
entity := bb.getEntityByID(entityID)
if entity != nil && entity.IsPet() && entity.GetOwner() != nil {
ownerID := entity.GetOwner().GetID()
if bb.hateList.GetHate(ownerID) == 0 {
bb.hateList.AddHate(ownerID, 0)
}
}
}
// GetHate returns the hate value for an entity
func (bb *BaseBrain) GetHate(entityID int32) int32 {
return bb.hateList.GetHate(entityID)
}
// ClearHate removes all hate entries
func (bb *BaseBrain) ClearHate() {
bb.hateList.Clear()
// TODO: Update entities' HatedBy lists
}
// ClearHateForEntity removes hate for a specific entity
func (bb *BaseBrain) ClearHateForEntity(entityID int32) {
bb.hateList.RemoveHate(entityID)
// TODO: Update entity's HatedBy list
}
// GetMostHated returns the ID of the most hated entity
func (bb *BaseBrain) GetMostHated() int32 {
return bb.hateList.GetMostHated()
}
// GetHatePercentage returns the hate percentage for an entity
func (bb *BaseBrain) GetHatePercentage(entityID int32) int8 {
return bb.hateList.GetHatePercentage(entityID)
}
// GetHateList returns a copy of the hate list
func (bb *BaseBrain) GetHateList() map[int32]*HateEntry {
return bb.hateList.GetAllEntries()
}
// AddToEncounter adds an entity to the encounter list
func (bb *BaseBrain) AddToEncounter(entityID, characterID int32, isPlayer, isBot bool) bool {
success := bb.encounterList.AddEntity(entityID, characterID, isPlayer, isBot)
if success {
bb.mutex.Lock()
bb.statistics.EncounterEvents++
bb.mutex.Unlock()
}
return success
}
// IsEntityInEncounter checks if an entity is in the encounter
func (bb *BaseBrain) IsEntityInEncounter(entityID int32) bool {
return bb.encounterList.IsEntityInEncounter(entityID)
}
// IsPlayerInEncounter checks if a player is in the encounter
func (bb *BaseBrain) IsPlayerInEncounter(characterID int32) bool {
return bb.encounterList.IsPlayerInEncounter(characterID)
}
// HasPlayerInEncounter returns whether any player is in the encounter
func (bb *BaseBrain) HasPlayerInEncounter() bool {
return bb.encounterList.HasPlayerInEncounter()
}
// GetEncounterSize returns the size of the encounter list
func (bb *BaseBrain) GetEncounterSize() int {
return bb.encounterList.Size()
}
// ClearEncounter removes all entities from the encounter
func (bb *BaseBrain) ClearEncounter() {
bb.encounterList.Clear()
// TODO: Remove spells from NPC
}
// CheckLootAllowed checks if an entity can loot this NPC
func (bb *BaseBrain) CheckLootAllowed(entityID int32) bool {
// TODO: Implement loot method checking, chest timers, etc.
// Basic check - is entity in encounter?
return bb.encounterList.IsEntityInEncounter(entityID)
}
// ProcessSpell attempts to cast a spell
func (bb *BaseBrain) ProcessSpell(target Entity, distance float32) bool {
if bb.npc == nil {
return false
}
// Check cast percentage and conditions
castChance := bb.npc.GetCastPercentage()
if castChance <= 0 {
return false
}
// TODO: Implement random chance checking
// TODO: Check for stifled, feared conditions
// Get next spell to cast
spell := bb.npc.GetNextSpell(target, distance)
if spell == nil {
return false
}
// Determine spell target
var spellTarget Spawn = target
if spell.IsFriendlySpell() {
// TODO: Find best friendly target (lowest HP group member)
spellTarget = bb.npc
}
// Cast the spell
success := bb.castSpell(spell, spellTarget, false)
if success {
bb.mutex.Lock()
bb.statistics.SpellsCast++
bb.mutex.Unlock()
}
return success
}
// ProcessMelee handles melee combat
func (bb *BaseBrain) ProcessMelee(target Entity, distance float32) {
if bb.npc == nil || target == nil {
return
}
maxCombatRange := bb.getMaxCombatRange()
if distance > maxCombatRange {
bb.MoveCloser(target)
} else {
if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelDetailed {
bb.logger.LogDebug("NPC %s is within melee range of %s", bb.npc.GetName(), target.GetName())
}
// Check if attack is allowed
if !bb.npc.AttackAllowed(target) {
return
}
currentTime := time.Now().UnixMilli()
// Primary weapon attack
if bb.npc.PrimaryWeaponReady() && !bb.npc.IsDazed() && !bb.npc.IsFeared() {
if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelVerbose {
bb.logger.LogDebug("NPC %s swings primary weapon at %s", bb.npc.GetName(), target.GetName())
}
bb.npc.SetPrimaryLastAttackTime(currentTime)
bb.npc.MeleeAttack(target, distance, true)
bb.mutex.Lock()
bb.statistics.MeleeAttacks++
bb.mutex.Unlock()
// TODO: Call spawn script for auto attack tick
}
// Secondary weapon attack
if bb.npc.SecondaryWeaponReady() && !bb.npc.IsDazed() {
bb.npc.SetSecondaryLastAttackTime(currentTime)
bb.npc.MeleeAttack(target, distance, false)
bb.mutex.Lock()
bb.statistics.MeleeAttacks++
bb.mutex.Unlock()
}
}
}
// CheckBuffs checks and casts buff spells
func (bb *BaseBrain) CheckBuffs() bool {
if bb.npc == nil {
return false
}
// Don't buff in combat, while casting, stunned, etc.
if bb.npc.GetInCombat() || bb.npc.IsCasting() || bb.npc.IsMezzedOrStunned() ||
!bb.npc.IsAlive() || bb.npc.IsStifled() || !bb.HasRecovered() {
return false
}
// Get next buff spell
buffSpell := bb.npc.GetNextBuffSpell(bb.npc)
if buffSpell == nil {
return false
}
// Try to cast on self first
if bb.castSpell(buffSpell, bb.npc, false) {
return true
}
// TODO: Try to buff group members
return false
}
// HasRecovered checks if the brain has recovered from spell casting
func (bb *BaseBrain) HasRecovered() bool {
return bb.state.HasRecovered()
}
// MoveCloser moves the NPC closer to a target
func (bb *BaseBrain) MoveCloser(target Spawn) {
if bb.npc == nil || target == nil {
return
}
maxCombatRange := bb.getMaxCombatRange()
if bb.npc.GetFollowTarget() != target {
bb.npc.SetFollowTarget(target, maxCombatRange)
}
if bb.npc.GetFollowTarget() != nil && !bb.npc.IsFollowing() {
bb.npc.CalculateRunningLocation(true)
bb.npc.SetFollowing(true)
}
}
// GetStatistics returns brain performance statistics
func (bb *BaseBrain) GetStatistics() *BrainStatistics {
bb.mutex.RLock()
defer bb.mutex.RUnlock()
// Return a copy
return &BrainStatistics{
ThinkCycles: bb.statistics.ThinkCycles,
SpellsCast: bb.statistics.SpellsCast,
MeleeAttacks: bb.statistics.MeleeAttacks,
HateEvents: bb.statistics.HateEvents,
EncounterEvents: bb.statistics.EncounterEvents,
AverageThinkTime: bb.statistics.AverageThinkTime,
LastThinkTime: bb.statistics.LastThinkTime,
TotalActiveTime: bb.statistics.TotalActiveTime,
}
}
// ResetStatistics resets all performance statistics
func (bb *BaseBrain) ResetStatistics() {
bb.mutex.Lock()
defer bb.mutex.Unlock()
bb.statistics = NewBrainStatistics()
}
// Helper methods
// shouldBreakPursuit checks if the NPC should break pursuit of a target
func (bb *BaseBrain) shouldBreakPursuit(target Entity, runbackDistance float32) bool {
if target == nil {
return false
}
// Check max chase distance
maxChase := bb.getMaxChaseDistance()
if runbackDistance > maxChase {
return true
}
// Check water creature restrictions
if bb.npc != nil && bb.npc.IsWaterCreature() && !bb.npc.IsFlyingCreature() && !target.InWater() {
return true
}
return false
}
// castSpell casts a spell on a target
func (bb *BaseBrain) castSpell(spell Spell, target Spawn, calculateRunLoc bool) bool {
if spell == nil || bb.npc == nil {
return false
}
if calculateRunLoc {
bb.npc.CalculateRunningLocation(true)
}
// TODO: Process spell through zone
// bb.npc.GetZone().ProcessSpell(spell, bb.npc, target)
// Set spell recovery time
castTime := spell.GetCastTime() * RecoveryTimeMultiple
recoveryTime := spell.GetRecoveryTime() * RecoveryTimeMultiple
totalRecovery := time.Now().UnixMilli() + int64(castTime) + int64(recoveryTime) + int64(SpellRecoveryBuffer)
bb.state.SetSpellRecovery(totalRecovery)
return true
}
// handleRunbackStages handles the various stages of runback
func (bb *BaseBrain) handleRunbackStages() {
if bb.npc == nil {
return
}
runbackLoc := bb.npc.GetRunbackLocation()
if runbackLoc == nil {
return
}
// TODO: Implement runback stage handling
// This would involve movement management and position updates
}
// getEntityByID retrieves an entity by ID (placeholder)
func (bb *BaseBrain) getEntityByID(entityID int32) Entity {
// TODO: Implement entity lookup through zone
return nil
}
// getMaxChaseDistance returns the maximum chase distance
func (bb *BaseBrain) getMaxChaseDistance() float32 {
// TODO: Check NPC info struct and zone rules
return MaxChaseDistance
}
// getMaxCombatRange returns the maximum combat range
func (bb *BaseBrain) getMaxCombatRange() float32 {
// TODO: Check zone rules
return MaxCombatRange
}

View File

@ -0,0 +1,90 @@
package ai
// AI tick constants
const (
DefaultThinkTick int32 = 250 // Default think tick in milliseconds (1/4 second)
FastThinkTick int32 = 100 // Fast think tick for active AI
SlowThinkTick int32 = 1000 // Slow think tick for idle AI
BlankBrainTick int32 = 50000 // Very slow tick for blank brain
MaxThinkTick int32 = 60000 // Maximum think tick (1 minute)
)
// Combat constants
const (
MaxChaseDistance float32 = 150.0 // Default max chase distance
MaxCombatRange float32 = 25.0 // Default max combat range
RunbackThreshold float32 = 1.0 // Distance threshold for runback
)
// Hate system constants
const (
MinHateValue int32 = 1 // Minimum hate value (0 or negative is invalid)
MaxHateValue int32 = 2147483647 // Maximum hate value (INT_MAX)
DefaultHateValue int32 = 100 // Default hate amount
MaxHateListSize int = 100 // Maximum entities in hate list
)
// Encounter system constants
const (
MaxEncounterSize int = 50 // Maximum entities in encounter list
)
// Spell recovery constants
const (
SpellRecoveryBuffer int32 = 2000 // Additional recovery time buffer (2 seconds)
)
// Brain type constants for identification
const (
BrainTypeDefault int8 = 0
BrainTypeCombatPet int8 = 1
BrainTypeNonCombatPet int8 = 2
BrainTypeBlank int8 = 3
BrainTypeLua int8 = 4
BrainTypeDumbFire int8 = 5
)
// Pet movement constants
const (
PetMovementFollow int8 = 0
PetMovementStay int8 = 1
PetMovementGuard int8 = 2
)
// Encounter state constants
const (
EncounterStateAvailable int8 = 0
EncounterStateLocked int8 = 1
EncounterStateBroken int8 = 2
)
// Combat decision constants
const (
MeleeAttackChance int = 70 // Base chance for melee attack
SpellCastChance int = 30 // Base chance for spell casting
BuffCheckChance int = 50 // Chance to check for buffs
)
// AI state flags
const (
AIStateIdle int32 = 0
AIStateCombat int32 = 1
AIStateFollowing int32 = 2
AIStateRunback int32 = 3
AIStateCasting int32 = 4
AIStateMoving int32 = 5
)
// Debug levels
const (
DebugLevelNone int8 = 0
DebugLevelBasic int8 = 1
DebugLevelDetailed int8 = 2
DebugLevelVerbose int8 = 3
)
// Timer constants
const (
MillisecondsPerSecond int32 = 1000
RecoveryTimeMultiple int32 = 10 // Multiply cast/recovery times by 10
)

View File

@ -0,0 +1,483 @@
package ai
import "fmt"
// Logger interface for AI logging
type Logger interface {
LogInfo(message string, args ...interface{})
LogError(message string, args ...interface{})
LogDebug(message string, args ...interface{})
LogWarning(message string, args ...interface{})
}
// NPC interface defines the required NPC functionality for AI
type NPC interface {
// Basic NPC information
GetID() int32
GetName() string
GetHP() int32
GetTotalHP() int32
SetHP(int32)
IsAlive() bool
// Combat state
GetInCombat() bool
InCombat(bool)
GetTarget() Entity
SetTarget(Entity)
// Pet functionality
IsPet() bool
GetOwner() Entity
// Movement and positioning
GetX() float32
GetY() float32
GetZ() float32
GetDistance(Entity) float32
FaceTarget(Entity, bool)
IsFollowing() bool
SetFollowing(bool)
GetFollowTarget() Spawn
SetFollowTarget(Spawn, float32)
CalculateRunningLocation(bool)
ClearRunningLocations()
// Runback functionality
IsRunningBack() bool
GetRunbackLocation() *MovementLocation
GetRunbackDistance() float32
Runback(float32)
ShouldCallRunback() bool
SetCallRunback(bool)
// Status effects
IsMezzedOrStunned() bool
IsCasting() bool
IsDazed() bool
IsFeared() bool
IsStifled() bool
InWater() bool
IsWaterCreature() bool
IsFlyingCreature() bool
// Combat mechanics
AttackAllowed(Entity) bool
PrimaryWeaponReady() bool
SecondaryWeaponReady() bool
SetPrimaryLastAttackTime(int64)
SetSecondaryLastAttackTime(int64)
MeleeAttack(Entity, float32, bool)
// Spell casting
GetCastPercentage() int8
GetNextSpell(Entity, float32) Spell
GetNextBuffSpell(Spawn) Spell
SetCastOnAggroCompleted(bool)
CheckLoS(Entity) bool
// Movement pausing
IsPauseMovementTimerActive() bool
// Encounter state
SetEncounterState(int8)
// Scripts
GetSpawnScript() string
// Utility
KillSpawn(NPC)
}
// Entity interface for combat entities
type Entity interface {
Spawn
GetID() int32
GetName() string
GetHP() int32
GetTotalHP() int32
IsPlayer() bool
IsBot() bool
IsPet() bool
GetOwner() Entity
InWater() bool
}
// Spawn interface for basic spawn functionality
type Spawn interface {
GetID() int32
GetName() string
GetX() float32
GetY() float32
GetZ() float32
}
// Spell interface for spell data
type Spell interface {
GetSpellID() int32
GetName() string
IsFriendlySpell() bool
GetCastTime() int32
GetRecoveryTime() int32
GetRange() float32
GetMinRange() float32
}
// MovementLocation represents a location for movement/runback
type MovementLocation struct {
X float32
Y float32
Z float32
GridID int32
Stage int32
}
// LuaInterface defines Lua script integration
type LuaInterface interface {
RunSpawnScript(script, function string, npc NPC, target Entity) error
}
// Zone interface for zone-related AI operations
type Zone interface {
GetSpawnByID(int32) Spawn
ProcessSpell(spell Spell, caster NPC, target Spawn) error
CallSpawnScript(npc NPC, scriptType string, args ...interface{}) error
}
// AIManager provides high-level management of the AI system
type AIManager struct {
brains map[int32]Brain // Map of NPC ID to brain
activeCount int64 // Number of active brains
totalThinks int64 // Total think cycles processed
logger Logger // Logger for AI events
luaInterface LuaInterface // Lua script interface
}
// NewAIManager creates a new AI manager
func NewAIManager(logger Logger, luaInterface LuaInterface) *AIManager {
return &AIManager{
brains: make(map[int32]Brain),
activeCount: 0,
totalThinks: 0,
logger: logger,
luaInterface: luaInterface,
}
}
// AddBrain adds a brain for an NPC
func (am *AIManager) AddBrain(npcID int32, brain Brain) error {
if brain == nil {
return fmt.Errorf("brain cannot be nil")
}
if _, exists := am.brains[npcID]; exists {
return fmt.Errorf("brain already exists for NPC %d", npcID)
}
am.brains[npcID] = brain
if brain.IsActive() {
am.activeCount++
}
if am.logger != nil {
am.logger.LogDebug("Added brain for NPC %d (type: %d)", npcID, brain.GetBrainType())
}
return nil
}
// RemoveBrain removes a brain for an NPC
func (am *AIManager) RemoveBrain(npcID int32) {
if brain, exists := am.brains[npcID]; exists {
if brain.IsActive() {
am.activeCount--
}
delete(am.brains, npcID)
if am.logger != nil {
am.logger.LogDebug("Removed brain for NPC %d", npcID)
}
}
}
// GetBrain retrieves a brain for an NPC
func (am *AIManager) GetBrain(npcID int32) Brain {
return am.brains[npcID]
}
// CreateBrainForNPC creates and adds the appropriate brain for an NPC
func (am *AIManager) CreateBrainForNPC(npc NPC, brainType int8, options ...interface{}) error {
if npc == nil {
return fmt.Errorf("NPC cannot be nil")
}
npcID := npc.GetID()
// Create brain based on type
var brain Brain
switch brainType {
case BrainTypeCombatPet:
brain = NewCombatPetBrain(npc, am.logger)
case BrainTypeNonCombatPet:
brain = NewNonCombatPetBrain(npc, am.logger)
case BrainTypeBlank:
brain = NewBlankBrain(npc, am.logger)
case BrainTypeLua:
brain = NewLuaBrain(npc, am.logger, am.luaInterface)
case BrainTypeDumbFire:
if len(options) >= 2 {
if target, ok := options[0].(Entity); ok {
if expireTime, ok := options[1].(int32); ok {
brain = NewDumbFirePetBrain(npc, target, expireTime, am.logger)
}
}
}
if brain == nil {
return fmt.Errorf("invalid options for dumbfire brain")
}
default:
brain = NewBaseBrain(npc, am.logger)
}
return am.AddBrain(npcID, brain)
}
// ProcessAllBrains runs think cycles for all active brains
func (am *AIManager) ProcessAllBrains() {
currentTime := currentTimeMillis()
for npcID, brain := range am.brains {
if !brain.IsActive() {
continue
}
// Check if it's time to think
lastThink := brain.GetLastThink()
thinkTick := brain.GetThinkTick()
if currentTime-lastThink >= int64(thinkTick) {
if err := brain.Think(); err != nil {
if am.logger != nil {
am.logger.LogError("Brain think error for NPC %d: %v", npcID, err)
}
}
am.totalThinks++
}
}
}
// SetBrainActive sets the active state of a brain
func (am *AIManager) SetBrainActive(npcID int32, active bool) {
if brain := am.brains[npcID]; brain != nil {
wasActive := brain.IsActive()
brain.SetActive(active)
// Update active count
if wasActive && !active {
am.activeCount--
} else if !wasActive && active {
am.activeCount++
}
}
}
// GetActiveCount returns the number of active brains
func (am *AIManager) GetActiveCount() int64 {
return am.activeCount
}
// GetTotalThinks returns the total number of think cycles processed
func (am *AIManager) GetTotalThinks() int64 {
return am.totalThinks
}
// GetBrainCount returns the total number of brains
func (am *AIManager) GetBrainCount() int {
return len(am.brains)
}
// GetBrainsByType returns all brains of a specific type
func (am *AIManager) GetBrainsByType(brainType int8) []Brain {
var result []Brain
for _, brain := range am.brains {
if brain.GetBrainType() == brainType {
result = append(result, brain)
}
}
return result
}
// ClearAllBrains removes all brains
func (am *AIManager) ClearAllBrains() {
am.brains = make(map[int32]Brain)
am.activeCount = 0
if am.logger != nil {
am.logger.LogInfo("Cleared all AI brains")
}
}
// GetStatistics returns overall AI system statistics
func (am *AIManager) GetStatistics() *AIStatistics {
return &AIStatistics{
TotalBrains: len(am.brains),
ActiveBrains: int(am.activeCount),
TotalThinks: am.totalThinks,
BrainsByType: am.getBrainCountsByType(),
}
}
// getBrainCountsByType returns counts of brains by type
func (am *AIManager) getBrainCountsByType() map[string]int {
counts := make(map[string]int)
for _, brain := range am.brains {
typeName := getBrainTypeName(brain.GetBrainType())
counts[typeName]++
}
return counts
}
// AIStatistics contains AI system statistics
type AIStatistics struct {
TotalBrains int `json:"total_brains"`
ActiveBrains int `json:"active_brains"`
TotalThinks int64 `json:"total_thinks"`
BrainsByType map[string]int `json:"brains_by_type"`
}
// AIBrainAdapter provides NPC functionality for brains
type AIBrainAdapter struct {
npc NPC
logger Logger
}
// NewAIBrainAdapter creates a new brain adapter
func NewAIBrainAdapter(npc NPC, logger Logger) *AIBrainAdapter {
return &AIBrainAdapter{
npc: npc,
logger: logger,
}
}
// GetNPC returns the adapted NPC
func (aba *AIBrainAdapter) GetNPC() NPC {
return aba.npc
}
// ProcessAI processes AI for the NPC using its brain
func (aba *AIBrainAdapter) ProcessAI(brain Brain) error {
if brain == nil {
return fmt.Errorf("brain is nil")
}
if !brain.IsActive() {
return nil
}
return brain.Think()
}
// SetupDefaultBrain sets up a default brain for the NPC
func (aba *AIBrainAdapter) SetupDefaultBrain() Brain {
return NewBaseBrain(aba.npc, aba.logger)
}
// SetupPetBrain sets up a pet brain based on pet type
func (aba *AIBrainAdapter) SetupPetBrain(combatPet bool) Brain {
if combatPet {
return NewCombatPetBrain(aba.npc, aba.logger)
}
return NewNonCombatPetBrain(aba.npc, aba.logger)
}
// Utility functions
// getBrainTypeName returns the string name for a brain type
func getBrainTypeName(brainType int8) string {
switch brainType {
case BrainTypeDefault:
return "default"
case BrainTypeCombatPet:
return "combat_pet"
case BrainTypeNonCombatPet:
return "non_combat_pet"
case BrainTypeBlank:
return "blank"
case BrainTypeLua:
return "lua"
case BrainTypeDumbFire:
return "dumbfire"
default:
return "unknown"
}
}
// currentTimeMillis returns current time in milliseconds
func currentTimeMillis() int64 {
return time.Now().UnixMilli()
}
// HateListDebugger provides debugging functionality for hate lists
type HateListDebugger struct {
logger Logger
}
// NewHateListDebugger creates a new hate list debugger
func NewHateListDebugger(logger Logger) *HateListDebugger {
return &HateListDebugger{
logger: logger,
}
}
// PrintHateList prints a formatted hate list
func (hld *HateListDebugger) PrintHateList(npcName string, hateList map[int32]*HateEntry) {
if hld.logger == nil {
return
}
hld.logger.LogInfo("%s's Hate List", npcName)
hld.logger.LogInfo("-------------------")
if len(hateList) == 0 {
hld.logger.LogInfo("(empty)")
} else {
for entityID, entry := range hateList {
hld.logger.LogInfo("Entity %d: %d hate", entityID, entry.HateValue)
}
}
hld.logger.LogInfo("-------------------")
}
// PrintEncounterList prints a formatted encounter list
func (hld *HateListDebugger) PrintEncounterList(npcName string, encounterList map[int32]*EncounterEntry) {
if hld.logger == nil {
return
}
hld.logger.LogInfo("%s's Encounter List", npcName)
hld.logger.LogInfo("-------------------")
if len(encounterList) == 0 {
hld.logger.LogInfo("(empty)")
} else {
for entityID, entry := range encounterList {
entryType := "NPC"
if entry.IsPlayer {
entryType = "Player"
} else if entry.IsBot {
entryType = "Bot"
}
hld.logger.LogInfo("Entity %d (%s)", entityID, entryType)
}
}
hld.logger.LogInfo("-------------------")
}

504
internal/npc/ai/types.go Normal file
View File

@ -0,0 +1,504 @@
package ai
import (
"sync"
"time"
)
// HateEntry represents a single hate entry in the hate list
type HateEntry struct {
EntityID int32 // ID of the hated entity
HateValue int32 // Amount of hate (must be >= 1)
LastUpdated int64 // Timestamp of last hate update
}
// NewHateEntry creates a new hate entry
func NewHateEntry(entityID, hateValue int32) *HateEntry {
if hateValue < MinHateValue {
hateValue = MinHateValue
}
return &HateEntry{
EntityID: entityID,
HateValue: hateValue,
LastUpdated: time.Now().UnixMilli(),
}
}
// HateList manages the hate list for an NPC brain
type HateList struct {
entries map[int32]*HateEntry // Map of entity ID to hate entry
mutex sync.RWMutex // Thread safety
}
// NewHateList creates a new hate list
func NewHateList() *HateList {
return &HateList{
entries: make(map[int32]*HateEntry),
}
}
// AddHate adds or updates hate for an entity
func (hl *HateList) AddHate(entityID, hateValue int32) {
hl.mutex.Lock()
defer hl.mutex.Unlock()
if len(hl.entries) >= MaxHateListSize {
// Remove oldest entry if at capacity
hl.removeOldestEntry()
}
if entry, exists := hl.entries[entityID]; exists {
// Update existing entry
entry.HateValue += hateValue
if entry.HateValue < MinHateValue {
entry.HateValue = MinHateValue
}
entry.LastUpdated = time.Now().UnixMilli()
} else {
// Create new entry
if hateValue < MinHateValue {
hateValue = MinHateValue
}
hl.entries[entityID] = NewHateEntry(entityID, hateValue)
}
}
// GetHate returns the hate value for an entity
func (hl *HateList) GetHate(entityID int32) int32 {
hl.mutex.RLock()
defer hl.mutex.RUnlock()
if entry, exists := hl.entries[entityID]; exists {
return entry.HateValue
}
return 0
}
// RemoveHate removes hate for a specific entity
func (hl *HateList) RemoveHate(entityID int32) {
hl.mutex.Lock()
defer hl.mutex.Unlock()
delete(hl.entries, entityID)
}
// Clear removes all hate entries
func (hl *HateList) Clear() {
hl.mutex.Lock()
defer hl.mutex.Unlock()
hl.entries = make(map[int32]*HateEntry)
}
// GetMostHated returns the entity ID with the highest hate
func (hl *HateList) GetMostHated() int32 {
hl.mutex.RLock()
defer hl.mutex.RUnlock()
var mostHated int32 = 0
var highestHate int32 = 0
for entityID, entry := range hl.entries {
if entry.HateValue > highestHate {
highestHate = entry.HateValue
mostHated = entityID
}
}
return mostHated
}
// GetHatePercentage returns the percentage of hate for an entity
func (hl *HateList) GetHatePercentage(entityID int32) int8 {
hl.mutex.RLock()
defer hl.mutex.RUnlock()
entry, exists := hl.entries[entityID]
if !exists || entry.HateValue <= 0 {
return 0
}
// Calculate total hate
var totalHate int32 = 0
for _, e := range hl.entries {
totalHate += e.HateValue
}
if totalHate <= 0 {
return 0
}
percentage := float32(entry.HateValue) / float32(totalHate) * 100.0
return int8(percentage)
}
// GetAllEntries returns a copy of all hate entries
func (hl *HateList) GetAllEntries() map[int32]*HateEntry {
hl.mutex.RLock()
defer hl.mutex.RUnlock()
result := make(map[int32]*HateEntry)
for id, entry := range hl.entries {
result[id] = &HateEntry{
EntityID: entry.EntityID,
HateValue: entry.HateValue,
LastUpdated: entry.LastUpdated,
}
}
return result
}
// Size returns the number of entries in the hate list
func (hl *HateList) Size() int {
hl.mutex.RLock()
defer hl.mutex.RUnlock()
return len(hl.entries)
}
// removeOldestEntry removes the oldest hate entry (internal use only)
func (hl *HateList) removeOldestEntry() {
if len(hl.entries) == 0 {
return
}
var oldestID int32
var oldestTime int64 = time.Now().UnixMilli()
for id, entry := range hl.entries {
if entry.LastUpdated < oldestTime {
oldestTime = entry.LastUpdated
oldestID = id
}
}
if oldestID != 0 {
delete(hl.entries, oldestID)
}
}
// EncounterEntry represents a single encounter participant
type EncounterEntry struct {
EntityID int32 // ID of the entity
CharacterID int32 // Character ID for players (0 for NPCs)
AddedTime int64 // When entity was added to encounter
IsPlayer bool // Whether this is a player entity
IsBot bool // Whether this is a bot entity
}
// NewEncounterEntry creates a new encounter entry
func NewEncounterEntry(entityID, characterID int32, isPlayer, isBot bool) *EncounterEntry {
return &EncounterEntry{
EntityID: entityID,
CharacterID: characterID,
AddedTime: time.Now().UnixMilli(),
IsPlayer: isPlayer,
IsBot: isBot,
}
}
// EncounterList manages the encounter list for an NPC brain
type EncounterList struct {
entries map[int32]*EncounterEntry // Map of entity ID to encounter entry
playerEntries map[int32]int32 // Map of character ID to entity ID
playerInEncounter bool // Whether any player is in encounter
mutex sync.RWMutex // Thread safety
}
// NewEncounterList creates a new encounter list
func NewEncounterList() *EncounterList {
return &EncounterList{
entries: make(map[int32]*EncounterEntry),
playerEntries: make(map[int32]int32),
playerInEncounter: false,
}
}
// AddEntity adds an entity to the encounter list
func (el *EncounterList) AddEntity(entityID, characterID int32, isPlayer, isBot bool) bool {
el.mutex.Lock()
defer el.mutex.Unlock()
if len(el.entries) >= MaxEncounterSize {
return false
}
// Check if already in encounter
if _, exists := el.entries[entityID]; exists {
return false
}
// Add entry
entry := NewEncounterEntry(entityID, characterID, isPlayer, isBot)
el.entries[entityID] = entry
// Track player entries separately
if isPlayer && characterID > 0 {
el.playerEntries[characterID] = entityID
el.playerInEncounter = true
}
return true
}
// RemoveEntity removes an entity from the encounter list
func (el *EncounterList) RemoveEntity(entityID int32) {
el.mutex.Lock()
defer el.mutex.Unlock()
if entry, exists := el.entries[entityID]; exists {
// Remove from player entries if it's a player
if entry.IsPlayer && entry.CharacterID > 0 {
delete(el.playerEntries, entry.CharacterID)
}
// Remove main entry
delete(el.entries, entityID)
// Update player in encounter flag
el.updatePlayerInEncounter()
}
}
// Clear removes all entities from the encounter list
func (el *EncounterList) Clear() {
el.mutex.Lock()
defer el.mutex.Unlock()
el.entries = make(map[int32]*EncounterEntry)
el.playerEntries = make(map[int32]int32)
el.playerInEncounter = false
}
// IsEntityInEncounter checks if an entity is in the encounter
func (el *EncounterList) IsEntityInEncounter(entityID int32) bool {
el.mutex.RLock()
defer el.mutex.RUnlock()
_, exists := el.entries[entityID]
return exists
}
// IsPlayerInEncounter checks if a player (by character ID) is in the encounter
func (el *EncounterList) IsPlayerInEncounter(characterID int32) bool {
el.mutex.RLock()
defer el.mutex.RUnlock()
_, exists := el.playerEntries[characterID]
return exists
}
// HasPlayerInEncounter returns whether any player is in the encounter
func (el *EncounterList) HasPlayerInEncounter() bool {
el.mutex.RLock()
defer el.mutex.RUnlock()
return el.playerInEncounter
}
// Size returns the number of entities in the encounter
func (el *EncounterList) Size() int {
el.mutex.RLock()
defer el.mutex.RUnlock()
return len(el.entries)
}
// CountPlayerBots returns the number of players and bots in encounter
func (el *EncounterList) CountPlayerBots() int {
el.mutex.RLock()
defer el.mutex.RUnlock()
count := 0
for _, entry := range el.entries {
if entry.IsPlayer || entry.IsBot {
count++
}
}
return count
}
// GetAllEntityIDs returns a copy of all entity IDs in the encounter
func (el *EncounterList) GetAllEntityIDs() []int32 {
el.mutex.RLock()
defer el.mutex.RUnlock()
result := make([]int32, 0, len(el.entries))
for entityID := range el.entries {
result = append(result, entityID)
}
return result
}
// GetAllEntries returns a copy of all encounter entries
func (el *EncounterList) GetAllEntries() map[int32]*EncounterEntry {
el.mutex.RLock()
defer el.mutex.RUnlock()
result := make(map[int32]*EncounterEntry)
for id, entry := range el.entries {
result[id] = &EncounterEntry{
EntityID: entry.EntityID,
CharacterID: entry.CharacterID,
AddedTime: entry.AddedTime,
IsPlayer: entry.IsPlayer,
IsBot: entry.IsBot,
}
}
return result
}
// updatePlayerInEncounter updates the player in encounter flag (internal use only)
func (el *EncounterList) updatePlayerInEncounter() {
el.playerInEncounter = len(el.playerEntries) > 0
}
// BrainState represents the current state of a brain
type BrainState struct {
State int32 // Current AI state
LastThink int64 // Timestamp of last think cycle
ThinkTick int32 // Time between think cycles in milliseconds
SpellRecovery int64 // Timestamp when spell recovery completes
IsActive bool // Whether the brain is active
DebugLevel int8 // Debug output level
mutex sync.RWMutex
}
// NewBrainState creates a new brain state
func NewBrainState() *BrainState {
return &BrainState{
State: AIStateIdle,
LastThink: time.Now().UnixMilli(),
ThinkTick: DefaultThinkTick,
SpellRecovery: 0,
IsActive: true,
DebugLevel: DebugLevelNone,
}
}
// GetState returns the current AI state
func (bs *BrainState) GetState() int32 {
bs.mutex.RLock()
defer bs.mutex.RUnlock()
return bs.State
}
// SetState sets the current AI state
func (bs *BrainState) SetState(state int32) {
bs.mutex.Lock()
defer bs.mutex.Unlock()
bs.State = state
}
// GetLastThink returns the timestamp of the last think cycle
func (bs *BrainState) GetLastThink() int64 {
bs.mutex.RLock()
defer bs.mutex.RUnlock()
return bs.LastThink
}
// SetLastThink sets the timestamp of the last think cycle
func (bs *BrainState) SetLastThink(timestamp int64) {
bs.mutex.Lock()
defer bs.mutex.Unlock()
bs.LastThink = timestamp
}
// GetThinkTick returns the think tick interval
func (bs *BrainState) GetThinkTick() int32 {
bs.mutex.RLock()
defer bs.mutex.RUnlock()
return bs.ThinkTick
}
// SetThinkTick sets the think tick interval
func (bs *BrainState) SetThinkTick(tick int32) {
bs.mutex.Lock()
defer bs.mutex.Unlock()
if tick < 1 {
tick = 1
} else if tick > MaxThinkTick {
tick = MaxThinkTick
}
bs.ThinkTick = tick
}
// GetSpellRecovery returns the spell recovery timestamp
func (bs *BrainState) GetSpellRecovery() int64 {
bs.mutex.RLock()
defer bs.mutex.RUnlock()
return bs.SpellRecovery
}
// SetSpellRecovery sets the spell recovery timestamp
func (bs *BrainState) SetSpellRecovery(timestamp int64) {
bs.mutex.Lock()
defer bs.mutex.Unlock()
bs.SpellRecovery = timestamp
}
// HasRecovered checks if the brain has recovered from spell casting
func (bs *BrainState) HasRecovered() bool {
bs.mutex.RLock()
defer bs.mutex.RUnlock()
currentTime := time.Now().UnixMilli()
return bs.SpellRecovery <= currentTime
}
// IsActive returns whether the brain is active
func (bs *BrainState) IsActive() bool {
bs.mutex.RLock()
defer bs.mutex.RUnlock()
return bs.IsActive
}
// SetActive sets the brain's active state
func (bs *BrainState) SetActive(active bool) {
bs.mutex.Lock()
defer bs.mutex.Unlock()
bs.IsActive = active
}
// GetDebugLevel returns the debug level
func (bs *BrainState) GetDebugLevel() int8 {
bs.mutex.RLock()
defer bs.mutex.RUnlock()
return bs.DebugLevel
}
// SetDebugLevel sets the debug level
func (bs *BrainState) SetDebugLevel(level int8) {
bs.mutex.Lock()
defer bs.mutex.Unlock()
bs.DebugLevel = level
}
// BrainStatistics contains brain performance statistics
type BrainStatistics struct {
ThinkCycles int64 `json:"think_cycles"`
SpellsCast int64 `json:"spells_cast"`
MeleeAttacks int64 `json:"melee_attacks"`
HateEvents int64 `json:"hate_events"`
EncounterEvents int64 `json:"encounter_events"`
AverageThinkTime float64 `json:"average_think_time_ms"`
LastThinkTime int64 `json:"last_think_time"`
TotalActiveTime int64 `json:"total_active_time_ms"`
}
// NewBrainStatistics creates new brain statistics
func NewBrainStatistics() *BrainStatistics {
return &BrainStatistics{
ThinkCycles: 0,
SpellsCast: 0,
MeleeAttacks: 0,
HateEvents: 0,
EncounterEvents: 0,
AverageThinkTime: 0.0,
LastThinkTime: time.Now().UnixMilli(),
TotalActiveTime: 0,
}
}

324
internal/npc/ai/variants.go Normal file
View File

@ -0,0 +1,324 @@
package ai
import (
"fmt"
"time"
)
// CombatPetBrain extends the base brain for combat pets
type CombatPetBrain struct {
*BaseBrain
}
// NewCombatPetBrain creates a new combat pet brain
func NewCombatPetBrain(npc NPC, logger Logger) *CombatPetBrain {
brain := &CombatPetBrain{
BaseBrain: NewBaseBrain(npc, logger),
}
brain.brainType = BrainTypeCombatPet
return brain
}
// Think implements pet-specific AI logic
func (cpb *CombatPetBrain) Think() error {
// Call parent Think() for default combat behavior
if err := cpb.BaseBrain.Think(); err != nil {
return err
}
// Additional pet-specific logic
if cpb.npc.GetInCombat() || !cpb.npc.IsPet() || cpb.npc.IsMezzedOrStunned() {
return nil
}
if cpb.logger != nil && cpb.state.GetDebugLevel() >= DebugLevelDetailed {
cpb.logger.LogDebug("Combat pet AI for %s", cpb.npc.GetName())
}
// Check if owner has stay command set
owner := cpb.npc.GetOwner()
if owner != nil && owner.IsPlayer() {
// TODO: Check player's pet movement setting
// if player.GetInfoStruct().GetPetMovement() == PetMovementStay {
// return nil
// }
}
// Follow owner
if owner != nil {
cpb.npc.SetTarget(owner)
distance := cpb.npc.GetDistance(owner)
maxRange := cpb.getMaxCombatRange()
if distance > maxRange {
cpb.MoveCloser(owner)
}
}
return nil
}
// NonCombatPetBrain handles non-combat pets (cosmetic pets)
type NonCombatPetBrain struct {
*BaseBrain
}
// NewNonCombatPetBrain creates a new non-combat pet brain
func NewNonCombatPetBrain(npc NPC, logger Logger) *NonCombatPetBrain {
brain := &NonCombatPetBrain{
BaseBrain: NewBaseBrain(npc, logger),
}
brain.brainType = BrainTypeNonCombatPet
return brain
}
// Think implements non-combat pet AI (just following)
func (ncpb *NonCombatPetBrain) Think() error {
// Non-combat pets don't do combat AI
if !ncpb.npc.IsPet() || ncpb.npc.IsMezzedOrStunned() {
return nil
}
if ncpb.logger != nil && ncpb.state.GetDebugLevel() >= DebugLevelDetailed {
ncpb.logger.LogDebug("Non-combat pet AI for %s", ncpb.npc.GetName())
}
// Just follow owner
owner := ncpb.npc.GetOwner()
if owner != nil {
ncpb.npc.SetTarget(owner)
distance := ncpb.npc.GetDistance(owner)
maxRange := ncpb.getMaxCombatRange()
if distance > maxRange {
ncpb.MoveCloser(owner)
}
}
return nil
}
// BlankBrain provides a minimal AI that does nothing
type BlankBrain struct {
*BaseBrain
}
// NewBlankBrain creates a new blank brain
func NewBlankBrain(npc NPC, logger Logger) *BlankBrain {
brain := &BlankBrain{
BaseBrain: NewBaseBrain(npc, logger),
}
brain.brainType = BrainTypeBlank
brain.SetThinkTick(BlankBrainTick) // Very slow tick
return brain
}
// Think does nothing for blank brains
func (bb *BlankBrain) Think() error {
// Blank brain does nothing
return nil
}
// LuaBrain allows AI to be controlled by Lua scripts
type LuaBrain struct {
*BaseBrain
scriptInterface LuaInterface
}
// NewLuaBrain creates a new Lua-controlled brain
func NewLuaBrain(npc NPC, logger Logger, luaInterface LuaInterface) *LuaBrain {
brain := &LuaBrain{
BaseBrain: NewBaseBrain(npc, logger),
scriptInterface: luaInterface,
}
brain.brainType = BrainTypeLua
return brain
}
// Think calls the Lua script's Think function
func (lb *LuaBrain) Think() error {
if lb.scriptInterface == nil {
return fmt.Errorf("no Lua interface available")
}
if lb.npc == nil {
return fmt.Errorf("brain has no body")
}
script := lb.npc.GetSpawnScript()
if script == "" {
if lb.logger != nil {
lb.logger.LogError("Lua brain set on spawn without script")
}
return fmt.Errorf("no spawn script available")
}
// Call the Lua Think function
target := lb.npc.GetTarget()
err := lb.scriptInterface.RunSpawnScript(script, "Think", lb.npc, target)
if err != nil {
if lb.logger != nil {
lb.logger.LogError("Lua script Think function failed: %v", err)
}
return fmt.Errorf("Lua Think function failed: %w", err)
}
return nil
}
// DumbFirePetBrain handles dumbfire pets (temporary combat pets)
type DumbFirePetBrain struct {
*BaseBrain
expireTime int64
}
// NewDumbFirePetBrain creates a new dumbfire pet brain
func NewDumbFirePetBrain(npc NPC, target Entity, expireTimeMS int32, logger Logger) *DumbFirePetBrain {
brain := &DumbFirePetBrain{
BaseBrain: NewBaseBrain(npc, logger),
expireTime: time.Now().UnixMilli() + int64(expireTimeMS),
}
brain.brainType = BrainTypeDumbFire
// Add maximum hate for the target
if target != nil {
brain.AddHate(target.GetID(), MaxHateValue)
}
return brain
}
// AddHate only allows hate for the initial target
func (dfpb *DumbFirePetBrain) AddHate(entityID int32, hate int32) {
// Only add hate if we don't already have a target
if dfpb.GetMostHated() == 0 {
dfpb.BaseBrain.AddHate(entityID, hate)
}
}
// Think implements dumbfire pet AI
func (dfpb *DumbFirePetBrain) Think() error {
// Check if expired
if time.Now().UnixMilli() > dfpb.expireTime {
if dfpb.npc != nil && dfpb.npc.GetHP() > 0 {
if dfpb.logger != nil {
dfpb.logger.LogDebug("Dumbfire pet %s expired", dfpb.npc.GetName())
}
dfpb.npc.KillSpawn(dfpb.npc)
}
return nil
}
// Get target
targetID := dfpb.GetMostHated()
if targetID == 0 {
// No target, kill self
if dfpb.npc != nil && dfpb.npc.GetHP() > 0 {
if dfpb.logger != nil {
dfpb.logger.LogDebug("Dumbfire pet %s has no target", dfpb.npc.GetName())
}
dfpb.npc.KillSpawn(dfpb.npc)
}
return nil
}
target := dfpb.getEntityByID(targetID)
if target == nil {
// Target no longer exists, kill self
if dfpb.npc != nil && dfpb.npc.GetHP() > 0 {
dfpb.npc.KillSpawn(dfpb.npc)
}
return nil
}
// Skip if mezzed or stunned
if dfpb.npc.IsMezzedOrStunned() {
return nil
}
// Set target if not already set
if dfpb.npc.GetTarget() != target {
dfpb.npc.SetTarget(target)
dfpb.npc.FaceTarget(target, false)
}
// Enter combat if not already
if !dfpb.npc.GetInCombat() {
dfpb.npc.CalculateRunningLocation(true)
dfpb.npc.InCombat(true)
}
distance := dfpb.npc.GetDistance(target)
// Try to cast spells if we have line of sight
if dfpb.npc.CheckLoS(target) && !dfpb.npc.IsCasting() &&
(!dfpb.HasRecovered() || !dfpb.ProcessSpell(target, distance)) {
if dfpb.logger != nil && dfpb.state.GetDebugLevel() >= DebugLevelDetailed {
dfpb.logger.LogDebug("Dumbfire pet %s attempting melee on %s",
dfpb.npc.GetName(), target.GetName())
}
dfpb.npc.FaceTarget(target, false)
dfpb.ProcessMelee(target, distance)
}
return nil
}
// GetExpireTime returns when this dumbfire pet will expire
func (dfpb *DumbFirePetBrain) GetExpireTime() int64 {
return dfpb.expireTime
}
// SetExpireTime sets when this dumbfire pet will expire
func (dfpb *DumbFirePetBrain) SetExpireTime(expireTime int64) {
dfpb.expireTime = expireTime
}
// IsExpired checks if the dumbfire pet has expired
func (dfpb *DumbFirePetBrain) IsExpired() bool {
return time.Now().UnixMilli() > dfpb.expireTime
}
// ExtendExpireTime extends the expire time by the given duration
func (dfpb *DumbFirePetBrain) ExtendExpireTime(durationMS int32) {
dfpb.expireTime += int64(durationMS)
}
// Brain factory functions
// CreateBrain creates the appropriate brain type for an NPC
func CreateBrain(npc NPC, brainType int8, logger Logger, options ...interface{}) Brain {
switch brainType {
case BrainTypeCombatPet:
return NewCombatPetBrain(npc, logger)
case BrainTypeNonCombatPet:
return NewNonCombatPetBrain(npc, logger)
case BrainTypeBlank:
return NewBlankBrain(npc, logger)
case BrainTypeLua:
if len(options) > 0 {
if luaInterface, ok := options[0].(LuaInterface); ok {
return NewLuaBrain(npc, logger, luaInterface)
}
}
return NewBaseBrain(npc, logger) // Fallback to default
case BrainTypeDumbFire:
if len(options) >= 2 {
if target, ok := options[0].(Entity); ok {
if expireTime, ok := options[1].(int32); ok {
return NewDumbFirePetBrain(npc, target, expireTime, logger)
}
}
}
return NewBaseBrain(npc, logger) // Fallback to default
default:
return NewBaseBrain(npc, logger)
}
}

93
internal/npc/constants.go Normal file
View File

@ -0,0 +1,93 @@
package npc
// AI Strategy constants
const (
AIStrategyBalanced int8 = 1
AIStrategyOffensive int8 = 2
AIStrategyDefensive int8 = 3
)
// Randomize Appearances constants
const (
RandomizeGender int32 = 1
RandomizeRace int32 = 2
RandomizeModelType int32 = 4
RandomizeFacialHairType int32 = 8
RandomizeHairType int32 = 16
RandomizeWingType int32 = 64
RandomizeCheekType int32 = 128
RandomizeChinType int32 = 256
RandomizeEarType int32 = 512
RandomizeEyeBrowType int32 = 1024
RandomizeEyeType int32 = 2048
RandomizeLipType int32 = 4096
RandomizeNoseType int32 = 8192
RandomizeEyeColor int32 = 16384
RandomizeHairColor1 int32 = 32768
RandomizeHairColor2 int32 = 65536
RandomizeHairHighlight int32 = 131072
RandomizeHairFaceColor int32 = 262144
RandomizeHairFaceHigh int32 = 524288
RandomizeHairTypeColor int32 = 1048576
RandomizeHairTypeHigh int32 = 2097152
RandomizeSkinColor int32 = 4194304
RandomizeWingColor1 int32 = 8388608
RandomizeWingColor2 int32 = 16777216
RandomizeAll int32 = 33554431
)
// Pet Type constants
const (
PetTypeCombat int8 = 1
PetTypeCharmed int8 = 2
PetTypeDeity int8 = 3
PetTypeCosmetic int8 = 4
PetTypeDumbfire int8 = 5
)
// Cast Type constants
const (
CastOnSpawn int8 = 0
CastOnAggro int8 = 1
MaxCastTypes int8 = 2
)
// Default values
const (
DefaultCastPercentage int8 = 25
DefaultAggroRadius float32 = 10.0
DefaultRunbackSpeed float32 = 2.0
MaxSkillBonuses int = 100
MaxNPCSpells int = 50
MaxPauseTime int32 = 300000 // 5 minutes max pause
)
// NPC validation constants
const (
MinNPCLevel int8 = 1
MaxNPCLevel int8 = 100
MaxNPCNameLen int = 64
MinAppearanceID int32 = 0
MaxAppearanceID int32 = 999999
)
// Combat constants
const (
RunbackDistanceThreshold float32 = 5.0
HPRatioMin int8 = -100
HPRatioMax int8 = 100
DefaultMaxPetLevel int8 = 20
)
// Color randomization constants
const (
ColorRandomMin int8 = 0
ColorRandomMax int8 = 255
ColorVariation int8 = 30
)
// Movement constants
const (
DefaultPauseCheckMS int32 = 100
RunbackCheckMS int32 = 250
)

570
internal/npc/interfaces.go Normal file
View File

@ -0,0 +1,570 @@
package npc
// Database interface for NPC persistence
type Database interface {
LoadAllNPCs() ([]*NPC, error)
SaveNPC(npc *NPC) error
DeleteNPC(npcID int32) error
LoadNPCSpells(npcID int32) ([]*NPCSpell, error)
SaveNPCSpells(npcID int32, spells []*NPCSpell) error
LoadNPCSkills(npcID int32) (map[string]*Skill, error)
SaveNPCSkills(npcID int32, skills map[string]*Skill) error
}
// Logger interface for NPC logging
type Logger interface {
LogInfo(message string, args ...interface{})
LogError(message string, args ...interface{})
LogDebug(message string, args ...interface{})
LogWarning(message string, args ...interface{})
}
// Client interface for NPC-related client operations
type Client interface {
GetPlayer() Player
GetVersion() int16
SendNPCUpdate(npcData []byte) error
SendCombatUpdate(combatData []byte) error
SendSpellCast(spellData []byte) error
}
// Player interface for NPC-related player operations
type Player interface {
GetCharacterID() int32
GetName() string
GetLevel() int8
GetZoneID() int32
GetX() float32
GetY() float32
GetZ() float32
IsInCombat() bool
GetTarget() *NPC
SendMessage(message string)
}
// Zone interface for NPC zone operations
type Zone interface {
GetZoneID() int32
GetNPCs() []*NPC
AddNPC(npc *NPC) error
RemoveNPC(npcID int32) error
GetPlayersInRange(x, y, z, radius float32) []Player
ProcessEntityCommand(command string, client Client, target *NPC) error
CallSpawnScript(npc *NPC, scriptType string, args ...interface{}) error
}
// SpellManager interface for spell system integration
type SpellManager interface {
GetSpell(spellID int32, tier int8) Spell
CastSpell(caster *NPC, target interface{}, spell Spell) error
GetSpellEffect(entity interface{}, spellID int32) SpellEffect
ProcessSpell(spell Spell, caster *NPC, target interface{}) error
}
// Spell interface for spell data
type Spell interface {
GetSpellID() int32
GetName() string
GetTier() int8
GetRange() float32
GetMinRange() float32
GetPowerRequired() int32
IsFriendlySpell() bool
IsToggleSpell() bool
GetCastTime() int32
GetRecastTime() int32
}
// SpellEffect interface for active spell effects
type SpellEffect interface {
GetSpellID() int32
GetTier() int8
GetDuration() int32
GetRemainingTime() int32
IsExpired() bool
}
// SkillManager interface for skill system integration
type SkillManager interface {
GetSkill(skillID int32) MasterSkill
GetSkillByName(name string) MasterSkill
ApplySkillBonus(entity interface{}, skillID int32, bonus float32) error
RemoveSkillBonus(entity interface{}, skillID int32, bonus float32) error
}
// MasterSkill interface for skill definitions
type MasterSkill interface {
GetSkillID() int32
GetName() string
GetDescription() string
GetMaxValue() int16
}
// AppearanceManager interface for appearance system integration
type AppearanceManager interface {
GetAppearance(appearanceID int32) Appearance
GetAppearancesByName(name string) []Appearance
RandomizeAppearance(npc *NPC, flags int32) error
}
// Appearance interface for appearance data
type Appearance interface {
GetAppearanceID() int32
GetName() string
GetModelType() int16
GetRace() int16
GetGender() int8
}
// MovementManager interface for movement system integration
type MovementManager interface {
StartMovement(npc *NPC, x, y, z float32) error
StopMovement(npc *NPC) error
SetSpeed(npc *NPC, speed float32) error
NavigateToLocation(npc *NPC, x, y, z float32) error
IsMoving(npc *NPC) bool
}
// CombatManager interface for combat system integration
type CombatManager interface {
StartCombat(npc *NPC, target interface{}) error
EndCombat(npc *NPC) error
ProcessCombatRound(npc *NPC) error
CalculateDamage(attacker *NPC, target interface{}) int32
ApplyDamage(target interface{}, damage int32) error
}
// NPCAware interface for entities that can interact with NPCs
type NPCAware interface {
GetNPC() *NPC
IsNPC() bool
HandleNPCInteraction(npc *NPC, interactionType string) error
ReceiveNPCCommand(npc *NPC, command string) error
}
// EntityAdapter provides NPC functionality for entities
type EntityAdapter struct {
npc *NPC
logger Logger
}
// NewEntityAdapter creates a new entity adapter
func NewEntityAdapter(npc *NPC, logger Logger) *EntityAdapter {
return &EntityAdapter{
npc: npc,
logger: logger,
}
}
// GetNPC returns the associated NPC
func (ea *EntityAdapter) GetNPC() *NPC {
return ea.npc
}
// IsNPC always returns true for entity adapters
func (ea *EntityAdapter) IsNPC() bool {
return true
}
// HandleNPCInteraction processes interactions with other NPCs
func (ea *EntityAdapter) HandleNPCInteraction(otherNPC *NPC, interactionType string) error {
if ea.npc == nil || otherNPC == nil {
return fmt.Errorf("invalid NPC for interaction")
}
// Handle different interaction types
switch interactionType {
case "aggro":
return ea.handleAggroInteraction(otherNPC)
case "assist":
return ea.handleAssistInteraction(otherNPC)
case "trade":
return ea.handleTradeInteraction(otherNPC)
default:
if ea.logger != nil {
ea.logger.LogWarning("Unknown NPC interaction type: %s", interactionType)
}
return fmt.Errorf("unknown interaction type: %s", interactionType)
}
}
// ReceiveNPCCommand processes commands from other NPCs
func (ea *EntityAdapter) ReceiveNPCCommand(otherNPC *NPC, command string) error {
if ea.npc == nil || otherNPC == nil {
return fmt.Errorf("invalid NPC for command")
}
// Process the command
switch command {
case "follow":
return ea.handleFollowCommand(otherNPC)
case "attack":
return ea.handleAttackCommand(otherNPC)
case "retreat":
return ea.handleRetreatCommand(otherNPC)
default:
if ea.logger != nil {
ea.logger.LogWarning("Unknown NPC command: %s", command)
}
return fmt.Errorf("unknown command: %s", command)
}
}
// handleAggroInteraction processes aggro interactions
func (ea *EntityAdapter) handleAggroInteraction(otherNPC *NPC) error {
// TODO: Implement aggro logic between NPCs
if ea.logger != nil {
ea.logger.LogDebug("NPC %d received aggro from NPC %d",
ea.npc.GetNPCID(), otherNPC.GetNPCID())
}
return nil
}
// handleAssistInteraction processes assist interactions
func (ea *EntityAdapter) handleAssistInteraction(otherNPC *NPC) error {
// TODO: Implement assist logic between NPCs
if ea.logger != nil {
ea.logger.LogDebug("NPC %d received assist request from NPC %d",
ea.npc.GetNPCID(), otherNPC.GetNPCID())
}
return nil
}
// handleTradeInteraction processes trade interactions
func (ea *EntityAdapter) handleTradeInteraction(otherNPC *NPC) error {
// TODO: Implement trade logic between NPCs
if ea.logger != nil {
ea.logger.LogDebug("NPC %d received trade request from NPC %d",
ea.npc.GetNPCID(), otherNPC.GetNPCID())
}
return nil
}
// handleFollowCommand processes follow commands
func (ea *EntityAdapter) handleFollowCommand(otherNPC *NPC) error {
// TODO: Implement follow logic
if ea.logger != nil {
ea.logger.LogDebug("NPC %d received follow command from NPC %d",
ea.npc.GetNPCID(), otherNPC.GetNPCID())
}
return nil
}
// handleAttackCommand processes attack commands
func (ea *EntityAdapter) handleAttackCommand(otherNPC *NPC) error {
// TODO: Implement attack logic
if ea.logger != nil {
ea.logger.LogDebug("NPC %d received attack command from NPC %d",
ea.npc.GetNPCID(), otherNPC.GetNPCID())
}
return nil
}
// handleRetreatCommand processes retreat commands
func (ea *EntityAdapter) handleRetreatCommand(otherNPC *NPC) error {
// TODO: Implement retreat logic
if ea.logger != nil {
ea.logger.LogDebug("NPC %d received retreat command from NPC %d",
ea.npc.GetNPCID(), otherNPC.GetNPCID())
}
return nil
}
// SpellCasterAdapter provides spell casting functionality for NPCs
type SpellCasterAdapter struct {
npc *NPC
spellManager SpellManager
logger Logger
}
// NewSpellCasterAdapter creates a new spell caster adapter
func NewSpellCasterAdapter(npc *NPC, spellManager SpellManager, logger Logger) *SpellCasterAdapter {
return &SpellCasterAdapter{
npc: npc,
spellManager: spellManager,
logger: logger,
}
}
// GetNextSpell selects the next spell to cast based on AI strategy
func (sca *SpellCasterAdapter) GetNextSpell(target interface{}, distance float32) Spell {
if sca.npc == nil || sca.spellManager == nil {
return nil
}
// Check cast-on-aggro spells first
if !sca.npc.castOnAggroCompleted {
spell := sca.getNextCastOnAggroSpell(target)
if spell != nil {
return spell
}
sca.npc.castOnAggroCompleted = true
}
// Get spells based on AI strategy
strategy := sca.npc.GetAIStrategy()
return sca.getNextSpellByStrategy(target, distance, strategy)
}
// GetNextBuffSpell selects the next buff spell to cast
func (sca *SpellCasterAdapter) GetNextBuffSpell(target interface{}) Spell {
if sca.npc == nil || sca.spellManager == nil {
return nil
}
// Check cast-on-spawn spells first
castOnSpells := sca.npc.castOnSpells[CastOnSpawn]
for _, npcSpell := range castOnSpells {
spell := sca.spellManager.GetSpell(npcSpell.GetSpellID(), npcSpell.GetTier())
if spell != nil {
// Check if target already has this effect
if effect := sca.spellManager.GetSpellEffect(target, spell.GetSpellID()); effect != nil {
if effect.GetTier() < spell.GetTier() {
return spell // Upgrade existing effect
}
} else {
return spell // New effect
}
}
}
// Check regular spells for buffs
for _, npcSpell := range sca.npc.spells {
spell := sca.spellManager.GetSpell(npcSpell.GetSpellID(), npcSpell.GetTier())
if spell != nil && spell.IsFriendlySpell() && spell.IsToggleSpell() {
// Check if target already has this effect
if effect := sca.spellManager.GetSpellEffect(target, spell.GetSpellID()); effect != nil {
if effect.GetTier() < spell.GetTier() {
return spell // Upgrade existing effect
}
} else {
return spell // New effect
}
}
}
return nil
}
// CastSpell attempts to cast a spell
func (sca *SpellCasterAdapter) CastSpell(target interface{}, spell Spell) error {
if sca.npc == nil || sca.spellManager == nil || spell == nil {
return fmt.Errorf("invalid parameters for spell casting")
}
// Check casting conditions
if err := sca.checkCastingConditions(spell); err != nil {
return fmt.Errorf("casting conditions not met: %w", err)
}
// Cast the spell
if err := sca.spellManager.CastSpell(sca.npc, target, spell); err != nil {
return fmt.Errorf("failed to cast spell: %w", err)
}
if sca.logger != nil {
sca.logger.LogDebug("NPC %d cast spell %s (%d)",
sca.npc.GetNPCID(), spell.GetName(), spell.GetSpellID())
}
return nil
}
// getNextCastOnAggroSpell selects cast-on-aggro spells
func (sca *SpellCasterAdapter) getNextCastOnAggroSpell(target interface{}) Spell {
castOnSpells := sca.npc.castOnSpells[CastOnAggro]
for _, npcSpell := range castOnSpells {
spell := sca.spellManager.GetSpell(npcSpell.GetSpellID(), npcSpell.GetTier())
if spell != nil {
// Check if target doesn't already have this effect
if effect := sca.spellManager.GetSpellEffect(target, spell.GetSpellID()); effect == nil {
return spell
}
}
}
return nil
}
// getNextSpellByStrategy selects spells based on AI strategy
func (sca *SpellCasterAdapter) getNextSpellByStrategy(target interface{}, distance float32, strategy int8) Spell {
// TODO: Implement more sophisticated spell selection based on strategy
for _, npcSpell := range sca.npc.spells {
// Check HP ratio requirements
if npcSpell.GetRequiredHPRatio() != 0 {
// TODO: Implement HP ratio checking
}
spell := sca.spellManager.GetSpell(npcSpell.GetSpellID(), npcSpell.GetTier())
if spell == nil {
continue
}
// Check strategy compatibility
if strategy == AIStrategyOffensive && spell.IsFriendlySpell() {
continue
}
if strategy == AIStrategyDefensive && !spell.IsFriendlySpell() {
continue
}
// Check range and power requirements
if distance <= spell.GetRange() && distance >= spell.GetMinRange() {
// TODO: Check power requirements
return spell
}
}
return nil
}
// checkCastingConditions validates spell casting conditions
func (sca *SpellCasterAdapter) checkCastingConditions(spell Spell) error {
if sca.npc.Entity == nil {
return fmt.Errorf("NPC entity is nil")
}
// TODO: Implement power checking, cooldown checking, etc.
return nil
}
// CombatAdapter provides combat functionality for NPCs
type CombatAdapter struct {
npc *NPC
combatManager CombatManager
logger Logger
}
// NewCombatAdapter creates a new combat adapter
func NewCombatAdapter(npc *NPC, combatManager CombatManager, logger Logger) *CombatAdapter {
return &CombatAdapter{
npc: npc,
combatManager: combatManager,
logger: logger,
}
}
// EnterCombat handles entering combat state
func (ca *CombatAdapter) EnterCombat(target interface{}) error {
if ca.npc == nil {
return fmt.Errorf("NPC is nil")
}
// Start combat through combat manager
if ca.combatManager != nil {
if err := ca.combatManager.StartCombat(ca.npc, target); err != nil {
return fmt.Errorf("failed to start combat: %w", err)
}
}
// Update NPC state
ca.npc.InCombat(true)
if ca.logger != nil {
ca.logger.LogDebug("NPC %d entered combat", ca.npc.GetNPCID())
}
return nil
}
// ExitCombat handles exiting combat state
func (ca *CombatAdapter) ExitCombat() error {
if ca.npc == nil {
return fmt.Errorf("NPC is nil")
}
// End combat through combat manager
if ca.combatManager != nil {
if err := ca.combatManager.EndCombat(ca.npc); err != nil {
return fmt.Errorf("failed to end combat: %w", err)
}
}
// Update NPC state
ca.npc.InCombat(false)
if ca.logger != nil {
ca.logger.LogDebug("NPC %d exited combat", ca.npc.GetNPCID())
}
return nil
}
// ProcessCombat handles combat processing
func (ca *CombatAdapter) ProcessCombat() error {
if ca.npc == nil {
return fmt.Errorf("NPC is nil")
}
if ca.combatManager != nil {
return ca.combatManager.ProcessCombatRound(ca.npc)
}
return nil
}
// MovementAdapter provides movement functionality for NPCs
type MovementAdapter struct {
npc *NPC
movementManager MovementManager
logger Logger
}
// NewMovementAdapter creates a new movement adapter
func NewMovementAdapter(npc *NPC, movementManager MovementManager, logger Logger) *MovementAdapter {
return &MovementAdapter{
npc: npc,
movementManager: movementManager,
logger: logger,
}
}
// MoveToLocation moves the NPC to a specific location
func (ma *MovementAdapter) MoveToLocation(x, y, z float32) error {
if ma.npc == nil {
return fmt.Errorf("NPC is nil")
}
if ma.movementManager != nil {
return ma.movementManager.NavigateToLocation(ma.npc, x, y, z)
}
return fmt.Errorf("movement manager not available")
}
// StopMovement stops the NPC's movement
func (ma *MovementAdapter) StopMovement() error {
if ma.npc == nil {
return fmt.Errorf("NPC is nil")
}
if ma.movementManager != nil {
return ma.movementManager.StopMovement(ma.npc)
}
return fmt.Errorf("movement manager not available")
}
// IsMoving checks if the NPC is currently moving
func (ma *MovementAdapter) IsMoving() bool {
if ma.npc == nil || ma.movementManager == nil {
return false
}
return ma.movementManager.IsMoving(ma.npc)
}
// RunbackToSpawn moves the NPC back to its spawn location
func (ma *MovementAdapter) RunbackToSpawn() error {
if ma.npc == nil {
return fmt.Errorf("NPC is nil")
}
runbackLocation := ma.npc.GetRunbackLocation()
if runbackLocation == nil {
return fmt.Errorf("no runback location set")
}
return ma.MoveToLocation(runbackLocation.X, runbackLocation.Y, runbackLocation.Z)
}

762
internal/npc/manager.go Normal file
View File

@ -0,0 +1,762 @@
package npc
import (
"fmt"
"math/rand"
"strings"
"sync"
"time"
)
// Manager provides high-level management of the NPC system
type Manager struct {
npcs map[int32]*NPC // NPCs indexed by ID
npcsByZone map[int32][]*NPC // NPCs indexed by zone ID
npcsByAppearance map[int32][]*NPC // NPCs indexed by appearance ID
database Database // Database interface
logger Logger // Logger interface
spellManager SpellManager // Spell system interface
skillManager SkillManager // Skill system interface
appearanceManager AppearanceManager // Appearance system interface
mutex sync.RWMutex // Thread safety
// Statistics
totalNPCs int64
npcsInCombat int64
spellCastCount int64
skillUsageCount int64
runbackCount int64
aiStrategyCounts map[int8]int64
// Configuration
maxNPCs int32
defaultAggroRadius float32
enableAI bool
}
// NewManager creates a new NPC manager
func NewManager(database Database, logger Logger) *Manager {
return &Manager{
npcs: make(map[int32]*NPC),
npcsByZone: make(map[int32][]*NPC),
npcsByAppearance: make(map[int32][]*NPC),
database: database,
logger: logger,
aiStrategyCounts: make(map[int8]int64),
maxNPCs: 10000, // Default limit
defaultAggroRadius: DefaultAggroRadius,
enableAI: true,
}
}
// Initialize loads NPCs from database and sets up the system
func (m *Manager) Initialize() error {
if m.logger != nil {
m.logger.LogInfo("Initializing NPC manager...")
}
if m.database == nil {
if m.logger != nil {
m.logger.LogWarning("No database provided, starting with empty NPC list")
}
return nil
}
// Load NPCs from database
npcs, err := m.database.LoadAllNPCs()
if err != nil {
return fmt.Errorf("failed to load NPCs from database: %w", err)
}
for _, npc := range npcs {
if err := m.addNPCInternal(npc); err != nil {
if m.logger != nil {
m.logger.LogError("Failed to add NPC %d: %v", npc.GetNPCID(), err)
}
}
}
if m.logger != nil {
m.logger.LogInfo("Loaded %d NPCs from database", len(npcs))
}
return nil
}
// AddNPC adds a new NPC to the system
func (m *Manager) AddNPC(npc *NPC) error {
if npc == nil {
return fmt.Errorf("NPC cannot be nil")
}
if !npc.IsValid() {
return fmt.Errorf("NPC is not valid: %s", npc.String())
}
m.mutex.Lock()
defer m.mutex.Unlock()
if len(m.npcs) >= int(m.maxNPCs) {
return fmt.Errorf("maximum NPC limit reached (%d)", m.maxNPCs)
}
return m.addNPCInternal(npc)
}
// addNPCInternal adds an NPC without locking (internal use)
func (m *Manager) addNPCInternal(npc *NPC) error {
npcID := npc.GetNPCID()
// Check for duplicate ID
if _, exists := m.npcs[npcID]; exists {
return fmt.Errorf("NPC with ID %d already exists", npcID)
}
// Add to main index
m.npcs[npcID] = npc
// Add to zone index
if npc.Entity != nil {
zoneID := npc.Entity.GetZoneID()
m.npcsByZone[zoneID] = append(m.npcsByZone[zoneID], npc)
}
// Add to appearance index
appearanceID := npc.GetAppearanceID()
m.npcsByAppearance[appearanceID] = append(m.npcsByAppearance[appearanceID], npc)
// Update statistics
m.totalNPCs++
strategy := npc.GetAIStrategy()
m.aiStrategyCounts[strategy]++
// Save to database if available
if m.database != nil {
if err := m.database.SaveNPC(npc); err != nil {
// Remove from indexes if database save failed
delete(m.npcs, npcID)
m.removeFromZoneIndex(npc)
m.removeFromAppearanceIndex(npc)
m.totalNPCs--
m.aiStrategyCounts[strategy]--
return fmt.Errorf("failed to save NPC to database: %w", err)
}
}
if m.logger != nil {
m.logger.LogInfo("Added NPC %d: %s", npcID, npc.String())
}
return nil
}
// GetNPC retrieves an NPC by ID
func (m *Manager) GetNPC(id int32) *NPC {
m.mutex.RLock()
defer m.mutex.RUnlock()
return m.npcs[id]
}
// GetNPCsByZone retrieves all NPCs in a specific zone
func (m *Manager) GetNPCsByZone(zoneID int32) []*NPC {
m.mutex.RLock()
defer m.mutex.RUnlock()
npcs := m.npcsByZone[zoneID]
result := make([]*NPC, len(npcs))
copy(result, npcs)
return result
}
// GetNPCsByAppearance retrieves all NPCs with a specific appearance
func (m *Manager) GetNPCsByAppearance(appearanceID int32) []*NPC {
m.mutex.RLock()
defer m.mutex.RUnlock()
npcs := m.npcsByAppearance[appearanceID]
result := make([]*NPC, len(npcs))
copy(result, npcs)
return result
}
// RemoveNPC removes an NPC from the system
func (m *Manager) RemoveNPC(id int32) error {
m.mutex.Lock()
defer m.mutex.Unlock()
npc, exists := m.npcs[id]
if !exists {
return fmt.Errorf("NPC with ID %d does not exist", id)
}
// Remove from database first if available
if m.database != nil {
if err := m.database.DeleteNPC(id); err != nil {
return fmt.Errorf("failed to delete NPC from database: %w", err)
}
}
// Remove from all indexes
delete(m.npcs, id)
m.removeFromZoneIndex(npc)
m.removeFromAppearanceIndex(npc)
// Update statistics
m.totalNPCs--
strategy := npc.GetAIStrategy()
if count := m.aiStrategyCounts[strategy]; count > 0 {
m.aiStrategyCounts[strategy]--
}
if m.logger != nil {
m.logger.LogInfo("Removed NPC %d", id)
}
return nil
}
// UpdateNPC updates an existing NPC
func (m *Manager) UpdateNPC(npc *NPC) error {
if npc == nil {
return fmt.Errorf("NPC cannot be nil")
}
if !npc.IsValid() {
return fmt.Errorf("NPC is not valid: %s", npc.String())
}
m.mutex.Lock()
defer m.mutex.Unlock()
npcID := npc.GetNPCID()
oldNPC, exists := m.npcs[npcID]
if !exists {
return fmt.Errorf("NPC with ID %d does not exist", npcID)
}
// Update indexes if zone or appearance changed
if npc.Entity != nil && oldNPC.Entity != nil {
if npc.Entity.GetZoneID() != oldNPC.Entity.GetZoneID() {
m.removeFromZoneIndex(oldNPC)
zoneID := npc.Entity.GetZoneID()
m.npcsByZone[zoneID] = append(m.npcsByZone[zoneID], npc)
}
}
if npc.GetAppearanceID() != oldNPC.GetAppearanceID() {
m.removeFromAppearanceIndex(oldNPC)
appearanceID := npc.GetAppearanceID()
m.npcsByAppearance[appearanceID] = append(m.npcsByAppearance[appearanceID], npc)
}
// Update AI strategy statistics
oldStrategy := oldNPC.GetAIStrategy()
newStrategy := npc.GetAIStrategy()
if oldStrategy != newStrategy {
if count := m.aiStrategyCounts[oldStrategy]; count > 0 {
m.aiStrategyCounts[oldStrategy]--
}
m.aiStrategyCounts[newStrategy]++
}
// Update main index
m.npcs[npcID] = npc
// Save to database if available
if m.database != nil {
if err := m.database.SaveNPC(npc); err != nil {
return fmt.Errorf("failed to save NPC to database: %w", err)
}
}
if m.logger != nil {
m.logger.LogInfo("Updated NPC %d: %s", npcID, npc.String())
}
return nil
}
// CreateNPCFromTemplate creates a new NPC from an existing template
func (m *Manager) CreateNPCFromTemplate(templateID, newID int32) (*NPC, error) {
template := m.GetNPC(templateID)
if template == nil {
return nil, fmt.Errorf("template NPC with ID %d not found", templateID)
}
// Create new NPC from template
newNPC := NewNPCFromExisting(template)
newNPC.SetNPCID(newID)
// Add to system
if err := m.AddNPC(newNPC); err != nil {
return nil, fmt.Errorf("failed to add new NPC: %w", err)
}
return newNPC, nil
}
// GetRandomNPCByAppearance returns a random NPC with the specified appearance
func (m *Manager) GetRandomNPCByAppearance(appearanceID int32) *NPC {
npcs := m.GetNPCsByAppearance(appearanceID)
if len(npcs) == 0 {
return nil
}
return npcs[rand.Intn(len(npcs))]
}
// ProcessCombat handles combat processing for all NPCs
func (m *Manager) ProcessCombat() {
m.mutex.RLock()
npcs := make([]*NPC, 0, len(m.npcs))
for _, npc := range m.npcs {
if npc.Entity != nil && npc.Entity.GetInCombat() {
npcs = append(npcs, npc)
}
}
m.mutex.RUnlock()
// Process combat for each NPC in combat
for _, npc := range npcs {
npc.ProcessCombat()
}
// Update combat statistics
m.mutex.Lock()
m.npcsInCombat = int64(len(npcs))
m.mutex.Unlock()
}
// ProcessAI handles AI processing for all NPCs
func (m *Manager) ProcessAI() {
if !m.enableAI {
return
}
m.mutex.RLock()
npcs := make([]*NPC, 0, len(m.npcs))
for _, npc := range m.npcs {
npcs = append(npcs, npc)
}
m.mutex.RUnlock()
// Process AI for each NPC
for _, npc := range npcs {
if brain := npc.GetBrain(); brain != nil && brain.IsActive() {
if err := brain.Think(); err != nil && m.logger != nil {
m.logger.LogError("AI brain error for NPC %d: %v", npc.GetNPCID(), err)
}
}
}
}
// ProcessMovement handles movement processing for all NPCs
func (m *Manager) ProcessMovement() {
m.mutex.RLock()
npcs := make([]*NPC, 0, len(m.npcs))
for _, npc := range m.npcs {
npcs = append(npcs, npc)
}
m.mutex.RUnlock()
// Process movement for each NPC
for _, npc := range npcs {
// Check pause timer
if npc.IsPauseMovementTimerActive() {
continue
}
// Handle runback if needed
if npc.callRunback && npc.GetRunbackLocation() != nil {
npc.callRunback = false
npc.Runback(0, true)
m.mutex.Lock()
m.runbackCount++
m.mutex.Unlock()
}
}
}
// GetStatistics returns NPC system statistics
func (m *Manager) GetStatistics() *NPCStatistics {
m.mutex.RLock()
defer m.mutex.RUnlock()
// Create AI strategy counts by name
aiCounts := make(map[string]int)
for strategy, count := range m.aiStrategyCounts {
switch strategy {
case AIStrategyBalanced:
aiCounts["balanced"] = int(count)
case AIStrategyOffensive:
aiCounts["offensive"] = int(count)
case AIStrategyDefensive:
aiCounts["defensive"] = int(count)
default:
aiCounts[fmt.Sprintf("unknown_%d", strategy)] = int(count)
}
}
// Calculate average aggro radius
var totalAggro float32
npcCount := 0
for _, npc := range m.npcs {
totalAggro += npc.GetAggroRadius()
npcCount++
}
var avgAggro float32
if npcCount > 0 {
avgAggro = totalAggro / float32(npcCount)
}
// Count NPCs with spells and skills
npcsWithSpells := 0
npcsWithSkills := 0
for _, npc := range m.npcs {
if npc.HasSpells() {
npcsWithSpells++
}
if len(npc.skills) > 0 {
npcsWithSkills++
}
}
return &NPCStatistics{
TotalNPCs: int(m.totalNPCs),
NPCsInCombat: int(m.npcsInCombat),
NPCsWithSpells: npcsWithSpells,
NPCsWithSkills: npcsWithSkills,
AIStrategyCounts: aiCounts,
SpellCastCount: m.spellCastCount,
SkillUsageCount: m.skillUsageCount,
RunbackCount: m.runbackCount,
AverageAggroRadius: avgAggro,
}
}
// ValidateAllNPCs validates all NPCs in the system
func (m *Manager) ValidateAllNPCs() []string {
m.mutex.RLock()
npcs := make([]*NPC, 0, len(m.npcs))
for _, npc := range m.npcs {
npcs = append(npcs, npc)
}
m.mutex.RUnlock()
var issues []string
for _, npc := range npcs {
if !npc.IsValid() {
issues = append(issues, fmt.Sprintf("NPC %d is invalid: %s", npc.GetNPCID(), npc.String()))
}
}
return issues
}
// ProcessCommand handles NPC-related commands
func (m *Manager) ProcessCommand(command string, args []string) (string, error) {
switch command {
case "stats":
return m.handleStatsCommand(args)
case "validate":
return m.handleValidateCommand(args)
case "list":
return m.handleListCommand(args)
case "info":
return m.handleInfoCommand(args)
case "create":
return m.handleCreateCommand(args)
case "remove":
return m.handleRemoveCommand(args)
case "search":
return m.handleSearchCommand(args)
case "combat":
return m.handleCombatCommand(args)
default:
return "", fmt.Errorf("unknown NPC command: %s", command)
}
}
// Command handlers
func (m *Manager) handleStatsCommand(args []string) (string, error) {
stats := m.GetStatistics()
result := "NPC System Statistics:\n"
result += fmt.Sprintf("Total NPCs: %d\n", stats.TotalNPCs)
result += fmt.Sprintf("NPCs in Combat: %d\n", stats.NPCsInCombat)
result += fmt.Sprintf("NPCs with Spells: %d\n", stats.NPCsWithSpells)
result += fmt.Sprintf("NPCs with Skills: %d\n", stats.NPCsWithSkills)
result += fmt.Sprintf("Average Aggro Radius: %.2f\n", stats.AverageAggroRadius)
result += fmt.Sprintf("Spell Casts: %d\n", stats.SpellCastCount)
result += fmt.Sprintf("Skill Uses: %d\n", stats.SkillUsageCount)
result += fmt.Sprintf("Runbacks: %d\n", stats.RunbackCount)
if len(stats.AIStrategyCounts) > 0 {
result += "\nAI Strategy Distribution:\n"
for strategy, count := range stats.AIStrategyCounts {
result += fmt.Sprintf(" %s: %d\n", strategy, count)
}
}
return result, nil
}
func (m *Manager) handleValidateCommand(args []string) (string, error) {
issues := m.ValidateAllNPCs()
if len(issues) == 0 {
return "All NPCs are valid.", nil
}
result := fmt.Sprintf("Found %d issues with NPCs:\n", len(issues))
for i, issue := range issues {
if i >= 10 { // Limit output
result += "... (and more)\n"
break
}
result += fmt.Sprintf("%d. %s\n", i+1, issue)
}
return result, nil
}
func (m *Manager) handleListCommand(args []string) (string, error) {
m.mutex.RLock()
npcs := make([]*NPC, 0, len(m.npcs))
for _, npc := range m.npcs {
npcs = append(npcs, npc)
}
m.mutex.RUnlock()
if len(npcs) == 0 {
return "No NPCs loaded.", nil
}
result := fmt.Sprintf("NPCs (%d):\n", len(npcs))
count := 0
for _, npc := range npcs {
if count >= 20 { // Limit output
result += "... (and more)\n"
break
}
result += fmt.Sprintf(" %d: %s\n", npc.GetNPCID(), npc.String())
count++
}
return result, nil
}
func (m *Manager) handleInfoCommand(args []string) (string, error) {
if len(args) == 0 {
return "", fmt.Errorf("NPC ID required")
}
var npcID int32
if _, err := fmt.Sscanf(args[0], "%d", &npcID); err != nil {
return "", fmt.Errorf("invalid NPC ID: %s", args[0])
}
npc := m.GetNPC(npcID)
if npc == nil {
return fmt.Sprintf("NPC %d not found.", npcID), nil
}
result := fmt.Sprintf("NPC Information:\n")
result += fmt.Sprintf("ID: %d\n", npc.GetNPCID())
result += fmt.Sprintf("Appearance ID: %d\n", npc.GetAppearanceID())
result += fmt.Sprintf("AI Strategy: %d\n", npc.GetAIStrategy())
result += fmt.Sprintf("Cast Percentage: %d%%\n", npc.GetCastPercentage())
result += fmt.Sprintf("Aggro Radius: %.2f\n", npc.GetAggroRadius())
result += fmt.Sprintf("Has Spells: %v\n", npc.HasSpells())
result += fmt.Sprintf("Running Back: %v\n", npc.IsRunningBack())
if npc.Entity != nil {
result += fmt.Sprintf("Name: %s\n", npc.Entity.GetName())
result += fmt.Sprintf("Level: %d\n", npc.Entity.GetLevel())
result += fmt.Sprintf("Zone: %d\n", npc.Entity.GetZoneID())
result += fmt.Sprintf("In Combat: %v\n", npc.Entity.GetInCombat())
}
return result, nil
}
func (m *Manager) handleCreateCommand(args []string) (string, error) {
if len(args) < 2 {
return "", fmt.Errorf("usage: create <template_id> <new_id>")
}
var templateID, newID int32
if _, err := fmt.Sscanf(args[0], "%d", &templateID); err != nil {
return "", fmt.Errorf("invalid template ID: %s", args[0])
}
if _, err := fmt.Sscanf(args[1], "%d", &newID); err != nil {
return "", fmt.Errorf("invalid new ID: %s", args[1])
}
npc, err := m.CreateNPCFromTemplate(templateID, newID)
if err != nil {
return "", fmt.Errorf("failed to create NPC: %w", err)
}
return fmt.Sprintf("Successfully created NPC %d from template %d", newID, templateID), nil
}
func (m *Manager) handleRemoveCommand(args []string) (string, error) {
if len(args) == 0 {
return "", fmt.Errorf("NPC ID required")
}
var npcID int32
if _, err := fmt.Sscanf(args[0], "%d", &npcID); err != nil {
return "", fmt.Errorf("invalid NPC ID: %s", args[0])
}
if err := m.RemoveNPC(npcID); err != nil {
return "", fmt.Errorf("failed to remove NPC: %w", err)
}
return fmt.Sprintf("Successfully removed NPC %d", npcID), nil
}
func (m *Manager) handleSearchCommand(args []string) (string, error) {
if len(args) == 0 {
return "", fmt.Errorf("search term required")
}
searchTerm := strings.ToLower(args[0])
m.mutex.RLock()
var results []*NPC
for _, npc := range m.npcs {
if npc.Entity != nil {
name := strings.ToLower(npc.Entity.GetName())
if strings.Contains(name, searchTerm) {
results = append(results, npc)
}
}
}
m.mutex.RUnlock()
if len(results) == 0 {
return fmt.Sprintf("No NPCs found matching '%s'.", args[0]), nil
}
result := fmt.Sprintf("Found %d NPCs matching '%s':\n", len(results), args[0])
for i, npc := range results {
if i >= 20 { // Limit output
result += "... (and more)\n"
break
}
result += fmt.Sprintf(" %d: %s\n", npc.GetNPCID(), npc.String())
}
return result, nil
}
func (m *Manager) handleCombatCommand(args []string) (string, error) {
result := "Combat Processing Status:\n"
result += fmt.Sprintf("NPCs in Combat: %d\n", m.npcsInCombat)
result += fmt.Sprintf("Total Spell Casts: %d\n", m.spellCastCount)
result += fmt.Sprintf("Total Runbacks: %d\n", m.runbackCount)
return result, nil
}
// Helper methods
func (m *Manager) removeFromZoneIndex(npc *NPC) {
if npc.Entity == nil {
return
}
zoneID := npc.Entity.GetZoneID()
npcs := m.npcsByZone[zoneID]
for i, n := range npcs {
if n == npc {
// Remove from slice
m.npcsByZone[zoneID] = append(npcs[:i], npcs[i+1:]...)
break
}
}
// Clean up empty slices
if len(m.npcsByZone[zoneID]) == 0 {
delete(m.npcsByZone, zoneID)
}
}
func (m *Manager) removeFromAppearanceIndex(npc *NPC) {
appearanceID := npc.GetAppearanceID()
npcs := m.npcsByAppearance[appearanceID]
for i, n := range npcs {
if n == npc {
// Remove from slice
m.npcsByAppearance[appearanceID] = append(npcs[:i], npcs[i+1:]...)
break
}
}
// Clean up empty slices
if len(m.npcsByAppearance[appearanceID]) == 0 {
delete(m.npcsByAppearance, appearanceID)
}
}
// SetManagers sets the external system managers
func (m *Manager) SetManagers(spellMgr SpellManager, skillMgr SkillManager, appearanceMgr AppearanceManager) {
m.mutex.Lock()
defer m.mutex.Unlock()
m.spellManager = spellMgr
m.skillManager = skillMgr
m.appearanceManager = appearanceMgr
}
// Configuration methods
func (m *Manager) SetMaxNPCs(max int32) {
m.mutex.Lock()
defer m.mutex.Unlock()
m.maxNPCs = max
}
func (m *Manager) SetDefaultAggroRadius(radius float32) {
m.mutex.Lock()
defer m.mutex.Unlock()
m.defaultAggroRadius = radius
}
func (m *Manager) SetAIEnabled(enabled bool) {
m.mutex.Lock()
defer m.mutex.Unlock()
m.enableAI = enabled
}
// GetNPCCount returns the total number of NPCs
func (m *Manager) GetNPCCount() int32 {
m.mutex.RLock()
defer m.mutex.RUnlock()
return int32(len(m.npcs))
}
// Shutdown gracefully shuts down the manager
func (m *Manager) Shutdown() {
if m.logger != nil {
m.logger.LogInfo("Shutting down NPC manager...")
}
// Stop all AI brains
m.mutex.Lock()
for _, npc := range m.npcs {
if brain := npc.GetBrain(); brain != nil {
brain.SetActive(false)
}
}
// Clear all data
m.npcs = make(map[int32]*NPC)
m.npcsByZone = make(map[int32][]*NPC)
m.npcsByAppearance = make(map[int32][]*NPC)
m.mutex.Unlock()
}

806
internal/npc/npc.go Normal file
View File

@ -0,0 +1,806 @@
package npc
import (
"fmt"
"math"
"math/rand"
"sync"
"time"
"eq2emu/internal/common"
"eq2emu/internal/entity"
"eq2emu/internal/spawn"
)
// NewNPC creates a new NPC with default values
func NewNPC() *NPC {
npc := &NPC{
Entity: entity.NewEntity(),
appearanceID: 0,
npcID: 0,
aiStrategy: AIStrategyBalanced,
attackType: 0,
castPercentage: DefaultCastPercentage,
maxPetLevel: DefaultMaxPetLevel,
aggroRadius: DefaultAggroRadius,
baseAggroRadius: DefaultAggroRadius,
runback: nil,
runningBack: false,
runbackHeadingDir1: 0,
runbackHeadingDir2: 0,
pauseTimer: NewTimer(),
primarySpellList: 0,
secondarySpellList: 0,
primarySkillList: 0,
secondarySkillList: 0,
equipmentListID: 0,
skills: make(map[string]*Skill),
spells: make([]*NPCSpell, 0),
castOnSpells: make(map[int8][]*NPCSpell),
skillBonuses: make(map[int32]*SkillBonus),
hasSpells: false,
castOnAggroCompleted: false,
shardID: 0,
shardCharID: 0,
shardCreatedTimestamp: 0,
callRunback: false,
}
// Initialize cast-on spell arrays
npc.castOnSpells[CastOnSpawn] = make([]*NPCSpell, 0)
npc.castOnSpells[CastOnAggro] = make([]*NPCSpell, 0)
// Create default brain
npc.brain = NewDefaultBrain(npc)
return npc
}
// NewNPCFromExisting creates a copy of an existing NPC with randomization
func NewNPCFromExisting(oldNPC *NPC) *NPC {
if oldNPC == nil {
return NewNPC()
}
npc := NewNPC()
// Copy basic properties
npc.npcID = oldNPC.npcID
npc.appearanceID = oldNPC.appearanceID
npc.aiStrategy = oldNPC.aiStrategy
npc.attackType = oldNPC.attackType
npc.castPercentage = oldNPC.castPercentage
npc.maxPetLevel = oldNPC.maxPetLevel
npc.baseAggroRadius = oldNPC.baseAggroRadius
npc.aggroRadius = oldNPC.baseAggroRadius
// Copy spell lists
npc.primarySpellList = oldNPC.primarySpellList
npc.secondarySpellList = oldNPC.secondarySpellList
npc.primarySkillList = oldNPC.primarySkillList
npc.secondarySkillList = oldNPC.secondarySkillList
npc.equipmentListID = oldNPC.equipmentListID
// Copy entity data (stats, appearance, etc.)
if oldNPC.Entity != nil {
npc.Entity = oldNPC.Entity.Copy().(*entity.Entity)
}
// Handle level randomization
if oldNPC.Entity != nil {
minLevel := oldNPC.Entity.GetMinLevel()
maxLevel := oldNPC.Entity.GetMaxLevel()
if minLevel < maxLevel {
randomLevel := minLevel + int8(rand.Intn(int(maxLevel-minLevel)+1))
npc.Entity.SetLevel(randomLevel)
}
}
// Copy skills (deep copy)
npc.copySkills(oldNPC)
// Copy spells (deep copy)
npc.copySpells(oldNPC)
// Handle appearance randomization
if oldNPC.Entity != nil && oldNPC.Entity.GetRandomize() > 0 {
npc.randomizeAppearance(oldNPC.Entity.GetRandomize())
}
return npc
}
// IsNPC always returns true for NPC instances
func (n *NPC) IsNPC() bool {
return true
}
// GetAppearanceID returns the appearance ID
func (n *NPC) GetAppearanceID() int32 {
n.mutex.RLock()
defer n.mutex.RUnlock()
return n.appearanceID
}
// SetAppearanceID sets the appearance ID
func (n *NPC) SetAppearanceID(id int32) {
n.mutex.Lock()
defer n.mutex.Unlock()
n.appearanceID = id
}
// GetNPCID returns the NPC database ID
func (n *NPC) GetNPCID() int32 {
n.mutex.RLock()
defer n.mutex.RUnlock()
return n.npcID
}
// SetNPCID sets the NPC database ID
func (n *NPC) SetNPCID(id int32) {
n.mutex.Lock()
defer n.mutex.Unlock()
n.npcID = id
}
// AI Strategy methods
func (n *NPC) GetAIStrategy() int8 {
n.mutex.RLock()
defer n.mutex.RUnlock()
return n.aiStrategy
}
func (n *NPC) SetAIStrategy(strategy int8) {
n.mutex.Lock()
defer n.mutex.Unlock()
n.aiStrategy = strategy
}
// Attack Type methods
func (n *NPC) GetAttackType() int8 {
n.mutex.RLock()
defer n.mutex.RUnlock()
return n.attackType
}
func (n *NPC) SetAttackType(attackType int8) {
n.mutex.Lock()
defer n.mutex.Unlock()
n.attackType = attackType
}
// Cast Percentage methods
func (n *NPC) GetCastPercentage() int8 {
n.mutex.RLock()
defer n.mutex.RUnlock()
return n.castPercentage
}
func (n *NPC) SetCastPercentage(percentage int8) {
n.mutex.Lock()
defer n.mutex.Unlock()
if percentage < 0 {
percentage = 0
} else if percentage > 100 {
percentage = 100
}
n.castPercentage = percentage
}
// Aggro Radius methods
func (n *NPC) GetAggroRadius() float32 {
n.mutex.RLock()
defer n.mutex.RUnlock()
return n.aggroRadius
}
func (n *NPC) SetAggroRadius(radius float32, overrideBase bool) {
n.mutex.Lock()
defer n.mutex.Unlock()
if n.baseAggroRadius == 0.0 || overrideBase {
n.baseAggroRadius = radius
}
n.aggroRadius = radius
}
func (n *NPC) GetBaseAggroRadius() float32 {
n.mutex.RLock()
defer n.mutex.RUnlock()
return n.baseAggroRadius
}
// Pet Level methods
func (n *NPC) GetMaxPetLevel() int8 {
n.mutex.RLock()
defer n.mutex.RUnlock()
return n.maxPetLevel
}
func (n *NPC) SetMaxPetLevel(level int8) {
n.mutex.Lock()
defer n.mutex.Unlock()
n.maxPetLevel = level
}
// Spell List methods
func (n *NPC) GetPrimarySpellList() int32 {
n.mutex.RLock()
defer n.mutex.RUnlock()
return n.primarySpellList
}
func (n *NPC) SetPrimarySpellList(id int32) {
n.mutex.Lock()
defer n.mutex.Unlock()
n.primarySpellList = id
}
func (n *NPC) GetSecondarySpellList() int32 {
n.mutex.RLock()
defer n.mutex.RUnlock()
return n.secondarySpellList
}
func (n *NPC) SetSecondarySpellList(id int32) {
n.mutex.Lock()
defer n.mutex.Unlock()
n.secondarySpellList = id
}
// Skill List methods
func (n *NPC) GetPrimarySkillList() int32 {
n.mutex.RLock()
defer n.mutex.RUnlock()
return n.primarySkillList
}
func (n *NPC) SetPrimarySkillList(id int32) {
n.mutex.Lock()
defer n.mutex.Unlock()
n.primarySkillList = id
}
func (n *NPC) GetSecondarySkillList() int32 {
n.mutex.RLock()
defer n.mutex.RUnlock()
return n.secondarySkillList
}
func (n *NPC) SetSecondarySkillList(id int32) {
n.mutex.Lock()
defer n.mutex.Unlock()
n.secondarySkillList = id
}
// Equipment List methods
func (n *NPC) GetEquipmentListID() int32 {
n.mutex.RLock()
defer n.mutex.RUnlock()
return n.equipmentListID
}
func (n *NPC) SetEquipmentListID(id int32) {
n.mutex.Lock()
defer n.mutex.Unlock()
n.equipmentListID = id
}
// HasSpells returns whether the NPC has any spells
func (n *NPC) HasSpells() bool {
n.mutex.RLock()
defer n.mutex.RUnlock()
return n.hasSpells
}
// GetSpells returns a copy of all spells
func (n *NPC) GetSpells() []*NPCSpell {
n.mutex.RLock()
defer n.mutex.RUnlock()
result := make([]*NPCSpell, len(n.spells))
for i, spell := range n.spells {
result[i] = spell.Copy()
}
return result
}
// SetSpells sets the NPC's spell list
func (n *NPC) SetSpells(spells []*NPCSpell) {
n.mutex.Lock()
defer n.mutex.Unlock()
// Clear existing cast-on spells
for i := int8(0); i < MaxCastTypes; i++ {
n.castOnSpells[i] = make([]*NPCSpell, 0)
}
// Clear existing spells
n.spells = make([]*NPCSpell, 0)
if spells == nil || len(spells) == 0 {
n.hasSpells = false
return
}
n.hasSpells = true
// Process spells and separate cast-on types
for _, spell := range spells {
if spell == nil {
continue
}
spellCopy := spell.Copy()
if spellCopy.GetCastOnSpawn() {
n.castOnSpells[CastOnSpawn] = append(n.castOnSpells[CastOnSpawn], spellCopy)
} else if spellCopy.GetCastOnInitialAggro() {
n.castOnSpells[CastOnAggro] = append(n.castOnSpells[CastOnAggro], spellCopy)
} else {
n.spells = append(n.spells, spellCopy)
}
}
}
// GetSkillByName returns a skill by name
func (n *NPC) GetSkillByName(name string, checkUpdate bool) *Skill {
n.mutex.RLock()
defer n.mutex.RUnlock()
skill, exists := n.skills[name]
if !exists {
return nil
}
// Random skill increase (10% chance)
if checkUpdate && skill.GetCurrentVal() < skill.MaxVal && rand.Intn(100) >= 90 {
skill.IncreaseSkill()
}
return skill
}
// GetSkillByID returns a skill by ID (requires master skill list lookup)
func (n *NPC) GetSkillByID(id int32, checkUpdate bool) *Skill {
// TODO: Implement skill lookup by ID using master skill list
// For now, return nil as we need the master skill list integration
return nil
}
// SetSkills sets the NPC's skills
func (n *NPC) SetSkills(skills map[string]*Skill) {
n.mutex.Lock()
defer n.mutex.Unlock()
// Clear existing skills
n.skills = make(map[string]*Skill)
// Copy skills
if skills != nil {
for name, skill := range skills {
if skill != nil {
n.skills[name] = &Skill{
SkillID: skill.SkillID,
Name: skill.Name,
CurrentVal: skill.CurrentVal,
MaxVal: skill.MaxVal,
}
}
}
}
}
// AddSkillBonus adds a skill bonus from a spell
func (n *NPC) AddSkillBonus(spellID, skillID int32, value float32) {
if value == 0 {
return
}
n.mutex.Lock()
defer n.mutex.Unlock()
// Get or create skill bonus
skillBonus, exists := n.skillBonuses[spellID]
if !exists {
skillBonus = NewSkillBonus(spellID)
n.skillBonuses[spellID] = skillBonus
}
// Add the skill bonus
skillBonus.AddSkill(skillID, value)
// Apply bonus to existing skills
for _, skill := range n.skills {
if skill.SkillID == skillID {
skill.CurrentVal += int16(value)
skill.MaxVal += int16(value)
}
}
}
// RemoveSkillBonus removes skill bonuses from a spell
func (n *NPC) RemoveSkillBonus(spellID int32) {
n.mutex.Lock()
defer n.mutex.Unlock()
skillBonus, exists := n.skillBonuses[spellID]
if !exists {
return
}
// Remove bonuses from skills
bonuses := skillBonus.GetSkills()
for _, bonus := range bonuses {
for _, skill := range n.skills {
if skill.SkillID == bonus.SkillID {
skill.CurrentVal -= int16(bonus.Value)
skill.MaxVal -= int16(bonus.Value)
}
}
}
// Remove the skill bonus
delete(n.skillBonuses, spellID)
}
// Runback Location methods
func (n *NPC) SetRunbackLocation(x, y, z float32, gridID int32, resetHP bool) {
n.mutex.Lock()
defer n.mutex.Unlock()
n.runback = &MovementLocation{
X: x,
Y: y,
Z: z,
GridID: gridID,
Stage: 0,
ResetHPOnRunback: resetHP,
UseNavPath: false,
Mapped: false,
}
}
func (n *NPC) GetRunbackLocation() *MovementLocation {
n.mutex.RLock()
defer n.mutex.RUnlock()
if n.runback == nil {
return nil
}
return n.runback.Copy()
}
func (n *NPC) GetRunbackDistance() float32 {
n.mutex.RLock()
defer n.mutex.RUnlock()
if n.runback == nil || n.Entity == nil {
return 0
}
// Calculate distance using basic distance formula
dx := n.Entity.GetX() - n.runback.X
dy := n.Entity.GetY() - n.runback.Y
dz := n.Entity.GetZ() - n.runback.Z
return float32(math.Sqrt(float64(dx*dx + dy*dy + dz*dz)))
}
func (n *NPC) ClearRunback() {
n.mutex.Lock()
defer n.mutex.Unlock()
n.runback = nil
n.runningBack = false
n.runbackHeadingDir1 = 0
n.runbackHeadingDir2 = 0
}
// StartRunback sets the current location as the runback point
func (n *NPC) StartRunback(resetHP bool) {
if n.GetRunbackLocation() != nil {
return
}
if n.Entity == nil {
return
}
n.mutex.Lock()
defer n.mutex.Unlock()
n.runback = &MovementLocation{
X: n.Entity.GetX(),
Y: n.Entity.GetY(),
Z: n.Entity.GetZ(),
GridID: n.Entity.GetLocation(),
Stage: 0,
ResetHPOnRunback: resetHP,
UseNavPath: false,
Mapped: false,
}
// Store original heading
n.runbackHeadingDir1 = n.Entity.GetHeading()
n.runbackHeadingDir2 = n.Entity.GetHeading() // In C++ these are separate values
}
// Runback initiates runback movement
func (n *NPC) Runback(distance float32, stopFollowing bool) {
if n.runback == nil {
return
}
if distance == 0.0 {
distance = n.GetRunbackDistance()
}
n.mutex.Lock()
n.runningBack = true
n.mutex.Unlock()
// TODO: Implement actual movement logic
// This would integrate with the movement system
if stopFollowing && n.Entity != nil {
n.Entity.SetFollowing(false)
}
}
// IsRunningBack returns whether the NPC is currently running back
func (n *NPC) IsRunningBack() bool {
n.mutex.RLock()
defer n.mutex.RUnlock()
return n.runningBack
}
// Movement pause methods
func (n *NPC) PauseMovement(periodMS int32) bool {
if periodMS < 1 {
periodMS = 1
}
if periodMS > MaxPauseTime {
periodMS = MaxPauseTime
}
// TODO: Integrate with movement system to stop movement
// For now, just start the pause timer
n.pauseTimer.Start(periodMS, true)
return true
}
func (n *NPC) IsPauseMovementTimerActive() bool {
if n.pauseTimer.Check() {
n.pauseTimer.Disable()
n.callRunback = true
}
return n.pauseTimer.Enabled()
}
// Brain methods
func (n *NPC) GetBrain() Brain {
n.brainMutex.RLock()
defer n.brainMutex.RUnlock()
return n.brain
}
func (n *NPC) SetBrain(brain Brain) {
n.brainMutex.Lock()
defer n.brainMutex.Unlock()
// Validate brain matches this NPC
if brain != nil && brain.GetBody() != n {
// TODO: Log error
return
}
n.brain = brain
}
// Shard methods
func (n *NPC) GetShardID() int32 {
n.mutex.RLock()
defer n.mutex.RUnlock()
return n.shardID
}
func (n *NPC) SetShardID(id int32) {
n.mutex.Lock()
defer n.mutex.Unlock()
n.shardID = id
}
func (n *NPC) GetShardCharID() int32 {
n.mutex.RLock()
defer n.mutex.RUnlock()
return n.shardCharID
}
func (n *NPC) SetShardCharID(id int32) {
n.mutex.Lock()
defer n.mutex.Unlock()
n.shardCharID = id
}
func (n *NPC) GetShardCreatedTimestamp() int64 {
n.mutex.RLock()
defer n.mutex.RUnlock()
return n.shardCreatedTimestamp
}
func (n *NPC) SetShardCreatedTimestamp(timestamp int64) {
n.mutex.Lock()
defer n.mutex.Unlock()
n.shardCreatedTimestamp = timestamp
}
// HandleUse processes entity command usage
func (n *NPC) HandleUse(client Client, commandType string) bool {
if client == nil || len(commandType) == 0 {
return false
}
// Check if NPC shows command icons
if n.Entity == nil {
return false
}
// TODO: Implement entity command processing
// This would integrate with the command system
return false
}
// InCombat handles combat state changes
func (n *NPC) InCombat(val bool) {
if n.Entity == nil {
return
}
currentCombat := n.Entity.GetInCombat()
if currentCombat == val {
return
}
n.Entity.SetInCombat(val)
if val {
// Entering combat
if n.GetRunbackLocation() == nil {
n.StartRunback(true)
}
// Set max speed for combat
if n.Entity.GetMaxSpeed() > 0 {
n.Entity.SetSpeed(n.Entity.GetMaxSpeed())
}
// TODO: Add combat icon, call spawn scripts, etc.
} else {
// Leaving combat
// TODO: Remove combat icon, call combat reset scripts, etc.
if n.Entity.GetHP() > 0 {
// TODO: Re-enable action states, stop heroic opportunities
}
}
}
// ProcessCombat handles combat processing
func (n *NPC) ProcessCombat() {
// TODO: Implement combat processing logic
// This would handle spell casting, AI decisions, etc.
}
// Copy helper methods
func (n *NPC) copySkills(oldNPC *NPC) {
if oldNPC == nil {
return
}
oldNPC.mutex.RLock()
oldSkills := make(map[string]*Skill)
for name, skill := range oldNPC.skills {
if skill != nil {
oldSkills[name] = &Skill{
SkillID: skill.SkillID,
Name: skill.Name,
CurrentVal: skill.CurrentVal,
MaxVal: skill.MaxVal,
}
}
}
oldNPC.mutex.RUnlock()
n.SetSkills(oldSkills)
}
func (n *NPC) copySpells(oldNPC *NPC) {
if oldNPC == nil {
return
}
oldNPC.mutex.RLock()
oldSpells := make([]*NPCSpell, len(oldNPC.spells))
for i, spell := range oldNPC.spells {
if spell != nil {
oldSpells[i] = spell.Copy()
}
}
// Also copy cast-on spells
for castType, spells := range oldNPC.castOnSpells {
for _, spell := range spells {
if spell != nil {
oldSpells = append(oldSpells, spell.Copy())
}
}
}
oldNPC.mutex.RUnlock()
n.SetSpells(oldSpells)
}
// randomizeAppearance applies appearance randomization
func (n *NPC) randomizeAppearance(flags int32) {
// TODO: Implement full appearance randomization
// This is a complex system that would integrate with the appearance system
// For now, just implement basic randomization
if n.Entity == nil {
return
}
// Random gender
if flags&RandomizeGender != 0 {
gender := int8(rand.Intn(2) + 1) // 1 or 2
n.Entity.SetGender(gender)
}
// Random race (simplified)
if flags&RandomizeRace != 0 {
// TODO: Implement race randomization based on alignment
race := int16(rand.Intn(21)) // 0-20 for basic races
n.Entity.SetRace(race)
}
// Color randomization
if flags&RandomizeSkinColor != 0 {
// TODO: Implement skin color randomization
}
// More randomization options would be implemented here
}
// Validation methods
func (n *NPC) IsValid() bool {
if n.Entity == nil {
return false
}
// Basic validation
if n.Entity.GetLevel() < MinNPCLevel || n.Entity.GetLevel() > MaxNPCLevel {
return false
}
if n.appearanceID < MinAppearanceID || n.appearanceID > MaxAppearanceID {
return false
}
return true
}
// String returns a string representation of the NPC
func (n *NPC) String() string {
if n.Entity == nil {
return fmt.Sprintf("NPC{ID: %d, AppearanceID: %d, Entity: nil}", n.npcID, n.appearanceID)
}
return fmt.Sprintf("NPC{ID: %d, Name: %s, Level: %d, AppearanceID: %d}",
n.npcID, n.Entity.GetName(), n.Entity.GetLevel(), n.appearanceID)
}

440
internal/npc/types.go Normal file
View File

@ -0,0 +1,440 @@
package npc
import (
"sync"
"time"
"eq2emu/internal/common"
"eq2emu/internal/entity"
"eq2emu/internal/spawn"
)
// NPCSpell represents a spell configuration for NPCs
type NPCSpell struct {
ListID int32 // Spell list identifier
SpellID int32 // Spell ID from master spell list
Tier int8 // Spell tier
CastOnSpawn bool // Cast when NPC spawns
CastOnInitialAggro bool // Cast when first entering combat
RequiredHPRatio int8 // HP ratio requirement for casting (-100 to 100)
mutex sync.RWMutex
}
// NewNPCSpell creates a new NPCSpell
func NewNPCSpell() *NPCSpell {
return &NPCSpell{
ListID: 0,
SpellID: 0,
Tier: 1,
CastOnSpawn: false,
CastOnInitialAggro: false,
RequiredHPRatio: 0,
}
}
// Copy creates a deep copy of the NPCSpell
func (ns *NPCSpell) Copy() *NPCSpell {
ns.mutex.RLock()
defer ns.mutex.RUnlock()
return &NPCSpell{
ListID: ns.ListID,
SpellID: ns.SpellID,
Tier: ns.Tier,
CastOnSpawn: ns.CastOnSpawn,
CastOnInitialAggro: ns.CastOnInitialAggro,
RequiredHPRatio: ns.RequiredHPRatio,
}
}
// Getters
func (ns *NPCSpell) GetListID() int32 {
ns.mutex.RLock()
defer ns.mutex.RUnlock()
return ns.ListID
}
func (ns *NPCSpell) GetSpellID() int32 {
ns.mutex.RLock()
defer ns.mutex.RUnlock()
return ns.SpellID
}
func (ns *NPCSpell) GetTier() int8 {
ns.mutex.RLock()
defer ns.mutex.RUnlock()
return ns.Tier
}
func (ns *NPCSpell) GetCastOnSpawn() bool {
ns.mutex.RLock()
defer ns.mutex.RUnlock()
return ns.CastOnSpawn
}
func (ns *NPCSpell) GetCastOnInitialAggro() bool {
ns.mutex.RLock()
defer ns.mutex.RUnlock()
return ns.CastOnInitialAggro
}
func (ns *NPCSpell) GetRequiredHPRatio() int8 {
ns.mutex.RLock()
defer ns.mutex.RUnlock()
return ns.RequiredHPRatio
}
// Setters
func (ns *NPCSpell) SetListID(id int32) {
ns.mutex.Lock()
defer ns.mutex.Unlock()
ns.ListID = id
}
func (ns *NPCSpell) SetSpellID(id int32) {
ns.mutex.Lock()
defer ns.mutex.Unlock()
ns.SpellID = id
}
func (ns *NPCSpell) SetTier(tier int8) {
ns.mutex.Lock()
defer ns.mutex.Unlock()
ns.Tier = tier
}
func (ns *NPCSpell) SetCastOnSpawn(cast bool) {
ns.mutex.Lock()
defer ns.mutex.Unlock()
ns.CastOnSpawn = cast
}
func (ns *NPCSpell) SetCastOnInitialAggro(cast bool) {
ns.mutex.Lock()
defer ns.mutex.Unlock()
ns.CastOnInitialAggro = cast
}
func (ns *NPCSpell) SetRequiredHPRatio(ratio int8) {
ns.mutex.Lock()
defer ns.mutex.Unlock()
ns.RequiredHPRatio = ratio
}
// SkillBonus represents a skill bonus from spells
type SkillBonus struct {
SpellID int32 // Spell providing the bonus
Skills map[int32]*SkillBonusValue // Map of skill ID to bonus value
mutex sync.RWMutex
}
// SkillBonusValue represents the actual bonus value for a skill
type SkillBonusValue struct {
SkillID int32 // Skill receiving the bonus
Value float32 // Bonus amount
}
// NewSkillBonus creates a new SkillBonus
func NewSkillBonus(spellID int32) *SkillBonus {
return &SkillBonus{
SpellID: spellID,
Skills: make(map[int32]*SkillBonusValue),
}
}
// AddSkill adds a skill bonus
func (sb *SkillBonus) AddSkill(skillID int32, value float32) {
sb.mutex.Lock()
defer sb.mutex.Unlock()
sb.Skills[skillID] = &SkillBonusValue{
SkillID: skillID,
Value: value,
}
}
// RemoveSkill removes a skill bonus
func (sb *SkillBonus) RemoveSkill(skillID int32) bool {
sb.mutex.Lock()
defer sb.mutex.Unlock()
if _, exists := sb.Skills[skillID]; exists {
delete(sb.Skills, skillID)
return true
}
return false
}
// GetSkills returns a copy of all skill bonuses
func (sb *SkillBonus) GetSkills() map[int32]*SkillBonusValue {
sb.mutex.RLock()
defer sb.mutex.RUnlock()
result := make(map[int32]*SkillBonusValue)
for id, bonus := range sb.Skills {
result[id] = &SkillBonusValue{
SkillID: bonus.SkillID,
Value: bonus.Value,
}
}
return result
}
// MovementLocation represents a movement destination for runback
type MovementLocation struct {
X float32 // X coordinate
Y float32 // Y coordinate
Z float32 // Z coordinate
GridID int32 // Grid location ID
Stage int32 // Movement stage
ResetHPOnRunback bool // Whether to reset HP when reaching location
UseNavPath bool // Whether to use navigation pathfinding
Mapped bool // Whether location is mapped
}
// NewMovementLocation creates a new MovementLocation
func NewMovementLocation(x, y, z float32, gridID int32) *MovementLocation {
return &MovementLocation{
X: x,
Y: y,
Z: z,
GridID: gridID,
Stage: 0,
ResetHPOnRunback: false,
UseNavPath: false,
Mapped: false,
}
}
// Copy creates a deep copy of the MovementLocation
func (ml *MovementLocation) Copy() *MovementLocation {
return &MovementLocation{
X: ml.X,
Y: ml.Y,
Z: ml.Z,
GridID: ml.GridID,
Stage: ml.Stage,
ResetHPOnRunback: ml.ResetHPOnRunback,
UseNavPath: ml.UseNavPath,
Mapped: ml.Mapped,
}
}
// NPC represents a non-player character extending Entity
type NPC struct {
*entity.Entity // Embedded entity for combat capabilities
// Core NPC properties
appearanceID int32 // Appearance ID for client display
npcID int32 // NPC database ID
aiStrategy int8 // AI strategy (balanced/offensive/defensive)
attackType int8 // Attack type preference
castPercentage int8 // Percentage chance to cast spells
maxPetLevel int8 // Maximum pet level
// Combat and movement
aggroRadius float32 // Aggro detection radius
baseAggroRadius float32 // Base aggro radius (for resets)
runback *MovementLocation // Runback location when leaving combat
runningBack bool // Currently running back to spawn point
runbackHeadingDir1 int16 // Original heading direction 1
runbackHeadingDir2 int16 // Original heading direction 2
pauseTimer *Timer // Movement pause timer
// Spell and skill management
primarySpellList int32 // Primary spell list ID
secondarySpellList int32 // Secondary spell list ID
primarySkillList int32 // Primary skill list ID
secondarySkillList int32 // Secondary skill list ID
equipmentListID int32 // Equipment list ID
skills map[string]*Skill // NPC skills by name
spells []*NPCSpell // Available spells
castOnSpells map[int8][]*NPCSpell // Spells to cast by trigger type
skillBonuses map[int32]*SkillBonus // Skill bonuses from spells
hasSpells bool // Whether NPC has any spells
castOnAggroCompleted bool // Whether cast-on-aggro spells are done
// Brain/AI system (placeholder for now)
brain Brain // AI brain for decision making
// Shard system (for cross-server functionality)
shardID int32 // Shard identifier
shardCharID int32 // Character ID on shard
shardCreatedTimestamp int64 // Timestamp when created on shard
// Thread safety
mutex sync.RWMutex // Main NPC mutex
brainMutex sync.RWMutex // Brain-specific mutex
// Atomic flags for thread-safe state management
callRunback bool // Flag to trigger runback
}
// Timer represents a simple timer for NPC operations
type Timer struct {
duration time.Duration
startTime time.Time
enabled bool
mutex sync.RWMutex
}
// NewTimer creates a new timer
func NewTimer() *Timer {
return &Timer{
enabled: false,
}
}
// Start starts the timer with the given duration
func (t *Timer) Start(durationMS int32, reset bool) {
t.mutex.Lock()
defer t.mutex.Unlock()
if reset || !t.enabled {
t.duration = time.Duration(durationMS) * time.Millisecond
t.startTime = time.Now()
t.enabled = true
}
}
// Check checks if the timer has expired
func (t *Timer) Check() bool {
t.mutex.RLock()
defer t.mutex.RUnlock()
if !t.enabled {
return false
}
return time.Since(t.startTime) >= t.duration
}
// Enabled returns whether the timer is currently enabled
func (t *Timer) Enabled() bool {
t.mutex.RLock()
defer t.mutex.RUnlock()
return t.enabled
}
// Disable disables the timer
func (t *Timer) Disable() {
t.mutex.Lock()
defer t.mutex.Unlock()
t.enabled = false
}
// Skill represents an NPC skill (simplified from C++ version)
type Skill struct {
SkillID int32 // Skill identifier
Name string // Skill name
CurrentVal int16 // Current skill value
MaxVal int16 // Maximum skill value
mutex sync.RWMutex
}
// NewSkill creates a new skill
func NewSkill(id int32, name string, current, max int16) *Skill {
return &Skill{
SkillID: id,
Name: name,
CurrentVal: current,
MaxVal: max,
}
}
// GetCurrentVal returns the current skill value
func (s *Skill) GetCurrentVal() int16 {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.CurrentVal
}
// SetCurrentVal sets the current skill value
func (s *Skill) SetCurrentVal(val int16) {
s.mutex.Lock()
defer s.mutex.Unlock()
s.CurrentVal = val
}
// IncreaseSkill increases the skill value (with random chance)
func (s *Skill) IncreaseSkill() bool {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.CurrentVal < s.MaxVal {
s.CurrentVal++
return true
}
return false
}
// Brain interface represents the AI brain system (placeholder)
type Brain interface {
Think() error
GetBody() *NPC
SetBody(*NPC)
IsActive() bool
SetActive(bool)
}
// DefaultBrain provides a simple brain implementation
type DefaultBrain struct {
npc *NPC
active bool
mutex sync.RWMutex
}
// NewDefaultBrain creates a new default brain
func NewDefaultBrain(npc *NPC) *DefaultBrain {
return &DefaultBrain{
npc: npc,
active: true,
}
}
// Think processes AI logic (placeholder implementation)
func (b *DefaultBrain) Think() error {
// TODO: Implement AI thinking logic
return nil
}
// GetBody returns the NPC this brain controls
func (b *DefaultBrain) GetBody() *NPC {
b.mutex.RLock()
defer b.mutex.RUnlock()
return b.npc
}
// SetBody sets the NPC this brain controls
func (b *DefaultBrain) SetBody(npc *NPC) {
b.mutex.Lock()
defer b.mutex.Unlock()
b.npc = npc
}
// IsActive returns whether the brain is active
func (b *DefaultBrain) IsActive() bool {
b.mutex.RLock()
defer b.mutex.RUnlock()
return b.active
}
// SetActive sets the brain's active state
func (b *DefaultBrain) SetActive(active bool) {
b.mutex.Lock()
defer b.mutex.Unlock()
b.active = active
}
// NPCStatistics contains NPC system statistics
type NPCStatistics struct {
TotalNPCs int `json:"total_npcs"`
NPCsInCombat int `json:"npcs_in_combat"`
NPCsWithSpells int `json:"npcs_with_spells"`
NPCsWithSkills int `json:"npcs_with_skills"`
AIStrategyCounts map[string]int `json:"ai_strategy_counts"`
SpellCastCount int64 `json:"spell_cast_count"`
SkillUsageCount int64 `json:"skill_usage_count"`
RunbackCount int64 `json:"runback_count"`
AverageAggroRadius float32 `json:"average_aggro_radius"`
}

426
internal/quests/actions.go Normal file
View File

@ -0,0 +1,426 @@
package quests
// Action management methods for Quest
// AddCompleteAction adds a completion action for a step
func (q *Quest) AddCompleteAction(stepID int32, action string) {
q.completeActionsMutex.Lock()
defer q.completeActionsMutex.Unlock()
q.CompleteActions[stepID] = action
q.SetSaveNeeded(true)
}
// AddProgressAction adds a progress action for a step
func (q *Quest) AddProgressAction(stepID int32, action string) {
q.progressActionsMutex.Lock()
defer q.progressActionsMutex.Unlock()
q.ProgressActions[stepID] = action
q.SetSaveNeeded(true)
}
// AddFailedAction adds a failure action for a step
func (q *Quest) AddFailedAction(stepID int32, action string) {
q.failedActionsMutex.Lock()
defer q.failedActionsMutex.Unlock()
q.FailedActions[stepID] = action
q.SetSaveNeeded(true)
}
// RemoveCompleteAction removes a completion action for a step
func (q *Quest) RemoveCompleteAction(stepID int32) bool {
q.completeActionsMutex.Lock()
defer q.completeActionsMutex.Unlock()
if _, exists := q.CompleteActions[stepID]; exists {
delete(q.CompleteActions, stepID)
q.SetSaveNeeded(true)
return true
}
return false
}
// RemoveProgressAction removes a progress action for a step
func (q *Quest) RemoveProgressAction(stepID int32) bool {
q.progressActionsMutex.Lock()
defer q.progressActionsMutex.Unlock()
if _, exists := q.ProgressActions[stepID]; exists {
delete(q.ProgressActions, stepID)
q.SetSaveNeeded(true)
return true
}
return false
}
// RemoveFailedAction removes a failure action for a step
func (q *Quest) RemoveFailedAction(stepID int32) bool {
q.failedActionsMutex.Lock()
defer q.failedActionsMutex.Unlock()
if _, exists := q.FailedActions[stepID]; exists {
delete(q.FailedActions, stepID)
q.SetSaveNeeded(true)
return true
}
return false
}
// GetCompleteAction returns the completion action for a step
func (q *Quest) GetCompleteAction(stepID int32) (string, bool) {
q.completeActionsMutex.RLock()
defer q.completeActionsMutex.RUnlock()
action, exists := q.CompleteActions[stepID]
return action, exists
}
// GetProgressAction returns the progress action for a step
func (q *Quest) GetProgressAction(stepID int32) (string, bool) {
q.progressActionsMutex.RLock()
defer q.progressActionsMutex.RUnlock()
action, exists := q.ProgressActions[stepID]
return action, exists
}
// GetFailedAction returns the failure action for a step
func (q *Quest) GetFailedAction(stepID int32) (string, bool) {
q.failedActionsMutex.RLock()
defer q.failedActionsMutex.RUnlock()
action, exists := q.FailedActions[stepID]
return action, exists
}
// HasCompleteAction checks if a step has a completion action
func (q *Quest) HasCompleteAction(stepID int32) bool {
q.completeActionsMutex.RLock()
defer q.completeActionsMutex.RUnlock()
_, exists := q.CompleteActions[stepID]
return exists
}
// HasProgressAction checks if a step has a progress action
func (q *Quest) HasProgressAction(stepID int32) bool {
q.progressActionsMutex.RLock()
defer q.progressActionsMutex.RUnlock()
_, exists := q.ProgressActions[stepID]
return exists
}
// HasFailedAction checks if a step has a failure action
func (q *Quest) HasFailedAction(stepID int32) bool {
q.failedActionsMutex.RLock()
defer q.failedActionsMutex.RUnlock()
_, exists := q.FailedActions[stepID]
return exists
}
// SetCompleteAction sets the general quest completion action
func (q *Quest) SetCompleteAction(action string) {
q.CompleteAction = action
q.SetSaveNeeded(true)
}
// GetQuestCompleteAction returns the general quest completion action
func (q *Quest) GetQuestCompleteAction() string {
return q.CompleteAction
}
// ClearAllActions removes all actions for all steps
func (q *Quest) ClearAllActions() {
q.completeActionsMutex.Lock()
q.CompleteActions = make(map[int32]string)
q.completeActionsMutex.Unlock()
q.progressActionsMutex.Lock()
q.ProgressActions = make(map[int32]string)
q.progressActionsMutex.Unlock()
q.failedActionsMutex.Lock()
q.FailedActions = make(map[int32]string)
q.failedActionsMutex.Unlock()
q.CompleteAction = ""
q.SetSaveNeeded(true)
}
// ClearStepActions removes all actions for a specific step
func (q *Quest) ClearStepActions(stepID int32) {
q.RemoveCompleteAction(stepID)
q.RemoveProgressAction(stepID)
q.RemoveFailedAction(stepID)
}
// GetAllCompleteActions returns a copy of all completion actions
func (q *Quest) GetAllCompleteActions() map[int32]string {
q.completeActionsMutex.RLock()
defer q.completeActionsMutex.RUnlock()
actions := make(map[int32]string)
for stepID, action := range q.CompleteActions {
actions[stepID] = action
}
return actions
}
// GetAllProgressActions returns a copy of all progress actions
func (q *Quest) GetAllProgressActions() map[int32]string {
q.progressActionsMutex.RLock()
defer q.progressActionsMutex.RUnlock()
actions := make(map[int32]string)
for stepID, action := range q.ProgressActions {
actions[stepID] = action
}
return actions
}
// GetAllFailedActions returns a copy of all failure actions
func (q *Quest) GetAllFailedActions() map[int32]string {
q.failedActionsMutex.RLock()
defer q.failedActionsMutex.RUnlock()
actions := make(map[int32]string)
for stepID, action := range q.FailedActions {
actions[stepID] = action
}
return actions
}
// ExecuteCompleteAction executes a completion action for a step
func (q *Quest) ExecuteCompleteAction(stepID int32) error {
action, exists := q.GetCompleteAction(stepID)
if !exists || action == "" {
return fmt.Errorf("no completion action for step %d", stepID)
}
// TODO: Execute Lua script with action
// This would integrate with the Lua interface when available
return q.executeLuaAction("complete", stepID, action)
}
// ExecuteProgressAction executes a progress action for a step
func (q *Quest) ExecuteProgressAction(stepID int32, progressAmount int32) error {
action, exists := q.GetProgressAction(stepID)
if !exists || action == "" {
return fmt.Errorf("no progress action for step %d", stepID)
}
// TODO: Execute Lua script with action and progress amount
return q.executeLuaActionWithProgress("progress", stepID, action, progressAmount)
}
// ExecuteFailedAction executes a failure action for a step
func (q *Quest) ExecuteFailedAction(stepID int32) error {
action, exists := q.GetFailedAction(stepID)
if !exists || action == "" {
return fmt.Errorf("no failure action for step %d", stepID)
}
// TODO: Execute Lua script with action
return q.executeLuaAction("failed", stepID, action)
}
// ExecuteQuestCompleteAction executes the general quest completion action
func (q *Quest) ExecuteQuestCompleteAction() error {
if q.CompleteAction == "" {
return nil // No action to execute
}
// TODO: Execute Lua script with quest completion action
return q.executeLuaAction("quest_complete", 0, q.CompleteAction)
}
// Placeholder methods for Lua integration (to be implemented when Lua interface is available)
// executeLuaAction executes a Lua action script
func (q *Quest) executeLuaAction(actionType string, stepID int32, action string) error {
// TODO: Implement Lua script execution
// This is a placeholder that would integrate with the Lua interface
// when it becomes available in the Go codebase
// For now, just log the action that would be executed
fmt.Printf("Quest %d: Would execute %s action for step %d: %s\n", q.ID, actionType, stepID, action)
return nil
}
// executeLuaActionWithProgress executes a Lua action script with progress information
func (q *Quest) executeLuaActionWithProgress(actionType string, stepID int32, action string, progress int32) error {
// TODO: Implement Lua script execution with progress parameter
// This is a placeholder that would integrate with the Lua interface
// For now, just log the action that would be executed
fmt.Printf("Quest %d: Would execute %s action for step %d with progress %d: %s\n", q.ID, actionType, stepID, progress, action)
return nil
}
// Action validation methods
// ValidateActions validates all quest actions
func (q *Quest) ValidateActions() error {
// Validate completion actions
q.completeActionsMutex.RLock()
for stepID, action := range q.CompleteActions {
if err := q.validateAction(stepID, action, "completion"); err != nil {
q.completeActionsMutex.RUnlock()
return err
}
}
q.completeActionsMutex.RUnlock()
// Validate progress actions
q.progressActionsMutex.RLock()
for stepID, action := range q.ProgressActions {
if err := q.validateAction(stepID, action, "progress"); err != nil {
q.progressActionsMutex.RUnlock()
return err
}
}
q.progressActionsMutex.RUnlock()
// Validate failure actions
q.failedActionsMutex.RLock()
for stepID, action := range q.FailedActions {
if err := q.validateAction(stepID, action, "failure"); err != nil {
q.failedActionsMutex.RUnlock()
return err
}
}
q.failedActionsMutex.RUnlock()
// Validate quest completion action
if q.CompleteAction != "" {
if err := q.validateAction(0, q.CompleteAction, "quest completion"); err != nil {
return err
}
}
return nil
}
// validateAction validates a single action
func (q *Quest) validateAction(stepID int32, action, actionType string) error {
if action == "" {
return fmt.Errorf("empty %s action for step %d", actionType, stepID)
}
if len(action) > MaxCompleteActionLength {
return fmt.Errorf("%s action too long for step %d (max %d)", actionType, stepID, MaxCompleteActionLength)
}
// Validate that the step exists (except for quest completion action)
if stepID > 0 {
if step := q.GetQuestStep(stepID); step == nil {
return fmt.Errorf("%s action references non-existent step %d", actionType, stepID)
}
}
// Basic Lua syntax validation (very basic check)
if err := q.validateLuaSyntax(action); err != nil {
return fmt.Errorf("invalid Lua syntax in %s action for step %d: %w", actionType, stepID, err)
}
return nil
}
// validateLuaSyntax performs basic Lua syntax validation
func (q *Quest) validateLuaSyntax(luaCode string) error {
// TODO: Implement proper Lua syntax validation
// This is a placeholder that does very basic checks
// Check for balanced parentheses, brackets, and braces
if err := q.checkBalancedDelimiters(luaCode); err != nil {
return err
}
// Check for obviously invalid syntax patterns
invalidPatterns := []string{
"--[[", // Unfinished multi-line comments
"function(", // Function without closing
}
for _, pattern := range invalidPatterns {
if strings.Contains(luaCode, pattern) {
// Do more thorough checking for these patterns
// This is just a basic check - real validation would be more sophisticated
}
}
return nil
}
// checkBalancedDelimiters checks if delimiters are balanced in Lua code
func (q *Quest) checkBalancedDelimiters(code string) error {
stack := make([]rune, 0)
pairs := map[rune]rune{
')': '(',
']': '[',
'}': '{',
}
inString := false
inComment := false
var stringChar rune
for i, char := range code {
// Handle string literals
if (char == '"' || char == '\'') && !inComment {
if !inString {
inString = true
stringChar = char
} else if char == stringChar {
inString = false
}
continue
}
// Skip everything inside strings
if inString {
continue
}
// Handle line comments
if i > 0 && code[i-1] == '-' && char == '-' && !inComment {
inComment = true
continue
}
// End line comment at newline
if inComment && char == '\n' {
inComment = false
continue
}
// Skip everything in comments
if inComment {
continue
}
// Check delimiters
switch char {
case '(', '[', '{':
stack = append(stack, char)
case ')', ']', '}':
if len(stack) == 0 {
return fmt.Errorf("unmatched closing delimiter '%c'", char)
}
expected := pairs[char]
if stack[len(stack)-1] != expected {
return fmt.Errorf("mismatched delimiter: expected '%c', got '%c'", expected, char)
}
stack = stack[:len(stack)-1]
}
}
if len(stack) > 0 {
return fmt.Errorf("unclosed delimiter '%c'", stack[len(stack)-1])
}
return nil
}

View File

@ -0,0 +1,77 @@
package quests
// Quest step type constants
const (
StepTypeKill int8 = 1
StepTypeChat int8 = 2
StepTypeObtainItem int8 = 3
StepTypeLocation int8 = 4
StepTypeSpell int8 = 5
StepTypeNormal int8 = 6
StepTypeCraft int8 = 7
StepTypeHarvest int8 = 8
StepTypeKillRaceReq int8 = 9 // kill using race type requirement instead of npc db id
)
// Quest display status constants
const (
DisplayStatusHidden int32 = 0
DisplayStatusNoCheck int32 = 1
DisplayStatusYellow int32 = 2
DisplayStatusCompleted int32 = 4
DisplayStatusRepeatable int32 = 8
DisplayStatusCanShare int32 = 16
DisplayStatusCompleteFlag int32 = 32
DisplayStatusShow int32 = 64
DisplayStatusCheck int32 = 128
)
// Quest shareable flags
const (
ShareableNone int32 = 0
ShareableActive int32 = 1
ShareableDuring int32 = 2
ShareableCompleted int32 = 4
)
// Default quest values
const (
DefaultIcon int16 = 11
DefaultPrereqLevel int8 = 1
DefaultVisible int8 = 1
DefaultTaskGroupNum int16 = 1
)
// Quest validation limits
const (
MaxQuestNameLength int = 255
MaxQuestDescriptionLength int = 2048
MaxStepDescriptionLength int = 1024
MaxTaskGroupNameLength int = 255
MaxCompleteActionLength int = 512
)
// Location-based constants
const (
MinLocationVariation float32 = 0.1
MaxLocationVariation float32 = 1000.0
)
// Percentage constants
const (
MinPercentage float32 = 0.0
MaxPercentage float32 = 100.0
)
// Status constants
const (
StatusInactive int32 = 0
StatusActive int32 = 1
StatusComplete int32 = 2
StatusFailed int32 = 3
)
// Random constants
const (
DefaultRandomRange float32 = 100.0
)

View File

@ -0,0 +1,476 @@
package quests
// Player interface defines the required player functionality for quest system
type Player interface {
// Basic player information
GetID() int32
GetName() string
GetLevel() int8
GetTSLevel() int8
GetClass() int8
GetTSClass() int8
GetRace() int8
// Client interface for packet sending
GetClient() Client
// Quest-related player methods
GetQuest(questID int32) *Quest
HasQuest(questID int32) bool
HasQuestBeenCompleted(questID int32) bool
AddQuest(quest *Quest) error
RemoveQuest(questID int32) bool
// Position and zone information
GetX() float32
GetY() float32
GetZ() float32
GetZoneID() int32
// Faction information
GetFactionValue(factionID int32) int32
// Experience and rewards
AddCoins(amount int64)
AddExp(amount int32)
AddTSExp(amount int32)
AddStatusPoints(amount int32)
// Inventory management
GetPlayerItemList() ItemList
// Arrow color calculation (difficulty indication)
GetArrowColor(level int8) int8
GetTSArrowColor(level int8) int8
}
// Client interface defines client functionality needed for quest system
type Client interface {
// Basic client information
GetVersion() int16
GetNameCRC() int32
GetPlayer() Player
// Packet sending
QueuePacket(packet Packet)
SimpleMessage(color int32, message string)
// Quest-specific client methods
SendQuestJournalUpdate(quest *Quest, forceUpdate bool)
PopulateQuestRewardItems(items *[]Item, packet PacketStruct, arrayName ...string)
}
// Spawn interface defines spawn functionality needed for quest system
type Spawn interface {
// Basic spawn information
GetID() int32
GetDatabaseID() int32
GetName() string
GetRace() int8
GetModelType() int16
// Position information
GetX() float32
GetY() float32
GetZ() float32
GetZoneID() int32
}
// Spell interface defines spell functionality needed for quest system
type Spell interface {
GetSpellID() int32
GetName() string
}
// Item interface defines item functionality needed for quest system
type Item interface {
GetItemID() int32
GetName() string
GetIcon(version int16) int32
GetDetails() ItemDetails
GetStackCount() int32
}
// ItemDetails interface defines item detail functionality
type ItemDetails interface {
GetItemID() int32
GetUniqueID() int32
GetCount() int32
}
// ItemList interface defines item list functionality
type ItemList interface {
GetItemFromID(itemID int32, count int32) Item
}
// Packet interface defines packet functionality
type Packet interface {
// Packet data methods (placeholder)
Serialize() []byte
}
// PacketStruct interface defines packet structure functionality
type PacketStruct interface {
// Packet building methods
SetDataByName(name string, value interface{}, index ...int)
SetArrayLengthByName(name string, length int)
SetArrayDataByName(name string, value interface{}, index int)
SetSubArrayLengthByName(name string, length int, index int)
SetSubArrayDataByName(name string, value interface{}, index1, index2 int)
SetSubstructArrayDataByName(substruct, name string, value interface{}, index int)
SetItemArrayDataByName(name string, item Item, player Player, index int, flag ...int)
Serialize() Packet
GetVersion() int16
}
// QuestOfferer interface defines NPCs that can offer quests
type QuestOfferer interface {
Spawn
GetOfferedQuests() []int32
CanOfferQuest(questID int32, player Player) bool
OfferQuest(questID int32, player Player) error
}
// QuestReceiver interface defines NPCs that can receive completed quests
type QuestReceiver interface {
Spawn
GetAcceptedQuests() []int32
CanAcceptQuest(questID int32, player Player) bool
AcceptCompletedQuest(questID int32, player Player) error
}
// QuestValidator interface defines quest validation functionality
type QuestValidator interface {
ValidatePrerequisites(quest *Quest, player Player) error
ValidateQuestCompletion(quest *Quest, player Player) error
ValidateQuestSharing(quest *Quest, sharer, receiver Player) error
}
// QuestRewardProcessor interface defines quest reward processing
type QuestRewardProcessor interface {
ProcessQuestRewards(quest *Quest, player Player) error
CalculateRewards(quest *Quest, player Player) *QuestRewards
GiveRewards(rewards *QuestRewards, player Player) error
}
// QuestRewards contains calculated quest rewards
type QuestRewards struct {
Coins int64 `json:"coins"`
Experience int32 `json:"experience"`
TSExperience int32 `json:"ts_experience"`
StatusPoints int32 `json:"status_points"`
Items []Item `json:"items"`
FactionRewards map[int32]int32 `json:"faction_rewards"`
}
// QuestEventHandler interface defines quest event handling
type QuestEventHandler interface {
OnQuestStarted(quest *Quest, player Player)
OnQuestStepCompleted(quest *Quest, step *QuestStep, player Player)
OnQuestCompleted(quest *Quest, player Player)
OnQuestFailed(quest *Quest, step *QuestStep, player Player)
OnQuestAbandoned(quest *Quest, player Player)
OnQuestShared(quest *Quest, sharer, receiver Player)
}
// Database interface defines database operations for quest system
type Database interface {
// Quest CRUD operations
LoadQuest(questID int32) (*Quest, error)
SaveQuest(quest *Quest) error
DeleteQuest(questID int32) error
LoadAllQuests() ([]*Quest, error)
// Player quest operations
LoadPlayerQuests(playerID int32) ([]*Quest, error)
SavePlayerQuest(playerID int32, quest *Quest) error
DeletePlayerQuest(playerID, questID int32) error
MarkQuestCompleted(playerID, questID int32) error
IsQuestCompleted(playerID, questID int32) bool
// Quest step operations
SaveQuestStepProgress(playerID, questID, stepID int32, progress int32) error
LoadQuestStepProgress(playerID, questID, stepID int32) (int32, error)
}
// Logger interface defines logging functionality for quest system
type Logger interface {
LogInfo(message string, args ...interface{})
LogError(message string, args ...interface{})
LogDebug(message string, args ...interface{})
LogWarning(message string, args ...interface{})
}
// Configuration interface defines quest system configuration
type Configuration interface {
GetMaxPlayerQuests() int
GetQuestSharingEnabled() bool
GetQuestFailureEnabled() bool
GetQuestTimersEnabled() bool
GetXPBonusMultiplier() float64
GetCoinBonusMultiplier() float64
GetStatusBonusMultiplier() float64
}
// QuestSystemAdapter provides integration with other game systems
type QuestSystemAdapter struct {
questManager *QuestManager
validator QuestValidator
rewardProcessor QuestRewardProcessor
eventHandler QuestEventHandler
database Database
logger Logger
config Configuration
}
// NewQuestSystemAdapter creates a new quest system adapter
func NewQuestSystemAdapter(questManager *QuestManager, validator QuestValidator, rewardProcessor QuestRewardProcessor, eventHandler QuestEventHandler, database Database, logger Logger, config Configuration) *QuestSystemAdapter {
return &QuestSystemAdapter{
questManager: questManager,
validator: validator,
rewardProcessor: rewardProcessor,
eventHandler: eventHandler,
database: database,
logger: logger,
config: config,
}
}
// StartQuest handles starting a new quest for a player
func (qsa *QuestSystemAdapter) StartQuest(questID int32, player Player) error {
// Get quest from master list
quest := qsa.questManager.GetMasterList().GetQuest(questID, true)
if quest == nil {
return fmt.Errorf("quest %d not found", questID)
}
// Validate prerequisites
if qsa.validator != nil {
if err := qsa.validator.ValidatePrerequisites(quest, player); err != nil {
return fmt.Errorf("quest prerequisites not met: %w", err)
}
}
// Check if player can accept more quests
if qsa.config != nil {
maxQuests := qsa.config.GetMaxPlayerQuests()
if maxQuests > 0 && qsa.questManager.GetPlayerQuestCount(player.GetID()) >= maxQuests {
return fmt.Errorf("player has too many active quests")
}
}
// Give quest to player
_, err := qsa.questManager.GiveQuestToPlayer(player.GetID(), questID)
if err != nil {
return err
}
// Save to database
if qsa.database != nil {
if err := qsa.database.SavePlayerQuest(player.GetID(), quest); err != nil {
qsa.logger.LogError("Failed to save player quest to database: %v", err)
}
}
// Trigger event
if qsa.eventHandler != nil {
qsa.eventHandler.OnQuestStarted(quest, player)
}
// Log
if qsa.logger != nil {
qsa.logger.LogInfo("Player %s (%d) started quest %s (%d)", player.GetName(), player.GetID(), quest.Name, questID)
}
return nil
}
// CompleteQuest handles completing a quest for a player
func (qsa *QuestSystemAdapter) CompleteQuest(questID int32, player Player) error {
// Get player's quest
quest := qsa.questManager.GetPlayerQuest(player.GetID(), questID)
if quest == nil {
return fmt.Errorf("player does not have quest %d", questID)
}
// Validate completion
if qsa.validator != nil {
if err := qsa.validator.ValidateQuestCompletion(quest, player); err != nil {
return fmt.Errorf("quest completion validation failed: %w", err)
}
}
// Check if quest is actually complete
if !quest.GetCompleted() {
return fmt.Errorf("quest is not complete")
}
// Process rewards
if qsa.rewardProcessor != nil {
if err := qsa.rewardProcessor.ProcessQuestRewards(quest, player); err != nil {
qsa.logger.LogError("Failed to process quest rewards: %v", err)
return fmt.Errorf("failed to process quest rewards: %w", err)
}
}
// Mark as completed in database
if qsa.database != nil {
if err := qsa.database.MarkQuestCompleted(player.GetID(), questID); err != nil {
qsa.logger.LogError("Failed to mark quest as completed in database: %v", err)
}
}
// Remove from active quests if not repeatable
if !quest.IsRepeatable() {
qsa.questManager.RemoveQuestFromPlayer(player.GetID(), questID)
}
// Trigger event
if qsa.eventHandler != nil {
qsa.eventHandler.OnQuestCompleted(quest, player)
}
// Log
if qsa.logger != nil {
qsa.logger.LogInfo("Player %s (%d) completed quest %s (%d)", player.GetName(), player.GetID(), quest.Name, questID)
}
return nil
}
// UpdateQuestProgress handles updating quest step progress
func (qsa *QuestSystemAdapter) UpdateQuestProgress(questID, stepID, progress int32, player Player) error {
// Get player's quest
quest := qsa.questManager.GetPlayerQuest(player.GetID(), questID)
if quest == nil {
return fmt.Errorf("player does not have quest %d", questID)
}
// Update progress
if !quest.AddStepProgress(stepID, progress) {
return fmt.Errorf("failed to update quest step progress")
}
// Check if step is now complete
step := quest.GetQuestStep(stepID)
if step != nil && step.Complete() {
// Execute complete action if exists
if quest.HasCompleteAction(stepID) {
if err := quest.ExecuteCompleteAction(stepID); err != nil {
qsa.logger.LogError("Failed to execute quest step complete action: %v", err)
}
}
// Trigger event
if qsa.eventHandler != nil {
qsa.eventHandler.OnQuestStepCompleted(quest, step, player)
}
// Log
if qsa.logger != nil {
qsa.logger.LogInfo("Player %s (%d) completed step %d of quest %s (%d)", player.GetName(), player.GetID(), stepID, quest.Name, questID)
}
}
// Save progress to database
if qsa.database != nil {
if err := qsa.database.SaveQuestStepProgress(player.GetID(), questID, stepID, step.GetStepProgress()); err != nil {
qsa.logger.LogError("Failed to save quest step progress to database: %v", err)
}
}
return nil
}
// AbandonQuest handles abandoning a quest
func (qsa *QuestSystemAdapter) AbandonQuest(questID int32, player Player) error {
// Get player's quest
quest := qsa.questManager.GetPlayerQuest(player.GetID(), questID)
if quest == nil {
return fmt.Errorf("player does not have quest %d", questID)
}
// Check if quest can be abandoned
if !quest.CanDeleteQuest {
return fmt.Errorf("quest cannot be abandoned")
}
// Remove from player
if !qsa.questManager.RemoveQuestFromPlayer(player.GetID(), questID) {
return fmt.Errorf("failed to remove quest from player")
}
// Remove from database
if qsa.database != nil {
if err := qsa.database.DeletePlayerQuest(player.GetID(), questID); err != nil {
qsa.logger.LogError("Failed to delete player quest from database: %v", err)
}
}
// Trigger event
if qsa.eventHandler != nil {
qsa.eventHandler.OnQuestAbandoned(quest, player)
}
// Log
if qsa.logger != nil {
qsa.logger.LogInfo("Player %s (%d) abandoned quest %s (%d)", player.GetName(), player.GetID(), quest.Name, questID)
}
return nil
}
// ShareQuest handles sharing a quest between players
func (qsa *QuestSystemAdapter) ShareQuest(questID int32, sharer, receiver Player) error {
if !qsa.config.GetQuestSharingEnabled() {
return fmt.Errorf("quest sharing is disabled")
}
// Get sharer's quest
quest := qsa.questManager.GetPlayerQuest(sharer.GetID(), questID)
if quest == nil {
return fmt.Errorf("sharer does not have quest %d", questID)
}
// Validate sharing
if qsa.validator != nil {
if err := qsa.validator.ValidateQuestSharing(quest, sharer, receiver); err != nil {
return fmt.Errorf("quest sharing validation failed: %w", err)
}
}
// Check if receiver can accept more quests
if qsa.config != nil {
maxQuests := qsa.config.GetMaxPlayerQuests()
if maxQuests > 0 && qsa.questManager.GetPlayerQuestCount(receiver.GetID()) >= maxQuests {
return fmt.Errorf("receiver has too many active quests")
}
}
// Give quest to receiver
_, err := qsa.questManager.GiveQuestToPlayer(receiver.GetID(), questID)
if err != nil {
return fmt.Errorf("failed to give quest to receiver: %w", err)
}
// Save to database
if qsa.database != nil {
receiverQuest := qsa.questManager.GetPlayerQuest(receiver.GetID(), questID)
if err := qsa.database.SavePlayerQuest(receiver.GetID(), receiverQuest); err != nil {
qsa.logger.LogError("Failed to save shared quest to database: %v", err)
}
}
// Trigger event
if qsa.eventHandler != nil {
qsa.eventHandler.OnQuestShared(quest, sharer, receiver)
}
// Log
if qsa.logger != nil {
qsa.logger.LogInfo("Player %s (%d) shared quest %s (%d) with %s (%d)",
sharer.GetName(), sharer.GetID(), quest.Name, questID, receiver.GetName(), receiver.GetID())
}
return nil
}

450
internal/quests/manager.go Normal file
View File

@ -0,0 +1,450 @@
package quests
import (
"fmt"
"sync"
)
// MasterQuestList manages all quests in the system
type MasterQuestList struct {
quests map[int32]*Quest
mutex sync.RWMutex
}
// NewMasterQuestList creates a new master quest list
func NewMasterQuestList() *MasterQuestList {
return &MasterQuestList{
quests: make(map[int32]*Quest),
}
}
// AddQuest adds a quest to the master list
func (mql *MasterQuestList) AddQuest(questID int32, quest *Quest) error {
if quest == nil {
return fmt.Errorf("quest cannot be nil")
}
if questID != quest.ID {
return fmt.Errorf("quest ID mismatch: provided %d, quest has %d", questID, quest.ID)
}
mql.mutex.Lock()
defer mql.mutex.Unlock()
if _, exists := mql.quests[questID]; exists {
return fmt.Errorf("quest %d already exists", questID)
}
mql.quests[questID] = quest
return nil
}
// GetQuest returns a quest by ID, optionally creating a copy
func (mql *MasterQuestList) GetQuest(questID int32, copyQuest bool) *Quest {
mql.mutex.RLock()
defer mql.mutex.RUnlock()
quest, exists := mql.quests[questID]
if !exists {
return nil
}
if copyQuest {
return quest.Copy()
}
return quest
}
// RemoveQuest removes a quest from the master list
func (mql *MasterQuestList) RemoveQuest(questID int32) bool {
mql.mutex.Lock()
defer mql.mutex.Unlock()
if _, exists := mql.quests[questID]; exists {
delete(mql.quests, questID)
return true
}
return false
}
// HasQuest checks if a quest exists
func (mql *MasterQuestList) HasQuest(questID int32) bool {
mql.mutex.RLock()
defer mql.mutex.RUnlock()
_, exists := mql.quests[questID]
return exists
}
// GetAllQuests returns a copy of all quests map
func (mql *MasterQuestList) GetAllQuests() map[int32]*Quest {
mql.mutex.RLock()
defer mql.mutex.RUnlock()
quests := make(map[int32]*Quest)
for id, quest := range mql.quests {
quests[id] = quest
}
return quests
}
// GetQuestsByLevel returns quests within a level range
func (mql *MasterQuestList) GetQuestsByLevel(minLevel, maxLevel int8) []*Quest {
mql.mutex.RLock()
defer mql.mutex.RUnlock()
var quests []*Quest
for _, quest := range mql.quests {
if quest.Level >= minLevel && quest.Level <= maxLevel {
quests = append(quests, quest)
}
}
return quests
}
// GetQuestsByType returns quests of a specific type
func (mql *MasterQuestList) GetQuestsByType(questType string) []*Quest {
mql.mutex.RLock()
defer mql.mutex.RUnlock()
var quests []*Quest
for _, quest := range mql.quests {
if quest.Type == questType {
quests = append(quests, quest)
}
}
return quests
}
// GetQuestsByZone returns quests in a specific zone
func (mql *MasterQuestList) GetQuestsByZone(zone string) []*Quest {
mql.mutex.RLock()
defer mql.mutex.RUnlock()
var quests []*Quest
for _, quest := range mql.quests {
if quest.Zone == zone {
quests = append(quests, quest)
}
}
return quests
}
// GetQuestsByGiver returns quests given by a specific NPC
func (mql *MasterQuestList) GetQuestsByGiver(giverID int32) []*Quest {
mql.mutex.RLock()
defer mql.mutex.RUnlock()
var quests []*Quest
for _, quest := range mql.quests {
if quest.QuestGiver == giverID {
quests = append(quests, quest)
}
}
return quests
}
// GetRepeatableQuests returns all repeatable quests
func (mql *MasterQuestList) GetRepeatableQuests() []*Quest {
mql.mutex.RLock()
defer mql.mutex.RUnlock()
var quests []*Quest
for _, quest := range mql.quests {
if quest.Repeatable {
quests = append(quests, quest)
}
}
return quests
}
// GetQuestCount returns the total number of quests
func (mql *MasterQuestList) GetQuestCount() int {
mql.mutex.RLock()
defer mql.mutex.RUnlock()
return len(mql.quests)
}
// Clear removes all quests
func (mql *MasterQuestList) Clear() {
mql.mutex.Lock()
defer mql.mutex.Unlock()
mql.quests = make(map[int32]*Quest)
}
// Reload clears and reloads all quests
func (mql *MasterQuestList) Reload() {
mql.Clear()
// TODO: Implement quest reloading from database or files
// This would typically involve:
// 1. Loading quest data from database
// 2. Creating Quest objects
// 3. Adding them to the master list
// For now, this is a placeholder
fmt.Println("Quest reload requested - implementation pending")
}
// ValidateAllQuests validates all quests in the master list
func (mql *MasterQuestList) ValidateAllQuests() []error {
mql.mutex.RLock()
defer mql.mutex.RUnlock()
var errors []error
for questID, quest := range mql.quests {
if err := quest.ValidateQuest(); err != nil {
errors = append(errors, fmt.Errorf("quest %d validation failed: %w", questID, err))
}
}
return errors
}
// GetQuestStatistics returns basic statistics about the quest system
func (mql *MasterQuestList) GetQuestStatistics() *QuestStatistics {
mql.mutex.RLock()
defer mql.mutex.RUnlock()
stats := &QuestStatistics{
TotalQuests: len(mql.quests),
QuestsByType: make(map[string]int),
QuestsByLevel: make(map[int8]int),
RepeatableCount: 0,
HiddenCount: 0,
}
for _, quest := range mql.quests {
// Count by type
stats.QuestsByType[quest.Type]++
// Count by level
stats.QuestsByLevel[quest.Level]++
// Count special flags
if quest.Repeatable {
stats.RepeatableCount++
}
if quest.Hidden {
stats.HiddenCount++
}
}
return stats
}
// QuestStatistics contains statistical information about quests
type QuestStatistics struct {
TotalQuests int `json:"total_quests"`
QuestsByType map[string]int `json:"quests_by_type"`
QuestsByLevel map[int8]int `json:"quests_by_level"`
RepeatableCount int `json:"repeatable_count"`
HiddenCount int `json:"hidden_count"`
}
// QuestManager provides high-level quest management functionality
type QuestManager struct {
masterList *MasterQuestList
playerQuests map[int32]map[int32]*Quest // playerID -> questID -> quest
mutex sync.RWMutex
}
// NewQuestManager creates a new quest manager
func NewQuestManager() *QuestManager {
return &QuestManager{
masterList: NewMasterQuestList(),
playerQuests: make(map[int32]map[int32]*Quest),
}
}
// GetMasterList returns the master quest list
func (qm *QuestManager) GetMasterList() *MasterQuestList {
return qm.masterList
}
// GiveQuestToPlayer assigns a quest to a player
func (qm *QuestManager) GiveQuestToPlayer(playerID, questID int32) (*Quest, error) {
// Get quest from master list (copy it)
quest := qm.masterList.GetQuest(questID, true)
if quest == nil {
return nil, fmt.Errorf("quest %d not found", questID)
}
qm.mutex.Lock()
defer qm.mutex.Unlock()
// Initialize player quest map if needed
if qm.playerQuests[playerID] == nil {
qm.playerQuests[playerID] = make(map[int32]*Quest)
}
// Check if player already has this quest
if _, exists := qm.playerQuests[playerID][questID]; exists {
return nil, fmt.Errorf("player %d already has quest %d", playerID, questID)
}
// Assign quest to player
qm.playerQuests[playerID][questID] = quest
return quest, nil
}
// RemoveQuestFromPlayer removes a quest from a player
func (qm *QuestManager) RemoveQuestFromPlayer(playerID, questID int32) bool {
qm.mutex.Lock()
defer qm.mutex.Unlock()
if playerQuests, exists := qm.playerQuests[playerID]; exists {
if _, questExists := playerQuests[questID]; questExists {
delete(playerQuests, questID)
// Clean up empty player quest map
if len(playerQuests) == 0 {
delete(qm.playerQuests, playerID)
}
return true
}
}
return false
}
// GetPlayerQuest returns a specific quest for a player
func (qm *QuestManager) GetPlayerQuest(playerID, questID int32) *Quest {
qm.mutex.RLock()
defer qm.mutex.RUnlock()
if playerQuests, exists := qm.playerQuests[playerID]; exists {
return playerQuests[questID]
}
return nil
}
// GetPlayerQuests returns all quests for a player
func (qm *QuestManager) GetPlayerQuests(playerID int32) map[int32]*Quest {
qm.mutex.RLock()
defer qm.mutex.RUnlock()
if playerQuests, exists := qm.playerQuests[playerID]; exists {
// Return a copy
quests := make(map[int32]*Quest)
for questID, quest := range playerQuests {
quests[questID] = quest
}
return quests
}
return make(map[int32]*Quest)
}
// PlayerHasQuest checks if a player has a specific quest
func (qm *QuestManager) PlayerHasQuest(playerID, questID int32) bool {
return qm.GetPlayerQuest(playerID, questID) != nil
}
// GetPlayerQuestCount returns the number of quests a player has
func (qm *QuestManager) GetPlayerQuestCount(playerID int32) int {
qm.mutex.RLock()
defer qm.mutex.RUnlock()
if playerQuests, exists := qm.playerQuests[playerID]; exists {
return len(playerQuests)
}
return 0
}
// ClearPlayerQuests removes all quests from a player
func (qm *QuestManager) ClearPlayerQuests(playerID int32) {
qm.mutex.Lock()
defer qm.mutex.Unlock()
delete(qm.playerQuests, playerID)
}
// GetAllPlayerIDs returns all player IDs that have quests
func (qm *QuestManager) GetAllPlayerIDs() []int32 {
qm.mutex.RLock()
defer qm.mutex.RUnlock()
var playerIDs []int32
for playerID := range qm.playerQuests {
playerIDs = append(playerIDs, playerID)
}
return playerIDs
}
// UpdatePlayerQuestProgress updates progress for a player's quest step
func (qm *QuestManager) UpdatePlayerQuestProgress(playerID, questID, stepID, progress int32) bool {
quest := qm.GetPlayerQuest(playerID, questID)
if quest == nil {
return false
}
return quest.AddStepProgress(stepID, progress)
}
// CompletePlayerQuestStep marks a quest step as complete for a player
func (qm *QuestManager) CompletePlayerQuestStep(playerID, questID, stepID int32) bool {
quest := qm.GetPlayerQuest(playerID, questID)
if quest == nil {
return false
}
return quest.SetStepComplete(stepID)
}
// IsPlayerQuestComplete checks if a player's quest is complete
func (qm *QuestManager) IsPlayerQuestComplete(playerID, questID int32) bool {
quest := qm.GetPlayerQuest(playerID, questID)
if quest == nil {
return false
}
return quest.GetCompleted()
}
// GetPlayerQuestStatistics returns statistics for a player's quests
func (qm *QuestManager) GetPlayerQuestStatistics(playerID int32) *PlayerQuestStatistics {
quests := qm.GetPlayerQuests(playerID)
stats := &PlayerQuestStatistics{
TotalQuests: len(quests),
CompletedQuests: 0,
QuestsByType: make(map[string]int),
QuestsByLevel: make(map[int8]int),
}
for _, quest := range quests {
if quest.GetCompleted() {
stats.CompletedQuests++
}
stats.QuestsByType[quest.Type]++
stats.QuestsByLevel[quest.Level]++
}
return stats
}
// PlayerQuestStatistics contains statistical information about a player's quests
type PlayerQuestStatistics struct {
TotalQuests int `json:"total_quests"`
CompletedQuests int `json:"completed_quests"`
QuestsByType map[string]int `json:"quests_by_type"`
QuestsByLevel map[int8]int `json:"quests_by_level"`
}

View File

@ -0,0 +1,378 @@
package quests
// Prerequisite management methods for Quest
// SetPrereqLevel sets the minimum level requirement
func (q *Quest) SetPrereqLevel(level int8) {
q.PrereqLevel = level
q.SetSaveNeeded(true)
}
// SetPrereqTSLevel sets the minimum tradeskill level requirement
func (q *Quest) SetPrereqTSLevel(level int8) {
q.PrereqTSLevel = level
q.SetSaveNeeded(true)
}
// SetPrereqMaxLevel sets the maximum level requirement
func (q *Quest) SetPrereqMaxLevel(level int8) {
q.PrereqMaxLevel = level
q.SetSaveNeeded(true)
}
// SetPrereqMaxTSLevel sets the maximum tradeskill level requirement
func (q *Quest) SetPrereqMaxTSLevel(level int8) {
q.PrereqMaxTSLevel = level
q.SetSaveNeeded(true)
}
// AddPrereqClass adds a class prerequisite
func (q *Quest) AddPrereqClass(classID int8) {
q.PrereqClasses = append(q.PrereqClasses, classID)
q.SetSaveNeeded(true)
}
// AddPrereqTradeskillClass adds a tradeskill class prerequisite
func (q *Quest) AddPrereqTradeskillClass(classID int8) {
q.PrereqTSClasses = append(q.PrereqTSClasses, classID)
q.SetSaveNeeded(true)
}
// AddPrereqModelType adds a model type prerequisite
func (q *Quest) AddPrereqModelType(modelType int16) {
q.PrereqModelTypes = append(q.PrereqModelTypes, modelType)
q.SetSaveNeeded(true)
}
// AddPrereqRace adds a race prerequisite
func (q *Quest) AddPrereqRace(race int8) {
q.PrereqRaces = append(q.PrereqRaces, race)
q.SetSaveNeeded(true)
}
// AddPrereqQuest adds a quest prerequisite
func (q *Quest) AddPrereqQuest(questID int32) {
q.PrereqQuests = append(q.PrereqQuests, questID)
q.SetSaveNeeded(true)
}
// AddPrereqFaction adds a faction prerequisite
func (q *Quest) AddPrereqFaction(factionID int32, min, max int32) {
faction := NewQuestFactionPrereq(factionID, min, max)
q.PrereqFactions = append(q.PrereqFactions, faction)
q.SetSaveNeeded(true)
}
// RemovePrereqClass removes a class prerequisite
func (q *Quest) RemovePrereqClass(classID int8) bool {
for i, class := range q.PrereqClasses {
if class == classID {
q.PrereqClasses = append(q.PrereqClasses[:i], q.PrereqClasses[i+1:]...)
q.SetSaveNeeded(true)
return true
}
}
return false
}
// RemovePrereqTradeskillClass removes a tradeskill class prerequisite
func (q *Quest) RemovePrereqTradeskillClass(classID int8) bool {
for i, class := range q.PrereqTSClasses {
if class == classID {
q.PrereqTSClasses = append(q.PrereqTSClasses[:i], q.PrereqTSClasses[i+1:]...)
q.SetSaveNeeded(true)
return true
}
}
return false
}
// RemovePrereqModelType removes a model type prerequisite
func (q *Quest) RemovePrereqModelType(modelType int16) bool {
for i, model := range q.PrereqModelTypes {
if model == modelType {
q.PrereqModelTypes = append(q.PrereqModelTypes[:i], q.PrereqModelTypes[i+1:]...)
q.SetSaveNeeded(true)
return true
}
}
return false
}
// RemovePrereqRace removes a race prerequisite
func (q *Quest) RemovePrereqRace(race int8) bool {
for i, r := range q.PrereqRaces {
if r == race {
q.PrereqRaces = append(q.PrereqRaces[:i], q.PrereqRaces[i+1:]...)
q.SetSaveNeeded(true)
return true
}
}
return false
}
// RemovePrereqQuest removes a quest prerequisite
func (q *Quest) RemovePrereqQuest(questID int32) bool {
for i, quest := range q.PrereqQuests {
if quest == questID {
q.PrereqQuests = append(q.PrereqQuests[:i], q.PrereqQuests[i+1:]...)
q.SetSaveNeeded(true)
return true
}
}
return false
}
// RemovePrereqFaction removes a faction prerequisite
func (q *Quest) RemovePrereqFaction(factionID int32) bool {
for i, faction := range q.PrereqFactions {
if faction.FactionID == factionID {
q.PrereqFactions = append(q.PrereqFactions[:i], q.PrereqFactions[i+1:]...)
q.SetSaveNeeded(true)
return true
}
}
return false
}
// HasPrereqClass checks if a class is a prerequisite
func (q *Quest) HasPrereqClass(classID int8) bool {
for _, class := range q.PrereqClasses {
if class == classID {
return true
}
}
return false
}
// HasPrereqTradeskillClass checks if a tradeskill class is a prerequisite
func (q *Quest) HasPrereqTradeskillClass(classID int8) bool {
for _, class := range q.PrereqTSClasses {
if class == classID {
return true
}
}
return false
}
// HasPrereqModelType checks if a model type is a prerequisite
func (q *Quest) HasPrereqModelType(modelType int16) bool {
for _, model := range q.PrereqModelTypes {
if model == modelType {
return true
}
}
return false
}
// HasPrereqRace checks if a race is a prerequisite
func (q *Quest) HasPrereqRace(race int8) bool {
for _, r := range q.PrereqRaces {
if r == race {
return true
}
}
return false
}
// HasPrereqQuest checks if a quest is a prerequisite
func (q *Quest) HasPrereqQuest(questID int32) bool {
for _, quest := range q.PrereqQuests {
if quest == questID {
return true
}
}
return false
}
// HasPrereqFaction checks if a faction is a prerequisite
func (q *Quest) HasPrereqFaction(factionID int32) bool {
for _, faction := range q.PrereqFactions {
if faction.FactionID == factionID {
return true
}
}
return false
}
// GetPrereqFaction returns the faction prerequisite for a given faction ID
func (q *Quest) GetPrereqFaction(factionID int32) *QuestFactionPrereq {
for _, faction := range q.PrereqFactions {
if faction.FactionID == factionID {
return faction
}
}
return nil
}
// ClearAllPrerequisites removes all prerequisites
func (q *Quest) ClearAllPrerequisites() {
q.PrereqLevel = DefaultPrereqLevel
q.PrereqTSLevel = 0
q.PrereqMaxLevel = 0
q.PrereqMaxTSLevel = 0
q.PrereqFactions = q.PrereqFactions[:0]
q.PrereqRaces = q.PrereqRaces[:0]
q.PrereqModelTypes = q.PrereqModelTypes[:0]
q.PrereqClasses = q.PrereqClasses[:0]
q.PrereqTSClasses = q.PrereqTSClasses[:0]
q.PrereqQuests = q.PrereqQuests[:0]
q.SetSaveNeeded(true)
}
// Prerequisite validation methods
// ValidatePrerequisites validates all quest prerequisites
func (q *Quest) ValidatePrerequisites() error {
// Validate level requirements
if q.PrereqLevel < 1 || q.PrereqLevel > 100 {
return fmt.Errorf("prerequisite level must be between 1 and 100")
}
if q.PrereqTSLevel < 0 || q.PrereqTSLevel > 100 {
return fmt.Errorf("prerequisite tradeskill level must be between 0 and 100")
}
if q.PrereqMaxLevel > 0 && q.PrereqMaxLevel < q.PrereqLevel {
return fmt.Errorf("prerequisite max level cannot be less than min level")
}
if q.PrereqMaxTSLevel > 0 && q.PrereqMaxTSLevel < q.PrereqTSLevel {
return fmt.Errorf("prerequisite max tradeskill level cannot be less than min tradeskill level")
}
// Validate class prerequisites
if err := q.validateClassPrerequisites(); err != nil {
return err
}
// Validate race prerequisites
if err := q.validateRacePrerequisites(); err != nil {
return err
}
// Validate faction prerequisites
if err := q.validateFactionPrerequisites(); err != nil {
return err
}
return nil
}
// validateClassPrerequisites validates class prerequisites
func (q *Quest) validateClassPrerequisites() error {
// Check for duplicate classes
classMap := make(map[int8]bool)
for _, classID := range q.PrereqClasses {
if classMap[classID] {
return fmt.Errorf("duplicate class prerequisite: %d", classID)
}
classMap[classID] = true
// Validate class ID range (basic validation)
if classID < 1 || classID > 100 {
return fmt.Errorf("invalid class ID in prerequisites: %d", classID)
}
}
// Check for duplicate tradeskill classes
tsClassMap := make(map[int8]bool)
for _, classID := range q.PrereqTSClasses {
if tsClassMap[classID] {
return fmt.Errorf("duplicate tradeskill class prerequisite: %d", classID)
}
tsClassMap[classID] = true
// Validate tradeskill class ID range
if classID < 1 || classID > 100 {
return fmt.Errorf("invalid tradeskill class ID in prerequisites: %d", classID)
}
}
return nil
}
// validateRacePrerequisites validates race prerequisites
func (q *Quest) validateRacePrerequisites() error {
// Check for duplicate races
raceMap := make(map[int8]bool)
for _, race := range q.PrereqRaces {
if raceMap[race] {
return fmt.Errorf("duplicate race prerequisite: %d", race)
}
raceMap[race] = true
// Validate race ID range
if race < 1 || race > 50 {
return fmt.Errorf("invalid race ID in prerequisites: %d", race)
}
}
// Check for duplicate model types
modelMap := make(map[int16]bool)
for _, modelType := range q.PrereqModelTypes {
if modelMap[modelType] {
return fmt.Errorf("duplicate model type prerequisite: %d", modelType)
}
modelMap[modelType] = true
// Validate model type range
if modelType < 1 {
return fmt.Errorf("invalid model type in prerequisites: %d", modelType)
}
}
return nil
}
// validateFactionPrerequisites validates faction prerequisites
func (q *Quest) validateFactionPrerequisites() error {
// Check for duplicate factions and validate ranges
factionMap := make(map[int32]bool)
for _, faction := range q.PrereqFactions {
if factionMap[faction.FactionID] {
return fmt.Errorf("duplicate faction prerequisite: %d", faction.FactionID)
}
factionMap[faction.FactionID] = true
// Validate faction ID
if faction.FactionID <= 0 {
return fmt.Errorf("invalid faction ID in prerequisites: %d", faction.FactionID)
}
// Validate faction value ranges
if faction.Min < -50000 || faction.Min > 50000 {
return fmt.Errorf("faction %d min value out of range: %d", faction.FactionID, faction.Min)
}
if faction.Max < -50000 || faction.Max > 50000 {
return fmt.Errorf("faction %d max value out of range: %d", faction.FactionID, faction.Max)
}
// Check min/max relationship
if faction.Max != 0 && faction.Max < faction.Min {
return fmt.Errorf("faction %d max value cannot be less than min value", faction.FactionID)
}
}
// Check for duplicate quest prerequisites
questMap := make(map[int32]bool)
for _, questID := range q.PrereqQuests {
if questMap[questID] {
return fmt.Errorf("duplicate quest prerequisite: %d", questID)
}
questMap[questID] = true
// Don't allow self-reference
if questID == q.ID {
return fmt.Errorf("quest cannot be a prerequisite of itself")
}
// Validate quest ID
if questID <= 0 {
return fmt.Errorf("invalid quest ID in prerequisites: %d", questID)
}
}
return nil
}

737
internal/quests/quest.go Normal file
View File

@ -0,0 +1,737 @@
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
}

427
internal/quests/rewards.go Normal file
View File

@ -0,0 +1,427 @@
package quests
// Reward management methods for Quest
// AddRewardCoins adds coin rewards
func (q *Quest) AddRewardCoins(copper, silver, gold, platinum int32) {
q.RewardCoins = int64(copper) + int64(silver)*100 + int64(gold)*10000 + int64(platinum)*1000000
q.SetSaveNeeded(true)
}
// AddRewardCoinsMax sets the maximum coin reward
func (q *Quest) AddRewardCoinsMax(coins int64) {
q.RewardCoinsMax = coins
q.SetSaveNeeded(true)
}
// AddRewardFaction adds a faction reward
func (q *Quest) AddRewardFaction(factionID int32, amount int32) {
q.RewardFactions[factionID] = amount
q.SetSaveNeeded(true)
}
// RemoveRewardFaction removes a faction reward
func (q *Quest) RemoveRewardFaction(factionID int32) bool {
if _, exists := q.RewardFactions[factionID]; exists {
delete(q.RewardFactions, factionID)
q.SetSaveNeeded(true)
return true
}
return false
}
// SetRewardStatus sets the status point reward
func (q *Quest) SetRewardStatus(amount int32) {
q.RewardStatus = amount
q.SetSaveNeeded(true)
}
// SetRewardComment sets the reward comment text
func (q *Quest) SetRewardComment(comment string) {
q.RewardComment = comment
q.SetSaveNeeded(true)
}
// SetRewardXP sets the experience point reward
func (q *Quest) SetRewardXP(xp int32) {
q.RewardExp = xp
q.SetSaveNeeded(true)
}
// SetRewardTSXP sets the tradeskill experience point reward
func (q *Quest) SetRewardTSXP(xp int32) {
q.RewardTSExp = xp
q.SetSaveNeeded(true)
}
// SetGeneratedCoin sets the generated coin amount
func (q *Quest) SetGeneratedCoin(coin int64) {
q.GeneratedCoin = coin
q.SetSaveNeeded(true)
}
// GetCoinsReward returns the base coin reward
func (q *Quest) GetCoinsReward() int64 {
return q.RewardCoins
}
// GetCoinsRewardMax returns the maximum coin reward
func (q *Quest) GetCoinsRewardMax() int64 {
return q.RewardCoinsMax
}
// GetGeneratedCoin returns the generated coin amount
func (q *Quest) GetGeneratedCoin() int64 {
return q.GeneratedCoin
}
// GetExpReward returns the experience point reward
func (q *Quest) GetExpReward() int32 {
return q.RewardExp
}
// GetTSExpReward returns the tradeskill experience point reward
func (q *Quest) GetTSExpReward() int32 {
return q.RewardTSExp
}
// GetStatusPoints returns the status point reward
func (q *Quest) GetStatusPoints() int32 {
return q.RewardStatus
}
// GetRewardFactions returns the faction rewards map
func (q *Quest) GetRewardFactions() map[int32]int32 {
return q.RewardFactions
}
// GetRewardFaction returns the faction reward amount for a specific faction
func (q *Quest) GetRewardFaction(factionID int32) (int32, bool) {
amount, exists := q.RewardFactions[factionID]
return amount, exists
}
// HasRewardFaction checks if a faction has a reward
func (q *Quest) HasRewardFaction(factionID int32) bool {
_, exists := q.RewardFactions[factionID]
return exists
}
// ClearAllRewards removes all rewards
func (q *Quest) ClearAllRewards() {
q.RewardCoins = 0
q.RewardCoinsMax = 0
q.RewardStatus = 0
q.RewardComment = ""
q.RewardExp = 0
q.RewardTSExp = 0
q.GeneratedCoin = 0
q.RewardFactions = make(map[int32]int32)
q.SetSaveNeeded(true)
}
// Temporary reward methods
// SetStatusTmpReward sets the temporary status reward
func (q *Quest) SetStatusTmpReward(status int32) {
q.TmpRewardStatus = status
q.SetSaveNeeded(true)
}
// GetStatusTmpReward returns the temporary status reward
func (q *Quest) GetStatusTmpReward() int32 {
return q.TmpRewardStatus
}
// SetCoinTmpReward sets the temporary coin reward
func (q *Quest) SetCoinTmpReward(coins int64) {
q.TmpRewardCoins = coins
q.SetSaveNeeded(true)
}
// GetCoinTmpReward returns the temporary coin reward
func (q *Quest) GetCoinTmpReward() int64 {
return q.TmpRewardCoins
}
// ClearTmpRewards clears temporary rewards
func (q *Quest) ClearTmpRewards() {
q.TmpRewardStatus = 0
q.TmpRewardCoins = 0
q.SetSaveNeeded(true)
}
// Status earning methods
// SetStatusToEarnMin sets the minimum status to earn
func (q *Quest) SetStatusToEarnMin(value int32) {
q.StatusToEarnMin = value
q.SetSaveNeeded(true)
}
// GetStatusToEarnMin returns the minimum status to earn
func (q *Quest) GetStatusToEarnMin() int32 {
return q.StatusToEarnMin
}
// SetStatusToEarnMax sets the maximum status to earn
func (q *Quest) SetStatusToEarnMax(value int32) {
q.StatusToEarnMax = value
q.SetSaveNeeded(true)
}
// GetStatusToEarnMax returns the maximum status to earn
func (q *Quest) GetStatusToEarnMax() int32 {
return q.StatusToEarnMax
}
// SetStatusEarned sets the quest status earned
func (q *Quest) SetStatusEarned(status int32) {
q.Status = status
q.SetSaveNeeded(true)
}
// GetStatusEarned returns the quest status earned
func (q *Quest) GetStatusEarned() int32 {
return q.Status
}
// Reward calculation methods
// CalculateCoinsReward calculates the actual coin reward based on level and other factors
func (q *Quest) CalculateCoinsReward(playerLevel int8) int64 {
baseReward := q.RewardCoins
if baseReward <= 0 {
return 0
}
// Use generated coin if set
if q.GeneratedCoin > 0 {
return q.GeneratedCoin
}
// Level-based coin scaling (simplified version)
levelMultiplier := float64(playerLevel) / float64(q.Level)
if levelMultiplier > 1.5 {
levelMultiplier = 1.5 // Cap the multiplier
} else if levelMultiplier < 0.5 {
levelMultiplier = 0.5 // Floor the multiplier
}
calculatedReward := int64(float64(baseReward) * levelMultiplier)
// Apply max reward cap if set
if q.RewardCoinsMax > 0 && calculatedReward > q.RewardCoinsMax {
calculatedReward = q.RewardCoinsMax
}
return calculatedReward
}
// CalculateExpReward calculates the actual experience reward based on level and other factors
func (q *Quest) CalculateExpReward(playerLevel int8) int32 {
baseReward := q.RewardExp
if baseReward <= 0 {
return 0
}
// Level-based experience scaling
if playerLevel > q.Level {
// Reduced XP for overleveled players
levelDiff := playerLevel - q.Level
reduction := float64(levelDiff) * 0.1
if reduction > 0.8 {
reduction = 0.8 // Max 80% reduction
}
return int32(float64(baseReward) * (1.0 - reduction))
} else if playerLevel < q.Level {
// Bonus XP for underleveled players (small bonus)
levelDiff := q.Level - playerLevel
bonus := float64(levelDiff) * 0.05
if bonus > 0.25 {
bonus = 0.25 // Max 25% bonus
}
return int32(float64(baseReward) * (1.0 + bonus))
}
return baseReward
}
// CalculateTSExpReward calculates the actual tradeskill experience reward
func (q *Quest) CalculateTSExpReward(playerTSLevel int8) int32 {
baseReward := q.RewardTSExp
if baseReward <= 0 {
return 0
}
// Similar scaling as regular XP but for tradeskill level
if playerTSLevel > q.Level {
levelDiff := playerTSLevel - q.Level
reduction := float64(levelDiff) * 0.1
if reduction > 0.8 {
reduction = 0.8
}
return int32(float64(baseReward) * (1.0 - reduction))
} else if playerTSLevel < q.Level {
levelDiff := q.Level - playerTSLevel
bonus := float64(levelDiff) * 0.05
if bonus > 0.25 {
bonus = 0.25
}
return int32(float64(baseReward) * (1.0 + bonus))
}
return baseReward
}
// CalculateStatusReward calculates the actual status reward
func (q *Quest) CalculateStatusReward() int32 {
// Use status earned if set, otherwise use base reward status
if q.Status > 0 {
return q.Status
}
return q.RewardStatus
}
// Reward validation methods
// ValidateRewards validates all quest rewards
func (q *Quest) ValidateRewards() error {
// Validate coin rewards
if q.RewardCoins < 0 {
return fmt.Errorf("reward coins cannot be negative")
}
if q.RewardCoinsMax < 0 {
return fmt.Errorf("reward coins max cannot be negative")
}
if q.RewardCoinsMax > 0 && q.RewardCoinsMax < q.RewardCoins {
return fmt.Errorf("reward coins max cannot be less than base reward coins")
}
// Validate experience rewards
if q.RewardExp < 0 {
return fmt.Errorf("reward experience cannot be negative")
}
if q.RewardTSExp < 0 {
return fmt.Errorf("reward tradeskill experience cannot be negative")
}
// Validate status rewards
if q.RewardStatus < 0 {
return fmt.Errorf("reward status cannot be negative")
}
if q.Status < 0 {
return fmt.Errorf("status earned cannot be negative")
}
// Validate temporary rewards
if q.TmpRewardCoins < 0 {
return fmt.Errorf("temporary reward coins cannot be negative")
}
if q.TmpRewardStatus < 0 {
return fmt.Errorf("temporary reward status cannot be negative")
}
// Validate status to earn ranges
if q.StatusToEarnMin < 0 {
return fmt.Errorf("status to earn min cannot be negative")
}
if q.StatusToEarnMax < 0 {
return fmt.Errorf("status to earn max cannot be negative")
}
if q.StatusToEarnMax > 0 && q.StatusToEarnMax < q.StatusToEarnMin {
return fmt.Errorf("status to earn max cannot be less than min")
}
// Validate faction rewards
for factionID, amount := range q.RewardFactions {
if factionID <= 0 {
return fmt.Errorf("invalid faction ID in rewards: %d", factionID)
}
// Faction amounts can be negative (reputation loss)
if amount < -50000 || amount > 50000 {
return fmt.Errorf("faction reward amount out of range for faction %d: %d", factionID, amount)
}
}
// Validate reward comment length
if len(q.RewardComment) > MaxCompleteActionLength {
return fmt.Errorf("reward comment too long (max %d)", MaxCompleteActionLength)
}
return nil
}
// Helper methods for coin conversion
// ConvertCoinsToComponents breaks down coins into copper, silver, gold, platinum
func ConvertCoinsToComponents(totalCoins int64) (copper, silver, gold, platinum int32) {
platinum = int32(totalCoins / 1000000)
remaining := totalCoins % 1000000
gold = int32(remaining / 10000)
remaining = remaining % 10000
silver = int32(remaining / 100)
copper = int32(remaining % 100)
return copper, silver, gold, platinum
}
// ConvertComponentsToCoins converts individual coin components to total coins
func ConvertComponentsToCoins(copper, silver, gold, platinum int32) int64 {
return int64(copper) + int64(silver)*100 + int64(gold)*10000 + int64(platinum)*1000000
}
// FormatCoinsString formats coins as a readable string
func FormatCoinsString(totalCoins int64) string {
if totalCoins == 0 {
return "0 copper"
}
copper, silver, gold, platinum := ConvertCoinsToComponents(totalCoins)
var parts []string
if platinum > 0 {
parts = append(parts, fmt.Sprintf("%d platinum", platinum))
}
if gold > 0 {
parts = append(parts, fmt.Sprintf("%d gold", gold))
}
if silver > 0 {
parts = append(parts, fmt.Sprintf("%d silver", silver))
}
if copper > 0 {
parts = append(parts, fmt.Sprintf("%d copper", copper))
}
if len(parts) == 0 {
return "0 copper"
}
if len(parts) == 1 {
return parts[0]
}
// Join with commas and "and" for the last item
result := ""
for i, part := range parts {
if i == 0 {
result = part
} else if i == len(parts)-1 {
result += " and " + part
} else {
result += ", " + part
}
}
return result
}

602
internal/quests/types.go Normal file
View File

@ -0,0 +1,602 @@
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
}