capturing latest changes before letting AI take over

This commit is contained in:
2025-11-21 18:54:26 -05:00
parent dcbffdf161
commit cbd77bcad9
9 changed files with 1862 additions and 0 deletions

10
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,10 @@
{
"cSpell.words": [
"WAPI"
],
"cSpell.ignoreWords": [
"BLACKWING",
"GURUB",
"undeath"
]
}

View File

@@ -0,0 +1,237 @@
# Combat Resurrection System
## Overview
The Combat Resurrection system allows players to resurrect fallen allies mid-combat by standing near their corpse and channeling for a configurable duration. This feature is inspired by modern battle royale games like Apex Legends and Warzone, making WoW combat more interactive and team-oriented.
## Features
### Core Mechanics
- **Proximity-based resurrection**: Must be within 5 yards of the dead player
- **Channeling system**: 5-second channel time (configurable)
- **Progress tracking**: Real-time tracking of resurrection progress
- **Cooldown system**: 60-second cooldown per player after successful resurrection
### Interruption Conditions
- **Movement**: Resurrector moves more than 2 yards from starting position
- **Damage**: Resurrector takes damage during channeling
- **Spell Casting**: Resurrector casts another spell
- **Death**: Resurrector dies during channeling
- **Distance**: Target moves too far away (>5 yards)
- **Map Change**: Either player changes maps
### Restrictions
- **Group Membership**: Players must be in the same group (configurable)
- **Combat Allowed**: Can resurrect during combat (configurable)
- **Single Resurrector**: Only one player can resurrect a target at a time
## Configuration
All settings can be adjusted in the `CONFIG` object at the top of `combat-resurrection.ts`:
```typescript
const CONFIG: CombatRezConfig = {
// Core mechanics
channelTime: 5.0, // 5 seconds to complete
maxRange: 5.0, // 5 yards maximum distance
healthPercent: 0.30, // Resurrect with 30% health
manaPercent: 0.20, // Resurrect with 20% mana
resurrectSickness: false, // No resurrection sickness
// Restrictions
allowInCombat: true, // Allow mid-combat resurrection
requireGroupMember: true, // Must be in same group
cooldownSeconds: 60, // 60 second cooldown per player
maxSimultaneousRez: 1, // Only one person can rez a target at a time
// Interruption conditions
interruptOnMovement: true, // Cancel if resurrector moves
interruptOnDamage: true, // Cancel if resurrector takes damage
interruptOnSpellCast: true, // Cancel if resurrector casts spell
movementTolerance: 2.0, // 2 yards movement tolerance
// Visual/Audio (WoW 3.3.5a spell IDs and sounds)
castBarSpellId: 2006, // Resurrection spell for cast bar
channelSpellId: 2006, // Resurrection visual effect
soundStart: 8212, // Holy spell sound
soundComplete: 12867, // Success/completion sound
soundInterrupt: 847, // Error/interrupt sound
// Debugging
enableDebugMessages: true, // Show debug messages
};
```
## Usage
### In-Game Commands
#### `.rez` or `.resurrect`
Attempts to resurrect the nearest dead player within range.
**Example:**
```
.rez
```
**Output:**
- Success: "Resurrecting [PlayerName]... Stay still!"
- Failure: Error message explaining why resurrection cannot be started
#### `.rezstatus`
Shows the current resurrection status and cooldown information.
**Example:**
```
.rezstatus
```
**Output:**
- If resurrecting: "Resurrection in progress: 50%"
- If on cooldown: "Cooldown: 45 seconds remaining"
- Otherwise: "No active resurrection."
### Gameplay Flow
1. **Find a Dead Ally**: Move within 5 yards of a dead group member
2. **Start Resurrection**: Type `.rez` command
3. **Stay Still**: Remain within 2 yards of your starting position
4. **Avoid Damage**: Don't take damage or cast spells
5. **Wait**: Channel completes after 5 seconds
6. **Success**: Ally is resurrected with 30% health and 20% mana
## Testing Checklist
### Basic Functionality
- [ ] Resurrection completes successfully after 5 seconds
- [ ] Resurrected player has 30% health and 20% mana
- [ ] Cooldown is applied after successful resurrection
- [ ] Sound effects play at start and completion
### Interruption Tests
- [ ] Resurrection interrupts when resurrector moves >2 yards
- [ ] Resurrection interrupts when resurrector takes damage
- [ ] Resurrection interrupts when resurrector casts a spell
- [ ] Resurrection interrupts when resurrector dies
- [ ] Resurrection interrupts when target moves >5 yards away
### Validation Tests
- [ ] Cannot resurrect if not in same group (when enabled)
- [ ] Cannot resurrect if on cooldown
- [ ] Cannot resurrect if already resurrecting someone
- [ ] Cannot resurrect if target is not dead
- [ ] Cannot resurrect if target is too far away
- [ ] Cannot resurrect if on different map
### Edge Cases
- [ ] Multiple players trying to rez same target (first one wins)
- [ ] Target releases spirit during resurrection (interrupts)
- [ ] Resurrector disconnects during resurrection (cleans up)
- [ ] Target disconnects during resurrection (interrupts)
- [ ] Map change during resurrection (interrupts)
## Technical Details
### Event Hooks Used
- `PLAYER_EVENT_ON_KILLED_BY_CREATURE`: Detect when player dies
- `PLAYER_EVENT_ON_SPELL_CAST`: Detect spell casting for interruption
- `PLAYER_EVENT_ON_ENTER_COMBAT`: Track damage for interruption
- `PLAYER_EVENT_ON_LOGOUT`: Cleanup on player logout
- `PLAYER_EVENT_ON_COMMAND`: Handle `.rez` commands
- `MAP_EVENT_ON_UPDATE`: Update resurrection progress every tick
### State Management
- **activeRezAttempts**: Map of active resurrection attempts (key: resurrector GUID)
- **rezCooldowns**: Map of cooldown expiry times (key: player GUID)
- **lastDamageTaken**: Map of last damage timestamps (key: player GUID)
### Key Functions
- `StartResurrection()`: Initiates a resurrection attempt
- `CompleteResurrection()`: Finalizes resurrection and restores player
- `InterruptResurrection()`: Cancels resurrection with reason
- `UpdateResurrections()`: Called every map update to track progress
- `CanStartResurrection()`: Validates all resurrection requirements
## Known Issues
### TypeScript Lint Warning
There is a minor TypeScript lint warning on line 118 regarding arithmetic operations with `GetGameTime()`. This is a known TypeScript-to-Lua transpiler quirk and does not affect functionality. The code compiles and runs correctly in-game.
**Warning:**
```
The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.
```
**Status:** This is cosmetic and can be safely ignored. The Lua output is correct.
## Future Enhancements
### Planned Features
1. **Client-Side UI**: Progress bar and keybind system (hold 'E' to resurrect)
2. **Class-Specific Bonuses**: Faster resurrection for healers
3. **Item Requirements**: Optional reagent consumption
4. **Achievement Tracking**: "Life Saver" achievement for X resurrections
5. **Statistics**: Track resurrections given/received
6. **Visual Customization**: Different effects per class
7. **Mobile Resurrection**: Allow slow movement while channeling
### Configuration Presets
#### Quick Resurrection (PvP)
```typescript
channelTime: 3.0,
cooldownSeconds: 30,
interruptOnDamage: false,
```
#### Challenging Resurrection (Hardcore)
```typescript
channelTime: 10.0,
cooldownSeconds: 120,
healthPercent: 0.10,
resurrectSickness: true,
```
#### No Restrictions (Testing)
```typescript
requireGroupMember: false,
cooldownSeconds: 0,
interruptOnMovement: false,
interruptOnDamage: false,
```
## Troubleshooting
### "No dead players nearby"
- Ensure you're within 5 yards of a dead player
- Check that the player is actually dead (not just ghost form)
### "You must be in the same group to resurrect"
- Both players must be in the same party or raid
- Set `requireGroupMember: false` in config to disable this check
### "You must wait X seconds before resurrecting again"
- Cooldown is active after successful resurrection
- Adjust `cooldownSeconds` in config or wait for cooldown to expire
### Resurrection interrupted unexpectedly
- Check debug messages (if enabled) for specific reason
- Common causes: movement, damage, spell casting
- Adjust interruption settings in config if needed
## Support
For issues, questions, or feature requests related to the Combat Resurrection system, please check:
1. This README for configuration options
2. Debug messages (enable `enableDebugMessages: true`)
3. Server console output for error messages
4. Existing codebase patterns in `/modules/gameplay/`
## Credits
Inspired by modern battle royale resurrection mechanics from:
- Apex Legends
- Call of Duty: Warzone
- Fortnite
Implemented for AzerothCore using the Eluna Lua Engine.

