azeroth-shell 0.2.0 → 0.4.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +43 -10
  3. package/bin/azsh.mjs +15 -1
  4. package/package.json +7 -3
  5. package/src/cli/hdArt.mjs +1 -1
  6. package/src/cli/iconArt.mjs +11 -1
  7. package/src/cli/modelArt.mjs +7 -4
  8. package/src/cli/oocPrep.mjs +32 -0
  9. package/src/cli/repl.mjs +16 -0
  10. package/src/cli/sixel.mjs +26 -14
  11. package/src/cli/text.mjs +16 -0
  12. package/src/cli/tui.mjs +40 -13
  13. package/src/cli/tuiAutopilot.mjs +17 -8
  14. package/src/cli/tuiBags.mjs +37 -4
  15. package/src/cli/tuiBulk.mjs +83 -4
  16. package/src/cli/tuiCheck.mjs +328 -19
  17. package/src/cli/tuiCity.mjs +31 -31
  18. package/src/cli/tuiCompanionKit.mjs +26 -8
  19. package/src/cli/tuiCompanions.mjs +189 -29
  20. package/src/cli/tuiCreate.mjs +7 -0
  21. package/src/cli/tuiDungeon.mjs +113 -33
  22. package/src/cli/tuiFight.mjs +63 -21
  23. package/src/cli/tuiFightPanes.mjs +15 -6
  24. package/src/cli/tuiFloor.mjs +20 -8
  25. package/src/cli/tuiGear.mjs +9 -10
  26. package/src/cli/tuiLoot.mjs +5 -6
  27. package/src/cli/tuiMap.mjs +214 -0
  28. package/src/cli/tuiPaperdoll.mjs +6 -5
  29. package/src/cli/tuiPartyStage.mjs +36 -7
  30. package/src/cli/tuiPet.mjs +16 -8
  31. package/src/cli/tuiProfessions.mjs +23 -23
  32. package/src/cli/tuiSaves.mjs +14 -0
  33. package/src/cli/tuiSearch.mjs +4 -5
  34. package/src/cli/tuiSettings.mjs +3 -4
  35. package/src/cli/tuiStage.mjs +72 -16
  36. package/src/cli/tuiTalents.mjs +3 -4
  37. package/src/cli/tuiViews.mjs +374 -85
  38. package/src/cli/tuiWiki.mjs +2 -3
  39. package/src/cli/tuiWorld.mjs +88 -5
  40. package/src/data/gear-art.bin.br +0 -0
  41. package/src/data/hd-art.json +36 -36
  42. package/src/data/model-art-worn.bin.br +0 -0
  43. package/src/data/model-art.bin.gz +0 -0
  44. package/src/data/tables/creatures.json.gz +0 -0
  45. package/src/data/world-map-sixel.bin.br +0 -0
  46. package/src/data/world-map.bin.gz +0 -0
  47. package/src/domain/character/character.ts +6 -0
  48. package/src/domain/pet/pet.ts +31 -0
  49. package/src/engine/combat/pickAction.ts +3494 -3487
  50. package/src/engine/combat/sessionChase.ts +12 -1
  51. package/src/engine/game/loadReconcile.ts +21 -1
  52. package/src/engine/game/pets.ts +33 -1
  53. package/src/engine/game/professions.ts +4 -0
  54. package/src/engine/game/squadQuestShare.ts +13 -1
  55. package/src/engine/game/types.ts +4 -0
