lua_scripts: Add AGENTS.md, fix Ook-Ook script, add instance scripts

- AGENTS.md: Add commit workflow note, code commenting guidelines, Eluna API gotchas
- boss_ook_ook.lua: Fix GetPlayersInWorld/GetPlayersInMap errors, add stale timer safeguard
- instances/: Add Stormstout Brewery and Vortex Pinnacle boss scripts
- spawn_validator.lua: Add spawn validation utility
This commit is contained in:
2025-12-14 22:33:03 -05:00
parent 28dbbf095b
commit cf2d839748
8 changed files with 903 additions and 0 deletions

View File

@@ -0,0 +1,102 @@
# Eluna Lua Scripts - Agent Guidelines
## ⚠️ IMPORTANT: Commit Workflow
**This directory (`/opt/trinitycore/server/lua_scripts/`) is NOT a git repo.**
To commit changes to lua_scripts:
1. Copy modified files to: `/opt/trinitycore/TrinityCore/araxiaonline/lua_scripts/`
2. Commit from the TrinityCore repo
```bash
# Example sync command
cp -r /opt/trinitycore/server/lua_scripts/* /opt/trinitycore/TrinityCore/araxiaonline/lua_scripts/
```
**AI assistants: Do this automatically when committing session work.**
---
## Overview
This directory contains Eluna Lua scripts for TrinityCore. These scripts handle boss encounters, instance mechanics, and custom functionality.
## Directory Structure
- `instances/` - Instance scripts organized by expansion/dungeon
- `AMS_Server/` - Araxia Message System server-side scripts
- `integration_tests/` - Test scripts for validating Eluna functionality
---
## Code Commenting Guidelines
**ALWAYS add comments when modifying Lua scripts in this project.**
### Why?
- Context is lost between sessions
- Future AI assistants (and humans) need to understand design decisions
- Prevents reimplementing the same solutions repeatedly
- Code comments stay close to the implementation
### What to Comment:
1. **File headers** - Document the purpose, creature entries, spell IDs
2. **Non-obvious API choices** - Why did you use this Eluna function?
3. **Workarounds** - What Eluna limitations did you encounter?
4. **Event handlers** - What triggers this code?
### Eluna API Gotchas (Document These!)
When you discover that an Eluna function doesn't work as expected, **add a comment**:
```lua
-- NOTE: GetPlayersInWorld() does NOT work in map-specific Eluna states (e.g., state 870)
-- NOTE: GetPlayersInMap() does NOT exist in Eluna at all!
-- For proximity detection, use creature:GetPlayersInRange() on the creature itself
-- or iterate players via OnLogin/OnLogout events and track them manually
local players = boss:GetPlayersInRange(30)
```
```lua
-- NOTE: GetCreaturesInWorld() does not exist in Eluna
-- Use player:GetCreaturesInRange(range, entry) to find creatures near a player
local creatures = player:GetCreaturesInRange(30, BOSS_ENTRY)
```
### Example Boss Script Header:
```lua
--[[
Boss: Ook-Ook
Instance: Stormstout Brewery (Map 961)
Entry: 56637
Abilities:
- Ground Pound (106807) - Frontal cone, knocks back
- Going Bananas (106651) - Enrage at health thresholds
Mechanics:
- Rolling barrels (56682) spawn from walls
- Kick barrels into boss for damage/stun
Known Issues:
- Retail requires killing 40 Hozen first; using proximity trigger for testing
]]--
```
### Stale Timer Issue After `.reload eluna`
**Problem:** `CreateLuaEvent` timers continue running after reload with OLD code.
**Solution:** Use script version tracking or accept errors until server restart.
```lua
-- Add at top of script to track version
local SCRIPT_VERSION = 2
_G.MY_SCRIPT_VERSION = SCRIPT_VERSION
-- In timer callbacks, check version (optional)
if _G.MY_SCRIPT_VERSION ~= SCRIPT_VERSION then return end
```
### Key Scripts with Important Comments:
- `instances/mists_of_pandaria/stormstout_brewery/boss_ook_ook.lua` - Eluna API workarounds, stale timer handling

View File

