mirror of
https://github.com/araxiaonline/ets-module-collection.git
synced 2026-06-13 02:52:20 -04:00
AI IKn progress mod for item upgrades
This commit is contained in:
174
AI_README.md
Normal file
174
AI_README.md
Normal 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");
|
||||
```
|
||||
13762
data/mythic-heroic/enchantments.json
Normal file
13762
data/mythic-heroic/enchantments.json
Normal file
File diff suppressed because it is too large
Load Diff
32
data/mythic-heroic/stat-preference.md
Normal file
32
data/mythic-heroic/stat-preference.md
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
154
modules/classes/item-analyzer.ts
Normal file
154
modules/classes/item-analyzer.ts
Normal 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";
|
||||
}
|
||||
}
|
||||
}
|
||||
1650
modules/constants/mythic-data.ts
Normal file
1650
modules/constants/mythic-data.ts
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user