View File

@@ -0,0 +1,550 @@
/** @noSelfInFile **/
/** @ts-expect-error */
let aio: AIO = {};
print("[Combat Rez Client] File loaded on server");
if (!aio.AddAddon()) {
print("[Combat Rez Client] Executing client-side code");
// ============================================================================
// HELPER FUNCTIONS
// ============================================================================
// Helper function to create unique IDs for frames
const id = (name: string): string => `CombatRez_${name}`;
// ============================================================================
// CONFIGURATION
// ============================================================================
const UI_CONFIG = {
buttonWidth: 150,
buttonHeight: 40,
progressBarWidth: 250,
progressBarHeight: 25,
yOffset: 200, // Distance from bottom of screen
updateInterval: 0.1, // Update every 0.1 seconds
};
// ============================================================================
// STATE
// ============================================================================
let rezButton: WoWAPI.Frame | null = null;
let progressBar: WoWAPI.Frame | null = null;
let progressBarFill: WoWAPI.Texture | null = null;
let progressBarText: WoWAPI.FontString | null = null;
let updateFrame: WoWAPI.Frame | null = null;
let loadedPopup: WoWAPI.Frame | null = null;
let isResurrecting = false;
let resurrectProgress = 0;
let nearbyDeadPlayer: string | null = null;
// ============================================================================
// UI CREATION
// ============================================================================
/**
* Create the revive button
*/
function CreateReviveButton(): void {
if (rezButton) return;
// @ts-ignore
rezButton = CreateFrame("Button", id("ReviveButton"), UIParent, "UIPanelButtonTemplate");
rezButton.SetSize(UI_CONFIG.buttonWidth, UI_CONFIG.buttonHeight);
rezButton.SetPoint("BOTTOM", UIParent, "BOTTOM", 0, UI_CONFIG.yOffset);
// Add a glowing background to make it more visible
// @ts-ignore
const glow = rezButton.CreateTexture(null, "BACKGROUND");
glow.SetAllPoints(rezButton);
// @ts-ignore
glow.SetTexture(0.2, 1, 0.2, 0.3); // Green glow
// @ts-ignore
rezButton.SetText("Revive Player");
// @ts-ignore
rezButton.SetScript("OnClick", () => {
if (nearbyDeadPlayer) {
// Send message to server to start resurrection
aio.Handle("CombatRez", "StartResurrection", nearbyDeadPlayer);
print(`[Combat Rez] Attempting to revive ${nearbyDeadPlayer}`);
}
});
// @ts-ignore
rezButton.SetScript("OnEnter", () => {
if (nearbyDeadPlayer) {
// @ts-ignore
GameTooltip.SetOwner(rezButton, "ANCHOR_TOP");
// @ts-ignore
GameTooltip.SetText(`Click to revive ${nearbyDeadPlayer}`, 1, 1, 1);
// @ts-ignore
GameTooltip.AddLine("Stay still during channeling", 0.8, 0.8, 0.8);
// @ts-ignore
GameTooltip.Show();
}
});
// @ts-ignore
rezButton.SetScript("OnLeave", () => {
// @ts-ignore
GameTooltip.Hide();
});
rezButton.Hide();
}
/**
* Create the progress bar
*/
function CreateProgressBar(): void {
if (progressBar) return;
// Main frame
// @ts-ignore
progressBar = CreateFrame("Frame", id("ProgressBar"), UIParent);
progressBar.SetSize(UI_CONFIG.progressBarWidth, UI_CONFIG.progressBarHeight);
progressBar.SetPoint("BOTTOM", UIParent, "BOTTOM", 0, UI_CONFIG.yOffset + 50);
// Background - dark
// @ts-ignore
const bg = progressBar.CreateTexture(null, "BACKGROUND");
bg.SetAllPoints(progressBar);
// @ts-ignore
bg.SetTexture(0.1, 0.1, 0.1, 0.9);
// Border - thin green outline
// @ts-ignore
const borderTop = progressBar.CreateTexture(null, "BORDER");
borderTop.SetPoint("TOPLEFT", progressBar, "TOPLEFT");
borderTop.SetPoint("TOPRIGHT", progressBar, "TOPRIGHT");
borderTop.SetHeight(1);
// @ts-ignore
borderTop.SetTexture(0.2, 1, 0.2, 1);
// @ts-ignore
const borderBottom = progressBar.CreateTexture(null, "BORDER");
borderBottom.SetPoint("BOTTOMLEFT", progressBar, "BOTTOMLEFT");
borderBottom.SetPoint("BOTTOMRIGHT", progressBar, "BOTTOMRIGHT");
borderBottom.SetHeight(1);
// @ts-ignore
borderBottom.SetTexture(0.2, 1, 0.2, 1);
// Fill (progress indicator)
// @ts-ignore
progressBarFill = progressBar.CreateTexture(null, "ARTWORK");
progressBarFill.SetPoint("LEFT", progressBar, "LEFT", 1, 0);
progressBarFill.SetSize(UI_CONFIG.progressBarWidth - 2, UI_CONFIG.progressBarHeight - 2);
// @ts-ignore
progressBarFill.SetTexture(0.2, 0.8, 0.3, 0.9); // Bright green
progressBarFill.SetWidth(0); // Start at 0 width
// Text
// @ts-ignore
progressBarText = progressBar.CreateFontString(null, "OVERLAY", "GameFontNormal");
progressBarText.SetPoint("CENTER", progressBar, "CENTER", 0, 0);
progressBarText.SetText("Resurrecting... 0%");
progressBarText.SetTextColor(1, 1, 1);
progressBar.Hide();
}
/**
* Create the update frame for checking nearby dead players
*/
function CreateUpdateFrame(): void {
if (updateFrame) return;
// @ts-ignore
updateFrame = CreateFrame("Frame", id("UpdateFrame"));
let elapsed = 0;
// @ts-ignore
updateFrame.SetScript("OnUpdate", (self: any, delta: number) => {
elapsed += delta;
if (elapsed < UI_CONFIG.updateInterval) {
return;
}
elapsed = 0;
// Only check for nearby dead players if not currently resurrecting
if (!isResurrecting) {
CheckForNearbyDeadPlayers();
}
});
}
// ============================================================================
// LOGIC FUNCTIONS
// ============================================================================
/**
* Check if there are any dead players nearby
*/
function CheckForNearbyDeadPlayers(): void {
// @ts-ignore - WoW 3.3.5 API
const numRaidMembers = GetNumRaidMembers();
// @ts-ignore - WoW 3.3.5 API
const numPartyMembers = GetNumPartyMembers();
const isInRaid = numRaidMembers > 0;
const numGroupMembers = isInRaid ? numRaidMembers : numPartyMembers;
if (numGroupMembers === 0) {
HideReviveButton();
return;
}
let foundDeadPlayer = false;
// Check raid/party members
for (let i = 1; i <= numGroupMembers; i++) {
const unit = (isInRaid ? `raid${i}` : `party${i}`) as WoWAPI.UnitId;
if (UnitExists(unit) === 1) {
const health = UnitHealth(unit);
// Check if unit is dead (health == 0)
// Note: In WoW 3.3.5, we can't easily check if they're a ghost client-side
// The server will validate if resurrection is possible
if (health === 0) {
// @ts-ignore - CheckInteractDistance not in type declarations
const distance = CheckInteractDistance(unit, 3); // 3 = 10 yards, close enough for our 5 yard check
if (distance) {
const name = UnitName(unit);
ShowReviveButton(name);
foundDeadPlayer = true;
break;
}
}
}
}
if (!foundDeadPlayer) {
HideReviveButton();
}
}
/**
* Show the revive button
*/
function ShowReviveButton(playerName: string): void {
if (!rezButton) return;
nearbyDeadPlayer = playerName;
// @ts-ignore
rezButton.SetText(`Revive ${playerName}`);
rezButton.Show();
print(`[Combat Rez] Button shown for ${playerName} at bottom center (200px up)`);
}
/**
* Hide the revive button
*/
function HideReviveButton(): void {
if (!rezButton) return;
nearbyDeadPlayer = null;
rezButton.Hide();
}
/**
* Start showing resurrection progress
*/
function StartResurrectionUI(targetName: string): void {
isResurrecting = true;
resurrectProgress = 0;
// Hide button, show progress bar
HideReviveButton();
if (progressBar) {
progressBar.Show();
}
UpdateProgressBar(0, targetName);
}
/**
* Update the progress bar
*/
function UpdateProgressBar(progress: number, targetName: string): void {
if (!progressBar || !progressBarFill || !progressBarText) return;
resurrectProgress = progress;
// Update fill width
const maxWidth = UI_CONFIG.progressBarWidth - 4;
const fillWidth = maxWidth * (progress / 100);
progressBarFill.SetWidth(fillWidth);
// Update text
progressBarText.SetText(`Resurrecting ${targetName}... ${Math.floor(progress)}%`);
}
/**
* Complete resurrection UI
*/
function CompleteResurrectionUI(success: boolean, message: string): void {
isResurrecting = false;
resurrectProgress = 0;
if (progressBar) {
progressBar.Hide();
}
// Show message
if (message) {
// @ts-ignore
print(message);
}
// Play sound
if (success) {
// @ts-ignore
PlaySound(12867); // Success sound
} else {
// @ts-ignore
PlaySound(847); // Error sound
}
}
/**
* Interrupt resurrection UI
*/
function InterruptResurrectionUI(reason: string): void {
CompleteResurrectionUI(false, `Resurrection interrupted: ${reason}`);
}
// ============================================================================
// INITIALIZATION
// ============================================================================
/**
* Create a simple popup to confirm the client loaded
*/
function ShowLoadedPopup(): void {
// Create popup if it doesn't exist
if (!loadedPopup) {
// @ts-ignore
loadedPopup = CreateFrame("Frame", id("LoadedPopup"), UIParent);
loadedPopup.SetSize(300, 120);
loadedPopup.SetPoint("CENTER", UIParent, "CENTER", 0, 0);
// Make it movable
// @ts-ignore
loadedPopup.SetMovable(true);
// @ts-ignore
loadedPopup.EnableMouse(true);
// @ts-ignore
loadedPopup.RegisterForDrag("LeftButton");
// @ts-ignore
loadedPopup.SetScript("OnDragStart", function(self: any) { self.StartMoving(); });
// @ts-ignore
loadedPopup.SetScript("OnDragStop", function(self: any) { self.StopMovingOrSizing(); });
// Background - use solid color texture
// @ts-ignore
const bg = loadedPopup.CreateTexture(null, "BACKGROUND");
bg.SetAllPoints(loadedPopup);
// @ts-ignore
bg.SetTexture(0.05, 0.05, 0.05, 0.95); // Dark gray, almost opaque
// Border frame for a cleaner look
// @ts-ignore
const topBorder = loadedPopup.CreateTexture(null, "BORDER");
topBorder.SetPoint("TOPLEFT", loadedPopup, "TOPLEFT", 0, 0);
topBorder.SetPoint("TOPRIGHT", loadedPopup, "TOPRIGHT", 0, 0);
topBorder.SetHeight(2);
// @ts-ignore
topBorder.SetTexture(0.2, 1, 0.2, 1); // Green border
// Title text
// @ts-ignore
const title = loadedPopup.CreateFontString(null, "OVERLAY", "GameFontNormalLarge");
title.SetPoint("TOP", loadedPopup, "TOP", 0, -15);
title.SetText("Combat Resurrection");
title.SetTextColor(0.2, 1, 0.2);
// Message text
// @ts-ignore
const message = loadedPopup.CreateFontString(null, "OVERLAY", "GameFontNormal");
message.SetPoint("CENTER", loadedPopup, "CENTER", 0, 5);
message.SetText("Client UI Loaded Successfully!");
message.SetTextColor(1, 1, 1);
// Info text
// @ts-ignore
const info = loadedPopup.CreateFontString(null, "OVERLAY", "GameFontNormalSmall");
info.SetPoint("TOP", message, "BOTTOM", 0, -10);
info.SetText("Type /testrez to test the UI");
info.SetTextColor(0.8, 0.8, 0.8);
// Close button
// @ts-ignore
const closeBtn = CreateFrame("Button", null, loadedPopup, "UIPanelButtonTemplate");
closeBtn.SetSize(80, 25);
closeBtn.SetPoint("BOTTOM", loadedPopup, "BOTTOM", 0, 10);
// @ts-ignore
closeBtn.SetText("Close");
// @ts-ignore
closeBtn.SetScript("OnClick", () => {
if (loadedPopup) loadedPopup.Hide();
});
}
// Show the popup
if (loadedPopup) {
loadedPopup.Show();
}
}
/**
* Initialize the Combat Rez UI
*/
function InitializeCombatRezUI(): void {
print("[Combat Rez UI] InitializeCombatRezUI called!");
CreateReviveButton();
print("[Combat Rez UI] Revive button created");
CreateProgressBar();
print("[Combat Rez UI] Progress bar created");
CreateUpdateFrame();
print("[Combat Rez UI] Update frame created - checking for dead players every 0.1s");
print("[Combat Rez UI] Loaded successfully!");
print("[Combat Rez UI] Button will appear when near a dead party/raid member");
print("[Combat Rez UI] Type '/testrez button' to manually show the button");
// Show popup confirmation
ShowLoadedPopup();
print("[Combat Rez UI] Popup shown");
}
/**
* Test command to manually trigger UI elements
*/
function RegisterTestCommands(): void {
// @ts-ignore
_G["SLASH_COMBATREZTEST1"] = "/testrez";
// @ts-ignore
_G["SLASH_COMBATREZTEST2"] = "/reztest";
// @ts-ignore
_G.SlashCmdList["COMBATREZTEST"] = function(msg: string) {
const args = msg.toLowerCase().split(" ");
const cmd = args[0];
if (cmd === "popup") {
// Show the loaded popup
ShowLoadedPopup();
print("[Combat Rez UI] Popup shown.");
} else if (cmd === "button" || cmd === "") {
// Show the button with a test name
ShowReviveButton("TestPlayer");
print("[Combat Rez UI] Button shown. Click to test.");
} else if (cmd === "progress") {
// Show progress bar
StartResurrectionUI("TestPlayer");
// Simulate progress
let progress = 0;
const interval = 0.5; // Update every 0.5 seconds
// @ts-ignore
const ticker = CreateFrame("Frame", id("TestTicker"));
let elapsed = 0;
// @ts-ignore
ticker.SetScript("OnUpdate", function(self: any, delta: number) {
elapsed += delta;
if (elapsed >= interval) {
elapsed = 0;
progress += 10;
if (progress <= 100) {
UpdateProgressBar(progress, "TestPlayer");
} else {
// @ts-ignore
ticker.SetScript("OnUpdate", null);
CompleteResurrectionUI(true, "Test resurrection complete!");
}
}
});
} else if (cmd === "interrupt") {
// Test interruption
InterruptResurrectionUI("Test interruption");
} else if (cmd === "hide") {
// Hide everything
HideReviveButton();
if (progressBar) {
progressBar.Hide();
}
print("[Combat Rez UI] UI hidden.");
} else {
print("[Combat Rez UI] Commands:");
print(" /testrez popup - Show info popup");
print(" /testrez button - Show revive button");
print(" /testrez progress - Simulate full resurrection");
print(" /testrez interrupt - Test interruption");
print(" /testrez hide - Hide all UI");
}
};
print("[Combat Rez UI] Slash commands registered: /testrez or /reztest");
}
// Register AIO handlers
const CombatRezHandlers = aio.AddHandlers("CombatRez", {
/**
* Server tells client to start showing resurrection progress
*/
OnResurrectionStarted: (player: any, targetName: string) => {
StartResurrectionUI(targetName);
},
/**
* Server sends progress updates
*/
OnResurrectionProgress: (player: any, progress: number, targetName: string) => {
UpdateProgressBar(progress, targetName);
},
/**
* Server tells client resurrection completed
*/
OnResurrectionComplete: (player: any, success: boolean, message: string) => {
CompleteResurrectionUI(success, message);
},
/**
* Server tells client resurrection was interrupted
*/
OnResurrectionInterrupted: (player: any, reason: string) => {
InterruptResurrectionUI(reason);
},
});
// Initialize on load
// @ts-ignore
const initFrame = CreateFrame("Frame", id("InitFrame"));
// @ts-ignore
InitializeCombatRezUI();
RegisterTestCommands();
initFrame.Show();
}