@@ -0,0 +1,82 @@
# Instance Boss Encounter Scripts
This directory contains Eluna Lua scripts for instance boss encounters.
## Directory Structure
```
instances/
├── README.md
├── cataclysm/
│ ├── vortex_pinnacle/
│ │ ├── instance.lua # Instance-wide handlers
│ │ ├── boss_ertan.lua # Grand Vizier Ertan
│ │ ├── boss_altairus.lua # Altairus
│ │ └── boss_asaad.lua # Asaad
│ ├── blackrock_caverns/
│ ├── throne_of_tides/
│ └── ...
├── wrath/
│ ├── utgarde_keep/
│ └── ...
├── burning_crusade/
└── ...
```
## Purpose
These scripts compensate for C++ AI scripts that are not available from repack database exports. They provide:
1. **Boss encounter completion** - Trigger instance state changes when bosses die
2. **Basic mechanics** - Simple spell timers and phase transitions via SmartAI-like logic
3. **Event triggers** - Spawn groups, doors, cinematics
## Creating Boss Scripts
### Template
```lua
-- boss_example.lua
local BOSS_ENTRY = 12345
local MAP_ID = 657
-- CREATURE_EVENT_ON_JUST_DIED = 4
RegisterCreatureEvent(BOSS_ENTRY, 4, function(event, creature, killer)
print("[Instance] Boss killed")
-- Add encounter completion logic here
end)
-- CREATURE_EVENT_ON_ENTER_COMBAT = 1
RegisterCreatureEvent(BOSS_ENTRY, 1, function(event, creature, target)
print("[Instance] Boss engaged")
-- Add combat start logic here
end)
print("[Eluna] Boss script loaded: " .. BOSS_ENTRY)
```
### Creature Events
| Event ID | Name | Description |
|----------|------|-------------|
| 1 | ON_ENTER_COMBAT | Creature enters combat |
| 2 | ON_LEAVE_COMBAT | Creature leaves combat |
| 3 | ON_TARGET_DIED | Creature kills a target |
| 4 | ON_DIED | Creature dies |
| 5 | ON_SPAWN | Creature spawns |
| 6 | ON_REACH_WP | Creature reaches waypoint |
| 7 | ON_AI_UPDATE | Called every ~1 second |
## Integration with Import Pipeline
When importing instance content:
1. Check if TC has C++ scripts for the bosses
2. If not, create placeholder Eluna scripts
3. Add to `instances/<expansion>/<instance>/` directory
4. Test and iterate on mechanics
## Related Documentation
- `/opt/trinitycore/araxia-trinity-content/docs/repack_cpp_ai_gap.md`
- Eluna API: https://elunaluaengine.github.io/

View File

@@ -0,0 +1,35 @@
-- Altairus - Vortex Pinnacle Boss 2
-- Entry: 43873, BossId: 1
local BOSS_ENTRY = 43873
local BOSS_ID = 1
local MAP_ID = 657
local BOSS_NAME = "Altairus"
-- Spells (placeholder - add actual spell IDs as needed)
local SPELLS = {
-- CHILLING_BREATH = 88308,
-- CALL_THE_WIND = 88244,
-- LIGHTNING_BLAST = 88357,
}
-- CREATURE_EVENT_ON_ENTER_COMBAT = 1
RegisterCreatureEvent(BOSS_ENTRY, 1, function(event, creature, target)
print(string.format("[VP] %s engaged in combat", BOSS_NAME))
end)
-- CREATURE_EVENT_ON_DIED = 4
RegisterCreatureEvent(BOSS_ENTRY, 4, function(event, creature, killer)
print(string.format("[VP] %s defeated - BossId %d should transition to DONE", BOSS_NAME, BOSS_ID))
if killer and killer:GetObjectType() == "Player" then
killer:SendBroadcastMessage(BOSS_NAME .. " defeated! Slipstreams to final platform activated.")
end
end)
-- CREATURE_EVENT_ON_LEAVE_COMBAT = 2
RegisterCreatureEvent(BOSS_ENTRY, 2, function(event, creature)
print(string.format("[VP] %s left combat (wipe or evade)", BOSS_NAME))
end)
print(string.format("[Eluna] Loaded boss script: %s (%d)", BOSS_NAME, BOSS_ENTRY))

