mirror of
https://github.com/araxiaonline/TrinityCore.git
synced 2026-06-13 03:32:28 -04:00
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:
102
araxiaonline/lua_scripts/AGENTS.md
Normal file
102
araxiaonline/lua_scripts/AGENTS.md
Normal 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
|
||||
82
araxiaonline/lua_scripts/instances/README.md
Normal file
82
araxiaonline/lua_scripts/instances/README.md
Normal 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/
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
@@ -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")
|
||||
@@ -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) .. ")")
|
||||
260
araxiaonline/lua_scripts/spawn_validator.lua
Normal file
260
araxiaonline/lua_scripts/spawn_validator.lua
Normal 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")
|
||||
Reference in New Issue
Block a user