azeroth-shell 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +326 -82
- package/bin/azsh.mjs +21 -19
- package/package.json +9 -1
- package/src/autopilot/plan.ts +8 -11
- package/src/autopilot/view.ts +12 -11
- package/src/cli/combatLogFormat.ts +22 -0
- package/src/cli/gzLoader.mjs +47 -6
- package/src/cli/hdArt.mjs +81 -0
- package/src/cli/iconArt.mjs +135 -0
- package/src/cli/iconGrid.mjs +100 -0
- package/src/cli/modelArt.mjs +295 -0
- package/src/cli/paperdoll.mjs +76 -0
- package/src/cli/perfCheck.mjs +56 -0
- package/src/cli/repl.mjs +32 -7
- package/src/cli/rotationText.mjs +6 -6
- package/src/cli/sixel.mjs +307 -0
- package/src/cli/term.mjs +106 -12
- package/src/cli/text.mjs +5 -3
- package/src/cli/tui.mjs +231 -95
- package/src/cli/tuiAutopilot.mjs +347 -0
- package/src/cli/tuiBg.mjs +370 -41
- package/src/cli/tuiCheck.mjs +362 -27
- package/src/cli/tuiCity.mjs +118 -13
- package/src/cli/tuiCompanions.mjs +25 -15
- package/src/cli/tuiCreate.mjs +223 -0
- package/src/cli/tuiDungeon.mjs +1 -1
- package/src/cli/tuiFight.mjs +423 -86
- package/src/cli/tuiFightPanes.mjs +27 -8
- package/src/cli/tuiFloor.mjs +1 -1
- package/src/cli/tuiGear.mjs +10 -6
- package/src/cli/tuiLoot.mjs +1 -0
- package/src/cli/tuiPaperdoll.mjs +107 -0
- package/src/cli/tuiPartyStage.mjs +177 -0
- package/src/cli/tuiProfessions.mjs +4 -2
- package/src/cli/tuiRotationCond.mjs +390 -0
- package/src/cli/tuiSaves.mjs +10 -1
- package/src/cli/tuiSearch.mjs +75 -22
- package/src/cli/tuiStage.mjs +304 -0
- package/src/cli/tuiTalentTree.mjs +124 -0
- package/src/cli/tuiViews.mjs +284 -110
- package/src/cli/tuiWorld.mjs +2 -2
- package/src/data/gear-art.bin.br +0 -0
- package/src/data/hd-art.json +114 -0
- package/src/data/icon-art.bin.gz +0 -0
- package/src/data/model-art.bin.gz +0 -0
- package/src/data/rotations/pve/paladin/retribution.json +2 -2
- package/src/domain/bg/match.ts +85 -24
- package/src/domain/bg/strategies.ts +144 -29
- package/src/domain/bg/types.ts +2 -0
- package/src/domain/bg/wsg.ts +35 -20
- package/src/domain/quests/doQuest.ts +24 -6
- package/src/domain/rotation/build.ts +3031 -3031
- package/src/domain/rotation/classes/paladin.ts +16 -11
- package/src/domain/rotation/evaluate.ts +3 -0
- package/src/engine/combat/applySpell.ts +20 -9
- package/src/engine/combat/applySpellLate.ts +16 -12
- package/src/engine/combat/areaAura.ts +284 -283
- package/src/engine/combat/fighterFactory.ts +1 -1
- package/src/engine/combat/pickAction.ts +39 -1
- package/src/engine/combat/procsDamage.ts +31 -1
- package/src/engine/combat/sessionChase.ts +7 -1
- package/src/engine/combat/sessionMotionKit.ts +40 -2
- package/src/quests/quests.ts +60 -0
|
@@ -47,26 +47,31 @@ export function finalizePaladinSealJudge(
|
|
|
47
47
|
|
|
48
48
|
const pri = sealKindPriority(spec);
|
|
49
49
|
// Kind first (resolved effects); rank only ties same kind (highest rank wins).
|
|
50
|
-
const
|
|
51
|
-
const kindA = sealKindOf(resolvedSealDef(a
|
|
52
|
-
const kindB = sealKindOf(resolvedSealDef(b
|
|
50
|
+
const bySpec = (a: SpellDef, b: SpellDef): number => {
|
|
51
|
+
const kindA = sealKindOf(resolvedSealDef(a));
|
|
52
|
+
const kindB = sealKindOf(resolvedSealDef(b));
|
|
53
53
|
return (pri[kindB] ?? -1) - (pri[kindA] ?? -1)
|
|
54
|
-
|| (b.
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
|| (b.rank ?? 0) - (a.rank ?? 0);
|
|
55
|
+
};
|
|
56
|
+
// The pick is among every seal the paladin KNOWS, not only the ones the pack wrote down. The
|
|
57
|
+
// 20-39 Retribution band authored Seal of Righteousness alone, so a Ret who took Seal of Command
|
|
58
|
+
// at 20 kept casting the trainer seal until the 40 band named Command. The rule the pack placed is
|
|
59
|
+
// kept where it sits; only the seal it casts changes.
|
|
60
|
+
const knownSeals = [...knownById.values()].filter((d) => isSealSpell(d));
|
|
61
|
+
const authored = sealRules.map((s) => s.def);
|
|
62
|
+
const best = [...(knownSeals.length ? knownSeals : authored)].sort(bySpec)[0]!;
|
|
63
|
+
const bestId = best.id;
|
|
64
|
+
const authoredIds = new Set(authored.map((d) => d.id));
|
|
60
65
|
|
|
61
66
|
const out: RotationRule[] = [];
|
|
62
67
|
let keptBestSeal = false;
|
|
63
68
|
for (const r of combat) {
|
|
64
|
-
if (r.action.kind === 'spell' &&
|
|
65
|
-
if (r.action.kind === 'spell' && r.action.spellId === bestId) {
|
|
69
|
+
if (r.action.kind === 'spell' && authoredIds.has(r.action.spellId)) {
|
|
66
70
|
if (keptBestSeal) continue; // one seal upkeep rule only
|
|
67
71
|
keptBestSeal = true;
|
|
68
72
|
out.push({
|
|
69
73
|
...r,
|
|
74
|
+
action: { ...r.action, spellId: bestId },
|
|
70
75
|
conditions: [cond('self', 'buffPresent', '==', 0, bestId)],
|
|
71
76
|
});
|
|
72
77
|
continue;
|
|
@@ -1000,6 +1000,9 @@ function pullSequenceAction(
|
|
|
1000
1000
|
const condSnap = snapForAction(snap, action);
|
|
1001
1001
|
if (!conditionsPass(condSnap, conditionsForEval(rule))) continue; // not needed this pull
|
|
1002
1002
|
if (!actionUsable(snap, action)) {
|
|
1003
|
+
// An immune target never makes the opener usable: waiting on it held a fire mage at range
|
|
1004
|
+
// forever against a Fire Elemental. Step past it like a spent row.
|
|
1005
|
+
if (action.kind === 'spell' && snap.target?.immuneSpellIds?.has(action.spellId)) continue;
|
|
1003
1006
|
const inMelee = snap.self.inMelee === true
|
|
1004
1007
|
|| (!!snap.enemyDists && snap.enemyDists.some((d) => d <= IN_MELEE_YD));
|
|
1005
1008
|
if (inMelee || snap.enemyCount === 0) continue;
|
|
@@ -281,7 +281,17 @@ export type installApplySpellDeps = {
|
|
|
281
281
|
};
|
|
282
282
|
|
|
283
283
|
export function installApplySpell(ctx: CombatContext, deps: installApplySpellDeps): void {
|
|
284
|
-
|
|
284
|
+
// One aura row per holder, spell and instant. The push sits in the per-effect loop, so a two-effect
|
|
285
|
+
// buff (Dampen Magic: spell and healing taken) read "You gain" twice for one cast.
|
|
286
|
+
let lastAuraKey = '';
|
|
287
|
+
const push = (e: any) => {
|
|
288
|
+
if (e.kind === 'aura') {
|
|
289
|
+
const key = e.applied && e.spellId != null ? `${e.t}|${e.actor}|${e.spellId}` : '';
|
|
290
|
+
if (key && key === lastAuraKey) return;
|
|
291
|
+
lastAuraKey = key;
|
|
292
|
+
}
|
|
293
|
+
ctx.push(e);
|
|
294
|
+
};
|
|
285
295
|
const player = ctx.player;
|
|
286
296
|
const pet = ctx.pet;
|
|
287
297
|
const effects = ctx.effects;
|
|
@@ -2220,7 +2230,8 @@ export function installApplySpell(ctx: CombatContext, deps: installApplySpellDep
|
|
|
2220
2230
|
};
|
|
2221
2231
|
// One-target apply, hoisted so the area-aura maintain pass can RE-apply it when an ally walks
|
|
2222
2232
|
// back into radius. Idempotent: an existing row refreshes duration and never re-adds flat mods.
|
|
2223
|
-
|
|
2233
|
+
// `now` is that pass's clock for the log; the expiry stays on the cast's (the holder's remaining life).
|
|
2234
|
+
const applyBuffToTarget = (buffTgt: Fighter, now = t): void => {
|
|
2224
2235
|
// Totem.cpp:282-287: mixed-hit school damage still lands; the APPLY_AURA index does not.
|
|
2225
2236
|
const hostileBuff = isHostileBuffEffect(eff, baseAmount);
|
|
2226
2237
|
if (
|
|
@@ -2353,10 +2364,10 @@ export function installApplySpell(ctx: CombatContext, deps: installApplySpellDep
|
|
|
2353
2364
|
});
|
|
2354
2365
|
}
|
|
2355
2366
|
buffTgt.buffActive.add(spell.id);
|
|
2356
|
-
push({ t, kind: 'aura', actor: buffTgt.side, name: spell.name, applied: true, spellId: spell.id });
|
|
2367
|
+
push({ t: now, kind: 'aura', actor: buffTgt.side, name: spell.name, applied: true, spellId: spell.id });
|
|
2357
2368
|
if (buffTgt.side !== src.side) {
|
|
2358
2369
|
push({
|
|
2359
|
-
t, kind: 'spell_hit', actor: src.side, target: buffTgt.side,
|
|
2370
|
+
t: now, kind: 'spell_hit', actor: src.side, target: buffTgt.side,
|
|
2360
2371
|
spellId: spell.id, name: spell.name, result: 'hit', amount: 0, school: spell.school,
|
|
2361
2372
|
});
|
|
2362
2373
|
}
|
|
@@ -2424,7 +2435,7 @@ export function installApplySpell(ctx: CombatContext, deps: installApplySpellDep
|
|
|
2424
2435
|
}
|
|
2425
2436
|
buffTgt.buffActive.delete(row.spellId);
|
|
2426
2437
|
effects.splice(at, 1);
|
|
2427
|
-
push({ t, kind: 'aura', actor: buffTgt.side, name: row.name, applied: false });
|
|
2438
|
+
push({ t: now, kind: 'aura', actor: buffTgt.side, name: row.name, applied: false });
|
|
2428
2439
|
}
|
|
2429
2440
|
}
|
|
2430
2441
|
}
|
|
@@ -2457,10 +2468,10 @@ export function installApplySpell(ctx: CombatContext, deps: installApplySpellDep
|
|
|
2457
2468
|
});
|
|
2458
2469
|
// The aura row lives on the target's buff bar. Also emit a spell_hit for the caster so a
|
|
2459
2470
|
// damageless hostile debuff shows in the fight log as a cast.
|
|
2460
|
-
push({ t, kind: 'aura', actor: buffTgt.side, name: spell.name, applied: true, spellId: spell.id });
|
|
2471
|
+
push({ t: now, kind: 'aura', actor: buffTgt.side, name: spell.name, applied: true, spellId: spell.id });
|
|
2461
2472
|
if (buffTgt.side !== src.side) {
|
|
2462
2473
|
push({
|
|
2463
|
-
t, kind: 'spell_hit', actor: src.side, target: buffTgt.side,
|
|
2474
|
+
t: now, kind: 'spell_hit', actor: src.side, target: buffTgt.side,
|
|
2464
2475
|
spellId: spell.id, name: spell.name, result: 'hit', amount: 0, school: spell.school,
|
|
2465
2476
|
});
|
|
2466
2477
|
}
|
|
@@ -2530,9 +2541,9 @@ export function installApplySpell(ctx: CombatContext, deps: installApplySpellDep
|
|
|
2530
2541
|
exclusionGroup: spell.exclusionGroup,
|
|
2531
2542
|
// AreaAura::Update applies the MISSING rank only. An unguarded re-apply emitted 50 events
|
|
2532
2543
|
// for one Blood Pact in a 2.5 s fight — the totem kit path guards the same way.
|
|
2533
|
-
apply: (m: Fighter) => {
|
|
2544
|
+
apply: (m: Fighter, now: number) => {
|
|
2534
2545
|
if (hasAreaAuraRow(ctx.effectsOn(m.side), m, spell.id, src.side)) return;
|
|
2535
|
-
applyBuffToTarget(m);
|
|
2546
|
+
applyBuffToTarget(m, now);
|
|
2536
2547
|
},
|
|
2537
2548
|
strip: (m: Fighter) => {
|
|
2538
2549
|
stripAreaAuraRowsFrom(effects, m, spell.id, src.side, reverseActiveEffect, ctx.effectsOn(m.side));
|
|
@@ -216,7 +216,11 @@ if (eff.kind === 'summon_totem') {
|
|
|
216
216
|
};
|
|
217
217
|
// Register the kit aura as a live class-A holder so the maintain pass keeps re-checking the
|
|
218
218
|
// totem-to-ally distance. The origin is the TOTEM; the mod owner is the shaman.
|
|
219
|
-
|
|
219
|
+
// Each kit closure takes `now` (default: the summon's `t`): the maintain pass calls it later for an
|
|
220
|
+
// ally walking into radius, and the summon-time clock put that row's first tick in the past, so
|
|
221
|
+
// every missed Healing Stream tick landed at once.
|
|
222
|
+
// Expiry stays on the totem's clock, the holder's remaining life.
|
|
223
|
+
const registerTotemAreaAura = (abil: SpellDef, apply: (fr: Fighter, now: number) => void): void => {
|
|
220
224
|
registerAreaAuraSource?.({
|
|
221
225
|
holderSide: built.side,
|
|
222
226
|
spellId: abil.id,
|
|
@@ -256,7 +260,7 @@ if (eff.kind === 'summon_totem') {
|
|
|
256
260
|
isWithinRadiusYd(posOf?.(built.side), posOf?.(en.side), kitRadiusYd(abil) ?? 10),
|
|
257
261
|
)
|
|
258
262
|
: (ae.applyTo === 'self' ? [player] : inRadiusParty(abil));
|
|
259
|
-
const applyKitBuff = (fr: Fighter): void => {
|
|
263
|
+
const applyKitBuff = (fr: Fighter, now = t): void => {
|
|
260
264
|
if (dead(fr)) return;
|
|
261
265
|
if (hasAreaAuraRow(rowsOn(fr), fr, abil.id, built.side)) return;
|
|
262
266
|
applyBuffMod(fr, ae.mod, amount);
|
|
@@ -265,7 +269,7 @@ if (eff.kind === 'summon_totem') {
|
|
|
265
269
|
kind: 'buff', source: built.side, target: fr.side, spellId: abil.id, name: abil.name,
|
|
266
270
|
mod: ae.mod, amount, expiresAt: t + dur, fromTotem: built.side,
|
|
267
271
|
});
|
|
268
|
-
push({ t, kind: 'aura', actor: fr.side, name: abil.name, applied: true });
|
|
272
|
+
push({ t: now, kind: 'aura', actor: fr.side, name: abil.name, applied: true });
|
|
269
273
|
};
|
|
270
274
|
// continue (not return): one dead friendly must not abort party aura reach.
|
|
271
275
|
// Cite: Totem.cpp UnSummon party strip :145–157; APPLY_AREA_AURA_PARTY effect 35.
|
|
@@ -278,7 +282,7 @@ if (eff.kind === 'summon_totem') {
|
|
|
278
282
|
// Cite: APPLY_AREA_AURA_PARTY; Totem.cpp UnSummon removes aura on all subgroup members.
|
|
279
283
|
const tickMs = Math.max(1, ae.tickMs);
|
|
280
284
|
const kitPower = ae.kind === 'restore_power' ? (ae.powerType ?? POWER_MANA) | 0 : undefined;
|
|
281
|
-
const applyKitTick = (fr: Fighter): void => {
|
|
285
|
+
const applyKitTick = (fr: Fighter, now = t): void => {
|
|
282
286
|
if (dead(fr)) return;
|
|
283
287
|
if (hasAreaAuraRow(rowsOn(fr), fr, abil.id, built.side)) return;
|
|
284
288
|
// The aura lands on every living party member for strip symmetry; the pool it pays
|
|
@@ -286,11 +290,11 @@ if (eff.kind === 'summon_totem') {
|
|
|
286
290
|
fr.buffActive.add(abil.id);
|
|
287
291
|
effects.push({
|
|
288
292
|
kind: ae.kind, source: built.side, target: fr.side, spellId: abil.id, name: abil.name,
|
|
289
|
-
amountPerTick: ae.amountPerTick, pctOfMax: ae.pctOfMax, tickMs, nextTickAt:
|
|
293
|
+
amountPerTick: ae.amountPerTick, pctOfMax: ae.pctOfMax, tickMs, nextTickAt: now + tickMs,
|
|
290
294
|
...(kitPower != null ? { powerType: kitPower } : {}),
|
|
291
295
|
expiresAt: t + built.durationMs, fromTotem: built.side,
|
|
292
296
|
});
|
|
293
|
-
push({ t, kind: 'aura', actor: fr.side, name: abil.name, applied: true });
|
|
297
|
+
push({ t: now, kind: 'aura', actor: fr.side, name: abil.name, applied: true });
|
|
294
298
|
};
|
|
295
299
|
// Healing Stream / Mana Spring only tick on allies standing in the totem's
|
|
296
300
|
// radius — walk out and the stream stops for you (§4.9.1).
|
|
@@ -315,7 +319,7 @@ if (eff.kind === 'summon_totem') {
|
|
|
315
319
|
} else if (ae.kind === 'resist') {
|
|
316
320
|
// MOD_RESISTANCE party aura — the Frost, Fire and Nature Resistance Totems. Same party
|
|
317
321
|
// reach and strip symmetry as the stat buffs; without it they were summoned decoration.
|
|
318
|
-
const applyKitResist = (fr: Fighter): void => {
|
|
322
|
+
const applyKitResist = (fr: Fighter, now = t): void => {
|
|
319
323
|
if (dead(fr)) return;
|
|
320
324
|
if (hasAreaAuraRow(rowsOn(fr), fr, abil.id, built.side)) return;
|
|
321
325
|
applyResistMod(fr, ae.school, ae.amount);
|
|
@@ -325,7 +329,7 @@ if (eff.kind === 'summon_totem') {
|
|
|
325
329
|
name: abil.name, school: ae.school, amount: ae.amount,
|
|
326
330
|
expiresAt: t + built.durationMs, fromTotem: built.side,
|
|
327
331
|
});
|
|
328
|
-
push({ t, kind: 'aura', actor: fr.side, name: abil.name, applied: true });
|
|
332
|
+
push({ t: now, kind: 'aura', actor: fr.side, name: abil.name, applied: true });
|
|
329
333
|
};
|
|
330
334
|
for (const fr of inRadiusParty(abil)) applyKitResist(fr);
|
|
331
335
|
registerTotemAreaAura(abil, applyKitResist);
|
|
@@ -335,10 +339,10 @@ if (eff.kind === 'summon_totem') {
|
|
|
335
339
|
applyFlatThreat(tot, ae.amount, abil as unknown as SpellDef);
|
|
336
340
|
} else if (ae.kind === 'spell_magnet') {
|
|
337
341
|
const magnetDur = ae.durationMs > 0 ? ae.durationMs : built.durationMs;
|
|
338
|
-
const applyKitMagnet = (fr: Fighter): void => {
|
|
342
|
+
const applyKitMagnet = (fr: Fighter, now = t): void => {
|
|
339
343
|
applySpellMagnetToParty({
|
|
340
344
|
effects, effectsOn, party: [fr], source: built.side, spell: abil,
|
|
341
|
-
expiresAt: t + magnetDur, t, push, touchEffect,
|
|
345
|
+
expiresAt: t + magnetDur, t: now, push, touchEffect,
|
|
342
346
|
});
|
|
343
347
|
};
|
|
344
348
|
for (const fr of inRadiusParty(abil)) applyKitMagnet(fr);
|
|
@@ -374,11 +378,11 @@ if (eff.kind === 'summon_totem') {
|
|
|
374
378
|
const magnetFx = trig.effects.find((e) => e.kind === 'spell_magnet');
|
|
375
379
|
if (!magnetFx || magnetFx.kind !== 'spell_magnet') continue;
|
|
376
380
|
const magnetDur = magnetFx.durationMs > 0 ? magnetFx.durationMs : built.durationMs;
|
|
377
|
-
const applyTrigMagnet = (fr: Fighter): void => {
|
|
381
|
+
const applyTrigMagnet = (fr: Fighter, now = t): void => {
|
|
378
382
|
applySpellMagnetToParty({
|
|
379
383
|
effects, effectsOn, party: [fr], source: built.side,
|
|
380
384
|
spell: { id: trig.id, name: trig.name, procCharges: magnetFx.charges },
|
|
381
|
-
expiresAt: t + magnetDur, t, push, touchEffect,
|
|
385
|
+
expiresAt: t + magnetDur, t: now, push, touchEffect,
|
|
382
386
|
});
|
|
383
387
|
};
|
|
384
388
|
for (const fr of inRadiusParty(trig)) applyTrigMagnet(fr);
|