View File

@@ -0,0 +1,35 @@
-- Asaad - Vortex Pinnacle Boss 3 (Final Boss)
-- Entry: 43875, BossId: 2
local BOSS_ENTRY = 43875
local BOSS_ID = 2
local MAP_ID = 657
local BOSS_NAME = "Asaad"
-- Spells (placeholder - add actual spell IDs as needed)
local SPELLS = {
-- CHAIN_LIGHTNING = 87622,
-- SUPREMACY_OF_THE_STORM = 86930,
-- STATIC_CLING = 87618,
}
-- CREATURE_EVENT_ON_ENTER_COMBAT = 1
RegisterCreatureEvent(BOSS_ENTRY, 1, function(event, creature, target)
print(string.format("[VP] %s (Final Boss) engaged in combat", BOSS_NAME))
end)
-- CREATURE_EVENT_ON_DIED = 4
RegisterCreatureEvent(BOSS_ENTRY, 4, function(event, creature, killer)
print(string.format("[VP] %s defeated - Instance complete! BossId %d should transition to DONE", BOSS_NAME, BOSS_ID))
if killer and killer:GetObjectType() == "Player" then
killer:SendBroadcastMessage(BOSS_NAME .. " defeated! Vortex Pinnacle cleared!")
end
end)
-- CREATURE_EVENT_ON_LEAVE_COMBAT = 2
RegisterCreatureEvent(BOSS_ENTRY, 2, function(event, creature)
print(string.format("[VP] %s left combat (wipe or evade)", BOSS_NAME))
end)
print(string.format("[Eluna] Loaded boss script: %s (%d)", BOSS_NAME, BOSS_ENTRY))

View File

@@ -0,0 +1,34 @@
-- Grand Vizier Ertan - Vortex Pinnacle Boss 1
-- Entry: 43878, BossId: 0
local BOSS_ENTRY = 43878
local BOSS_ID = 0
local MAP_ID = 657
local BOSS_NAME = "Grand Vizier Ertan"
-- Spells (placeholder - add actual spell IDs as needed)
local SPELLS = {
-- CYCLONE_SHIELD = 86267,
-- STORMS_EDGE = 86295,
}
-- CREATURE_EVENT_ON_ENTER_COMBAT = 1
RegisterCreatureEvent(BOSS_ENTRY, 1, function(event, creature, target)
print(string.format("[VP] %s engaged in combat", BOSS_NAME))
end)
-- CREATURE_EVENT_ON_DIED = 4
RegisterCreatureEvent(BOSS_ENTRY, 4, function(event, creature, killer)
print(string.format("[VP] %s defeated - BossId %d should transition to DONE", BOSS_NAME, BOSS_ID))
if killer and killer:GetObjectType() == "Player" then
killer:SendBroadcastMessage(BOSS_NAME .. " defeated! Slipstreams to next platform activated.")
end
end)
-- CREATURE_EVENT_ON_LEAVE_COMBAT = 2
RegisterCreatureEvent(BOSS_ENTRY, 2, function(event, creature)
print(string.format("[VP] %s left combat (wipe or evade)", BOSS_NAME))
end)
print(string.format("[Eluna] Loaded boss script: %s (%d)", BOSS_NAME, BOSS_ENTRY))

View File