@@ -248,7 +248,18 @@ export function createChaseArm(deps: ChaseDeps): {
248
248
  if (isEnemySide(f.side)) {
249
249
  // The GRADE is the full contact ring while the PARK is half of it, which is the split the C++
250
250
  // makes — and it retires the knife edge where one ULP of step error triggered a full-HP evade.
251
- const reach = Math.max(pairContact, longestOffenseRangeYd(f) + EVADE_REACH_EPS_YD);
251
+ let reach = Math.max(pairContact, longestOffenseRangeYd(f) + EVADE_REACH_EPS_YD);
252
+ // With no ranged kit the park below stops at the short offense, so grade against it too. A
253
+ // Razormane Scout's Shoot 6660 infers instant_nuke: it parked at 24 yd, shot, and reset every 5 s.
254
+ // Scheduled spells only, so a kit row the mob never casts still lets it evade.
255
+ if (dist > reach) {
256
+ const castable = castableKitIds(f, ctx.spellOf);
257
+ for (const s of shortOffenseList(f)) {
258
+ if (castable && !castable.has(s.id | 0)) continue;
259
+ if (!spellTargetTypeAllowed(s, tgt.creatureType ?? 0) || focusImmunityBlocksSpell(s, tgt, 1)) continue;
260
+ reach = Math.max(reach, (s.rangeMax ?? 0) + EVADE_REACH_EPS_YD);
261
+ }
262
+ }
252
263
  const prev = unreachableFrom.get(f.side);
253
264
  const victimMoved = !!prev && (
254
265
  to.z != null && prev.victimZ != null
@@ -48,7 +48,7 @@ import { emptyInventory } from '../../domain/items/inventory.ts';
48
48
  import { trimSoulShards } from '../../domain/bots/soulShardCap.ts';
49
49
  import { companionLevel } from '../../domain/bots/squad.ts';
50
50
  import { syncCompanionProfessionTools } from './companionTraining.ts';
51
- import { petHasHappiness, reconcilePetPartyAuras } from '../../domain/pet/pet.ts';
51
+ import { petHasHappiness, reconcilePetPartyAuras, tpSpentOnKnown } from '../../domain/pet/pet.ts';
52
52
  import { playerbotJunkItem } from '../../domain/bots/playerbotItemEligibility.ts';
53
53
  import type { EquippedMap } from '../../domain/character/character.ts';
54
54
  import { createAliveDeathFlow, isPlayerAlive } from '../../domain/run/death.ts';
@@ -324,6 +324,26 @@ export function applyLoadReconcile(ds: DataStore, state: GameState, opts?: impor
324
324
  }
325
325
  }
326
326
 
327
+ // A pet tamed before the bake dropped its rank-1 filler was charged training points for rank 1 of
328
+ // every family chain on top of its real kit, and may still queue those ranks to teach. Refund the
329
+ // difference once and keep the queue to the real kit. Cite: Pet.cpp:1955-1998 InitPetCreateSpells.
330
+ {
331
+ for (const p of [state.char.pet, state.char.storedPet, ...(state.char.stablePets ?? [])]) {
332
+ if (!p || p.innateKit) continue;
333
+ const cdef = ds.creature(p.creature);
334
+ if (!cdef || !petHasHappiness(cdef)) continue;
335
+ const kit = cdef.petDefaultSpells ?? [];
336
+ const filler = (cdef.petSpells ?? []).filter((id) => {
337
+ const s = ds.spell(id);
338
+ return !!s && !s.passive && (s.rank | 0) <= 1;
339
+ });
340
+ const refund = tpSpentOnKnown([...kit, ...filler], ds) - tpSpentOnKnown(kit, ds);
341
+ if (refund > 0 && p.trainingPoints != null) p.trainingPoints += refund;
342
+ if (p.teachSpells) p.teachSpells = p.teachSpells.filter((id) => kit.includes(id));
343
+ p.innateKit = [...kit];
344
+ }
345
+ }
346
+
327
347
  // Action bars: normalize any loaded/legacy shape, and for a save that predates them
328
348
  // (empty) seed bar 1 with the player's usable abilities so combat isn't blank.
329
349
  state.actionBars = normalizeBars(state.actionBars);
@@ -3,7 +3,7 @@
3
3
 
4
4
  import {
5
5
  newPet, petHasHappiness, petTaughtIds, petRepertoireHas,
6
- feedPetHappinessGain, petDiet, isPetFood, tpForSpell, hasTpForSpell,
6
+ feedPetHappinessGain, petDiet, isPetFood, tpForSpell, hasTpForSpell, petUntrainCost, untrainPetState,
7
7
  LOYALTY_REBELLIOUS, FEED_PET_DURATION_S,
8
8
  autoFeedThreshold, AUTO_FEED_TARGET, HAPPINESS_MAX,
9
9
  applyPetPartyAuras, stripPetPartyAuras, petPartyAuraSpellIds, reconcilePetPartyAuras,
@@ -156,6 +156,8 @@ export function createPetsModule(ctx: GameCtx, extras: PetsExtras): Pick<
156
156
  | 'setPetAutoFeed'
157
157
  | 'buyPetSpell'
158
158
  | 'teachPetSpell'
159
+ | 'untrainPet'
160
+ | 'untrainPetCost'
159
161
  | 'togglePetAutocast'
160
162
  | 'setPetCommand'
161
163
  | 'setPetReact'
@@ -319,6 +321,36 @@ export function createPetsModule(ctx: GameCtx, extras: PetsExtras): Pick<
319
321
  return { ok: true };
320
322
  },
321
323
 
324
+ untrainPetCost(nowMs) {
325
+ const pet = state.char.pet;
326
+ const cdef = pet ? ds.creature(pet.creature) : undefined;
327
+ if (!pet || !cdef || !petHasHappiness(cdef)) return null;
328
+ return petUntrainCost(pet, nowMs ?? Date.now());
329
+ },
330
+
331
+ untrainPet(trainerEntry, nowMs) {
332
+ const trainer = ds.trainer(trainerEntry);
333
+ if (!trainer || !(trainer.subname ?? '').toLowerCase().includes('pet trainer')) {
334
+ return { ok: false, reason: 'That is not a pet trainer' };
335
+ }
336
+ if (!canInteractWithCreature(ds, state.char.race, trainerEntry)) {
337
+ return { ok: false, reason: 'They refuse to deal with you.' };
338
+ }
339
+ const pet = state.char.pet;
340
+ if (!pet) return { ok: false, reason: 'You have no pet' };
341
+ if (pet.health <= 0) return { ok: false, reason: 'Your pet is dead' };
342
+ const cdef = ds.creature(pet.creature);
343
+ if (!cdef || !petHasHappiness(cdef)) return { ok: false, reason: 'Only a hunter pet can be untrained' };
344
+ if (!petTaughtIds(state.char, cdef).length) return { ok: false, reason: `${pet.name} has nothing to forget` };
345
+ const now = nowMs ?? Date.now();
346
+ const cost = petUntrainCost(pet, now);
347
+ if (state.char.gold < cost) return { ok: false, reason: 'Not enough money' };
348
+ state.char.gold -= cost;
349
+ state.char.pet = { ...untrainPetState(pet, cdef, state.char.level, ds), resetTalentsCost: cost, resetTalentsTime: now };
350
+ log(`${pet.name} forgets its training for ${moneyMarked(cost)} - ${state.char.pet.trainingPoints} training points to spend again.`);
351
+ return { ok: true, spent: cost };
352
+ },
353
+
322
354
  togglePetAutocast(spellId) {
323
355
  const pet = state.char.pet;
324
356
  if (!pet) return { ok: false, reason: 'You have no pet' };
@@ -167,6 +167,7 @@ import {
167
167
  creditSquadGoEnder,
168
168
  creditSquadKill,
169
169
  creditSquadLogs,
170
+ giveSquadQuestItem,
170
171
  } from './squadQuestShare.ts';
171
172
  import { applyGoSpellcasterCast } from './applyGoSpellcasterCast.ts';
172
173
  import { moneyMarked } from '../../domain/economy/money.ts';
@@ -1558,6 +1559,9 @@ export function createProfessionsModule(
1558
1559
  const yielded: LootDrop[] = goLootTable.length
1559
1560
  ? mergeLootDrops(Array.from({ length: miningTaps }, () => rollLoot(goLootTable, rng, goLootCtx)).flat())
1560
1561
  : [];
1562
+ // Each companion on the quest takes their own copy, as kill loot and dungeon chests already do.
1563
+ // Without it a quest item from an object never reached them and their copy could not finish.
1564
+ for (const y of yielded) giveSquadQuestItem(state, ds, y.item, y.count);
1561
1565
  // HOLD: the object keeps its contents until the player clicks them (the loot window rolls
1562
1566
  // each item's identity as it fills, so taking never re-rolls a suffix).
1563
1567
  let heldLoot: PendingLoot | undefined;
@@ -206,6 +206,10 @@ function isPermanentQuestRefusal(reason: string | undefined): boolean {
206
206
  return !!reason && (reason.startsWith('Not a quest for ') || reason === 'Unknown quest');
207
207
  }
208
208
 
209
+ function isUnfinishedTurnIn(reason: string | undefined): boolean {
210
+ return reason === 'Quest is not complete' || reason === 'Quest failed';
211
+ }
212
+
209
213
  export function shareSquadAccept(
210
214
  state: GameState,
211
215
  ds: DataStore,
@@ -237,7 +241,15 @@ export function shareSquadTurnIn(
237
241
  if (!r.ok) {
238
242
  // Only for one who HAS the quest — a companion who never took it was named at accept.
239
243
  const held = getQuest(companionQuestLog(comp), entry);
240
- if (held && held.status !== 'turnedIn') feed?.(`${comp.name} cannot hand it in: ${r.reason}.`);
244
+ if (!held || held.status === 'turnedIn') continue;
245
+ // The party moves on once the player hands in, so a copy that never finished (its item never
246
+ // reached their bags) would sit in their log for good and fill it. Bags full keeps the quest.
247
+ if (isUnfinishedTurnIn(r.reason)) {
248
+ comp.questLog = abandonQuest(companionQuestLog(comp), entry);
249
+ feed?.(`${comp.name} never finished it and drops it.`);
250
+ } else {
251
+ feed?.(`${comp.name} cannot hand it in: ${r.reason}.`);
252
+ }
241
253
  continue;
242
254
  }
243
255
  if ((r.money ?? 0) > 0) state.char = { ...state.char, gold: state.char.gold + (r.money ?? 0) };
@@ -768,6 +768,10 @@ export interface Game {
768
768
  // Beast Training (spell 5149): teach the CURRENT pet a rank from the repertoire, spending ITS training
769
769
  // points (chain-aware). Cite: Pet.cpp GetTPForSpell/learnSpell.
770
770
  teachPetSpell(spellId: number): ActionResult;
771
+ // Untrain the current hunter pet at a pet trainer: forget every taught ability, training points back
772
+ // to level × (loyalty − 1), for the escalating fee petUntrainCost reads. Cite: HandlePetUnlearnOpcode.
773
+ untrainPet(trainerEntry: number, nowMs?: number): ActionResult<{ spent: number }>;
774
+ untrainPetCost(nowMs?: number): number | null;
771
775
  // Toggle autocast for a pet ability (right-click on the pet bar). Rank-agnostic by name chain; disabled
772
776
  // abilities stay learned but leave the combat kit (buildPet).
773
777
  togglePetAutocast(spellId: number): ActionResult<{ enabled: boolean }>;