View File

@@ -0,0 +1,733 @@
/**
* Combat Resurrection System
*
* Allows players to resurrect fallen allies during combat with:
* - 5-second channeling time
* - Proximity requirement (5 yards)
* - Movement/damage/spell interruption
* - 60-second cooldown
* - Group membership requirement
* - Visual and audio feedback
*/
/** @noSelfInFile **/
/** @ts-expect-error */
let aio: AIO = {};
// ============================================================================
// CONFIGURATION
// ============================================================================
interface CombatRezConfig {
// Core mechanics
channelTime: number; // Time in seconds to complete resurrection
maxRange: number; // Maximum distance in yards between players
healthPercent: number; // Health percentage restored on resurrection (0.0 - 1.0)
manaPercent: number; // Mana percentage restored on resurrection (0.0 - 1.0)
resurrectSickness: boolean; // Apply resurrection sickness debuff
// Restrictions
allowInCombat: boolean; // Allow resurrection while in combat
requireGroupMember: boolean; // Require players to be in same group
cooldownSeconds: number; // Cooldown per player before they can rez again
maxSimultaneousRez: number; // Max number of people that can rez same target
// Interruption conditions
interruptOnMovement: boolean; // Cancel if resurrector moves
interruptOnDamage: boolean; // Cancel if resurrector takes damage
interruptOnSpellCast: boolean; // Cancel if resurrector casts a spell
movementTolerance: number; // Distance in yards resurrector can move before interrupt
// Visual/Audio
castBarSpellId: number; // Spell ID for cast bar visual effect
channelSpellId: number; // Spell ID for channeling visual
soundStart: number; // Sound ID when starting resurrection
soundComplete: number; // Sound ID when resurrection completes
soundInterrupt: number; // Sound ID when interrupted
// Debugging
enableDebugMessages: boolean; // Show debug messages to players
}
const CONFIG: CombatRezConfig = {
// Core mechanics
channelTime: 5.0, // 5 seconds to complete
maxRange: 5.0, // 5 yards maximum distance
healthPercent: 0.30, // Resurrect with 30% health
manaPercent: 0.20, // Resurrect with 20% mana
resurrectSickness: false, // No resurrection sickness
// Restrictions
allowInCombat: true, // Allow mid-combat resurrection
requireGroupMember: true, // Must be in same group
cooldownSeconds: 60, // 60 second cooldown per player
maxSimultaneousRez: 1, // Only one person can rez a target at a time
// Interruption conditions
interruptOnMovement: true, // Cancel if resurrector moves
interruptOnDamage: true, // Cancel if resurrector takes damage
interruptOnSpellCast: true, // Cancel if resurrector casts spell
movementTolerance: 2.0, // 2 yards movement tolerance
// Visual/Audio (WoW 3.3.5a spell IDs and sounds)
castBarSpellId: 2006, // Resurrection spell for cast bar
channelSpellId: 2006, // Resurrection visual effect
soundStart: 8212, // Holy spell sound
soundComplete: 12867, // Success/completion sound
soundInterrupt: 847, // Error/interrupt sound
// Debugging
enableDebugMessages: true, // Show debug messages
};
// ============================================================================
// STATE TRACKING
// ============================================================================
interface RezAttempt {
rezzerGuid: number; // GUID of player performing resurrection
targetGuid: number; // GUID of dead player being resurrected
elapsedTime: number; // Current elapsed time in ms (updated by timed event)
startTimestamp: number; // Server timestamp when started (for damage tracking)
startX: number; // Starting X coordinate of resurrector
startY: number; // Starting Y coordinate of resurrector
startZ: number; // Starting Z coordinate of resurrector
mapId: number; // Map ID where resurrection is happening
instanceId: number; // Instance ID
timerEventId: number; // Timed event ID for the 100ms ticker
}
// Active resurrection attempts - key is resurrector GUID
const activeRezAttempts: Map<number, RezAttempt> = new Map();
// Resurrection cooldowns - key is player GUID, value is timestamp when cooldown expires
const rezCooldowns: Map<number, number> = new Map();
// Track damage taken for interruption - key is player GUID, value is last damage timestamp
const lastDamageTaken: Map<number, number> = new Map();
// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================
// Simple timestamp counter (incremented by MAP_EVENT_ON_UPDATE)
let serverTimestamp = 0;
/**
* Get current server time in milliseconds
*/
function GetServerTimeMs(): number {
return serverTimestamp;
}
/**
* Check if a player is on cooldown for resurrection
*/
function IsOnCooldown(playerGuid: number): boolean {
const cooldownExpiry = rezCooldowns.get(playerGuid);
if (!cooldownExpiry) return false;
const currentTime = GetServerTimeMs();
if (currentTime >= cooldownExpiry) {
rezCooldowns.delete(playerGuid);
return false;
}
return true;
}
/**
* Get remaining cooldown time in seconds
*/
function GetRemainingCooldown(playerGuid: number): number {
const cooldownExpiry = rezCooldowns.get(playerGuid);
if (!cooldownExpiry) return 0;
const currentTime = GetServerTimeMs();
const remaining = Math.max(0, cooldownExpiry - currentTime);
return Math.ceil(remaining / 1000);
}
/**
* Set cooldown for a player
*/
function SetCooldown(playerGuid: number): void {
const currentTime = GetServerTimeMs();
const cooldownExpiry = currentTime + (CONFIG.cooldownSeconds * 1000);
rezCooldowns.set(playerGuid, cooldownExpiry);
}
/**
* Calculate distance between two players
*/
function GetPlayerDistance(player1: Player, player2: Player): number {
const [x1, y1, z1] = player1.GetLocation();
const [x2, y2, z2] = player2.GetLocation();
const dx = x2 - x1;
const dy = y2 - y1;
const dz = z2 - z1;
return Math.sqrt(dx * dx + dy * dy + dz * dz);
}
/**
* Check if resurrector has moved too far from starting position
*/
function HasMovedTooFar(attempt: RezAttempt, currentX: number, currentY: number, currentZ: number): boolean {
const dx = currentX - attempt.startX;
const dy = currentY - attempt.startY;
const dz = currentZ - attempt.startZ;
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
return distance > CONFIG.movementTolerance;
}
/**
* Find dead player near the resurrector
*/
function FindNearbyDeadPlayer(resurrector: Player): Player | null {
const nearbyPlayers = resurrector.GetPlayersInRange(CONFIG.maxRange);
for (let i = 0; i < nearbyPlayers.length; i++) {
const player = nearbyPlayers[i];
if (player.IsDead() && player.GetGUIDLow() !== resurrector.GetGUIDLow()) {
return player;
}
}
return null;
}
/**
* Count how many players are currently resurrecting the target
*/
function CountActiveRezzersForTarget(targetGuid: number): number {
let count = 0;
activeRezAttempts.forEach((attempt) => {
if (attempt.targetGuid === targetGuid) {
count++;
}
});
return count;
}
/**
* Send debug message if debugging is enabled
*/
function DebugMessage(player: Player, message: string): void {
if (CONFIG.enableDebugMessages) {
player.SendBroadcastMessage(`[Combat Rez Debug] ${message}`);
}
}
// ============================================================================
// VALIDATION FUNCTIONS
// ============================================================================
/**
* Check if a player can start resurrecting another player
*/
function CanStartResurrection(resurrector: Player, target: Player): [boolean, string] {
// Check if resurrector is valid
if (!resurrector || !resurrector.IsInWorld()) {
return [false, "Resurrector is not valid"];
}
// Check if target is valid
if (!target || !target.IsInWorld()) {
return [false, "Target is not valid"];
}
// Check if resurrector is alive
if (resurrector.IsDead()) {
return [false, "You must be alive to resurrect others"];
}
// Check if target is dead
if (!target.IsDead()) {
return [false, "Target is not dead"];
}
// Check if resurrector is already resurrecting someone
if (activeRezAttempts.has(resurrector.GetGUIDLow())) {
return [false, "You are already resurrecting someone"];
}
// Check cooldown
if (IsOnCooldown(resurrector.GetGUIDLow())) {
const remaining = GetRemainingCooldown(resurrector.GetGUIDLow());
return [false, `You must wait ${remaining} seconds before resurrecting again`];
}
// Check if too many people are already resurrecting this target
const activeRezzers = CountActiveRezzersForTarget(target.GetGUIDLow());
if (activeRezzers >= CONFIG.maxSimultaneousRez) {
return [false, "Someone is already resurrecting this player"];
}
// Check combat restriction
if (!CONFIG.allowInCombat && resurrector.IsInCombat()) {
return [false, "You cannot resurrect while in combat"];
}
// Check group membership
if (CONFIG.requireGroupMember) {
const rezzerGroup = resurrector.GetGroup();
const targetGroup = target.GetGroup();
if (!rezzerGroup || !targetGroup) {
return [false, "You must be in the same group to resurrect"];
}
if (rezzerGroup.GetGUID() !== targetGroup.GetGUID()) {
return [false, "You must be in the same group to resurrect"];
}
}
// Check distance
const distance = GetPlayerDistance(resurrector, target);
if (distance > CONFIG.maxRange) {
return [false, `Target is too far away (${Math.floor(distance)} yards)`];
}
// Check if on same map
if (resurrector.GetMapId() !== target.GetMapId()) {
return [false, "Target is on a different map"];
}
return [true, "OK"];
}
// ============================================================================
// CORE RESURRECTION FUNCTIONS
// ============================================================================
/**
* Timed event that updates elapsed time every 100ms
*/
function OnResurrectionTimer(delay: number, repeats: number, resurrector: Player): void {
const attempt = activeRezAttempts.get(resurrector.GetGUIDLow());
if (!attempt) return;
// Increment elapsed time by the delay (100ms)
attempt.elapsedTime += delay;
// Check if complete
const channelTimeMs = CONFIG.channelTime * 1000;
if (attempt.elapsedTime >= channelTimeMs) {
const target = GetPlayerByGUID(attempt.targetGuid);
if (target) {
CompleteResurrection(resurrector, target);
}
}
}
/**
* Start a resurrection attempt
*/
function StartResurrection(resurrector: Player, target: Player): boolean {
const [canStart, reason] = CanStartResurrection(resurrector, target);
if (!canStart) {
resurrector.SendBroadcastMessage(reason);
resurrector.PlayDirectSound(CONFIG.soundInterrupt);
DebugMessage(resurrector, `Cannot start resurrection: ${reason}`);
return false;
}
const [x, y, z] = resurrector.GetLocation();
const channelTimeMs = CONFIG.channelTime * 1000;
// Register 100ms timer event - will run for channelTime duration
const timerEventId = resurrector.RegisterEvent(OnResurrectionTimer, 100, Math.ceil(channelTimeMs / 100));
const attempt: RezAttempt = {
rezzerGuid: resurrector.GetGUIDLow(),
targetGuid: target.GetGUIDLow(),
elapsedTime: 0,
startTimestamp: GetServerTimeMs(),
startX: x,
startY: y,
startZ: z,
mapId: resurrector.GetMapId(),
instanceId: resurrector.GetInstanceId(),
timerEventId: timerEventId,
};
activeRezAttempts.set(resurrector.GetGUIDLow(), attempt);
// Visual and audio feedback
resurrector.PlayDirectSound(CONFIG.soundStart);
resurrector.CastSpell(resurrector, CONFIG.channelSpellId, true);
// Notify both players
resurrector.SendBroadcastMessage(`Resurrecting ${target.GetName()}... Stay still!`);
target.SendBroadcastMessage(`${resurrector.GetName()} is resurrecting you...`);
// Send to client UI
aio.Handle(resurrector, "CombatRez", "OnResurrectionStarted", target.GetName());
DebugMessage(resurrector, `Started resurrection of ${target.GetName()}`);
return true;
}
/**
* Complete a resurrection attempt
*/
function CompleteResurrection(resurrector: Player, target: Player): void {
const attempt = activeRezAttempts.get(resurrector.GetGUIDLow());
if (!attempt) return;
// Cancel the timed event
if (attempt.timerEventId) {
resurrector.RemoveEventById(attempt.timerEventId);
}
// Remove the attempt
activeRezAttempts.delete(resurrector.GetGUIDLow());
// Set cooldown
SetCooldown(resurrector.GetGUIDLow());
// Resurrect the target
target.ResurrectPlayer(CONFIG.healthPercent, CONFIG.resurrectSickness);
// Restore mana if applicable
if (target.GetPowerType() === Powers.POWER_MANA) {
const maxMana = target.GetMaxPower(Powers.POWER_MANA);
const manaToRestore = Math.floor(maxMana * CONFIG.manaPercent);
target.SetPower(Powers.POWER_MANA, manaToRestore);
}
// Visual and audio feedback
resurrector.PlayDirectSound(CONFIG.soundComplete);
target.PlayDirectSound(CONFIG.soundComplete);
// Notify both players
resurrector.SendBroadcastMessage(`Successfully resurrected ${target.GetName()}!`);
target.SendBroadcastMessage(`You have been resurrected by ${resurrector.GetName()}!`);
// Send to client UI
aio.Handle(resurrector, "CombatRez", "OnResurrectionComplete", true, `Successfully resurrected ${target.GetName()}!`);
DebugMessage(resurrector, `Completed resurrection of ${target.GetName()}`);
}
/**
* Interrupt a resurrection attempt
*/
function InterruptResurrection(rezzerGuid: number, reason: string): void {
const attempt = activeRezAttempts.get(rezzerGuid);
if (!attempt) return;
// Get the resurrector player
const resurrector = GetPlayerByGUID(rezzerGuid);
// Cancel the timed event
if (resurrector && attempt.timerEventId) {
resurrector.RemoveEventById(attempt.timerEventId);
}
// Remove the attempt
activeRezAttempts.delete(rezzerGuid);
if (resurrector) {
resurrector.PlayDirectSound(CONFIG.soundInterrupt);
resurrector.SendBroadcastMessage(`Resurrection interrupted: ${reason}`);
resurrector.InterruptSpell(CurrentSpellTypes.CURRENT_CHANNELED_SPELL);
// Send to client UI
aio.Handle(resurrector, "CombatRez", "OnResurrectionInterrupted", reason);
DebugMessage(resurrector, `Resurrection interrupted: ${reason}`);
}
// Notify the target
const target = GetPlayerByGUID(attempt.targetGuid);
if (target) {
target.SendBroadcastMessage(`Resurrection interrupted!`);
}
}
/**
* Update all active resurrection attempts
*/
function UpdateResurrections(diff: number): void {
const currentTime = GetServerTimeMs();
const channelTimeMs = CONFIG.channelTime * 1000;
// Create array of attempts to avoid modifying map during iteration
const attempts: RezAttempt[] = [];
activeRezAttempts.forEach((attempt) => attempts.push(attempt));
for (const attempt of attempts) {
const resurrector = GetPlayerByGUID(attempt.rezzerGuid);
const target = GetPlayerByGUID(attempt.targetGuid);
// Validate both players still exist
if (!resurrector || !target) {
InterruptResurrection(attempt.rezzerGuid, "Player no longer available");
continue;
}
// Check if resurrector is still alive
if (resurrector.IsDead()) {
InterruptResurrection(attempt.rezzerGuid, "You died");
continue;
}
// Check if target is still dead
if (!target.IsDead()) {
InterruptResurrection(attempt.rezzerGuid, "Target is no longer dead");
continue;
}
// Check if players are still on same map
if (resurrector.GetMapId() !== attempt.mapId || target.GetMapId() !== attempt.mapId) {
InterruptResurrection(attempt.rezzerGuid, "Map changed");
continue;
}
// Check distance
const distance = GetPlayerDistance(resurrector, target);
if (distance > CONFIG.maxRange) {
InterruptResurrection(attempt.rezzerGuid, "Target moved too far away");
continue;
}
// Check if resurrector moved too far
if (CONFIG.interruptOnMovement) {
const [x, y, z] = resurrector.GetLocation();
if (HasMovedTooFar(attempt, x, y, z)) {
InterruptResurrection(attempt.rezzerGuid, "You moved too far");
continue;
}
}
// Check if resurrector took damage
if (CONFIG.interruptOnDamage) {
const lastDamage = lastDamageTaken.get(attempt.rezzerGuid);
if (lastDamage && lastDamage > attempt.startTimestamp) {
InterruptResurrection(attempt.rezzerGuid, "You took damage");
continue;
}
}
// elapsedTime is updated by the timed event, just read it
// Send progress updates to client UI
const progress = Math.floor((attempt.elapsedTime / channelTimeMs) * 100);
const lastProgress = Math.floor(((attempt.elapsedTime - diff) / channelTimeMs) * 100);
// Update UI every 10% or on significant changes
if (Math.floor(progress / 10) !== Math.floor(lastProgress / 10)) {
aio.Handle(resurrector, "CombatRez", "OnResurrectionProgress", progress, target.GetName());
if (CONFIG.enableDebugMessages) {
DebugMessage(resurrector, `Resurrection progress: ${progress}%`);
}
}
}
}
// ============================================================================
// EVENT HANDLERS
// ============================================================================
/**
* Handle player death - track for damage interruption
*/
const OnPlayerKilled: player_event_on_killed_by_creature = (
event: number,
killer: Creature,
killed: Player
) => {
// If player was resurrecting someone, interrupt it
if (activeRezAttempts.has(killed.GetGUIDLow())) {
InterruptResurrection(killed.GetGUIDLow(), "You died");
}
};
/**
* Handle spell cast - interrupt resurrection if configured
*/
const OnSpellCast: player_event_on_spell_cast = (
event: number,
player: Player,
spell: Spell,
skipCheck: boolean
) => {
if (!CONFIG.interruptOnSpellCast) return;
// Don't interrupt for the channeling spell itself
if (spell.GetEntry() === CONFIG.channelSpellId) return;
// If player is resurrecting someone, interrupt it
if (activeRezAttempts.has(player.GetGUIDLow())) {
InterruptResurrection(player.GetGUIDLow(), "You cast a spell");
}
};
/**
* Handle map updates - track resurrection progress
*/
const OnMapUpdate: map_event_on_update = (
event: number,
map: EMap,
diff: number
) => {
// Increment server timestamp
serverTimestamp += diff;
UpdateResurrections(diff);
};
/**
* Handle player entering combat - track for damage interruption
*/
const OnEnterCombat: player_event_on_enter_combat = (
event: number,
player: Player,
enemy: Unit
) => {
// Mark that player took damage (entering combat implies damage)
lastDamageTaken.set(player.GetGUIDLow(), GetServerTimeMs());
};
/**
* Handle player logout - cleanup
*/
const OnLogout: player_event_on_logout = (
event: number,
player: Player
) => {
const playerGuid = player.GetGUIDLow();
// Interrupt any active resurrection
if (activeRezAttempts.has(playerGuid)) {
InterruptResurrection(playerGuid, "You logged out");
}
// Clean up damage tracking
lastDamageTaken.delete(playerGuid);
};
// ============================================================================
// COMMAND HANDLER (for testing and manual triggering)
// ============================================================================
/**
* Handle player commands for resurrection
*/
const OnCommand: player_event_on_command = (
event: number,
player: Player,
command: string
): boolean => {
const [cmd, ...args] = command.toLowerCase().split(" ");
if (cmd === "rez" || cmd === "resurrect") {
const target = FindNearbyDeadPlayer(player);
if (!target) {
player.SendBroadcastMessage("No dead players nearby.");
return false;
}
StartResurrection(player, target);
return false;
}
if (cmd === "rezstatus") {
const attempt = activeRezAttempts.get(player.GetGUIDLow());
if (attempt) {
const progress = Math.floor((attempt.elapsedTime / (CONFIG.channelTime * 1000)) * 100);
player.SendBroadcastMessage(`Resurrection in progress: ${progress}%`);
} else {
player.SendBroadcastMessage("No active resurrection.");
}
if (IsOnCooldown(player.GetGUIDLow())) {
const remaining = GetRemainingCooldown(player.GetGUIDLow());
player.SendBroadcastMessage(`Cooldown: ${remaining} seconds remaining`);
}
return false;
}
return true; // Let other commands pass through
};
// ============================================================================
// AIO HANDLERS (Server-side)
// ============================================================================
/**
* Handle client request to start resurrection
*/
function HandleStartResurrection(player: Player, targetName: string): void {
// Find the target player by name
const target = GetPlayerByName(targetName);
if (!target) {
player.SendBroadcastMessage(`Could not find player: ${targetName}`);
aio.Handle(player, "CombatRez", "OnResurrectionComplete", false, `Could not find player: ${targetName}`);
return;
}
StartResurrection(player, target);
}
// ============================================================================
// AIO HANDLERS (Server-side)
// ============================================================================
const CombatRezServerHandlers = {
StartResurrection: (player: Player, targetName: string) => {
HandleStartResurrection(player, targetName);
},
};
// Register the handlers with AIO
aio.AddHandlers("CombatRez", CombatRezServerHandlers);
// ============================================================================
// EVENT REGISTRATION
// ============================================================================
// Register player events
RegisterPlayerEvent(
PlayerEvents.PLAYER_EVENT_ON_KILLED_BY_CREATURE,
(...args) => OnPlayerKilled(...args)
);
RegisterPlayerEvent(
PlayerEvents.PLAYER_EVENT_ON_SPELL_CAST,
(...args) => OnSpellCast(...args)
);
RegisterPlayerEvent(
PlayerEvents.PLAYER_EVENT_ON_ENTER_COMBAT,
(...args) => OnEnterCombat(...args)
);
RegisterPlayerEvent(
PlayerEvents.PLAYER_EVENT_ON_LOGOUT,
(...args) => OnLogout(...args)
);
RegisterPlayerEvent(
PlayerEvents.PLAYER_EVENT_ON_COMMAND,
(...args) => OnCommand(...args)
);
// Register server event for map updates (applies to all maps)
RegisterServerEvent(
ServerEvents.MAP_EVENT_ON_UPDATE,
(...args) => OnMapUpdate(...args)
);
print("[Combat Resurrection] Module loaded successfully!");
print(`[Combat Resurrection] Channel time: ${CONFIG.channelTime}s, Range: ${CONFIG.maxRange} yards, Cooldown: ${CONFIG.cooldownSeconds}s`);

