AI IKn progress mod for item upgrades

This commit is contained in:
2025-11-23 00:14:06 -05:00
parent cbd77bcad9
commit f1af4f72db
9 changed files with 16007 additions and 141 deletions

174
AI_README.md Normal file
View File

@@ -0,0 +1,174 @@
# AI Developer Guide for ETS Module Collection
This repository contains World of Warcraft server modules written in TypeScript, which are transpiled to Lua using **TypeScriptToLua (TSTL)**. These modules utilize the **Eluna** engine and the **AIO (All In One)** library for client-server communication.
## 1. Core Technologies & Context
* **Eluna Engine**: The scripting engine used by the WoW server (AzerothCore). [Documentation](https://www.azerothcore.org/eluna/)
* **TypeScriptToLua (TSTL)**: Transpiles TypeScript code to Lua 5.2 compatible code.
* **AIO**: Handles communication between the server (Lua) and the client (Lua/XML).
* **UI Assets**:
* Interface files: `../3.3.5-interface-files`
* Textures: `../wow-ui-textures`
## 2. Critical Constraints
When writing code for this repository, you **MUST** adhere to the following constraints:
1. **TypeScript Limitations**:
* Not all TypeScript features are available. The code is transpiled to Lua.
* Avoid complex TypeScript features that do not map cleanly to Lua (e.g., complex generics, certain decorators).
2. **Lua Tuples**:
* Lua functions often return multiple values (tuples).
* TypeScript handles this via TSTL specific patterns (e.g., `const [val1, val2] = someLuaFunc()`).
3. **Import Restrictions**:
* **Imports can only be 1 level deep.**
* You cannot import a file that itself imports another file. Keep dependency chains flat.
4. **JavaScript Built-ins**:
* **Do NOT use specialized JavaScript built-ins** (e.g., `Promise`, `async/await`, complex `Array` methods not supported by TSTL).
* Only use features supported and compiled by TSTL into Lua.
5. **Module Naming & Structure**:
* AIO Modules **MUST** follow this naming convention:
* Server side: `[module].server.ts`
* Client side: `[module].client.ts`
* This naming is required for the build system to compile them correctly.
## 3. Architecture & Patterns
### AIO Modules (Client/Server)
AIO modules allow you to write both server-side logic and client-side UI logic in TypeScript.
**Pattern:**
* **Shared Logic**: Define interfaces and types in the server file or a shared file (if 1 level deep).
* **Server Entry (`.server.ts`)**:
* Handles game events, database interactions, and sends data to the client.
* Uses `aio.Handle(player, 'ModuleName', 'FunctionName', ...args)` to call client functions.
* **Client Entry (`.client.ts`)**:
* Handles UI creation, event listeners, and sends data to the server.
* **MUST** include the `if(!aio.AddAddon())` check to prevent execution on the server side during initialization.
* Uses `aio.Handle('ModuleName', 'FunctionName', ...args)` to call server functions.
### Example Structure (`botmgr`)
Refer to `modules/UI/botmgr` for a complete example.
**Server (`botmgr.server.ts`):**
```typescript
/** @ts-expect-error */
let aio: AIO = {}; // Polyfill for TS
import { BotUnit } from "./botUnit";
// Define handlers callable by client
const botMgrHandlers = aio.AddHandlers('BotMgr', {
"EquipTheItem": EquipTheItem,
"UnequipTheItem": UnequipTheItem
});
function EquipTheItem(player: Player, ...args) { ... }
```
**Client (`botmgr.client.ts`):**
```typescript
/** @noSelfInFile **/
/** @ts-expect-error */
let aio: AIO = {};
// Check to ensure this runs only on client
if(!aio.AddAddon()) {
const botMgrHandlers = aio.AddHandlers('BotMgr', {});
// Client-side handlers callable by server
botMgrHandlers.UpdateBotData = (data: BotData) => {
// Update UI
}
// Call server
aio.Handle("BotMgr", "EquipTheItem", ...args);
}
```
## 4. Development Tips
* **Global Polyfills**: Some globals are polyfilled in the main entry. Be aware of `incObjectEntries` and `incParseInt` patterns seen in examples.
* **UI Construction**: Use the `WoWAPI` types for creating frames and textures.
* **Debugging**: Use `print()` for basic logging.
## 5. Common Utilities & Classes
The repository provides shared utilities in `modules/classes/`. **Prefer using these over custom implementations.**
### Logger (`modules/classes/logger.ts`)
Wraps standard Eluna printing functions with log levels.
```typescript
import { Logger } from "../../classes/logger";
const log = new Logger("MyModule");
log.info("Something happened");
log.error("Something went wrong");
```
### UI Utils (`modules/classes/ui-utils.ts`)
Helper functions for common UI tasks.
* `colors(name)`: Returns WoW color codes (e.g., `colors('RED')`).
* `CreateItemButton(parent, name, itemId, ...)`: Creates a button with an item icon and tooltip.
* `MakeDraggable(frame)`: Enables drag-to-move on a frame.
* `EscapeCloseable(frame)`: Allows the frame to be closed with the Escape key.
## 6. Constants & ID Mappings
Centralized constants are located in `modules/constants/idmaps.ts`. **Always import from here** instead of hardcoding IDs.
* **Equipment**: `BotEquipSlot`, `BotSlotName`
* **Stats**: `BotStat`, `BotStatLabel`
* **Player Info**: `ClassesMapping`, `RacesMapping`, `TalentSpecs`
* **Items**: `ItemQuality`, `ItemStat`, `SocketBonus`
## 7. Server-Side Patterns
### Database Interaction
Use Eluna's global DB functions for character data.
```typescript
// Query
const result = CharDBQuery(`SELECT * FROM custom_table WHERE guid = ${player.GetGUID()}`);
if (result) {
const value = result.GetUInt32(0);
}
// Execute
CharDBExecute(`INSERT INTO custom_table (guid, value) VALUES (${player.GetGUID()}, 1)`);
```
### State Management
For modules requiring state (like `mythicplus`), use a `Map` keyed by player GUID or Entry ID.
```typescript
const StateStorage: Map<number, MyState> = new Map();
// Accessing state
const state = StateStorage.get(player.GetGUIDLow());
```
### Event Handling
Register events using global Eluna functions.
```typescript
RegisterPlayerEvent(PlayerEvents.PLAYER_EVENT_ON_COMMAND, (...args) => MyCommandHandler(...args));
RegisterGameObjectEvent(ENTRY_ID, GameObjectEvents.GAMEOBJECT_EVENT_ON_USE, (...args) => OnUse(...args));
```
## 8. Client-Side Patterns
### Frame Creation
Use standard WoW API (via `WoWAPI` types) to create frames.
```typescript
const frame = CreateFrame("Frame", "MyFrame", UIParent, "UIPanelDialogTemplate");
frame.SetSize(300, 300);
frame.SetPoint("CENTER");
```
### Sound
Use `PlaySoundFile` or `PlaySound` for audio feedback.
```typescript
PlaySound("igCharacterInfoOpen");
PlaySoundFile("Sound\\Interface\\LootCoinLarge.wav", "Master");
```

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,32 @@
Class Spec Priority 1 Priority 2 Priority 3 Priority 4 Priority 5 Priority 6 Priority 7 Priority 8
Druid Balance Spell Power Critical Strike Hit Rating Intellect Haste Spirit
Druid Feral (Tank) Agility Stamina Armor Defense Rating Dodge Rating Expertise
Druid Feral (DPS) Agility Hit Rating Critical Strike Haste Armor Penetration Expertise
Druid Restoration Spell Power Critical Strike Intellect Haste Spirit
Warrior Arms (DPS) Armor Penetration Hit Rating Strength Critical Strike Expertise Rating Haste Rating
Warrior Fury (DPS) Expertise Rating Hit Rating Armor Penetration Strength Critical Strike Haste Rating
Warrior Protection (Tank) Stamina Defense Rating Hit Rating Expertise Rating Armor Dodge Rating
Paladin Holy (Healer) Spell Haste Intellect Mp5 Spell Crit — —
Paladin Protection (Tank) Stamina Defense Rating Hit Rating Expertise Rating Block Value Dodge Rating
Paladin Retribution (DPS) Expertise Rating Hit Rating Strength Critical Strike Haste Rating Armor Penetration
Death Knight Blood (Tank) Hit Rating Defense Rating Stamina Expertise Rating Dodge Rating —
Death Knight Frost (DPS) Hit Rating Strength Expertise Rating Armor Penetration Critical Strike Haste Rating
Death Knight Unholy (DPS) Strength Hit Rating Haste Rating Armor Penetration Critical Strike Expertise Rating
Hunter Beast Mastery Attack Power Hit Rating Agility Critical Strike Armor Penetration Haste
Hunter Marksmanship Armor Penetration Hit Rating Agility Critical Strike Attack Power Haste
Hunter Survival Agility Hit Rating Critical Strike Attack Power Haste Armor Penetration
Rogue Assassination Agility Critical Strike Rating Hit Rating Attack Power Haste Rating Armor Penetration
Rogue Combat Agility Hit Rating Attack Power Armor Penetration Haste Rating Armor Penetration
Rogue Subtlety Agility Critical Strike Rating Critical Strike Rating Strength/Attack Power Haste Rating
Mage Arcane (DPS) Spell Power Spell Hit Rating Haste Crit Spirit Intellect Stamina Mp5
Mage Fire (DPS) Spell Power Hit Rating Crit Haste Spirit Intellect Stamina Mp5
Mage Frost (DPS) Spell Power Spell Hit Rating Haste Crit Spirit Intellect Stamina Mp5
Warlock Affliction Spell Power Hit Rating Haste Crit Spirit Intellect
Warlock Demonology Spell Power Hit Rating Haste Crit Spirit Intellect
Warlock Destruction Spell Power Hit Rating Haste Crit Spirit Intellect
Priest Discipline Spell Power Haste (soft cap) Crit Haste (past soft cap) Intellect Spirit Mp5
Priest Holy Intellect Haste Spell Power Spirit Crit Mp5 —
Priest Shadow Spell Power Hit Rating Haste Crit Spirit — —
Shaman Enhancement Expertise Hit Rating Attack Power Haste Crit Agility
Shaman Elemental Spell Power Hit Rating Haste Crit Intellect Mp5
Shaman Restoration Haste Spell Power Crit Mp5 Intellect Spirit

View File

@@ -635,6 +635,11 @@ const handleBossDeath: creature_event_on_died = (event: number, creature: Creatu
return false;
}
if(!config.spawnConfig) {
PrintError(`No spawn config found for boss ID ${bossId} on map ${mapId}`);
return false;
}
// Spawn the blue portal
const startLoc = config.spawnConfig.portalLocation;
const endLoc = config.spawnConfig.moveToLocation;
@@ -648,7 +653,7 @@ const handleBossDeath: creature_event_on_died = (event: number, creature: Creatu
);
// Spawn the special NPC at the portal location
const npc = portal.SpawnCreature(
const npc = portal?.SpawnCreature(
NPCIds[npcType],
startLoc.x, startLoc.y, startLoc.z, startLoc.o,
TempSummonType.TEMPSUMMON_TIMED_DESPAWN, despawnTime

View File

@@ -1,168 +1,257 @@
// /** @ts-expect-error */
// let aio: AIO = {};
/** @ts-expect-error */
let aio: AIO = {};
// import { Logger } from "../../classes/logger";
// import type { MythicPlusState } from "./mythicplus.state";
import { Logger } from "../../classes/logger";
import type { MythicPlusState } from "./mythicplus.state";
import { MythicEnchantments, ClassStatPriorities } from "../../constants/mythic-data";
import { ClassesMapping } from "../../constants/idmaps";
import { ItemAnalyzer, ItemRole } from "../../classes/item-analyzer";
// const logger = new Logger("MythicPlusMod");
const logger = new Logger("MythicPlusMod");
// // PlayerGUID -> MythicPlusState
// const StateStorage: Map<number, MythicPlusState> = new Map();
// PlayerGUID -> MythicPlusState
const StateStorage: Map<number, MythicPlusState> = new Map();
// // This looks up the current group id for the player -1 indicates no group
// function getPlayerGroupId(player: Player): number {
// const result = CharDBQuery(`SELECT m.guid FROM acore_characters.characters c left join acore_characters.group_member m on c.guid = m.memberGuid where c.guid = ${player.GetGUID()}`);
// This looks up the current group id for the player -1 indicates no group
function getPlayerGroupId(player: Player): number {
const result = CharDBQuery(`SELECT m.guid FROM acore_characters.characters c left join acore_characters.group_member m on c.guid = m.memberGuid where c.guid = ${player.GetGUID()}`);
// if(!result) {
// return -1;
// }
if(!result) {
return -1;
}
// return result.GetUInt32(0);
// }
return result.GetUInt32(0);
}
// // Get the difficulty alread set for the player or group
// function _getDifficulty(player: Player): number {
// const difficulty = player.GetDifficulty();
// const groupId = getPlayerGroupId(player);
// Get the difficulty alread set for the player or group
function _getDifficulty(player: Player): number {
const difficulty = player.GetDifficulty();
const groupId = getPlayerGroupId(player);
// logger.debug(`MythicPlusMod: Getting difficulty for ${player.GetName()} with difficulty ${difficulty} and group ${groupId}`);
logger.debug(`MythicPlusMod: Getting difficulty for ${player.GetName()} with difficulty ${difficulty} and group ${groupId}`);
// if(groupId == -1) {
// aio.Handle(player, "MythicPlus", "SetDifficulty", difficulty);
// }
if(groupId == -1) {
aio.Handle(player, "MythicPlus", "SetDifficulty", difficulty);
}
// const result = CharDBQuery(`SELECT difficulty FROM group_difficulty WHERE guid = ${groupId}`);
// if(result) {
// logger.debug(`MythicPlusMod: Setting difficulty for ${player.GetName()} to ${result.GetUInt32(0)}`);
// return result.GetUInt32(0);
// }
// }
const result = CharDBQuery(`SELECT difficulty FROM group_difficulty WHERE guid = ${groupId}`);
if(result) {
logger.debug(`MythicPlusMod: Setting difficulty for ${player.GetName()} to ${result.GetUInt32(0)}`);
return result.GetUInt32(0);
}
return 0;
}
// // Set the difficulty for the encounter
// function _setDifficulty(player: Player, difficulty: number): void {
// const groupId = getPlayerGroupId(player);
// const group = player.GetGroup();
// if(groupId == -1) {
// player.SendNotification('You must be in a group to set a mythic+ difficulty');
// return;
// }
// logger.debug(`Setting difficulty for ${player.GetName()} to ${difficulty}`);
// Set the difficulty for the encounter
function _setDifficulty(player: Player, difficulty: number): void {
const groupId = getPlayerGroupId(player);
const group = player.GetGroup();
if(groupId == -1) {
player.SendNotification('You must be in a group to set a mythic+ difficulty');
return;
}
logger.debug(`Setting difficulty for ${player.GetName()} to ${difficulty}`);
// if(! group.IsLeader(player.GetGUID())) {
// return;
// }
if(! group.IsLeader(player.GetGUID())) {
return;
}
// const map = player.GetMap();
// if(map.IsDungeon() != false) {
// player.SendNotification('You can not change the difficulty in a dungeon');
// return;
// }
const map = player.GetMap();
if(map.IsDungeon() != false) {
player.SendNotification('You can not change the difficulty in a dungeon');
return;
}
// // 0 is the lowest difficulty and 4 is the highest
// if(difficulty > 4) {
// logger.error(`Invalid difficulty set: ${difficulty}`);
// }
// 0 is the lowest difficulty and 4 is the highest
if(difficulty > 4) {
logger.error(`Invalid difficulty set: ${difficulty}`);
}
// if(difficulty == 0) {
// CharDBExecute(`DELETE FROM group_difficulty WHERE guid = ${groupId}`);
// return;
// }
if(difficulty == 0) {
CharDBExecute(`DELETE FROM group_difficulty WHERE guid = ${groupId}`);
return;
}
// CharDBExecute(`REPLACE INTO group_difficulty (guid, difficulty) VALUES (${groupId}, ${difficulty})`);
// }
CharDBExecute(`REPLACE INTO group_difficulty (guid, difficulty) VALUES (${groupId}, ${difficulty})`);
}
// function SetDifficulty(this:void, player: Player, difficulty: number): void {
// _setDifficulty(player, difficulty);
// aio.Handle(player, 'MythicPlus', 'UpdateState', StateStorage.get(player.GetGUIDLow()));
// }
function SetDifficulty(this:void, player: Player, difficulty: number): void {
_setDifficulty(player, difficulty);
aio.Handle(player, 'MythicPlus', 'UpdateState', StateStorage.get(player.GetGUIDLow()));
}
// // This is used to
// function _refreshState(player: Player) {
// if(player.IsInGroup()) {
// const groupId = getPlayerGroupId(player);
// const groupLeader = player.GetGroup().GetLeaderGUID();
// const isLeader = player.GetGUID() == groupLeader;
// const difficulty = _getDifficulty(player);
// StateStorage.set(player.GetGUIDLow(), {difficulty, inGroup: true, groupId, groupLeader, isLeader});
// return;
// } else {
// StateStorage.set(player.GetGUIDLow(), {difficulty: _getDifficulty(player), inGroup: false, groupId: -1, groupLeader: -1, isLeader: false});
// This is used to
function _refreshState(player: Player) {
if(player.IsInGroup()) {
const groupId = getPlayerGroupId(player);
const groupLeader = player.GetGroup().GetLeaderGUID();
const isLeader = player.GetGUID() == groupLeader;
const difficulty = _getDifficulty(player);
StateStorage.set(player.GetGUIDLow(), {difficulty, inGroup: true, groupId, groupLeader, isLeader});
return;
} else {
StateStorage.set(player.GetGUIDLow(), {difficulty: _getDifficulty(player), inGroup: false, groupId: -1, groupLeader: -1, isLeader: false});
}
}
// Update the state from what is on the server and send it back to the client.
function GetState(this:void, player: Player): void {
_refreshState(player);
const state = StateStorage.get(player.GetGUIDLow());
aio.Handle(player, 'MythicPlus', 'UpdateState', state);
}
// This is the command to open the mythic plus panel
const OpenUI: player_event_on_command = (event: number,player: Player, command: string): boolean => {
if(command == 'mythicplus') {
const state = StateStorage.get(player.GetGUIDLow());
logger.debug(`OpenUI command
player: ${player.GetName()},
difficulty ${state.difficulty},
groupId: ${state.groupId},
groupLeader: ${state.groupLeader},
isLeader: ${state.isLeader}`
);
aio.Handle(player, 'MythicPlus', 'ShowUI', StateStorage.get(player.GetGUIDLow()));
return false;
}
return true;
};
// This enables the client to fire a request to open the mythic plus panel after updating state
function ShowUI(this:void, player: Player): void {
_refreshState(player);
aio.Handle(player, 'MythicPlus', 'ShowUI', StateStorage.get(player.GetGUIDLow()));
}
RegisterPlayerEvent(PlayerEvents.PLAYER_EVENT_ON_COMMAND, (...args) => OpenUI(...args));
const MPStartState: player_event_on_login = (_event: number, player: Player): void => {
_refreshState(player);
aio.Handle(player, 'MythicPlus', 'UpdateState', StateStorage.get(player.GetGUIDLow()));
};
// On login set up the mythic panel mod state for the player
RegisterPlayerEvent(PlayerEvents.PLAYER_EVENT_ON_LOGIN, (...args) => MPStartState(...args));
// const MPGroupDisband: group_event_on_disband = (_event: number, group: Group): void => {
// const members = group.GetMembers();
// for(let i = 0; i < members.length; i++) {
// _refreshState(members[i]);
// aio.Handle(members[i], 'MythicPlus', 'UpdateState', StateStorage.get(members[i].GetGUIDLow()));
// }
// }
// // Update the state from what is on the server and send it back to the client.
// function GetState(this:void, player: Player): void {
// _refreshState(player);
// const state = StateStorage.get(player.GetGUIDLow());
// aio.Handle(player, 'MythicPlus', 'UpdateState', state);
// RegisterGroupEvent(GroupEvents.GROUP_EVENT_ON_DISBAND, (...args) => MPGroupDisband(...args));
// When a leader change happens need to update the state storage for each leader to enable changes in the panel.
// const MPLeaderChange: group_event_on_leader_change = (_event: number,group: Group, leader: number, oldLeader: number): void => {
// _refreshState(GetPlayerByGUID(leader));
// aio.Handle(GetPlayerByGUID(leader), 'MythicPlus', 'UpdateState', StateStorage.get(leader));
// _refreshState(GetPlayerByGUID(oldLeader));
// aio.Handle(GetPlayerByGUID(oldLeader), 'MythicPlus', 'UpdateState', StateStorage.get(oldLeader));
// }
// // This is the command to open the mythic plus panel
// const OpenUI: player_event_on_command = (event: number,player: Player, command: string): boolean => {
// if(command == 'mythicplus') {
// const state = StateStorage.get(player.GetGUIDLow());
// logger.debug(`OpenUI command
// player: ${player.GetName()},
// difficulty ${state.difficulty},
// groupId: ${state.groupId},
// groupLeader: ${state.groupLeader},
// isLeader: ${state.isLeader}`
// );
// aio.Handle(player, 'MythicPlus', 'ShowUI', StateStorage.get(player.GetGUIDLow()));
// return false;
// }
// return true;
// };
// // This enables the client to fire a request to open the mythic plus panel after updating state
// function ShowUI(this:void, player: Player): void {
// _refreshState(player);
// aio.Handle(player, 'MythicPlus', 'ShowUI', StateStorage.get(player.GetGUIDLow()));
// }
// RegisterPlayerEvent(PlayerEvents.PLAYER_EVENT_ON_COMMAND, (...args) => OpenUI(...args));
// const MPStartState: player_event_on_login = (_event: number, player: Player): void => {
// _refreshState(player);
// aio.Handle(player, 'MythicPlus', 'UpdateState', StateStorage.get(player.GetGUIDLow()));
// };
// // On login set up the mythic panel mod state for the player
// RegisterPlayerEvent(PlayerEvents.PLAYER_EVENT_ON_LOGIN, (...args) => MPStartState(...args));
// // const MPGroupDisband: group_event_on_disband = (_event: number, group: Group): void => {
// // const members = group.GetMembers();
// // for(let i = 0; i < members.length; i++) {
// // _refreshState(members[i]);
// // aio.Handle(members[i], 'MythicPlus', 'UpdateState', StateStorage.get(members[i].GetGUIDLow()));
// // }
// // }
// // RegisterGroupEvent(GroupEvents.GROUP_EVENT_ON_DISBAND, (...args) => MPGroupDisband(...args));
// // When a leader change happens need to update the state storage for each leader to enable changes in the panel.
// // const MPLeaderChange: group_event_on_leader_change = (_event: number,group: Group, leader: number, oldLeader: number): void => {
// // _refreshState(GetPlayerByGUID(leader));
// // aio.Handle(GetPlayerByGUID(leader), 'MythicPlus', 'UpdateState', StateStorage.get(leader));
// // _refreshState(GetPlayerByGUID(oldLeader));
// // aio.Handle(GetPlayerByGUID(oldLeader), 'MythicPlus', 'UpdateState', StateStorage.get(oldLeader));
// // }
// // RegisterGroupEvent(GroupEvents.GROUP_EVENT_ON_LEADER_CHANGE, (...args) => MPLeaderChange(...args));
// RegisterGroupEvent(GroupEvents.GROUP_EVENT_ON_LEADER_CHANGE, (...args) => MPLeaderChange(...args));
// const DeletePlayerState: player_event_on_logout = (event: number, player: Player) => {
// StateStorage.delete(player.GetGUIDLow());
// };
const DeletePlayerState: player_event_on_logout = (event: number, player: Player) => {
StateStorage.delete(player.GetGUIDLow());
};
// RegisterPlayerEvent(PlayerEvents.PLAYER_EVENT_ON_LOGOUT, (...args) => DeletePlayerState(...args));
RegisterPlayerEvent(PlayerEvents.PLAYER_EVENT_ON_LOGOUT, (...args) => DeletePlayerState(...args));
// // API Handlers available to the client
// const MPHandlers = aio.AddHandlers("MythicPlus", {
// ShowUI,
// SetDifficulty,
// GetState,
// });
// API Handlers available to the client
const MPHandlers = aio.AddHandlers("MythicPlus", {
ShowUI,
SetDifficulty,
GetState,
});
// --- Mythic Enchantment Logic ---
function GetEnchantmentForStat(stat: string): number | null {
// Map stat names from priorities to enchantment effect names or logic
// This is a simplified matching logic. In a real scenario, we'd need a more robust mapping.
// For now, we search for the stat name in the enchantment name or effect.
// Simple mapping for demonstration
const statMap: {[key: string]: string} = {
"Spell Power": "Spell Power",
"Critical Strike": "Critical Strike",
"Hit Rating": "Hit Rating",
"Intellect": "Mana", // Approximation
"Haste": "Haste",
"Spirit": "Mana", // Approximation
"Agility": "Agility",
"Stamina": "Health",
"Armor": "Defense", // Approximation
"Defense Rating": "Defense",
"Dodge Rating": "Dodge",
"Expertise": "Expertise", // Might not exist in JSON
"Strength": "Strength", // Might not exist in JSON
"Armor Penetration": "Armor Penetration", // Might not exist in JSON
"Attack Power": "Attack Power",
"Mp5": "Mana per 5 sec",
"Spell Crit": "Critical Strike",
"Spell Haste": "Haste",
"Block Value": "Block",
};
const targetEffect = statMap[stat];
if (!targetEffect) return null;
// Filter enchantments that match the target effect
const candidates = MythicEnchantments.filter(e => e.name && e.name.includes(targetEffect));
if (candidates.length === 0) return null;
// Pick a random one
const choice = candidates[Math.floor(Math.random() * candidates.length)];
return choice.ID;
}
// ... (existing code)
const OnLootItem: player_event_on_loot_item = (event: number, player: Player, item: Item, count: number): void => {
// 1. Check if Mythic+ is active (difficulty > 0)
const state = StateStorage.get(player.GetGUIDLow());
if (!state || state.difficulty <= 0) {
return;
}
// 2. Check Item Quality (Epic or higher)
if (item.GetQuality() < 4) { // 4 = Epic
return;
}
// 3. Analyze Item Role
const analyzer = new ItemAnalyzer(item);
const role = analyzer.GetRole();
if (role === ItemRole.UNKNOWN) {
// Fallback or skip? Let's skip for now to avoid weird enchants.
return;
}
// 4. Pick a stat based on Role
const targetStat = analyzer.GetBestStatForRole(role);
// 5. Find enchantment
const enchantId = GetEnchantmentForStat(targetStat);
if (enchantId) {
// Apply enchantment to Slot 7 (Bonus Slot) to avoid conflicts with standard enchants (Slot 0) or sockets.
// Standard slots: 0=Perm, 1=Temp, 2-4=Sockets, 5=Bonus, 6=Prismatic
const BONUS_SLOT = 7;
item.SetEnchantment(BONUS_SLOT, enchantId);
player.SendNotification(`Mythic Bonus! Your ${item.GetItemLink()} (${role}) was enchanted with ${targetStat}!`);
logger.info(`Applied enchantment ${enchantId} (${targetStat}) to ${item.GetName()} [${role}] for ${player.GetName()} in slot ${BONUS_SLOT}`);
}
};
RegisterPlayerEvent(PlayerEvents.PLAYER_EVENT_ON_LOOT_ITEM, (...args) => OnLootItem(...args));

View File

@@ -0,0 +1,154 @@
import { BotStat } from "../constants/idmaps";
export enum ItemRole {
TANK = "Tank",
HEALER = "Healer",
PHYSICAL_DPS = "PhysicalDPS",
CASTER_DPS = "CasterDPS",
UNKNOWN = "Unknown"
}
export class ItemAnalyzer {
private item: Item;
private stats: Map<number, number> = new Map();
constructor(item: Item) {
this.item = item;
this.parseStats();
}
private parseStats() {
const entry = this.item.GetEntry();
// Query item_template for stats
// Note: item_template has 10 stat slots usually, but let's check 8 as per example
const sql = `SELECT
stat_type1, stat_value1,
stat_type2, stat_value2,
stat_type3, stat_value3,
stat_type4, stat_value4,
stat_type5, stat_value5,
stat_type6, stat_value6,
stat_type7, stat_value7,
stat_type8, stat_value8,
stat_type9, stat_value9,
stat_type10, stat_value10
FROM item_template WHERE entry = ${entry}`;
const result = WorldDBQuery(sql);
if (result) {
for (let i = 1; i <= 10; i++) {
const type = result.GetUInt32( (i-1)*2 ); // 0, 2, 4...
const value = result.GetInt32( (i-1)*2 + 1 ); // 1, 3, 5...
if (type > 0 && value !== 0) {
this.stats.set(type, value);
}
}
}
}
public GetRole(): ItemRole {
const subClass = this.item.GetSubClass();
const itemClass = this.item.GetClass();
// 2 = Weapon, 4 = Armor
if (itemClass === 4) { // Armor
// Check for Tank Stats
if (this.hasAnyStat([
BotStat.DEFENSE_SKILL_RATING,
BotStat.DODGE_RATING,
BotStat.PARRY_RATING,
BotStat.BLOCK_RATING,
BotStat.BLOCK_VALUE
])) {
return ItemRole.TANK;
}
// Check for Healer Stats (Spirit, Mp5)
// Note: Some caster gear has Spirit, but Mp5 is very healer specific usually.
if (this.hasAnyStat([BotStat.MANA_REGENERATION])) {
return ItemRole.HEALER;
}
// Intellect/Spell Power -> Caster or Healer
if (this.hasAnyStat([BotStat.INTELLECT, BotStat.SPELL_POWER])) {
// If Plate + Int -> Holy Paladin -> Healer
// (Ret uses Str, Prot uses Stam/Str/Def)
if (subClass === 4) { // Plate
return ItemRole.HEALER;
}
// If Mail + Int -> Shaman (Ele/Resto) or Paladin (Low level) -> 80 is Shaman
// Ele = Caster, Resto = Healer.
// Hard to distinguish without Hit Rating (Caster) vs Spirit/Mp5 (Healer)
if (this.hasAnyStat([BotStat.HIT_RATING, BotStat.HIT_SPELL_RATING])) {
return ItemRole.CASTER_DPS;
}
// If no Hit, and has Int, lean towards Healer if it has Spirit?
// Warlocks/Mages use Spirit too.
// Let's default to Caster DPS unless it has Mp5 or is Plate.
// Actually, let's check for Spirit.
if (this.hasAnyStat([BotStat.SPIRIT])) {
// Cloth + Spirit + Hit = Warlock/Mage/Priest DPS
// Cloth + Spirit + No Hit = Priest Healer?
if (this.hasAnyStat([BotStat.HIT_RATING])) {
return ItemRole.CASTER_DPS;
}
// Ambiguous. Let's default to Healer for Spirit items without Hit?
return ItemRole.HEALER;
}
return ItemRole.CASTER_DPS;
}
// Agility/Strength/AP -> Physical DPS or Tank (if no def stats)
if (this.hasAnyStat([
BotStat.AGILITY,
BotStat.STRENGTH,
BotStat.ATTACK_POWER,
BotStat.ARMOR_PENETRATION_RATING,
BotStat.EXPERTISE_RATING
])) {
// If Plate + Strength + No Tank Stats -> Ret/DK/War DPS
return ItemRole.PHYSICAL_DPS;
}
}
if (itemClass === 2) { // Weapon
// Similar logic
if (this.hasAnyStat([BotStat.SPELL_POWER, BotStat.INTELLECT])) {
if (this.hasAnyStat([BotStat.MANA_REGENERATION])) return ItemRole.HEALER;
if (this.hasAnyStat([BotStat.HIT_RATING])) return ItemRole.CASTER_DPS;
return ItemRole.CASTER_DPS; // Default
}
if (this.hasAnyStat([BotStat.DEFENSE_SKILL_RATING, BotStat.DODGE_RATING, BotStat.PARRY_RATING])) {
return ItemRole.TANK;
}
return ItemRole.PHYSICAL_DPS;
}
return ItemRole.UNKNOWN;
}
private hasAnyStat(stats: number[]): boolean {
for (const s of stats) {
if (this.stats.has(s)) return true;
}
return false;
}
public GetBestStatForRole(role: ItemRole): string {
switch (role) {
case ItemRole.TANK:
return "Stamina"; // Safe bet
case ItemRole.HEALER:
return "Spell Power";
case ItemRole.CASTER_DPS:
return "Spell Power"; // or Haste/Crit
case ItemRole.PHYSICAL_DPS:
return "Attack Power"; // or Agi/Str
default:
return "Stamina";
}
}
}

File diff suppressed because it is too large Load Diff