significant work on automatic conversion

This commit is contained in:
Sky Johnson 2025-07-30 11:53:39 -05:00
parent fd75638fc6
commit 0c048db2d5
43 changed files with 15275 additions and 0 deletions

249
CLAUDE.md Normal file
View File

@ -0,0 +1,249 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
EQ2Go is a Go rewrite of the EverQuest II server emulator from C++ EQ2EMu. Implements core MMO server functionality with authentication, world simulation, and game mechanics using modern Go practices.
## Development Commands
### Building the Project
```bash
# Build login server
go build -o bin/login_server ./cmd/login_server
# Build world server
go build -o bin/world_server ./cmd/world_server
# Build both servers
go build ./cmd/...
```
### Running the Servers
```bash
# Run login server (requires login_config.json)
./bin/login_server
# Run world server (creates world_config.json with defaults if missing)
./bin/world_server
# With custom configuration
./bin/world_server -listen-port 9001 -web-port 8081 -log-level debug
```
### Testing
```bash
# Run all tests
go test ./...
# Test specific packages
go test ./internal/udp
go test ./internal/packets/parser
# Run tests with verbose output
go test -v ./...
# Test with race detection
go test -race ./...
```
### Development Tools
```bash
# Format code
go fmt ./...
# Run linter (if golangci-lint is installed)
golangci-lint run
# Tidy modules
go mod tidy
# Check for vulnerabilities
go run golang.org/x/vuln/cmd/govulncheck@latest ./...
```
## Architecture Overview
### Core Components
**Login Server** - Client authentication, character management, world server coordination
**World Server** - Game simulation engine, zones, NPCs, combat, quests, web admin interface
**Shared Libraries:**
- `common/` - EQ2-specific data types and constants
- `database/` - SQLite wrapper with simplified query interface
- `udp/` - Custom UDP protocol with reliability layer
- `packets/` - XML-driven packet definition parser
- `achievements/` - Achievement system
- `spawn/` - Base game entity system (positioning, commands, scripting)
- `entity/` - Combat-capable entities with spell effects, stats, pet management
- `spells/` - Complete spell system with casting mechanics and processing
- `titles/` - Character title system with achievement integration
- `trade/` - Player trading system with item/coin exchange and validation
- `object/` - Interactive objects extending spawn system (merchants, transporters, devices, collectors)
- `races/` - Race system with all EQ2 races, alignment classification, stat modifiers, and entity integration
### Network Protocol
EverQuest II UDP protocol with reliability layer, RC4 encryption, CRC validation, connection management, packet combining.
### Database Layer
SQLite with simplified query interface, transactions, connection pooling, parameter binding.
### Packet System
XML-driven packet definitions with version-specific formats, conditional fields, templates, dynamic arrays.
## Key Files and Locations
**Server Entry Points:**
- `cmd/login_server/main.go`: Login server startup and configuration
- `cmd/world_server/main.go`: World server startup with command-line options
**Core Data Structures:**
- `internal/common/types.go`: EQ2-specific types (EQ2Color, EQ2Equipment, AppearanceData, etc.)
**Network Implementation:**
- `internal/udp/server.go`: Multi-connection UDP server
- `internal/udp/connection.go`: Individual client connection handling
- `internal/udp/protocol.go`: Protocol packet types and constants
**Database:**
- `internal/database/wrapper.go`: Simplified SQLite interface
- Database files: `login.db`, `world.db` (created automatically)
**Spawn System:**
- `internal/spawn/spawn.go`: Base spawn entity with position, stats, commands, and scripting
- `internal/spawn/spawn_lists.go`: Spawn location and entry management
- `internal/spawn/README.md`: Comprehensive spawn system documentation
**Entity System:**
- `internal/entity/entity.go`: Combat-capable spawn extension with spell casting and pet management
- `internal/entity/info_struct.go`: Comprehensive character statistics and information management
- `internal/entity/spell_effects.go`: DEPRECATED - now imports from spells package
- `internal/entity/README.md`: Complete entity system documentation
**Spells System:**
- Core: `spell_data.go`, `spell.go`, `spell_effects.go`, `spell_manager.go`, `constants.go`
- Processing: `process_constants.go`, `spell_process.go`, `spell_targeting.go`, `spell_resources.go`
- Docs: `README.md`, `SPELL_PROCESS.md`
**Titles System:**
- `internal/titles/title.go`: Core title data structures and player title management
- `internal/titles/constants.go`: Title system constants, categories, and rarity levels
- `internal/titles/master_list.go`: Global title registry and management with categorization
- `internal/titles/player_titles.go`: Individual player title collections and active title tracking
- `internal/titles/title_manager.go`: Central title system coordinator with background cleanup
- `internal/titles/integration.go`: Integration systems for earning titles through achievements, quests, PvP, events
- `internal/titles/README.md`: Complete title system documentation
**Trade System:**
- `internal/trade/trade.go`: Core trade mechanics with two-party item/coin exchange
- `internal/trade/types.go`: Trade data structures, participants, and validation
- `internal/trade/constants.go`: Trade constants, error codes, and configuration
- `internal/trade/utils.go`: Coin calculations, validation helpers, and formatting
- `internal/trade/manager.go`: Trade service with high-level management and placeholders
**Object System:**
- `internal/object/object.go`: Core object mechanics extending spawn functionality with interaction
- `internal/object/constants.go`: Object constants, spawn types, and interaction types
- `internal/object/manager.go`: Object manager with zone-based and type-based indexing
- `internal/object/integration.go`: Spawn system integration with ObjectSpawn wrapper
- `internal/object/interfaces.go`: Integration interfaces for trade/spell systems and entity adapters
**Race System:**
- `internal/races/races.go`: Core race management with all 21 EQ2 races and alignment mapping
- `internal/races/constants.go`: Race IDs, names, and display constants
- `internal/races/utils.go`: Race utilities with stat modifiers, descriptions, and parsing
- `internal/races/integration.go`: Entity system integration with RaceAware interface
- `internal/races/manager.go`: High-level race management with statistics and command processing
**Packet Definitions:**
- `internal/packets/xml/`: XML packet structure definitions
- `internal/packets/PARSER.md`: Packet definition language documentation
- `internal/packets/parser/`: Parser implementation for XML packet definitions
## Configuration
**Login Server**: Requires `login_config.json` with database and server settings
**World Server**: Creates `world_config.json` with defaults:
```json
{
"listen_addr": "0.0.0.0",
"listen_port": 9000,
"max_clients": 1000,
"web_port": 8080,
"database_path": "world.db",
"log_level": "info"
}
```
Command-line flags override JSON configuration.
## Important Dependencies
**Core Dependencies:**
- `zombiezen.com/go/sqlite`: Modern SQLite driver
- `golang.org/x/crypto`: Cryptographic functions
**Module Information:**
- Go version: 1.24.5
- Module name: `eq2emu`
## Development Notes
**Architecture Transition**: This Go implementation is based on the extensive C++ EQ2EMu codebase documented in `EQ2EMU_Architecture_White_Paper.md`. The Go version maintains compatibility with the original protocol while modernizing the implementation.
**Packet Handling**: The XML packet definition system allows for easy addition of new packet types without code changes. See `internal/packets/PARSER.md` for syntax reference.
**Database Schema**: Currently uses SQLite for development/testing. Production deployments may require migration to PostgreSQL or MySQL for better concurrent access.
**Spawn System**: The spawn system (`internal/spawn/`) provides the foundation for all game entities. It's converted from the C++ EQ2EMu codebase with modern Go concurrency patterns.
**Entity System**: The entity system (`internal/entity/`) extends spawns with combat capabilities, implementing the complete EverQuest II character statistics system, spell effect management, and pet systems. Key features include:
- **InfoStruct**: Complete character statistics (attributes, resistances, experience, currency, equipment data)
- **Entity**: Combat-capable spawn with spell casting, pet management, and bonus calculations
- **Thread Safety**: All systems use proper Go concurrency patterns with mutexes and atomic operations
- **Stat Calculations**: Dynamic stat calculations with bonuses from equipment, spells, and other sources
- **Pet Management**: Support for multiple pet types (summon, charm, deity, cosmetic) with proper ownership tracking
**Spells System**: Complete spell management with spell definitions, effect system, and real-time casting engine. Features spell processing (50ms intervals), comprehensive targeting (self/single/group/AOE), resource management (power/health/concentration), cast/recast timers, interrupt handling, heroic opportunities, and thread-safe operations.
**Titles System**: Character title management with prefix/suffix positioning, categories, rarity levels, achievement integration, and thread-safe operations.
**Trade System**: Player-to-player trading with item/coin exchange, validation (no-trade, heirloom restrictions), slot management, acceptance workflow, bot trading support, and client version compatibility (6/12 slots).
**Object System**: Interactive world objects extending spawn system with merchants, transporters, devices, and collectors. Features clickable interaction, command handling, device IDs, size randomization, transport/teleport support, and complete spawn integration with entity/item adapters for trade/spell system compatibility.
**Race System**: Complete EverQuest II race management with all 21 races (Human through Aerakyn), alignment classification (good/evil/neutral), racial stat modifiers, starting locations, lore descriptions, and full entity system integration. Features randomization by alignment, compatibility checking, usage statistics, and RaceAware interface for seamless integration with existing systems.
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.
## Code Documentation Standards
**Function Documentation**: All functions must have thorough documentation explaining their purpose, parameters, return values, and any important behavior. Do NOT follow the standard Go convention of starting comments with the function name - instead, write clear explanations of what the function does.
**Examples:**
Good:
```go
// Creates a new UDP server instance with the specified address and packet handler.
// The server will listen on the given address and route incoming packets to the handler.
// Returns an error if the address is invalid or the socket cannot be bound.
func NewServer(addr string, handler PacketHandler, config ...Config) (*Server, error) {
```
Poor (standard Go convention - avoid this):
```go
// NewServer creates a new UDP server instance with the specified address and packet handler.
func NewServer(addr string, handler PacketHandler, config ...Config) (*Server, error) {
```
**Additional Documentation Requirements:**
- Document all public types, constants, and variables
- Include examples in documentation for complex functions
- Explain any non-obvious algorithms or business logic
- Document error conditions and edge cases
- For packet-related code, reference the relevant XML definitions or protocol specifications

199
internal/classes.cpp Normal file
View File

@ -0,0 +1,199 @@
/*
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 "../common/debug.h"
#include "../common/Log.h"
#include "classes.h"
#include "../common/MiscFunctions.h"
#include <algorithm>
Classes::Classes(){
class_map["COMMONER"] = 0;
class_map["FIGHTER"] = 1;
class_map["WARRIOR"] = 2;
class_map["GUARDIAN"] = 3;
class_map["BERSERKER"] = 4;
class_map["BRAWLER"] = 5;
class_map["MONK"] = 6;
class_map["BRUISER"] = 7;
class_map["CRUSADER"] = 8;
class_map["SHADOWKNIGHT"] = 9;
class_map["PALADIN"] = 10;
class_map["PRIEST"] = 11;
class_map["CLERIC"] = 12;
class_map["TEMPLAR"] = 13;
class_map["INQUISITOR"] = 14;
class_map["DRUID"] = 15;
class_map["WARDEN"] = 16;
class_map["FURY"] = 17;
class_map["SHAMAN"] = 18;
class_map["MYSTIC"] = 19;
class_map["DEFILER"] = 20;
class_map["MAGE"] = 21;
class_map["SORCERER"] = 22;
class_map["WIZARD"] = 23;
class_map["WARLOCK"] = 24;
class_map["ENCHANTER"] = 25;
class_map["ILLUSIONIST"] = 26;
class_map["COERCER"] = 27;
class_map["SUMMONER"] = 28;
class_map["CONJUROR"] = 29;
class_map["NECROMANCER"] = 30;
class_map["SCOUT"] = 31;
class_map["ROGUE"] = 32;
class_map["SWASHBUCKLER"] = 33;
class_map["BRIGAND"] = 34;
class_map["BARD"] = 35;
class_map["TROUBADOR"] = 36;
class_map["DIRGE"] = 37;
class_map["PREDATOR"] = 38;
class_map["RANGER"] = 39;
class_map["ASSASSIN"] = 40;
class_map["ANIMALIST"] = 41;
class_map["BEASTLORD"] = 42;
class_map["SHAPER"] = 43;
class_map["CHANNELER"] = 44;
class_map["ARTISAN"] = 45;
class_map["CRAFTSMAN"] = 46;
class_map["PROVISIONER"] = 47;
class_map["WOODWORKER"] = 48;
class_map["CARPENTER"] = 49;
class_map["OUTFITTER"] = 50;
class_map["ARMORER"] = 51;
class_map["WEAPONSMITH"] = 52;
class_map["TAILOR"] = 53;
class_map["SCHOLAR"] = 54;
class_map["JEWELER"] = 55;
class_map["SAGE"] = 56;
class_map["ALCHEMIST"] = 57;
}
int8 Classes::GetBaseClass(int8 class_id) {
int8 ret = 0;
if(class_id>=WARRIOR && class_id <= PALADIN)
ret = FIGHTER;
if((class_id>=CLERIC && class_id <= DEFILER) || (class_id == SHAPER || class_id == CHANNELER))
ret = PRIEST;
if(class_id>=SORCERER && class_id <= NECROMANCER)
ret = MAGE;
if(class_id>=ROGUE && class_id <= BEASTLORD)
ret = SCOUT;
LogWrite(WORLD__DEBUG, 5, "World", "%s returning base class ID: %i", __FUNCTION__, ret);
return ret;
}
int8 Classes::GetSecondaryBaseClass(int8 class_id){
int8 ret = 0;
if(class_id==GUARDIAN || class_id == BERSERKER)
ret = WARRIOR;
if(class_id==MONK || class_id == BRUISER)
ret = BRAWLER;
if(class_id==SHADOWKNIGHT || class_id == PALADIN)
ret = CRUSADER;
if(class_id==TEMPLAR || class_id == INQUISITOR)
ret = CLERIC;
if(class_id==WARDEN || class_id == FURY)
ret = DRUID;
if(class_id==MYSTIC || class_id == DEFILER)
ret = SHAMAN;
if(class_id==WIZARD || class_id == WARLOCK)
ret = SORCERER;
if(class_id==ILLUSIONIST || class_id == COERCER)
ret = ENCHANTER;
if(class_id==CONJUROR || class_id == NECROMANCER)
ret = SUMMONER;
if(class_id==SWASHBUCKLER || class_id == BRIGAND)
ret = ROGUE;
if(class_id==TROUBADOR || class_id == DIRGE)
ret = BARD;
if(class_id==RANGER || class_id == ASSASSIN)
ret = PREDATOR;
if(class_id==BEASTLORD)
ret = ANIMALIST;
if(class_id == CHANNELER)
ret = SHAPER;
LogWrite(WORLD__DEBUG, 5, "World", "%s returning secondary class ID: %i", __FUNCTION__, ret);
return ret;
}
int8 Classes::GetTSBaseClass(int8 class_id) {
int8 ret = 0;
if (class_id + 42 >= ARTISAN)
ret = ARTISAN - 44;
else
ret = class_id;
LogWrite(WORLD__DEBUG, 5, "World", "%s returning base tradeskill class ID: %i", __FUNCTION__, ret);
return ret;
}
int8 Classes::GetSecondaryTSBaseClass(int8 class_id) {
int8 ret = class_id + 42;
if (ret == ARTISAN)
ret = ARTISAN - 44;
else if (ret >= CRAFTSMAN && ret < OUTFITTER)
ret = CRAFTSMAN - 44;
else if (ret >= OUTFITTER && ret < SCHOLAR)
ret = OUTFITTER - 44;
else if (ret >= SCHOLAR)
ret = SCHOLAR - 44;
else
ret = class_id;
LogWrite(WORLD__DEBUG, 5, "World", "%s returning secondary tradeskill class ID: %i", __FUNCTION__, ret);
return ret;
}
sint8 Classes::GetClassID(const char* name){
string class_name = string(name);
class_name = ToUpper(class_name);
if(class_map.count(class_name) == 1) {
LogWrite(WORLD__DEBUG, 5, "World", "%s returning class ID: %i for class name %s", __FUNCTION__, class_map[class_name], class_name.c_str());
return class_map[class_name];
}
LogWrite(WORLD__WARNING, 0, "World", "Could not find class_id in function: %s (return -1)", __FUNCTION__);
return -1;
}
const char* Classes::GetClassName(int8 class_id){
map<string, int8>::iterator itr;
for(itr = class_map.begin(); itr != class_map.end(); itr++){
if(itr->second == class_id) {
LogWrite(WORLD__DEBUG, 5, "World", "%s returning class name: %s for class_id %i", __FUNCTION__, itr->first.c_str(), class_id);
return itr->first.c_str();
}
}
LogWrite(WORLD__WARNING, 0, "World", "Could not find class name in function: %s (return 0)", __FUNCTION__);
return 0;
}
string Classes::GetClassNameCase(int8 class_id) {
map<string, int8>::iterator itr;
for (itr = class_map.begin(); itr != class_map.end(); itr++){
if (itr->second == class_id) {
string class_name = string(itr->first);
transform(itr->first.begin() + 1, itr->first.end(), class_name.begin() + 1, ::tolower);
class_name[0] = ::toupper(class_name[0]);
LogWrite(WORLD__DEBUG, 5, "World", "%s returning class name: %s for class_id %i", __FUNCTION__, class_name.c_str(), class_id);
return class_name;
}
}
LogWrite(WORLD__WARNING, 0, "World", "Could not find class name in function: %s (return blank)", __FUNCTION__);
return "";
}

119
internal/classes.h Normal file
View File

@ -0,0 +1,119 @@
/*
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 CLASSES_CH
#define CLASSES_CH
#include "../common/types.h"
#include <map>
using namespace std;
#define COMMONER 0
#define FIGHTER 1
#define WARRIOR 2
#define GUARDIAN 3
#define BERSERKER 4
#define BRAWLER 5
#define MONK 6
#define BRUISER 7
#define CRUSADER 8
#define SHADOWKNIGHT 9
#define PALADIN 10
#define PRIEST 11
#define CLERIC 12
#define TEMPLAR 13
#define INQUISITOR 14
#define DRUID 15
#define WARDEN 16
#define FURY 17
#define SHAMAN 18
#define MYSTIC 19
#define DEFILER 20
#define MAGE 21
#define SORCERER 22
#define WIZARD 23
#define WARLOCK 24
#define ENCHANTER 25
#define ILLUSIONIST 26
#define COERCER 27
#define SUMMONER 28
#define CONJUROR 29
#define NECROMANCER 30
#define SCOUT 31
#define ROGUE 32
#define SWASHBUCKLER 33
#define BRIGAND 34
#define BARD 35
#define TROUBADOR 36
#define DIRGE 37
#define PREDATOR 38
#define RANGER 39
#define ASSASSIN 40
#define ANIMALIST 41
#define BEASTLORD 42
#define SHAPER 43
#define CHANNELER 44
//Tradeskills
// 0 - transmuting/tinkering
#define ARTISAN 45 // 1
#define CRAFTSMAN 46 // 2
#define PROVISIONER 47 // 3
#define WOODWORKER 48 // 4
#define CARPENTER 49 // 5
#define OUTFITTER 50 // 6
#define ARMORER 51 // 7
#define WEAPONSMITH 52 // 8
#define TAILOR 53 // 9
#define SCHOLAR 54 // 10
#define JEWELER 55 // 11
#define SAGE 56 // 12
#define ALCHEMIST 57 // 13
//43 - artisan
//44 - craftsman
//45 - provisioner
//46 - Woodworker
//47 - carpenter
//48 - armorer
//49 - weaponsmith
//50 - tailor
//51 -
//52 - jeweler
//53 - sage
//54 - alch
#define CLASSIC_MAX_ADVENTURE_CLASS 40 // there is a 41, but its 'scantestbase'
#define CLASSIC_MAX_TRADESKILL_CLASS 13
#define MAX_CLASSES 58
class Classes {
public:
Classes();
char* GetEQClassName(int8 class_, int8 level);
const char* GetClassName(int8 class_id);
string GetClassNameCase(int8 class_id);
sint8 GetClassID(const char* name);
int8 GetBaseClass(int8 class_id);
int8 GetSecondaryBaseClass(int8 class_id);
int8 GetTSBaseClass(int8 class_id);
int8 GetSecondaryTSBaseClass(int8 class_id);
private:
map<string, int8> class_map;
};
#endif

167
internal/entity/README.md Normal file
View File

@ -0,0 +1,167 @@
# Entity Package
The Entity package provides the core combat and magic systems for EverQuest II server emulation. It extends the base Spawn system with combat capabilities, spell effects, and character statistics management.
## Overview
The Entity system is built on three main components:
1. **InfoStruct** - Comprehensive character statistics and information
2. **SpellEffectManager** - Manages all spell effects, buffs, debuffs, and bonuses
3. **Entity** - Combat-capable spawn with spell casting and pet management
## Architecture
```
Spawn (base class)
└── Entity (combat-capable)
├── Player (player characters)
└── NPC (non-player characters)
```
## Core Components
### InfoStruct
Contains all character statistics including:
- Primary attributes (STR, STA, AGI, WIS, INT)
- Combat stats (attack, mitigation, avoidance)
- Resistances (heat, cold, magic, mental, divine, disease, poison)
- Experience points and currency
- Equipment and weapon information
- Group and encounter settings
**Thread Safety**: All access methods use RWMutex for safe concurrent access.
### SpellEffectManager
Manages four types of spell effects:
1. **Maintained Effects** - Buffs that consume concentration
2. **Spell Effects** - Temporary buffs/debuffs with durations
3. **Detrimental Effects** - Debuffs and harmful effects
4. **Bonus Values** - Stat modifications from various sources
**Key Features**:
- Automatic expiration handling
- Control effect tracking (stun, root, mez, etc.)
- Bonus calculations with class/race/faction requirements
- Thread-safe effect management
### Entity
Combat-capable spawn that extends base Spawn functionality:
**Combat Systems**:
- Health/Power/Savagery management
- Combat state tracking (in combat, casting)
- Damage resistance calculations
- Speed and movement modifiers
**Magic Systems**:
- Spell effect application and removal
- Concentration-based maintained spells
- Bonus stat calculations
- Control effect immunity
**Pet Systems**:
- Multiple pet types (summon, charm, deity, cosmetic)
- Pet ownership and dismissal
- Pet spell tracking
## Usage Examples
### Creating an Entity
```go
entity := NewEntity()
entity.GetInfoStruct().SetName("TestEntity")
entity.GetInfoStruct().SetLevel(50)
entity.GetInfoStruct().SetStr(100.0)
```
### Managing Spell Effects
```go
// Add a maintained spell (buff)
success := entity.AddMaintainedSpell("Heroic Strength", 12345, 300.0, 2)
// Add a temporary effect
entity.AddSpellEffect(54321, casterID, 60.0)
// Add a detrimental effect
entity.AddDetrimentalSpell(99999, attackerID, 30.0, 1)
```
### Stat Calculations
```go
// Get effective stats (base + bonuses)
str := entity.GetStr()
sta := entity.GetSta()
primary := entity.GetPrimaryStat()
// Recalculate all bonuses
entity.CalculateBonuses()
```
### Pet Management
```go
// Set a summon pet
entity.SetPet(petEntity)
// Check pet status
if entity.GetPet() != nil && !entity.IsPetDismissing() {
// Pet is active
}
```
## Constants and Enums
### Pet Types
- `PetTypeSummon` - Summoned pets
- `PetTypeCharm` - Charmed creatures
- `PetTypeDeity` - Deity pets
- `PetTypeCosmetic` - Cosmetic pets
### Control Effects
- `ControlEffectStun` - Cannot move or act
- `ControlEffectRoot` - Cannot move
- `ControlEffectMez` - Mesmerized
- `ControlEffectDaze` - Dazed
- `ControlEffectFear` - Feared
- `ControlEffectSlow` - Movement slowed
- `ControlEffectSnare` - Movement impaired
- `ControlEffectCharm` - Mind controlled
## Thread Safety
All Entity operations are thread-safe using:
- `sync.RWMutex` for read/write operations
- `sync.atomic` for simple state flags
- Separate mutexes for different subsystems to minimize lock contention
## Integration with Spawn System
The Entity extends the base Spawn class and requires:
- `spawn.NewSpawn()` for initialization
- Access to Spawn position and basic methods
- Integration with zone update systems
## Future Extensions
Areas marked with TODO comments for future implementation:
- Complete item and equipment systems
- Combat calculation methods
- Threat and hate management
- Group combat mechanics
- Spell casting systems
- LUA script integration
## Files
- `entity.go` - Main Entity class implementation
- `info_struct.go` - Character statistics and information
- `spell_effects.go` - Spell effect management system
- `README.md` - This documentation file

668
internal/entity/entity.go Normal file
View File

@ -0,0 +1,668 @@
package entity
import (
"math"
"sync"
"sync/atomic"
"eq2emu/internal/common"
"eq2emu/internal/spawn"
)
// Combat and pet types
const (
PetTypeSummon = 1
PetTypeCharm = 2
PetTypeDeity = 3
PetTypeCosmetic = 4
)
// Entity represents a combat-capable spawn (NPCs and Players)
// Extends the base Spawn with combat, magic, and equipment systems
type Entity struct {
*spawn.Spawn // Embedded spawn for basic functionality
// Core entity information
infoStruct *InfoStruct // All entity statistics and information
// Combat state
inCombat atomic.Bool // Whether currently in combat
casting atomic.Bool // Whether currently casting
maxSpeed float32 // Maximum movement speed
baseSpeed float32 // Base movement speed
speedMultiplier float32 // Speed multiplier
// Position tracking for movement
lastX float32 // Last known X position
lastY float32 // Last known Y position
lastZ float32 // Last known Z position
lastHeading float32 // Last known heading
// Regeneration
regenHpRate int16 // Health regeneration rate
regenPowerRate int16 // Power regeneration rate
// Spell and effect management
spellEffectManager *SpellEffectManager
// Pet system
pet *Entity // Summon pet
charmedPet *Entity // Charmed pet
deityPet *Entity // Deity pet
cosmeticPet *Entity // Cosmetic pet
owner int32 // Owner entity ID (if this is a pet)
petType int8 // Type of pet
petSpellID int32 // Spell ID used to create/control pet
petSpellTier int8 // Tier of pet spell
petDismissing atomic.Bool // Whether pet is being dismissed
// Group and social
// TODO: Add GroupMemberInfo when group system is implemented
// groupMemberInfo *GroupMemberInfo
// Trading
// TODO: Add Trade when trade system is implemented
// trade *Trade
// Deity and alignment
deity int8 // Deity ID
// Equipment and appearance
features common.CharFeatures // Character appearance features
equipment common.EQ2Equipment // Equipment appearance
// Threat and hate management
threatTransferPercent float32 // Percentage of threat to transfer
// Spell-related flags
hasSeeInvisSpell atomic.Bool // Has see invisible spell
hasSeeHideSpell atomic.Bool // Has see hidden spell
// Proc system
// TODO: Add proc list when implemented
// procList []Proc
// Thread safety
spellEffectMutex sync.RWMutex
maintainedMutex sync.RWMutex
detrimentalMutex sync.RWMutex
commandMutex sync.Mutex
bonusCalculationMutex sync.RWMutex
}
// NewEntity creates a new Entity with default values
// Must be called by subclasses (NPC, Player) for proper initialization
func NewEntity() *Entity {
e := &Entity{
Spawn: spawn.NewSpawn(),
infoStruct: NewInfoStruct(),
maxSpeed: 6.0,
baseSpeed: 0.0,
speedMultiplier: 1.0,
lastX: -1,
lastY: -1,
lastZ: -1,
lastHeading: -1,
regenHpRate: 0,
regenPowerRate: 0,
spellEffectManager: NewSpellEffectManager(),
pet: nil,
charmedPet: nil,
deityPet: nil,
cosmeticPet: nil,
owner: 0,
petType: 0,
petSpellID: 0,
petSpellTier: 0,
deity: 0,
threatTransferPercent: 0.0,
}
// Initialize features and equipment
e.features = common.CharFeatures{}
e.equipment = common.EQ2Equipment{}
// Set initial state
e.inCombat.Store(false)
e.casting.Store(false)
e.petDismissing.Store(false)
e.hasSeeInvisSpell.Store(false)
e.hasSeeHideSpell.Store(false)
return e
}
// IsEntity returns true (implements Spawn interface)
func (e *Entity) IsEntity() bool {
return true
}
// GetInfoStruct returns the entity's info structure
func (e *Entity) GetInfoStruct() *InfoStruct {
return e.infoStruct
}
// SetInfoStruct updates the entity's info structure
func (e *Entity) SetInfoStruct(info *InfoStruct) {
if info != nil {
e.infoStruct = info
}
}
// GetClient returns the client for this entity (overridden by Player)
func (e *Entity) GetClient() interface{} {
return nil
}
// Combat state methods
// IsInCombat returns whether the entity is currently in combat
func (e *Entity) IsInCombat() bool {
return e.inCombat.Load()
}
// SetInCombat updates the combat state
func (e *Entity) SetInCombat(inCombat bool) {
e.inCombat.Store(inCombat)
}
// IsCasting returns whether the entity is currently casting
func (e *Entity) IsCasting() bool {
return e.casting.Load()
}
// SetCasting updates the casting state
func (e *Entity) SetCasting(casting bool) {
e.casting.Store(casting)
}
// Speed and movement methods
// GetMaxSpeed returns the maximum movement speed
func (e *Entity) GetMaxSpeed() float32 {
return e.maxSpeed
}
// SetMaxSpeed updates the maximum movement speed
func (e *Entity) SetMaxSpeed(speed float32) {
e.maxSpeed = speed
}
// GetBaseSpeed returns the base movement speed
func (e *Entity) GetBaseSpeed() float32 {
return e.baseSpeed
}
// SetBaseSpeed updates the base movement speed
func (e *Entity) SetBaseSpeed(speed float32) {
e.baseSpeed = speed
}
// GetSpeedMultiplier returns the speed multiplier
func (e *Entity) GetSpeedMultiplier() float32 {
return e.speedMultiplier
}
// SetSpeedMultiplier updates the speed multiplier
func (e *Entity) SetSpeedMultiplier(multiplier float32) {
e.speedMultiplier = multiplier
}
// CalculateEffectiveSpeed calculates the current effective speed
func (e *Entity) CalculateEffectiveSpeed() float32 {
baseSpeed := e.baseSpeed
if baseSpeed <= 0 {
baseSpeed = e.maxSpeed
}
return baseSpeed * e.speedMultiplier
}
// Position tracking methods
// GetLastPosition returns the last known position
func (e *Entity) GetLastPosition() (float32, float32, float32, float32) {
return e.lastX, e.lastY, e.lastZ, e.lastHeading
}
// SetLastPosition updates the last known position
func (e *Entity) SetLastPosition(x, y, z, heading float32) {
e.lastX = x
e.lastY = y
e.lastZ = z
e.lastHeading = heading
}
// HasMoved checks if the entity has moved since last position update
func (e *Entity) HasMoved() bool {
currentX := e.GetX()
currentY := e.GetY()
currentZ := e.GetZ()
currentHeading := e.GetHeading()
const epsilon = 0.01 // Small threshold for floating point comparison
return math.Abs(float64(currentX-e.lastX)) > epsilon ||
math.Abs(float64(currentY-e.lastY)) > epsilon ||
math.Abs(float64(currentZ-e.lastZ)) > epsilon ||
math.Abs(float64(currentHeading-e.lastHeading)) > epsilon
}
// Stat calculation methods
// GetStr returns the effective strength stat
func (e *Entity) GetStr() int16 {
base := int16(e.infoStruct.GetStr())
bonus := int16(e.spellEffectManager.GetBonusValue(1, 0, e.infoStruct.GetRace(), 0)) // Stat type 1 = STR
return base + bonus
}
// GetSta returns the effective stamina stat
func (e *Entity) GetSta() int16 {
base := int16(e.infoStruct.GetSta())
bonus := int16(e.spellEffectManager.GetBonusValue(2, 0, e.infoStruct.GetRace(), 0)) // Stat type 2 = STA
return base + bonus
}
// GetAgi returns the effective agility stat
func (e *Entity) GetAgi() int16 {
base := int16(e.infoStruct.GetAgi())
bonus := int16(e.spellEffectManager.GetBonusValue(3, 0, e.infoStruct.GetRace(), 0)) // Stat type 3 = AGI
return base + bonus
}
// GetWis returns the effective wisdom stat
func (e *Entity) GetWis() int16 {
base := int16(e.infoStruct.GetWis())
bonus := int16(e.spellEffectManager.GetBonusValue(4, 0, e.infoStruct.GetRace(), 0)) // Stat type 4 = WIS
return base + bonus
}
// GetIntel returns the effective intelligence stat
func (e *Entity) GetIntel() int16 {
base := int16(e.infoStruct.GetIntel())
bonus := int16(e.spellEffectManager.GetBonusValue(5, 0, e.infoStruct.GetRace(), 0)) // Stat type 5 = INT
return base + bonus
}
// GetPrimaryStat returns the highest primary stat value
func (e *Entity) GetPrimaryStat() int16 {
str := e.GetStr()
sta := e.GetSta()
agi := e.GetAgi()
wis := e.GetWis()
intel := e.GetIntel()
primary := str
if sta > primary {
primary = sta
}
if agi > primary {
primary = agi
}
if wis > primary {
primary = wis
}
if intel > primary {
primary = intel
}
return primary
}
// Resistance methods
// GetHeatResistance returns heat resistance
func (e *Entity) GetHeatResistance() int16 {
return e.infoStruct.GetResistance("heat")
}
// GetColdResistance returns cold resistance
func (e *Entity) GetColdResistance() int16 {
return e.infoStruct.GetResistance("cold")
}
// GetMagicResistance returns magic resistance
func (e *Entity) GetMagicResistance() int16 {
return e.infoStruct.GetResistance("magic")
}
// GetMentalResistance returns mental resistance
func (e *Entity) GetMentalResistance() int16 {
return e.infoStruct.GetResistance("mental")
}
// GetDivineResistance returns divine resistance
func (e *Entity) GetDivineResistance() int16 {
return e.infoStruct.GetResistance("divine")
}
// GetDiseaseResistance returns disease resistance
func (e *Entity) GetDiseaseResistance() int16 {
return e.infoStruct.GetResistance("disease")
}
// GetPoisonResistance returns poison resistance
func (e *Entity) GetPoisonResistance() int16 {
return e.infoStruct.GetResistance("poison")
}
// Spell effect management methods
// AddMaintainedSpell adds a maintained spell effect
func (e *Entity) AddMaintainedSpell(name string, spellID int32, duration float32, concentration int8) bool {
// Check if we have enough concentration
if !e.infoStruct.AddConcentration(int16(concentration)) {
return false
}
effect := NewMaintainedEffects(name, spellID, duration)
effect.ConcUsed = concentration
e.maintainedMutex.Lock()
defer e.maintainedMutex.Unlock()
if !e.spellEffectManager.AddMaintainedEffect(effect) {
// Failed to add, return concentration
e.infoStruct.RemoveConcentration(int16(concentration))
return false
}
return true
}
// RemoveMaintainedSpell removes a maintained spell effect
func (e *Entity) RemoveMaintainedSpell(spellID int32) bool {
e.maintainedMutex.Lock()
defer e.maintainedMutex.Unlock()
// Get the effect to check concentration usage
effect := e.spellEffectManager.GetMaintainedEffect(spellID)
if effect == nil {
return false
}
// Return concentration
e.infoStruct.RemoveConcentration(int16(effect.ConcUsed))
return e.spellEffectManager.RemoveMaintainedEffect(spellID)
}
// GetMaintainedSpell retrieves a maintained spell effect
func (e *Entity) GetMaintainedSpell(spellID int32) *MaintainedEffects {
e.maintainedMutex.RLock()
defer e.maintainedMutex.RUnlock()
return e.spellEffectManager.GetMaintainedEffect(spellID)
}
// AddSpellEffect adds a temporary spell effect
func (e *Entity) AddSpellEffect(spellID int32, casterID int32, duration float32) bool {
effect := NewSpellEffects(spellID, casterID, duration)
e.spellEffectMutex.Lock()
defer e.spellEffectMutex.Unlock()
return e.spellEffectManager.AddSpellEffect(effect)
}
// RemoveSpellEffect removes a spell effect
func (e *Entity) RemoveSpellEffect(spellID int32) bool {
e.spellEffectMutex.Lock()
defer e.spellEffectMutex.Unlock()
return e.spellEffectManager.RemoveSpellEffect(spellID)
}
// AddDetrimentalSpell adds a detrimental effect
func (e *Entity) AddDetrimentalSpell(spellID int32, casterID int32, duration float32, detType int8) {
effect := NewDetrimentalEffects(spellID, casterID, duration)
effect.DetType = detType
e.detrimentalMutex.Lock()
defer e.detrimentalMutex.Unlock()
e.spellEffectManager.AddDetrimentalEffect(*effect)
}
// RemoveDetrimentalSpell removes a detrimental effect
func (e *Entity) RemoveDetrimentalSpell(spellID int32, casterID int32) bool {
e.detrimentalMutex.Lock()
defer e.detrimentalMutex.Unlock()
return e.spellEffectManager.RemoveDetrimentalEffect(spellID, casterID)
}
// GetDetrimentalEffect retrieves a detrimental effect
func (e *Entity) GetDetrimentalEffect(spellID int32, casterID int32) *DetrimentalEffects {
e.detrimentalMutex.RLock()
defer e.detrimentalMutex.RUnlock()
return e.spellEffectManager.GetDetrimentalEffect(spellID, casterID)
}
// HasControlEffect checks if the entity has a specific control effect
func (e *Entity) HasControlEffect(controlType int8) bool {
return e.spellEffectManager.HasControlEffect(controlType)
}
// Bonus system methods
// AddSkillBonus adds a skill-related bonus
func (e *Entity) AddSkillBonus(spellID int32, skillID int32, value float32) {
bonus := NewBonusValues(spellID, int16(skillID+100), value) // Skill bonuses use type 100+
e.spellEffectManager.AddBonus(bonus)
}
// AddStatBonus adds a stat bonus
func (e *Entity) AddStatBonus(spellID int32, statType int16, value float32) {
bonus := NewBonusValues(spellID, statType, value)
e.spellEffectManager.AddBonus(bonus)
}
// CalculateBonuses recalculates all stat bonuses and updates InfoStruct
func (e *Entity) CalculateBonuses() {
e.bonusCalculationMutex.Lock()
defer e.bonusCalculationMutex.Unlock()
// Reset effects to base values
e.infoStruct.ResetEffects()
entityClass := int64(1 << e.infoStruct.GetClass1()) // Convert class to bitmask
race := e.infoStruct.GetRace()
factionID := int32(e.GetFactionID())
// Apply stat bonuses
strBonus := e.spellEffectManager.GetBonusValue(1, entityClass, race, factionID)
staBonus := e.spellEffectManager.GetBonusValue(2, entityClass, race, factionID)
agiBonus := e.spellEffectManager.GetBonusValue(3, entityClass, race, factionID)
wisBonus := e.spellEffectManager.GetBonusValue(4, entityClass, race, factionID)
intelBonus := e.spellEffectManager.GetBonusValue(5, entityClass, race, factionID)
// Update InfoStruct with bonuses
e.infoStruct.SetStr(e.infoStruct.GetStr() + strBonus)
e.infoStruct.SetSta(e.infoStruct.GetSta() + staBonus)
e.infoStruct.SetAgi(e.infoStruct.GetAgi() + agiBonus)
e.infoStruct.SetWis(e.infoStruct.GetWis() + wisBonus)
e.infoStruct.SetIntel(e.infoStruct.GetIntel() + intelBonus)
// Apply resistance bonuses
heatBonus := int16(e.spellEffectManager.GetBonusValue(10, entityClass, race, factionID))
coldBonus := int16(e.spellEffectManager.GetBonusValue(11, entityClass, race, factionID))
magicBonus := int16(e.spellEffectManager.GetBonusValue(12, entityClass, race, factionID))
mentalBonus := int16(e.spellEffectManager.GetBonusValue(13, entityClass, race, factionID))
divineBonus := int16(e.spellEffectManager.GetBonusValue(14, entityClass, race, factionID))
diseaseBonus := int16(e.spellEffectManager.GetBonusValue(15, entityClass, race, factionID))
poisonBonus := int16(e.spellEffectManager.GetBonusValue(16, entityClass, race, factionID))
e.infoStruct.SetResistance("heat", e.infoStruct.GetResistance("heat")+heatBonus)
e.infoStruct.SetResistance("cold", e.infoStruct.GetResistance("cold")+coldBonus)
e.infoStruct.SetResistance("magic", e.infoStruct.GetResistance("magic")+magicBonus)
e.infoStruct.SetResistance("mental", e.infoStruct.GetResistance("mental")+mentalBonus)
e.infoStruct.SetResistance("divine", e.infoStruct.GetResistance("divine")+divineBonus)
e.infoStruct.SetResistance("disease", e.infoStruct.GetResistance("disease")+diseaseBonus)
e.infoStruct.SetResistance("poison", e.infoStruct.GetResistance("poison")+poisonBonus)
}
// Pet management methods
// GetPet returns the summon pet
func (e *Entity) GetPet() *Entity {
return e.pet
}
// SetPet sets the summon pet
func (e *Entity) SetPet(pet *Entity) {
e.pet = pet
if pet != nil {
pet.owner = e.GetID()
pet.petType = PetTypeSummon
}
}
// GetCharmedPet returns the charmed pet
func (e *Entity) GetCharmedPet() *Entity {
return e.charmedPet
}
// SetCharmedPet sets the charmed pet
func (e *Entity) SetCharmedPet(pet *Entity) {
e.charmedPet = pet
if pet != nil {
pet.owner = e.GetID()
pet.petType = PetTypeCharm
}
}
// GetDeityPet returns the deity pet
func (e *Entity) GetDeityPet() *Entity {
return e.deityPet
}
// SetDeityPet sets the deity pet
func (e *Entity) SetDeityPet(pet *Entity) {
e.deityPet = pet
if pet != nil {
pet.owner = e.GetID()
pet.petType = PetTypeDeity
}
}
// GetCosmeticPet returns the cosmetic pet
func (e *Entity) GetCosmeticPet() *Entity {
return e.cosmeticPet
}
// SetCosmeticPet sets the cosmetic pet
func (e *Entity) SetCosmeticPet(pet *Entity) {
e.cosmeticPet = pet
if pet != nil {
pet.owner = e.GetID()
pet.petType = PetTypeCosmetic
}
}
// GetOwner returns the owner entity ID (if this is a pet)
func (e *Entity) GetOwner() int32 {
return e.owner
}
// GetPetType returns the type of pet this entity is
func (e *Entity) GetPetType() int8 {
return e.petType
}
// IsPetDismissing returns whether the pet is being dismissed
func (e *Entity) IsPetDismissing() bool {
return e.petDismissing.Load()
}
// SetPetDismissing sets whether the pet is being dismissed
func (e *Entity) SetPetDismissing(dismissing bool) {
e.petDismissing.Store(dismissing)
}
// Utility methods
// GetDeity returns the deity ID
func (e *Entity) GetDeity() int8 {
return e.deity
}
// SetDeity updates the deity ID
func (e *Entity) SetDeity(deity int8) {
e.deity = deity
}
// GetDodgeChance calculates the dodge chance (to be overridden by subclasses)
func (e *Entity) GetDodgeChance() float32 {
// Base implementation, should be overridden
return 5.0 // 5% base dodge chance
}
// HasSeeInvisSpell returns whether the entity has see invisible
func (e *Entity) HasSeeInvisSpell() bool {
return e.hasSeeInvisSpell.Load()
}
// SetSeeInvisSpell updates the see invisible state
func (e *Entity) SetSeeInvisSpell(hasSpell bool) {
e.hasSeeInvisSpell.Store(hasSpell)
}
// HasSeeHideSpell returns whether the entity has see hidden
func (e *Entity) HasSeeHideSpell() bool {
return e.hasSeeHideSpell.Load()
}
// SetSeeHideSpell updates the see hidden state
func (e *Entity) SetSeeHideSpell(hasSpell bool) {
e.hasSeeHideSpell.Store(hasSpell)
}
// Cleanup methods
// DeleteSpellEffects removes all spell effects
func (e *Entity) DeleteSpellEffects(removeClient bool) {
e.spellEffectMutex.Lock()
e.maintainedMutex.Lock()
e.detrimentalMutex.Lock()
defer e.spellEffectMutex.Unlock()
defer e.maintainedMutex.Unlock()
defer e.detrimentalMutex.Unlock()
// Clear all maintained effects and return concentration
effects := e.spellEffectManager.GetAllMaintainedEffects()
for _, effect := range effects {
e.infoStruct.RemoveConcentration(int16(effect.ConcUsed))
}
e.spellEffectManager.ClearAllEffects()
}
// RemoveSpells removes spell effects, optionally only unfriendly ones
func (e *Entity) RemoveSpells(unfriendlyOnly bool) {
// TODO: Implement when we can determine friendly vs unfriendly spells
if !unfriendlyOnly {
e.DeleteSpellEffects(false)
}
}
// Update methods
// ProcessEffects handles periodic effect processing
func (e *Entity) ProcessEffects() {
// Clean up expired effects
e.spellEffectManager.CleanupExpiredEffects()
// TODO: Apply periodic effect damage/healing
// TODO: Handle effect-based stat changes
}
// TODO: Additional methods to implement:
// - Combat calculation methods (damage, healing, etc.)
// - Equipment bonus application methods
// - Spell casting methods
// - Threat and hate management methods
// - Group combat methods
// - Many more combat and magic related methods

View File

@ -0,0 +1,854 @@
package entity
import (
"sync"
"time"
)
// RaceAlignment represents character alignment types
type RaceAlignment int
const (
AlignmentEvil RaceAlignment = 0
AlignmentGood RaceAlignment = 1
)
// InfoStruct contains all character statistics and information
// This is a comprehensive structure that manages all entity stats,
// equipment bonuses, and various game mechanics data
type InfoStruct struct {
// Basic character information
name string
class1 int8 // Primary class
class2 int8 // Secondary class
class3 int8 // Tertiary class
race int8 // Character race
gender int8 // Character gender
level int16 // Current level
maxLevel int16 // Maximum achievable level
effectiveLevel int16 // Effective level for calculations
tradeskillLevel int16 // Tradeskill level
tradeskillMaxLevel int16 // Maximum tradeskill level
// Concentration system (for maintaining spells)
curConcentration int16 // Current concentration used
maxConcentration int16 // Maximum concentration available
// Combat statistics
curAttack int32 // Current attack rating
attackBase int32 // Base attack rating
curMitigation int32 // Current mitigation (damage reduction)
maxMitigation int32 // Maximum mitigation
mitigationBase int32 // Base mitigation
mitigationMod float32 // Mitigation modifier
// Avoidance statistics
avoidanceDisplay int16 // Display value for avoidance
curAvoidance float32 // Current avoidance percentage
baseAvoidancePct int16 // Base avoidance percentage
avoidanceBase int16 // Base avoidance rating
maxAvoidance int16 // Maximum avoidance
parry float32 // Parry chance
parryBase float32 // Base parry
deflection int16 // Deflection rating
deflectionBase int16 // Base deflection
block int16 // Block rating
blockBase int16 // Base block
// Primary attributes
str float32 // Strength
sta float32 // Stamina
agi float32 // Agility
wis float32 // Wisdom
intel float32 // Intelligence
strBase float32 // Base strength
staBase float32 // Base stamina
agiBase float32 // Base agility
wisBase float32 // Base wisdom
intelBase float32 // Base intelligence
// Resistances
heat int16 // Heat resistance
cold int16 // Cold resistance
magic int16 // Magic resistance
mental int16 // Mental resistance
divine int16 // Divine resistance
disease int16 // Disease resistance
poison int16 // Poison resistance
heatBase int16 // Base heat resistance
coldBase int16 // Base cold resistance
magicBase int16 // Base magic resistance
mentalBase int16 // Base mental resistance
divineBase int16 // Base divine resistance
diseaseBase int16 // Base disease resistance
poisonBase int16 // Base poison resistance
elementalBase int16 // Base elemental resistance
noxiousBase int16 // Base noxious resistance
arcaneBase int16 // Base arcane resistance
// Currency
coinCopper int32 // Copper coins
coinSilver int32 // Silver coins
coinGold int32 // Gold coins
coinPlat int32 // Platinum coins
bankCoinCopper int32 // Banked copper
bankCoinSilver int32 // Banked silver
bankCoinGold int32 // Banked gold
bankCoinPlat int32 // Banked platinum
statusPoints int32 // Status points
// Character details
deity string // Deity name
weight int32 // Current weight
maxWeight int32 // Maximum carrying capacity
// Tradeskill classes
tradeskillClass1 int8 // Primary tradeskill class
tradeskillClass2 int8 // Secondary tradeskill class
tradeskillClass3 int8 // Tertiary tradeskill class
// Account and character age
accountAgeBase int16 // Base account age
accountAgeBonus [19]int8 // Account age bonuses
// Combat and damage
absorb int32 // Damage absorption
// Experience points
xp int32 // Current experience
xpNeeded int32 // Experience needed for next level
xpDebt float32 // Experience debt
xpYellow int32 // Yellow (bonus) experience
xpYellowVitalityBar int32 // Yellow vitality bar
xpBlueVitalityBar int32 // Blue vitality bar
xpBlue int32 // Blue (rested) experience
tsXp int32 // Tradeskill experience
tsXpNeeded int32 // Tradeskill experience needed
tradeskillExpYellow int32 // Tradeskill yellow experience
tradeskillExpBlue int32 // Tradeskill blue experience
xpVitality float32 // Experience vitality
tradeskillXpVitality float32 // Tradeskill experience vitality
// Flags and states
flags int32 // General flags
flags2 int32 // Additional flags
// Specialized mitigation
mitigationSkill1 int16 // Mitigation skill 1
mitigationSkill2 int16 // Mitigation skill 2
mitigationSkill3 int16 // Mitigation skill 3
mitigationPve int16 // PvE mitigation
mitigationPvp int16 // PvP mitigation
// Combat modifiers
abilityModifier int16 // Ability modifier
criticalMitigation int16 // Critical hit mitigation
blockChance int16 // Block chance
uncontestedParry int16 // Uncontested parry
uncontestedBlock int16 // Uncontested block
uncontestedDodge int16 // Uncontested dodge
uncontestedRiposte int16 // Uncontested riposte
critChance int16 // Critical hit chance
critBonus int16 // Critical hit bonus
potency int16 // Spell potency
hateMod int16 // Hate modifier
reuseSpeed int16 // Ability reuse speed
castingSpeed int16 // Spell casting speed
recoverySpeed int16 // Recovery speed
spellReuseSpeed int16 // Spell reuse speed
spellMultiAttack int16 // Spell multi-attack
// Size and physical attributes
sizeMod float32 // Size modifier
ignoreSizeModCalc int8 // Ignore size modifier calculations
dps int16 // Damage per second
dpsMultiplier int16 // DPS multiplier
attackSpeed int16 // Attack speed
haste int16 // Haste
multiAttack int16 // Multi-attack chance
flurry int16 // Flurry chance
meleeAe int16 // Melee area effect
strikethrough int16 // Strikethrough
accuracy int16 // Accuracy
offensiveSpeed int16 // Offensive speed
// Environmental
rain int16 // Rain resistance/affinity
wind int16 // Wind resistance/affinity
alignment int8 // Character alignment
// Pet information
petId int32 // Pet ID
petName string // Pet name
petHealthPct float32 // Pet health percentage
petPowerPct float32 // Pet power percentage
petMovement int8 // Pet movement mode
petBehavior int8 // Pet behavior mode
// Character abilities
vision int8 // Vision type
breatheUnderwater int8 // Can breathe underwater
biography string // Character biography
drunk int8 // Drunk level
// Regeneration
powerRegen int16 // Power regeneration rate
hpRegen int16 // Health regeneration rate
powerRegenOverride int16 // Power regen override
hpRegenOverride int16 // Health regen override
// Movement types
waterType int8 // Water movement type
flyingType int8 // Flying movement type
// Special flags
noInterrupt int8 // Cannot be interrupted
interactionFlag int8 // Interaction flag
tag1 int8 // Tag 1
mood int8 // Mood state
// Weapon timing
rangeLastAttackTime int32 // Last ranged attack time
primaryLastAttackTime int32 // Last primary attack time
secondaryLastAttackTime int32 // Last secondary attack time
primaryAttackDelay int32 // Primary attack delay
secondaryAttackDelay int32 // Secondary attack delay
rangedAttackDelay int32 // Ranged attack delay
// Weapon information
primaryWeaponType int8 // Primary weapon type
secondaryWeaponType int8 // Secondary weapon type
rangedWeaponType int8 // Ranged weapon type
primaryWeaponDmgLow int16 // Primary weapon min damage
primaryWeaponDmgHigh int16 // Primary weapon max damage
secondaryWeaponDmgLow int16 // Secondary weapon min damage
secondaryWeaponDmgHigh int16 // Secondary weapon max damage
rangedWeaponDmgLow int16 // Ranged weapon min damage
rangedWeaponDmgHigh int16 // Ranged weapon max damage
wieldType int8 // Weapon wield type
attackType int8 // Attack type
primaryWeaponDelay int16 // Primary weapon delay
secondaryWeaponDelay int16 // Secondary weapon delay
rangedWeaponDelay int16 // Ranged weapon delay
// Weapon overrides
overridePrimaryWeapon int8 // Override primary weapon
overrideSecondaryWeapon int8 // Override secondary weapon
overrideRangedWeapon int8 // Override ranged weapon
// NPC specific
friendlyTargetNpc int8 // Friendly target NPC flag
lastClaimTime int32 // Last claim time
// Encounter system
engagedEncounter int8 // Engaged in encounter
lockableEncounter int8 // Encounter can be locked
// Player flags
firstWorldLogin int8 // First world login flag
reloadPlayerSpells int8 // Reload player spells flag
// Group settings
groupLootMethod int8 // Group loot method
groupLootItemsRarity int8 // Group loot rarity threshold
groupAutoSplit int8 // Auto-split loot
groupDefaultYell int8 // Default yell in group
groupAutolock int8 // Auto-lock group
groupLockMethod int8 // Group lock method
groupSoloAutolock int8 // Solo auto-lock
groupAutoLootMethod int8 // Auto-loot method
assistAutoAttack int8 // Assist auto-attack
// Action state
actionState string // Current action state
// Spell and ability reductions
spellMulticastChance int16 // Spell multicast chance
maxSpellReductionOverride int16 // Max spell reduction override
maxChaseDistance float32 // Maximum chase distance
// Thread safety
mutex sync.RWMutex
}
// NewInfoStruct creates a new InfoStruct with default values
// Initializes all fields to appropriate defaults for a new entity
func NewInfoStruct() *InfoStruct {
return &InfoStruct{
name: "",
class1: 0,
class2: 0,
class3: 0,
race: 0,
gender: 0,
level: 0,
maxLevel: 0,
effectiveLevel: 0,
tradeskillLevel: 0,
tradeskillMaxLevel: 0,
curConcentration: 0,
maxConcentration: 5, // Default max concentration
curAttack: 0,
attackBase: 0,
curMitigation: 0,
maxMitigation: 0,
mitigationBase: 0,
mitigationMod: 0,
avoidanceDisplay: 0,
curAvoidance: 0.0,
baseAvoidancePct: 0,
avoidanceBase: 0,
maxAvoidance: 0,
parry: 0.0,
parryBase: 0.0,
deflection: 0,
deflectionBase: 0,
block: 0,
blockBase: 0,
str: 0.0,
sta: 0.0,
agi: 0.0,
wis: 0.0,
intel: 0.0,
strBase: 0.0,
staBase: 0.0,
agiBase: 0.0,
wisBase: 0.0,
intelBase: 0.0,
heat: 0,
cold: 0,
magic: 0,
mental: 0,
divine: 0,
disease: 0,
poison: 0,
heatBase: 0,
coldBase: 0,
magicBase: 0,
mentalBase: 0,
divineBase: 0,
diseaseBase: 0,
poisonBase: 0,
elementalBase: 0,
noxiousBase: 0,
arcaneBase: 0,
coinCopper: 0,
coinSilver: 0,
coinGold: 0,
coinPlat: 0,
bankCoinCopper: 0,
bankCoinSilver: 0,
bankCoinGold: 0,
bankCoinPlat: 0,
statusPoints: 0,
deity: "",
weight: 0,
maxWeight: 0,
tradeskillClass1: 0,
tradeskillClass2: 0,
tradeskillClass3: 0,
accountAgeBase: 0,
absorb: 0,
xp: 0,
xpNeeded: 0,
xpDebt: 0.0,
xpYellow: 0,
xpYellowVitalityBar: 0,
xpBlueVitalityBar: 0,
xpBlue: 0,
tsXp: 0,
tsXpNeeded: 0,
tradeskillExpYellow: 0,
tradeskillExpBlue: 0,
flags: 0,
flags2: 0,
xpVitality: 0,
tradeskillXpVitality: 0,
mitigationSkill1: 0,
mitigationSkill2: 0,
mitigationSkill3: 0,
mitigationPve: 0,
mitigationPvp: 0,
abilityModifier: 0,
criticalMitigation: 0,
blockChance: 0,
uncontestedParry: 0,
uncontestedBlock: 0,
uncontestedDodge: 0,
uncontestedRiposte: 0,
critChance: 0,
critBonus: 0,
potency: 0,
hateMod: 0,
reuseSpeed: 0,
castingSpeed: 0,
recoverySpeed: 0,
spellReuseSpeed: 0,
spellMultiAttack: 0,
sizeMod: 0.0,
ignoreSizeModCalc: 0,
dps: 0,
dpsMultiplier: 0,
attackSpeed: 0,
haste: 0,
multiAttack: 0,
flurry: 0,
meleeAe: 0,
strikethrough: 0,
accuracy: 0,
offensiveSpeed: 0,
rain: 0,
wind: 0,
alignment: 0,
petId: 0,
petName: "",
petHealthPct: 0.0,
petPowerPct: 0.0,
petMovement: 0,
petBehavior: 0,
vision: 0,
breatheUnderwater: 0,
biography: "",
drunk: 0,
powerRegen: 0,
hpRegen: 0,
powerRegenOverride: 0,
hpRegenOverride: 0,
waterType: 0,
flyingType: 0,
noInterrupt: 0,
interactionFlag: 0,
tag1: 0,
mood: 0,
rangeLastAttackTime: 0,
primaryLastAttackTime: 0,
secondaryLastAttackTime: 0,
primaryAttackDelay: 0,
secondaryAttackDelay: 0,
rangedAttackDelay: 0,
primaryWeaponType: 0,
secondaryWeaponType: 0,
rangedWeaponType: 0,
primaryWeaponDmgLow: 0,
primaryWeaponDmgHigh: 0,
secondaryWeaponDmgLow: 0,
secondaryWeaponDmgHigh: 0,
rangedWeaponDmgLow: 0,
rangedWeaponDmgHigh: 0,
wieldType: 0,
attackType: 0,
primaryWeaponDelay: 0,
secondaryWeaponDelay: 0,
rangedWeaponDelay: 0,
overridePrimaryWeapon: 0,
overrideSecondaryWeapon: 0,
overrideRangedWeapon: 0,
friendlyTargetNpc: 0,
lastClaimTime: 0,
engagedEncounter: 0,
lockableEncounter: 1,
firstWorldLogin: 0,
reloadPlayerSpells: 0,
groupLootMethod: 1,
groupLootItemsRarity: 0,
groupAutoSplit: 1,
groupDefaultYell: 1,
groupAutolock: 0,
groupLockMethod: 0,
groupSoloAutolock: 0,
groupAutoLootMethod: 0,
assistAutoAttack: 0,
actionState: "",
spellMulticastChance: 0,
maxSpellReductionOverride: 0,
maxChaseDistance: 0.0,
}
}
// GetName returns the entity's name
func (info *InfoStruct) GetName() string {
info.mutex.RLock()
defer info.mutex.RUnlock()
return info.name
}
// SetName updates the entity's name
func (info *InfoStruct) SetName(name string) {
info.mutex.Lock()
defer info.mutex.Unlock()
info.name = name
}
// GetLevel returns the entity's level
func (info *InfoStruct) GetLevel() int16 {
info.mutex.RLock()
defer info.mutex.RUnlock()
return info.level
}
// SetLevel updates the entity's level
func (info *InfoStruct) SetLevel(level int16) {
info.mutex.Lock()
defer info.mutex.Unlock()
info.level = level
}
// GetEffectiveLevel returns the entity's effective level for calculations
func (info *InfoStruct) GetEffectiveLevel() int16 {
info.mutex.RLock()
defer info.mutex.RUnlock()
return info.effectiveLevel
}
// SetEffectiveLevel updates the entity's effective level
func (info *InfoStruct) SetEffectiveLevel(level int16) {
info.mutex.Lock()
defer info.mutex.Unlock()
info.effectiveLevel = level
}
// GetClass1 returns the primary class
func (info *InfoStruct) GetClass1() int8 {
info.mutex.RLock()
defer info.mutex.RUnlock()
return info.class1
}
// SetClass1 updates the primary class
func (info *InfoStruct) SetClass1(class int8) {
info.mutex.Lock()
defer info.mutex.Unlock()
info.class1 = class
}
// GetRace returns the entity's race
func (info *InfoStruct) GetRace() int8 {
info.mutex.RLock()
defer info.mutex.RUnlock()
return info.race
}
// SetRace updates the entity's race
func (info *InfoStruct) SetRace(race int8) {
info.mutex.Lock()
defer info.mutex.Unlock()
info.race = race
}
// GetGender returns the entity's gender
func (info *InfoStruct) GetGender() int8 {
info.mutex.RLock()
defer info.mutex.RUnlock()
return info.gender
}
// SetGender updates the entity's gender
func (info *InfoStruct) SetGender(gender int8) {
info.mutex.Lock()
defer info.mutex.Unlock()
info.gender = gender
}
// GetStr returns the strength stat
func (info *InfoStruct) GetStr() float32 {
info.mutex.RLock()
defer info.mutex.RUnlock()
return info.str
}
// SetStr updates the strength stat
func (info *InfoStruct) SetStr(str float32) {
info.mutex.Lock()
defer info.mutex.Unlock()
info.str = str
}
// GetSta returns the stamina stat
func (info *InfoStruct) GetSta() float32 {
info.mutex.RLock()
defer info.mutex.RUnlock()
return info.sta
}
// SetSta updates the stamina stat
func (info *InfoStruct) SetSta(sta float32) {
info.mutex.Lock()
defer info.mutex.Unlock()
info.sta = sta
}
// GetAgi returns the agility stat
func (info *InfoStruct) GetAgi() float32 {
info.mutex.RLock()
defer info.mutex.RUnlock()
return info.agi
}
// SetAgi updates the agility stat
func (info *InfoStruct) SetAgi(agi float32) {
info.mutex.Lock()
defer info.mutex.Unlock()
info.agi = agi
}
// GetWis returns the wisdom stat
func (info *InfoStruct) GetWis() float32 {
info.mutex.RLock()
defer info.mutex.RUnlock()
return info.wis
}
// SetWis updates the wisdom stat
func (info *InfoStruct) SetWis(wis float32) {
info.mutex.Lock()
defer info.mutex.Unlock()
info.wis = wis
}
// GetIntel returns the intelligence stat
func (info *InfoStruct) GetIntel() float32 {
info.mutex.RLock()
defer info.mutex.RUnlock()
return info.intel
}
// SetIntel updates the intelligence stat
func (info *InfoStruct) SetIntel(intel float32) {
info.mutex.Lock()
defer info.mutex.Unlock()
info.intel = intel
}
// GetMaxConcentration returns the maximum concentration
func (info *InfoStruct) GetMaxConcentration() int16 {
info.mutex.RLock()
defer info.mutex.RUnlock()
return info.maxConcentration
}
// SetMaxConcentration updates the maximum concentration
func (info *InfoStruct) SetMaxConcentration(maxConcentration int16) {
info.mutex.Lock()
defer info.mutex.Unlock()
info.maxConcentration = maxConcentration
}
// GetCurConcentration returns the current concentration used
func (info *InfoStruct) GetCurConcentration() int16 {
info.mutex.RLock()
defer info.mutex.RUnlock()
return info.curConcentration
}
// SetCurConcentration updates the current concentration used
func (info *InfoStruct) SetCurConcentration(curConcentration int16) {
info.mutex.Lock()
defer info.mutex.Unlock()
info.curConcentration = curConcentration
}
// AddConcentration adds to the current concentration used
func (info *InfoStruct) AddConcentration(amount int16) bool {
info.mutex.Lock()
defer info.mutex.Unlock()
if info.curConcentration+amount > info.maxConcentration {
return false // Not enough concentration available
}
info.curConcentration += amount
return true
}
// RemoveConcentration removes from the current concentration used
func (info *InfoStruct) RemoveConcentration(amount int16) {
info.mutex.Lock()
defer info.mutex.Unlock()
info.curConcentration -= amount
if info.curConcentration < 0 {
info.curConcentration = 0
}
}
// GetCoins returns total coin value in copper
func (info *InfoStruct) GetCoins() int32 {
info.mutex.RLock()
defer info.mutex.RUnlock()
return info.coinCopper + (info.coinSilver * 100) +
(info.coinGold * 10000) + (info.coinPlat * 1000000)
}
// AddCoins adds coins to the entity (automatically distributes to appropriate denominations)
func (info *InfoStruct) AddCoins(copperAmount int32) {
info.mutex.Lock()
defer info.mutex.Unlock()
totalCopper := info.coinCopper + copperAmount
// Convert to higher denominations
info.coinPlat += totalCopper / 1000000
totalCopper %= 1000000
info.coinGold += totalCopper / 10000
totalCopper %= 10000
info.coinSilver += totalCopper / 100
info.coinCopper = totalCopper % 100
}
// RemoveCoins removes coins from the entity
func (info *InfoStruct) RemoveCoins(copperAmount int32) bool {
info.mutex.Lock()
defer info.mutex.Unlock()
totalCopper := info.coinCopper + (info.coinSilver * 100) +
(info.coinGold * 10000) + (info.coinPlat * 1000000)
if totalCopper < copperAmount {
return false // Not enough coins
}
totalCopper -= copperAmount
// Redistribute
info.coinPlat = totalCopper / 1000000
totalCopper %= 1000000
info.coinGold = totalCopper / 10000
totalCopper %= 10000
info.coinSilver = totalCopper / 100
info.coinCopper = totalCopper % 100
return true
}
// GetResistance returns the resistance value for a specific type
func (info *InfoStruct) GetResistance(resistType string) int16 {
info.mutex.RLock()
defer info.mutex.RUnlock()
switch resistType {
case "heat":
return info.heat
case "cold":
return info.cold
case "magic":
return info.magic
case "mental":
return info.mental
case "divine":
return info.divine
case "disease":
return info.disease
case "poison":
return info.poison
default:
return 0
}
}
// SetResistance updates a resistance value
func (info *InfoStruct) SetResistance(resistType string, value int16) {
info.mutex.Lock()
defer info.mutex.Unlock()
switch resistType {
case "heat":
info.heat = value
case "cold":
info.cold = value
case "magic":
info.magic = value
case "mental":
info.mental = value
case "divine":
info.divine = value
case "disease":
info.disease = value
case "poison":
info.poison = value
}
}
// ResetEffects resets all temporary effects and bonuses
// This method should be called when recalculating all bonuses
func (info *InfoStruct) ResetEffects() {
info.mutex.Lock()
defer info.mutex.Unlock()
// Reset stats to base values
info.str = info.strBase
info.sta = info.staBase
info.agi = info.agiBase
info.wis = info.wisBase
info.intel = info.intelBase
info.heat = info.heatBase
info.cold = info.coldBase
info.magic = info.magicBase
info.mental = info.mentalBase
info.divine = info.divineBase
info.disease = info.diseaseBase
info.poison = info.poisonBase
info.parry = info.parryBase
info.deflection = info.deflectionBase
info.block = info.blockBase
// Reset combat stats to base
info.curAttack = info.attackBase
info.curMitigation = info.mitigationBase
info.curAvoidance = float32(info.baseAvoidancePct)
}
// CalculatePrimaryStat returns the highest primary stat value
func (info *InfoStruct) CalculatePrimaryStat() float32 {
info.mutex.RLock()
defer info.mutex.RUnlock()
primary := info.str
if info.sta > primary {
primary = info.sta
}
if info.agi > primary {
primary = info.agi
}
if info.wis > primary {
primary = info.wis
}
if info.intel > primary {
primary = info.intel
}
return primary
}
// Clone creates a deep copy of the InfoStruct
func (info *InfoStruct) Clone() *InfoStruct {
info.mutex.RLock()
defer info.mutex.RUnlock()
clone := &InfoStruct{}
*clone = *info // Copy all fields
// Copy the account age bonus array
copy(clone.accountAgeBonus[:], info.accountAgeBonus[:])
return clone
}
// GetUptime returns how long the entity has been active (for players, time logged in)
func (info *InfoStruct) GetUptime() time.Duration {
// TODO: Implement when we have login tracking
return time.Duration(0)
}
// TODO: Additional methods to implement:
// - Experience calculation methods
// - Weapon damage calculation methods
// - Spell-related stat methods
// - Group and encounter methods
// - Equipment bonus application methods

View File

@ -0,0 +1,37 @@
package entity
// DEPRECATED: This file now imports spell effect structures from the spells package.
// The spell effect management has been moved to internal/spells for better organization.
import (
"eq2emu/internal/spells"
)
// Re-export spell effect types for backward compatibility
// These will eventually be removed in favor of direct imports from spells package
type BonusValues = spells.BonusValues
type MaintainedEffects = spells.MaintainedEffects
type SpellEffects = spells.SpellEffects
type DetrimentalEffects = spells.DetrimentalEffects
type SpellEffectManager = spells.SpellEffectManager
// Re-export constructor functions
var NewBonusValues = spells.NewBonusValues
var NewMaintainedEffects = spells.NewMaintainedEffects
var NewSpellEffects = spells.NewSpellEffects
var NewDetrimentalEffects = spells.NewDetrimentalEffects
var NewSpellEffectManager = spells.NewSpellEffectManager
// Re-export constants
const (
ControlEffectStun = spells.ControlEffectStun
ControlEffectRoot = spells.ControlEffectRoot
ControlEffectMez = spells.ControlEffectMez
ControlEffectDaze = spells.ControlEffectDaze
ControlEffectFear = spells.ControlEffectFear
ControlEffectSlow = spells.ControlEffectSlow
ControlEffectSnare = spells.ControlEffectSnare
ControlEffectCharm = spells.ControlEffectCharm
ControlMaxEffects = spells.ControlMaxEffects
)

View File

@ -0,0 +1,35 @@
package object
// Object constants converted from C++ Object.cpp
const (
// Object spawn type (from C++ constructor)
ObjectSpawnType = 2
// Object appearance defaults (from C++ constructor)
ObjectActivityStatus = 64 // Default activity status
ObjectPosState = 1 // Default position state
ObjectDifficulty = 0 // Default difficulty
// Object interaction constants
ObjectShowCommandIcon = 1 // Show command icon when interactable
)
// Object device ID constants
const (
DeviceIDNone = 0 // No device ID assigned
DeviceIDDefault = 0 // Default device ID
)
// Object state constants
const (
ObjectStateInactive = 0 // Object is inactive
ObjectStateActive = 1 // Object is active and usable
)
// Object interaction types
const (
InteractionTypeNone = 0 // No interaction available
InteractionTypeCommand = 1 // Command-based interaction
InteractionTypeTransport = 2 // Transport/teleport interaction
InteractionTypeDevice = 3 // Device-based interaction
)

View File

@ -0,0 +1,281 @@
package object
import (
"fmt"
"eq2emu/internal/spawn"
"eq2emu/internal/common"
)
// ObjectSpawn represents an object that extends spawn functionality
// This properly integrates with the existing spawn system
type ObjectSpawn struct {
*spawn.Spawn // Embed the spawn functionality
// Object-specific properties
clickable bool // Whether the object can be clicked/interacted with
deviceID int8 // Device ID for interactive objects
}
// NewObjectSpawn creates a new object spawn with default values
func NewObjectSpawn() *ObjectSpawn {
// Create base spawn
baseSpawn := spawn.NewSpawn()
// Set object-specific spawn properties
baseSpawn.SetSpawnType(ObjectSpawnType)
// Set object appearance defaults
appearance := baseSpawn.GetAppearance()
appearance.ActivityStatus = ObjectActivityStatus
appearance.Pos.State = ObjectPosState
appearance.Difficulty = ObjectDifficulty
baseSpawn.SetAppearance(appearance)
return &ObjectSpawn{
Spawn: baseSpawn,
clickable: false,
deviceID: DeviceIDNone,
}
}
// SetClickable sets whether the object can be clicked
func (os *ObjectSpawn) SetClickable(clickable bool) {
os.clickable = clickable
}
// IsClickable returns whether the object can be clicked
func (os *ObjectSpawn) IsClickable() bool {
return os.clickable
}
// SetDeviceID sets the device ID for interactive objects
func (os *ObjectSpawn) SetDeviceID(deviceID int8) {
os.deviceID = deviceID
}
// GetDeviceID returns the device ID
func (os *ObjectSpawn) GetDeviceID() int8 {
return os.deviceID
}
// IsObject always returns true for object spawns
func (os *ObjectSpawn) IsObject() bool {
return true
}
// Copy creates a deep copy of the object spawn
func (os *ObjectSpawn) Copy() *ObjectSpawn {
// Copy base spawn
newSpawn := os.Spawn.Copy()
// Create new object spawn
newObjectSpawn := &ObjectSpawn{
Spawn: newSpawn,
clickable: os.clickable,
deviceID: os.deviceID,
}
return newObjectSpawn
}
// HandleUse processes object interaction by a client
func (os *ObjectSpawn) HandleUse(clientID int32, command string) error {
// Use the base object's HandleUse logic but with spawn integration
object := &Object{}
// Copy relevant properties for handling
object.clickable = os.clickable
object.deviceID = os.deviceID
object.transporterID = os.GetTransporterID()
object.appearanceShowCommandIcon = int8(0)
if os.GetAppearance().ShowCommandIcon == 1 {
object.appearanceShowCommandIcon = ObjectShowCommandIcon
}
// TODO: Copy command lists when they're integrated with spawn system
return object.HandleUse(clientID, command)
}
// SetShowCommandIcon sets whether to show the command icon
func (os *ObjectSpawn) SetShowCommandIcon(show bool) {
appearance := os.GetAppearance()
if show {
appearance.ShowCommandIcon = ObjectShowCommandIcon
} else {
appearance.ShowCommandIcon = 0
}
os.SetAppearance(appearance)
}
// ShowsCommandIcon returns whether the command icon is shown
func (os *ObjectSpawn) ShowsCommandIcon() bool {
return os.GetAppearance().ShowCommandIcon == ObjectShowCommandIcon
}
// GetObjectInfo returns comprehensive information about the object spawn
func (os *ObjectSpawn) GetObjectInfo() map[string]interface{} {
info := make(map[string]interface{})
// Add spawn info
info["spawn_id"] = os.GetID()
info["database_id"] = os.GetDatabaseID()
info["zone_name"] = os.GetZoneName()
info["spawn_type"] = os.GetSpawnType()
info["size"] = os.GetSize()
// Add object-specific info
info["clickable"] = os.clickable
info["device_id"] = os.deviceID
info["shows_command_icon"] = os.ShowsCommandIcon()
info["transporter_id"] = os.GetTransporterID()
info["merchant_id"] = os.GetMerchantID()
info["is_collector"] = os.IsCollector()
// Add position info
appearance := os.GetAppearance()
info["x"] = appearance.Pos.X
info["y"] = appearance.Pos.Y
info["z"] = appearance.Pos.Z
info["heading"] = appearance.Pos.Dir1
return info
}
// ObjectSpawnManager manages object spawns specifically
type ObjectSpawnManager struct {
spawnManager *spawn.SpawnManager // Reference to global spawn manager
objects map[int32]*ObjectSpawn // Object spawns by spawn ID
}
// NewObjectSpawnManager creates a new object spawn manager
func NewObjectSpawnManager(spawnManager *spawn.SpawnManager) *ObjectSpawnManager {
return &ObjectSpawnManager{
spawnManager: spawnManager,
objects: make(map[int32]*ObjectSpawn),
}
}
// AddObjectSpawn adds an object spawn to both the object and spawn managers
func (osm *ObjectSpawnManager) AddObjectSpawn(objectSpawn *ObjectSpawn) error {
// Add to spawn manager first
if err := osm.spawnManager.AddSpawn(objectSpawn.Spawn); err != nil {
return err
}
// Add to object tracking
osm.objects[objectSpawn.GetID()] = objectSpawn
return nil
}
// RemoveObjectSpawn removes an object spawn from both managers
func (osm *ObjectSpawnManager) RemoveObjectSpawn(spawnID int32) error {
// Remove from object tracking
delete(osm.objects, spawnID)
// Remove from spawn manager
return osm.spawnManager.RemoveSpawn(spawnID)
}
// GetObjectSpawn retrieves an object spawn by ID
func (osm *ObjectSpawnManager) GetObjectSpawn(spawnID int32) *ObjectSpawn {
return osm.objects[spawnID]
}
// GetObjectSpawnsByZone returns all object spawns in a zone
func (osm *ObjectSpawnManager) GetObjectSpawnsByZone(zoneName string) []*ObjectSpawn {
result := make([]*ObjectSpawn, 0)
// Get all spawns in zone and filter for objects
spawns := osm.spawnManager.GetSpawnsByZone(zoneName)
for _, spawn := range spawns {
if spawn.GetSpawnType() == ObjectSpawnType {
if objectSpawn, exists := osm.objects[spawn.GetID()]; exists {
result = append(result, objectSpawn)
}
}
}
return result
}
// GetInteractiveObjectSpawns returns all interactive object spawns
func (osm *ObjectSpawnManager) GetInteractiveObjectSpawns() []*ObjectSpawn {
result := make([]*ObjectSpawn, 0)
for _, objectSpawn := range osm.objects {
if objectSpawn.IsClickable() || objectSpawn.ShowsCommandIcon() {
result = append(result, objectSpawn)
}
}
return result
}
// ProcessObjectInteraction handles interaction with an object spawn
func (osm *ObjectSpawnManager) ProcessObjectInteraction(spawnID, clientID int32, command string) error {
objectSpawn := osm.GetObjectSpawn(spawnID)
if objectSpawn == nil {
return fmt.Errorf("object spawn %d not found", spawnID)
}
return objectSpawn.HandleUse(clientID, command)
}
// ConvertSpawnToObject converts a regular spawn to an object spawn (if applicable)
func ConvertSpawnToObject(spawn *spawn.Spawn) *ObjectSpawn {
if spawn.GetSpawnType() != ObjectSpawnType {
return nil
}
objectSpawn := &ObjectSpawn{
Spawn: spawn,
clickable: false, // Default, should be loaded from data
deviceID: DeviceIDNone,
}
// Set clickable based on appearance flags or other indicators
appearance := spawn.GetAppearance()
if appearance.ShowCommandIcon == ObjectShowCommandIcon {
objectSpawn.clickable = true
}
return objectSpawn
}
// LoadObjectSpawnFromData loads object spawn data from database/config
// This would be called when loading spawns from the database
func LoadObjectSpawnFromData(spawnData map[string]interface{}) *ObjectSpawn {
objectSpawn := NewObjectSpawn()
// Load basic spawn data
if databaseID, ok := spawnData["database_id"].(int32); ok {
objectSpawn.SetDatabaseID(databaseID)
}
if zoneName, ok := spawnData["zone"].(string); ok {
objectSpawn.SetZoneName(zoneName)
}
// Load object-specific data
if clickable, ok := spawnData["clickable"].(bool); ok {
objectSpawn.SetClickable(clickable)
}
if deviceID, ok := spawnData["device_id"].(int8); ok {
objectSpawn.SetDeviceID(deviceID)
}
// Load position data
if x, ok := spawnData["x"].(float32); ok {
appearance := objectSpawn.GetAppearance()
appearance.Pos.X = x
objectSpawn.SetAppearance(appearance)
}
// TODO: Load other properties as needed
return objectSpawn
}

View File

@ -0,0 +1,320 @@
package object
import (
"time"
)
// SpawnInterface defines the interface that spawn-based objects must implement
// This allows objects to integrate with systems expecting spawn entities
type SpawnInterface interface {
// Basic identification
GetID() int32
GetDatabaseID() int32
// Zone and positioning
GetZoneName() string
SetZoneName(string)
GetX() float32
GetY() float32
GetZ() float32
GetHeading() float32
// Spawn properties
GetSpawnType() int8
SetSpawnType(int8)
GetSize() int16
SetSize(int16)
// State flags
IsAlive() bool
SetAlive(bool)
IsRunning() bool
SetRunning(bool)
// Entity properties for spell/trade integration
GetFactionID() int32
SetFactionID(int32)
GetTarget() int32
SetTarget(int32)
}
// ObjectInterface defines the interface for interactive objects
type ObjectInterface interface {
SpawnInterface
// Object-specific properties
IsObject() bool
IsClickable() bool
SetClickable(bool)
GetDeviceID() int8
SetDeviceID(int8)
// Interaction
HandleUse(clientID int32, command string) error
ShowsCommandIcon() bool
SetShowCommandIcon(bool)
// Merchant functionality
GetMerchantID() int32
SetMerchantID(int32)
GetMerchantType() int8
SetMerchantType(int8)
IsCollector() bool
SetCollector(bool)
// Transport functionality
GetTransporterID() int32
SetTransporterID(int32)
// Copying
Copy() ObjectInterface
}
// EntityInterface defines the interface for entities in trade/spell systems
// ObjectSpawn implements this to integrate with existing systems
type EntityInterface interface {
GetID() int32
GetName() string
IsPlayer() bool
IsBot() bool
HasCoins(amount int64) bool
GetClientVersion() int32
}
// ItemInterface defines the interface for items that objects might provide
// This allows objects to act as item sources (merchants, containers, etc.)
type ItemInterface interface {
GetID() int32
GetName() string
GetQuantity() int32
GetIcon(version int32) int32
IsNoTrade() bool
IsHeirloom() bool
IsAttuned() bool
GetCreationTime() time.Time
GetGroupCharacterIDs() []int32
}
// ObjectSpawnAsEntity is an adapter that makes ObjectSpawn implement EntityInterface
// This allows objects to be used in trade and spell systems
type ObjectSpawnAsEntity struct {
*ObjectSpawn
name string
isPlayer bool
isBot bool
coinsAmount int64
clientVersion int32
}
// NewObjectSpawnAsEntity creates an entity adapter for an object spawn
func NewObjectSpawnAsEntity(objectSpawn *ObjectSpawn) *ObjectSpawnAsEntity {
return &ObjectSpawnAsEntity{
ObjectSpawn: objectSpawn,
name: "Object", // Default name, should be loaded from data
isPlayer: false,
isBot: false,
coinsAmount: 0,
clientVersion: 1000,
}
}
// EntityInterface implementation for ObjectSpawnAsEntity
// GetName returns the object's name
func (osae *ObjectSpawnAsEntity) GetName() string {
return osae.name
}
// SetName sets the object's name
func (osae *ObjectSpawnAsEntity) SetName(name string) {
osae.name = name
}
// IsPlayer returns false for objects
func (osae *ObjectSpawnAsEntity) IsPlayer() bool {
return osae.isPlayer
}
// IsBot returns whether this object behaves like a bot
func (osae *ObjectSpawnAsEntity) IsBot() bool {
return osae.isBot
}
// SetBot sets whether this object behaves like a bot
func (osae *ObjectSpawnAsEntity) SetBot(isBot bool) {
osae.isBot = isBot
}
// HasCoins returns whether the object has sufficient coins (for merchant objects)
func (osae *ObjectSpawnAsEntity) HasCoins(amount int64) bool {
return osae.coinsAmount >= amount
}
// SetCoins sets the object's coin amount (for merchant objects)
func (osae *ObjectSpawnAsEntity) SetCoins(amount int64) {
osae.coinsAmount = amount
}
// GetClientVersion returns the client version (default for objects)
func (osae *ObjectSpawnAsEntity) GetClientVersion() int32 {
return osae.clientVersion
}
// SetClientVersion sets the client version
func (osae *ObjectSpawnAsEntity) SetClientVersion(version int32) {
osae.clientVersion = version
}
// ObjectItem represents an item provided by an object (merchants, containers, etc.)
type ObjectItem struct {
id int32
name string
quantity int32
iconID int32
noTrade bool
heirloom bool
attuned bool
creationTime time.Time
groupCharacterIDs []int32
}
// NewObjectItem creates a new object item
func NewObjectItem(id int32, name string, quantity int32) *ObjectItem {
return &ObjectItem{
id: id,
name: name,
quantity: quantity,
iconID: 0,
noTrade: false,
heirloom: false,
attuned: false,
creationTime: time.Now(),
groupCharacterIDs: make([]int32, 0),
}
}
// ItemInterface implementation for ObjectItem
// GetID returns the item ID
func (oi *ObjectItem) GetID() int32 {
return oi.id
}
// GetName returns the item name
func (oi *ObjectItem) GetName() string {
return oi.name
}
// GetQuantity returns the item quantity
func (oi *ObjectItem) GetQuantity() int32 {
return oi.quantity
}
// SetQuantity sets the item quantity
func (oi *ObjectItem) SetQuantity(quantity int32) {
oi.quantity = quantity
}
// GetIcon returns the item icon ID
func (oi *ObjectItem) GetIcon(version int32) int32 {
return oi.iconID
}
// SetIcon sets the item icon ID
func (oi *ObjectItem) SetIcon(iconID int32) {
oi.iconID = iconID
}
// IsNoTrade returns whether the item is no-trade
func (oi *ObjectItem) IsNoTrade() bool {
return oi.noTrade
}
// SetNoTrade sets whether the item is no-trade
func (oi *ObjectItem) SetNoTrade(noTrade bool) {
oi.noTrade = noTrade
}
// IsHeirloom returns whether the item is heirloom
func (oi *ObjectItem) IsHeirloom() bool {
return oi.heirloom
}
// SetHeirloom sets whether the item is heirloom
func (oi *ObjectItem) SetHeirloom(heirloom bool) {
oi.heirloom = heirloom
}
// IsAttuned returns whether the item is attuned
func (oi *ObjectItem) IsAttuned() bool {
return oi.attuned
}
// SetAttuned sets whether the item is attuned
func (oi *ObjectItem) SetAttuned(attuned bool) {
oi.attuned = attuned
}
// GetCreationTime returns when the item was created
func (oi *ObjectItem) GetCreationTime() time.Time {
return oi.creationTime
}
// SetCreationTime sets when the item was created
func (oi *ObjectItem) SetCreationTime(creationTime time.Time) {
oi.creationTime = creationTime
}
// GetGroupCharacterIDs returns the group character IDs for heirloom sharing
func (oi *ObjectItem) GetGroupCharacterIDs() []int32 {
return oi.groupCharacterIDs
}
// SetGroupCharacterIDs sets the group character IDs for heirloom sharing
func (oi *ObjectItem) SetGroupCharacterIDs(characterIDs []int32) {
oi.groupCharacterIDs = make([]int32, len(characterIDs))
copy(oi.groupCharacterIDs, characterIDs)
}
// Utility functions for integration
// CreateMerchantObjectSpawn creates an object spawn configured as a merchant
func CreateMerchantObjectSpawn(merchantID int32, merchantType int8) *ObjectSpawn {
objectSpawn := NewObjectSpawn()
objectSpawn.SetMerchantID(merchantID)
objectSpawn.SetMerchantType(merchantType)
objectSpawn.SetClickable(true)
objectSpawn.SetShowCommandIcon(true)
return objectSpawn
}
// CreateTransportObjectSpawn creates an object spawn configured as a transporter
func CreateTransportObjectSpawn(transporterID int32) *ObjectSpawn {
objectSpawn := NewObjectSpawn()
objectSpawn.SetTransporterID(transporterID)
objectSpawn.SetClickable(true)
objectSpawn.SetShowCommandIcon(true)
return objectSpawn
}
// CreateDeviceObjectSpawn creates an object spawn configured as an interactive device
func CreateDeviceObjectSpawn(deviceID int8) *ObjectSpawn {
objectSpawn := NewObjectSpawn()
objectSpawn.SetDeviceID(deviceID)
objectSpawn.SetClickable(true)
objectSpawn.SetShowCommandIcon(true)
return objectSpawn
}
// CreateCollectorObjectSpawn creates an object spawn configured as a collector
func CreateCollectorObjectSpawn() *ObjectSpawn {
objectSpawn := NewObjectSpawn()
objectSpawn.SetCollector(true)
objectSpawn.SetClickable(true)
objectSpawn.SetShowCommandIcon(true)
return objectSpawn
}

419
internal/object/manager.go Normal file
View File

@ -0,0 +1,419 @@
package object
import (
"fmt"
"sync"
)
// ObjectManager manages all objects in the game world
type ObjectManager struct {
objects map[int32]*Object // Objects by database ID
// Zone-based indexing
objectsByZone map[string][]*Object // Objects grouped by zone
// Type-based indexing
interactiveObjects []*Object // Objects that can be interacted with
transportObjects []*Object // Objects that provide transport
merchantObjects []*Object // Objects that are merchants
collectorObjects []*Object // Objects that are collectors
// Thread safety
mutex sync.RWMutex
}
// NewObjectManager creates a new object manager
func NewObjectManager() *ObjectManager {
return &ObjectManager{
objects: make(map[int32]*Object),
objectsByZone: make(map[string][]*Object),
interactiveObjects: make([]*Object, 0),
transportObjects: make([]*Object, 0),
merchantObjects: make([]*Object, 0),
collectorObjects: make([]*Object, 0),
}
}
// AddObject adds an object to the manager
func (om *ObjectManager) AddObject(object *Object) error {
if object == nil {
return fmt.Errorf("cannot add nil object")
}
om.mutex.Lock()
defer om.mutex.Unlock()
databaseID := object.GetDatabaseID()
if databaseID == 0 {
return fmt.Errorf("object must have a valid database ID")
}
// Check if object already exists
if _, exists := om.objects[databaseID]; exists {
return fmt.Errorf("object with database ID %d already exists", databaseID)
}
// Add to main collection
om.objects[databaseID] = object
// Add to zone collection
zoneName := object.GetZoneName()
if zoneName != "" {
om.objectsByZone[zoneName] = append(om.objectsByZone[zoneName], object)
}
// Add to type-based collections
om.updateObjectIndices(object, true)
return nil
}
// RemoveObject removes an object from the manager
func (om *ObjectManager) RemoveObject(databaseID int32) error {
om.mutex.Lock()
defer om.mutex.Unlock()
object, exists := om.objects[databaseID]
if !exists {
return fmt.Errorf("object with database ID %d not found", databaseID)
}
// Remove from main collection
delete(om.objects, databaseID)
// Remove from zone collection
zoneName := object.GetZoneName()
if zoneName != "" {
if zoneObjects, exists := om.objectsByZone[zoneName]; exists {
for i, obj := range zoneObjects {
if obj.GetDatabaseID() == databaseID {
om.objectsByZone[zoneName] = append(zoneObjects[:i], zoneObjects[i+1:]...)
break
}
}
// Clean up empty zone collection
if len(om.objectsByZone[zoneName]) == 0 {
delete(om.objectsByZone, zoneName)
}
}
}
// Remove from type-based collections
om.updateObjectIndices(object, false)
return nil
}
// GetObject retrieves an object by database ID
func (om *ObjectManager) GetObject(databaseID int32) *Object {
om.mutex.RLock()
defer om.mutex.RUnlock()
return om.objects[databaseID]
}
// GetObjectsByZone retrieves all objects in a specific zone
func (om *ObjectManager) GetObjectsByZone(zoneName string) []*Object {
om.mutex.RLock()
defer om.mutex.RUnlock()
if objects, exists := om.objectsByZone[zoneName]; exists {
// Return a copy to prevent external modification
result := make([]*Object, len(objects))
copy(result, objects)
return result
}
return make([]*Object, 0)
}
// GetInteractiveObjects returns all objects that can be interacted with
func (om *ObjectManager) GetInteractiveObjects() []*Object {
om.mutex.RLock()
defer om.mutex.RUnlock()
result := make([]*Object, len(om.interactiveObjects))
copy(result, om.interactiveObjects)
return result
}
// GetTransportObjects returns all objects that provide transport
func (om *ObjectManager) GetTransportObjects() []*Object {
om.mutex.RLock()
defer om.mutex.RUnlock()
result := make([]*Object, len(om.transportObjects))
copy(result, om.transportObjects)
return result
}
// GetMerchantObjects returns all merchant objects
func (om *ObjectManager) GetMerchantObjects() []*Object {
om.mutex.RLock()
defer om.mutex.RUnlock()
result := make([]*Object, len(om.merchantObjects))
copy(result, om.merchantObjects)
return result
}
// GetCollectorObjects returns all collector objects
func (om *ObjectManager) GetCollectorObjects() []*Object {
om.mutex.RLock()
defer om.mutex.RUnlock()
result := make([]*Object, len(om.collectorObjects))
copy(result, om.collectorObjects)
return result
}
// GetObjectCount returns the total number of objects
func (om *ObjectManager) GetObjectCount() int {
om.mutex.RLock()
defer om.mutex.RUnlock()
return len(om.objects)
}
// GetZoneCount returns the number of zones with objects
func (om *ObjectManager) GetZoneCount() int {
om.mutex.RLock()
defer om.mutex.RUnlock()
return len(om.objectsByZone)
}
// GetObjectsByType returns objects filtered by specific criteria
func (om *ObjectManager) GetObjectsByType(objectType string) []*Object {
om.mutex.RLock()
defer om.mutex.RUnlock()
switch objectType {
case "interactive":
result := make([]*Object, len(om.interactiveObjects))
copy(result, om.interactiveObjects)
return result
case "transport":
result := make([]*Object, len(om.transportObjects))
copy(result, om.transportObjects)
return result
case "merchant":
result := make([]*Object, len(om.merchantObjects))
copy(result, om.merchantObjects)
return result
case "collector":
result := make([]*Object, len(om.collectorObjects))
copy(result, om.collectorObjects)
return result
default:
return make([]*Object, 0)
}
}
// FindObjectsInZone finds objects in a zone matching specific criteria
func (om *ObjectManager) FindObjectsInZone(zoneName string, filter func(*Object) bool) []*Object {
om.mutex.RLock()
defer om.mutex.RUnlock()
zoneObjects, exists := om.objectsByZone[zoneName]
if !exists {
return make([]*Object, 0)
}
result := make([]*Object, 0)
for _, obj := range zoneObjects {
if filter == nil || filter(obj) {
result = append(result, obj)
}
}
return result
}
// FindObjectByName finds the first object with a matching name (placeholder)
func (om *ObjectManager) FindObjectByName(name string) *Object {
om.mutex.RLock()
defer om.mutex.RUnlock()
// TODO: Implement name searching when spawn name system is integrated
// For now, return nil
return nil
}
// UpdateObject updates an existing object's properties
func (om *ObjectManager) UpdateObject(databaseID int32, updateFn func(*Object)) error {
om.mutex.Lock()
defer om.mutex.Unlock()
object, exists := om.objects[databaseID]
if !exists {
return fmt.Errorf("object with database ID %d not found", databaseID)
}
// Store old zone for potential reindexing
oldZone := object.GetZoneName()
// Apply updates
updateFn(object)
// Check if zone changed and reindex if necessary
newZone := object.GetZoneName()
if oldZone != newZone {
// Remove from old zone
if oldZone != "" {
if zoneObjects, exists := om.objectsByZone[oldZone]; exists {
for i, obj := range zoneObjects {
if obj.GetDatabaseID() == databaseID {
om.objectsByZone[oldZone] = append(zoneObjects[:i], zoneObjects[i+1:]...)
break
}
}
// Clean up empty zone collection
if len(om.objectsByZone[oldZone]) == 0 {
delete(om.objectsByZone, oldZone)
}
}
}
// Add to new zone
if newZone != "" {
om.objectsByZone[newZone] = append(om.objectsByZone[newZone], object)
}
}
// Update type-based indices
om.rebuildIndicesForObject(object)
return nil
}
// ClearZone removes all objects from a specific zone
func (om *ObjectManager) ClearZone(zoneName string) int {
om.mutex.Lock()
defer om.mutex.Unlock()
zoneObjects, exists := om.objectsByZone[zoneName]
if !exists {
return 0
}
count := len(zoneObjects)
// Remove objects from main collection and indices
for _, obj := range zoneObjects {
databaseID := obj.GetDatabaseID()
delete(om.objects, databaseID)
om.updateObjectIndices(obj, false)
}
// Clear zone collection
delete(om.objectsByZone, zoneName)
return count
}
// GetStatistics returns statistics about objects in the manager
func (om *ObjectManager) GetStatistics() map[string]interface{} {
om.mutex.RLock()
defer om.mutex.RUnlock()
stats := make(map[string]interface{})
stats["total_objects"] = len(om.objects)
stats["zones_with_objects"] = len(om.objectsByZone)
stats["interactive_objects"] = len(om.interactiveObjects)
stats["transport_objects"] = len(om.transportObjects)
stats["merchant_objects"] = len(om.merchantObjects)
stats["collector_objects"] = len(om.collectorObjects)
// Zone breakdown
zoneStats := make(map[string]int)
for zoneName, objects := range om.objectsByZone {
zoneStats[zoneName] = len(objects)
}
stats["objects_by_zone"] = zoneStats
return stats
}
// Shutdown clears all objects and prepares for shutdown
func (om *ObjectManager) Shutdown() {
om.mutex.Lock()
defer om.mutex.Unlock()
om.objects = make(map[int32]*Object)
om.objectsByZone = make(map[string][]*Object)
om.interactiveObjects = make([]*Object, 0)
om.transportObjects = make([]*Object, 0)
om.merchantObjects = make([]*Object, 0)
om.collectorObjects = make([]*Object, 0)
}
// Private helper methods
// updateObjectIndices updates type-based indices for an object
func (om *ObjectManager) updateObjectIndices(object *Object, add bool) {
if add {
// Add to type-based collections
if object.IsClickable() || len(object.GetPrimaryCommands()) > 0 || len(object.GetSecondaryCommands()) > 0 {
om.interactiveObjects = append(om.interactiveObjects, object)
}
if object.GetTransporterID() > 0 {
om.transportObjects = append(om.transportObjects, object)
}
if object.GetMerchantID() > 0 {
om.merchantObjects = append(om.merchantObjects, object)
}
if object.IsCollector() {
om.collectorObjects = append(om.collectorObjects, object)
}
} else {
// Remove from type-based collections
databaseID := object.GetDatabaseID()
om.interactiveObjects = removeObjectFromSlice(om.interactiveObjects, databaseID)
om.transportObjects = removeObjectFromSlice(om.transportObjects, databaseID)
om.merchantObjects = removeObjectFromSlice(om.merchantObjects, databaseID)
om.collectorObjects = removeObjectFromSlice(om.collectorObjects, databaseID)
}
}
// rebuildIndicesForObject rebuilds type-based indices for an object (used after updates)
func (om *ObjectManager) rebuildIndicesForObject(object *Object) {
databaseID := object.GetDatabaseID()
// Remove from all type-based collections
om.interactiveObjects = removeObjectFromSlice(om.interactiveObjects, databaseID)
om.transportObjects = removeObjectFromSlice(om.transportObjects, databaseID)
om.merchantObjects = removeObjectFromSlice(om.merchantObjects, databaseID)
om.collectorObjects = removeObjectFromSlice(om.collectorObjects, databaseID)
// Re-add based on current properties
om.updateObjectIndices(object, true)
}
// removeObjectFromSlice removes an object from a slice by database ID
func removeObjectFromSlice(slice []*Object, databaseID int32) []*Object {
for i, obj := range slice {
if obj.GetDatabaseID() == databaseID {
return append(slice[:i], slice[i+1:]...)
}
}
return slice
}
// Global object manager instance
var globalObjectManager *ObjectManager
var initObjectManagerOnce sync.Once
// GetGlobalObjectManager returns the global object manager (singleton)
func GetGlobalObjectManager() *ObjectManager {
initObjectManagerOnce.Do(func() {
globalObjectManager = NewObjectManager()
})
return globalObjectManager
}

603
internal/object/object.go Normal file
View File

@ -0,0 +1,603 @@
package object
import (
"fmt"
"math/rand"
"strings"
"sync"
)
// Object represents a game object that extends spawn functionality
// Converted from C++ Object class
type Object struct {
// Embed spawn functionality - TODO: Use actual spawn.Spawn when integrated
// spawn.Spawn
// Object-specific properties
clickable bool // Whether the object can be clicked/interacted with
zoneName string // Name of the zone this object belongs to
deviceID int8 // Device ID for interactive objects
// Inherited spawn properties (placeholder until spawn integration)
databaseID int32
size int16
sizeOffset int8
merchantID int32
merchantType int8
merchantMinLevel int8
merchantMaxLevel int8
isCollector bool
factionID int32
totalHP int32
totalPower int32
currentHP int32
currentPower int32
transporterID int32
soundsDisabled bool
omittedByDBFlag bool
lootTier int8
lootDropType int8
spawnScript string
spawnScriptSetDB bool
primaryCommandListID int32
secondaryCommandListID int32
// Appearance data placeholder - TODO: Use actual appearance struct
appearanceActivityStatus int8
appearancePosState int8
appearanceDifficulty int8
appearanceShowCommandIcon int8
// Command lists - TODO: Use actual command structures
primaryCommands []string
secondaryCommands []string
// Thread safety
mutex sync.RWMutex
}
// NewObject creates a new object with default values
// Converted from C++ Object::Object constructor
func NewObject() *Object {
return &Object{
clickable: false,
zoneName: "",
deviceID: DeviceIDNone,
appearanceActivityStatus: ObjectActivityStatus,
appearancePosState: ObjectPosState,
appearanceDifficulty: ObjectDifficulty,
appearanceShowCommandIcon: 0,
primaryCommands: make([]string, 0),
secondaryCommands: make([]string, 0),
}
}
// SetClickable sets whether the object can be clicked
func (o *Object) SetClickable(clickable bool) {
o.mutex.Lock()
defer o.mutex.Unlock()
o.clickable = clickable
}
// IsClickable returns whether the object can be clicked
func (o *Object) IsClickable() bool {
o.mutex.RLock()
defer o.mutex.RUnlock()
return o.clickable
}
// SetZone sets the zone name for this object
// Converted from C++ Object::SetZone
func (o *Object) SetZone(zoneName string) {
o.mutex.Lock()
defer o.mutex.Unlock()
o.zoneName = zoneName
}
// GetZoneName returns the zone name for this object
func (o *Object) GetZoneName() string {
o.mutex.RLock()
defer o.mutex.RUnlock()
return o.zoneName
}
// SetDeviceID sets the device ID for interactive objects
// Converted from C++ Object::SetDeviceID
func (o *Object) SetDeviceID(deviceID int8) {
o.mutex.Lock()
defer o.mutex.Unlock()
o.deviceID = deviceID
}
// GetDeviceID returns the device ID
// Converted from C++ Object::GetDeviceID
func (o *Object) GetDeviceID() int8 {
o.mutex.RLock()
defer o.mutex.RUnlock()
return o.deviceID
}
// IsObject always returns true for Object instances
// Converted from C++ Object::IsObject
func (o *Object) IsObject() bool {
return true
}
// Copy creates a deep copy of the object
// Converted from C++ Object::Copy
func (o *Object) Copy() *Object {
o.mutex.RLock()
defer o.mutex.RUnlock()
newObject := NewObject()
// Copy basic properties
newObject.clickable = o.clickable
newObject.zoneName = o.zoneName
newObject.deviceID = o.deviceID
newObject.databaseID = o.databaseID
newObject.merchantID = o.merchantID
newObject.merchantType = o.merchantType
newObject.merchantMinLevel = o.merchantMinLevel
newObject.merchantMaxLevel = o.merchantMaxLevel
newObject.isCollector = o.isCollector
newObject.factionID = o.factionID
newObject.totalHP = o.totalHP
newObject.totalPower = o.totalPower
newObject.currentHP = o.currentHP
newObject.currentPower = o.currentPower
newObject.transporterID = o.transporterID
newObject.soundsDisabled = o.soundsDisabled
newObject.omittedByDBFlag = o.omittedByDBFlag
newObject.lootTier = o.lootTier
newObject.lootDropType = o.lootDropType
newObject.spawnScript = o.spawnScript
newObject.spawnScriptSetDB = o.spawnScriptSetDB
newObject.primaryCommandListID = o.primaryCommandListID
newObject.secondaryCommandListID = o.secondaryCommandListID
// Copy appearance data
newObject.appearanceActivityStatus = o.appearanceActivityStatus
newObject.appearancePosState = o.appearancePosState
newObject.appearanceDifficulty = o.appearanceDifficulty
newObject.appearanceShowCommandIcon = o.appearanceShowCommandIcon
// Handle size with random offset (from C++ logic)
if o.sizeOffset > 0 {
offset := o.sizeOffset + 1
tmpSize := int32(o.size) + (rand.Int31n(int32(offset)) - rand.Int31n(int32(offset)))
if tmpSize < 0 {
tmpSize = 1
} else if tmpSize >= 0xFFFF {
tmpSize = 0xFFFF
}
newObject.size = int16(tmpSize)
} else {
newObject.size = o.size
}
// Copy command lists
newObject.primaryCommands = make([]string, len(o.primaryCommands))
copy(newObject.primaryCommands, o.primaryCommands)
newObject.secondaryCommands = make([]string, len(o.secondaryCommands))
copy(newObject.secondaryCommands, o.secondaryCommands)
return newObject
}
// HandleUse processes object interaction by a client
// Converted from C++ Object::HandleUse
func (o *Object) HandleUse(clientID int32, command string) error {
o.mutex.RLock()
defer o.mutex.RUnlock()
// TODO: Implement transport destination handling when zone system is available
// This would check for transporter ID and process teleportation
if o.transporterID > 0 {
// Handle transport logic
return o.handleTransport(clientID)
}
// Handle command-based interaction
if len(command) > 0 && o.appearanceShowCommandIcon == ObjectShowCommandIcon {
return o.handleCommand(clientID, command)
}
return fmt.Errorf("object is not interactive")
}
// handleTransport processes transport/teleport functionality
func (o *Object) handleTransport(clientID int32) error {
// TODO: Implement when zone and transport systems are available
// This would:
// 1. Get transport destinations for this object
// 2. Present options to the client
// 3. Process teleportation request
return fmt.Errorf("transport system not yet implemented")
}
// handleCommand processes command-based object interaction
func (o *Object) handleCommand(clientID int32, command string) error {
// TODO: Implement when entity command system is available
// This would:
// 1. Find the entity command by name
// 2. Validate client permissions
// 3. Execute the command
command = strings.TrimSpace(strings.ToLower(command))
// Check if command exists in primary or secondary commands
for _, cmd := range o.primaryCommands {
if strings.ToLower(cmd) == command {
return o.executeCommand(clientID, cmd)
}
}
for _, cmd := range o.secondaryCommands {
if strings.ToLower(cmd) == command {
return o.executeCommand(clientID, cmd)
}
}
return fmt.Errorf("command '%s' not found", command)
}
// executeCommand executes a specific command for a client
func (o *Object) executeCommand(clientID int32, command string) error {
// TODO: Implement actual command execution when command system is available
// For now, just return success for valid commands
return nil
}
// Serialize returns packet data for this object
// Converted from C++ Object::serialize
func (o *Object) Serialize(playerID int32, version int16) ([]byte, error) {
// TODO: Implement actual serialization when packet system is available
// This would call the spawn serialization method
return nil, fmt.Errorf("serialization not yet implemented")
}
// Getter/Setter methods for inherited spawn properties
// SetCollector sets whether this object is a collector
func (o *Object) SetCollector(isCollector bool) {
o.mutex.Lock()
defer o.mutex.Unlock()
o.isCollector = isCollector
}
// IsCollector returns whether this object is a collector
func (o *Object) IsCollector() bool {
o.mutex.RLock()
defer o.mutex.RUnlock()
return o.isCollector
}
// SetMerchantID sets the merchant ID
func (o *Object) SetMerchantID(merchantID int32) {
o.mutex.Lock()
defer o.mutex.Unlock()
o.merchantID = merchantID
}
// GetMerchantID returns the merchant ID
func (o *Object) GetMerchantID() int32 {
o.mutex.RLock()
defer o.mutex.RUnlock()
return o.merchantID
}
// SetMerchantType sets the merchant type
func (o *Object) SetMerchantType(merchantType int8) {
o.mutex.Lock()
defer o.mutex.Unlock()
o.merchantType = merchantType
}
// GetMerchantType returns the merchant type
func (o *Object) GetMerchantType() int8 {
o.mutex.RLock()
defer o.mutex.RUnlock()
return o.merchantType
}
// SetMerchantLevelRange sets the merchant level range
func (o *Object) SetMerchantLevelRange(minLevel, maxLevel int8) {
o.mutex.Lock()
defer o.mutex.Unlock()
o.merchantMinLevel = minLevel
o.merchantMaxLevel = maxLevel
}
// GetMerchantMinLevel returns the minimum merchant level
func (o *Object) GetMerchantMinLevel() int8 {
o.mutex.RLock()
defer o.mutex.RUnlock()
return o.merchantMinLevel
}
// GetMerchantMaxLevel returns the maximum merchant level
func (o *Object) GetMerchantMaxLevel() int8 {
o.mutex.RLock()
defer o.mutex.RUnlock()
return o.merchantMaxLevel
}
// SetSize sets the object size
func (o *Object) SetSize(size int16) {
o.mutex.Lock()
defer o.mutex.Unlock()
o.size = size
}
// GetSize returns the object size
func (o *Object) GetSize() int16 {
o.mutex.RLock()
defer o.mutex.RUnlock()
return o.size
}
// SetSizeOffset sets the size randomization offset
func (o *Object) SetSizeOffset(offset int8) {
o.mutex.Lock()
defer o.mutex.Unlock()
o.sizeOffset = offset
}
// GetSizeOffset returns the size randomization offset
func (o *Object) GetSizeOffset() int8 {
o.mutex.RLock()
defer o.mutex.RUnlock()
return o.sizeOffset
}
// SetPrimaryCommands sets the primary command list
func (o *Object) SetPrimaryCommands(commands []string) {
o.mutex.Lock()
defer o.mutex.Unlock()
o.primaryCommands = make([]string, len(commands))
copy(o.primaryCommands, commands)
}
// GetPrimaryCommands returns the primary command list
func (o *Object) GetPrimaryCommands() []string {
o.mutex.RLock()
defer o.mutex.RUnlock()
commands := make([]string, len(o.primaryCommands))
copy(commands, o.primaryCommands)
return commands
}
// SetSecondaryCommands sets the secondary command list
func (o *Object) SetSecondaryCommands(commands []string) {
o.mutex.Lock()
defer o.mutex.Unlock()
o.secondaryCommands = make([]string, len(commands))
copy(o.secondaryCommands, commands)
}
// GetSecondaryCommands returns the secondary command list
func (o *Object) GetSecondaryCommands() []string {
o.mutex.RLock()
defer o.mutex.RUnlock()
commands := make([]string, len(o.secondaryCommands))
copy(commands, o.secondaryCommands)
return commands
}
// SetDatabaseID sets the database ID
func (o *Object) SetDatabaseID(id int32) {
o.mutex.Lock()
defer o.mutex.Unlock()
o.databaseID = id
}
// GetDatabaseID returns the database ID
func (o *Object) GetDatabaseID() int32 {
o.mutex.RLock()
defer o.mutex.RUnlock()
return o.databaseID
}
// SetFactionID sets the faction ID
func (o *Object) SetFactionID(factionID int32) {
o.mutex.Lock()
defer o.mutex.Unlock()
o.factionID = factionID
}
// GetFactionID returns the faction ID
func (o *Object) GetFactionID() int32 {
o.mutex.RLock()
defer o.mutex.RUnlock()
return o.factionID
}
// SetTotalHP sets the total hit points
func (o *Object) SetTotalHP(hp int32) {
o.mutex.Lock()
defer o.mutex.Unlock()
o.totalHP = hp
}
// GetTotalHP returns the total hit points
func (o *Object) GetTotalHP() int32 {
o.mutex.RLock()
defer o.mutex.RUnlock()
return o.totalHP
}
// SetTotalPower sets the total power
func (o *Object) SetTotalPower(power int32) {
o.mutex.Lock()
defer o.mutex.Unlock()
o.totalPower = power
}
// GetTotalPower returns the total power
func (o *Object) GetTotalPower() int32 {
o.mutex.RLock()
defer o.mutex.RUnlock()
return o.totalPower
}
// SetHP sets the current hit points
func (o *Object) SetHP(hp int32) {
o.mutex.Lock()
defer o.mutex.Unlock()
o.currentHP = hp
}
// GetHP returns the current hit points
func (o *Object) GetHP() int32 {
o.mutex.RLock()
defer o.mutex.RUnlock()
return o.currentHP
}
// SetPower sets the current power
func (o *Object) SetPower(power int32) {
o.mutex.Lock()
defer o.mutex.Unlock()
o.currentPower = power
}
// GetPower returns the current power
func (o *Object) GetPower() int32 {
o.mutex.RLock()
defer o.mutex.RUnlock()
return o.currentPower
}
// SetTransporterID sets the transporter ID
func (o *Object) SetTransporterID(transporterID int32) {
o.mutex.Lock()
defer o.mutex.Unlock()
o.transporterID = transporterID
}
// GetTransporterID returns the transporter ID
func (o *Object) GetTransporterID() int32 {
o.mutex.RLock()
defer o.mutex.RUnlock()
return o.transporterID
}
// SetSoundsDisabled sets whether sounds are disabled
func (o *Object) SetSoundsDisabled(disabled bool) {
o.mutex.Lock()
defer o.mutex.Unlock()
o.soundsDisabled = disabled
}
// IsSoundsDisabled returns whether sounds are disabled
func (o *Object) IsSoundsDisabled() bool {
o.mutex.RLock()
defer o.mutex.RUnlock()
return o.soundsDisabled
}
// SetOmittedByDBFlag sets the omitted by DB flag
func (o *Object) SetOmittedByDBFlag(omitted bool) {
o.mutex.Lock()
defer o.mutex.Unlock()
o.omittedByDBFlag = omitted
}
// IsOmittedByDBFlag returns the omitted by DB flag
func (o *Object) IsOmittedByDBFlag() bool {
o.mutex.RLock()
defer o.mutex.RUnlock()
return o.omittedByDBFlag
}
// SetLootTier sets the loot tier
func (o *Object) SetLootTier(tier int8) {
o.mutex.Lock()
defer o.mutex.Unlock()
o.lootTier = tier
}
// GetLootTier returns the loot tier
func (o *Object) GetLootTier() int8 {
o.mutex.RLock()
defer o.mutex.RUnlock()
return o.lootTier
}
// SetLootDropType sets the loot drop type
func (o *Object) SetLootDropType(dropType int8) {
o.mutex.Lock()
defer o.mutex.Unlock()
o.lootDropType = dropType
}
// GetLootDropType returns the loot drop type
func (o *Object) GetLootDropType() int8 {
o.mutex.RLock()
defer o.mutex.RUnlock()
return o.lootDropType
}
// SetSpawnScript sets the spawn script
func (o *Object) SetSpawnScript(script string) {
o.mutex.Lock()
defer o.mutex.Unlock()
o.spawnScript = script
o.spawnScriptSetDB = len(script) > 0
}
// GetSpawnScript returns the spawn script
func (o *Object) GetSpawnScript() string {
o.mutex.RLock()
defer o.mutex.RUnlock()
return o.spawnScript
}
// GetSpawnScriptSetDB returns whether spawn script is set from DB
func (o *Object) GetSpawnScriptSetDB() bool {
o.mutex.RLock()
defer o.mutex.RUnlock()
return o.spawnScriptSetDB
}
// SetShowCommandIcon sets whether to show the command icon
func (o *Object) SetShowCommandIcon(show bool) {
o.mutex.Lock()
defer o.mutex.Unlock()
if show {
o.appearanceShowCommandIcon = ObjectShowCommandIcon
} else {
o.appearanceShowCommandIcon = 0
}
}
// ShowsCommandIcon returns whether the command icon is shown
func (o *Object) ShowsCommandIcon() bool {
o.mutex.RLock()
defer o.mutex.RUnlock()
return o.appearanceShowCommandIcon == ObjectShowCommandIcon
}
// GetObjectInfo returns comprehensive information about the object
func (o *Object) GetObjectInfo() map[string]interface{} {
o.mutex.RLock()
defer o.mutex.RUnlock()
info := make(map[string]interface{})
info["clickable"] = o.clickable
info["zone_name"] = o.zoneName
info["device_id"] = o.deviceID
info["database_id"] = o.databaseID
info["size"] = o.size
info["merchant_id"] = o.merchantID
info["transporter_id"] = o.transporterID
info["is_collector"] = o.isCollector
info["sounds_disabled"] = o.soundsDisabled
info["primary_commands"] = len(o.primaryCommands)
info["secondary_commands"] = len(o.secondaryCommands)
info["shows_command_icon"] = o.ShowsCommandIcon()
return info
}

View File

@ -0,0 +1,90 @@
package races
// Race ID constants converted from C++ races.h
const (
RaceBarbarian = 0
RaceDarkElf = 1
RaceDwarf = 2
RaceErudite = 3
RaceFroglok = 4
RaceGnome = 5
RaceHalfElf = 6
RaceHalfling = 7
RaceHighElf = 8
RaceHuman = 9
RaceIksar = 10
RaceKerra = 11
RaceOgre = 12
RaceRatonga = 13
RaceTroll = 14
RaceWoodElf = 15
RaceFae = 16
RaceArasai = 17
RaceSarnak = 18
RaceVampire = 19
RaceAerakyn = 20
)
// Maximum race ID for validation
const (
MaxRaceID = 20
MinRaceID = 0
DefaultRaceID = RaceHuman // Default to Human if lookup fails
)
// Race alignment types
const (
AlignmentGood = "good"
AlignmentEvil = "evil"
AlignmentNeutral = "neutral"
)
// Race name constants for lookup (uppercase keys from C++)
const (
RaceNameBarbarian = "BARBARIAN"
RaceNameDarkElf = "DARKELF"
RaceNameDwarf = "DWARF"
RaceNameErudite = "ERUDITE"
RaceNameFroglok = "FROGLOK"
RaceNameGnome = "GNOME"
RaceNameHalfElf = "HALFELF"
RaceNameHalfling = "HALFLING"
RaceNameHighElf = "HIGHELF"
RaceNameHuman = "HUMAN"
RaceNameIksar = "IKSAR"
RaceNameKerra = "KERRA"
RaceNameOgre = "OGRE"
RaceNameRatonga = "RATONGA"
RaceNameTroll = "TROLL"
RaceNameWoodElf = "WOODELF"
RaceNameFaeLight = "FAE_LIGHT"
RaceNameFaeDark = "FAE_DARK"
RaceNameSarnak = "SARNAK"
RaceNameVampire = "VAMPIRE"
RaceNameAerakyn = "AERAKYN"
)
// Race display names (proper case from C++)
const (
DisplayNameBarbarian = "Barbarian"
DisplayNameDarkElf = "Dark Elf"
DisplayNameDwarf = "Dwarf"
DisplayNameErudite = "Erudite"
DisplayNameFroglok = "Froglok"
DisplayNameGnome = "Gnome"
DisplayNameHalfElf = "Half Elf"
DisplayNameHalfling = "Halfling"
DisplayNameHighElf = "High Elf"
DisplayNameHuman = "Human"
DisplayNameIksar = "Iksar"
DisplayNameKerra = "Kerra"
DisplayNameOgre = "Ogre"
DisplayNameRatonga = "Ratonga"
DisplayNameTroll = "Troll"
DisplayNameWoodElf = "Wood Elf"
DisplayNameFae = "Fae"
DisplayNameArasai = "Arasai"
DisplayNameSarnak = "Sarnak"
DisplayNameVampire = "Vampire"
DisplayNameAerakyn = "Aerakyn"
)

View File

@ -0,0 +1,291 @@
package races
import (
"fmt"
)
// RaceAware interface for entities that have race information
type RaceAware interface {
GetRace() int8
SetRace(int8)
}
// EntityWithRace interface extends RaceAware with additional entity properties
type EntityWithRace interface {
RaceAware
GetID() int32
GetName() string
}
// RaceIntegration provides race-related functionality for other systems
type RaceIntegration struct {
races *Races
utils *RaceUtils
}
// NewRaceIntegration creates a new race integration helper
func NewRaceIntegration() *RaceIntegration {
return &RaceIntegration{
races: GetGlobalRaces(),
utils: NewRaceUtils(),
}
}
// ValidateEntityRace validates an entity's race and provides detailed information
func (ri *RaceIntegration) ValidateEntityRace(entity RaceAware) (bool, string, map[string]interface{}) {
raceID := entity.GetRace()
if !ri.races.IsValidRaceID(raceID) {
return false, fmt.Sprintf("Invalid race ID: %d", raceID), nil
}
raceInfo := ri.races.GetRaceInfo(raceID)
return true, "Valid race", raceInfo
}
// ApplyRacialBonuses applies racial stat bonuses to an entity
// This would integrate with the stat system when available
func (ri *RaceIntegration) ApplyRacialBonuses(entity RaceAware, stats map[string]*int16) {
raceID := entity.GetRace()
if !ri.races.IsValidRaceID(raceID) {
return
}
// Get racial modifiers
modifiers := ri.utils.GetRaceStatModifiers(raceID)
// Apply modifiers to stats
for statName, modifier := range modifiers {
if statPtr, exists := stats[statName]; exists && statPtr != nil {
*statPtr += int16(modifier)
}
}
}
// GetEntityRaceInfo returns comprehensive race information for an entity
func (ri *RaceIntegration) GetEntityRaceInfo(entity EntityWithRace) map[string]interface{} {
info := make(map[string]interface{})
// Basic entity info
info["entity_id"] = entity.GetID()
info["entity_name"] = entity.GetName()
// Race information
raceID := entity.GetRace()
raceInfo := ri.races.GetRaceInfo(raceID)
info["race"] = raceInfo
// Additional race-specific info
info["description"] = ri.utils.GetRaceDescription(raceID)
info["starting_location"] = ri.utils.GetRaceStartingLocation(raceID)
info["stat_modifiers"] = ri.utils.GetRaceStatModifiers(raceID)
info["aliases"] = ri.utils.GetRaceAliases(raceID)
info["compatible_races"] = ri.utils.GetCompatibleRaces(raceID)
return info
}
// ChangeEntityRace changes an entity's race with validation
func (ri *RaceIntegration) ChangeEntityRace(entity RaceAware, newRaceID int8) error {
if !ri.races.IsValidRaceID(newRaceID) {
return fmt.Errorf("invalid race ID: %d", newRaceID)
}
oldRaceID := entity.GetRace()
// Validate the race transition
if valid, reason := ri.utils.ValidateRaceTransition(oldRaceID, newRaceID); !valid {
return fmt.Errorf("race change not allowed: %s", reason)
}
// Perform the race change
entity.SetRace(newRaceID)
return nil
}
// GetRandomRaceForEntity returns a random race appropriate for an entity
func (ri *RaceIntegration) GetRandomRaceForEntity(alignment string) int8 {
return ri.utils.GetRandomRaceByAlignment(alignment)
}
// CheckRaceCompatibility checks if two entities' races are compatible
func (ri *RaceIntegration) CheckRaceCompatibility(entity1, entity2 RaceAware) bool {
race1 := entity1.GetRace()
race2 := entity2.GetRace()
if !ri.races.IsValidRaceID(race1) || !ri.races.IsValidRaceID(race2) {
return false
}
// Same race is always compatible
if race1 == race2 {
return true
}
// Check alignment compatibility
alignment1 := ri.races.GetRaceAlignment(race1)
alignment2 := ri.races.GetRaceAlignment(race2)
// Neutral races are compatible with everyone
if alignment1 == AlignmentNeutral || alignment2 == AlignmentNeutral {
return true
}
// Same alignment races are compatible
return alignment1 == alignment2
}
// FormatEntityRace returns a formatted race name for an entity
func (ri *RaceIntegration) FormatEntityRace(entity RaceAware, format string) string {
raceID := entity.GetRace()
return ri.utils.FormatRaceName(raceID, format)
}
// GetEntityRaceAlignment returns an entity's race alignment
func (ri *RaceIntegration) GetEntityRaceAlignment(entity RaceAware) string {
raceID := entity.GetRace()
return ri.races.GetRaceAlignment(raceID)
}
// IsEntityGoodRace checks if an entity is a good-aligned race
func (ri *RaceIntegration) IsEntityGoodRace(entity RaceAware) bool {
raceID := entity.GetRace()
return ri.races.IsGoodRace(raceID)
}
// IsEntityEvilRace checks if an entity is an evil-aligned race
func (ri *RaceIntegration) IsEntityEvilRace(entity RaceAware) bool {
raceID := entity.GetRace()
return ri.races.IsEvilRace(raceID)
}
// IsEntityNeutralRace checks if an entity is a neutral race
func (ri *RaceIntegration) IsEntityNeutralRace(entity RaceAware) bool {
raceID := entity.GetRace()
return ri.races.IsNeutralRace(raceID)
}
// GetEntitiesByRace filters entities by race
func (ri *RaceIntegration) GetEntitiesByRace(entities []RaceAware, raceID int8) []RaceAware {
result := make([]RaceAware, 0)
for _, entity := range entities {
if entity.GetRace() == raceID {
result = append(result, entity)
}
}
return result
}
// GetEntitiesByAlignment filters entities by race alignment
func (ri *RaceIntegration) GetEntitiesByAlignment(entities []RaceAware, alignment string) []RaceAware {
result := make([]RaceAware, 0)
for _, entity := range entities {
entityAlignment := ri.GetEntityRaceAlignment(entity)
if entityAlignment == alignment {
result = append(result, entity)
}
}
return result
}
// ValidateRaceForClass checks if a race/class combination is valid
func (ri *RaceIntegration) ValidateRaceForClass(raceID, classID int8) (bool, string) {
if !ri.races.IsValidRaceID(raceID) {
return false, "Invalid race"
}
// Use the utility function (which currently allows all combinations)
if ri.utils.ValidateRaceForClass(raceID, classID) {
return true, ""
}
raceName := ri.races.GetRaceNameCase(raceID)
return false, fmt.Sprintf("Race %s cannot be class %d", raceName, classID)
}
// GetRaceStartingStats returns the starting stats for a race
func (ri *RaceIntegration) GetRaceStartingStats(raceID int8) map[string]int16 {
baseStats := map[string]int16{
"strength": 50,
"stamina": 50,
"agility": 50,
"wisdom": 50,
"intelligence": 50,
}
// Apply racial modifiers
modifiers := ri.utils.GetRaceStatModifiers(raceID)
for statName, modifier := range modifiers {
if baseStat, exists := baseStats[statName]; exists {
baseStats[statName] = baseStat + int16(modifier)
}
}
return baseStats
}
// CreateRaceSpecificEntity creates entity data with race-specific properties
func (ri *RaceIntegration) CreateRaceSpecificEntity(raceID int8) map[string]interface{} {
if !ri.races.IsValidRaceID(raceID) {
return nil
}
entityData := make(map[string]interface{})
// Basic race info
entityData["race_id"] = raceID
entityData["race_name"] = ri.races.GetRaceNameCase(raceID)
entityData["alignment"] = ri.races.GetRaceAlignment(raceID)
// Starting stats
entityData["starting_stats"] = ri.GetRaceStartingStats(raceID)
// Starting location
entityData["starting_location"] = ri.utils.GetRaceStartingLocation(raceID)
// Race description
entityData["description"] = ri.utils.GetRaceDescription(raceID)
return entityData
}
// GetRaceSelectionData returns data for race selection UI
func (ri *RaceIntegration) GetRaceSelectionData() map[string]interface{} {
data := make(map[string]interface{})
// All available races
allRaces := ri.races.GetAllRaces()
raceList := make([]map[string]interface{}, 0, len(allRaces))
for raceID, friendlyName := range allRaces {
raceData := map[string]interface{}{
"id": raceID,
"name": friendlyName,
"alignment": ri.races.GetRaceAlignment(raceID),
"description": ri.utils.GetRaceDescription(raceID),
"stats": ri.GetRaceStartingStats(raceID),
}
raceList = append(raceList, raceData)
}
data["races"] = raceList
data["statistics"] = ri.utils.GetRaceStatistics()
return data
}
// Global race integration instance
var globalRaceIntegration *RaceIntegration
// GetGlobalRaceIntegration returns the global race integration helper
func GetGlobalRaceIntegration() *RaceIntegration {
if globalRaceIntegration == nil {
globalRaceIntegration = NewRaceIntegration()
}
return globalRaceIntegration
}

380
internal/races/manager.go Normal file
View File

@ -0,0 +1,380 @@
package races
import (
"fmt"
"sync"
)
// RaceManager provides high-level race management functionality
type RaceManager struct {
races *Races
utils *RaceUtils
integration *RaceIntegration
// Statistics tracking
raceUsageStats map[int8]int32 // Track how often each race is used
// Thread safety
mutex sync.RWMutex
}
// NewRaceManager creates a new race manager
func NewRaceManager() *RaceManager {
return &RaceManager{
races: GetGlobalRaces(),
utils: NewRaceUtils(),
integration: NewRaceIntegration(),
raceUsageStats: make(map[int8]int32),
}
}
// RegisterRaceUsage tracks race usage for statistics
func (rm *RaceManager) RegisterRaceUsage(raceID int8) {
if !rm.races.IsValidRaceID(raceID) {
return
}
rm.mutex.Lock()
defer rm.mutex.Unlock()
rm.raceUsageStats[raceID]++
}
// GetRaceUsageStats returns race usage statistics
func (rm *RaceManager) GetRaceUsageStats() map[int8]int32 {
rm.mutex.RLock()
defer rm.mutex.RUnlock()
// Return a copy to prevent external modification
stats := make(map[int8]int32)
for raceID, count := range rm.raceUsageStats {
stats[raceID] = count
}
return stats
}
// GetMostPopularRace returns the most frequently used race
func (rm *RaceManager) GetMostPopularRace() (int8, int32) {
rm.mutex.RLock()
defer rm.mutex.RUnlock()
var mostPopularRace int8 = -1
var maxUsage int32 = 0
for raceID, usage := range rm.raceUsageStats {
if usage > maxUsage {
maxUsage = usage
mostPopularRace = raceID
}
}
return mostPopularRace, maxUsage
}
// GetLeastPopularRace returns the least frequently used race
func (rm *RaceManager) GetLeastPopularRace() (int8, int32) {
rm.mutex.RLock()
defer rm.mutex.RUnlock()
var leastPopularRace int8 = -1
var minUsage int32 = -1
for raceID, usage := range rm.raceUsageStats {
if minUsage == -1 || usage < minUsage {
minUsage = usage
leastPopularRace = raceID
}
}
return leastPopularRace, minUsage
}
// ResetUsageStats clears all usage statistics
func (rm *RaceManager) ResetUsageStats() {
rm.mutex.Lock()
defer rm.mutex.Unlock()
rm.raceUsageStats = make(map[int8]int32)
}
// ProcessRaceCommand handles race-related commands
func (rm *RaceManager) ProcessRaceCommand(command string, args []string) (string, error) {
switch command {
case "list":
return rm.handleListCommand(args)
case "info":
return rm.handleInfoCommand(args)
case "random":
return rm.handleRandomCommand(args)
case "stats":
return rm.handleStatsCommand(args)
case "search":
return rm.handleSearchCommand(args)
default:
return "", fmt.Errorf("unknown race command: %s", command)
}
}
// handleListCommand lists races by criteria
func (rm *RaceManager) handleListCommand(args []string) (string, error) {
if len(args) == 0 {
// List all races
allRaces := rm.races.GetAllRaces()
result := "All Races:\n"
for raceID, friendlyName := range allRaces {
alignment := rm.races.GetRaceAlignment(raceID)
result += fmt.Sprintf("%d: %s (%s)\n", raceID, friendlyName, alignment)
}
return result, nil
}
// List races by alignment
alignment := args[0]
raceIDs := rm.races.GetRacesByAlignment(alignment)
if len(raceIDs) == 0 {
return fmt.Sprintf("No races found for alignment: %s", alignment), nil
}
result := fmt.Sprintf("%s Races:\n", alignment)
for _, raceID := range raceIDs {
friendlyName := rm.races.GetRaceNameCase(raceID)
result += fmt.Sprintf("%d: %s\n", raceID, friendlyName)
}
return result, nil
}
// handleInfoCommand provides detailed information about a race
func (rm *RaceManager) handleInfoCommand(args []string) (string, error) {
if len(args) == 0 {
return "", fmt.Errorf("race name or ID required")
}
// Try to parse as race name or ID
raceID := rm.utils.ParseRaceName(args[0])
if raceID == -1 {
return fmt.Sprintf("Invalid race: %s", args[0]), nil
}
raceInfo := rm.races.GetRaceInfo(raceID)
if !raceInfo["valid"].(bool) {
return fmt.Sprintf("Invalid race ID: %d", raceID), nil
}
result := fmt.Sprintf("Race Information:\n")
result += fmt.Sprintf("ID: %d\n", raceID)
result += fmt.Sprintf("Name: %s\n", raceInfo["display_name"])
result += fmt.Sprintf("Alignment: %s\n", raceInfo["alignment"])
result += fmt.Sprintf("Description: %s\n", rm.utils.GetRaceDescription(raceID))
result += fmt.Sprintf("Starting Location: %s\n", rm.utils.GetRaceStartingLocation(raceID))
// Add stat modifiers
modifiers := rm.utils.GetRaceStatModifiers(raceID)
if len(modifiers) > 0 {
result += "Stat Modifiers:\n"
for stat, modifier := range modifiers {
sign := "+"
if modifier < 0 {
sign = ""
}
result += fmt.Sprintf(" %s: %s%d\n", stat, sign, modifier)
}
}
// Add usage statistics if available
rm.mutex.RLock()
usage, hasUsage := rm.raceUsageStats[raceID]
rm.mutex.RUnlock()
if hasUsage {
result += fmt.Sprintf("Usage Count: %d\n", usage)
}
return result, nil
}
// handleRandomCommand generates random races
func (rm *RaceManager) handleRandomCommand(args []string) (string, error) {
alignment := AlignmentNeutral
if len(args) > 0 {
alignment = args[0]
}
raceID := rm.utils.GetRandomRaceByAlignment(alignment)
if raceID == -1 {
return "Failed to generate random race", nil
}
friendlyName := rm.races.GetRaceNameCase(raceID)
raceAlignment := rm.races.GetRaceAlignment(raceID)
return fmt.Sprintf("Random %s Race: %s (ID: %d)", raceAlignment, friendlyName, raceID), nil
}
// handleStatsCommand shows race system statistics
func (rm *RaceManager) handleStatsCommand(args []string) (string, error) {
systemStats := rm.utils.GetRaceStatistics()
usageStats := rm.GetRaceUsageStats()
result := "Race System Statistics:\n"
result += fmt.Sprintf("Total Races: %d\n", systemStats["total_races"])
result += fmt.Sprintf("Good Races: %d\n", systemStats["good_races"])
result += fmt.Sprintf("Evil Races: %d\n", systemStats["evil_races"])
result += fmt.Sprintf("Neutral Races: %d\n", systemStats["neutral_races"])
if len(usageStats) > 0 {
result += "\nUsage Statistics:\n"
mostPopular, maxUsage := rm.GetMostPopularRace()
leastPopular, minUsage := rm.GetLeastPopularRace()
if mostPopular != -1 {
mostPopularName := rm.races.GetRaceNameCase(mostPopular)
result += fmt.Sprintf("Most Popular: %s (%d uses)\n", mostPopularName, maxUsage)
}
if leastPopular != -1 {
leastPopularName := rm.races.GetRaceNameCase(leastPopular)
result += fmt.Sprintf("Least Popular: %s (%d uses)\n", leastPopularName, minUsage)
}
}
return result, nil
}
// handleSearchCommand searches for races by pattern
func (rm *RaceManager) handleSearchCommand(args []string) (string, error) {
if len(args) == 0 {
return "", fmt.Errorf("search pattern required")
}
pattern := args[0]
matchingRaces := rm.utils.GetRacesByPattern(pattern)
if len(matchingRaces) == 0 {
return fmt.Sprintf("No races found matching pattern: %s", pattern), nil
}
result := fmt.Sprintf("Races matching '%s':\n", pattern)
for _, raceID := range matchingRaces {
friendlyName := rm.races.GetRaceNameCase(raceID)
alignment := rm.races.GetRaceAlignment(raceID)
result += fmt.Sprintf("%d: %s (%s)\n", raceID, friendlyName, alignment)
}
return result, nil
}
// ValidateEntityRaces validates races for a collection of entities
func (rm *RaceManager) ValidateEntityRaces(entities []RaceAware) map[string]interface{} {
validationResults := make(map[string]interface{})
validCount := 0
invalidCount := 0
raceDistribution := make(map[int8]int)
for i, entity := range entities {
raceID := entity.GetRace()
isValid := rm.races.IsValidRaceID(raceID)
if isValid {
validCount++
raceDistribution[raceID]++
} else {
invalidCount++
}
// Track invalid entities
if !isValid {
if validationResults["invalid_entities"] == nil {
validationResults["invalid_entities"] = make([]map[string]interface{}, 0)
}
invalidList := validationResults["invalid_entities"].([]map[string]interface{})
invalidList = append(invalidList, map[string]interface{}{
"index": i,
"race_id": raceID,
})
validationResults["invalid_entities"] = invalidList
}
}
validationResults["total_entities"] = len(entities)
validationResults["valid_count"] = validCount
validationResults["invalid_count"] = invalidCount
validationResults["race_distribution"] = raceDistribution
return validationResults
}
// GetRaceRecommendations returns race recommendations for character creation
func (rm *RaceManager) GetRaceRecommendations(preferences map[string]interface{}) []int8 {
recommendations := make([]int8, 0)
// Check for alignment preference
if alignment, exists := preferences["alignment"]; exists {
if alignmentStr, ok := alignment.(string); ok {
raceIDs := rm.races.GetRacesByAlignment(alignmentStr)
recommendations = append(recommendations, raceIDs...)
}
}
// Check for specific stat preferences
if preferredStats, exists := preferences["preferred_stats"]; exists {
if stats, ok := preferredStats.([]string); ok {
allRaces := rm.races.GetAllRaces()
for raceID := range allRaces {
modifiers := rm.utils.GetRaceStatModifiers(raceID)
// Check if this race has bonuses in preferred stats
hasPreferredBonus := false
for _, preferredStat := range stats {
if modifier, exists := modifiers[preferredStat]; exists && modifier > 0 {
hasPreferredBonus = true
break
}
}
if hasPreferredBonus {
recommendations = append(recommendations, raceID)
}
}
}
}
// If no specific preferences, recommend popular races
if len(recommendations) == 0 {
// Get usage stats and recommend most popular races
usageStats := rm.GetRaceUsageStats()
if len(usageStats) > 0 {
// Sort by usage and take top races
// For simplicity, just return all races with usage > 0
for raceID, usage := range usageStats {
if usage > 0 {
recommendations = append(recommendations, raceID)
}
}
}
// If still no recommendations, return a default set
if len(recommendations) == 0 {
recommendations = []int8{RaceHuman, RaceHighElf, RaceDwarf, RaceDarkElf}
}
}
return recommendations
}
// Global race manager instance
var globalRaceManager *RaceManager
var initRaceManagerOnce sync.Once
// GetGlobalRaceManager returns the global race manager (singleton)
func GetGlobalRaceManager() *RaceManager {
initRaceManagerOnce.Do(func() {
globalRaceManager = NewRaceManager()
})
return globalRaceManager
}

387
internal/races/races.go Normal file
View File

@ -0,0 +1,387 @@
package races
import (
"math/rand"
"strings"
"sync"
)
// Races manages race information and lookups
// Converted from C++ Races class
type Races struct {
// Race name to ID mapping (uppercase keys)
raceMap map[string]int8
// ID to friendly name mapping
friendlyNameMap map[int8]string
// Alignment-based race lists for randomization
goodRaces []string
evilRaces []string
// Thread safety
mutex sync.RWMutex
}
// NewRaces creates a new races manager with all EQ2 races
// Converted from C++ Races::Races constructor
func NewRaces() *Races {
races := &Races{
raceMap: make(map[string]int8),
friendlyNameMap: make(map[int8]string),
goodRaces: make([]string, 0),
evilRaces: make([]string, 0),
}
races.initializeRaces()
return races
}
// initializeRaces sets up all race mappings
func (r *Races) initializeRaces() {
// Initialize race name to ID mappings (from C++ constructor)
r.raceMap[RaceNameBarbarian] = RaceBarbarian
r.raceMap[RaceNameDarkElf] = RaceDarkElf
r.raceMap[RaceNameDwarf] = RaceDwarf
r.raceMap[RaceNameErudite] = RaceErudite
r.raceMap[RaceNameFroglok] = RaceFroglok
r.raceMap[RaceNameGnome] = RaceGnome
r.raceMap[RaceNameHalfElf] = RaceHalfElf
r.raceMap[RaceNameHalfling] = RaceHalfling
r.raceMap[RaceNameHighElf] = RaceHighElf
r.raceMap[RaceNameHuman] = RaceHuman
r.raceMap[RaceNameIksar] = RaceIksar
r.raceMap[RaceNameKerra] = RaceKerra
r.raceMap[RaceNameOgre] = RaceOgre
r.raceMap[RaceNameRatonga] = RaceRatonga
r.raceMap[RaceNameTroll] = RaceTroll
r.raceMap[RaceNameWoodElf] = RaceWoodElf
r.raceMap[RaceNameFaeLight] = RaceFae
r.raceMap[RaceNameFaeDark] = RaceArasai
r.raceMap[RaceNameSarnak] = RaceSarnak
r.raceMap[RaceNameVampire] = RaceVampire
r.raceMap[RaceNameAerakyn] = RaceAerakyn
// Initialize friendly display names (from C++ constructor)
r.friendlyNameMap[RaceBarbarian] = DisplayNameBarbarian
r.friendlyNameMap[RaceDarkElf] = DisplayNameDarkElf
r.friendlyNameMap[RaceDwarf] = DisplayNameDwarf
r.friendlyNameMap[RaceErudite] = DisplayNameErudite
r.friendlyNameMap[RaceFroglok] = DisplayNameFroglok
r.friendlyNameMap[RaceGnome] = DisplayNameGnome
r.friendlyNameMap[RaceHalfElf] = DisplayNameHalfElf
r.friendlyNameMap[RaceHalfling] = DisplayNameHalfling
r.friendlyNameMap[RaceHighElf] = DisplayNameHighElf
r.friendlyNameMap[RaceHuman] = DisplayNameHuman
r.friendlyNameMap[RaceIksar] = DisplayNameIksar
r.friendlyNameMap[RaceKerra] = DisplayNameKerra
r.friendlyNameMap[RaceOgre] = DisplayNameOgre
r.friendlyNameMap[RaceRatonga] = DisplayNameRatonga
r.friendlyNameMap[RaceTroll] = DisplayNameTroll
r.friendlyNameMap[RaceWoodElf] = DisplayNameWoodElf
r.friendlyNameMap[RaceFae] = DisplayNameFae
r.friendlyNameMap[RaceArasai] = DisplayNameArasai
r.friendlyNameMap[RaceSarnak] = DisplayNameSarnak
r.friendlyNameMap[RaceVampire] = DisplayNameVampire
r.friendlyNameMap[RaceAerakyn] = DisplayNameAerakyn
// Initialize good races (from C++ race_map_good)
// "Neutral" races appear in both lists for /randomize functionality
r.goodRaces = []string{
RaceNameDwarf, // 0
RaceNameFaeLight, // 1
RaceNameFroglok, // 2
RaceNameHalfling, // 3
RaceNameHighElf, // 4
RaceNameWoodElf, // 5
RaceNameBarbarian, // 6 (neutral)
RaceNameErudite, // 7 (neutral)
RaceNameGnome, // 8 (neutral)
RaceNameHalfElf, // 9 (neutral)
RaceNameHuman, // 10 (neutral)
RaceNameKerra, // 11 (neutral)
RaceNameVampire, // 12 (neutral)
RaceNameAerakyn, // 13 (neutral)
}
// Initialize evil races (from C++ race_map_evil)
r.evilRaces = []string{
RaceNameFaeDark, // 0
RaceNameDarkElf, // 1
RaceNameIksar, // 2
RaceNameOgre, // 3
RaceNameRatonga, // 4
RaceNameSarnak, // 5
RaceNameTroll, // 6
RaceNameBarbarian, // 7 (neutral)
RaceNameErudite, // 8 (neutral)
RaceNameGnome, // 9 (neutral)
RaceNameHalfElf, // 10 (neutral)
RaceNameHuman, // 11 (neutral)
RaceNameKerra, // 12 (neutral)
RaceNameVampire, // 13 (neutral)
RaceNameAerakyn, // 14 (neutral)
}
}
// GetRaceID returns the race ID for a given race name
// Converted from C++ Races::GetRaceID
func (r *Races) GetRaceID(name string) int8 {
r.mutex.RLock()
defer r.mutex.RUnlock()
raceName := strings.ToUpper(strings.TrimSpace(name))
if raceID, exists := r.raceMap[raceName]; exists {
return raceID
}
return -1 // Invalid race
}
// GetRaceName returns the uppercase race name for a given ID
// Converted from C++ Races::GetRaceName
func (r *Races) GetRaceName(raceID int8) string {
r.mutex.RLock()
defer r.mutex.RUnlock()
// Search through race map to find the name
for name, id := range r.raceMap {
if id == raceID {
return name
}
}
return "" // Invalid race ID
}
// GetRaceNameCase returns the friendly display name for a given race ID
// Converted from C++ Races::GetRaceNameCase
func (r *Races) GetRaceNameCase(raceID int8) string {
r.mutex.RLock()
defer r.mutex.RUnlock()
if friendlyName, exists := r.friendlyNameMap[raceID]; exists {
return friendlyName
}
return "" // Invalid race ID
}
// GetRandomGoodRace returns a random good-aligned race ID
// Converted from C++ Races::GetRaceNameGood
func (r *Races) GetRandomGoodRace() int8 {
r.mutex.RLock()
defer r.mutex.RUnlock()
if len(r.goodRaces) == 0 {
return DefaultRaceID
}
randomIndex := rand.Intn(len(r.goodRaces))
raceName := r.goodRaces[randomIndex]
if raceID, exists := r.raceMap[raceName]; exists {
return raceID
}
return DefaultRaceID // Default to Human if error
}
// GetRandomEvilRace returns a random evil-aligned race ID
// Converted from C++ Races::GetRaceNameEvil
func (r *Races) GetRandomEvilRace() int8 {
r.mutex.RLock()
defer r.mutex.RUnlock()
if len(r.evilRaces) == 0 {
return DefaultRaceID
}
randomIndex := rand.Intn(len(r.evilRaces))
raceName := r.evilRaces[randomIndex]
if raceID, exists := r.raceMap[raceName]; exists {
return raceID
}
return DefaultRaceID // Default to Human if error
}
// IsValidRaceID checks if a race ID is valid
func (r *Races) IsValidRaceID(raceID int8) bool {
return raceID >= MinRaceID && raceID <= MaxRaceID
}
// GetAllRaces returns all race IDs and their friendly names
func (r *Races) GetAllRaces() map[int8]string {
r.mutex.RLock()
defer r.mutex.RUnlock()
result := make(map[int8]string)
for raceID, friendlyName := range r.friendlyNameMap {
result[raceID] = friendlyName
}
return result
}
// GetRacesByAlignment returns races filtered by alignment
func (r *Races) GetRacesByAlignment(alignment string) []int8 {
r.mutex.RLock()
defer r.mutex.RUnlock()
var raceNames []string
switch strings.ToLower(alignment) {
case AlignmentGood:
raceNames = r.goodRaces
case AlignmentEvil:
raceNames = r.evilRaces
default:
// Return all races for neutral or unknown alignment
result := make([]int8, 0, len(r.friendlyNameMap))
for raceID := range r.friendlyNameMap {
result = append(result, raceID)
}
return result
}
result := make([]int8, 0, len(raceNames))
for _, raceName := range raceNames {
if raceID, exists := r.raceMap[raceName]; exists {
result = append(result, raceID)
}
}
return result
}
// IsGoodRace checks if a race is considered good-aligned
func (r *Races) IsGoodRace(raceID int8) bool {
r.mutex.RLock()
defer r.mutex.RUnlock()
raceName := ""
for name, id := range r.raceMap {
if id == raceID {
raceName = name
break
}
}
if raceName == "" {
return false
}
for _, goodRace := range r.goodRaces {
if goodRace == raceName {
return true
}
}
return false
}
// IsEvilRace checks if a race is considered evil-aligned
func (r *Races) IsEvilRace(raceID int8) bool {
r.mutex.RLock()
defer r.mutex.RUnlock()
raceName := ""
for name, id := range r.raceMap {
if id == raceID {
raceName = name
break
}
}
if raceName == "" {
return false
}
for _, evilRace := range r.evilRaces {
if evilRace == raceName {
return true
}
}
return false
}
// IsNeutralRace checks if a race appears in both good and evil lists (neutral)
func (r *Races) IsNeutralRace(raceID int8) bool {
return r.IsGoodRace(raceID) && r.IsEvilRace(raceID)
}
// GetRaceAlignment returns the primary alignment of a race
func (r *Races) GetRaceAlignment(raceID int8) string {
if r.IsNeutralRace(raceID) {
return AlignmentNeutral
} else if r.IsGoodRace(raceID) {
return AlignmentGood
} else if r.IsEvilRace(raceID) {
return AlignmentEvil
}
return AlignmentNeutral // Default for invalid races
}
// GetRaceCount returns the total number of races
func (r *Races) GetRaceCount() int {
r.mutex.RLock()
defer r.mutex.RUnlock()
return len(r.friendlyNameMap)
}
// GetGoodRaceCount returns the number of good-aligned races
func (r *Races) GetGoodRaceCount() int {
r.mutex.RLock()
defer r.mutex.RUnlock()
return len(r.goodRaces)
}
// GetEvilRaceCount returns the number of evil-aligned races
func (r *Races) GetEvilRaceCount() int {
r.mutex.RLock()
defer r.mutex.RUnlock()
return len(r.evilRaces)
}
// GetRaceInfo returns comprehensive information about a race
func (r *Races) GetRaceInfo(raceID int8) map[string]interface{} {
r.mutex.RLock()
defer r.mutex.RUnlock()
info := make(map[string]interface{})
if !r.IsValidRaceID(raceID) {
info["valid"] = false
return info
}
info["valid"] = true
info["race_id"] = raceID
info["name"] = r.GetRaceName(raceID)
info["display_name"] = r.GetRaceNameCase(raceID)
info["alignment"] = r.GetRaceAlignment(raceID)
info["is_good"] = r.IsGoodRace(raceID)
info["is_evil"] = r.IsEvilRace(raceID)
info["is_neutral"] = r.IsNeutralRace(raceID)
return info
}
// Global races instance
var globalRaces *Races
var initRacesOnce sync.Once
// GetGlobalRaces returns the global races manager (singleton)
func GetGlobalRaces() *Races {
initRacesOnce.Do(func() {
globalRaces = NewRaces()
})
return globalRaces
}

351
internal/races/utils.go Normal file
View File

@ -0,0 +1,351 @@
package races
import (
"fmt"
"math/rand"
"strings"
)
// RaceUtils provides utility functions for race operations
type RaceUtils struct {
races *Races
}
// NewRaceUtils creates a new race utilities instance
func NewRaceUtils() *RaceUtils {
return &RaceUtils{
races: GetGlobalRaces(),
}
}
// ParseRaceName attempts to parse a race name from various input formats
func (ru *RaceUtils) ParseRaceName(input string) int8 {
if input == "" {
return -1
}
// Try direct lookup first
raceID := ru.races.GetRaceID(input)
if raceID != -1 {
return raceID
}
// Try with common variations
variations := []string{
strings.ToUpper(input),
strings.ReplaceAll(strings.ToUpper(input), " ", ""),
strings.ReplaceAll(strings.ToUpper(input), "_", ""),
strings.ReplaceAll(strings.ToUpper(input), "-", ""),
}
for _, variation := range variations {
if raceID := ru.races.GetRaceID(variation); raceID != -1 {
return raceID
}
}
// Try matching against friendly names (case insensitive)
inputLower := strings.ToLower(input)
allRaces := ru.races.GetAllRaces()
for raceID, friendlyName := range allRaces {
if strings.ToLower(friendlyName) == inputLower {
return raceID
}
}
return -1 // Not found
}
// FormatRaceName returns a properly formatted race name
func (ru *RaceUtils) FormatRaceName(raceID int8, format string) string {
switch strings.ToLower(format) {
case "display", "friendly", "proper":
return ru.races.GetRaceNameCase(raceID)
case "upper", "uppercase":
return ru.races.GetRaceName(raceID)
case "lower", "lowercase":
return strings.ToLower(ru.races.GetRaceName(raceID))
default:
return ru.races.GetRaceNameCase(raceID) // Default to friendly name
}
}
// GetRandomRaceByAlignment returns a random race for the specified alignment
func (ru *RaceUtils) GetRandomRaceByAlignment(alignment string) int8 {
switch strings.ToLower(alignment) {
case AlignmentGood:
return ru.races.GetRandomGoodRace()
case AlignmentEvil:
return ru.races.GetRandomEvilRace()
default:
// For neutral or any other alignment, pick from all races
allRaces := ru.races.GetAllRaces()
if len(allRaces) == 0 {
return DefaultRaceID
}
// Convert map to slice for random selection
raceIDs := make([]int8, 0, len(allRaces))
for raceID := range allRaces {
raceIDs = append(raceIDs, raceID)
}
return raceIDs[rand.Intn(len(raceIDs))]
}
}
// ValidateRaceForClass checks if a race is valid for a specific class
// This is a placeholder for future class-race restrictions
func (ru *RaceUtils) ValidateRaceForClass(raceID, classID int8) bool {
// TODO: Implement class-race restrictions when class system is available
// For now, all races can be all classes
return ru.races.IsValidRaceID(raceID)
}
// GetRaceDescription returns a description of the race
func (ru *RaceUtils) GetRaceDescription(raceID int8) string {
// This would typically come from a database or configuration
// For now, provide basic descriptions based on race
switch raceID {
case RaceHuman:
return "Versatile and adaptable, humans are found throughout Norrath."
case RaceBarbarian:
return "Hardy warriors from the frozen lands of Everfrost."
case RaceDarkElf:
return "Cunning and magical, the dark elves hail from Neriak."
case RaceDwarf:
return "Stout and strong, dwarves are master craftsmen and fighters."
case RaceErudite:
return "Intelligent and scholarly, erudites value knowledge above all."
case RaceFroglok:
return "Honorable amphibians who have overcome great adversity."
case RaceGnome:
return "Small but ingenious, gnomes are masters of tinkering and magic."
case RaceHalfElf:
return "Caught between two worlds, half elves combine human adaptability with elven grace."
case RaceHalfling:
return "Small and nimble, halflings are natural rogues and adventurers."
case RaceHighElf:
return "Noble and proud, high elves are paragons of magical prowess."
case RaceIksar:
return "Ancient lizardfolk with a proud warrior tradition."
case RaceKerra:
return "Feline humanoids known for their agility and curiosity."
case RaceOgre:
return "Massive and powerful, ogres are fearsome warriors."
case RaceRatonga:
return "Clever rodent-folk who excel at stealth and cunning."
case RaceTroll:
return "Large and brutish, trolls possess incredible strength and regeneration."
case RaceWoodElf:
return "Forest dwellers with unmatched skill in archery and nature magic."
case RaceFae:
return "Magical fairy-folk with wings and a connection to nature."
case RaceArasai:
return "Dark counterparts to the Fae, corrupted by shadow magic."
case RaceSarnak:
return "Draconic humanoids with scales and ancient wisdom."
case RaceVampire:
return "Undead beings with supernatural powers and bloodthirst."
case RaceAerakyn:
return "Dragon-blooded humanoids with draconic heritage and flight."
default:
return "An unknown race with mysterious origins."
}
}
// GetRaceStatModifiers returns racial stat modifiers for character creation
// This is a placeholder for future stat system integration
func (ru *RaceUtils) GetRaceStatModifiers(raceID int8) map[string]int8 {
// TODO: Implement racial stat modifiers when stat system is available
// This would typically come from database or configuration files
modifiers := make(map[string]int8)
// Example modifiers (these would need to be balanced and come from data)
switch raceID {
case RaceBarbarian:
modifiers["strength"] = 2
modifiers["stamina"] = 1
modifiers["intelligence"] = -1
case RaceDarkElf:
modifiers["intelligence"] = 2
modifiers["agility"] = 1
modifiers["wisdom"] = -1
case RaceDwarf:
modifiers["stamina"] = 2
modifiers["strength"] = 1
modifiers["agility"] = -1
case RaceErudite:
modifiers["intelligence"] = 3
modifiers["wisdom"] = 1
modifiers["strength"] = -2
case RaceGnome:
modifiers["intelligence"] = 2
modifiers["agility"] = 1
modifiers["strength"] = -2
case RaceHighElf:
modifiers["intelligence"] = 2
modifiers["wisdom"] = 1
modifiers["stamina"] = -1
case RaceOgre:
modifiers["strength"] = 3
modifiers["stamina"] = 2
modifiers["intelligence"] = -2
modifiers["agility"] = -1
case RaceTroll:
modifiers["strength"] = 2
modifiers["stamina"] = 3
modifiers["intelligence"] = -2
modifiers["wisdom"] = -1
default:
// Humans and other races have no modifiers (balanced)
break
}
return modifiers
}
// GetRaceStartingLocation returns the starting city/zone for a race
func (ru *RaceUtils) GetRaceStartingLocation(raceID int8) string {
// TODO: This would typically come from database configuration
switch raceID {
case RaceHuman, RaceBarbarian, RaceErudite, RaceKerra:
return "Qeynos"
case RaceDarkElf, RaceOgre, RaceRatonga, RaceTroll:
return "Freeport"
case RaceDwarf, RaceGnome, RaceHalfling:
return "New Halas"
case RaceHighElf, RaceWoodElf, RaceHalfElf:
return "Kelethin"
case RaceIksar, RaceSarnak:
return "Gorowyn"
case RaceFroglok:
return "Temple of Cazic-Thule"
case RaceFae:
return "Greater Faydark"
case RaceArasai:
return "Darklight Wood"
case RaceVampire:
return "Neriak"
case RaceAerakyn:
return "Draconic Starting Area"
default:
return "Qeynos" // Default starting location
}
}
// GetCompatibleRaces returns races that can interact peacefully
func (ru *RaceUtils) GetCompatibleRaces(raceID int8) []int8 {
// This is based on lore and alignment - races of similar alignment are generally compatible
alignment := ru.races.GetRaceAlignment(raceID)
return ru.races.GetRacesByAlignment(alignment)
}
// FormatRaceList returns a formatted string of race names
func (ru *RaceUtils) FormatRaceList(raceIDs []int8, separator string) string {
if len(raceIDs) == 0 {
return ""
}
names := make([]string, len(raceIDs))
for i, raceID := range raceIDs {
names[i] = ru.races.GetRaceNameCase(raceID)
}
return strings.Join(names, separator)
}
// GetRacesByPattern returns races matching a name pattern
func (ru *RaceUtils) GetRacesByPattern(pattern string) []int8 {
pattern = strings.ToLower(pattern)
result := make([]int8, 0)
allRaces := ru.races.GetAllRaces()
for raceID, friendlyName := range allRaces {
if strings.Contains(strings.ToLower(friendlyName), pattern) {
result = append(result, raceID)
}
}
return result
}
// ValidateRaceTransition checks if a race change is allowed
func (ru *RaceUtils) ValidateRaceTransition(fromRaceID, toRaceID int8) (bool, string) {
if !ru.races.IsValidRaceID(fromRaceID) {
return false, "Invalid source race"
}
if !ru.races.IsValidRaceID(toRaceID) {
return false, "Invalid target race"
}
if fromRaceID == toRaceID {
return false, "Cannot change to the same race"
}
// TODO: Implement specific race change restrictions when needed
// For now, allow all transitions
return true, ""
}
// GetRaceAliases returns common aliases for a race
func (ru *RaceUtils) GetRaceAliases(raceID int8) []string {
aliases := make([]string, 0)
switch raceID {
case RaceDarkElf:
aliases = append(aliases, "DE", "Dark Elf", "Teir'Dal")
case RaceHighElf:
aliases = append(aliases, "HE", "High Elf", "Koada'Dal")
case RaceWoodElf:
aliases = append(aliases, "WE", "Wood Elf", "Feir'Dal")
case RaceHalfElf:
aliases = append(aliases, "Half-Elf", "Half Elf", "Ayr'Dal")
case RaceFae:
aliases = append(aliases, "Fairy", "Pixie")
case RaceArasai:
aliases = append(aliases, "Dark Fae", "Shadow Fae")
case RaceVampire:
aliases = append(aliases, "Vamp", "Undead")
case RaceAerakyn:
aliases = append(aliases, "Dragon-kin", "Dragonborn")
}
// Always include the official names
aliases = append(aliases, ru.races.GetRaceName(raceID))
aliases = append(aliases, ru.races.GetRaceNameCase(raceID))
return aliases
}
// GetRaceStatistics returns statistics about the race system
func (ru *RaceUtils) GetRaceStatistics() map[string]interface{} {
stats := make(map[string]interface{})
stats["total_races"] = ru.races.GetRaceCount()
stats["good_races"] = ru.races.GetGoodRaceCount()
stats["evil_races"] = ru.races.GetEvilRaceCount()
// Count neutral races (appear in both lists)
neutralCount := 0
allRaces := ru.races.GetAllRaces()
for raceID := range allRaces {
if ru.races.IsNeutralRace(raceID) {
neutralCount++
}
}
stats["neutral_races"] = neutralCount
// Race distribution by alignment
alignmentDistribution := make(map[string][]string)
for raceID, friendlyName := range allRaces {
alignment := ru.races.GetRaceAlignment(raceID)
alignmentDistribution[alignment] = append(alignmentDistribution[alignment], friendlyName)
}
stats["alignment_distribution"] = alignmentDistribution
return stats
}

166
internal/spawn/README.md Normal file
View File

@ -0,0 +1,166 @@
# Spawn System
This package implements the EverQuest II spawn system, converted from the original C++ codebase to Go.
## Overview
The spawn system manages all entities that can appear in the game world, including NPCs, objects, widgets, signs, and ground spawns. It handles their positioning, movement, combat states, and interactions.
## Key Components
### Spawn (`spawn.go`)
The `Spawn` struct is the base class for all entities in the game world. It provides:
- **Basic Properties**: ID, name, level, position, heading
- **State Management**: Health, power, alive status, combat state
- **Movement System**: Position tracking, movement queues, pathfinding
- **Command System**: Interactive commands players can use
- **Loot System**: Item drops, coin rewards, loot distribution
- **Scripting Integration**: Lua script support for dynamic behavior
- **Thread Safety**: Atomic operations and mutexes for concurrent access
Key features:
- Thread-safe operations using atomic values and mutexes
- Extensible design allowing subclasses (NPC, Player, Object, etc.)
- Comprehensive state tracking with change notifications
- Support for temporary variables for Lua scripting
- Equipment and appearance management
- Group and faction relationships
### Spawn Lists (`spawn_lists.go`)
Manages spawn locations and spawn entries:
- **SpawnEntry**: Defines what can spawn at a location with configuration
- **SpawnLocation**: Represents a point in the world where spawns appear
- **SpawnLocationManager**: Manages collections of spawn locations
Features:
- Randomized spawn selection based on percentages
- Position offsets for spawn variety
- Respawn timing with random offsets
- Stat overrides for spawn customization
- Grid-based location management
## Architecture Notes
### Thread Safety
The spawn system is designed for high-concurrency access:
- Atomic values for frequently-accessed state flags
- Read-write mutexes for complex data structures
- Separate mutexes for different subsystems to minimize contention
### Memory Management
Go's garbage collector handles memory management, but the system includes:
- Proper cleanup methods for complex structures
- Resource pooling where appropriate
- Efficient data structures to minimize allocations
### Extensibility
The base Spawn struct is designed to be extended:
- Virtual method patterns using interfaces
- Type checking methods (IsNPC, IsPlayer, etc.)
- Extensible command and scripting systems
## TODO Items
Many features are marked with TODO comments for future implementation:
### High Priority
- **Zone System Integration**: Zone server references and notifications
- **Client System**: Player client connections and packet handling
- **Item System**: Complete item and equipment implementation
- **Combat System**: Damage calculation and combat mechanics
### Medium Priority
- **Lua Scripting**: Full Lua integration for spawn behavior
- **Movement System**: Pathfinding and advanced movement
- **Quest System**: Quest requirement checking and progression
- **Map System**: Collision detection and height maps
### Low Priority
- **Region System**: Area-based effects and triggers
- **Housing System**: Player housing integration
- **Transportation**: Mounts and vehicles
- **Advanced Physics**: Knockback and projectile systems
## Usage Examples
### Creating a Basic Spawn
```go
spawn := NewSpawn()
spawn.SetName("a goblin warrior")
spawn.SetLevel(10)
spawn.SetX(100.0)
spawn.SetY(0.0)
spawn.SetZ(200.0)
spawn.SetTotalHP(500)
spawn.SetHP(500)
```
### Managing Spawn Locations
```go
manager := NewSpawnLocationManager()
location := NewSpawnLocation()
location.SetPosition(100.0, 0.0, 200.0)
location.SetOffsets(5.0, 0.0, 5.0) // 5 unit random offset
entry := NewSpawnEntry()
entry.SpawnID = 12345
entry.SpawnPercentage = 75.0
entry.Respawn = 300 // 5 minutes
location.AddSpawnEntry(entry)
manager.AddLocation(1001, location)
```
### Adding Commands
```go
spawn.AddPrimaryEntityCommand(
"hail", // command name
10.0, // max distance
"hail_npc", // internal command
"Too far away", // error message
0, // cast time
0, // spell visual
true, // default allow
)
```
## Integration Points
The spawn system integrates with several other systems:
- **Database**: Loading and saving spawn data
- **Network**: Serializing spawn data for clients
- **Zone**: Spawn management within zones
- **Combat**: Damage and death handling
- **Scripting**: Lua event handling
- **Items**: Equipment and loot management
## Performance Considerations
- Position updates use atomic operations to minimize locking
- Command lists are copied on access to prevent race conditions
- Spawn changes are batched and sent efficiently to clients
- Memory usage is optimized for large numbers of concurrent spawns
## Migration from C++
This Go implementation maintains compatibility with the original C++ EQ2EMu spawn system while modernizing the architecture:
- Converted C-style arrays to Go slices
- Replaced manual memory management with garbage collection
- Used Go's concurrency primitives instead of platform-specific threading
- Maintained the same packet structures and network protocol
- Preserved the same database schema and data relationships
The API surface remains similar to ease porting of existing scripts and configurations.

1156
internal/spawn/spawn.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,446 @@
package spawn
import (
"sync"
)
// Spawn entry types
const (
SpawnEntryTypeNPC = 0
SpawnEntryTypeObject = 1
SpawnEntryTypeWidget = 2
SpawnEntryTypeSign = 3
SpawnEntryTypeGroundSpawn = 4
)
// SpawnEntry represents a possible spawn at a location with its configuration
type SpawnEntry struct {
SpawnEntryID int32 // Unique identifier for this spawn entry
SpawnLocationID int32 // ID of the location this entry belongs to
SpawnType int8 // Type of spawn (NPC, Object, Widget, etc.)
SpawnID int32 // ID of the actual spawn template
SpawnPercentage float32 // Chance this spawn will appear
Respawn int32 // Base respawn time in seconds
RespawnOffsetLow int32 // Minimum random offset for respawn
RespawnOffsetHigh int32 // Maximum random offset for respawn
DuplicatedSpawn bool // Whether this spawn can appear multiple times
ExpireTime int32 // Time before spawn expires (0 = permanent)
ExpireOffset int32 // Random offset for expire time
// Spawn location overrides - these override the base spawn's stats
LevelOverride int32 // Override spawn level
HPOverride int32 // Override spawn HP
MPOverride int32 // Override spawn MP/Power
StrengthOverride int32 // Override strength stat
StaminaOverride int32 // Override stamina stat
WisdomOverride int32 // Override wisdom stat
IntelligenceOverride int32 // Override intelligence stat
AgilityOverride int32 // Override agility stat
HeatOverride int32 // Override heat resistance
ColdOverride int32 // Override cold resistance
MagicOverride int32 // Override magic resistance
MentalOverride int32 // Override mental resistance
DivineOverride int32 // Override divine resistance
DiseaseOverride int32 // Override disease resistance
PoisonOverride int32 // Override poison resistance
DifficultyOverride int32 // Override encounter level/difficulty
}
// NewSpawnEntry creates a new spawn entry with default values
func NewSpawnEntry() *SpawnEntry {
return &SpawnEntry{
SpawnEntryID: 0,
SpawnLocationID: 0,
SpawnType: SpawnEntryTypeNPC,
SpawnID: 0,
SpawnPercentage: 100.0,
Respawn: 600, // 10 minutes default
RespawnOffsetLow: 0,
RespawnOffsetHigh: 0,
DuplicatedSpawn: true,
ExpireTime: 0,
ExpireOffset: 0,
LevelOverride: 0,
HPOverride: 0,
MPOverride: 0,
StrengthOverride: 0,
StaminaOverride: 0,
WisdomOverride: 0,
IntelligenceOverride: 0,
AgilityOverride: 0,
HeatOverride: 0,
ColdOverride: 0,
MagicOverride: 0,
MentalOverride: 0,
DivineOverride: 0,
DiseaseOverride: 0,
PoisonOverride: 0,
DifficultyOverride: 0,
}
}
// GetSpawnTypeName returns a human-readable name for the spawn type
func (se *SpawnEntry) GetSpawnTypeName() string {
switch se.SpawnType {
case SpawnEntryTypeNPC:
return "NPC"
case SpawnEntryTypeObject:
return "Object"
case SpawnEntryTypeWidget:
return "Widget"
case SpawnEntryTypeSign:
return "Sign"
case SpawnEntryTypeGroundSpawn:
return "GroundSpawn"
default:
return "Unknown"
}
}
// HasStatOverrides returns true if this entry has any stat overrides configured
func (se *SpawnEntry) HasStatOverrides() bool {
return se.LevelOverride != 0 || se.HPOverride != 0 || se.MPOverride != 0 ||
se.StrengthOverride != 0 || se.StaminaOverride != 0 || se.WisdomOverride != 0 ||
se.IntelligenceOverride != 0 || se.AgilityOverride != 0 || se.HeatOverride != 0 ||
se.ColdOverride != 0 || se.MagicOverride != 0 || se.MentalOverride != 0 ||
se.DivineOverride != 0 || se.DiseaseOverride != 0 || se.PoisonOverride != 0 ||
se.DifficultyOverride != 0
}
// GetActualRespawnTime calculates the actual respawn time including random offset
func (se *SpawnEntry) GetActualRespawnTime() int32 {
if se.RespawnOffsetLow == 0 && se.RespawnOffsetHigh == 0 {
return se.Respawn
}
// TODO: Implement random number generation
// For now, return base respawn time
return se.Respawn
}
// SpawnLocation represents a location in the world where spawns can appear
type SpawnLocation struct {
// Position data
X float32 // X coordinate in world space
Y float32 // Y coordinate in world space
Z float32 // Z coordinate in world space
Heading float32 // Direction the spawn faces
Pitch float32 // Pitch angle
Roll float32 // Roll angle
// Offset ranges for randomizing spawn positions
XOffset float32 // Random X offset range (+/-)
YOffset float32 // Random Y offset range (+/-)
ZOffset float32 // Random Z offset range (+/-)
// Location metadata
PlacementID int32 // Unique placement identifier
GridID int32 // Grid cell this location belongs to
Script string // Lua script to run for this location
Conditional int8 // Conditional flag for spawn logic
// Spawn management
Entities []*SpawnEntry // List of possible spawns at this location
TotalPercentage float32 // Sum of all spawn percentages
// Thread safety
mutex sync.RWMutex
}
// NewSpawnLocation creates a new spawn location with default values
func NewSpawnLocation() *SpawnLocation {
return &SpawnLocation{
X: 0.0,
Y: 0.0,
Z: 0.0,
Heading: 0.0,
Pitch: 0.0,
Roll: 0.0,
XOffset: 0.0,
YOffset: 0.0,
ZOffset: 0.0,
PlacementID: 0,
GridID: 0,
Script: "",
Conditional: 0,
Entities: make([]*SpawnEntry, 0),
TotalPercentage: 0.0,
}
}
// AddSpawnEntry adds a spawn entry to this location
func (sl *SpawnLocation) AddSpawnEntry(entry *SpawnEntry) {
if entry == nil {
return
}
sl.mutex.Lock()
defer sl.mutex.Unlock()
sl.Entities = append(sl.Entities, entry)
sl.TotalPercentage += entry.SpawnPercentage
}
// RemoveSpawnEntry removes a spawn entry by its ID
func (sl *SpawnLocation) RemoveSpawnEntry(entryID int32) bool {
sl.mutex.Lock()
defer sl.mutex.Unlock()
for i, entry := range sl.Entities {
if entry.SpawnEntryID == entryID {
// Remove from slice
sl.Entities = append(sl.Entities[:i], sl.Entities[i+1:]...)
sl.TotalPercentage -= entry.SpawnPercentage
return true
}
}
return false
}
// GetSpawnEntries returns a copy of all spawn entries
func (sl *SpawnLocation) GetSpawnEntries() []*SpawnEntry {
sl.mutex.RLock()
defer sl.mutex.RUnlock()
entries := make([]*SpawnEntry, len(sl.Entities))
copy(entries, sl.Entities)
return entries
}
// GetSpawnEntryCount returns the number of spawn entries at this location
func (sl *SpawnLocation) GetSpawnEntryCount() int {
sl.mutex.RLock()
defer sl.mutex.RUnlock()
return len(sl.Entities)
}
// GetSpawnEntryByID finds a spawn entry by its ID
func (sl *SpawnLocation) GetSpawnEntryByID(entryID int32) *SpawnEntry {
sl.mutex.RLock()
defer sl.mutex.RUnlock()
for _, entry := range sl.Entities {
if entry.SpawnEntryID == entryID {
return entry
}
}
return nil
}
// CalculateRandomPosition returns a randomized position within the offset ranges
// TODO: Implement proper random number generation
func (sl *SpawnLocation) CalculateRandomPosition() (float32, float32, float32) {
sl.mutex.RLock()
defer sl.mutex.RUnlock()
// For now, return base position
// In full implementation, would add random offsets within ranges
return sl.X, sl.Y, sl.Z
}
// SelectRandomSpawn selects a spawn entry based on spawn percentages
// Returns nil if no spawn is selected (based on random chance)
func (sl *SpawnLocation) SelectRandomSpawn() *SpawnEntry {
sl.mutex.RLock()
defer sl.mutex.RUnlock()
if len(sl.Entities) == 0 || sl.TotalPercentage <= 0 {
return nil
}
// TODO: Implement proper random selection based on percentages
// For now, return first entry if any exist
if len(sl.Entities) > 0 {
return sl.Entities[0]
}
return nil
}
// HasScript returns true if this location has a Lua script
func (sl *SpawnLocation) HasScript() bool {
return sl.Script != ""
}
// IsConditional returns true if this location has conditional spawning
func (sl *SpawnLocation) IsConditional() bool {
return sl.Conditional != 0
}
// GetDistance calculates distance from this location to specified coordinates
func (sl *SpawnLocation) GetDistance(x, y, z float32, ignoreY bool) float32 {
dx := sl.X - x
dy := sl.Y - y
dz := sl.Z - z
if ignoreY {
return float32(dx*dx + dz*dz)
}
return float32(dx*dx + dy*dy + dz*dz)
}
// SetPosition updates the location's position coordinates
func (sl *SpawnLocation) SetPosition(x, y, z float32) {
sl.mutex.Lock()
defer sl.mutex.Unlock()
sl.X = x
sl.Y = y
sl.Z = z
}
// SetRotation updates the location's rotation angles
func (sl *SpawnLocation) SetRotation(heading, pitch, roll float32) {
sl.mutex.Lock()
defer sl.mutex.Unlock()
sl.Heading = heading
sl.Pitch = pitch
sl.Roll = roll
}
// SetOffsets updates the location's position offset ranges
func (sl *SpawnLocation) SetOffsets(xOffset, yOffset, zOffset float32) {
sl.mutex.Lock()
defer sl.mutex.Unlock()
sl.XOffset = xOffset
sl.YOffset = yOffset
sl.ZOffset = zOffset
}
// RecalculateTotalPercentage recalculates the total spawn percentage
// Should be called if spawn entry percentages are modified directly
func (sl *SpawnLocation) RecalculateTotalPercentage() {
sl.mutex.Lock()
defer sl.mutex.Unlock()
sl.TotalPercentage = 0.0
for _, entry := range sl.Entities {
sl.TotalPercentage += entry.SpawnPercentage
}
}
// Cleanup releases resources used by this spawn location
func (sl *SpawnLocation) Cleanup() {
sl.mutex.Lock()
defer sl.mutex.Unlock()
// Clear spawn entries
sl.Entities = nil
sl.TotalPercentage = 0.0
}
// SpawnLocationManager manages collections of spawn locations
type SpawnLocationManager struct {
locations map[int32]*SpawnLocation // Key is placement ID
mutex sync.RWMutex
}
// NewSpawnLocationManager creates a new spawn location manager
func NewSpawnLocationManager() *SpawnLocationManager {
return &SpawnLocationManager{
locations: make(map[int32]*SpawnLocation),
}
}
// AddLocation adds a spawn location to the manager
func (slm *SpawnLocationManager) AddLocation(placementID int32, location *SpawnLocation) {
if location == nil {
return
}
slm.mutex.Lock()
defer slm.mutex.Unlock()
location.PlacementID = placementID
slm.locations[placementID] = location
}
// RemoveLocation removes a spawn location by placement ID
func (slm *SpawnLocationManager) RemoveLocation(placementID int32) bool {
slm.mutex.Lock()
defer slm.mutex.Unlock()
if location, exists := slm.locations[placementID]; exists {
location.Cleanup()
delete(slm.locations, placementID)
return true
}
return false
}
// GetLocation retrieves a spawn location by placement ID
func (slm *SpawnLocationManager) GetLocation(placementID int32) *SpawnLocation {
slm.mutex.RLock()
defer slm.mutex.RUnlock()
return slm.locations[placementID]
}
// GetAllLocations returns a copy of all spawn locations
func (slm *SpawnLocationManager) GetAllLocations() map[int32]*SpawnLocation {
slm.mutex.RLock()
defer slm.mutex.RUnlock()
locations := make(map[int32]*SpawnLocation)
for id, location := range slm.locations {
locations[id] = location
}
return locations
}
// GetLocationCount returns the number of spawn locations
func (slm *SpawnLocationManager) GetLocationCount() int {
slm.mutex.RLock()
defer slm.mutex.RUnlock()
return len(slm.locations)
}
// GetLocationsInRange returns all locations within the specified distance of coordinates
func (slm *SpawnLocationManager) GetLocationsInRange(x, y, z, maxDistance float32, ignoreY bool) []*SpawnLocation {
slm.mutex.RLock()
defer slm.mutex.RUnlock()
var locations []*SpawnLocation
maxDistanceSquared := maxDistance * maxDistance
for _, location := range slm.locations {
distance := location.GetDistance(x, y, z, ignoreY)
if distance <= maxDistanceSquared {
locations = append(locations, location)
}
}
return locations
}
// GetLocationsByGridID returns all locations in a specific grid
func (slm *SpawnLocationManager) GetLocationsByGridID(gridID int32) []*SpawnLocation {
slm.mutex.RLock()
defer slm.mutex.RUnlock()
var locations []*SpawnLocation
for _, location := range slm.locations {
if location.GridID == gridID {
locations = append(locations, location)
}
}
return locations
}
// Clear removes all spawn locations
func (slm *SpawnLocationManager) Clear() {
slm.mutex.Lock()
defer slm.mutex.Unlock()
// Cleanup all locations
for _, location := range slm.locations {
location.Cleanup()
}
slm.locations = make(map[int32]*SpawnLocation)
}

68
internal/spells/README.md Normal file
View File

@ -0,0 +1,68 @@
# Spells System
Complete spell system for EverQuest II server emulation with spell definitions, casting mechanics, effects, and processing.
## Components
**Core System:**
- **SpellData/Spell** - Spell definitions with properties, levels, effects, LUA data
- **SpellEffectManager** - Active effects management (buffs, debuffs, bonuses)
- **MasterSpellList/SpellBook** - Global registry and per-character spell collections
**Spell Processing:**
- **SpellProcess** - Real-time casting engine (50ms intervals) with timers, queues, interrupts
- **SpellTargeting** - All target types: self, single, group, AOE, PBAE with validation
- **SpellResourceChecker** - Power, health, concentration, savagery, dissonance management
- **SpellManager** - High-level coordinator integrating all systems
## Key Features
- **Real-time Processing**: Cast/recast timers, active spell tracking, interrupt handling
- **Comprehensive Targeting**: Range, LOS, spell criteria validation for all target types
- **Resource Management**: All EQ2 resource types with validation and consumption
- **Effect System**: 30 maintained effects, 45 spell effects, detrimental effects
- **Heroic Opportunities**: Solo/group coordination with timing
- **Thread Safety**: Concurrent access with proper mutexes
- **80+ Effect Types**: All spell modifications from original C++ system
## Usage
```go
// Create and use spell manager
spellManager := spells.NewSpellManager()
// Cast spell with full validation
err := spellManager.CastSpell(casterID, targetID, spellID)
if err != nil {
log.Printf("Spell failed: %v", err)
}
// Check spell readiness
canCast, reason := spellManager.CanCastSpell(casterID, targetID, spellID)
// Process spells in main loop
spellManager.ProcessSpells()
// Manage spell books
spellBook := spellManager.GetSpellBook(characterID)
spellBook.AddSpell(spell)
spellBook.SetSpellBarSlot(0, 1, spell)
// Effect management
sem := spells.NewSpellEffectManager()
sem.AddMaintainedEffect(maintainedEffect)
sem.AddSpellEffect(tempEffect)
```
## Files
**Core**: `constants.go`, `spell_data.go`, `spell.go`, `spell_effects.go`, `spell_manager.go`
**Processing**: `process_constants.go`, `spell_process.go`, `spell_targeting.go`, `spell_resources.go`
**Docs**: `README.md`, `SPELL_PROCESS.md`
## Integration
**Database**: Spell data loading, character spell book persistence
**Packet System**: Spell info serialization, effect updates
**Entity System**: SpellEffectManager embedded, stat integration
**LUA Scripting**: Custom spell behaviors, effect calculations

View File

@ -0,0 +1,112 @@
# Spell Processing System
Comprehensive spell casting engine managing all aspects of spell processing including timers, targeting, resource management, and heroic opportunities.
## Components
**SpellProcess** - Core engine managing active spells, cast/recast timers, interrupt queues, spell queues, and heroic opportunities
**SpellTargeting** - Target selection for all spell types (self, single, group, AOE, PBAE) with validation
**SpellResourceChecker** - Resource validation/consumption for power, health, concentration, savagery, dissonance
**SpellManager** - High-level coordinator integrating all systems with comprehensive casting API
## Key Data Structures
```go
// Active spell instance
type LuaSpell struct {
Spell *Spell // Spell definition
CasterID int32 // Casting entity
Targets []int32 // Target list
Timer SpellTimer // Duration/tick timing
NumCalls int32 // Tick count
Interrupted bool // Interrupt state
}
// Cast timing
type CastTimer struct {
CasterID int32 // Casting entity
SpellID int32 // Spell being cast
StartTime time.Time // Cast start
Duration time.Duration // Cast time
}
// Cooldown timing
type RecastTimer struct {
CasterID int32 // Entity with cooldown
SpellID int32 // Spell on cooldown
Duration time.Duration // Cooldown time
LinkedTimer int32 // Shared cooldown group
}
```
## Usage
```go
// Main spell processing loop (50ms intervals)
spellManager.ProcessSpells()
// Cast spell with full validation
err := spellManager.CastSpell(casterID, targetID, spellID)
// Interrupt casting
spellManager.InterruptSpell(entityID, spellID, errorCode, canceled, fromMovement)
// Queue management
spellManager.AddSpellToQueue(spellID, casterID, targetID, priority)
spellManager.RemoveSpellFromQueue(spellID, casterID)
// Status checks
ready := spellManager.IsSpellReady(spellID, casterID)
recastTime := spellManager.GetSpellRecastTime(spellID, casterID)
canCast, reason := spellManager.CanCastSpell(casterID, targetID, spellID)
// Resource checking
resourceChecker := spellManager.GetResourceChecker()
powerResult := resourceChecker.CheckPower(luaSpell, customPowerReq)
allResults := resourceChecker.CheckAllResources(luaSpell, 0, 0)
success := resourceChecker.ConsumeAllResources(luaSpell, 0, 0)
// Targeting
targeting := spellManager.GetTargeting()
targetResult := targeting.GetSpellTargets(luaSpell, options)
```
## Effect Types (80+)
**Stat Modifications**: Health, power, stats (STR/AGI/STA/INT/WIS), resistances, attack, mitigation
**Spell Modifications**: Cast time, power req, range, duration, resistibility, crit chance
**Actions**: Damage, healing, DOT/HOT, resurrect, summon, mount, invisibility
**Control**: Stun, root, mez, fear, charm, blind, kill
**Special**: Change race/size/title, faction, exp, tradeskill bonuses
## Target Types
**TargetTypeSelf** (0) - Self-only spells
**TargetTypeSingle** (1) - Single target with validation
**TargetTypeGroup** (2) - Group members
**TargetTypeGroupAE** (3) - Group area effect
**TargetTypeAE** (4) - True area effect
**TargetTypePBAE** (5) - Point blank area effect
## Interrupt System
**Causes**: Movement, damage, stun, mesmerize, fear, manual cancellation, out of range
**Processing**: Queued interrupts processed every cycle with proper cleanup
**Error Codes**: Match client expectations for proper UI feedback
## Performance
- **50ms Processing**: Matches client update expectations
- **Efficient Indexing**: Fast lookups by caster, spell, target
- **Thread Safety**: Concurrent access with proper locking
- **Memory Management**: Cleanup of expired timers and effects
- **Batch Operations**: Multiple resource checks/targeting in single calls
## Integration Points
**Entity System**: Caster/target info, position data, combat state
**Zone System**: Position validation, line-of-sight, spawn management
**Group System**: Group member targeting and coordination
**Database**: Persistent spell data, character spell books
**Packet System**: Client communication for spell states
**LUA System**: Custom spell scripting (future)

View File

@ -0,0 +1,140 @@
package spells
// Spell target types
const (
SpellTargetSelf = 0
SpellTargetEnemy = 1
SpellTargetGroupAE = 2
SpellTargetCasterPet = 3
SpellTargetEnemyPet = 4
SpellTargetEnemyCorpse = 5
SpellTargetGroupCorpse = 6
SpellTargetNone = 7
SpellTargetRaidAE = 8
SpellTargetOtherGroupAE = 9
)
// Spell book types
const (
SpellBookTypeSpell = 0
SpellBookTypeCombatArt = 1
SpellBookTypeAbility = 2
SpellBookTypeTradeskill = 3
SpellBookTypeNotShown = 4
)
// Spell cast types
const (
SpellCastTypeNormal = 0
SpellCastTypeToggle = 1
)
// Spell error codes
const (
SpellErrorNotEnoughKnowledge = 1
SpellErrorInterrupted = 2
SpellErrorTakeEffectMorePowerful = 3
SpellErrorTakeEffectSameSpell = 4
SpellErrorCannotCastDead = 5
SpellErrorNotAlive = 6
SpellErrorNotDead = 7
SpellErrorCannotCastSitting = 8
SpellErrorCannotCastUncon = 9
SpellErrorAlreadyCasting = 10
SpellErrorRecovering = 11
SpellErrorNonCombatOnly = 12
SpellErrorCannotCastStunned = 13
SpellErrorCannotCastStiffled = 14
SpellErrorCannotCastCharmed = 15
SpellErrorNotWhileMounted = 16
SpellErrorNotWhileFlying = 17
SpellErrorNotWhileClimbing = 18
SpellErrorNotReady = 19
SpellErrorCantSeeTarget = 20
SpellErrorIncorrectStance = 21
SpellErrorCannotCastFeignDeath = 22
SpellErrorInventoryFull = 23
SpellErrorNotEnoughCoin = 24
SpellErrorNotAllowedHere = 25
SpellErrorNotWhileCrafting = 26
SpellErrorOnlyWhenCrafting = 27
SpellErrorItemNotAttuned = 28
SpellErrorItemWornOut = 29
SpellErrorMustEquipWeapon = 30
SpellErrorWeaponBroken = 31
SpellErrorCannotCastFeared = 32
SpellErrorTargetImmuneHostile = 33
SpellErrorTargetImmuneBeneficial = 34
SpellErrorNoTauntSpells = 35
SpellErrorCannotUseInBattlegrounds = 36
SpellErrorCannotPrepare = 37
SpellErrorNoEligibleTarget = 38
SpellErrorNoTargetsInRange = 39
SpellErrorTooClose = 40
SpellErrorTooFarAway = 41
SpellErrorTargetTooWeak = 42
SpellErrorTargetTooPowerful = 43
SpellErrorTargetNotPlayer = 44
SpellErrorTargetNotNPC = 45
SpellErrorTargetNotOwner = 46
SpellErrorTargetNotGrouped = 47
SpellErrorCannotCastInCombat = 48
SpellErrorCannotCastOutOfCombat = 49
SpellErrorTargetNotCorrectType = 50
SpellErrorTargetNotCorrectClass = 51
SpellErrorTargetNotCorrectRace = 52
SpellErrorNoCaster = 53
SpellErrorCannotCastOnCorpse = 54
SpellErrorCannotCastOnGuild = 55
SpellErrorCannotCastOnRaid = 56
SpellErrorCannotCastOnGroup = 57
SpellErrorCannotCastOnSelf = 58
SpellErrorTargetNotInGroup = 59
SpellErrorTargetNotInRaid = 60
SpellErrorCannotCastThatOnYourself = 61
SpellErrorAbilityUnavailable = 62
SpellErrorCannotPrepareWhileCasting = 63
SpellErrorNoPowerRegaining = 64
)
// Control effect types (moved from entity package)
const (
ControlEffectStun = iota
ControlEffectRoot
ControlEffectMez
ControlEffectDaze
ControlEffectFear
ControlEffectSlow
ControlEffectSnare
ControlEffectCharm
)
const (
ControlMaxEffects = 8 // Maximum number of control effect types
)
// GivenByType represents how a spell was acquired
type GivenByType int8
const (
GivenByUnset GivenByType = 0
GivenByTradeskillClass GivenByType = 1
GivenBySpellScroll GivenByType = 2
GivenByAltAdvancement GivenByType = 3
GivenByRace GivenByType = 4
GivenByRacialInnate GivenByType = 5
GivenByRacialTradition GivenByType = 6
GivenByClass GivenByType = 7
GivenByCharacterTrait GivenByType = 8
GivenByFocusAbility GivenByType = 9
GivenByClassTraining GivenByType = 10
GivenByWarderSpell GivenByType = 11
)
// Spell component types for LUA data
const (
SpellLUADataTypeInt = 0
SpellLUADataTypeFloat = 1
SpellLUADataTypeBool = 2
SpellLUADataTypeString = 3
)

View File

@ -0,0 +1,304 @@
package spells
// Spell effect modification types - from SpellProcess.h
const (
ModifyHealth = 1
ModifyFocus = 2
ModifyDefense = 3
ModifyPower = 4
ModifySpeed = 5
ModifyInt = 6
ModifyWis = 7
ModifyStr = 8
ModifyAgi = 9
ModifySta = 10
ModifyColdResist = 11
ModifyHeatResist = 12
ModifyDiseaseResist = 13
ModifyPoisonResist = 14
ModifyMagicResist = 15
ModifyMentalResist = 16
ModifyDivineResist = 17
ModifyAttack = 18
ModifyMitigation = 19
ModifyAvoidance = 20
ModifyConcentration = 21
ModifyExp = 22
ModifyFaction = 23
ChangeSize = 24
ChangeRace = 25
ChangeLocation = 26
ChangeZone = 27
ChangePrefixTitle = 28
ChangeDeity = 29
ChangeLastName = 30
ModifyHaste = 31
ModifySkill = 32
ChangeTarget = 33
ChangeLevel = 34
ModifySpellCastTime = 35
ModifySpellPowerReq = 36
ModifySpellHealthReq = 37
ModifySpellRecovery = 38
ModifySpellRecastTime = 39
ModifySpellRadius = 40
ModifySpellAOETargets = 41
ModifySpellRange = 42
ModifySpellDuration = 43
ModifySpellResistibility = 44
ModifyDamage = 45
ModifyDelay = 46
ModifyTradeskillExp = 47
AddMount = 48
RemoveMount = 49
ModifySpellCritChance = 50
ModifyCritChance = 51
SummonItem = 52
ModifyJump = 53
ModifyFallSpeed = 54
InflictDamage = 55
AddDot = 56
RemoveDot = 57
HealTarget = 58
HealAOE = 59
InflictAOEDamage = 60
HealGroupAOE = 61
AddAOEDot = 62
RemoveAOEDot = 63
AddHot = 64
RemoveHot = 65
ModifyAggroRange = 66
BlindTarget = 67
UnblindTarget = 68
KillTarget = 69
ResurrectTarget = 70
ChangeSuffixTitle = 71
SummonPet = 72
ModifyHate = 73
AddReactiveHeal = 74
ModifyPowerRegen = 75
ModifyHPRegen = 76
FeignDeath = 77
ModifyVision = 78
Invisibility = 79
CharmTarget = 80
ModifyTradeskillDurability = 81
ModifyTradeskillProgress = 82
)
// Active spell states
const (
ActiveSpellNormal = 0
ActiveSpellAdd = 1
ActiveSpellRemove = 2
)
// Spell process constants
const (
GetValueBadValue = 0xFFFFFFFF
ProcessCheckInterval = 50 // milliseconds between process checks
)
// Cast timer states
const (
CastTimerActive = 0
CastTimerComplete = 1
CastTimerExpired = 2
CastTimerCanceled = 3
)
// Interrupt error codes
const (
InterruptErrorNone = 0
InterruptErrorMovement = 1
InterruptErrorDamage = 2
InterruptErrorStun = 3
InterruptErrorMesmerize = 4
InterruptErrorFear = 5
InterruptErrorRoot = 6
InterruptErrorCanceled = 7
InterruptErrorInvalidTarget = 8
InterruptErrorOutOfRange = 9
InterruptErrorInsufficientPower = 10
InterruptErrorInsufficientHealth = 11
InterruptErrorInsufficientConcentration = 12
)
// Spell queue priorities
const (
QueuePriorityLow = 0
QueuePriorityNormal = 1
QueuePriorityHigh = 2
QueuePriorityUrgent = 3
)
// Heroic Opportunity states
const (
HeroicOpInactive = 0
HeroicOpActive = 1
HeroicOpComplete = 2
HeroicOpFailed = 3
HeroicOpCanceled = 4
)
// Resource check types
const (
ResourceCheckPower = 1
ResourceCheckHealth = 2
ResourceCheckConcentration = 3
ResourceCheckSavagery = 4
ResourceCheckDissonance = 5
)
// Spell targeting types
const (
TargetTypeSelf = 0
TargetTypeSingle = 1
TargetTypeGroup = 2
TargetTypeGroupAE = 3
TargetTypeAE = 4
TargetTypePBAE = 5 // Point Blank Area Effect
TargetTypeCorpse = 6
TargetTypeItem = 7
TargetTypeLocation = 8
TargetTypeNone = 9
)
// Spell resist types
const (
ResistTypeNone = 0
ResistTypeMagic = 1
ResistTypeDivine = 2
ResistTypeMental = 3
ResistTypeCold = 4
ResistTypeHeat = 5
ResistTypeDisease = 6
ResistTypePoison = 7
ResistTypeArcane = 8
ResistTypeNoxious = 9
ResistTypeElemental = 10
)
// Spell damage types
const (
DamageTypeSlashing = 0
DamageTypeCrushing = 1
DamageTypePiercing = 2
DamageTypeBurning = 3
DamageTypeFreezing = 4
DamageTypeAcid = 5
DamageTypePoison = 6
DamageTypeDisease = 7
DamageTypeMental = 8
DamageTypeDivine = 9
DamageTypeMagic = 10
)
// Spell effect duration types
const (
DurationTypeInstant = 0
DurationTypeTemporary = 1
DurationTypePermanent = 2
DurationTypeUntilCanceled = 3
DurationTypeConditional = 4
)
// Maximum values for spell system limits
const (
MaxCastTimers = 1000 // Maximum number of active cast timers
MaxRecastTimers = 5000 // Maximum number of active recast timers
MaxActiveSpells = 10000 // Maximum number of active spells
MaxQueuedSpells = 50 // Maximum spells per player queue
MaxSpellTargets = 100 // Maximum targets per spell
MaxInterrupts = 500 // Maximum queued interrupts
MaxHeroicOps = 100 // Maximum active heroic opportunities
)
// Spell book constants
const (
SpellBookTabGeneral = 0
SpellBookTabCombatArts = 1
SpellBookTabSpells = 2
SpellBookTabTradeskills = 3
SpellBookTabReligious = 4
SpellBookTabTempSpells = 5
SpellBookTabCharacteristics = 6
SpellBookTabKnowledgeSpells = 7
SpellBookTabHeroicOps = 8
)
// Spell effect categories for organization
const (
EffectCategoryBuff = "Buff"
EffectCategoryDebuff = "Debuff"
EffectCategoryDamage = "Damage"
EffectCategoryHealing = "Healing"
EffectCategorySummon = "Summon"
EffectCategoryTransport = "Transport"
EffectCategoryUtility = "Utility"
EffectCategoryControl = "Control"
)
// Spell component types
const (
ComponentTypeNone = 0
ComponentTypeVerbal = 1
ComponentTypeSomatic = 2
ComponentTypeMaterial = 3
ComponentTypeFocus = 4
ComponentTypeDivine = 5
)
// Spell school types
const (
SchoolTypeGeneral = 0
SchoolTypeElemental = 1
SchoolTypeSpiritual = 2
SchoolTypeArcane = 3
SchoolTypeNature = 4
SchoolTypeTemporal = 5
)
// Casting requirement flags
const (
RequireLineOfSight = 1 << 0
RequireNotMoving = 1 << 1
RequireNotInCombat = 1 << 2
RequireTargetAlive = 1 << 3
RequireTargetDead = 1 << 4
RequireGrouped = 1 << 5
RequireNotGrouped = 1 << 6
RequireGuild = 1 << 7
RequirePeaceful = 1 << 8
)
// Spell failure reasons
const (
FailureReasonNone = 0
FailureReasonInsufficientPower = 1
FailureReasonInsufficientHealth = 2
FailureReasonInsufficientConc = 3
FailureReasonInterrupted = 4
FailureReasonOutOfRange = 5
FailureReasonInvalidTarget = 6
FailureReasonResisted = 7
FailureReasonImmune = 8
FailureReasonBlocked = 9
FailureReasonReflected = 10
FailureReasonAbsorbed = 11
FailureReasonFizzled = 12
FailureReasonMissed = 13
FailureReasonRequirementNotMet = 14
)
// Special spell flags for unique behaviors
const (
SpellFlagCannotBeResisted = 1 << 0
SpellFlagCannotBeReflected = 1 << 1
SpellFlagCannotBeAbsorbed = 1 << 2
SpellFlagIgnoreImmunity = 1 << 3
SpellFlagBypassProtections = 1 << 4
SpellFlagAlwaysHits = 1 << 5
SpellFlagCanCritical = 1 << 6
SpellFlagNoInterrupt = 1 << 7
)

655
internal/spells/spell.go Normal file
View File

@ -0,0 +1,655 @@
package spells
import (
"fmt"
"sync"
)
// Spell represents a complete spell with data, levels, effects, and LUA data
// This is the main spell class converted from C++ Spell class
type Spell struct {
// Core spell data
data *SpellData
// Spell progression and requirements
levels []*LevelArray // Level requirements by class
effects []*SpellDisplayEffect // Display effects for tooltips
luaData []*LUAData // LUA script data
// Computed properties (cached for performance)
healSpell bool // Cached: is this a healing spell
buffSpell bool // Cached: is this a buff spell
damageSpell bool // Cached: is this a damage spell
controlSpell bool // Cached: is this a control spell
offenseSpell bool // Cached: is this an offensive spell
copiedSpell bool // Whether this is a copied/derived spell
// Runtime state
stayLocked bool // Whether spell should stay locked
// Thread safety
mutex sync.RWMutex
}
// NewSpell creates a new spell with default spell data
func NewSpell() *Spell {
return &Spell{
data: NewSpellData(),
levels: make([]*LevelArray, 0),
effects: make([]*SpellDisplayEffect, 0),
luaData: make([]*LUAData, 0),
healSpell: false,
buffSpell: false,
damageSpell: false,
controlSpell: false,
offenseSpell: false,
copiedSpell: false,
stayLocked: false,
}
}
// NewSpellFromData creates a new spell with existing spell data
func NewSpellFromData(spellData *SpellData) *Spell {
s := NewSpell()
s.data = spellData
s.computeSpellProperties()
return s
}
// NewSpellCopy creates a copy of an existing spell (for spell upgrades/variants)
func NewSpellCopy(hostSpell *Spell, uniqueSpell bool) *Spell {
if hostSpell == nil {
return NewSpell()
}
hostSpell.mutex.RLock()
defer hostSpell.mutex.RUnlock()
s := &Spell{
data: hostSpell.data.Clone(),
levels: make([]*LevelArray, 0),
effects: make([]*SpellDisplayEffect, 0),
luaData: make([]*LUAData, 0),
healSpell: hostSpell.healSpell,
buffSpell: hostSpell.buffSpell,
damageSpell: hostSpell.damageSpell,
controlSpell: hostSpell.controlSpell,
offenseSpell: hostSpell.offenseSpell,
copiedSpell: true,
stayLocked: hostSpell.stayLocked,
}
// Copy levels
for _, level := range hostSpell.levels {
newLevel := &LevelArray{
AdventureClass: level.AdventureClass,
TradeskillClass: level.TradeskillClass,
SpellLevel: level.SpellLevel,
ClassicSpellLevel: level.ClassicSpellLevel,
}
s.levels = append(s.levels, newLevel)
}
// Copy effects
for _, effect := range hostSpell.effects {
newEffect := &SpellDisplayEffect{
Percentage: effect.Percentage,
Subbullet: effect.Subbullet,
Description: effect.Description,
NeedsDBSave: effect.NeedsDBSave,
}
s.effects = append(s.effects, newEffect)
}
// Copy LUA data
for _, lua := range hostSpell.luaData {
newLua := &LUAData{
Type: lua.Type,
IntValue: lua.IntValue,
BoolValue: lua.BoolValue,
FloatValue: lua.FloatValue,
StringValue: lua.StringValue,
StringValue2: lua.StringValue2,
IntValue2: lua.IntValue2,
FloatValue2: lua.FloatValue2,
StringHelper: lua.StringHelper,
NeedsDBSave: lua.NeedsDBSave,
}
s.luaData = append(s.luaData, newLua)
}
// If unique spell, generate new ID
if uniqueSpell {
// TODO: Generate unique spell ID when spell ID management is implemented
// s.data.SetID(GenerateUniqueSpellID())
}
return s
}
// GetSpellData returns the core spell data (thread-safe)
func (s *Spell) GetSpellData() *SpellData {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.data
}
// GetSpellID returns the spell's unique ID
func (s *Spell) GetSpellID() int32 {
return s.data.GetID()
}
// GetName returns the spell's name
func (s *Spell) GetName() string {
return s.data.GetName()
}
// GetDescription returns the spell's description
func (s *Spell) GetDescription() string {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.data.Description
}
// GetSpellTier returns the spell's tier
func (s *Spell) GetSpellTier() int8 {
return s.data.GetTier()
}
// GetSpellDuration returns the spell's duration
func (s *Spell) GetSpellDuration() int32 {
return s.data.GetDuration()
}
// GetSpellIcon returns the spell's icon ID
func (s *Spell) GetSpellIcon() int16 {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.data.Icon
}
// GetSpellIconBackdrop returns the spell's icon backdrop
func (s *Spell) GetSpellIconBackdrop() int16 {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.data.IconBackdrop
}
// GetSpellIconHeroicOp returns the spell's heroic opportunity icon
func (s *Spell) GetSpellIconHeroicOp() int16 {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.data.IconHeroicOp
}
// AddSpellLevel adds a level requirement for a specific class
func (s *Spell) AddSpellLevel(adventureClass, tradeskillClass int8, level int16, classicLevel float32) {
s.mutex.Lock()
defer s.mutex.Unlock()
levelArray := &LevelArray{
AdventureClass: adventureClass,
TradeskillClass: tradeskillClass,
SpellLevel: level,
ClassicSpellLevel: classicLevel,
}
s.levels = append(s.levels, levelArray)
}
// AddSpellEffect adds a display effect to the spell
func (s *Spell) AddSpellEffect(percentage, subbullet int8, description string) {
s.mutex.Lock()
defer s.mutex.Unlock()
effect := &SpellDisplayEffect{
Percentage: percentage,
Subbullet: subbullet,
Description: description,
NeedsDBSave: true,
}
s.effects = append(s.effects, effect)
}
// AddSpellLuaData adds LUA data to the spell
func (s *Spell) AddSpellLuaData(dataType int8, intValue, intValue2 int32, floatValue, floatValue2 float32, boolValue bool, stringValue, stringValue2, helper string) {
s.mutex.Lock()
defer s.mutex.Unlock()
luaData := &LUAData{
Type: dataType,
IntValue: intValue,
BoolValue: boolValue,
FloatValue: floatValue,
StringValue: stringValue,
StringValue2: stringValue2,
IntValue2: intValue2,
FloatValue2: floatValue2,
StringHelper: helper,
NeedsDBSave: true,
}
s.luaData = append(s.luaData, luaData)
}
// Convenience methods for adding specific LUA data types
// AddSpellLuaDataInt adds integer LUA data
func (s *Spell) AddSpellLuaDataInt(value, value2 int32, helper string) {
s.AddSpellLuaData(SpellLUADataTypeInt, value, value2, 0.0, 0.0, false, "", "", helper)
}
// AddSpellLuaDataFloat adds float LUA data
func (s *Spell) AddSpellLuaDataFloat(value, value2 float32, helper string) {
s.AddSpellLuaData(SpellLUADataTypeFloat, 0, 0, value, value2, false, "", "", helper)
}
// AddSpellLuaDataBool adds boolean LUA data
func (s *Spell) AddSpellLuaDataBool(value bool, helper string) {
s.AddSpellLuaData(SpellLUADataTypeBool, 0, 0, 0.0, 0.0, value, "", "", helper)
}
// AddSpellLuaDataString adds string LUA data
func (s *Spell) AddSpellLuaDataString(value, value2, helper string) {
s.AddSpellLuaData(SpellLUADataTypeString, 0, 0, 0.0, 0.0, false, value, value2, helper)
}
// GetSpellLevels returns the spell level requirements
func (s *Spell) GetSpellLevels() []*LevelArray {
s.mutex.RLock()
defer s.mutex.RUnlock()
// Return a copy to prevent external modification
levels := make([]*LevelArray, len(s.levels))
copy(levels, s.levels)
return levels
}
// GetSpellEffects returns the spell display effects
func (s *Spell) GetSpellEffects() []*SpellDisplayEffect {
s.mutex.RLock()
defer s.mutex.RUnlock()
// Return a copy to prevent external modification
effects := make([]*SpellDisplayEffect, len(s.effects))
copy(effects, s.effects)
return effects
}
// GetSpellEffectSafe returns a spell effect safely by index
func (s *Spell) GetSpellEffectSafe(index int) *SpellDisplayEffect {
s.mutex.RLock()
defer s.mutex.RUnlock()
if index < 0 || index >= len(s.effects) {
return nil
}
return s.effects[index]
}
// GetLUAData returns the spell's LUA data
func (s *Spell) GetLUAData() []*LUAData {
s.mutex.RLock()
defer s.mutex.RUnlock()
// Return a copy to prevent external modification
luaData := make([]*LUAData, len(s.luaData))
copy(luaData, s.luaData)
return luaData
}
// Spell classification methods (cached for performance)
// IsHealSpell returns whether this is a healing spell
func (s *Spell) IsHealSpell() bool {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.healSpell
}
// IsBuffSpell returns whether this is a buff spell
func (s *Spell) IsBuffSpell() bool {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.buffSpell
}
// IsDamageSpell returns whether this is a damage spell
func (s *Spell) IsDamageSpell() bool {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.damageSpell
}
// IsControlSpell returns whether this is a control spell
func (s *Spell) IsControlSpell() bool {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.controlSpell
}
// IsOffenseSpell returns whether this is an offensive spell
func (s *Spell) IsOffenseSpell() bool {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.offenseSpell
}
// IsCopiedSpell returns whether this is a copied spell
func (s *Spell) IsCopiedSpell() bool {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.copiedSpell
}
// GetStayLocked returns whether the spell should stay locked
func (s *Spell) GetStayLocked() bool {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.stayLocked
}
// StayLocked sets whether the spell should stay locked
func (s *Spell) StayLocked(val bool) {
s.mutex.Lock()
defer s.mutex.Unlock()
s.stayLocked = val
}
// Cast type checking methods
// CastWhileStunned returns whether spell can be cast while stunned
func (s *Spell) CastWhileStunned() bool {
return s.data.CanCastWhileStunned()
}
// CastWhileMezzed returns whether spell can be cast while mezzed
func (s *Spell) CastWhileMezzed() bool {
return s.data.CanCastWhileMezzed()
}
// CastWhileStifled returns whether spell can be cast while stifled
func (s *Spell) CastWhileStifled() bool {
return s.data.CanCastWhileStifled()
}
// CastWhileFeared returns whether spell can be cast while feared
func (s *Spell) CastWhileFeared() bool {
return s.data.CanCastWhileFeared()
}
// Requirement checking methods
// GetLevelRequired returns the required level for a specific player
func (s *Spell) GetLevelRequired(playerClass int8, tradeskillClass int8) int16 {
s.mutex.RLock()
defer s.mutex.RUnlock()
for _, level := range s.levels {
if level.AdventureClass == playerClass || level.TradeskillClass == tradeskillClass {
return level.SpellLevel
}
}
return 0 // No specific requirement found
}
// GetHPRequired returns HP required to cast (could be modified by entity stats)
func (s *Spell) GetHPRequired() int16 {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.data.HPReq
}
// GetPowerRequired returns power required to cast
func (s *Spell) GetPowerRequired() float32 {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.data.PowerReq
}
// GetSavageryRequired returns savagery required to cast
func (s *Spell) GetSavageryRequired() int16 {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.data.SavageryReq
}
// GetDissonanceRequired returns dissonance required to cast
func (s *Spell) GetDissonanceRequired() int16 {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.data.DissonanceReq
}
// computeSpellProperties analyzes the spell to determine its classification
func (s *Spell) computeSpellProperties() {
// This would analyze spell effects, target types, etc. to determine spell classification
// For now, use basic logic based on spell data
s.buffSpell = s.data.IsBuffSpell()
s.controlSpell = s.data.IsControlSpell()
s.offenseSpell = s.data.IsOffenseSpell()
s.healSpell = s.data.IsHealSpell()
s.damageSpell = s.data.IsDamageSpell()
}
// String returns a string representation of the spell
func (s *Spell) String() string {
return fmt.Sprintf("Spell[ID=%d, Name=%s, Tier=%d]",
s.GetSpellID(), s.GetName(), s.GetSpellTier())
}
// LuaSpell represents an active spell instance with runtime state
// This is converted from the C++ LuaSpell class
type LuaSpell struct {
// Core identification
Spell *Spell // Reference to the spell definition
CasterID int32 // ID of the entity casting this spell
// Targeting
InitialTarget int32 // Original target ID
Targets []int32 // Current target IDs
// Timing and state
Timer SpellTimer // Timer for duration/tick tracking
NumCalls int32 // Number of times spell has ticked
Restored bool // Whether this spell was restored from DB
SlotPos int16 // Spell book slot position
// Runtime flags
Interrupted bool // Whether spell was interrupted
Deleted bool // Whether spell is marked for deletion
HasProc bool // Whether spell has proc effects
CasterCharID int32 // Character ID of caster (for cross-zone)
DamageRemaining int32 // Remaining damage for DOT spells
EffectBitmask int32 // Bitmask of active effects
// Spell-specific data
CustomFunction string // Custom LUA function name
ResurrectHP float32 // HP to restore on resurrect
ResurrectPower float32 // Power to restore on resurrect
// Thread safety
mutex sync.RWMutex
}
// SpellTimer represents timing information for an active spell
type SpellTimer struct {
StartTime int64 // When the spell started (milliseconds)
Duration int32 // Total duration in milliseconds
SetAtTrigger int64 // Time when timer was set/triggered
}
// NewLuaSpell creates a new LuaSpell instance
func NewLuaSpell(spell *Spell, casterID int32) *LuaSpell {
return &LuaSpell{
Spell: spell,
CasterID: casterID,
InitialTarget: 0,
Targets: make([]int32, 0),
Timer: SpellTimer{},
NumCalls: 0,
Restored: false,
SlotPos: 0,
Interrupted: false,
Deleted: false,
HasProc: false,
CasterCharID: 0,
DamageRemaining: 0,
EffectBitmask: 0,
CustomFunction: "",
ResurrectHP: 0.0,
ResurrectPower: 0.0,
}
}
// GetTargets returns a copy of the target list
func (ls *LuaSpell) GetTargets() []int32 {
ls.mutex.RLock()
defer ls.mutex.RUnlock()
targets := make([]int32, len(ls.Targets))
copy(targets, ls.Targets)
return targets
}
// AddTarget adds a target to the spell
func (ls *LuaSpell) AddTarget(targetID int32) {
ls.mutex.Lock()
defer ls.mutex.Unlock()
// Check if target already exists
for _, id := range ls.Targets {
if id == targetID {
return
}
}
ls.Targets = append(ls.Targets, targetID)
}
// RemoveTarget removes a target from the spell
func (ls *LuaSpell) RemoveTarget(targetID int32) bool {
ls.mutex.Lock()
defer ls.mutex.Unlock()
for i, id := range ls.Targets {
if id == targetID {
ls.Targets = append(ls.Targets[:i], ls.Targets[i+1:]...)
return true
}
}
return false
}
// HasTarget checks if the spell targets a specific entity
func (ls *LuaSpell) HasTarget(targetID int32) bool {
ls.mutex.RLock()
defer ls.mutex.RUnlock()
for _, id := range ls.Targets {
if id == targetID {
return true
}
}
return false
}
// GetTargetCount returns the number of targets
func (ls *LuaSpell) GetTargetCount() int {
ls.mutex.RLock()
defer ls.mutex.RUnlock()
return len(ls.Targets)
}
// ClearTargets removes all targets from the spell
func (ls *LuaSpell) ClearTargets() {
ls.mutex.Lock()
defer ls.mutex.Unlock()
ls.Targets = ls.Targets[:0]
}
// SetCustomFunction sets a custom LUA function for this spell instance
func (ls *LuaSpell) SetCustomFunction(functionName string) {
ls.mutex.Lock()
defer ls.mutex.Unlock()
ls.CustomFunction = functionName
}
// GetCustomFunction returns the custom LUA function name
func (ls *LuaSpell) GetCustomFunction() string {
ls.mutex.RLock()
defer ls.mutex.RUnlock()
return ls.CustomFunction
}
// MarkForDeletion marks the spell for removal
func (ls *LuaSpell) MarkForDeletion() {
ls.mutex.Lock()
defer ls.mutex.Unlock()
ls.Deleted = true
}
// IsDeleted returns whether the spell is marked for deletion
func (ls *LuaSpell) IsDeleted() bool {
ls.mutex.RLock()
defer ls.mutex.RUnlock()
return ls.Deleted
}
// SetInterrupted marks the spell as interrupted
func (ls *LuaSpell) SetInterrupted(interrupted bool) {
ls.mutex.Lock()
defer ls.mutex.Unlock()
ls.Interrupted = interrupted
}
// IsInterrupted returns whether the spell was interrupted
func (ls *LuaSpell) IsInterrupted() bool {
ls.mutex.RLock()
defer ls.mutex.RUnlock()
return ls.Interrupted
}
// SetResurrectValues sets HP and power restoration values for resurrect spells
func (ls *LuaSpell) SetResurrectValues(hp, power float32) {
ls.mutex.Lock()
defer ls.mutex.Unlock()
ls.ResurrectHP = hp
ls.ResurrectPower = power
}
// GetResurrectValues returns HP and power restoration values
func (ls *LuaSpell) GetResurrectValues() (float32, float32) {
ls.mutex.RLock()
defer ls.mutex.RUnlock()
return ls.ResurrectHP, ls.ResurrectPower
}
// String returns a string representation of the LuaSpell
func (ls *LuaSpell) String() string {
ls.mutex.RLock()
defer ls.mutex.RUnlock()
spellName := "Unknown"
if ls.Spell != nil {
spellName = ls.Spell.GetName()
}
return fmt.Sprintf("LuaSpell[%s, Caster=%d, Targets=%d]",
spellName, ls.CasterID, len(ls.Targets))
}

View File

@ -0,0 +1,434 @@
package spells
import (
"sync"
)
// LevelArray represents spell level requirements for different classes
type LevelArray struct {
AdventureClass int8 // Adventure class ID
TradeskillClass int8 // Tradeskill class ID
SpellLevel int16 // Required level
ClassicSpellLevel float32 // Classic spell level calculation
}
// SpellDisplayEffect represents a displayed effect in spell descriptions
type SpellDisplayEffect struct {
Percentage int8 // Effect percentage
Subbullet int8 // Subbullet indicator
Description string // Effect description text
NeedsDBSave bool // Whether this needs database saving
}
// LUAData represents Lua script data for spells
type LUAData struct {
Type int8 // Data type (int, float, bool, string)
IntValue int32 // Integer value
BoolValue bool // Boolean value
FloatValue float32 // Float value
StringValue string // String value
StringValue2 string // Second string value
IntValue2 int32 // Second integer value
FloatValue2 float32 // Second float value
StringHelper string // Helper string for identification
NeedsDBSave bool // Whether this needs database saving
}
// SpellData contains all core spell information
// This is the main spell data structure converted from C++ SpellData
type SpellData struct {
// Basic identification
SpellBookType int32 // Type of spell book this belongs to
ID int32 // Unique spell ID
InheritedSpellID int32 // ID of spell this inherits from
Name string // Spell name
Description string // Spell description
// Visual information
Icon int16 // Spell icon ID
IconHeroicOp int16 // Heroic opportunity icon
IconBackdrop int16 // Icon backdrop
SpellVisual int32 // Visual effect ID
// Classification
Type int16 // Spell type
SpellType int8 // Additional spell type classification
ClassSkill int32 // Required class skill
MinClassSkillReq int16 // Minimum class skill requirement
MasterySkill int32 // Mastery skill required
TSLocIndex int8 // Tradeskill location index
Tier int8 // Spell tier
NumLevels int8 // Number of levels this spell has
// Resource requirements
HPReq int16 // Health points required
HPUpkeep int16 // Health points upkeep
PowerReq float32 // Power required to cast
PowerByLevel bool // Power requirement scales by level
PowerUpkeep int16 // Power upkeep cost
SavageryReq int16 // Savagery required
SavageryUpkeep int16 // Savagery upkeep
DissonanceReq int16 // Dissonance required
DissonanceUpkeep int16 // Dissonance upkeep
ReqConcentration int16 // Concentration required
// Percentage-based requirements
PowerReqPercent int8 // Power requirement as percentage of max
HPReqPercent int8 // HP requirement as percentage of max
SavageryReqPercent int8 // Savagery requirement as percentage
DissonanceReqPercent int8 // Dissonance requirement as percentage
// Targeting and range
TargetType int8 // Type of target (self, enemy, group, etc.)
Range float32 // Casting range
MinRange float32 // Minimum casting range
Radius float32 // Area of effect radius
MaxAOETargets int16 // Maximum AoE targets
FriendlySpell int8 // Whether this is a friendly spell
// Timing
CastTime int16 // Cast time in deciseconds
OrigCastTime int16 // Original cast time (before modifications)
Recovery float32 // Recovery time
Recast float32 // Recast delay
LinkedTimer int32 // Linked timer ID
CallFrequency int32 // How often spell effect is called
// Duration and resistibility
Duration1 int32 // Primary duration
Duration2 int32 // Secondary duration
Resistibility float32 // How resistible the spell is
DurationUntilCancel bool // Duration lasts until cancelled
// Combat and effect properties
HitBonus float32 // Hit bonus provided
CanEffectRaid int8 // Can affect raid members
AffectOnlyGroupMembers int8 // Only affects group members
GroupSpell int8 // Is a group spell
DetType int8 // Detrimental type
Incurable bool // Cannot be cured
ControlEffectType int8 // Type of control effect
// Behavioral flags
CastType int8 // Cast type (normal, toggle)
CastingFlags int32 // Various casting flags
CastWhileMoving bool // Can cast while moving
PersistThroughDeath bool // Persists through death
NotMaintained bool // Not a maintained spell
IsAA bool // Is an Alternate Advancement ability
CanFizzle bool // Can fizzle on cast
Interruptable bool // Can be interrupted
IsActive bool // Spell is active/enabled
// Savage bar (for certain spell types)
SavageBar int8 // Savage bar requirement
SavageBarSlot int8 // Savage bar slot
// Messages
SuccessMessage string // Message on successful cast
FadeMessage string // Message when spell fades
FadeMessageOthers string // Fade message for others
EffectMessage string // Effect message
// Scripting
LuaScript string // Lua script filename
// Versioning and classification
DisplaySpellTier int8 // Displayed tier
SOESpellCRC int32 // SOE spell CRC
SpellNameCRC int32 // Spell name CRC
TypeGroupSpellID int32 // Type group spell ID
GivenBy string // Description of how spell was obtained
GivenByType GivenByType // Type of how spell was obtained
// Thread safety
mutex sync.RWMutex
}
// NewSpellData creates a new SpellData with default values
func NewSpellData() *SpellData {
return &SpellData{
SpellBookType: SpellBookTypeSpell,
ID: 0,
InheritedSpellID: 0,
Name: "",
Description: "",
Icon: 0,
IconHeroicOp: 0,
IconBackdrop: 0,
SpellVisual: 0,
Type: 0,
SpellType: 0,
ClassSkill: 0,
MinClassSkillReq: 0,
MasterySkill: 0,
TSLocIndex: 0,
Tier: 1,
NumLevels: 1,
HPReq: 0,
HPUpkeep: 0,
PowerReq: 0.0,
PowerByLevel: false,
PowerUpkeep: 0,
SavageryReq: 0,
SavageryUpkeep: 0,
DissonanceReq: 0,
DissonanceUpkeep: 0,
ReqConcentration: 0,
PowerReqPercent: 0,
HPReqPercent: 0,
SavageryReqPercent: 0,
DissonanceReqPercent: 0,
TargetType: SpellTargetSelf,
Range: 0.0,
MinRange: 0.0,
Radius: 0.0,
MaxAOETargets: 0,
FriendlySpell: 1,
CastTime: 0,
OrigCastTime: 0,
Recovery: 0.0,
Recast: 0.0,
LinkedTimer: 0,
CallFrequency: 0,
Duration1: 0,
Duration2: 0,
Resistibility: 0.0,
DurationUntilCancel: false,
HitBonus: 0.0,
CanEffectRaid: 0,
AffectOnlyGroupMembers: 0,
GroupSpell: 0,
DetType: 0,
Incurable: false,
ControlEffectType: 0,
CastType: SpellCastTypeNormal,
CastingFlags: 0,
CastWhileMoving: false,
PersistThroughDeath: false,
NotMaintained: false,
IsAA: false,
CanFizzle: true,
Interruptable: true,
IsActive: true,
SavageBar: 0,
SavageBarSlot: 0,
SuccessMessage: "",
FadeMessage: "",
FadeMessageOthers: "",
EffectMessage: "",
LuaScript: "",
DisplaySpellTier: 1,
SOESpellCRC: 0,
SpellNameCRC: 0,
TypeGroupSpellID: 0,
GivenBy: "",
GivenByType: GivenByUnset,
}
}
// GetID returns the spell ID (thread-safe)
func (sd *SpellData) GetID() int32 {
sd.mutex.RLock()
defer sd.mutex.RUnlock()
return sd.ID
}
// SetID updates the spell ID (thread-safe)
func (sd *SpellData) SetID(id int32) {
sd.mutex.Lock()
defer sd.mutex.Unlock()
sd.ID = id
}
// GetName returns the spell name (thread-safe)
func (sd *SpellData) GetName() string {
sd.mutex.RLock()
defer sd.mutex.RUnlock()
return sd.Name
}
// SetName updates the spell name (thread-safe)
func (sd *SpellData) SetName(name string) {
sd.mutex.Lock()
defer sd.mutex.Unlock()
sd.Name = name
}
// GetTier returns the spell tier
func (sd *SpellData) GetTier() int8 {
sd.mutex.RLock()
defer sd.mutex.RUnlock()
return sd.Tier
}
// GetDuration returns the primary spell duration
func (sd *SpellData) GetDuration() int32 {
sd.mutex.RLock()
defer sd.mutex.RUnlock()
return sd.Duration1
}
// GetCastTime returns the cast time
func (sd *SpellData) GetCastTime() int16 {
sd.mutex.RLock()
defer sd.mutex.RUnlock()
return sd.CastTime
}
// GetTargetType returns the target type
func (sd *SpellData) GetTargetType() int8 {
sd.mutex.RLock()
defer sd.mutex.RUnlock()
return sd.TargetType
}
// GetRange returns the casting range
func (sd *SpellData) GetRange() float32 {
sd.mutex.RLock()
defer sd.mutex.RUnlock()
return sd.Range
}
// IsHealSpell determines if this is a healing spell
func (sd *SpellData) IsHealSpell() bool {
// TODO: Implement based on spell effects or type classification
return false
}
// IsBuffSpell determines if this is a buff spell
func (sd *SpellData) IsBuffSpell() bool {
// TODO: Implement based on spell effects or duration
return sd.GetDuration() > 0 && sd.FriendlySpell == 1
}
// IsDamageSpell determines if this is a damage spell
func (sd *SpellData) IsDamageSpell() bool {
// TODO: Implement based on spell effects
return false
}
// IsControlSpell determines if this is a control spell
func (sd *SpellData) IsControlSpell() bool {
sd.mutex.RLock()
defer sd.mutex.RUnlock()
return sd.ControlEffectType > 0
}
// IsOffenseSpell determines if this is an offensive spell
func (sd *SpellData) IsOffenseSpell() bool {
sd.mutex.RLock()
defer sd.mutex.RUnlock()
return sd.FriendlySpell == 0
}
// CanCastWhileStunned returns whether spell can be cast while stunned
func (sd *SpellData) CanCastWhileStunned() bool {
// Check casting flags for stun immunity
return (sd.CastingFlags & 0x01) != 0
}
// CanCastWhileMezzed returns whether spell can be cast while mezzed
func (sd *SpellData) CanCastWhileMezzed() bool {
// Check casting flags for mez immunity
return (sd.CastingFlags & 0x02) != 0
}
// CanCastWhileStifled returns whether spell can be cast while stifled
func (sd *SpellData) CanCastWhileStifled() bool {
// Check casting flags for stifle immunity
return (sd.CastingFlags & 0x04) != 0
}
// CanCastWhileFeared returns whether spell can be cast while feared
func (sd *SpellData) CanCastWhileFeared() bool {
// Check casting flags for fear immunity
return (sd.CastingFlags & 0x08) != 0
}
// Clone creates a deep copy of the SpellData
func (sd *SpellData) Clone() *SpellData {
sd.mutex.RLock()
defer sd.mutex.RUnlock()
clone := &SpellData{
SpellBookType: sd.SpellBookType,
ID: sd.ID,
InheritedSpellID: sd.InheritedSpellID,
Name: sd.Name,
Description: sd.Description,
Icon: sd.Icon,
IconHeroicOp: sd.IconHeroicOp,
IconBackdrop: sd.IconBackdrop,
SpellVisual: sd.SpellVisual,
Type: sd.Type,
SpellType: sd.SpellType,
ClassSkill: sd.ClassSkill,
MinClassSkillReq: sd.MinClassSkillReq,
MasterySkill: sd.MasterySkill,
TSLocIndex: sd.TSLocIndex,
Tier: sd.Tier,
NumLevels: sd.NumLevels,
HPReq: sd.HPReq,
HPUpkeep: sd.HPUpkeep,
PowerReq: sd.PowerReq,
PowerByLevel: sd.PowerByLevel,
PowerUpkeep: sd.PowerUpkeep,
SavageryReq: sd.SavageryReq,
SavageryUpkeep: sd.SavageryUpkeep,
DissonanceReq: sd.DissonanceReq,
DissonanceUpkeep: sd.DissonanceUpkeep,
ReqConcentration: sd.ReqConcentration,
PowerReqPercent: sd.PowerReqPercent,
HPReqPercent: sd.HPReqPercent,
SavageryReqPercent: sd.SavageryReqPercent,
DissonanceReqPercent: sd.DissonanceReqPercent,
TargetType: sd.TargetType,
Range: sd.Range,
MinRange: sd.MinRange,
Radius: sd.Radius,
MaxAOETargets: sd.MaxAOETargets,
FriendlySpell: sd.FriendlySpell,
CastTime: sd.CastTime,
OrigCastTime: sd.OrigCastTime,
Recovery: sd.Recovery,
Recast: sd.Recast,
LinkedTimer: sd.LinkedTimer,
CallFrequency: sd.CallFrequency,
Duration1: sd.Duration1,
Duration2: sd.Duration2,
Resistibility: sd.Resistibility,
DurationUntilCancel: sd.DurationUntilCancel,
HitBonus: sd.HitBonus,
CanEffectRaid: sd.CanEffectRaid,
AffectOnlyGroupMembers: sd.AffectOnlyGroupMembers,
GroupSpell: sd.GroupSpell,
DetType: sd.DetType,
Incurable: sd.Incurable,
ControlEffectType: sd.ControlEffectType,
CastType: sd.CastType,
CastingFlags: sd.CastingFlags,
CastWhileMoving: sd.CastWhileMoving,
PersistThroughDeath: sd.PersistThroughDeath,
NotMaintained: sd.NotMaintained,
IsAA: sd.IsAA,
CanFizzle: sd.CanFizzle,
Interruptable: sd.Interruptable,
IsActive: sd.IsActive,
SavageBar: sd.SavageBar,
SavageBarSlot: sd.SavageBarSlot,
SuccessMessage: sd.SuccessMessage,
FadeMessage: sd.FadeMessage,
FadeMessageOthers: sd.FadeMessageOthers,
EffectMessage: sd.EffectMessage,
LuaScript: sd.LuaScript,
DisplaySpellTier: sd.DisplaySpellTier,
SOESpellCRC: sd.SOESpellCRC,
SpellNameCRC: sd.SpellNameCRC,
TypeGroupSpellID: sd.TypeGroupSpellID,
GivenBy: sd.GivenBy,
GivenByType: sd.GivenByType,
}
return clone
}

View File

@ -0,0 +1,621 @@
package spells
import (
"sync"
"time"
)
// BonusValues represents a stat bonus from equipment, spells, or other sources
// Moved from entity package to centralize spell-related structures
type BonusValues struct {
SpellID int32 // ID of spell providing this bonus
Tier int8 // Tier of the bonus
Type int16 // Type of bonus (stat type)
Value float32 // Bonus value
ClassReq int64 // Required class bitmask
RaceReq []int16 // Required race IDs
FactionReq []int16 // Required faction IDs
// TODO: Add LuaSpell reference when spell system is implemented
// LuaSpell *LuaSpell // Associated Lua spell
}
// NewBonusValues creates a new bonus value entry
func NewBonusValues(spellID int32, bonusType int16, value float32) *BonusValues {
return &BonusValues{
SpellID: spellID,
Tier: 1,
Type: bonusType,
Value: value,
ClassReq: 0,
RaceReq: make([]int16, 0),
FactionReq: make([]int16, 0),
}
}
// MeetsRequirements checks if an entity meets the requirements for this bonus
func (bv *BonusValues) MeetsRequirements(entityClass int64, race int16, factionID int32) bool {
// Check class requirement
if bv.ClassReq != 0 && (entityClass&bv.ClassReq) == 0 {
return false
}
// Check race requirement
if len(bv.RaceReq) > 0 {
raceMatch := false
for _, reqRace := range bv.RaceReq {
if reqRace == race {
raceMatch = true
break
}
}
if !raceMatch {
return false
}
}
// Check faction requirement
if len(bv.FactionReq) > 0 {
factionMatch := false
for _, reqFaction := range bv.FactionReq {
if reqFaction == int16(factionID) {
factionMatch = true
break
}
}
if !factionMatch {
return false
}
}
return true
}
// MaintainedEffects represents a buff that is actively maintained on an entity
// Moved from entity package to centralize spell-related structures
type MaintainedEffects struct {
Name [60]byte // Name of the spell
Target int32 // Target entity ID
TargetType int8 // Type of target
SpellID int32 // Spell ID
InheritedSpellID int32 // Inherited spell ID (for spell upgrades)
SlotPos int32 // Slot position in maintained effects
Icon int16 // Icon to display
IconBackdrop int16 // Icon backdrop
ConcUsed int8 // Concentration used
Tier int8 // Spell tier
TotalTime float32 // Total duration of effect
ExpireTimestamp int32 // When the effect expires
// TODO: Add LuaSpell reference when spell system is implemented
// Spell *LuaSpell // Associated Lua spell
}
// NewMaintainedEffects creates a new maintained effect
func NewMaintainedEffects(name string, spellID int32, duration float32) *MaintainedEffects {
effect := &MaintainedEffects{
Target: 0,
TargetType: 0,
SpellID: spellID,
InheritedSpellID: 0,
SlotPos: 0,
Icon: 0,
IconBackdrop: 0,
ConcUsed: 0,
Tier: 1,
TotalTime: duration,
ExpireTimestamp: int32(time.Now().Unix()) + int32(duration),
}
// Copy name to fixed-size array
nameBytes := []byte(name)
copy(effect.Name[:], nameBytes)
return effect
}
// GetName returns the spell name as a string
func (me *MaintainedEffects) GetName() string {
// Find the null terminator
nameBytes := me.Name[:]
for i, b := range nameBytes {
if b == 0 {
return string(nameBytes[:i])
}
}
return string(nameBytes)
}
// IsExpired checks if the maintained effect has expired
func (me *MaintainedEffects) IsExpired() bool {
if me.TotalTime <= 0 {
return false // Permanent effect
}
return int32(time.Now().Unix()) >= me.ExpireTimestamp
}
// GetTimeRemaining returns the time remaining for this effect
func (me *MaintainedEffects) GetTimeRemaining() float32 {
if me.TotalTime <= 0 {
return -1 // Permanent effect
}
remaining := float32(me.ExpireTimestamp - int32(time.Now().Unix()))
if remaining < 0 {
return 0
}
return remaining
}
// SpellEffects represents a temporary spell effect on an entity
// Moved from entity package to centralize spell-related structures
type SpellEffects struct {
SpellID int32 // Spell ID
InheritedSpellID int32 // Inherited spell ID
// TODO: Add Entity reference when implemented
// Caster *Entity // Entity that cast the spell
CasterID int32 // ID of caster entity (temporary)
TotalTime float32 // Total duration
ExpireTimestamp int32 // When effect expires
Icon int16 // Icon to display
IconBackdrop int16 // Icon backdrop
Tier int8 // Spell tier
// TODO: Add LuaSpell reference when spell system is implemented
// Spell *LuaSpell // Associated Lua spell
}
// NewSpellEffects creates a new spell effect
func NewSpellEffects(spellID int32, casterID int32, duration float32) *SpellEffects {
return &SpellEffects{
SpellID: spellID,
InheritedSpellID: 0,
CasterID: casterID,
TotalTime: duration,
ExpireTimestamp: int32(time.Now().Unix()) + int32(duration),
Icon: 0,
IconBackdrop: 0,
Tier: 1,
}
}
// IsExpired checks if the spell effect has expired
func (se *SpellEffects) IsExpired() bool {
if se.TotalTime <= 0 {
return false // Permanent effect
}
return int32(time.Now().Unix()) >= se.ExpireTimestamp
}
// GetTimeRemaining returns the time remaining for this effect
func (se *SpellEffects) GetTimeRemaining() float32 {
if se.TotalTime <= 0 {
return -1 // Permanent effect
}
remaining := float32(se.ExpireTimestamp - int32(time.Now().Unix()))
if remaining < 0 {
return 0
}
return remaining
}
// DetrimentalEffects represents a debuff or harmful effect on an entity
// Moved from entity package to centralize spell-related structures
type DetrimentalEffects struct {
SpellID int32 // Spell ID
InheritedSpellID int32 // Inherited spell ID
// TODO: Add Entity reference when implemented
// Caster *Entity // Entity that cast the spell
CasterID int32 // ID of caster entity (temporary)
ExpireTimestamp int32 // When effect expires
Icon int16 // Icon to display
IconBackdrop int16 // Icon backdrop
Tier int8 // Spell tier
DetType int8 // Detrimental type
Incurable bool // Cannot be cured
ControlEffect int8 // Control effect type
TotalTime float32 // Total duration
// TODO: Add LuaSpell reference when spell system is implemented
// Spell *LuaSpell // Associated Lua spell
}
// NewDetrimentalEffects creates a new detrimental effect
func NewDetrimentalEffects(spellID int32, casterID int32, duration float32) *DetrimentalEffects {
return &DetrimentalEffects{
SpellID: spellID,
InheritedSpellID: 0,
CasterID: casterID,
ExpireTimestamp: int32(time.Now().Unix()) + int32(duration),
Icon: 0,
IconBackdrop: 0,
Tier: 1,
DetType: 0,
Incurable: false,
ControlEffect: 0,
TotalTime: duration,
}
}
// IsExpired checks if the detrimental effect has expired
func (de *DetrimentalEffects) IsExpired() bool {
if de.TotalTime <= 0 {
return false // Permanent effect
}
return int32(time.Now().Unix()) >= de.ExpireTimestamp
}
// GetTimeRemaining returns the time remaining for this effect
func (de *DetrimentalEffects) GetTimeRemaining() float32 {
if de.TotalTime <= 0 {
return -1 // Permanent effect
}
remaining := float32(de.ExpireTimestamp - int32(time.Now().Unix()))
if remaining < 0 {
return 0
}
return remaining
}
// IsControlEffect checks if this detrimental is a control effect
func (de *DetrimentalEffects) IsControlEffect() bool {
return de.ControlEffect > 0
}
// SpellEffectManager manages all spell effects for an entity
// Moved from entity package to centralize spell-related structures
type SpellEffectManager struct {
// Maintained effects (buffs that use concentration)
maintainedEffects [30]*MaintainedEffects
maintainedMutex sync.RWMutex
// Temporary spell effects (buffs/debuffs with durations)
spellEffects [45]*SpellEffects
effectsMutex sync.RWMutex
// Detrimental effects (debuffs)
detrimentalEffects []DetrimentalEffects
detrimentalMutex sync.RWMutex
// Control effects organized by type
controlEffects map[int8][]*DetrimentalEffects
controlMutex sync.RWMutex
// Detrimental count by type for stacking limits
detCountList map[int8]int8
countMutex sync.RWMutex
// Bonus list for stat modifications
bonusList []*BonusValues
bonusMutex sync.RWMutex
// Immunity list organized by effect type
immunities map[int8][]*DetrimentalEffects
immunityMutex sync.RWMutex
}
// NewSpellEffectManager creates a new spell effect manager
func NewSpellEffectManager() *SpellEffectManager {
return &SpellEffectManager{
maintainedEffects: [30]*MaintainedEffects{},
spellEffects: [45]*SpellEffects{},
detrimentalEffects: make([]DetrimentalEffects, 0),
controlEffects: make(map[int8][]*DetrimentalEffects),
detCountList: make(map[int8]int8),
bonusList: make([]*BonusValues, 0),
immunities: make(map[int8][]*DetrimentalEffects),
}
}
// AddMaintainedEffect adds a maintained effect to an available slot
func (sem *SpellEffectManager) AddMaintainedEffect(effect *MaintainedEffects) bool {
sem.maintainedMutex.Lock()
defer sem.maintainedMutex.Unlock()
// Find an empty slot
for i := 0; i < len(sem.maintainedEffects); i++ {
if sem.maintainedEffects[i] == nil {
effect.SlotPos = int32(i)
sem.maintainedEffects[i] = effect
return true
}
}
return false // No available slots
}
// RemoveMaintainedEffect removes a maintained effect by spell ID
func (sem *SpellEffectManager) RemoveMaintainedEffect(spellID int32) bool {
sem.maintainedMutex.Lock()
defer sem.maintainedMutex.Unlock()
for i := 0; i < len(sem.maintainedEffects); i++ {
if sem.maintainedEffects[i] != nil && sem.maintainedEffects[i].SpellID == spellID {
sem.maintainedEffects[i] = nil
return true
}
}
return false
}
// GetMaintainedEffect retrieves a maintained effect by spell ID
func (sem *SpellEffectManager) GetMaintainedEffect(spellID int32) *MaintainedEffects {
sem.maintainedMutex.RLock()
defer sem.maintainedMutex.RUnlock()
for _, effect := range sem.maintainedEffects {
if effect != nil && effect.SpellID == spellID {
return effect
}
}
return nil
}
// GetAllMaintainedEffects returns a copy of all active maintained effects
func (sem *SpellEffectManager) GetAllMaintainedEffects() []*MaintainedEffects {
sem.maintainedMutex.RLock()
defer sem.maintainedMutex.RUnlock()
effects := make([]*MaintainedEffects, 0)
for _, effect := range sem.maintainedEffects {
if effect != nil {
effects = append(effects, effect)
}
}
return effects
}
// AddSpellEffect adds a temporary spell effect to an available slot
func (sem *SpellEffectManager) AddSpellEffect(effect *SpellEffects) bool {
sem.effectsMutex.Lock()
defer sem.effectsMutex.Unlock()
// Find an empty slot
for i := 0; i < len(sem.spellEffects); i++ {
if sem.spellEffects[i] == nil {
sem.spellEffects[i] = effect
return true
}
}
return false // No available slots
}
// RemoveSpellEffect removes a spell effect by spell ID
func (sem *SpellEffectManager) RemoveSpellEffect(spellID int32) bool {
sem.effectsMutex.Lock()
defer sem.effectsMutex.Unlock()
for i := 0; i < len(sem.spellEffects); i++ {
if sem.spellEffects[i] != nil && sem.spellEffects[i].SpellID == spellID {
sem.spellEffects[i] = nil
return true
}
}
return false
}
// GetSpellEffect retrieves a spell effect by spell ID
func (sem *SpellEffectManager) GetSpellEffect(spellID int32) *SpellEffects {
sem.effectsMutex.RLock()
defer sem.effectsMutex.RUnlock()
for _, effect := range sem.spellEffects {
if effect != nil && effect.SpellID == spellID {
return effect
}
}
return nil
}
// AddDetrimentalEffect adds a detrimental effect
func (sem *SpellEffectManager) AddDetrimentalEffect(effect DetrimentalEffects) {
sem.detrimentalMutex.Lock()
defer sem.detrimentalMutex.Unlock()
sem.detrimentalEffects = append(sem.detrimentalEffects, effect)
// Update detrimental count
sem.countMutex.Lock()
sem.detCountList[effect.DetType]++
sem.countMutex.Unlock()
// Add to control effects if applicable
if effect.IsControlEffect() {
sem.controlMutex.Lock()
if sem.controlEffects[effect.ControlEffect] == nil {
sem.controlEffects[effect.ControlEffect] = make([]*DetrimentalEffects, 0)
}
sem.controlEffects[effect.ControlEffect] = append(sem.controlEffects[effect.ControlEffect], &effect)
sem.controlMutex.Unlock()
}
}
// RemoveDetrimentalEffect removes a detrimental effect by spell ID and caster
func (sem *SpellEffectManager) RemoveDetrimentalEffect(spellID int32, casterID int32) bool {
sem.detrimentalMutex.Lock()
defer sem.detrimentalMutex.Unlock()
for i, effect := range sem.detrimentalEffects {
if effect.SpellID == spellID && effect.CasterID == casterID {
// Remove from slice
sem.detrimentalEffects = append(sem.detrimentalEffects[:i], sem.detrimentalEffects[i+1:]...)
// Update detrimental count
sem.countMutex.Lock()
sem.detCountList[effect.DetType]--
if sem.detCountList[effect.DetType] <= 0 {
delete(sem.detCountList, effect.DetType)
}
sem.countMutex.Unlock()
// Remove from control effects if applicable
if effect.IsControlEffect() {
sem.removeFromControlEffects(effect.ControlEffect, spellID, casterID)
}
return true
}
}
return false
}
// removeFromControlEffects removes a control effect from the control effects map
func (sem *SpellEffectManager) removeFromControlEffects(controlType int8, spellID int32, casterID int32) {
sem.controlMutex.Lock()
defer sem.controlMutex.Unlock()
if effects, exists := sem.controlEffects[controlType]; exists {
for i, effect := range effects {
if effect.SpellID == spellID && effect.CasterID == casterID {
sem.controlEffects[controlType] = append(effects[:i], effects[i+1:]...)
if len(sem.controlEffects[controlType]) == 0 {
delete(sem.controlEffects, controlType)
}
break
}
}
}
}
// GetDetrimentalEffect retrieves a detrimental effect by spell ID and caster
func (sem *SpellEffectManager) GetDetrimentalEffect(spellID int32, casterID int32) *DetrimentalEffects {
sem.detrimentalMutex.RLock()
defer sem.detrimentalMutex.RUnlock()
for i, effect := range sem.detrimentalEffects {
if effect.SpellID == spellID && effect.CasterID == casterID {
return &sem.detrimentalEffects[i]
}
}
return nil
}
// HasControlEffect checks if the entity has a specific control effect active
func (sem *SpellEffectManager) HasControlEffect(controlType int8) bool {
sem.controlMutex.RLock()
defer sem.controlMutex.RUnlock()
effects, exists := sem.controlEffects[controlType]
return exists && len(effects) > 0
}
// AddBonus adds a stat bonus
func (sem *SpellEffectManager) AddBonus(bonus *BonusValues) {
sem.bonusMutex.Lock()
defer sem.bonusMutex.Unlock()
sem.bonusList = append(sem.bonusList, bonus)
}
// RemoveBonus removes a stat bonus by spell ID
func (sem *SpellEffectManager) RemoveBonus(spellID int32) bool {
sem.bonusMutex.Lock()
defer sem.bonusMutex.Unlock()
for i, bonus := range sem.bonusList {
if bonus.SpellID == spellID {
sem.bonusList = append(sem.bonusList[:i], sem.bonusList[i+1:]...)
return true
}
}
return false
}
// GetBonusValue calculates the total bonus value for a specific stat type
func (sem *SpellEffectManager) GetBonusValue(bonusType int16, entityClass int64, race int16, factionID int32) float32 {
sem.bonusMutex.RLock()
defer sem.bonusMutex.RUnlock()
var total float32 = 0
for _, bonus := range sem.bonusList {
if bonus.Type == bonusType && bonus.MeetsRequirements(entityClass, race, factionID) {
total += bonus.Value
}
}
return total
}
// CleanupExpiredEffects removes all expired effects
func (sem *SpellEffectManager) CleanupExpiredEffects() {
// Clean maintained effects
sem.maintainedMutex.Lock()
for i, effect := range sem.maintainedEffects {
if effect != nil && effect.IsExpired() {
sem.maintainedEffects[i] = nil
}
}
sem.maintainedMutex.Unlock()
// Clean spell effects
sem.effectsMutex.Lock()
for i, effect := range sem.spellEffects {
if effect != nil && effect.IsExpired() {
sem.spellEffects[i] = nil
}
}
sem.effectsMutex.Unlock()
// Clean detrimental effects
sem.detrimentalMutex.Lock()
newDetrimentals := make([]DetrimentalEffects, 0)
for _, effect := range sem.detrimentalEffects {
if !effect.IsExpired() {
newDetrimentals = append(newDetrimentals, effect)
} else {
// Update count
sem.countMutex.Lock()
sem.detCountList[effect.DetType]--
if sem.detCountList[effect.DetType] <= 0 {
delete(sem.detCountList, effect.DetType)
}
sem.countMutex.Unlock()
// Remove from control effects
if effect.IsControlEffect() {
sem.removeFromControlEffects(effect.ControlEffect, effect.SpellID, effect.CasterID)
}
}
}
sem.detrimentalEffects = newDetrimentals
sem.detrimentalMutex.Unlock()
}
// ClearAllEffects removes all spell effects
func (sem *SpellEffectManager) ClearAllEffects() {
sem.maintainedMutex.Lock()
for i := range sem.maintainedEffects {
sem.maintainedEffects[i] = nil
}
sem.maintainedMutex.Unlock()
sem.effectsMutex.Lock()
for i := range sem.spellEffects {
sem.spellEffects[i] = nil
}
sem.effectsMutex.Unlock()
sem.detrimentalMutex.Lock()
sem.detrimentalEffects = make([]DetrimentalEffects, 0)
sem.detrimentalMutex.Unlock()
sem.controlMutex.Lock()
sem.controlEffects = make(map[int8][]*DetrimentalEffects)
sem.controlMutex.Unlock()
sem.countMutex.Lock()
sem.detCountList = make(map[int8]int8)
sem.countMutex.Unlock()
sem.bonusMutex.Lock()
sem.bonusList = make([]*BonusValues, 0)
sem.bonusMutex.Unlock()
}

View File

@ -0,0 +1,597 @@
package spells
import (
"fmt"
"sync"
"sync/atomic"
)
// SpellScriptTimer represents a timer for spell script execution
type SpellScriptTimer struct {
// TODO: Add LuaSpell reference when implemented
// Spell *LuaSpell // The spell being timed
SpellID int32 // Spell ID for identification
CustomFunction string // Custom function to call
Time int32 // Timer duration
Caster int32 // Caster entity ID
Target int32 // Target entity ID
DeleteWhenDone bool // Whether to delete timer when finished
}
// MasterSpellList manages all spells in the game
// This replaces the C++ MasterSpellList functionality
type MasterSpellList struct {
// Spell storage
spells map[int32]*Spell // Spells by ID
spellsByName map[string]*Spell // Spells by name for lookup
spellsByTier map[int32]map[int8]*Spell // Spells by ID and tier
// ID management
maxSpellID int32 // Highest assigned spell ID
// Thread safety
mutex sync.RWMutex
}
// NewMasterSpellList creates a new master spell list
func NewMasterSpellList() *MasterSpellList {
return &MasterSpellList{
spells: make(map[int32]*Spell),
spellsByName: make(map[string]*Spell),
spellsByTier: make(map[int32]map[int8]*Spell),
maxSpellID: 0,
}
}
// AddSpell adds a spell to the master list
func (msl *MasterSpellList) AddSpell(spell *Spell) bool {
if spell == nil {
return false
}
msl.mutex.Lock()
defer msl.mutex.Unlock()
spellID := spell.GetSpellID()
// Update max spell ID
if spellID > msl.maxSpellID {
msl.maxSpellID = spellID
}
// Add to main spell map
msl.spells[spellID] = spell
// Add to name lookup
name := spell.GetName()
if name != "" {
msl.spellsByName[name] = spell
}
// Add to tier lookup
tier := spell.GetSpellTier()
if msl.spellsByTier[spellID] == nil {
msl.spellsByTier[spellID] = make(map[int8]*Spell)
}
msl.spellsByTier[spellID][tier] = spell
return true
}
// GetSpell retrieves a spell by ID
func (msl *MasterSpellList) GetSpell(spellID int32) *Spell {
msl.mutex.RLock()
defer msl.mutex.RUnlock()
return msl.spells[spellID]
}
// GetSpellByName retrieves a spell by name
func (msl *MasterSpellList) GetSpellByName(name string) *Spell {
msl.mutex.RLock()
defer msl.mutex.RUnlock()
return msl.spellsByName[name]
}
// GetSpellByIDAndTier retrieves a specific tier of a spell
func (msl *MasterSpellList) GetSpellByIDAndTier(spellID int32, tier int8) *Spell {
msl.mutex.RLock()
defer msl.mutex.RUnlock()
if tierMap, exists := msl.spellsByTier[spellID]; exists {
return tierMap[tier]
}
return nil
}
// RemoveSpell removes a spell from the master list
func (msl *MasterSpellList) RemoveSpell(spellID int32) bool {
msl.mutex.Lock()
defer msl.mutex.Unlock()
spell, exists := msl.spells[spellID]
if !exists {
return false
}
// Remove from main map
delete(msl.spells, spellID)
// Remove from name lookup
name := spell.GetName()
if name != "" {
delete(msl.spellsByName, name)
}
// Remove from tier lookup
delete(msl.spellsByTier, spellID)
return true
}
// GetNewMaxSpellID returns the next available spell ID
func (msl *MasterSpellList) GetNewMaxSpellID() int32 {
return atomic.AddInt32(&msl.maxSpellID, 1)
}
// GetSpellCount returns the total number of spells
func (msl *MasterSpellList) GetSpellCount() int {
msl.mutex.RLock()
defer msl.mutex.RUnlock()
return len(msl.spells)
}
// GetAllSpells returns a copy of all spells (expensive operation)
func (msl *MasterSpellList) GetAllSpells() []*Spell {
msl.mutex.RLock()
defer msl.mutex.RUnlock()
spells := make([]*Spell, 0, len(msl.spells))
for _, spell := range msl.spells {
spells = append(spells, spell)
}
return spells
}
// GetSpellsByType returns all spells of a specific type
func (msl *MasterSpellList) GetSpellsByType(spellType int16) []*Spell {
msl.mutex.RLock()
defer msl.mutex.RUnlock()
spells := make([]*Spell, 0)
for _, spell := range msl.spells {
if spell.GetSpellData().Type == spellType {
spells = append(spells, spell)
}
}
return spells
}
// GetSpellsByBookType returns all spells of a specific book type
func (msl *MasterSpellList) GetSpellsByBookType(bookType int32) []*Spell {
msl.mutex.RLock()
defer msl.mutex.RUnlock()
spells := make([]*Spell, 0)
for _, spell := range msl.spells {
if spell.GetSpellData().SpellBookType == bookType {
spells = append(spells, spell)
}
}
return spells
}
// Global spell list instance
var masterSpellList *MasterSpellList
var initSpellListOnce sync.Once
// GetMasterSpellList returns the global master spell list (singleton)
func GetMasterSpellList() *MasterSpellList {
initSpellListOnce.Do(func() {
masterSpellList = NewMasterSpellList()
})
return masterSpellList
}
// SpellCasting represents an active spell casting attempt
type SpellCasting struct {
Spell *Spell // The spell being cast
Caster int32 // Caster entity ID
Target int32 // Target entity ID
CastTime int32 // Total cast time
TimeRemaining int32 // Time remaining in cast
Interrupted bool // Whether casting was interrupted
// TODO: Add Entity references when implemented
// CasterEntity *Entity
// TargetEntity *Entity
}
// SpellBook represents a character's spell book
type SpellBook struct {
// Spell storage organized by type
spells map[int32]*Spell // All known spells by ID
spellsByType map[int32][]*Spell // Spells organized by book type
// Spell bar/hotbar assignments
spellBars map[int8]map[int8]*Spell // [bar][slot] = spell
// Thread safety
mutex sync.RWMutex
}
// NewSpellBook creates a new spell book
func NewSpellBook() *SpellBook {
return &SpellBook{
spells: make(map[int32]*Spell),
spellsByType: make(map[int32][]*Spell),
spellBars: make(map[int8]map[int8]*Spell),
}
}
// AddSpell adds a spell to the spell book
func (sb *SpellBook) AddSpell(spell *Spell) bool {
if spell == nil {
return false
}
sb.mutex.Lock()
defer sb.mutex.Unlock()
spellID := spell.GetSpellID()
// Check if spell already exists
if _, exists := sb.spells[spellID]; exists {
return false
}
// Add to main collection
sb.spells[spellID] = spell
// Add to type collection
bookType := spell.GetSpellData().SpellBookType
if sb.spellsByType[bookType] == nil {
sb.spellsByType[bookType] = make([]*Spell, 0)
}
sb.spellsByType[bookType] = append(sb.spellsByType[bookType], spell)
return true
}
// RemoveSpell removes a spell from the spell book
func (sb *SpellBook) RemoveSpell(spellID int32) bool {
sb.mutex.Lock()
defer sb.mutex.Unlock()
spell, exists := sb.spells[spellID]
if !exists {
return false
}
// Remove from main collection
delete(sb.spells, spellID)
// Remove from type collection
bookType := spell.GetSpellData().SpellBookType
if spells, exists := sb.spellsByType[bookType]; exists {
for i, s := range spells {
if s.GetSpellID() == spellID {
sb.spellsByType[bookType] = append(spells[:i], spells[i+1:]...)
break
}
}
}
// Remove from spell bars
for barID, bar := range sb.spellBars {
for slot, s := range bar {
if s != nil && s.GetSpellID() == spellID {
delete(sb.spellBars[barID], slot)
}
}
}
return true
}
// GetSpell retrieves a spell from the spell book
func (sb *SpellBook) GetSpell(spellID int32) *Spell {
sb.mutex.RLock()
defer sb.mutex.RUnlock()
return sb.spells[spellID]
}
// HasSpell checks if the spell book contains a specific spell
func (sb *SpellBook) HasSpell(spellID int32) bool {
sb.mutex.RLock()
defer sb.mutex.RUnlock()
_, exists := sb.spells[spellID]
return exists
}
// GetSpellsByType returns all spells of a specific book type
func (sb *SpellBook) GetSpellsByType(bookType int32) []*Spell {
sb.mutex.RLock()
defer sb.mutex.RUnlock()
if spells, exists := sb.spellsByType[bookType]; exists {
// Return a copy to prevent external modification
result := make([]*Spell, len(spells))
copy(result, spells)
return result
}
return make([]*Spell, 0)
}
// SetSpellBarSlot assigns a spell to a specific bar and slot
func (sb *SpellBook) SetSpellBarSlot(barID, slot int8, spell *Spell) bool {
sb.mutex.Lock()
defer sb.mutex.Unlock()
// Ensure the spell is in the spell book
if spell != nil {
if _, exists := sb.spells[spell.GetSpellID()]; !exists {
return false
}
}
// Initialize bar if needed
if sb.spellBars[barID] == nil {
sb.spellBars[barID] = make(map[int8]*Spell)
}
// Set the slot
if spell == nil {
delete(sb.spellBars[barID], slot)
} else {
sb.spellBars[barID][slot] = spell
}
return true
}
// GetSpellBarSlot retrieves the spell at a specific bar and slot
func (sb *SpellBook) GetSpellBarSlot(barID, slot int8) *Spell {
sb.mutex.RLock()
defer sb.mutex.RUnlock()
if bar, exists := sb.spellBars[barID]; exists {
return bar[slot]
}
return nil
}
// GetSpellCount returns the total number of spells in the book
func (sb *SpellBook) GetSpellCount() int {
sb.mutex.RLock()
defer sb.mutex.RUnlock()
return len(sb.spells)
}
// String returns a string representation of the spell book
func (sb *SpellBook) String() string {
return fmt.Sprintf("SpellBook[Spells=%d, Types=%d]",
sb.GetSpellCount(), len(sb.spellsByType))
}
// SpellManager manages the master spell list, player spell books, and spell processing
type SpellManager struct {
masterList *MasterSpellList // Global spell definitions
spellBooks map[int32]*SpellBook // Player spell books by character ID
spellProcess *SpellProcess // Spell processing system
targeting *SpellTargeting // Spell targeting system
resourceChecker *SpellResourceChecker // Resource checking system
mutex sync.RWMutex // Thread safety
}
// NewSpellManager creates a new spell manager
func NewSpellManager() *SpellManager {
return &SpellManager{
masterList: NewMasterSpellList(),
spellBooks: make(map[int32]*SpellBook),
spellProcess: NewSpellProcess(),
targeting: NewSpellTargeting(),
resourceChecker: NewSpellResourceChecker(),
}
}
// GetMasterList returns the master spell list
func (sm *SpellManager) GetMasterList() *MasterSpellList {
return sm.masterList
}
// GetSpellProcess returns the spell processing system
func (sm *SpellManager) GetSpellProcess() *SpellProcess {
return sm.spellProcess
}
// GetTargeting returns the spell targeting system
func (sm *SpellManager) GetTargeting() *SpellTargeting {
return sm.targeting
}
// GetResourceChecker returns the resource checking system
func (sm *SpellManager) GetResourceChecker() *SpellResourceChecker {
return sm.resourceChecker
}
// GetSpellBook retrieves or creates a spell book for a character
func (sm *SpellManager) GetSpellBook(characterID int32) *SpellBook {
sm.mutex.Lock()
defer sm.mutex.Unlock()
if book, exists := sm.spellBooks[characterID]; exists {
return book
}
// Create new spell book
book := NewSpellBook()
sm.spellBooks[characterID] = book
return book
}
// RemoveSpellBook removes a character's spell book (when they log out)
func (sm *SpellManager) RemoveSpellBook(characterID int32) {
sm.mutex.Lock()
defer sm.mutex.Unlock()
delete(sm.spellBooks, characterID)
}
// ProcessSpells processes all active spells (should be called regularly)
func (sm *SpellManager) ProcessSpells() {
sm.spellProcess.Process()
}
// CastSpell attempts to cast a spell for a character
func (sm *SpellManager) CastSpell(casterID, targetID, spellID int32) error {
// Get the spell from master list
spell := sm.masterList.GetSpell(spellID)
if spell == nil {
return fmt.Errorf("spell %d not found", spellID)
}
// Create LuaSpell instance
luaSpell := NewLuaSpell(spell, casterID)
luaSpell.InitialTarget = targetID
// Check resources
results := sm.resourceChecker.CheckAllResources(luaSpell, 0, 0)
for _, result := range results {
if !result.HasSufficient {
return fmt.Errorf("insufficient resources: %s", result.ErrorMessage)
}
}
// Get targets
targetResult := sm.targeting.GetSpellTargets(luaSpell, nil)
if targetResult.ErrorCode != 0 {
return fmt.Errorf("targeting failed: %s", targetResult.ErrorMessage)
}
if len(targetResult.ValidTargets) == 0 {
return fmt.Errorf("no valid targets found")
}
// TODO: Add spell to cast queue or process immediately
// This would integrate with the entity system to actually cast the spell
return nil
}
// InterruptSpell interrupts a spell being cast by an entity
func (sm *SpellManager) InterruptSpell(entityID, spellID int32, errorCode int16, cancel, fromMovement bool) {
sm.spellProcess.Interrupt(entityID, spellID, errorCode, cancel, fromMovement)
}
// AddSpellToQueue adds a spell to a player's casting queue
func (sm *SpellManager) AddSpellToQueue(spellID, casterID, targetID int32, priority int32) {
sm.spellProcess.AddSpellToQueue(spellID, casterID, targetID, priority)
}
// RemoveSpellFromQueue removes a spell from a player's casting queue
func (sm *SpellManager) RemoveSpellFromQueue(spellID, casterID int32) bool {
return sm.spellProcess.RemoveSpellFromQueue(spellID, casterID)
}
// IsSpellReady checks if a spell is ready to cast (not on cooldown)
func (sm *SpellManager) IsSpellReady(spellID, casterID int32) bool {
return sm.spellProcess.IsReady(spellID, casterID)
}
// GetSpellRecastTime returns remaining recast time for a spell
func (sm *SpellManager) GetSpellRecastTime(spellID, casterID int32) int32 {
remaining := sm.spellProcess.GetRecastTimeRemaining(spellID, casterID)
return int32(remaining.Milliseconds())
}
// CanCastSpell performs comprehensive checks to determine if a spell can be cast
func (sm *SpellManager) CanCastSpell(casterID, targetID, spellID int32) (bool, string) {
// Check if spell exists
spell := sm.masterList.GetSpell(spellID)
if spell == nil {
return false, "Spell not found"
}
// Check if spell is ready (not on cooldown)
if !sm.spellProcess.IsReady(spellID, casterID) {
remaining := sm.spellProcess.GetRecastTimeRemaining(spellID, casterID)
return false, fmt.Sprintf("Spell on cooldown for %d seconds", int(remaining.Seconds()))
}
// Create temporary LuaSpell for resource checks
luaSpell := NewLuaSpell(spell, casterID)
luaSpell.InitialTarget = targetID
// Check resources
results := sm.resourceChecker.CheckAllResources(luaSpell, 0, 0)
for _, result := range results {
if !result.HasSufficient {
return false, result.ErrorMessage
}
}
// Check targeting
targetResult := sm.targeting.GetSpellTargets(luaSpell, nil)
if targetResult.ErrorCode != 0 {
return false, targetResult.ErrorMessage
}
if len(targetResult.ValidTargets) == 0 {
return false, "No valid targets"
}
return true, ""
}
// GetSpellInfo returns comprehensive information about a spell
func (sm *SpellManager) GetSpellInfo(spellID int32) map[string]interface{} {
spell := sm.masterList.GetSpell(spellID)
if spell == nil {
return nil
}
info := make(map[string]interface{})
// Basic spell info
info["id"] = spell.GetSpellID()
info["name"] = spell.GetName()
info["description"] = spell.GetDescription()
info["tier"] = spell.GetSpellTier()
info["type"] = spell.GetSpellData().SpellBookType
// Resource requirements
info["resources"] = sm.resourceChecker.GetResourceSummary(spell)
// Targeting info
info["targeting"] = sm.targeting.GetTargetingInfo(spell)
// Classification
info["is_buff"] = spell.IsBuffSpell()
info["is_debuff"] = !spell.IsBuffSpell() && spell.IsControlSpell()
info["is_heal"] = spell.IsHealSpell()
info["is_damage"] = spell.IsDamageSpell()
info["is_offensive"] = spell.IsOffenseSpell()
return info
}
// GetActiveSpellCount returns the number of active spells being processed
func (sm *SpellManager) GetActiveSpellCount() int {
return sm.spellProcess.GetActiveSpellCount()
}
// Shutdown gracefully shuts down the spell manager
func (sm *SpellManager) Shutdown() {
sm.spellProcess.RemoveAllSpells(false)
}

View File

@ -0,0 +1,591 @@
package spells
import (
"fmt"
"sync"
"time"
)
// InterruptStruct represents a spell interruption event
type InterruptStruct struct {
InterruptedEntityID int32 // ID of the entity being interrupted
SpellID int32 // ID of the spell being interrupted
ErrorCode int16 // Error code for the interruption
FromMovement bool // Whether interruption was caused by movement
Canceled bool // Whether the spell was canceled vs interrupted
Timestamp time.Time // When the interrupt occurred
}
// CastTimer represents a spell casting timer
type CastTimer struct {
CasterID int32 // ID of the entity casting
TargetID int32 // ID of the target
SpellID int32 // ID of the spell being cast
ZoneID int32 // ID of the zone where casting occurs
StartTime time.Time // When casting started
Duration time.Duration // How long the cast takes
InHeroicOpp bool // Whether this is part of a heroic opportunity
DeleteTimer bool // Flag to mark timer for deletion
IsEntityCommand bool // Whether this is an entity command vs spell
mutex sync.RWMutex // Thread safety
}
// RecastTimer represents a spell recast cooldown timer
type RecastTimer struct {
CasterID int32 // ID of the entity with the recast
ClientID int32 // ID of the client (if player)
SpellID int32 // ID of the spell on cooldown
LinkedTimerID int32 // ID of linked timer group
TypeGroupSpellID int32 // Type group for timer sharing
StartTime time.Time // When the recast started
Duration time.Duration // How long the recast lasts
StayLocked bool // Whether spell stays locked after recast
mutex sync.RWMutex // Thread safety
}
// CastSpell represents a spell casting request
type CastSpell struct {
CasterID int32 // ID of the entity casting
TargetID int32 // ID of the target
SpellID int32 // ID of the spell to cast
ZoneID int32 // ID of the zone
}
// SpellQueue represents a player's spell queue entry
type SpellQueueEntry struct {
SpellID int32 // ID of the queued spell
Priority int32 // Queue priority
QueuedTime time.Time // When the spell was queued
TargetID int32 // Target for the spell
HostileOnly bool // Whether this is a hostile-only queue
}
// HeroicOpportunity represents a heroic opportunity instance
type HeroicOpportunity struct {
ID int32 // Unique identifier
InitiatorID int32 // ID of the player/group that started it
TargetID int32 // ID of the target
StartTime time.Time // When the HO started
Duration time.Duration // Total time allowed
CurrentStep int32 // Current step in the sequence
TotalSteps int32 // Total steps in the sequence
IsGroup bool // Whether this is a group HO
Complete bool // Whether the HO completed successfully
WheelID int32 // ID of the wheel type
mutex sync.RWMutex // Thread safety
}
// SpellProcess manages all spell casting for a zone
type SpellProcess struct {
// Core collections
activeSpells map[int32]*LuaSpell // Active spells by spell instance ID
castTimers []*CastTimer // Active cast timers
recastTimers []*RecastTimer // Active recast timers
interruptQueue []*InterruptStruct // Queued interruptions
spellQueues map[int32][]*SpellQueueEntry // Player spell queues by player ID
// Heroic Opportunities
soloHeroicOps map[int32]*HeroicOpportunity // Solo HOs by client ID
groupHeroicOps map[int32]*HeroicOpportunity // Group HOs by group ID
// Targeting and removal
removeTargetList map[int32][]int32 // Targets to remove by spell ID
spellCancelList []int32 // Spells marked for cancellation
// State management
lastProcessTime time.Time // Last time Process() was called
nextSpellID int32 // Next available spell instance ID
// Thread safety
mutex sync.RWMutex // Main process mutex
// TODO: Add when other systems are available
// zoneServer *ZoneServer // Reference to zone server
// luaInterface *LuaInterface // Reference to Lua interface
}
// NewSpellProcess creates a new spell process instance
func NewSpellProcess() *SpellProcess {
return &SpellProcess{
activeSpells: make(map[int32]*LuaSpell),
castTimers: make([]*CastTimer, 0),
recastTimers: make([]*RecastTimer, 0),
interruptQueue: make([]*InterruptStruct, 0),
spellQueues: make(map[int32][]*SpellQueueEntry),
soloHeroicOps: make(map[int32]*HeroicOpportunity),
groupHeroicOps: make(map[int32]*HeroicOpportunity),
removeTargetList: make(map[int32][]int32),
spellCancelList: make([]int32, 0),
lastProcessTime: time.Now(),
nextSpellID: 1,
}
}
// Process handles the main spell processing loop
func (sp *SpellProcess) Process() {
sp.mutex.Lock()
defer sp.mutex.Unlock()
now := time.Now()
// Only process every 50ms to match C++ implementation
if now.Sub(sp.lastProcessTime) < time.Duration(ProcessCheckInterval)*time.Millisecond {
return
}
sp.lastProcessTime = now
// Process active spells (duration checks, ticks)
sp.processActiveSpells(now)
// Process spell cancellations
sp.processSpellCancellations()
// Process interrupts
sp.processInterrupts()
// Process cast timers
sp.processCastTimers(now)
// Process recast timers
sp.processRecastTimers(now)
// Process spell queues
sp.processSpellQueues()
// Process heroic opportunities
sp.processHeroicOpportunities(now)
}
// processActiveSpells handles duration checks and spell ticks
func (sp *SpellProcess) processActiveSpells(now time.Time) {
expiredSpells := make([]int32, 0)
for spellID, luaSpell := range sp.activeSpells {
if luaSpell == nil {
expiredSpells = append(expiredSpells, spellID)
continue
}
// Check if spell duration has expired
// TODO: Implement proper duration checking based on spell data
// This would check luaSpell.spell.GetSpellData().duration1 etc.
// Check if spell needs to tick
// TODO: Implement spell tick processing
// This would call ProcessSpell(luaSpell, false) for tick effects
}
// Remove expired spells
for _, spellID := range expiredSpells {
sp.deleteCasterSpell(spellID, "expired")
}
}
// processSpellCancellations handles spells marked for cancellation
func (sp *SpellProcess) processSpellCancellations() {
if len(sp.spellCancelList) == 0 {
return
}
canceledSpells := make([]int32, len(sp.spellCancelList))
copy(canceledSpells, sp.spellCancelList)
sp.spellCancelList = sp.spellCancelList[:0] // Clear the list
for _, spellID := range canceledSpells {
sp.deleteCasterSpell(spellID, "canceled")
}
}
// processInterrupts handles queued spell interruptions
func (sp *SpellProcess) processInterrupts() {
if len(sp.interruptQueue) == 0 {
return
}
interrupts := make([]*InterruptStruct, len(sp.interruptQueue))
copy(interrupts, sp.interruptQueue)
sp.interruptQueue = sp.interruptQueue[:0] // Clear the queue
for _, interrupt := range interrupts {
sp.checkInterrupt(interrupt)
}
}
// processCastTimers handles spell casting completion
func (sp *SpellProcess) processCastTimers(now time.Time) {
completedTimers := make([]*CastTimer, 0)
remainingTimers := make([]*CastTimer, 0)
for _, timer := range sp.castTimers {
if timer.DeleteTimer {
// Timer marked for deletion
continue
}
if now.Sub(timer.StartTime) >= timer.Duration {
// Cast time completed
timer.DeleteTimer = true
completedTimers = append(completedTimers, timer)
// TODO: Send finish cast packet to client
// TODO: Call CastProcessedSpell or CastProcessedEntityCommand
} else {
remainingTimers = append(remainingTimers, timer)
}
}
sp.castTimers = remainingTimers
}
// processRecastTimers handles spell cooldown expiration
func (sp *SpellProcess) processRecastTimers(now time.Time) {
expiredTimers := make([]*RecastTimer, 0)
remainingTimers := make([]*RecastTimer, 0)
for _, timer := range sp.recastTimers {
if now.Sub(timer.StartTime) >= timer.Duration {
// Recast timer expired
expiredTimers = append(expiredTimers, timer)
// TODO: Unlock spell for the caster if not a maintained effect
// TODO: Send spell book update to client
} else {
remainingTimers = append(remainingTimers, timer)
}
}
sp.recastTimers = remainingTimers
}
// processSpellQueues handles queued spells for players
func (sp *SpellProcess) processSpellQueues() {
for playerID, queue := range sp.spellQueues {
if len(queue) == 0 {
continue
}
// TODO: Check if player is casting and can cast next spell
// TODO: Process highest priority spell from queue
// This would call ProcessSpell for the queued spell
_ = playerID // Placeholder to avoid unused variable error
}
}
// processHeroicOpportunities handles heroic opportunity timers
func (sp *SpellProcess) processHeroicOpportunities(now time.Time) {
// Process solo heroic opportunities
expiredSolo := make([]int32, 0)
for clientID, ho := range sp.soloHeroicOps {
if now.Sub(ho.StartTime) >= ho.Duration {
ho.Complete = true
expiredSolo = append(expiredSolo, clientID)
// TODO: Send heroic opportunity update packet
}
}
for _, clientID := range expiredSolo {
delete(sp.soloHeroicOps, clientID)
}
// Process group heroic opportunities
expiredGroup := make([]int32, 0)
for groupID, ho := range sp.groupHeroicOps {
if now.Sub(ho.StartTime) >= ho.Duration {
ho.Complete = true
expiredGroup = append(expiredGroup, groupID)
// TODO: Send heroic opportunity update to all group members
}
}
for _, groupID := range expiredGroup {
delete(sp.groupHeroicOps, groupID)
}
}
// RemoveCaster removes references to a caster when they are destroyed
func (sp *SpellProcess) RemoveCaster(casterID int32) {
sp.mutex.Lock()
defer sp.mutex.Unlock()
// Remove from active spells
expiredSpells := make([]int32, 0)
for spellID, luaSpell := range sp.activeSpells {
if luaSpell != nil && luaSpell.CasterID == casterID {
luaSpell.CasterID = 0 // Clear caster reference
expiredSpells = append(expiredSpells, spellID)
}
}
// Clean up spells with invalid casters
for _, spellID := range expiredSpells {
sp.deleteCasterSpell(spellID, "caster removed")
}
// Remove cast timers for this caster
remainingCastTimers := make([]*CastTimer, 0)
for _, timer := range sp.castTimers {
if timer.CasterID != casterID {
remainingCastTimers = append(remainingCastTimers, timer)
}
}
sp.castTimers = remainingCastTimers
// Remove recast timers for this caster
remainingRecastTimers := make([]*RecastTimer, 0)
for _, timer := range sp.recastTimers {
if timer.CasterID != casterID {
remainingRecastTimers = append(remainingRecastTimers, timer)
}
}
sp.recastTimers = remainingRecastTimers
// Remove spell queue for this caster
delete(sp.spellQueues, casterID)
}
// Interrupt creates an interrupt request for a casting entity
func (sp *SpellProcess) Interrupt(entityID int32, spellID int32, errorCode int16, cancel, fromMovement bool) {
sp.mutex.Lock()
defer sp.mutex.Unlock()
interrupt := &InterruptStruct{
InterruptedEntityID: entityID,
SpellID: spellID,
ErrorCode: errorCode,
FromMovement: fromMovement,
Canceled: cancel,
Timestamp: time.Now(),
}
sp.interruptQueue = append(sp.interruptQueue, interrupt)
}
// checkInterrupt processes a single interrupt
func (sp *SpellProcess) checkInterrupt(interrupt *InterruptStruct) {
if interrupt == nil {
return
}
// TODO: Implement interrupt processing
// This would:
// 1. Find the casting entity
// 2. Send finish cast packet if needed
// 3. Remove spell timers from spawn
// 4. Set entity casting state to false
// 5. Send interrupt packet to zone
// 6. Send spell failed packet if error code > 0
// 7. Unlock spell for player
// 8. Send spell book update
fmt.Printf("Processing interrupt for entity %d, spell %d, error %d\n",
interrupt.InterruptedEntityID, interrupt.SpellID, interrupt.ErrorCode)
}
// IsReady checks if an entity can cast a spell (not on recast)
func (sp *SpellProcess) IsReady(spellID, casterID int32) bool {
sp.mutex.RLock()
defer sp.mutex.RUnlock()
// TODO: Check if caster is currently casting
// if caster.IsCasting() { return false }
// Check recast timers
for _, timer := range sp.recastTimers {
if timer.SpellID == spellID && timer.CasterID == casterID {
return false // Still on cooldown
}
}
return true
}
// AddSpellToQueue adds a spell to a player's casting queue
func (sp *SpellProcess) AddSpellToQueue(spellID, casterID, targetID int32, priority int32) {
sp.mutex.Lock()
defer sp.mutex.Unlock()
entry := &SpellQueueEntry{
SpellID: spellID,
Priority: priority,
QueuedTime: time.Now(),
TargetID: targetID,
}
if sp.spellQueues[casterID] == nil {
sp.spellQueues[casterID] = make([]*SpellQueueEntry, 0)
}
// Add to queue (TODO: sort by priority)
sp.spellQueues[casterID] = append(sp.spellQueues[casterID], entry)
// Limit queue size
if len(sp.spellQueues[casterID]) > MaxQueuedSpells {
sp.spellQueues[casterID] = sp.spellQueues[casterID][1:] // Remove oldest
}
}
// RemoveSpellFromQueue removes a specific spell from a player's queue
func (sp *SpellProcess) RemoveSpellFromQueue(spellID, casterID int32) bool {
sp.mutex.Lock()
defer sp.mutex.Unlock()
queue, exists := sp.spellQueues[casterID]
if !exists {
return false
}
for i, entry := range queue {
if entry.SpellID == spellID {
// Remove entry from queue
sp.spellQueues[casterID] = append(queue[:i], queue[i+1:]...)
return true
}
}
return false
}
// ClearSpellQueue clears a player's spell queue
func (sp *SpellProcess) ClearSpellQueue(casterID int32, hostileOnly bool) {
sp.mutex.Lock()
defer sp.mutex.Unlock()
if !hostileOnly {
delete(sp.spellQueues, casterID)
return
}
// TODO: Remove only hostile spells
// This would require checking spell data to determine if spell is hostile
}
// AddSpellCancel marks a spell for cancellation
func (sp *SpellProcess) AddSpellCancel(spellID int32) {
sp.mutex.Lock()
defer sp.mutex.Unlock()
sp.spellCancelList = append(sp.spellCancelList, spellID)
}
// deleteCasterSpell removes a spell from active processing
func (sp *SpellProcess) deleteCasterSpell(spellID int32, reason string) bool {
luaSpell, exists := sp.activeSpells[spellID]
if !exists {
return false
}
// TODO: Implement proper spell removal
// This would:
// 1. Handle concentration return for toggle spells
// 2. Check recast for non-duration-until-cancel spells
// 3. Unlock spell for player
// 4. Remove procs from caster
// 5. Remove maintained spell from caster
// 6. Remove targets from spell
// 7. Process spell removal effects
fmt.Printf("Removing spell %d, reason: %s\n", spellID, reason)
delete(sp.activeSpells, spellID)
// Clean up removal targets list
delete(sp.removeTargetList, spellID)
_ = luaSpell // Placeholder to avoid unused variable error
return true
}
// GetActiveSpellCount returns the number of active spells
func (sp *SpellProcess) GetActiveSpellCount() int {
sp.mutex.RLock()
defer sp.mutex.RUnlock()
return len(sp.activeSpells)
}
// GetQueuedSpellCount returns the number of queued spells for a player
func (sp *SpellProcess) GetQueuedSpellCount(casterID int32) int {
sp.mutex.RLock()
defer sp.mutex.RUnlock()
if queue, exists := sp.spellQueues[casterID]; exists {
return len(queue)
}
return 0
}
// GetRecastTimeRemaining returns remaining recast time for a spell
func (sp *SpellProcess) GetRecastTimeRemaining(spellID, casterID int32) time.Duration {
sp.mutex.RLock()
defer sp.mutex.RUnlock()
for _, timer := range sp.recastTimers {
if timer.SpellID == spellID && timer.CasterID == casterID {
elapsed := time.Since(timer.StartTime)
if elapsed >= timer.Duration {
return 0
}
return timer.Duration - elapsed
}
}
return 0
}
// RemoveAllSpells removes all spells from processing (used during shutdown)
func (sp *SpellProcess) RemoveAllSpells(reloadSpells bool) {
sp.mutex.Lock()
defer sp.mutex.Unlock()
// Clear all spell collections
if reloadSpells {
// Keep some data for reload
sp.activeSpells = make(map[int32]*LuaSpell)
} else {
// Complete cleanup
for spellID := range sp.activeSpells {
sp.deleteCasterSpell(spellID, "shutdown")
}
sp.activeSpells = make(map[int32]*LuaSpell)
}
sp.castTimers = make([]*CastTimer, 0)
sp.recastTimers = make([]*RecastTimer, 0)
sp.interruptQueue = make([]*InterruptStruct, 0)
sp.spellQueues = make(map[int32][]*SpellQueueEntry)
sp.soloHeroicOps = make(map[int32]*HeroicOpportunity)
sp.groupHeroicOps = make(map[int32]*HeroicOpportunity)
sp.removeTargetList = make(map[int32][]int32)
sp.spellCancelList = make([]int32, 0)
}
// NewCastTimer creates a new cast timer
func (ct *CastTimer) IsExpired() bool {
ct.mutex.RLock()
defer ct.mutex.RUnlock()
return time.Since(ct.StartTime) >= ct.Duration
}
// NewRecastTimer creates a new recast timer
func (rt *RecastTimer) IsExpired() bool {
rt.mutex.RLock()
defer rt.mutex.RUnlock()
return time.Since(rt.StartTime) >= rt.Duration
}
// GetRemainingTime returns remaining time on the recast
func (rt *RecastTimer) GetRemainingTime() time.Duration {
rt.mutex.RLock()
defer rt.mutex.RUnlock()
elapsed := time.Since(rt.StartTime)
if elapsed >= rt.Duration {
return 0
}
return rt.Duration - elapsed
}

View File

@ -0,0 +1,470 @@
package spells
import (
"fmt"
"sync"
)
// SpellResourceChecker handles checking and consuming spell casting resources
type SpellResourceChecker struct {
// TODO: Add references to entity system when available
// entityManager *EntityManager
mutex sync.RWMutex
}
// ResourceCheckResult represents the result of a resource check
type ResourceCheckResult struct {
HasSufficient bool // Whether entity has sufficient resources
CurrentValue float32 // Current value of the resource
RequiredValue float32 // Required value for the spell
ResourceType int32 // Type of resource checked
ErrorMessage string // Error message if check failed
}
// NewSpellResourceChecker creates a new resource checker
func NewSpellResourceChecker() *SpellResourceChecker {
return &SpellResourceChecker{}
}
// CheckPower verifies if the caster has enough power to cast the spell
// Converted from C++ SpellProcess::CheckPower
func (src *SpellResourceChecker) CheckPower(luaSpell *LuaSpell, customPowerReq float32) *ResourceCheckResult {
if luaSpell == nil || luaSpell.Spell == nil {
return &ResourceCheckResult{
HasSufficient: false,
CurrentValue: 0,
RequiredValue: 0,
ResourceType: ResourceCheckPower,
ErrorMessage: "Invalid spell",
}
}
powerRequired := customPowerReq
if powerRequired == 0 {
powerRequired = luaSpell.Spell.GetPowerRequired()
}
// TODO: Get actual power from entity when entity system is available
// For now, assume entity has sufficient power
currentPower := float32(1000.0) // Placeholder
result := &ResourceCheckResult{
HasSufficient: currentPower >= powerRequired,
CurrentValue: currentPower,
RequiredValue: powerRequired,
ResourceType: ResourceCheckPower,
ErrorMessage: "",
}
if !result.HasSufficient {
result.ErrorMessage = fmt.Sprintf("Insufficient power: need %.1f, have %.1f", powerRequired, currentPower)
}
return result
}
// TakePower consumes power for spell casting
// Converted from C++ SpellProcess::TakePower
func (src *SpellResourceChecker) TakePower(luaSpell *LuaSpell, customPowerReq float32) bool {
result := src.CheckPower(luaSpell, customPowerReq)
if !result.HasSufficient {
return false
}
// TODO: Actually deduct power from entity when entity system is available
// This would call something like:
// entity.GetInfoStruct().SetPower(currentPower - powerRequired)
// entity.GetZone().TriggerCharSheetTimer() // Update client display
return true
}
// CheckHP verifies if the caster has enough health to cast the spell
// Converted from C++ SpellProcess::CheckHP
func (src *SpellResourceChecker) CheckHP(luaSpell *LuaSpell, customHPReq float32) *ResourceCheckResult {
if luaSpell == nil || luaSpell.Spell == nil {
return &ResourceCheckResult{
HasSufficient: false,
CurrentValue: 0,
RequiredValue: 0,
ResourceType: ResourceCheckHealth,
ErrorMessage: "Invalid spell",
}
}
hpRequired := customHPReq
if hpRequired == 0 {
hpRequired = float32(luaSpell.Spell.GetHPRequired())
}
// TODO: Get actual HP from entity when entity system is available
// For now, assume entity has sufficient HP
currentHP := float32(1000.0) // Placeholder
result := &ResourceCheckResult{
HasSufficient: currentHP >= hpRequired,
CurrentValue: currentHP,
RequiredValue: hpRequired,
ResourceType: ResourceCheckHealth,
ErrorMessage: "",
}
if !result.HasSufficient {
result.ErrorMessage = fmt.Sprintf("Insufficient health: need %.1f, have %.1f", hpRequired, currentHP)
}
return result
}
// TakeHP consumes health for spell casting
// Converted from C++ SpellProcess::TakeHP
func (src *SpellResourceChecker) TakeHP(luaSpell *LuaSpell, customHPReq float32) bool {
result := src.CheckHP(luaSpell, customHPReq)
if !result.HasSufficient {
return false
}
// TODO: Actually deduct HP from entity when entity system is available
// This would call something like:
// entity.GetInfoStruct().SetHP(currentHP - hpRequired)
// entity.GetZone().TriggerCharSheetTimer() // Update client display
return true
}
// CheckConcentration verifies if the caster has enough concentration to cast the spell
// Converted from C++ SpellProcess::CheckConcentration
func (src *SpellResourceChecker) CheckConcentration(luaSpell *LuaSpell) *ResourceCheckResult {
if luaSpell == nil || luaSpell.Spell == nil {
return &ResourceCheckResult{
HasSufficient: false,
CurrentValue: 0,
RequiredValue: 0,
ResourceType: ResourceCheckConcentration,
ErrorMessage: "Invalid spell",
}
}
spellData := luaSpell.Spell.GetSpellData()
if spellData == nil {
return &ResourceCheckResult{
HasSufficient: false,
CurrentValue: 0,
RequiredValue: 0,
ResourceType: ResourceCheckConcentration,
ErrorMessage: "Invalid spell data",
}
}
concentrationRequired := float32(spellData.ReqConcentration)
// TODO: Get actual concentration from entity when entity system is available
// For now, assume entity has sufficient concentration
currentConcentration := float32(100.0) // Placeholder
result := &ResourceCheckResult{
HasSufficient: currentConcentration >= concentrationRequired,
CurrentValue: currentConcentration,
RequiredValue: concentrationRequired,
ResourceType: ResourceCheckConcentration,
ErrorMessage: "",
}
if !result.HasSufficient {
result.ErrorMessage = fmt.Sprintf("Insufficient concentration: need %.1f, have %.1f", concentrationRequired, currentConcentration)
}
return result
}
// AddConcentration adds concentration for maintained spells
// Converted from C++ SpellProcess::AddConcentration
func (src *SpellResourceChecker) AddConcentration(luaSpell *LuaSpell) bool {
result := src.CheckConcentration(luaSpell)
if !result.HasSufficient {
return false
}
// TODO: Actually deduct concentration from entity when entity system is available
// This would call something like:
// currentConc := entity.GetInfoStruct().GetCurConcentration()
// entity.GetInfoStruct().SetCurConcentration(currentConc + concentrationRequired)
// entity.GetZone().TriggerCharSheetTimer() // Update client display
return true
}
// CheckSavagery verifies if the caster has enough savagery to cast the spell
// Converted from C++ SpellProcess::CheckSavagery
func (src *SpellResourceChecker) CheckSavagery(luaSpell *LuaSpell) *ResourceCheckResult {
if luaSpell == nil || luaSpell.Spell == nil {
return &ResourceCheckResult{
HasSufficient: false,
CurrentValue: 0,
RequiredValue: 0,
ResourceType: ResourceCheckSavagery,
ErrorMessage: "Invalid spell",
}
}
savageryRequired := float32(luaSpell.Spell.GetSavageryRequired())
// TODO: Get actual savagery from entity when entity system is available
// For now, assume entity has sufficient savagery
currentSavagery := float32(100.0) // Placeholder
result := &ResourceCheckResult{
HasSufficient: currentSavagery >= savageryRequired,
CurrentValue: currentSavagery,
RequiredValue: savageryRequired,
ResourceType: ResourceCheckSavagery,
ErrorMessage: "",
}
if !result.HasSufficient {
result.ErrorMessage = fmt.Sprintf("Insufficient savagery: need %.1f, have %.1f", savageryRequired, currentSavagery)
}
return result
}
// TakeSavagery consumes savagery for spell casting
// Converted from C++ SpellProcess::TakeSavagery
func (src *SpellResourceChecker) TakeSavagery(luaSpell *LuaSpell) bool {
result := src.CheckSavagery(luaSpell)
if !result.HasSufficient {
return false
}
// TODO: Actually deduct savagery from entity when entity system is available
// This would call something like:
// entity.GetInfoStruct().SetSavagery(currentSavagery - savageryRequired)
// entity.GetZone().TriggerCharSheetTimer() // Update client display
return true
}
// CheckDissonance verifies if the caster has enough dissonance to cast the spell
// Converted from C++ SpellProcess::CheckDissonance
func (src *SpellResourceChecker) CheckDissonance(luaSpell *LuaSpell) *ResourceCheckResult {
if luaSpell == nil || luaSpell.Spell == nil {
return &ResourceCheckResult{
HasSufficient: false,
CurrentValue: 0,
RequiredValue: 0,
ResourceType: ResourceCheckDissonance,
ErrorMessage: "Invalid spell",
}
}
dissonanceRequired := float32(luaSpell.Spell.GetDissonanceRequired())
// TODO: Get actual dissonance from entity when entity system is available
// For now, assume entity has sufficient dissonance
currentDissonance := float32(100.0) // Placeholder
result := &ResourceCheckResult{
HasSufficient: currentDissonance >= dissonanceRequired,
CurrentValue: currentDissonance,
RequiredValue: dissonanceRequired,
ResourceType: ResourceCheckDissonance,
ErrorMessage: "",
}
if !result.HasSufficient {
result.ErrorMessage = fmt.Sprintf("Insufficient dissonance: need %.1f, have %.1f", dissonanceRequired, currentDissonance)
}
return result
}
// AddDissonance adds dissonance for spell casting
// Converted from C++ SpellProcess::AddDissonance
func (src *SpellResourceChecker) AddDissonance(luaSpell *LuaSpell) bool {
result := src.CheckDissonance(luaSpell)
if !result.HasSufficient {
return false
}
// TODO: Actually add dissonance to entity when entity system is available
// This would call something like:
// entity.GetInfoStruct().SetDissonance(currentDissonance + dissonanceRequired)
// entity.GetZone().TriggerCharSheetTimer() // Update client display
return true
}
// CheckAllResources performs a comprehensive resource check for a spell
func (src *SpellResourceChecker) CheckAllResources(luaSpell *LuaSpell, customPowerReq, customHPReq float32) []ResourceCheckResult {
results := make([]ResourceCheckResult, 0)
// Check power
powerResult := src.CheckPower(luaSpell, customPowerReq)
if powerResult.RequiredValue > 0 {
results = append(results, *powerResult)
}
// Check health
hpResult := src.CheckHP(luaSpell, customHPReq)
if hpResult.RequiredValue > 0 {
results = append(results, *hpResult)
}
// Check concentration
concResult := src.CheckConcentration(luaSpell)
if concResult.RequiredValue > 0 {
results = append(results, *concResult)
}
// Check savagery
savageryResult := src.CheckSavagery(luaSpell)
if savageryResult.RequiredValue > 0 {
results = append(results, *savageryResult)
}
// Check dissonance
dissonanceResult := src.CheckDissonance(luaSpell)
if dissonanceResult.RequiredValue > 0 {
results = append(results, *dissonanceResult)
}
return results
}
// ConsumeAllResources attempts to consume all required resources for a spell
func (src *SpellResourceChecker) ConsumeAllResources(luaSpell *LuaSpell, customPowerReq, customHPReq float32) bool {
// First check all resources
results := src.CheckAllResources(luaSpell, customPowerReq, customHPReq)
// Verify all resources are sufficient
for _, result := range results {
if !result.HasSufficient {
return false
}
}
// Consume resources
success := true
// Take power if required
powerResult := src.CheckPower(luaSpell, customPowerReq)
if powerResult.RequiredValue > 0 {
success = success && src.TakePower(luaSpell, customPowerReq)
}
// Take health if required
hpResult := src.CheckHP(luaSpell, customHPReq)
if hpResult.RequiredValue > 0 {
success = success && src.TakeHP(luaSpell, customHPReq)
}
// Add concentration if required (for maintained spells)
spellData := luaSpell.Spell.GetSpellData()
if spellData != nil && spellData.CastType == SpellCastTypeToggle {
concResult := src.CheckConcentration(luaSpell)
if concResult.RequiredValue > 0 {
success = success && src.AddConcentration(luaSpell)
}
}
// Take savagery if required
savageryResult := src.CheckSavagery(luaSpell)
if savageryResult.RequiredValue > 0 {
success = success && src.TakeSavagery(luaSpell)
}
// Add dissonance if required
dissonanceResult := src.CheckDissonance(luaSpell)
if dissonanceResult.RequiredValue > 0 {
success = success && src.AddDissonance(luaSpell)
}
return success
}
// GetResourceSummary returns a summary of all resource requirements for a spell
func (src *SpellResourceChecker) GetResourceSummary(spell *Spell) map[string]float32 {
summary := make(map[string]float32)
if spell == nil {
return summary
}
summary["power"] = spell.GetPowerRequired()
summary["health"] = float32(spell.GetHPRequired())
summary["savagery"] = float32(spell.GetSavageryRequired())
summary["dissonance"] = float32(spell.GetDissonanceRequired())
spellData := spell.GetSpellData()
if spellData != nil {
summary["concentration"] = float32(spellData.ReqConcentration)
}
return summary
}
// ValidateResourceRequirements checks if resource requirements are reasonable
func (src *SpellResourceChecker) ValidateResourceRequirements(spell *Spell) []string {
errors := make([]string, 0)
if spell == nil {
errors = append(errors, "Spell is nil")
return errors
}
// Check for negative requirements
if spell.GetPowerRequired() < 0 {
errors = append(errors, "Power requirement cannot be negative")
}
if spell.GetHPRequired() < 0 {
errors = append(errors, "Health requirement cannot be negative")
}
if spell.GetSavageryRequired() < 0 {
errors = append(errors, "Savagery requirement cannot be negative")
}
if spell.GetDissonanceRequired() < 0 {
errors = append(errors, "Dissonance requirement cannot be negative")
}
spellData := spell.GetSpellData()
if spellData != nil {
if spellData.ReqConcentration < 0 {
errors = append(errors, "Concentration requirement cannot be negative")
}
// Check for excessive requirements
if spell.GetPowerRequired() > 10000 {
errors = append(errors, "Power requirement seems excessive (>10000)")
}
if spell.GetHPRequired() > 5000 {
errors = append(errors, "Health requirement seems excessive (>5000)")
}
}
return errors
}
// GetFailureReason returns an appropriate failure reason code based on resource check results
func (src *SpellResourceChecker) GetFailureReason(results []ResourceCheckResult) int32 {
for _, result := range results {
if !result.HasSufficient {
switch result.ResourceType {
case ResourceCheckPower:
return FailureReasonInsufficientPower
case ResourceCheckHealth:
return FailureReasonInsufficientHealth
case ResourceCheckConcentration:
return FailureReasonInsufficientConc
default:
return FailureReasonRequirementNotMet
}
}
}
return FailureReasonNone
}

View File

@ -0,0 +1,442 @@
package spells
import (
"fmt"
"sync"
)
// SpellTargeting handles spell target selection and validation
type SpellTargeting struct {
// TODO: Add references to zone and entity systems when available
// zoneServer *ZoneServer
// entityManager *EntityManager
mutex sync.RWMutex
}
// TargetingResult represents the result of spell targeting
type TargetingResult struct {
ValidTargets []int32 // List of valid target IDs
InvalidTargets []int32 // List of invalid target IDs
ErrorCode int32 // Error code if targeting failed
ErrorMessage string // Human readable error message
}
// TargetingOptions contains options for target selection
type TargetingOptions struct {
BypassSpellChecks bool // Skip spell-specific target validation
BypassRangeChecks bool // Skip range validation
MaxRange float32 // Override maximum range
IgnoreLineOfSight bool // Ignore line of sight requirements
}
// NewSpellTargeting creates a new spell targeting system
func NewSpellTargeting() *SpellTargeting {
return &SpellTargeting{}
}
// GetSpellTargets finds all valid targets for a spell and adds them to the LuaSpell
// This is the main targeting function converted from C++ SpellProcess::GetSpellTargets
func (st *SpellTargeting) GetSpellTargets(luaSpell *LuaSpell, options *TargetingOptions) *TargetingResult {
if luaSpell == nil || luaSpell.Spell == nil {
return &TargetingResult{
ValidTargets: make([]int32, 0),
InvalidTargets: make([]int32, 0),
ErrorCode: FailureReasonInvalidTarget,
ErrorMessage: "Invalid spell or LuaSpell",
}
}
spell := luaSpell.Spell
spellData := spell.GetSpellData()
if spellData == nil {
return &TargetingResult{
ValidTargets: make([]int32, 0),
InvalidTargets: make([]int32, 0),
ErrorCode: FailureReasonInvalidTarget,
ErrorMessage: "Invalid spell data",
}
}
// Default options if none provided
if options == nil {
options = &TargetingOptions{}
}
result := &TargetingResult{
ValidTargets: make([]int32, 0),
InvalidTargets: make([]int32, 0),
ErrorCode: 0,
ErrorMessage: "",
}
// Determine targeting method based on spell target type
switch spellData.TargetType {
case TargetTypeSelf:
st.getSelfTargets(luaSpell, result, options)
case TargetTypeSingle:
st.getSingleTarget(luaSpell, result, options)
case TargetTypeGroup:
st.getGroupTargets(luaSpell, result, options)
case TargetTypeGroupAE:
st.getGroupAETargets(luaSpell, result, options)
case TargetTypeAE:
st.getAETargets(luaSpell, result, options)
case TargetTypePBAE:
st.getPBAETargets(luaSpell, result, options)
default:
result.ErrorCode = FailureReasonInvalidTarget
result.ErrorMessage = fmt.Sprintf("Unsupported target type: %d", spellData.TargetType)
}
return result
}
// getSelfTargets handles self-targeting spells
func (st *SpellTargeting) getSelfTargets(luaSpell *LuaSpell, result *TargetingResult, options *TargetingOptions) {
if luaSpell.CasterID != 0 {
result.ValidTargets = append(result.ValidTargets, luaSpell.CasterID)
luaSpell.AddTarget(luaSpell.CasterID)
} else {
result.ErrorCode = FailureReasonInvalidTarget
result.ErrorMessage = "No valid caster for self-targeting spell"
}
}
// getSingleTarget handles single-target spells
func (st *SpellTargeting) getSingleTarget(luaSpell *LuaSpell, result *TargetingResult, options *TargetingOptions) {
targetID := luaSpell.InitialTarget
if targetID == 0 {
targetID = luaSpell.CasterID // Default to self if no target
}
if st.isValidTarget(luaSpell, targetID, options) {
result.ValidTargets = append(result.ValidTargets, targetID)
luaSpell.AddTarget(targetID)
} else {
result.InvalidTargets = append(result.InvalidTargets, targetID)
result.ErrorCode = FailureReasonInvalidTarget
result.ErrorMessage = "Target is not valid for this spell"
}
}
// getGroupTargets handles group-targeting spells
func (st *SpellTargeting) getGroupTargets(luaSpell *LuaSpell, result *TargetingResult, options *TargetingOptions) {
// TODO: Implement group targeting when group system is available
// This would:
// 1. Get the caster's group
// 2. Iterate through group members
// 3. Validate each member as a target
// 4. Add valid members to the target list
// For now, just target self as placeholder
if luaSpell.CasterID != 0 {
result.ValidTargets = append(result.ValidTargets, luaSpell.CasterID)
luaSpell.AddTarget(luaSpell.CasterID)
}
}
// getGroupAETargets handles group area effect spells
func (st *SpellTargeting) getGroupAETargets(luaSpell *LuaSpell, result *TargetingResult, options *TargetingOptions) {
// TODO: Implement group AE targeting
// This is similar to group targeting but may include pets and other considerations
st.getGroupTargets(luaSpell, result, options)
}
// getAETargets handles area effect spells (true AOE)
func (st *SpellTargeting) getAETargets(luaSpell *LuaSpell, result *TargetingResult, options *TargetingOptions) {
// TODO: Implement AOE targeting when zone system is available
// This would:
// 1. Get the spell's radius from spell data
// 2. Find all entities within radius of the target location
// 3. Filter based on spell criteria (friendly/hostile, etc.)
// 4. Apply maximum target limits
// 5. Add valid targets to the list
// For now, implement basic logic
spellData := luaSpell.Spell.GetSpellData()
maxTargets := int32(10) // TODO: Use spellData.AOENodeNumber when field exists
if maxTargets <= 0 {
maxTargets = 10 // Default limit
}
// Placeholder: just target the initial target for now
if luaSpell.InitialTarget != 0 && st.isValidTarget(luaSpell, luaSpell.InitialTarget, options) {
result.ValidTargets = append(result.ValidTargets, luaSpell.InitialTarget)
luaSpell.AddTarget(luaSpell.InitialTarget)
}
// TODO: Use actual AOE node number when SpellData is updated
_ = maxTargets // Suppress unused variable warning
}
// getPBAETargets handles point-blank area effect spells (centered on caster)
func (st *SpellTargeting) getPBAETargets(luaSpell *LuaSpell, result *TargetingResult, options *TargetingOptions) {
// TODO: Implement PBAE targeting when zone system is available
// This is similar to AE but centered on the caster instead of a target location
// For now, just target self
if luaSpell.CasterID != 0 {
result.ValidTargets = append(result.ValidTargets, luaSpell.CasterID)
luaSpell.AddTarget(luaSpell.CasterID)
}
}
// isValidTarget validates whether a target is valid for a spell
func (st *SpellTargeting) isValidTarget(luaSpell *LuaSpell, targetID int32, options *TargetingOptions) bool {
if targetID == 0 {
return false
}
if options.BypassSpellChecks {
return true
}
// TODO: Implement comprehensive target validation when entity system is available
// This would check:
// 1. Target exists and is alive/dead as required
// 2. Target is within range
// 3. Line of sight if required
// 4. Spell can affect target type (player/NPC/etc.)
// 5. Faction/relationship requirements
// 6. Target is not immune to spell effects
// 7. Spell-specific requirements
return true // Placeholder - accept all targets for now
}
// ValidateRange checks if a target is within spell range
func (st *SpellTargeting) ValidateRange(casterID, targetID int32, spell *Spell, options *TargetingOptions) bool {
if options != nil && options.BypassRangeChecks {
return true
}
// TODO: Implement range checking when position system is available
// This would:
// 1. Get caster position
// 2. Get target position
// 3. Calculate distance
// 4. Compare to spell range
return true // Placeholder - all targets in range for now
}
// ValidateLineOfSight checks if caster has line of sight to target
func (st *SpellTargeting) ValidateLineOfSight(casterID, targetID int32, options *TargetingOptions) bool {
if options != nil && options.IgnoreLineOfSight {
return true
}
// TODO: Implement line of sight checking when zone geometry is available
// This would use ray tracing to check for obstacles between caster and target
return true // Placeholder - always have LOS for now
}
// GetPlayerGroupTargets gets valid group member targets for a spell
// This is converted from C++ SpellProcess::GetPlayerGroupTargets
func (st *SpellTargeting) GetPlayerGroupTargets(targetPlayerID, casterID int32, luaSpell *LuaSpell, options *TargetingOptions) bool {
// TODO: Implement when group system is available
// This would:
// 1. Get the target player's group
// 2. Validate caster can target this group
// 3. Add all valid group members as targets
// 4. Handle pets if included in group targeting
// For now, just add the target player
if st.isValidTarget(luaSpell, targetPlayerID, options) {
luaSpell.AddTarget(targetPlayerID)
return true
}
return false
}
// AddSelfAndPet adds caster and their pet to spell targets
func (st *SpellTargeting) AddSelfAndPet(luaSpell *LuaSpell, casterID int32, onlyPet bool) {
if !onlyPet && casterID != 0 {
luaSpell.AddTarget(casterID)
}
// TODO: Add pet targeting when pet system is available
// This would:
// 1. Get caster's active pet
// 2. Validate pet as target
// 3. Add pet to target list
}
// AddNPCGroupOrSelfTarget adds NPC group members or self to targets
func (st *SpellTargeting) AddNPCGroupOrSelfTarget(luaSpell *LuaSpell, targetID int32) {
// TODO: Implement NPC group targeting when NPC AI system is available
// NPCs may have different grouping mechanics than players
// For now, just add the target
if targetID != 0 {
luaSpell.AddTarget(targetID)
}
}
// CalculateDistance calculates distance between two entities
func (st *SpellTargeting) CalculateDistance(entity1ID, entity2ID int32) float32 {
// TODO: Implement when position system is available
// This would get positions and calculate 3D distance
return 0.0 // Placeholder
}
// IsInRange checks if target is within spell range
func (st *SpellTargeting) IsInRange(spell *Spell, distance float32) bool {
if spell == nil {
return false
}
spellData := spell.GetSpellData()
if spellData == nil {
return false
}
maxRange := spellData.Range
if maxRange <= 0 {
return true // No range limit
}
return distance <= maxRange
}
// GetSpellRange returns the maximum range for a spell
func (st *SpellTargeting) GetSpellRange(spell *Spell) float32 {
if spell == nil {
return 0.0
}
spellData := spell.GetSpellData()
if spellData == nil {
return 0.0
}
return spellData.Range
}
// GetSpellRadius returns the area effect radius for a spell
func (st *SpellTargeting) GetSpellRadius(spell *Spell) float32 {
if spell == nil {
return 0.0
}
spellData := spell.GetSpellData()
if spellData == nil {
return 0.0
}
return spellData.Radius
}
// GetMaxTargets returns the maximum number of targets for an AOE spell
func (st *SpellTargeting) GetMaxTargets(spell *Spell) int32 {
if spell == nil {
return 1
}
spellData := spell.GetSpellData()
if spellData == nil {
return 1
}
// TODO: Use spellData.AOENodeNumber when field exists
maxTargets := int32(1) // Default to single target
return maxTargets
}
// IsHostileSpell determines if a spell is hostile/aggressive
func (st *SpellTargeting) IsHostileSpell(spell *Spell) bool {
if spell == nil {
return false
}
// Check if spell is classified as offensive
return spell.IsOffenseSpell()
}
// IsFriendlySpell determines if a spell is beneficial/friendly
func (st *SpellTargeting) IsFriendlySpell(spell *Spell) bool {
if spell == nil {
return false
}
// Check if spell is classified as beneficial
return spell.IsBuffSpell() || spell.IsHealSpell()
}
// CanTargetSelf checks if spell can target the caster
func (st *SpellTargeting) CanTargetSelf(spell *Spell) bool {
if spell == nil {
return false
}
spellData := spell.GetSpellData()
if spellData == nil {
return false
}
// Check target type and spell flags
return spellData.TargetType == TargetTypeSelf ||
spellData.TargetType == TargetTypeGroup ||
spellData.TargetType == TargetTypePBAE
}
// CanTargetOthers checks if spell can target entities other than caster
func (st *SpellTargeting) CanTargetOthers(spell *Spell) bool {
if spell == nil {
return false
}
spellData := spell.GetSpellData()
if spellData == nil {
return false
}
// Most target types can target others, self-only spells cannot
return spellData.TargetType != TargetTypeSelf
}
// RequiresTarget checks if spell requires an explicit target selection
func (st *SpellTargeting) RequiresTarget(spell *Spell) bool {
if spell == nil {
return false
}
spellData := spell.GetSpellData()
if spellData == nil {
return false
}
// Single target spells typically require explicit targeting
return spellData.TargetType == TargetTypeSingle
}
// GetTargetingInfo returns information about spell targeting requirements
func (st *SpellTargeting) GetTargetingInfo(spell *Spell) map[string]interface{} {
info := make(map[string]interface{})
if spell == nil {
return info
}
spellData := spell.GetSpellData()
if spellData == nil {
return info
}
info["target_type"] = spellData.TargetType
info["range"] = spellData.Range
info["radius"] = spellData.Radius
info["max_targets"] = int32(1) // TODO: Use spellData.AOENodeNumber when field exists
info["can_target_self"] = st.CanTargetSelf(spell)
info["can_target_others"] = st.CanTargetOthers(spell)
info["requires_target"] = st.RequiresTarget(spell)
info["is_hostile"] = st.IsHostileSpell(spell)
info["is_friendly"] = st.IsFriendlySpell(spell)
return info
}

226
internal/titles/README.md Normal file
View File

@ -0,0 +1,226 @@
# Title System
The title system manages character titles in the EverQuest II server emulator, allowing players to earn, display, and manage various titles that represent their achievements and progression.
## Overview
The title system consists of several key components:
- **Master Titles List**: Global registry of all available titles
- **Player Title Collections**: Individual player title ownership and preferences
- **Title Manager**: Central coordination and management
- **Integration Systems**: Hooks for earning titles through various game activities
## Core Components
### Title Structure
Each title has the following properties:
- **ID**: Unique identifier
- **Name**: Display name of the title
- **Description**: Detailed description shown in UI
- **Position**: Whether it appears as prefix or suffix
- **Category**: Organizational category (Combat, Tradeskill, etc.)
- **Source**: How the title is obtained (Achievement, Quest, etc.)
- **Rarity**: Common, Uncommon, Rare, Epic, Legendary, Unique
- **Requirements**: Conditions that must be met to earn the title
- **Flags**: Various behavioral modifiers (Hidden, Temporary, Unique, etc.)
### Title Positioning
Titles can be displayed in two positions relative to the character name:
- **Prefix**: Appears before the character name (e.g., "Master John")
- **Suffix**: Appears after the character name (e.g., "John the Brave")
Players can have one active prefix and one active suffix title simultaneously.
### Title Sources
Titles can be obtained through various means:
- **Achievements**: Completing specific achievements
- **Quests**: Finishing particular quest lines
- **Tradeskills**: Reaching mastery in crafting
- **Combat**: Battle-related accomplishments
- **Exploration**: Discovering new areas
- **PvP**: Player vs Player activities
- **Guild**: Guild progression and achievements
- **Events**: Special server events
- **Rare**: Uncommon encounters or collections
## Usage Examples
### Basic Title Management
```go
// Create a title manager
titleManager := titles.NewTitleManager()
// Create a new title
title, err := titleManager.CreateTitle(
"Dragon Slayer", // name
"Defeated an ancient dragon", // description
titles.CategoryCombat, // category
titles.TitlePositionSuffix, // position
titles.TitleSourceAchievement, // source
titles.TitleRarityEpic, // rarity
)
// Grant a title to a player
err = titleManager.GrantTitle(playerID, titleID, achievementID, 0)
// Set active titles
err = titleManager.SetPlayerActivePrefix(playerID, prefixTitleID)
err = titleManager.SetPlayerActiveSuffix(playerID, suffixTitleID)
// Get formatted player name with titles
formattedName := titleManager.GetPlayerFormattedName(playerID, "John")
// Result: "Master John the Dragon Slayer"
```
### Achievement Integration
```go
// Set up achievement integration
integrationManager := titles.NewIntegrationManager(titleManager)
// Create achievement-linked title
title, err := titleManager.CreateAchievementTitle(
"Dungeon Master",
"Completed 100 dungeons",
achievementID,
titles.TitlePositionPrefix,
titles.TitleRarityRare,
)
// When achievement is completed
err = integrationManager.GetAchievementIntegration().OnAchievementCompleted(playerID, achievementID)
```
### Event Titles
```go
// Start a seasonal event with title reward
eventIntegration := integrationManager.GetEventIntegration()
err = eventIntegration.StartEvent(
"Halloween 2024",
"Spooky seasonal event",
7*24*time.Hour, // 1 week duration
halloweenTitleID,
)
// Grant participation title
err = eventIntegration.OnEventParticipation(playerID, "Halloween 2024")
```
## File Structure
- `constants.go`: Title system constants and enums
- `title.go`: Core title and player title data structures
- `master_list.go`: Global title registry and management
- `player_titles.go`: Individual player title collections
- `title_manager.go`: Central title system coordinator
- `integration.go`: Integration systems for earning titles
- `README.md`: This documentation file
## Database Integration
The title system is designed to integrate with the database layer:
- **Master titles**: Stored in `titles` table
- **Player titles**: Stored in `character_titles` table
- **Title requirements**: Stored in `title_requirements` table
Database methods are marked as TODO and will be implemented when the database package is available.
## Network Packets
The system supports the following network packets:
- **TitleUpdate**: Sends player's available titles to client
- **UpdateTitle**: Updates displayed title information
Packet structures are defined based on the XML definitions in the packets directory.
## Title Categories
The system organizes titles into logical categories:
- **Combat**: Battle and PvP related titles
- **Tradeskill**: Crafting and gathering achievements
- **Exploration**: Zone discovery and travel
- **Social**: Community and roleplay titles
- **Achievement**: General accomplishment titles
- **Quest**: Story and mission completion
- **Rare**: Uncommon encounters and collections
- **Seasonal**: Time-limited event titles
- **Guild**: Organization-based titles
- **Raid**: Group content achievements
- **Class**: Profession-specific titles
- **Race**: Heritage and background titles
## Title Rarity System
Titles have six rarity levels with associated colors:
1. **Common** (White): Easily obtainable titles
2. **Uncommon** (Green): Moderate effort required
3. **Rare** (Blue): Significant achievement needed
4. **Epic** (Purple): Exceptional accomplishment
5. **Legendary** (Orange): Extremely difficult to obtain
6. **Unique** (Red): One-of-a-kind titles
## Special Title Types
### Temporary Titles
Some titles expire after a certain period:
- Event titles that expire when the event ends
- Temporary status titles (e.g., "Newcomer")
- Achievement-based titles with time limits
### Unique Titles
Certain titles can only be held by one player at a time:
- "First to reach max level"
- "Server champion"
- Special recognition titles
### Account-Wide Titles
Some titles are shared across all characters on an account:
- Beta tester recognition
- Special event participation
- Founder rewards
## Thread Safety
All title system components are thread-safe using appropriate synchronization:
- `sync.RWMutex` for read-heavy operations
- Atomic operations where appropriate
- Proper locking hierarchy to prevent deadlocks
## Performance Considerations
- Title lookups are optimized with indexed maps
- Player title data is cached in memory
- Background cleanup processes handle expired titles
- Database operations are batched when possible
## Future Enhancements
Planned improvements include:
- Advanced title search and filtering
- Title display customization options
- Title trading/gifting system
- Dynamic title generation
- Integration with guild systems
- Advanced achievement requirements
- Title collection statistics and tracking

View File

@ -0,0 +1,106 @@
package titles
// Title types and constants
const (
// Title positioning
TitlePositionPrefix = 1 // Title appears before character name
TitlePositionSuffix = 0 // Title appears after character name
// Title sources - how titles are obtained
TitleSourceAchievement = 1 // From completing achievements
TitleSourceQuest = 2 // From completing quests
TitleSourceTradeskill = 3 // From tradeskill mastery
TitleSourceCombat = 4 // From combat achievements
TitleSourceExploration = 5 // From exploring zones
TitleSourceRare = 6 // From rare collections/encounters
TitleSourceGuildRank = 7 // From guild progression
TitleSourcePvP = 8 // From PvP activities
TitleSourceRaid = 9 // From raid completions
TitleSourceHoliday = 10 // From holiday events
TitleSourceBetaTester = 11 // Beta testing rewards
TitleSourceDeveloper = 12 // Developer/GM titles
TitleSourceRoleplay = 13 // Roleplay-related titles
TitleSourceMiscellaneous = 14 // Other/uncategorized
// Title display formats
DisplayFormatSimple = 0 // Just the title text
DisplayFormatWithBrackets = 1 // [Title]
DisplayFormatWithQuotes = 2 // "Title"
DisplayFormatWithCommas = 3 // ,Title,
// Title rarity levels
TitleRarityCommon = 0 // Common titles easily obtained
TitleRarityUncommon = 1 // Moderately difficult to obtain
TitleRarityRare = 2 // Difficult to obtain
TitleRarityEpic = 3 // Very difficult to obtain
TitleRarityLegendary = 4 // Extremely rare titles
TitleRarityUnique = 5 // One-of-a-kind titles
// Title categories for organization
CategoryCombat = "Combat"
CategoryTradeskill = "Tradeskill"
CategoryExploration = "Exploration"
CategorySocial = "Social"
CategoryAchievement = "Achievement"
CategoryQuest = "Quest"
CategoryRare = "Rare"
CategorySeasonal = "Seasonal"
CategoryGuild = "Guild"
CategoryPvP = "PvP"
CategoryRaid = "Raid"
CategoryClass = "Class"
CategoryRace = "Race"
CategoryMiscellaneous = "Miscellaneous"
// Title unlock requirements
RequirementTypeLevel = 1 // Character level requirement
RequirementTypeQuest = 2 // Specific quest completion
RequirementTypeAchievement = 3 // Achievement completion
RequirementTypeSkill = 4 // Skill level requirement
RequirementTypeTradeskill = 5 // Tradeskill level
RequirementTypeCollection = 6 // Collection completion
RequirementTypeKill = 7 // Kill count requirement
RequirementTypeExploration = 8 // Zone discovery
RequirementTypeTime = 9 // Time-based requirement
RequirementTypeItem = 10 // Item possession
RequirementTypeGuild = 11 // Guild membership/rank
RequirementTypeFaction = 12 // Faction standing
RequirementTypeClass = 13 // Specific class requirement
RequirementTypeRace = 14 // Specific race requirement
RequirementTypeAlignment = 15 // Good/Evil alignment
RequirementTypeZone = 16 // Specific zone requirement
RequirementTypeExpansion = 17 // Expansion ownership
// Title flags
FlagHidden = 1 << 0 // Hidden from normal display
FlagAccountWide = 1 << 1 // Available to all characters on account
FlagUnique = 1 << 2 // Only one player can have this title
FlagTemporary = 1 << 3 // Title expires after time
FlagEventRestricted = 1 << 4 // Only available during events
FlagNoLongerAvailable = 1 << 5 // Legacy title no longer obtainable
FlagStarter = 1 << 6 // Available to new characters
FlagGMOnly = 1 << 7 // Game Master only
FlagBetaRestricted = 1 << 8 // Beta tester only
FlagRoleplayFriendly = 1 << 9 // Designed for roleplay
// Maximum limits
MaxTitleNameLength = 255 // Maximum characters in title name
MaxTitleDescriptionLength = 512 // Maximum characters in description
MaxPlayerTitles = 500 // Maximum titles per player
MaxTitleRequirements = 10 // Maximum requirements per title
// Title IDs - Special/System titles (using negative IDs to avoid conflicts)
TitleIDNone = 0 // No title selected
TitleIDCitizen = -1 // Default citizen title
TitleIDVisitor = -2 // Default visitor title
TitleIDNewcomer = -3 // New player title
TitleIDReturning = -4 // Returning player title
// Color codes for title rarity display
ColorCommon = 0xFFFFFF // White
ColorUncommon = 0x00FF00 // Green
ColorRare = 0x0080FF // Blue
ColorEpic = 0x8000FF // Purple
ColorLegendary = 0xFF8000 // Orange
ColorUnique = 0xFF0000 // Red
)

View File

@ -0,0 +1,403 @@
package titles
import (
"fmt"
"time"
)
// AchievementIntegration handles title granting from achievements
type AchievementIntegration struct {
titleManager *TitleManager
}
// NewAchievementIntegration creates a new achievement integration handler
func NewAchievementIntegration(titleManager *TitleManager) *AchievementIntegration {
return &AchievementIntegration{
titleManager: titleManager,
}
}
// OnAchievementCompleted is called when a player completes an achievement
func (ai *AchievementIntegration) OnAchievementCompleted(playerID int32, achievementID uint32) error {
return ai.titleManager.ProcessAchievementCompletion(playerID, achievementID)
}
// QuestIntegration handles title granting from quest completion
type QuestIntegration struct {
titleManager *TitleManager
questTitles map[uint32]int32 // Maps quest ID to title ID
}
// NewQuestIntegration creates a new quest integration handler
func NewQuestIntegration(titleManager *TitleManager) *QuestIntegration {
qi := &QuestIntegration{
titleManager: titleManager,
questTitles: make(map[uint32]int32),
}
// Initialize quest-to-title mappings
qi.initializeQuestTitles()
return qi
}
// initializeQuestTitles sets up quest-to-title relationships
func (qi *QuestIntegration) initializeQuestTitles() {
// TODO: Load quest-title mappings from database or configuration
// These would be examples of quest rewards that grant titles
// Example mappings (these would come from quest definitions):
// qi.questTitles[1001] = heroTitleID // "Hero of Qeynos" from major storyline
// qi.questTitles[2001] = explorerTitleID // "Explorer" from exploration quest
// qi.questTitles[3001] = merchantTitleID // "Master Merchant" from trading quest
}
// OnQuestCompleted is called when a player completes a quest
func (qi *QuestIntegration) OnQuestCompleted(playerID int32, questID uint32) error {
titleID, exists := qi.questTitles[questID]
if !exists {
return nil // Quest doesn't grant a title
}
return qi.titleManager.GrantTitle(playerID, titleID, 0, questID)
}
// AddQuestTitleMapping adds a quest-to-title relationship
func (qi *QuestIntegration) AddQuestTitleMapping(questID uint32, titleID int32) {
qi.questTitles[questID] = titleID
}
// RemoveQuestTitleMapping removes a quest-to-title relationship
func (qi *QuestIntegration) RemoveQuestTitleMapping(questID uint32) {
delete(qi.questTitles, questID)
}
// LevelIntegration handles title granting based on character level
type LevelIntegration struct {
titleManager *TitleManager
levelTitles map[int32]int32 // Maps level to title ID
}
// NewLevelIntegration creates a new level integration handler
func NewLevelIntegration(titleManager *TitleManager) *LevelIntegration {
li := &LevelIntegration{
titleManager: titleManager,
levelTitles: make(map[int32]int32),
}
// Initialize level-based titles
li.initializeLevelTitles()
return li
}
// initializeLevelTitles sets up level milestone titles
func (li *LevelIntegration) initializeLevelTitles() {
// TODO: Create and register level milestone titles
// These would be created in the title manager and their IDs stored here
// Example level titles:
// li.levelTitles[10] = noviceTitleID // "Novice" at level 10
// li.levelTitles[25] = adeptTitleID // "Adept" at level 25
// li.levelTitles[50] = veteranTitleID // "Veteran" at level 50
// li.levelTitles[80] = masterTitleID // "Master" at level 80
// li.levelTitles[90] = championTitleID // "Champion" at level 90
}
// OnLevelUp is called when a player levels up
func (li *LevelIntegration) OnLevelUp(playerID, newLevel int32) error {
titleID, exists := li.levelTitles[newLevel]
if !exists {
return nil // No title for this level
}
return li.titleManager.GrantTitle(playerID, titleID, 0, 0)
}
// GuildIntegration handles title granting from guild activities
type GuildIntegration struct {
titleManager *TitleManager
}
// NewGuildIntegration creates a new guild integration handler
func NewGuildIntegration(titleManager *TitleManager) *GuildIntegration {
return &GuildIntegration{
titleManager: titleManager,
}
}
// OnGuildRankChanged is called when a player's guild rank changes
func (gi *GuildIntegration) OnGuildRankChanged(playerID int32, guildID, newRank int32) error {
// TODO: Implement guild rank titles
// Different guild ranks could grant different titles
// Example: Guild leaders get "Guild Leader" title
// if newRank == GUILD_RANK_LEADER {
// return gi.titleManager.GrantTitle(playerID, guildLeaderTitleID, 0, 0)
// }
return nil
}
// OnGuildAchievement is called when a guild completes an achievement
func (gi *GuildIntegration) OnGuildAchievement(guildID int32, achievementID uint32, memberIDs []int32) error {
// TODO: Implement guild achievement titles
// Guild achievements could grant titles to all participating members
// Example: Grant title to all guild members who participated
// for _, memberID := range memberIDs {
// err := gi.titleManager.GrantTitle(memberID, guildAchievementTitleID, achievementID, 0)
// if err != nil {
// // Log error but continue processing other members
// }
// }
return nil
}
// PvPIntegration handles title granting from PvP activities
type PvPIntegration struct {
titleManager *TitleManager
pvpStats map[int32]*PvPStats // Track PvP statistics per player
}
// PvPStats tracks player PvP statistics for title eligibility
type PvPStats struct {
PlayerKills int32
PlayerDeaths int32
HonorPoints int32
LastKillTime time.Time
KillStreak int32
MaxKillStreak int32
}
// NewPvPIntegration creates a new PvP integration handler
func NewPvPIntegration(titleManager *TitleManager) *PvPIntegration {
return &PvPIntegration{
titleManager: titleManager,
pvpStats: make(map[int32]*PvPStats),
}
}
// OnPlayerKill is called when a player kills another player
func (pi *PvPIntegration) OnPlayerKill(killerID, victimID int32, honorGained int32) error {
// Update killer stats
killerStats := pi.getOrCreateStats(killerID)
killerStats.PlayerKills++
killerStats.HonorPoints += honorGained
killerStats.LastKillTime = time.Now()
killerStats.KillStreak++
if killerStats.KillStreak > killerStats.MaxKillStreak {
killerStats.MaxKillStreak = killerStats.KillStreak
}
// Update victim stats
victimStats := pi.getOrCreateStats(victimID)
victimStats.PlayerDeaths++
victimStats.KillStreak = 0 // Reset kill streak on death
// Check for PvP milestone titles
return pi.checkPvPTitles(killerID, killerStats)
}
// getOrCreateStats gets or creates PvP stats for a player
func (pi *PvPIntegration) getOrCreateStats(playerID int32) *PvPStats {
stats, exists := pi.pvpStats[playerID]
if !exists {
stats = &PvPStats{}
pi.pvpStats[playerID] = stats
}
return stats
}
// checkPvPTitles checks if player qualifies for any PvP titles
func (pi *PvPIntegration) checkPvPTitles(playerID int32, stats *PvPStats) error {
// TODO: Implement PvP title thresholds and grant appropriate titles
// Example PvP title thresholds:
// if stats.PlayerKills >= 100 && stats.PlayerKills < 500 {
// pi.titleManager.GrantTitle(playerID, slayerTitleID, 0, 0)
// } else if stats.PlayerKills >= 500 && stats.PlayerKills < 1000 {
// pi.titleManager.GrantTitle(playerID, killerTitleID, 0, 0)
// } else if stats.PlayerKills >= 1000 {
// pi.titleManager.GrantTitle(playerID, warlordTitleID, 0, 0)
// }
// if stats.MaxKillStreak >= 10 {
// pi.titleManager.GrantTitle(playerID, unstoppableTitleID, 0, 0)
// }
return nil
}
// EventIntegration handles title granting from special events
type EventIntegration struct {
titleManager *TitleManager
activeEvents map[string]*Event // Active events by name
eventTitles map[string]int32 // Maps event name to title ID
}
// Event represents a time-limited server event
type Event struct {
Name string
StartTime time.Time
EndTime time.Time
IsActive bool
Description string
TitleID int32 // Title granted for participation
}
// NewEventIntegration creates a new event integration handler
func NewEventIntegration(titleManager *TitleManager) *EventIntegration {
return &EventIntegration{
titleManager: titleManager,
activeEvents: make(map[string]*Event),
eventTitles: make(map[string]int32),
}
}
// StartEvent activates a special event
func (ei *EventIntegration) StartEvent(name, description string, duration time.Duration, titleID int32) error {
event := &Event{
Name: name,
StartTime: time.Now(),
EndTime: time.Now().Add(duration),
IsActive: true,
Description: description,
TitleID: titleID,
}
ei.activeEvents[name] = event
ei.eventTitles[name] = titleID
return nil
}
// EndEvent deactivates a special event
func (ei *EventIntegration) EndEvent(name string) error {
event, exists := ei.activeEvents[name]
if !exists {
return fmt.Errorf("event %s does not exist", name)
}
event.IsActive = false
delete(ei.activeEvents, name)
return nil
}
// OnEventParticipation is called when a player participates in an event
func (ei *EventIntegration) OnEventParticipation(playerID int32, eventName string) error {
event, exists := ei.activeEvents[eventName]
if !exists || !event.IsActive {
return fmt.Errorf("event %s is not active", eventName)
}
// Check if event is still within time bounds
now := time.Now()
if now.Before(event.StartTime) || now.After(event.EndTime) {
event.IsActive = false
delete(ei.activeEvents, eventName)
return fmt.Errorf("event %s has expired", eventName)
}
// Grant event participation title
if event.TitleID > 0 {
return ei.titleManager.GrantTitle(playerID, event.TitleID, 0, 0)
}
return nil
}
// GetActiveEvents returns all currently active events
func (ei *EventIntegration) GetActiveEvents() []*Event {
result := make([]*Event, 0, len(ei.activeEvents))
for _, event := range ei.activeEvents {
// Check if event is still valid
now := time.Now()
if now.After(event.EndTime) {
event.IsActive = false
continue
}
if event.IsActive {
result = append(result, event)
}
}
return result
}
// TitleEarnedCallback represents a callback function called when a title is earned
type TitleEarnedCallback func(playerID, titleID int32, source string)
// IntegrationManager coordinates all title integration systems
type IntegrationManager struct {
titleManager *TitleManager
achievementIntegration *AchievementIntegration
questIntegration *QuestIntegration
levelIntegration *LevelIntegration
guildIntegration *GuildIntegration
pvpIntegration *PvPIntegration
eventIntegration *EventIntegration
callbacks []TitleEarnedCallback
}
// NewIntegrationManager creates a comprehensive integration manager
func NewIntegrationManager(titleManager *TitleManager) *IntegrationManager {
return &IntegrationManager{
titleManager: titleManager,
achievementIntegration: NewAchievementIntegration(titleManager),
questIntegration: NewQuestIntegration(titleManager),
levelIntegration: NewLevelIntegration(titleManager),
guildIntegration: NewGuildIntegration(titleManager),
pvpIntegration: NewPvPIntegration(titleManager),
eventIntegration: NewEventIntegration(titleManager),
callbacks: make([]TitleEarnedCallback, 0),
}
}
// AddTitleEarnedCallback adds a callback to be notified when titles are earned
func (im *IntegrationManager) AddTitleEarnedCallback(callback TitleEarnedCallback) {
im.callbacks = append(im.callbacks, callback)
}
// NotifyTitleEarned calls all registered callbacks when a title is earned
func (im *IntegrationManager) NotifyTitleEarned(playerID, titleID int32, source string) {
for _, callback := range im.callbacks {
callback(playerID, titleID, source)
}
}
// GetAchievementIntegration returns the achievement integration handler
func (im *IntegrationManager) GetAchievementIntegration() *AchievementIntegration {
return im.achievementIntegration
}
// GetQuestIntegration returns the quest integration handler
func (im *IntegrationManager) GetQuestIntegration() *QuestIntegration {
return im.questIntegration
}
// GetLevelIntegration returns the level integration handler
func (im *IntegrationManager) GetLevelIntegration() *LevelIntegration {
return im.levelIntegration
}
// GetGuildIntegration returns the guild integration handler
func (im *IntegrationManager) GetGuildIntegration() *GuildIntegration {
return im.guildIntegration
}
// GetPvPIntegration returns the PvP integration handler
func (im *IntegrationManager) GetPvPIntegration() *PvPIntegration {
return im.pvpIntegration
}
// GetEventIntegration returns the event integration handler
func (im *IntegrationManager) GetEventIntegration() *EventIntegration {
return im.eventIntegration
}

View File

@ -0,0 +1,420 @@
package titles
import (
"fmt"
"sync"
)
// MasterTitlesList manages all available titles in the game
type MasterTitlesList struct {
titles map[int32]*Title // All titles indexed by ID
categorized map[string][]*Title // Titles grouped by category
bySource map[int32][]*Title // Titles grouped by source
byRarity map[int32][]*Title // Titles grouped by rarity
byAchievement map[uint32]*Title // Titles indexed by achievement ID
nextID int32 // Next available title ID
mutex sync.RWMutex // Thread safety
}
// NewMasterTitlesList creates a new master titles list
func NewMasterTitlesList() *MasterTitlesList {
mtl := &MasterTitlesList{
titles: make(map[int32]*Title),
categorized: make(map[string][]*Title),
bySource: make(map[int32][]*Title),
byRarity: make(map[int32][]*Title),
byAchievement: make(map[uint32]*Title),
nextID: 1,
}
// Initialize default titles
mtl.initializeDefaultTitles()
return mtl
}
// initializeDefaultTitles creates the basic system titles
func (mtl *MasterTitlesList) initializeDefaultTitles() {
// System titles with negative IDs
citizen := NewTitle(TitleIDCitizen, "Citizen")
citizen.SetDescription("Default citizen title")
citizen.SetFlag(FlagStarter)
citizen.Position = TitlePositionSuffix
mtl.addTitleInternal(citizen)
visitor := NewTitle(TitleIDVisitor, "Visitor")
visitor.SetDescription("Temporary visitor status")
visitor.SetFlag(FlagTemporary)
visitor.Position = TitlePositionSuffix
mtl.addTitleInternal(visitor)
newcomer := NewTitle(TitleIDNewcomer, "Newcomer")
newcomer.SetDescription("New player welcome title")
newcomer.SetFlag(FlagStarter)
newcomer.ExpirationHours = 168 // 1 week
newcomer.Position = TitlePositionPrefix
mtl.addTitleInternal(newcomer)
returning := NewTitle(TitleIDReturning, "Returning")
returning.SetDescription("Welcome back title for returning players")
returning.SetFlag(FlagTemporary)
returning.ExpirationHours = 72 // 3 days
returning.Position = TitlePositionPrefix
mtl.addTitleInternal(returning)
}
// AddTitle adds a new title to the master list
func (mtl *MasterTitlesList) AddTitle(title *Title) error {
mtl.mutex.Lock()
defer mtl.mutex.Unlock()
if title == nil {
return fmt.Errorf("cannot add nil title")
}
// Assign ID if not set
if title.ID == 0 {
title.ID = mtl.nextID
mtl.nextID++
}
// Check for duplicate ID
if _, exists := mtl.titles[title.ID]; exists {
return fmt.Errorf("title with ID %d already exists", title.ID)
}
// Validate title name length
if len(title.Name) > MaxTitleNameLength {
return fmt.Errorf("title name exceeds maximum length of %d characters", MaxTitleNameLength)
}
// Validate description length
if len(title.Description) > MaxTitleDescriptionLength {
return fmt.Errorf("title description exceeds maximum length of %d characters", MaxTitleDescriptionLength)
}
// Check for unique titles
if title.IsUnique() {
// TODO: Check if any player already has this unique title
}
return mtl.addTitleInternal(title)
}
// addTitleInternal adds a title without validation (used internally)
func (mtl *MasterTitlesList) addTitleInternal(title *Title) error {
// Add to main map
mtl.titles[title.ID] = title
// Add to category index
if mtl.categorized[title.Category] == nil {
mtl.categorized[title.Category] = make([]*Title, 0)
}
mtl.categorized[title.Category] = append(mtl.categorized[title.Category], title)
// Add to source index
if mtl.bySource[title.Source] == nil {
mtl.bySource[title.Source] = make([]*Title, 0)
}
mtl.bySource[title.Source] = append(mtl.bySource[title.Source], title)
// Add to rarity index
if mtl.byRarity[title.Rarity] == nil {
mtl.byRarity[title.Rarity] = make([]*Title, 0)
}
mtl.byRarity[title.Rarity] = append(mtl.byRarity[title.Rarity], title)
// Add to achievement index if applicable
if title.AchievementID > 0 {
mtl.byAchievement[title.AchievementID] = title
}
// Update next ID if necessary
if title.ID >= mtl.nextID {
mtl.nextID = title.ID + 1
}
return nil
}
// GetTitle retrieves a title by ID
func (mtl *MasterTitlesList) GetTitle(id int32) (*Title, bool) {
mtl.mutex.RLock()
defer mtl.mutex.RUnlock()
title, exists := mtl.titles[id]
if !exists {
return nil, false
}
return title.Clone(), true
}
// GetTitleByName retrieves a title by name (case-sensitive)
func (mtl *MasterTitlesList) GetTitleByName(name string) (*Title, bool) {
mtl.mutex.RLock()
defer mtl.mutex.RUnlock()
for _, title := range mtl.titles {
if title.Name == name {
return title.Clone(), true
}
}
return nil, false
}
// GetTitleByAchievement retrieves a title associated with an achievement
func (mtl *MasterTitlesList) GetTitleByAchievement(achievementID uint32) (*Title, bool) {
mtl.mutex.RLock()
defer mtl.mutex.RUnlock()
title, exists := mtl.byAchievement[achievementID]
if !exists {
return nil, false
}
return title.Clone(), true
}
// GetTitlesByCategory retrieves all titles in a specific category
func (mtl *MasterTitlesList) GetTitlesByCategory(category string) []*Title {
mtl.mutex.RLock()
defer mtl.mutex.RUnlock()
titles := mtl.categorized[category]
if titles == nil {
return make([]*Title, 0)
}
// Return clones to prevent external modification
result := make([]*Title, len(titles))
for i, title := range titles {
result[i] = title.Clone()
}
return result
}
// GetTitlesBySource retrieves all titles from a specific source
func (mtl *MasterTitlesList) GetTitlesBySource(source int32) []*Title {
mtl.mutex.RLock()
defer mtl.mutex.RUnlock()
titles := mtl.bySource[source]
if titles == nil {
return make([]*Title, 0)
}
// Return clones to prevent external modification
result := make([]*Title, len(titles))
for i, title := range titles {
result[i] = title.Clone()
}
return result
}
// GetTitlesByRarity retrieves all titles of a specific rarity
func (mtl *MasterTitlesList) GetTitlesByRarity(rarity int32) []*Title {
mtl.mutex.RLock()
defer mtl.mutex.RUnlock()
titles := mtl.byRarity[rarity]
if titles == nil {
return make([]*Title, 0)
}
// Return clones to prevent external modification
result := make([]*Title, len(titles))
for i, title := range titles {
result[i] = title.Clone()
}
return result
}
// GetAllTitles retrieves all titles (excluding hidden ones by default)
func (mtl *MasterTitlesList) GetAllTitles(includeHidden bool) []*Title {
mtl.mutex.RLock()
defer mtl.mutex.RUnlock()
result := make([]*Title, 0, len(mtl.titles))
for _, title := range mtl.titles {
if !includeHidden && title.IsHidden() {
continue
}
result = append(result, title.Clone())
}
return result
}
// GetAvailableCategories returns all categories that have titles
func (mtl *MasterTitlesList) GetAvailableCategories() []string {
mtl.mutex.RLock()
defer mtl.mutex.RUnlock()
categories := make([]string, 0, len(mtl.categorized))
for category := range mtl.categorized {
categories = append(categories, category)
}
return categories
}
// RemoveTitle removes a title from the master list
func (mtl *MasterTitlesList) RemoveTitle(id int32) error {
mtl.mutex.Lock()
defer mtl.mutex.Unlock()
title, exists := mtl.titles[id]
if !exists {
return fmt.Errorf("title with ID %d does not exist", id)
}
// Remove from main map
delete(mtl.titles, id)
// Remove from category index
mtl.removeFromSlice(&mtl.categorized[title.Category], title)
if len(mtl.categorized[title.Category]) == 0 {
delete(mtl.categorized, title.Category)
}
// Remove from source index
mtl.removeFromSlice(&mtl.bySource[title.Source], title)
if len(mtl.bySource[title.Source]) == 0 {
delete(mtl.bySource, title.Source)
}
// Remove from rarity index
mtl.removeFromSlice(&mtl.byRarity[title.Rarity], title)
if len(mtl.byRarity[title.Rarity]) == 0 {
delete(mtl.byRarity, title.Rarity)
}
// Remove from achievement index if applicable
if title.AchievementID > 0 {
delete(mtl.byAchievement, title.AchievementID)
}
return nil
}
// removeFromSlice removes a title from a slice
func (mtl *MasterTitlesList) removeFromSlice(slice *[]*Title, title *Title) {
for i, t := range *slice {
if t.ID == title.ID {
*slice = append((*slice)[:i], (*slice)[i+1:]...)
break
}
}
}
// UpdateTitle updates an existing title
func (mtl *MasterTitlesList) UpdateTitle(title *Title) error {
mtl.mutex.Lock()
defer mtl.mutex.Unlock()
if title == nil {
return fmt.Errorf("cannot update with nil title")
}
existing, exists := mtl.titles[title.ID]
if !exists {
return fmt.Errorf("title with ID %d does not exist", title.ID)
}
// Remove old title from indices
mtl.removeFromSlice(&mtl.categorized[existing.Category], existing)
mtl.removeFromSlice(&mtl.bySource[existing.Source], existing)
mtl.removeFromSlice(&mtl.byRarity[existing.Rarity], existing)
if existing.AchievementID > 0 {
delete(mtl.byAchievement, existing.AchievementID)
}
// Update the title
mtl.titles[title.ID] = title
// Re-add to indices with new values
if mtl.categorized[title.Category] == nil {
mtl.categorized[title.Category] = make([]*Title, 0)
}
mtl.categorized[title.Category] = append(mtl.categorized[title.Category], title)
if mtl.bySource[title.Source] == nil {
mtl.bySource[title.Source] = make([]*Title, 0)
}
mtl.bySource[title.Source] = append(mtl.bySource[title.Source], title)
if mtl.byRarity[title.Rarity] == nil {
mtl.byRarity[title.Rarity] = make([]*Title, 0)
}
mtl.byRarity[title.Rarity] = append(mtl.byRarity[title.Rarity], title)
if title.AchievementID > 0 {
mtl.byAchievement[title.AchievementID] = title
}
return nil
}
// GetTitleCount returns the total number of titles
func (mtl *MasterTitlesList) GetTitleCount() int {
mtl.mutex.RLock()
defer mtl.mutex.RUnlock()
return len(mtl.titles)
}
// ValidateTitle checks if a title meets all requirements
func (mtl *MasterTitlesList) ValidateTitle(title *Title) error {
if title == nil {
return fmt.Errorf("title cannot be nil")
}
if len(title.Name) == 0 {
return fmt.Errorf("title name cannot be empty")
}
if len(title.Name) > MaxTitleNameLength {
return fmt.Errorf("title name exceeds maximum length of %d characters", MaxTitleNameLength)
}
if len(title.Description) > MaxTitleDescriptionLength {
return fmt.Errorf("title description exceeds maximum length of %d characters", MaxTitleDescriptionLength)
}
if len(title.Requirements) > MaxTitleRequirements {
return fmt.Errorf("title has too many requirements (max %d)", MaxTitleRequirements)
}
// Validate position
if title.Position != TitlePositionPrefix && title.Position != TitlePositionSuffix {
return fmt.Errorf("invalid title position: %d", title.Position)
}
// Validate rarity
if title.Rarity < TitleRarityCommon || title.Rarity > TitleRarityUnique {
return fmt.Errorf("invalid title rarity: %d", title.Rarity)
}
return nil
}
// LoadFromDatabase would load titles from the database
// TODO: Implement database integration with zone/database package
func (mtl *MasterTitlesList) LoadFromDatabase() error {
// TODO: Implement database loading
return fmt.Errorf("LoadFromDatabase not yet implemented - requires database integration")
}
// SaveToDatabase would save titles to the database
// TODO: Implement database integration with zone/database package
func (mtl *MasterTitlesList) SaveToDatabase() error {
// TODO: Implement database saving
return fmt.Errorf("SaveToDatabase not yet implemented - requires database integration")
}

View File

@ -0,0 +1,462 @@
package titles
import (
"fmt"
"sync"
"time"
)
// PlayerTitlesList manages titles owned by a specific player
type PlayerTitlesList struct {
playerID int32 // Character ID
titles map[int32]*PlayerTitle // Owned titles indexed by title ID
activePrefixID int32 // Currently active prefix title ID (0 = none)
activeSuffixID int32 // Currently active suffix title ID (0 = none)
masterList *MasterTitlesList // Reference to master titles list
mutex sync.RWMutex // Thread safety
}
// TitlePacketData represents title data for network packets
type TitlePacketData struct {
PlayerID uint32 `json:"player_id"`
PlayerName string `json:"player_name"`
PrefixTitle string `json:"prefix_title"`
SuffixTitle string `json:"suffix_title"`
SubTitle string `json:"sub_title"`
LastName string `json:"last_name"`
Titles []TitleEntry `json:"titles"`
NumTitles uint16 `json:"num_titles"`
CurrentPrefix int16 `json:"current_prefix"`
CurrentSuffix int16 `json:"current_suffix"`
}
// TitleEntry represents a single title for packet transmission
type TitleEntry struct {
Name string `json:"name"`
IsPrefix bool `json:"is_prefix"`
}
// NewPlayerTitlesList creates a new player titles list
func NewPlayerTitlesList(playerID int32, masterList *MasterTitlesList) *PlayerTitlesList {
ptl := &PlayerTitlesList{
playerID: playerID,
titles: make(map[int32]*PlayerTitle),
activePrefixID: TitleIDNone,
activeSuffixID: TitleIDCitizen, // Default suffix title
masterList: masterList,
}
// Grant default citizen title
ptl.grantDefaultTitle()
return ptl
}
// grantDefaultTitle grants the basic citizen title to new players
func (ptl *PlayerTitlesList) grantDefaultTitle() {
citizenTitle := NewPlayerTitle(TitleIDCitizen, ptl.playerID)
citizenTitle.IsActive = true
citizenTitle.IsPrefix = false // Suffix title
ptl.titles[TitleIDCitizen] = citizenTitle
}
// AddTitle grants a title to the player
func (ptl *PlayerTitlesList) AddTitle(titleID int32, sourceAchievementID, sourceQuestID uint32) error {
ptl.mutex.Lock()
defer ptl.mutex.Unlock()
// Check if player already has this title
if _, exists := ptl.titles[titleID]; exists {
return fmt.Errorf("player %d already has title %d", ptl.playerID, titleID)
}
// Verify title exists in master list
masterTitle, exists := ptl.masterList.GetTitle(titleID)
if !exists {
return fmt.Errorf("title %d does not exist in master list", titleID)
}
// Check if we've hit the maximum title limit
if len(ptl.titles) >= MaxPlayerTitles {
return fmt.Errorf("player %d has reached maximum title limit of %d", ptl.playerID, MaxPlayerTitles)
}
// Check for unique title restrictions
if masterTitle.IsUnique() {
// TODO: Check database to ensure no other player has this unique title
}
// Create player title entry
playerTitle := NewPlayerTitle(titleID, ptl.playerID)
playerTitle.AchievementID = sourceAchievementID
playerTitle.QuestID = sourceQuestID
// Set expiration if it's a temporary title
if masterTitle.ExpirationHours > 0 {
playerTitle.SetExpiration(masterTitle.ExpirationHours)
}
ptl.titles[titleID] = playerTitle
return nil
}
// RemoveTitle removes a title from the player
func (ptl *PlayerTitlesList) RemoveTitle(titleID int32) error {
ptl.mutex.Lock()
defer ptl.mutex.Unlock()
playerTitle, exists := ptl.titles[titleID]
if !exists {
return fmt.Errorf("player %d does not have title %d", ptl.playerID, titleID)
}
// If this title is currently active, deactivate it
if playerTitle.IsActive {
if playerTitle.IsPrefix && ptl.activePrefixID == titleID {
ptl.activePrefixID = TitleIDNone
} else if !playerTitle.IsPrefix && ptl.activeSuffixID == titleID {
ptl.activeSuffixID = TitleIDCitizen // Revert to default
}
}
delete(ptl.titles, titleID)
return nil
}
// HasTitle checks if the player owns a specific title
func (ptl *PlayerTitlesList) HasTitle(titleID int32) bool {
ptl.mutex.RLock()
defer ptl.mutex.RUnlock()
_, exists := ptl.titles[titleID]
return exists
}
// GetTitle retrieves a player's title information
func (ptl *PlayerTitlesList) GetTitle(titleID int32) (*PlayerTitle, bool) {
ptl.mutex.RLock()
defer ptl.mutex.RUnlock()
title, exists := ptl.titles[titleID]
if !exists {
return nil, false
}
return title.Clone(), true
}
// SetActivePrefix sets the active prefix title
func (ptl *PlayerTitlesList) SetActivePrefix(titleID int32) error {
ptl.mutex.Lock()
defer ptl.mutex.Unlock()
// Allow clearing prefix title
if titleID == TitleIDNone {
// Deactivate current prefix if any
if ptl.activePrefixID != TitleIDNone {
if currentTitle, exists := ptl.titles[ptl.activePrefixID]; exists {
currentTitle.IsActive = false
}
}
ptl.activePrefixID = TitleIDNone
return nil
}
// Check if player owns the title
playerTitle, exists := ptl.titles[titleID]
if !exists {
return fmt.Errorf("player %d does not own title %d", ptl.playerID, titleID)
}
// Verify title can be used as prefix
masterTitle, exists := ptl.masterList.GetTitle(titleID)
if !exists {
return fmt.Errorf("title %d not found in master list", titleID)
}
if masterTitle.Position != TitlePositionPrefix {
return fmt.Errorf("title %d cannot be used as prefix", titleID)
}
// Check if title has expired
if playerTitle.IsExpired() {
return fmt.Errorf("title %d has expired", titleID)
}
// Deactivate current prefix
if ptl.activePrefixID != TitleIDNone {
if currentTitle, exists := ptl.titles[ptl.activePrefixID]; exists {
currentTitle.IsActive = false
}
}
// Activate new prefix
playerTitle.Activate(true)
ptl.activePrefixID = titleID
return nil
}
// SetActiveSuffix sets the active suffix title
func (ptl *PlayerTitlesList) SetActiveSuffix(titleID int32) error {
ptl.mutex.Lock()
defer ptl.mutex.Unlock()
// Check if player owns the title
playerTitle, exists := ptl.titles[titleID]
if !exists {
return fmt.Errorf("player %d does not own title %d", ptl.playerID, titleID)
}
// Verify title can be used as suffix
masterTitle, exists := ptl.masterList.GetTitle(titleID)
if !exists {
return fmt.Errorf("title %d not found in master list", titleID)
}
if masterTitle.Position != TitlePositionSuffix {
return fmt.Errorf("title %d cannot be used as suffix", titleID)
}
// Check if title has expired
if playerTitle.IsExpired() {
return fmt.Errorf("title %d has expired", titleID)
}
// Deactivate current suffix
if ptl.activeSuffixID != TitleIDNone {
if currentTitle, exists := ptl.titles[ptl.activeSuffixID]; exists {
currentTitle.IsActive = false
}
}
// Activate new suffix
playerTitle.Activate(false)
ptl.activeSuffixID = titleID
return nil
}
// GetActivePrefixTitle returns the currently active prefix title
func (ptl *PlayerTitlesList) GetActivePrefixTitle() (*Title, bool) {
ptl.mutex.RLock()
defer ptl.mutex.RUnlock()
if ptl.activePrefixID == TitleIDNone {
return nil, false
}
return ptl.masterList.GetTitle(ptl.activePrefixID)
}
// GetActiveSuffixTitle returns the currently active suffix title
func (ptl *PlayerTitlesList) GetActiveSuffixTitle() (*Title, bool) {
ptl.mutex.RLock()
defer ptl.mutex.RUnlock()
if ptl.activeSuffixID == TitleIDNone {
return nil, false
}
return ptl.masterList.GetTitle(ptl.activeSuffixID)
}
// GetAllTitles returns all titles owned by the player
func (ptl *PlayerTitlesList) GetAllTitles() []*PlayerTitle {
ptl.mutex.RLock()
defer ptl.mutex.RUnlock()
result := make([]*PlayerTitle, 0, len(ptl.titles))
for _, title := range ptl.titles {
result = append(result, title.Clone())
}
return result
}
// GetAvailablePrefixTitles returns all titles that can be used as prefix
func (ptl *PlayerTitlesList) GetAvailablePrefixTitles() []*Title {
ptl.mutex.RLock()
defer ptl.mutex.RUnlock()
result := make([]*Title, 0)
for titleID := range ptl.titles {
if masterTitle, exists := ptl.masterList.GetTitle(titleID); exists {
if masterTitle.Position == TitlePositionPrefix {
// Check if not expired
if playerTitle := ptl.titles[titleID]; !playerTitle.IsExpired() {
result = append(result, masterTitle)
}
}
}
}
return result
}
// GetAvailableSuffixTitles returns all titles that can be used as suffix
func (ptl *PlayerTitlesList) GetAvailableSuffixTitles() []*Title {
ptl.mutex.RLock()
defer ptl.mutex.RUnlock()
result := make([]*Title, 0)
for titleID := range ptl.titles {
if masterTitle, exists := ptl.masterList.GetTitle(titleID); exists {
if masterTitle.Position == TitlePositionSuffix {
// Check if not expired
if playerTitle := ptl.titles[titleID]; !playerTitle.IsExpired() {
result = append(result, masterTitle)
}
}
}
}
return result
}
// CleanupExpiredTitles removes expired temporary titles
func (ptl *PlayerTitlesList) CleanupExpiredTitles() int {
ptl.mutex.Lock()
defer ptl.mutex.Unlock()
expiredCount := 0
expiredTitles := make([]int32, 0)
// Find expired titles
for titleID, playerTitle := range ptl.titles {
if playerTitle.IsExpired() {
expiredTitles = append(expiredTitles, titleID)
expiredCount++
}
}
// Remove expired titles
for _, titleID := range expiredTitles {
playerTitle := ptl.titles[titleID]
// If this expired title is currently active, deactivate it
if playerTitle.IsActive {
if playerTitle.IsPrefix && ptl.activePrefixID == titleID {
ptl.activePrefixID = TitleIDNone
} else if !playerTitle.IsPrefix && ptl.activeSuffixID == titleID {
ptl.activeSuffixID = TitleIDCitizen // Revert to default
}
}
delete(ptl.titles, titleID)
}
return expiredCount
}
// GetTitleCount returns the number of titles owned by the player
func (ptl *PlayerTitlesList) GetTitleCount() int {
ptl.mutex.RLock()
defer ptl.mutex.RUnlock()
return len(ptl.titles)
}
// BuildPacketData creates title data for network transmission
func (ptl *PlayerTitlesList) BuildPacketData(playerName string) *TitlePacketData {
ptl.mutex.RLock()
defer ptl.mutex.RUnlock()
data := &TitlePacketData{
PlayerID: uint32(ptl.playerID),
PlayerName: playerName,
PrefixTitle: "",
SuffixTitle: "",
SubTitle: "", // TODO: Implement subtitle system
LastName: "", // TODO: Implement last name system
Titles: make([]TitleEntry, 0, len(ptl.titles)),
CurrentPrefix: int16(ptl.activePrefixID),
CurrentSuffix: int16(ptl.activeSuffixID),
}
// Get active prefix title name
if prefixTitle, exists := ptl.GetActivePrefixTitle(); exists {
data.PrefixTitle = prefixTitle.GetDisplayName()
}
// Get active suffix title name
if suffixTitle, exists := ptl.GetActiveSuffixTitle(); exists {
data.SuffixTitle = suffixTitle.GetDisplayName()
}
// Build title array for UI
for titleID := range ptl.titles {
if masterTitle, exists := ptl.masterList.GetTitle(titleID); exists {
// Skip hidden titles unless it's a GM viewing
// TODO: Add GM check parameter
if masterTitle.IsHidden() {
continue
}
// Skip expired titles
if ptl.titles[titleID].IsExpired() {
continue
}
entry := TitleEntry{
Name: masterTitle.GetDisplayName(),
IsPrefix: masterTitle.Position == TitlePositionPrefix,
}
data.Titles = append(data.Titles, entry)
}
}
data.NumTitles = uint16(len(data.Titles))
return data
}
// GrantTitleFromAchievement grants a title when an achievement is completed
func (ptl *PlayerTitlesList) GrantTitleFromAchievement(achievementID uint32) error {
// Find title associated with this achievement
title, exists := ptl.masterList.GetTitleByAchievement(achievementID)
if !exists {
return nil // No title associated with this achievement
}
// Grant the title
return ptl.AddTitle(title.ID, achievementID, 0)
}
// LoadFromDatabase would load player titles from the database
// TODO: Implement database integration with zone/database package
func (ptl *PlayerTitlesList) LoadFromDatabase() error {
// TODO: Implement database loading
return fmt.Errorf("LoadFromDatabase not yet implemented - requires database integration")
}
// SaveToDatabase would save player titles to the database
// TODO: Implement database integration with zone/database package
func (ptl *PlayerTitlesList) SaveToDatabase() error {
// TODO: Implement database saving
return fmt.Errorf("SaveToDatabase not yet implemented - requires database integration")
}
// GetFormattedName returns the player name with active titles applied
func (ptl *PlayerTitlesList) GetFormattedName(playerName string) string {
ptl.mutex.RLock()
defer ptl.mutex.RUnlock()
result := playerName
// Add prefix if active
if prefixTitle, exists := ptl.GetActivePrefixTitle(); exists {
result = prefixTitle.GetDisplayName() + " " + result
}
// Add suffix if active
if suffixTitle, exists := ptl.GetActiveSuffixTitle(); exists {
result = result + " " + suffixTitle.GetDisplayName()
}
return result
}

325
internal/titles/title.go Normal file
View File

@ -0,0 +1,325 @@
package titles
import (
"sync"
"time"
)
// Title represents a single character title with all its properties
type Title struct {
ID int32 `json:"id"` // Unique title identifier
Name string `json:"name"` // Display name of the title
Description string `json:"description"` // Description shown in UI
// Positioning and display
Position int32 `json:"position"` // TitlePositionPrefix or TitlePositionSuffix
DisplayFormat int32 `json:"display_format"` // How the title is formatted
Color uint32 `json:"color"` // Color code for display
// Classification
Source int32 `json:"source"` // How the title is obtained (TitleSource*)
Category string `json:"category"` // Category for organization
Rarity int32 `json:"rarity"` // Title rarity level
// Requirements and restrictions
Requirements []TitleRequirement `json:"requirements"` // What's needed to unlock
Flags uint32 `json:"flags"` // Various title flags
// Metadata
CreatedDate time.Time `json:"created_date"` // When title was added
LastModified time.Time `json:"last_modified"` // When title was last updated
MinLevel int32 `json:"min_level"` // Minimum character level
MaxLevel int32 `json:"max_level"` // Maximum character level (0 = no limit)
ExpansionID int32 `json:"expansion_id"` // Required expansion
AchievementID uint32 `json:"achievement_id"` // Associated achievement if any
// Expiration (for temporary titles)
ExpirationHours int32 `json:"expiration_hours"` // Hours until expiration (0 = permanent)
mutex sync.RWMutex // Thread safety
}
// TitleRequirement represents a single requirement for unlocking a title
type TitleRequirement struct {
Type int32 `json:"type"` // RequirementType* constant
Value int32 `json:"value"` // Required value/amount
StringValue string `json:"string_value"` // For string-based requirements
Description string `json:"description"` // Human-readable requirement description
}
// PlayerTitle represents a title owned by a player
type PlayerTitle struct {
TitleID int32 `json:"title_id"` // Reference to Title.ID
PlayerID int32 `json:"player_id"` // Character ID who owns it
EarnedDate time.Time `json:"earned_date"` // When the title was earned
ExpiresAt time.Time `json:"expires_at"` // When temporary title expires (zero for permanent)
IsActive bool `json:"is_active"` // Whether title is currently displayed
IsPrefix bool `json:"is_prefix"` // True if used as prefix, false for suffix
// Achievement context
AchievementID uint32 `json:"achievement_id"` // Achievement that granted this title
QuestID uint32 `json:"quest_id"` // Quest that granted this title
mutex sync.RWMutex // Thread safety
}
// NewTitle creates a new title with default values
func NewTitle(id int32, name string) *Title {
return &Title{
ID: id,
Name: name,
Position: TitlePositionSuffix,
DisplayFormat: DisplayFormatSimple,
Color: ColorCommon,
Source: TitleSourceMiscellaneous,
Category: CategoryMiscellaneous,
Rarity: TitleRarityCommon,
Requirements: make([]TitleRequirement, 0),
Flags: 0,
CreatedDate: time.Now(),
LastModified: time.Now(),
MinLevel: 1,
MaxLevel: 0, // No limit
ExpansionID: 0, // Base game
AchievementID: 0,
ExpirationHours: 0, // Permanent
}
}
// NewPlayerTitle creates a new player title entry
func NewPlayerTitle(titleID, playerID int32) *PlayerTitle {
return &PlayerTitle{
TitleID: titleID,
PlayerID: playerID,
EarnedDate: time.Now(),
ExpiresAt: time.Time{}, // Zero value = permanent
IsActive: false,
IsPrefix: false,
AchievementID: 0,
QuestID: 0,
}
}
// IsExpired checks if a temporary title has expired
func (pt *PlayerTitle) IsExpired() bool {
pt.mutex.RLock()
defer pt.mutex.RUnlock()
if pt.ExpiresAt.IsZero() {
return false // Permanent title
}
return time.Now().After(pt.ExpiresAt)
}
// SetExpiration sets when a temporary title expires
func (pt *PlayerTitle) SetExpiration(hours int32) {
pt.mutex.Lock()
defer pt.mutex.Unlock()
if hours <= 0 {
pt.ExpiresAt = time.Time{} // Make permanent
} else {
pt.ExpiresAt = time.Now().Add(time.Duration(hours) * time.Hour)
}
}
// Activate makes this title the active one for the player
func (pt *PlayerTitle) Activate(isPrefix bool) {
pt.mutex.Lock()
defer pt.mutex.Unlock()
pt.IsActive = true
pt.IsPrefix = isPrefix
}
// Deactivate removes this title from active display
func (pt *PlayerTitle) Deactivate() {
pt.mutex.Lock()
defer pt.mutex.Unlock()
pt.IsActive = false
}
// Clone creates a deep copy of the title
func (t *Title) Clone() *Title {
t.mutex.RLock()
defer t.mutex.RUnlock()
clone := &Title{
ID: t.ID,
Name: t.Name,
Description: t.Description,
Position: t.Position,
DisplayFormat: t.DisplayFormat,
Color: t.Color,
Source: t.Source,
Category: t.Category,
Rarity: t.Rarity,
Requirements: make([]TitleRequirement, len(t.Requirements)),
Flags: t.Flags,
CreatedDate: t.CreatedDate,
LastModified: t.LastModified,
MinLevel: t.MinLevel,
MaxLevel: t.MaxLevel,
ExpansionID: t.ExpansionID,
AchievementID: t.AchievementID,
ExpirationHours: t.ExpirationHours,
}
copy(clone.Requirements, t.Requirements)
return clone
}
// Clone creates a deep copy of the player title
func (pt *PlayerTitle) Clone() *PlayerTitle {
pt.mutex.RLock()
defer pt.mutex.RUnlock()
return &PlayerTitle{
TitleID: pt.TitleID,
PlayerID: pt.PlayerID,
EarnedDate: pt.EarnedDate,
ExpiresAt: pt.ExpiresAt,
IsActive: pt.IsActive,
IsPrefix: pt.IsPrefix,
AchievementID: pt.AchievementID,
QuestID: pt.QuestID,
}
}
// HasFlag checks if the title has a specific flag set
func (t *Title) HasFlag(flag uint32) bool {
t.mutex.RLock()
defer t.mutex.RUnlock()
return (t.Flags & flag) != 0
}
// SetFlag sets a specific flag on the title
func (t *Title) SetFlag(flag uint32) {
t.mutex.Lock()
defer t.mutex.Unlock()
t.Flags |= flag
t.LastModified = time.Now()
}
// ClearFlag removes a specific flag from the title
func (t *Title) ClearFlag(flag uint32) {
t.mutex.Lock()
defer t.mutex.Unlock()
t.Flags &^= flag
t.LastModified = time.Now()
}
// AddRequirement adds a new requirement to the title
func (t *Title) AddRequirement(reqType int32, value int32, stringValue, description string) {
t.mutex.Lock()
defer t.mutex.Unlock()
req := TitleRequirement{
Type: reqType,
Value: value,
StringValue: stringValue,
Description: description,
}
t.Requirements = append(t.Requirements, req)
t.LastModified = time.Now()
}
// ClearRequirements removes all requirements from the title
func (t *Title) ClearRequirements() {
t.mutex.Lock()
defer t.mutex.Unlock()
t.Requirements = make([]TitleRequirement, 0)
t.LastModified = time.Now()
}
// IsHidden checks if the title is hidden from normal display
func (t *Title) IsHidden() bool {
return t.HasFlag(FlagHidden)
}
// IsAccountWide checks if the title is available to all characters on the account
func (t *Title) IsAccountWide() bool {
return t.HasFlag(FlagAccountWide)
}
// IsUnique checks if only one player can have this title
func (t *Title) IsUnique() bool {
return t.HasFlag(FlagUnique)
}
// IsTemporary checks if the title expires after a certain time
func (t *Title) IsTemporary() bool {
return t.HasFlag(FlagTemporary)
}
// IsGMOnly checks if the title is restricted to Game Masters
func (t *Title) IsGMOnly() bool {
return t.HasFlag(FlagGMOnly)
}
// GetDisplayName returns the formatted title name for display
func (t *Title) GetDisplayName() string {
t.mutex.RLock()
defer t.mutex.RUnlock()
switch t.DisplayFormat {
case DisplayFormatWithBrackets:
return "[" + t.Name + "]"
case DisplayFormatWithQuotes:
return "\"" + t.Name + "\""
case DisplayFormatWithCommas:
return "," + t.Name + ","
default:
return t.Name
}
}
// SetDescription updates the title description
func (t *Title) SetDescription(description string) {
t.mutex.Lock()
defer t.mutex.Unlock()
t.Description = description
t.LastModified = time.Now()
}
// SetCategory updates the title category
func (t *Title) SetCategory(category string) {
t.mutex.Lock()
defer t.mutex.Unlock()
t.Category = category
t.LastModified = time.Now()
}
// SetRarity updates the title rarity and adjusts color accordingly
func (t *Title) SetRarity(rarity int32) {
t.mutex.Lock()
defer t.mutex.Unlock()
t.Rarity = rarity
// Update color based on rarity
switch rarity {
case TitleRarityCommon:
t.Color = ColorCommon
case TitleRarityUncommon:
t.Color = ColorUncommon
case TitleRarityRare:
t.Color = ColorRare
case TitleRarityEpic:
t.Color = ColorEpic
case TitleRarityLegendary:
t.Color = ColorLegendary
case TitleRarityUnique:
t.Color = ColorUnique
}
t.LastModified = time.Now()
}

View File

@ -0,0 +1,382 @@
package titles
import (
"fmt"
"sync"
"time"
)
// TitleManager manages the entire title system for the server
type TitleManager struct {
masterList *MasterTitlesList // Global title definitions
playerLists map[int32]*PlayerTitlesList // Player-specific title collections
mutex sync.RWMutex // Thread safety
// Background cleanup
cleanupTicker *time.Ticker
stopCleanup chan bool
// Statistics
totalTitlesGranted int64
totalTitlesExpired int64
}
// NewTitleManager creates a new title manager instance
func NewTitleManager() *TitleManager {
tm := &TitleManager{
masterList: NewMasterTitlesList(),
playerLists: make(map[int32]*PlayerTitlesList),
totalTitlesGranted: 0,
totalTitlesExpired: 0,
}
// Start background cleanup process
tm.startCleanupProcess()
return tm
}
// GetMasterList returns the master titles list
func (tm *TitleManager) GetMasterList() *MasterTitlesList {
return tm.masterList
}
// GetPlayerTitles retrieves or creates a player's title collection
func (tm *TitleManager) GetPlayerTitles(playerID int32) *PlayerTitlesList {
tm.mutex.Lock()
defer tm.mutex.Unlock()
playerList, exists := tm.playerLists[playerID]
if !exists {
playerList = NewPlayerTitlesList(playerID, tm.masterList)
tm.playerLists[playerID] = playerList
}
return playerList
}
// GrantTitle grants a title to a player
func (tm *TitleManager) GrantTitle(playerID, titleID int32, sourceAchievementID, sourceQuestID uint32) error {
playerList := tm.GetPlayerTitles(playerID)
err := playerList.AddTitle(titleID, sourceAchievementID, sourceQuestID)
if err == nil {
tm.mutex.Lock()
tm.totalTitlesGranted++
tm.mutex.Unlock()
}
return err
}
// RevokeTitle removes a title from a player
func (tm *TitleManager) RevokeTitle(playerID, titleID int32) error {
tm.mutex.RLock()
playerList, exists := tm.playerLists[playerID]
tm.mutex.RUnlock()
if !exists {
return fmt.Errorf("player %d has no titles", playerID)
}
return playerList.RemoveTitle(titleID)
}
// SetPlayerActivePrefix sets a player's active prefix title
func (tm *TitleManager) SetPlayerActivePrefix(playerID, titleID int32) error {
playerList := tm.GetPlayerTitles(playerID)
return playerList.SetActivePrefix(titleID)
}
// SetPlayerActiveSuffix sets a player's active suffix title
func (tm *TitleManager) SetPlayerActiveSuffix(playerID, titleID int32) error {
playerList := tm.GetPlayerTitles(playerID)
return playerList.SetActiveSuffix(titleID)
}
// GetPlayerFormattedName returns a player's name with active titles
func (tm *TitleManager) GetPlayerFormattedName(playerID int32, playerName string) string {
tm.mutex.RLock()
playerList, exists := tm.playerLists[playerID]
tm.mutex.RUnlock()
if !exists {
return playerName
}
return playerList.GetFormattedName(playerName)
}
// ProcessAchievementCompletion handles title grants from achievement completion
func (tm *TitleManager) ProcessAchievementCompletion(playerID int32, achievementID uint32) error {
playerList := tm.GetPlayerTitles(playerID)
return playerList.GrantTitleFromAchievement(achievementID)
}
// CreateTitle creates a new title and adds it to the master list
func (tm *TitleManager) CreateTitle(name, description, category string, position, source, rarity int32) (*Title, error) {
title := NewTitle(0, name) // ID will be assigned automatically
title.SetDescription(description)
title.SetCategory(category)
title.Position = position
title.Source = source
title.SetRarity(rarity)
err := tm.masterList.AddTitle(title)
if err != nil {
return nil, err
}
return title, nil
}
// CreateAchievementTitle creates a title tied to a specific achievement
func (tm *TitleManager) CreateAchievementTitle(name, description string, achievementID uint32, position, rarity int32) (*Title, error) {
title := NewTitle(0, name)
title.SetDescription(description)
title.SetCategory(CategoryAchievement)
title.Position = position
title.Source = TitleSourceAchievement
title.SetRarity(rarity)
title.AchievementID = achievementID
err := tm.masterList.AddTitle(title)
if err != nil {
return nil, err
}
return title, nil
}
// CreateTemporaryTitle creates a title that expires after a certain time
func (tm *TitleManager) CreateTemporaryTitle(name, description string, hours int32, position, source, rarity int32) (*Title, error) {
title := NewTitle(0, name)
title.SetDescription(description)
title.Position = position
title.Source = source
title.SetRarity(rarity)
title.ExpirationHours = hours
title.SetFlag(FlagTemporary)
err := tm.masterList.AddTitle(title)
if err != nil {
return nil, err
}
return title, nil
}
// CreateUniqueTitle creates a title that only one player can have
func (tm *TitleManager) CreateUniqueTitle(name, description string, position, source int32) (*Title, error) {
title := NewTitle(0, name)
title.SetDescription(description)
title.Position = position
title.Source = source
title.SetRarity(TitleRarityUnique)
title.SetFlag(FlagUnique)
err := tm.masterList.AddTitle(title)
if err != nil {
return nil, err
}
return title, nil
}
// GetTitlesByCategory retrieves all titles in a category
func (tm *TitleManager) GetTitlesByCategory(category string) []*Title {
return tm.masterList.GetTitlesByCategory(category)
}
// GetTitlesBySource retrieves all titles from a specific source
func (tm *TitleManager) GetTitlesBySource(source int32) []*Title {
return tm.masterList.GetTitlesBySource(source)
}
// GetTitlesByRarity retrieves all titles of a specific rarity
func (tm *TitleManager) GetTitlesByRarity(rarity int32) []*Title {
return tm.masterList.GetTitlesByRarity(rarity)
}
// SearchTitles searches for titles by name (case-insensitive partial match)
func (tm *TitleManager) SearchTitles(query string) []*Title {
allTitles := tm.masterList.GetAllTitles(false) // Exclude hidden
result := make([]*Title, 0)
// Simple case-insensitive contains search
// TODO: Implement more sophisticated search with fuzzy matching
for _, title := range allTitles {
if contains(title.Name, query) || contains(title.Description, query) {
result = append(result, title)
}
}
return result
}
// contains performs case-insensitive substring search
func contains(s, substr string) bool {
// Simple implementation - could be improved with proper Unicode handling
sLower := []rune(s)
substrLower := []rune(substr)
for i := range sLower {
if sLower[i] >= 'A' && sLower[i] <= 'Z' {
sLower[i] = sLower[i] + 32
}
}
for i := range substrLower {
if substrLower[i] >= 'A' && substrLower[i] <= 'Z' {
substrLower[i] = substrLower[i] + 32
}
}
sStr := string(sLower)
subStr := string(substrLower)
for i := 0; i <= len(sStr)-len(subStr); i++ {
if sStr[i:i+len(subStr)] == subStr {
return true
}
}
return false
}
// GetPlayerTitlePacketData builds packet data for a player's titles
func (tm *TitleManager) GetPlayerTitlePacketData(playerID int32, playerName string) *TitlePacketData {
tm.mutex.RLock()
playerList, exists := tm.playerLists[playerID]
tm.mutex.RUnlock()
if !exists {
// Create basic packet data with default titles
return &TitlePacketData{
PlayerID: uint32(playerID),
PlayerName: playerName,
PrefixTitle: "",
SuffixTitle: "Citizen",
SubTitle: "",
LastName: "",
Titles: []TitleEntry{{"Citizen", false}},
NumTitles: 1,
CurrentPrefix: int16(TitleIDNone),
CurrentSuffix: int16(TitleIDCitizen),
}
}
return playerList.BuildPacketData(playerName)
}
// startCleanupProcess begins the background cleanup of expired titles
func (tm *TitleManager) startCleanupProcess() {
tm.cleanupTicker = time.NewTicker(1 * time.Hour) // Run cleanup every hour
tm.stopCleanup = make(chan bool)
go func() {
for {
select {
case <-tm.cleanupTicker.C:
tm.cleanupExpiredTitles()
case <-tm.stopCleanup:
tm.cleanupTicker.Stop()
return
}
}
}()
}
// StopCleanupProcess stops the background cleanup
func (tm *TitleManager) StopCleanupProcess() {
if tm.stopCleanup != nil {
tm.stopCleanup <- true
}
}
// cleanupExpiredTitles removes expired titles from all players
func (tm *TitleManager) cleanupExpiredTitles() {
tm.mutex.RLock()
playerLists := make([]*PlayerTitlesList, 0, len(tm.playerLists))
for _, list := range tm.playerLists {
playerLists = append(playerLists, list)
}
tm.mutex.RUnlock()
totalExpired := 0
for _, playerList := range playerLists {
expired := playerList.CleanupExpiredTitles()
totalExpired += expired
}
if totalExpired > 0 {
tm.mutex.Lock()
tm.totalTitlesExpired += int64(totalExpired)
tm.mutex.Unlock()
}
}
// GetStatistics returns title system statistics
func (tm *TitleManager) GetStatistics() map[string]interface{} {
tm.mutex.RLock()
defer tm.mutex.RUnlock()
return map[string]interface{}{
"total_titles": tm.masterList.GetTitleCount(),
"total_players": len(tm.playerLists),
"titles_granted": tm.totalTitlesGranted,
"titles_expired": tm.totalTitlesExpired,
"available_categories": tm.masterList.GetAvailableCategories(),
}
}
// RemovePlayerFromMemory removes a player's title data from memory (when they log out)
func (tm *TitleManager) RemovePlayerFromMemory(playerID int32) {
tm.mutex.Lock()
defer tm.mutex.Unlock()
delete(tm.playerLists, playerID)
}
// LoadPlayerTitles loads a player's titles from database
// TODO: Implement database integration with zone/database package
func (tm *TitleManager) LoadPlayerTitles(playerID int32) error {
playerList := tm.GetPlayerTitles(playerID)
return playerList.LoadFromDatabase()
}
// SavePlayerTitles saves a player's titles to database
// TODO: Implement database integration with zone/database package
func (tm *TitleManager) SavePlayerTitles(playerID int32) error {
tm.mutex.RLock()
playerList, exists := tm.playerLists[playerID]
tm.mutex.RUnlock()
if !exists {
return fmt.Errorf("player %d has no title data to save", playerID)
}
return playerList.SaveToDatabase()
}
// LoadMasterTitles loads all titles from database
// TODO: Implement database integration with zone/database package
func (tm *TitleManager) LoadMasterTitles() error {
return tm.masterList.LoadFromDatabase()
}
// SaveMasterTitles saves all titles to database
// TODO: Implement database integration with zone/database package
func (tm *TitleManager) SaveMasterTitles() error {
return tm.masterList.SaveToDatabase()
}
// ValidateTitle validates a title before adding it
func (tm *TitleManager) ValidateTitle(title *Title) error {
return tm.masterList.ValidateTitle(title)
}
// Shutdown gracefully shuts down the title manager
func (tm *TitleManager) Shutdown() {
tm.StopCleanupProcess()
}

View File

@ -0,0 +1,39 @@
package trade
// Trade error codes converted from C++ Trade.cpp
const (
TradeResultSuccess = 0 // Item successfully added to trade
TradeResultAlreadyInTrade = 1 // Item already in trade
TradeResultNoTrade = 2 // Item is no-trade
TradeResultHeirloom = 3 // Item is heirloom and cannot be traded
TradeResultInvalidSlot = 254 // Slot is full or invalid
TradeResultSlotOutOfRange = 255 // Slot is out of range
TradeResultInsufficientQty = 253 // Not enough quantity to trade
)
// Trade packet types converted from C++ Trade.cpp
const (
TradePacketTypeOpen = 1 // Open trade window
TradePacketTypeCancel = 2 // Cancel trade
TradePacketTypeAccept = 16 // Accept trade
TradePacketTypeComplete = 24 // Trade completed
)
// Trade slot configuration
const (
TradeMaxSlotsDefault = 12 // Default max slots for newer clients
TradeMaxSlotsLegacy = 6 // Max slots for older clients (version <= 561)
TradeSlotAutoFind = 255 // Automatically find next free slot
)
// Coin conversion constants (from C++ CalculateCoins)
const (
CoinsPlatinumThreshold = 1000000 // 1 platinum = 1,000,000 copper
CoinsGoldThreshold = 10000 // 1 gold = 10,000 copper
CoinsSilverThreshold = 100 // 1 silver = 100 copper
)
// Trade validation constants
const (
TradeSlotEmpty = -1 // Indicates empty trade slot
)

378
internal/trade/manager.go Normal file
View File

@ -0,0 +1,378 @@
package trade
import (
"fmt"
"sync"
"time"
)
// TradeService provides high-level trade management functionality
// This integrates the trade system with the broader server architecture
type TradeService struct {
tradeManager *TradeManager
// Trade configuration
maxTradeDuration time.Duration // Maximum time a trade can be active
// TODO: Add references to other systems when available
// entityManager *EntityManager
// packetManager *PacketManager
// logManager *LogManager
mutex sync.RWMutex
}
// NewTradeService creates a new trade service
func NewTradeService() *TradeService {
return &TradeService{
tradeManager: NewTradeManager(),
maxTradeDuration: 30 * time.Minute, // Default 30 minute timeout
}
}
// InitiateTrade starts a trade between two entities
// This is the main entry point for starting trades
func (ts *TradeService) InitiateTrade(initiatorID, targetID int32) (*Trade, error) {
ts.mutex.Lock()
defer ts.mutex.Unlock()
// Check if either entity is already in a trade
if existingTrade := ts.tradeManager.GetTrade(initiatorID); existingTrade != nil {
return nil, fmt.Errorf("initiator is already in a trade")
}
if existingTrade := ts.tradeManager.GetTrade(targetID); existingTrade != nil {
return nil, fmt.Errorf("target is already in a trade")
}
// TODO: Get actual entities when entity system is available
// For now, create placeholder entities
initiator := &PlaceholderEntity{ID: initiatorID}
target := &PlaceholderEntity{ID: targetID}
// Create new trade
trade := NewTrade(initiator, target)
if trade == nil {
return nil, fmt.Errorf("failed to create trade")
}
// Add to trade manager
ts.tradeManager.AddTrade(trade)
return trade, nil
}
// GetTrade retrieves an active trade for an entity
func (ts *TradeService) GetTrade(entityID int32) *Trade {
return ts.tradeManager.GetTrade(entityID)
}
// AddItemToTrade adds an item to a player's trade offer
func (ts *TradeService) AddItemToTrade(entityID int32, item Item, quantity int32, slot int8) error {
trade := ts.tradeManager.GetTrade(entityID)
if trade == nil {
return fmt.Errorf("entity is not in a trade")
}
return trade.AddItemToTrade(entityID, item, quantity, slot)
}
// RemoveItemFromTrade removes an item from a player's trade offer
func (ts *TradeService) RemoveItemFromTrade(entityID int32, slot int8) error {
trade := ts.tradeManager.GetTrade(entityID)
if trade == nil {
return fmt.Errorf("entity is not in a trade")
}
return trade.RemoveItemFromTrade(entityID, slot)
}
// AddCoinsToTrade adds coins to a player's trade offer
func (ts *TradeService) AddCoinsToTrade(entityID int32, amount int64) error {
trade := ts.tradeManager.GetTrade(entityID)
if trade == nil {
return fmt.Errorf("entity is not in a trade")
}
return trade.AddCoinsToTrade(entityID, amount)
}
// RemoveCoinsFromTrade removes coins from a player's trade offer
func (ts *TradeService) RemoveCoinsFromTrade(entityID int32, amount int64) error {
trade := ts.tradeManager.GetTrade(entityID)
if trade == nil {
return fmt.Errorf("entity is not in a trade")
}
return trade.RemoveCoinsFromTrade(entityID, amount)
}
// AcceptTrade marks a player as having accepted their trade
func (ts *TradeService) AcceptTrade(entityID int32) (bool, error) {
trade := ts.tradeManager.GetTrade(entityID)
if trade == nil {
return false, fmt.Errorf("entity is not in a trade")
}
completed, err := trade.SetTradeAccepted(entityID)
if err != nil {
return false, err
}
// If trade completed, remove from manager
if completed {
ts.tradeManager.RemoveTrade(trade.GetTrader1ID())
}
return completed, nil
}
// CancelTrade cancels an active trade
func (ts *TradeService) CancelTrade(entityID int32) error {
trade := ts.tradeManager.GetTrade(entityID)
if trade == nil {
return fmt.Errorf("entity is not in a trade")
}
err := trade.CancelTrade(entityID)
if err != nil {
return err
}
// Remove from manager
ts.tradeManager.RemoveTrade(trade.GetTrader1ID())
return nil
}
// GetTradeInfo returns comprehensive information about a trade
func (ts *TradeService) GetTradeInfo(entityID int32) (map[string]interface{}, error) {
trade := ts.tradeManager.GetTrade(entityID)
if trade == nil {
return nil, fmt.Errorf("entity is not in a trade")
}
return trade.GetTradeInfo(), nil
}
// GetActiveTradeCount returns the number of active trades
func (ts *TradeService) GetActiveTradeCount() int {
return ts.tradeManager.GetActiveTradeCount()
}
// ProcessTrades handles periodic trade processing (timeouts, cleanup, etc.)
func (ts *TradeService) ProcessTrades() {
ts.mutex.Lock()
defer ts.mutex.Unlock()
// TODO: Implement trade timeout processing
// This would check for trades that have been active too long and auto-cancel them
// Get all active trades
// Check each trade's start time against maxTradeDuration
// Cancel expired trades
}
// GetTradeStatistics returns statistics about trade activity
func (ts *TradeService) GetTradeStatistics() map[string]interface{} {
stats := make(map[string]interface{})
stats["active_trades"] = ts.tradeManager.GetActiveTradeCount()
stats["max_trade_duration_minutes"] = ts.maxTradeDuration.Minutes()
// TODO: Add more statistics when logging/metrics system is available
// - Total trades completed today
// - Average trade completion time
// - Most traded items
// - Trade success/failure rates
return stats
}
// ValidateTradeRequest checks if a trade request is valid
func (ts *TradeService) ValidateTradeRequest(initiatorID, targetID int32) error {
if initiatorID == targetID {
return fmt.Errorf("cannot trade with yourself")
}
if initiatorID <= 0 || targetID <= 0 {
return fmt.Errorf("invalid entity IDs")
}
// Check if either entity is already in a trade
if ts.tradeManager.GetTrade(initiatorID) != nil {
return fmt.Errorf("initiator is already in a trade")
}
if ts.tradeManager.GetTrade(targetID) != nil {
return fmt.Errorf("target is already in a trade")
}
// TODO: Add additional validation when entity system is available:
// - Verify both entities exist and are online
// - Check if entities are in the same zone
// - Verify entities are within trade range
// - Check for any trade restrictions or bans
return nil
}
// ForceCompleteTrade completes a trade regardless of acceptance state (admin function)
func (ts *TradeService) ForceCompleteTrade(entityID int32) error {
trade := ts.tradeManager.GetTrade(entityID)
if trade == nil {
return fmt.Errorf("entity is not in a trade")
}
// Force both participants to accepted state
trade.trader1.HasAccepted = true
trade.trader2.HasAccepted = true
// Complete the trade
completed, err := trade.SetTradeAccepted(entityID)
if err != nil {
return err
}
if completed {
ts.tradeManager.RemoveTrade(trade.GetTrader1ID())
}
return nil
}
// ForceCancelTrade cancels a trade regardless of state (admin function)
func (ts *TradeService) ForceCancelTrade(entityID int32, reason string) error {
trade := ts.tradeManager.GetTrade(entityID)
if trade == nil {
return fmt.Errorf("entity is not in a trade")
}
// TODO: Log the forced cancellation with reason when logging system is available
err := trade.CancelTrade(entityID)
if err != nil {
return err
}
ts.tradeManager.RemoveTrade(trade.GetTrader1ID())
return nil
}
// Shutdown gracefully shuts down the trade service
func (ts *TradeService) Shutdown() {
ts.mutex.Lock()
defer ts.mutex.Unlock()
// TODO: Cancel all active trades with appropriate notifications
// For now, just clear the trade manager
ts.tradeManager = NewTradeManager()
}
// PlaceholderEntity is a temporary implementation until the entity system is available
type PlaceholderEntity struct {
ID int32
Name string
IsPlayerFlag bool
IsBotFlag bool
CoinsAmount int64
ClientVer int32
}
// GetID returns the entity ID
func (pe *PlaceholderEntity) GetID() int32 {
return pe.ID
}
// GetName returns the entity name
func (pe *PlaceholderEntity) GetName() string {
if pe.Name == "" {
return fmt.Sprintf("Entity_%d", pe.ID)
}
return pe.Name
}
// IsPlayer returns whether this is a player entity
func (pe *PlaceholderEntity) IsPlayer() bool {
return pe.IsPlayerFlag
}
// IsBot returns whether this is a bot entity
func (pe *PlaceholderEntity) IsBot() bool {
return pe.IsBotFlag
}
// HasCoins checks if the entity has sufficient coins
func (pe *PlaceholderEntity) HasCoins(amount int64) bool {
return pe.CoinsAmount >= amount
}
// GetClientVersion returns the client version
func (pe *PlaceholderEntity) GetClientVersion() int32 {
if pe.ClientVer == 0 {
return 1000 // Default to newer client
}
return pe.ClientVer
}
// PlaceholderItem is a temporary implementation until the item system is available
type PlaceholderItem struct {
ID int32
Name string
Quantity int32
IconID int32
NoTradeFlag bool
HeirloomFlag bool
AttunedFlag bool
CreatedTime time.Time
GroupIDs []int32
}
// GetID returns the item ID
func (pi *PlaceholderItem) GetID() int32 {
return pi.ID
}
// GetName returns the item name
func (pi *PlaceholderItem) GetName() string {
if pi.Name == "" {
return fmt.Sprintf("Item_%d", pi.ID)
}
return pi.Name
}
// GetQuantity returns the item quantity
func (pi *PlaceholderItem) GetQuantity() int32 {
return pi.Quantity
}
// GetIcon returns the item icon ID
func (pi *PlaceholderItem) GetIcon(version int32) int32 {
return pi.IconID
}
// IsNoTrade returns whether the item is no-trade
func (pi *PlaceholderItem) IsNoTrade() bool {
return pi.NoTradeFlag
}
// IsHeirloom returns whether the item is heirloom
func (pi *PlaceholderItem) IsHeirloom() bool {
return pi.HeirloomFlag
}
// IsAttuned returns whether the item is attuned
func (pi *PlaceholderItem) IsAttuned() bool {
return pi.AttunedFlag
}
// GetCreationTime returns when the item was created
func (pi *PlaceholderItem) GetCreationTime() time.Time {
return pi.CreatedTime
}
// GetGroupCharacterIDs returns the group character IDs for heirloom sharing
func (pi *PlaceholderItem) GetGroupCharacterIDs() []int32 {
return pi.GroupIDs
}

487
internal/trade/trade.go Normal file
View File

@ -0,0 +1,487 @@
package trade
import (
"fmt"
"sync"
"time"
)
// Trade represents an active trade between two entities
// Converted from C++ Trade class
type Trade struct {
// Core trade participants
trader1 *TradeParticipant // First trader (initiator)
trader2 *TradeParticipant // Second trader (recipient)
// Trade state
state TradeState // Current state of the trade
startTime time.Time // When the trade was initiated
// Thread safety
mutex sync.RWMutex
// TODO: Add references to packet system and entity manager when available
// packetManager *PacketManager
// entityManager *EntityManager
}
// NewTrade creates a new trade between two entities
// Converted from C++ Trade::Trade constructor
func NewTrade(entity1, entity2 Entity) *Trade {
if entity1 == nil || entity2 == nil {
return nil
}
trade := &Trade{
trader1: NewTradeParticipant(entity1.GetID(), entity1.IsBot(), entity1.GetClientVersion()),
trader2: NewTradeParticipant(entity2.GetID(), entity2.IsBot(), entity2.GetClientVersion()),
state: TradeStateActive,
startTime: time.Now(),
}
// TODO: Open trade window when packet system is available
// trade.openTradeWindow()
return trade
}
// GetTrader1ID returns the ID of the first trader
func (t *Trade) GetTrader1ID() int32 {
t.mutex.RLock()
defer t.mutex.RUnlock()
return t.trader1.EntityID
}
// GetTrader2ID returns the ID of the second trader
func (t *Trade) GetTrader2ID() int32 {
t.mutex.RLock()
defer t.mutex.RUnlock()
return t.trader2.EntityID
}
// GetTradee returns the other participant in the trade
// Converted from C++ Trade::GetTradee
func (t *Trade) GetTradee(entityID int32) int32 {
t.mutex.RLock()
defer t.mutex.RUnlock()
if t.trader1.EntityID == entityID {
return t.trader2.EntityID
} else if t.trader2.EntityID == entityID {
return t.trader1.EntityID
}
return 0 // Invalid entity ID
}
// GetParticipant returns the trade participant for an entity
func (t *Trade) GetParticipant(entityID int32) *TradeParticipant {
t.mutex.RLock()
defer t.mutex.RUnlock()
if t.trader1.EntityID == entityID {
return t.trader1
} else if t.trader2.EntityID == entityID {
return t.trader2
}
return nil
}
// GetState returns the current trade state
func (t *Trade) GetState() TradeState {
t.mutex.RLock()
defer t.mutex.RUnlock()
return t.state
}
// AddItemToTrade adds an item to a participant's trade offer
// Converted from C++ Trade::AddItemToTrade
func (t *Trade) AddItemToTrade(entityID int32, item Item, quantity int32, slot int8) error {
if t.state != TradeStateActive {
return &TradeValidationError{
Code: TradeResultInvalidSlot,
Message: "Trade is not active",
}
}
t.mutex.Lock()
defer t.mutex.Unlock()
participant := t.getParticipantUnsafe(entityID)
if participant == nil {
return &TradeValidationError{
Code: TradeResultInvalidSlot,
Message: "Entity is not part of this trade",
}
}
// Auto-find slot if needed
if slot == TradeSlotAutoFind {
slot = participant.GetNextFreeSlot()
}
// Validate slot
if slot < 0 || slot >= participant.MaxSlots {
return &TradeValidationError{
Code: TradeResultSlotOutOfRange,
Message: fmt.Sprintf("Invalid trade slot: %d", slot),
}
}
// Check if slot is already occupied
if _, exists := participant.Items[slot]; exists {
return &TradeValidationError{
Code: TradeResultInvalidSlot,
Message: "Trade slot is already occupied",
}
}
// Validate quantity
if quantity <= 0 {
quantity = 1
}
if quantity > item.GetQuantity() {
return &TradeValidationError{
Code: TradeResultInsufficientQty,
Message: "Not enough quantity available",
}
}
// Check if item is already in trade
if participant.HasItem(item.GetID()) {
return &TradeValidationError{
Code: TradeResultAlreadyInTrade,
Message: "Item is already in trade",
}
}
// Validate item tradability
otherID := t.getTradeeUnsafe(entityID)
if err := t.validateItemTradability(item, entityID, otherID); err != nil {
return err
}
// Add item to trade
participant.Items[slot] = TradeItemInfo{
Item: item,
Quantity: quantity,
}
// Reset acceptance flags
t.trader1.HasAccepted = false
t.trader2.HasAccepted = false
// TODO: Send trade packet when packet system is available
// t.sendTradePacket()
return nil
}
// RemoveItemFromTrade removes an item from a participant's trade offer
// Converted from C++ Trade::RemoveItemFromTrade
func (t *Trade) RemoveItemFromTrade(entityID int32, slot int8) error {
if t.state != TradeStateActive {
return &TradeValidationError{
Code: TradeResultInvalidSlot,
Message: "Trade is not active",
}
}
t.mutex.Lock()
defer t.mutex.Unlock()
participant := t.getParticipantUnsafe(entityID)
if participant == nil {
return &TradeValidationError{
Code: TradeResultInvalidSlot,
Message: "Entity is not part of this trade",
}
}
// Check if slot has an item
if _, exists := participant.Items[slot]; !exists {
return &TradeValidationError{
Code: TradeResultInvalidSlot,
Message: "Trade slot is empty",
}
}
// Remove item
delete(participant.Items, slot)
// Reset acceptance flags
t.trader1.HasAccepted = false
t.trader2.HasAccepted = false
// TODO: Send trade packet when packet system is available
// t.sendTradePacket()
return nil
}
// AddCoinsToTrade adds coins to a participant's trade offer
// Converted from C++ Trade::AddCoinToTrade
func (t *Trade) AddCoinsToTrade(entityID int32, amount int64) error {
if t.state != TradeStateActive {
return &TradeValidationError{
Code: TradeResultInvalidSlot,
Message: "Trade is not active",
}
}
if amount <= 0 {
return &TradeValidationError{
Code: TradeResultInvalidSlot,
Message: "Invalid coin amount",
}
}
t.mutex.Lock()
defer t.mutex.Unlock()
participant := t.getParticipantUnsafe(entityID)
if participant == nil {
return &TradeValidationError{
Code: TradeResultInvalidSlot,
Message: "Entity is not part of this trade",
}
}
newTotal := participant.Coins + amount
// TODO: Validate entity has sufficient coins when entity system is available
// For now, assume validation is done elsewhere
participant.Coins = newTotal
// Reset acceptance flags
t.trader1.HasAccepted = false
t.trader2.HasAccepted = false
// TODO: Send trade packet when packet system is available
// t.sendTradePacket()
return nil
}
// RemoveCoinsFromTrade removes coins from a participant's trade offer
// Converted from C++ Trade::RemoveCoinFromTrade
func (t *Trade) RemoveCoinsFromTrade(entityID int32, amount int64) error {
if t.state != TradeStateActive {
return &TradeValidationError{
Code: TradeResultInvalidSlot,
Message: "Trade is not active",
}
}
t.mutex.Lock()
defer t.mutex.Unlock()
participant := t.getParticipantUnsafe(entityID)
if participant == nil {
return &TradeValidationError{
Code: TradeResultInvalidSlot,
Message: "Entity is not part of this trade",
}
}
if amount >= participant.Coins {
participant.Coins = 0
} else {
participant.Coins -= amount
}
// Reset acceptance flags
t.trader1.HasAccepted = false
t.trader2.HasAccepted = false
// TODO: Send trade packet when packet system is available
// t.sendTradePacket()
return nil
}
// SetTradeAccepted marks a participant as having accepted the trade
// Converted from C++ Trade::SetTradeAccepted
func (t *Trade) SetTradeAccepted(entityID int32) (bool, error) {
if t.state != TradeStateActive {
return false, &TradeValidationError{
Code: TradeResultInvalidSlot,
Message: "Trade is not active",
}
}
t.mutex.Lock()
defer t.mutex.Unlock()
participant := t.getParticipantUnsafe(entityID)
if participant == nil {
return false, &TradeValidationError{
Code: TradeResultInvalidSlot,
Message: "Entity is not part of this trade",
}
}
participant.HasAccepted = true
// Check if both parties have accepted
if t.trader1.HasAccepted && t.trader2.HasAccepted {
// Complete the trade
t.completeTrade()
return true, nil
}
// TODO: Send acceptance packet to other trader when packet system is available
return false, nil
}
// HasAcceptedTrade checks if a participant has accepted the trade
// Converted from C++ Trade::HasAcceptedTrade
func (t *Trade) HasAcceptedTrade(entityID int32) bool {
t.mutex.RLock()
defer t.mutex.RUnlock()
participant := t.getParticipantUnsafe(entityID)
if participant == nil {
return false
}
return participant.HasAccepted
}
// CancelTrade cancels the trade
// Converted from C++ Trade::CancelTrade
func (t *Trade) CancelTrade(entityID int32) error {
t.mutex.Lock()
defer t.mutex.Unlock()
if t.state != TradeStateActive {
return &TradeValidationError{
Code: TradeResultInvalidSlot,
Message: "Trade is not active",
}
}
t.state = TradeStateCanceled
// TODO: Send cancel packets to both traders when packet system is available
// TODO: Clear trade references on entities when entity system is available
return nil
}
// GetTraderSlot returns the item in a specific slot for a trader
// Converted from C++ Trade::GetTraderSlot
func (t *Trade) GetTraderSlot(entityID int32, slot int8) Item {
t.mutex.RLock()
defer t.mutex.RUnlock()
participant := t.getParticipantUnsafe(entityID)
if participant == nil {
return nil
}
if itemInfo, exists := participant.Items[slot]; exists {
return itemInfo.Item
}
return nil
}
// GetTradeInfo returns comprehensive information about the trade
func (t *Trade) GetTradeInfo() map[string]interface{} {
t.mutex.RLock()
defer t.mutex.RUnlock()
info := make(map[string]interface{})
info["state"] = t.state
info["start_time"] = t.startTime
info["trader1_id"] = t.trader1.EntityID
info["trader2_id"] = t.trader2.EntityID
info["trader1_accepted"] = t.trader1.HasAccepted
info["trader2_accepted"] = t.trader2.HasAccepted
info["trader1_items"] = len(t.trader1.Items)
info["trader2_items"] = len(t.trader2.Items)
info["trader1_coins"] = t.trader1.Coins
info["trader2_coins"] = t.trader2.Coins
return info
}
// Private helper methods
// getParticipantUnsafe returns participant without locking (must be called with lock held)
func (t *Trade) getParticipantUnsafe(entityID int32) *TradeParticipant {
if t.trader1.EntityID == entityID {
return t.trader1
} else if t.trader2.EntityID == entityID {
return t.trader2
}
return nil
}
// getTradeeUnsafe returns the other trader's ID without locking
func (t *Trade) getTradeeUnsafe(entityID int32) int32 {
if t.trader1.EntityID == entityID {
return t.trader2.EntityID
} else if t.trader2.EntityID == entityID {
return t.trader1.EntityID
}
return 0
}
// validateItemTradability checks if an item can be traded between entities
// Converted from C++ Trade::CheckItem
func (t *Trade) validateItemTradability(item Item, traderID, otherID int32) error {
// Check no-trade flag
if item.IsNoTrade() {
// TODO: Only allow no-trade items with bots when bot system is available
return &TradeValidationError{
Code: TradeResultNoTrade,
Message: "Item cannot be traded",
}
}
// Check heirloom restrictions
if item.IsHeirloom() {
// TODO: Implement heirloom group checking when group system is available
// For now, allow heirloom trades with basic time/attunement checks
if item.IsAttuned() {
return &TradeValidationError{
Code: TradeResultHeirloom,
Message: "Attuned heirloom items cannot be traded",
}
}
// Check time-based restrictions (48 hours default)
creationTime := item.GetCreationTime()
if time.Since(creationTime) > 48*time.Hour {
return &TradeValidationError{
Code: TradeResultHeirloom,
Message: "Heirloom item sharing period has expired",
}
}
}
return nil
}
// completeTrade finalizes the trade and transfers items/coins
// Converted from C++ Trade::CompleteTrade
func (t *Trade) completeTrade() {
t.state = TradeStateCompleted
// TODO: Implement actual item/coin transfer when entity and inventory systems are available
// This would involve:
// 1. Remove items/coins from each trader's inventory
// 2. Add the other trader's items/coins to their inventory
// 3. Send completion packets
// 4. Log the trade
// 5. Clear trade references on entities
// For now, just mark as completed
}

188
internal/trade/types.go Normal file
View File

@ -0,0 +1,188 @@
package trade
import (
"sync"
"time"
)
// TradeItemInfo represents an item in a trade slot
// Converted from C++ TradeItemInfo struct
type TradeItemInfo struct {
Item *Item // TODO: Replace with actual Item type when available
Quantity int32 // Quantity of the item being traded
}
// CoinAmounts represents the breakdown of coins in EQ2 currency
type CoinAmounts struct {
Platinum int32 // Platinum coins
Gold int32 // Gold coins
Silver int32 // Silver coins
Copper int32 // Copper coins (base unit)
}
// TradeState represents the current state of a trade
type TradeState int32
const (
TradeStateActive TradeState = 0 // Trade is active and ongoing
TradeStateAccepted TradeState = 1 // Trade has been accepted by both parties
TradeStateCanceled TradeState = 2 // Trade was canceled
TradeStateCompleted TradeState = 3 // Trade was completed successfully
)
// TradeValidationError represents validation errors during trade operations
type TradeValidationError struct {
Code int32 // Error code from trade constants
Message string // Human readable error message
}
// Error implements the error interface
func (e *TradeValidationError) Error() string {
return e.Message
}
// TradeParticipant represents one side of a trade
type TradeParticipant struct {
EntityID int32 // Entity ID of the participant
Items map[int8]TradeItemInfo // Items being traded by slot
Coins int64 // Total coins being offered (in copper)
HasAccepted bool // Whether participant has accepted the trade
MaxSlots int8 // Maximum trade slots for this participant
IsBot bool // Whether this participant is a bot
ClientVersion int32 // Client version (affects slot count)
}
// NewTradeParticipant creates a new trade participant
func NewTradeParticipant(entityID int32, isBot bool, clientVersion int32) *TradeParticipant {
maxSlots := TradeMaxSlotsDefault
if clientVersion <= 561 {
maxSlots = TradeMaxSlotsLegacy
}
return &TradeParticipant{
EntityID: entityID,
Items: make(map[int8]TradeItemInfo),
Coins: 0,
HasAccepted: false,
MaxSlots: int8(maxSlots),
IsBot: isBot,
ClientVersion: clientVersion,
}
}
// GetNextFreeSlot finds the next available slot for items
func (tp *TradeParticipant) GetNextFreeSlot() int8 {
for slot := int8(0); slot < tp.MaxSlots; slot++ {
if _, exists := tp.Items[slot]; !exists {
return slot
}
}
return TradeSlotAutoFind // No free slots available
}
// HasItem checks if participant has a specific item in trade
func (tp *TradeParticipant) HasItem(itemID int32) bool {
for _, itemInfo := range tp.Items {
if itemInfo.Item != nil && itemInfo.Item.GetID() == itemID {
return true
}
}
return false
}
// GetItemCount returns the total number of items in trade
func (tp *TradeParticipant) GetItemCount() int {
return len(tp.Items)
}
// ClearItems removes all items from the trade
func (tp *TradeParticipant) ClearItems() {
tp.Items = make(map[int8]TradeItemInfo)
}
// GetCoinAmounts converts total copper to coin denominations
func (tp *TradeParticipant) GetCoinAmounts() CoinAmounts {
return CalculateCoins(tp.Coins)
}
// Item represents a placeholder item interface
// TODO: Replace with actual Item implementation when available
type Item interface {
GetID() int32
GetName() string
GetQuantity() int32
GetIcon(version int32) int32
IsNoTrade() bool
IsHeirloom() bool
IsAttuned() bool
GetCreationTime() time.Time
GetGroupCharacterIDs() []int32
}
// Entity represents a placeholder entity interface
// TODO: Replace with actual Entity implementation when available
type Entity interface {
GetID() int32
GetName() string
IsPlayer() bool
IsBot() bool
HasCoins(amount int64) bool
GetClientVersion() int32
}
// TradeManager handles multiple active trades
type TradeManager struct {
trades map[int32]*Trade // Active trades by trader1 ID
mutex sync.RWMutex // Thread safety
}
// NewTradeManager creates a new trade manager
func NewTradeManager() *TradeManager {
return &TradeManager{
trades: make(map[int32]*Trade),
}
}
// GetTrade retrieves an active trade for an entity
func (tm *TradeManager) GetTrade(entityID int32) *Trade {
tm.mutex.RLock()
defer tm.mutex.RUnlock()
// Check if entity is trader1
if trade, exists := tm.trades[entityID]; exists {
return trade
}
// Check if entity is trader2
for _, trade := range tm.trades {
if trade.GetTrader2ID() == entityID {
return trade
}
}
return nil
}
// AddTrade adds a new trade to management
func (tm *TradeManager) AddTrade(trade *Trade) {
tm.mutex.Lock()
defer tm.mutex.Unlock()
tm.trades[trade.GetTrader1ID()] = trade
}
// RemoveTrade removes a trade from management
func (tm *TradeManager) RemoveTrade(trader1ID int32) {
tm.mutex.Lock()
defer tm.mutex.Unlock()
delete(tm.trades, trader1ID)
}
// GetActiveTradeCount returns the number of active trades
func (tm *TradeManager) GetActiveTradeCount() int {
tm.mutex.RLock()
defer tm.mutex.RUnlock()
return len(tm.trades)
}

207
internal/trade/utils.go Normal file
View File

@ -0,0 +1,207 @@
package trade
import (
"fmt"
"strings"
)
// CalculateCoins converts a total copper amount to coin denominations
// Converted from C++ Trade::CalculateCoins
func CalculateCoins(totalCopper int64) CoinAmounts {
coins := CoinAmounts{}
remaining := totalCopper
// Calculate platinum (1,000,000 copper = 1 platinum)
if remaining >= CoinsPlatinumThreshold {
coins.Platinum = int32(remaining / CoinsPlatinumThreshold)
remaining -= int64(coins.Platinum) * CoinsPlatinumThreshold
}
// Calculate gold (10,000 copper = 1 gold)
if remaining >= CoinsGoldThreshold {
coins.Gold = int32(remaining / CoinsGoldThreshold)
remaining -= int64(coins.Gold) * CoinsGoldThreshold
}
// Calculate silver (100 copper = 1 silver)
if remaining >= CoinsSilverThreshold {
coins.Silver = int32(remaining / CoinsSilverThreshold)
remaining -= int64(coins.Silver) * CoinsSilverThreshold
}
// Remaining is copper
if remaining > 0 {
coins.Copper = int32(remaining)
}
return coins
}
// CoinsToCopper converts coin denominations to total copper
func CoinsToCopper(coins CoinAmounts) int64 {
total := int64(coins.Copper)
total += int64(coins.Silver) * CoinsSilverThreshold
total += int64(coins.Gold) * CoinsGoldThreshold
total += int64(coins.Platinum) * CoinsPlatinumThreshold
return total
}
// FormatCoins returns a human-readable string representation of coins
func FormatCoins(totalCopper int64) string {
if totalCopper == 0 {
return "0 copper"
}
coins := CalculateCoins(totalCopper)
parts := make([]string, 0, 4)
if coins.Platinum > 0 {
parts = append(parts, fmt.Sprintf("%d platinum", coins.Platinum))
}
if coins.Gold > 0 {
parts = append(parts, fmt.Sprintf("%d gold", coins.Gold))
}
if coins.Silver > 0 {
parts = append(parts, fmt.Sprintf("%d silver", coins.Silver))
}
if coins.Copper > 0 {
parts = append(parts, fmt.Sprintf("%d copper", coins.Copper))
}
return strings.Join(parts, ", ")
}
// ValidateTradeSlot checks if a slot number is valid for a participant
func ValidateTradeSlot(slot int8, maxSlots int8) bool {
return slot >= 0 && slot < maxSlots
}
// ValidateTradeQuantity checks if a quantity is valid for trading
func ValidateTradeQuantity(quantity, available int32) bool {
return quantity > 0 && quantity <= available
}
// FormatTradeError returns a formatted error message for trade operations
func FormatTradeError(code int32) string {
switch code {
case TradeResultSuccess:
return "Success"
case TradeResultAlreadyInTrade:
return "Item is already in the trade"
case TradeResultNoTrade:
return "Item cannot be traded"
case TradeResultHeirloom:
return "Heirloom item cannot be traded to this player"
case TradeResultInvalidSlot:
return "Invalid or occupied trade slot"
case TradeResultSlotOutOfRange:
return "Trade slot is out of range"
case TradeResultInsufficientQty:
return "Insufficient quantity to trade"
default:
return fmt.Sprintf("Unknown trade error: %d", code)
}
}
// GetClientMaxSlots returns the maximum trade slots for a client version
// Converted from C++ Trade constructor logic
func GetClientMaxSlots(clientVersion int32) int8 {
if clientVersion <= 561 {
return TradeMaxSlotsLegacy
}
return TradeMaxSlotsDefault
}
// IsValidTradeState checks if a trade operation is valid for the current state
func IsValidTradeState(state TradeState, operation string) bool {
switch operation {
case "add_item", "remove_item", "add_coins", "remove_coins", "accept":
return state == TradeStateActive
case "cancel":
return state == TradeStateActive
case "complete":
return state == TradeStateAccepted
default:
return false
}
}
// GenerateTradeLogEntry creates a log entry for trade operations
func GenerateTradeLogEntry(tradeID string, operation string, entityID int32, details interface{}) string {
return fmt.Sprintf("[Trade:%s] %s by entity %d: %v", tradeID, operation, entityID, details)
}
// CompareTradeItems checks if two trade item infos are equivalent
func CompareTradeItems(item1, item2 TradeItemInfo) bool {
if item1.Item == nil && item2.Item == nil {
return item1.Quantity == item2.Quantity
}
if item1.Item == nil || item2.Item == nil {
return false
}
return item1.Item.GetID() == item2.Item.GetID() &&
item1.Quantity == item2.Quantity
}
// CalculateTradeValue estimates the total value of items and coins in a trade
// This is a helper function for trade balancing and analysis
func CalculateTradeValue(participant *TradeParticipant) map[string]interface{} {
value := make(map[string]interface{})
// Add coin value
value["coins"] = participant.Coins
value["coins_formatted"] = FormatCoins(participant.Coins)
// Add item information
itemCount := len(participant.Items)
value["item_count"] = itemCount
if itemCount > 0 {
items := make([]map[string]interface{}, 0, itemCount)
for slot, itemInfo := range participant.Items {
itemData := make(map[string]interface{})
itemData["slot"] = slot
itemData["quantity"] = itemInfo.Quantity
if itemInfo.Item != nil {
itemData["item_id"] = itemInfo.Item.GetID()
itemData["item_name"] = itemInfo.Item.GetName()
}
items = append(items, itemData)
}
value["items"] = items
}
return value
}
// ValidateTradeCompletion checks if a trade is ready to be completed
func ValidateTradeCompletion(trade *Trade) []string {
errors := make([]string, 0)
if trade.GetState() != TradeStateActive {
errors = append(errors, "Trade is not in active state")
return errors
}
// Check if both parties have accepted
trader1Accepted := trade.HasAcceptedTrade(trade.GetTrader1ID())
trader2Accepted := trade.HasAcceptedTrade(trade.GetTrader2ID())
if !trader1Accepted {
errors = append(errors, "Trader 1 has not accepted the trade")
}
if !trader2Accepted {
errors = append(errors, "Trader 2 has not accepted the trade")
}
// TODO: Add additional validation when entity system is available:
// - Verify entities still exist and are online
// - Check inventory space for received items
// - Validate coin amounts against actual entity wealth
// - Check for item/trade restrictions that may have changed
return errors
}