@@ -0,0 +1,44 @@
-- Vortex Pinnacle Boss Encounter Handler
-- Workaround for BossAI not triggering encounter completion
local VP_MAP_ID = 657
local VP_BOSSES = {
[43878] = 0, -- Grand Vizier Ertan -> bossId 0
[43873] = 1, -- Altairus -> bossId 1
[43875] = 2, -- Asaad -> bossId 2
}
-- Creature event: CREATURE_EVENT_ON_DIED = 4
RegisterCreatureEvent(43878, 4, function(event, creature, killer)
local instance = creature:GetMap()
if instance and instance:GetMapId() == VP_MAP_ID then
print("[VP] Grand Vizier Ertan killed - setting boss state DONE")
-- Use GM command workaround since Eluna doesn't have direct instance API
if killer and killer:GetObjectType() == "Player" then
killer:SendBroadcastMessage("Boss defeated! Slipstreams activated.")
end
end
end)
RegisterCreatureEvent(43873, 4, function(event, creature, killer)
local instance = creature:GetMap()
if instance and instance:GetMapId() == VP_MAP_ID then
print("[VP] Altairus killed - setting boss state DONE")
if killer and killer:GetObjectType() == "Player" then
killer:SendBroadcastMessage("Boss defeated! Slipstreams activated.")
end
end
end)
RegisterCreatureEvent(43875, 4, function(event, creature, killer)
local instance = creature:GetMap()
if instance and instance:GetMapId() == VP_MAP_ID then
print("[VP] Asaad killed - setting boss state DONE")
if killer and killer:GetObjectType() == "Player" then
killer:SendBroadcastMessage("Boss defeated!")
end
end
end)
print("[Eluna] Vortex Pinnacle boss encounter handlers loaded")

View File