View File

@@ -0,0 +1,95 @@
import { isFinalBoss } from "../classes/mapzones";
const InvisTriggerHalloween = 12999;
const VisualSpellH = 35847;
const RewardChestH = 951020;
// Zone IDs where Halloween chests can spawn
const ALLOWED_ZONES = [
532, // Karazhan
533, // Naxxramas
44 // Scarlet Monastery Graveyard
];
// Mythic difficulty flag
const MYTHIC_DIFFICULTY = 3;
const showVisualSpellH: gameobject_event_on_spawn = (event: number, gameObject: GameObject) => {
const invisCaster = gameObject.SpawnCreature(InvisTriggerHalloween, gameObject.GetX(), gameObject.GetY(), gameObject.GetZ(), 0, TempSummonType.TEMPSUMMON_MANUAL_DESPAWN);
const entry = gameObject.GetEntry();
invisCaster.CastSpellAoF(gameObject.GetX(), gameObject.GetY(), gameObject.GetZ(), VisualSpellH);
};
const removeVisualSpellH: gameobject_event_on_use = (event: number, gameObject: GameObject) => {
const creatures = gameObject.GetCreaturesInRange(3, InvisTriggerHalloween);
for(let i = 0; i < creatures.length; i++) {
creatures[i].DespawnOrUnsummon();
}
return false;
};
// Function to spawn Halloween chest when final boss is killed
const spawnHalloweenChest = (player: Player, boss: Creature): void => {
const map = player.GetMap();
// Check if in allowed zone
const mapId = map.GetMapId();
if (!ALLOWED_ZONES.includes(mapId)) {
return;
}
// Get boss position for chest spawning
const x = boss.GetX();
const y = boss.GetY();
const z = boss.GetZ();
const o = boss.GetO();
// Spawn the Halloween chest near the boss location (slightly offset to avoid collision)
const chest = player.SummonGameObject(RewardChestH, x + 2, y + 2, z, o, 300); // 5 minute despawn timer
if (chest) {
PrintInfo(`Spawned Halloween chest ${RewardChestH} at ${x}, ${y}, ${z} after ${boss.GetName()} kill in zone ${mapId} on Mythic difficulty`);
// Send spooky message to all players in the instance
const players = map.GetPlayers(TeamId.TEAM_ALLIANCE).concat(map.GetPlayers(TeamId.TEAM_HORDE));
for (let i = 0; i < players.length; i++) {
const instancePlayer = players[i];
if (instancePlayer) {
instancePlayer.SendChatMessageToPlayer(
ChatMsg.CHAT_MSG_RAID_BOSS_EMOTE,
Language.LANG_COMMON,
`|cffFF6600🎃 A mysterious Halloween chest has materialized from the shadows! 🎃|r`,
instancePlayer
);
}
}
}
};
// Function to handle final boss kills and spawn Halloween chests
const handleHalloweenBossKill: player_event_on_kill_creature = (
event: number,
killer: Player,
killed: Creature
): boolean => {
// Check if the killed creature is a final boss
PrintInfo("Checking if killed creature is a final boss for Halloween Chest")
PrintInfo(`Killed ${killed.GetName()} ${killed.GetEntry()} is final Boss ${isFinalBoss(killed.GetEntry())}`);
if (isFinalBoss(killed.GetEntry())) {
spawnHalloweenChest(killer, killed);
}
return false; // Don't interfere with other event handlers
};
// // Register the Halloween chest visual effects
RegisterGameObjectEvent(RewardChestH, GameObjectEvents.GAMEOBJECT_EVENT_ON_SPAWN, (...args) => showVisualSpellH(...args));
RegisterGameObjectEvent(RewardChestH, GameObjectEvents.GAMEOBJECT_EVENT_ON_USE, (...args) => removeVisualSpellH(...args));
// Register the boss kill event handler for Halloween chest spawning
RegisterPlayerEvent(PlayerEvents.PLAYER_EVENT_ON_KILL_CREATURE, (...args) => handleHalloweenBossKill(...args));

