mirror of
https://github.com/araxiaonline/ets-module-collection.git
synced 2026-06-13 02:52:20 -04:00
1176 lines
44 KiB
TypeScript
1176 lines
44 KiB
TypeScript
/** @ts-expect-error */
|
|
let aio: AIO = {};
|
|
|
|
import { MapNames, MapIds, BossIDs } from "../../classes/mapzones";
|
|
import * as Spells from "./mythic_custom_spells";
|
|
import { SetTrigger, GetTrigger } from "../../classes/triggers";
|
|
|
|
/**
|
|
* This is the file for managing new NPC interactions
|
|
* - Mick Ashwild - 9500561 teaches leather/fire fusions
|
|
* - Thorin Firehand - 9500562 teaches ore/cold fusions
|
|
* - Elowyn Threadbinder - 9500563 teaches cloth/arcane fusions
|
|
* - Shivey - 9500564 teaches alchemy/nature fusions
|
|
* - ??? - 9500565 teaches gem/essence fusions
|
|
* - ??? - Old Witch teaches shadow fusion
|
|
*
|
|
* The player will meet different characters at different times so the hello will check to see whch
|
|
* audio file and gossip menu needs to be shown. The list of locations an npc will show up is below
|
|
*
|
|
* Mick
|
|
* Rare Leather Fusion : Wailing Caverns Verdan the everliving
|
|
* Mythic Leather Fusion : Sartharion Obsidian Sanctum
|
|
* Rare Fire Fusion : Hellfire Ramparts Nazan + Vazruden
|
|
* Mythic Fire Fusion : Onyxia Lair Flamegor
|
|
* Casual Spawns : World ends Tavern Shatrath
|
|
*
|
|
* Thorin
|
|
* Rare Ore Fusion : BRD Magmus
|
|
* Mythic Ore Fusion : Ragnaros
|
|
* Casual Spawns : The Great Forge
|
|
*/
|
|
|
|
export enum NPCType {
|
|
MICK_ASHWILD = 'mick',
|
|
THORIN_FIREHAND = 'thorin',
|
|
ELOWYN_THREADBINDER = 'elowen',
|
|
SHIVEY = 'shivey',
|
|
STEVE = 'steve',
|
|
VAERIC = 'vaeric',
|
|
AGATHA = 'agatha',
|
|
SYLVIA = 'sylvia',
|
|
|
|
}
|
|
|
|
export const NPCIds: Record<NPCType, number> = {
|
|
[NPCType.MICK_ASHWILD]: 9500561,
|
|
[NPCType.THORIN_FIREHAND]: 9500562,
|
|
[NPCType.ELOWYN_THREADBINDER]: 9500563,
|
|
[NPCType.SHIVEY]: 9500564,
|
|
[NPCType.STEVE]: 9500565,
|
|
[NPCType.VAERIC]: 9500566,
|
|
[NPCType.AGATHA]: 9500567,
|
|
[NPCType.SYLVIA]: 9500568,
|
|
|
|
};
|
|
|
|
export enum GobjectType {
|
|
PORTAL = 'portal',
|
|
}
|
|
|
|
export const GobjectIds: Record<GobjectType, number> = {
|
|
[GobjectType.PORTAL]: 181508
|
|
}
|
|
|
|
const AUDIO_BASE_PATH = "Interface\\Modules\\MythicPlus\\Audio\\";
|
|
|
|
export const AudioPaths: Record<NPCType, string> = {
|
|
[NPCType.MICK_ASHWILD]: AUDIO_BASE_PATH + "Mick\\mick-",
|
|
[NPCType.THORIN_FIREHAND]: AUDIO_BASE_PATH + "Thorin\\thorin-",
|
|
[NPCType.ELOWYN_THREADBINDER]: AUDIO_BASE_PATH + "Elowen\\elowen-",
|
|
[NPCType.SHIVEY]: AUDIO_BASE_PATH + "Shivey\\shivey-",
|
|
[NPCType.STEVE]: AUDIO_BASE_PATH + "Steve\\steve-",
|
|
[NPCType.VAERIC]: AUDIO_BASE_PATH + "Vaeric\\vaeric-",
|
|
[NPCType.AGATHA]: AUDIO_BASE_PATH + "Agatha\\agatha-",
|
|
[NPCType.SYLVIA]: AUDIO_BASE_PATH + "Sylvia\\sylvia-",
|
|
};
|
|
|
|
function getAudioFile(npcName: NPCType, file: string): string {
|
|
return AudioPaths[npcName] + file + ".mp3";
|
|
}
|
|
|
|
function isInMythicPlus(playerId: number, instanceId: number): boolean {
|
|
const result = CharDBQuery("select guid, instanceId, difficulty from mp_player_instance_data where guid = " + playerId + " and instanceId = " + instanceId);
|
|
if(result && result.GetUInt32(2) >= 3) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Track NPC dialog animation state
|
|
interface ActiveNpcState {
|
|
time: number;
|
|
intro: boolean;
|
|
audioActive: boolean;
|
|
players?: string[]; // list of players nearby when the NPC spawns
|
|
lastEmote?: number;
|
|
outro?: string[]; // list of players that have heard the outro
|
|
}
|
|
interface PlayedAudioState {
|
|
playerName: string;
|
|
audioFile: string;
|
|
played: boolean;
|
|
}
|
|
|
|
// Track the audio that has been played for each for this instance id.
|
|
// key: InstanceId value: playerId: audioFile
|
|
let playedAudio: Record<string, Record<string, string[]>> = {};
|
|
|
|
|
|
// This will prevent audio from being played more than once for the same player using a global map
|
|
function PlayAudioOnce(player: Player, audioFile: string, npcType?: NPCType, forcePlay?: boolean): void {
|
|
const instanceId = player.GetInstanceId();
|
|
const mapId = player.GetMapId();
|
|
const playerName = player.GetName();
|
|
|
|
// Get audio options from NPC config if provided
|
|
let audioOptions: {volume: number, duration: number} | undefined;
|
|
if (npcType) {
|
|
const npcConfig = getNPCMapConfig(npcType, mapId);
|
|
if (npcConfig?.audioOptions) {
|
|
audioOptions = npcConfig.audioOptions;
|
|
}
|
|
}
|
|
|
|
// Default audio options if not specified in NPC config
|
|
if (!audioOptions) {
|
|
audioOptions = {
|
|
volume: 0.5,
|
|
duration: 45
|
|
};
|
|
}
|
|
|
|
if(!playedAudio[instanceId]) {
|
|
playedAudio[instanceId] = {};
|
|
}
|
|
|
|
// If nothing has been played to the player create the audio entry
|
|
if(!playedAudio[instanceId][playerName]) {
|
|
playedAudio[instanceId][playerName] = [];
|
|
playedAudio[instanceId][playerName].push(audioFile);
|
|
aio.Handle(player, 'AIOAudioPlayer', 'PlaySingleSound', audioFile, audioOptions, playerName);
|
|
return;
|
|
}
|
|
|
|
if(playedAudio[instanceId][playerName].includes(audioFile) && forcePlay !== true) {
|
|
return;
|
|
}
|
|
|
|
playedAudio[instanceId][playerName].push(audioFile);
|
|
aio.Handle(player, 'AIOAudioPlayer', 'PlaySingleSound', audioFile, audioOptions, playerName);
|
|
}
|
|
|
|
|
|
// Track the ai update loop for each instance of a NPC which
|
|
let npcState: Record<string, ActiveNpcState> = {};
|
|
|
|
// Create emote maps for npcs while they are talking keys are seconds into audio and values are emote types
|
|
const emotesMap: Record<string, Record<number, EmoteType>> = {};
|
|
|
|
emotesMap[`${NPCIds[NPCType.MICK_ASHWILD]}-${MapIds[MapNames.WAILING_CAVERNS]}`] = {
|
|
1: EmoteType.STATE_TALK,
|
|
6: EmoteType.STATE_EXCLAIM,
|
|
8: EmoteType.STATE_TALK,
|
|
10: EmoteType.ONESHOT_NONE,
|
|
12: EmoteType.STATE_POINT,
|
|
14: EmoteType.ONESHOT_NONE,
|
|
15: EmoteType.STATE_TALK,
|
|
26: EmoteType.STATE_EXCLAIM,
|
|
28: EmoteType.STATE_TALK,
|
|
35: EmoteType.ONESHOT_EXCLAMATION,
|
|
37: EmoteType.STATE_TALK,
|
|
41: EmoteType.STATE_POINT,
|
|
43: EmoteType.ONESHOT_NONE
|
|
};
|
|
|
|
emotesMap[`${NPCIds[NPCType.SHIVEY]}-${MapIds[MapNames.SCHOLOMANCE]}`] = {
|
|
1: EmoteType.STATE_TALK,
|
|
3: EmoteType.STATE_EXCLAIM,
|
|
6: EmoteType.STATE_TALK,
|
|
10: EmoteType.STATE_LAUGH,
|
|
12: EmoteType.STATE_TALK,
|
|
15: EmoteType.STATE_EXCLAIM,
|
|
17: EmoteType.STATE_TALK,
|
|
23: EmoteType.STATE_EXCLAIM,
|
|
25: EmoteType.ONESHOT_NONE,
|
|
26: EmoteType.STATE_POINT,
|
|
28: EmoteType.ONESHOT_NONE,
|
|
};
|
|
emotesMap[`${NPCIds[NPCType.THORIN_FIREHAND]}-${MapIds[MapNames.BLACKROCK_SPIRE_UPPER]}`] = {
|
|
1: EmoteType.STATE_TALK,
|
|
10: EmoteType.ONESHOT_YES,
|
|
12: EmoteType.ONESHOT_TALK,
|
|
17: EmoteType.ONESHOT_NO,
|
|
19: EmoteType.ONESHOT_TALK,
|
|
28: EmoteType.ONESHOT_NO,
|
|
31: EmoteType.ONESHOT_TALK,
|
|
34: EmoteType.ONESHOT_POINT,
|
|
36: EmoteType.ONESHOT_NONE
|
|
};
|
|
|
|
emotesMap[`${NPCIds[NPCType.ELOWYN_THREADBINDER]}-${MapIds[MapNames.STRATHOLME]}`] = {
|
|
1: EmoteType.STATE_TALK,
|
|
10: EmoteType.ONESHOT_YES,
|
|
12: EmoteType.ONESHOT_TALK,
|
|
17: EmoteType.STATE_EXCLAIM,
|
|
19: EmoteType.ONESHOT_TALK,
|
|
28: EmoteType.ONESHOT_YES,
|
|
30: EmoteType.ONESHOT_TALK,
|
|
31: EmoteType.ONESHOT_NONE
|
|
};
|
|
|
|
emotesMap[`${NPCIds[NPCType.STEVE]}-${MapIds[MapNames.UTGARDE_PINNACLE]}`] = {
|
|
1: EmoteType.STATE_TALK,
|
|
10: EmoteType.ONESHOT_YES,
|
|
12: EmoteType.ONESHOT_TALK,
|
|
17: EmoteType.STATE_EXCLAIM,
|
|
19: EmoteType.ONESHOT_TALK,
|
|
28: EmoteType.ONESHOT_YES,
|
|
30: EmoteType.ONESHOT_TALK,
|
|
38: EmoteType.ONESHOT_EXCLAMATION,
|
|
40: EmoteType.ONESHOT_NO,
|
|
42: EmoteType.ONESHOT_TALK,
|
|
43: EmoteType.ONESHOT_POINT,
|
|
45: EmoteType.ONESHOT_EXCLAMATION,
|
|
47: EmoteType.ONESHOT_TALK,
|
|
54: EmoteType.ONESHOT_YES,
|
|
56: EmoteType.ONESHOT_TALK,
|
|
64: EmoteType.ONESHOT_NONE
|
|
};
|
|
|
|
emotesMap[`${NPCIds[NPCType.THORIN_FIREHAND]}-${MapIds[MapNames.HALLS_OF_LIGHTNING]}`] = {
|
|
1: EmoteType.STATE_TALK,
|
|
10: EmoteType.ONESHOT_YES,
|
|
12: EmoteType.ONESHOT_TALK,
|
|
17: EmoteType.STATE_EXCLAIM,
|
|
19: EmoteType.ONESHOT_TALK,
|
|
28: EmoteType.ONESHOT_YES,
|
|
30: EmoteType.ONESHOT_TALK,
|
|
46: EmoteType.ONESHOT_YES,
|
|
48: EmoteType.ONESHOT_TALK,
|
|
52: EmoteType.ONESHOT_NONE,
|
|
};
|
|
|
|
emotesMap[`${NPCIds[NPCType.MICK_ASHWILD]}-${MapIds[MapNames.HELLFIRE_RAMPARTS]}`] = {
|
|
1: EmoteType.STATE_EXCLAIM,
|
|
3: EmoteType.STATE_TALK,
|
|
10: EmoteType.ONESHOT_YES,
|
|
12: EmoteType.ONESHOT_TALK,
|
|
17: EmoteType.STATE_EXCLAIM,
|
|
19: EmoteType.ONESHOT_TALK,
|
|
28: EmoteType.ONESHOT_YES,
|
|
30: EmoteType.ONESHOT_TALK,
|
|
38: EmoteType.ONESHOT_EXCLAMATION,
|
|
40: EmoteType.ONESHOT_NO,
|
|
42: EmoteType.ONESHOT_TALK,
|
|
43: EmoteType.ONESHOT_POINT,
|
|
45: EmoteType.ONESHOT_EXCLAMATION,
|
|
47: EmoteType.ONESHOT_TALK,
|
|
54: EmoteType.ONESHOT_YES,
|
|
56: EmoteType.ONESHOT_TALK,
|
|
84: EmoteType.ONESHOT_NONE
|
|
};
|
|
|
|
emotesMap[`${NPCIds[NPCType.SHIVEY]}-${MapIds[MapNames.NEXUS]}`] = {
|
|
1: EmoteType.STATE_TALK,
|
|
10: EmoteType.ONESHOT_YES,
|
|
12: EmoteType.ONESHOT_TALK,
|
|
17: EmoteType.ONESHOT_NO,
|
|
19: EmoteType.ONESHOT_TALK,
|
|
28: EmoteType.ONESHOT_NO,
|
|
31: EmoteType.ONESHOT_TALK,
|
|
34: EmoteType.ONESHOT_POINT,
|
|
36: EmoteType.ONESHOT_TALK,
|
|
39: EmoteType.ONESHOT_NONE
|
|
};
|
|
|
|
// Spell to spell MapId
|
|
const spellToMap: Record<number, number> = {
|
|
[MapIds[MapNames.WAILING_CAVERNS]]: Spells.SPELL_LEATHER_FUSION,
|
|
[MapIds[MapNames.SCHOLOMANCE]]: Spells.SPELL_ALCHEMY_FUSION,
|
|
[MapIds[MapNames.BLACKROCK_SPIRE_UPPER]]: Spells.SPELL_ORE_FUSION,
|
|
[MapIds[MapNames.STRATHOLME]]: Spells.SPELL_CLOTH_FUSION,
|
|
[MapIds[MapNames.UTGARDE_PINNACLE]]: Spells.SPELL_ESSENCE_FUSION,
|
|
[MapIds[MapNames.HALLS_OF_LIGHTNING]]: Spells.SPELL_GEM_FUSION,
|
|
[MapIds[MapNames.HELLFIRE_RAMPARTS]]: Spells.SPELL_FLAME_FUSION,
|
|
[MapIds[MapNames.NEXUS]]: Spells.SPELL_COLD_FUSION,
|
|
};
|
|
|
|
// Does this player already know what the special NPC is offering.
|
|
function PlayerHasSpell(player: Player): boolean {
|
|
const mapId = player.GetMapId();
|
|
if(spellToMap[mapId]) {
|
|
return player.HasSpell(spellToMap[mapId]);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Get the instance state of the NPC for this group and instance.
|
|
function GetNPCState(creature: Creature): ActiveNpcState {
|
|
const instanceId = creature.GetInstanceId();
|
|
const creatureGuid = creature.GetGUIDLow();
|
|
let state = npcState[`${instanceId}-${creatureGuid}`];
|
|
|
|
if(!state) {
|
|
state = {
|
|
time: 0,
|
|
intro: false,
|
|
outro: [],
|
|
audioActive: false,
|
|
players: []
|
|
};
|
|
npcState[`${instanceId}-${creatureGuid}`] = state;
|
|
}
|
|
return state;
|
|
}
|
|
|
|
interface NPCMapConfig {
|
|
introAudio: string;
|
|
outroAudio: string;
|
|
teachYesAudio?: string;
|
|
teachNoAudio?: string;
|
|
completionTime: number;
|
|
spellId?: number;
|
|
gossipOptions: {
|
|
knownSpell: string; // Text shown when player already knows the spell
|
|
learnSpell: string; // Text for learning the spell
|
|
decline: string; // Text for declining to learn
|
|
};
|
|
audioOptions?: {
|
|
volume: number;
|
|
duration: number;
|
|
};
|
|
// Spawn configuration when this NPC appears after a boss kill
|
|
spawnConfig?: {
|
|
bossId: number;
|
|
portalLocation: { x: number; y: number; z: number; o: number }; // Where to spawn the portal and NPC
|
|
moveToLocation: { x: number; y: number; z: number }; // Where the NPC should walk to
|
|
despawnTime?: number; // How long before the NPC despawns (ms)
|
|
};
|
|
}
|
|
|
|
const NpcConfigMap: Record<string, Record<number, NPCMapConfig>> = {
|
|
[NPCType.MICK_ASHWILD]: {
|
|
[MapIds[MapNames.WAILING_CAVERNS]]: {
|
|
introAudio: "rare-hello",
|
|
outroAudio: "rare-goodbye",
|
|
teachYesAudio: "teach-rare-yes",
|
|
teachNoAudio: "teach-rare-no",
|
|
completionTime: 43,
|
|
spellId: Spells.SPELL_LEATHER_FUSION,
|
|
gossipOptions: {
|
|
knownSpell: "Got nuthin' for ya friend.",
|
|
learnSpell: "Learn Leather Fusion (requires grandmaster)",
|
|
decline: "Best to come another time"
|
|
},
|
|
audioOptions: {
|
|
volume: 1.0,
|
|
duration: 45
|
|
},
|
|
spawnConfig: {
|
|
bossId: 5775, // Verdan the Everliving
|
|
portalLocation: { x: -79.273, y: 4.999, z: -30.962, o: 2.20 },
|
|
moveToLocation: { x: -83.252, y: 19.723, z: -31.076 },
|
|
despawnTime: 1200000 // 20 minutes
|
|
}
|
|
},
|
|
[MapIds[MapNames.HELLFIRE_RAMPARTS]]: {
|
|
introAudio: "fire-hello",
|
|
outroAudio: "fire-goodbye",
|
|
teachYesAudio: "teach-fire-yes",
|
|
teachNoAudio: "teach-fire-no",
|
|
completionTime: 85,
|
|
spellId: Spells.SPELL_FLAME_FUSION,
|
|
gossipOptions: {
|
|
knownSpell: "Got nuthin' for ya friend.",
|
|
learnSpell: "Learn Flame Fusion (Requires 10,000g and 1 Jug of Badlands Bourbon)",
|
|
decline: "Best to come another time"
|
|
},
|
|
audioOptions: {
|
|
volume: 1.0,
|
|
duration: 85
|
|
},
|
|
spawnConfig: {
|
|
bossId: 17536, // Nazan
|
|
portalLocation: { x: -1439, y: 1764, z: 81.98, o: 5.55 },
|
|
moveToLocation: { x: -1433, y: 1761, z: 81.80 },
|
|
despawnTime: 1200000 // 20 minutes
|
|
}
|
|
},
|
|
// Add other maps for Mick
|
|
},
|
|
[NPCType.SHIVEY]: {
|
|
[MapIds[MapNames.SCHOLOMANCE]]: {
|
|
introAudio: "rare-hello",
|
|
outroAudio: "rare-goodbye",
|
|
teachYesAudio: "teach-rare-yes",
|
|
teachNoAudio: "teach-rare-no",
|
|
completionTime: 29,
|
|
spellId: Spells.SPELL_ALCHEMY_FUSION,
|
|
gossipOptions: {
|
|
knownSpell: "Why you staring at me like that, get lost.",
|
|
learnSpell: "Learn Alchemy Fusion (requires grandmaster)",
|
|
decline: "No thanks"
|
|
},
|
|
audioOptions: {
|
|
volume: 0.5,
|
|
duration: 29
|
|
},
|
|
spawnConfig: {
|
|
bossId: 10508, // Ras Frostwhisper
|
|
portalLocation: { x: 25.457, y: 152.83, z: 83.54, o: 0.54 },
|
|
moveToLocation: { x: 40.22, y: 159.23, z: 83.54 },
|
|
despawnTime: 1200000 // 20 minutes
|
|
}
|
|
},
|
|
[MapIds[MapNames.NEXUS]]: {
|
|
introAudio: "cold-hello",
|
|
outroAudio: "cold-goodbye",
|
|
teachYesAudio: "teach-cold-yes",
|
|
teachNoAudio: "teach-cold-no",
|
|
completionTime: 40,
|
|
spellId: Spells.SPELL_COLD_FUSION,
|
|
gossipOptions: {
|
|
knownSpell: "Why you staring at me like that, get lost.",
|
|
learnSpell: "Learn Cold Fusion (10,000g and Flask of Death)",
|
|
decline: "No thanks"
|
|
},
|
|
audioOptions: {
|
|
volume: 0.7,
|
|
duration: 40
|
|
},
|
|
spawnConfig: {
|
|
bossId: 26794, // Ormorok The Tree Shaper
|
|
portalLocation: { x: 264.9, y: -255.4, z: -8.1, o: 5.89 },
|
|
moveToLocation: { x: 283.5, y: -239, z: -8.25 },
|
|
despawnTime: 1200000 // 20 minutes
|
|
}
|
|
}
|
|
// Add other maps for Shivey
|
|
},
|
|
[NPCType.THORIN_FIREHAND]: {
|
|
[MapIds[MapNames.BLACKROCK_SPIRE_UPPER]]: {
|
|
introAudio: "rare-hello",
|
|
outroAudio: "rare-goodbye",
|
|
teachYesAudio: "teach-rare-yes",
|
|
teachNoAudio: "teach-rare-no",
|
|
completionTime: 36,
|
|
spellId: Spells.SPELL_ORE_FUSION,
|
|
gossipOptions: {
|
|
knownSpell: "...Leave me alone",
|
|
learnSpell: "Learn Ore Fusion (requires grandmaster)",
|
|
decline: "No thanks"
|
|
},
|
|
audioOptions: {
|
|
volume: 1.0,
|
|
duration: 36
|
|
},
|
|
spawnConfig: {
|
|
bossId: 9938, // Magmus
|
|
portalLocation: { x: 1394.31, y: -703.50, z: -92.08, o: 1.08 },
|
|
moveToLocation: { x: 1388.74, y: -692, z: -92.05 },
|
|
despawnTime: 1200000 // 20 minutes
|
|
}
|
|
},
|
|
[MapIds[MapNames.HALLS_OF_LIGHTNING]]: {
|
|
introAudio: "rare-lightning-hello",
|
|
outroAudio: "rare-lightning-goodbye",
|
|
teachYesAudio: "teach-rare-lightning-yes",
|
|
teachNoAudio: "teach-rare-lightning-no",
|
|
completionTime: 52,
|
|
spellId: Spells.SPELL_GEM_FUSION,
|
|
gossipOptions: {
|
|
knownSpell: "...Leave me alone",
|
|
learnSpell: "Learn Gem Fusion (requires grandmaster)",
|
|
decline: "No thanks"
|
|
},
|
|
audioOptions: {
|
|
volume: 1.0,
|
|
duration: 52
|
|
},
|
|
spawnConfig: {
|
|
bossId: 28587, // Volkan
|
|
portalLocation: { x: 1331.91, y: -126.63, z: 56.71, o: 4.71 },
|
|
moveToLocation: { x: 1331.91, y: -139.77, z: 53.27 },
|
|
despawnTime: 1200000 // 20 minutes
|
|
}
|
|
}
|
|
// Add other maps for Shivey
|
|
},
|
|
[NPCType.ELOWYN_THREADBINDER]: {
|
|
[MapIds[MapNames.STRATHOLME]]: {
|
|
introAudio: "rare-hello",
|
|
outroAudio: "rare-goodbye",
|
|
teachYesAudio: "teach-rare-yes",
|
|
teachNoAudio: "teach-rare-no",
|
|
completionTime: 31,
|
|
spellId: Spells.SPELL_CLOTH_FUSION,
|
|
gossipOptions: {
|
|
knownSpell: "Please leave me to mourn.",
|
|
learnSpell: "Learn Cloth Fusion (requires grandmaster)",
|
|
decline: "Another time"
|
|
},
|
|
audioOptions: {
|
|
volume: 1.0,
|
|
duration: 31
|
|
},
|
|
spawnConfig: {
|
|
bossId: 10436, // Baroness Anastari
|
|
portalLocation: { x: 3866.14, y: -3702.15, z: 141.78, o: 2.12 },
|
|
moveToLocation: { x: 3865.73, y: -3696.5, z: 141.90 },
|
|
despawnTime: 1200000 // 20 minutes
|
|
}
|
|
}
|
|
// Add other maps for Shivey
|
|
},
|
|
[NPCType.STEVE]: {
|
|
[MapIds[MapNames.UTGARDE_PINNACLE]]: {
|
|
introAudio: "rare-hello",
|
|
outroAudio: "rare-goodbye",
|
|
teachYesAudio: "teach-rare-yes",
|
|
teachNoAudio: "teach-rare-no",
|
|
completionTime: 65,
|
|
spellId: Spells.SPELL_ESSENCE_FUSION,
|
|
gossipOptions: {
|
|
knownSpell: "Queues are taking forever...",
|
|
learnSpell: "Learn Essence Fusion (requires grandmaster)",
|
|
decline: "Another time"
|
|
},
|
|
audioOptions: {
|
|
volume: 0.8,
|
|
duration: 65
|
|
},
|
|
spawnConfig: {
|
|
bossId: 26687, // Gortok
|
|
portalLocation: { x: 314.63, y: -461.73, z: 104.71, o: 2.725 },
|
|
moveToLocation: { x: 313.21, y: -452.52, z: 104.71 },
|
|
despawnTime: 1200000 // 20 minutes
|
|
}
|
|
}
|
|
// Add other maps for Shivey
|
|
},
|
|
// Add other NPCs
|
|
};
|
|
|
|
function getNPCMapConfig(npcType: NPCType, mapId: number): NPCMapConfig | undefined {
|
|
return NpcConfigMap[npcType]?.[mapId];
|
|
}
|
|
|
|
// Helper function to get NPC type from entry
|
|
function getNPCTypeFromEntry(entry: number): NPCType | undefined {
|
|
for (const [type, id] of Object.entries(NPCIds)) {
|
|
if (id === entry) {
|
|
return type as NPCType;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
const handleNPCAIUpdates: creature_event_on_aiupdate = (event: number, creature: Creature, diff: number): boolean => {
|
|
|
|
const creatureGuid = creature.GetGUIDLow();
|
|
const instanceId = creature.GetInstanceId();
|
|
const mapId = creature.GetMapId();
|
|
const npcEntry = creature.GetEntry();
|
|
const state = npcState[`${instanceId}-${creatureGuid}`];
|
|
const previousTime = state.time || 0;
|
|
|
|
// Get NPC type and config
|
|
const npcType = getNPCTypeFromEntry(npcEntry);
|
|
if (!npcType) return true;
|
|
|
|
const npcConfig = getNPCMapConfig(npcType, mapId);
|
|
if (!npcConfig) return true;
|
|
|
|
state.time = (previousTime + diff);
|
|
const seconds = Math.ceil(state.time / 1000);
|
|
|
|
// Handle emotes based on the emote map for this NPC in this map
|
|
const npcEmoteMap = emotesMap[`${npcEntry}-${mapId}`];
|
|
if(npcEmoteMap && npcEmoteMap[seconds] && npcEmoteMap[seconds] !== state.lastEmote) {
|
|
PrintDebug(`Emote for ${npcEntry} at ${seconds} seconds`);
|
|
creature.EmoteState(npcEmoteMap[seconds]);
|
|
state.lastEmote = npcEmoteMap[seconds];
|
|
}
|
|
|
|
// Check if the audio/dialog sequence has completed based on config
|
|
if(seconds >= npcConfig.completionTime) {
|
|
ClearUniqueCreatureEvents(creature.GetGUID(), instanceId);
|
|
state.audioActive = false;
|
|
state.intro = true;
|
|
|
|
// Show gossip menu to all players who were in range when the NPC started talking
|
|
for(let i=0; i < state.players.length; i++) {
|
|
const playerId = state.players[i];
|
|
|
|
if(!playerId) {
|
|
PrintError(`${creature.GetName()} gossip menu failed no valid player in state`);
|
|
continue;
|
|
}
|
|
const player = GetPlayerByName(playerId);
|
|
if(player) {
|
|
npcHello(player, creature, player.GetMapId());
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
const BossMapping: Record<number, NPCType> = {
|
|
[5775]: NPCType.MICK_ASHWILD, // Verdan the Everliving
|
|
[10508]: NPCType.SHIVEY, // Ras Frostwhisper
|
|
[9938]: NPCType.THORIN_FIREHAND, // Magmus
|
|
[10436]: NPCType.ELOWYN_THREADBINDER, // Baroness Anastari
|
|
[26687]: NPCType.STEVE, // Gortok
|
|
[28587]: NPCType.THORIN_FIREHAND, // Volkahn
|
|
[17536]: NPCType.MICK_ASHWILD, // Nazan
|
|
[26794]: NPCType.SHIVEY, // Ormorok The Tree Shaper
|
|
};
|
|
|
|
// Generic function for handling boss deaths and spawning NPCs
|
|
const handleBossDeath: creature_event_on_died = (event: number, creature: Creature, killer: Creature): boolean => {
|
|
const mapId = creature.GetMapId();
|
|
const bossId = creature.GetEntry();
|
|
const npcType = BossMapping[bossId];
|
|
|
|
if(!npcType) {
|
|
PrintError(`No NPC type found for boss ID ${bossId}`);
|
|
return false;
|
|
}
|
|
|
|
const config = getNPCMapConfig(npcType, mapId);
|
|
if(!config) {
|
|
PrintError(`No NPC configured to spawn for boss ID ${bossId} on map ${mapId}`);
|
|
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;
|
|
const despawnTime = config.spawnConfig.despawnTime || 1200000;
|
|
|
|
PrintDebug(`${creature.GetName()} died, spawning ${npcType}`);
|
|
|
|
const portal = creature.SummonGameObject(
|
|
GobjectIds[GobjectType.PORTAL],
|
|
startLoc.x, startLoc.y, startLoc.z, startLoc.o
|
|
);
|
|
|
|
// Spawn the special NPC at the portal location
|
|
const npc = portal?.SpawnCreature(
|
|
NPCIds[npcType],
|
|
startLoc.x, startLoc.y, startLoc.z, startLoc.o,
|
|
TempSummonType.TEMPSUMMON_TIMED_DESPAWN, despawnTime
|
|
);
|
|
|
|
if(!npc) {
|
|
PrintError(`Failed to spawn ${npcType} for boss ID ${bossId} on map ${mapId}`);
|
|
return false;
|
|
}
|
|
|
|
// Make the NPC walk to the destination
|
|
npc.SetWalk(true);
|
|
npc.MoveTo(1, endLoc.x, endLoc.y, endLoc.z);
|
|
|
|
return false; // return false to continue normal action
|
|
};
|
|
|
|
// Helper function to get all map configs for an NPC type
|
|
function getNPCMapConfigs(npcType: NPCType): Record<number, NPCMapConfig> {
|
|
const configs: Record<string, Record<number, NPCMapConfig>> = {
|
|
[NPCType.MICK_ASHWILD]: {
|
|
[MapIds[MapNames.WAILING_CAVERNS]]: getNPCMapConfig(NPCType.MICK_ASHWILD, MapIds[MapNames.WAILING_CAVERNS])!
|
|
// Add other maps for Mick
|
|
},
|
|
[NPCType.SHIVEY]: {
|
|
[MapIds[MapNames.SCHOLOMANCE]]: getNPCMapConfig(NPCType.SHIVEY, MapIds[MapNames.SCHOLOMANCE])!
|
|
// Add other maps for Shivey
|
|
}
|
|
// Add other NPCs
|
|
};
|
|
|
|
return configs[npcType] || {};
|
|
}
|
|
|
|
// Generic menu handler used by all special trainers
|
|
function npcHello(player: Player, creature: Creature, mapId: number, known?: boolean): void {
|
|
player.GossipClearMenu();
|
|
|
|
const npcEntry = creature.GetEntry();
|
|
const npcType = getNPCTypeFromEntry(npcEntry);
|
|
if (!npcType) return;
|
|
|
|
const npcConfig = getNPCMapConfig(npcType, mapId);
|
|
if (!npcConfig) return;
|
|
|
|
if(known) {
|
|
player.GossipMenuAddItem(0, npcConfig.gossipOptions.knownSpell, 1, 999);
|
|
player.GossipSendMenu(1, creature, 90000);
|
|
return;
|
|
}
|
|
|
|
if (npcConfig.spellId) {
|
|
player.GossipMenuAddItem(3, npcConfig.gossipOptions.learnSpell, 1, npcConfig.spellId);
|
|
}
|
|
player.GossipMenuAddItem(0, npcConfig.gossipOptions.decline, 1, 999);
|
|
player.GossipSendMenu(1, creature, 90000);
|
|
|
|
}
|
|
|
|
// This will handle the selection of the spell and granting the player the spell.
|
|
const handleNpcSelect: gossip_event_on_select = (event: number, player: Player, creature: Creature, sender: number, selection: number): boolean => {
|
|
|
|
const state = GetNPCState(creature);
|
|
const npcEntry = creature.GetEntry();
|
|
const npcType = getNPCTypeFromEntry(npcEntry);
|
|
if (!npcType) return;
|
|
|
|
const npcConfig = getNPCMapConfig(npcType, player.GetMapId());
|
|
if (!npcConfig) return;
|
|
|
|
PrintDebug(`Player ${player.GetName()} selected ${selection}`);
|
|
|
|
// 999 is a signal nothing to do
|
|
if(selection === 999) {
|
|
// play the outro audio for the player if they have not heard it yet.
|
|
PlayAudioOnce(player, getAudioFile(npcType, npcConfig.outroAudio), npcType);
|
|
player.GossipClearMenu();
|
|
player.GossipComplete();
|
|
return true;
|
|
}
|
|
|
|
// Check the range of the spell we are trying to learn to make sure there is not a problem.
|
|
if(selection < Spells.SPELL_ORE_FUSION || selection > Spells.SPELL_EARTH_FUSION_RANK_2) {
|
|
PrintError(`The selection ${selection} is not in the range of learnable spells check coding!!`);
|
|
return true;
|
|
}
|
|
|
|
const skillRequirements: Record<number, {skillId: number, requiredLevel: number}> = {
|
|
[Spells.SPELL_LEATHER_FUSION]: { skillId: 165, requiredLevel: 450 }, // Leatherworking
|
|
[Spells.SPELL_ORE_FUSION]: { skillId: 164, requiredLevel: 450 }, // Blacksmithing
|
|
[Spells.SPELL_CLOTH_FUSION]: { skillId: 197, requiredLevel: 450 }, // Tailoring
|
|
[Spells.SPELL_ALCHEMY_FUSION]: { skillId: 171, requiredLevel: 450 }, // Alchemy
|
|
[Spells.SPELL_GEM_FUSION]: { skillId: 755, requiredLevel: 450 }, // Jewelcrafting
|
|
[Spells.SPELL_ESSENCE_FUSION]: { skillId: 333, requiredLevel: 450 } // Enchanting
|
|
};
|
|
|
|
// these are 10,000 a piece and a special item
|
|
const costRequirement: Record<number, {cost: number, itemId: number, itemQty: number}> = {
|
|
[Spells.SPELL_FLAME_FUSION]: { cost: 10000*10000, itemId: 2595, itemQty: 1 }, // Jug of Badlands Bourbon
|
|
[Spells.SPELL_COLD_FUSION]: { cost: 10000*10000, itemId: 35716, itemQty: 1 }, // Flask of Pure Death
|
|
[Spells.SPELL_DARK_FUSION]: { cost: 10000*10000, itemId: 197, itemQty: 1 },
|
|
[Spells.SPELL_ARCANE_FUSION]: { cost: 10000*10000, itemId: 171, itemQty: 1 },
|
|
[Spells.SPELL_EARTH_FUSION]: { cost: 10000*10000, itemId: 755, itemQty: 1 },
|
|
};
|
|
|
|
const requirement = skillRequirements[selection];
|
|
const cost = costRequirement[selection];
|
|
|
|
if (requirement) {
|
|
const playerSkill = player.GetSkillValue(requirement.skillId);
|
|
PrintDebug(`Learning spell ${selection} for player ${player.GetName()}`);
|
|
PrintDebug(`Player skill ${playerSkill} for skill ID ${requirement.skillId}`);
|
|
|
|
if (playerSkill >= requirement.requiredLevel) {
|
|
// Player has required skill level, teach the spell
|
|
player.LearnSpell(selection);
|
|
PrintDebug(`Learning spell ${selection} for player ${player.GetName()}`);
|
|
PlayAudioOnce(player, getAudioFile(npcType, npcConfig.teachYesAudio || "teach-rare-yes"), npcType);
|
|
player.CastSpell(player, 40333);
|
|
} else {
|
|
// Player doesn't have required skill level
|
|
PrintDebug(`FAILING ${selection} for player ${player.GetName()}`);
|
|
PlayAudioOnce(player, getAudioFile(npcType, npcConfig.teachNoAudio || "teach-rare-no"), npcType);
|
|
}
|
|
} else if(cost) {
|
|
const playerMoney = player.GetCoinage();
|
|
const playerItem = player.GetItemCount(cost.itemId);
|
|
if(playerMoney >= cost.cost && playerItem >= cost.itemQty) {
|
|
player.SetCoinage(playerMoney - cost.cost);
|
|
player.RemoveItem(cost.itemId, cost.itemQty);
|
|
player.LearnSpell(selection);
|
|
PlayAudioOnce(player, getAudioFile(npcType, npcConfig.teachYesAudio), npcType);
|
|
player.CastSpell(player, 40333);
|
|
} else {
|
|
PrintDebug(`FAILING ${selection} for player ${player.GetName()}`);
|
|
PlayAudioOnce(player, getAudioFile(npcType, npcConfig.teachNoAudio), npcType);
|
|
}
|
|
} else {
|
|
PrintError(`No skill requirement defined for spell ${selection}`);
|
|
}
|
|
|
|
player.GossipComplete();
|
|
return true;
|
|
}
|
|
|
|
// Micks events
|
|
const handleNpcHello: gossip_event_on_hello = (event: number, player: Player, creature: Creature): boolean => {
|
|
const npcEntry = creature.GetEntry();
|
|
const mapId = player.GetMapId();
|
|
const instanceId = player.GetInstanceId();
|
|
|
|
const npcType = getNPCTypeFromEntry(npcEntry);
|
|
if (!npcType) {
|
|
return false; // Not one of our special NPCs
|
|
}
|
|
|
|
// Get the global creature state for special NPCs
|
|
const myState = GetNPCState(creature);
|
|
const npcConfig = getNPCMapConfig(npcType, mapId);
|
|
if (!npcConfig) {
|
|
return false; // NPC not configured for this map
|
|
}
|
|
|
|
creature.SetFacingToObject(player);
|
|
|
|
// Check if player already knows the spell for this location
|
|
if (PlayerHasSpell(player)) {
|
|
npcHello(player, creature, mapId, true);
|
|
PlayAudioOnce(player, getAudioFile(npcType, npcConfig.outroAudio));
|
|
return true;
|
|
}
|
|
|
|
if (!myState.audioActive) {
|
|
// Get all nearby players
|
|
const nearPlayers = creature.GetPlayersInRange(35);
|
|
|
|
for (let i = 0; i < nearPlayers.length; i++) {
|
|
const nearPlayer = nearPlayers[i];
|
|
myState.players.push(nearPlayer.GetName());
|
|
|
|
// Play appropriate audio based on whether player knows the spell
|
|
if (PlayerHasSpell(nearPlayer)) {
|
|
// Player already knows the spell, play outro audio
|
|
PlayAudioOnce(nearPlayer, getAudioFile(npcType, npcConfig.outroAudio), npcType);
|
|
} else {
|
|
// Player doesn't know the spell, play intro audio
|
|
PlayAudioOnce(nearPlayer, getAudioFile(npcType, npcConfig.introAudio), npcType);
|
|
}
|
|
}
|
|
|
|
creature.EmoteState(EmoteType.ONESHOT_NONE);
|
|
myState.audioActive = true;
|
|
ClearUniqueCreatureEvents(creature.GetGUID(), instanceId, CreatureEvents.CREATURE_EVENT_ON_AIUPDATE);
|
|
RegisterUniqueCreatureEvent(creature.GetGUID(), instanceId, CreatureEvents.CREATURE_EVENT_ON_AIUPDATE,
|
|
(...args) => handleNPCAIUpdates(...args));
|
|
|
|
|
|
} else {
|
|
// NPC already active, just add this player if not already tracked
|
|
if (!myState.players.includes(player.GetName())) {
|
|
myState.players.push(player.GetName());
|
|
}
|
|
|
|
// If intro is complete, show appropriate gossip menu
|
|
if (myState.intro) {
|
|
npcHello(player, creature, mapId, PlayerHasSpell(player));
|
|
}
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
/**** Boss Kill Handlers ****/
|
|
|
|
// Register boss death events based on NPC configurations
|
|
function registerBossDeathEvents() {
|
|
// Set to track which boss IDs we've already registered
|
|
const registeredBossIds = new Set<number>();
|
|
|
|
// Loop through all NPCs and their map configs
|
|
for (const npcType in NpcConfigMap) {
|
|
const mapConfigs = NpcConfigMap[npcType];
|
|
|
|
|
|
// Check each map config for this NPC
|
|
for (const mapIdStr in mapConfigs) {
|
|
const config = mapConfigs[mapIdStr];
|
|
|
|
PrintDebug(`Registering boss death events for ${npcType} in map ${mapIdStr}`);
|
|
|
|
// If this NPC has a spawn config with a boss ID
|
|
if (config.spawnConfig?.bossId) {
|
|
const bossId = config.spawnConfig.bossId;
|
|
|
|
// Only register each boss ID once
|
|
if (!registeredBossIds.has(bossId)) {
|
|
RegisterCreatureEvent(bossId, CreatureEvents.CREATURE_EVENT_ON_DIED, (...args) => handleBossDeath(...args));
|
|
PrintDebug(`Registered death event for boss ID ${bossId}`);
|
|
registeredBossIds.add(bossId);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Register all boss death events
|
|
registerBossDeathEvents();
|
|
|
|
/**** NPC Events Handlers ****/
|
|
|
|
// Registers the creature state to our instance state map and sets default state values.
|
|
function commonCreatureRegister(creature: Creature) {
|
|
ClearUniqueCreatureEvents(creature.GetGUID(), creature.GetInstanceId());
|
|
npcState[`${creature.GetInstanceId()}-${creature.GetGUIDLow()}`] = {
|
|
time: 0,
|
|
intro: false,
|
|
outro: [],
|
|
audioActive: false,
|
|
players: []
|
|
};
|
|
}
|
|
|
|
// Register events only for specific, known NPCs
|
|
const validNPCs = [
|
|
NPCType.MICK_ASHWILD,
|
|
NPCType.SHIVEY,
|
|
NPCType.THORIN_FIREHAND,
|
|
NPCType.ELOWYN_THREADBINDER,
|
|
NPCType.STEVE,
|
|
// Add other valid NPCs here as you implement them
|
|
];
|
|
|
|
for (let i = 0; i < validNPCs.length; i++) {
|
|
const npcType = validNPCs[i];
|
|
const npcId = NPCIds[npcType];
|
|
|
|
if (npcId) {
|
|
// Register spawn event
|
|
RegisterCreatureEvent(npcId, CreatureEvents.CREATURE_EVENT_ON_SPAWN, (event: number, creature: Creature): boolean => {
|
|
PrintDebug(`NPC ${npcType} (ID: ${npcId}) spawned`);
|
|
commonCreatureRegister(creature);
|
|
return false;
|
|
});
|
|
|
|
// Register remove event
|
|
RegisterCreatureEvent(npcId, CreatureEvents.CREATURE_EVENT_ON_REMOVE, (event: number, creature: Creature): boolean => {
|
|
PrintDebug(`NPC ${npcType} (ID: ${npcId}) removed`);
|
|
commonCreatureRegister(creature);
|
|
return false;
|
|
});
|
|
|
|
// Register gossip events
|
|
RegisterCreatureGossipEvent(npcId, GossipEvents.GOSSIP_EVENT_ON_HELLO, (...args) => handleNpcHello(...args));
|
|
RegisterCreatureGossipEvent(npcId, GossipEvents.GOSSIP_EVENT_ON_SELECT, (...args) => handleNpcSelect(...args));
|
|
|
|
PrintDebug(`Registered all events for NPC ${npcType} (ID: ${npcId})`);
|
|
} else {
|
|
PrintError(`Failed to register events for NPC ${npcType} - ID not found`);
|
|
}
|
|
}
|
|
|
|
/**** Server events for state management around instances ****/
|
|
|
|
const cleanupAudio: map_event_on_destroy = (event: number, map: EMap) => {
|
|
playedAudio[map.GetInstanceId()] = {};
|
|
};
|
|
|
|
// Register Map Event on Destroy
|
|
RegisterServerEvent(ServerEvents.MAP_EVENT_ON_DESTROY, (...args) => cleanupAudio(...args));
|
|
|
|
const createPlayedAudio: map_event_on_destroy = (event: number, map: EMap) => {
|
|
playedAudio[map.GetInstanceId()] = {};
|
|
};
|
|
|
|
// Register Map Event on Destroy
|
|
RegisterServerEvent(ServerEvents.MAP_EVENT_ON_DESTROY, (...args) => createPlayedAudio(...args));
|
|
|
|
const resetPlayedAudio: eluna_event_on_lua_state_open = (event: number) => {
|
|
PrintDebug("resetting played audio");
|
|
playedAudio = {};
|
|
};
|
|
const resetPlayedAudioClose: eluna_event_on_lua_state_close = (event: number) => {
|
|
PrintDebug("resetting played audio");
|
|
playedAudio = {};
|
|
}
|
|
|
|
// Register Server Event on Lua State Open
|
|
RegisterServerEvent(ServerEvents.ELUNA_EVENT_ON_LUA_STATE_OPEN, (...args) => resetPlayedAudio(...args));
|
|
RegisterServerEvent(ServerEvents.ELUNA_EVENT_ON_LUA_STATE_CLOSE, (...args) => resetPlayedAudioClose(...args));
|
|
|
|
// Lore Audio Player
|
|
|
|
function showVaericGossip(player: Player, creature: Creature) {
|
|
player.GossipClearMenu();
|
|
player.GossipMenuAddItem(0, "Tell me what has happened, again", 1,1);
|
|
player.GossipMenuAddItem(9, "Increase my strength and agility", 1,2);
|
|
player.GossipMenuAddItem(0, "Goodbye, Vaeric", 1,999);
|
|
player.GossipSendMenu(60566, creature, 90566);
|
|
}
|
|
|
|
const vaericIntro: gossip_event_on_hello = (event: number, player: Player, creature: Creature): boolean => {
|
|
creature.SetFacingToObject(player);
|
|
|
|
if(!GetTrigger(player.GetGUID(), "VAERIC_INTRO")) {
|
|
PlayAudioOnce(player, getAudioFile(NPCType.VAERIC, "lore-intro"));
|
|
SetTrigger({triggerName: "VAERIC_INTRO", characterGuid: player.GetGUID(), isSet: true});
|
|
creature.EmoteState(EmoteType.ONESHOT_TALK);
|
|
} else {
|
|
PlayAudioOnce(player, getAudioFile(NPCType.VAERIC, "hello-1"), null, true);
|
|
}
|
|
|
|
// player.RegisterEvent((delay: number, repeats: number, player: Player) => {
|
|
showVaericGossip(player, creature);
|
|
// }, 150);
|
|
|
|
creature.RegisterEvent((delay: number, repeats: number, creature: Creature) => {
|
|
creature.EmoteState(EmoteType.ONESHOT_NONE);
|
|
}, 12000);
|
|
|
|
return true;
|
|
};
|
|
|
|
const vaericSelect: gossip_event_on_select = (event: number, player: Player, creature: Creature, sender: number, selection: number): boolean => {
|
|
|
|
PrintInfo(`Vaeric select: ${selection} from sender ${sender}`);
|
|
|
|
if(selection === 1) {
|
|
creature.EmoteState(EmoteType.ONESHOT_TALK);
|
|
PlayAudioOnce(player, getAudioFile(NPCType.VAERIC, "lore-intro"), undefined, true);
|
|
creature.RegisterEvent((delay: number, repeats: number, creature: Creature) => {
|
|
creature.EmoteState(EmoteType.ONESHOT_NONE);
|
|
}, 12000);
|
|
} else if(selection === 2) {
|
|
PlayAudioOnce(player, getAudioFile(NPCType.VAERIC, "stronger"), undefined, true);
|
|
aio.Handle(player, "MythicAdvUI", "ShowUpgradeWindow", "Attack");
|
|
} else if(selection === 999) {
|
|
PlayAudioOnce(player, getAudioFile(NPCType.VAERIC, "goodbye-1"), undefined, true);
|
|
player.GossipComplete();
|
|
creature.EmoteState(EmoteType.ONESHOT_NONE);
|
|
return false;
|
|
}
|
|
|
|
showVaericGossip(player, creature);
|
|
|
|
return false;
|
|
};
|
|
|
|
RegisterCreatureGossipEvent(NPCIds[NPCType.VAERIC], GossipEvents.GOSSIP_EVENT_ON_HELLO, (...args) => vaericIntro(...args));
|
|
RegisterCreatureGossipEvent(NPCIds[NPCType.VAERIC], GossipEvents.GOSSIP_EVENT_ON_SELECT, (...args) => vaericSelect(...args));
|
|
|
|
|
|
// Agatha
|
|
function showAgathaGossip(player: Player, creature: Creature) {
|
|
player.GossipClearMenu();
|
|
player.GossipMenuAddItem(0, "Who are you again?", 1,1);
|
|
player.GossipMenuAddItem(0, "Tell me about the relics.", 1,2);
|
|
player.GossipMenuAddItem(0, "How else can you assist me?", 1,3);
|
|
player.GossipMenuAddItem(0, "Why can't you erase Kel'Thuzad?", 1,4);
|
|
player.GossipMenuAddItem(3, "Improve my mind and spirit", 1,5);
|
|
player.GossipMenuAddItem(3, "Help me resist shadow energy", 1,6);
|
|
player.GossipMenuAddItem(0, "Goodbye, Agatha", 1,999);
|
|
player.GossipSendMenu(60567, creature, 90567);
|
|
}
|
|
|
|
const agathaIntro: gossip_event_on_hello = (event: number, player: Player, creature: Creature): boolean => {
|
|
creature.SetFacingToObject(player);
|
|
|
|
if(!GetTrigger(player.GetGUID(), "AGATHA_INTRO")) {
|
|
PlayAudioOnce(player, getAudioFile(NPCType.AGATHA, "intro-lore"));
|
|
SetTrigger({triggerName: "AGATHA_INTRO", characterGuid: player.GetGUID(), isSet: true});
|
|
} else {
|
|
PlayAudioOnce(player, getAudioFile(NPCType.AGATHA, "hello-1"), null, true);
|
|
}
|
|
|
|
showAgathaGossip(player, creature);
|
|
|
|
return true;
|
|
};
|
|
|
|
const agathaSelect: gossip_event_on_select = (event: number, player: Player, creature: Creature, sender: number, selection: number): boolean => {
|
|
|
|
PrintInfo(`Agatha select: ${selection} from sender ${sender}`);
|
|
|
|
if(selection === 1) {
|
|
PlayAudioOnce(player, getAudioFile(NPCType.AGATHA, "intro-lore"), undefined, true);
|
|
} else if(selection === 2) {
|
|
PlayAudioOnce(player, getAudioFile(NPCType.AGATHA, "dice"), undefined, true);
|
|
} else if(selection === 3) {
|
|
PlayAudioOnce(player, getAudioFile(NPCType.AGATHA, "meetings"), undefined, true);
|
|
}
|
|
else if(selection === 4) {
|
|
PlayAudioOnce(player, getAudioFile(NPCType.AGATHA, "stop-asking"), undefined, true);
|
|
} else if(selection === 5) {
|
|
PlayAudioOnce(player, getAudioFile(NPCType.AGATHA, "stronger"), undefined, true);
|
|
aio.Handle(player, "MythicAdvUI", "ShowUpgradeWindow", "Magic");
|
|
} else if(selection === 6) {
|
|
PlayAudioOnce(player, getAudioFile(NPCType.AGATHA, "shadow"), undefined, true);
|
|
aio.Handle(player, "MythicAdvUI", "ShowUpgradeWindow", "Shadow");
|
|
}
|
|
|
|
else if(selection === 999) {
|
|
PlayAudioOnce(player, getAudioFile(NPCType.AGATHA, "goodbye"), undefined, true);
|
|
player.GossipComplete();
|
|
return false;
|
|
}
|
|
|
|
showAgathaGossip(player, creature);
|
|
|
|
return false;
|
|
};
|
|
|
|
RegisterCreatureGossipEvent(NPCIds[NPCType.AGATHA], GossipEvents.GOSSIP_EVENT_ON_HELLO, (...args) => agathaIntro(...args));
|
|
RegisterCreatureGossipEvent(NPCIds[NPCType.AGATHA], GossipEvents.GOSSIP_EVENT_ON_SELECT, (...args) => agathaSelect(...args));
|
|
|
|
// Slyvia
|
|
function showSylviaGossip(player: Player, creature: Creature) {
|
|
player.GossipClearMenu();
|
|
player.GossipMenuAddItem(0, "Who are you again?", 1,1);
|
|
player.GossipMenuAddItem(3, "Improve my stamina", 1,2);
|
|
player.GossipMenuAddItem(3, "Grant me resistance against the elements", 1,3);
|
|
player.GossipMenuAddItem(3, "Grant me resistance against arcane and poisons", 1,4);
|
|
player.GossipMenuAddItem(0, "Goodbye, ", 1,999);
|
|
player.GossipSendMenu(60568, creature, 90568);
|
|
}
|
|
|
|
const sylviaIntro: gossip_event_on_hello = (event: number, player: Player, creature: Creature): boolean => {
|
|
|
|
if(!creature || !player) {
|
|
AIO_debug("Sylvia intro: creature is null");
|
|
return false;
|
|
}
|
|
|
|
if(!GetTrigger(player.GetGUID(), "SYLVIA_INTRO")) {
|
|
creature.EmoteState(EmoteType.ONESHOT_TALK);
|
|
PlayAudioOnce(player, getAudioFile(NPCType.SYLVIA, "intro"));
|
|
SetTrigger({triggerName: "SYLVIA_INTRO", characterGuid: player.GetGUID(), isSet: true});
|
|
creature.RegisterEvent((delay: number, repeats: number, creature: Creature) => {
|
|
creature.EmoteState(EmoteType.ONESHOT_NONE);
|
|
}, 10000);
|
|
} else {
|
|
PlayAudioOnce(player, getAudioFile(NPCType.SYLVIA, "hello"), null, true);
|
|
}
|
|
|
|
showSylviaGossip(player, creature);
|
|
|
|
return true;
|
|
};
|
|
|
|
const sylviaSelect: gossip_event_on_select = (event: number, player: Player, creature: Creature, sender: number, selection: number): boolean => {
|
|
|
|
if(selection === 1) {
|
|
creature.EmoteState(EmoteType.ONESHOT_TALK);
|
|
PlayAudioOnce(player, getAudioFile(NPCType.SYLVIA, "intro"), undefined, true);
|
|
creature.RegisterEvent((delay: number, repeats: number, creature: Creature) => {
|
|
creature.EmoteState(EmoteType.ONESHOT_NONE);
|
|
}, 10000);
|
|
} else if(selection === 2) {
|
|
PlayAudioOnce(player, getAudioFile(NPCType.SYLVIA, "stronger"), undefined, true);
|
|
aio.Handle(player, "MythicAdvUI", "ShowUpgradeWindow", "Defense");
|
|
} else if(selection === 3) {
|
|
PlayAudioOnce(player, getAudioFile(NPCType.SYLVIA, "fire-ice"), undefined, true);
|
|
aio.Handle(player, "MythicAdvUI", "ShowUpgradeWindow", "FireFrost");
|
|
}
|
|
else if(selection === 4) {
|
|
PlayAudioOnce(player, getAudioFile(NPCType.SYLVIA, "arcane-nature"), undefined, true);
|
|
aio.Handle(player, "MythicAdvUI", "ShowUpgradeWindow", "NatureArcane");
|
|
|
|
} else if(selection === 999) {
|
|
PlayAudioOnce(player, getAudioFile(NPCType.SYLVIA, "goodbye"), undefined, true);
|
|
player.GossipComplete();
|
|
return false;
|
|
}
|
|
|
|
showSylviaGossip(player, creature);
|
|
|
|
return false;
|
|
};
|
|
|
|
RegisterCreatureGossipEvent(NPCIds[NPCType.SYLVIA], GossipEvents.GOSSIP_EVENT_ON_HELLO, (...args) => sylviaIntro(...args));
|
|
RegisterCreatureGossipEvent(NPCIds[NPCType.SYLVIA], GossipEvents.GOSSIP_EVENT_ON_SELECT, (...args) => sylviaSelect(...args)); |