Release Healer Man 0.1.40
This commit is contained in:
1 parent
ed0d2fea3d
commit
197a64117c
215 files changed
+5131
-181
No files matched your search
@@ -34,6 +34,14 @@ function entryDependencies(enterDungeon: () => boolean) {
|
||||
}
|
||||
|
||||
describe("online group activity entry", () => {
|
||||
it("enters with the leader's source variant and boss difficulty as a fresh shared run", async () => {
|
||||
const dependencies = entryDependencies(() => true);
|
||||
await enterOnlineGroupActivity({ ...DUNGEON_ACTIVITY, difficultyId: "heroic", bossDifficulty: 9 },
|
||||
"damage", vi.fn(), dependencies);
|
||||
expect(dependencies.enterDungeon).toHaveBeenCalledWith("deadmines", "damage", {
|
||||
difficultyId: "heroic", bossDifficulty: 9, freshRun: true,
|
||||
});
|
||||
});
|
||||
it("leaves a dungeon activity unconsumed when dungeon entry fails", async () => {
|
||||
const consumeActivity = vi.fn();
|
||||
const dependencies = entryDependencies(() => false);
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { ManastormPartySize } from "../game/manastormProgress";
|
||||
import type { OnlineGroupActivity } from "./onlineGroupTypes";
|
||||
import { useContentManagerSnapshot } from "../content/useContentManager";
|
||||
import { SHELVED_MODES_ENABLED } from "./releaseFeatures";
|
||||
import type { DungeonEntrySettings } from "./dungeonShellRuntimeBridge";
|
||||
import { installShelvedOnlineGroupRuntime } from "@shelvedOnlineGroupRuntime";
|
||||
|
||||
const GROUP_POLL_MS = 2_500;
|
||||
@@ -17,7 +18,7 @@ const ACTIVITY_JOIN_WINDOW_MS = 10 * 60 * 1000;
|
||||
|
||||
interface OnlineGroupActivityEntryDependencies {
|
||||
readonly installDungeonRuntime: () => Promise<void>;
|
||||
readonly enterDungeon: (dungeonId: string, role?: PartyRole) => boolean;
|
||||
readonly enterDungeon: (dungeonId: string, role?: PartyRole, settings?: DungeonEntrySettings) => boolean;
|
||||
readonly installManastormRuntime: () => Promise<void>;
|
||||
readonly enterManastorm: (role?: PartyRole, partySize?: ManastormPartySize, startingLevel?: number) => boolean;
|
||||
}
|
||||
@@ -27,7 +28,7 @@ const DEFAULT_ACTIVITY_ENTRY_DEPENDENCIES: OnlineGroupActivityEntryDependencies
|
||||
const { installDungeonShellRuntime } = await import("./dungeonShellRuntime");
|
||||
installDungeonShellRuntime();
|
||||
},
|
||||
enterDungeon: (dungeonId, role) => useShellStore.getState().enterDungeon(dungeonId, role),
|
||||
enterDungeon: (dungeonId, role, settings) => useShellStore.getState().enterDungeon(dungeonId, role, settings),
|
||||
installManastormRuntime: async () => {
|
||||
await installShelvedOnlineGroupRuntime();
|
||||
},
|
||||
@@ -47,7 +48,11 @@ export async function enterOnlineGroupActivity(
|
||||
let entered: boolean;
|
||||
if (activity.type === "dungeon") {
|
||||
await dependencies.installDungeonRuntime();
|
||||
entered = dependencies.enterDungeon(activity.contentId, role);
|
||||
entered = activity.bossDifficulty === undefined && activity.difficultyId === undefined
|
||||
? dependencies.enterDungeon(activity.contentId, role)
|
||||
: dependencies.enterDungeon(activity.contentId, role, {
|
||||
difficultyId: activity.difficultyId, bossDifficulty: activity.bossDifficulty, freshRun: true,
|
||||
});
|
||||
} else {
|
||||
if (!SHELVED_MODES_ENABLED) return false;
|
||||
await dependencies.installManastormRuntime();
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
updateCharacterEquipment,
|
||||
updateCharacterDungeonRunCheckpoint,
|
||||
updateCharacterInventory,
|
||||
updateCharacterBossCards,
|
||||
updateCharacterManastormProgress,
|
||||
updateCharacterProgression,
|
||||
updateCharacterSettings,
|
||||
@@ -381,6 +382,20 @@ describe("local profile repository", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("persists card copies and claim receipts per character across roster reloads", () => {
|
||||
const storage = new MemoryStorage();
|
||||
const offline = createOfflineSession();
|
||||
const draft = { ownerId: offline.ownerId, name: "Collector", raceId: "human" as const, classId: "priest" as const, gender: "female" as const, appearance: { skinColor: 0, face: 0, hairStyle: 0, hairColor: 0, feature: 0 } };
|
||||
const character = createCharacter(draft, storage).character!;
|
||||
const other = createCharacter({ ...draft, name: "Other" }, storage).character!;
|
||||
const bossCards = { counts: { "wow:lady anacondra": { normal: 7, epic: 1 } }, lastClaims: { spawn: "kill:7" } };
|
||||
expect(updateCharacterBossCards(offline.ownerId, character.id, bossCards, storage)?.bossCards).toEqual(bossCards);
|
||||
const roster = listCharacters(offline.ownerId, storage);
|
||||
expect(roster.find((entry) => entry.id === character.id)?.bossCards).toEqual(bossCards);
|
||||
expect(roster.find((entry) => entry.id === other.id)?.bossCards).toEqual({ counts: {}, lastClaims: {} });
|
||||
expect(roster.find((entry) => entry.id === character.id)?.inventory).toEqual([]);
|
||||
});
|
||||
|
||||
it("persists threat-meter visibility and position per character", () => {
|
||||
const storage = new MemoryStorage();
|
||||
const offline = createOfflineSession();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { CharacterProfile, PlayerSession } from "./types";
|
||||
import { normalizeBossCardCollection, type BossCardCollection } from "../game/bossCards";
|
||||
import { normalizeUiPreferences, type UiPreferences } from "../game/uiLayout";
|
||||
import {
|
||||
contentCategoryForClass,
|
||||
@@ -190,6 +191,7 @@ function normalizeCharacterProfile(value: CharacterProfile): CharacterProfile {
|
||||
actionLoadouts,
|
||||
inventory,
|
||||
equipment,
|
||||
bossCards: normalizeBossCardCollection(value.bossCards),
|
||||
manastormProgress,
|
||||
dungeonRunCheckpoint,
|
||||
settings,
|
||||
@@ -728,6 +730,24 @@ export function updateCharacterEquipment(
|
||||
return updated;
|
||||
}
|
||||
|
||||
export function updateCharacterBossCards(
|
||||
ownerId: string,
|
||||
characterId: string,
|
||||
bossCards: BossCardCollection,
|
||||
storage: StorageLike | null = browserLocalStorage(),
|
||||
): CharacterProfile | null {
|
||||
if (!storage) return null;
|
||||
const database = readDatabase(storage);
|
||||
let updated: CharacterProfile | null = null;
|
||||
database.rosters[ownerId] = (database.rosters[ownerId] ?? []).map((character) => {
|
||||
if (character.id !== characterId) return character;
|
||||
updated = normalizeCharacterProfile({ ...character, bossCards });
|
||||
return updated;
|
||||
});
|
||||
if (updated) writeDatabase(database, storage);
|
||||
return updated;
|
||||
}
|
||||
|
||||
export function updateCharacterSettings(
|
||||
ownerId: string,
|
||||
characterId: string,
|
||||
|
||||
@@ -17,6 +17,17 @@ beforeEach(() => {
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
describe("offline dungeon leaderboard", () => {
|
||||
it("isolates all ten difficulty levels, ranks, pages, and legacy records", () => {
|
||||
saveLocalDungeonRun(record("legacy-fastest", 1));
|
||||
for (let difficulty = 1; difficulty <= 10; difficulty += 1) {
|
||||
const run = { ...record(`slider-run-${difficulty}`, 10000 + difficulty), bossDifficulty: difficulty };
|
||||
saveLocalDungeonRun(run);
|
||||
expect(localDungeonLeaderboard(dungeon.id, dungeon.defaultDifficultyId, "final", 0, difficulty).entries
|
||||
.map((entry) => entry.id)).toEqual([run.id]);
|
||||
expect(localDungeonRunPlacement(run)).toEqual({ finalRank: 1, finalTotal: 1, fullRank: 1, fullTotal: 1 });
|
||||
}
|
||||
expect(localDungeonLeaderboard(dungeon.id, dungeon.defaultDifficultyId, "final").total).toBe(1);
|
||||
});
|
||||
it("persists full/final filters, ties, pages, and the placement of a run outside the first page", () => {
|
||||
for (let index = 0; index < 25; index += 1) saveLocalDungeonRun(record(`offline-${index}`, 1000 + index * 100));
|
||||
const fastest = record("offline-fastest", 500, false);
|
||||
|
||||
@@ -41,8 +41,9 @@ export function saveLocalDungeonRun(record: DungeonRunRecord): void {
|
||||
}
|
||||
|
||||
export function localDungeonLeaderboard(dungeonId: string, difficultyId: string,
|
||||
filter: DungeonClearFilter, offset = 0): DungeonLeaderboard {
|
||||
filter: DungeonClearFilter, offset = 0, bossDifficulty = 0): DungeonLeaderboard {
|
||||
const runs = localRuns().filter((entry) => entry.dungeonId === dungeonId && entry.difficultyId === difficultyId
|
||||
&& (entry.bossDifficulty ?? 0) === bossDifficulty
|
||||
&& (filter === "final" || entry.fullClear)).sort((a, b) => a.durationMs - b.durationMs
|
||||
|| a.finishedAt - b.finishedAt || a.id.localeCompare(b.id));
|
||||
return { total: runs.length, entries: runs.slice(offset, offset + DUNGEON_LEADERBOARD_PAGE_SIZE)
|
||||
@@ -50,18 +51,19 @@ export function localDungeonLeaderboard(dungeonId: string, difficultyId: string,
|
||||
}
|
||||
|
||||
export function localDungeonRunPlacement(record: DungeonRunRecord): DungeonRunPlacement {
|
||||
const final = localDungeonLeaderboard(record.dungeonId, record.difficultyId, "final");
|
||||
const full = localDungeonLeaderboard(record.dungeonId, record.difficultyId, "full");
|
||||
const runs = localRuns().filter((entry) => entry.dungeonId === record.dungeonId && entry.difficultyId === record.difficultyId);
|
||||
const final = localDungeonLeaderboard(record.dungeonId, record.difficultyId, "final", 0, record.bossDifficulty ?? 0);
|
||||
const full = localDungeonLeaderboard(record.dungeonId, record.difficultyId, "full", 0, record.bossDifficulty ?? 0);
|
||||
const runs = localRuns().filter((entry) => entry.dungeonId === record.dungeonId && entry.difficultyId === record.difficultyId
|
||||
&& (entry.bossDifficulty ?? 0) === (record.bossDifficulty ?? 0));
|
||||
return { finalRank: 1 + runs.filter((entry) => entry.durationMs < record.durationMs).length,
|
||||
finalTotal: final.total, fullTotal: full.total,
|
||||
fullRank: record.fullClear ? 1 + runs.filter((entry) => entry.fullClear && entry.durationMs < record.durationMs).length : null };
|
||||
}
|
||||
|
||||
export async function fetchDungeonLeaderboard(dungeonId: string, difficultyId: string,
|
||||
filter: DungeonClearFilter, offset: number, signal?: AbortSignal): Promise<DungeonLeaderboard> {
|
||||
filter: DungeonClearFilter, offset: number, signal?: AbortSignal, bossDifficulty = 0): Promise<DungeonLeaderboard> {
|
||||
const query = new URLSearchParams({ dungeon: dungeonId, difficulty: difficultyId, clear: filter,
|
||||
offset: String(offset), limit: String(DUNGEON_LEADERBOARD_PAGE_SIZE) });
|
||||
offset: String(offset), limit: String(DUNGEON_LEADERBOARD_PAGE_SIZE), bossDifficulty: String(bossDifficulty) });
|
||||
return requestOnlineJson(`/api/dungeon-leaderboards?${query}`, { signal });
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
type PartyRole,
|
||||
} from "../game/partyRoles";
|
||||
import { useShellStore } from "./shellStore";
|
||||
import { registerDungeonShellRuntime } from "./dungeonShellRuntimeBridge";
|
||||
import { registerDungeonShellRuntime, type DungeonEntrySettings } from "./dungeonShellRuntimeBridge";
|
||||
import { touchCharacter } from "./accountRepository";
|
||||
import { specializationById } from "../game/specializationCatalog";
|
||||
import { dungeonContentIsUsable } from "../content/contentManager";
|
||||
@@ -17,7 +17,7 @@ export function installDungeonShellRuntime(): void {
|
||||
registerDungeonShellRuntime({ enterDungeon });
|
||||
}
|
||||
|
||||
function enterDungeon(dungeonId: string, requestedRole?: PartyRole): boolean {
|
||||
function enterDungeon(dungeonId: string, requestedRole?: PartyRole, settings?: DungeonEntrySettings): boolean {
|
||||
const dungeon = dungeonById(dungeonId);
|
||||
if (!dungeon || !dungeon.available) {
|
||||
useShellStore.setState({ notice: "That dungeon is not available yet." });
|
||||
@@ -55,14 +55,19 @@ function enterDungeon(dungeonId: string, requestedRole?: PartyRole): boolean {
|
||||
});
|
||||
return false;
|
||||
}
|
||||
const resume = activeCharacter.dungeonRunCheckpoint?.dungeonId === dungeonId
|
||||
const checkpoint = activeCharacter.dungeonRunCheckpoint;
|
||||
const resume = !settings?.freshRun && checkpoint?.dungeonId === dungeonId
|
||||
&& checkpoint.run?.finishedAt == null
|
||||
&& (settings?.difficultyId === undefined || checkpoint.difficultyId === settings.difficultyId)
|
||||
&& (settings?.bossDifficulty === undefined || checkpoint.run?.bossDifficulty === settings.bossDifficulty)
|
||||
? activeCharacter.dungeonRunCheckpoint
|
||||
: null;
|
||||
if (!activateDungeonSession(dungeonId, resume?.difficultyId, {
|
||||
if (!activateDungeonSession(dungeonId, settings?.difficultyId ?? resume?.difficultyId, {
|
||||
bossDifficulty: settings?.bossDifficulty ?? resume?.run?.bossDifficulty,
|
||||
spawn: resume?.spawn,
|
||||
encounterSnapshots: resume?.encounters,
|
||||
runProgress: resume?.run,
|
||||
legacyResume: Boolean(resume && !resume.run),
|
||||
legacyResume: Boolean(resume && resume.run?.bossDifficulty === undefined),
|
||||
})) {
|
||||
useShellStore.setState({
|
||||
notice: dungeon.availabilityReason ?? "That dungeon failed its latest validation gate.",
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import type { PartyRole } from "../game/partyRoles";
|
||||
|
||||
export interface DungeonShellRuntime {
|
||||
enterDungeon: (dungeonId: string, playerRole?: PartyRole) => boolean;
|
||||
enterDungeon: (dungeonId: string, playerRole?: PartyRole, settings?: DungeonEntrySettings) => boolean;
|
||||
}
|
||||
|
||||
export interface DungeonEntrySettings {
|
||||
readonly difficultyId?: string;
|
||||
readonly bossDifficulty?: number;
|
||||
readonly freshRun?: boolean;
|
||||
}
|
||||
|
||||
let installedRuntime: DungeonShellRuntime | null = null;
|
||||
@@ -13,4 +19,3 @@ export function registerDungeonShellRuntime(runtime: DungeonShellRuntime): void
|
||||
export function dungeonShellRuntime(): DungeonShellRuntime | null {
|
||||
return installedRuntime;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ export interface OnlineGroupMember {
|
||||
}
|
||||
|
||||
export interface OnlineGroupActivity {
|
||||
readonly difficultyId?: string;
|
||||
readonly bossDifficulty?: number;
|
||||
readonly id: string;
|
||||
readonly type: "dungeon" | "manastorm";
|
||||
readonly contentId: string;
|
||||
@@ -56,6 +58,8 @@ export interface OnlineGroupSnapshot {
|
||||
}
|
||||
|
||||
export interface StartOnlineGroupActivityInput {
|
||||
readonly difficultyId?: string;
|
||||
readonly bossDifficulty?: number;
|
||||
readonly type: OnlineGroupActivity["type"];
|
||||
readonly contentId: string;
|
||||
readonly fillWithAi: boolean;
|
||||
|
||||
@@ -13,7 +13,16 @@ describe("on-demand shell runtime delegation", () => {
|
||||
registerDungeonShellRuntime({ enterDungeon });
|
||||
|
||||
expect(useShellStore.getState().enterDungeon("wailing-caverns", "healer")).toBe(true);
|
||||
expect(enterDungeon).toHaveBeenCalledWith("wailing-caverns", "healer");
|
||||
expect(enterDungeon).toHaveBeenCalledWith("wailing-caverns", "healer", undefined);
|
||||
});
|
||||
|
||||
it("delegates dungeon entry settings to its phase runtime", () => {
|
||||
const enterDungeon = vi.fn(() => true);
|
||||
const settings = { difficultyId: "heroic", bossDifficulty: 5, freshRun: true };
|
||||
registerDungeonShellRuntime({ enterDungeon });
|
||||
|
||||
expect(useShellStore.getState().enterDungeon("wailing-caverns", "healer", settings)).toBe(true);
|
||||
expect(enterDungeon).toHaveBeenCalledWith("wailing-caverns", "healer", settings);
|
||||
});
|
||||
|
||||
normalReleaseIt("ignores registered Manastorm delegates in a normal release", () => {
|
||||
|
||||
+17
-4
@@ -11,6 +11,7 @@ import {
|
||||
updateCharacterActionBindings,
|
||||
updateCharacterEquipment,
|
||||
updateCharacterInventory,
|
||||
updateCharacterBossCards,
|
||||
updateCharacterManastormProgress,
|
||||
updateCharacterDungeonRunCheckpoint,
|
||||
updateCharacterProgression,
|
||||
@@ -38,6 +39,7 @@ import type { CharacterDraft, CharacterProfile, PlayerSession, ShellPhase } from
|
||||
import type { DungeonId } from "../game/dungeonRegistry";
|
||||
import { DEFAULT_DUNGEON_ID } from "../game/dungeonDefaults";
|
||||
import type { InventoryItem } from "../game/lootTypes";
|
||||
import type { BossCardCollection } from "../game/bossCards";
|
||||
import type { EquipmentAssignments } from "../game/equipment";
|
||||
import type { GameplaySettings } from "../game/combatStore";
|
||||
import {
|
||||
@@ -48,7 +50,7 @@ import {
|
||||
import type { PartyRole } from "../game/partyRoles";
|
||||
import type { ManastormPartySize, ManastormProgress } from "../game/manastormProgress";
|
||||
import type { DungeonRunCheckpoint } from "../game/dungeonRunCheckpoint";
|
||||
import { dungeonShellRuntime } from "./dungeonShellRuntimeBridge";
|
||||
import { dungeonShellRuntime, type DungeonEntrySettings } from "./dungeonShellRuntimeBridge";
|
||||
import { manastormShellRuntime } from "./manastormShellRuntimeBridge";
|
||||
import {
|
||||
activateOnlineSession,
|
||||
@@ -95,7 +97,7 @@ export interface ShellState {
|
||||
chooseRomSecondaryClass: (classId: RomClassId) => boolean;
|
||||
swapSelectedRomClasses: () => boolean;
|
||||
returnToMainMenu: () => void;
|
||||
enterDungeon: (dungeonId: string, playerRole?: PartyRole) => boolean;
|
||||
enterDungeon: (dungeonId: string, playerRole?: PartyRole, settings?: DungeonEntrySettings) => boolean;
|
||||
enterManastorm: (
|
||||
playerRole?: PartyRole,
|
||||
partySize?: ManastormPartySize,
|
||||
@@ -109,6 +111,7 @@ export interface ShellState {
|
||||
saveActiveProgression: (level: number, experience: number, talentRanks: Record<string, number>) => void;
|
||||
saveActiveActionBindings: (actionBindings: Readonly<Record<string, string | null>>) => void;
|
||||
saveActiveInventory: (inventory: readonly InventoryItem[]) => void;
|
||||
saveActiveBossCards: (bossCards: BossCardCollection) => void;
|
||||
saveActiveEquipment: (equipment: EquipmentAssignments) => void;
|
||||
saveActiveSettings: (settings: GameplaySettings) => void;
|
||||
updateCombatPresentationSettings: (patch: Partial<CombatPresentationSettings>) => void;
|
||||
@@ -450,9 +453,9 @@ export const useShellStore = create<ShellState>((set, get) => ({
|
||||
}
|
||||
set({ phase: "main-menu", selectedCharacterId: currentId, activeCharacter: null, notice: "" });
|
||||
},
|
||||
enterDungeon: (dungeonId, requestedRole) => {
|
||||
enterDungeon: (dungeonId, requestedRole, settings) => {
|
||||
const runtime = dungeonShellRuntime();
|
||||
if (runtime) return runtime.enterDungeon(dungeonId, requestedRole);
|
||||
if (runtime) return runtime.enterDungeon(dungeonId, requestedRole, settings);
|
||||
set({ notice: "Dungeon data is still loading. Try again in a moment." });
|
||||
return false;
|
||||
},
|
||||
@@ -535,6 +538,16 @@ export const useShellStore = create<ShellState>((set, get) => ({
|
||||
characters: state.characters.map((character) => character.id === updated.id ? updated : character),
|
||||
}));
|
||||
},
|
||||
saveActiveBossCards: (bossCards) => {
|
||||
const { session, activeCharacter } = get();
|
||||
if (!session || !activeCharacter) return;
|
||||
const updated = updateCharacterBossCards(session.ownerId, activeCharacter.id, bossCards);
|
||||
if (!updated) return;
|
||||
set((state) => ({
|
||||
activeCharacter: updated,
|
||||
characters: state.characters.map((character) => character.id === updated.id ? updated : character),
|
||||
}));
|
||||
},
|
||||
saveActiveEquipment: (equipment) => {
|
||||
const { session, activeCharacter } = get();
|
||||
if (!session || !activeCharacter) return;
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { EquipmentAssignments } from "../game/equipment";
|
||||
import type { GameplaySettings } from "../game/combatStore";
|
||||
import type { ManastormProgress } from "../game/manastormProgress";
|
||||
import type { DungeonRunCheckpoint } from "../game/dungeonRunCheckpoint";
|
||||
import type { BossCardCollection } from "../game/bossCards";
|
||||
|
||||
export type ShellPhase = "login" | "characters" | "create-character" | "main-menu" | "class-hall" | "dungeons" | "manastorms" | "arenas" | "arena-game" | "gm-admin" | "game";
|
||||
export type SessionKind = "account" | "offline";
|
||||
@@ -41,6 +42,7 @@ export interface CharacterProfile {
|
||||
actionLoadouts?: Record<string, Record<string, string | null>>;
|
||||
inventory: InventoryItem[];
|
||||
equipment: EquipmentAssignments;
|
||||
bossCards?: BossCardCollection;
|
||||
/** Present on normalized and newly created profiles; optional for legacy callers. */
|
||||
manastormProgress?: ManastormProgress;
|
||||
/** Safe resume point only; active combat, timers, and temporary waves are never persisted. */
|
||||
|
||||
@@ -100,6 +100,9 @@ export function CombatBridge() {
|
||||
if (state.inventory !== previousState.inventory) {
|
||||
shell.saveActiveInventory(state.inventory);
|
||||
}
|
||||
if (state.bossCards !== previousState.bossCards) {
|
||||
shell.saveActiveBossCards(state.bossCards);
|
||||
}
|
||||
if (state.equipment !== previousState.equipment) {
|
||||
shell.saveActiveEquipment(state.equipment);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ function grantEncounterLoot(lootTableId: string, now: number): boolean {
|
||||
if (!table) return false;
|
||||
const combat = useCombatStore.getState();
|
||||
const drops = rollBossLoot(table.bossKey, combat.level, { now });
|
||||
if (!drops.length) return false;
|
||||
if (!drops.length) return true;
|
||||
const inventory = [...combat.inventory, ...drops];
|
||||
useCombatStore.setState({ inventory });
|
||||
useShellStore.getState().saveActiveInventory(inventory);
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
resetDungeonEncounters,
|
||||
} from "./dungeonEncounterRuntime";
|
||||
import { applySharedWorld } from "./OnlineSessionBridge";
|
||||
import { useCombatStore } from "./combatStore";
|
||||
import { useGameStore } from "./store";
|
||||
import {
|
||||
WAILING_NARALEX_ENCOUNTER,
|
||||
@@ -58,4 +59,19 @@ describe("online shared-world hydration", () => {
|
||||
revision: authority.revision,
|
||||
});
|
||||
});
|
||||
|
||||
it("awards the authority's card rolls once to a follower, including after reconnect", () => {
|
||||
useCombatStore.getState().initializeCharacter({ classId: "priest", level: 20 });
|
||||
useCombatStore.getState().registerMob("shared-boss", { name: "Lady Anacondra", boss: true, maxHealth: 1 });
|
||||
const mob = useCombatStore.getState().mobs["shared-boss"];
|
||||
const world = { ...EMPTY_SHARED_WORLD, mobs: { "shared-boss": { ...mob, dead: true, looted: true, health: 0, bossCardDrop: { bossKey: "wow:lady anacondra", spawnId: mob.id, claimId: "shared-kill:1", rarities: ["normal", "epic"] as const } } } };
|
||||
applySharedWorld(world);
|
||||
applySharedWorld(world);
|
||||
expect(useCombatStore.getState().bossCards.counts["wow:lady anacondra"]).toEqual({ normal: 1, epic: 1 });
|
||||
const bossCards = JSON.parse(JSON.stringify(useCombatStore.getState().bossCards));
|
||||
useCombatStore.getState().initializeCharacter({ classId: "priest", level: 20, bossCards });
|
||||
applySharedWorld(world);
|
||||
expect(useCombatStore.getState().bossCards.counts["wow:lady anacondra"]).toEqual({ normal: 1, epic: 1 });
|
||||
expect(useCombatStore.getState().inventory).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -89,6 +89,11 @@ function sharedWorld(): OnlineSharedWorld {
|
||||
|
||||
export function applySharedWorld(world: OnlineSharedWorld): void {
|
||||
useCombatStore.setState({ mobs: world.mobs });
|
||||
for (const mob of Object.values(world.mobs)) {
|
||||
if (mob.dead && mob.looted && mob.bossCardDrop) {
|
||||
useCombatStore.getState().collectSharedBossCards(mob.bossCardDrop, mob.name, mob.diedAt ?? Date.now());
|
||||
}
|
||||
}
|
||||
usePartyStore.setState({
|
||||
members: world.partyMembers,
|
||||
command: world.partyRuntime.command as ReturnType<typeof usePartyStore.getState>["command"],
|
||||
|
||||
@@ -194,7 +194,7 @@ export type AbilityEffect =
|
||||
/** Fraction of the target ability's remaining cooldown removed at execution time. */
|
||||
readonly amountPercent?: number;
|
||||
}
|
||||
| { readonly kind: "trigger-spell"; readonly spellId: number; readonly withValue?: number }
|
||||
| { readonly kind: "trigger-spell"; readonly spellId: number; readonly withValue?: number; readonly expanded?: boolean }
|
||||
| {
|
||||
/** A known combat-side DBC script hook that requires a spell-specific handler. */
|
||||
readonly kind: "scripted";
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { preloadAbilityCatalogsForClasses } from "../abilityCatalog";
|
||||
import { PLAYER_AGGRO_ID } from "../aggro";
|
||||
import { installCombatClock } from "../combatClock";
|
||||
import { subscribeCombatEvents } from "../combatEvents";
|
||||
import { resetCombatSpatialProvider } from "../combatSpatial";
|
||||
import { useCombatStore } from "../combatStore";
|
||||
import { dungeonEnemyScaling } from "../dungeonBossScaling";
|
||||
import { requireDungeonDefinition } from "../dungeonRegistry";
|
||||
import { clearMobRuntimeRegistry, setMobPosition } from "../mobRuntimeRegistry";
|
||||
import { advanceDungeonPartyCombat, partyCombatRangeBand, resetPartyRuntimeClock } from "../partyRuntime";
|
||||
import { clearPartyRuntimeRegistry, registerPartyRuntimePosition, updatePartyRuntimeTargetVisibility } from "../partyRuntimeRegistry";
|
||||
import { usePartyStore } from "../partyStore";
|
||||
import { specializationsForClass } from "../specializationCatalog";
|
||||
import { BenchmarkController } from "./benchmarkController";
|
||||
import type { BenchmarkBuildDefinition, BenchmarkScenarioDefinition } from "./benchmarkTypes";
|
||||
|
||||
const scenario: BenchmarkScenarioDefinition = {
|
||||
id: "dungeon-boss", version: 1, name: "Dungeon boss", description: "Full party against an authored dungeon boss",
|
||||
family: "damage", roles: ["damage", "tank", "healer"], durationMs: 60_000, tickMs: 100,
|
||||
hostileTargets: 1, partyLayout: "melee", attacks: [],
|
||||
};
|
||||
|
||||
/** Uses the live boss stats, companion AI and baseline player policy with a controlled clock. */
|
||||
export async function runDungeonBossBenchmark(
|
||||
build: BenchmarkBuildDefinition, bossId: string, seed: string, mechanics = false,
|
||||
) {
|
||||
await preloadAbilityCatalogsForClasses([build.classId]);
|
||||
const dungeon = requireDungeonDefinition("ragefire-chasm");
|
||||
const spawn = dungeon.staticSpawns.find((entry) => entry.id === bossId)!;
|
||||
const entity = dungeon.entities[spawn.entityId];
|
||||
const scaling = dungeonEnemyScaling(dungeon, entity, bossId, build.level, "normal", 1);
|
||||
const specs = specializationsForClass(build.classId).filter((spec) => spec.role === build.role);
|
||||
const specialization = specs.find((spec) => spec.displayName === build.orientation) ?? specs[0];
|
||||
clearMobRuntimeRegistry(); clearPartyRuntimeRegistry(); resetPartyRuntimeClock(); resetCombatSpatialProvider();
|
||||
let now = 1_000_000;
|
||||
const restoreClock = installCombatClock(() => now);
|
||||
let playerDamage = 0; let partyDamage = 0; let incomingDamage = 0; let healing = 0; let casts = 0;
|
||||
const damageByAbility: Record<string, number> = {};
|
||||
const stop = subscribeCombatEvents((event) => {
|
||||
if (event.kind === "damage" && event.targetActorId === bossId) {
|
||||
if (event.sourceActorId === PLAYER_AGGRO_ID) playerDamage += event.effectiveAmount;
|
||||
else partyDamage += event.effectiveAmount;
|
||||
const key = `${event.sourceActorId}:${event.abilityId ?? "unknown"}`;
|
||||
damageByAbility[key] = (damageByAbility[key] ?? 0) + event.effectiveAmount;
|
||||
}
|
||||
if (event.kind === "damage" && event.sourceActorId === bossId) incomingDamage += event.effectiveAmount;
|
||||
if (event.kind === "healing") healing += event.effectiveAmount;
|
||||
});
|
||||
try {
|
||||
const combat = useCombatStore.getState();
|
||||
combat.initializeCharacter({ classId: build.classId, secondaryClassId: build.secondaryClassId,
|
||||
specializationId: specialization?.id, role: build.role, level: build.level,
|
||||
talentRanks: build.talentRanks, inventory: build.inventory, equipment: build.equipment });
|
||||
combat.setPlayerPosition([-2, 0, 0]);
|
||||
usePartyStore.getState().initializeParty(seed, build.level, useCombatStore.getState().maxHealth, build.role, 5, build.source);
|
||||
combat.synchronizeActors();
|
||||
combat.registerMob(bossId, { name: entity.name, boss: true, serverEntry: entity.combat?.serverEntry,
|
||||
...scaling, attacks: entity.combat?.attacks, xpReward: 0 });
|
||||
setMobPosition(bossId, [0, 0, 0]);
|
||||
for (const member of usePartyStore.getState().members) {
|
||||
registerPartyRuntimePosition(member.id, [-partyCombatRangeBand(member).preferred, 0, 0]);
|
||||
updatePartyRuntimeTargetVisibility(member.id, bossId, true);
|
||||
}
|
||||
combat.selectMob(bossId);
|
||||
const tank = usePartyStore.getState().members.find((member) => member.role === "tank");
|
||||
combat.damageMob(bossId, 1, now, {
|
||||
actorId: build.role === "tank" ? PLAYER_AGGRO_ID : tank!.id, role: "tank", abilityId: "benchmark-pull",
|
||||
}, "physical");
|
||||
const controller = new BenchmarkController();
|
||||
for (let elapsed = 100; elapsed <= scenario.durationMs; elapsed += 100) {
|
||||
now = 1_000_000 + elapsed;
|
||||
combat.tick(.1, now);
|
||||
advanceDungeonPartyCombat(now);
|
||||
if (mechanics) combat.advanceMobCombat(now);
|
||||
if (controller.act(now, build.role, scenario, false).cast) casts += 1;
|
||||
const state = useCombatStore.getState();
|
||||
const mob = state.mobs[bossId];
|
||||
if (mob.dead || (mechanics && state.health <= 0)) {
|
||||
return { seconds: mob.dead ? ((mob.diedAt ?? now) - 1_000_000) / 1000 : null,
|
||||
elapsedSeconds: elapsed / 1000, outcome: mob.dead ? "defeated" : "player-death",
|
||||
maxHealth: scaling.maxHealth, playerDamage, partyDamage, incomingDamage, healing, casts, damageByAbility,
|
||||
playerAlive: state.health > 0, deaths: usePartyStore.getState().members.filter((member) => member.health <= 0).length,
|
||||
attacks: mob.attackRevision, specialization: specialization?.displayName,
|
||||
party: usePartyStore.getState().members.map((member) => `${member.classId}:${member.specialization}`) };
|
||||
}
|
||||
}
|
||||
return { seconds: null, elapsedSeconds: scenario.durationMs / 1000, outcome: "timeout",
|
||||
maxHealth: scaling.maxHealth, remainingHealth: useCombatStore.getState().mobs[bossId].health,
|
||||
playerDamage, partyDamage, incomingDamage, healing, casts, damageByAbility,
|
||||
playerAlive: useCombatStore.getState().health > 0,
|
||||
deaths: usePartyStore.getState().members.filter((member) => member.health <= 0).length,
|
||||
attacks: useCombatStore.getState().mobs[bossId].attackRevision,
|
||||
specialization: specialization?.displayName,
|
||||
party: usePartyStore.getState().members.map((member) => `${member.classId}:${member.specialization}`) };
|
||||
} finally {
|
||||
stop(); restoreClock(); clearMobRuntimeRegistry(); clearPartyRuntimeRegistry();
|
||||
resetPartyRuntimeClock(); resetCombatSpatialProvider();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { DUNGEON_DEFINITIONS } from "./dungeonRegistry";
|
||||
import type { DungeonDefinition } from "./dungeonTypes";
|
||||
import type { MobModelStyle } from "./mobPopulation";
|
||||
import { BOSS_CARD_RARITIES, type BossCardCollection, type BossCardRarity } from "./bossCards";
|
||||
|
||||
export interface BossCardDefinition {
|
||||
readonly key: string;
|
||||
readonly name: string;
|
||||
readonly category: "wow" | "rom";
|
||||
readonly model?: MobModelStyle;
|
||||
readonly dungeons: readonly { readonly id: string; readonly title: string }[];
|
||||
readonly sources: readonly { readonly dungeonId: string; readonly entityId: string }[];
|
||||
}
|
||||
|
||||
/** Same boss in multiple dungeons shares a collection, while WoW and RoM remain distinct. */
|
||||
export function buildBossCardCatalog(definitions: readonly DungeonDefinition[]): readonly BossCardDefinition[] {
|
||||
const bosses = new Map<string, BossCardDefinition>();
|
||||
for (const dungeon of definitions) {
|
||||
for (const entity of Object.values(dungeon.entities)) {
|
||||
if (entity.kind !== "boss") continue;
|
||||
const category = dungeon.expansion === "runewaker" ? "rom" : "wow";
|
||||
const key = `${category}:${entity.name.trim().toLowerCase().replace(/\s+/g, " ")}`;
|
||||
const existing = bosses.get(key);
|
||||
bosses.set(key, {
|
||||
key,
|
||||
name: existing?.name ?? entity.name,
|
||||
category,
|
||||
model: existing?.model ?? entity.visual.model,
|
||||
dungeons: existing?.dungeons.some((entry) => entry.id === dungeon.id)
|
||||
? existing.dungeons
|
||||
: [...(existing?.dungeons ?? []), { id: dungeon.id, title: dungeon.title }],
|
||||
sources: [...(existing?.sources ?? []), { dungeonId: dungeon.id, entityId: entity.id }],
|
||||
});
|
||||
}
|
||||
}
|
||||
return [...bosses.values()].map((boss) => ({
|
||||
...boss,
|
||||
dungeons: [...boss.dungeons].sort((a, b) => a.title.localeCompare(b.title)),
|
||||
})).sort((a, b) => a.name.localeCompare(b.name) || a.key.localeCompare(b.key));
|
||||
}
|
||||
|
||||
export const BOSS_CARD_CATALOG = buildBossCardCatalog(DUNGEON_DEFINITIONS);
|
||||
const cardsByKey = new Map(BOSS_CARD_CATALOG.map((card) => [card.key, card]));
|
||||
|
||||
export function bossCardByKey(key: string): BossCardDefinition | null {
|
||||
return cardsByKey.get(key) ?? null;
|
||||
}
|
||||
|
||||
export function bossCardForSource(sourceId: string, dungeonId: string, name?: string): BossCardDefinition | null {
|
||||
return BOSS_CARD_CATALOG.find((card) => card.sources.some(
|
||||
(source) => source.dungeonId === dungeonId && source.entityId === sourceId,
|
||||
)) ?? BOSS_CARD_CATALOG.find((card) => card.sources.some((source) => source.entityId === sourceId))
|
||||
?? BOSS_CARD_CATALOG.find((card) => card.name === name && card.dungeons.some((entry) => entry.id === dungeonId))
|
||||
?? null;
|
||||
}
|
||||
|
||||
export interface BossCardRow {
|
||||
readonly boss: BossCardDefinition;
|
||||
readonly rarity: BossCardRarity;
|
||||
readonly copies: number;
|
||||
}
|
||||
export type BossCardGrouping = "all" | "dungeon" | "rarity";
|
||||
export interface BossCardGroup {
|
||||
readonly key: string;
|
||||
readonly title: string;
|
||||
readonly rows: readonly BossCardRow[];
|
||||
}
|
||||
|
||||
export function groupBossCards(
|
||||
collection: BossCardCollection,
|
||||
options: { readonly earnedOnly: boolean; readonly grouping: BossCardGrouping; readonly search?: string; readonly category?: "all" | "wow" | "rom" },
|
||||
catalog = BOSS_CARD_CATALOG,
|
||||
): readonly BossCardGroup[] {
|
||||
const query = options.search?.trim().toLowerCase() ?? "";
|
||||
const rows = catalog.flatMap((boss) => {
|
||||
if (options.category && options.category !== "all" && boss.category !== options.category) return [];
|
||||
if (query && !`${boss.name} ${boss.dungeons.map((entry) => entry.title).join(" ")}`.toLowerCase().includes(query)) return [];
|
||||
return BOSS_CARD_RARITIES.flatMap((rarity) => {
|
||||
const copies = collection.counts[boss.key]?.[rarity] ?? 0;
|
||||
return options.earnedOnly && !copies ? [] : [{ boss, rarity, copies }];
|
||||
});
|
||||
});
|
||||
if (options.grouping === "rarity") {
|
||||
return BOSS_CARD_RARITIES.map((rarity) => ({
|
||||
key: rarity, title: `${rarity.charAt(0).toUpperCase()}${rarity.slice(1)} cards`, rows: rows.filter((row) => row.rarity === rarity),
|
||||
})).filter((group) => group.rows.length);
|
||||
}
|
||||
if (options.grouping === "dungeon") {
|
||||
const groups = new Map<string, BossCardGroup>();
|
||||
for (const row of rows) for (const dungeon of row.boss.dungeons) {
|
||||
const existing = groups.get(dungeon.id);
|
||||
groups.set(dungeon.id, { key: dungeon.id, title: dungeon.title, rows: [...(existing?.rows ?? []), row] });
|
||||
}
|
||||
return [...groups.values()].sort((a, b) => a.title.localeCompare(b.title));
|
||||
}
|
||||
return rows.length ? [{ key: "all", title: "All boss cards", rows }] : [];
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { collectBossCardDrop, emptyBossCardCollection, normalizeBossCardCollection, rollBossCards, type BossCardDrop } from "./bossCards";
|
||||
import { BOSS_CARD_CATALOG, bossCardForSource, buildBossCardCatalog, groupBossCards } from "./bossCardCatalog";
|
||||
import { DUNGEON_DEFINITIONS } from "./dungeonRegistry";
|
||||
import { useCombatStore } from "./combatStore";
|
||||
import { useGameStore } from "./store";
|
||||
import { allowedLootDrops } from "./lootPolicy";
|
||||
import { EMPTY_ITEM_STATS } from "./itemStats";
|
||||
|
||||
describe("boss card rewards", () => {
|
||||
it("guarantees Normal and rolls each bonus independently at its exact boundary", () => {
|
||||
const random = vi.fn().mockReturnValueOnce(0.099).mockReturnValueOnce(0.02).mockReturnValueOnce(0.009);
|
||||
expect(rollBossCards(random)).toEqual(["normal", "rare", "legendary"]);
|
||||
expect(random).toHaveBeenCalledTimes(3);
|
||||
expect(rollBossCards(() => 0)).toEqual(["normal", "rare", "epic", "legendary"]);
|
||||
expect(rollBossCards(() => 0.1)).toEqual(["normal"]);
|
||||
expect(rollBossCards(() => NaN)).toEqual(["normal"]);
|
||||
});
|
||||
|
||||
it("counts copies and makes repeated world rewards idempotent after save normalization", () => {
|
||||
const drop: BossCardDrop = { bossKey: "wow:lady anacondra", spawnId: "boss-spawn", claimId: "kill:1", rarities: ["normal", "rare"] };
|
||||
const first = collectBossCardDrop(emptyBossCardCollection(), drop);
|
||||
expect(collectBossCardDrop(first, drop)).toBe(first);
|
||||
const restored = normalizeBossCardCollection(JSON.parse(JSON.stringify(first)));
|
||||
expect(collectBossCardDrop(restored, drop)).toBe(restored);
|
||||
expect(collectBossCardDrop(restored, { ...drop, claimId: "kill:2" }).counts[drop.bossKey]).toEqual({ normal: 2, rare: 2 });
|
||||
const later = collectBossCardDrop(restored, { ...drop, claimId: "5000:2:boss-spawn" });
|
||||
expect(collectBossCardDrop(later, { ...drop, claimId: "4000:1:boss-spawn" })).toBe(later);
|
||||
});
|
||||
|
||||
it("migrates old profiles and rejects corrupt counts", () => {
|
||||
expect(normalizeBossCardCollection(undefined)).toEqual(emptyBossCardCollection());
|
||||
expect(normalizeBossCardCollection({ counts: { boss: { normal: 3.9, rare: -1, epic: Infinity, legendary: "5" } } }).counts).toEqual({ boss: { normal: 3 } });
|
||||
});
|
||||
|
||||
it("grants cards from a boss without an equipment loot table exactly once", () => {
|
||||
const dungeon = DUNGEON_DEFINITIONS.find((entry) => entry.id === "ragefire-chasm")!;
|
||||
const boss = Object.values(dungeon.entities).find((entry) => entry.kind === "boss")!;
|
||||
useGameStore.getState().activateDungeon(dungeon.id);
|
||||
useCombatStore.getState().initializeCharacter({ classId: "priest", level: 79 });
|
||||
useCombatStore.getState().registerMob("no-loot-boss", { name: boss.name, boss: true, lootSourceId: boss.id, maxHealth: 1, xpReward: 0 });
|
||||
useCombatStore.getState().damageMob("no-loot-boss", 1, 4000);
|
||||
const key = bossCardForSource(boss.id, dungeon.id)!.key;
|
||||
expect(useCombatStore.getState().bossCards.counts[key]?.normal).toBe(1);
|
||||
expect(useCombatStore.getState().inventory).toEqual([]);
|
||||
expect(useCombatStore.getState().lootMob("no-loot-boss", 4001)).toBe(false);
|
||||
expect(useCombatStore.getState().bossCards.counts[key]?.normal).toBe(1);
|
||||
useCombatStore.getState().initializeCharacter({ classId: "priest", level: 79 });
|
||||
expect(useCombatStore.getState().bossCards).toEqual(emptyBossCardCollection());
|
||||
useGameStore.getState().activateDungeon("wailing-caverns");
|
||||
});
|
||||
|
||||
it.each([1, 40, 79, 80])("preserves non-equipment while allowing equipment only at 80 (level %i)", (level) => {
|
||||
const item = { instanceId: "i", itemId: 1, itemKey: "item", name: "Item", quality: "rare" as const, inventorySlot: "waist", armorType: "cloth", weaponType: null, itemLevel: level, sourceItemLevel: 1, stats: EMPTY_ITEM_STATS, quantity: 1, sourceBossKey: "boss", sourceBossName: "Boss", acquiredAt: 1 };
|
||||
const misc = { ...item, instanceId: "m", inventorySlot: "misc" };
|
||||
expect(allowedLootDrops([item, misc], level)).toEqual(level === 80 ? [item, misc] : [misc]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("boss card catalogue and filters", () => {
|
||||
it("covers every registered boss with its actual model and dungeon", () => {
|
||||
expect(BOSS_CARD_CATALOG.length).toBeGreaterThan(100);
|
||||
for (const dungeon of DUNGEON_DEFINITIONS) for (const boss of Object.values(dungeon.entities)) {
|
||||
if (boss.kind !== "boss") continue;
|
||||
const card = bossCardForSource(boss.id, dungeon.id)!;
|
||||
expect(card, `${dungeon.id}:${boss.id}`).not.toBeNull();
|
||||
expect(card.dungeons).toContainEqual({ id: dungeon.id, title: dungeon.title });
|
||||
if (boss.visual.model) expect(card.model).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("merges the same boss across dungeons and groups without changing earned copies", () => {
|
||||
const wc = DUNGEON_DEFINITIONS.find((entry) => entry.id === "wailing-caverns")!;
|
||||
const boss = wc.entities["lady-anacondra"];
|
||||
const catalog = buildBossCardCatalog([
|
||||
{ ...wc, entities: { [boss.id]: boss } },
|
||||
{ ...wc, id: "second-dungeon", title: "Second Dungeon", entities: { [boss.id]: boss } },
|
||||
]);
|
||||
expect(catalog).toHaveLength(1);
|
||||
expect(catalog[0].dungeons).toHaveLength(2);
|
||||
const collection = { counts: { [catalog[0].key]: { normal: 5, legendary: 1 } }, lastClaims: {} };
|
||||
expect(groupBossCards(collection, { earnedOnly: false, grouping: "all" }, catalog)[0].rows).toHaveLength(4);
|
||||
expect(groupBossCards(collection, { earnedOnly: true, grouping: "all" }, catalog)[0].rows.map((row) => row.copies)).toEqual([5, 1]);
|
||||
const dungeons = groupBossCards(collection, { earnedOnly: true, grouping: "dungeon" }, catalog);
|
||||
expect(dungeons).toHaveLength(2);
|
||||
expect(dungeons.every((group) => group.rows[0].copies === 5)).toBe(true);
|
||||
expect(groupBossCards(collection, { earnedOnly: true, grouping: "rarity" }, catalog).map((group) => group.key)).toEqual(["normal", "legendary"]);
|
||||
expect(groupBossCards(collection, { earnedOnly: false, grouping: "all", search: "second dungeon" }, catalog)[0].rows).toHaveLength(4);
|
||||
expect(groupBossCards(collection, { earnedOnly: true, grouping: "all", category: "rom" }, catalog)).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
export const BOSS_CARD_RARITIES = ["normal", "rare", "epic", "legendary"] as const;
|
||||
export type BossCardRarity = (typeof BOSS_CARD_RARITIES)[number];
|
||||
|
||||
export const BOSS_CARD_RARITY_INFO = {
|
||||
normal: { label: "Normal", chance: 1, odds: "100%", color: "#d8d5cb" },
|
||||
rare: { label: "Rare", chance: 1 / 10, odds: "1 in 10", color: "#69b4ff" },
|
||||
epic: { label: "Epic", chance: 1 / 50, odds: "1 in 50", color: "#c493ff" },
|
||||
legendary: { label: "Legendary", chance: 1 / 100, odds: "1 in 100", color: "#ffb45b" },
|
||||
} as const;
|
||||
|
||||
export type BossCardCounts = Partial<Record<BossCardRarity, number>>;
|
||||
export interface BossCardCollection {
|
||||
readonly counts: Readonly<Record<string, BossCardCounts>>;
|
||||
/** Last reward per spawned boss prevents shared-world replay and reconnect duplicates. */
|
||||
readonly lastClaims: Readonly<Record<string, string>>;
|
||||
}
|
||||
|
||||
export interface BossCardDrop {
|
||||
readonly bossKey: string;
|
||||
readonly spawnId: string;
|
||||
readonly claimId: string;
|
||||
readonly rarities: readonly BossCardRarity[];
|
||||
}
|
||||
|
||||
export function emptyBossCardCollection(): BossCardCollection {
|
||||
return { counts: {}, lastClaims: {} };
|
||||
}
|
||||
|
||||
export function normalizeBossCardCollection(value: unknown): BossCardCollection {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return emptyBossCardCollection();
|
||||
const raw = value as Partial<BossCardCollection>;
|
||||
const counts: Record<string, BossCardCounts> = Object.create(null);
|
||||
if (raw.counts && typeof raw.counts === "object" && !Array.isArray(raw.counts)) {
|
||||
for (const [key, candidate] of Object.entries(raw.counts)) {
|
||||
if (!key || !candidate || typeof candidate !== "object" || Array.isArray(candidate)) continue;
|
||||
const entry: BossCardCounts = {};
|
||||
for (const rarity of BOSS_CARD_RARITIES) {
|
||||
const count = candidate[rarity];
|
||||
if (typeof count === "number" && Number.isFinite(count) && count >= 1) {
|
||||
entry[rarity] = Math.min(Number.MAX_SAFE_INTEGER, Math.floor(count));
|
||||
}
|
||||
}
|
||||
if (Object.keys(entry).length) counts[key] = entry;
|
||||
}
|
||||
}
|
||||
const lastClaims = raw.lastClaims && typeof raw.lastClaims === "object" && !Array.isArray(raw.lastClaims)
|
||||
? Object.fromEntries(Object.entries(raw.lastClaims).filter(([key, claim]) => key && typeof claim === "string"))
|
||||
: {};
|
||||
return { counts, lastClaims };
|
||||
}
|
||||
|
||||
/** Independent bonus rolls; Normal is always awarded, including with all three bonuses. */
|
||||
export function rollBossCards(random: () => number = Math.random): readonly BossCardRarity[] {
|
||||
return BOSS_CARD_RARITIES.filter((rarity) => {
|
||||
if (rarity === "normal") return true;
|
||||
const roll = random();
|
||||
return Number.isFinite(roll) && Math.max(0, Math.min(1, roll)) < BOSS_CARD_RARITY_INFO[rarity].chance;
|
||||
});
|
||||
}
|
||||
|
||||
export function collectBossCardDrop(collection: BossCardCollection, drop: BossCardDrop): BossCardCollection {
|
||||
const lastClaim = collection.lastClaims[drop.spawnId];
|
||||
if (lastClaim === drop.claimId) return collection;
|
||||
// Generated claims begin with the death's epoch time. Ignore older snapshots
|
||||
// even after the same spawn has awarded a subsequent kill.
|
||||
if (lastClaim && /^\d+:/.test(lastClaim) && /^\d+:/.test(drop.claimId)) {
|
||||
const [lastTime, lastRevision] = lastClaim.split(":").map(Number);
|
||||
const [time, revision] = drop.claimId.split(":").map(Number);
|
||||
if (time < lastTime || (time === lastTime && revision <= lastRevision)) return collection;
|
||||
}
|
||||
const counts = { ...collection.counts[drop.bossKey] };
|
||||
for (const rarity of new Set(drop.rarities)) {
|
||||
if (BOSS_CARD_RARITIES.includes(rarity)) {
|
||||
counts[rarity] = Math.min(Number.MAX_SAFE_INTEGER, (counts[rarity] ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
return {
|
||||
counts: { ...collection.counts, [drop.bossKey]: counts },
|
||||
lastClaims: { ...collection.lastClaims, [drop.spawnId]: drop.claimId },
|
||||
};
|
||||
}
|
||||
@@ -188,11 +188,19 @@ describe("Ascension Conquest ability catalog", () => {
|
||||
});
|
||||
expect(Object.values(COA_TRIGGERED_ABILITIES_BY_SPELL_ID).filter((ability) => (
|
||||
ability.effects.some((effect) => effect.kind !== "utility")
|
||||
))).toHaveLength(547);
|
||||
))).toHaveLength(546);
|
||||
for (const [spellId, ability] of Object.entries(COA_TRIGGERED_ABILITIES_BY_SPELL_ID)) {
|
||||
expect(ability.dbcSpellId, spellId).toBe(Number(spellId));
|
||||
expect(ability.effects.length, spellId).toBeGreaterThan(0);
|
||||
expect(ability.effects.some((effect) => effect.kind === "scripted" || effect.kind === "unsupported"), spellId).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("does not invent damage for proc auras or cooldown-only trigger payloads", () => {
|
||||
expect(COA_TRIGGERED_ABILITIES_BY_SPELL_ID[707821].effects.some((effect) => effect.kind === "damage")).toBe(false);
|
||||
expect(COA_TRIGGERED_ABILITIES_BY_SPELL_ID[807661].effects.every((effect) => effect.kind === "utility")).toBe(true);
|
||||
expect(COA_TRIGGERED_ABILITIES_BY_SPELL_ID[712682].effects).toContainEqual(
|
||||
expect.objectContaining({ kind: "trigger-spell", spellId: 712476, expanded: true }),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -880,6 +880,9 @@ function effectsForSpell(
|
||||
effects.push({
|
||||
kind: "trigger-spell",
|
||||
spellId: triggeredSpellId,
|
||||
// Expanded payloads below execute in the parent; retain this marker
|
||||
// for source lookup without executing the same payload a second time.
|
||||
expanded: depth < 2 && !visitedSpellIds.has(triggeredSpellId) && Boolean(coaSourceSpellById(triggeredSpellId)),
|
||||
...([141, 142, 183].includes(effect.effectType) ? { withValue: effect.basePoints } : {}),
|
||||
});
|
||||
if (depth < 2 && !visitedSpellIds.has(triggeredSpellId)) {
|
||||
@@ -987,12 +990,13 @@ function effectsForSpell(
|
||||
}
|
||||
|
||||
const hasHealing = effects.some((effect) => ["heal", "hot"].includes(effect.kind));
|
||||
const hasDamage = effects.some((effect) => ["damage", "dot"].includes(effect.kind)
|
||||
|| (effect.kind === "apply-aura" && effect.aura.procs?.some((proc) => proc.action.kind === "damage")));
|
||||
const hasDamage = effects.some((effect) => ["damage", "dot", "trigger-spell"].includes(effect.kind)
|
||||
|| (effect.kind === "apply-aura" && effect.aura.procs?.some((proc) => ["damage", "custom"].includes(proc.action.kind))));
|
||||
const describesHealing = /\b(?:heal(?:s|ing|ed)?|restores? .*health|mends?|rejuvenat\w*)\b/i.test(description);
|
||||
const describesDamage = /\b(?:deal(?:s|ing)?|inflict(?:s|ing)?|caus(?:e|es|ing)|damag(?:e|es|ing))\b[^.]*\bdamage\b/i.test(description);
|
||||
if (!hasHealing && describesHealing) effects.unshift({ kind: "heal", coefficient: 1 });
|
||||
if (!hasDamage && describesDamage) effects.unshift({ kind: "damage", coefficient: 1 });
|
||||
const describesDamageCondition = /^\s*Dealing\b.*\b(?:reduces?|increases?)\b/i.test(description);
|
||||
if (!hasDamage && describesDamage && !describesDamageCondition) effects.unshift({ kind: "damage", coefficient: 1 });
|
||||
if (!effects.some((effect) => ["damage", "heal", "dot", "hot", "shield"].includes(effect.kind))) {
|
||||
if (/\bswap health percentages\b/i.test(description)) {
|
||||
const minimum = Number(description.match(/to\s+(\d+(?:\.\d+)?)%/)?.[1] ?? 40);
|
||||
|
||||
@@ -798,7 +798,7 @@ describe("combat store", () => {
|
||||
expect(useCombatStore.getState().lootMob("lootable", 30_000)).toBe(false);
|
||||
});
|
||||
|
||||
it("rolls SQL boss loot at the killing player's level and grants it automatically exactly once", () => {
|
||||
it("suppresses leveling equipment while granting boss cards automatically exactly once", () => {
|
||||
useCombatStore.getState().initializeCharacter({
|
||||
classId: "priest",
|
||||
level: 1,
|
||||
@@ -825,11 +825,9 @@ describe("combat store", () => {
|
||||
looted: true,
|
||||
corpseDespawnAt: 5_000 + MINIMUM_CORPSE_VISIBLE_MS,
|
||||
});
|
||||
expect(corpse.pendingLoot).toHaveLength(1);
|
||||
expect(corpse.pendingLoot[0]).toMatchObject({
|
||||
itemLevel: 1,
|
||||
sourceBossKey: "lady-anacondra",
|
||||
});
|
||||
expect(corpse.pendingLoot).toEqual([]);
|
||||
expect(corpse.bossCardDrop?.rarities).toContain("normal");
|
||||
expect(useCombatStore.getState().bossCards.counts["wow:lady anacondra"]?.normal).toBe(1);
|
||||
|
||||
expect(useCombatStore.getState().inventory).toEqual(corpse.pendingLoot);
|
||||
expect(useCombatStore.getState().bossLootRewards).toEqual([
|
||||
@@ -846,7 +844,8 @@ describe("combat store", () => {
|
||||
expect(useCombatStore.getState().bossLootRewards).toEqual([]);
|
||||
expect(useCombatStore.getState().lootMob("anacondra-spawn", 5_100)).toBe(false);
|
||||
expect(useCombatStore.getState().lootMob("anacondra-spawn", 5_200)).toBe(false);
|
||||
expect(useCombatStore.getState().inventory).toHaveLength(1);
|
||||
expect(useCombatStore.getState().inventory).toHaveLength(0);
|
||||
expect(useCombatStore.getState().bossCards.counts["wow:lady anacondra"]?.normal).toBe(1);
|
||||
});
|
||||
|
||||
it("aggros every surviving packmate even when the pulled mob dies to the opening hit", () => {
|
||||
@@ -1073,6 +1072,26 @@ describe("combat store", () => {
|
||||
}));
|
||||
});
|
||||
|
||||
it("prevents Death Pact from instantly killing an encounter boss", () => {
|
||||
useCombatStore.getState().initializeCharacter({ classId: "death-knight", level: 80 });
|
||||
registerTarget("pact-boss", [2, 0, 0], 1_000_000, 0);
|
||||
useCombatStore.getState().registerMob("pact-boss", { name: "Pact boss", boss: true, maxHealth: 1_000_000, xpReward: 0 });
|
||||
useCombatStore.setState({ resource: 100, resourcePools: { "runic-power": 100 } });
|
||||
expect(useCombatStore.getState().castAbility("wow335-death-knight-death-pact", undefined, 1_000).ok).toBe(true);
|
||||
expect(useCombatStore.getState().mobs["pact-boss"]).toMatchObject({ dead: false, health: 1_000_000 });
|
||||
});
|
||||
|
||||
it("prevents repeated Favor activations across branches of one Guardian hit", async () => {
|
||||
const build = (await loadCanonicalBenchmarkBuilds(80, ["guardian"]))
|
||||
.find((candidate) => candidate.role === "damage")!;
|
||||
useCombatStore.getState().initializeCharacter({ classId: build.classId, level: build.level, talentRanks: build.talentRanks });
|
||||
registerTarget("guardian-proc-dummy", [2, 0, 0], 1_000_000, 0);
|
||||
expect(useCombatStore.getState().castAbility("coa-18-reprisal-69-800316", undefined, 1_000).ok).toBe(true);
|
||||
const hits = useCombatStore.getState().combatEvents.filter((event) => event.kind === "damage"
|
||||
&& event.abilityId === "coa-triggered-712476");
|
||||
expect(hits.length).toBeLessThanOrEqual(2);
|
||||
});
|
||||
|
||||
it("resets encounter health without erasing earned progression", () => {
|
||||
registerTarget("mob", [2, 0, 0], 1, 100);
|
||||
useCombatStore.getState().damageMob("mob", 5, 1000);
|
||||
@@ -1478,6 +1497,7 @@ describe("combat store", () => {
|
||||
});
|
||||
|
||||
useCombatStore.getState().damageMob("storm-boss", 10_000, 30_000);
|
||||
expect(useCombatStore.getState().mobs["storm-boss"].pendingLoot).toHaveLength(2);
|
||||
expect(useCombatStore.getState().mobs["storm-boss"].pendingLoot).toHaveLength(0);
|
||||
expect(useCombatStore.getState().mobs["storm-boss"].bossCardDrop?.rarities).toContain("normal");
|
||||
});
|
||||
});
|
||||
+70
-16
@@ -128,6 +128,8 @@ import {
|
||||
type TriggeredAuraProc,
|
||||
} from "./combatAuras";
|
||||
import { rollBossLoot } from "./lootCatalog";
|
||||
import { bossCardForSource } from "./bossCardCatalog";
|
||||
import { collectBossCardDrop, emptyBossCardCollection, normalizeBossCardCollection, rollBossCards, type BossCardCollection, type BossCardDrop } from "./bossCards";
|
||||
import { adaptRunewakerEnemyDamage } from "./runewakerCombatFormula";
|
||||
import { channelAmountEffects } from "./combatChannelAmounts";
|
||||
import { normalizeInventory, type InventoryItem } from "./lootTypes";
|
||||
@@ -357,6 +359,7 @@ export interface MobCombatState {
|
||||
readonly dead: boolean;
|
||||
readonly hasLoot: boolean;
|
||||
readonly pendingLoot: readonly InventoryItem[];
|
||||
readonly bossCardDrop?: BossCardDrop;
|
||||
readonly looted: boolean;
|
||||
readonly despawned: boolean;
|
||||
readonly diedAt: number | null;
|
||||
@@ -454,6 +457,7 @@ export interface BossLootReward {
|
||||
readonly bossId: string;
|
||||
readonly bossName: string;
|
||||
readonly items: readonly InventoryItem[];
|
||||
readonly cards?: BossCardDrop;
|
||||
readonly acquiredAt: number;
|
||||
}
|
||||
|
||||
@@ -525,6 +529,7 @@ export interface CharacterCombatSeed {
|
||||
readonly talentRanks?: TalentRanks;
|
||||
readonly actionBindings?: unknown;
|
||||
readonly inventory?: unknown;
|
||||
readonly bossCards?: unknown;
|
||||
readonly equipment?: unknown;
|
||||
readonly settings?: Partial<GameplaySettings>;
|
||||
}
|
||||
@@ -667,6 +672,8 @@ export interface CombatState {
|
||||
talentModifiers: readonly TalentModifier[];
|
||||
actionBindings: ActionBindings;
|
||||
inventory: readonly InventoryItem[];
|
||||
bossCards: BossCardCollection;
|
||||
collectSharedBossCards: (drop: BossCardDrop, bossName: string, now: number) => void;
|
||||
equipment: EquipmentAssignments;
|
||||
gearStats: ItemStats;
|
||||
settings: GameplaySettings;
|
||||
@@ -785,6 +792,7 @@ interface MutableCastWork {
|
||||
secondaryClassId: ClassId | null;
|
||||
/** Active synchronous spell/source/target keys used to break proc cycles. */
|
||||
triggeredSpellStack?: string[];
|
||||
triggeredProcKeys?: Set<string>;
|
||||
}
|
||||
|
||||
let feedbackSequence = 0;
|
||||
@@ -1336,6 +1344,12 @@ function resolvePlayerDamageResult(
|
||||
const attacker: CombatAttackerStats = {
|
||||
...baseAttacker,
|
||||
criticalChancePercent: (baseAttacker.criticalChancePercent ?? 0) + criticalChanceFromTalents,
|
||||
criticalDamageMultiplier: applyTalentModifierValue(
|
||||
2,
|
||||
work.talentModifiers,
|
||||
"critical-damage-percent",
|
||||
ability ? abilityTalentContext(ability, periodic) : {},
|
||||
),
|
||||
};
|
||||
const immune = mob.schoolImmunities.includes(school);
|
||||
return resolveCombatResult({
|
||||
@@ -2280,17 +2294,29 @@ function evaluateProcsInWork(
|
||||
event: ProcEvent,
|
||||
now: number,
|
||||
): readonly TriggeredAuraProc[] {
|
||||
const result = evaluateAuraProcs(
|
||||
work.auras,
|
||||
event,
|
||||
now,
|
||||
({ aura, proc }) => stableCombatUnit(
|
||||
`${aura.instanceId}:${proc.id}:${event.trigger}:${event.abilityId ?? ""}:${event.otherId ?? ""}:${now}`,
|
||||
),
|
||||
);
|
||||
work.auras = [...result.auras];
|
||||
executeTriggeredAuraProcsInWork(work, result.triggered, now);
|
||||
return result.triggered;
|
||||
const root = work.triggeredProcKeys === undefined;
|
||||
const triggeredKeys = work.triggeredProcKeys ?? new Set<string>();
|
||||
work.triggeredProcKeys = triggeredKeys;
|
||||
try {
|
||||
const result = evaluateAuraProcs(
|
||||
work.auras,
|
||||
event,
|
||||
now,
|
||||
({ aura, proc }) => triggeredKeys.has(`${aura.instanceId}:${proc.id}`)
|
||||
? 1
|
||||
: stableCombatUnit(
|
||||
`${aura.instanceId}:${proc.id}:${event.trigger}:${event.abilityId ?? ""}:${event.otherId ?? ""}:${now}`,
|
||||
),
|
||||
);
|
||||
// A proc can activate once for the originating event. Its damage must not
|
||||
// reactivate it through a different branch of the same triggered chain.
|
||||
for (const proc of result.triggered) triggeredKeys.add(`${proc.auraInstanceId}:${proc.procId}`);
|
||||
work.auras = [...result.auras];
|
||||
executeTriggeredAuraProcsInWork(work, result.triggered, now);
|
||||
return result.triggered;
|
||||
} finally {
|
||||
if (root) work.triggeredProcKeys = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function eligibleTargetIds(state: CombatState, origin: CombatPosition, maxRange: number): string[] {
|
||||
@@ -2600,7 +2626,16 @@ function damageMobInWork(
|
||||
bonusItemChance: mob.bonusLootChance,
|
||||
})
|
||||
: mob.pendingLoot;
|
||||
const hasLoot = killed ? mob.hasLoot || pendingLoot.length > 0 : mob.hasLoot;
|
||||
const card = killed && mob.boss
|
||||
? bossCardForSource(mob.lootSourceId ?? mob.id, useGameStore.getState().activeDungeonId, mob.name)
|
||||
: null;
|
||||
const bossCardDrop = card ? {
|
||||
bossKey: card.key,
|
||||
spawnId: mob.id,
|
||||
claimId: `${now}:${mob.deathRevision + 1}:${mob.id}`,
|
||||
rarities: rollBossCards(),
|
||||
} satisfies BossCardDrop : mob.bossCardDrop;
|
||||
const hasLoot = killed ? mob.hasLoot || pendingLoot.length > 0 || Boolean(bossCardDrop) : mob.hasLoot;
|
||||
const xpAwarded = canAward ? grantMobExperience(classId, work, mob.xpReward) : 0;
|
||||
engageMobGroupInWork(work, id, now, source.actorId);
|
||||
const engagedMob = work.mobs[id] ?? mob;
|
||||
@@ -2618,6 +2653,7 @@ function damageMobInWork(
|
||||
dead: killed,
|
||||
hasLoot,
|
||||
pendingLoot,
|
||||
bossCardDrop,
|
||||
looted: killed ? false : mob.looted,
|
||||
despawned: false,
|
||||
diedAt: killed ? now : mob.diedAt,
|
||||
@@ -3025,7 +3061,7 @@ function executeTriggeredSpellInWork(
|
||||
} else if (effect.kind === "execute") {
|
||||
for (const mobId of mobTargets) {
|
||||
const mob = work.mobs[mobId];
|
||||
if (mob) damageMobInWork(
|
||||
if (mob && !mob.boss) damageMobInWork(
|
||||
work.classId,
|
||||
work,
|
||||
mobId,
|
||||
@@ -3064,7 +3100,7 @@ function executeTriggeredSpellInWork(
|
||||
work.cooldowns[candidate.id] = Math.max(now, readyAt - reduction);
|
||||
}
|
||||
}
|
||||
} else if (effect.kind === "trigger-spell" && depth < 1 && effect.spellId !== spellId) {
|
||||
} else if (effect.kind === "trigger-spell" && !effect.expanded && depth < 1 && effect.spellId !== spellId) {
|
||||
executeTriggeredSpellInWork(work, effect.spellId, sourceId, targetId, now, depth + 1, allowCoaHidden);
|
||||
}
|
||||
}
|
||||
@@ -3409,6 +3445,7 @@ function initialState() {
|
||||
talentModifiers,
|
||||
actionBindings: defaultActionBindings(defaultActionBarForClass(classId)),
|
||||
inventory: [] as readonly InventoryItem[],
|
||||
bossCards: emptyBossCardCollection(),
|
||||
equipment: {} as EquipmentAssignments,
|
||||
gearStats: EMPTY_ITEM_STATS,
|
||||
settings: DEFAULT_GAMEPLAY_SETTINGS,
|
||||
@@ -3481,6 +3518,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
talentModifiers,
|
||||
actionBindings: normalizeActionBindings(profile.actionBindings, defaultActionBarForCharacter(classId, secondaryClassId, progression.level)),
|
||||
inventory,
|
||||
bossCards: normalizeBossCardCollection(profile.bossCards),
|
||||
equipment,
|
||||
gearStats,
|
||||
settings: normalizedSettings(DEFAULT_GAMEPLAY_SETTINGS, profile.settings ?? {}),
|
||||
@@ -3717,11 +3755,13 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
bossId: id,
|
||||
bossName: mob.name,
|
||||
items: mob.pendingLoot,
|
||||
...(mob.bossCardDrop ? { cards: mob.bossCardDrop } : {}),
|
||||
acquiredAt: now,
|
||||
} satisfies BossLootReward
|
||||
: null;
|
||||
return {
|
||||
inventory: [...state.inventory, ...mob.pendingLoot],
|
||||
bossCards: mob.bossCardDrop ? collectBossCardDrop(state.bossCards, mob.bossCardDrop) : state.bossCards,
|
||||
mobs: {
|
||||
...state.mobs,
|
||||
[id]: {
|
||||
@@ -3745,6 +3785,17 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
: state.bossLootRewards.filter((reward) => reward.id !== id),
|
||||
})),
|
||||
|
||||
collectSharedBossCards: (drop, bossName, now) => set((state) => {
|
||||
const bossCards = collectBossCardDrop(state.bossCards, drop);
|
||||
if (bossCards === state.bossCards) return state;
|
||||
return {
|
||||
bossCards,
|
||||
bossLootRewards: [...state.bossLootRewards, {
|
||||
id: ++bossLootRewardSequence, bossId: drop.spawnId, bossName, items: [], cards: drop, acquiredAt: now,
|
||||
}],
|
||||
};
|
||||
}),
|
||||
|
||||
equipItem: (instanceId, ownerId = PLAYER_EQUIPMENT_OWNER_ID) => {
|
||||
const state = get();
|
||||
const ownerClassId = ownerId === PLAYER_EQUIPMENT_OWNER_ID
|
||||
@@ -5166,7 +5217,9 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
} else if (effect.kind === "execute") {
|
||||
for (const id of affectedMobIds) {
|
||||
const mob = work.mobs[id];
|
||||
if (!mob || mob.dead) continue;
|
||||
// DBC instant-kill effects include pet sacrifices such as Death Pact.
|
||||
// They must never bypass a boss encounter's health and mechanics.
|
||||
if (!mob || mob.dead || mob.boss) continue;
|
||||
damageMobInWork(
|
||||
state.classId,
|
||||
work,
|
||||
@@ -5320,7 +5373,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
} else if (effect.kind === "cooldown-reset") {
|
||||
cooldownAdjustments.push(effect);
|
||||
} else if (effect.kind === "trigger-spell") {
|
||||
if (effect.spellId !== ability.dbcSpellId) {
|
||||
if (!effect.expanded && effect.spellId !== ability.dbcSpellId) {
|
||||
executeTriggeredSpellInWork(
|
||||
work,
|
||||
effect.spellId,
|
||||
@@ -8359,6 +8412,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
health: preserveDefeated ? 0 : mob.maxHealth,
|
||||
dead: preserveDefeated,
|
||||
pendingLoot: preserveDefeated ? mob.pendingLoot : [],
|
||||
bossCardDrop: preserveDefeated ? mob.bossCardDrop : undefined,
|
||||
looted: preserveDefeated ? mob.looted : false,
|
||||
despawned: preserveDefeated ? mob.despawned : false,
|
||||
diedAt: preserveDefeated ? mob.diedAt : null,
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { afterAll, describe, expect, it } from "vitest";
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { BASE_CLASS_IDS, COA_CLASS_IDS } from "../app/characterCatalog";
|
||||
import { loadCanonicalBenchmarkBuilds } from "./benchmark/benchmarkBuilds";
|
||||
import { runDungeonBossBenchmark } from "./benchmark/dungeonBossBenchmark";
|
||||
import { requireDungeonDefinition } from "./dungeonRegistry";
|
||||
import { finalDungeonBoss } from "./dungeonRunStore";
|
||||
|
||||
const dungeon = requireDungeonDefinition("ragefire-chasm");
|
||||
const final = finalDungeonBoss(dungeon)!;
|
||||
const ordinary = dungeon.bosses.find((boss) => boss.id !== final.id)!;
|
||||
const samples: unknown[] = [];
|
||||
afterAll(() => {
|
||||
if (process.env.BOSS_BALANCE_REPORT !== "1") return;
|
||||
mkdirSync("artifacts/boss-difficulty", { recursive: true });
|
||||
writeFileSync("artifacts/boss-difficulty/wow-coa-balance.json", `${JSON.stringify({
|
||||
difficulty: 1, dungeon: dungeon.id, durationLimitSeconds: 60,
|
||||
targets: { ordinary: 25, final: 45 }, policy: "baseline-v1", gear: "resolved starter heirlooms", samples,
|
||||
}, null, 2)}\n`);
|
||||
});
|
||||
|
||||
describe("default boss difficulty with WoW and CoA classes", () => {
|
||||
for (const level of [8, 20, 40, 80]) {
|
||||
it(`benchmarks every class-role build at level ${level}`, async () => {
|
||||
const builds = await loadCanonicalBenchmarkBuilds(level, [...BASE_CLASS_IDS, ...COA_CLASS_IDS]);
|
||||
const tooFast: string[] = [];
|
||||
for (const build of builds) {
|
||||
// Let the test worker deliver progress between CPU-heavy simulations.
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 0));
|
||||
for (const seed of ["class-balance-1", "class-balance-2", "class-balance-3"]) {
|
||||
const boss = await runDungeonBossBenchmark(build, ordinary.id, seed);
|
||||
const last = await runDungeonBossBenchmark(build, final.id, seed);
|
||||
samples.push({ level, classId: build.classId, source: build.source, role: build.role, orientation: build.orientation, seed, boss, final: last });
|
||||
if ((boss.seconds ?? boss.elapsedSeconds) < 25 || (last.seconds ?? last.elapsedSeconds) < 45) {
|
||||
tooFast.push(`${build.id}: ${boss.seconds}/${last.seconds}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(tooFast).toEqual([]);
|
||||
}, 180_000);
|
||||
}
|
||||
|
||||
for (const level of [8, 80]) {
|
||||
it(`records healer outcomes with the final boss attacking at level ${level}`, async () => {
|
||||
const builds = (await loadCanonicalBenchmarkBuilds(level, [...BASE_CLASS_IDS, ...COA_CLASS_IDS]))
|
||||
.filter((build) => build.role === "healer");
|
||||
for (const build of builds) {
|
||||
const last = await runDungeonBossBenchmark(build, final.id, "class-balance-2", true);
|
||||
samples.push({ level, classId: build.classId, source: build.source, role: build.role, mechanics: true, final: last });
|
||||
expect(last.attacks, build.id).toBeGreaterThan(5);
|
||||
expect(last.incomingDamage, build.id).toBeGreaterThan(0);
|
||||
}
|
||||
}, 60_000);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,123 @@
|
||||
import { afterAll, describe, expect, it } from "vitest";
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { dungeonEnemyScaling } from "./dungeonBossScaling";
|
||||
import { requireDungeonDefinition } from "./dungeonRegistry";
|
||||
import { finalDungeonBoss } from "./dungeonRunStore";
|
||||
import { useCombatStore } from "./combatStore";
|
||||
import { usePartyStore } from "./partyStore";
|
||||
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
|
||||
import { clearPartyRuntimeRegistry, registerPartyRuntimePosition, updatePartyRuntimeTargetVisibility } from "./partyRuntimeRegistry";
|
||||
import { advanceDungeonPartyCombat, partyCombatRangeBand, resetPartyRuntimeClock } from "./partyRuntime";
|
||||
import { subscribeCombatEvents } from "./combatEvents";
|
||||
import { bossDifficultyMultipliers } from "./dungeonDifficulty";
|
||||
import { abilitiesForClass } from "./abilityCatalog";
|
||||
|
||||
const dungeon = requireDungeonDefinition("ragefire-chasm");
|
||||
const final = finalDungeonBoss(dungeon)!;
|
||||
const samples: { level: number; seed: string; boss: unknown; final: unknown }[] = [];
|
||||
afterAll(() => {
|
||||
if (process.env.BOSS_BALANCE_REPORT !== "1") return;
|
||||
mkdirSync("artifacts/boss-difficulty", { recursive: true });
|
||||
writeFileSync("artifacts/boss-difficulty/ragefire-balance.json", `${JSON.stringify({ samples }, null, 2)}\n`);
|
||||
});
|
||||
|
||||
function fight(bossId: string, level: number, difficulty: number, seed: string, mechanics = false, playerDamage = false) {
|
||||
clearMobRuntimeRegistry(); clearPartyRuntimeRegistry(); resetPartyRuntimeClock();
|
||||
const entity = dungeon.entities[dungeon.staticSpawns.find((spawn) => spawn.id === bossId)!.entityId];
|
||||
const scaling = dungeonEnemyScaling(dungeon, entity, bossId, level, "normal", difficulty);
|
||||
const combat = useCombatStore.getState();
|
||||
const classId = playerDamage ? "rom-mage" : "rom-priest";
|
||||
combat.initializeCharacter({ classId, raceId: "rom-human", level });
|
||||
combat.setPlayerPosition([-18, 0, 0]);
|
||||
usePartyStore.getState().initializeParty(seed, level, useCombatStore.getState().health, playerDamage ? "damage" : "healer", 5, "rom");
|
||||
combat.synchronizeActors();
|
||||
combat.registerMob(bossId, { name: entity.name, boss: true, serverEntry: entity.combat?.serverEntry,
|
||||
...scaling, attacks: entity.combat?.attacks, xpReward: 0 });
|
||||
setMobPosition(bossId, [0, 0, 0]);
|
||||
for (const member of usePartyStore.getState().members) {
|
||||
registerPartyRuntimePosition(member.id, [-partyCombatRangeBand(member).preferred, 0, 0]);
|
||||
updatePartyRuntimeTargetVisibility(member.id, bossId, true);
|
||||
}
|
||||
let firstDamage: number | undefined;
|
||||
let incomingDamage = 0; let healing = 0;
|
||||
const heal = abilitiesForClass("rom-priest").find((ability) => ability.name === "Urgent Heal")!;
|
||||
const damage = abilitiesForClass(classId).find((ability) => ability.target === "hostile"
|
||||
&& ability.effects.some((effect) => effect.kind === "damage"))!;
|
||||
combat.selectMob(bossId);
|
||||
const stop = subscribeCombatEvents((event) => {
|
||||
if (event.kind === "damage" && event.targetActorId === bossId && event.effectiveAmount > 0) firstDamage ??= event.occurredAt;
|
||||
if (event.kind === "damage" && event.sourceActorId === bossId) incomingDamage += event.effectiveAmount;
|
||||
if (event.kind === "healing") healing += event.effectiveAmount;
|
||||
});
|
||||
combat.damageMob(bossId, 1, 1000);
|
||||
try {
|
||||
for (let now = 1100; now <= 241000; now += 100) {
|
||||
combat.tick(.1, now); advanceDungeonPartyCombat(now);
|
||||
if (playerDamage && !useCombatStore.getState().activeCast) combat.castAbility(damage.id, undefined, now);
|
||||
if (mechanics) {
|
||||
combat.advanceMobCombat(now);
|
||||
const injured = usePartyStore.getState().members.filter((member) => member.health > 0)
|
||||
.sort((left, right) => left.health / left.maxHealth - right.health / right.maxHealth)[0];
|
||||
if (injured && injured.health < injured.maxHealth * .8 && !useCombatStore.getState().activeCast) {
|
||||
usePartyStore.getState().selectMember(injured.id); combat.castAbility(heal.id, undefined, now);
|
||||
}
|
||||
}
|
||||
const mob = useCombatStore.getState().mobs[bossId];
|
||||
if (mob.dead) return { seconds: ((mob.diedAt ?? now) - firstDamage!) / 1000,
|
||||
incomingDamage, healing, attacks: mob.attackRevision,
|
||||
maxHealth: mob.maxHealth, classes: usePartyStore.getState().members.map((member) => member.classId) };
|
||||
}
|
||||
return { seconds: null, maxHealth: scaling.maxHealth };
|
||||
} finally { stop(); }
|
||||
}
|
||||
|
||||
describe("dungeon boss durability and difficulty", () => {
|
||||
it("increases boss health and damage at every slider step without changing trash", () => {
|
||||
const spawn = dungeon.staticSpawns.find((entry) => entry.id === final.id)!;
|
||||
const entity = dungeon.entities[spawn.entityId];
|
||||
const trash = dungeon.staticSpawns.find((entry) => dungeon.entities[entry.entityId].kind === "mob")!;
|
||||
const easy = dungeonEnemyScaling(dungeon, entity, final.id, 8, "normal", 1);
|
||||
for (let difficulty = 2; difficulty <= 10; difficulty += 1) {
|
||||
const scaled = dungeonEnemyScaling(dungeon, entity, final.id, 8, "normal", difficulty);
|
||||
expect(scaled.maxHealth).toBe(easy.maxHealth * bossDifficultyMultipliers(difficulty).health);
|
||||
expect(scaled.damageMultiplier).toBe(easy.damageMultiplier * bossDifficultyMultipliers(difficulty).damage);
|
||||
expect(dungeonEnemyScaling(dungeon, dungeon.entities[trash.entityId], trash.id, 8, "normal", difficulty))
|
||||
.toEqual(dungeonEnemyScaling(dungeon, dungeon.entities[trash.entityId], trash.id, 8, "normal", 1));
|
||||
}
|
||||
});
|
||||
|
||||
it("allows real boss attacks and Priest healing before Ragefire's final boss dies", () => {
|
||||
const result = fight(final.id, 8, 1, "rom-health-1", true);
|
||||
expect(result.seconds).not.toBeNull(); expect(result.seconds!).toBeGreaterThanOrEqual(45);
|
||||
expect(result.attacks).toBeGreaterThan(5); expect(result.incomingDamage).toBeGreaterThan(0);
|
||||
expect(result.healing).toBeGreaterThan(0);
|
||||
expect(usePartyStore.getState().members.every((member) => member.health > 0)).toBe(true);
|
||||
});
|
||||
|
||||
it("retains encounter time with a damage-role Mage contributing to the RoM party", () => {
|
||||
for (const level of [1, 8, 20, 40, 80]) {
|
||||
const ordinary = dungeon.bosses.find((boss) => boss.id !== final.id)!;
|
||||
const boss = fight(ordinary.id, level, 1, "rom-health-1", false, true);
|
||||
const last = fight(final.id, level, 1, "rom-health-1", false, true);
|
||||
samples.push({ level, seed: "player-mage", boss, final: last });
|
||||
expect(boss.seconds).not.toBeNull(); expect(last.seconds).not.toBeNull();
|
||||
expect(boss.seconds!, JSON.stringify({ level, boss, last })).toBeGreaterThanOrEqual(25);
|
||||
expect(last.seconds!, JSON.stringify({ level, boss, last })).toBeGreaterThanOrEqual(45);
|
||||
}
|
||||
});
|
||||
|
||||
for (const level of [1, 8, 20, 40, 80]) {
|
||||
it(`gives a RoM party time to play Ragefire boss mechanics at level ${level}`, () => {
|
||||
const ordinary = dungeon.bosses.find((boss) => boss.id !== final.id)!;
|
||||
for (let index = 0; index < 12; index += 1) {
|
||||
const seed = `rom-health-${index}`;
|
||||
const boss = fight(ordinary.id, level, 1, seed);
|
||||
const last = fight(final.id, level, 1, seed);
|
||||
samples.push({ level, seed, boss, final: last });
|
||||
expect(boss.seconds).not.toBeNull(); expect(last.seconds).not.toBeNull();
|
||||
expect(boss.seconds!).toBeGreaterThanOrEqual(25);
|
||||
expect(last.seconds!).toBeGreaterThanOrEqual(45);
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { creatureRuntimeBaseStats } from "./azerothCoreReference";
|
||||
import { bossDifficultyMultipliers } from "./dungeonDifficulty";
|
||||
import { dungeonEnemyHealthBaseline, scaledDungeonEnemyLevel } from "./dungeonLevelScaling";
|
||||
import { finalDungeonBoss } from "./dungeonRunStore";
|
||||
import type { DungeonDefinition } from "./dungeonTypes";
|
||||
import type { PopulationEntityDefinition } from "./mobPopulation";
|
||||
|
||||
/** Shared by rendering and deterministic combat benchmarks; preserves authored stats above the floor. */
|
||||
export function dungeonEnemyScaling(dungeon: DungeonDefinition, entity: PopulationEntityDefinition,
|
||||
instanceId: string, characterLevel: number, difficultyId: string, bossDifficulty: number) {
|
||||
const boss = entity.kind === "boss";
|
||||
const level = scaledDungeonEnemyLevel(characterLevel, boss);
|
||||
const authoredHealth = entity.combat?.healthMultiplier ?? 1;
|
||||
const authoredDamage = entity.combat?.damageMultiplier ?? 1;
|
||||
const baseline = dungeonEnemyHealthBaseline(level, dungeon.expansion);
|
||||
const native = entity.combat?.serverEntry ? creatureRuntimeBaseStats(entity.combat.serverEntry, level) : null;
|
||||
const originalHealth = (baseline ?? native?.health ?? (boss ? 260 + level * 65 : 72 + level * 22)) * authoredHealth;
|
||||
const multipliers = boss ? bossDifficultyMultipliers(bossDifficulty) : { health: 1, damage: 1 };
|
||||
const finalBoss = boss && finalDungeonBoss(dungeon, difficultyId)?.id === instanceId;
|
||||
// Calibrated against active RoM, WoW and CoA class-role rotations with heirlooms.
|
||||
// Every class faces the same floor; the margin covers stronger CoA burst builds.
|
||||
const bossFloor = (1_000 + level * 1_200 + level * level * 22) * 1.75 * (finalBoss ? 1.8 : 1);
|
||||
return { level, maxHealth: (boss ? Math.max(originalHealth, bossFloor) : originalHealth) * multipliers.health,
|
||||
healthMultiplier: authoredHealth * multipliers.health, damageMultiplier: authoredDamage * multipliers.damage };
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/** Separate from the source dungeon's Normal/Heroic creature variant. */
|
||||
export function validBossDifficulty(value: unknown): value is number {
|
||||
return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 10;
|
||||
}
|
||||
|
||||
export function bossDifficultyMultipliers(level: number) {
|
||||
const difficulty = validBossDifficulty(level) ? level : 1;
|
||||
return { health: 1 + (difficulty - 1) * 0.25, damage: 1 + (difficulty - 1) * 0.15 };
|
||||
}
|
||||
|
||||
export function bossDifficultyLabel(level?: number): string {
|
||||
return level ? `Difficulty ${level}${level === 1 ? " · Easy" : ""}` : "Legacy";
|
||||
}
|
||||
@@ -10,6 +10,18 @@ const party: readonly DungeonRunMember[] = [{ name: "Tester", classId: "rom-prie
|
||||
beforeEach(() => { useDungeonRunStore.setState({ progress: null }); });
|
||||
|
||||
describe("dungeon completion and run timing", () => {
|
||||
it("preserves the selected difficulty on resume and starts a new timer when it changes", () => {
|
||||
const run = useDungeonRunStore.getState();
|
||||
run.begin("deadmines", "normal", undefined, false, 7); run.start(1000, party);
|
||||
const saved = normalizeDungeonRunProgress(JSON.parse(JSON.stringify(useDungeonRunStore.getState().progress)))!;
|
||||
run.begin("deadmines", "normal", saved, false, 7);
|
||||
expect(useDungeonRunStore.getState().progress).toEqual(saved);
|
||||
run.begin("deadmines", "normal", saved, false, 8);
|
||||
expect(useDungeonRunStore.getState().progress).toMatchObject({ bossDifficulty: 8, startedAt: null });
|
||||
expect(useDungeonRunStore.getState().progress!.id).not.toBe(saved.id);
|
||||
expect(normalizeDungeonRunProgress({ ...saved, bossDifficulty: 11 })).toBeNull();
|
||||
expect(normalizeDungeonRunProgress({ ...saved, bossDifficulty: 1.5 })).toBeNull();
|
||||
});
|
||||
it("completes Barren Caves when Anglo dies, records its real spawn id, and survives checkpoint hydration", () => {
|
||||
const dungeon = requireDungeonDefinition("barren-caves");
|
||||
activateDungeonEncounterDefinitions(dungeon.id, dungeon.encounters ?? [], "normal");
|
||||
|
||||
@@ -22,6 +22,7 @@ export function completedDungeonRun(progress: DungeonRunProgress): DungeonRunRec
|
||||
const definition = dungeonDefinitionById(progress.dungeonId);
|
||||
if (!definition || !progress.timed || progress.finishedAt === null || progress.durationMs === null) return null;
|
||||
return { id: progress.id, dungeonId: progress.dungeonId, difficultyId: progress.difficultyId,
|
||||
bossDifficulty: progress.bossDifficulty,
|
||||
finishedAt: progress.finishedAt, durationMs: progress.durationMs, killedBossIds: progress.killedBossIds,
|
||||
party: progress.party, fullClear: dungeonBossesForDifficulty(definition, progress.difficultyId)
|
||||
.every((boss) => progress.killedBossIds.includes(boss.id)) };
|
||||
@@ -29,7 +30,7 @@ export function completedDungeonRun(progress: DungeonRunProgress): DungeonRunRec
|
||||
|
||||
interface DungeonRunState {
|
||||
readonly progress: DungeonRunProgress | null;
|
||||
begin: (dungeonId: string, difficultyId: string, resume?: DungeonRunProgress, legacyResume?: boolean) => void;
|
||||
begin: (dungeonId: string, difficultyId: string, resume?: DungeonRunProgress, legacyResume?: boolean, bossDifficulty?: number) => void;
|
||||
start: (now: number, party: readonly DungeonRunMember[]) => void;
|
||||
advance: (completedObjectiveIds: readonly string[], deadBossIds: readonly string[], now: number,
|
||||
party: readonly DungeonRunMember[]) => void;
|
||||
@@ -37,10 +38,11 @@ interface DungeonRunState {
|
||||
|
||||
export const useDungeonRunStore = create<DungeonRunState>((set, get) => ({
|
||||
progress: null,
|
||||
begin: (dungeonId, difficultyId, resume, legacyResume = false) => set({
|
||||
progress: resume?.dungeonId === dungeonId && resume.difficultyId === difficultyId ? resume : {
|
||||
begin: (dungeonId, difficultyId, resume, legacyResume = false, bossDifficulty = 1) => set({
|
||||
progress: resume?.dungeonId === dungeonId && resume.difficultyId === difficultyId
|
||||
&& resume.bossDifficulty === bossDifficulty ? resume : {
|
||||
id: globalThis.crypto.randomUUID(), dungeonId, difficultyId, startedAt: null,
|
||||
finishedAt: null, durationMs: null, killedBossIds: [], party: [], timed: !legacyResume,
|
||||
finishedAt: null, durationMs: null, killedBossIds: [], party: [], timed: !legacyResume, bossDifficulty,
|
||||
},
|
||||
}),
|
||||
start: (now, party) => {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { validBossDifficulty } from "./dungeonDifficulty";
|
||||
|
||||
export interface DungeonRunMember {
|
||||
readonly name: string;
|
||||
readonly classId: string;
|
||||
@@ -11,6 +13,7 @@ export interface DungeonRunProgress {
|
||||
readonly id: string;
|
||||
readonly dungeonId: string;
|
||||
readonly difficultyId: string;
|
||||
readonly bossDifficulty?: number;
|
||||
readonly startedAt: number | null;
|
||||
readonly finishedAt: number | null;
|
||||
readonly durationMs: number | null;
|
||||
@@ -23,6 +26,7 @@ export interface DungeonRunRecord {
|
||||
readonly id: string;
|
||||
readonly dungeonId: string;
|
||||
readonly difficultyId: string;
|
||||
readonly bossDifficulty?: number;
|
||||
readonly durationMs: number;
|
||||
readonly finishedAt: number;
|
||||
readonly killedBossIds: readonly string[];
|
||||
@@ -57,6 +61,7 @@ export function normalizeDungeonRunProgress(value: unknown): DungeonRunProgress
|
||||
const text = (entry: unknown) => typeof entry === "string" && entry.length > 0 && entry.length <= 100;
|
||||
const time = (entry: unknown) => entry === null || (typeof entry === "number" && Number.isFinite(entry) && entry >= 0);
|
||||
if (!text(input.id) || !text(input.dungeonId) || !text(input.difficultyId)
|
||||
|| (input.bossDifficulty !== undefined && !validBossDifficulty(input.bossDifficulty))
|
||||
|| !time(input.startedAt) || !time(input.finishedAt) || !time(input.durationMs)
|
||||
|| typeof input.timed !== "boolean" || !Array.isArray(input.killedBossIds)
|
||||
|| input.killedBossIds.length > 100 || !input.killedBossIds.every(text)
|
||||
@@ -70,6 +75,7 @@ export function normalizeDungeonRunProgress(value: unknown): DungeonRunProgress
|
||||
|| (input.finishedAt !== null && (input.durationMs === null || input.party.length === 0
|
||||
|| input.durationMs !== Math.max(1, Math.round(input.finishedAt - input.startedAt!))))) return null;
|
||||
return { id: input.id, dungeonId: input.dungeonId, difficultyId: input.difficultyId,
|
||||
...(input.bossDifficulty !== undefined ? { bossDifficulty: input.bossDifficulty } : {}),
|
||||
startedAt: input.startedAt, finishedAt: input.finishedAt, durationMs: input.durationMs,
|
||||
timed: input.timed, killedBossIds: [...new Set(input.killedBossIds)],
|
||||
party: input.party.map((member) => ({ name: member.name, classId: member.classId,
|
||||
|
||||
@@ -20,8 +20,10 @@ import type { DungeonEncounterSnapshot } from "./dungeonEncounterTypes";
|
||||
import { dungeonContentIsUsable } from "../content/contentManager";
|
||||
import { useDungeonRunStore } from "./dungeonRunStore";
|
||||
import type { DungeonRunProgress } from "./dungeonRunTypes";
|
||||
import { validBossDifficulty } from "./dungeonDifficulty";
|
||||
|
||||
export interface DungeonSessionOptions {
|
||||
readonly bossDifficulty?: number;
|
||||
readonly gameMode?: GameMode;
|
||||
readonly spawn?: WorldSpawn;
|
||||
readonly gmTestMode?: boolean;
|
||||
@@ -48,12 +50,15 @@ export function activateDungeonSession(
|
||||
if (!definition || !dungeonCanEnter(dungeonId) || !dungeonContentIsUsable(dungeonId)) {
|
||||
return false;
|
||||
}
|
||||
if ((difficultyId !== undefined && !definition.difficultyVariants.some((variant) => variant.id === difficultyId))
|
||||
|| (options.bossDifficulty !== undefined && !validBossDifficulty(options.bossDifficulty))) return false;
|
||||
|
||||
const previousDefinition = dungeonDefinitionById(useGameStore.getState().activeDungeonId);
|
||||
const bossDifficulty = options.bossDifficulty ?? options.runProgress?.bossDifficulty ?? 1;
|
||||
if (previousDefinition) releaseDungeonGltfCache(previousDefinition);
|
||||
if (!options.preserveCharacter) {
|
||||
useDungeonRunStore.getState().begin(dungeonId, difficultyId ?? definition.defaultDifficultyId,
|
||||
options.runProgress, options.legacyResume);
|
||||
options.runProgress, options.legacyResume, bossDifficulty);
|
||||
}
|
||||
activateDungeonEncounterDefinitions(
|
||||
dungeonId,
|
||||
@@ -103,6 +108,7 @@ export function activateDungeonSession(
|
||||
spawn: shelvedDungeonSpawn(dungeonId, options.spawn),
|
||||
gmTestMode: options.gmTestMode,
|
||||
gmReviewTarget: options.gmReviewTarget,
|
||||
bossDifficulty,
|
||||
});
|
||||
if (options.encounterSnapshots?.length) {
|
||||
importDungeonEncounterSnapshots(dungeonId, options.encounterSnapshots, { authoritative: true });
|
||||
@@ -124,6 +130,7 @@ export function restartActiveDungeonSession(): boolean {
|
||||
gmTestMode: game.gmTestMode,
|
||||
gmReviewTarget: game.activeGmReviewTarget,
|
||||
preserveCharacter: true,
|
||||
bossDifficulty: game.activeBossDifficulty,
|
||||
encounterSnapshots,
|
||||
});
|
||||
}
|
||||
@@ -50,9 +50,18 @@ describe("dungeon session asset eviction", () => {
|
||||
expect(clear).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects invalid difficulty settings before changing the dungeon or evicting assets", () => {
|
||||
const active = useGameStore.getState().activeDungeonId;
|
||||
const clear = vi.spyOn(useGLTF, "clear").mockImplementation(() => undefined);
|
||||
expect(activateDungeonSession("wailing-caverns", "missing")).toBe(false);
|
||||
expect(activateDungeonSession("wailing-caverns", undefined, { bossDifficulty: 11 })).toBe(false);
|
||||
expect(useGameStore.getState().activeDungeonId).toBe(active);
|
||||
expect(clear).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("restarts the active dungeon in place while preserving its party and encounter state", () => {
|
||||
const active = dungeonDefinitionById("wailing-caverns")!;
|
||||
expect(activateDungeonSession(active.id)).toBe(true);
|
||||
expect(activateDungeonSession(active.id, undefined, { bossDifficulty: 6 })).toBe(true);
|
||||
usePartyStore.getState().initializeParty("retry-party", 20, 600, "healer");
|
||||
const members = usePartyStore.getState().members.map((member) => member.id);
|
||||
const current = dungeonEncounterSnapshot(WAILING_NARALEX_ENCOUNTER.id)!;
|
||||
@@ -64,6 +73,7 @@ describe("dungeon session asset eviction", () => {
|
||||
const clear = vi.spyOn(useGLTF, "clear").mockImplementation(() => undefined);
|
||||
|
||||
expect(restartActiveDungeonSession()).toBe(true);
|
||||
expect(useGameStore.getState().activeBossDifficulty).toBe(6);
|
||||
|
||||
expect(clear.mock.calls.map(([url]) => url)).toEqual(dungeonGltfUrls(active));
|
||||
expect(useGameStore.getState().sessionRevision).toBe(revision + 1);
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"schemaVersion":1,"classId":"barbarian","abilities":[{"id":"coa-tree-30762","classId":"barbarian","dbcSpellId":500918,"name":"Ancestral Roar","unlockLevel":1,"icon":"/assets/ui/spells/_d3threateningshout.png","sigil":"AR","description":"Ancestral RoarRank 1Instant cast25 sec cooldownRequires Enraged Roar with ancestral fervor, healing you for 100+0+AP*0.5.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":25000,"gcdMs":1500,"cost":{"resource":"energy","amount":0},"effects":[{"kind":"heal","coefficient":1.6580645243326824,"basePoints":100,"dieSides":7,"pointsPerLevel":3.200000047683716,"bonusPowerCoefficient":0,"sourceLevel":16,"maxScalingLevel":22},{"kind":"utility","action":"class-script","effectType":3,"auraType":0,"miscValue":3,"triggerSpellId":0,"durationMs":0}],"passive":false,"source":"coa-tree","ranks":[{"rank":1,"spellId":500918,"level":1,"description":"Ancestral RoarRank 1Instant cast25 sec cooldownRequires Enraged Roar with ancestral fervor, healing you for 100+0+AP*0.5.","castMode":"instant","castTimeMs":0,"cooldownMs":25000,"gcdMs":1500,"cost":{"resource":"energy","amount":0},"effects":[{"kind":"heal","coefficient":1.6580645243326824,"basePoints":100,"dieSides":7,"pointsPerLevel":3.200000047683716,"bonusPowerCoefficient":0,"sourceLevel":16,"maxScalingLevel":22},{"kind":"utility","action":"class-script","effectType":3,"auraType":0,"miscValue":3,"triggerSpellId":0,"durationMs":0}]}],"talentEntryId":30762},{"id":"coa-12-ancestral-spirit-47-705153","classId":"barbarian","dbcSpellId":705153,"name":"Ancestral Spirit","unlockLevel":1,"icon":"/assets/ui/spells/inv_misc_questionmark.png","sigil":"AS","description":"Ancestral SpiritRank 1Reduces the Energy cost of your Spirit spells and increases the duration of your Shouts by 50%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"energy","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705153:effect:0:aura:108","name":"Ancestral Spirit (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":-0.5}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":14,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705153:effect:1:aura:108","name":"Ancestral Spirit (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":0.5}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":1,"triggerSpellId":0}],"passive":true,"source":"coa-progression","ranks":[{"rank":1,"spellId":705153,"level":1,"description":"Ancestral SpiritRank 1Reduces the Energy cost of your Spirit spells and increases the duration of your Shouts by 50%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"energy","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705153:effect:0:aura:108","name":"Ancestral Spirit (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":-0.5}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":14,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705153:effect:1:aura:108","name":"Ancestral Spirit (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":0.5}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":1,"triggerSpellId":0}]},{"rank":2,"spellId":705154,"level":1,"description":"Ancestral SpiritRank 2Reduces the Energy cost of your Spirit spells and increases the duration of your Shouts by 100%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"energy","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705154:effect:0:aura:108","name":"Ancestral Spirit (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":-1}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":14,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705154:effect:1:aura:108","name":"Ancestral Spirit (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":1}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":1,"triggerSpellId":0}]}]},{"id":"coa-tree-34279","classId":"barbarian","dbcSpellId":560456,"name":"Athleticism","unlockLevel":1,"icon":"/assets/ui/spells/5_run_border.png","sigil":"AT","descriptiLine truncated
|
||||
{"schemaVersion":1,"classId":"barbarian","abilities":[{"id":"coa-tree-30762","classId":"barbarian","dbcSpellId":500918,"name":"Ancestral Roar","unlockLevel":1,"icon":"/assets/ui/spells/_d3threateningshout.png","sigil":"AR","description":"Ancestral RoarRank 1Instant cast25 sec cooldownRequires Enraged Roar with ancestral fervor, healing you for 100+0+AP*0.5.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":25000,"gcdMs":1500,"cost":{"resource":"energy","amount":0},"effects":[{"kind":"heal","coefficient":1.6580645243326824,"basePoints":100,"dieSides":7,"pointsPerLevel":3.200000047683716,"bonusPowerCoefficient":0,"sourceLevel":16,"maxScalingLevel":22},{"kind":"utility","action":"class-script","effectType":3,"auraType":0,"miscValue":3,"triggerSpellId":0,"durationMs":0}],"passive":false,"source":"coa-tree","ranks":[{"rank":1,"spellId":500918,"level":1,"description":"Ancestral RoarRank 1Instant cast25 sec cooldownRequires Enraged Roar with ancestral fervor, healing you for 100+0+AP*0.5.","castMode":"instant","castTimeMs":0,"cooldownMs":25000,"gcdMs":1500,"cost":{"resource":"energy","amount":0},"effects":[{"kind":"heal","coefficient":1.6580645243326824,"basePoints":100,"dieSides":7,"pointsPerLevel":3.200000047683716,"bonusPowerCoefficient":0,"sourceLevel":16,"maxScalingLevel":22},{"kind":"utility","action":"class-script","effectType":3,"auraType":0,"miscValue":3,"triggerSpellId":0,"durationMs":0}]}],"talentEntryId":30762},{"id":"coa-12-ancestral-spirit-47-705153","classId":"barbarian","dbcSpellId":705153,"name":"Ancestral Spirit","unlockLevel":1,"icon":"/assets/ui/spells/inv_misc_questionmark.png","sigil":"AS","description":"Ancestral SpiritRank 1Reduces the Energy cost of your Spirit spells and increases the duration of your Shouts by 50%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"energy","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705153:effect:0:aura:108","name":"Ancestral Spirit (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":-0.5}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":14,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705153:effect:1:aura:108","name":"Ancestral Spirit (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":0.5}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":1,"triggerSpellId":0}],"passive":true,"source":"coa-progression","ranks":[{"rank":1,"spellId":705153,"level":1,"description":"Ancestral SpiritRank 1Reduces the Energy cost of your Spirit spells and increases the duration of your Shouts by 50%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"energy","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705153:effect:0:aura:108","name":"Ancestral Spirit (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":-0.5}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":14,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705153:effect:1:aura:108","name":"Ancestral Spirit (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":0.5}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":1,"triggerSpellId":0}]},{"rank":2,"spellId":705154,"level":1,"description":"Ancestral SpiritRank 2Reduces the Energy cost of your Spirit spells and increases the duration of your Shouts by 100%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"energy","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705154:effect:0:aura:108","name":"Ancestral Spirit (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":-1}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":14,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705154:effect:1:aura:108","name":"Ancestral Spirit (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":1}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":1,"triggerSpellId":0}]}]},{"id":"coa-tree-34279","classId":"barbarian","dbcSpellId":560456,"name":"Athleticism","unlockLevel":1,"icon":"/assets/ui/spells/5_run_border.png","sigil":"AT","descriptiLine truncated
|
||||
@@ -1 +1 @@
|
||||
{"schemaVersion":1,"classId":"bloodmage","abilities":[{"id":"coa-tree-29871","classId":"bloodmage","dbcSpellId":504285,"name":"Alacrity","unlockLevel":1,"icon":"/assets/ui/spells/5_run_border.png","sigil":"AL","description":"Increases all primary stats by 4%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:504285:effect:0:aura:137","name":"Alacrity (Aura 137)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"all-primary","operation":"percent","value":0.04}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":137,"miscValue":-1,"triggerSpellId":0}],"passive":true,"source":"coa-tree","ranks":[{"rank":1,"spellId":504285,"level":1,"description":"Increases all primary stats by 4%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:504285:effect:0:aura:137","name":"Alacrity (Aura 137)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"all-primary","operation":"percent","value":0.04}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":137,"miscValue":-1,"triggerSpellId":0}]},{"rank":2,"spellId":504893,"level":1,"description":"Increases all primary stats by 8%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:504893:effect:0:aura:137","name":"Alacrity (Aura 137)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"all-primary","operation":"percent","value":0.08}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":137,"miscValue":-1,"triggerSpellId":0}]}],"talentEntryId":29871},{"id":"coa-20-atheranns-wickedness-73-704642","classId":"bloodmage","dbcSpellId":704642,"name":"Atherann's Wickedness","unlockLevel":1,"icon":"/assets/ui/spells/spell_shadow_destructivesoul.png","sigil":"AS","description":"Atherann's WickednessRank 1Increases your Shadow damage dealt by 4% and increases your hit chance by 3%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:704642:effect:0:aura:79","name":"Atherann's Wickedness (Aura 79)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"damage","direction":"dealt","school":"shadow","operation":"percent","value":0.04}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":79,"miscValue":32,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:704642:effect:1:aura:333","name":"Atherann's Wickedness (Aura 333)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh"},"recipient":"caster","sourceEffectType":6,"sourceAuraType":333,"miscValue":0,"triggerSpellId":0}],"passive":true,"source":"coa-progression","ranks":[{"rank":1,"spellId":704642,"level":1,"description":"Atherann's WickednessRank 1Increases your Shadow damage dealt by 4% and increases your hit chance by 3%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:704642:effect:0:aura:79","name":"Atherann's Wickedness (Aura 79)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"damage","direction":"dealt","school":"shadow","operation":"percent","value":0.04}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":79,"miscValue":32,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:704642:effect:1:aura:333","name":"Atherann's Wickedness (Aura 333)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh"},"recipient":"caster","sourceEffectType":6,"sourceAuraType":333,"miscValue":0,"triggerSpellId":0}]},{"rank":2,"spellId":704643,"level":1,"description":"Atherann's WickednessRank 2Increases the damage of Atherann's Anguish by 20%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:704643:effect:0:aura:108","name":"Atherann's Wickedness (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":0.2}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":0,"triggerSpellId":0}]}]},{"id":"coa-tree-8215","classId":"bloodmage","dbcSpellId":504627,"name":"Blood Bond","unlockLevel":1,"icon":"/assets/ui/spells/nhi_bloodsacrifice_border.png","sigil":"BB","description":"Bond yourself to an ally for 30 minutes, causing direct damage taken by tLine truncated
|
||||
{"schemaVersion":1,"classId":"bloodmage","abilities":[{"id":"coa-tree-29871","classId":"bloodmage","dbcSpellId":504285,"name":"Alacrity","unlockLevel":1,"icon":"/assets/ui/spells/5_run_border.png","sigil":"AL","description":"Increases all primary stats by 4%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:504285:effect:0:aura:137","name":"Alacrity (Aura 137)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"all-primary","operation":"percent","value":0.04}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":137,"miscValue":-1,"triggerSpellId":0}],"passive":true,"source":"coa-tree","ranks":[{"rank":1,"spellId":504285,"level":1,"description":"Increases all primary stats by 4%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:504285:effect:0:aura:137","name":"Alacrity (Aura 137)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"all-primary","operation":"percent","value":0.04}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":137,"miscValue":-1,"triggerSpellId":0}]},{"rank":2,"spellId":504893,"level":1,"description":"Increases all primary stats by 8%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:504893:effect:0:aura:137","name":"Alacrity (Aura 137)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"all-primary","operation":"percent","value":0.08}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":137,"miscValue":-1,"triggerSpellId":0}]}],"talentEntryId":29871},{"id":"coa-20-atheranns-wickedness-73-704642","classId":"bloodmage","dbcSpellId":704642,"name":"Atherann's Wickedness","unlockLevel":1,"icon":"/assets/ui/spells/spell_shadow_destructivesoul.png","sigil":"AS","description":"Atherann's WickednessRank 1Increases your Shadow damage dealt by 4% and increases your hit chance by 3%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:704642:effect:0:aura:79","name":"Atherann's Wickedness (Aura 79)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"damage","direction":"dealt","school":"shadow","operation":"percent","value":0.04}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":79,"miscValue":32,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:704642:effect:1:aura:333","name":"Atherann's Wickedness (Aura 333)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh"},"recipient":"caster","sourceEffectType":6,"sourceAuraType":333,"miscValue":0,"triggerSpellId":0}],"passive":true,"source":"coa-progression","ranks":[{"rank":1,"spellId":704642,"level":1,"description":"Atherann's WickednessRank 1Increases your Shadow damage dealt by 4% and increases your hit chance by 3%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:704642:effect:0:aura:79","name":"Atherann's Wickedness (Aura 79)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"damage","direction":"dealt","school":"shadow","operation":"percent","value":0.04}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":79,"miscValue":32,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:704642:effect:1:aura:333","name":"Atherann's Wickedness (Aura 333)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh"},"recipient":"caster","sourceEffectType":6,"sourceAuraType":333,"miscValue":0,"triggerSpellId":0}]},{"rank":2,"spellId":704643,"level":1,"description":"Atherann's WickednessRank 2Increases the damage of Atherann's Anguish by 20%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:704643:effect:0:aura:108","name":"Atherann's Wickedness (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":0.2}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":0,"triggerSpellId":0}]}]},{"id":"coa-tree-8215","classId":"bloodmage","dbcSpellId":504627,"name":"Blood Bond","unlockLevel":1,"icon":"/assets/ui/spells/nhi_bloodsacrifice_border.png","sigil":"BB","description":"Bond yourself to an ally for 30 minutes, causing direct damage taken by tLine truncated
|
||||
@@ -1 +1 @@
|
||||
{"schemaVersion":1,"classId":"chronomancer","abilities":[{"id":"coa-tree-30277","classId":"chronomancer","dbcSpellId":800857,"name":"Accelerated Recovery","unlockLevel":1,"icon":"/assets/ui/spells/spell_holy_healingfocus.png","sigil":"AR","description":"Accelerated RecoveryRank 118% of base mana Instant castAccelerates the bodies natural healing process, healing the target for (15*1000/3)*(12+0+Heal*0.23) over 15 sec. Periodic healing done by this scales with your spell haste.","target":"friendly","range":{"min":0,"max":40},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":18,"percentage":true},"effects":[{"kind":"apply-aura","aura":{"id":"coa:800857:effect:0:aura:8","name":"Accelerated Recovery (Aura 8)","disposition":"buff","hideFromAuraStrip":true,"durationMs":15000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic"},"sourceEffectType":6,"sourceAuraType":8,"miscValue":0,"triggerSpellId":0},{"kind":"hot","coefficient":0.24070173263549804,"basePoints":12,"dieSides":1,"pointsPerLevel":0.6052629947662354,"bonusPowerCoefficient":0,"sourceLevel":10,"maxScalingLevel":16,"ticks":5,"intervalMs":3000},{"kind":"apply-aura","aura":{"id":"coa:800857:effect:1:aura:283","name":"Accelerated Recovery (Aura 283)","disposition":"buff","durationMs":15000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic"},"sourceEffectType":3,"sourceAuraType":283,"miscValue":0,"triggerSpellId":0}],"passive":false,"source":"coa-tree","ranks":[{"rank":1,"spellId":800857,"level":1,"description":"Accelerated RecoveryRank 118% of base mana Instant castAccelerates the bodies natural healing process, healing the target for (15*1000/3)*(12+0+Heal*0.23) over 15 sec. Periodic healing done by this scales with your spell haste.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":18,"percentage":true},"effects":[{"kind":"apply-aura","aura":{"id":"coa:800857:effect:0:aura:8","name":"Accelerated Recovery (Aura 8)","disposition":"buff","hideFromAuraStrip":true,"durationMs":15000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic"},"sourceEffectType":6,"sourceAuraType":8,"miscValue":0,"triggerSpellId":0},{"kind":"hot","coefficient":0.24070173263549804,"basePoints":12,"dieSides":1,"pointsPerLevel":0.6052629947662354,"bonusPowerCoefficient":0,"sourceLevel":10,"maxScalingLevel":16,"ticks":5,"intervalMs":3000},{"kind":"apply-aura","aura":{"id":"coa:800857:effect:1:aura:283","name":"Accelerated Recovery (Aura 283)","disposition":"buff","durationMs":15000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic"},"sourceEffectType":3,"sourceAuraType":283,"miscValue":0,"triggerSpellId":0}]}],"talentEntryId":30277},{"id":"coa-tree-4032","classId":"chronomancer","dbcSpellId":92119,"name":"Aeon of Resilience","unlockLevel":1,"icon":"/assets/ui/spells/inv_trinket_80_kultiras02a.png","sigil":"AO","description":"Level 10 Passive Teaches you Aeon of Resilience.Aeon of ResilienceReduces Epoch's cast time by -25% and it's mana cost by -10%, and healing done by it now reduces the target's damage taken by -5% for 6 seconds.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"heal","coefficient":1},{"kind":"apply-aura","aura":{"id":"coa:92119:effect:0:aura:4","name":"Aeon of Resilience (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":0,"triggerSpellId":0}],"passive":true,"source":"coa-tree","ranks":[{"rank":1,"spellId":92119,"level":1,"description":"Level 10 Passive Teaches you Aeon of Resilience.Aeon of ResilienceReduces Epoch's cast time by -25% and it's mana cost by -10%, and healing done by it now reduces the target's damage taken by -5% for 6 seconds.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"heal","coefficient":1},{"kind":"apply-aura","aura":{"id":"coa:92119:effect:0:aura:4","name":"Aeon of Resilience (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":0,"triggerSpellId":0}]}],"talentEntryId":4032},{"id":"coa-tree-6214","classId":"chronomancer","dbcSpellId":560948,"name":"Ahead of the Game","unlockLevel":1,"icon":"/assets/ui/spells/achievement_bg_win_av_x_times.png","sigil":"AO","description":"Every 10th instance of your damage dealt or healing now gives you 60 increased spell power for 10 seconds.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"heal","coefficient":1},{"kind":"apply-aura","aura":{"id":"coa:560948:effect:0:aura:42","name":"AheadLine truncated
|
||||
{"schemaVersion":1,"classId":"chronomancer","abilities":[{"id":"coa-tree-30277","classId":"chronomancer","dbcSpellId":800857,"name":"Accelerated Recovery","unlockLevel":1,"icon":"/assets/ui/spells/spell_holy_healingfocus.png","sigil":"AR","description":"Accelerated RecoveryRank 118% of base mana Instant castAccelerates the bodies natural healing process, healing the target for (15*1000/3)*(12+0+Heal*0.23) over 15 sec. Periodic healing done by this scales with your spell haste.","target":"friendly","range":{"min":0,"max":40},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":18,"percentage":true},"effects":[{"kind":"apply-aura","aura":{"id":"coa:800857:effect:0:aura:8","name":"Accelerated Recovery (Aura 8)","disposition":"buff","hideFromAuraStrip":true,"durationMs":15000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic"},"sourceEffectType":6,"sourceAuraType":8,"miscValue":0,"triggerSpellId":0},{"kind":"hot","coefficient":0.24070173263549804,"basePoints":12,"dieSides":1,"pointsPerLevel":0.6052629947662354,"bonusPowerCoefficient":0,"sourceLevel":10,"maxScalingLevel":16,"ticks":5,"intervalMs":3000},{"kind":"apply-aura","aura":{"id":"coa:800857:effect:1:aura:283","name":"Accelerated Recovery (Aura 283)","disposition":"buff","durationMs":15000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic"},"sourceEffectType":3,"sourceAuraType":283,"miscValue":0,"triggerSpellId":0}],"passive":false,"source":"coa-tree","ranks":[{"rank":1,"spellId":800857,"level":1,"description":"Accelerated RecoveryRank 118% of base mana Instant castAccelerates the bodies natural healing process, healing the target for (15*1000/3)*(12+0+Heal*0.23) over 15 sec. Periodic healing done by this scales with your spell haste.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":18,"percentage":true},"effects":[{"kind":"apply-aura","aura":{"id":"coa:800857:effect:0:aura:8","name":"Accelerated Recovery (Aura 8)","disposition":"buff","hideFromAuraStrip":true,"durationMs":15000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic"},"sourceEffectType":6,"sourceAuraType":8,"miscValue":0,"triggerSpellId":0},{"kind":"hot","coefficient":0.24070173263549804,"basePoints":12,"dieSides":1,"pointsPerLevel":0.6052629947662354,"bonusPowerCoefficient":0,"sourceLevel":10,"maxScalingLevel":16,"ticks":5,"intervalMs":3000},{"kind":"apply-aura","aura":{"id":"coa:800857:effect:1:aura:283","name":"Accelerated Recovery (Aura 283)","disposition":"buff","durationMs":15000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic"},"sourceEffectType":3,"sourceAuraType":283,"miscValue":0,"triggerSpellId":0}]}],"talentEntryId":30277},{"id":"coa-tree-4032","classId":"chronomancer","dbcSpellId":92119,"name":"Aeon of Resilience","unlockLevel":1,"icon":"/assets/ui/spells/inv_trinket_80_kultiras02a.png","sigil":"AO","description":"Level 10 Passive Teaches you Aeon of Resilience.Aeon of ResilienceReduces Epoch's cast time by -25% and it's mana cost by -10%, and healing done by it now reduces the target's damage taken by -5% for 6 seconds.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"heal","coefficient":1},{"kind":"apply-aura","aura":{"id":"coa:92119:effect:0:aura:4","name":"Aeon of Resilience (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":0,"triggerSpellId":0}],"passive":true,"source":"coa-tree","ranks":[{"rank":1,"spellId":92119,"level":1,"description":"Level 10 Passive Teaches you Aeon of Resilience.Aeon of ResilienceReduces Epoch's cast time by -25% and it's mana cost by -10%, and healing done by it now reduces the target's damage taken by -5% for 6 seconds.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"heal","coefficient":1},{"kind":"apply-aura","aura":{"id":"coa:92119:effect:0:aura:4","name":"Aeon of Resilience (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":0,"triggerSpellId":0}]}],"talentEntryId":4032},{"id":"coa-tree-6214","classId":"chronomancer","dbcSpellId":560948,"name":"Ahead of the Game","unlockLevel":1,"icon":"/assets/ui/spells/achievement_bg_win_av_x_times.png","sigil":"AO","description":"Every 10th instance of your damage dealt or healing now gives you 60 increased spell power for 10 seconds.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"heal","coefficient":1},{"kind":"apply-aura","aura":{"id":"coa:560948:effect:0:aura:42","name":"AheadLine truncated
|
||||
@@ -1 +1 @@
|
||||
{"schemaVersion":1,"classId":"cultist","abilities":[{"id":"coa-25-abyssal-decay-89-500705","classId":"cultist","dbcSpellId":500705,"name":"Abyssal Decay","unlockLevel":1,"icon":"/assets/ui/spells/spell_priest_void_blast.png","sigil":"AD","description":"Abyssal DecayRank 19% of base mana Instant castApplies an Eldritch curse to an enemy, causing 9+(PL*0.6) Shadow damage every 3 sec for 18 sec. Generates 5 Insanity.","target":"hostile","range":{"min":0,"max":30},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":9,"percentage":true},"effects":[{"kind":"apply-aura","aura":{"id":"coa:500705:effect:0:aura:3","name":"Abyssal Decay (Aura 3)","disposition":"debuff","hideFromAuraStrip":true,"durationMs":18000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"curse"},"sourceEffectType":6,"sourceAuraType":3,"miscValue":0,"triggerSpellId":0},{"kind":"dot","coefficient":0.24166666716337204,"basePoints":9,"dieSides":5,"pointsPerLevel":0.6000000238418579,"bonusPowerCoefficient":0,"sourceLevel":1,"maxScalingLevel":9,"ticks":6,"intervalMs":3000,"tag":"spell-500705"},{"kind":"utility","action":"class-script","effectType":168,"auraType":0,"miscValue":120640,"triggerSpellId":0,"durationMs":18000}],"passive":false,"source":"coa-progression","ranks":[{"rank":1,"spellId":500705,"level":1,"description":"Abyssal DecayRank 19% of base mana Instant castApplies an Eldritch curse to an enemy, causing 9+(PL*0.6) Shadow damage every 3 sec for 18 sec. Generates 5 Insanity.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":9,"percentage":true},"effects":[{"kind":"apply-aura","aura":{"id":"coa:500705:effect:0:aura:3","name":"Abyssal Decay (Aura 3)","disposition":"debuff","hideFromAuraStrip":true,"durationMs":18000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"curse"},"sourceEffectType":6,"sourceAuraType":3,"miscValue":0,"triggerSpellId":0},{"kind":"dot","coefficient":0.24166666716337204,"basePoints":9,"dieSides":5,"pointsPerLevel":0.6000000238418579,"bonusPowerCoefficient":0,"sourceLevel":1,"maxScalingLevel":9,"ticks":6,"intervalMs":3000,"tag":"spell-500705"},{"kind":"utility","action":"class-script","effectType":168,"auraType":0,"miscValue":120640,"triggerSpellId":0,"durationMs":18000}]},{"rank":2,"spellId":502144,"level":10,"description":"Abyssal DecayRank 29% of base mana Instant castApplies an Eldritch curse to an enemy, causing 17+(PL*1-10*1) Shadow damage every 3 sec for 18 sec. Generates 5 Insanity.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":9,"percentage":true},"effects":[{"kind":"apply-aura","aura":{"id":"coa:502144:effect:0:aura:3","name":"Abyssal Decay (Aura 3)","disposition":"debuff","hideFromAuraStrip":true,"durationMs":18000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"curse"},"sourceEffectType":6,"sourceAuraType":3,"miscValue":0,"triggerSpellId":0},{"kind":"dot","coefficient":0.36066666603088376,"basePoints":17,"dieSides":1,"pointsPerLevel":1.0049999952316284,"bonusPowerCoefficient":0,"sourceLevel":10,"maxScalingLevel":18,"ticks":6,"intervalMs":3000,"tag":"spell-502144"},{"kind":"utility","action":"class-script","effectType":168,"auraType":0,"miscValue":120640,"triggerSpellId":0,"durationMs":18000}]},{"rank":3,"spellId":502145,"level":19,"description":"Abyssal DecayRank 39% of base mana Instant castApplies an Eldritch curse to an enemy, causing 31+(PL*1.4-19*1.4) Shadow damage every 3 sec for 18 sec. Generates 5 Insanity.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":9,"percentage":true},"effects":[{"kind":"apply-aura","aura":{"id":"coa:502145:effect:0:aura:3","name":"Abyssal Decay (Aura 3)","disposition":"debuff","hideFromAuraStrip":true,"durationMs":18000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"curse"},"sourceEffectType":6,"sourceAuraType":3,"miscValue":0,"triggerSpellId":0},{"kind":"dot","coefficient":0.5590245174426659,"basePoints":31,"dieSides":1,"pointsPerLevel":1.3695000410079956,"bonusPowerCoefficient":0,"sourceLevel":19,"maxScalingLevel":27,"ticks":6,"intervalMs":3000,"tag":"spell-502145"},{"kind":"utility","action":"class-script","effectType":168,"auraType":0,"miscValue":120640,"triggerSpellId":0,"durationMs":18000}]},{"rank":4,"spellId":502146,"level":28,"description":"Abyssal DecayRank 49% of base mana Instant castApplies an Eldritch curse to an enemy, causing 55+(PL*1.7-28*1.7) Shadow damage every 3 sec for18 sec. Generates 5 Insanity.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":9,"percentage":true},"effects":[{"kind":"apply-aura","aura":{"id":"coa:502146:effect:0:aura:3","name":"Abyssal Decay (Aura 3)","disposition":"debuff","hideFromAuraStrip":true,"durationMs":18000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"curse"},"sourceEffectType":6,"sourceAuraType":3,"miscValue":0,"triggerSpellIdLine truncated
|
||||
{"schemaVersion":1,"classId":"cultist","abilities":[{"id":"coa-25-abyssal-decay-89-500705","classId":"cultist","dbcSpellId":500705,"name":"Abyssal Decay","unlockLevel":1,"icon":"/assets/ui/spells/spell_priest_void_blast.png","sigil":"AD","description":"Abyssal DecayRank 19% of base mana Instant castApplies an Eldritch curse to an enemy, causing 9+(PL*0.6) Shadow damage every 3 sec for 18 sec. Generates 5 Insanity.","target":"hostile","range":{"min":0,"max":30},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":9,"percentage":true},"effects":[{"kind":"apply-aura","aura":{"id":"coa:500705:effect:0:aura:3","name":"Abyssal Decay (Aura 3)","disposition":"debuff","hideFromAuraStrip":true,"durationMs":18000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"curse"},"sourceEffectType":6,"sourceAuraType":3,"miscValue":0,"triggerSpellId":0},{"kind":"dot","coefficient":0.24166666716337204,"basePoints":9,"dieSides":5,"pointsPerLevel":0.6000000238418579,"bonusPowerCoefficient":0,"sourceLevel":1,"maxScalingLevel":9,"ticks":6,"intervalMs":3000,"tag":"spell-500705"},{"kind":"utility","action":"class-script","effectType":168,"auraType":0,"miscValue":120640,"triggerSpellId":0,"durationMs":18000}],"passive":false,"source":"coa-progression","ranks":[{"rank":1,"spellId":500705,"level":1,"description":"Abyssal DecayRank 19% of base mana Instant castApplies an Eldritch curse to an enemy, causing 9+(PL*0.6) Shadow damage every 3 sec for 18 sec. Generates 5 Insanity.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":9,"percentage":true},"effects":[{"kind":"apply-aura","aura":{"id":"coa:500705:effect:0:aura:3","name":"Abyssal Decay (Aura 3)","disposition":"debuff","hideFromAuraStrip":true,"durationMs":18000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"curse"},"sourceEffectType":6,"sourceAuraType":3,"miscValue":0,"triggerSpellId":0},{"kind":"dot","coefficient":0.24166666716337204,"basePoints":9,"dieSides":5,"pointsPerLevel":0.6000000238418579,"bonusPowerCoefficient":0,"sourceLevel":1,"maxScalingLevel":9,"ticks":6,"intervalMs":3000,"tag":"spell-500705"},{"kind":"utility","action":"class-script","effectType":168,"auraType":0,"miscValue":120640,"triggerSpellId":0,"durationMs":18000}]},{"rank":2,"spellId":502144,"level":10,"description":"Abyssal DecayRank 29% of base mana Instant castApplies an Eldritch curse to an enemy, causing 17+(PL*1-10*1) Shadow damage every 3 sec for 18 sec. Generates 5 Insanity.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":9,"percentage":true},"effects":[{"kind":"apply-aura","aura":{"id":"coa:502144:effect:0:aura:3","name":"Abyssal Decay (Aura 3)","disposition":"debuff","hideFromAuraStrip":true,"durationMs":18000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"curse"},"sourceEffectType":6,"sourceAuraType":3,"miscValue":0,"triggerSpellId":0},{"kind":"dot","coefficient":0.36066666603088376,"basePoints":17,"dieSides":1,"pointsPerLevel":1.0049999952316284,"bonusPowerCoefficient":0,"sourceLevel":10,"maxScalingLevel":18,"ticks":6,"intervalMs":3000,"tag":"spell-502144"},{"kind":"utility","action":"class-script","effectType":168,"auraType":0,"miscValue":120640,"triggerSpellId":0,"durationMs":18000}]},{"rank":3,"spellId":502145,"level":19,"description":"Abyssal DecayRank 39% of base mana Instant castApplies an Eldritch curse to an enemy, causing 31+(PL*1.4-19*1.4) Shadow damage every 3 sec for 18 sec. Generates 5 Insanity.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":9,"percentage":true},"effects":[{"kind":"apply-aura","aura":{"id":"coa:502145:effect:0:aura:3","name":"Abyssal Decay (Aura 3)","disposition":"debuff","hideFromAuraStrip":true,"durationMs":18000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"curse"},"sourceEffectType":6,"sourceAuraType":3,"miscValue":0,"triggerSpellId":0},{"kind":"dot","coefficient":0.5590245174426659,"basePoints":31,"dieSides":1,"pointsPerLevel":1.3695000410079956,"bonusPowerCoefficient":0,"sourceLevel":19,"maxScalingLevel":27,"ticks":6,"intervalMs":3000,"tag":"spell-502145"},{"kind":"utility","action":"class-script","effectType":168,"auraType":0,"miscValue":120640,"triggerSpellId":0,"durationMs":18000}]},{"rank":4,"spellId":502146,"level":28,"description":"Abyssal DecayRank 49% of base mana Instant castApplies an Eldritch curse to an enemy, causing 55+(PL*1.7-28*1.7) Shadow damage every 3 sec for18 sec. Generates 5 Insanity.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":9,"percentage":true},"effects":[{"kind":"apply-aura","aura":{"id":"coa:502146:effect:0:aura:3","name":"Abyssal Decay (Aura 3)","disposition":"debuff","hideFromAuraStrip":true,"durationMs":18000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"curse"},"sourceEffectType":6,"sourceAuraType":3,"miscValue":0,"triggerSpellIdLine truncated
|
||||
@@ -1 +1 @@
|
||||
{"schemaVersion":1,"classId":"felsworn","abilities":[{"id":"coa-tree-29686","classId":"felsworn","dbcSpellId":803904,"name":"Annihilation","unlockLevel":1,"icon":"/assets/ui/spells/ability_boss_lordanthricystgreen.png","sigil":"AN","description":"Gorge on fel energy, guaranteeing your next direct instance of damage to critically strike, but causing it to consume 5% of your maximum health. Lasts for 5 attacks or 12 seconds.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":60000,"gcdMs":1500,"cost":{"resource":"energy","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:803904:effect:0:aura:290","name":"Annihilation (Aura 290)","disposition":"buff","durationMs":12000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic"},"recipient":"caster","sourceEffectType":6,"sourceAuraType":290,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:803904:effect:1:aura:42","name":"Annihilation (Aura 42)","disposition":"buff","durationMs":12000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic","procs":[{"id":"coa:803904:proc:1:807961","triggers":["crit","hit"],"chance":1,"charges":5,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":807961}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":42,"miscValue":0,"triggerSpellId":807961}],"passive":false,"source":"coa-tree","ranks":[{"rank":1,"spellId":803904,"level":1,"description":"Gorge on fel energy, guaranteeing your next direct instance of damage to critically strike, but causing it to consume 5% of your maximum health. Lasts for 5 attacks or 12 seconds.","castMode":"instant","castTimeMs":0,"cooldownMs":60000,"gcdMs":1500,"cost":{"resource":"energy","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:803904:effect:0:aura:290","name":"Annihilation (Aura 290)","disposition":"buff","durationMs":12000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic"},"recipient":"caster","sourceEffectType":6,"sourceAuraType":290,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:803904:effect:1:aura:42","name":"Annihilation (Aura 42)","disposition":"buff","durationMs":12000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic","procs":[{"id":"coa:803904:proc:1:807961","triggers":["crit","hit"],"chance":1,"charges":5,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":807961}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":42,"miscValue":0,"triggerSpellId":807961}]}],"talentEntryId":29686},{"id":"coa-tree-30301","classId":"felsworn","dbcSpellId":560822,"name":"Anti-Magic","unlockLevel":1,"icon":"/assets/ui/spells/spell_shadow_antimagicshell.png","sigil":"AM","description":"When Hateforged Barrier is removed from you, you now reduce -10% of all Magic damage taken for 8 seconds. In addition, Hateforged Barrier now heals for an additional 2% of your maximum health each tick.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"heal","coefficient":1},{"kind":"apply-aura","aura":{"id":"coa:560822:effect:0:aura:4","name":"Anti-Magic (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:560822:effect:1:aura:107","name":"Anti-Magic (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":0.02}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":3,"triggerSpellId":0}],"passive":true,"source":"coa-tree","ranks":[{"rank":1,"spellId":560822,"level":1,"description":"When Hateforged Barrier is removed from you, you now reduce -10% of all Magic damage taken for 8 seconds. In addition, Hateforged Barrier now heals for an additional 2% of your maximum health each tick.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"heal","coefficient":1},{"kind":"apply-aura","aura":{"id":"coa:560822:effect:0:aura:4","name":"Anti-Magic (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:560822:effect:1:aura:107","name":"Anti-Magic (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":0.02}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":3,"triggerSpellId":0}]}],"talentEntryId":30301},{"id":"coa-tree-33671","classId":"felsworn","dbcSpellId":706266,"namLine truncated
|
||||
{"schemaVersion":1,"classId":"felsworn","abilities":[{"id":"coa-tree-29686","classId":"felsworn","dbcSpellId":803904,"name":"Annihilation","unlockLevel":1,"icon":"/assets/ui/spells/ability_boss_lordanthricystgreen.png","sigil":"AN","description":"Gorge on fel energy, guaranteeing your next direct instance of damage to critically strike, but causing it to consume 5% of your maximum health. Lasts for 5 attacks or 12 seconds.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":60000,"gcdMs":1500,"cost":{"resource":"energy","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:803904:effect:0:aura:290","name":"Annihilation (Aura 290)","disposition":"buff","durationMs":12000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic"},"recipient":"caster","sourceEffectType":6,"sourceAuraType":290,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:803904:effect:1:aura:42","name":"Annihilation (Aura 42)","disposition":"buff","durationMs":12000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic","procs":[{"id":"coa:803904:proc:1:807961","triggers":["crit","hit"],"chance":1,"charges":5,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":807961}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":42,"miscValue":0,"triggerSpellId":807961}],"passive":false,"source":"coa-tree","ranks":[{"rank":1,"spellId":803904,"level":1,"description":"Gorge on fel energy, guaranteeing your next direct instance of damage to critically strike, but causing it to consume 5% of your maximum health. Lasts for 5 attacks or 12 seconds.","castMode":"instant","castTimeMs":0,"cooldownMs":60000,"gcdMs":1500,"cost":{"resource":"energy","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:803904:effect:0:aura:290","name":"Annihilation (Aura 290)","disposition":"buff","durationMs":12000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic"},"recipient":"caster","sourceEffectType":6,"sourceAuraType":290,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:803904:effect:1:aura:42","name":"Annihilation (Aura 42)","disposition":"buff","durationMs":12000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic","procs":[{"id":"coa:803904:proc:1:807961","triggers":["crit","hit"],"chance":1,"charges":5,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":807961}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":42,"miscValue":0,"triggerSpellId":807961}]}],"talentEntryId":29686},{"id":"coa-tree-30301","classId":"felsworn","dbcSpellId":560822,"name":"Anti-Magic","unlockLevel":1,"icon":"/assets/ui/spells/spell_shadow_antimagicshell.png","sigil":"AM","description":"When Hateforged Barrier is removed from you, you now reduce -10% of all Magic damage taken for 8 seconds. In addition, Hateforged Barrier now heals for an additional 2% of your maximum health each tick.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"heal","coefficient":1},{"kind":"apply-aura","aura":{"id":"coa:560822:effect:0:aura:4","name":"Anti-Magic (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:560822:effect:1:aura:107","name":"Anti-Magic (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":0.02}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":3,"triggerSpellId":0}],"passive":true,"source":"coa-tree","ranks":[{"rank":1,"spellId":560822,"level":1,"description":"When Hateforged Barrier is removed from you, you now reduce -10% of all Magic damage taken for 8 seconds. In addition, Hateforged Barrier now heals for an additional 2% of your maximum health each tick.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"heal","coefficient":1},{"kind":"apply-aura","aura":{"id":"coa:560822:effect:0:aura:4","name":"Anti-Magic (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:560822:effect:1:aura:107","name":"Anti-Magic (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":0.02}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":3,"triggerSpellId":0}]}],"talentEntryId":30301},{"id":"coa-tree-33671","classId":"felsworn","dbcSpellId":706266,"namLine truncated
|
||||
@@ -1 +1 @@
|
||||
{"schemaVersion":1,"classId":"guardian","abilities":[{"id":"coa-18-banner-boy-68-705358","classId":"guardian","dbcSpellId":705358,"name":"Banner Boy","unlockLevel":1,"icon":"/assets/ui/spells/achievement_bg_captureflag_wsg.png","sigil":"BB","description":"Banner BoyRank 1Reduces the Energy cost of Banners by 10.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705358:effect:0:aura:107","name":"Banner Boy (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":-0.1}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":14,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705358:effect:1:aura:4","name":"Banner Boy (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":127,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705358:effect:2:aura:4","name":"Banner Boy (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":3,"triggerSpellId":0}],"passive":true,"source":"coa-progression","ranks":[{"rank":1,"spellId":705358,"level":1,"description":"Banner BoyRank 1Reduces the Energy cost of Banners by 10.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705358:effect:0:aura:107","name":"Banner Boy (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":-0.1}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":14,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705358:effect:1:aura:4","name":"Banner Boy (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":127,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705358:effect:2:aura:4","name":"Banner Boy (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":3,"triggerSpellId":0}]}]},{"id":"coa-tree-11797","classId":"guardian","dbcSpellId":803683,"name":"Battle Drums","unlockLevel":1,"icon":"/assets/ui/spells/inv_archaeology_70_tauren_drum.png","sigil":"BD","description":"Place down 3 Battle Drums around you for 1 minute. Allies may click on the drums to increase their critical strike chance, haste, damage, and healing done by 10% and causes their damage dealt to trigger a Sound of War for their next 5 instances of damage or 30 seconds.","target":"hostile","range":{"min":0,"max":0},"radius":3,"castMode":"instant","castTimeMs":0,"cooldownMs":180000,"gcdMs":1500,"cost":{"resource":"energy","amount":50},"effects":[{"kind":"damage","coefficient":1},{"kind":"heal","coefficient":1},{"kind":"utility","action":"portal","effectType":50,"auraType":0,"miscValue":9000117,"triggerSpellId":0,"durationMs":60000},{"kind":"utility","action":"portal","effectType":50,"auraType":0,"miscValue":9000117,"triggerSpellId":0,"durationMs":60000},{"kind":"utility","action":"portal","effectType":50,"auraType":0,"miscValue":9000117,"triggerSpellId":0,"durationMs":60000}],"passive":false,"source":"coa-tree","ranks":[{"rank":1,"spellId":803683,"level":1,"description":"Place down 3 Battle Drums around you for 1 minute. Allies may click on the drums to increase their critical strike chance, haste, damage, and healing done by 10% and causes their damage dealt to trigger a Sound of War for their next 5 instances of damage or 30 seconds.","castMode":"instant","castTimeMs":0,"cooldownMs":180000,"gcdMs":1500,"cost":{"resource":"energy","amount":50},"effects":[{"kind":"damage","coefficient":1},{"kind":"heal","coefficient":1},{"kind":"utility","action":"portal","effectType":50,"auraType":0,"miscValue":9000117,"triggerSpellId":0,"durationMs":60000},{"kind":"utility","action":"portal","effectType":50,"auraType":0,"miscValue":9000117,"triggerSpellId":0,"durationMs":60000},{"kind":"utility","action":"portal","effectType":50,"auraType":0,"miscValue":9000117,"triggerSpellId":0,"durationMs":60000}]}],"talentEntryId":11797},{"id":"guardian-battle-rush","classId":"guardian","dbcSpellId":802197,"name":"Battle Rush","unlockLevel":1,"icon":"/assets/ui/spells/warrior_talent_icon_skirmisher.png","sigil":"BR","description":"Battle RushRank 1Instant cast30 sec cooldownCharge an enemy, dealing 16+0+AP*0.09996 Physical damage and stunnLine truncated
|
||||
{"schemaVersion":1,"classId":"guardian","abilities":[{"id":"coa-18-banner-boy-68-705358","classId":"guardian","dbcSpellId":705358,"name":"Banner Boy","unlockLevel":1,"icon":"/assets/ui/spells/achievement_bg_captureflag_wsg.png","sigil":"BB","description":"Banner BoyRank 1Reduces the Energy cost of Banners by 10.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705358:effect:0:aura:107","name":"Banner Boy (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":-0.1}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":14,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705358:effect:1:aura:4","name":"Banner Boy (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":127,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705358:effect:2:aura:4","name":"Banner Boy (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":3,"triggerSpellId":0}],"passive":true,"source":"coa-progression","ranks":[{"rank":1,"spellId":705358,"level":1,"description":"Banner BoyRank 1Reduces the Energy cost of Banners by 10.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705358:effect:0:aura:107","name":"Banner Boy (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":-0.1}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":14,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705358:effect:1:aura:4","name":"Banner Boy (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":127,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705358:effect:2:aura:4","name":"Banner Boy (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":3,"triggerSpellId":0}]}]},{"id":"coa-tree-11797","classId":"guardian","dbcSpellId":803683,"name":"Battle Drums","unlockLevel":1,"icon":"/assets/ui/spells/inv_archaeology_70_tauren_drum.png","sigil":"BD","description":"Place down 3 Battle Drums around you for 1 minute. Allies may click on the drums to increase their critical strike chance, haste, damage, and healing done by 10% and causes their damage dealt to trigger a Sound of War for their next 5 instances of damage or 30 seconds.","target":"hostile","range":{"min":0,"max":0},"radius":3,"castMode":"instant","castTimeMs":0,"cooldownMs":180000,"gcdMs":1500,"cost":{"resource":"energy","amount":50},"effects":[{"kind":"damage","coefficient":1},{"kind":"heal","coefficient":1},{"kind":"utility","action":"portal","effectType":50,"auraType":0,"miscValue":9000117,"triggerSpellId":0,"durationMs":60000},{"kind":"utility","action":"portal","effectType":50,"auraType":0,"miscValue":9000117,"triggerSpellId":0,"durationMs":60000},{"kind":"utility","action":"portal","effectType":50,"auraType":0,"miscValue":9000117,"triggerSpellId":0,"durationMs":60000}],"passive":false,"source":"coa-tree","ranks":[{"rank":1,"spellId":803683,"level":1,"description":"Place down 3 Battle Drums around you for 1 minute. Allies may click on the drums to increase their critical strike chance, haste, damage, and healing done by 10% and causes their damage dealt to trigger a Sound of War for their next 5 instances of damage or 30 seconds.","castMode":"instant","castTimeMs":0,"cooldownMs":180000,"gcdMs":1500,"cost":{"resource":"energy","amount":50},"effects":[{"kind":"damage","coefficient":1},{"kind":"heal","coefficient":1},{"kind":"utility","action":"portal","effectType":50,"auraType":0,"miscValue":9000117,"triggerSpellId":0,"durationMs":60000},{"kind":"utility","action":"portal","effectType":50,"auraType":0,"miscValue":9000117,"triggerSpellId":0,"durationMs":60000},{"kind":"utility","action":"portal","effectType":50,"auraType":0,"miscValue":9000117,"triggerSpellId":0,"durationMs":60000}]}],"talentEntryId":11797},{"id":"guardian-battle-rush","classId":"guardian","dbcSpellId":802197,"name":"Battle Rush","unlockLevel":1,"icon":"/assets/ui/spells/warrior_talent_icon_skirmisher.png","sigil":"BR","description":"Battle RushRank 1Instant cast30 sec cooldownCharge an enemy, dealing 16+0+AP*0.09996 Physical damage and stunnLine truncated
|
||||
@@ -1 +1 @@
|
||||
{"schemaVersion":1,"classId":"knight-of-xoroth","abilities":[{"id":"coa-tree-30701","classId":"knight-of-xoroth","dbcSpellId":805679,"name":"Black Shield","unlockLevel":1,"icon":"/assets/ui/spells/spell_warlock_soulburn_haunt.png","sigil":"BS","description":"Consume all of your Demon's Blood stacks to create a demonic shield that absorbs 860 damage, increased by 20% per Demon's Blood stack, for 30 seconds.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":120000,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"shield","coefficient":1,"durationMs":15000},{"kind":"utility","action":"class-script","effectType":3,"auraType":0,"miscValue":0,"triggerSpellId":0,"durationMs":0}],"passive":false,"source":"coa-tree","ranks":[{"rank":1,"spellId":805679,"level":1,"description":"Consume all of your Demon's Blood stacks to create a demonic shield that absorbs 860 damage, increased by 20% per Demon's Blood stack, for 30 seconds.","castMode":"instant","castTimeMs":0,"cooldownMs":120000,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"shield","coefficient":1,"durationMs":15000},{"kind":"utility","action":"class-script","effectType":3,"auraType":0,"miscValue":0,"triggerSpellId":0,"durationMs":0}]}],"talentEntryId":30701},{"id":"coa-tree-34064","classId":"knight-of-xoroth","dbcSpellId":300395,"name":"Boiling Blood","unlockLevel":1,"icon":"/assets/ui/spells/nhi_bloodboil_border.png","sigil":"BB","description":"Casting Suffuse now increases your healing received by 25% for the first 6 seconds of it's duration.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"heal","coefficient":1},{"kind":"apply-aura","aura":{"id":"coa:300395:effect:0:aura:42","name":"Boiling Blood (Aura 42)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","procs":[{"id":"coa:300395:proc:0:300394","triggers":["cast","heal"],"chance":1,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":300394}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":42,"miscValue":0,"triggerSpellId":300394}],"passive":true,"source":"coa-tree","ranks":[{"rank":1,"spellId":300395,"level":1,"description":"Casting Suffuse now increases your healing received by 25% for the first 6 seconds of it's duration.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"heal","coefficient":1},{"kind":"apply-aura","aura":{"id":"coa:300395:effect:0:aura:42","name":"Boiling Blood (Aura 42)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","procs":[{"id":"coa:300395:proc:0:300394","triggers":["cast","heal"],"chance":1,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":300394}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":42,"miscValue":0,"triggerSpellId":300394}]}],"talentEntryId":34064},{"id":"coa-tree-4018","classId":"knight-of-xoroth","dbcSpellId":92104,"name":"Brimstone Buckler","unlockLevel":1,"icon":"/assets/ui/spells/ability_warlock_improveddemonictactics.png","sigil":"BB","description":"Level 10 Passive Successful blocks now have a 30% chance to summon 1 Hellfire Imp to aid you in battle for 12 seconds. Can only occur once every 3 sec.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:92104:effect:0:aura:23","name":"Brimstone Buckler (Aura 23)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","procs":[{"id":"coa:92104:proc:0:562029","triggers":["periodic"],"chance":0.3,"internalCooldownMs":3000,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":562029}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":23,"miscValue":0,"triggerSpellId":562029},{"kind":"apply-aura","aura":{"id":"coa:92104:effect:1:aura:42","name":"Brimstone Buckler (Aura 42)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","procs":[{"id":"coa:92104:proc:1:805966","triggers":["block"],"chance":0.3,"internalCooldownMs":3000,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":805966}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":42,"miscValue":0,"triggerSpellId":805966}],"passive":true,"source":"coa-tree","ranks":[{"rank":1,"spellId":92104,"level":1,"description":"Level 10 Passive Successful blocks now have a 30% chance to summon 1 Hellfire Imp to aid you in battle for 12 seconds. Can only occur once every 3 sec.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:92104:effect:0:aura:23","name":"Brimstone Buckler (Aura 23)","disposition":"buff","durationMs":null,"maxSLine truncated
|
||||
{"schemaVersion":1,"classId":"knight-of-xoroth","abilities":[{"id":"coa-tree-30701","classId":"knight-of-xoroth","dbcSpellId":805679,"name":"Black Shield","unlockLevel":1,"icon":"/assets/ui/spells/spell_warlock_soulburn_haunt.png","sigil":"BS","description":"Consume all of your Demon's Blood stacks to create a demonic shield that absorbs 860 damage, increased by 20% per Demon's Blood stack, for 30 seconds.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":120000,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"shield","coefficient":1,"durationMs":15000},{"kind":"utility","action":"class-script","effectType":3,"auraType":0,"miscValue":0,"triggerSpellId":0,"durationMs":0}],"passive":false,"source":"coa-tree","ranks":[{"rank":1,"spellId":805679,"level":1,"description":"Consume all of your Demon's Blood stacks to create a demonic shield that absorbs 860 damage, increased by 20% per Demon's Blood stack, for 30 seconds.","castMode":"instant","castTimeMs":0,"cooldownMs":120000,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"shield","coefficient":1,"durationMs":15000},{"kind":"utility","action":"class-script","effectType":3,"auraType":0,"miscValue":0,"triggerSpellId":0,"durationMs":0}]}],"talentEntryId":30701},{"id":"coa-tree-34064","classId":"knight-of-xoroth","dbcSpellId":300395,"name":"Boiling Blood","unlockLevel":1,"icon":"/assets/ui/spells/nhi_bloodboil_border.png","sigil":"BB","description":"Casting Suffuse now increases your healing received by 25% for the first 6 seconds of it's duration.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"heal","coefficient":1},{"kind":"apply-aura","aura":{"id":"coa:300395:effect:0:aura:42","name":"Boiling Blood (Aura 42)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","procs":[{"id":"coa:300395:proc:0:300394","triggers":["cast","heal"],"chance":1,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":300394}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":42,"miscValue":0,"triggerSpellId":300394}],"passive":true,"source":"coa-tree","ranks":[{"rank":1,"spellId":300395,"level":1,"description":"Casting Suffuse now increases your healing received by 25% for the first 6 seconds of it's duration.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"heal","coefficient":1},{"kind":"apply-aura","aura":{"id":"coa:300395:effect:0:aura:42","name":"Boiling Blood (Aura 42)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","procs":[{"id":"coa:300395:proc:0:300394","triggers":["cast","heal"],"chance":1,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":300394}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":42,"miscValue":0,"triggerSpellId":300394}]}],"talentEntryId":34064},{"id":"coa-tree-4018","classId":"knight-of-xoroth","dbcSpellId":92104,"name":"Brimstone Buckler","unlockLevel":1,"icon":"/assets/ui/spells/ability_warlock_improveddemonictactics.png","sigil":"BB","description":"Level 10 Passive Successful blocks now have a 30% chance to summon 1 Hellfire Imp to aid you in battle for 12 seconds. Can only occur once every 3 sec.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:92104:effect:0:aura:23","name":"Brimstone Buckler (Aura 23)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","procs":[{"id":"coa:92104:proc:0:562029","triggers":["periodic"],"chance":0.3,"internalCooldownMs":3000,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":562029}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":23,"miscValue":0,"triggerSpellId":562029},{"kind":"apply-aura","aura":{"id":"coa:92104:effect:1:aura:42","name":"Brimstone Buckler (Aura 42)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","procs":[{"id":"coa:92104:proc:1:805966","triggers":["block"],"chance":0.3,"internalCooldownMs":3000,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":805966}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":42,"miscValue":0,"triggerSpellId":805966}],"passive":true,"source":"coa-tree","ranks":[{"rank":1,"spellId":92104,"level":1,"description":"Level 10 Passive Successful blocks now have a 30% chance to summon 1 Hellfire Imp to aid you in battle for 12 seconds. Can only occur once every 3 sec.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:92104:effect:0:aura:23","name":"Brimstone Buckler (Aura 23)","disposition":"buff","durationMs":null,"maxSLine truncated
|
||||
@@ -1 +1 @@
|
||||
{"schemaVersion":1,"classId":"necromancer","abilities":[{"id":"coa-tree-30600","classId":"necromancer","dbcSpellId":805428,"name":"Animate: Frost Wyrm","unlockLevel":1,"icon":"/assets/ui/spells/5_dragoncoldbreath_border.png","sigil":"AF","description":"Animate a Frost Wyrm that flies behind the Necromancer that blasts nearby enemies every 2 sec for 30 seconds, dealing 1500 + 14.88% frost SP + 14.88% Intellect Frost damage.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":120000,"gcdMs":1500,"cost":{"resource":"mana","amount":31,"percentage":true},"effects":[{"kind":"summon","creatureId":50177,"coefficient":0.08,"durationMs":30000}],"passive":false,"source":"coa-tree","ranks":[{"rank":1,"spellId":805428,"level":1,"description":"Animate a Frost Wyrm that flies behind the Necromancer that blasts nearby enemies every 2 sec for 30 seconds, dealing 1500 + 14.88% frost SP + 14.88% Intellect Frost damage.","castMode":"instant","castTimeMs":0,"cooldownMs":120000,"gcdMs":1500,"cost":{"resource":"mana","amount":31,"percentage":true},"effects":[{"kind":"summon","creatureId":50177,"coefficient":0.08,"durationMs":30000}]}],"talentEntryId":30600},{"id":"coa-23-animate-zombies-84-504315","classId":"necromancer","dbcSpellId":504315,"name":"Animate: Zombies","unlockLevel":1,"icon":"/assets/ui/spells/zombiewc3.png","sigil":"AZ","description":"Animate: ZombiesRank 121% of base mana 3 sec cast15 sec cooldownAnimate 2 Zombies to aid you in combat for 15 sec. Every 3 sec, Zombies emit Zombie Plague, dealing 79+1.0 Points Per Level+SP*0.1 Plague Damage to nearby enemies.","target":"hostile","range":{"min":0,"max":0},"radius":3,"castMode":"cast","castTimeMs":3000,"cooldownMs":15000,"gcdMs":1000,"cost":{"resource":"mana","amount":21,"percentage":true},"effects":[{"kind":"trigger-spell","spellId":525379,"withValue":2},{"kind":"damage","coefficient":1},{"kind":"summon","creatureId":503031,"coefficient":0.08,"durationMs":15000},{"kind":"utility","action":"class-script","effectType":3,"auraType":0,"miscValue":524287,"triggerSpellId":500991,"durationMs":100}],"passive":false,"source":"coa-progression","ranks":[{"rank":1,"spellId":504315,"level":1,"description":"Animate: ZombiesRank 121% of base mana 3 sec cast15 sec cooldownAnimate 2 Zombies to aid you in combat for 15 sec. Every 3 sec, Zombies emit Zombie Plague, dealing 79+1.0 Points Per Level+SP*0.1 Plague Damage to nearby enemies.","castMode":"cast","castTimeMs":3000,"cooldownMs":15000,"gcdMs":1000,"cost":{"resource":"mana","amount":21,"percentage":true},"effects":[{"kind":"trigger-spell","spellId":525379,"withValue":2},{"kind":"damage","coefficient":1},{"kind":"summon","creatureId":503031,"coefficient":0.08,"durationMs":15000},{"kind":"utility","action":"class-script","effectType":3,"auraType":0,"miscValue":524287,"triggerSpellId":500991,"durationMs":100}]},{"rank":2,"spellId":525378,"level":30,"description":"Animate: ZombiesRank 223% of base mana 3 sec cast15 sec cooldownAnimate 3 Zombies to aid you in combat for 15 sec. Every 3 sec, Zombies emit Zombie Plague, dealing 79+1.0 Points Per Level+SP*0.1 Plague Damage to nearby enemies.","castMode":"cast","castTimeMs":3000,"cooldownMs":15000,"gcdMs":1000,"cost":{"resource":"mana","amount":23,"percentage":true},"effects":[{"kind":"trigger-spell","spellId":525379,"withValue":3},{"kind":"damage","coefficient":1},{"kind":"summon","creatureId":503031,"coefficient":0.08,"durationMs":15000},{"kind":"utility","action":"class-script","effectType":3,"auraType":0,"miscValue":524287,"triggerSpellId":500991,"durationMs":100}]},{"rank":3,"spellId":525381,"level":51,"description":"Animate: ZombiesRank 324% of base mana 3 sec cast15 sec cooldownAnimate 4 Zombies to aid you in combat for 10 sec. Every 3 sec, Zombies emit Zombie Plague, dealing 79+1.0 Points Per Level+SP*0.1 Plague Damage to nearby enemies.","castMode":"cast","castTimeMs":3000,"cooldownMs":15000,"gcdMs":1000,"cost":{"resource":"mana","amount":24,"percentage":true},"effects":[{"kind":"trigger-spell","spellId":525379,"withValue":4},{"kind":"damage","coefficient":1},{"kind":"summon","creatureId":503031,"coefficient":0.08,"durationMs":15000},{"kind":"utility","action":"class-script","effectType":3,"auraType":0,"miscValue":524287,"triggerSpellId":500991,"durationMs":100}]}]},{"id":"coa-tree-29774","classId":"necromancer","dbcSpellId":802121,"name":"Bone Tithe","unlockLevel":1,"icon":"/assets/ui/spells/ossuary_bond.png","sigil":"BT","description":"Siphon life from your undead minions, sacrificing 40% of their health to grant yourself an absorb shield equal to half of the health stolen for 20 seconds. Usable while stunned.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":120000,"gcdMs":1500,"cost":{"resource":"mana","amount":25,"percentage":true},"effects":[{"kind":"shield","coefficient":1,"durationMs":15000},{"kind":"utility","action":"class-script","effectType":77,"auraType":0,"miscValue":0,"triggerSpellId"Line truncated
|
||||
{"schemaVersion":1,"classId":"necromancer","abilities":[{"id":"coa-tree-30600","classId":"necromancer","dbcSpellId":805428,"name":"Animate: Frost Wyrm","unlockLevel":1,"icon":"/assets/ui/spells/5_dragoncoldbreath_border.png","sigil":"AF","description":"Animate a Frost Wyrm that flies behind the Necromancer that blasts nearby enemies every 2 sec for 30 seconds, dealing 1500 + 14.88% frost SP + 14.88% Intellect Frost damage.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":120000,"gcdMs":1500,"cost":{"resource":"mana","amount":31,"percentage":true},"effects":[{"kind":"summon","creatureId":50177,"coefficient":0.08,"durationMs":30000}],"passive":false,"source":"coa-tree","ranks":[{"rank":1,"spellId":805428,"level":1,"description":"Animate a Frost Wyrm that flies behind the Necromancer that blasts nearby enemies every 2 sec for 30 seconds, dealing 1500 + 14.88% frost SP + 14.88% Intellect Frost damage.","castMode":"instant","castTimeMs":0,"cooldownMs":120000,"gcdMs":1500,"cost":{"resource":"mana","amount":31,"percentage":true},"effects":[{"kind":"summon","creatureId":50177,"coefficient":0.08,"durationMs":30000}]}],"talentEntryId":30600},{"id":"coa-23-animate-zombies-84-504315","classId":"necromancer","dbcSpellId":504315,"name":"Animate: Zombies","unlockLevel":1,"icon":"/assets/ui/spells/zombiewc3.png","sigil":"AZ","description":"Animate: ZombiesRank 121% of base mana 3 sec cast15 sec cooldownAnimate 2 Zombies to aid you in combat for 15 sec. Every 3 sec, Zombies emit Zombie Plague, dealing 79+1.0 Points Per Level+SP*0.1 Plague Damage to nearby enemies.","target":"hostile","range":{"min":0,"max":0},"radius":3,"castMode":"cast","castTimeMs":3000,"cooldownMs":15000,"gcdMs":1000,"cost":{"resource":"mana","amount":21,"percentage":true},"effects":[{"kind":"trigger-spell","spellId":525379,"expanded":true,"withValue":2},{"kind":"damage","coefficient":1},{"kind":"summon","creatureId":503031,"coefficient":0.08,"durationMs":15000},{"kind":"utility","action":"class-script","effectType":3,"auraType":0,"miscValue":524287,"triggerSpellId":500991,"durationMs":100}],"passive":false,"source":"coa-progression","ranks":[{"rank":1,"spellId":504315,"level":1,"description":"Animate: ZombiesRank 121% of base mana 3 sec cast15 sec cooldownAnimate 2 Zombies to aid you in combat for 15 sec. Every 3 sec, Zombies emit Zombie Plague, dealing 79+1.0 Points Per Level+SP*0.1 Plague Damage to nearby enemies.","castMode":"cast","castTimeMs":3000,"cooldownMs":15000,"gcdMs":1000,"cost":{"resource":"mana","amount":21,"percentage":true},"effects":[{"kind":"trigger-spell","spellId":525379,"expanded":true,"withValue":2},{"kind":"damage","coefficient":1},{"kind":"summon","creatureId":503031,"coefficient":0.08,"durationMs":15000},{"kind":"utility","action":"class-script","effectType":3,"auraType":0,"miscValue":524287,"triggerSpellId":500991,"durationMs":100}]},{"rank":2,"spellId":525378,"level":30,"description":"Animate: ZombiesRank 223% of base mana 3 sec cast15 sec cooldownAnimate 3 Zombies to aid you in combat for 15 sec. Every 3 sec, Zombies emit Zombie Plague, dealing 79+1.0 Points Per Level+SP*0.1 Plague Damage to nearby enemies.","castMode":"cast","castTimeMs":3000,"cooldownMs":15000,"gcdMs":1000,"cost":{"resource":"mana","amount":23,"percentage":true},"effects":[{"kind":"trigger-spell","spellId":525379,"expanded":true,"withValue":3},{"kind":"damage","coefficient":1},{"kind":"summon","creatureId":503031,"coefficient":0.08,"durationMs":15000},{"kind":"utility","action":"class-script","effectType":3,"auraType":0,"miscValue":524287,"triggerSpellId":500991,"durationMs":100}]},{"rank":3,"spellId":525381,"level":51,"description":"Animate: ZombiesRank 324% of base mana 3 sec cast15 sec cooldownAnimate 4 Zombies to aid you in combat for 10 sec. Every 3 sec, Zombies emit Zombie Plague, dealing 79+1.0 Points Per Level+SP*0.1 Plague Damage to nearby enemies.","castMode":"cast","castTimeMs":3000,"cooldownMs":15000,"gcdMs":1000,"cost":{"resource":"mana","amount":24,"percentage":true},"effects":[{"kind":"trigger-spell","spellId":525379,"expanded":true,"withValue":4},{"kind":"damage","coefficient":1},{"kind":"summon","creatureId":503031,"coefficient":0.08,"durationMs":15000},{"kind":"utility","action":"class-script","effectType":3,"auraType":0,"miscValue":524287,"triggerSpellId":500991,"durationMs":100}]}]},{"id":"coa-tree-29774","classId":"necromancer","dbcSpellId":802121,"name":"Bone Tithe","unlockLevel":1,"icon":"/assets/ui/spells/ossuary_bond.png","sigil":"BT","description":"Siphon life from your undead minions, sacrificing 40% of their health to grant yourself an absorb shield equal to half of the health stolen for 20 seconds. Usable while stunned.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":120000,"gcdMs":1500,"cost":{"resource":"mana","amount":25,"percentage":true},"effects":[{"kind":"shield","coefficient":1,"durationMs":15000},{"kind":"utility","action":"class-scrLine truncated
|
||||
@@ -1 +1 @@
|
||||
{"schemaVersion":1,"classId":"primalist","abilities":[{"id":"coa-tree-29362","classId":"primalist","dbcSpellId":800094,"name":"Bearskin","unlockLevel":1,"icon":"/assets/ui/spells/spell_druid_primaltenacity.png","sigil":"BE","description":"Reduces all damage taken by -60% and reduces the duration of root, stun, and incapacitate effects on you by -30% for 10 seconds.","target":"hostile","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":300000,"gcdMs":1500,"cost":{"resource":"mana","amount":24,"percentage":true},"effects":[{"kind":"status","status":"sleep","durationMs":10000},{"kind":"apply-aura","aura":{"id":"coa:800094:effect:0:aura:87","name":"Bearskin (Aura 87)","disposition":"buff","durationMs":10000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic","modifiers":[{"kind":"damage","direction":"taken","operation":"percent","value":-0.6}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":87,"miscValue":127,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:800094:effect:1:aura:232","name":"Bearskin (Aura 232)","disposition":"buff","durationMs":10000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic"},"recipient":"caster","sourceEffectType":6,"sourceAuraType":232,"miscValue":5,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:800094:effect:2:aura:232","name":"Bearskin (Aura 232)","disposition":"buff","durationMs":10000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic"},"recipient":"caster","sourceEffectType":6,"sourceAuraType":232,"miscValue":12,"triggerSpellId":0}],"passive":false,"source":"coa-tree","ranks":[{"rank":1,"spellId":800094,"level":1,"description":"Reduces all damage taken by -60% and reduces the duration of root, stun, and incapacitate effects on you by -30% for 10 seconds.","castMode":"instant","castTimeMs":0,"cooldownMs":300000,"gcdMs":1500,"cost":{"resource":"mana","amount":24,"percentage":true},"effects":[{"kind":"status","status":"sleep","durationMs":10000},{"kind":"apply-aura","aura":{"id":"coa:800094:effect:0:aura:87","name":"Bearskin (Aura 87)","disposition":"buff","durationMs":10000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic","modifiers":[{"kind":"damage","direction":"taken","operation":"percent","value":-0.6}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":87,"miscValue":127,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:800094:effect:1:aura:232","name":"Bearskin (Aura 232)","disposition":"buff","durationMs":10000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic"},"recipient":"caster","sourceEffectType":6,"sourceAuraType":232,"miscValue":5,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:800094:effect:2:aura:232","name":"Bearskin (Aura 232)","disposition":"buff","durationMs":10000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic"},"recipient":"caster","sourceEffectType":6,"sourceAuraType":232,"miscValue":12,"triggerSpellId":0}]}],"talentEntryId":29362},{"id":"coa-tree-29252","classId":"primalist","dbcSpellId":500692,"name":"Boulder Dash","unlockLevel":1,"icon":"/assets/ui/spells/custom_elementl(earth)_b_01_border.png","sigil":"BD","description":"Raise a boulder out of the ground and encase yourself within it, preventing you from attacking or casting spells. For 10 seconds, you roll forward at 70% increased movement speed, dealing 1 + 50% AP + 100% SP Physical damage to enemies you roll over, and you are immune to crowd control effects.","target":"hostile","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":180000,"gcdMs":1250,"cost":{"resource":"mana","amount":35,"percentage":true},"effects":[{"kind":"damage","coefficient":1},{"kind":"apply-aura","aura":{"id":"coa:500692:effect:0:aura:23","name":"Boulder Dash (Aura 23)","disposition":"buff","durationMs":10000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic","procs":[{"id":"coa:500692:proc:0:500693","triggers":["periodic"],"chance":1,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":500693}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":23,"miscValue":0,"triggerSpellId":500693},{"kind":"apply-aura","aura":{"id":"coa:500692:effect:1:aura:31","name":"Boulder Dash (Aura 31)","disposition":"buff","durationMs":10000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic","modifiers":[{"kind":"stat","stat":"movement-speed","operation":"percent","value":0.7}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":31,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:500692:effect:2:aura:241","name":"Boulder Dash (Aura 241)","disposition":"buff","durationMs":10000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic"},"recipient":"caster","sourceEffectType":6,"sourceAuraType":241,"miscValue":0,"triggerSpellId":0}],"passive":false,"source":"coa-tree","ranks":[{"rank":1,"spellId":500692,"level":1,"description":"Raise a boulder out of the ground aLine truncated
|
||||
{"schemaVersion":1,"classId":"primalist","abilities":[{"id":"coa-tree-29362","classId":"primalist","dbcSpellId":800094,"name":"Bearskin","unlockLevel":1,"icon":"/assets/ui/spells/spell_druid_primaltenacity.png","sigil":"BE","description":"Reduces all damage taken by -60% and reduces the duration of root, stun, and incapacitate effects on you by -30% for 10 seconds.","target":"hostile","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":300000,"gcdMs":1500,"cost":{"resource":"mana","amount":24,"percentage":true},"effects":[{"kind":"status","status":"sleep","durationMs":10000},{"kind":"apply-aura","aura":{"id":"coa:800094:effect:0:aura:87","name":"Bearskin (Aura 87)","disposition":"buff","durationMs":10000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic","modifiers":[{"kind":"damage","direction":"taken","operation":"percent","value":-0.6}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":87,"miscValue":127,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:800094:effect:1:aura:232","name":"Bearskin (Aura 232)","disposition":"buff","durationMs":10000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic"},"recipient":"caster","sourceEffectType":6,"sourceAuraType":232,"miscValue":5,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:800094:effect:2:aura:232","name":"Bearskin (Aura 232)","disposition":"buff","durationMs":10000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic"},"recipient":"caster","sourceEffectType":6,"sourceAuraType":232,"miscValue":12,"triggerSpellId":0}],"passive":false,"source":"coa-tree","ranks":[{"rank":1,"spellId":800094,"level":1,"description":"Reduces all damage taken by -60% and reduces the duration of root, stun, and incapacitate effects on you by -30% for 10 seconds.","castMode":"instant","castTimeMs":0,"cooldownMs":300000,"gcdMs":1500,"cost":{"resource":"mana","amount":24,"percentage":true},"effects":[{"kind":"status","status":"sleep","durationMs":10000},{"kind":"apply-aura","aura":{"id":"coa:800094:effect:0:aura:87","name":"Bearskin (Aura 87)","disposition":"buff","durationMs":10000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic","modifiers":[{"kind":"damage","direction":"taken","operation":"percent","value":-0.6}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":87,"miscValue":127,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:800094:effect:1:aura:232","name":"Bearskin (Aura 232)","disposition":"buff","durationMs":10000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic"},"recipient":"caster","sourceEffectType":6,"sourceAuraType":232,"miscValue":5,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:800094:effect:2:aura:232","name":"Bearskin (Aura 232)","disposition":"buff","durationMs":10000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic"},"recipient":"caster","sourceEffectType":6,"sourceAuraType":232,"miscValue":12,"triggerSpellId":0}]}],"talentEntryId":29362},{"id":"coa-tree-29252","classId":"primalist","dbcSpellId":500692,"name":"Boulder Dash","unlockLevel":1,"icon":"/assets/ui/spells/custom_elementl(earth)_b_01_border.png","sigil":"BD","description":"Raise a boulder out of the ground and encase yourself within it, preventing you from attacking or casting spells. For 10 seconds, you roll forward at 70% increased movement speed, dealing 1 + 50% AP + 100% SP Physical damage to enemies you roll over, and you are immune to crowd control effects.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":180000,"gcdMs":1250,"cost":{"resource":"mana","amount":35,"percentage":true},"effects":[{"kind":"apply-aura","aura":{"id":"coa:500692:effect:0:aura:23","name":"Boulder Dash (Aura 23)","disposition":"buff","durationMs":10000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic","procs":[{"id":"coa:500692:proc:0:500693","triggers":["periodic"],"chance":1,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":500693}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":23,"miscValue":0,"triggerSpellId":500693},{"kind":"apply-aura","aura":{"id":"coa:500692:effect:1:aura:31","name":"Boulder Dash (Aura 31)","disposition":"buff","durationMs":10000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic","modifiers":[{"kind":"stat","stat":"movement-speed","operation":"percent","value":0.7}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":31,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:500692:effect:2:aura:241","name":"Boulder Dash (Aura 241)","disposition":"buff","durationMs":10000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic"},"recipient":"caster","sourceEffectType":6,"sourceAuraType":241,"miscValue":0,"triggerSpellId":0}],"passive":false,"source":"coa-tree","ranks":[{"rank":1,"spellId":500692,"level":1,"description":"Raise a boulder out of the ground and encase yourself within it, preventLine truncated
|
||||
@@ -1 +1 @@
|
||||
{"schemaVersion":1,"classId":"pyromancer","abilities":[{"id":"coa-tree-33843","classId":"pyromancer","dbcSpellId":300921,"name":"Al Dente","unlockLevel":1,"icon":"/assets/ui/spells/achievement_cooking_masteroftheoven.png","sigil":"AD","description":"Targets affected by Gaze of Ysera now take 15% increased Fire damage.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:300921:effect:0:aura:107","name":"Al Dente (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":0.15}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":23,"triggerSpellId":0}],"passive":true,"source":"coa-tree","ranks":[{"rank":1,"spellId":300921,"level":1,"description":"Targets affected by Gaze of Ysera now take 15% increased Fire damage.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:300921:effect:0:aura:107","name":"Al Dente (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":0.15}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":23,"triggerSpellId":0}]}],"talentEntryId":33843},{"id":"coa-tree-33882","classId":"pyromancer","dbcSpellId":504397,"name":"Blessing of the Firelands","unlockLevel":1,"icon":"/assets/ui/spells/nhi_fire_soul_border.png","sigil":"BO","description":"Each stack of Heat now increases the damage dealt by Dragon Leap by 6%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:504397:effect:0:aura:107","name":"Blessing of the Firelands (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":0.06}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":12,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:504397:effect:1:aura:107","name":"Blessing of the Firelands (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":0.05}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":23,"triggerSpellId":0}],"passive":true,"source":"coa-tree","ranks":[{"rank":1,"spellId":504397,"level":1,"description":"Each stack of Heat now increases the damage dealt by Dragon Leap by 6%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:504397:effect:0:aura:107","name":"Blessing of the Firelands (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":0.06}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":12,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:504397:effect:1:aura:107","name":"Blessing of the Firelands (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":0.05}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":23,"triggerSpellId":0}]}],"talentEntryId":33882},{"id":"coa-tree-32499","classId":"pyromancer","dbcSpellId":300759,"name":"Bright Flames","unlockLevel":1,"icon":"/assets/ui/spells/spell_frost_fireresistancetotem.png","sigil":"BF","description":"Increases the amount absorbed by Inferno Barrier by 10% and the damage it deals by 15%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:300759:effect:0:aura:317","name":"Bright Flames (Aura 317)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh"},"recipient":"caster","sourceEffectType":6,"sourceAuraType":317,"miscValue":127,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:300759:effect:1:aura:108","name":"Bright Flames (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":0.15}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":0,"triggerSpellId":0}],"passive":true,"source":"coa-tree","ranks":[{"rank":1,"spellId":300759,"level":1,"description":"Increases the amount absorbed by Inferno Barrier by 10% and the damage it dLine truncated
|
||||
{"schemaVersion":1,"classId":"pyromancer","abilities":[{"id":"coa-tree-33843","classId":"pyromancer","dbcSpellId":300921,"name":"Al Dente","unlockLevel":1,"icon":"/assets/ui/spells/achievement_cooking_masteroftheoven.png","sigil":"AD","description":"Targets affected by Gaze of Ysera now take 15% increased Fire damage.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:300921:effect:0:aura:107","name":"Al Dente (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":0.15}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":23,"triggerSpellId":0}],"passive":true,"source":"coa-tree","ranks":[{"rank":1,"spellId":300921,"level":1,"description":"Targets affected by Gaze of Ysera now take 15% increased Fire damage.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:300921:effect:0:aura:107","name":"Al Dente (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":0.15}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":23,"triggerSpellId":0}]}],"talentEntryId":33843},{"id":"coa-tree-33882","classId":"pyromancer","dbcSpellId":504397,"name":"Blessing of the Firelands","unlockLevel":1,"icon":"/assets/ui/spells/nhi_fire_soul_border.png","sigil":"BO","description":"Each stack of Heat now increases the damage dealt by Dragon Leap by 6%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:504397:effect:0:aura:107","name":"Blessing of the Firelands (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":0.06}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":12,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:504397:effect:1:aura:107","name":"Blessing of the Firelands (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":0.05}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":23,"triggerSpellId":0}],"passive":true,"source":"coa-tree","ranks":[{"rank":1,"spellId":504397,"level":1,"description":"Each stack of Heat now increases the damage dealt by Dragon Leap by 6%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:504397:effect:0:aura:107","name":"Blessing of the Firelands (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":0.06}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":12,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:504397:effect:1:aura:107","name":"Blessing of the Firelands (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":0.05}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":23,"triggerSpellId":0}]}],"talentEntryId":33882},{"id":"coa-tree-32499","classId":"pyromancer","dbcSpellId":300759,"name":"Bright Flames","unlockLevel":1,"icon":"/assets/ui/spells/spell_frost_fireresistancetotem.png","sigil":"BF","description":"Increases the amount absorbed by Inferno Barrier by 10% and the damage it deals by 15%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:300759:effect:0:aura:317","name":"Bright Flames (Aura 317)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh"},"recipient":"caster","sourceEffectType":6,"sourceAuraType":317,"miscValue":127,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:300759:effect:1:aura:108","name":"Bright Flames (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":0.15}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":0,"triggerSpellId":0}],"passive":true,"source":"coa-tree","ranks":[{"rank":1,"spellId":300759,"level":1,"description":"Increases the amount absorbed by Inferno Barrier by 10% and the damage it dLine truncated
|
||||
@@ -1 +1 @@
|
||||
{"schemaVersion":1,"classId":"ranger","abilities":[{"id":"coa-21-agile-none-560796","classId":"ranger","dbcSpellId":560796,"name":"Agile","unlockLevel":1,"icon":"/assets/ui/spells/inv_sword_38.png","sigil":"AG","description":"AgileRank 2Increases the damage of Wild Strike, Whipvine Arrow, and Ravage by 10%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:560796:effect:0:aura:108","name":"Agile (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":0.1}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:560796:effect:1:aura:108","name":"Agile (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":0.1}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":22,"triggerSpellId":0}],"passive":true,"source":"coa-progression","ranks":[{"rank":2,"spellId":560796,"level":1,"description":"AgileRank 2Increases the damage of Wild Strike, Whipvine Arrow, and Ravage by 10%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:560796:effect:0:aura:108","name":"Agile (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":0.1}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:560796:effect:1:aura:108","name":"Agile (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":0.1}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":22,"triggerSpellId":0}]}]},{"id":"coa-21-archery-training-76-705036","classId":"ranger","dbcSpellId":705036,"name":"Archery Training","unlockLevel":1,"icon":"/assets/ui/spells/inv_bow_1h_draenorcrafted_d_01_a_horde.png","sigil":"AT","description":"Archery TrainingRank 1Increases the damage of Skullpiercer by 3% and your critical strike chance by 1%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705036:effect:0:aura:290","name":"Archery Training (Aura 290)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh"},"recipient":"caster","sourceEffectType":6,"sourceAuraType":290,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705036:effect:1:aura:108","name":"Archery Training (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":0.03}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":0,"triggerSpellId":0}],"passive":true,"source":"coa-progression","ranks":[{"rank":1,"spellId":705036,"level":1,"description":"Archery TrainingRank 1Increases the damage of Skullpiercer by 3% and your critical strike chance by 1%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705036:effect:0:aura:290","name":"Archery Training (Aura 290)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh"},"recipient":"caster","sourceEffectType":6,"sourceAuraType":290,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705036:effect:1:aura:108","name":"Archery Training (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":0.03}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":0,"triggerSpellId":0}]},{"rank":2,"spellId":705037,"level":1,"description":"Archery TrainingRank 2Increases the damage of Skullpiercer by 6% and your critical strike chance by 2%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705037:effect:0:aura:290","name":"Archery Training (Aura 290)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh"},"recipient":"caster","sourceEffectType":6,"sourceAuraType":290,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705037:effect:1:aura:108","name":"Archery Training (Aura 108)","disposition":"buff","dLine truncated
|
||||
{"schemaVersion":1,"classId":"ranger","abilities":[{"id":"coa-21-agile-none-560796","classId":"ranger","dbcSpellId":560796,"name":"Agile","unlockLevel":1,"icon":"/assets/ui/spells/inv_sword_38.png","sigil":"AG","description":"AgileRank 2Increases the damage of Wild Strike, Whipvine Arrow, and Ravage by 10%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:560796:effect:0:aura:108","name":"Agile (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":0.1}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:560796:effect:1:aura:108","name":"Agile (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":0.1}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":22,"triggerSpellId":0}],"passive":true,"source":"coa-progression","ranks":[{"rank":2,"spellId":560796,"level":1,"description":"AgileRank 2Increases the damage of Wild Strike, Whipvine Arrow, and Ravage by 10%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:560796:effect:0:aura:108","name":"Agile (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":0.1}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:560796:effect:1:aura:108","name":"Agile (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":0.1}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":22,"triggerSpellId":0}]}]},{"id":"coa-21-archery-training-76-705036","classId":"ranger","dbcSpellId":705036,"name":"Archery Training","unlockLevel":1,"icon":"/assets/ui/spells/inv_bow_1h_draenorcrafted_d_01_a_horde.png","sigil":"AT","description":"Archery TrainingRank 1Increases the damage of Skullpiercer by 3% and your critical strike chance by 1%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705036:effect:0:aura:290","name":"Archery Training (Aura 290)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh"},"recipient":"caster","sourceEffectType":6,"sourceAuraType":290,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705036:effect:1:aura:108","name":"Archery Training (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":0.03}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":0,"triggerSpellId":0}],"passive":true,"source":"coa-progression","ranks":[{"rank":1,"spellId":705036,"level":1,"description":"Archery TrainingRank 1Increases the damage of Skullpiercer by 3% and your critical strike chance by 1%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705036:effect:0:aura:290","name":"Archery Training (Aura 290)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh"},"recipient":"caster","sourceEffectType":6,"sourceAuraType":290,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705036:effect:1:aura:108","name":"Archery Training (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":0.03}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":0,"triggerSpellId":0}]},{"rank":2,"spellId":705037,"level":1,"description":"Archery TrainingRank 2Increases the damage of Skullpiercer by 6% and your critical strike chance by 2%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705037:effect:0:aura:290","name":"Archery Training (Aura 290)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh"},"recipient":"caster","sourceEffectType":6,"sourceAuraType":290,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705037:effect:1:aura:108","name":"Archery Training (Aura 108)","disposition":"buff","dLine truncated
|
||||
@@ -1 +1 @@
|
||||
{"schemaVersion":1,"classId":"reaper","abilities":[{"id":"coa-tree-6708","classId":"reaper","dbcSpellId":706790,"name":"Backswing","unlockLevel":1,"icon":"/assets/ui/spells/novart_magicspell_(32)_border.png","sigil":"BA","description":"Increases the Runic Power generated by Reap by 5.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:706790:effect:0:aura:107","name":"Backswing (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":0.5}]},"sourceEffectType":6,"sourceAuraType":107,"miscValue":3,"triggerSpellId":0}],"passive":true,"source":"coa-tree","ranks":[{"rank":1,"spellId":706790,"level":1,"description":"Increases the Runic Power generated by Reap by 5.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:706790:effect:0:aura:107","name":"Backswing (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":0.5}]},"sourceEffectType":6,"sourceAuraType":107,"miscValue":3,"triggerSpellId":0}]}],"talentEntryId":6708},{"id":"coa-tree-34500","classId":"reaper","dbcSpellId":560412,"name":"Between Shadows","unlockLevel":1,"icon":"/assets/ui/spells/5_shadowworld_border.png","sigil":"BS","description":"Casting Spectre Stride now generates 1 Reaped Soul.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:560412:effect:0:aura:42","name":"Between Shadows (Aura 42)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","procs":[{"id":"coa:560412:proc:0:504390","triggers":["cast"],"chance":1,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":504390}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":42,"miscValue":0,"triggerSpellId":504390}],"passive":true,"source":"coa-tree","ranks":[{"rank":1,"spellId":560412,"level":1,"description":"Casting Spectre Stride now generates 1 Reaped Soul.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:560412:effect:0:aura:42","name":"Between Shadows (Aura 42)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","procs":[{"id":"coa:560412:proc:0:504390","triggers":["cast"],"chance":1,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":504390}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":42,"miscValue":0,"triggerSpellId":504390}]}],"talentEntryId":34500},{"id":"coa-tree-31289","classId":"reaper","dbcSpellId":680337,"name":"Bolstered Form","unlockLevel":1,"icon":"/assets/ui/spells/achievement_raid_torghast_shadowscourge_prisonofnerzhul.png","sigil":"BF","description":"Assume a bolstered form for 15 seconds, generating 30 Runic Power, increasing your Armor and your parry chance by 30%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":60000,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:680337:effect:0:aura:101","name":"Bolstered Form (Aura 101)","disposition":"buff","durationMs":15000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic","modifiers":[{"kind":"stat","stat":"resistance:physical","operation":"percent","value":0.25}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":101,"miscValue":1,"triggerSpellId":0},{"kind":"trigger-spell","spellId":359505},{"kind":"resource","amount":300},{"kind":"apply-aura","aura":{"id":"coa:680337:effect:2:aura:47","name":"Bolstered Form (Aura 47)","disposition":"buff","durationMs":15000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic","modifiers":[{"kind":"stat","stat":"parry","operation":"percent","value":0.25}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":47,"miscValue":127,"triggerSpellId":0}],"passive":false,"source":"coa-tree","ranks":[{"rank":1,"spellId":680337,"level":1,"description":"Assume a bolstered form for 15 seconds, generating 30 Runic Power, increasing your Armor and your parry chance by 30%.","castMode":"instant","castTimeMs":0,"cooldownMs":60000,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:680337:effect:0:aura:101","name":"Bolstered Form (Aura 101)","disposition":"buff","durationMs":15000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic","modifiers":[{"kind":"stat","stat":"resistance:physical","operation":"percent","value":0.25}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":1Line truncated
|
||||
{"schemaVersion":1,"classId":"reaper","abilities":[{"id":"coa-tree-6708","classId":"reaper","dbcSpellId":706790,"name":"Backswing","unlockLevel":1,"icon":"/assets/ui/spells/novart_magicspell_(32)_border.png","sigil":"BA","description":"Increases the Runic Power generated by Reap by 5.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:706790:effect:0:aura:107","name":"Backswing (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":0.5}]},"sourceEffectType":6,"sourceAuraType":107,"miscValue":3,"triggerSpellId":0}],"passive":true,"source":"coa-tree","ranks":[{"rank":1,"spellId":706790,"level":1,"description":"Increases the Runic Power generated by Reap by 5.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:706790:effect:0:aura:107","name":"Backswing (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":0.5}]},"sourceEffectType":6,"sourceAuraType":107,"miscValue":3,"triggerSpellId":0}]}],"talentEntryId":6708},{"id":"coa-tree-34500","classId":"reaper","dbcSpellId":560412,"name":"Between Shadows","unlockLevel":1,"icon":"/assets/ui/spells/5_shadowworld_border.png","sigil":"BS","description":"Casting Spectre Stride now generates 1 Reaped Soul.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:560412:effect:0:aura:42","name":"Between Shadows (Aura 42)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","procs":[{"id":"coa:560412:proc:0:504390","triggers":["cast"],"chance":1,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":504390}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":42,"miscValue":0,"triggerSpellId":504390}],"passive":true,"source":"coa-tree","ranks":[{"rank":1,"spellId":560412,"level":1,"description":"Casting Spectre Stride now generates 1 Reaped Soul.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:560412:effect:0:aura:42","name":"Between Shadows (Aura 42)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","procs":[{"id":"coa:560412:proc:0:504390","triggers":["cast"],"chance":1,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":504390}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":42,"miscValue":0,"triggerSpellId":504390}]}],"talentEntryId":34500},{"id":"coa-tree-31289","classId":"reaper","dbcSpellId":680337,"name":"Bolstered Form","unlockLevel":1,"icon":"/assets/ui/spells/achievement_raid_torghast_shadowscourge_prisonofnerzhul.png","sigil":"BF","description":"Assume a bolstered form for 15 seconds, generating 30 Runic Power, increasing your Armor and your parry chance by 30%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":60000,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:680337:effect:0:aura:101","name":"Bolstered Form (Aura 101)","disposition":"buff","durationMs":15000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic","modifiers":[{"kind":"stat","stat":"resistance:physical","operation":"percent","value":0.25}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":101,"miscValue":1,"triggerSpellId":0},{"kind":"trigger-spell","spellId":359505,"expanded":true},{"kind":"resource","amount":300},{"kind":"apply-aura","aura":{"id":"coa:680337:effect:2:aura:47","name":"Bolstered Form (Aura 47)","disposition":"buff","durationMs":15000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic","modifiers":[{"kind":"stat","stat":"parry","operation":"percent","value":0.25}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":47,"miscValue":127,"triggerSpellId":0}],"passive":false,"source":"coa-tree","ranks":[{"rank":1,"spellId":680337,"level":1,"description":"Assume a bolstered form for 15 seconds, generating 30 Runic Power, increasing your Armor and your parry chance by 30%.","castMode":"instant","castTimeMs":0,"cooldownMs":60000,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:680337:effect:0:aura:101","name":"Bolstered Form (Aura 101)","disposition":"buff","durationMs":15000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic","modifiers":[{"kind":"stat","stat":"resistance:physical","operation":"percent","value":0.25}]},"recipient":"caster","sourceEffectType":6,"sLine truncated
|
||||
@@ -1 +1 @@
|
||||
{"schemaVersion":1,"classId":"runemaster","abilities":[{"id":"coa-32-ancient-carvings-117-705616","classId":"runemaster","dbcSpellId":705616,"name":"Ancient Carvings","unlockLevel":1,"icon":"/assets/ui/spells/inv_relics_6orunestone_ogremissive.png","sigil":"AC","description":"Ancient CarvingsRank 2InstantReduces the cooldown of Words by 20%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705616:effect:0:aura:108","name":"Ancient Carvings (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":-0.2}]},"recipient":"caster","sourceEffectType":3,"sourceAuraType":108,"miscValue":11,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705616:effect:1:aura:4","name":"Ancient Carvings (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705616:effect:2:aura:4","name":"Ancient Carvings (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":34,"triggerSpellId":0}],"passive":true,"source":"coa-progression","ranks":[{"rank":2,"spellId":705616,"level":1,"description":"Ancient CarvingsRank 2InstantReduces the cooldown of Words by 20%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705616:effect:0:aura:108","name":"Ancient Carvings (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":-0.2}]},"recipient":"caster","sourceEffectType":3,"sourceAuraType":108,"miscValue":11,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705616:effect:1:aura:4","name":"Ancient Carvings (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705616:effect:2:aura:4","name":"Ancient Carvings (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":34,"triggerSpellId":0}]}]},{"id":"coa-tree-29226","classId":"runemaster","dbcSpellId":806797,"name":"Ancient Teachings","unlockLevel":1,"icon":"/assets/ui/spells/ability_mage_potentspirit.png","sigil":"AT","description":"Increases the effectiveness of Etching of the Dextrous by 20%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:806797:effect:0:aura:216","name":"Ancient Teachings (Aura 216)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"haste","operation":"percent","value":0}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":216,"miscValue":126,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:806797:effect:1:aura:192","name":"Ancient Teachings (Aura 192)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"haste","operation":"percent","value":0}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":192,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:806797:effect:2:aura:108","name":"Ancient Teachings (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":0.2}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":8,"triggerSpellId":0}],"passive":true,"source":"coa-tree","ranks":[{"rank":1,"spellId":806797,"level":1,"description":"Increases the effectiveness of Etching of the Dextrous by 20%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:806797:effect:0:aura:216","name":"Ancient Teachings (Aura 216)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"haste","operation":"percent","value":0}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":216,"miscValue":126,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:806797:effect:1:aura:192","name":"Ancient Teachings (Aura 192)","disposition":Line truncated
|
||||
{"schemaVersion":1,"classId":"runemaster","abilities":[{"id":"coa-32-ancient-carvings-117-705616","classId":"runemaster","dbcSpellId":705616,"name":"Ancient Carvings","unlockLevel":1,"icon":"/assets/ui/spells/inv_relics_6orunestone_ogremissive.png","sigil":"AC","description":"Ancient CarvingsRank 2InstantReduces the cooldown of Words by 20%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705616:effect:0:aura:108","name":"Ancient Carvings (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":-0.2}]},"recipient":"caster","sourceEffectType":3,"sourceAuraType":108,"miscValue":11,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705616:effect:1:aura:4","name":"Ancient Carvings (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705616:effect:2:aura:4","name":"Ancient Carvings (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":34,"triggerSpellId":0}],"passive":true,"source":"coa-progression","ranks":[{"rank":2,"spellId":705616,"level":1,"description":"Ancient CarvingsRank 2InstantReduces the cooldown of Words by 20%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705616:effect:0:aura:108","name":"Ancient Carvings (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":-0.2}]},"recipient":"caster","sourceEffectType":3,"sourceAuraType":108,"miscValue":11,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705616:effect:1:aura:4","name":"Ancient Carvings (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705616:effect:2:aura:4","name":"Ancient Carvings (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":34,"triggerSpellId":0}]}]},{"id":"coa-tree-29226","classId":"runemaster","dbcSpellId":806797,"name":"Ancient Teachings","unlockLevel":1,"icon":"/assets/ui/spells/ability_mage_potentspirit.png","sigil":"AT","description":"Increases the effectiveness of Etching of the Dextrous by 20%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:806797:effect:0:aura:216","name":"Ancient Teachings (Aura 216)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"haste","operation":"percent","value":0}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":216,"miscValue":126,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:806797:effect:1:aura:192","name":"Ancient Teachings (Aura 192)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"haste","operation":"percent","value":0}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":192,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:806797:effect:2:aura:108","name":"Ancient Teachings (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":0.2}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":8,"triggerSpellId":0}],"passive":true,"source":"coa-tree","ranks":[{"rank":1,"spellId":806797,"level":1,"description":"Increases the effectiveness of Etching of the Dextrous by 20%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:806797:effect:0:aura:216","name":"Ancient Teachings (Aura 216)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"haste","operation":"percent","value":0}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":216,"miscValue":126,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:806797:effect:1:aura:192","name":"Ancient Teachings (Aura 192)","disposition":Line truncated
|
||||
@@ -1 +1 @@
|
||||
{"schemaVersion":1,"classId":"starcaller","abilities":[{"id":"coa-26-arrows-of-starlight-93-704733","classId":"starcaller","dbcSpellId":704733,"name":"Arrows of Starlight","unlockLevel":1,"icon":"/assets/ui/spells/ability_ironmaidens_ironshot.png","sigil":"AO","description":"Arrows of StarlightRank 1Increases the damage of Moon Arrow by 20%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:704733:effect:0:aura:108","name":"Arrows of Starlight (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":0.2}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":0,"triggerSpellId":0}],"passive":true,"source":"coa-progression","ranks":[{"rank":1,"spellId":704733,"level":1,"description":"Arrows of StarlightRank 1Increases the damage of Moon Arrow by 20%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:704733:effect:0:aura:108","name":"Arrows of Starlight (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":0.2}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":0,"triggerSpellId":0}]}]},{"id":"coa-tree-30838","classId":"starcaller","dbcSpellId":806155,"name":"Astral Aegis","unlockLevel":1,"icon":"/assets/ui/spells/ability_bastion_monk.png","sigil":"AA","description":"Drains 30% Maximum Mana For 10 seconds, direct Magic damage taken will silence the attacker for 1 seconds and deal 57 + 18% arcane SP Arcane damage to them.","target":"hostile","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":30000,"gcdMs":1500,"cost":{"resource":"mana","amount":19,"percentage":true},"effects":[{"kind":"damage","coefficient":1},{"kind":"apply-aura","aura":{"id":"coa:806155:effect:0:aura:87","name":"Astral Aegis (Aura 87)","disposition":"buff","durationMs":10000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic","modifiers":[{"kind":"damage","direction":"taken","operation":"percent","value":0}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":87,"miscValue":127,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:806155:effect:1:aura:42","name":"Astral Aegis (Aura 42)","disposition":"buff","durationMs":10000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic","procs":[{"id":"coa:806155:proc:1:806453","triggers":["damage-taken","hit"],"chance":1,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":806453}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":42,"miscValue":0,"triggerSpellId":806453}],"passive":false,"source":"coa-tree","ranks":[{"rank":1,"spellId":806155,"level":1,"description":"Drains 30% Maximum Mana For 10 seconds, direct Magic damage taken will silence the attacker for 1 seconds and deal 57 + 18% arcane SP Arcane damage to them.","castMode":"instant","castTimeMs":0,"cooldownMs":30000,"gcdMs":1500,"cost":{"resource":"mana","amount":19,"percentage":true},"effects":[{"kind":"damage","coefficient":1},{"kind":"apply-aura","aura":{"id":"coa:806155:effect:0:aura:87","name":"Astral Aegis (Aura 87)","disposition":"buff","durationMs":10000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic","modifiers":[{"kind":"damage","direction":"taken","operation":"percent","value":0}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":87,"miscValue":127,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:806155:effect:1:aura:42","name":"Astral Aegis (Aura 42)","disposition":"buff","durationMs":10000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic","procs":[{"id":"coa:806155:proc:1:806453","triggers":["damage-taken","hit"],"chance":1,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":806453}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":42,"miscValue":0,"triggerSpellId":806453}]}],"talentEntryId":30838},{"id":"starcaller-astral-flare","classId":"starcaller","dbcSpellId":800499,"name":"Astral Flare","unlockLevel":1,"icon":"/assets/ui/spells/spell_shadow_manaburn.png","sigil":"AF","description":"Astral FlareRank 12 sec castYou call an Astral Flare down from the sky, dealing 1 Arcane Damage to an enemy and regenerating 5% of your Maximum Mana.","target":"hostile","range":{"min":0,"max":40},"castMode":"cast","castTimeMs":2000,"cooldownMs":0,"gcdMs":1250,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"trigger-spell","spellId":800507,"withValue":10},{"kind":"damage","coefficient":0.08,"basePoints":1,"dieSides":1,"pointsPerLevel":0,"bonusPowerCoefficient":0,"sourceLevel":6},{"kind":"trigger-spell","spellId":804096,"withValLine truncated
|
||||
{"schemaVersion":1,"classId":"starcaller","abilities":[{"id":"coa-26-arrows-of-starlight-93-704733","classId":"starcaller","dbcSpellId":704733,"name":"Arrows of Starlight","unlockLevel":1,"icon":"/assets/ui/spells/ability_ironmaidens_ironshot.png","sigil":"AO","description":"Arrows of StarlightRank 1Increases the damage of Moon Arrow by 20%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:704733:effect:0:aura:108","name":"Arrows of Starlight (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":0.2}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":0,"triggerSpellId":0}],"passive":true,"source":"coa-progression","ranks":[{"rank":1,"spellId":704733,"level":1,"description":"Arrows of StarlightRank 1Increases the damage of Moon Arrow by 20%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:704733:effect:0:aura:108","name":"Arrows of Starlight (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":0.2}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":0,"triggerSpellId":0}]}]},{"id":"coa-tree-30838","classId":"starcaller","dbcSpellId":806155,"name":"Astral Aegis","unlockLevel":1,"icon":"/assets/ui/spells/ability_bastion_monk.png","sigil":"AA","description":"Drains 30% Maximum Mana For 10 seconds, direct Magic damage taken will silence the attacker for 1 seconds and deal 57 + 18% arcane SP Arcane damage to them.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":30000,"gcdMs":1500,"cost":{"resource":"mana","amount":19,"percentage":true},"effects":[{"kind":"apply-aura","aura":{"id":"coa:806155:effect:0:aura:87","name":"Astral Aegis (Aura 87)","disposition":"buff","durationMs":10000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic","modifiers":[{"kind":"damage","direction":"taken","operation":"percent","value":0}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":87,"miscValue":127,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:806155:effect:1:aura:42","name":"Astral Aegis (Aura 42)","disposition":"buff","durationMs":10000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic","procs":[{"id":"coa:806155:proc:1:806453","triggers":["damage-taken","hit"],"chance":1,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":806453}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":42,"miscValue":0,"triggerSpellId":806453}],"passive":false,"source":"coa-tree","ranks":[{"rank":1,"spellId":806155,"level":1,"description":"Drains 30% Maximum Mana For 10 seconds, direct Magic damage taken will silence the attacker for 1 seconds and deal 57 + 18% arcane SP Arcane damage to them.","castMode":"instant","castTimeMs":0,"cooldownMs":30000,"gcdMs":1500,"cost":{"resource":"mana","amount":19,"percentage":true},"effects":[{"kind":"apply-aura","aura":{"id":"coa:806155:effect:0:aura:87","name":"Astral Aegis (Aura 87)","disposition":"buff","durationMs":10000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic","modifiers":[{"kind":"damage","direction":"taken","operation":"percent","value":0}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":87,"miscValue":127,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:806155:effect:1:aura:42","name":"Astral Aegis (Aura 42)","disposition":"buff","durationMs":10000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic","procs":[{"id":"coa:806155:proc:1:806453","triggers":["damage-taken","hit"],"chance":1,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":806453}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":42,"miscValue":0,"triggerSpellId":806453}]}],"talentEntryId":30838},{"id":"starcaller-astral-flare","classId":"starcaller","dbcSpellId":800499,"name":"Astral Flare","unlockLevel":1,"icon":"/assets/ui/spells/spell_shadow_manaburn.png","sigil":"AF","description":"Astral FlareRank 12 sec castYou call an Astral Flare down from the sky, dealing 1 Arcane Damage to an enemy and regenerating 5% of your Maximum Mana.","target":"hostile","range":{"min":0,"max":40},"castMode":"cast","castTimeMs":2000,"cooldownMs":0,"gcdMs":1250,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"trigger-spell","spellId":800507,"expanded":true,"withValue":10},{"kind":"damage","coefficient":0.08,"basePoints":1,"dieSides":1,"pointsPerLevel":0,"bonusPowerCoefficient":0,"sourceLevel":6},{"kind":"trigger-spell","spellId":804096,"expanded":true,"withValue":5},{"kind":"resource","amount":2,"pLine truncated
|
||||
@@ -1 +1 @@
|
||||
{"schemaVersion":1,"classId":"stormbringer","abilities":[{"id":"coa-tree-34916","classId":"stormbringer","dbcSpellId":807462,"name":"Aether Mastery","unlockLevel":1,"icon":"/assets/ui/spells/achievement_zone_netherstorm_01.png","sigil":"AM","description":"Allows 30% of your mana regeneration to continue while casting and increases your critical strike chance by 2%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:807462:effect:0:aura:134","name":"Aether Mastery (Aura 134)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"resource","resource":"mana","property":"regeneration","operation":"percent","value":0.3}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":134,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:807462:effect:1:aura:290","name":"Aether Mastery (Aura 290)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh"},"recipient":"caster","sourceEffectType":6,"sourceAuraType":290,"miscValue":0,"triggerSpellId":0}],"passive":true,"source":"coa-tree","ranks":[{"rank":1,"spellId":807462,"level":1,"description":"Allows 30% of your mana regeneration to continue while casting and increases your critical strike chance by 2%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:807462:effect:0:aura:134","name":"Aether Mastery (Aura 134)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"resource","resource":"mana","property":"regeneration","operation":"percent","value":0.3}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":134,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:807462:effect:1:aura:290","name":"Aether Mastery (Aura 290)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh"},"recipient":"caster","sourceEffectType":6,"sourceAuraType":290,"miscValue":0,"triggerSpellId":0}]},{"rank":2,"spellId":807887,"level":1,"description":"Allows 60% of your mana regeneration to continue while casting and increases your critical strike chance by 4%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:807887:effect:0:aura:134","name":"Aether Mastery (Aura 134)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"resource","resource":"mana","property":"regeneration","operation":"percent","value":0.6}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":134,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:807887:effect:1:aura:290","name":"Aether Mastery (Aura 290)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh"},"recipient":"caster","sourceEffectType":6,"sourceAuraType":290,"miscValue":0,"triggerSpellId":0}]}],"talentEntryId":34916},{"id":"coa-16-aethermancy-62-705690","classId":"stormbringer","dbcSpellId":705690,"name":"Aethermancy","unlockLevel":1,"icon":"/assets/ui/spells/spell_shaman_improvedreincarnation.png","sigil":"AE","description":"AethermancyRank 1Increases the critical strike chance of your Invigorating Surge by 20%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705690:effect:0:aura:107","name":"Aethermancy (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":0.2}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":7,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705690:effect:1:aura:107","name":"Aethermancy (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":1}]},"recipient":"caster","sourceEffectType":3,"sourceAuraType":107,"miscValue":7,"triggerSpellId":0}],"passive":true,"source":"coa-progression","ranks":[{"rank":1,"spellId":705690,"level":1,"description":"AethermancyRank 1Increases the critical strike chance of your Invigorating Surge by 20%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705690:effect:0:aura:107","name":"Aethermancy (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":0.2}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":7,"triggerSpellId":0},{"kinLine truncated
|
||||
{"schemaVersion":1,"classId":"stormbringer","abilities":[{"id":"coa-tree-34916","classId":"stormbringer","dbcSpellId":807462,"name":"Aether Mastery","unlockLevel":1,"icon":"/assets/ui/spells/achievement_zone_netherstorm_01.png","sigil":"AM","description":"Allows 30% of your mana regeneration to continue while casting and increases your critical strike chance by 2%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:807462:effect:0:aura:134","name":"Aether Mastery (Aura 134)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"resource","resource":"mana","property":"regeneration","operation":"percent","value":0.3}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":134,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:807462:effect:1:aura:290","name":"Aether Mastery (Aura 290)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh"},"recipient":"caster","sourceEffectType":6,"sourceAuraType":290,"miscValue":0,"triggerSpellId":0}],"passive":true,"source":"coa-tree","ranks":[{"rank":1,"spellId":807462,"level":1,"description":"Allows 30% of your mana regeneration to continue while casting and increases your critical strike chance by 2%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:807462:effect:0:aura:134","name":"Aether Mastery (Aura 134)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"resource","resource":"mana","property":"regeneration","operation":"percent","value":0.3}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":134,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:807462:effect:1:aura:290","name":"Aether Mastery (Aura 290)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh"},"recipient":"caster","sourceEffectType":6,"sourceAuraType":290,"miscValue":0,"triggerSpellId":0}]},{"rank":2,"spellId":807887,"level":1,"description":"Allows 60% of your mana regeneration to continue while casting and increases your critical strike chance by 4%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:807887:effect:0:aura:134","name":"Aether Mastery (Aura 134)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"resource","resource":"mana","property":"regeneration","operation":"percent","value":0.6}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":134,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:807887:effect:1:aura:290","name":"Aether Mastery (Aura 290)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh"},"recipient":"caster","sourceEffectType":6,"sourceAuraType":290,"miscValue":0,"triggerSpellId":0}]}],"talentEntryId":34916},{"id":"coa-16-aethermancy-62-705690","classId":"stormbringer","dbcSpellId":705690,"name":"Aethermancy","unlockLevel":1,"icon":"/assets/ui/spells/spell_shaman_improvedreincarnation.png","sigil":"AE","description":"AethermancyRank 1Increases the critical strike chance of your Invigorating Surge by 20%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705690:effect:0:aura:107","name":"Aethermancy (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":0.2}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":7,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705690:effect:1:aura:107","name":"Aethermancy (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":1}]},"recipient":"caster","sourceEffectType":3,"sourceAuraType":107,"miscValue":7,"triggerSpellId":0}],"passive":true,"source":"coa-progression","ranks":[{"rank":1,"spellId":705690,"level":1,"description":"AethermancyRank 1Increases the critical strike chance of your Invigorating Surge by 20%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705690:effect:0:aura:107","name":"Aethermancy (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":0.2}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":7,"triggerSpellId":0},{"kinLine truncated
|
||||
@@ -1 +1 @@
|
||||
{"schemaVersion":1,"classId":"sun-cleric","abilities":[{"id":"coa-tree-4048","classId":"sun-cleric","dbcSpellId":92137,"name":"Angelic Presence","unlockLevel":1,"icon":"/assets/ui/spells/ability_paladin_conviction.png","sigil":"AP","description":"Level 10 Passive Avoiding an attack now has a 20% chance to grant you Angelic Presence.Angelic PresenceYour next Illumination within 15 seconds is instant cast and free of cost.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:92137:effect:0:aura:42","name":"Angelic Presence (Aura 42)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","procs":[{"id":"coa:92137:proc:0:800722","triggers":["cast","hit"],"chance":0.2,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":800722}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":42,"miscValue":0,"triggerSpellId":800722},{"kind":"apply-aura","aura":{"id":"coa:92137:effect:1:aura:79","name":"Angelic Presence (Aura 79)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"damage","direction":"dealt","operation":"percent","value":0}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":79,"miscValue":127,"triggerSpellId":0}],"passive":true,"source":"coa-tree","ranks":[{"rank":1,"spellId":92137,"level":1,"description":"Level 10 Passive Avoiding an attack now has a 20% chance to grant you Angelic Presence.Angelic PresenceYour next Illumination within 15 seconds is instant cast and free of cost.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:92137:effect:0:aura:42","name":"Angelic Presence (Aura 42)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","procs":[{"id":"coa:92137:proc:0:800722","triggers":["cast","hit"],"chance":0.2,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":800722}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":42,"miscValue":0,"triggerSpellId":800722},{"kind":"apply-aura","aura":{"id":"coa:92137:effect:1:aura:79","name":"Angelic Presence (Aura 79)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"damage","direction":"dealt","operation":"percent","value":0}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":79,"miscValue":127,"triggerSpellId":0}]}],"talentEntryId":4048},{"id":"coa-27-bastion-of-vengeance-97-704925","classId":"sun-cleric","dbcSpellId":704925,"name":"Bastion of Vengeance","unlockLevel":1,"icon":"/assets/ui/spells/inv_shield_1h_artifactnorgannon_d_06.png","sigil":"BO","description":"Bastion of VengeanceRank 1Increases your block chance by 2% and gives successful blocks a 50% chance to reduce the remaining cooldown of Vengeful Slam by 0.01 sec.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:704925:effect:0:aura:42","name":"Bastion of Vengeance (Aura 42)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","procs":[{"id":"coa:704925:proc:0:704926","triggers":["block"],"chance":0.5,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":704926}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":42,"miscValue":0,"triggerSpellId":704926},{"kind":"apply-aura","aura":{"id":"coa:704925:effect:1:aura:51","name":"Bastion of Vengeance (Aura 51)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"block","operation":"percent","value":0.02}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":51,"miscValue":0,"triggerSpellId":0}],"passive":true,"source":"coa-progression","ranks":[{"rank":1,"spellId":704925,"level":1,"description":"Bastion of VengeanceRank 1Increases your block chance by 2% and gives successful blocks a 50% chance to reduce the remaining cooldown of Vengeful Slam by 0.01 sec.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:704925:effect:0:aura:42","name":"Bastion of Vengeance (Aura 42)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","procs":[{"id":"coa:704925:proc:0:704926","triggers":["block"],"chance":0.5,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":704926}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":42,"miscValue":0,"triggerSpellId":704926},{"kind":"apply-aura","aura":{"id":"coa:704925:effect:1:aura:51","name":"Bastion of Vengeance (Aura 51)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","statLine truncated
|
||||
{"schemaVersion":1,"classId":"sun-cleric","abilities":[{"id":"coa-tree-4048","classId":"sun-cleric","dbcSpellId":92137,"name":"Angelic Presence","unlockLevel":1,"icon":"/assets/ui/spells/ability_paladin_conviction.png","sigil":"AP","description":"Level 10 Passive Avoiding an attack now has a 20% chance to grant you Angelic Presence.Angelic PresenceYour next Illumination within 15 seconds is instant cast and free of cost.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:92137:effect:0:aura:42","name":"Angelic Presence (Aura 42)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","procs":[{"id":"coa:92137:proc:0:800722","triggers":["cast","hit"],"chance":0.2,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":800722}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":42,"miscValue":0,"triggerSpellId":800722},{"kind":"apply-aura","aura":{"id":"coa:92137:effect:1:aura:79","name":"Angelic Presence (Aura 79)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"damage","direction":"dealt","operation":"percent","value":0}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":79,"miscValue":127,"triggerSpellId":0}],"passive":true,"source":"coa-tree","ranks":[{"rank":1,"spellId":92137,"level":1,"description":"Level 10 Passive Avoiding an attack now has a 20% chance to grant you Angelic Presence.Angelic PresenceYour next Illumination within 15 seconds is instant cast and free of cost.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:92137:effect:0:aura:42","name":"Angelic Presence (Aura 42)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","procs":[{"id":"coa:92137:proc:0:800722","triggers":["cast","hit"],"chance":0.2,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":800722}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":42,"miscValue":0,"triggerSpellId":800722},{"kind":"apply-aura","aura":{"id":"coa:92137:effect:1:aura:79","name":"Angelic Presence (Aura 79)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"damage","direction":"dealt","operation":"percent","value":0}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":79,"miscValue":127,"triggerSpellId":0}]}],"talentEntryId":4048},{"id":"coa-27-bastion-of-vengeance-97-704925","classId":"sun-cleric","dbcSpellId":704925,"name":"Bastion of Vengeance","unlockLevel":1,"icon":"/assets/ui/spells/inv_shield_1h_artifactnorgannon_d_06.png","sigil":"BO","description":"Bastion of VengeanceRank 1Increases your block chance by 2% and gives successful blocks a 50% chance to reduce the remaining cooldown of Vengeful Slam by 0.01 sec.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:704925:effect:0:aura:42","name":"Bastion of Vengeance (Aura 42)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","procs":[{"id":"coa:704925:proc:0:704926","triggers":["block"],"chance":0.5,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":704926}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":42,"miscValue":0,"triggerSpellId":704926},{"kind":"apply-aura","aura":{"id":"coa:704925:effect:1:aura:51","name":"Bastion of Vengeance (Aura 51)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"block","operation":"percent","value":0.02}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":51,"miscValue":0,"triggerSpellId":0}],"passive":true,"source":"coa-progression","ranks":[{"rank":1,"spellId":704925,"level":1,"description":"Bastion of VengeanceRank 1Increases your block chance by 2% and gives successful blocks a 50% chance to reduce the remaining cooldown of Vengeful Slam by 0.01 sec.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:704925:effect:0:aura:42","name":"Bastion of Vengeance (Aura 42)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","procs":[{"id":"coa:704925:proc:0:704926","triggers":["block"],"chance":0.5,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":704926}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":42,"miscValue":0,"triggerSpellId":704926},{"kind":"apply-aura","aura":{"id":"coa:704925:effect:1:aura:51","name":"Bastion of Vengeance (Aura 51)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","statLine truncated
|
||||
@@ -1 +1 @@
|
||||
{"schemaVersion":1,"classId":"templar","abilities":[{"id":"coa-19-alertness-71-706321","classId":"templar","dbcSpellId":706321,"name":"Alertness","unlockLevel":1,"icon":"/assets/ui/spells/spell_holy_powerinfusion.png","sigil":"AL","description":"AlertnessRank 1Reduces damage taken by 5%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:706321:effect:0:aura:87","name":"Alertness (Aura 87)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"damage","direction":"taken","operation":"percent","value":-0.05}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":87,"miscValue":127,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:706321:effect:1:aura:107","name":"Alertness (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":0.05}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":5,"triggerSpellId":0}],"passive":true,"source":"coa-progression","ranks":[{"rank":1,"spellId":706321,"level":1,"description":"AlertnessRank 1Reduces damage taken by 5%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:706321:effect:0:aura:87","name":"Alertness (Aura 87)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"damage","direction":"taken","operation":"percent","value":-0.05}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":87,"miscValue":127,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:706321:effect:1:aura:107","name":"Alertness (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":0.05}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":5,"triggerSpellId":0}]},{"rank":2,"spellId":706322,"level":1,"description":"AlertnessRank 2Reduces Physical Damage taken by 4% and increases the range of Benediction by 10 yds.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:706322:effect:0:aura:87","name":"Alertness (Aura 87)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"damage","direction":"taken","operation":"percent","value":-0.04}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":87,"miscValue":127,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:706322:effect:1:aura:107","name":"Alertness (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":0.1}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":5,"triggerSpellId":0}]}]},{"id":"coa-tree-34375","classId":"templar","dbcSpellId":801457,"name":"Binding Scripture","unlockLevel":1,"icon":"/assets/ui/spells/_d3breathofheaven.png","sigil":"BS","description":"Healing an ally now redirects 10% of their damage taken to you and reduces your damage taken by -5% for 10 seconds, stacking 3 times.","target":"hostile","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"damage","coefficient":1},{"kind":"heal","coefficient":1},{"kind":"apply-aura","aura":{"id":"coa:801457:effect:0:aura:42","name":"Binding Scripture (Aura 42)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","procs":[{"id":"coa:801457:proc:0:705309","triggers":["heal","damage-taken"],"chance":1,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":705309}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":42,"miscValue":0,"triggerSpellId":705309}],"passive":true,"source":"coa-tree","ranks":[{"rank":1,"spellId":801457,"level":1,"description":"Healing an ally now redirects 10% of their damage taken to you and reduces your damage taken by -5% for 10 seconds, stacking 3 times.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"damage","coefficient":1},{"kind":"heal","coefficient":1},{"kind":"apply-aura","aura":{"id":"coa:801457:effect:0:aura:42","name":"Binding Scripture (Aura 42)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","procs":[{"id":"coa:801457:proc:0:705309","triggers":["heal","damage-taken"],"chance":1,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":705309}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":42,"miscValue":0,"triggerSpellId":705309}]}],"taleLine truncated
|
||||
{"schemaVersion":1,"classId":"templar","abilities":[{"id":"coa-19-alertness-71-706321","classId":"templar","dbcSpellId":706321,"name":"Alertness","unlockLevel":1,"icon":"/assets/ui/spells/spell_holy_powerinfusion.png","sigil":"AL","description":"AlertnessRank 1Reduces damage taken by 5%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:706321:effect:0:aura:87","name":"Alertness (Aura 87)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"damage","direction":"taken","operation":"percent","value":-0.05}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":87,"miscValue":127,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:706321:effect:1:aura:107","name":"Alertness (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":0.05}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":5,"triggerSpellId":0}],"passive":true,"source":"coa-progression","ranks":[{"rank":1,"spellId":706321,"level":1,"description":"AlertnessRank 1Reduces damage taken by 5%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:706321:effect:0:aura:87","name":"Alertness (Aura 87)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"damage","direction":"taken","operation":"percent","value":-0.05}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":87,"miscValue":127,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:706321:effect:1:aura:107","name":"Alertness (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":0.05}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":5,"triggerSpellId":0}]},{"rank":2,"spellId":706322,"level":1,"description":"AlertnessRank 2Reduces Physical Damage taken by 4% and increases the range of Benediction by 10 yds.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:706322:effect:0:aura:87","name":"Alertness (Aura 87)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"damage","direction":"taken","operation":"percent","value":-0.04}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":87,"miscValue":127,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:706322:effect:1:aura:107","name":"Alertness (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":0.1}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":5,"triggerSpellId":0}]}]},{"id":"coa-tree-34375","classId":"templar","dbcSpellId":801457,"name":"Binding Scripture","unlockLevel":1,"icon":"/assets/ui/spells/_d3breathofheaven.png","sigil":"BS","description":"Healing an ally now redirects 10% of their damage taken to you and reduces your damage taken by -5% for 10 seconds, stacking 3 times.","target":"friendly","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"heal","coefficient":1},{"kind":"apply-aura","aura":{"id":"coa:801457:effect:0:aura:42","name":"Binding Scripture (Aura 42)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","procs":[{"id":"coa:801457:proc:0:705309","triggers":["heal","damage-taken"],"chance":1,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":705309}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":42,"miscValue":0,"triggerSpellId":705309}],"passive":true,"source":"coa-tree","ranks":[{"rank":1,"spellId":801457,"level":1,"description":"Healing an ally now redirects 10% of their damage taken to you and reduces your damage taken by -5% for 10 seconds, stacking 3 times.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"heal","coefficient":1},{"kind":"apply-aura","aura":{"id":"coa:801457:effect:0:aura:42","name":"Binding Scripture (Aura 42)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","procs":[{"id":"coa:801457:proc:0:705309","triggers":["heal","damage-taken"],"chance":1,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":705309}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":42,"miscValue":0,"triggerSpellId":705309}]}],"talentEntryId":34375},{"id":"coa-tree-29704","classId":"templar","dbcSpLine truncated
|
||||
@@ -1 +1 @@
|
||||
{"schemaVersion":1,"classId":"tinker","abilities":[{"id":"coa-tree-29440","classId":"tinker","dbcSpellId":801744,"name":"Air Strike","unlockLevel":1,"icon":"/assets/ui/spells/ability_mount_gyrocoptor.png","sigil":"AS","description":"Mark the target location for an Air Strike. After 7 seconds, it deals 1250 Fire damage to up to 12 enemies caught within the blast. This ability always critically strikes.","target":"hostile","range":{"min":0,"max":60},"radius":20,"castMode":"instant","castTimeMs":0,"cooldownMs":60000,"gcdMs":1500,"cost":{"resource":"mana","amount":25,"percentage":true},"effects":[{"kind":"damage","coefficient":1},{"kind":"apply-aura","aura":{"id":"coa:801744:effect:0:aura:23","name":"Air Strike (Aura 23)","disposition":"debuff","durationMs":7000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic","procs":[{"id":"coa:801744:proc:0:801745","triggers":["periodic"],"chance":1,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":801745}}}]},"sourceEffectType":27,"sourceAuraType":23,"miscValue":0,"triggerSpellId":801745}],"passive":false,"source":"coa-tree","ranks":[{"rank":1,"spellId":801744,"level":1,"description":"Mark the target location for an Air Strike. After 7 seconds, it deals 1250 Fire damage to up to 12 enemies caught within the blast. This ability always critically strikes.","castMode":"instant","castTimeMs":0,"cooldownMs":60000,"gcdMs":1500,"cost":{"resource":"mana","amount":25,"percentage":true},"effects":[{"kind":"damage","coefficient":1},{"kind":"apply-aura","aura":{"id":"coa:801744:effect:0:aura:23","name":"Air Strike (Aura 23)","disposition":"debuff","durationMs":7000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic","procs":[{"id":"coa:801744:proc:0:801745","triggers":["periodic"],"chance":1,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":801745}}}]},"sourceEffectType":27,"sourceAuraType":23,"miscValue":0,"triggerSpellId":801745}]}],"talentEntryId":29440},{"id":"coa-tree-5997","classId":"tinker","dbcSpellId":520445,"name":"Auto Resuscitation Device","unlockLevel":1,"icon":"/assets/ui/spells/inv_misc_enggizmos_15.png","sigil":"AR","description":"Attach a resuscitation device to your target ally for 20 seconds. When the target would otherwise die, they are instead healed for 50% of their maximum health.","target":"friendly","range":{"min":0,"max":20},"castMode":"cast","castTimeMs":1500,"cooldownMs":180000,"gcdMs":1500,"cost":{"resource":"mana","amount":40,"percentage":true},"effects":[{"kind":"heal","coefficient":1},{"kind":"apply-aura","aura":{"id":"coa:520445:effect:0:aura:69","name":"Auto Resuscitation Device (Aura 69)","disposition":"buff","durationMs":20000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic","absorbs":[{"id":"coa:520445:absorb:0","amount":20,"perStack":false}]},"sourceEffectType":6,"sourceAuraType":69,"miscValue":127,"triggerSpellId":524780}],"passive":false,"source":"coa-tree","ranks":[{"rank":1,"spellId":520445,"level":1,"description":"Attach a resuscitation device to your target ally for 20 seconds. When the target would otherwise die, they are instead healed for 50% of their maximum health.","castMode":"cast","castTimeMs":1500,"cooldownMs":180000,"gcdMs":1500,"cost":{"resource":"mana","amount":40,"percentage":true},"effects":[{"kind":"heal","coefficient":1},{"kind":"apply-aura","aura":{"id":"coa:520445:effect:0:aura:69","name":"Auto Resuscitation Device (Aura 69)","disposition":"buff","durationMs":20000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic","absorbs":[{"id":"coa:520445:absorb:0","amount":20,"perStack":false}]},"sourceEffectType":6,"sourceAuraType":69,"miscValue":127,"triggerSpellId":524780}]}],"talentEntryId":5997},{"id":"coa-28-blasting-barrels-99-705835","classId":"tinker","dbcSpellId":705835,"name":"Blasting Barrels","unlockLevel":1,"icon":"/assets/ui/spells/5_gunshot_(3)_border.png","sigil":"BB","description":"Blasting BarrelsRank 1InstantIncreases critical strike damage by 0% and increases damage dealt by Rounds by 0%.","target":"hostile","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"damage","coefficient":1}],"passive":true,"source":"coa-progression","ranks":[{"rank":1,"spellId":705835,"level":1,"description":"Blasting BarrelsRank 1InstantIncreases critical strike damage by 0% and increases damage dealt by Rounds by 0%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"damage","coefficient":1}]}]},{"id":"coa-tree-30583","classId":"tinker","dbcSpellId":560754,"name":"Build: Alarm Beacon","unlockLevel":1,"icon":"/assets/ui/spells/inv_lightforgedmechsuit.png","sigil":"BA","description":"3 Charges, 15 sec recharge Throw down an alarm beacon for 10 seconds, removing fear, charm and sleep effects from up to 1 ally within 20 yds every 1 sec.","target":"hostile","range":{"min":0,Line truncated
|
||||
{"schemaVersion":1,"classId":"tinker","abilities":[{"id":"coa-tree-29440","classId":"tinker","dbcSpellId":801744,"name":"Air Strike","unlockLevel":1,"icon":"/assets/ui/spells/ability_mount_gyrocoptor.png","sigil":"AS","description":"Mark the target location for an Air Strike. After 7 seconds, it deals 1250 Fire damage to up to 12 enemies caught within the blast. This ability always critically strikes.","target":"hostile","range":{"min":0,"max":60},"radius":20,"castMode":"instant","castTimeMs":0,"cooldownMs":60000,"gcdMs":1500,"cost":{"resource":"mana","amount":25,"percentage":true},"effects":[{"kind":"apply-aura","aura":{"id":"coa:801744:effect:0:aura:23","name":"Air Strike (Aura 23)","disposition":"debuff","durationMs":7000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic","procs":[{"id":"coa:801744:proc:0:801745","triggers":["periodic"],"chance":1,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":801745}}}]},"sourceEffectType":27,"sourceAuraType":23,"miscValue":0,"triggerSpellId":801745}],"passive":false,"source":"coa-tree","ranks":[{"rank":1,"spellId":801744,"level":1,"description":"Mark the target location for an Air Strike. After 7 seconds, it deals 1250 Fire damage to up to 12 enemies caught within the blast. This ability always critically strikes.","castMode":"instant","castTimeMs":0,"cooldownMs":60000,"gcdMs":1500,"cost":{"resource":"mana","amount":25,"percentage":true},"effects":[{"kind":"apply-aura","aura":{"id":"coa:801744:effect:0:aura:23","name":"Air Strike (Aura 23)","disposition":"debuff","durationMs":7000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic","procs":[{"id":"coa:801744:proc:0:801745","triggers":["periodic"],"chance":1,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":801745}}}]},"sourceEffectType":27,"sourceAuraType":23,"miscValue":0,"triggerSpellId":801745}]}],"talentEntryId":29440},{"id":"coa-tree-5997","classId":"tinker","dbcSpellId":520445,"name":"Auto Resuscitation Device","unlockLevel":1,"icon":"/assets/ui/spells/inv_misc_enggizmos_15.png","sigil":"AR","description":"Attach a resuscitation device to your target ally for 20 seconds. When the target would otherwise die, they are instead healed for 50% of their maximum health.","target":"friendly","range":{"min":0,"max":20},"castMode":"cast","castTimeMs":1500,"cooldownMs":180000,"gcdMs":1500,"cost":{"resource":"mana","amount":40,"percentage":true},"effects":[{"kind":"heal","coefficient":1},{"kind":"apply-aura","aura":{"id":"coa:520445:effect:0:aura:69","name":"Auto Resuscitation Device (Aura 69)","disposition":"buff","durationMs":20000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic","absorbs":[{"id":"coa:520445:absorb:0","amount":20,"perStack":false}]},"sourceEffectType":6,"sourceAuraType":69,"miscValue":127,"triggerSpellId":524780}],"passive":false,"source":"coa-tree","ranks":[{"rank":1,"spellId":520445,"level":1,"description":"Attach a resuscitation device to your target ally for 20 seconds. When the target would otherwise die, they are instead healed for 50% of their maximum health.","castMode":"cast","castTimeMs":1500,"cooldownMs":180000,"gcdMs":1500,"cost":{"resource":"mana","amount":40,"percentage":true},"effects":[{"kind":"heal","coefficient":1},{"kind":"apply-aura","aura":{"id":"coa:520445:effect:0:aura:69","name":"Auto Resuscitation Device (Aura 69)","disposition":"buff","durationMs":20000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"magic","absorbs":[{"id":"coa:520445:absorb:0","amount":20,"perStack":false}]},"sourceEffectType":6,"sourceAuraType":69,"miscValue":127,"triggerSpellId":524780}]}],"talentEntryId":5997},{"id":"coa-28-blasting-barrels-99-705835","classId":"tinker","dbcSpellId":705835,"name":"Blasting Barrels","unlockLevel":1,"icon":"/assets/ui/spells/5_gunshot_(3)_border.png","sigil":"BB","description":"Blasting BarrelsRank 1InstantIncreases critical strike damage by 0% and increases damage dealt by Rounds by 0%.","target":"hostile","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"damage","coefficient":1}],"passive":true,"source":"coa-progression","ranks":[{"rank":1,"spellId":705835,"level":1,"description":"Blasting BarrelsRank 1InstantIncreases critical strike damage by 0% and increases damage dealt by Rounds by 0%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"damage","coefficient":1}]}]},{"id":"coa-tree-30583","classId":"tinker","dbcSpellId":560754,"name":"Build: Alarm Beacon","unlockLevel":1,"icon":"/assets/ui/spells/inv_lightforgedmechsuit.png","sigil":"BA","description":"3 Charges, 15 sec recharge Throw down an alarm beacon for 10 seconds, removing fear, charm and sleep effects from up to 1 ally within 20 yds every 1 sec.","target":"hostile","range":{"min":0,"max":20},"radius":5,"castMode":"instant","castTimeMs":0,"cooldownMsLine truncated
|
||||
@@ -1 +1 @@
|
||||
{"schemaVersion":1,"classId":"venomancer","abilities":[{"id":"coa-29-acceleration-103-705960","classId":"venomancer","dbcSpellId":705960,"name":"Acceleration","unlockLevel":1,"icon":"/assets/ui/spells/_meat_rotten.png","sigil":"AC","description":"AccelerationRank 2Increases the tick rate of Wilt and Shadra's Vigil by 10%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705960:effect:0:aura:108","name":"Acceleration (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":-0.1}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":19,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705960:effect:1:aura:108","name":"Acceleration (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":-0.1}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":19,"triggerSpellId":0}],"passive":true,"source":"coa-progression","ranks":[{"rank":2,"spellId":705960,"level":1,"description":"AccelerationRank 2Increases the tick rate of Wilt and Shadra's Vigil by 10%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705960:effect:0:aura:108","name":"Acceleration (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":-0.1}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":19,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705960:effect:1:aura:108","name":"Acceleration (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":-0.1}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":19,"triggerSpellId":0}]}]},{"id":"coa-tree-6086","classId":"venomancer","dbcSpellId":805775,"name":"Adrenal Venom","unlockLevel":1,"icon":"/assets/ui/spells/trade_alchemy_dpotion_a26.png","sigil":"AV","description":"Activate Adrenal Venom for 120 minutes, giving your casts a 25% chance to increase your haste by 6% for 6 seconds, stacking 3 times. You can have a maximum of 2 unique Venoms active at a time.","target":"self","range":{"min":0,"max":0},"castMode":"cast","castTimeMs":1000,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":20,"percentage":true},"effects":[{"kind":"apply-aura","aura":{"id":"coa:805775:effect:0:aura:42","name":"Adrenal Venom (Aura 42)","disposition":"buff","durationMs":7200000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"poison","procs":[{"id":"coa:805775:proc:0:805894","triggers":["cast"],"chance":0.25,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":805894}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":42,"miscValue":0,"triggerSpellId":805894},{"kind":"apply-aura","aura":{"id":"coa:805775:effect:1:aura:107","name":"Adrenal Venom (Aura 107)","disposition":"buff","durationMs":7200000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"poison","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":0}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":17,"triggerSpellId":0}],"passive":false,"source":"coa-tree","ranks":[{"rank":1,"spellId":805775,"level":1,"description":"Activate Adrenal Venom for 120 minutes, giving your casts a 25% chance to increase your haste by 6% for 6 seconds, stacking 3 times. You can have a maximum of 2 unique Venoms active at a time.","castMode":"cast","castTimeMs":1000,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":20,"percentage":true},"effects":[{"kind":"apply-aura","aura":{"id":"coa:805775:effect:0:aura:42","name":"Adrenal Venom (Aura 42)","disposition":"buff","durationMs":7200000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"poison","procs":[{"id":"coa:805775:proc:0:805894","triggers":["cast"],"chance":0.25,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":805894}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":42,"miscValue":0,"triggerSpellId":805894},{"kind":"apply-aura","aura":{"id":"coa:805775:effect:1:aura:107","name":"Adrenal Venom (Aura 107)","disposition":"buff","durationMs":7200000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"poison","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":0}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":17,"triggerSpellId":0}]}],"talentEntryId":6086},{"id":"coa-tree-29574"Line truncated
|
||||
{"schemaVersion":1,"classId":"venomancer","abilities":[{"id":"coa-29-acceleration-103-705960","classId":"venomancer","dbcSpellId":705960,"name":"Acceleration","unlockLevel":1,"icon":"/assets/ui/spells/_meat_rotten.png","sigil":"AC","description":"AccelerationRank 2Increases the tick rate of Wilt and Shadra's Vigil by 10%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705960:effect:0:aura:108","name":"Acceleration (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":-0.1}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":19,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705960:effect:1:aura:108","name":"Acceleration (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":-0.1}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":19,"triggerSpellId":0}],"passive":true,"source":"coa-progression","ranks":[{"rank":2,"spellId":705960,"level":1,"description":"AccelerationRank 2Increases the tick rate of Wilt and Shadra's Vigil by 10%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705960:effect:0:aura:108","name":"Acceleration (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":-0.1}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":19,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705960:effect:1:aura:108","name":"Acceleration (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":-0.1}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":19,"triggerSpellId":0}]}]},{"id":"coa-tree-6086","classId":"venomancer","dbcSpellId":805775,"name":"Adrenal Venom","unlockLevel":1,"icon":"/assets/ui/spells/trade_alchemy_dpotion_a26.png","sigil":"AV","description":"Activate Adrenal Venom for 120 minutes, giving your casts a 25% chance to increase your haste by 6% for 6 seconds, stacking 3 times. You can have a maximum of 2 unique Venoms active at a time.","target":"self","range":{"min":0,"max":0},"castMode":"cast","castTimeMs":1000,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":20,"percentage":true},"effects":[{"kind":"apply-aura","aura":{"id":"coa:805775:effect:0:aura:42","name":"Adrenal Venom (Aura 42)","disposition":"buff","durationMs":7200000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"poison","procs":[{"id":"coa:805775:proc:0:805894","triggers":["cast"],"chance":0.25,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":805894}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":42,"miscValue":0,"triggerSpellId":805894},{"kind":"apply-aura","aura":{"id":"coa:805775:effect:1:aura:107","name":"Adrenal Venom (Aura 107)","disposition":"buff","durationMs":7200000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"poison","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":0}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":17,"triggerSpellId":0}],"passive":false,"source":"coa-tree","ranks":[{"rank":1,"spellId":805775,"level":1,"description":"Activate Adrenal Venom for 120 minutes, giving your casts a 25% chance to increase your haste by 6% for 6 seconds, stacking 3 times. You can have a maximum of 2 unique Venoms active at a time.","castMode":"cast","castTimeMs":1000,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":20,"percentage":true},"effects":[{"kind":"apply-aura","aura":{"id":"coa:805775:effect:0:aura:42","name":"Adrenal Venom (Aura 42)","disposition":"buff","durationMs":7200000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"poison","procs":[{"id":"coa:805775:proc:0:805894","triggers":["cast"],"chance":0.25,"action":{"kind":"custom","id":"wow335-trigger-spell","data":{"spellId":805894}}}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":42,"miscValue":0,"triggerSpellId":805894},{"kind":"apply-aura","aura":{"id":"coa:805775:effect:1:aura:107","name":"Adrenal Venom (Aura 107)","disposition":"buff","durationMs":7200000,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"poison","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":0}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":17,"triggerSpellId":0}]}],"talentEntryId":6086},{"id":"coa-tree-29574"Line truncated
|
||||
@@ -1 +1 @@
|
||||
{"schemaVersion":1,"classId":"witch-doctor","abilities":[{"id":"coa-tree-30147","classId":"witch-doctor","dbcSpellId":705921,"name":"Alchemical Enhancement","unlockLevel":1,"icon":"/assets/ui/spells/inv_alchemy_71_potion4.png","sigil":"AE","description":"Increases your haste by 5%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"energy","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705921:effect:0:aura:216","name":"Alchemical Enhancement (Aura 216)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"haste","operation":"percent","value":0.05}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":216,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705921:effect:1:aura:192","name":"Alchemical Enhancement (Aura 192)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"haste","operation":"percent","value":0.05}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":192,"miscValue":127,"triggerSpellId":0}],"passive":true,"source":"coa-tree","ranks":[{"rank":1,"spellId":705921,"level":1,"description":"Increases your haste by 5%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"energy","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705921:effect:0:aura:216","name":"Alchemical Enhancement (Aura 216)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"haste","operation":"percent","value":0.05}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":216,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705921:effect:1:aura:192","name":"Alchemical Enhancement (Aura 192)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"haste","operation":"percent","value":0.05}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":192,"miscValue":127,"triggerSpellId":0}]}],"talentEntryId":30147},{"id":"coa-tree-11323","classId":"witch-doctor","dbcSpellId":301297,"name":"Blatant Curse","unlockLevel":1,"icon":"/assets/ui/spells/spell_nature_removecurse.png","sigil":"BC","description":"Reduces the mana cost of your Jinxes by -25%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:301297:effect:0:aura:108","name":"Blatant Curse (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"curse","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":0}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":8,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:301297:effect:1:aura:108","name":"Blatant Curse (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"curse","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":-0.25}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":14,"triggerSpellId":0}],"passive":true,"source":"coa-tree","ranks":[{"rank":1,"spellId":301297,"level":1,"description":"Reduces the mana cost of your Jinxes by -25%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:301297:effect:0:aura:108","name":"Blatant Curse (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"curse","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":0}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":8,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:301297:effect:1:aura:108","name":"Blatant Curse (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"curse","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":-0.25}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":14,"triggerSpellId":0}]}],"talentEntryId":11323},{"id":"coa-tree-12048","classId":"witch-doctor","dbcSpellId":572870,"name":"Blessing of Hir'eek","unlockLevel":1,"icon":"/assets/ui/spells/trade_archaeology_trollbatidol.png","sigil":"BO","description":"Your Hexbreak now removes an additional curse effect.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:572870:effect:0:aura:107","name":"Blessing of Hir'eek (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"sLine truncated
|
||||
{"schemaVersion":1,"classId":"witch-doctor","abilities":[{"id":"coa-tree-30147","classId":"witch-doctor","dbcSpellId":705921,"name":"Alchemical Enhancement","unlockLevel":1,"icon":"/assets/ui/spells/inv_alchemy_71_potion4.png","sigil":"AE","description":"Increases your haste by 5%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"energy","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705921:effect:0:aura:216","name":"Alchemical Enhancement (Aura 216)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"haste","operation":"percent","value":0.05}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":216,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705921:effect:1:aura:192","name":"Alchemical Enhancement (Aura 192)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"haste","operation":"percent","value":0.05}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":192,"miscValue":127,"triggerSpellId":0}],"passive":true,"source":"coa-tree","ranks":[{"rank":1,"spellId":705921,"level":1,"description":"Increases your haste by 5%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"energy","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705921:effect:0:aura:216","name":"Alchemical Enhancement (Aura 216)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"haste","operation":"percent","value":0.05}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":216,"miscValue":0,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705921:effect:1:aura:192","name":"Alchemical Enhancement (Aura 192)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"haste","operation":"percent","value":0.05}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":192,"miscValue":127,"triggerSpellId":0}]}],"talentEntryId":30147},{"id":"coa-tree-11323","classId":"witch-doctor","dbcSpellId":301297,"name":"Blatant Curse","unlockLevel":1,"icon":"/assets/ui/spells/spell_nature_removecurse.png","sigil":"BC","description":"Reduces the mana cost of your Jinxes by -25%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:301297:effect:0:aura:108","name":"Blatant Curse (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"curse","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":0}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":8,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:301297:effect:1:aura:108","name":"Blatant Curse (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"curse","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":-0.25}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":14,"triggerSpellId":0}],"passive":true,"source":"coa-tree","ranks":[{"rank":1,"spellId":301297,"level":1,"description":"Reduces the mana cost of your Jinxes by -25%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:301297:effect:0:aura:108","name":"Blatant Curse (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"curse","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":0}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":8,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:301297:effect:1:aura:108","name":"Blatant Curse (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","dispelCategory":"curse","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":-0.25}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":14,"triggerSpellId":0}]}],"talentEntryId":11323},{"id":"coa-tree-12048","classId":"witch-doctor","dbcSpellId":572870,"name":"Blessing of Hir'eek","unlockLevel":1,"icon":"/assets/ui/spells/trade_archaeology_trollbatidol.png","sigil":"BO","description":"Your Hexbreak now removes an additional curse effect.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"mana","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:572870:effect:0:aura:107","name":"Blessing of Hir'eek (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"sLine truncated
|
||||
@@ -1 +1 @@
|
||||
{"schemaVersion":1,"classId":"witch-hunter","abilities":[{"id":"coa-15-agile-steps-58-705521","classId":"witch-hunter","dbcSpellId":705521,"name":"Agile Steps","unlockLevel":1,"icon":"/assets/ui/spells/inv_boot_cloth_nzothraidmythic_d_01.png","sigil":"AS","description":"Agile StepsRank 1Reduces the cooldown of Vault by 2 sec.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705521:effect:0:aura:4","name":"Agile Steps (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":127,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705521:effect:1:aura:107","name":"Agile Steps (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":-20}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":11,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705521:effect:2:aura:4","name":"Agile Steps (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":1,"triggerSpellId":0}],"passive":true,"source":"coa-progression","ranks":[{"rank":1,"spellId":705521,"level":1,"description":"Agile StepsRank 1Reduces the cooldown of Vault by 2 sec.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705521:effect:0:aura:4","name":"Agile Steps (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":127,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705521:effect:1:aura:107","name":"Agile Steps (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":-20}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":11,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705521:effect:2:aura:4","name":"Agile Steps (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":1,"triggerSpellId":0}]},{"rank":2,"spellId":705522,"level":1,"description":"Agile StepsRank 2Reduces the cooldown of Vault by 4 sec.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705522:effect:0:aura:4","name":"Agile Steps (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":127,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705522:effect:1:aura:107","name":"Agile Steps (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":-40}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":11,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705522:effect:2:aura:4","name":"Agile Steps (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":1,"triggerSpellId":0}]},{"rank":3,"spellId":705523,"level":1,"description":"Agile StepsRank 3InstantReduces the cooldown of Vault by 3 sec.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705523:effect:0:aura:4","name":"Agile Steps (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":127,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705523:effect:1:aura:107","name":"Agile Steps (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":-30}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":11,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705523:effect:2:aura:4","name":"Agile Steps (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceALine truncated
|
||||
{"schemaVersion":1,"classId":"witch-hunter","abilities":[{"id":"coa-15-agile-steps-58-705521","classId":"witch-hunter","dbcSpellId":705521,"name":"Agile Steps","unlockLevel":1,"icon":"/assets/ui/spells/inv_boot_cloth_nzothraidmythic_d_01.png","sigil":"AS","description":"Agile StepsRank 1Reduces the cooldown of Vault by 2 sec.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705521:effect:0:aura:4","name":"Agile Steps (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":127,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705521:effect:1:aura:107","name":"Agile Steps (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":-20}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":11,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705521:effect:2:aura:4","name":"Agile Steps (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":1,"triggerSpellId":0}],"passive":true,"source":"coa-progression","ranks":[{"rank":1,"spellId":705521,"level":1,"description":"Agile StepsRank 1Reduces the cooldown of Vault by 2 sec.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705521:effect:0:aura:4","name":"Agile Steps (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":127,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705521:effect:1:aura:107","name":"Agile Steps (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":-20}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":11,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705521:effect:2:aura:4","name":"Agile Steps (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":1,"triggerSpellId":0}]},{"rank":2,"spellId":705522,"level":1,"description":"Agile StepsRank 2Reduces the cooldown of Vault by 4 sec.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705522:effect:0:aura:4","name":"Agile Steps (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":127,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705522:effect:1:aura:107","name":"Agile Steps (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":-40}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":11,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705522:effect:2:aura:4","name":"Agile Steps (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":1,"triggerSpellId":0}]},{"rank":3,"spellId":705523,"level":1,"description":"Agile StepsRank 3InstantReduces the cooldown of Vault by 3 sec.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"rage","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705523:effect:0:aura:4","name":"Agile Steps (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":4,"miscValue":127,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705523:effect:1:aura:107","name":"Agile Steps (Aura 107)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"attack-power","operation":"percent","value":-30}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":107,"miscValue":11,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705523:effect:2:aura:4","name":"Agile Steps (Aura 4)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","utilityTags":["coa-aura:4"]},"recipient":"caster","sourceEffectType":6,"sourceALine truncated
|
||||
@@ -1 +1 @@
|
||||
{"schemaVersion":1,"abilitiesByClass":{"barbarian":[{"id":"coa-tree-30762","classId":"barbarian","dbcSpellId":500918,"name":"Ancestral Roar","unlockLevel":1,"icon":"/assets/ui/spells/_d3threateningshout.png","sigil":"AR","description":"Ancestral RoarRank 1Instant cast25 sec cooldownRequires Enraged Roar with ancestral fervor, healing you for 100+0+AP*0.5.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":25000,"gcdMs":1500,"cost":{"resource":"energy","amount":0},"effects":[{"kind":"heal","coefficient":1.6580645243326824,"basePoints":100,"dieSides":7,"pointsPerLevel":3.200000047683716,"bonusPowerCoefficient":0,"sourceLevel":16,"maxScalingLevel":22},{"kind":"utility","action":"class-script","effectType":3,"auraType":0,"miscValue":3,"triggerSpellId":0,"durationMs":0}],"passive":false,"source":"coa-tree","ranks":[{"rank":1,"spellId":500918,"level":1,"description":"Ancestral RoarRank 1Instant cast25 sec cooldownRequires Enraged Roar with ancestral fervor, healing you for 100+0+AP*0.5.","castMode":"instant","castTimeMs":0,"cooldownMs":25000,"gcdMs":1500,"cost":{"resource":"energy","amount":0},"effects":[{"kind":"heal","coefficient":1.6580645243326824,"basePoints":100,"dieSides":7,"pointsPerLevel":3.200000047683716,"bonusPowerCoefficient":0,"sourceLevel":16,"maxScalingLevel":22},{"kind":"utility","action":"class-script","effectType":3,"auraType":0,"miscValue":3,"triggerSpellId":0,"durationMs":0}]}],"talentEntryId":30762},{"id":"coa-12-ancestral-spirit-47-705153","classId":"barbarian","dbcSpellId":705153,"name":"Ancestral Spirit","unlockLevel":1,"icon":"/assets/ui/spells/inv_misc_questionmark.png","sigil":"AS","description":"Ancestral SpiritRank 1Reduces the Energy cost of your Spirit spells and increases the duration of your Shouts by 50%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"energy","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705153:effect:0:aura:108","name":"Ancestral Spirit (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":-0.5}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":14,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705153:effect:1:aura:108","name":"Ancestral Spirit (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":0.5}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":1,"triggerSpellId":0}],"passive":true,"source":"coa-progression","ranks":[{"rank":1,"spellId":705153,"level":1,"description":"Ancestral SpiritRank 1Reduces the Energy cost of your Spirit spells and increases the duration of your Shouts by 50%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"energy","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705153:effect:0:aura:108","name":"Ancestral Spirit (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":-0.5}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":14,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705153:effect:1:aura:108","name":"Ancestral Spirit (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":0.5}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":1,"triggerSpellId":0}]},{"rank":2,"spellId":705154,"level":1,"description":"Ancestral SpiritRank 2Reduces the Energy cost of your Spirit spells and increases the duration of your Shouts by 100%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"energy","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705154:effect:0:aura:108","name":"Ancestral Spirit (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":-1}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":14,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705154:effect:1:aura:108","name":"Ancestral Spirit (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":1}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":1,"triggerSpellId":0}]}]},{"id":"coa-tree-34279","classId":"barbarian","dbcSpellId":560456,"name":"Athleticism","unlockLevel":1,"icon":"/assets/ui/spells/5_run_border.png","sigil":"AT","descriptionLine truncated
|
||||
{"schemaVersion":1,"abilitiesByClass":{"barbarian":[{"id":"coa-tree-30762","classId":"barbarian","dbcSpellId":500918,"name":"Ancestral Roar","unlockLevel":1,"icon":"/assets/ui/spells/_d3threateningshout.png","sigil":"AR","description":"Ancestral RoarRank 1Instant cast25 sec cooldownRequires Enraged Roar with ancestral fervor, healing you for 100+0+AP*0.5.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":25000,"gcdMs":1500,"cost":{"resource":"energy","amount":0},"effects":[{"kind":"heal","coefficient":1.6580645243326824,"basePoints":100,"dieSides":7,"pointsPerLevel":3.200000047683716,"bonusPowerCoefficient":0,"sourceLevel":16,"maxScalingLevel":22},{"kind":"utility","action":"class-script","effectType":3,"auraType":0,"miscValue":3,"triggerSpellId":0,"durationMs":0}],"passive":false,"source":"coa-tree","ranks":[{"rank":1,"spellId":500918,"level":1,"description":"Ancestral RoarRank 1Instant cast25 sec cooldownRequires Enraged Roar with ancestral fervor, healing you for 100+0+AP*0.5.","castMode":"instant","castTimeMs":0,"cooldownMs":25000,"gcdMs":1500,"cost":{"resource":"energy","amount":0},"effects":[{"kind":"heal","coefficient":1.6580645243326824,"basePoints":100,"dieSides":7,"pointsPerLevel":3.200000047683716,"bonusPowerCoefficient":0,"sourceLevel":16,"maxScalingLevel":22},{"kind":"utility","action":"class-script","effectType":3,"auraType":0,"miscValue":3,"triggerSpellId":0,"durationMs":0}]}],"talentEntryId":30762},{"id":"coa-12-ancestral-spirit-47-705153","classId":"barbarian","dbcSpellId":705153,"name":"Ancestral Spirit","unlockLevel":1,"icon":"/assets/ui/spells/inv_misc_questionmark.png","sigil":"AS","description":"Ancestral SpiritRank 1Reduces the Energy cost of your Spirit spells and increases the duration of your Shouts by 50%.","target":"self","range":{"min":0,"max":0},"castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"energy","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705153:effect:0:aura:108","name":"Ancestral Spirit (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":-0.5}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":14,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705153:effect:1:aura:108","name":"Ancestral Spirit (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":0.5}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":1,"triggerSpellId":0}],"passive":true,"source":"coa-progression","ranks":[{"rank":1,"spellId":705153,"level":1,"description":"Ancestral SpiritRank 1Reduces the Energy cost of your Spirit spells and increases the duration of your Shouts by 50%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"energy","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705153:effect:0:aura:108","name":"Ancestral Spirit (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":-0.5}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":14,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705153:effect:1:aura:108","name":"Ancestral Spirit (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":0.5}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":1,"triggerSpellId":0}]},{"rank":2,"spellId":705154,"level":1,"description":"Ancestral SpiritRank 2Reduces the Energy cost of your Spirit spells and increases the duration of your Shouts by 100%.","castMode":"instant","castTimeMs":0,"cooldownMs":0,"gcdMs":1500,"cost":{"resource":"energy","amount":0},"effects":[{"kind":"apply-aura","aura":{"id":"coa:705154:effect:0:aura:108","name":"Ancestral Spirit (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":-1}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":14,"triggerSpellId":0},{"kind":"apply-aura","aura":{"id":"coa:705154:effect:1:aura:108","name":"Ancestral Spirit (Aura 108)","disposition":"buff","durationMs":null,"maxStacks":1,"stackBehavior":"refresh","modifiers":[{"kind":"stat","stat":"ranged-attack-power","operation":"percent","value":1}]},"recipient":"caster","sourceEffectType":6,"sourceAuraType":108,"miscValue":1,"triggerSpellId":0}]}]},{"id":"coa-tree-34279","classId":"barbarian","dbcSpellId":560456,"name":"Athleticism","unlockLevel":1,"icon":"/assets/ui/spells/5_run_border.png","sigil":"AT","descriptionLine truncated
|
||||
File diff suppressed because it is too large.
Load diff
@@ -30,14 +30,14 @@ describe("SQL-generated boss loot catalog", () => {
|
||||
});
|
||||
|
||||
it("rolls recognizable boss items while setting item level from the player", () => {
|
||||
const first = rollBossLoot("lady-anacondra", 37, { random: () => 0, now: 12_345 });
|
||||
const last = rollBossLoot("lady-anacondra", 37, { random: () => 0.999, now: 12_346 });
|
||||
const first = rollBossLoot("lady-anacondra", 80, { random: () => 0, now: 12_345 });
|
||||
const last = rollBossLoot("lady-anacondra", 80, { random: () => 0.999, now: 12_346 });
|
||||
|
||||
expect(first).toHaveLength(1);
|
||||
expect(first[0]).toMatchObject({
|
||||
itemId: 5404,
|
||||
name: "Serpent's Shoulders",
|
||||
itemLevel: 37,
|
||||
itemLevel: 85,
|
||||
sourceItemLevel: 23,
|
||||
sourceBossKey: "lady-anacondra",
|
||||
acquiredAt: 12_345,
|
||||
@@ -45,7 +45,7 @@ describe("SQL-generated boss loot catalog", () => {
|
||||
expect(last[0]).toMatchObject({
|
||||
itemId: 10412,
|
||||
name: "Belt of the Fang",
|
||||
itemLevel: 37,
|
||||
itemLevel: 85,
|
||||
sourceItemLevel: 21,
|
||||
});
|
||||
expect(first[0].stats.agility).toBeGreaterThan(0);
|
||||
@@ -62,16 +62,16 @@ describe("SQL-generated boss loot catalog", () => {
|
||||
});
|
||||
|
||||
it("adds at most one bonus weighted item without changing normal rolls", () => {
|
||||
const normal = rollBossLoot("lady-anacondra", 20, {
|
||||
const normal = rollBossLoot("lady-anacondra", 80, {
|
||||
random: () => 0,
|
||||
now: 20_000,
|
||||
});
|
||||
const guaranteedBonus = rollBossLoot("lady-anacondra", 20, {
|
||||
const guaranteedBonus = rollBossLoot("lady-anacondra", 80, {
|
||||
random: () => 0,
|
||||
now: 20_001,
|
||||
bonusItemChance: 1,
|
||||
});
|
||||
const failedBonus = rollBossLoot("lady-anacondra", 20, {
|
||||
const failedBonus = rollBossLoot("lady-anacondra", 80, {
|
||||
random: () => 0.999,
|
||||
now: 20_002,
|
||||
bonusItemChance: 0.5,
|
||||
@@ -83,4 +83,8 @@ describe("SQL-generated boss loot catalog", () => {
|
||||
expect(new Set(guaranteedBonus.map((item) => item.instanceId)).size).toBe(2);
|
||||
expect(guaranteedBonus.every((item) => item.sourceBossKey === "lady-anacondra")).toBe(true);
|
||||
});
|
||||
|
||||
it.each([1, 37, 79])("suppresses equipment and bonus equipment at level %i", (level) => {
|
||||
expect(rollBossLoot("lady-anacondra", level, { random: () => 0, bonusItemChance: 1 })).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from "./itemStats";
|
||||
import type { InventoryItem, ItemQuality } from "./lootTypes";
|
||||
import { rollRunewakerBossLoot, runewakerBossHasLoot } from "./runewakerLootCatalog";
|
||||
import { allowedLootDrops } from "./lootPolicy";
|
||||
|
||||
export interface LootCatalogItem {
|
||||
readonly itemId: number;
|
||||
@@ -153,5 +154,5 @@ export function rollBossLoot(
|
||||
addWeightedDrop();
|
||||
}
|
||||
|
||||
return drops;
|
||||
return allowedLootDrops(drops, playerLevel);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { equipmentSlotForItem } from "./equipment";
|
||||
import type { InventoryItem } from "./lootTypes";
|
||||
|
||||
/** Heirlooms cover leveling gear. Keep authored non-equipment rewards at every level. */
|
||||
export function allowedLootDrops(drops: readonly InventoryItem[], playerLevel: number): readonly InventoryItem[] {
|
||||
return Number.isFinite(playerLevel) && playerLevel >= 80
|
||||
? drops
|
||||
: drops.filter((item) => equipmentSlotForItem(item) === null);
|
||||
}
|
||||
@@ -14,7 +14,7 @@ describe("Manastorm corpse loot", () => {
|
||||
useManastormStore.getState().resetRun();
|
||||
});
|
||||
|
||||
it("guarantees Kresh's normal item but waits for an explicit reward claim", () => {
|
||||
it("guarantees Kresh's Normal card but waits for an explicit reward claim", () => {
|
||||
const kresh = MANASTORM_ENCOUNTERS.find((encounter) => encounter.id === "kresh")!;
|
||||
useGameStore.getState().activateDungeon(kresh.dungeonId, undefined, {
|
||||
gameMode: "manastorm",
|
||||
@@ -34,13 +34,14 @@ describe("Manastorm corpse loot", () => {
|
||||
useCombatStore.getState().damageMob(kresh.bossRuntimeId, 1, 10_000);
|
||||
|
||||
const corpse = useCombatStore.getState().mobs[kresh.bossRuntimeId];
|
||||
expect(corpse.pendingLoot).toHaveLength(1);
|
||||
expect(corpse.pendingLoot[0]?.sourceBossKey).toBe("kresh");
|
||||
expect(corpse.pendingLoot).toEqual([]);
|
||||
expect(corpse.bossCardDrop?.rarities).toContain("normal");
|
||||
expect(useCombatStore.getState().inventory).toEqual([]);
|
||||
expect(useCombatStore.getState().mobs[kresh.bossRuntimeId]?.looted).toBe(false);
|
||||
expect(lootCurrentManastormBoss(10_100)).toBe(true);
|
||||
expect(useCombatStore.getState().inventory).toEqual(corpse.pendingLoot);
|
||||
expect(useCombatStore.getState().mobs[kresh.bossRuntimeId]?.looted).toBe(true);
|
||||
expect(useCombatStore.getState().bossCards.counts["wow:kresh"]?.normal).toBe(1);
|
||||
expect(useCombatStore.getState().bossLootRewards[0]).toMatchObject({
|
||||
bossId: kresh.bossRuntimeId,
|
||||
bossName: kresh.bossName,
|
||||
@@ -52,7 +53,7 @@ describe("Manastorm corpse loot", () => {
|
||||
it("opens a cache into concrete loot scaled to the character level", () => {
|
||||
useCombatStore.getState().initializeCharacter({
|
||||
classId: "priest",
|
||||
level: 37,
|
||||
level: 80,
|
||||
xp: 0,
|
||||
inventory: [],
|
||||
});
|
||||
@@ -77,7 +78,7 @@ describe("Manastorm corpse loot", () => {
|
||||
expect(useManastormStore.getState().rewardCache).not.toContainEqual(cache);
|
||||
expect(useCombatStore.getState().inventory).toEqual([
|
||||
expect.objectContaining({
|
||||
itemLevel: 37,
|
||||
itemLevel: 85,
|
||||
acquiredAt: 12_345,
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -984,7 +984,7 @@ function executePartyAbility(
|
||||
} else if (effect.kind === "execute") {
|
||||
for (const hostileTargetId of hostileTargets) {
|
||||
const target = useCombatStore.getState().mobs[hostileTargetId];
|
||||
if (target && target.health / Math.max(1, target.maxHealth) <= 0.2) {
|
||||
if (target && !target.boss && target.health / Math.max(1, target.maxHealth) <= 0.2) {
|
||||
effectiveAmount += combat.damageMob(
|
||||
hostileTargetId,
|
||||
Math.max(target.health, partyAbilityAmount(runtimeMember, ability, effect)),
|
||||
@@ -1078,7 +1078,7 @@ function executePartyAbility(
|
||||
resourcePools,
|
||||
resource: resource === runtimeMember.resourceType ? resourcePools[resource] ?? runtimeMember.resource : runtimeMember.resource,
|
||||
};
|
||||
} else if (effect.kind === "trigger-spell" && depth < 2) {
|
||||
} else if (effect.kind === "trigger-spell" && !effect.expanded && depth < 2) {
|
||||
const triggered = abilitiesForClass(member.classId).find((candidate) => candidate.dbcSpellId === effect.spellId)
|
||||
?? triggeredAbilityBySpellId(effect.spellId);
|
||||
if (triggered && triggered.id !== ability.id) {
|
||||
|
||||
@@ -53,6 +53,7 @@ export interface PartyMember {
|
||||
readonly gender: GenderId;
|
||||
readonly appearance: CharacterAppearance;
|
||||
readonly classId: ClassId;
|
||||
readonly secondaryClassId?: ClassId | null;
|
||||
readonly specialization: string;
|
||||
readonly specializationId: string | null;
|
||||
readonly talentTreeId: string;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import generatedCatalog from "./generated/runewakerLootCatalog.json";
|
||||
import { allowedLootDrops } from "./lootPolicy";
|
||||
import { EMPTY_ITEM_STATS } from "./itemStats";
|
||||
import type { InventoryItem } from "./lootTypes";
|
||||
|
||||
@@ -142,5 +143,5 @@ export function rollRunewakerBossLoot(
|
||||
const item = rollSourceSlot(boss, bonusRoll, playerLevel, levelDelta, acquiredAt, random);
|
||||
if (item) drops.push(item);
|
||||
}
|
||||
return drops;
|
||||
return allowedLootDrops(drops, playerLevel);
|
||||
}
|
||||
+6
-1
@@ -4,11 +4,12 @@ import { DEFAULT_DUNGEON_ID, requireDungeonDefinition, type DungeonId } from "./
|
||||
import type { Vector3Tuple, WorldSpawn } from "./dungeonTypes";
|
||||
import type { ActionBindingLayer } from "./actionBindings";
|
||||
import type { GmReviewTarget } from "./gmContentReview";
|
||||
import { validBossDifficulty } from "./dungeonDifficulty";
|
||||
|
||||
export type AssetStatus = "checking" | "loading" | "ready" | "proxy" | "error";
|
||||
export type InputMode = "keyboard" | "gamepad";
|
||||
export type GameMode = "dungeon" | "manastorm";
|
||||
export type GameOverlay = "pause" | "report-issue" | "party" | "inventory" | "map" | "boss-guide" | "spellbook" | "talents" | "bindings" | "options" | "ui-layout" | "keybinds" | "gm" | "dungeon-results" | null;
|
||||
export type GameOverlay = "pause" | "report-issue" | "party" | "inventory" | "boss-cards" | "map" | "boss-guide" | "spellbook" | "talents" | "bindings" | "options" | "ui-layout" | "keybinds" | "gm" | "dungeon-results" | null;
|
||||
|
||||
interface PlayerSnapshot {
|
||||
position: Vector3Tuple;
|
||||
@@ -20,6 +21,7 @@ interface PlayerSnapshot {
|
||||
export interface GameState {
|
||||
activeDungeonId: DungeonId;
|
||||
activeDifficultyId: string;
|
||||
activeBossDifficulty: number;
|
||||
gameMode: GameMode;
|
||||
gmTestMode: boolean;
|
||||
activeGmReviewTarget: GmReviewTarget | null;
|
||||
@@ -54,6 +56,7 @@ export interface GameState {
|
||||
readonly spawn?: WorldSpawn;
|
||||
readonly gmTestMode?: boolean;
|
||||
readonly gmReviewTarget?: GmReviewTarget | null;
|
||||
readonly bossDifficulty?: number;
|
||||
},
|
||||
) => void;
|
||||
openOverlay: (overlay: Exclude<GameOverlay, null>) => void;
|
||||
@@ -82,6 +85,7 @@ const entrance = initialDungeon.entrance;
|
||||
export const useGameStore = create<GameState>((set) => ({
|
||||
activeDungeonId: DEFAULT_DUNGEON_ID,
|
||||
activeDifficultyId: initialDungeon.defaultDifficultyId,
|
||||
activeBossDifficulty: 1,
|
||||
gameMode: "dungeon",
|
||||
gmTestMode: false,
|
||||
activeGmReviewTarget: null,
|
||||
@@ -118,6 +122,7 @@ export const useGameStore = create<GameState>((set) => ({
|
||||
return {
|
||||
activeDungeonId,
|
||||
activeDifficultyId,
|
||||
activeBossDifficulty: validBossDifficulty(options?.bossDifficulty) ? options.bossDifficulty : 1,
|
||||
gameMode: options?.gameMode ?? "dungeon",
|
||||
gmTestMode: options?.gmTestMode ?? false,
|
||||
activeGmReviewTarget: options?.gmReviewTarget ?? null,
|
||||
|
||||
@@ -66,4 +66,12 @@ describe("talent runtime modifiers", () => {
|
||||
expect.objectContaining({ kind: "healing-percent", amount: 5 }),
|
||||
]));
|
||||
});
|
||||
|
||||
it("keeps critical damage bonuses out of ordinary hit damage", () => {
|
||||
const modifiers = parseTalentModifiers("Increases the critical damage of your spells by 150%.")
|
||||
.map((modifier) => ({ ...modifier, sourceTalentId: "test", sourceTalentName: "Thunderous", rank: 1, description: "" }));
|
||||
expect(modifiers).toContainEqual(expect.objectContaining({ kind: "critical-damage-percent", amount: 150 }));
|
||||
expect(applyTalentModifierValue(100, modifiers, "damage-percent", { abilityName: "Arm of Thorim" })).toBe(100);
|
||||
expect(applyTalentModifierValue(2, modifiers, "critical-damage-percent", { abilityName: "Arm of Thorim" })).toBe(5);
|
||||
});
|
||||
});
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
|
||||
export type TalentModifierKind =
|
||||
| "damage-percent"
|
||||
| "critical-damage-percent"
|
||||
| "healing-percent"
|
||||
| "damage-taken-percent"
|
||||
| "max-health-percent"
|
||||
@@ -70,6 +71,8 @@ export function parseTalentModifiers(description: string): readonly ParsedModifi
|
||||
if (/cooldown/.test(lower)) add({ kind: "cooldown-percent", amount, unit: "percent", scope });
|
||||
if (/damage (?:done )?to you|damage you (?:take|suffer)|damage taken/.test(lower)) {
|
||||
add({ kind: "damage-taken-percent", amount, unit: "percent", scope });
|
||||
} else if (/\bcritical damage\b/.test(lower)) {
|
||||
add({ kind: "critical-damage-percent", amount, unit: "percent", scope });
|
||||
} else if (/\bdamage\b/.test(lower)) {
|
||||
add({ kind: "damage-percent", amount, unit: "percent", scope });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { DUNGEON_DEFINITIONS, requireDungeonDefinition } from "./dungeonRegistry";
|
||||
import { wowDungeonMapFor, wowMapContains, wowMapFloorFor, wowMapHeading, wowMapPoint, wowMapWorldPosition, type WowDungeonMap } from "./wowDungeonMap";
|
||||
|
||||
function mapFor(id: string) {
|
||||
const map = wowDungeonMapFor(requireDungeonDefinition(id));
|
||||
expect(map, id).not.toBeNull();
|
||||
return map!;
|
||||
}
|
||||
|
||||
describe("original WoW dungeon map coordinates", () => {
|
||||
it("maps WoW west/north to the top-left, east/south to the bottom-right", () => {
|
||||
const map: WowDungeonMap = { mapId: 1, coordinateTransform: "test", runtimeToWow: [1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0],
|
||||
floors: [{ level: 1, name: "Test", bounds: [0, 100, 0, 150], image: "test", checksum: "test" }] };
|
||||
expect(wowMapPoint(map, map.floors[0], [100, 0, 150])).toEqual([0, 0]);
|
||||
expect(wowMapPoint(map, map.floors[0], [0, 0, 0])).toEqual([1002, 668]);
|
||||
expect(wowMapPoint(map, map.floors[0], [50, 99, 75])).toEqual([501, 334]);
|
||||
expect(wowMapContains(map, map.floors[0], [101, 0, 150])).toBe(false);
|
||||
});
|
||||
|
||||
it("uses authored Wailing Caverns coordinates rather than stretching its world bounds", () => {
|
||||
const dungeon = requireDungeonDefinition("wailing-caverns"), map = mapFor(dungeon.id);
|
||||
const source = wowMapWorldPosition(map, dungeon.entrance.footPosition);
|
||||
expect(source[0]).toBeCloseTo(-dungeon.entrance.footPosition[0], 5);
|
||||
expect(source[1]).toBeCloseTo(dungeon.entrance.footPosition[2], 5);
|
||||
expect(wowMapContains(map, wowMapFloorFor(map, dungeon.entrance.footPosition), dungeon.entrance.footPosition)).toBe(true);
|
||||
});
|
||||
|
||||
it("inverts the shifted Deadmines ADT basis", () => {
|
||||
const dungeon = requireDungeonDefinition("deadmines"), map = mapFor(dungeon.id);
|
||||
const source = wowMapWorldPosition(map, dungeon.entrance.footPosition);
|
||||
expect(source[0]).toBeCloseTo(-16.4, 1);
|
||||
expect(source[1]).toBeCloseTo(-383.07, 1);
|
||||
expect(source[2]).toBeCloseTo(61.78, 1);
|
||||
});
|
||||
|
||||
it("aligns the event-driven Trial of the Champion arena entrance", () => {
|
||||
const dungeon = requireDungeonDefinition("trial-of-the-champion"), map = mapFor(dungeon.id);
|
||||
expect(wowMapWorldPosition(map, dungeon.entrance.footPosition)).toEqual([805.227, 618.038, 412.393]);
|
||||
expect(wowMapContains(map, wowMapFloorFor(map, dungeon.entrance.footPosition), dungeon.entrance.footPosition)).toBe(true);
|
||||
});
|
||||
|
||||
it.each(["hellfire-ramparts", "razorfen-kraul", "scholomance", "dire-maul"])("aligns the rotated %s environment", (id) => {
|
||||
const dungeon = requireDungeonDefinition(id), map = mapFor(id);
|
||||
const floor = wowMapFloorFor(map, dungeon.entrance.footPosition);
|
||||
expect(wowMapContains(map, floor, dungeon.entrance.footPosition)).toBe(true);
|
||||
const heading = wowMapHeading(map, floor, dungeon.entrance.footPosition, 0.7);
|
||||
expect(Math.hypot(...heading)).toBeCloseTo(23, 5);
|
||||
});
|
||||
|
||||
it("allows explicitly selecting the other Deadmines floor", () => {
|
||||
const dungeon = requireDungeonDefinition("deadmines"), map = mapFor(dungeon.id);
|
||||
expect(wowMapFloorFor(map, dungeon.entrance.footPosition, 2).level).toBe(2);
|
||||
expect(wowMapFloorFor(map, dungeon.entrance.footPosition, 999).level).not.toBe(999);
|
||||
});
|
||||
|
||||
it("falls back for non-WoW maps and replaced environments", () => {
|
||||
expect(wowDungeonMapFor(requireDungeonDefinition("forsaken-abbey"))).toBeNull();
|
||||
const dungeon = requireDungeonDefinition("wailing-caverns");
|
||||
expect(wowDungeonMapFor({ ...dungeon, provenance: { ...dungeon.provenance, coordinateTransform: "changed" } })).toBeNull();
|
||||
});
|
||||
|
||||
it("packages complete, checksummed PNG floors with each available map", () => {
|
||||
let count = 0;
|
||||
for (const dungeon of DUNGEON_DEFINITIONS) {
|
||||
const map = wowDungeonMapFor(dungeon);
|
||||
if (!map) continue;
|
||||
count += 1;
|
||||
expect(map.runtimeToWow).toHaveLength(12);
|
||||
expect(map.runtimeToWow.every(Number.isFinite)).toBe(true);
|
||||
expect(new Set(map.floors.map((floor) => floor.level)).size).toBe(map.floors.length);
|
||||
for (const floor of map.floors) {
|
||||
const png = readFileSync(`public${floor.image}`);
|
||||
expect(png.toString("ascii", 1, 4)).toBe("PNG");
|
||||
expect([png.readUInt32BE(16), png.readUInt32BE(20)]).toEqual([1002, 668]);
|
||||
expect(createHash("sha256").update(png).digest("hex")).toBe(floor.checksum);
|
||||
expect(floor.bounds.every(Number.isFinite)).toBe(true);
|
||||
}
|
||||
}
|
||||
expect(count).toBeGreaterThanOrEqual(50);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import catalog from "./generated/wowDungeonMaps.generated.json";
|
||||
import type { DungeonDefinition, Vector3Tuple } from "./dungeonTypes";
|
||||
|
||||
export interface WowMapFloor {
|
||||
readonly level: number;
|
||||
readonly name: string;
|
||||
/** World MinX, MaxX, MinY, MaxY; X points north and Y points west. */
|
||||
readonly bounds: readonly [number, number, number, number];
|
||||
readonly image: string;
|
||||
readonly checksum: string;
|
||||
}
|
||||
export interface WowDungeonMap {
|
||||
readonly mapId: number;
|
||||
readonly coordinateTransform: string;
|
||||
readonly visualChecksum?: string;
|
||||
/** Three row-major affine rows converting package position to WoW XYZ. */
|
||||
readonly runtimeToWow: readonly number[];
|
||||
readonly floors: readonly WowMapFloor[];
|
||||
}
|
||||
const maps = catalog.dungeons as unknown as Readonly<Record<string, WowDungeonMap>>;
|
||||
|
||||
export function wowDungeonMapFor(dungeon: DungeonDefinition): WowDungeonMap | null {
|
||||
const map = maps[dungeon.id];
|
||||
// A replacement environment must be reimported, never silently misaligned.
|
||||
return map?.mapId === dungeon.mapId
|
||||
&& map.coordinateTransform === dungeon.provenance.coordinateTransform
|
||||
&& map.visualChecksum === dungeon.assets.visual[0]?.checksum ? map : null;
|
||||
}
|
||||
|
||||
export function wowMapWorldPosition(map: WowDungeonMap, position: Vector3Tuple): Vector3Tuple {
|
||||
return [0, 1, 2].map((row) => {
|
||||
const i = row * 4;
|
||||
return map.runtimeToWow[i] * position[0] + map.runtimeToWow[i + 1] * position[1]
|
||||
+ map.runtimeToWow[i + 2] * position[2] + map.runtimeToWow[i + 3];
|
||||
}) as unknown as Vector3Tuple;
|
||||
}
|
||||
|
||||
export function wowMapPoint(map: WowDungeonMap, floor: WowMapFloor, position: Vector3Tuple): readonly [number, number] {
|
||||
const [x, y] = wowMapWorldPosition(map, position);
|
||||
const [minX, maxX, minY, maxY] = floor.bounds;
|
||||
return [(maxY - y) / (maxY - minY) * 1002, (maxX - x) / (maxX - minX) * 668];
|
||||
}
|
||||
|
||||
export function wowMapContains(map: WowDungeonMap, floor: WowMapFloor, position: Vector3Tuple): boolean {
|
||||
const [x, y] = wowMapPoint(map, floor, position);
|
||||
return Number.isFinite(x) && Number.isFinite(y) && x >= 0 && x <= 1002 && y >= 0 && y <= 668;
|
||||
}
|
||||
|
||||
/** Smallest containing floor wins; stacked rooms can be selected explicitly in the map panel. */
|
||||
export function wowMapFloorFor(map: WowDungeonMap, position: Vector3Tuple, level?: number | null): WowMapFloor {
|
||||
const explicit = map.floors.find((floor) => floor.level === level);
|
||||
if (explicit) return explicit;
|
||||
const containing = map.floors.filter((floor) => floor.level !== 0 && wowMapContains(map, floor, position));
|
||||
const size = (floor: WowMapFloor) => (floor.bounds[1] - floor.bounds[0]) * (floor.bounds[3] - floor.bounds[2]);
|
||||
return containing.sort((a, b) => size(a) - size(b) || a.level - b.level)[0]
|
||||
?? map.floors.find((floor) => floor.level === 0) ?? map.floors[0];
|
||||
}
|
||||
|
||||
export function wowMapHeading(map: WowDungeonMap, floor: WowMapFloor, position: Vector3Tuple, yaw: number): readonly [number, number] {
|
||||
const [x, y] = wowMapPoint(map, floor, position);
|
||||
const [targetX, targetY] = wowMapPoint(map, floor, [position[0] + Math.sin(yaw), position[1], position[2] + Math.cos(yaw)]);
|
||||
const length = Math.hypot(targetX - x, targetY - y) || 1;
|
||||
return [(targetX - x) / length * 23, (targetY - y) / length * 23];
|
||||
}
|
||||
@@ -45,7 +45,8 @@ import {
|
||||
} from "../game/mobRuntimeRegistry";
|
||||
import { combatHasLineOfSight } from "../game/combatSpatial";
|
||||
import { registerDungeonRuntimeReset } from "../game/dungeonRuntimeReset";
|
||||
import { dungeonEnemyHealthBaseline, scaledDungeonEnemyLevel } from "../game/dungeonLevelScaling";
|
||||
import { scaledDungeonEnemyLevel } from "../game/dungeonLevelScaling";
|
||||
import { dungeonEnemyScaling } from "../game/dungeonBossScaling";
|
||||
import { requireDungeonDefinition } from "../game/dungeonRegistry";
|
||||
import { stepFearedMobAway } from "../game/mobMotion";
|
||||
import {
|
||||
@@ -886,6 +887,9 @@ function ProxyMob({
|
||||
const showAggroRanges = useCombatStore((state) => state.settings.showMobAggroRanges);
|
||||
const gameMode = useGameStore((state) => state.gameMode);
|
||||
const dungeonExpansion = useGameStore((state) => requireDungeonDefinition(state.activeDungeonId).expansion);
|
||||
const dungeon = useGameStore((state) => requireDungeonDefinition(state.activeDungeonId));
|
||||
const difficultyId = useGameStore((state) => state.activeDifficultyId);
|
||||
const bossDifficulty = useGameStore((state) => state.activeBossDifficulty);
|
||||
const characterLevel = useCombatStore((state) => state.level);
|
||||
const manastormLevel = useManastormStore((state) => state.level);
|
||||
const manastormPartySize = useManastormStore((state) => state.partySize);
|
||||
@@ -928,20 +932,20 @@ function ProxyMob({
|
||||
const level = gameMode === "manastorm"
|
||||
? definition.combat?.level
|
||||
: scaledDungeonEnemyLevel(characterLevel, boss);
|
||||
const healthMultiplier = baseHealthMultiplier * (manastormModifiers?.enemyStatMultiplier ?? 1);
|
||||
const healthBaseline = gameMode === "dungeon"
|
||||
? dungeonEnemyHealthBaseline(level ?? characterLevel, dungeonExpansion)
|
||||
: undefined;
|
||||
const scaling = gameMode === "dungeon"
|
||||
? dungeonEnemyScaling(dungeon, definition, instanceId, characterLevel, difficultyId, bossDifficulty)
|
||||
: null;
|
||||
const healthMultiplier = scaling?.healthMultiplier ?? baseHealthMultiplier * (manastormModifiers?.enemyStatMultiplier ?? 1);
|
||||
useCombatStore.getState().registerMob(instanceId, {
|
||||
name: definition.name,
|
||||
serverEntry: definition.combat?.serverEntry,
|
||||
boss,
|
||||
level,
|
||||
maxHealth: healthBaseline === undefined ? undefined : healthBaseline * healthMultiplier,
|
||||
maxHealth: scaling?.maxHealth,
|
||||
hasLoot: Boolean((boss && bossHasLoot(definition.id)) || definition.hasLoot),
|
||||
lootSourceId: boss ? definition.id : undefined,
|
||||
healthMultiplier,
|
||||
damageMultiplier: baseDamageMultiplier * (manastormModifiers?.enemyStatMultiplier ?? 1),
|
||||
damageMultiplier: scaling?.damageMultiplier ?? baseDamageMultiplier * (manastormModifiers?.enemyStatMultiplier ?? 1),
|
||||
bonusLootChance: manastormModifiers?.bonusLootChance,
|
||||
moveSpeed: definition.combat?.moveSpeed,
|
||||
aggroRange: definition.combat?.aggroRange,
|
||||
@@ -968,6 +972,9 @@ function ProxyMob({
|
||||
instanceId,
|
||||
gameMode,
|
||||
dungeonExpansion,
|
||||
dungeon,
|
||||
difficultyId,
|
||||
bossDifficulty,
|
||||
manastormLevel,
|
||||
manastormMapRewardMultiplier,
|
||||
manastormModeId,
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { type Group } from "three";
|
||||
import { classById, raceById, type GenderId, type RaceId } from "../app/characterCatalog";
|
||||
import { classById, isRomClassId, raceById, type GenderId, type RaceId } from "../app/characterCatalog";
|
||||
import { CharacterModel } from "../avatar/CharacterModel";
|
||||
import { ProceduralAvatar } from "../avatar/ProceduralAvatar";
|
||||
import { useCombatStore } from "../game/combatStore";
|
||||
@@ -1412,6 +1412,9 @@ function PartyActors({
|
||||
{members.map((member, index) => {
|
||||
const characterClass = classById(member.classId);
|
||||
const race = raceById(member.raceId);
|
||||
const subtitle = isRomClassId(member.classId)
|
||||
? `${characterClass.name}${member.secondaryClassId ? `/${classById(member.secondaryClassId).name}` : ""}`
|
||||
: `${race.name} · ${member.specialization}`;
|
||||
const avatarMetrics = PARTY_AVATAR_METRICS[`${member.raceId}:${member.gender}`]
|
||||
?? DEFAULT_PARTY_AVATAR_METRICS;
|
||||
const position = positionsRef.current.get(member.id) ?? initialMemberPosition(index);
|
||||
@@ -1471,7 +1474,7 @@ function PartyActors({
|
||||
...partyLabelStyle,
|
||||
opacity: member.health > 0 ? 1 : 0.55,
|
||||
}}>
|
||||
<div>{member.name}<br /><small>{race.name} · {member.specialization}</small></div>
|
||||
<div>{member.name}<br /><small>{subtitle}</small></div>
|
||||
</Html>
|
||||
) : null}
|
||||
</group>
|
||||
|
||||
+6
-1
@@ -527,7 +527,7 @@ kbd { color: var(--moss-bright); font: inherit; font-size: 0.64rem; }
|
||||
top: 50%;
|
||||
right: max(18px, env(safe-area-inset-right));
|
||||
z-index: 30;
|
||||
width: min(470px, calc(100vw - 36px));
|
||||
width: min(760px, calc(100vw - 36px));
|
||||
max-height: calc(100vh - 36px);
|
||||
padding: 18px;
|
||||
transform: translateY(-50%);
|
||||
@@ -540,6 +540,11 @@ kbd { color: var(--moss-bright); font: inherit; font-size: 0.64rem; }
|
||||
}
|
||||
.map-panel header { display: flex; align-items: start; justify-content: space-between; }
|
||||
.map-panel h2 { margin: 0; font-family: Georgia, serif; font-size: 1.5rem; font-weight: 400; }
|
||||
.map-panel__tools { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px; margin-top: 12px; color: var(--muted); font-size: .7rem; }
|
||||
.map-panel__tools label { display: flex; align-items: center; gap: 8px; }
|
||||
.map-panel__tools select { max-width: min(260px, 60vw); padding: 6px 8px; border: 1px solid var(--edge); border-radius: 6px; color: var(--text); background: #122319; }
|
||||
.map-panel__patrol-toggle input { accent-color: #cdb67b; }
|
||||
.map-marker-label { fill: #fff1c0; font-weight: 700; text-anchor: middle; dominant-baseline: central; pointer-events: none; }
|
||||
.icon-button { width: 34px; color: var(--muted); }
|
||||
.map-panel__canvas { margin: 14px 0 12px; overflow: hidden; border: 1px solid rgba(187, 208, 160, 0.14); border-radius: 10px; background: #07140f; }
|
||||
.map-panel svg { display: block; width: 100%; height: auto; }
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { createElement } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { BossCardCollection } from "./BossCardCollection";
|
||||
import { BOSS_CARD_CATALOG } from "../game/bossCardCatalog";
|
||||
|
||||
describe("boss card display", () => {
|
||||
it("shows per-rarity copy totals and every dungeon for the visible boss", () => {
|
||||
const boss = BOSS_CARD_CATALOG[0];
|
||||
const markup = renderToStaticMarkup(createElement(BossCardCollection, {
|
||||
onClose: () => {}, characterName: "Collector",
|
||||
collection: { counts: { [boss.key]: { normal: 7, rare: 1 } }, lastClaims: {} },
|
||||
}));
|
||||
expect(markup).toContain("7 copies");
|
||||
expect(markup).toContain("1 copy");
|
||||
expect(markup).toContain("8 total copies");
|
||||
expect(markup).toContain("Group by dungeon");
|
||||
expect(markup).toContain("Group by rarity");
|
||||
for (const dungeon of boss.dungeons) expect(markup).toContain(dungeon.title);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
import { lazy, Suspense, useMemo, useState, type CSSProperties } from "react";
|
||||
import { selectedCharacter, useShellStore } from "../app/shellStore";
|
||||
import { useCombatStore } from "../game/combatStore";
|
||||
import { useGameStore } from "../game/store";
|
||||
import { BOSS_CARD_CATALOG, groupBossCards, type BossCardGrouping } from "../game/bossCardCatalog";
|
||||
import { BOSS_CARD_RARITIES, BOSS_CARD_RARITY_INFO, emptyBossCardCollection, type BossCardCollection as Collection } from "../game/bossCards";
|
||||
import { useMenuController, type MenuAction } from "../input/useMenuController";
|
||||
import { ControllerButton } from "./ControllerButton";
|
||||
import { dungeonContentIsUsable } from "../content/contentManager";
|
||||
import { useContentManagerSnapshot } from "../content/useContentManager";
|
||||
import "./bossCards.css";
|
||||
|
||||
const BossCardPortrait = lazy(() => import("./BossCardPortrait"));
|
||||
const EMPTY_COLLECTION = emptyBossCardCollection();
|
||||
const PAGE_SIZE = 8;
|
||||
|
||||
export function BossCardCollection({ onClose, collection, characterName }: {
|
||||
readonly onClose: () => void;
|
||||
readonly collection: Collection;
|
||||
readonly characterName: string;
|
||||
}) {
|
||||
useContentManagerSnapshot();
|
||||
const [earnedOnly, setEarnedOnly] = useState(false);
|
||||
const [grouping, setGrouping] = useState<BossCardGrouping>("all");
|
||||
const [category, setCategory] = useState<"all" | "wow" | "rom">("all");
|
||||
const [search, setSearch] = useState("");
|
||||
const [page, setPage] = useState(0);
|
||||
const groups = useMemo(() => groupBossCards(collection, { earnedOnly, grouping, search, category }), [collection, earnedOnly, grouping, search, category]);
|
||||
const rows = useMemo(() => groups.flatMap((group) => group.rows.map((row) => ({ ...row, group }))), [groups]);
|
||||
const pageCount = Math.max(1, Math.ceil(rows.length / PAGE_SIZE));
|
||||
const currentPage = Math.min(page, pageCount - 1);
|
||||
const visibleRows = rows.slice(currentPage * PAGE_SIZE, (currentPage + 1) * PAGE_SIZE);
|
||||
const summary = useMemo(() => {
|
||||
const counts = BOSS_CARD_CATALOG.flatMap((boss) => BOSS_CARD_RARITIES.map((rarity) => collection.counts[boss.key]?.[rarity] ?? 0));
|
||||
return { earned: counts.filter((copies) => copies > 0).length, copies: counts.reduce((sum, copies) => sum + copies, 0), total: counts.length };
|
||||
}, [collection]);
|
||||
const filter = (earned: boolean) => { setEarnedOnly(earned); setPage(0); };
|
||||
const groupBy = (value: BossCardGrouping) => { setGrouping(value); setPage(0); };
|
||||
const actions: MenuAction[] = [
|
||||
{ id: "cards-close", run: onClose },
|
||||
{ id: "cards-all", run: () => filter(false) },
|
||||
{ id: "cards-earned", run: () => filter(true) },
|
||||
{ id: "cards-boss", run: () => groupBy("all") },
|
||||
{ id: "cards-dungeon", run: () => groupBy("dungeon") },
|
||||
{ id: "cards-rarity", run: () => groupBy("rarity") },
|
||||
{ id: "cards-prev", enabled: currentPage > 0, run: () => setPage(currentPage - 1) },
|
||||
{ id: "cards-next", enabled: currentPage + 1 < pageCount, run: () => setPage(currentPage + 1) },
|
||||
];
|
||||
const controller = useMenuController(actions, { initialId: "cards-all", onBack: onClose });
|
||||
const controlProps = { selectedId: controller.selectedId, select: controller.select };
|
||||
|
||||
return <div className="modal-backdrop boss-collection-backdrop" role="presentation" onPointerDown={(event) => event.stopPropagation()}>
|
||||
<section className="boss-collection" role="dialog" aria-modal="true" aria-labelledby="boss-collection-title">
|
||||
<header className="boss-collection__header">
|
||||
<div><p className="eyebrow">{characterName}’s collection log</p><h2 id="boss-collection-title">Boss Cards</h2></div>
|
||||
<ControllerButton {...controlProps} controlId="cards-close" className="icon-button" onClick={onClose} aria-label="Close boss card collection">×</ControllerButton>
|
||||
</header>
|
||||
<div className="boss-collection__summary">
|
||||
<strong>{summary.earned} / {summary.total} cards earned</strong><span>{summary.copies.toLocaleString()} total copies</span>
|
||||
<p>Every boss awards Normal. Rare, Epic, and Legendary roll independently as bonus cards.</p>
|
||||
</div>
|
||||
<div className="boss-collection__tools">
|
||||
<div role="group" aria-label="Collection filter">
|
||||
<ControllerButton {...controlProps} controlId="cards-all" aria-pressed={!earnedOnly} onClick={() => filter(false)}>All</ControllerButton>
|
||||
<ControllerButton {...controlProps} controlId="cards-earned" aria-pressed={earnedOnly} onClick={() => filter(true)}>Earned</ControllerButton>
|
||||
</div>
|
||||
<div role="group" aria-label="Group cards">
|
||||
<ControllerButton {...controlProps} controlId="cards-boss" aria-pressed={grouping === "all"} onClick={() => groupBy("all")}>By boss</ControllerButton>
|
||||
<ControllerButton {...controlProps} controlId="cards-dungeon" aria-pressed={grouping === "dungeon"} onClick={() => groupBy("dungeon")}>Group by dungeon</ControllerButton>
|
||||
<ControllerButton {...controlProps} controlId="cards-rarity" aria-pressed={grouping === "rarity"} onClick={() => groupBy("rarity")}>Group by rarity</ControllerButton>
|
||||
</div>
|
||||
<label className="boss-collection__search">Search<input type="search" placeholder="Boss or dungeon name" value={search} onChange={(event) => { setSearch(event.target.value); setPage(0); }} /></label>
|
||||
<label>World<select value={category} onChange={(event) => { setCategory(event.target.value as typeof category); setPage(0); }}><option value="all">All worlds</option><option value="wow">WoW</option><option value="rom">RoM</option></select></label>
|
||||
</div>
|
||||
<div className="boss-collection__pages" aria-label="Collection pages">
|
||||
<ControllerButton {...controlProps} controlId="cards-prev" disabled={currentPage === 0} onClick={() => setPage(currentPage - 1)}>Previous</ControllerButton>
|
||||
<span role="status">Page {currentPage + 1} / {pageCount} · {rows.length} card{rows.length === 1 ? "" : "s"}{grouping === "dungeon" ? " across dungeons" : ""}</span>
|
||||
<ControllerButton {...controlProps} controlId="cards-next" disabled={currentPage + 1 >= pageCount} onClick={() => setPage(currentPage + 1)}>Next</ControllerButton>
|
||||
</div>
|
||||
<div className="boss-collection__scroll">
|
||||
{visibleRows.length ? <div className="boss-collection__grid">
|
||||
{visibleRows.map((row, index) => {
|
||||
const rarity = BOSS_CARD_RARITY_INFO[row.rarity];
|
||||
return <div className="boss-collection__entry" key={`${row.group.key}:${row.boss.key}:${row.rarity}`}>
|
||||
{grouping !== "all" && (index === 0 || visibleRows[index - 1].group.key !== row.group.key) && <h3 className="boss-collection__group-title">{row.group.title}</h3>}
|
||||
<article className={`boss-card ${row.copies ? "boss-card--earned" : "boss-card--unearned"}`} style={{ "--card-color": rarity.color } as CSSProperties}>
|
||||
<header><span>{rarity.label}</span><small>{rarity.odds}</small></header>
|
||||
<Suspense fallback={<span className="boss-card__model-status">Loading boss model…</span>}><BossCardPortrait model={row.boss.model} name={row.boss.name} installed={row.boss.dungeons.some((entry) => dungeonContentIsUsable(entry.id))} /></Suspense>
|
||||
<div className="boss-card__details"><h3>{row.boss.name}</h3><small>{row.boss.category === "wow" ? "World of Warcraft" : "Runes of Magic"}</small>
|
||||
<p className="boss-card__dungeons">{row.boss.dungeons.map((entry) => entry.title).join(" · ")}</p>
|
||||
</div>
|
||||
<footer><strong>{row.copies.toLocaleString()} {row.copies === 1 ? "copy" : "copies"}</strong><span>{row.copies ? "Earned" : "Not earned"}</span></footer>
|
||||
</article>
|
||||
</div>;
|
||||
})}
|
||||
</div> : <p className="boss-collection__empty">{earnedOnly ? "No earned cards match these filters. Defeat a boss to begin your collection." : "No boss cards match your search."}</p>}
|
||||
</div>
|
||||
</section>
|
||||
</div>;
|
||||
}
|
||||
|
||||
export default function BossCardCollectionScreen({ onClose }: { readonly onClose?: () => void }) {
|
||||
const savedCharacter = useShellStore(selectedCharacter);
|
||||
const activeCharacter = useShellStore((state) => state.activeCharacter);
|
||||
const phase = useShellStore((state) => state.phase);
|
||||
const liveCollection = useCombatStore((state) => state.bossCards);
|
||||
const character = activeCharacter ?? savedCharacter;
|
||||
return <BossCardCollection onClose={onClose ?? (() => useGameStore.getState().closeOverlay())}
|
||||
collection={phase === "game" ? liveCollection : character?.bossCards ?? EMPTY_COLLECTION} characterName={character?.name ?? "Adventurer"} />;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Component, Suspense, useMemo, type ReactNode } from "react";
|
||||
import { Canvas } from "@react-three/fiber";
|
||||
import { Bounds } from "@react-three/drei";
|
||||
import { clone as cloneSkeleton } from "three/examples/jsm/utils/SkeletonUtils.js";
|
||||
import { useGameGLTF } from "../scene/useGameGLTF";
|
||||
import type { MobModelStyle } from "../game/mobPopulation";
|
||||
|
||||
class PortraitBoundary extends Component<{ children: ReactNode }, { failed: boolean }> {
|
||||
state = { failed: false };
|
||||
static getDerivedStateFromError() { return { failed: true }; }
|
||||
render() {
|
||||
return this.state.failed ? <span className="boss-card__model-status">Install this dungeon in Downloads to view its boss model.</span> : this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
function BossModel({ model }: { readonly model: MobModelStyle }) {
|
||||
const gltf = useGameGLTF(model.url);
|
||||
const scene = useMemo(() => cloneSkeleton(gltf.scene), [gltf.scene]);
|
||||
return <group rotation={[0, model.rotationY, 0]} dispose={null}><primitive object={scene} /></group>;
|
||||
}
|
||||
|
||||
/** Static previews render only when loaded/resized; pagination limits simultaneous WebGL contexts. */
|
||||
export default function BossCardPortrait({ model, name, installed }: { readonly model?: MobModelStyle; readonly name: string; readonly installed: boolean }) {
|
||||
if (!installed) return <span className="boss-card__model-status">Install this dungeon in Downloads to view its boss model.</span>;
|
||||
if (!model) return <span className="boss-card__model-status">Boss model not yet available</span>;
|
||||
return <PortraitBoundary key={model.url}>
|
||||
<div className="boss-card__portrait" role="img" aria-label={`${name} boss model`}>
|
||||
<Canvas frameloop="demand" dpr={1} camera={{ fov: 35, position: [3, 2, 5] }}
|
||||
gl={{ alpha: true, antialias: true, powerPreference: "low-power" }} fallback={<span>3D preview unavailable</span>}>
|
||||
<ambientLight intensity={1.5} />
|
||||
<hemisphereLight args={["#ede3cf", "#303040", 2]} />
|
||||
<directionalLight position={[3, 5, 6]} intensity={2.5} />
|
||||
<Suspense fallback={null}>
|
||||
<Bounds fit clip observe margin={1.25} maxDuration={0}>
|
||||
<BossModel model={model} />
|
||||
</Bounds>
|
||||
</Suspense>
|
||||
</Canvas>
|
||||
</div>
|
||||
</PortraitBoundary>;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, type CSSProperties } from "react";
|
||||
import { BOSS_CARD_RARITY_INFO } from "../game/bossCards";
|
||||
import { useCombatStore } from "../game/combatStore";
|
||||
import { useGameStore } from "../game/store";
|
||||
import { classCanEquipItem, PLAYER_EQUIPMENT_OWNER_ID } from "../game/equipment";
|
||||
@@ -83,6 +84,9 @@ export function BossLootWindow() {
|
||||
<div className="boss-loot-window__body">
|
||||
<p className="boss-loot-window__eyebrow">Loot acquired automatically</p>
|
||||
<div className="boss-loot-window__items">
|
||||
{reward.cards?.rarities.map((rarity) => <article key={rarity} className="boss-loot-window__item" style={{ "--item-quality": BOSS_CARD_RARITY_INFO[rarity].color } as CSSProperties}>
|
||||
<b aria-hidden="true">◆</b><p><strong>{BOSS_CARD_RARITY_INFO[rarity].label} Boss Card</strong><small>{reward.bossName} · Collection log</small></p><em>+1</em>
|
||||
</article>)}
|
||||
{reward.items.map((item) => {
|
||||
const comparison = classCanEquipItem(classId, item)
|
||||
? compareEquipmentUpgrade(item, loadout, profile, heirloomContext)
|
||||
@@ -100,15 +104,15 @@ export function BossLootWindow() {
|
||||
{item.quantity > 1 ? <em>×{item.quantity}</em> : null}
|
||||
</article>;
|
||||
})}
|
||||
{!reward.items.length ? (
|
||||
{!reward.items.length && !reward.cards?.rarities.length ? (
|
||||
<p className="boss-loot-window__empty">No items dropped this time.</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<span>{reward.items.length} item{reward.items.length === 1 ? "" : "s"} added to shared inventory</span>
|
||||
<button type="button" onClick={viewInventory}>View inventory</button>
|
||||
<span>{reward.cards?.rarities.length ? `${reward.cards.rarities.length} card${reward.cards.rarities.length === 1 ? "" : "s"} added to collection${reward.items.length ? ` · ${reward.items.length} items acquired` : ""}` : `${reward.items.length} items added to shared inventory`}</span>
|
||||
{reward.cards ? <button type="button" onClick={() => { dismiss(); useGameStore.getState().openOverlay("boss-cards"); }}>View collection</button> : <button type="button" onClick={viewInventory}>View inventory</button>}
|
||||
</footer>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -43,6 +43,7 @@ function safePercent(current: number, maximum: number): number {
|
||||
function overlayName(overlay: GameOverlay, romSkills = false): string {
|
||||
if (overlay === "party") return "Party management";
|
||||
if (overlay === "inventory") return "Inventory & equipment";
|
||||
if (overlay === "boss-cards") return "Boss card collection";
|
||||
if (overlay === "spellbook") return "Spellbook";
|
||||
if (overlay === "talents") return romSkills ? "Skills" : "Talents";
|
||||
if (overlay === "bindings") return "Ability controls";
|
||||
@@ -462,7 +463,7 @@ export function CompanionPanel({ snapshot, send, preferControllerBinds = false }
|
||||
</section>
|
||||
<div className="companion-tactical__overview">
|
||||
<section className="companion-map-card">
|
||||
<DungeonMapGraphic position={snapshot.game.playerPosition} yaw={snapshot.game.cameraYaw} compact />
|
||||
<DungeonMapGraphic position={snapshot.game.playerPosition} yaw={snapshot.game.cameraYaw} dungeonId={snapshot.game.dungeonId} compact />
|
||||
<div>
|
||||
<span>{snapshot.game.overlay ? "Upper display" : "Current area"}</span>
|
||||
<strong>{snapshot.game.overlay ? overlayName(snapshot.game.overlay, romSkills) : snapshot.game.areaName}</strong>
|
||||
@@ -495,6 +496,7 @@ export function CompanionPanel({ snapshot, send, preferControllerBinds = false }
|
||||
<button type="button" className={snapshot.game.overlay === "spellbook" ? "is-active" : ""} aria-pressed={snapshot.game.overlay === "spellbook"} onClick={() => toggleDestination("spellbook")}>Spells</button>
|
||||
<button type="button" className={snapshot.game.overlay === "talents" ? "is-active" : ""} aria-pressed={snapshot.game.overlay === "talents"} onClick={() => toggleDestination("talents")}>{skillMenuLabel}</button>
|
||||
<button type="button" className={snapshot.game.overlay === "inventory" ? "is-active" : ""} aria-pressed={snapshot.game.overlay === "inventory"} onClick={() => toggleDestination("inventory")}>Gear</button>
|
||||
<button type="button" className={snapshot.game.overlay === "boss-cards" ? "is-active" : ""} aria-pressed={snapshot.game.overlay === "boss-cards"} onClick={() => toggleDestination("boss-cards")}>Cards</button>
|
||||
<button type="button" className={snapshot.game.overlay === "bindings" ? "is-active" : ""} aria-pressed={snapshot.game.overlay === "bindings"} onClick={() => toggleDestination("bindings")}>Controls</button>
|
||||
<button type="button" className={snapshot.game.overlay === "options" ? "is-active" : ""} aria-pressed={snapshot.game.overlay === "options"} onClick={() => toggleDestination("options")}>Options</button>
|
||||
<button type="button" className={snapshot.game.overlay === "pause" ? "is-active" : ""} aria-pressed={snapshot.game.overlay === "pause"} onClick={() => toggleDestination("pause")}>{snapshot.game.overlay === "pause" ? "Resume" : "Pause"}</button>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { bossDifficultyLabel, bossDifficultyMultipliers } from "../game/dungeonDifficulty";
|
||||
import { ControllerButton } from "./ControllerButton";
|
||||
import "./dungeonRuns.css";
|
||||
|
||||
export function DungeonDifficultySlider({ value, onChange, selectedId, select }: {
|
||||
readonly value: number;
|
||||
readonly onChange: (value: number) => void;
|
||||
readonly selectedId: string;
|
||||
readonly select: (id: string) => void;
|
||||
}) {
|
||||
const multipliers = bossDifficultyMultipliers(value);
|
||||
const controls = { selectedId, select };
|
||||
return <section className="dungeon-difficulty" aria-label="Boss difficulty">
|
||||
<label htmlFor="boss-difficulty"><strong>Boss difficulty</strong><span>{bossDifficultyLabel(value)}</span></label>
|
||||
<div className="dungeon-difficulty__control">
|
||||
<ControllerButton {...controls} controlId="boss-difficulty-down" className="front-button" aria-label="Decrease boss difficulty"
|
||||
disabled={value === 1} onClick={() => onChange(value - 1)}>−</ControllerButton>
|
||||
<input id="boss-difficulty" aria-label="Boss difficulty" type="range" min="1" max="10" step="1" value={value}
|
||||
aria-valuetext={bossDifficultyLabel(value)} onChange={(event) => onChange(Number(event.target.value))} />
|
||||
<ControllerButton {...controls} controlId="boss-difficulty-up" className="front-button" aria-label="Increase boss difficulty"
|
||||
disabled={value === 10} onClick={() => onChange(value + 1)}>+</ControllerButton>
|
||||
</div>
|
||||
<p>Boss health ×{multipliers.health.toFixed(2)} · Damage ×{multipliers.damage.toFixed(2)}</p>
|
||||
<small>Easy targets 30-second bosses and 45-second final bosses. Difficulty is fixed for the expedition. Each level has its own leaderboard.</small>
|
||||
</section>;
|
||||
}
|
||||
@@ -55,7 +55,7 @@ export function DungeonLeaderboard({ dungeonTitle, data, scope, loading, filter,
|
||||
<time>{formatDungeonDuration(entry.durationMs)}</time><em aria-hidden="true">{expanded.has(entry.id) ? "−" : "+"}</em>
|
||||
</ControllerButton>
|
||||
{expanded.has(entry.id) ? <div id={`run-details-${entry.id}`}><DungeonRunDetails dungeonId={entry.dungeonId}
|
||||
difficultyId={entry.difficultyId} party={entry.party} killedBossIds={entry.killedBossIds} /></div> : null}
|
||||
difficultyId={entry.difficultyId} bossDifficulty={entry.bossDifficulty} party={entry.party} killedBossIds={entry.killedBossIds} /></div> : null}
|
||||
</li>)}
|
||||
</ol>}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { createElement } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { requireDungeonDefinition } from "../game/dungeonRegistry";
|
||||
import { DungeonMapGraphic } from "./DungeonMapGraphic";
|
||||
|
||||
function render(id: string, options: { compact?: boolean; showPatrols?: boolean; floorLevel?: number } = {}) {
|
||||
const dungeon = requireDungeonDefinition(id);
|
||||
return renderToStaticMarkup(createElement(DungeonMapGraphic, { dungeonId: id, position: dungeon.entrance.footPosition, yaw: 0, ...options }));
|
||||
}
|
||||
|
||||
describe("dungeon map artwork", () => {
|
||||
it("renders authentic Wailing Caverns artwork and player/entrance/numbered encounters", () => {
|
||||
const html = render("wailing-caverns");
|
||||
expect(html).toContain("wailing-caverns/maps/floor-1.png");
|
||||
expect(html).toContain('viewBox="0 0 1002 668"');
|
||||
expect(html).toContain("map-player");
|
||||
expect(html).toContain("Dungeon entrance");
|
||||
expect(html).toContain("Lady Anacondra");
|
||||
expect(html).toContain("map-marker-label");
|
||||
expect(html).not.toContain("map-contour");
|
||||
expect(html).not.toContain("map-patrol");
|
||||
});
|
||||
it("keeps the compact map clear of labels and patrols", () => {
|
||||
const html = render("wailing-caverns", { compact: true, showPatrols: true });
|
||||
expect(html).toContain("original game map");
|
||||
expect(html).not.toContain("map-marker-label");
|
||||
expect(html).not.toContain("map-patrol");
|
||||
});
|
||||
it("shows explicitly selected floor artwork", () => {
|
||||
expect(render("deadmines", { floorLevel: 2 })).toContain("deadmines/maps/floor-2.png");
|
||||
});
|
||||
it("preserves the survey fallback for RuneWaker dungeons", () => {
|
||||
const html = render("forsaken-abbey");
|
||||
expect(html).toContain('viewBox="0 0 430 300"');
|
||||
expect(html).not.toContain("<image");
|
||||
expect(html).toContain('data-artwork="unavailable"');
|
||||
});
|
||||
it("only draws patrol overlays on request", () => {
|
||||
expect(render("wailing-caverns", { showPatrols: true })).toContain("map-patrol");
|
||||
});
|
||||
});
|
||||
@@ -1,49 +1,78 @@
|
||||
import { useEffect, useId, useState } from "react";
|
||||
import { requireDungeonDefinition } from "../game/dungeonRegistry";
|
||||
import { useGameStore } from "../game/store";
|
||||
import { resolveContentUrl } from "../content/contentManager";
|
||||
import { wowDungeonMapFor, wowMapContains, wowMapFloorFor, wowMapHeading, wowMapPoint } from "../game/wowDungeonMap";
|
||||
|
||||
export function DungeonMapGraphic({ position, yaw, compact = false, dungeonId }: {
|
||||
export type MapArtworkStatus = "loading" | "ready" | "unavailable";
|
||||
|
||||
export function DungeonMapGraphic({ position, yaw, compact = false, dungeonId, floorLevel, showPatrols = false, onArtworkStatus }: {
|
||||
position: readonly [number, number, number];
|
||||
yaw: number;
|
||||
compact?: boolean;
|
||||
dungeonId?: string;
|
||||
floorLevel?: number | null;
|
||||
showPatrols?: boolean;
|
||||
onArtworkStatus?: (status: MapArtworkStatus) => void;
|
||||
}) {
|
||||
const activeDungeonId = useGameStore((state) => state.activeDungeonId);
|
||||
const dungeon = requireDungeonDefinition(dungeonId ?? activeDungeonId);
|
||||
const { width: viewWidth, height: viewHeight, padding, contours } = dungeon.presentation.map;
|
||||
const map = wowDungeonMapFor(dungeon);
|
||||
const floor = map ? wowMapFloorFor(map, position, floorLevel) : null;
|
||||
const [failedImage, setFailedImage] = useState<string | null>(null);
|
||||
const [loadedImage, setLoadedImage] = useState<string | null>(null);
|
||||
const artwork = map && floor && failedImage !== floor.image ? floor : null;
|
||||
const status = !artwork ? "unavailable" : loadedImage === artwork.image ? "ready" : "loading";
|
||||
useEffect(() => { onArtworkStatus?.(status); }, [onArtworkStatus, status]);
|
||||
const presentation = dungeon.presentation.map;
|
||||
const viewWidth = artwork ? 1002 : presentation.width;
|
||||
const viewHeight = artwork ? 668 : presentation.height;
|
||||
const { padding, contours } = presentation;
|
||||
const width = Math.max(1, dungeon.bounds.max[0] - dungeon.bounds.min[0]);
|
||||
const depth = Math.max(1, dungeon.bounds.max[2] - dungeon.bounds.min[2]);
|
||||
const project = (point: readonly [number, number, number]): readonly [number, number] => [
|
||||
padding + ((point[0] - dungeon.bounds.min[0]) / width) * (viewWidth - padding * 2),
|
||||
padding + ((point[2] - dungeon.bounds.min[2]) / depth) * (viewHeight - padding * 2),
|
||||
];
|
||||
const project = (point: readonly [number, number, number]): readonly [number, number] => artwork && map
|
||||
? wowMapPoint(map, artwork, point)
|
||||
: [padding + ((point[0] - dungeon.bounds.min[0]) / width) * (viewWidth - padding * 2),
|
||||
padding + ((point[2] - dungeon.bounds.min[2]) / depth) * (viewHeight - padding * 2)];
|
||||
const visible = (point: readonly [number, number, number]) => !artwork || !map || wowMapContains(map, artwork, point);
|
||||
const [playerX, playerY] = project(position);
|
||||
const headingX = Math.sin(yaw) * 12;
|
||||
const headingY = Math.cos(yaw) * 12;
|
||||
const id = `${dungeon.id}-${compact ? "companion" : "dialog"}-map`;
|
||||
const [headingX, headingY] = artwork && map ? wowMapHeading(map, artwork, position, yaw) : [Math.sin(yaw) * 12, Math.cos(yaw) * 12];
|
||||
const markerScale = artwork ? 2 : 1;
|
||||
const id = `dungeon-map-${useId().replace(/:/g, "")}`;
|
||||
return (
|
||||
<svg viewBox={`0 0 ${viewWidth} ${viewHeight}`} role="img" aria-label={`${dungeon.title} patrol and encounter map`}>
|
||||
<svg viewBox={`0 0 ${viewWidth} ${viewHeight}`} role="img" aria-label={`${dungeon.title}${artwork ? ` — ${artwork.name}, original game map` : " survey map"}`} data-artwork={status} data-floor={artwork?.level}>
|
||||
<defs>
|
||||
<filter id={`${id}-glow`}><feGaussianBlur stdDeviation="2.5" result="blur" /></filter>
|
||||
<pattern id={`${id}-grid`} width="28" height="28" patternUnits="userSpaceOnUse">
|
||||
<path d="M 28 0 L 0 0 0 28" fill="none" stroke="rgba(154,190,139,.09)" strokeWidth="1" />
|
||||
</pattern>
|
||||
<clipPath id={`${id}-clip`}><rect width={viewWidth} height={viewHeight} /></clipPath>
|
||||
</defs>
|
||||
<rect width={viewWidth} height={viewHeight} fill={`url(#${id}-grid)`} />
|
||||
{contours.map((path, index) => (
|
||||
<path key={path} className={index === 0 ? "map-contour" : "map-contour map-contour--thin"} d={path} />
|
||||
))}
|
||||
{!compact && dungeon.roamingPacks.map((pack) => {
|
||||
const points = pack.waypoints.map((waypoint) => project(waypoint).join(",")).join(" ");
|
||||
const [startX, startY] = project(pack.waypoints[0]);
|
||||
return <g key={pack.id} className="map-patrol"><polyline points={points} /><circle cx={startX} cy={startY} r="3" /><title>{pack.name}</title></g>;
|
||||
})}
|
||||
{dungeon.areas.map((checkpoint, index) => {
|
||||
const [x, y] = project(checkpoint.center);
|
||||
return <g key={checkpoint.id} className={index === 0 ? "map-anchor map-anchor--entrance" : "map-anchor"}><circle cx={x} cy={y} r={compact ? 4 : 5} /><title>{checkpoint.name}</title></g>;
|
||||
})}
|
||||
<circle className="map-player-glow" cx={playerX} cy={playerY} r="10" filter={`url(#${id}-glow)`} />
|
||||
<circle className="map-player" cx={playerX} cy={playerY} r="5.5" />
|
||||
<line className="map-heading" x1={playerX} y1={playerY} x2={playerX + headingX} y2={playerY + headingY} />
|
||||
{artwork ? <image key={artwork.image} href={resolveContentUrl(artwork.image)} width="1002" height="668" preserveAspectRatio="none"
|
||||
onLoad={() => setLoadedImage(artwork.image)} onError={() => setFailedImage(artwork.image)} />
|
||||
: contours.map((path, index) => <path key={path} className={index === 0 ? "map-contour" : "map-contour map-contour--thin"} d={path} />)}
|
||||
<g clipPath={`url(#${id}-clip)`}>
|
||||
{!compact && showPatrols && dungeon.roamingPacks.filter((pack) => pack.waypoints.length > 0).map((pack) => {
|
||||
const points = pack.waypoints.map((waypoint) => project(waypoint).join(",")).join(" ");
|
||||
return <g key={pack.id} className="map-patrol"><polyline points={points} style={{ strokeWidth: markerScale * 1.5 }} /><title>{pack.name}</title></g>;
|
||||
})}
|
||||
{visible(dungeon.entrance.footPosition) && (() => {
|
||||
const [x, y] = project(dungeon.entrance.footPosition);
|
||||
return <g className="map-anchor map-anchor--entrance"><circle cx={x} cy={y} r={markerScale * 5} style={{ strokeWidth: markerScale * 1.5 }} /><title>Dungeon entrance</title></g>;
|
||||
})()}
|
||||
{dungeon.objectiveOrder.filter((objective) => visible(objective.position)).map((objective) => {
|
||||
const index = dungeon.objectiveOrder.indexOf(objective);
|
||||
const [x, y] = project(objective.position);
|
||||
return <g key={objective.id} className="map-anchor"><circle cx={x} cy={y} r={markerScale * (compact ? 4 : 8)} style={{ strokeWidth: markerScale * 1.5 }} />
|
||||
{!compact && <text className="map-marker-label" x={x} y={y} fontSize={markerScale * 9}>{index + 1}</text>}<title>{`${index + 1}. ${objective.name}`}</title></g>;
|
||||
})}
|
||||
{visible(position) && <g><title>You are here — camera direction</title>
|
||||
<circle className="map-player-glow" cx={playerX} cy={playerY} r={markerScale * 10} filter={`url(#${id}-glow)`} />
|
||||
<circle className="map-player" cx={playerX} cy={playerY} r={markerScale * 5.5} style={{ strokeWidth: markerScale * 2 }} />
|
||||
<line className="map-heading" x1={playerX} y1={playerY} x2={playerX + headingX} y2={playerY + headingY} style={{ strokeWidth: markerScale * 2.5 }} />
|
||||
</g>}
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import catalog from "../game/generated/dungeonLeaderboardCatalog.generated.json";
|
||||
import type { DungeonRunMember } from "../game/dungeonRunTypes";
|
||||
import { bossDifficultyLabel } from "../game/dungeonDifficulty";
|
||||
|
||||
const className = (id: string) => catalog.classes.find((entry) => entry.id === id)?.name ?? id;
|
||||
|
||||
export function DungeonRunDetails({ dungeonId, difficultyId, party, killedBossIds }: {
|
||||
export function DungeonRunDetails({ dungeonId, difficultyId, bossDifficulty, party, killedBossIds }: {
|
||||
readonly bossDifficulty?: number;
|
||||
readonly dungeonId: string;
|
||||
readonly difficultyId: string;
|
||||
readonly party: readonly DungeonRunMember[];
|
||||
@@ -13,6 +15,7 @@ export function DungeonRunDetails({ dungeonId, difficultyId, party, killedBossId
|
||||
const ids = dungeon?.difficulties.find((entry) => entry.id === difficultyId)?.bossIds ?? [];
|
||||
const bosses = dungeon?.bosses.filter((boss) => ids.includes(boss.id)) ?? [];
|
||||
return <div className="dungeon-run-details">
|
||||
<p>{bossDifficultyLabel(bossDifficulty)} · {difficultyId === "heroic" ? "Heroic" : "Normal"}</p>
|
||||
<h4>Party</h4>
|
||||
<ul className="dungeon-run-party">
|
||||
{party.map((member, index) => <li key={`${member.name}-${index}`}>
|
||||
|
||||
@@ -64,7 +64,7 @@ export function DungeonRunResultsDialog({ progress, ranking, onFinish, onLoot, o
|
||||
{!progress.timed ? <p className="dungeon-results__note">This resumed run has no saved timer. Start a new expedition to record a leaderboard time.</p> : null}
|
||||
{ranking.scope === "device" ? <p className="dungeon-results__note">Your time is saved on this device. Reconnect to submit it to the world leaderboard.</p> : null}
|
||||
<div className="dungeon-results__details"><DungeonRunDetails dungeonId={progress.dungeonId}
|
||||
difficultyId={progress.difficultyId} party={progress.party} killedBossIds={progress.killedBossIds} /></div>
|
||||
difficultyId={progress.difficultyId} bossDifficulty={progress.bossDifficulty} party={progress.party} killedBossIds={progress.killedBossIds} /></div>
|
||||
<footer>
|
||||
<ControllerButton {...controls} controlId="run-loot" className="button" onClick={onLoot}>Stay and loot</ControllerButton>
|
||||
<ControllerButton {...controls} controlId="run-finish" className="button button--primary" onClick={onFinish}>Return to main menu</ControllerButton>
|
||||
|
||||
@@ -31,6 +31,8 @@ import { useOnlineGroupStore } from "../app/onlineGroupStore";
|
||||
import { specializationById } from "../game/specializationCatalog";
|
||||
import { DUNGEON_RELEASE_STAGE_LABELS } from "../game/dungeonReleaseStage";
|
||||
import { useDungeonReports } from "./useDungeonReports";
|
||||
import { DungeonDifficultySlider } from "./DungeonDifficultySlider";
|
||||
import { bossDifficultyLabel } from "../game/dungeonDifficulty";
|
||||
|
||||
installDungeonShellRuntime();
|
||||
|
||||
@@ -82,10 +84,11 @@ export function DungeonSelectScreen() {
|
||||
const selectedDefinition = selectedDungeon ? dungeonDefinitionById(selectedDungeon.id) : null;
|
||||
const [clearFilter, setClearFilter] = useState<DungeonClearFilter>("final");
|
||||
const [difficultyId, setDifficultyId] = useState("");
|
||||
const [bossDifficulty, setBossDifficulty] = useState(1);
|
||||
const [expandedRuns, setExpandedRuns] = useState<ReadonlySet<string>>(new Set());
|
||||
const leaderboardDifficulty = selectedDefinition?.difficultyVariants.find((variant) => variant.id === difficultyId)
|
||||
?? selectedDefinition?.difficultyVariants.find((variant) => variant.id === selectedDefinition.defaultDifficultyId);
|
||||
const leaderboard = useDungeonLeaderboard(selectedDungeon?.id ?? "", leaderboardDifficulty?.id ?? "normal", clearFilter);
|
||||
const leaderboard = useDungeonLeaderboard(selectedDungeon?.id ?? "", leaderboardDifficulty?.id ?? "normal", clearFilter, bossDifficulty);
|
||||
const toggleRun = (id: string) => setExpandedRuns((current) => {
|
||||
const next = new Set(current); if (next.has(id)) next.delete(id); else next.add(id); return next;
|
||||
});
|
||||
@@ -121,7 +124,7 @@ export function DungeonSelectScreen() {
|
||||
const continueAsRole = async (role: PartyRole) => {
|
||||
if (!selectedDungeon?.available || !character) return;
|
||||
if (!onlineGroup || !session || session.kind !== "account") {
|
||||
enterDungeon(selectedDungeon.id, role);
|
||||
enterDungeon(selectedDungeon.id, role, { difficultyId: leaderboardDifficulty?.id, bossDifficulty });
|
||||
return;
|
||||
}
|
||||
if (!await updateOnlineMember(session, character, role)) return;
|
||||
@@ -132,6 +135,8 @@ export function DungeonSelectScreen() {
|
||||
await startOnlineActivity(session, {
|
||||
type: "dungeon",
|
||||
contentId: selectedDungeon.id,
|
||||
difficultyId: leaderboardDifficulty?.id,
|
||||
bossDifficulty,
|
||||
partySize: 5,
|
||||
});
|
||||
};
|
||||
@@ -199,6 +204,8 @@ export function DungeonSelectScreen() {
|
||||
{ id: "leaderboard-full", run: () => setClearFilter("full") },
|
||||
{ id: "leaderboard-difficulty", run: cycleDifficulty, enabled: (selectedDefinition?.difficultyVariants.length ?? 0) > 1 },
|
||||
{ id: "leaderboard-refresh", run: leaderboard.refresh },
|
||||
{ id: "boss-difficulty-down", run: () => setBossDifficulty((value) => Math.max(1, value - 1)), enabled: bossDifficulty > 1 },
|
||||
{ id: "boss-difficulty-up", run: () => setBossDifficulty((value) => Math.min(10, value + 1)), enabled: bossDifficulty < 10 },
|
||||
...leaderboard.data.entries.map((entry) => ({ id: `leaderboard-run-${entry.id}`, run: () => toggleRun(entry.id) })),
|
||||
{ id: "leaderboard-prev", run: () => leaderboard.setOffset(Math.max(0, leaderboard.offset - DUNGEON_LEADERBOARD_PAGE_SIZE)),
|
||||
enabled: !leaderboard.loading && leaderboard.offset > 0 },
|
||||
@@ -219,7 +226,7 @@ export function DungeonSelectScreen() {
|
||||
run: () => setRolePickerOpen(false),
|
||||
}] : []),
|
||||
{ id: "dungeon-back", run: returnToMainMenu },
|
||||
], [availableRoles, categoryDungeons, downloading, firstCategoryDungeon, pendingDownload, returnToMainMenu, rolePickerOpen, selectedCategory, selectedDungeon, selectedDefinition, leaderboard, expandedRuns, leaderboardDifficulty]);
|
||||
], [availableRoles, categoryDungeons, downloading, firstCategoryDungeon, pendingDownload, returnToMainMenu, rolePickerOpen, selectedCategory, selectedDungeon, selectedDefinition, leaderboard, expandedRuns, leaderboardDifficulty, bossDifficulty]);
|
||||
const controller = useMenuController(actions, {
|
||||
initialId: firstAvailable ? `dungeon-${firstAvailable.id}` : "dungeon-back",
|
||||
onBack: pendingDownload
|
||||
@@ -318,7 +325,7 @@ export function DungeonSelectScreen() {
|
||||
<DungeonLeaderboard dungeonTitle={selectedDungeon.title} data={leaderboard.data} scope={leaderboard.scope}
|
||||
loading={leaderboard.loading} filter={clearFilter} onFilter={setClearFilter}
|
||||
expanded={expandedRuns} onExpand={toggleRun} offset={leaderboard.offset} onPage={leaderboard.setOffset}
|
||||
onRefresh={leaderboard.refresh} difficultyLabel={leaderboardDifficulty?.difficulty === "heroic" ? "Heroic" : "Normal"}
|
||||
onRefresh={leaderboard.refresh} difficultyLabel={`${bossDifficultyLabel(bossDifficulty)} · ${leaderboardDifficulty?.difficulty === "heroic" ? "Heroic" : "Normal"}`}
|
||||
onDifficulty={(selectedDefinition?.difficultyVariants.length ?? 0) > 1 ? cycleDifficulty : undefined}
|
||||
selectedId={controller.selectedId} select={controller.select} />
|
||||
<section className="dungeon-select-copy">
|
||||
@@ -326,6 +333,8 @@ export function DungeonSelectScreen() {
|
||||
<h2>{selectedDungeon.title}</h2>
|
||||
<span className="dungeon-release-stage">{DUNGEON_RELEASE_STAGE_LABELS[selectedDungeon.releaseStage]}</span>
|
||||
<p>{selectedDungeon.summary}</p>
|
||||
<DungeonDifficultySlider value={bossDifficulty} onChange={setBossDifficulty}
|
||||
selectedId={controller.selectedId} select={controller.select} />
|
||||
{!canStart && selectedDungeon.availabilityReason && (
|
||||
<p className="front-notice" role="status">{selectedDungeon.availabilityReason}</p>
|
||||
)}
|
||||
|
||||
@@ -24,6 +24,7 @@ const PartyManagementPanel = lazy(() => loadPartyManagementPanel().then((module)
|
||||
const BossGuidePanel = lazy(() => loadBossGuidePanel().then((module) => ({ default: module.BossGuidePanel })));
|
||||
|
||||
const TalentGameplayPanel = lazy(loadTalentGameplayPanel);
|
||||
const BossCardCollectionScreen = lazy(() => import("./BossCardCollection"));
|
||||
|
||||
function TalentsPanelLoading() {
|
||||
const openOverlay = useGameStore((state) => state.openOverlay);
|
||||
@@ -101,6 +102,7 @@ export function GameplayPanels() {
|
||||
|
||||
return (
|
||||
<>
|
||||
{overlay === "boss-cards" ? <Suspense fallback={null}><BossCardCollectionScreen /></Suspense> : null}
|
||||
{overlay === "party"
|
||||
? PreloadedPartyManagementPanel
|
||||
? <PreloadedPartyManagementPanel />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { lazy, Suspense, useMemo, useState } from "react";
|
||||
import { classById, FACTIONS, raceById } from "../app/characterCatalog";
|
||||
import { selectedCharacter, useShellStore } from "../app/shellStore";
|
||||
import { DualDisplayFrame } from "../components/DualDisplayFrame";
|
||||
@@ -11,6 +11,7 @@ import { BrandMark, ControllerLegend, FrontSurface } from "./FrontSurface";
|
||||
import { OnlineGroupPanel } from "./OnlineGroupPanel";
|
||||
import { sessionCanUseShelvedGmTools, SHELVED_MODES_ENABLED } from "../app/releaseFeatures";
|
||||
import { DungeonReportsScreen } from "./DungeonReportsScreen";
|
||||
const BossCardCollectionScreen = lazy(() => import("./BossCardCollection"));
|
||||
|
||||
const GAME_MODES = [
|
||||
{
|
||||
@@ -66,14 +67,17 @@ const GAME_MODES = [
|
||||
export function MainMenuScreen() {
|
||||
const [contentManagerOpen, setContentManagerOpen] = useState(false);
|
||||
const [reportsOpen, setReportsOpen] = useState(false);
|
||||
const [cardsOpen, setCardsOpen] = useState(false);
|
||||
if (cardsOpen) return <Suspense fallback={<p role="status">Opening collection log…</p>}><BossCardCollectionScreen onClose={() => setCardsOpen(false)} /></Suspense>;
|
||||
if (contentManagerOpen) return <ContentManagerScreen onClose={() => setContentManagerOpen(false)} />;
|
||||
if (reportsOpen) return <DungeonReportsScreen onClose={() => setReportsOpen(false)} />;
|
||||
return <MainMenuDestinations onOpenDownloads={() => setContentManagerOpen(true)} onOpenReports={() => setReportsOpen(true)} />;
|
||||
return <MainMenuDestinations onOpenDownloads={() => setContentManagerOpen(true)} onOpenReports={() => setReportsOpen(true)} onOpenCards={() => setCardsOpen(true)} />;
|
||||
}
|
||||
|
||||
function MainMenuDestinations({ onOpenDownloads, onOpenReports }: {
|
||||
function MainMenuDestinations({ onOpenDownloads, onOpenReports, onOpenCards }: {
|
||||
readonly onOpenDownloads: () => void;
|
||||
readonly onOpenReports: () => void;
|
||||
readonly onOpenCards: () => void;
|
||||
}) {
|
||||
const activeCharacter = useShellStore((state) => state.activeCharacter);
|
||||
const rosterCharacter = useShellStore(selectedCharacter);
|
||||
@@ -117,7 +121,8 @@ function MainMenuDestinations({ onOpenDownloads, onOpenReports }: {
|
||||
...(character ? [{ id: "mode-class-hall", run: openClassHall }] : []),
|
||||
...(isGm ? [{ id: "mode-gm-tools", run: openGmAdmin }] : []),
|
||||
{ id: "mode-reports", run: onOpenReports },
|
||||
], [character, isGm, onOpenDownloads, onOpenReports, openArenas, openClassHall, openDungeons, openGmAdmin, openManastorms, returnToCharacters]);
|
||||
{ id: "mode-cards", run: onOpenCards },
|
||||
], [character, isGm, onOpenCards, onOpenDownloads, onOpenReports, openArenas, openClassHall, openDungeons, openGmAdmin, openManastorms, returnToCharacters]);
|
||||
const controller = useMenuController(actions, {
|
||||
initialId: "mode-dungeons",
|
||||
onBack: returnToCharacters,
|
||||
@@ -173,6 +178,10 @@ function MainMenuDestinations({ onOpenDownloads, onOpenReports }: {
|
||||
</header>
|
||||
<OnlineGroupPanel />
|
||||
<nav className="main-menu-mode-grid" aria-label="Game modes">
|
||||
<ControllerButton controlId="mode-cards" selectedId={controller.selectedId} select={controller.select}
|
||||
className="front-button main-menu-mode main-menu-mode--available" type="button" onClick={onOpenCards}>
|
||||
<b aria-hidden="true">◆</b><span><strong>Boss Cards</strong><small>Track your collection, rarities, and copies from every dungeon.</small></span><em>Open</em>
|
||||
</ControllerButton>
|
||||
<ControllerButton controlId="mode-reports" selectedId={controller.selectedId} select={controller.select}
|
||||
className="front-button main-menu-mode main-menu-mode--available" type="button" onClick={onOpenReports}>
|
||||
<b aria-hidden="true">!</b>
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { createElement } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { useGameStore } from "../game/store";
|
||||
import { getInputPreferences, publishInputPreferences, setCapturingInput } from "../input/keybindRuntime";
|
||||
import { closeMapForKey, MapPanel } from "./MapPanel";
|
||||
|
||||
// Render the live client state rather than Zustand's immutable initial SSR snapshot.
|
||||
vi.mock("../game/store", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../game/store")>();
|
||||
return { ...actual, useGameStore: Object.assign(
|
||||
(select: (state: ReturnType<typeof actual.useGameStore.getState>) => unknown) => select(actual.useGameStore.getState()), actual.useGameStore,
|
||||
) };
|
||||
});
|
||||
|
||||
const initialPreferences = getInputPreferences();
|
||||
const initialState = useGameStore.getState();
|
||||
|
||||
function key(code: string, extra = {}) {
|
||||
return { code, ctrlKey: false, altKey: false, shiftKey: false, metaKey: false, repeat: false, defaultPrevented: false,
|
||||
preventDefault: vi.fn(), stopPropagation: vi.fn(), ...extra };
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
publishInputPreferences(initialPreferences);
|
||||
setCapturingInput(false);
|
||||
useGameStore.setState(initialState);
|
||||
});
|
||||
|
||||
describe("map dialog controls", () => {
|
||||
it("closes with M even when a map form control owns focus", () => {
|
||||
publishInputPreferences({ ...initialPreferences, pc: { ...initialPreferences.pc, map: "KeyM" } });
|
||||
const event = key("KeyM");
|
||||
const close = vi.fn();
|
||||
closeMapForKey(event, close);
|
||||
expect(close).toHaveBeenCalledOnce();
|
||||
expect(event.preventDefault).toHaveBeenCalledOnce();
|
||||
expect(event.stopPropagation).toHaveBeenCalledOnce();
|
||||
});
|
||||
it("honors the rebound map shortcut", () => {
|
||||
publishInputPreferences({ ...initialPreferences, pc: { ...initialPreferences.pc, map: "Ctrl+KeyG" } });
|
||||
const close = vi.fn();
|
||||
closeMapForKey(key("KeyM"), close);
|
||||
closeMapForKey(key("KeyG", { ctrlKey: true }), close);
|
||||
expect(close).toHaveBeenCalledOnce();
|
||||
});
|
||||
it("ignores repeats, consumed events, other keys and keybind capture", () => {
|
||||
const close = vi.fn();
|
||||
closeMapForKey(key("KeyM", { repeat: true }), close);
|
||||
closeMapForKey(key("KeyM", { defaultPrevented: true }), close);
|
||||
closeMapForKey(key("Space"), close);
|
||||
setCapturingInput(true);
|
||||
closeMapForKey(key("KeyM"), close);
|
||||
expect(close).not.toHaveBeenCalled();
|
||||
});
|
||||
it("renders available floors and the optional patrol control", () => {
|
||||
useGameStore.setState({ activeDungeonId: "deadmines", overlay: "map" });
|
||||
const html = renderToStaticMarkup(createElement(MapPanel));
|
||||
expect(html).toContain("Map floor");
|
||||
expect(html).toContain("Auto — best match");
|
||||
expect(html).toContain('value="2"');
|
||||
expect(html).toContain("Patrol routes");
|
||||
expect(html).toContain("Choose a floor when rooms overlap");
|
||||
});
|
||||
it("unmounts when the map is closed", () => {
|
||||
useGameStore.setState({ overlay: null });
|
||||
expect(renderToStaticMarkup(createElement(MapPanel))).toBe("");
|
||||
});
|
||||
});
|
||||
+43
-7
@@ -1,10 +1,24 @@
|
||||
import { useState, type KeyboardEvent } from "react";
|
||||
import { inputKeyForEvent } from "../input/keybinds";
|
||||
import { getInputPreferences, isCapturingInput } from "../input/keybindRuntime";
|
||||
import { useInputBindingLabels } from "../input/useInputBindingLabels";
|
||||
import { useGameStore } from "../game/store";
|
||||
import { useMenuController } from "../input/useMenuController";
|
||||
import { DungeonMapGraphic } from "./DungeonMapGraphic";
|
||||
import { DungeonMapGraphic, type MapArtworkStatus } from "./DungeonMapGraphic";
|
||||
import { requireDungeonDefinition } from "../game/dungeonRegistry";
|
||||
import { resolveStagePresentation } from "../game/manastormStagePresentation";
|
||||
import { useManastormStore } from "../game/manastormStore";
|
||||
import { wowDungeonMapFor, wowMapContains, wowMapFloorFor, wowMapPoint } from "../game/wowDungeonMap";
|
||||
|
||||
export function closeMapForKey(event: Pick<KeyboardEvent, "code" | "ctrlKey" | "altKey" | "shiftKey" | "metaKey" | "repeat" | "defaultPrevented" | "preventDefault" | "stopPropagation">, close: () => void) {
|
||||
if (event.repeat || event.defaultPrevented || isCapturingInput()) return;
|
||||
const key = inputKeyForEvent(event);
|
||||
if (!key || key !== getInputPreferences().pc.map) return;
|
||||
// The gameplay listener intentionally ignores focused form controls.
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
close();
|
||||
}
|
||||
|
||||
function MapDialog() {
|
||||
const bindingLabel = useInputBindingLabels();
|
||||
@@ -17,6 +31,13 @@ function MapDialog() {
|
||||
const encounter = useManastormStore((state) => state.currentEncounter);
|
||||
const activeDungeonId = useGameStore((state) => state.activeDungeonId);
|
||||
const dungeon = requireDungeonDefinition(activeDungeonId);
|
||||
const map = wowDungeonMapFor(dungeon);
|
||||
const [floorLevel, setFloorLevel] = useState<number | null>(null);
|
||||
const [showPatrols, setShowPatrols] = useState(false);
|
||||
const [artworkStatus, setArtworkStatus] = useState<MapArtworkStatus>(map ? "loading" : "unavailable");
|
||||
const floor = map ? wowMapFloorFor(map, position, floorLevel) : null;
|
||||
const mapPosition = map && floor ? wowMapPoint(map, floor, position) : null;
|
||||
const positionOnMap = Boolean(map && floor && wowMapContains(map, floor, position));
|
||||
const stagePresentation = resolveStagePresentation(gameMode, encounter, {
|
||||
title: dungeon.title,
|
||||
mapId: dungeon.mapId,
|
||||
@@ -25,27 +46,41 @@ function MapDialog() {
|
||||
useMenuController([], { onBack: () => openOverlay("pause") });
|
||||
|
||||
return (
|
||||
<aside className="map-panel" role="dialog" aria-modal="true" aria-labelledby="map-title">
|
||||
<aside className="map-panel" role="dialog" aria-modal="true" aria-labelledby="map-title" onKeyDown={(event) => closeMapForKey(event, close)}>
|
||||
<header>
|
||||
<div>
|
||||
<p className="eyebrow">Expedition survey</p>
|
||||
<p className="eyebrow">{artworkStatus === "ready" ? "Dungeon atlas" : "Expedition survey"}</p>
|
||||
<h2 id="map-title">{stagePresentation.title}</h2>
|
||||
</div>
|
||||
<button type="button" className="icon-button" onClick={close} aria-label="Close map">X</button>
|
||||
</header>
|
||||
|
||||
<div className="map-panel__tools">
|
||||
{map && map.floors.length > 1 && <label>Map floor
|
||||
<select value={floorLevel ?? "auto"} onChange={(event) => setFloorLevel(event.target.value === "auto" ? null : Number(event.target.value))}>
|
||||
<option value="auto">Auto — best match</option>
|
||||
{map.floors.map((entry) => <option key={entry.level} value={entry.level}>{entry.name}</option>)}
|
||||
</select>
|
||||
</label>}
|
||||
<label className="map-panel__patrol-toggle"><input type="checkbox" checked={showPatrols} onChange={(event) => setShowPatrols(event.target.checked)} />Patrol routes</label>
|
||||
</div>
|
||||
<div className="map-panel__canvas">
|
||||
<DungeonMapGraphic position={position} yaw={yaw} />
|
||||
<DungeonMapGraphic position={position} yaw={yaw} floorLevel={floorLevel} showPatrols={showPatrols} onArtworkStatus={setArtworkStatus} />
|
||||
</div>
|
||||
|
||||
<div className="map-panel__readout">
|
||||
<div><span>Current area</span><strong>{stagePresentation.areaName}</strong></div>
|
||||
<div><span>Position</span><strong>{position.map((value) => Math.round(value)).join(" / ")}</strong></div>
|
||||
<div><span>{artworkStatus === "ready" ? floor?.name : "Position"}</span><strong>{artworkStatus === "ready" && mapPosition
|
||||
? positionOnMap ? `${(mapPosition[0] / 1002 * 100).toFixed(1)}%, ${(mapPosition[1] / 668 * 100).toFixed(1)}%` : "Outside selected floor"
|
||||
: position.map((value) => Math.round(value)).join(" / ")}</strong></div>
|
||||
</div>
|
||||
<p className="map-panel__note">
|
||||
{stagePresentation.usingFallbackEnvironment
|
||||
? "Stage location is authoritative; the survey artwork is temporary fallback geometry. "
|
||||
: "Gold markers show encounters. Dashed lines show database patrol routes. "}
|
||||
: artworkStatus === "ready"
|
||||
? "Original game artwork. White: you and camera direction. Green: entrance. Gold: numbered encounters. "
|
||||
: artworkStatus === "loading" ? "Loading original game artwork. " : "Original artwork unavailable; showing the survey fallback. "}
|
||||
{map && map.floors.length > 1 && "Choose a floor when rooms overlap. "}
|
||||
Press {bindingLabel("pc", "map")} to close or {bindingLabel("controller", "menu-back")} to return to Pause.
|
||||
</p>
|
||||
</aside>
|
||||
@@ -54,5 +89,6 @@ function MapDialog() {
|
||||
|
||||
export function MapPanel() {
|
||||
const open = useGameStore((state) => state.overlay === "map");
|
||||
return open ? <MapDialog /> : null;
|
||||
const activeDungeonId = useGameStore((state) => state.activeDungeonId);
|
||||
return open ? <MapDialog key={activeDungeonId} /> : null;
|
||||
}
|
||||
+8
-2
@@ -3,6 +3,8 @@ import type { Vector3Tuple } from "../game/dungeonManifest";
|
||||
import { DungeonMapGraphic } from "./DungeonMapGraphic";
|
||||
import { useInputBindingLabels } from "../input/useInputBindingLabels";
|
||||
import { useGameStore } from "../game/store";
|
||||
import { requireDungeonDefinition } from "../game/dungeonRegistry";
|
||||
import { wowDungeonMapFor, wowMapFloorFor, wowMapHeading } from "../game/wowDungeonMap";
|
||||
|
||||
export interface MinimapProps {
|
||||
position: Vector3Tuple;
|
||||
@@ -23,15 +25,19 @@ export function Minimap({
|
||||
}: MinimapProps) {
|
||||
const bindingLabel = useInputBindingLabels();
|
||||
const inputMode = useGameStore((state) => state.inputMode);
|
||||
const dungeonId = useGameStore((state) => state.activeDungeonId);
|
||||
const map = wowDungeonMapFor(requireDungeonDefinition(dungeonId));
|
||||
const direction = map ? wowMapHeading(map, wowMapFloorFor(map, position), position, yaw) : null;
|
||||
const compassYaw = direction ? Math.atan2(direction[0], -direction[1]) : yaw;
|
||||
const mapKey = bindingLabel(inputMode === "gamepad" ? "controller" : "pc", "map");
|
||||
const heading = ((Math.round((yaw * 180) / Math.PI) % 360) + 360) % 360;
|
||||
const heading = ((Math.round((compassYaw * 180) / Math.PI) % 360) + 360) % 360;
|
||||
return (
|
||||
<section className="hud-minimap" aria-label="Dungeon minimap">
|
||||
<button type="button" className="hud-minimap__map" onClick={onOpenMap} aria-label="Open full dungeon map">
|
||||
<span className="hud-minimap__north" aria-hidden="true">N</span>
|
||||
<span
|
||||
className="hud-minimap__viewport"
|
||||
style={{ "--minimap-turn": rotationMode === "player-up" ? `${-yaw}rad` : "0rad" } as CSSProperties}
|
||||
style={{ "--minimap-turn": rotationMode === "player-up" ? `${-compassYaw}rad` : "0rad" } as CSSProperties}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<DungeonMapGraphic position={position} yaw={yaw} compact />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, type CSSProperties } from "react";
|
||||
import { classById, raceById } from "../app/characterCatalog";
|
||||
import { classById, isRomClassId, raceById } from "../app/characterCatalog";
|
||||
import { useShellStore } from "../app/shellStore";
|
||||
import { useCombatStore } from "../game/combatStore";
|
||||
import {
|
||||
@@ -115,6 +115,8 @@ function MemberFrame({ member, selected, onSelect }: {
|
||||
}) {
|
||||
const characterClass = classById(member.classId);
|
||||
const race = raceById(member.raceId);
|
||||
const isRom = isRomClassId(member.classId);
|
||||
const classLabel = `${characterClass.name}${member.secondaryClassId ? `/${classById(member.secondaryClassId).name}` : ""}`;
|
||||
const percent = healthPercent(member.health, member.maxHealth);
|
||||
const absorb = useCombatStore((state) => state.auras.reduce((total, aura) => (
|
||||
aura.targetId === member.id
|
||||
@@ -138,11 +140,11 @@ function MemberFrame({ member, selected, onSelect }: {
|
||||
className={`party-frame ${selected ? "is-selected" : ""} ${dispellableCount ? "is-dispellable" : ""} ${member.health <= 0 ? "is-down" : ""}`}
|
||||
onClick={onSelect}
|
||||
aria-pressed={selected}
|
||||
aria-label={`Target ${member.name}, ${member.gender} ${race.name}, ${member.specialization} ${characterClass.name}`}
|
||||
aria-label={isRom ? `Target ${member.name}, ${classLabel}` : `Target ${member.name}, ${member.gender} ${race.name}, ${member.specialization} ${characterClass.name}`}
|
||||
>
|
||||
<span className="party-frame__sigil" style={{ "--party-class-color": characterClass.color } as CSSProperties}>{characterClass.sigil}</span>
|
||||
<span className="party-frame__body">
|
||||
<span className="party-frame__identity"><strong>{member.name}</strong><small>{PARTY_ROLE_LABELS[member.role]} · {race.name} · {member.specialization}</small></span>
|
||||
<span className="party-frame__identity"><strong>{member.name}</strong><small style={isRom ? { textTransform: "none" } : undefined}>{isRom ? classLabel : `${PARTY_ROLE_LABELS[member.role]} · ${race.name} · ${member.specialization}`}</small></span>
|
||||
{dispellableCount ? <span className="party-frame__dispel" title={dispellableNames}>DISPEL {dispellableCount}</span> : null}
|
||||
<span className="party-frame__health" role="progressbar" aria-label={`${member.name} health`} aria-valuemin={0} aria-valuemax={member.maxHealth} aria-valuenow={Math.max(0, member.health)}>
|
||||
<i style={{ width: `${percent}%` }} />
|
||||
|
||||
@@ -40,6 +40,7 @@ function PauseDialog() {
|
||||
{ id: "pause-resume", run: resume },
|
||||
{ id: "pause-party", run: () => openOverlay("party") },
|
||||
{ id: "pause-inventory", run: () => openOverlay("inventory") },
|
||||
{ id: "pause-boss-cards", run: () => openOverlay("boss-cards") },
|
||||
{ id: "pause-map", run: () => openOverlay("map") },
|
||||
{ id: "pause-boss-guide", run: () => openOverlay("boss-guide") },
|
||||
{ id: "pause-spellbook", run: () => openOverlay("spellbook") },
|
||||
@@ -78,6 +79,7 @@ function PauseDialog() {
|
||||
{menuButton("pause-resume", "Resume expedition", resume, true)}
|
||||
{menuButton("pause-party", "Party management", () => openOverlay("party"))}
|
||||
{menuButton("pause-inventory", "Inventory & equipment", () => openOverlay("inventory"))}
|
||||
{menuButton("pause-boss-cards", "Boss card collection", () => openOverlay("boss-cards"))}
|
||||
{menuButton("pause-map", "Dungeon map", () => openOverlay("map"))}
|
||||
{menuButton("pause-boss-guide", "Boss guide", () => openOverlay("boss-guide"))}
|
||||
{menuButton("pause-spellbook", "Spellbook", () => openOverlay("spellbook"))}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
.boss-collection-backdrop { z-index: 90; padding: 20px; }
|
||||
.boss-collection {
|
||||
--card-gold: #cbb37b;
|
||||
display: flex; flex-direction: column; width: min(1080px, 100%); height: min(900px, 95vh);
|
||||
min-height: 0; padding: 24px; color: #eee8d8; background: linear-gradient(145deg, #24252c, #11161e);
|
||||
border: 1px solid #716044; border-radius: 12px; box-shadow: 0 24px 90px #000b; pointer-events: auto;
|
||||
}
|
||||
.boss-collection__header { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||
.boss-collection__header h2 { margin: 3px 0 14px; font-family: Georgia, serif; font-size: 32px; color: #ead8af; }
|
||||
.boss-collection__header .eyebrow { margin: 0; color: var(--card-gold); }
|
||||
.boss-collection__summary { display: flex; flex-wrap: wrap; gap: 8px 24px; font-size: 14px; }
|
||||
.boss-collection__summary p { flex-basis: 100%; color: #bcbcbf; margin: 2px 0 14px; font-size: 12px; }
|
||||
.boss-collection__tools { display: flex; flex-wrap: wrap; align-items: end; gap: 10px; margin-bottom: 12px; }
|
||||
.boss-collection__tools [role="group"] { display: flex; gap: 4px; flex-wrap: wrap; }
|
||||
.boss-collection button { padding: 9px 12px; border-radius: 5px; border: 1px solid #5e594e; background: #292e38; color: #eee8d8; cursor: pointer; }
|
||||
.boss-collection button[aria-pressed="true"] { color: #fff1cb; background: #55472d; border-color: var(--card-gold); }
|
||||
.boss-collection button:disabled { opacity: .4; cursor: default; }
|
||||
.boss-collection button:focus-visible, .boss-collection .is-controller-selected { outline: 2px solid var(--card-gold); outline-offset: 2px; }
|
||||
.boss-collection__tools label { display: grid; gap: 4px; color: #bcbcbf; font-size: 11px; }
|
||||
.boss-collection__tools input, .boss-collection__tools select { padding: 9px 10px; border: 1px solid #5e594e; border-radius: 5px; background: #171c25; color: #eee8d8; min-width: 0; }
|
||||
.boss-collection__search { flex: 1 1 180px; }
|
||||
.boss-collection__pages { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding-bottom: 12px; font-size: 12px; color: #c7c6c3; }
|
||||
.boss-collection__scroll { flex: 1; min-height: 0; overflow-y: auto; padding: 3px 5px 10px 3px; }
|
||||
.boss-collection__grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; }
|
||||
.boss-collection__entry { display: contents; }
|
||||
.boss-collection__group-title { grid-column: 1 / -1; margin: 5px 0 0; font-family: Georgia, serif; color: #ead8af; font-size: 20px; }
|
||||
.boss-card { display: flex; flex-direction: column; min-width: 0; border: 1px solid var(--card-color); border-radius: 8px; background: radial-gradient(ellipse at 50% 30%, #3c404d, #191c25 75%); overflow: hidden; }
|
||||
.boss-card--unearned { border-color: color-mix(in srgb, var(--card-color) 50%, #333); }
|
||||
.boss-card header, .boss-card footer { display: flex; align-items: center; justify-content: space-between; gap: 6px; padding: 10px 12px; background: #10141b99; }
|
||||
.boss-card header { color: var(--card-color); font-size: 13px; }
|
||||
.boss-card header small { font-size: 11px; }
|
||||
.boss-card__portrait, .boss-card__model-status { height: 140px; flex-shrink: 0; }
|
||||
.boss-card__model-status { display: grid; place-items: center; padding: 20px; text-align: center; font-size: 12px; color: #c4c4c9; }
|
||||
.boss-card__details { padding: 10px 12px; flex: 1; }
|
||||
.boss-card__details h3 { margin: 0 0 5px; color: #f3eedd; font-family: Georgia, serif; font-size: 17px; line-height: 1.25; }
|
||||
.boss-card__details > small { color: #939dac; font-size: 10px; }
|
||||
.boss-card__dungeons { color: #c8c4b8; font-size: 11px; line-height: 1.4; margin: 7px 0 0; }
|
||||
.boss-card footer { font-size: 11px; }
|
||||
.boss-card footer strong { color: var(--card-color); font-size: 13px; }
|
||||
.boss-card footer span { color: #9daba4; }
|
||||
.boss-collection__empty { padding: 70px 20px; text-align: center; color: #c8c4b8; }
|
||||
.boss-loot-window__card { --item-quality: var(--card-color); }
|
||||
@media (max-width: 720px) {
|
||||
.boss-collection-backdrop { padding: 8px; }
|
||||
.boss-collection { padding: 14px; height: 95dvh; }
|
||||
.boss-collection__header h2 { font-size: 26px; }
|
||||
.boss-collection__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
||||
.boss-collection button { min-height: 40px; padding: 8px 10px; font-size: 12px; }
|
||||
.boss-card__portrait, .boss-card__model-status { height: 120px; }
|
||||
.boss-card header, .boss-card footer, .boss-card__details { padding: 8px; }
|
||||
.boss-card__details h3 { font-size: 15px; }
|
||||
.boss-collection__pages span { text-align: center; font-size: 10px; }
|
||||
}
|
||||
@@ -1,3 +1,18 @@
|
||||
.dungeon-difficulty {
|
||||
padding: 14px;
|
||||
margin: 16px 0;
|
||||
border: 1px solid var(--front-line, #61543e);
|
||||
border-radius: 8px;
|
||||
background: var(--panel-solid, #171c20);
|
||||
}
|
||||
.dungeon-difficulty label { display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
|
||||
.dungeon-difficulty label span { color: var(--front-gold, #e6c680); }
|
||||
.dungeon-difficulty__control { display: flex; align-items: center; gap: 12px; margin: 12px 0; }
|
||||
.dungeon-difficulty__control input { min-width: 0; width: 100%; accent-color: var(--front-gold, #e6c680); }
|
||||
.dungeon-difficulty__control button { min-width: 40px; min-height: 40px; font-size: 20px; }
|
||||
.dungeon-difficulty p { margin: 0 0 6px; }
|
||||
.dungeon-difficulty small { display: block; line-height: 1.5; color: var(--front-dim, #c0bbae); }
|
||||
|
||||
.dungeon-leaderboard, .dungeon-results {
|
||||
--run-gold: var(--front-gold, #d8ae57);
|
||||
--run-green: var(--front-green-bright, #c8e28d);
|
||||
|
||||
@@ -3,13 +3,13 @@ import { fetchDungeonLeaderboard, localDungeonLeaderboard, subscribeDungeonLeade
|
||||
type LeaderboardScope } from "../app/dungeonLeaderboardClient";
|
||||
import type { DungeonClearFilter, DungeonLeaderboard } from "../game/dungeonRunTypes";
|
||||
|
||||
export function useDungeonLeaderboard(dungeonId: string, difficultyId: string, filter: DungeonClearFilter) {
|
||||
export function useDungeonLeaderboard(dungeonId: string, difficultyId: string, filter: DungeonClearFilter, bossDifficulty = 1) {
|
||||
const [offset, setOffset] = useState(0);
|
||||
const [revision, setRevision] = useState(0);
|
||||
const [state, setState] = useState<{ data: DungeonLeaderboard; scope: LeaderboardScope; loading: boolean }>({
|
||||
data: { entries: [], total: 0 }, scope: "world", loading: true,
|
||||
});
|
||||
useEffect(() => { setOffset(0); }, [dungeonId, difficultyId, filter]);
|
||||
useEffect(() => { setOffset(0); }, [dungeonId, difficultyId, filter, bossDifficulty]);
|
||||
useEffect(() => subscribeDungeonLeaderboard(() => setRevision((value) => value + 1)), []);
|
||||
useEffect(() => {
|
||||
const refresh = () => setRevision((value) => value + 1);
|
||||
@@ -21,12 +21,12 @@ export function useDungeonLeaderboard(dungeonId: string, difficultyId: string, f
|
||||
let live = true;
|
||||
const timeout = setTimeout(() => controller.abort(), 8000);
|
||||
setState({ data: { entries: [], total: 0 }, scope: "world", loading: true });
|
||||
if (dungeonId) void fetchDungeonLeaderboard(dungeonId, difficultyId, filter, offset, controller.signal)
|
||||
if (dungeonId) void fetchDungeonLeaderboard(dungeonId, difficultyId, filter, offset, controller.signal, bossDifficulty)
|
||||
.then((data) => { if (live) setState({ data, scope: "world", loading: false }); })
|
||||
.catch(() => { if (live) setState({ data: localDungeonLeaderboard(dungeonId, difficultyId, filter, offset),
|
||||
.catch(() => { if (live) setState({ data: localDungeonLeaderboard(dungeonId, difficultyId, filter, offset, bossDifficulty),
|
||||
scope: "device", loading: false }); });
|
||||
else setState({ data: { entries: [], total: 0 }, scope: "world", loading: false });
|
||||
return () => { live = false; controller.abort(); clearTimeout(timeout); };
|
||||
}, [dungeonId, difficultyId, filter, offset, revision]);
|
||||
}, [dungeonId, difficultyId, filter, offset, revision, bossDifficulty]);
|
||||
return { ...state, offset, setOffset, refresh: () => setRevision((value) => value + 1) };
|
||||
}
|
||||
Reference in new issue
Block a user