View File

@@ -0,0 +1,15 @@
/**
* This patch removes spells from the game introduced that did not have cooldowns making them overpowered.
*/
const player_event_on_spell_cast: player_event_on_spell_cast = (event: number, player: Player, spell: Spell, skipCheck: boolean) => {
// Sets the cooldown of the spell to 120000ms after being cast if the spell is between 8500000 and 8800000;
if(spell.GetEntry() >= 8500000 && spell.GetEntry() <= 8800000) {
spell.Cancel();
return true;
}
};
// Register
// RegisterPlayerEvent(PlayerEvents.PLAYER_EVENT_ON_SPELL_CAST, (...args) => player_event_on_spell_cast(...args));

View File

@@ -0,0 +1,126 @@
/**
* Necrolord Varkul - Blackrock Depths Boss
* This module handles the combat mechanics for Necrolord Varkul
*/
const VARKUL_NPC_ID = 400125;
const VARKUL_SPELL_IDS = {
SHADOW_BOLT: 12739,
FROST_NOVA: 9915,
FROST_ARMOR: 10220,
RAISE_DEAD: 52478,
BLINK: 1953,
SHADOWFLAME: 47897
};
const VARKUL_YELL_IDS = {
AGGRO: "The Scourge shall consume all who dare enter these depths!",
RAISE_DEAD: "Rise, my minions! Feast on their flesh!",
KILLED_PLAYER: "Your soul now belongs to the Scourge!"
};
function VarkulCastShadowBolt(delay: number, repeats: number, creature: Creature): void {
const victim = creature.GetVictim();
if (victim) {
creature.CastSpell(victim, VARKUL_SPELL_IDS.SHADOW_BOLT, false);
}
}
function VarkulCastFrostNova(delay: number, repeats: number, creature: Creature): void {
const victim = creature.GetVictim();
if (victim) {
creature.CastSpell(victim, VARKUL_SPELL_IDS.FROST_NOVA, true);
}
}
function VarkulCastFrostArmor(delay: number, repeats: number, creature: Creature): void {
creature.CastSpell(creature, VARKUL_SPELL_IDS.FROST_ARMOR, true);
}
function VarkulCastRaiseDead(delay: number, repeats: number, creature: Creature): void {
creature.SendUnitYell(VARKUL_YELL_IDS.RAISE_DEAD, 0);
creature.CastSpell(creature, VARKUL_SPELL_IDS.RAISE_DEAD, true);
}
function VarkulCastBlink(delay: number, repeats: number, creature: Creature): void {
creature.CastSpell(creature, VARKUL_SPELL_IDS.BLINK, true);
const nearestPlayer = creature.GetNearestPlayer(30, 1);
if (nearestPlayer) {
creature.AttackStart(nearestPlayer);
}
if (Math.floor(Math.random() * 2) + 1 === 1) {
creature.RegisterEvent(VarkulCastBlink, 1000, 1);
}
}
function VarkulCastShadowflame(delay: number, repeats: number, creature: Creature): void {
if (Math.floor(Math.random() * 4) + 1 <= 3) {
const victim = creature.GetVictim();
if (victim) {
creature.CastSpell(victim, VARKUL_SPELL_IDS.SHADOWFLAME, false);
}
}
}
const VarkulOnEnterCombat: creature_event_on_enter_combat = (event: number, creature: Creature, target: Unit) => {
creature.SendUnitYell(VARKUL_YELL_IDS.AGGRO, 0);
creature.RegisterEvent(VarkulCastShadowBolt, Math.floor(Math.random() * (6000 - 4000 + 1)) + 4000, 0);
creature.RegisterEvent(VarkulCastFrostNova, Math.floor(Math.random() * (14000 - 10000 + 1)) + 10000, 0);
creature.RegisterEvent(VarkulCastFrostArmor, 1000, 1);
creature.RegisterEvent(VarkulCastRaiseDead, Math.floor(Math.random() * (17000 - 13000 + 1)) + 13000, 0);
creature.RegisterEvent(VarkulCastBlink, Math.floor(Math.random() * (20000 - 16000 + 1)) + 16000, 0);
creature.RegisterEvent(VarkulCastShadowflame, 16000, 0);
return false;
};
const VarkulOnLeaveCombat: creature_event_on_leave_combat = (event: number, creature: Creature) => {
creature.RemoveEvents();
return false;
};
const VarkulOnTargetDied: creature_event_on_target_died = (event: number, creature: Creature, victim: Unit) => {
creature.SendUnitYell(VARKUL_YELL_IDS.KILLED_PLAYER, 0);
return false;
};
const VarkulOnDied: creature_event_on_died = (event: number, creature: Creature, killer: Unit) => {
creature.RemoveEvents();
return false;
};
const VarkulOnSpawn: creature_event_on_spawn = (event: number, creature: Creature) => {
creature.SetEquipmentSlots(60132, 0, 0);
return false;
};
RegisterCreatureEvent(
VARKUL_NPC_ID,
CreatureEvents.CREATURE_EVENT_ON_ENTER_COMBAT,
(...args) => VarkulOnEnterCombat(...args)
);
RegisterCreatureEvent(
VARKUL_NPC_ID,
CreatureEvents.CREATURE_EVENT_ON_LEAVE_COMBAT,
(...args) => VarkulOnLeaveCombat(...args)
);
RegisterCreatureEvent(
VARKUL_NPC_ID,
CreatureEvents.CREATURE_EVENT_ON_TARGET_DIED,
(...args) => VarkulOnTargetDied(...args)
);
RegisterCreatureEvent(
VARKUL_NPC_ID,
CreatureEvents.CREATURE_EVENT_ON_DIED,
(...args) => VarkulOnDied(...args)
);
RegisterCreatureEvent(
VARKUL_NPC_ID,
CreatureEvents.CREATURE_EVENT_ON_SPAWN,
(...args) => VarkulOnSpawn(...args)
);

