azeroth-shell 0.1.1 → 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 +19 -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/fileRepository.mjs +46 -1
- 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 +36 -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 +252 -98
- package/src/cli/tuiAutopilot.mjs +347 -0
- package/src/cli/tuiBg.mjs +371 -42
- package/src/cli/tuiCheck.mjs +415 -28
- 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 +425 -88
- package/src/cli/tuiFightPanes.mjs +27 -8
- package/src/cli/tuiFloor.mjs +2 -2
- 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 +22 -3
- package/src/cli/tuiSearch.mjs +75 -22
- package/src/cli/tuiSettings.mjs +15 -0
- package/src/cli/tuiSimLab.mjs +1 -1
- package/src/cli/tuiStage.mjs +304 -0
- package/src/cli/tuiTalentTree.mjs +124 -0
- package/src/cli/tuiViews.mjs +296 -115
- package/src/cli/tuiWorld.mjs +4 -5
- 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
package/src/domain/bg/wsg.ts
CHANGED
|
@@ -178,27 +178,19 @@ export function installWsg(world: BgWorld): WsgEvent[] {
|
|
|
178
178
|
|
|
179
179
|
if (obj.state !== 'base' && obj.state !== 'ground') continue;
|
|
180
180
|
|
|
181
|
-
// Someone standing on it either takes it (enemy) or returns it (owner, dropped only).
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
returnFlag(team, u);
|
|
190
|
-
break;
|
|
191
|
-
}
|
|
192
|
-
continue;
|
|
193
|
-
}
|
|
194
|
-
// Enemy flag: pick it up. One flag per unit.
|
|
195
|
-
if (u.carryingFlag) continue;
|
|
196
|
-
obj.state = 'carried';
|
|
197
|
-
obj.carrier = u.id;
|
|
198
|
-
u.carryingFlag = flagSpellCarriedBy(u.team);
|
|
199
|
-
log({ t: w.t, kind: 'pickup', team: u.team, unit: u.id });
|
|
200
|
-
break;
|
|
181
|
+
// Someone standing on it either takes it (enemy) or returns it (owner, dropped only). With
|
|
182
|
+
// both sides on a dropped flag the nearest hand wins; the first body in roster order used to,
|
|
183
|
+
// so the team added to the world first won every contested flag and every match.
|
|
184
|
+
const u = flagToucher(w, obj, team);
|
|
185
|
+
if (!u) continue;
|
|
186
|
+
if (u.team === team) {
|
|
187
|
+
returnFlag(team, u);
|
|
188
|
+
continue;
|
|
201
189
|
}
|
|
190
|
+
obj.state = 'carried';
|
|
191
|
+
obj.carrier = u.id;
|
|
192
|
+
u.carryingFlag = flagSpellCarriedBy(u.team);
|
|
193
|
+
log({ t: w.t, kind: 'pickup', team: u.team, unit: u.id });
|
|
202
194
|
}
|
|
203
195
|
|
|
204
196
|
// A carrier standing on its OWN flag, with that flag at base, scores.
|
|
@@ -257,6 +249,29 @@ export function installWsg(world: BgWorld): WsgEvent[] {
|
|
|
257
249
|
return events;
|
|
258
250
|
}
|
|
259
251
|
|
|
252
|
+
// The body that gets a flag everyone within reach wants. The owner's side can only return a dropped
|
|
253
|
+
// flag; the other side's runner is preferred over its escorts at the base, since a pickup by the
|
|
254
|
+
// escort sent the runner home empty-handed. Exact ties (bodies stacked on one point) alternate by
|
|
255
|
+
// tick, so neither side is favoured by roster order.
|
|
256
|
+
function flagToucher(w: BgWorld, obj: { pos: BgPos; state: string }, owner: BgTeam): BgUnit | null {
|
|
257
|
+
let best: BgUnit | null = null;
|
|
258
|
+
let bd = Infinity;
|
|
259
|
+
let bestRunner = false;
|
|
260
|
+
const odd = Math.floor(w.t / Math.max(1, w.tickMs)) % 2 === 1;
|
|
261
|
+
for (const u of w.units.values()) {
|
|
262
|
+
if (!w.alive(u)) continue;
|
|
263
|
+
const own = u.team === owner;
|
|
264
|
+
if (own ? obj.state !== 'ground' : !!u.carryingFlag) continue;
|
|
265
|
+
const d = detHypot(u.pos.x - obj.pos.x, u.pos.y - obj.pos.y);
|
|
266
|
+
if (d > WS_FLAG_INTERACT_YD) continue;
|
|
267
|
+
const runner = !own && u.role === 'flag-carrier';
|
|
268
|
+
if (!best || (runner && !bestRunner) || (runner === bestRunner && (d < bd || (d === bd && odd)))) {
|
|
269
|
+
best = u; bd = d; bestRunner = runner;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return best;
|
|
273
|
+
}
|
|
274
|
+
|
|
260
275
|
/** Where a dead unit of `team` comes back. Cite: world_safe_locs 771 / 772. */
|
|
261
276
|
export function wsgGraveyard(team: BgTeam): BgPos {
|
|
262
277
|
const gy = WSG_GEOMETRY.graveyard[team];
|
|
@@ -44,6 +44,8 @@ import { auraIdsForSnapshot, itemGrantsAdCommissionAura, snapshotHasAdCommission
|
|
|
44
44
|
import {
|
|
45
45
|
lootDropAllowed,
|
|
46
46
|
compareQuestDroppers,
|
|
47
|
+
pickQuestDropper,
|
|
48
|
+
dropperPickSalt,
|
|
47
49
|
creatureFarmLevel,
|
|
48
50
|
creatureFarmRank,
|
|
49
51
|
canTakeQuestForCondition,
|
|
@@ -935,6 +937,8 @@ function fightStep(
|
|
|
935
937
|
const skip = opts?.skipCreatures;
|
|
936
938
|
const hunt = huntableCreatures(ds, state.char.zoneId, state.questlog, state.char.race);
|
|
937
939
|
const summoned = new Set(corpseUseSummonedEntries(ds, def));
|
|
940
|
+
const salt = dropperPickSalt(state);
|
|
941
|
+
const killable: { entry: number; level: number; chance: number; rank: number }[] = [];
|
|
938
942
|
for (const kt of creatureCreditTargets(def)) {
|
|
939
943
|
if ((qs.kills[String(kt.creatureEntry)] ?? 0) >= kt.count) continue;
|
|
940
944
|
if (skip?.has(kt.creatureEntry)) continue;
|
|
@@ -949,7 +953,17 @@ function fightStep(
|
|
|
949
953
|
// Friendly + noXp: beginFight refuses it as script credit, so proposing it strands the quest.
|
|
950
954
|
if (isScriptCreditOnlyTarget(ds, kt.creatureEntry, state.questlog)) continue;
|
|
951
955
|
// Named kill target is the quest — fight it even when it is not on zone.huntable.
|
|
952
|
-
|
|
956
|
+
const cr = ds.creature(kt.creatureEntry);
|
|
957
|
+
if (cr) killable.push({ entry: kt.creatureEntry, level: creatureFarmLevel(cr), chance: 0, rank: creatureFarmRank(cr) });
|
|
958
|
+
}
|
|
959
|
+
if (killable.length > 0) {
|
|
960
|
+
// Several unmet kill objectives: rotate among the near-equal ones that are actually here, so a
|
|
961
|
+
// multi-template objective does not open on its first-listed entry every pull.
|
|
962
|
+
const here = new Set(hunt.map((c) => c.entry));
|
|
963
|
+
const local = killable.filter((k) => here.has(k.entry));
|
|
964
|
+
if (local.length === 0) return { kind: 'fight', creatureEntry: killable[0].entry };
|
|
965
|
+
local.sort((a, b) => compareQuestDroppers(a, b, state.char.level) || a.entry - b.entry);
|
|
966
|
+
return { kind: 'fight', creatureEntry: pickQuestDropper(local, state.char.level, salt)!.entry };
|
|
953
967
|
}
|
|
954
968
|
const farm = corpseUseFightEntries(ds, def, qs, state.inventory);
|
|
955
969
|
if (farm.length) {
|
|
@@ -986,7 +1000,8 @@ function fightStep(
|
|
|
986
1000
|
}
|
|
987
1001
|
const usable = [...byEntry.values()];
|
|
988
1002
|
usable.sort((a, b) => compareQuestDroppers(a, b, state.char.level) || a.entry - b.entry);
|
|
989
|
-
|
|
1003
|
+
const dropper = pickQuestDropper(usable, state.char.level, salt);
|
|
1004
|
+
if (dropper) return { kind: 'fight', creatureEntry: dropper.entry };
|
|
990
1005
|
if (unmetItems.length === 0) return null;
|
|
991
1006
|
// farm[] is one union over every itemTarget, and many named templates are not on zone.huntable.
|
|
992
1007
|
// Prefer an entry that drops what we still need; 40 of 1366 farm quests gate it behind a condition.
|
|
@@ -1008,7 +1023,8 @@ function fightStep(
|
|
|
1008
1023
|
? farmDrop
|
|
1009
1024
|
: farmScored.filter((s) => !farmCandidateIsHopeless(ds, def, s.ce, unmetItems, owned, snap));
|
|
1010
1025
|
farmPool.sort((a, b) => compareQuestDroppers(a, b, state.char.level) || a.ce - b.ce);
|
|
1011
|
-
|
|
1026
|
+
const farmPick = pickQuestDropper(farmPool, state.char.level, salt);
|
|
1027
|
+
if (farmPick) return { kind: 'fight', creatureEntry: farmPick.ce };
|
|
1012
1028
|
for (const it of unmetItems) {
|
|
1013
1029
|
const fight = pickFightable(ds, state, creatureDroppersForItem(ds, it.item, snap, state.char.level), skip);
|
|
1014
1030
|
if (fight != null) return { kind: 'fight', creatureEntry: fight };
|
|
@@ -1055,13 +1071,14 @@ function dungeonOnlyWork(
|
|
|
1055
1071
|
}
|
|
1056
1072
|
|
|
1057
1073
|
// Farm entries for one item, ordered the way fightStep picks: noise floor first, then compareQuestDroppers,
|
|
1058
|
-
// so the travel hop names the mob the fight will actually open on.
|
|
1074
|
+
// then the salted pick moved to the front, so the travel hop names the mob the fight will actually open on.
|
|
1059
1075
|
function rankFarmForItem(
|
|
1060
1076
|
ds: DataStore,
|
|
1061
1077
|
farm: readonly number[],
|
|
1062
1078
|
itemId: number,
|
|
1063
1079
|
snap: ConditionSnapshot,
|
|
1064
1080
|
playerLevel: number,
|
|
1081
|
+
salt: number,
|
|
1065
1082
|
): number[] {
|
|
1066
1083
|
const scored: { ce: number; chance: number; level: number; rank: number }[] = [];
|
|
1067
1084
|
const floor = dropperNoiseFloor(ds, itemId, snap);
|
|
@@ -1079,7 +1096,8 @@ function rankFarmForItem(
|
|
|
1079
1096
|
const drops = scored.filter((s) => s.chance > 0);
|
|
1080
1097
|
const pool = drops.length > 0 ? drops : scored;
|
|
1081
1098
|
pool.sort((a, b) => compareQuestDroppers(a, b, playerLevel) || a.ce - b.ce);
|
|
1082
|
-
|
|
1099
|
+
const first = pickQuestDropper(pool, playerLevel, salt);
|
|
1100
|
+
return first ? [first.ce, ...pool.filter((s) => s !== first).map((s) => s.ce)] : [];
|
|
1083
1101
|
}
|
|
1084
1102
|
|
|
1085
1103
|
function farmTravelStep(
|
|
@@ -1108,7 +1126,7 @@ function farmTravelStep(
|
|
|
1108
1126
|
if (countItem(ownedOf(state), it.item) >= it.count) continue;
|
|
1109
1127
|
if (isProvidedItem(def, it.item)) continue;
|
|
1110
1128
|
const ces = (def.farm ?? []).length
|
|
1111
|
-
? rankFarmForItem(ds, def.farm ?? [], it.item, snap, state.char.level)
|
|
1129
|
+
? rankFarmForItem(ds, def.farm ?? [], it.item, snap, state.char.level, dropperPickSalt(state))
|
|
1112
1130
|
: creatureDroppersForItem(ds, it.item, snap, state.char.level);
|
|
1113
1131
|
for (const ce of ces) {
|
|
1114
1132
|
if (opts?.skipCreatures?.has(ce)) continue;
|