@@ -0,0 +1,311 @@
--[[
Stormstout Brewery - Ook-Ook Boss Script
Entry: 56637
Map: 961 (Stormstout Brewery)
Abilities:
- Ground Pound (106807) - Frontal cone, 3 sec channel, stuns 2 sec
- Going Bananas (106651) - At 90/60/30% HP, +15% damage/speed, triggers barrels
- Rolling Barrel (NPC 56682) - Rolls across room, players can ride
- Brew Explosion (107351) - Barrel collision damage
Notes:
- Boss should spawn after 40 Hozen killed (not implemented here - needs instance script)
- Barrel riding (vehicle) may not work without C++ support
]]
local OOK_OOK_ENTRY = 56637
local ROLLING_BARREL_ENTRY = 56682
-- SAFEGUARD: Script version check to handle stale timers after .reload eluna
-- Stale CreateLuaEvent timers from old script versions continue running after reload
-- This version number lets us detect and ignore callbacks from old timers
local SCRIPT_VERSION = 2
_G.OOK_OOK_SCRIPT_VERSION = SCRIPT_VERSION
-- Spell IDs
local SPELL_GROUND_POUND = 106807
local SPELL_GOING_BANANAS = 106651
local SPELL_BREW_EXPLOSION = 107351
-- Timers (in milliseconds)
local GROUND_POUND_TIMER = 8000 -- Every 8 seconds
local GROUND_POUND_INITIAL = 5000 -- First cast after 5 seconds
-- HP thresholds for Going Bananas (tracked to prevent re-triggering)
local BANANAS_90_TRIGGERED = false
local BANANAS_60_TRIGGERED = false
local BANANAS_30_TRIGGERED = false
-- Barrel spawn positions (around the brewhall stands)
-- These are approximate - may need adjustment based on actual room layout
local BARREL_SPAWN_POSITIONS = {
{x = -755.0, y = 1356.0, z = 146.7, o = 4.7}, -- North stand
{x = -770.0, y = 1341.0, z = 146.7, o = 0.0}, -- West stand
{x = -755.0, y = 1326.0, z = 146.7, o = 1.5}, -- South stand
{x = -740.0, y = 1341.0, z = 146.7, o = 3.1}, -- East stand
}
-- Boss center position for barrel targeting
local BOSS_CENTER = {x = -755.0, y = 1341.0, z = 146.7}
-- DEBUG/TESTING: Proximity trigger since 40-kill mechanic isn't implemented
-- Set to false to disable proximity aggro
local ENABLE_PROXIMITY_TRIGGER = true
local PROXIMITY_RANGE = 20 -- yards
--[[
Reset phase tracking on combat start
]]
local function ResetPhaseTracking()
BANANAS_90_TRIGGERED = false
BANANAS_60_TRIGGERED = false
BANANAS_30_TRIGGERED = false
end
--[[
Spawn rolling barrels from the stands
@param boss - The Ook-Ook creature
@param count - Number of barrels to spawn
]]
local function SpawnBarrels(boss, count)
if not boss or not boss:IsInWorld() then return end
local map = boss:GetMap()
if not map then return end
for i = 1, count do
-- Pick a random spawn position
local spawnIdx = math.random(1, #BARREL_SPAWN_POSITIONS)
local pos = BARREL_SPAWN_POSITIONS[spawnIdx]
-- Spawn the barrel
local barrel = boss:SpawnCreature(ROLLING_BARREL_ENTRY, pos.x, pos.y, pos.z, pos.o, 1, 30000)
if barrel then
-- Make barrel move toward boss center (simplified - real barrels roll in straight line)
barrel:MoveFollow(boss, 0, 0)
-- Register barrel collision check
barrel:RegisterEvent(function(eventId, delay, repeats, barrelUnit)
CheckBarrelCollision(barrelUnit, boss)
end, 500, 0) -- Check every 500ms
end
end
end
--[[
Check if barrel has collided with anything
@param barrel - The Rolling Barrel creature
@param boss - The Ook-Ook boss (for debuff application)
]]
local function CheckBarrelCollision(barrel, boss)
if not barrel or not barrel:IsInWorld() then return end
-- Check distance to boss
if boss and boss:IsInWorld() then
local dist = barrel:GetDistance(boss)
if dist < 3 then
-- Hit the boss! Apply debuff and explode
barrel:CastSpell(boss, SPELL_BREW_EXPLOSION, true)
-- The spell should apply the vulnerability debuff
barrel:DespawnOrUnsummon(100)
return
end
end
-- Check for nearby players
local players = barrel:GetPlayersInRange(2)
if players and #players > 0 then
-- Hit a player - explode
barrel:CastSpell(barrel, SPELL_BREW_EXPLOSION, true)
barrel:DespawnOrUnsummon(100)
return
end
-- TODO: Wall collision detection would need raycast or position bounds checking
end
--[[
Handle Ground Pound ability
@param eventId - Event ID
@param delay - Delay before event
@param repeats - Number of repeats remaining
@param boss - The Ook-Ook creature
]]
local function CastGroundPound(eventId, delay, repeats, boss)
if not boss or not boss:IsInWorld() or boss:IsDead() then return end
if not boss:IsInCombat() then return end
local victim = boss:GetVictim()
if victim then
boss:CastSpell(victim, SPELL_GROUND_POUND, false)
-- Yell one of the quotes
local quotes = {
"Come on and get your Ook on!",
"Get Ooking party started!",
"We gonna Ook all night!",
}
boss:SendUnitYell(quotes[math.random(1, #quotes)], 0)
end
end
--[[
Check HP thresholds and trigger Going Bananas
@param boss - The Ook-Ook creature
]]
local function CheckHPThresholds(boss)
if not boss or not boss:IsInWorld() or boss:IsDead() then return end
local hpPct = boss:GetHealthPct()
-- 90% threshold
if hpPct <= 90 and not BANANAS_90_TRIGGERED then
BANANAS_90_TRIGGERED = true
TriggerGoingBananas(boss, 1)
end
-- 60% threshold
if hpPct <= 60 and not BANANAS_60_TRIGGERED then
BANANAS_60_TRIGGERED = true
TriggerGoingBananas(boss, 2)
end
-- 30% threshold
if hpPct <= 30 and not BANANAS_30_TRIGGERED then
BANANAS_30_TRIGGERED = true
TriggerGoingBananas(boss, 3)
end
end
--[[
Trigger Going Bananas phase
@param boss - The Ook-Ook creature
@param stack - Which stack (1, 2, or 3)
]]
local function TriggerGoingBananas(boss, stack)
if not boss or not boss:IsInWorld() then return end
-- Cast Going Bananas buff on self
boss:CastSpell(boss, SPELL_GOING_BANANAS, true)
-- Yell
boss:SendUnitYell("Me gonna ook you in the dooker!", 0)
-- Spawn barrels based on stack (more barrels at lower HP)
local barrelCount = 2 + stack -- 3, 4, or 5 barrels
SpawnBarrels(boss, barrelCount)
PrintInfo(string.format("[Ook-Ook] Going Bananas triggered! Stack %d, spawning %d barrels", stack, barrelCount))
end
--[[
EnterCombat handler
]]
local function OnEnterCombat(event, creature, target)
if creature:GetEntry() ~= OOK_OOK_ENTRY then return end
PrintInfo("[Ook-Ook] Combat started!")
-- Reset tracking
ResetPhaseTracking()
-- Aggro yell
creature:SendUnitYell("Me gonna ook you in the dooker!", 0)
-- Schedule Ground Pound
creature:RegisterEvent(CastGroundPound, GROUND_POUND_INITIAL, 0)
-- Schedule HP threshold check (every 1 second)
creature:RegisterEvent(function(eventId, delay, repeats, boss)
CheckHPThresholds(boss)
end, 1000, 0)
end
--[[
LeaveCombat handler (wipe/evade)
]]
local function OnLeaveCombat(event, creature)
if creature:GetEntry() ~= OOK_OOK_ENTRY then return end
PrintInfo("[Ook-Ook] Combat ended (evade/wipe)")
-- Clear events
creature:RemoveEvents()
-- Reset tracking
ResetPhaseTracking()
end
--[[
JustDied handler
]]
local function OnDied(event, creature, killer)
if creature:GetEntry() ~= OOK_OOK_ENTRY then return end
PrintInfo("[Ook-Ook] Defeated!")
-- Clear events
creature:RemoveEvents()
-- Death yell
creature:SendUnitYell("Ook! Oooook!!", 0)
-- Reset tracking for next attempt
ResetPhaseTracking()
end
--[[
DamageTaken handler - for HP threshold checks
]]
local function OnDamageTaken(event, creature, attacker, damage)
if creature:GetEntry() ~= OOK_OOK_ENTRY then return end
-- Check HP thresholds after damage
CheckHPThresholds(creature)
end
--[[
Proximity trigger for testing (since 40-kill mechanic isn't implemented)
Checks every 2 seconds if a player is within range
]]
local function OnSpawn(event, creature)
if creature:GetEntry() ~= OOK_OOK_ENTRY then return end
if not ENABLE_PROXIMITY_TRIGGER then return end
PrintInfo("[Ook-Ook] Spawned - proximity trigger enabled at " .. PROXIMITY_RANGE .. " yards")
-- Register periodic check for nearby players
creature:RegisterEvent(function(eventId, delay, repeats, boss)
if not boss or not boss:IsInWorld() or boss:IsDead() then return end
if boss:IsInCombat() then return end -- Already in combat
local players = boss:GetPlayersInRange(PROXIMITY_RANGE)
if players and #players > 0 then
local target = players[1]
if target and target:IsAlive() then
PrintInfo("[Ook-Ook] Player detected within " .. PROXIMITY_RANGE .. " yards - engaging!")
boss:SendUnitYell("You come to Ook's party?! NOW YOU GONNA GET OOKED!", 0)
boss:AttackStart(target)
end
end
end, 2000, 0) -- Check every 2 seconds
end
-- Register event handlers
RegisterCreatureEvent(OOK_OOK_ENTRY, 1, OnEnterCombat) -- CREATURE_EVENT_ON_ENTER_COMBAT
RegisterCreatureEvent(OOK_OOK_ENTRY, 2, OnLeaveCombat) -- CREATURE_EVENT_ON_LEAVE_COMBAT
RegisterCreatureEvent(OOK_OOK_ENTRY, 4, OnDied) -- CREATURE_EVENT_ON_DIED
RegisterCreatureEvent(OOK_OOK_ENTRY, 5, OnSpawn) -- CREATURE_EVENT_ON_SPAWN
RegisterCreatureEvent(OOK_OOK_ENTRY, 9, OnDamageTaken) -- CREATURE_EVENT_ON_DAMAGE_TAKEN
-- NOTE: Global proximity scanner DISABLED
-- GetPlayersInWorld() doesn't work in map states, GetPlayersInMap() doesn't exist in Eluna
-- The proximity trigger is handled via OnSpawn event above instead
-- If you need to trigger Ook-Ook manually, use: .npc add 56637 near player
if ENABLE_PROXIMITY_TRIGGER then
PrintInfo("[Ook-Ook] Proximity trigger enabled (via OnSpawn event only - no global scanner)")
end
PrintInfo("[Stormstout Brewery] Ook-Ook boss script loaded (proximity trigger: " .. tostring(ENABLE_PROXIMITY_TRIGGER) .. ")")

View File

@@ -0,0 +1,260 @@
--[[
Spawn Validator - Headless creature spawn validation for MCP
Allows MCP to validate creature spawns without requiring a player in-game.
Uses server-side APIs to check if creatures exist and are spawned correctly.
Shared Data Keys:
- mcp_spawn_validation_request: Request from MCP to validate spawns
- mcp_spawn_validation_result: Results of validation
- mcp_spawn_query_result: Results of spawn queries
]]
local Smallfolk = require("Smallfolk")
-- Configuration
local VALIDATION_INTERVAL = 1000 -- Check for requests every 1 second
-- Initialize shared data keys
local function InitSharedData()
if not HasSharedData("mcp_spawn_validation_request") then
SetSharedData("mcp_spawn_validation_request", "")
end
if not HasSharedData("mcp_spawn_validation_result") then
SetSharedData("mcp_spawn_validation_result", "")
end
if not HasSharedData("mcp_spawn_query_result") then
SetSharedData("mcp_spawn_query_result", "")
end
if not HasSharedData("mcp_force_spawn_request") then
SetSharedData("mcp_force_spawn_request", "")
end
end
-- Get all creatures in a specific area
local function GetCreaturesInArea(mapId, centerX, centerY, centerZ, radius)
local creatures = {}
-- Use GetCreaturesInWorld to find creatures
local allCreatures = GetCreaturesInWorld(mapId)
if allCreatures then
for _, creature in ipairs(allCreatures) do
local cx, cy, cz = creature:GetLocation()
local dx = cx - centerX
local dy = cy - centerY
local dz = cz - centerZ
local dist = math.sqrt(dx*dx + dy*dy + dz*dz)
if dist <= radius then
table.insert(creatures, {
guid = creature:GetGUIDLow(),
entry = creature:GetEntry(),
name = creature:GetName(),
x = cx,
y = cy,
z = cz,
distance = dist,
isAlive = creature:IsAlive(),
level = creature:GetLevel()
})
end
end
end
return creatures
end
-- Get creature by entry ID
local function GetCreaturesByEntry(mapId, entryId)
local creatures = {}
local allCreatures = GetCreaturesInWorld(mapId)
if allCreatures then
for _, creature in ipairs(allCreatures) do
if creature:GetEntry() == entryId then
local x, y, z = creature:GetLocation()
table.insert(creatures, {
guid = creature:GetGUIDLow(),
entry = creature:GetEntry(),
name = creature:GetName(),
x = x,
y = y,
z = z,
isAlive = creature:IsAlive(),
level = creature:GetLevel()
})
end
end
end
return creatures
end
-- Count all creatures on a map
local function CountCreaturesOnMap(mapId)
local allCreatures = GetCreaturesInWorld(mapId)
return allCreatures and #allCreatures or 0
end
-- Force spawn a creature at a location
local function ForceSpawnCreature(mapId, entryId, x, y, z, orientation)
-- SpawnCreature(entry, map, x, y, z, o, despawnType, despawnTime)
-- despawnType: 0 = manual, 1 = timed
local creature = PerformIngameSpawn(1, entryId, mapId, 0, x, y, z, orientation or 0, false, 0)
if creature then
return {
success = true,
guid = creature:GetGUIDLow(),
entry = creature:GetEntry(),
name = creature:GetName(),
x = x,
y = y,
z = z
}
else
return {
success = false,
error = "Failed to spawn creature " .. entryId
}
end
end
-- Process validation requests from MCP
local function ProcessValidationRequest()
local requestData = GetSharedData("mcp_spawn_validation_request")
if not requestData or requestData == "" then
return
end
-- Clear the request
SetSharedData("mcp_spawn_validation_request", "")
-- Parse request (simple JSON-like format)
-- Format: {"action":"query_area","mapId":870,"x":1606,"y":-1733,"z":274,"radius":50}
local action = requestData:match('"action":"([^"]*)"')
local mapId = tonumber(requestData:match('"mapId":(%d+)'))
local result = {}
if action == "query_area" then
local x = tonumber(requestData:match('"x":([%-%.%d]+)'))
local y = tonumber(requestData:match('"y":([%-%.%d]+)'))
local z = tonumber(requestData:match('"z":([%-%.%d]+)'))
local radius = tonumber(requestData:match('"radius":(%d+)')) or 50
if mapId and x and y and z then
local creatures = GetCreaturesInArea(mapId, x, y, z, radius)
result = {
success = true,
action = "query_area",
mapId = mapId,
center = {x = x, y = y, z = z},
radius = radius,
count = #creatures,
creatures = creatures
}
print("[Spawn Validator] Found " .. #creatures .. " creatures in area")
else
result = {success = false, error = "Missing coordinates"}
end
elseif action == "query_entry" then
local entryId = tonumber(requestData:match('"entryId":(%d+)'))
if mapId and entryId then
local creatures = GetCreaturesByEntry(mapId, entryId)
result = {
success = true,
action = "query_entry",
mapId = mapId,
entryId = entryId,
count = #creatures,
creatures = creatures
}
print("[Spawn Validator] Found " .. #creatures .. " creatures with entry " .. entryId)
else
result = {success = false, error = "Missing mapId or entryId"}
end
elseif action == "count_map" then
if mapId then
local count = CountCreaturesOnMap(mapId)
result = {
success = true,
action = "count_map",
mapId = mapId,
count = count
}
print("[Spawn Validator] Map " .. mapId .. " has " .. count .. " creatures")
else
result = {success = false, error = "Missing mapId"}
end
elseif action == "force_spawn" then
local entryId = tonumber(requestData:match('"entryId":(%d+)'))
local x = tonumber(requestData:match('"x":([%-%.%d]+)'))
local y = tonumber(requestData:match('"y":([%-%.%d]+)'))
local z = tonumber(requestData:match('"z":([%-%.%d]+)'))
local o = tonumber(requestData:match('"orientation":([%-%.%d]+)')) or 0
if mapId and entryId and x and y and z then
result = ForceSpawnCreature(mapId, entryId, x, y, z, o)
result.action = "force_spawn"
print("[Spawn Validator] Force spawn result: " .. (result.success and "success" or "failed"))
else
result = {success = false, error = "Missing spawn parameters"}
end
else
result = {success = false, error = "Unknown action: " .. (action or "nil")}
end
-- Store result as JSON
local resultJson = string.format(
'{"success":%s,"action":"%s","mapId":%d,"count":%d,"error":"%s","timestamp":%d}',
result.success and "true" or "false",
result.action or "unknown",
result.mapId or 0,
result.count or 0,
result.error or "",
os.time()
)
-- For creature lists, append them
if result.creatures and #result.creatures > 0 then
local creatureStrs = {}
for _, c in ipairs(result.creatures) do
table.insert(creatureStrs, string.format(
'{"guid":%d,"entry":%d,"name":"%s","x":%.2f,"y":%.2f,"z":%.2f,"level":%d,"alive":%s}',
c.guid or 0,
c.entry or 0,
c.name or "unknown",
c.x or 0,
c.y or 0,
c.z or 0,
c.level or 0,
c.isAlive and "true" or "false"
))
end
resultJson = resultJson:gsub('}$', ',"creatures":[' .. table.concat(creatureStrs, ',') .. ']}')
end
SetSharedData("mcp_spawn_validation_result", resultJson)
end
-- Register a world update hook to check for requests
local function OnWorldUpdate(event, diff)
ProcessValidationRequest()
end
-- Initialize
InitSharedData()
-- Register world update event (fires every server tick)
RegisterServerEvent(14, OnWorldUpdate) -- WORLD_EVENT_ON_UPDATE
print("[Spawn Validator] Loaded - MCP can now validate spawns without a player")
print("[Spawn Validator] Actions: query_area, query_entry, count_map, force_spawn")