View File

@@ -0,0 +1,85 @@
/**
* Prince Tenris Mirkblood - Karazhan Boss
* This module handles the combat mechanics for Prince Tenris Mirkblood
*/
const PRINCE_TENRIS_NPC_ID = 28194; // The id of Prince Tenris Mirkblood
const PRINCE_TENRIS_SPELL_IDS = {
BLOOD_MIRROR: 70838,
SUMMON_SANGUINE_SPIRIT: 51280,
SANGUINE_STRIKE: 51285,
BLOOD_SWOOP: 50923
};
function PrinceTenrisCastBloodMirror(delay: number, repeats: number, creature: Creature): void {
const raidMembers = creature.GetPlayersInRange(100); // Get all raid members in range
if (raidMembers.length > 0) {
const targetIndex = Math.floor(Math.random() * raidMembers.length);
const randomTarget = raidMembers[targetIndex];
if (randomTarget) {
creature.CastSpell(randomTarget, PRINCE_TENRIS_SPELL_IDS.BLOOD_MIRROR);
}
}
}
function PrinceTenrisCastSanguineSpirit(delay: number, repeats: number, creature: Creature): void {
const victim = creature.GetVictim();
if (victim) {
creature.CastSpell(victim, PRINCE_TENRIS_SPELL_IDS.SUMMON_SANGUINE_SPIRIT, true);
}
}
function PrinceTenrisCastSanguineStrike(delay: number, repeats: number, creature: Creature): void {
const victim = creature.GetVictim();
if (victim) {
creature.CastSpell(victim, PRINCE_TENRIS_SPELL_IDS.SANGUINE_STRIKE);
}
}
function PrinceTenrisCastBloodSwoop(delay: number, repeats: number, creature: Creature): void {
const raidMembers = creature.GetPlayersInRange(100); // Get all raid members in range
if (raidMembers.length > 0) {
const targetIndex = Math.floor(Math.random() * raidMembers.length);
const randomTarget = raidMembers[targetIndex];
if (randomTarget) {
creature.CastSpell(randomTarget, PRINCE_TENRIS_SPELL_IDS.BLOOD_SWOOP);
}
}
}
const PrinceTenrisOnEnterCombat: creature_event_on_enter_combat = (event: number, creature: Creature, target: Unit) => {
creature.RegisterEvent(PrinceTenrisCastBloodMirror, Math.floor(Math.random() * (42000 - 26000 + 1)) + 26000, 0);
creature.RegisterEvent(PrinceTenrisCastSanguineSpirit, Math.floor(Math.random() * (20000 - 14000 + 1)) + 14000, 0);
creature.RegisterEvent(PrinceTenrisCastSanguineStrike, Math.floor(Math.random() * (21000 - 13000 + 1)) + 13000, 0);
creature.RegisterEvent(PrinceTenrisCastBloodSwoop, 23000, 0);
return false;
};
const PrinceTenrisOnLeaveCombat: creature_event_on_leave_combat = (event: number, creature: Creature) => {
creature.RemoveEvents();
return false;
};
const PrinceTenrisOnDied: creature_event_on_died = (event: number, creature: Creature, killer: Unit) => {
creature.RemoveEvents();
return false;
};
RegisterCreatureEvent(
PRINCE_TENRIS_NPC_ID,
CreatureEvents.CREATURE_EVENT_ON_ENTER_COMBAT,
(...args) => PrinceTenrisOnEnterCombat(...args)
);
RegisterCreatureEvent(
PRINCE_TENRIS_NPC_ID,
CreatureEvents.CREATURE_EVENT_ON_LEAVE_COMBAT,
(...args) => PrinceTenrisOnLeaveCombat(...args)
);
RegisterCreatureEvent(
PRINCE_TENRIS_NPC_ID,
CreatureEvents.CREATURE_EVENT_ON_DIED,
(...args) => PrinceTenrisOnDied(...args)
);

11
tsconfig.single.json Normal file
View File

@@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "ESNext",
"lib": ["ESNext"],
"module": "CommonJS",
"outDir": "./dist/single-file",
"rootDir": "./",
"strict": true
},
"include": ["modules/UI/mythicplus/mythic_npcs.ts"]
}