mirror of
https://github.com/araxiaonline/ets-module-collection.git
synced 2026-06-13 02:52:20 -04:00
capturing latest changes before letting AI take over
This commit is contained in:
10
.vscode/settings.json
vendored
Normal file
10
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"WAPI"
|
||||
],
|
||||
"cSpell.ignoreWords": [
|
||||
"BLACKWING",
|
||||
"GURUB",
|
||||
"undeath"
|
||||
]
|
||||
}
|
||||
237
development/gameplay/COMBAT_REZ_README.md
Normal file
237
development/gameplay/COMBAT_REZ_README.md
Normal 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.
|
||||
550
development/gameplay/combat-resurrection.client.ts
Normal file
550
development/gameplay/combat-resurrection.client.ts
Normal 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();
|
||||
|
||||
}
|
||||
733
development/gameplay/combat-resurrection.ts
Normal file
733
development/gameplay/combat-resurrection.ts
Normal 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`);
|
||||
95
modules/gameobject/halloweenchest.ts
Normal file
95
modules/gameobject/halloweenchest.ts
Normal 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));
|
||||
15
modules/patches/patch-2025901.ts
Normal file
15
modules/patches/patch-2025901.ts
Normal 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));
|
||||
126
modules/zones/brd/necrolord_varkul.ts
Normal file
126
modules/zones/brd/necrolord_varkul.ts
Normal 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)
|
||||
);
|
||||
85
modules/zones/kara/prince_tenaris.ts
Normal file
85
modules/zones/kara/prince_tenaris.ts
Normal 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
11
tsconfig.single.json
Normal 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"]
|
||||
}
|
||||
Reference in New Issue
Block a user