diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1398f90 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "cSpell.words": [ + "WAPI" + ], + "cSpell.ignoreWords": [ + "BLACKWING", + "GURUB", + "undeath" + ] +} \ No newline at end of file diff --git a/development/gameplay/COMBAT_REZ_README.md b/development/gameplay/COMBAT_REZ_README.md new file mode 100644 index 0000000..e8ecd65 --- /dev/null +++ b/development/gameplay/COMBAT_REZ_README.md @@ -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. diff --git a/development/gameplay/combat-resurrection.client.ts b/development/gameplay/combat-resurrection.client.ts new file mode 100644 index 0000000..6d863bd --- /dev/null +++ b/development/gameplay/combat-resurrection.client.ts @@ -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(); + +} diff --git a/development/gameplay/combat-resurrection.ts b/development/gameplay/combat-resurrection.ts new file mode 100644 index 0000000..59fc91d --- /dev/null +++ b/development/gameplay/combat-resurrection.ts @@ -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 = new Map(); + +// Resurrection cooldowns - key is player GUID, value is timestamp when cooldown expires +const rezCooldowns: Map = new Map(); + +// Track damage taken for interruption - key is player GUID, value is last damage timestamp +const lastDamageTaken: Map = 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`); diff --git a/modules/gameobject/halloweenchest.ts b/modules/gameobject/halloweenchest.ts new file mode 100644 index 0000000..3ee6114 --- /dev/null +++ b/modules/gameobject/halloweenchest.ts @@ -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)); diff --git a/modules/patches/patch-2025901.ts b/modules/patches/patch-2025901.ts new file mode 100644 index 0000000..51bbaae --- /dev/null +++ b/modules/patches/patch-2025901.ts @@ -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)); diff --git a/modules/zones/brd/necrolord_varkul.ts b/modules/zones/brd/necrolord_varkul.ts new file mode 100644 index 0000000..3bfe8c6 --- /dev/null +++ b/modules/zones/brd/necrolord_varkul.ts @@ -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) +); diff --git a/modules/zones/kara/prince_tenaris.ts b/modules/zones/kara/prince_tenaris.ts new file mode 100644 index 0000000..32111b8 --- /dev/null +++ b/modules/zones/kara/prince_tenaris.ts @@ -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) +); diff --git a/tsconfig.single.json b/tsconfig.single.json new file mode 100644 index 0000000..461dd9b --- /dev/null +++ b/tsconfig.single.json @@ -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"] +}