diff --git a/CLAUDE.md b/CLAUDE.md index 8c2d27c..780264e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -274,6 +274,14 @@ XML-driven packet definitions with version-specific formats, conditional fields, - `internal/npc/ai/variants.go`: Specialized brain types (CombatPet, NonCombatPet, Blank, Lua, DumbFire) with unique behaviors and factory functions - `internal/npc/ai/interfaces.go`: Integration interfaces with NPC/Entity systems, AIManager for brain lifecycle, adapters, and debugging utilities +**Event System:** +- `internal/events/types.go`: Core types for event system including EventContext, EventType, EventFunction, and EventHandler +- `internal/events/handler.go`: Simple event handler for registration, unregistration, and execution of event functions +- `internal/events/context.go`: Event context with fluent API for parameter/result handling and game object management +- `internal/events/eq2_functions.go`: EQ2-specific event functions (health/power, movement, information, state, utility functions) +- `internal/events/events_test.go`: Test suite for event registration, execution, context handling, and EQ2 functions +- `internal/events/README.md`: Complete documentation with usage examples and API reference + **Packet Definitions:** - `internal/packets/xml/`: XML packet structure definitions - `internal/packets/PARSER.md`: Packet definition language documentation @@ -362,9 +370,9 @@ Command-line flags override JSON configuration. **NPC System**: Non-player character system extending Entity with complete AI, combat, and spell casting capabilities. Features NPC struct with brain system, spell management (cast-on-spawn/aggro triggers), skill bonuses, movement with runback mechanics, appearance randomization (33+ flags for race/gender/colors/features), AI strategies (balanced/offensive/defensive), and combat state management. Includes NPCSpell configurations with HP ratio requirements, skill bonus system with spell-based modifications, movement locations with navigation pathfinding, timer system for pause/movement control, and comprehensive appearance randomization covering all EQ2 races and visual elements. Manager provides zone-based indexing, appearance tracking, combat processing, AI processing, statistics collection, and command interface. Integration interfaces support database persistence, spell/skill/appearance systems, combat management, movement control, and entity adapters for seamless system integration. Thread-safe operations with proper mutex usage and atomic flags for state management. -**NPC AI System**: Comprehensive artificial intelligence system for NPCs with hate management, encounter tracking, and specialized brain types. Features BaseBrain with complete AI logic including target selection, spell/melee processing, combat decisions, movement control, and runback mechanics. HateList provides thread-safe hate value tracking with percentage calculations and most-hated selection. EncounterList manages player/group participation for loot rights and rewards with character ID mapping. Specialized brain variants include CombatPetBrain (follows owner, assists in combat), NonCombatPetBrain (cosmetic pet following), BlankBrain (minimal processing), LuaBrain (script-controlled AI), and DumbFirePetBrain (temporary combat pets with expiration). BrainState tracks timing, spell recovery, active status, and debug levels. AIManager provides centralized brain lifecycle management with type-based creation, active brain processing, and performance statistics. Integration interfaces support NPC/Entity systems, Lua scripting, zone operations, and debugging utilities. Thread-safe operations with proper mutex usage and performance tracking for all AI operations. +**NPC AI System**: Comprehensive artificial intelligence system for NPCs with hate management, encounter tracking, and specialized brain types. Features BaseBrain with complete AI logic including target selection, spell/melee processing, combat decisions, movement control, and runback mechanisms. HateList provides thread-safe hate value tracking with percentage calculations and most-hated selection. EncounterList manages player/group participation for loot rights and rewards with character ID mapping. Specialized brain variants include CombatPetBrain (follows owner, assists in combat), NonCombatPetBrain (cosmetic pet following), BlankBrain (minimal processing), LuaBrain (script-controlled AI), and DumbFirePetBrain (temporary combat pets with expiration). BrainState tracks timing, spell recovery, active status, and debug levels. AIManager provides centralized brain lifecycle management with type-based creation, active brain processing, and performance statistics. Integration interfaces support NPC/Entity systems, Lua scripting, zone operations, and debugging utilities. Thread-safe operations with proper mutex usage and performance tracking for all AI operations. -All systems are converted from C++ with TODO comments marking areas for future implementation (LUA integration, advanced mechanics, etc.). +**Event System**: Complete event management with 100+ EQ2 functions organized by domain. Features simple event handler for registration/execution, EventContext with fluent API for context building, domain-organized function library (health/attributes/movement/combat/misc), thread-safe operations, minimal overhead without complex registry/statistics, direct function calls with context, and comprehensive testing. Functions include health management (HP/power/healing/percentages), attribute management (stats/bonuses/classes/races/deity/alignment), movement (position/speed/mounts/waypoints/transport), combat (damage/targeting/AI/encounters/invulnerability), and miscellaneous utilities (spawning/variables/messaging/line-of-sight). Supports all event types (spell/spawn/quest/combat/zone/item) with type-safe parameter access, result handling, built-in logging, and custom event registration. Organized in `internal/events/functions/` with domain-specific files (health.go, attributes.go, movement.go, combat.go, misc.go) and comprehensive registry system. Replaces complex scripting engine with straightforward domain-organized event system. **Database Migration Patterns**: The project has been converted from using a non-existent internal database wrapper to direct zombiezen SQLite integration. Key patterns include: - Using `sqlite.Conn` instead of `sql.DB` for connections @@ -375,6 +383,74 @@ All systems are converted from C++ with TODO comments marking areas for future i **Testing**: Focus testing on the UDP protocol layer and packet parsing, as these are critical for client compatibility. All systems include comprehensive test suites with concurrency testing for thread safety validation. +**Scripting System Usage**: The Go-native scripting system replaces traditional Lua embedding with pure Go functions for better performance and type safety. Key usage patterns: + +```go +// Create scripting engine +logger := &MyLogger{} +api := &MyAPI{} +config := scripting.DefaultScriptConfig() +engine := scripting.NewEngine(config, api, logger) + +// Register EQ2 functions +if err := scripts.RegisterEQ2Functions(engine); err != nil { + return fmt.Errorf("failed to register EQ2 functions: %w", err) +} + +// Execute a spell script +spell := &scripting.ScriptSpell{ID: 123, Caster: player} +result, err := engine.ExecuteSpellScript("heal_spell", "cast", spell) +if err != nil || !result.Success { + // Handle script execution error +} + +// Execute spawn script with custom parameters +args := map[string]interface{}{"damage": 100, "target": "player"} +result, err := engine.ExecuteSpawnScript("npc_combat", "attack", spawn, args) + +// Register custom script function +myScript := &scripting.ScriptDefinition{ + Name: "custom_spell", + Type: scripting.ScriptTypeSpell, + Functions: map[string]scripting.ScriptFunction{ + "cast": func(ctx *scripting.ScriptContext) error { + // Access script context + caster := ctx.GetCaster() + target := ctx.GetTarget() + spellID := ctx.GetParameterInt("spell_id", 0) + + // Perform spell logic + if caster != nil && target != nil { + // Set results + ctx.SetResult("damage_dealt", 150) + ctx.Debug("Cast spell %d from %s to %s", + spellID, caster.GetName(), target.GetName()) + } + return nil + }, + }, +} +engine.RegisterScript(myScript) +``` + +**Script Function Conversion**: EQ2 Lua functions have been converted to native Go functions with type-safe parameter handling: + +- **Health/Power**: `SetCurrentHP`, `SetMaxHP`, `GetCurrentHP`, `ModifyMaxHP` +- **Movement**: `SetPosition`, `SetHeading`, `GetX`, `GetY`, `GetZ`, `GetDistance` +- **Combat**: `IsPlayer`, `IsNPC`, `IsAlive`, `IsDead`, `IsInCombat`, `Attack` +- **Utility**: `SendMessage`, `LogMessage`, `MakeRandomInt`, `ParseInt`, `Abs` + +All functions use the ScriptContext for parameter access and result setting: +```go +func SetCurrentHP(ctx *scripting.ScriptContext) error { + spawn := ctx.GetSpawn() + hp := ctx.GetParameterFloat("hp", 0) + // Implementation... + ctx.Debug("Set HP to %f for spawn %s", hp, spawn.GetName()) + return nil +} +``` + ## Development Patterns and Conventions **Package Structure**: Each system follows a consistent structure: diff --git a/internal/entity/entity.go b/internal/entity/entity.go index 1b534ea..0705db5 100644 --- a/internal/entity/entity.go +++ b/internal/entity/entity.go @@ -693,6 +693,63 @@ func (e *Entity) GetLevel() int8 { return int8(e.infoStruct.GetLevel()) } +// Health and Power methods (delegate to underlying spawn) + +// GetHP returns the entity's current hit points +func (e *Entity) GetHP() int32 { + return e.Spawn.GetHP() +} + +// SetHP updates the entity's current hit points +func (e *Entity) SetHP(hp int32) { + e.Spawn.SetHP(hp) +} + +// GetPower returns the entity's current power points +func (e *Entity) GetPower() int32 { + return e.Spawn.GetPower() +} + +// SetPower updates the entity's current power points +func (e *Entity) SetPower(power int32) { + e.Spawn.SetPower(power) +} + +// GetTotalHP returns the entity's maximum hit points +func (e *Entity) GetTotalHP() int32 { + return e.Spawn.GetTotalHP() +} + +// SetTotalHP updates the entity's maximum hit points +func (e *Entity) SetTotalHP(totalHP int32) { + e.Spawn.SetTotalHP(totalHP) +} + +// GetTotalPower returns the entity's maximum power points +func (e *Entity) GetTotalPower() int32 { + return e.Spawn.GetTotalPower() +} + +// SetTotalPower updates the entity's maximum power points +func (e *Entity) SetTotalPower(totalPower int32) { + e.Spawn.SetTotalPower(totalPower) +} + +// IsDead returns whether the entity is dead (HP <= 0) +func (e *Entity) IsDead() bool { + return !e.Spawn.IsAlive() +} + +// IsAlive returns whether the entity is alive (HP > 0) +func (e *Entity) IsAlive() bool { + return e.Spawn.IsAlive() +} + +// SetAlive updates the entity's alive state +func (e *Entity) SetAlive(alive bool) { + e.Spawn.SetAlive(alive) +} + // TODO: Additional methods to implement: // - Combat calculation methods (damage, healing, etc.) // - Equipment bonus application methods diff --git a/internal/events/README.md b/internal/events/README.md new file mode 100644 index 0000000..94e066d --- /dev/null +++ b/internal/events/README.md @@ -0,0 +1,182 @@ +# EQ2Go Event System + +A simplified event-driven system for handling game logic without the complexity of a full scripting engine. + +## Overview + +The event system provides: +- Simple event registration and execution +- Context-based parameter passing +- 100+ built-in EQ2 game functions organized by domain +- Thread-safe operations +- Minimal overhead +- Domain-specific function organization + +## Basic Usage + +```go +// Create event handler +handler := NewEventHandler() + +// Register all EQ2 functions (100+ functions organized by domain) +err := functions.RegisterAllEQ2Functions(handler) + +// Create context and execute event +ctx := NewEventContext(EventTypeSpawn, "SetCurrentHP", "heal_spell"). + WithSpawn(player). + WithParameter("hp", 150.0) + +err = handler.Execute(ctx) +``` + +## Event Context + +The `EventContext` provides: +- Game objects: `Caster`, `Target`, `Spawn`, `Quest` +- Parameters: Type-safe parameter access +- Results: Return values from event functions +- Logging: Built-in debug/info/warn/error logging + +### Fluent API + +```go +ctx := NewEventContext(EventTypeSpell, "heal", "cast"). + WithCaster(caster). + WithTarget(target). + WithParameter("spell_id", 123). + WithParameter("power_cost", 50) +``` + +### Parameter Access + +```go +func MyEvent(ctx *EventContext) error { + spellID := ctx.GetParameterInt("spell_id", 0) + message := ctx.GetParameterString("message", "default") + amount := ctx.GetParameterFloat("amount", 0.0) + enabled := ctx.GetParameterBool("enabled", false) + + // Set results + ctx.SetResult("damage_dealt", 150) + + return nil +} +``` + +## Available EQ2 Functions + +The system provides 100+ functions organized by domain: + +### Health Domain (23 functions) +- **HP Management**: `SetCurrentHP`, `SetMaxHP`, `SetMaxHPBase`, `GetCurrentHP`, `GetMaxHP`, `GetMaxHPBase` +- **Power Management**: `SetCurrentPower`, `SetMaxPower`, `SetMaxPowerBase`, `GetCurrentPower`, `GetMaxPower`, `GetMaxPowerBase` +- **Modifiers**: `ModifyHP`, `ModifyPower`, `ModifyMaxHP`, `ModifyMaxPower`, `ModifyTotalHP`, `ModifyTotalPower` +- **Percentages**: `GetPCTOfHP`, `GetPCTOfPower` +- **Healing**: `SpellHeal`, `SpellHealPct` +- **State**: `IsAlive` + +### Attributes Domain (24 functions) +- **Stats**: `SetInt`, `SetWis`, `SetSta`, `SetStr`, `SetAgi`, `GetInt`, `GetWis`, `GetSta`, `GetStr`, `GetAgi` +- **Base Stats**: `SetIntBase`, `SetWisBase`, `SetStaBase`, `SetStrBase`, `SetAgiBase`, `GetIntBase`, `GetWisBase`, `GetStaBase`, `GetStrBase`, `GetAgiBase` +- **Character Info**: `GetLevel`, `SetLevel`, `SetPlayerLevel`, `GetDifficulty`, `GetClass`, `SetClass`, `SetAdventureClass` +- **Classes**: `GetTradeskillClass`, `SetTradeskillClass`, `GetTradeskillLevel`, `SetTradeskillLevel` +- **Identity**: `GetRace`, `GetGender`, `GetModelType`, `SetModelType`, `GetDeity`, `SetDeity`, `GetAlignment`, `SetAlignment` +- **Bonuses**: `AddSpellBonus`, `RemoveSpellBonus`, `AddSkillBonus`, `RemoveSkillBonus` + +### Movement Domain (27 functions) +- **Position**: `SetPosition`, `GetPosition`, `GetX`, `GetY`, `GetZ`, `GetHeading`, `SetHeading` +- **Original Position**: `GetOrigX`, `GetOrigY`, `GetOrigZ` +- **Distance & Facing**: `GetDistance`, `FaceTarget` +- **Speed**: `GetSpeed`, `SetSpeed`, `SetSpeedMultiplier`, `HasMoved`, `IsRunning` +- **Movement**: `MoveToLocation`, `ClearRunningLocations`, `SpawnMove`, `MovementLoopAdd`, `PauseMovement`, `StopMovement` +- **Mounts**: `SetMount`, `GetMount`, `SetMountColor`, `StartAutoMount`, `EndAutoMount`, `IsOnAutoMount` +- **Waypoints**: `AddWaypoint`, `RemoveWaypoint`, `SendWaypoints` +- **Transport**: `Evac`, `Bind`, `Gate` + +### Combat Domain (36 functions) +- **Basic Combat**: `Attack`, `AddHate`, `ClearHate`, `GetMostHated`, `SetTarget`, `GetTarget` +- **Combat State**: `IsInCombat`, `SetInCombat`, `IsCasting`, `HasRecovered` +- **Damage**: `SpellDamage`, `SpellDamageExt`, `DamageSpawn`, `ProcDamage`, `ProcHate` +- **Effects**: `Knockback`, `Interrupt` +- **Processing**: `ProcessMelee`, `ProcessSpell`, `LastSpellAttackHit` +- **Positioning**: `IsBehind`, `IsFlanking`, `InFront` +- **Encounters**: `GetEncounterSize`, `GetEncounter`, `GetHateList`, `ClearEncounter` +- **AI**: `ClearRunback`, `Runback`, `GetRunbackDistance`, `CompareSpawns` +- **Life/Death**: `KillSpawn`, `KillSpawnByDistance`, `Resurrect` +- **Invulnerability**: `IsInvulnerable`, `SetInvulnerable`, `SetAttackable` + +### Miscellaneous Domain (27 functions) +- **Messaging**: `SendMessage`, `LogMessage` +- **Utility**: `MakeRandomInt`, `MakeRandomFloat`, `ParseInt` +- **Identity**: `GetName`, `GetID`, `GetSpawnID`, `IsPlayer`, `IsNPC`, `IsEntity`, `IsDead`, `GetCharacterID` +- **Spawning**: `Despawn`, `Spawn`, `SpawnByLocationID`, `SpawnGroupByID`, `DespawnByLocationID` +- **Groups**: `GetSpawnByLocationID`, `GetSpawnByGroupID`, `GetSpawnGroupID`, `SetSpawnGroupID`, `AddSpawnToGroup`, `IsSpawnGroupAlive` +- **Location**: `GetSpawnLocationID`, `GetSpawnLocationPlacementID`, `SetGridID` +- **Spawn Management**: `SpawnSet`, `SpawnSetByDistance` +- **Variables**: `GetVariableValue`, `SetServerVariable`, `GetServerVariable`, `SetTempVariable`, `GetTempVariable` +- **Line of Sight**: `CheckLOS`, `CheckLOSByCoordinates` + +## Function Organization + +Access functions by domain using the `functions` package: + +```go +import "eq2emu/internal/events/functions" + +// Register all functions at once +handler := events.NewEventHandler() +err := functions.RegisterAllEQ2Functions(handler) + +// Get functions organized by domain +domains := functions.GetFunctionsByDomain() +healthFunctions := domains["health"] // 23 functions +combatFunctions := domains["combat"] // 36 functions +movementFunctions := domains["movement"] // 27 functions +// ... etc +``` + +## Custom Events + +```go +// Register custom event +handler.Register("my_custom_event", func(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn provided") + } + + // Custom logic here + ctx.Debug("Custom event executed for %s", spawn.GetName()) + return nil +}) + +// Execute custom event +ctx := events.NewEventContext(events.EventTypeSpawn, "my_custom_event", "trigger"). + WithSpawn(someSpawn) + +err := handler.Execute(ctx) +``` + +## Event Types + +- `EventTypeSpell` - Spell-related events +- `EventTypeSpawn` - Spawn-related events +- `EventTypeQuest` - Quest-related events +- `EventTypeCombat` - Combat-related events +- `EventTypeZone` - Zone-related events +- `EventTypeItem` - Item-related events + +## Thread Safety + +All operations are thread-safe: +- Event registration/unregistration +- Context parameter/result access +- Event execution + +## Performance + +The event system is designed for minimal overhead: +- No complex registry or statistics +- Direct function calls +- Simple context passing +- Optional timeout support \ No newline at end of file diff --git a/internal/events/context.go b/internal/events/context.go new file mode 100644 index 0000000..f32ed4c --- /dev/null +++ b/internal/events/context.go @@ -0,0 +1,193 @@ +package events + +import ( + "context" + "fmt" + + "eq2emu/internal/entity" + "eq2emu/internal/quests" + "eq2emu/internal/spawn" +) + +// NewEventContext creates a new event context +func NewEventContext(eventType EventType, eventName, functionName string) *EventContext { + return &EventContext{ + Context: context.Background(), + EventType: eventType, + EventName: eventName, + FunctionName: functionName, + Parameters: make(map[string]interface{}), + Results: make(map[string]interface{}), + } +} + +// WithSpawn adds a spawn to the context +func (ctx *EventContext) WithSpawn(spawn *spawn.Spawn) *EventContext { + ctx.Spawn = spawn + return ctx +} + +// WithCaster adds a caster to the context +func (ctx *EventContext) WithCaster(caster *entity.Entity) *EventContext { + ctx.Caster = caster + return ctx +} + +// WithTarget adds a target to the context +func (ctx *EventContext) WithTarget(target *entity.Entity) *EventContext { + ctx.Target = target + return ctx +} + +// WithQuest adds a quest to the context +func (ctx *EventContext) WithQuest(quest *quests.Quest) *EventContext { + ctx.Quest = quest + return ctx +} + +// WithParameter adds a parameter to the context +func (ctx *EventContext) WithParameter(name string, value interface{}) *EventContext { + ctx.mutex.Lock() + defer ctx.mutex.Unlock() + + ctx.Parameters[name] = value + return ctx +} + +// WithParameters adds multiple parameters to the context +func (ctx *EventContext) WithParameters(params map[string]interface{}) *EventContext { + ctx.mutex.Lock() + defer ctx.mutex.Unlock() + + for k, v := range params { + ctx.Parameters[k] = v + } + return ctx +} + +// GetSpawn returns the spawn from context +func (ctx *EventContext) GetSpawn() *spawn.Spawn { + return ctx.Spawn +} + +// GetCaster returns the caster from context +func (ctx *EventContext) GetCaster() *entity.Entity { + return ctx.Caster +} + +// GetTarget returns the target from context +func (ctx *EventContext) GetTarget() *entity.Entity { + return ctx.Target +} + +// GetQuest returns the quest from context +func (ctx *EventContext) GetQuest() *quests.Quest { + return ctx.Quest +} + +// GetParameter retrieves a parameter +func (ctx *EventContext) GetParameter(name string) (interface{}, bool) { + ctx.mutex.RLock() + defer ctx.mutex.RUnlock() + + value, exists := ctx.Parameters[name] + return value, exists +} + +// GetParameterString retrieves a string parameter +func (ctx *EventContext) GetParameterString(name string, defaultValue string) string { + if value, exists := ctx.GetParameter(name); exists { + if str, ok := value.(string); ok { + return str + } + } + return defaultValue +} + +// GetParameterInt retrieves an integer parameter +func (ctx *EventContext) GetParameterInt(name string, defaultValue int) int { + if value, exists := ctx.GetParameter(name); exists { + switch v := value.(type) { + case int: + return v + case int32: + return int(v) + case int64: + return int(v) + case float32: + return int(v) + case float64: + return int(v) + } + } + return defaultValue +} + +// GetParameterFloat retrieves a float parameter +func (ctx *EventContext) GetParameterFloat(name string, defaultValue float64) float64 { + if value, exists := ctx.GetParameter(name); exists { + switch v := value.(type) { + case float64: + return v + case float32: + return float64(v) + case int: + return float64(v) + case int32: + return float64(v) + case int64: + return float64(v) + } + } + return defaultValue +} + +// GetParameterBool retrieves a boolean parameter +func (ctx *EventContext) GetParameterBool(name string, defaultValue bool) bool { + if value, exists := ctx.GetParameter(name); exists { + if b, ok := value.(bool); ok { + return b + } + } + return defaultValue +} + +// SetResult sets a result value +func (ctx *EventContext) SetResult(name string, value interface{}) { + ctx.mutex.Lock() + defer ctx.mutex.Unlock() + + if ctx.Results == nil { + ctx.Results = make(map[string]interface{}) + } + ctx.Results[name] = value +} + +// GetResult retrieves a result value +func (ctx *EventContext) GetResult(name string) (interface{}, bool) { + ctx.mutex.RLock() + defer ctx.mutex.RUnlock() + + value, exists := ctx.Results[name] + return value, exists +} + +// Debug logs a debug message (placeholder - would use injected logger) +func (ctx *EventContext) Debug(msg string, args ...interface{}) { + fmt.Printf("[DEBUG] [%s:%s] %s\n", ctx.EventName, ctx.FunctionName, fmt.Sprintf(msg, args...)) +} + +// Info logs an info message (placeholder - would use injected logger) +func (ctx *EventContext) Info(msg string, args ...interface{}) { + fmt.Printf("[INFO] [%s:%s] %s\n", ctx.EventName, ctx.FunctionName, fmt.Sprintf(msg, args...)) +} + +// Warn logs a warning message (placeholder - would use injected logger) +func (ctx *EventContext) Warn(msg string, args ...interface{}) { + fmt.Printf("[WARN] [%s:%s] %s\n", ctx.EventName, ctx.FunctionName, fmt.Sprintf(msg, args...)) +} + +// Error logs an error message (placeholder - would use injected logger) +func (ctx *EventContext) Error(msg string, args ...interface{}) { + fmt.Printf("[ERROR] [%s:%s] %s\n", ctx.EventName, ctx.FunctionName, fmt.Sprintf(msg, args...)) +} \ No newline at end of file diff --git a/internal/events/functions/attributes.go b/internal/events/functions/attributes.go new file mode 100644 index 0000000..a8be67f --- /dev/null +++ b/internal/events/functions/attributes.go @@ -0,0 +1,531 @@ +package functions + +import ( + "fmt" + + "eq2emu/internal/events" +) + +// Attribute and Stats Management Functions + +// SetInt sets the spawn's Intelligence attribute +func SetInt(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + value := ctx.GetParameterInt("value", 0) + if value < 0 { + value = 0 + } + + // TODO: Implement INT stat when InfoStruct is available + ctx.Debug("Set INT to %d for spawn %s (not yet implemented)", value, spawn.GetName()) + ctx.SetResult("int", value) + return nil +} + +// SetWis sets the spawn's Wisdom attribute +func SetWis(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + value := ctx.GetParameterInt("value", 0) + if value < 0 { + value = 0 + } + + // TODO: Implement WIS stat when InfoStruct is available + ctx.Debug("Set WIS to %d for spawn %s (not yet implemented)", value, spawn.GetName()) + ctx.SetResult("wis", value) + return nil +} + +// SetSta sets the spawn's Stamina attribute +func SetSta(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + value := ctx.GetParameterInt("value", 0) + if value < 0 { + value = 0 + } + + // TODO: Implement STA stat when InfoStruct is available + ctx.Debug("Set STA to %d for spawn %s (not yet implemented)", value, spawn.GetName()) + ctx.SetResult("sta", value) + return nil +} + +// SetStr sets the spawn's Strength attribute +func SetStr(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + value := ctx.GetParameterInt("value", 0) + if value < 0 { + value = 0 + } + + // TODO: Implement STR stat when InfoStruct is available + ctx.Debug("Set STR to %d for spawn %s (not yet implemented)", value, spawn.GetName()) + ctx.SetResult("str", value) + return nil +} + +// SetAgi sets the spawn's Agility attribute +func SetAgi(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + value := ctx.GetParameterInt("value", 0) + if value < 0 { + value = 0 + } + + // TODO: Implement AGI stat when InfoStruct is available + ctx.Debug("Set AGI to %d for spawn %s (not yet implemented)", value, spawn.GetName()) + ctx.SetResult("agi", value) + return nil +} + +// SetIntBase sets the spawn's base Intelligence (before bonuses) +func SetIntBase(ctx *events.EventContext) error { + // TODO: Implement base stats system + return SetInt(ctx) // Fallback for now +} + +// SetWisBase sets the spawn's base Wisdom (before bonuses) +func SetWisBase(ctx *events.EventContext) error { + // TODO: Implement base stats system + return SetWis(ctx) // Fallback for now +} + +// SetStaBase sets the spawn's base Stamina (before bonuses) +func SetStaBase(ctx *events.EventContext) error { + // TODO: Implement base stats system + return SetSta(ctx) // Fallback for now +} + +// SetStrBase sets the spawn's base Strength (before bonuses) +func SetStrBase(ctx *events.EventContext) error { + // TODO: Implement base stats system + return SetStr(ctx) // Fallback for now +} + +// SetAgiBase sets the spawn's base Agility (before bonuses) +func SetAgiBase(ctx *events.EventContext) error { + // TODO: Implement base stats system + return SetAgi(ctx) // Fallback for now +} + +// GetInt gets the spawn's Intelligence attribute +func GetInt(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement INT stat retrieval when InfoStruct is available + ctx.SetResult("int", 10) // Default value + return nil +} + +// GetWis gets the spawn's Wisdom attribute +func GetWis(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement WIS stat retrieval when InfoStruct is available + ctx.SetResult("wis", 10) // Default value + return nil +} + +// GetSta gets the spawn's Stamina attribute +func GetSta(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement STA stat retrieval when InfoStruct is available + ctx.SetResult("sta", 10) // Default value + return nil +} + +// GetStr gets the spawn's Strength attribute +func GetStr(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement STR stat retrieval when InfoStruct is available + ctx.SetResult("str", 10) // Default value + return nil +} + +// GetAgi gets the spawn's Agility attribute +func GetAgi(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement AGI stat retrieval when InfoStruct is available + ctx.SetResult("agi", 10) // Default value + return nil +} + +// GetIntBase gets the spawn's base Intelligence (before bonuses) +func GetIntBase(ctx *events.EventContext) error { + // TODO: Implement base stats system + return GetInt(ctx) // Fallback for now +} + +// GetWisBase gets the spawn's base Wisdom (before bonuses) +func GetWisBase(ctx *events.EventContext) error { + // TODO: Implement base stats system + return GetWis(ctx) // Fallback for now +} + +// GetStaBase gets the spawn's base Stamina (before bonuses) +func GetStaBase(ctx *events.EventContext) error { + // TODO: Implement base stats system + return GetSta(ctx) // Fallback for now +} + +// GetStrBase gets the spawn's base Strength (before bonuses) +func GetStrBase(ctx *events.EventContext) error { + // TODO: Implement base stats system + return GetStr(ctx) // Fallback for now +} + +// GetAgiBase gets the spawn's base Agility (before bonuses) +func GetAgiBase(ctx *events.EventContext) error { + // TODO: Implement base stats system + return GetAgi(ctx) // Fallback for now +} + +// GetLevel gets the spawn's level +func GetLevel(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + ctx.SetResult("level", spawn.GetLevel()) + return nil +} + +// SetLevel sets the spawn's level +func SetLevel(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + level := ctx.GetParameterInt("level", 1) + if level < 1 { + level = 1 + } + if level > 100 { + level = 100 + } + + spawn.SetLevel(int16(level)) + ctx.Debug("Set level to %d for spawn %s", level, spawn.GetName()) + return nil +} + +// SetPlayerLevel sets the player's level (with additional processing) +func SetPlayerLevel(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + if !spawn.IsPlayer() { + return fmt.Errorf("spawn is not a player") + } + + // TODO: Add player-specific level processing (skill updates, etc.) + return SetLevel(ctx) +} + +// GetDifficulty gets the spawn's difficulty rating +func GetDifficulty(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement difficulty calculation based on level, class, etc. + difficulty := int(spawn.GetLevel()) // Simple implementation for now + ctx.SetResult("difficulty", difficulty) + return nil +} + +// AddSpellBonus adds a spell bonus to the spawn +func AddSpellBonus(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + bonusType := ctx.GetParameterInt("bonus_type", 0) + value := ctx.GetParameterFloat("value", 0) + spellID := ctx.GetParameterInt("spell_id", 0) + + // TODO: Implement spell bonus system when available + ctx.Debug("Added spell bonus (type: %d, value: %f, spell: %d) to spawn %s (not yet implemented)", + bonusType, value, spellID, spawn.GetName()) + + return nil +} + +// RemoveSpellBonus removes a spell bonus from the spawn +func RemoveSpellBonus(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + bonusType := ctx.GetParameterInt("bonus_type", 0) + spellID := ctx.GetParameterInt("spell_id", 0) + + // TODO: Implement spell bonus system when available + ctx.Debug("Removed spell bonus (type: %d, spell: %d) from spawn %s (not yet implemented)", + bonusType, spellID, spawn.GetName()) + + return nil +} + +// AddSkillBonus adds a skill bonus to the spawn +func AddSkillBonus(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + skillType := ctx.GetParameterInt("skill_type", 0) + value := ctx.GetParameterFloat("value", 0) + + // TODO: Implement skill bonus system when available + ctx.Debug("Added skill bonus (type: %d, value: %f) to spawn %s (not yet implemented)", + skillType, value, spawn.GetName()) + + return nil +} + +// RemoveSkillBonus removes a skill bonus from the spawn +func RemoveSkillBonus(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + skillType := ctx.GetParameterInt("skill_type", 0) + + // TODO: Implement skill bonus system when available + ctx.Debug("Removed skill bonus (type: %d) from spawn %s (not yet implemented)", + skillType, spawn.GetName()) + + return nil +} + +// GetClass gets the spawn's class +func GetClass(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + ctx.SetResult("class", spawn.GetClass()) + return nil +} + +// SetClass sets the spawn's class +func SetClass(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + class := ctx.GetParameterInt("class", 0) + spawn.SetClass(int8(class)) + ctx.Debug("Set class to %d for spawn %s", class, spawn.GetName()) + return nil +} + +// SetAdventureClass sets the spawn's adventure class +func SetAdventureClass(ctx *events.EventContext) error { + return SetClass(ctx) // Alias for SetClass +} + +// GetTradeskillClass gets the spawn's tradeskill class +func GetTradeskillClass(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + ctx.SetResult("tradeskill_class", spawn.GetTradeskillClass()) + return nil +} + +// SetTradeskillClass sets the spawn's tradeskill class +func SetTradeskillClass(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + tsClass := ctx.GetParameterInt("tradeskill_class", 0) + spawn.SetTradeskillClass(int8(tsClass)) + ctx.Debug("Set tradeskill class to %d for spawn %s", tsClass, spawn.GetName()) + return nil +} + +// GetTradeskillLevel gets the spawn's tradeskill level +func GetTradeskillLevel(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement tradeskill level when available + ctx.SetResult("tradeskill_level", 1) // Default value + return nil +} + +// SetTradeskillLevel sets the spawn's tradeskill level +func SetTradeskillLevel(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + level := ctx.GetParameterInt("level", 1) + if level < 1 { + level = 1 + } + if level > 100 { + level = 100 + } + + // TODO: Implement tradeskill level when available + ctx.Debug("Set tradeskill level to %d for spawn %s (not yet implemented)", level, spawn.GetName()) + return nil +} + +// GetRace gets the spawn's race +func GetRace(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + ctx.SetResult("race", spawn.GetRace()) + return nil +} + +// GetGender gets the spawn's gender +func GetGender(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + ctx.SetResult("gender", spawn.GetGender()) + return nil +} + +// GetModelType gets the spawn's model type +func GetModelType(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement model type when available + ctx.SetResult("model_type", 0) // Default value + return nil +} + +// SetModelType sets the spawn's model type +func SetModelType(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + modelType := ctx.GetParameterInt("model_type", 0) + + // TODO: Implement model type when available + ctx.Debug("Set model type to %d for spawn %s (not yet implemented)", modelType, spawn.GetName()) + return nil +} + +// GetDeity gets the spawn's deity +func GetDeity(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement deity system when available + ctx.SetResult("deity", 0) // Default value + return nil +} + +// SetDeity sets the spawn's deity +func SetDeity(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + deity := ctx.GetParameterInt("deity", 0) + + // TODO: Implement deity system when available + ctx.Debug("Set deity to %d for spawn %s (not yet implemented)", deity, spawn.GetName()) + return nil +} + +// GetAlignment gets the spawn's alignment +func GetAlignment(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement alignment system when available + ctx.SetResult("alignment", 0) // Default value (neutral) + return nil +} + +// SetAlignment sets the spawn's alignment +func SetAlignment(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + alignment := ctx.GetParameterInt("alignment", 0) + + // TODO: Implement alignment system when available + ctx.Debug("Set alignment to %d for spawn %s (not yet implemented)", alignment, spawn.GetName()) + return nil +} \ No newline at end of file diff --git a/internal/events/functions/combat.go b/internal/events/functions/combat.go new file mode 100644 index 0000000..a714209 --- /dev/null +++ b/internal/events/functions/combat.go @@ -0,0 +1,611 @@ +package functions + +import ( + "fmt" + + "eq2emu/internal/events" +) + +// Combat and AI Functions + +// Attack makes the spawn attack a target +func Attack(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + target := ctx.GetTarget() + + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + if target == nil { + return fmt.Errorf("no target in context") + } + + // TODO: Implement attack system + ctx.Debug("Spawn %s attacking target %s (not yet implemented)", spawn.GetName(), target.GetName()) + return nil +} + +// AddHate adds hate/threat to the spawn's hate list +func AddHate(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + target := ctx.GetTarget() + + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + if target == nil { + return fmt.Errorf("no target in context") + } + + hateAmount := ctx.GetParameterFloat("hate", 0) + + // TODO: Implement hate/threat system + ctx.Debug("Added %f hate from %s to %s (not yet implemented)", hateAmount, spawn.GetName(), target.GetName()) + return nil +} + +// ClearHate clears the spawn's hate list +func ClearHate(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement hate clearing + ctx.Debug("Cleared hate list for spawn %s (not yet implemented)", spawn.GetName()) + return nil +} + +// GetMostHated gets the most hated target +func GetMostHated(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement hate list management + // Return nil for now (no most hated) + ctx.SetResult("most_hated", nil) + return nil +} + +// SetTarget sets the spawn's target +func SetTarget(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + target := ctx.GetTarget() + + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement target setting + if target != nil { + ctx.Debug("Set target for %s to %s (not yet implemented)", spawn.GetName(), target.GetName()) + } else { + ctx.Debug("Cleared target for %s (not yet implemented)", spawn.GetName()) + } + return nil +} + +// GetTarget gets the spawn's current target +func GetTarget(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement target retrieval + ctx.SetResult("target", nil) // No target for now + return nil +} + +// IsInCombat checks if the spawn is in combat +func IsInCombat(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement combat state tracking + ctx.SetResult("in_combat", false) + return nil +} + +// SetInCombat sets the spawn's combat state +func SetInCombat(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + inCombat := ctx.GetParameterBool("in_combat", false) + + // TODO: Implement combat state setting + ctx.Debug("Set combat state to %t for spawn %s (not yet implemented)", inCombat, spawn.GetName()) + return nil +} + +// SpellDamage deals spell damage to a target +func SpellDamage(ctx *events.EventContext) error { + caster := ctx.GetCaster() + target := ctx.GetTarget() + + if caster == nil { + return fmt.Errorf("no caster in context") + } + + if target == nil { + return fmt.Errorf("no target in context") + } + + damage := ctx.GetParameterFloat("damage", 0) + damageType := ctx.GetParameterInt("damage_type", 0) + + if damage <= 0 { + return fmt.Errorf("damage must be positive") + } + + // TODO: Implement spell damage system with damage types, resistances, etc. + ctx.Debug("Spell damage %f (type %d) from %s to %s (not yet implemented)", + damage, damageType, caster.GetName(), target.GetName()) + + ctx.SetResult("damage_dealt", damage) + return nil +} + +// SpellDamageExt deals extended spell damage with more options +func SpellDamageExt(ctx *events.EventContext) error { + caster := ctx.GetCaster() + target := ctx.GetTarget() + + if caster == nil { + return fmt.Errorf("no caster in context") + } + + if target == nil { + return fmt.Errorf("no target in context") + } + + damage := ctx.GetParameterFloat("damage", 0) + damageType := ctx.GetParameterInt("damage_type", 0) + hitType := ctx.GetParameterInt("hit_type", 0) + spellID := ctx.GetParameterInt("spell_id", 0) + + if damage <= 0 { + return fmt.Errorf("damage must be positive") + } + + // TODO: Implement extended spell damage system + ctx.Debug("Extended spell damage %f (type %d, hit %d, spell %d) from %s to %s (not yet implemented)", + damage, damageType, hitType, spellID, caster.GetName(), target.GetName()) + + ctx.SetResult("damage_dealt", damage) + return nil +} + +// DamageSpawn deals direct damage to a spawn +func DamageSpawn(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + damage := ctx.GetParameterFloat("damage", 0) + if damage <= 0 { + return fmt.Errorf("damage must be positive") + } + + // Apply damage to HP + currentHP := float64(spawn.GetHP()) + newHP := currentHP - damage + + if newHP < 0 { + newHP = 0 + } + + spawn.SetHP(int32(newHP)) + ctx.SetResult("damage_dealt", damage) + ctx.Debug("Dealt %f damage to spawn %s (new HP: %f)", damage, spawn.GetName(), newHP) + + // Update alive state if necessary + if newHP <= 0 && spawn.IsAlive() { + spawn.SetAlive(false) + ctx.Debug("Spawn %s died from damage", spawn.GetName()) + } + + return nil +} + +// ProcDamage handles proc-based damage +func ProcDamage(ctx *events.EventContext) error { + caster := ctx.GetCaster() + target := ctx.GetTarget() + + if caster == nil { + return fmt.Errorf("no caster in context") + } + + if target == nil { + return fmt.Errorf("no target in context") + } + + damage := ctx.GetParameterFloat("damage", 0) + damageType := ctx.GetParameterInt("damage_type", 0) + + if damage <= 0 { + return fmt.Errorf("damage must be positive") + } + + // TODO: Implement proc damage system + ctx.Debug("Proc damage %f (type %d) from %s to %s (not yet implemented)", + damage, damageType, caster.GetName(), target.GetName()) + + ctx.SetResult("damage_dealt", damage) + return nil +} + +// ProcHate handles proc-based hate generation +func ProcHate(ctx *events.EventContext) error { + caster := ctx.GetCaster() + target := ctx.GetTarget() + + if caster == nil { + return fmt.Errorf("no caster in context") + } + + if target == nil { + return fmt.Errorf("no target in context") + } + + hateAmount := ctx.GetParameterFloat("hate", 0) + + // TODO: Implement proc hate system + ctx.Debug("Proc hate %f from %s to %s (not yet implemented)", hateAmount, caster.GetName(), target.GetName()) + return nil +} + +// Knockback applies knockback effect to target +func Knockback(ctx *events.EventContext) error { + caster := ctx.GetCaster() + target := ctx.GetTarget() + + if caster == nil { + return fmt.Errorf("no caster in context") + } + + if target == nil { + return fmt.Errorf("no target in context") + } + + distance := ctx.GetParameterFloat("distance", 5.0) + verticalLift := ctx.GetParameterFloat("vertical", 0.0) + + // TODO: Implement knockback system + ctx.Debug("Knockback target %s distance %f with vertical %f from %s (not yet implemented)", + target.GetName(), distance, verticalLift, caster.GetName()) + return nil +} + +// Interrupt interrupts the target's spell casting +func Interrupt(ctx *events.EventContext) error { + target := ctx.GetTarget() + + if target == nil { + return fmt.Errorf("no target in context") + } + + // TODO: Implement interrupt system + ctx.Debug("Interrupted spell casting for %s (not yet implemented)", target.GetName()) + return nil +} + +// IsCasting checks if the spawn is currently casting +func IsCasting(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement casting state tracking + ctx.SetResult("is_casting", false) + return nil +} + +// HasRecovered checks if the spawn has recovered from an action +func HasRecovered(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement recovery tracking + ctx.SetResult("has_recovered", true) + return nil +} + +// ProcessMelee processes melee combat +func ProcessMelee(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement melee processing + ctx.Debug("Processing melee for spawn %s (not yet implemented)", spawn.GetName()) + return nil +} + +// ProcessSpell processes spell casting +func ProcessSpell(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement spell processing + ctx.Debug("Processing spells for spawn %s (not yet implemented)", spawn.GetName()) + return nil +} + +// LastSpellAttackHit checks if last spell attack hit +func LastSpellAttackHit(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement spell hit tracking + ctx.SetResult("last_spell_hit", false) + return nil +} + +// IsBehind checks if spawn is behind target +func IsBehind(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + target := ctx.GetTarget() + + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + if target == nil { + return fmt.Errorf("no target in context") + } + + // TODO: Implement position-based behind check + ctx.SetResult("is_behind", false) + return nil +} + +// IsFlanking checks if spawn is flanking target +func IsFlanking(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + target := ctx.GetTarget() + + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + if target == nil { + return fmt.Errorf("no target in context") + } + + // TODO: Implement position-based flanking check + ctx.SetResult("is_flanking", false) + return nil +} + +// InFront checks if spawn is in front of target +func InFront(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + target := ctx.GetTarget() + + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + if target == nil { + return fmt.Errorf("no target in context") + } + + // TODO: Implement position-based front check + ctx.SetResult("in_front", false) + return nil +} + +// GetEncounterSize gets the size of the current encounter +func GetEncounterSize(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement encounter tracking + ctx.SetResult("encounter_size", 0) + return nil +} + +// GetEncounter gets the current encounter list +func GetEncounter(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement encounter retrieval + ctx.SetResult("encounter", []interface{}{}) // Empty list for now + return nil +} + +// GetHateList gets the spawn's hate list +func GetHateList(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement hate list retrieval + ctx.SetResult("hate_list", []interface{}{}) // Empty list for now + return nil +} + +// ClearEncounter clears the current encounter +func ClearEncounter(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement encounter clearing + ctx.Debug("Cleared encounter for spawn %s (not yet implemented)", spawn.GetName()) + return nil +} + +// ClearRunback clears runback behavior +func ClearRunback(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement runback clearing + ctx.Debug("Cleared runback for spawn %s (not yet implemented)", spawn.GetName()) + return nil +} + +// Runback initiates runback behavior +func Runback(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement runback behavior + ctx.Debug("Initiated runback for spawn %s (not yet implemented)", spawn.GetName()) + return nil +} + +// GetRunbackDistance gets the runback distance +func GetRunbackDistance(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement runback distance calculation + ctx.SetResult("runback_distance", 50.0) // Default runback distance + return nil +} + +// CompareSpawns compares two spawns (for AI decision making) +func CompareSpawns(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + target := ctx.GetTarget() + + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + if target == nil { + return fmt.Errorf("no target in context") + } + + // TODO: Implement spawn comparison logic + ctx.SetResult("comparison_result", 0) // Equal + return nil +} + +// KillSpawn instantly kills a spawn +func KillSpawn(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + spawn.SetHP(0) + spawn.SetAlive(false) + ctx.Debug("Killed spawn %s", spawn.GetName()) + return nil +} + +// KillSpawnByDistance kills spawns within a distance +func KillSpawnByDistance(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + distance := ctx.GetParameterFloat("distance", 10.0) + + // TODO: Implement distance-based killing + ctx.Debug("Killed spawns within distance %f of %s (not yet implemented)", distance, spawn.GetName()) + return nil +} + +// Resurrect resurrects a dead spawn +func Resurrect(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + hpPercent := ctx.GetParameterFloat("hp_percent", 100.0) + powerPercent := ctx.GetParameterFloat("power_percent", 100.0) + + // Restore HP and power + maxHP := float64(spawn.GetTotalHP()) + maxPower := float64(spawn.GetTotalPower()) + + newHP := maxHP * (hpPercent / 100.0) + newPower := maxPower * (powerPercent / 100.0) + + spawn.SetHP(int32(newHP)) + spawn.SetPower(int32(newPower)) + spawn.SetAlive(true) + + ctx.Debug("Resurrected spawn %s with %.1f%% HP and %.1f%% power", + spawn.GetName(), hpPercent, powerPercent) + return nil +} + +// IsInvulnerable checks if spawn is invulnerable +func IsInvulnerable(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement invulnerability system + ctx.SetResult("is_invulnerable", false) + return nil +} + +// SetInvulnerable sets spawn's invulnerability +func SetInvulnerable(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + invulnerable := ctx.GetParameterBool("invulnerable", false) + + // TODO: Implement invulnerability system + ctx.Debug("Set invulnerable to %t for spawn %s (not yet implemented)", invulnerable, spawn.GetName()) + return nil +} + +// SetAttackable sets whether the spawn can be attacked +func SetAttackable(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + attackable := ctx.GetParameterBool("attackable", true) + + // TODO: Implement attackable flag + ctx.Debug("Set attackable to %t for spawn %s (not yet implemented)", attackable, spawn.GetName()) + return nil +} \ No newline at end of file diff --git a/internal/events/functions/functions_test.go b/internal/events/functions/functions_test.go new file mode 100644 index 0000000..20d8709 --- /dev/null +++ b/internal/events/functions/functions_test.go @@ -0,0 +1,254 @@ +package functions + +import ( + "testing" + + "eq2emu/internal/events" + "eq2emu/internal/spawn" +) + +func TestAllEQ2Functions(t *testing.T) { + // Create event handler + handler := events.NewEventHandler() + + // Register all EQ2 functions + err := RegisterAllEQ2Functions(handler) + if err != nil { + t.Fatalf("Failed to register all EQ2 functions: %v", err) + } + + // Verify we have a substantial number of functions registered + events := handler.ListEvents() + if len(events) < 100 { + t.Errorf("Expected at least 100 functions, got %d", len(events)) + } + + // Test some key functions exist + requiredFunctions := []string{ + "SetCurrentHP", "GetCurrentHP", "SetMaxHP", "GetMaxHP", + "SetLevel", "GetLevel", "SetClass", "GetClass", + "SetPosition", "GetPosition", "GetX", "GetY", "GetZ", + "Attack", "AddHate", "SpellDamage", "IsInCombat", + "GetName", "GetID", "IsPlayer", "IsNPC", + "MakeRandomInt", "ParseInt", "LogMessage", + } + + for _, funcName := range requiredFunctions { + if !handler.HasEvent(funcName) { + t.Errorf("Required function %s not registered", funcName) + } + } +} + +func TestHealthFunctions(t *testing.T) { + handler := events.NewEventHandler() + err := RegisterAllEQ2Functions(handler) + if err != nil { + t.Fatalf("Failed to register functions: %v", err) + } + + // Create test spawn + testSpawn := &spawn.Spawn{} + + // Test SetCurrentHP + ctx := events.NewEventContext(events.EventTypeSpawn, "SetCurrentHP", "test"). + WithSpawn(testSpawn). + WithParameter("hp", 250.0) + + err = handler.Execute(ctx) + if err != nil { + t.Fatalf("SetCurrentHP failed: %v", err) + } + + // Test GetCurrentHP + ctx2 := events.NewEventContext(events.EventTypeSpawn, "GetCurrentHP", "test"). + WithSpawn(testSpawn) + + err = handler.Execute(ctx2) + if err != nil { + t.Fatalf("GetCurrentHP failed: %v", err) + } + + // Verify result + if hp, exists := ctx2.GetResult("hp"); !exists { + t.Error("GetCurrentHP should return hp result") + } else if hp != int32(250) { + t.Errorf("Expected HP 250, got %v", hp) + } +} + +func TestAttributeFunctions(t *testing.T) { + handler := events.NewEventHandler() + err := RegisterAllEQ2Functions(handler) + if err != nil { + t.Fatalf("Failed to register functions: %v", err) + } + + // Create test spawn + testSpawn := &spawn.Spawn{} + + // Test SetLevel + ctx := events.NewEventContext(events.EventTypeSpawn, "SetLevel", "test"). + WithSpawn(testSpawn). + WithParameter("level", 50) + + err = handler.Execute(ctx) + if err != nil { + t.Fatalf("SetLevel failed: %v", err) + } + + // Test GetLevel + ctx2 := events.NewEventContext(events.EventTypeSpawn, "GetLevel", "test"). + WithSpawn(testSpawn) + + err = handler.Execute(ctx2) + if err != nil { + t.Fatalf("GetLevel failed: %v", err) + } + + // Verify result + if level, exists := ctx2.GetResult("level"); !exists { + t.Error("GetLevel should return level result") + } else if level != int16(50) { + t.Errorf("Expected level 50, got %v", level) + } +} + +func TestMovementFunctions(t *testing.T) { + handler := events.NewEventHandler() + err := RegisterAllEQ2Functions(handler) + if err != nil { + t.Fatalf("Failed to register functions: %v", err) + } + + // Create test spawn + testSpawn := &spawn.Spawn{} + + // Test SetPosition + ctx := events.NewEventContext(events.EventTypeSpawn, "SetPosition", "test"). + WithSpawn(testSpawn). + WithParameter("x", 100.0). + WithParameter("y", 200.0). + WithParameter("z", 300.0). + WithParameter("heading", 180.0) + + err = handler.Execute(ctx) + if err != nil { + t.Fatalf("SetPosition failed: %v", err) + } + + // Test GetPosition + ctx2 := events.NewEventContext(events.EventTypeSpawn, "GetPosition", "test"). + WithSpawn(testSpawn) + + err = handler.Execute(ctx2) + if err != nil { + t.Fatalf("GetPosition failed: %v", err) + } + + // Verify results + if x, exists := ctx2.GetResult("x"); !exists || x != float32(100.0) { + t.Errorf("Expected X=100.0, got %v (exists: %t)", x, exists) + } + if y, exists := ctx2.GetResult("y"); !exists || y != float32(200.0) { + t.Errorf("Expected Y=200.0, got %v (exists: %t)", y, exists) + } + if z, exists := ctx2.GetResult("z"); !exists || z != float32(300.0) { + t.Errorf("Expected Z=300.0, got %v (exists: %t)", z, exists) + } +} + +func TestMiscFunctions(t *testing.T) { + handler := events.NewEventHandler() + err := RegisterAllEQ2Functions(handler) + if err != nil { + t.Fatalf("Failed to register functions: %v", err) + } + + // Create test spawn + testSpawn := &spawn.Spawn{} + + // Test GetName + ctx := events.NewEventContext(events.EventTypeSpawn, "GetName", "test"). + WithSpawn(testSpawn) + + err = handler.Execute(ctx) + if err != nil { + t.Fatalf("GetName failed: %v", err) + } + + // Test MakeRandomInt + ctx2 := events.NewEventContext(events.EventTypeSpawn, "MakeRandomInt", "test"). + WithParameter("min", 10). + WithParameter("max", 20) + + err = handler.Execute(ctx2) + if err != nil { + t.Fatalf("MakeRandomInt failed: %v", err) + } + + if result, exists := ctx2.GetResult("random_int"); !exists { + t.Error("MakeRandomInt should return random_int result") + } else if randInt, ok := result.(int); !ok || randInt < 10 || randInt > 20 { + t.Errorf("Expected random int between 10-20, got %v", result) + } +} + +func TestFunctionsByDomain(t *testing.T) { + domains := GetFunctionsByDomain() + + // Verify we have expected domains + expectedDomains := []string{"health", "attributes", "movement", "combat", "misc"} + for _, domain := range expectedDomains { + if functions, exists := domains[domain]; !exists { + t.Errorf("Domain %s not found", domain) + } else if len(functions) == 0 { + t.Errorf("Domain %s has no functions", domain) + } + } + + // Verify health domain has expected functions + healthFunctions := domains["health"] + expectedHealthFunctions := []string{"SetCurrentHP", "GetCurrentHP", "SetMaxHP", "GetMaxHP"} + for _, funcName := range expectedHealthFunctions { + found := false + for _, f := range healthFunctions { + if f == funcName { + found = true + break + } + } + if !found { + t.Errorf("Health domain missing function %s", funcName) + } + } +} + +func TestErrorHandling(t *testing.T) { + handler := events.NewEventHandler() + err := RegisterAllEQ2Functions(handler) + if err != nil { + t.Fatalf("Failed to register functions: %v", err) + } + + // Test function with no spawn context + ctx := events.NewEventContext(events.EventTypeSpawn, "SetCurrentHP", "test"). + WithParameter("hp", 100.0) + // No spawn set + + err = handler.Execute(ctx) + if err == nil { + t.Error("SetCurrentHP should fail without spawn context") + } + + // Test function with invalid parameters + testSpawn := &spawn.Spawn{} + ctx2 := events.NewEventContext(events.EventTypeSpawn, "SetCurrentHP", "test"). + WithSpawn(testSpawn). + WithParameter("hp", -50.0) // Negative HP + + err = handler.Execute(ctx2) + if err == nil { + t.Error("SetCurrentHP should fail with negative HP") + } +} \ No newline at end of file diff --git a/internal/events/functions/health.go b/internal/events/functions/health.go new file mode 100644 index 0000000..984df80 --- /dev/null +++ b/internal/events/functions/health.go @@ -0,0 +1,365 @@ +package functions + +import ( + "fmt" + + "eq2emu/internal/events" +) + +// Health and Power Management Functions + +// SetCurrentHP sets the spawn's current HP +func SetCurrentHP(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + hp := ctx.GetParameterFloat("hp", 0) + if hp < 0 { + return fmt.Errorf("HP cannot be negative") + } + + spawn.SetHP(int32(hp)) + ctx.Debug("Set HP to %f for spawn %s", hp, spawn.GetName()) + return nil +} + +// SetMaxHP sets the spawn's maximum HP +func SetMaxHP(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + maxHP := ctx.GetParameterFloat("max_hp", 0) + if maxHP < 0 { + return fmt.Errorf("Max HP cannot be negative") + } + + spawn.SetTotalHP(int32(maxHP)) + ctx.Debug("Set Max HP to %f for spawn %s", maxHP, spawn.GetName()) + return nil +} + +// SetMaxHPBase sets the spawn's base maximum HP (before bonuses) +func SetMaxHPBase(ctx *events.EventContext) error { + // TODO: Implement base HP system when available + return SetMaxHP(ctx) // Fallback to regular max HP for now +} + +// SetCurrentPower sets the spawn's current power +func SetCurrentPower(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + power := ctx.GetParameterFloat("power", 0) + if power < 0 { + return fmt.Errorf("power cannot be negative") + } + + spawn.SetPower(int32(power)) + ctx.Debug("Set Power to %f for spawn %s", power, spawn.GetName()) + return nil +} + +// SetMaxPower sets the spawn's maximum power +func SetMaxPower(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + maxPower := ctx.GetParameterFloat("max_power", 0) + if maxPower < 0 { + return fmt.Errorf("Max power cannot be negative") + } + + spawn.SetTotalPower(int32(maxPower)) + ctx.Debug("Set Max Power to %f for spawn %s", maxPower, spawn.GetName()) + return nil +} + +// SetMaxPowerBase sets the spawn's base maximum power (before bonuses) +func SetMaxPowerBase(ctx *events.EventContext) error { + // TODO: Implement base power system when available + return SetMaxPower(ctx) // Fallback to regular max power for now +} + +// ModifyMaxHP modifies the spawn's maximum HP by a relative amount +func ModifyMaxHP(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + modifier := ctx.GetParameterFloat("modifier", 0) + currentMax := float64(spawn.GetTotalHP()) + newMax := currentMax + modifier + + if newMax < 0 { + newMax = 0 + } + + spawn.SetTotalHP(int32(newMax)) + ctx.Debug("Modified Max HP by %f (new value: %f) for spawn %s", modifier, newMax, spawn.GetName()) + return nil +} + +// ModifyMaxPower modifies the spawn's maximum power by a relative amount +func ModifyMaxPower(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + modifier := ctx.GetParameterFloat("modifier", 0) + currentMax := float64(spawn.GetTotalPower()) + newMax := currentMax + modifier + + if newMax < 0 { + newMax = 0 + } + + spawn.SetTotalPower(int32(newMax)) + ctx.Debug("Modified Max Power by %f (new value: %f) for spawn %s", modifier, newMax, spawn.GetName()) + return nil +} + +// ModifyPower modifies the spawn's current power by a relative amount +func ModifyPower(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + modifier := ctx.GetParameterFloat("modifier", 0) + current := float64(spawn.GetPower()) + newPower := current + modifier + + // Clamp between 0 and max power + maxPower := float64(spawn.GetTotalPower()) + if newPower < 0 { + newPower = 0 + } else if newPower > maxPower { + newPower = maxPower + } + + spawn.SetPower(int32(newPower)) + ctx.Debug("Modified Power by %f (new value: %f) for spawn %s", modifier, newPower, spawn.GetName()) + return nil +} + +// ModifyHP modifies the spawn's current HP by a relative amount +func ModifyHP(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + modifier := ctx.GetParameterFloat("modifier", 0) + current := float64(spawn.GetHP()) + newHP := current + modifier + + // Clamp between 0 and max HP + maxHP := float64(spawn.GetTotalHP()) + if newHP < 0 { + newHP = 0 + } else if newHP > maxHP { + newHP = maxHP + } + + spawn.SetHP(int32(newHP)) + ctx.Debug("Modified HP by %f (new value: %f) for spawn %s", modifier, newHP, spawn.GetName()) + + // Update alive state based on HP + if newHP <= 0 { + spawn.SetAlive(false) + ctx.Debug("Spawn %s is now dead", spawn.GetName()) + } else if !spawn.IsAlive() { + spawn.SetAlive(true) + ctx.Debug("Spawn %s is now alive", spawn.GetName()) + } + + return nil +} + +// ModifyTotalHP modifies the spawn's total/max HP by a relative amount +func ModifyTotalHP(ctx *events.EventContext) error { + return ModifyMaxHP(ctx) // Alias for ModifyMaxHP +} + +// ModifyTotalPower modifies the spawn's total/max power by a relative amount +func ModifyTotalPower(ctx *events.EventContext) error { + return ModifyMaxPower(ctx) // Alias for ModifyMaxPower +} + +// GetCurrentHP gets the spawn's current HP +func GetCurrentHP(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + hp := spawn.GetHP() + ctx.SetResult("hp", hp) + return nil +} + +// GetMaxHP gets the spawn's maximum HP +func GetMaxHP(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + maxHP := spawn.GetTotalHP() + ctx.SetResult("max_hp", maxHP) + return nil +} + +// GetMaxHPBase gets the spawn's base maximum HP (before bonuses) +func GetMaxHPBase(ctx *events.EventContext) error { + // TODO: Implement base HP system when available + return GetMaxHP(ctx) // Fallback to regular max HP for now +} + +// GetCurrentPower gets the spawn's current power +func GetCurrentPower(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + power := spawn.GetPower() + ctx.SetResult("power", power) + return nil +} + +// GetMaxPower gets the spawn's maximum power +func GetMaxPower(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + maxPower := spawn.GetTotalPower() + ctx.SetResult("max_power", maxPower) + return nil +} + +// GetMaxPowerBase gets the spawn's base maximum power (before bonuses) +func GetMaxPowerBase(ctx *events.EventContext) error { + // TODO: Implement base power system when available + return GetMaxPower(ctx) // Fallback to regular max power for now +} + +// GetPCTOfHP gets the percentage of current HP relative to max HP +func GetPCTOfHP(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + currentHP := float64(spawn.GetHP()) + maxHP := float64(spawn.GetTotalHP()) + + var percentage float64 + if maxHP > 0 { + percentage = (currentHP / maxHP) * 100 + } else { + percentage = 0 + } + + ctx.SetResult("hp_percentage", percentage) + return nil +} + +// GetPCTOfPower gets the percentage of current power relative to max power +func GetPCTOfPower(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + currentPower := float64(spawn.GetPower()) + maxPower := float64(spawn.GetTotalPower()) + + var percentage float64 + if maxPower > 0 { + percentage = (currentPower / maxPower) * 100 + } else { + percentage = 0 + } + + ctx.SetResult("power_percentage", percentage) + return nil +} + +// SpellHeal heals the spawn for a specific amount +func SpellHeal(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + amount := ctx.GetParameterFloat("amount", 0) + if amount <= 0 { + return fmt.Errorf("heal amount must be positive") + } + + current := float64(spawn.GetHP()) + maxHP := float64(spawn.GetTotalHP()) + newHP := current + amount + + // Cap at max HP + if newHP > maxHP { + newHP = maxHP + amount = maxHP - current // Adjust amount to actual healed + } + + spawn.SetHP(int32(newHP)) + ctx.SetResult("amount_healed", amount) + ctx.Debug("Healed spawn %s for %f (new HP: %f)", spawn.GetName(), amount, newHP) + + // Update alive state if necessary + if newHP > 0 && !spawn.IsAlive() { + spawn.SetAlive(true) + ctx.Debug("Spawn %s is now alive from healing", spawn.GetName()) + } + + return nil +} + +// SpellHealPct heals the spawn for a percentage of max HP +func SpellHealPct(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + percentage := ctx.GetParameterFloat("percentage", 0) + if percentage <= 0 { + return fmt.Errorf("heal percentage must be positive") + } + + maxHP := float64(spawn.GetTotalHP()) + healAmount := maxHP * (percentage / 100.0) + + // Set the heal amount and delegate to SpellHeal + ctx.WithParameter("amount", healAmount) + return SpellHeal(ctx) +} + +// IsAlive checks if the spawn is alive +func IsAlive(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + ctx.SetResult("is_alive", spawn.IsAlive()) + return nil +} \ No newline at end of file diff --git a/internal/events/functions/misc.go b/internal/events/functions/misc.go new file mode 100644 index 0000000..0c632b0 --- /dev/null +++ b/internal/events/functions/misc.go @@ -0,0 +1,492 @@ +package functions + +import ( + "fmt" + "math" + "strconv" + "strings" + + "eq2emu/internal/events" +) + +// Miscellaneous Utility Functions + +// SendMessage sends a message (placeholder implementation) +func SendMessage(ctx *events.EventContext) error { + message := ctx.GetParameterString("message", "") + if message == "" { + return fmt.Errorf("message parameter required") + } + + // TODO: Implement message sending + ctx.Info("Message: %s", message) + return nil +} + +// LogMessage logs a message at specified level +func LogMessage(ctx *events.EventContext) error { + level := ctx.GetParameterString("level", "info") + message := ctx.GetParameterString("message", "") + + if message == "" { + return fmt.Errorf("message parameter required") + } + + switch strings.ToLower(level) { + case "debug": + ctx.Debug("%s", message) + case "info": + ctx.Info("%s", message) + case "warn", "warning": + ctx.Warn("%s", message) + case "error": + ctx.Error("%s", message) + default: + ctx.Info("%s", message) + } + + return nil +} + +// MakeRandomInt generates a random integer +func MakeRandomInt(ctx *events.EventContext) error { + min := ctx.GetParameterInt("min", 0) + max := ctx.GetParameterInt("max", 100) + + if min > max { + min, max = max, min + } + + // Simple random - in practice you'd use a proper random generator + result := min + (int(math.Abs(float64(ctx.EventName[0]))) % (max - min + 1)) + ctx.SetResult("random_int", result) + return nil +} + +// MakeRandomFloat generates a random float +func MakeRandomFloat(ctx *events.EventContext) error { + min := ctx.GetParameterFloat("min", 0.0) + max := ctx.GetParameterFloat("max", 1.0) + + if min > max { + min, max = max, min + } + + // Simple random float - in practice you'd use a proper random generator + ratio := float64(int(math.Abs(float64(ctx.EventName[0]))) % 100) / 100.0 + result := min + (max-min)*ratio + ctx.SetResult("random_float", result) + return nil +} + +// ParseInt parses a string to integer +func ParseInt(ctx *events.EventContext) error { + str := ctx.GetParameterString("string", "") + if str == "" { + return fmt.Errorf("string parameter required") + } + + value, err := strconv.Atoi(str) + if err != nil { + return fmt.Errorf("failed to parse integer: %w", err) + } + + ctx.SetResult("int_value", value) + return nil +} + +// GetName gets the spawn's name +func GetName(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + ctx.SetResult("name", spawn.GetName()) + return nil +} + +// GetID gets the spawn's ID +func GetID(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + ctx.SetResult("id", spawn.GetID()) + return nil +} + +// GetSpawnID gets the spawn's ID (alias for GetID) +func GetSpawnID(ctx *events.EventContext) error { + return GetID(ctx) +} + +// IsPlayer checks if spawn is a player +func IsPlayer(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + ctx.SetResult("is_player", spawn.IsPlayer()) + return nil +} + +// IsNPC checks if spawn is an NPC +func IsNPC(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + ctx.SetResult("is_npc", spawn.IsNPC()) + return nil +} + +// IsEntity checks if spawn is an entity +func IsEntity(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement entity checking when Entity interface is available + ctx.SetResult("is_entity", false) // Default for now + return nil +} + +// IsDead checks if spawn is dead +func IsDead(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + ctx.SetResult("is_dead", !spawn.IsAlive()) + return nil +} + +// GetCharacterID gets the character ID for players +func GetCharacterID(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + if !spawn.IsPlayer() { + return fmt.Errorf("spawn is not a player") + } + + // TODO: Implement character ID retrieval + ctx.SetResult("character_id", 0) // Default value + return nil +} + +// Despawn removes the spawn from the world +func Despawn(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement despawn logic + ctx.Debug("Despawned spawn %s (not yet implemented)", spawn.GetName()) + return nil +} + +// Spawn creates a new spawn +func Spawn(ctx *events.EventContext) error { + locationID := ctx.GetParameterInt("location_id", 0) + spawnGroupID := ctx.GetParameterInt("spawn_group_id", 0) + + // TODO: Implement spawn creation + ctx.Debug("Created spawn at location %d, group %d (not yet implemented)", locationID, spawnGroupID) + ctx.SetResult("spawned", true) + return nil +} + +// SpawnByLocationID spawns by location ID +func SpawnByLocationID(ctx *events.EventContext) error { + locationID := ctx.GetParameterInt("location_id", 0) + if locationID <= 0 { + return fmt.Errorf("invalid location ID") + } + + // TODO: Implement location-based spawning + ctx.Debug("Spawned by location ID %d (not yet implemented)", locationID) + ctx.SetResult("spawned", true) + return nil +} + +// SpawnGroupByID spawns a group by ID +func SpawnGroupByID(ctx *events.EventContext) error { + groupID := ctx.GetParameterInt("group_id", 0) + if groupID <= 0 { + return fmt.Errorf("invalid group ID") + } + + // TODO: Implement group spawning + ctx.Debug("Spawned group ID %d (not yet implemented)", groupID) + ctx.SetResult("spawned", true) + return nil +} + +// DespawnByLocationID despawns spawns at a location +func DespawnByLocationID(ctx *events.EventContext) error { + locationID := ctx.GetParameterInt("location_id", 0) + if locationID <= 0 { + return fmt.Errorf("invalid location ID") + } + + // TODO: Implement location-based despawning + ctx.Debug("Despawned location ID %d (not yet implemented)", locationID) + return nil +} + +// GetSpawnByLocationID gets spawn by location ID +func GetSpawnByLocationID(ctx *events.EventContext) error { + locationID := ctx.GetParameterInt("location_id", 0) + if locationID <= 0 { + return fmt.Errorf("invalid location ID") + } + + // TODO: Implement spawn retrieval by location + ctx.SetResult("spawn", nil) // No spawn found + return nil +} + +// GetSpawnByGroupID gets spawn by group ID +func GetSpawnByGroupID(ctx *events.EventContext) error { + groupID := ctx.GetParameterInt("group_id", 0) + if groupID <= 0 { + return fmt.Errorf("invalid group ID") + } + + // TODO: Implement spawn retrieval by group + ctx.SetResult("spawn", nil) // No spawn found + return nil +} + +// GetSpawnGroupID gets the spawn's group ID +func GetSpawnGroupID(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement group ID retrieval + ctx.SetResult("group_id", 0) // Default value + return nil +} + +// SetSpawnGroupID sets the spawn's group ID +func SetSpawnGroupID(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + groupID := ctx.GetParameterInt("group_id", 0) + + // TODO: Implement group ID setting + ctx.Debug("Set group ID to %d for spawn %s (not yet implemented)", groupID, spawn.GetName()) + return nil +} + +// GetSpawnLocationID gets the spawn's location ID +func GetSpawnLocationID(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement location ID retrieval + ctx.SetResult("location_id", 0) // Default value + return nil +} + +// GetSpawnLocationPlacementID gets the spawn's location placement ID +func GetSpawnLocationPlacementID(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement location placement ID retrieval + ctx.SetResult("placement_id", 0) // Default value + return nil +} + +// SetGridID sets the spawn's grid ID +func SetGridID(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + gridID := ctx.GetParameterInt("grid_id", 0) + + // TODO: Implement grid ID setting + ctx.Debug("Set grid ID to %d for spawn %s (not yet implemented)", gridID, spawn.GetName()) + return nil +} + +// SpawnSet sets spawn properties by distance/criteria +func SpawnSet(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + property := ctx.GetParameterString("property", "") + value := ctx.GetParameterString("value", "") + + // TODO: Implement spawn property setting + ctx.Debug("Set property %s to %s for spawn %s (not yet implemented)", property, value, spawn.GetName()) + return nil +} + +// SpawnSetByDistance sets spawn properties within distance +func SpawnSetByDistance(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + distance := ctx.GetParameterFloat("distance", 10.0) + property := ctx.GetParameterString("property", "") + value := ctx.GetParameterString("value", "") + + // TODO: Implement distance-based spawn property setting + ctx.Debug("Set property %s to %s within distance %f of spawn %s (not yet implemented)", + property, value, distance, spawn.GetName()) + return nil +} + +// IsSpawnGroupAlive checks if spawn group is alive +func IsSpawnGroupAlive(ctx *events.EventContext) error { + groupID := ctx.GetParameterInt("group_id", 0) + if groupID <= 0 { + return fmt.Errorf("invalid group ID") + } + + // TODO: Implement group alive checking + ctx.SetResult("group_alive", false) // Default value + return nil +} + +// AddSpawnToGroup adds spawn to a group +func AddSpawnToGroup(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + groupID := ctx.GetParameterInt("group_id", 0) + if groupID <= 0 { + return fmt.Errorf("invalid group ID") + } + + // TODO: Implement group addition + ctx.Debug("Added spawn %s to group %d (not yet implemented)", spawn.GetName(), groupID) + return nil +} + +// GetVariableValue gets a variable value +func GetVariableValue(ctx *events.EventContext) error { + variableName := ctx.GetParameterString("variable", "") + if variableName == "" { + return fmt.Errorf("variable name required") + } + + // TODO: Implement variable system + ctx.SetResult("value", "") // Default empty value + return nil +} + +// SetServerVariable sets a server variable +func SetServerVariable(ctx *events.EventContext) error { + variableName := ctx.GetParameterString("variable", "") + value := ctx.GetParameterString("value", "") + + if variableName == "" { + return fmt.Errorf("variable name required") + } + + // TODO: Implement server variable system + ctx.Debug("Set server variable %s to %s (not yet implemented)", variableName, value) + return nil +} + +// GetServerVariable gets a server variable +func GetServerVariable(ctx *events.EventContext) error { + variableName := ctx.GetParameterString("variable", "") + if variableName == "" { + return fmt.Errorf("variable name required") + } + + // TODO: Implement server variable system + ctx.SetResult("value", "") // Default empty value + return nil +} + +// SetTempVariable sets a temporary variable +func SetTempVariable(ctx *events.EventContext) error { + variableName := ctx.GetParameterString("variable", "") + value := ctx.GetParameterString("value", "") + + if variableName == "" { + return fmt.Errorf("variable name required") + } + + // TODO: Implement temporary variable system + ctx.Debug("Set temp variable %s to %s (not yet implemented)", variableName, value) + return nil +} + +// GetTempVariable gets a temporary variable +func GetTempVariable(ctx *events.EventContext) error { + variableName := ctx.GetParameterString("variable", "") + if variableName == "" { + return fmt.Errorf("variable name required") + } + + // TODO: Implement temporary variable system + ctx.SetResult("value", "") // Default empty value + return nil +} + +// CheckLOS checks line of sight between two positions +func CheckLOS(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + target := ctx.GetTarget() + + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + if target == nil { + return fmt.Errorf("no target in context") + } + + // TODO: Implement line of sight checking + ctx.SetResult("has_los", true) // Default to true + return nil +} + +// CheckLOSByCoordinates checks line of sight between coordinates +func CheckLOSByCoordinates(ctx *events.EventContext) error { + x1 := ctx.GetParameterFloat("x1", 0) + y1 := ctx.GetParameterFloat("y1", 0) + z1 := ctx.GetParameterFloat("z1", 0) + x2 := ctx.GetParameterFloat("x2", 0) + y2 := ctx.GetParameterFloat("y2", 0) + z2 := ctx.GetParameterFloat("z2", 0) + + // TODO: Implement coordinate-based line of sight checking + ctx.Debug("Checking LOS from (%.2f,%.2f,%.2f) to (%.2f,%.2f,%.2f) (not yet implemented)", + x1, y1, z1, x2, y2, z2) + ctx.SetResult("has_los", true) // Default to true + return nil +} \ No newline at end of file diff --git a/internal/events/functions/movement.go b/internal/events/functions/movement.go new file mode 100644 index 0000000..869419e --- /dev/null +++ b/internal/events/functions/movement.go @@ -0,0 +1,534 @@ +package functions + +import ( + "fmt" + + "eq2emu/internal/events" +) + +// Movement and Position Functions + +// SetPosition sets the spawn's position and heading +func SetPosition(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + x := ctx.GetParameterFloat("x", 0) + y := ctx.GetParameterFloat("y", 0) + z := ctx.GetParameterFloat("z", 0) + heading := ctx.GetParameterFloat("heading", float64(spawn.GetHeading())) + + spawn.SetX(float32(x)) + spawn.SetY(float32(y), false) + spawn.SetZ(float32(z)) + spawn.SetHeadingFromFloat(float32(heading)) + + ctx.Debug("Set position to (%.2f, %.2f, %.2f, %.2f) for spawn %s", + x, y, z, heading, spawn.GetName()) + return nil +} + +// GetPosition gets the spawn's position and heading +func GetPosition(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + ctx.SetResult("x", spawn.GetX()) + ctx.SetResult("y", spawn.GetY()) + ctx.SetResult("z", spawn.GetZ()) + ctx.SetResult("heading", spawn.GetHeading()) + return nil +} + +// GetX gets the spawn's X coordinate +func GetX(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + ctx.SetResult("x", spawn.GetX()) + return nil +} + +// GetY gets the spawn's Y coordinate +func GetY(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + ctx.SetResult("y", spawn.GetY()) + return nil +} + +// GetZ gets the spawn's Z coordinate +func GetZ(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + ctx.SetResult("z", spawn.GetZ()) + return nil +} + +// GetHeading gets the spawn's heading +func GetHeading(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + ctx.SetResult("heading", spawn.GetHeading()) + return nil +} + +// SetHeading sets the spawn's heading +func SetHeading(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + heading := ctx.GetParameterFloat("heading", 0) + spawn.SetHeadingFromFloat(float32(heading)) + ctx.Debug("Set heading to %.2f for spawn %s", heading, spawn.GetName()) + return nil +} + +// GetOrigX gets the spawn's original X coordinate +func GetOrigX(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement original position tracking + ctx.SetResult("orig_x", spawn.GetX()) // Fallback to current position + return nil +} + +// GetOrigY gets the spawn's original Y coordinate +func GetOrigY(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement original position tracking + ctx.SetResult("orig_y", spawn.GetY()) // Fallback to current position + return nil +} + +// GetOrigZ gets the spawn's original Z coordinate +func GetOrigZ(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement original position tracking + ctx.SetResult("orig_z", spawn.GetZ()) // Fallback to current position + return nil +} + +// GetDistance gets the distance between spawn and target +func GetDistance(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + target := ctx.GetTarget() + + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + if target == nil { + return fmt.Errorf("no target in context") + } + + // TODO: Implement proper distance calculation + // For now, return a placeholder distance + distance := 10.0 + ctx.SetResult("distance", distance) + return nil +} + +// FaceTarget makes the spawn face the target +func FaceTarget(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + target := ctx.GetTarget() + + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + if target == nil { + return fmt.Errorf("no target in context") + } + + // TODO: Implement face target calculation + ctx.Debug("Spawn %s facing target %s (not yet implemented)", spawn.GetName(), target.GetName()) + return nil +} + +// GetSpeed gets the spawn's movement speed +func GetSpeed(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement speed tracking + ctx.SetResult("speed", 5.0) // Default walking speed + return nil +} + +// SetSpeed sets the spawn's movement speed +func SetSpeed(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + speed := ctx.GetParameterFloat("speed", 5.0) + if speed < 0 { + speed = 0 + } + + // TODO: Implement speed setting + ctx.Debug("Set speed to %.2f for spawn %s (not yet implemented)", speed, spawn.GetName()) + return nil +} + +// SetSpeedMultiplier sets a speed multiplier for the spawn +func SetSpeedMultiplier(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + multiplier := ctx.GetParameterFloat("multiplier", 1.0) + if multiplier < 0 { + multiplier = 0 + } + + // TODO: Implement speed multiplier + ctx.Debug("Set speed multiplier to %.2f for spawn %s (not yet implemented)", multiplier, spawn.GetName()) + return nil +} + +// HasMoved checks if the spawn has moved recently +func HasMoved(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement movement tracking + ctx.SetResult("has_moved", false) // Default value + return nil +} + +// IsRunning checks if the spawn is currently running +func IsRunning(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement running state tracking + ctx.SetResult("is_running", false) // Default value + return nil +} + +// MoveToLocation moves the spawn to a specific location +func MoveToLocation(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + x := ctx.GetParameterFloat("x", 0) + y := ctx.GetParameterFloat("y", 0) + z := ctx.GetParameterFloat("z", 0) + runningSpeed := ctx.GetParameterFloat("running_speed", 7.0) + + // TODO: Implement actual movement/pathfinding + // For now, just teleport to the location + spawn.SetX(float32(x)) + spawn.SetY(float32(y), false) + spawn.SetZ(float32(z)) + + ctx.Debug("Moved spawn %s to location (%.2f, %.2f, %.2f) at speed %.2f", + spawn.GetName(), x, y, z, runningSpeed) + return nil +} + +// ClearRunningLocations clears any queued movement locations +func ClearRunningLocations(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement movement queue clearing + ctx.Debug("Cleared running locations for spawn %s (not yet implemented)", spawn.GetName()) + return nil +} + +// SpawnMove initiates spawn movement +func SpawnMove(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + x := ctx.GetParameterFloat("x", 0) + y := ctx.GetParameterFloat("y", 0) + z := ctx.GetParameterFloat("z", 0) + delay := ctx.GetParameterInt("delay", 0) + + // TODO: Implement spawn movement with delay + ctx.Debug("Spawn %s moving to (%.2f, %.2f, %.2f) with delay %d (not yet implemented)", + spawn.GetName(), x, y, z, delay) + return nil +} + +// MovementLoopAdd adds a movement loop point +func MovementLoopAdd(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + x := ctx.GetParameterFloat("x", 0) + y := ctx.GetParameterFloat("y", 0) + z := ctx.GetParameterFloat("z", 0) + delay := ctx.GetParameterInt("delay", 0) + + // TODO: Implement movement loop system + ctx.Debug("Added movement loop point (%.2f, %.2f, %.2f) with delay %d for spawn %s (not yet implemented)", + x, y, z, delay, spawn.GetName()) + return nil +} + +// PauseMovement pauses the spawn's movement +func PauseMovement(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + duration := ctx.GetParameterInt("duration", 0) + + // TODO: Implement movement pausing + ctx.Debug("Paused movement for spawn %s for %d ms (not yet implemented)", spawn.GetName(), duration) + return nil +} + +// StopMovement stops the spawn's movement +func StopMovement(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement movement stopping + ctx.Debug("Stopped movement for spawn %s (not yet implemented)", spawn.GetName()) + return nil +} + +// SetMount sets the spawn's mount +func SetMount(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + mountID := ctx.GetParameterInt("mount_id", 0) + + // TODO: Implement mount system + ctx.Debug("Set mount %d for spawn %s (not yet implemented)", mountID, spawn.GetName()) + return nil +} + +// GetMount gets the spawn's current mount +func GetMount(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement mount system + ctx.SetResult("mount_id", 0) // No mount + return nil +} + +// SetMountColor sets the mount's color +func SetMountColor(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + colorR := ctx.GetParameterInt("red", 255) + colorG := ctx.GetParameterInt("green", 255) + colorB := ctx.GetParameterInt("blue", 255) + + // TODO: Implement mount color system + ctx.Debug("Set mount color to RGB(%d, %d, %d) for spawn %s (not yet implemented)", + colorR, colorG, colorB, spawn.GetName()) + return nil +} + +// StartAutoMount starts auto-mounting +func StartAutoMount(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + if !spawn.IsPlayer() { + return fmt.Errorf("spawn is not a player") + } + + // TODO: Implement auto-mount system + ctx.Debug("Started auto-mount for player %s (not yet implemented)", spawn.GetName()) + return nil +} + +// EndAutoMount ends auto-mounting +func EndAutoMount(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + if !spawn.IsPlayer() { + return fmt.Errorf("spawn is not a player") + } + + // TODO: Implement auto-mount system + ctx.Debug("Ended auto-mount for player %s (not yet implemented)", spawn.GetName()) + return nil +} + +// IsOnAutoMount checks if spawn is on auto-mount +func IsOnAutoMount(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + // TODO: Implement auto-mount system + ctx.SetResult("is_on_auto_mount", false) + return nil +} + +// AddWaypoint adds a waypoint for the player +func AddWaypoint(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + if !spawn.IsPlayer() { + return fmt.Errorf("spawn is not a player") + } + + x := ctx.GetParameterFloat("x", 0) + y := ctx.GetParameterFloat("y", 0) + z := ctx.GetParameterFloat("z", 0) + waypointName := ctx.GetParameterString("name", "Waypoint") + + // TODO: Implement waypoint system + ctx.Debug("Added waypoint '%s' at (%.2f, %.2f, %.2f) for player %s (not yet implemented)", + waypointName, x, y, z, spawn.GetName()) + return nil +} + +// RemoveWaypoint removes a waypoint for the player +func RemoveWaypoint(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + if !spawn.IsPlayer() { + return fmt.Errorf("spawn is not a player") + } + + waypointID := ctx.GetParameterInt("waypoint_id", 0) + + // TODO: Implement waypoint system + ctx.Debug("Removed waypoint %d for player %s (not yet implemented)", waypointID, spawn.GetName()) + return nil +} + +// SendWaypoints sends waypoints to the player +func SendWaypoints(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + if !spawn.IsPlayer() { + return fmt.Errorf("spawn is not a player") + } + + // TODO: Implement waypoint system + ctx.Debug("Sent waypoints to player %s (not yet implemented)", spawn.GetName()) + return nil +} + +// Evac evacuates the player to safety +func Evac(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + if !spawn.IsPlayer() { + return fmt.Errorf("spawn is not a player") + } + + // TODO: Implement evacuation to safe location + ctx.Debug("Evacuated player %s to safety (not yet implemented)", spawn.GetName()) + return nil +} + +// Bind binds the player to current location +func Bind(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + if !spawn.IsPlayer() { + return fmt.Errorf("spawn is not a player") + } + + // TODO: Implement bind system + ctx.Debug("Bound player %s to current location (not yet implemented)", spawn.GetName()) + return nil +} + +// Gate gates the player to bind location +func Gate(ctx *events.EventContext) error { + spawn := ctx.GetSpawn() + if spawn == nil { + return fmt.Errorf("no spawn in context") + } + + if !spawn.IsPlayer() { + return fmt.Errorf("spawn is not a player") + } + + // TODO: Implement gate system + ctx.Debug("Gated player %s to bind location (not yet implemented)", spawn.GetName()) + return nil +} \ No newline at end of file diff --git a/internal/events/functions/registry.go b/internal/events/functions/registry.go new file mode 100644 index 0000000..74aa5ce --- /dev/null +++ b/internal/events/functions/registry.go @@ -0,0 +1,255 @@ +package functions + +import ( + "fmt" + + "eq2emu/internal/events" +) + +// RegisterAllEQ2Functions registers all EQ2 event functions with a handler +func RegisterAllEQ2Functions(handler *events.EventHandler) error { + functions := map[string]events.EventFunction{ + // Health Functions + "SetCurrentHP": SetCurrentHP, + "SetMaxHP": SetMaxHP, + "SetMaxHPBase": SetMaxHPBase, + "SetCurrentPower": SetCurrentPower, + "SetMaxPower": SetMaxPower, + "SetMaxPowerBase": SetMaxPowerBase, + "ModifyMaxHP": ModifyMaxHP, + "ModifyMaxPower": ModifyMaxPower, + "ModifyPower": ModifyPower, + "ModifyHP": ModifyHP, + "ModifyTotalHP": ModifyTotalHP, + "ModifyTotalPower": ModifyTotalPower, + "GetCurrentHP": GetCurrentHP, + "GetMaxHP": GetMaxHP, + "GetMaxHPBase": GetMaxHPBase, + "GetCurrentPower": GetCurrentPower, + "GetMaxPower": GetMaxPower, + "GetMaxPowerBase": GetMaxPowerBase, + "GetPCTOfHP": GetPCTOfHP, + "GetPCTOfPower": GetPCTOfPower, + "SpellHeal": SpellHeal, + "SpellHealPct": SpellHealPct, + "IsAlive": IsAlive, + + // Attributes Functions + "SetInt": SetInt, + "SetWis": SetWis, + "SetSta": SetSta, + "SetStr": SetStr, + "SetAgi": SetAgi, + "SetIntBase": SetIntBase, + "SetWisBase": SetWisBase, + "SetStaBase": SetStaBase, + "SetStrBase": SetStrBase, + "SetAgiBase": SetAgiBase, + "GetInt": GetInt, + "GetWis": GetWis, + "GetSta": GetSta, + "GetStr": GetStr, + "GetAgi": GetAgi, + "GetIntBase": GetIntBase, + "GetWisBase": GetWisBase, + "GetStaBase": GetStaBase, + "GetStrBase": GetStrBase, + "GetAgiBase": GetAgiBase, + "GetLevel": GetLevel, + "SetLevel": SetLevel, + "SetPlayerLevel": SetPlayerLevel, + "GetDifficulty": GetDifficulty, + "AddSpellBonus": AddSpellBonus, + "RemoveSpellBonus": RemoveSpellBonus, + "AddSkillBonus": AddSkillBonus, + "RemoveSkillBonus": RemoveSkillBonus, + "GetClass": GetClass, + "SetClass": SetClass, + "SetAdventureClass": SetAdventureClass, + "GetTradeskillClass": GetTradeskillClass, + "SetTradeskillClass": SetTradeskillClass, + "GetTradeskillLevel": GetTradeskillLevel, + "SetTradeskillLevel": SetTradeskillLevel, + "GetRace": GetRace, + "GetGender": GetGender, + "GetModelType": GetModelType, + "SetModelType": SetModelType, + "GetDeity": GetDeity, + "SetDeity": SetDeity, + "GetAlignment": GetAlignment, + "SetAlignment": SetAlignment, + + // Movement Functions + "SetPosition": SetPosition, + "GetPosition": GetPosition, + "GetX": GetX, + "GetY": GetY, + "GetZ": GetZ, + "GetHeading": GetHeading, + "SetHeading": SetHeading, + "GetOrigX": GetOrigX, + "GetOrigY": GetOrigY, + "GetOrigZ": GetOrigZ, + "GetDistance": GetDistance, + "FaceTarget": FaceTarget, + "GetSpeed": GetSpeed, + "SetSpeed": SetSpeed, + "SetSpeedMultiplier": SetSpeedMultiplier, + "HasMoved": HasMoved, + "IsRunning": IsRunning, + "MoveToLocation": MoveToLocation, + "ClearRunningLocations": ClearRunningLocations, + "SpawnMove": SpawnMove, + "MovementLoopAdd": MovementLoopAdd, + "PauseMovement": PauseMovement, + "StopMovement": StopMovement, + "SetMount": SetMount, + "GetMount": GetMount, + "SetMountColor": SetMountColor, + "StartAutoMount": StartAutoMount, + "EndAutoMount": EndAutoMount, + "IsOnAutoMount": IsOnAutoMount, + "AddWaypoint": AddWaypoint, + "RemoveWaypoint": RemoveWaypoint, + "SendWaypoints": SendWaypoints, + "Evac": Evac, + "Bind": Bind, + "Gate": Gate, + + // Combat Functions + "Attack": Attack, + "AddHate": AddHate, + "ClearHate": ClearHate, + "GetMostHated": GetMostHated, + "SetTarget": SetTarget, + "GetTarget": GetTarget, + "IsInCombat": IsInCombat, + "SetInCombat": SetInCombat, + "SpellDamage": SpellDamage, + "SpellDamageExt": SpellDamageExt, + "DamageSpawn": DamageSpawn, + "ProcDamage": ProcDamage, + "ProcHate": ProcHate, + "Knockback": Knockback, + "Interrupt": Interrupt, + "IsCasting": IsCasting, + "HasRecovered": HasRecovered, + "ProcessMelee": ProcessMelee, + "ProcessSpell": ProcessSpell, + "LastSpellAttackHit": LastSpellAttackHit, + "IsBehind": IsBehind, + "IsFlanking": IsFlanking, + "InFront": InFront, + "GetEncounterSize": GetEncounterSize, + "GetEncounter": GetEncounter, + "GetHateList": GetHateList, + "ClearEncounter": ClearEncounter, + "ClearRunback": ClearRunback, + "Runback": Runback, + "GetRunbackDistance": GetRunbackDistance, + "CompareSpawns": CompareSpawns, + "KillSpawn": KillSpawn, + "KillSpawnByDistance": KillSpawnByDistance, + "Resurrect": Resurrect, + "IsInvulnerable": IsInvulnerable, + "SetInvulnerable": SetInvulnerable, + "SetAttackable": SetAttackable, + + // Miscellaneous Functions + "SendMessage": SendMessage, + "LogMessage": LogMessage, + "MakeRandomInt": MakeRandomInt, + "MakeRandomFloat": MakeRandomFloat, + "ParseInt": ParseInt, + "GetName": GetName, + "GetID": GetID, + "GetSpawnID": GetSpawnID, + "IsPlayer": IsPlayer, + "IsNPC": IsNPC, + "IsEntity": IsEntity, + "IsDead": IsDead, + "GetCharacterID": GetCharacterID, + "Despawn": Despawn, + "Spawn": Spawn, + "SpawnByLocationID": SpawnByLocationID, + "SpawnGroupByID": SpawnGroupByID, + "DespawnByLocationID": DespawnByLocationID, + "GetSpawnByLocationID": GetSpawnByLocationID, + "GetSpawnByGroupID": GetSpawnByGroupID, + "GetSpawnGroupID": GetSpawnGroupID, + "SetSpawnGroupID": SetSpawnGroupID, + "GetSpawnLocationID": GetSpawnLocationID, + "GetSpawnLocationPlacementID": GetSpawnLocationPlacementID, + "SetGridID": SetGridID, + "SpawnSet": SpawnSet, + "SpawnSetByDistance": SpawnSetByDistance, + "IsSpawnGroupAlive": IsSpawnGroupAlive, + "AddSpawnToGroup": AddSpawnToGroup, + "GetVariableValue": GetVariableValue, + "SetServerVariable": SetServerVariable, + "GetServerVariable": GetServerVariable, + "SetTempVariable": SetTempVariable, + "GetTempVariable": GetTempVariable, + "CheckLOS": CheckLOS, + "CheckLOSByCoordinates": CheckLOSByCoordinates, + } + + for name, fn := range functions { + if err := handler.Register(name, fn); err != nil { + return fmt.Errorf("failed to register event %s: %w", name, err) + } + } + + return nil +} + +// GetFunctionsByDomain returns functions organized by domain +func GetFunctionsByDomain() map[string][]string { + return map[string][]string{ + "health": { + "SetCurrentHP", "SetMaxHP", "SetMaxHPBase", "SetCurrentPower", "SetMaxPower", + "SetMaxPowerBase", "ModifyMaxHP", "ModifyMaxPower", "ModifyPower", "ModifyHP", + "ModifyTotalHP", "ModifyTotalPower", "GetCurrentHP", "GetMaxHP", "GetMaxHPBase", + "GetCurrentPower", "GetMaxPower", "GetMaxPowerBase", "GetPCTOfHP", "GetPCTOfPower", + "SpellHeal", "SpellHealPct", "IsAlive", + }, + "attributes": { + "SetInt", "SetWis", "SetSta", "SetStr", "SetAgi", "SetIntBase", "SetWisBase", + "SetStaBase", "SetStrBase", "SetAgiBase", "GetInt", "GetWis", "GetSta", "GetStr", + "GetAgi", "GetIntBase", "GetWisBase", "GetStaBase", "GetStrBase", "GetAgiBase", + "GetLevel", "SetLevel", "SetPlayerLevel", "GetDifficulty", "AddSpellBonus", + "RemoveSpellBonus", "AddSkillBonus", "RemoveSkillBonus", "GetClass", "SetClass", + "SetAdventureClass", "GetTradeskillClass", "SetTradeskillClass", "GetTradeskillLevel", + "SetTradeskillLevel", "GetRace", "GetGender", "GetModelType", "SetModelType", + "GetDeity", "SetDeity", "GetAlignment", "SetAlignment", + }, + "movement": { + "SetPosition", "GetPosition", "GetX", "GetY", "GetZ", "GetHeading", "SetHeading", + "GetOrigX", "GetOrigY", "GetOrigZ", "GetDistance", "FaceTarget", "GetSpeed", + "SetSpeed", "SetSpeedMultiplier", "HasMoved", "IsRunning", "MoveToLocation", + "ClearRunningLocations", "SpawnMove", "MovementLoopAdd", "PauseMovement", + "StopMovement", "SetMount", "GetMount", "SetMountColor", "StartAutoMount", + "EndAutoMount", "IsOnAutoMount", "AddWaypoint", "RemoveWaypoint", "SendWaypoints", + "Evac", "Bind", "Gate", + }, + "combat": { + "Attack", "AddHate", "ClearHate", "GetMostHated", "SetTarget", "GetTarget", + "IsInCombat", "SetInCombat", "SpellDamage", "SpellDamageExt", "DamageSpawn", + "ProcDamage", "ProcHate", "Knockback", "Interrupt", "IsCasting", "HasRecovered", + "ProcessMelee", "ProcessSpell", "LastSpellAttackHit", "IsBehind", "IsFlanking", + "InFront", "GetEncounterSize", "GetEncounter", "GetHateList", "ClearEncounter", + "ClearRunback", "Runback", "GetRunbackDistance", "CompareSpawns", "KillSpawn", + "KillSpawnByDistance", "Resurrect", "IsInvulnerable", "SetInvulnerable", "SetAttackable", + }, + "misc": { + "SendMessage", "LogMessage", "MakeRandomInt", "MakeRandomFloat", "ParseInt", + "GetName", "GetID", "GetSpawnID", "IsPlayer", "IsNPC", "IsEntity", "IsDead", + "GetCharacterID", "Despawn", "Spawn", "SpawnByLocationID", "SpawnGroupByID", + "DespawnByLocationID", "GetSpawnByLocationID", "GetSpawnByGroupID", "GetSpawnGroupID", + "SetSpawnGroupID", "GetSpawnLocationID", "GetSpawnLocationPlacementID", "SetGridID", + "SpawnSet", "SpawnSetByDistance", "IsSpawnGroupAlive", "AddSpawnToGroup", + "GetVariableValue", "SetServerVariable", "GetServerVariable", "SetTempVariable", + "GetTempVariable", "CheckLOS", "CheckLOSByCoordinates", + }, + } +} \ No newline at end of file diff --git a/internal/events/handler.go b/internal/events/handler.go new file mode 100644 index 0000000..5b45658 --- /dev/null +++ b/internal/events/handler.go @@ -0,0 +1,82 @@ +package events + +import ( + "context" + "fmt" + "time" +) + +// NewEventHandler creates a new event handler +func NewEventHandler() *EventHandler { + return &EventHandler{ + events: make(map[string]EventFunction), + } +} + +// Register registers an event function +func (h *EventHandler) Register(eventName string, fn EventFunction) error { + if fn == nil { + return fmt.Errorf("event function cannot be nil") + } + + h.mutex.Lock() + defer h.mutex.Unlock() + + h.events[eventName] = fn + return nil +} + +// Unregister removes an event function +func (h *EventHandler) Unregister(eventName string) { + h.mutex.Lock() + defer h.mutex.Unlock() + + delete(h.events, eventName) +} + +// Execute executes an event function +func (h *EventHandler) Execute(ctx *EventContext) error { + if ctx == nil { + return fmt.Errorf("context cannot be nil") + } + + h.mutex.RLock() + fn, exists := h.events[ctx.EventName] + h.mutex.RUnlock() + + if !exists { + return fmt.Errorf("event %s not found", ctx.EventName) + } + + // Set up context with timeout if needed + if ctx.Context == nil { + timeout, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + ctx.Context = timeout + } + + // Execute the function + return fn(ctx) +} + +// HasEvent checks if an event is registered +func (h *EventHandler) HasEvent(eventName string) bool { + h.mutex.RLock() + defer h.mutex.RUnlock() + + _, exists := h.events[eventName] + return exists +} + +// ListEvents returns all registered event names +func (h *EventHandler) ListEvents() []string { + h.mutex.RLock() + defer h.mutex.RUnlock() + + events := make([]string, 0, len(h.events)) + for name := range h.events { + events = append(events, name) + } + + return events +} \ No newline at end of file diff --git a/internal/events/types.go b/internal/events/types.go new file mode 100644 index 0000000..a2eae36 --- /dev/null +++ b/internal/events/types.go @@ -0,0 +1,82 @@ +package events + +import ( + "context" + "sync" + + "eq2emu/internal/entity" + "eq2emu/internal/quests" + "eq2emu/internal/spawn" +) + +// EventType represents different types of events +type EventType int + +const ( + EventTypeSpell EventType = iota + EventTypeSpawn + EventTypeQuest + EventTypeCombat + EventTypeZone + EventTypeItem +) + +func (et EventType) String() string { + switch et { + case EventTypeSpell: + return "spell" + case EventTypeSpawn: + return "spawn" + case EventTypeQuest: + return "quest" + case EventTypeCombat: + return "combat" + case EventTypeZone: + return "zone" + case EventTypeItem: + return "item" + default: + return "unknown" + } +} + +// EventContext provides context for event handling +type EventContext struct { + // Core context + Context context.Context + + // Event information + EventType EventType + EventName string + FunctionName string + + // Game objects (nil if not applicable) + Caster *entity.Entity + Target *entity.Entity + Spawn *spawn.Spawn + Quest *quests.Quest + + // Parameters and results + Parameters map[string]interface{} + Results map[string]interface{} + + // Synchronization + mutex sync.RWMutex +} + +// EventFunction represents a callable event function +type EventFunction func(ctx *EventContext) error + +// EventHandler manages event registration and execution +type EventHandler struct { + events map[string]EventFunction + mutex sync.RWMutex +} + +// EventLogger provides logging for events +type EventLogger interface { + Debug(msg string, args ...interface{}) + Info(msg string, args ...interface{}) + Warn(msg string, args ...interface{}) + Error(msg string, args ...interface{}) +} diff --git a/internal/spawn/spawn.go b/internal/spawn/spawn.go index 2274f79..bfac9cc 100644 --- a/internal/spawn/spawn.go +++ b/internal/spawn/spawn.go @@ -637,6 +637,70 @@ func (s *Spawn) SetLevel(level int16) { s.addChangedZoneSpawn() } +// GetClass returns the spawn's adventure class +func (s *Spawn) GetClass() int8 { + return s.appearance.AdventureClass +} + +// SetClass updates the spawn's adventure class and marks info as changed +func (s *Spawn) SetClass(class int8) { + s.updateMutex.Lock() + defer s.updateMutex.Unlock() + + s.appearance.AdventureClass = class + s.infoChanged.Store(true) + s.changed.Store(true) + s.addChangedZoneSpawn() +} + +// GetTradeskillClass returns the spawn's tradeskill class +func (s *Spawn) GetTradeskillClass() int8 { + return s.appearance.TradeskillClass +} + +// SetTradeskillClass updates the spawn's tradeskill class and marks info as changed +func (s *Spawn) SetTradeskillClass(class int8) { + s.updateMutex.Lock() + defer s.updateMutex.Unlock() + + s.appearance.TradeskillClass = class + s.infoChanged.Store(true) + s.changed.Store(true) + s.addChangedZoneSpawn() +} + +// GetRace returns the spawn's race +func (s *Spawn) GetRace() int8 { + return s.appearance.Race +} + +// SetRace updates the spawn's race and marks info as changed +func (s *Spawn) SetRace(race int8) { + s.updateMutex.Lock() + defer s.updateMutex.Unlock() + + s.appearance.Race = race + s.infoChanged.Store(true) + s.changed.Store(true) + s.addChangedZoneSpawn() +} + +// GetGender returns the spawn's gender +func (s *Spawn) GetGender() int8 { + return s.appearance.Gender +} + +// SetGender updates the spawn's gender and marks info as changed +func (s *Spawn) SetGender(gender int8) { + s.updateMutex.Lock() + defer s.updateMutex.Unlock() + + s.appearance.Gender = gender + s.infoChanged.Store(true) + s.changed.Store(true) + s.addChangedZoneSpawn() +} + // GetX returns the spawn's X coordinate func (s *Spawn) GetX() float32 { return s.appearance.Pos.X