azeroth-shell 0.1.2 → 0.3.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 +362 -85
- package/bin/azsh.mjs +35 -19
- package/package.json +12 -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 +145 -0
- package/src/cli/iconGrid.mjs +100 -0
- package/src/cli/modelArt.mjs +298 -0
- package/src/cli/oocPrep.mjs +32 -0
- package/src/cli/paperdoll.mjs +76 -0
- package/src/cli/perfCheck.mjs +56 -0
- package/src/cli/repl.mjs +48 -7
- package/src/cli/rotationText.mjs +6 -6
- package/src/cli/sixel.mjs +319 -0
- package/src/cli/term.mjs +106 -12
- package/src/cli/text.mjs +21 -3
- package/src/cli/tui.mjs +262 -102
- package/src/cli/tuiAutopilot.mjs +356 -0
- package/src/cli/tuiBags.mjs +37 -4
- package/src/cli/tuiBg.mjs +370 -41
- package/src/cli/tuiCheck.mjs +633 -37
- package/src/cli/tuiCity.mjs +146 -41
- package/src/cli/tuiCompanionKit.mjs +6 -6
- package/src/cli/tuiCompanions.mjs +211 -41
- package/src/cli/tuiCreate.mjs +230 -0
- package/src/cli/tuiDungeon.mjs +57 -21
- package/src/cli/tuiFight.mjs +467 -88
- package/src/cli/tuiFightPanes.mjs +42 -14
- package/src/cli/tuiFloor.mjs +21 -9
- package/src/cli/tuiGear.mjs +17 -14
- package/src/cli/tuiLoot.mjs +6 -6
- package/src/cli/tuiMap.mjs +214 -0
- package/src/cli/tuiPaperdoll.mjs +108 -0
- package/src/cli/tuiPartyStage.mjs +206 -0
- package/src/cli/tuiPet.mjs +16 -8
- package/src/cli/tuiProfessions.mjs +27 -25
- package/src/cli/tuiRotationCond.mjs +390 -0
- package/src/cli/tuiSaves.mjs +24 -1
- package/src/cli/tuiSearch.mjs +79 -27
- package/src/cli/tuiSettings.mjs +3 -4
- package/src/cli/tuiStage.mjs +360 -0
- package/src/cli/tuiTalentTree.mjs +124 -0
- package/src/cli/tuiTalents.mjs +3 -4
- package/src/cli/tuiViews.mjs +633 -172
- package/src/cli/tuiWiki.mjs +2 -3
- package/src/cli/tuiWorld.mjs +90 -7
- 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-worn.bin.br +0 -0
- package/src/data/model-art.bin.gz +0 -0
- package/src/data/rotations/pve/paladin/retribution.json +2 -2
- package/src/data/tables/creatures.json.gz +0 -0
- package/src/data/world-map-sixel.bin.br +0 -0
- package/src/data/world-map.bin.gz +0 -0
- 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/character/character.ts +6 -0
- package/src/domain/pet/pet.ts +31 -0
- 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 +3494 -3449
- package/src/engine/combat/procsDamage.ts +31 -1
- package/src/engine/combat/sessionChase.ts +19 -2
- package/src/engine/combat/sessionMotionKit.ts +40 -2
- package/src/engine/game/loadReconcile.ts +21 -1
- package/src/engine/game/pets.ts +33 -1
- package/src/engine/game/types.ts +4 -0
- package/src/quests/quests.ts +60 -0
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
// The Companions tab: your party and bench, hiring at an inn, one page per companion (role, gear, bags,
|
|
2
2
|
// professions) and their gathering trips, and the squad mode that decides who fights beside you. A trimmed
|
|
3
3
|
// port of phonecraft's companionsPanel.ts, companionMissionsDesk.ts and squadModePicker.ts.
|
|
4
|
-
import { bold, dim, cyan, yellow, green, red, qualityPaint, fit, plural, itemText } from './text.mjs';
|
|
4
|
+
import { bold, dim, cyan, yellow, green, red, inverse, qualityPaint, fit, cut, plural, itemText, vis, wrapLines, coinText, coin } from './text.mjs';
|
|
5
|
+
import { miniIcon } from './iconArt.mjs';
|
|
6
|
+
import { paperdollGrid, paperdollPlan } from './tuiPaperdoll.mjs';
|
|
5
7
|
import { itemLines } from './tuiViews.mjs';
|
|
6
8
|
import { recipeBook, openSendGoods, companionsOf } from './tuiProfessions.mjs';
|
|
7
9
|
import { SQUAD_MODES, SQUAD_MODE_LABEL, squadModeOf, setSquadMode } from './squad.mjs';
|
|
8
10
|
import { settingsRows, petRows, questRows, teachRow, bagRows, bankRows, shareDialog, vaultDialog, hireSearch, tavernDuel } from './tuiCompanionKit.mjs';
|
|
9
11
|
import { barsDialog, companionBarsOwner } from './tuiBars.mjs';
|
|
10
12
|
import { enchantLines } from './tuiGear.mjs';
|
|
11
|
-
import { moneyText } from '../domain/economy/money.ts';
|
|
12
13
|
import { className, raceName } from '../domain/core/ids.ts';
|
|
14
|
+
import { paperdollArt } from './paperdoll.mjs';
|
|
13
15
|
import { bagCapacity, EQUIP_SLOTS, SLOT_LABEL, itemSlotTargets } from '../domain/items/equipment.ts';
|
|
14
16
|
import { equippedEntry } from '../domain/items/itemInstance.ts';
|
|
15
17
|
import { companionCharacter, companionLevel, hireSkillMap, MAX_COMPANIONS, SQUAD_ROLES } from '../domain/bots/squad.ts';
|
|
16
18
|
import { companionEquipOpts } from '../domain/professions/autoDisenchant.ts';
|
|
17
19
|
import { companionSkillCap, companionTrainedRank, SECONDARY_SKILLS } from '../domain/bots/botProfessions.ts';
|
|
18
|
-
import { displayGearScore, gearProfileLabel } from '../domain/bots/companionHireCost.ts';
|
|
20
|
+
import { displayGearScore, gearProfileLabel, hireCostCopper } from '../domain/bots/companionHireCost.ts';
|
|
21
|
+
import { GEAR_PROFILES } from '../domain/bots/botGear.ts';
|
|
22
|
+
import { searchWorldBots, innZoneSalt, pickProfile, WORLD_BOT_COUNT } from '../domain/bots/botPool.ts';
|
|
19
23
|
import { maxHealth, maxPower } from '../domain/character/character.ts';
|
|
20
24
|
import { effectiveStats, effectiveArmor, attackPower, powerType } from '../domain/character/characterStats.ts';
|
|
21
|
-
import { critChanceDisplay, dodgeChanceDisplay } from '../domain/character/characterRatings.ts';
|
|
25
|
+
import { critChanceDisplay, dodgeChanceDisplay, hitChance } from '../domain/character/characterRatings.ts';
|
|
22
26
|
import {
|
|
23
27
|
canCompanionLearnProfession, countDoneMissions, hoursAffordable, isMissionSkill, missionEtaLabel, missionHoursLabel,
|
|
24
28
|
missionProgress, missionVerb, energyResetsAtMs, MAX_COMPANION_ENERGY,
|
|
@@ -62,9 +66,15 @@ function profTags(lines) {
|
|
|
62
66
|
|
|
63
67
|
function profLines(app, lines) {
|
|
64
68
|
if (!lines.length) return [` ${dim('none')}`];
|
|
65
|
-
return lines.map((l) =>
|
|
69
|
+
return lines.map((l) => { const prof = app.ds.profession(l.line); return paneLine(app, ` ${miniIcon(prof?.icon)}${fit(prof?.name ?? l.line, 15)} ${fit(`${l.current}/${l.max}`, 8)} ${dim(l.rank ?? '')}`); });
|
|
66
70
|
}
|
|
67
71
|
|
|
72
|
+
/** A detail-pane line kept to one row: a mini icon (sixel) is blank cells the pane's wrap would squeeze. */
|
|
73
|
+
function paneLine(app, line) { const w = (app.layout?.().detailW ?? 0) - 2; return w > 0 && vis(line) > w ? cut(line, w) : line; }
|
|
74
|
+
|
|
75
|
+
/** What a piece is, for the line under its name: Plate 412 armor, Sword 41-77. */
|
|
76
|
+
function gearKind(it) { return it ? [it.typeName, it.armor ? `${it.armor} armor` : '', it.weapon ? `${it.weapon.dmgMin}-${it.weapon.dmgMax}` : ''].filter(Boolean).join(' ') : ''; }
|
|
77
|
+
|
|
68
78
|
function statLines(app, char) {
|
|
69
79
|
const ds = app.ds, st = effectiveStats(char, ds), pt = powerType(char);
|
|
70
80
|
return [
|
|
@@ -76,14 +86,22 @@ function statLines(app, char) {
|
|
|
76
86
|
|
|
77
87
|
function statRows(app, char, note) {
|
|
78
88
|
const lines = statLines(app, char);
|
|
79
|
-
return lines.map((l) => ({ text: l.trim(), detail: () => [bold('Stats'), '', ...lines, '', dim(note)] }));
|
|
89
|
+
return lines.map((l) => ({ text: l.trim(), detail: () => withArt(app, char, [bold('Stats'), '', ...lines, '', dim(note)]) }));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The text, then `subject` (a companion, a candidate's body, any { race, gender, name, equipped }) drawn in their gear under it when the pane has room. */
|
|
93
|
+
function withArt(app, subject, text) {
|
|
94
|
+
const L = app.layout(), w = L.detailW - 2;
|
|
95
|
+
const used = text.reduce((n, line) => n + (vis(line) > w ? wrapLines(line, w).length : 1), 0); // as the pane wraps them
|
|
96
|
+
const art = paperdollArt(subject, w, L.mainH - used - 1);
|
|
97
|
+
return art ? [...text, '', ...art] : text;
|
|
80
98
|
}
|
|
81
99
|
|
|
82
100
|
function wornLines(app, gear) {
|
|
83
101
|
const out = [];
|
|
84
102
|
for (const slot of EQUIP_SLOTS) {
|
|
85
103
|
const e = equippedEntry(gear?.[slot]);
|
|
86
|
-
if (e != null) { const it = app.ds.item(e); out.push(` ${fit(SLOT_LABEL[slot] ?? slot, 10)} ${qualityPaint(it?.quality)(it?.name ?? e)}`); }
|
|
104
|
+
if (e != null) { const it = app.ds.item(e); out.push(paneLine(app, ` ${miniIcon(it?.icon)}${fit(SLOT_LABEL[slot] ?? slot, 10)} ${qualityPaint(it?.quality)(it?.name ?? e)}`)); }
|
|
87
105
|
}
|
|
88
106
|
return out.length ? out : [` ${dim('nothing')}`];
|
|
89
107
|
}
|
|
@@ -131,10 +149,10 @@ export function companionsView(app) {
|
|
|
131
149
|
rows(a) {
|
|
132
150
|
const g = a.g, squad = g.state.squad ?? [], bench = Object.values(g.state.botRoster ?? {});
|
|
133
151
|
const energy = g.companionPartyEnergy(Date.now());
|
|
134
|
-
const rows = [H(`Party ${squad.length}/${MAX_COMPANIONS} ${dim(`trip energy ${energy}/${MAX_COMPANION_ENERGY}`)} ${
|
|
152
|
+
const rows = [H(`Party ${squad.length}/${MAX_COMPANIONS} ${dim(`trip energy ${energy}/${MAX_COMPANION_ENERGY}`)} ${coinText(a.ch.gold)}`), squadModeRow(a)];
|
|
135
153
|
const row = (c) => ({
|
|
136
154
|
text: `${fit(c.name, 12)} ${fit(dim(`L${companionLevel(c, a.ch.level)} ${className(c.cls)}`), 12)} ${fit(roleText(c.role), 6)} ${c.mission ? yellow('away ') : ''}${profTags(compProfLines(a, c))}`,
|
|
137
|
-
detail: () => [...compLines(a, c), '', dim('Enter: open their page')],
|
|
155
|
+
detail: () => withArt(a, c, [...compLines(a, c), '', dim('Enter: open their page')]), // them in their gear
|
|
138
156
|
act: () => a.push(companionDialog(a, c.id)),
|
|
139
157
|
});
|
|
140
158
|
if (!squad.length && !bench.length) rows.push(H(''), H(dim(' You travel alone. Innkeepers hire out companions: they fight beside you, carry and craft.')));
|
|
@@ -143,8 +161,8 @@ export function companionsView(app) {
|
|
|
143
161
|
const inn = g.innkeeperHere();
|
|
144
162
|
rows.push(H(''), H('Hire'));
|
|
145
163
|
if (inn) {
|
|
146
|
-
const n =
|
|
147
|
-
rows.push({ text: `Hire a companion at ${inn.name} ${dim(`(${plural(n, 'face')})`)}`, detail: () => [bold(inn.name), 'Innkeeper', '', `${plural(n, 'adventurer')}
|
|
164
|
+
const n = hirePool(a).list.length;
|
|
165
|
+
rows.push({ text: `Hire a companion at ${inn.name} ${dim(`(${plural(n, 'face')})`)}`, detail: () => [bold(inn.name), 'Innkeeper', '', `${plural(n, 'adventurer')} of your faction looking for work, every level, sorted by role and kit. A hire joins at their own level in their own gear; a bench companion comes back free.`], act: () => a.push(hireDialog(a)) });
|
|
148
166
|
} else rows.push(H(dim(' No inn here. Companions are hired from innkeepers (Goldshire, Razor Hill, every capital).')));
|
|
149
167
|
if (squad.length || bench.length) {
|
|
150
168
|
const ups = g.squadUpgradePlan().length;
|
|
@@ -187,7 +205,41 @@ function yieldText(app, rows) {
|
|
|
187
205
|
|
|
188
206
|
// ---- hiring -------------------------------------------------------------------------------------------
|
|
189
207
|
|
|
190
|
-
|
|
208
|
+
// Every same-faction face not already yours, from the bot pool alone: who they are, their spec, kit and
|
|
209
|
+
// price. Their gear is the dear part (a gear walk each, 6 s for 3000), so a face gets it when you look at
|
|
210
|
+
// them. The inn's own slate rolls its kits as a set, and hiring prices a slate face by that roll, so those
|
|
211
|
+
// faces come from the slate.
|
|
212
|
+
function hirePool(app) {
|
|
213
|
+
const g = app.g, ch = g.state.char;
|
|
214
|
+
const taken = [...(g.state.squad ?? []).map((c) => c.id), ...Object.keys(g.state.botRoster ?? {})];
|
|
215
|
+
const key = `${ch.zoneId}|${ch.level}|${ch.race}|${g.innkeeperHere()?.entry}|${taken.sort().join(',')}`;
|
|
216
|
+
if (app.hirePoolMemo?.g === g && app.hirePoolMemo.key === key) return app.hirePoolMemo;
|
|
217
|
+
const slate = new Map(g.innCandidates().map((c) => [c.bot.id, c]));
|
|
218
|
+
const salt = innZoneSalt(ch.zoneId, ch.level);
|
|
219
|
+
const bots = g.innkeeperHere() ? searchWorldBots({ ds: app.ds, playerRace: ch.race, playerLevel: ch.level, zoneId: ch.zoneId, excludeIds: taken, filters: { roles: SQUAD_ROLES, limit: WORLD_BOT_COUNT } }) : [];
|
|
220
|
+
const list = bots.map((bot) => slate.get(bot.id) ?? (() => {
|
|
221
|
+
const spec = app.ds.botSpecs(bot.cls)?.[bot.specIdx], gearProfile = pickProfile(bot.id, salt, ch.level);
|
|
222
|
+
return { bot, ...(spec ? { specName: spec.name, specRole: spec.role } : {}), gearProfile, hireCostCopper: hireCostCopper(bot.level, gearProfile) };
|
|
223
|
+
})());
|
|
224
|
+
app.hirePoolMemo = { g, key, list, full: new Map(), tags: new Map() };
|
|
225
|
+
return app.hirePoolMemo;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** A pool face with their gear on: built once, through the engine's own search so it is the body a hire gets. */
|
|
229
|
+
function fullCand(app, c) {
|
|
230
|
+
if (c.gear) return c;
|
|
231
|
+
const memo = hirePool(app);
|
|
232
|
+
if (!memo.full.has(c.bot.id)) {
|
|
233
|
+
const b = c.bot;
|
|
234
|
+
memo.full.set(b.id, app.g.searchInnCandidates({ name: b.name, classes: [b.cls], minLevel: b.level, maxLevel: b.level, limit: WORLD_BOT_COUNT }).find((x) => x.bot.id === b.id) ?? null);
|
|
235
|
+
}
|
|
236
|
+
return memo.full.get(c.bot.id) ?? null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function findCand(app, botId) {
|
|
240
|
+
const c = [...app.g.innCandidates(), ...(app.hireResults ?? [])].find((x) => x.bot.id === botId) ?? hirePool(app).list.find((x) => x.bot.id === botId);
|
|
241
|
+
return c ? fullCand(app, c) : null;
|
|
242
|
+
}
|
|
191
243
|
|
|
192
244
|
// The body they would bring: same shell hireCandidate builds, on their own spec, for the stat preview.
|
|
193
245
|
function candCharacter(app, c) {
|
|
@@ -197,15 +249,16 @@ function candCharacter(app, c) {
|
|
|
197
249
|
|
|
198
250
|
function hireText(app, c, short = false) {
|
|
199
251
|
const cost = c.hireCostCopper ?? 0;
|
|
200
|
-
|
|
252
|
+
if (!cost) return yellow('free');
|
|
253
|
+
return (app.ch.gold >= cost ? yellow : red)(short ? shortMoney(cost) : coinText(cost));
|
|
201
254
|
}
|
|
202
255
|
|
|
203
256
|
// The two biggest coins only, for the list column: 1g 20s, 51s, 76c.
|
|
204
257
|
function shortMoney(copper) {
|
|
205
258
|
const g = Math.floor(copper / 10000), sv = Math.floor((copper % 10000) / 100), c = copper % 100;
|
|
206
|
-
if (g) return `${g}g${sv ? ` ${sv}s` : ''}`;
|
|
207
|
-
if (sv) return `${sv}s`;
|
|
208
|
-
return
|
|
259
|
+
if (g) return `${g}${coin('g')}${sv ? ` ${sv}${coin('s')}` : ''}`;
|
|
260
|
+
if (sv) return `${sv}${coin('s')}`;
|
|
261
|
+
return `${c}${coin('c')}`;
|
|
209
262
|
}
|
|
210
263
|
|
|
211
264
|
function candLines(app, c) {
|
|
@@ -223,7 +276,7 @@ function candLines(app, c) {
|
|
|
223
276
|
function hireAs(app, c, role) {
|
|
224
277
|
const r = app.g.hireCandidate(c.bot.id, role);
|
|
225
278
|
if (r.ok && app.hireResults) app.hireResults = app.hireResults.filter((x) => x.bot.id !== c.bot.id);
|
|
226
|
-
return app.result(r, `${r.name ?? c.bot.name} joins you as ${role}${r.costCopper ? ` for ${
|
|
279
|
+
return app.result(r, `${r.name ?? c.bot.name} joins you as ${role}${r.costCopper ? ` for ${coinText(r.costCopper)}` : ''}${r.benched ? ', on the bench' : ''}.`);
|
|
227
280
|
}
|
|
228
281
|
|
|
229
282
|
function pickRoleAndHire(app, c, after) {
|
|
@@ -231,31 +284,77 @@ function pickRoleAndHire(app, c, after) {
|
|
|
231
284
|
app.pick(`${c.bot.name}: what role?`, roles.map((r) => ({ label: `${roleText(r)}${r === c.specRole ? dim(' (their spec)') : ''}`, value: r })), (role) => { if (hireAs(app, c, role)) after?.(); });
|
|
232
285
|
}
|
|
233
286
|
|
|
287
|
+
// The hire list's two filters: a role tab (Left and Right) and a kit (t), each with its count under the other.
|
|
288
|
+
const HIRE_ROLES = [{ role: null, label: 'All' }, { role: 'dps', label: 'DPS' }, { role: 'healer', label: 'Heal' }, { role: 'tank', label: 'Tank' }];
|
|
289
|
+
const KIT_SHORT = { naked: 'Naked', shabby: 'Shabby', field: 'Field', twink: 'Twink', 'raid-mixed': 'Raid', 'raid-t2': 'Raid T2', 'raid-t3': 'Raid T3' };
|
|
290
|
+
const roleOfCand = (c) => c.specRole ?? 'dps';
|
|
291
|
+
|
|
234
292
|
function hireDialog(app, results) {
|
|
235
|
-
|
|
236
|
-
|
|
293
|
+
// The kits this list has, cheapest first; t steps through them and back to any.
|
|
294
|
+
const kitsOf = (list) => [null, ...GEAR_PROFILES.filter((p) => list.some((c) => c.gearProfile === p))];
|
|
295
|
+
const view = {
|
|
296
|
+
id: results ? 'hire-search' : 'hire', hint: 'Enter: look closer Left Right: role',
|
|
297
|
+
roleTab: 0, kit: null, placed: false,
|
|
298
|
+
// The filter rows scroll off a long list; the breadcrumb keeps them in sight.
|
|
299
|
+
get title() { return [results ? 'Search' : 'Hire', HIRE_ROLES[this.roleTab].role && HIRE_ROLES[this.roleTab].label, this.kit && KIT_SHORT[this.kit]].filter(Boolean).join(' · '); },
|
|
237
300
|
keys: {
|
|
238
301
|
h: { hint: 'hire', fn: (a, row) => { const c = row?.data && findCand(a, row.data); if (c) pickRoleAndHire(a, c); } },
|
|
302
|
+
t: { hint: 'kit', fn: (a) => stepKit(a, +1) },
|
|
303
|
+
T: { hint: 'kit back', fn: (a) => stepKit(a, -1) },
|
|
239
304
|
'/': { hint: 'search', fn: (a) => hireSearch(a, (found) => { a.hireResults = found; a.push(hireDialog(a, found)); }) },
|
|
240
305
|
D: { hint: 'duel', fn: (a, row) => { if (row?.data) tavernDuel(a, row.data); } },
|
|
241
306
|
},
|
|
307
|
+
onKey(a, k) {
|
|
308
|
+
const d = { right: +1, tab: +1, left: -1, 'shift-tab': -1 }[k];
|
|
309
|
+
if (!d) return false;
|
|
310
|
+
view.roleTab = (view.roleTab + d + HIRE_ROLES.length) % HIRE_ROLES.length;
|
|
311
|
+
view.placed = false;
|
|
312
|
+
a.render();
|
|
313
|
+
return true;
|
|
314
|
+
},
|
|
242
315
|
rows(a) {
|
|
243
|
-
const g = a.g,
|
|
316
|
+
const g = a.g, all = results ?? hirePool(a).list, memo = hirePool(a);
|
|
244
317
|
const squad = g.state.squad ?? [];
|
|
245
|
-
const
|
|
246
|
-
|
|
318
|
+
const { role } = HIRE_ROLES[view.roleTab];
|
|
319
|
+
const ofRole = (c) => !role || roleOfCand(c) === role, ofKit = (c) => !view.kit || c.gearProfile === view.kit;
|
|
320
|
+
const list = all.filter((c) => ofRole(c) && ofKit(c));
|
|
321
|
+
const roleTabs = HIRE_ROLES.map((t, i) => { const label = ` ${t.label} ${all.filter((c) => (!t.role || roleOfCand(c) === t.role) && ofKit(c)).length} `; return i === view.roleTab ? inverse(bold(label)) : dim(label); }).join(' ');
|
|
322
|
+
const kitTabs = kitsOf(all).map((p) => { const label = p ? `${KIT_SHORT[p] ?? p} ${all.filter((c) => ofRole(c) && c.gearProfile === p).length}` : 'Any'; return p === view.kit ? inverse(bold(` ${label} `)) : dim(` ${label} `); });
|
|
323
|
+
// Seven kits and their counts outrun the list pane at 60: they wrap onto a second row.
|
|
324
|
+
const kitW = a.listWOf(view, a.layout()) - 2, kitLines = [`Kit`];
|
|
325
|
+
for (const t of [...kitTabs, dim(' t')]) { if (vis(kitLines.at(-1)) + vis(t) > kitW) kitLines.push(' '); kitLines[kitLines.length - 1] += t; }
|
|
326
|
+
const rows = [
|
|
327
|
+
H(`${plural(list.length, 'adventurer')} for hire ${squad.length >= MAX_COMPANIONS ? yellow('party full: a new hire goes to the bench') : dim(`party ${squad.length}/${MAX_COMPANIONS}`)} ${coinText(a.ch.gold)}`),
|
|
328
|
+
H(`${roleTabs} ${dim('Left Right')}`), ...kitLines.map(H), H(''),
|
|
329
|
+
];
|
|
330
|
+
if (!list.length) rows.push(H(dim(all.length ? ' Nobody here fits that role and kit.' : ' Nobody is looking for work here right now.')));
|
|
247
331
|
for (const c of list) {
|
|
248
332
|
const b = c.bot;
|
|
333
|
+
if (!memo.tags.has(b.id)) memo.tags.set(b.id, profTags(candProfLines(c)));
|
|
249
334
|
rows.push({
|
|
250
|
-
text: `${fit(b.name, 12)} ${fit(dim(`L${b.level} ${className(b.cls)}`), 12)} ${fit(c.specRole ? roleText(c.specRole) : '', 6)} ${fit(hireText(a, c, true), 7)} ${
|
|
335
|
+
text: `${fit(b.name, 12)} ${fit(dim(`L${b.level} ${className(b.cls)}`), 12)} ${fit(c.specRole ? roleText(c.specRole) : '', 6)} ${fit(dim(KIT_SHORT[c.gearProfile] ?? ''), 7)} ${fit(hireText(a, c, true), 7)} ${memo.tags.get(b.id)}`,
|
|
251
336
|
data: b.id,
|
|
252
|
-
detail: () => [...candLines(a,
|
|
337
|
+
detail: () => { const full = fullCand(a, c); return full ? withArt(a, candCharacter(a, full), [...candLines(a, full), '', dim('Enter: their page h: hire')]) : []; },
|
|
253
338
|
act: () => a.push(candidateDialog(a, b.id)),
|
|
254
339
|
});
|
|
255
340
|
}
|
|
341
|
+
// A new list opens on your own level (the slate's window), not on the level 1s at its top.
|
|
342
|
+
if (!view.placed && list.length) {
|
|
343
|
+
view.placed = true;
|
|
344
|
+
const at = list.findIndex((c) => c.bot.level >= a.ch.level - 2);
|
|
345
|
+
view.cursor = rows.length - list.length + (at < 0 ? list.length - 1 : at);
|
|
346
|
+
view.scroll = at > 0 ? view.cursor - 3 : 0; // a few lower levels above it, or the filter rows when it heads the list
|
|
347
|
+
}
|
|
256
348
|
return rows;
|
|
257
349
|
},
|
|
258
350
|
};
|
|
351
|
+
const stepKit = (a, d) => {
|
|
352
|
+
const kits = kitsOf(results ?? hirePool(a).list);
|
|
353
|
+
view.kit = kits[(kits.indexOf(view.kit) + d + kits.length) % kits.length];
|
|
354
|
+
view.placed = false;
|
|
355
|
+
a.render();
|
|
356
|
+
};
|
|
357
|
+
return view;
|
|
259
358
|
}
|
|
260
359
|
|
|
261
360
|
/** One face at the inn: everything about them, and the hire buttons. */
|
|
@@ -266,7 +365,7 @@ function candidateDialog(app, botId) {
|
|
|
266
365
|
rows(a) {
|
|
267
366
|
const c = findCand(a, botId);
|
|
268
367
|
if (!c) { setTimeout(() => { if (a.top()?.id === viewId) a.pop(); }, 0); return [H('They are no longer looking for work.')]; }
|
|
269
|
-
const b = c.bot, ds = a.ds;
|
|
368
|
+
const b = c.bot, ds = a.ds, body = candCharacter(a, c);
|
|
270
369
|
const rows = [H(`${bold(b.name)} ${dim(`L${b.level} ${raceName(b.race)} ${className(b.cls)}`)} ${c.specName ?? ''} ${hireText(a, c)}`), H('')];
|
|
271
370
|
|
|
272
371
|
// Hire: their spec's role first; any role works, it rebuilds talents and rotation.
|
|
@@ -275,9 +374,9 @@ function candidateDialog(app, botId) {
|
|
|
275
374
|
for (const role of roles) {
|
|
276
375
|
rows.push({
|
|
277
376
|
text: `Hire as ${roleText(role)} for ${hireText(a, c)}${role === c.specRole ? dim(' their spec') : ''}`,
|
|
278
|
-
detail: () => [bold(`Hire ${b.name} as ${role}`),
|
|
377
|
+
detail: () => withArt(a, body, [bold(`Hire ${b.name} as ${role}`), `Cost: ${hireText(a, c)} you have ${coinText(a.ch.gold)}`,
|
|
279
378
|
role === c.specRole ? dim(`They already play ${c.specName}.`) : dim('They respec for the role: talents and rotation rebuild for free.'),
|
|
280
|
-
(a.g.state.squad ?? []).length >= MAX_COMPANIONS ? yellow('Your party is full: they start on the bench.') :
|
|
379
|
+
...((a.g.state.squad ?? []).length >= MAX_COMPANIONS ? [yellow('Your party is full: they start on the bench.')] : []), dim('A released hire refunds the price.')]),
|
|
281
380
|
act: () => { if (hireAs(a, c, role)) a.pop(); },
|
|
282
381
|
});
|
|
283
382
|
}
|
|
@@ -286,21 +385,22 @@ function candidateDialog(app, botId) {
|
|
|
286
385
|
for (const l of candProfLines(c)) {
|
|
287
386
|
const prof = ds.profession(l.line);
|
|
288
387
|
rows.push({
|
|
289
|
-
text: `${fit(prof?.name ?? l.line, 16)} ${fit(`${l.current}/${l.max}`, 8)} ${dim(l.rank ?? '')}
|
|
388
|
+
text: `${fit(prof?.name ?? l.line, 16)} ${fit(`${l.current}/${l.max}`, 8)} ${dim(l.rank ?? '')}`,
|
|
389
|
+
icon: prof?.icon, sub: dim(isSecondary(l.line) ? 'secondary' : prof?.kind ?? ''),
|
|
290
390
|
detail: () => [bold(prof?.name ?? l.line), `${l.current}/${l.max}${l.rank ? ` ${l.rank}` : ''}`, '',
|
|
291
391
|
isMissionSkill(l.line) ? `Once hired they can go on ${missionVerb(l.line).toLowerCase()} trips while you play.` : `Once hired they craft from their own ${prof?.name ?? l.line} recipe book with your materials.`,
|
|
292
392
|
dim('Companions train for free as they level, and can swap trades later.')],
|
|
293
393
|
});
|
|
294
394
|
}
|
|
295
395
|
|
|
296
|
-
rows.push(H(''), H('Stats'), ...statRows(a,
|
|
396
|
+
rows.push(H(''), H('Stats'), ...statRows(a, body, 'With the gear they arrive in, on their own spec.'));
|
|
297
397
|
|
|
298
398
|
rows.push(H(''), H(`Wearing ${dim(c.gearProfile ? `${gearProfileLabel(c.gearProfile)} kit` : '')}`));
|
|
299
399
|
for (const slot of EQUIP_SLOTS) {
|
|
300
400
|
const e = equippedEntry(c.gear?.[slot]);
|
|
301
401
|
if (e == null) continue;
|
|
302
402
|
const it = ds.item(e);
|
|
303
|
-
rows.push({ text: `${fit(SLOT_LABEL[slot] ?? slot, 10)} ${qualityPaint(it?.quality)(it?.name ?? e)}`, detail: () => (it ? itemLines(a, it) : []) });
|
|
403
|
+
rows.push({ text: `${fit(SLOT_LABEL[slot] ?? slot, 10)} ${qualityPaint(it?.quality)(it?.name ?? e)}`, icon: it?.icon, sub: dim(`${fit('', 10)} ${gearKind(it)}`), detail: () => (it ? itemLines(a, it) : []) });
|
|
304
404
|
}
|
|
305
405
|
return rows;
|
|
306
406
|
},
|
|
@@ -313,6 +413,7 @@ export function companionDialog(app, id) {
|
|
|
313
413
|
return {
|
|
314
414
|
id: `comp-${id}`, title: findComp(app, id)?.name ?? 'Companion', hint: 'Enter: act',
|
|
315
415
|
keys: {
|
|
416
|
+
c: { hint: 'character sheet', fn: (a) => a.push(companionSheet(a, id)) },
|
|
316
417
|
x: { hint: 'take off / drop', fn: (a, row) => {
|
|
317
418
|
const d = row?.data;
|
|
318
419
|
if (d?.slot && d.it) a.result(a.g.unequipCompanionSlot(id, d.slot), `${findComp(a, id)?.name} takes off ${itemText(d.it)}.`);
|
|
@@ -330,15 +431,19 @@ export function companionDialog(app, id) {
|
|
|
330
431
|
rows.push({ text: yellow(awayText(a, comp)), detail: () => missionStatusLines(a, comp), act: () => a.push(missionDialog(a, id, comp.mission.skill)) });
|
|
331
432
|
}
|
|
332
433
|
rows.push(H(''));
|
|
333
|
-
|
|
434
|
+
const note = (title, ...lines) => withArt(a, comp, [bold(title), '', ...lines]);
|
|
435
|
+
rows.push({ text: `Character sheet ${dim('c')}`, icon: 'inv_misc_head_human_01', sub: dim('their gear on the paperdoll, and their stats'),
|
|
436
|
+
detail: () => note('Character sheet', 'What they wear on the paperdoll, the way your Character tab shows you: each piece in its slot, its tooltip beside, their numbers underneath.'),
|
|
437
|
+
act: () => a.push(companionSheet(a, id)) });
|
|
438
|
+
rows.push({ text: `Role: ${roleText(comp.role)}`, detail: () => note('Role', 'Tank, healer or damage. Changing it is free and rebuilds their talents and rotation for the role.'),
|
|
334
439
|
act: () => a.pick(`${comp.name}: what role?`, SQUAD_ROLES.map((r) => ({ label: roleText(r), value: r })), (r) => a.result(g.setCompanionRole(id, r), `${comp.name} is now your ${r}.`)) });
|
|
335
440
|
rows.push(seated
|
|
336
|
-
? { text: 'Move to the bench', detail: () =>
|
|
337
|
-
: { text: green('Seat in the party'), detail: () =>
|
|
338
|
-
rows.push({ text: 'Put on their best gear', detail: () =>
|
|
339
|
-
rows.push({ text: 'Give them something from your bags…', detail: () =>
|
|
441
|
+
? { text: 'Move to the bench', detail: () => note('Bench', 'They stay yours with their gear, level and bags, and come back free from any inn or right here.'), act: () => a.result(g.dismissCompanion(id), `${comp.name} waits on the bench.`) }
|
|
442
|
+
: { text: green('Seat in the party'), detail: () => note('Party', `Up to ${MAX_COMPANIONS} companions travel with you.`), act: () => a.result(g.activateCompanion(id), `${comp.name} joins the party.`) });
|
|
443
|
+
rows.push({ text: 'Put on their best gear', detail: () => note('Best gear', 'Strict upgrades from their own bags first, then from yours. What you hand over becomes theirs.'), act: () => { const r = g.equipCompanionBest(id); a.result(r, r.equipped ? `${comp.name} puts on ${plural(r.equipped, 'piece')}.` : `${comp.name} already wears the best they can.`); } });
|
|
444
|
+
rows.push({ text: 'Give them something from your bags…', detail: () => note('Give', 'Anything you hand over is theirs. A Plans or Pattern they can use is learned on the spot.'), act: () => giveDialog(a, id) });
|
|
340
445
|
rows.push(...teachRow(a, comp));
|
|
341
|
-
rows.push({ text: 'Action bars…', detail: () =>
|
|
446
|
+
rows.push({ text: 'Action bars…', detail: () => note('Action bars', 'Their kit: what they can fire when you take them over, and the bag items their autopilot preps with.'), act: () => a.push(barsDialog(a, (x) => companionBarsOwner(x, findComp(x, id)))) });
|
|
342
447
|
rows.push(...settingsRows(a, comp));
|
|
343
448
|
rows.push(...petRows(a, comp));
|
|
344
449
|
rows.push(...questRows(a, comp));
|
|
@@ -349,7 +454,8 @@ export function companionDialog(app, id) {
|
|
|
349
454
|
for (const l of sortLines(profs?.lines ?? [])) {
|
|
350
455
|
const prof = ds.profession(l.line);
|
|
351
456
|
rows.push({
|
|
352
|
-
text: `${fit(prof?.name ?? l.line, 16)} ${fit(`${l.current}/${l.max}`, 8)} ${dim(l.rank ?? '')}${
|
|
457
|
+
text: `${fit(prof?.name ?? l.line, 16)} ${fit(`${l.current}/${l.max}`, 8)} ${dim(l.rank ?? '')}${comp.mission?.skill === l.line ? yellow(' away') : ''}`, data: { line: l.line },
|
|
458
|
+
icon: prof?.icon, sub: dim(`${isSecondary(l.line) ? 'secondary' : prof?.kind ?? ''} ${isMissionSkill(l.line) ? 'Enter: trips' : 'Enter: recipe book'}`),
|
|
353
459
|
detail: () => [bold(prof?.name ?? l.line), `${l.current}/${l.max}${l.rank ? ` ${l.rank}` : ''}`, '', dim(isMissionSkill(l.line) ? 'Enter: smelting and gathering trips' : 'Enter: their recipe book'), dim('x: drop it')],
|
|
354
460
|
act: () => a.push(recipeBook(a, id, l.line)),
|
|
355
461
|
});
|
|
@@ -374,6 +480,7 @@ export function companionDialog(app, id) {
|
|
|
374
480
|
const it = e != null ? ds.item(e) : null;
|
|
375
481
|
rows.push({
|
|
376
482
|
text: `${fit(SLOT_LABEL[slot] ?? slot, 10)} ${it ? qualityPaint(it.quality)(it.name) : dim('—')}`, data: { slot, it },
|
|
483
|
+
icon: it?.icon, sub: it ? dim(`${fit('', 10)} ${gearKind(it)}`) : undefined,
|
|
377
484
|
detail: () => (it ? itemLines(a, it, [...enchantLines(a, comp, slot), '', dim('Enter: change x: take off')]) : [dim(`Nothing in the ${SLOT_LABEL[slot] ?? slot} slot.`), '', dim('Enter: pick something for them to wear')]),
|
|
378
485
|
act: () => slotPicker(a, id, slot),
|
|
379
486
|
});
|
|
@@ -382,8 +489,71 @@ export function companionDialog(app, id) {
|
|
|
382
489
|
rows.push(...bagRows(a, comp, bagCapacity(companionCharacter(comp, lvl, ds), ds)));
|
|
383
490
|
rows.push(...bankRows(a, comp));
|
|
384
491
|
|
|
385
|
-
rows.push(H(''), { text: red(`Release ${comp.name} for good`), detail: () =>
|
|
386
|
-
act: () => a.confirm(`Release ${comp.name} for good? Their bags and gear go with them.`, () => { const r = g.releaseCompanions({ ids: [id] }); if (a.result(r, `${comp.name} leaves.${r.refundCopper ? ` You get ${
|
|
492
|
+
rows.push(H(''), { text: red(`Release ${comp.name} for good`), detail: () => note('Release', 'They leave your roster for good. You get the hire price back. Their bags and gear go with them.'),
|
|
493
|
+
act: () => a.confirm(`Release ${comp.name} for good? Their bags and gear go with them.`, () => { const r = g.releaseCompanions({ ids: [id] }); if (a.result(r, `${comp.name} leaves.${r.refundCopper ? ` You get ${coinText(r.refundCopper)} back.` : ''}`)) a.popTo('__none__'); }) });
|
|
494
|
+
return rows;
|
|
495
|
+
},
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// ---- their character sheet ------------------------------------------------------------------------------
|
|
500
|
+
|
|
501
|
+
/** Their numbers at the foot of the sheet's tooltip pane, laid out like yours under the Character tab. */
|
|
502
|
+
function sheetStats(app, char, w) {
|
|
503
|
+
const ds = app.ds, st = effectiveStats(char, ds), pt = powerType(char);
|
|
504
|
+
const pct = (x) => `${((x ?? 0) * 100).toFixed(2)}%`;
|
|
505
|
+
const attrs = [['Strength', st.str], ['Agility', st.agi], ['Stamina', st.sta], ['Intellect', st.int], ['Spirit', st.spi]];
|
|
506
|
+
const combat = [['Armor', effectiveArmor(char, ds)], ['Attack pwr', attackPower(char, ds)], ['Crit', pct(critChanceDisplay(char, ds))],
|
|
507
|
+
['Hit', pct(hitChance(char, ds))], ['Dodge', pct(dodgeChanceDisplay(char, ds))]];
|
|
508
|
+
return [
|
|
509
|
+
dim('─'.repeat(Math.max(0, w))),
|
|
510
|
+
`Health ${maxHealth(char, ds)} ${pt[0].toUpperCase() + pt.slice(1)} ${maxPower(char, ds)}`,
|
|
511
|
+
'',
|
|
512
|
+
...attrs.map(([name, v], i) => `${fit(name, 10)}${String(v).padStart(5)} ${fit(combat[i][0], 10)} ${String(combat[i][1]).padStart(7)}`),
|
|
513
|
+
];
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function sheetTitle(app, comp) {
|
|
517
|
+
const ilvl = displayGearScore(comp.equipped ?? {}, app.ds);
|
|
518
|
+
return `${bold(comp.name)} ${dim(`level ${companionLevel(comp, app.ch.level)} ${raceName(comp.race)} ${className(comp.cls)}`)} ${roleText(comp.role)}${ilvl ? dim(` ilvl ~${ilvl}`) : ''}`;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* A companion's character sheet: the Character tab's paperdoll (tuiPaperdoll.mjs) for them. Their pieces
|
|
523
|
+
* framed in quality colour down both sides, them in their gear in the middle, the tooltip beside and their
|
|
524
|
+
* stats at its foot. Enter picks from their bags and yours, x takes a piece off, u puts on every upgrade.
|
|
525
|
+
* Too small a window or no colour, and it lists the slots with their icons instead.
|
|
526
|
+
*/
|
|
527
|
+
export function companionSheet(app, id) {
|
|
528
|
+
const viewId = `sheet-${id}`;
|
|
529
|
+
const hint = 'Enter: change x: take off u: upgrades';
|
|
530
|
+
return {
|
|
531
|
+
id: viewId, title: 'Character sheet', hint: 'Enter: change',
|
|
532
|
+
detailFoot: (a, w) => { const c = findComp(a, id); return c ? sheetStats(a, companionCharacter(c, companionLevel(c, a.ch.level), a.ds), w) : []; },
|
|
533
|
+
keys: {
|
|
534
|
+
x: { hint: 'take off', fn: (a, row) => { const d = row?.data; if (d?.slot && d.it) a.result(a.g.unequipCompanionSlot(id, d.slot), `${findComp(a, id)?.name} takes off ${itemText(d.it)}.`); } },
|
|
535
|
+
u: { hint: 'put on upgrades', fn: (a) => { const c = findComp(a, id), r = a.g.equipCompanionBest(id); a.result(r, r.equipped ? `${c?.name} puts on ${plural(r.equipped, 'piece')}.` : `${c?.name} already wears the best they can.`); } },
|
|
536
|
+
},
|
|
537
|
+
grid: (a, rows, w, h) => { const c = findComp(a, id); return c ? paperdollGrid(a, rows, w, h, (cols, lines) => paperdollArt(c, cols, lines), sheetTitle(a, c), hint) : null; },
|
|
538
|
+
rows(a) {
|
|
539
|
+
const ds = a.ds, comp = findComp(a, id);
|
|
540
|
+
if (!comp) { setTimeout(() => { if (a.top()?.id === viewId) a.pop(); }, 0); return [H('They are gone.')]; }
|
|
541
|
+
const ups = a.g.companionUpgradesFor(id);
|
|
542
|
+
const rows = paperdollPlan(a, 0) ? [] : [{ text: sheetTitle(a, comp), header: true, plain: true }, H('')];
|
|
543
|
+
for (const slot of EQUIP_SLOTS) {
|
|
544
|
+
const e = equippedEntry(comp.equipped?.[slot]);
|
|
545
|
+
const it = e != null ? ds.item(e) : null;
|
|
546
|
+
const best = ups.filter((u) => u.slot === slot).sort((x, y) => y.delta - x.delta)[0];
|
|
547
|
+
const bestLines = best ? ['', green(`▲ Better: ${a.itemName(best.entry)} (+${Math.round(best.delta)})${best.from === 'player' ? ', in your bags' : ', in their bags'}`), dim('Enter: pick it u: put on every upgrade')] : [];
|
|
548
|
+
rows.push({
|
|
549
|
+
text: `${fit(SLOT_LABEL[slot] ?? slot, 10)} ${it ? qualityPaint(it.quality)(it.name) : dim('—')}${best ? green(' ▲') : ''}`, data: { slot, it },
|
|
550
|
+
icon: it?.icon, sub: it ? dim(`${fit('', 10)} ${gearKind(it)}`) : undefined,
|
|
551
|
+
cell: { slot, it, up: !!best, sub: dim(gearKind(it)) },
|
|
552
|
+
detail: () => (it ? itemLines(a, it, [...enchantLines(a, comp, slot), '', dim('Enter: change x: take off'), ...bestLines])
|
|
553
|
+
: [bold(SLOT_LABEL[slot] ?? slot), dim('Nothing worn here.'), '', dim('Enter: pick something for them to wear'), ...bestLines]),
|
|
554
|
+
act: () => slotPicker(a, id, slot),
|
|
555
|
+
});
|
|
556
|
+
}
|
|
387
557
|
return rows;
|
|
388
558
|
},
|
|
389
559
|
};
|
|
@@ -396,7 +566,7 @@ function giveDialog(app, id) {
|
|
|
396
566
|
if (!st.length) { app.flashMsg('Your bags are empty.'); return; }
|
|
397
567
|
app.pick(`Give to ${comp?.name}`, st.map((s, slot) => {
|
|
398
568
|
const it = app.ds.item(s.item);
|
|
399
|
-
return { label: `${qualityPaint(it?.quality)(it?.name ?? s.item)}${s.count > 1 ? ` x${s.count}` : ''}`, detail: () => itemLines(app, it), value: { s, slot, it } };
|
|
569
|
+
return { label: `${qualityPaint(it?.quality)(it?.name ?? s.item)}${s.count > 1 ? ` x${s.count}` : ''}`, icon: it?.icon, sub: dim(it?.typeName ?? ''), detail: () => itemLines(app, it), value: { s, slot, it } };
|
|
400
570
|
}), ({ slot, it }) => {
|
|
401
571
|
const r = app.g.giveItemToBotStack(id, slot);
|
|
402
572
|
app.result(r, `You give ${itemText(it, 'it')}${r.moved > 1 ? ` x${r.moved}` : ''} to ${comp?.name}.`);
|
|
@@ -414,7 +584,7 @@ function slotPicker(app, id, slot) {
|
|
|
414
584
|
const options = [];
|
|
415
585
|
const add = (s, from) => {
|
|
416
586
|
const it = ds.item(s.item), up = ups.get(s.item);
|
|
417
|
-
options.push({ label: `${qualityPaint(it.quality)(it.name)}${up ? green(` ▲ +${Math.round(up.delta)}`) : ''}${it.requiredLevel > lvl ? red(` L${it.requiredLevel}`) : ''}${dim(from === 'you' ? ' your bags' : ' their bags')}`, detail: () => itemLines(app, it), value: it });
|
|
587
|
+
options.push({ label: `${qualityPaint(it.quality)(it.name)}${up ? green(` ▲ +${Math.round(up.delta)}`) : ''}${it.requiredLevel > lvl ? red(` L${it.requiredLevel}`) : ''}${dim(from === 'you' ? ' your bags' : ' their bags')}`, icon: it.icon, sub: dim(gearKind(it)), detail: () => itemLines(app, it), value: it });
|
|
418
588
|
};
|
|
419
589
|
for (const s of comp.bags?.stacks ?? []) if (fits(s)) add(s, 'them');
|
|
420
590
|
for (const s of app.g.state.inventory.stacks) if (fits(s)) add(s, 'you');
|