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
package/src/cli/tuiCity.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// City services: the bank, the auction house and the mailbox. Same view shape as tuiViews.mjs; every
|
|
2
2
|
// change is a Game facade call. Where they open follows phonecraft's CITY_PANEL rule: the auction
|
|
3
3
|
// house in the six capitals, the bank there plus the goblin and Crossroads banker towns.
|
|
4
|
-
import { bold, dim, cyan, yellow, green, red, qualityPaint, fit, secondsText, plural, itemText } from './text.mjs';
|
|
5
|
-
import { itemLines } from './tuiViews.mjs';
|
|
4
|
+
import { bold, dim, cyan, yellow, green, red, inverse, vis, useColor, qualityPaint, fit, secondsText, plural, itemText, coinText } from './text.mjs';
|
|
6
5
|
import { moneyText } from '../domain/economy/money.ts';
|
|
6
|
+
import { itemLines } from './tuiViews.mjs';
|
|
7
7
|
import { bagCapacity } from '../domain/items/equipment.ts';
|
|
8
8
|
import { countItem } from '../domain/items/inventory.ts';
|
|
9
9
|
import { bankCapacity, nextBankBagSlotCost } from '../domain/items/bank.ts';
|
|
@@ -66,7 +66,7 @@ export function bankDialog(app, startMode = 'deposit') {
|
|
|
66
66
|
'+': { hint: 'buy bag slot', fn: (a) => {
|
|
67
67
|
const cost = nextBankBagSlotCost(a.g.state.bankBags);
|
|
68
68
|
if (cost == null) { a.flashMsg('You already own every bank bag slot.'); return; }
|
|
69
|
-
a.confirm(`Buy a bank bag slot for ${
|
|
69
|
+
a.confirm(`Buy a bank bag slot for ${coinText(cost)}?`, () => a.result(a.g.buyBankBagSlot(), `Bought a bank bag slot for ${coinText(cost)}.`));
|
|
70
70
|
} },
|
|
71
71
|
D: { hint: 'deposit all of it', fn: (a, row) => { if (mode !== 'deposit' || !row?.data?.s) return; const e = row.data.s.item; const r = a.g.bankDeposit(e, countItem(a.g.state.inventory, e)); a.result(r, r.ok ? `Deposited every ${a.itemLabel(e)} (${r.moved}).` : ''); } },
|
|
72
72
|
B: { hint: 'slot a bag', fn: (a, row) => {
|
|
@@ -84,8 +84,8 @@ export function bankDialog(app, startMode = 'deposit') {
|
|
|
84
84
|
const bankCap = bankCapacity(st.bankBags, ds), bagCap = bagCapacity(a.ch, ds);
|
|
85
85
|
const owned = st.bankBags.length, slotted = st.bankBags.filter((b) => b != null).length;
|
|
86
86
|
const rows = [
|
|
87
|
-
{ text: `${bold('Bank')} ${dim(`${st.bank.stacks.length}/${bankCap}`)} ${bold('Bags')} ${dim(`${st.inventory.stacks.length}/${bagCap}`)} ${
|
|
88
|
-
{ text: dim(`Bank bag slots: ${owned} owned, ${slotted} holding a bag${nextBankBagSlotCost(st.bankBags) != null ? `, next costs ${
|
|
87
|
+
{ text: `${bold('Bank')} ${dim(`${st.bank.stacks.length}/${bankCap}`)} ${bold('Bags')} ${dim(`${st.inventory.stacks.length}/${bagCap}`)} ${coinText(a.ch.gold)}`, header: true },
|
|
88
|
+
{ text: dim(`Bank bag slots: ${owned} owned, ${slotted} holding a bag${nextBankBagSlotCost(st.bankBags) != null ? `, next costs ${coinText(nextBankBagSlotCost(st.bankBags))}` : ''}`), header: true },
|
|
89
89
|
];
|
|
90
90
|
// Each bank bag slot: Enter takes the bag in it back to your bags (it has to be empty).
|
|
91
91
|
st.bankBags.forEach((b, slot) => {
|
|
@@ -103,7 +103,7 @@ export function bankDialog(app, startMode = 'deposit') {
|
|
|
103
103
|
const it = ds.item(s.item);
|
|
104
104
|
const tag = it?.slot ? dim(` ${it.slot}`) : it?.questItem ? dim(' quest') : '';
|
|
105
105
|
rows.push({
|
|
106
|
-
text: `${qualityPaint(it?.quality)(it?.name ?? `item ${s.item}`)}${s.count > 1 ? ` x${s.count}` : ''}${tag}`, data: { s, index },
|
|
106
|
+
text: `${qualityPaint(it?.quality)(it?.name ?? `item ${s.item}`)}${s.count > 1 ? ` x${s.count}` : ''}${tag}`, data: { s, index }, icon: it?.icon, sub: dim(it?.typeName ?? ''),
|
|
107
107
|
detail: () => itemLines(a, it, ['', dim(mode === 'deposit' ? 'Enter: deposit n: deposit some' : 'Enter: withdraw n: withdraw some')]),
|
|
108
108
|
act: () => move(a, index),
|
|
109
109
|
});
|
|
@@ -116,61 +116,159 @@ export function bankDialog(app, startMode = 'deposit') {
|
|
|
116
116
|
|
|
117
117
|
// ---- auction house ------------------------------------------------------------------------------
|
|
118
118
|
|
|
119
|
+
// Browse categories: ItemClass.dbc in AuctionFrame.lua order, as phonecraft's auctionModel.ts. Subclass names are
|
|
120
|
+
// ItemSubClass.dbc; the TBC-era consumable and trade goods subclasses the item table carries get their names too.
|
|
121
|
+
const AH_CLASSES = [
|
|
122
|
+
[2, 'Weapons'], [4, 'Armor'], [1, 'Container'], [0, 'Consumable'], [7, 'Trade Goods'],
|
|
123
|
+
[6, 'Projectile'], [11, 'Quiver'], [9, 'Recipe'], [5, 'Reagent'], [15, 'Miscellaneous'],
|
|
124
|
+
];
|
|
125
|
+
const AH_SUBCLASSES = {
|
|
126
|
+
0: ['Consumable', 'Potion', 'Elixir', 'Flask', 'Scroll', 'Food & Drink', 'Item Enhancement', 'Bandage', 'Other'],
|
|
127
|
+
1: ['Bag', 'Soul Bag', 'Herb Bag', 'Enchanting Bag', 'Engineering Bag'],
|
|
128
|
+
2: { 0: 'One-Handed Axes', 1: 'Two-Handed Axes', 2: 'Bows', 3: 'Guns', 4: 'One-Handed Maces', 5: 'Two-Handed Maces', 6: 'Polearms', 7: 'One-Handed Swords', 8: 'Two-Handed Swords', 10: 'Staves', 13: 'Fist Weapons', 14: 'Miscellaneous', 15: 'Daggers', 16: 'Thrown', 17: 'Spears', 18: 'Crossbows', 19: 'Wands', 20: 'Fishing Poles' },
|
|
129
|
+
4: { 0: 'Miscellaneous', 1: 'Cloth', 2: 'Leather', 3: 'Mail', 4: 'Plate', 6: 'Shields', 7: 'Librams', 8: 'Idols', 9: 'Totems' },
|
|
130
|
+
5: ['Reagent'],
|
|
131
|
+
6: { 2: 'Arrows', 3: 'Bullets' },
|
|
132
|
+
7: ['Trade Goods', 'Parts', 'Explosives', 'Devices', 'Jewelcrafting', 'Cloth', 'Leather', 'Metal & Stone', 'Meat', 'Herbs', 'Elemental', 'Other', 'Enchanting'],
|
|
133
|
+
9: ['Books', 'Leatherworking', 'Tailoring', 'Engineering', 'Blacksmithing', 'Cooking', 'Alchemy', 'First Aid', 'Enchanting', 'Fishing'],
|
|
134
|
+
11: { 2: 'Quivers', 3: 'Ammo Pouches' },
|
|
135
|
+
15: ['Junk'],
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/** [category, subcategory] of an item: a browse class (15 catches the rest) and a subclass name. */
|
|
139
|
+
function ahCategory(it) {
|
|
140
|
+
const cls = AH_CLASSES.some(([c]) => c === it?.itemClass) ? it.itemClass : 15;
|
|
141
|
+
const sub = it?.itemSubclass ?? it?.weaponSubclass ?? it?.armorSubclass ?? 0;
|
|
142
|
+
return [cls, AH_SUBCLASSES[cls]?.[sub] ?? 'Other'];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* One line of tabs, the picked one inverse; when it does not fit it scrolls to keep the pick in view. A strip
|
|
147
|
+
* under the cursor is drawn inverse whole, so there the pick is bracketed instead.
|
|
148
|
+
*/
|
|
149
|
+
export function tabStrip(labels, at, width, focused = false) {
|
|
150
|
+
const cells = labels.map((l, i) => (i === at ? (useColor && !focused ? inverse(` ${l} `) : focused ? bold(`[${l}]`) : `[${l}]`) : ` ${l} `));
|
|
151
|
+
let from = 0, to = cells.length;
|
|
152
|
+
const len = () => cells.slice(from, to).reduce((n, c) => n + vis(c), 0) + (from > 0 ? 2 : 0) + (to < cells.length ? 2 : 0);
|
|
153
|
+
while (len() > width && to - from > 1) { if (at - from > to - 1 - at) from++; else to--; }
|
|
154
|
+
return `${from > 0 ? dim('‹ ') : ''}${cells.slice(from, to).join('')}${to < cells.length ? dim(' ›') : ''}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
119
157
|
export function auctionDialog(app) {
|
|
120
158
|
app.g.refillAuctions(Date.now());
|
|
121
159
|
let mode = 'browse';
|
|
122
160
|
let filter = '';
|
|
161
|
+
let cat = 'all', sub = 'all';
|
|
123
162
|
const MODES = ['browse', 'sell', 'mine'];
|
|
124
|
-
|
|
163
|
+
// The category and subcategory strips are cursor rows: Up from the listings lands on them, Left Right turns them.
|
|
164
|
+
let lastRows = [];
|
|
165
|
+
const stripAt = () => lastRows[view.cursor]?.data?.strip;
|
|
166
|
+
const hint = () => {
|
|
167
|
+
if (mode === 'browse') {
|
|
168
|
+
const s = stripAt(), hasSub = lastRows.some((r) => r.data?.strip === 'sub');
|
|
169
|
+
if (s === 'cat') return `Left Right: category Down: ${hasSub ? 'subcategory' : 'listings'} Tab: sell`;
|
|
170
|
+
if (s === 'sub') return 'Left Right: subcategory Up: category Down: listings Tab: sell';
|
|
171
|
+
return `Enter: buyout Left Right: category Up to the tabs: ${hasSub ? 'subcategory' : 'category'} Tab: sell`;
|
|
172
|
+
}
|
|
173
|
+
return { sell: 'Enter: list for sale Tab: my auctions', mine: 'Enter: cancel listing Tab: browse' }[mode];
|
|
174
|
+
};
|
|
125
175
|
const unit = (x) => Math.round(x.buyoutCopper / x.quantity);
|
|
176
|
+
// Back to the top of the list; in browse the cursor goes to the first listing, past the strips.
|
|
177
|
+
let toList = true;
|
|
178
|
+
const top = () => { toList = true; view.scroll = 0; };
|
|
179
|
+
// The listings the search leaves, the categories they fall in (the picked one stays even when it empties)
|
|
180
|
+
// and the subcategories of the picked category, each with its count.
|
|
181
|
+
const browse = (a, now) => {
|
|
182
|
+
let all = a.g.browseAuctions(now).map((au) => { const it = a.ds.item(au.item); return { au, it, c: ahCategory(it) }; });
|
|
183
|
+
if (filter) all = all.filter((x) => (x.it?.name ?? '').toLowerCase().includes(filter));
|
|
184
|
+
const count = (pred) => all.filter(pred).length;
|
|
185
|
+
const cats = [{ key: 'all', label: 'All', n: all.length }];
|
|
186
|
+
for (const [c, label] of AH_CLASSES) { const n = count((x) => x.c[0] === c); if (n || c === cat) cats.push({ key: c, label, n }); }
|
|
187
|
+
const inCat = cat === 'all' ? all : all.filter((x) => x.c[0] === cat);
|
|
188
|
+
const subs = [{ key: 'all', label: 'All', n: inCat.length }];
|
|
189
|
+
for (const x of inCat) { const s = subs.find((y) => y.key === x.c[1]); if (s) s.n++; else subs.push({ key: x.c[1], label: x.c[1], n: 1 }); }
|
|
190
|
+
if (sub !== 'all' && !subs.some((s) => s.key === sub)) subs.push({ key: sub, label: sub, n: 0 });
|
|
191
|
+
const order = Object.values(AH_SUBCLASSES[cat] ?? {}), rank = (s) => (s.key === 'all' ? -1 : order.includes(s.key) ? order.indexOf(s.key) : order.length);
|
|
192
|
+
subs.sort((x, y) => rank(x) - rank(y));
|
|
193
|
+
const list = sub === 'all' ? inCat : inCat.filter((x) => x.c[1] === sub);
|
|
194
|
+
return { cats, subs, list };
|
|
195
|
+
};
|
|
196
|
+
// Turn a strip; on a strip the cursor stays put so it can keep turning, from a listing it goes back to the top.
|
|
197
|
+
const step = (a, which, d) => {
|
|
198
|
+
const b = browse(a, Date.now());
|
|
199
|
+
const opts = which === 'cat' ? b.cats : b.subs;
|
|
200
|
+
if (opts.length < 2) return;
|
|
201
|
+
const i = opts.findIndex((o) => o.key === (which === 'cat' ? cat : sub));
|
|
202
|
+
const next = opts[(i + d + opts.length) % opts.length].key;
|
|
203
|
+
if (which === 'cat') { cat = next; sub = 'all'; } else sub = next;
|
|
204
|
+
if (stripAt()) view.scroll = 0; else top();
|
|
205
|
+
};
|
|
126
206
|
const view = {
|
|
127
|
-
id: 'auction', title: 'Auction House',
|
|
207
|
+
id: 'auction', title: 'Auction House',
|
|
208
|
+
get hint() { return hint(); },
|
|
128
209
|
keys: {
|
|
129
|
-
s: { hint: 'search', fn: (a) => { if (mode !== 'browse') return; a.prompt('Search: ', (t) => { filter = t.trim().toLowerCase();
|
|
210
|
+
s: { hint: 'search', fn: (a) => { if (mode !== 'browse') return; a.prompt('Search: ', (t) => { filter = t.trim().toLowerCase(); top(); a.render(); }, filter); } },
|
|
211
|
+
'[': { fn: (a) => { if (mode === 'browse') step(a, 'sub', -1); } },
|
|
212
|
+
']': { fn: (a) => { if (mode === 'browse') step(a, 'sub', +1); } },
|
|
130
213
|
b: { hint: 'bid', fn: (a, row) => {
|
|
131
214
|
if (mode !== 'browse' || !row?.data?.auction) return;
|
|
132
215
|
const au = row.data.auction, min = minNextBid(au);
|
|
133
|
-
a.prompt(`Bid on ${a.itemLabel(au.item)} (at least ${
|
|
216
|
+
a.prompt(`Bid on ${a.itemLabel(au.item)} (at least ${coinText(min)}): `, (t) => {
|
|
134
217
|
const amount = parseMoney(t);
|
|
135
218
|
if (amount == null) { a.say(red('Say a price like 2g 50s or 250.')); a.render(); return; }
|
|
136
|
-
a.result(a.g.bidAuction(au.id, amount, Date.now()), `You bid ${
|
|
219
|
+
a.result(a.g.bidAuction(au.id, amount, Date.now()), `You bid ${coinText(amount)} on ${a.itemLabel(au.item)}.`);
|
|
137
220
|
a.afterAction();
|
|
138
221
|
}, moneyText(min));
|
|
139
222
|
} },
|
|
140
223
|
m: { hint: 'mailbox', fn: (a) => a.push(mailDialog(a)) },
|
|
141
224
|
},
|
|
142
225
|
onKey(a, k) {
|
|
143
|
-
if (k
|
|
144
|
-
|
|
226
|
+
if (mode === 'browse' && (k === 'left' || k === 'right')) { step(a, stripAt() ?? 'cat', k === 'right' ? +1 : -1); return true; }
|
|
227
|
+
if (k !== 'tab' && k !== 'shift-tab') return false;
|
|
228
|
+
mode = MODES[(MODES.indexOf(mode) + (k === 'tab' ? 1 : MODES.length - 1)) % MODES.length];
|
|
145
229
|
if (mode === 'browse') a.g.refillAuctions(Date.now());
|
|
146
|
-
|
|
230
|
+
top();
|
|
147
231
|
return true;
|
|
148
232
|
},
|
|
149
233
|
rows(a) {
|
|
150
234
|
const now = Date.now(), ds = a.ds;
|
|
151
235
|
const unread = a.g.unreadMailCount(now);
|
|
236
|
+
const L = a.layout(), width = (L.detailW ? L.listW : L.W) - 2;
|
|
237
|
+
const modeLabels = MODES.map((m) => ({ browse: 'Browse', sell: 'Sell', mine: 'My auctions' }[m]));
|
|
152
238
|
const rows = [
|
|
153
|
-
{ text: `${bold('Auction House')}
|
|
154
|
-
{ text: '', header: true },
|
|
239
|
+
{ text: `${bold('Auction House')} ${tabStrip(modeLabels, MODES.indexOf(mode), 40)} ${coinText(a.ch.gold)}${unread ? yellow(` ${unread} new mail`) : ''}`, header: true },
|
|
155
240
|
];
|
|
156
241
|
if (mode === 'browse') {
|
|
157
|
-
|
|
158
|
-
|
|
242
|
+
const b = browse(a, now);
|
|
243
|
+
const strip = (which, opts, key, label) => {
|
|
244
|
+
const focused = !toList && view.cursor === rows.length;
|
|
245
|
+
rows.push({
|
|
246
|
+
text: tabStrip(opts.map((o) => `${o.label} ${o.n}`), opts.findIndex((o) => o.key === key), width - 1, focused), data: { strip: which },
|
|
247
|
+
detail: () => [bold(label), '', dim('Left Right: pick one'), dim(`Down${which === 'cat' && rows.some((r) => r.data?.strip === 'sub') ? ': subcategories' : ' or Enter: the listings'}`)],
|
|
248
|
+
act: (a2) => a2.moveCursor(view, a2.rowsOf(view), +1),
|
|
249
|
+
});
|
|
250
|
+
};
|
|
251
|
+
strip('cat', b.cats, cat, 'Category');
|
|
252
|
+
if (cat !== 'all' && b.subs.length > 2) strip('sub', b.subs, sub, 'Subcategory');
|
|
253
|
+
rows.push({ text: '', header: true });
|
|
254
|
+
const list = b.list.map((x) => x.au);
|
|
159
255
|
list.sort((x, y) => (ds.item(x.item)?.name ?? '').localeCompare(ds.item(y.item)?.name ?? '') || unit(x) - unit(y));
|
|
160
|
-
|
|
161
|
-
|
|
256
|
+
const where = cat === 'all' ? '' : `${b.cats.find((c) => c.key === cat)?.label}${sub === 'all' ? '' : ` › ${sub}`}: `;
|
|
257
|
+
rows.push({ text: `${where}${filter ? `"${filter}": ` : ''}${plural(list.length, 'listing')}${filter ? dim(' s: change search') : dim(' s: search by name')}`, header: true });
|
|
258
|
+
if (!list.length) rows.push({ text: dim(filter || cat !== 'all' ? 'Nothing listed here.' : 'Nothing on the boards right now.'), header: true });
|
|
162
259
|
for (const au of list) {
|
|
163
260
|
const it = ds.item(au.item);
|
|
164
261
|
const can = au.buyoutCopper <= a.ch.gold;
|
|
165
262
|
const name = `${qualityPaint(it?.quality)(it?.name ?? au.item)}${au.quantity > 1 ? ` x${au.quantity}` : ''}`;
|
|
166
263
|
rows.push({
|
|
167
|
-
text: `${can ? name : dim(name)} ${
|
|
168
|
-
data: { auction: au },
|
|
169
|
-
detail: () => itemLines(a, it, ['', `Buyout: ${
|
|
170
|
-
act: () => a.confirm(`Buy ${itemText(it, String(au.item))}${au.quantity > 1 ? ` x${au.quantity}` : ''} for ${
|
|
264
|
+
text: `${can ? name : dim(name)} ${coinText(au.buyoutCopper)}${au.quantity > 1 ? dim(` (${coinText(unit(au))} each)`) : ''} ${dim('bid')} ${coinText(au.bidCopper)}${au.highBidder === a.ch.name ? green(' yours') : ''} ${timeLeft(au, now)}${it?.requiredLevel > a.ch.level ? red(` L${it.requiredLevel}`) : ''}`,
|
|
265
|
+
data: { auction: au }, icon: it?.icon, sub: dim(it?.typeName ?? ''),
|
|
266
|
+
detail: () => itemLines(a, it, ['', `Buyout: ${coinText(au.buyoutCopper)}`, `Current bid: ${coinText(au.bidCopper)}${au.highBidder ? dim(` (${au.highBidder === a.ch.name ? 'you' : au.highBidder})`) : dim(' (no bids)')}`, `Next bid: ${coinText(minNextBid(au))}`, dim(`Seller: ${au.seller}`), '', dim('Enter: buy out b: bid')]),
|
|
267
|
+
act: () => a.confirm(`Buy ${itemText(it, String(au.item))}${au.quantity > 1 ? ` x${au.quantity}` : ''} for ${coinText(au.buyoutCopper)}?`, () => a.result(a.g.buyoutAuction(au.id), `Bought ${itemText(it)}${au.quantity > 1 ? ` x${au.quantity}` : ''} for ${coinText(au.buyoutCopper)}.`)),
|
|
171
268
|
});
|
|
172
269
|
}
|
|
173
270
|
} else if (mode === 'sell') {
|
|
271
|
+
rows.push({ text: '', header: true });
|
|
174
272
|
const st = a.g.state.inventory.stacks;
|
|
175
273
|
rows.push({ text: dim('Pick something from your bags to put up for auction.'), header: true });
|
|
176
274
|
if (!st.length) rows.push({ text: dim('Your bags are empty.'), header: true });
|
|
@@ -179,12 +277,13 @@ export function auctionDialog(app) {
|
|
|
179
277
|
const soulbound = !!s.bound, quest = !!it?.questItem;
|
|
180
278
|
const name = `${qualityPaint(it?.quality)(it?.name ?? s.item)}${s.count > 1 ? ` x${s.count}` : ''}`;
|
|
181
279
|
rows.push({
|
|
182
|
-
text: `${soulbound || quest ? dim(name) : name}${soulbound ? red(' soulbound') : quest ? dim(' quest') : ''}${it?.sellPrice ? dim(` vendor ${
|
|
280
|
+
text: `${soulbound || quest ? dim(name) : name}${soulbound ? red(' soulbound') : quest ? dim(' quest') : ''}${it?.sellPrice ? dim(` vendor ${coinText(it.sellPrice * s.count)}`) : ''}`, data: { s }, icon: it?.icon, sub: dim(it?.typeName ?? ''),
|
|
183
281
|
detail: () => itemLines(a, it, ['', dim('Enter: set prices and list it')]),
|
|
184
282
|
act: () => { if (soulbound) a.flashMsg('Soulbound items cannot be auctioned.'); else if (quest) a.flashMsg('Quest items cannot be auctioned.'); else sellFlow(a, s, it); },
|
|
185
283
|
});
|
|
186
284
|
}
|
|
187
285
|
} else {
|
|
286
|
+
rows.push({ text: '', header: true });
|
|
188
287
|
const own = a.g.ownAuctions(), bids = a.g.myBidAuctions();
|
|
189
288
|
const mail = a.g.listMail(now);
|
|
190
289
|
rows.push({ text: `Mailbox: ${mail.length ? `${plural(mail.length, 'letter')} waiting` : 'empty'}${dim(' (sales and refunds arrive here)')}`, act: () => a.push(mailDialog(a)), data: {} });
|
|
@@ -193,9 +292,9 @@ export function auctionDialog(app) {
|
|
|
193
292
|
for (const au of own) {
|
|
194
293
|
const it = ds.item(au.item);
|
|
195
294
|
rows.push({
|
|
196
|
-
text: `${qualityPaint(it?.quality)(it?.name ?? au.item)}${au.quantity > 1 ? ` x${au.quantity}` : ''} ${dim('bid')} ${
|
|
197
|
-
data: { auction: au },
|
|
198
|
-
detail: () => itemLines(a, it, ['', `Buyout: ${
|
|
295
|
+
text: `${qualityPaint(it?.quality)(it?.name ?? au.item)}${au.quantity > 1 ? ` x${au.quantity}` : ''} ${dim('bid')} ${coinText(au.bidCopper)}${au.highBidder ? green(' (bid placed)') : ''} ${dim('buyout')} ${coinText(au.buyoutCopper)} ${timeLeft(au, now)}`,
|
|
296
|
+
data: { auction: au }, icon: it?.icon, sub: dim(it?.typeName ?? ''),
|
|
297
|
+
detail: () => itemLines(a, it, ['', `Buyout: ${coinText(au.buyoutCopper)}`, `Bid: ${coinText(au.bidCopper)}${au.highBidder ? ` by ${au.highBidder}` : ''}`, '', dim('Enter: cancel the listing (the deposit is lost)')]),
|
|
199
298
|
act: () => a.confirm(`Cancel the ${itemText(it)} listing?`, () => a.result(a.g.cancelAuction(au.id), `Listing cancelled. ${itemText(it)} is back in your bags.`)),
|
|
200
299
|
});
|
|
201
300
|
}
|
|
@@ -203,9 +302,15 @@ export function auctionDialog(app) {
|
|
|
203
302
|
if (!bids.length) rows.push({ text: dim(' You hold no high bids.'), header: true });
|
|
204
303
|
for (const au of bids) {
|
|
205
304
|
const it = ds.item(au.item);
|
|
206
|
-
rows.push({ text: ` ${qualityPaint(it?.quality)(it?.name ?? au.item)}${au.quantity > 1 ? ` x${au.quantity}` : ''} ${dim('your bid')} ${
|
|
305
|
+
rows.push({ text: ` ${qualityPaint(it?.quality)(it?.name ?? au.item)}${au.quantity > 1 ? ` x${au.quantity}` : ''} ${dim('your bid')} ${coinText(au.bidCopper)} ${timeLeft(au, now)}`, header: true });
|
|
207
306
|
}
|
|
208
307
|
}
|
|
308
|
+
if (toList) {
|
|
309
|
+
toList = false;
|
|
310
|
+
const first = mode === 'browse' ? rows.findIndex((r) => r.data?.auction) : -1;
|
|
311
|
+
view.cursor = first >= 0 ? first : 0;
|
|
312
|
+
}
|
|
313
|
+
lastRows = rows;
|
|
209
314
|
return rows;
|
|
210
315
|
},
|
|
211
316
|
};
|
|
@@ -217,11 +322,11 @@ function sellFlow(app, s, it) {
|
|
|
217
322
|
const finish = (qty, bid, buyout) => {
|
|
218
323
|
const opts = Object.entries(AH_DURATIONS_MS).map(([k, ms]) => {
|
|
219
324
|
const dep = auctionDeposit(it?.sellPrice ?? 0, qty, ms / 1000);
|
|
220
|
-
return { label: `${{ short: '2 hours', medium: '8 hours', long: '24 hours' }[k]} ${dim(`deposit ${
|
|
325
|
+
return { label: `${{ short: '2 hours', medium: '8 hours', long: '24 hours' }[k]} ${dim(`deposit ${coinText(dep)}`)}`, value: k };
|
|
221
326
|
});
|
|
222
327
|
app.pick('How long should it run?', opts, (dur) => {
|
|
223
328
|
const r = app.g.listAuction(s.item, qty, bid, buyout, dur, Date.now());
|
|
224
|
-
app.result(r, r.ok ? `Listed ${itemText(it)}${qty > 1 ? ` x${qty}` : ''}: bid ${
|
|
329
|
+
app.result(r, r.ok ? `Listed ${itemText(it)}${qty > 1 ? ` x${qty}` : ''}: bid ${coinText(bid)}, buyout ${coinText(buyout)}.` : '');
|
|
225
330
|
app.afterAction();
|
|
226
331
|
});
|
|
227
332
|
};
|
|
@@ -230,7 +335,7 @@ function sellFlow(app, s, it) {
|
|
|
230
335
|
app.prompt(`Starting bid for ${itemText(it)}${qty > 1 ? ` x${qty}` : ''} (e.g. 2g 50s): `, (t1) => {
|
|
231
336
|
const bid = parseMoney(t1);
|
|
232
337
|
if (bid == null || bid < 1) { app.say(red('Say a price like 2g 50s or 250.')); app.render(); return; }
|
|
233
|
-
app.prompt(`Buyout (at least ${
|
|
338
|
+
app.prompt(`Buyout (at least ${coinText(bid)}): `, (t2) => {
|
|
234
339
|
const buyout = parseMoney(t2);
|
|
235
340
|
if (buyout == null || buyout < bid) { app.say(red('The buyout must be at least the starting bid.')); app.render(); return; }
|
|
236
341
|
finish(qty, bid, buyout);
|
|
@@ -270,16 +375,16 @@ export function mailDialog(app) {
|
|
|
270
375
|
rows(a) {
|
|
271
376
|
const now = Date.now();
|
|
272
377
|
const mail = a.g.listMail(now);
|
|
273
|
-
const rows = [{ text: `${bold('Mailbox')} ${dim(plural(mail.length, 'letter'))} ${
|
|
378
|
+
const rows = [{ text: `${bold('Mailbox')} ${dim(plural(mail.length, 'letter'))} ${coinText(a.ch.gold)}${mail.length ? dim(' a: take everything') : ''}`, header: true }, { text: '', header: true }];
|
|
274
379
|
if (!mail.length) rows.push({ text: dim('No mail.'), header: true });
|
|
275
380
|
for (const m of mail) {
|
|
276
381
|
const atts = m.attachments.map((x) => `${a.itemLabel(x.entry)}${x.quantity > 1 ? ` x${x.quantity}` : ''}`);
|
|
277
|
-
const bits = [...(m.money ? [
|
|
382
|
+
const bits = [...(m.money ? [coinText(m.money)] : []), ...atts];
|
|
278
383
|
const read = (m.checked ?? 0) & 1;
|
|
279
384
|
rows.push({
|
|
280
|
-
text: `${read ? dim(m.subject) : bold(m.subject)} ${dim(`from ${m.sender}`)}${bits.length ? ` ${bits.join(', ')}` : ''}${m.cod ? red(` COD ${
|
|
281
|
-
data: { mail: m },
|
|
282
|
-
detail: () => [bold(m.subject), dim(`From ${m.sender}`), '', ...String(m.body ?? '').split('\n').filter(Boolean), ...(bits.length ? ['', cyan('Enclosed'), ...bits.map((b) => ` ${b}`)] : []), ...(m.cod ? ['', red(`Cash on delivery: ${
|
|
385
|
+
text: `${read ? dim(m.subject) : bold(m.subject)} ${dim(`from ${m.sender}`)}${bits.length ? ` ${bits.join(', ')}` : ''}${m.cod ? red(` COD ${coinText(m.cod)}`) : ''}`,
|
|
386
|
+
data: { mail: m }, icon: (m.attachments.length ? a.ds.item(m.attachments[0].entry)?.icon : null) ?? (m.money ? 'inv_misc_coin_03' : 'inv_letter_01'),
|
|
387
|
+
detail: () => [bold(m.subject), dim(`From ${m.sender}`), '', ...String(m.body ?? '').split('\n').filter(Boolean), ...(bits.length ? ['', cyan('Enclosed'), ...bits.map((b) => ` ${b}`)] : []), ...(m.cod ? ['', red(`Cash on delivery: ${coinText(m.cod)}`)] : []), '', dim(`Expires in ${secondsText(Math.max(0, m.expiresAt - now))}`)],
|
|
283
388
|
act: () => takeMail(a, m),
|
|
284
389
|
});
|
|
285
390
|
}
|
|
@@ -291,7 +396,7 @@ export function mailDialog(app) {
|
|
|
291
396
|
function takeMail(app, m) {
|
|
292
397
|
app.g.markMailRead(m.id);
|
|
293
398
|
let took = 0;
|
|
294
|
-
if (m.money) { const r = app.g.takeMailMoney(m.id); if (r.ok && r.taken) { took++; app.say(green(`You take ${
|
|
399
|
+
if (m.money) { const r = app.g.takeMailMoney(m.id); if (r.ok && r.taken) { took++; app.say(green(`You take ${coinText(r.taken)}.`)); } else if (!r.ok) app.say(red(r.reason)); }
|
|
295
400
|
for (let i = 0; i < 16; i++) {
|
|
296
401
|
const cur = app.g.listMail(Date.now()).find((x) => x.id === m.id);
|
|
297
402
|
if (!cur?.attachments.length) break;
|
|
@@ -360,7 +465,7 @@ export function composeDialog(app) {
|
|
|
360
465
|
dest.state.mailbox = r.destMailbox;
|
|
361
466
|
dest.updatedAt = Date.now();
|
|
362
467
|
await a.repo.save(dest);
|
|
363
|
-
a.say(green(`Mail sent to ${dest.state.char.name}.`) + dim(` Postage ${
|
|
468
|
+
a.say(green(`Mail sent to ${dest.state.char.name}.`) + dim(` Postage ${coinText(r.cost)}.`));
|
|
364
469
|
await a.persist();
|
|
365
470
|
if (a.top()?.id === 'mail-compose') a.pop(); else a.render();
|
|
366
471
|
};
|
|
@@ -371,7 +476,7 @@ export function composeDialog(app) {
|
|
|
371
476
|
const rows = [{ text: bold('Send mail'), header: true }, { text: '', header: true }];
|
|
372
477
|
rows.push({ text: `To ${m.to ? cyan(m.to.name) : dim('pick one of your characters')}`, act: (a2) => { view.picking = pickAlt(a2, (s) => { m.to = s; }); } });
|
|
373
478
|
rows.push({ text: `Subject ${m.subject || dim('(the first item, or "A letter")')}`, act: (a2) => a2.prompt('Subject: ', (t) => { m.subject = t.slice(0, 64); a2.render(); }, m.subject) });
|
|
374
|
-
rows.push({ text: `${m.cod ? 'COD ' : 'Money '} ${m.money ?
|
|
479
|
+
rows.push({ text: `${m.cod ? 'COD ' : 'Money '} ${m.money ? coinText(m.money) : dim('none')}`, act: (a2) => a2.prompt(m.cod ? 'Cash on delivery: ' : 'Money to send: ', (t) => { const c = parseMoney(t); if (c == null) a2.flashMsg('Say a price like 2g 50s or 250.'); else m.money = c; a2.render(); }) });
|
|
375
480
|
rows.push({ text: `Pay ${m.cod ? red('cash on delivery: they pay the money to take the items') : dim('the money is a gift')}`, act: () => { m.cod = !m.cod; } });
|
|
376
481
|
rows.push({ text: '', header: true }, { text: `Enclose ${dim(`${m.attach.size}/${MAX_MAIL_ITEMS}`)}`, header: true });
|
|
377
482
|
a.g.state.inventory.stacks.forEach((s, slot) => {
|
|
@@ -382,7 +487,7 @@ export function composeDialog(app) {
|
|
|
382
487
|
act: () => { if (on) m.attach.delete(slot); else if (m.attach.size < MAX_MAIL_ITEMS) m.attach.add(slot); else a.flashMsg(`At most ${MAX_MAIL_ITEMS} stacks in one letter.`); },
|
|
383
488
|
});
|
|
384
489
|
});
|
|
385
|
-
rows.push({ text: '', header: true }, { text: green(`Send ${dim(`S, postage ${
|
|
490
|
+
rows.push({ text: '', header: true }, { text: green(`Send ${dim(`S, postage ${coinText(MAIL_POSTAGE_COPPER)}`)}`), act: () => { view.sending = send(a); } });
|
|
386
491
|
return rows;
|
|
387
492
|
},
|
|
388
493
|
};
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
// The rest of a companion's page and the party-wide companion dialogs: spec, talent link, AoE, off-heal and xp
|
|
2
2
|
// lock; their pet; their quests; their bags and bank; the tavern duel; consumable sharing, the party vault
|
|
3
3
|
// and the hire search. Ports of phonecraft companionsPanel.ts, companionKitPanel.ts and consumableSharePanel.ts.
|
|
4
|
-
import { bold, dim, cyan, yellow, green, red, qualityPaint, fit, plural, itemText } from './text.mjs';
|
|
4
|
+
import { bold, dim, cyan, yellow, green, red, qualityPaint, fit, plural, itemText, coinText } from './text.mjs';
|
|
5
5
|
import { itemLines, noteSale } from './tuiViews.mjs';
|
|
6
6
|
import { companionUseWithPicker } from './tuiEnchantTarget.mjs';
|
|
7
7
|
import { startFight } from './tuiFight.mjs';
|
|
8
|
-
import { moneyText } from '../domain/economy/money.ts';
|
|
9
8
|
import { className } from '../domain/core/ids.ts';
|
|
10
9
|
import { companionLevel, companionCharacter } from '../domain/bots/squad.ts';
|
|
11
10
|
import { listTameablePets, unlockedWarlockDemons, WARLOCK_DEMONS, WARLOCK_DEMON_UNLOCK } from '../domain/bots/botPet.ts';
|
|
@@ -123,7 +122,7 @@ export function questRows(app, comp) {
|
|
|
123
122
|
act: () => {
|
|
124
123
|
if (o.kind === 'accept') { app.result(g.acceptCompanionQuest(id, o.quest), `${comp.name} takes ${o.title}.`); return; }
|
|
125
124
|
const q = ds.quest(o.quest);
|
|
126
|
-
const finish = (choice) => { const r = g.turnInCompanionQuest(id, o.quest, choice); app.result(r, r.ok ? `${comp.name} hands in ${o.title}${r.xp ? `: +${r.xp} xp` : ''}${r.money ? `, ${
|
|
125
|
+
const finish = (choice) => { const r = g.turnInCompanionQuest(id, o.quest, choice); app.result(r, r.ok ? `${comp.name} hands in ${o.title}${r.xp ? `: +${r.xp} xp` : ''}${r.money ? `, ${coinText(r.money)} to you` : ''}.` : ''); };
|
|
127
126
|
if (q?.rewChoiceItems?.length > 1) app.pick(`${comp.name}: choose a reward`, q.rewChoiceItems.map((it, i) => ({ label: app.itemLabel(it.item), detail: () => itemLines(app, ds.item(it.item)), value: i })), finish);
|
|
128
127
|
else finish(0);
|
|
129
128
|
},
|
|
@@ -165,12 +164,13 @@ export function bagRows(app, comp, cap) {
|
|
|
165
164
|
const name = `${itemText(it, 'it')}${s.count > 1 ? ` x${s.count}` : ''}`;
|
|
166
165
|
rows.push({
|
|
167
166
|
text: `${qualityPaint(it?.quality)(it?.name ?? s.item)}${s.count > 1 ? ` x${s.count}` : ''}`,
|
|
167
|
+
icon: it?.icon, sub: dim(`${it?.typeName ?? ''}${it?.sellPrice ? ` sells ${coinText(it.sellPrice * s.count)}` : ''}`),
|
|
168
168
|
detail: () => itemLines(app, it, ['', dim('Enter: take it u: use on them s: sell e: disenchant m: to another companion b: their bank d: destroy')]),
|
|
169
169
|
act: () => { const r = g.takeItemFromBotStack(id, slot); app.result(r, `You take ${it?.name ?? 'it'}${r.moved > 1 ? ` x${r.moved}` : ''} from ${comp.name}.`); },
|
|
170
170
|
keys: {
|
|
171
171
|
u: (a) => companionUseWithPicker(a, comp, s.item),
|
|
172
172
|
d: (a) => a.confirm(`Destroy ${name} from ${comp.name}'s bags?`, () => a.result(g.discardCompanionBagStack(id, slot), `Destroyed ${name}.`)),
|
|
173
|
-
s: (a) => { if (!a.vendor) { a.flashMsg('Open a vendor first.'); return; } const r = g.sellCompanionItemStack(id, slot); if (a.result(r, r.ok ? `Sold ${name} for ${
|
|
173
|
+
s: (a) => { if (!a.vendor) { a.flashMsg('Open a vendor first.'); return; } const r = g.sellCompanionItemStack(id, slot); if (a.result(r, r.ok ? `Sold ${name} for ${coinText(r.earned)}.` : '')) noteSale(a, s.item, r.qty, r.earned, comp); },
|
|
174
174
|
e: (a) => { const r = g.disenchantCompanionBagStack(id, slot); a.result(r, r.ok ? `It breaks into ${r.yielded.map((y) => `${a.itemLabel(y.item)}${y.count > 1 ? ` x${y.count}` : ''}`).join(', ')}.` : ''); },
|
|
175
175
|
b: (a) => a.result(g.companionBankDepositStack(id, slot), `${name} goes into ${comp.name}'s bank.`),
|
|
176
176
|
m: (a) => {
|
|
@@ -191,10 +191,10 @@ export function bankRows(app, comp) {
|
|
|
191
191
|
const rows = [H(''), H(`Their bank ${dim(`${bank.length} stacks Enter: back to their bags`)}`)];
|
|
192
192
|
bank.forEach((s, slot) => {
|
|
193
193
|
const it = ds.item(s.item);
|
|
194
|
-
rows.push({ text: `${qualityPaint(it?.quality)(it?.name ?? s.item)}${s.count > 1 ? ` x${s.count}` : ''}`, detail: () => itemLines(app, it), act: () => app.result(g.companionBankWithdrawStack(id, slot), `${it?.name} comes back out.`) });
|
|
194
|
+
rows.push({ text: `${qualityPaint(it?.quality)(it?.name ?? s.item)}${s.count > 1 ? ` x${s.count}` : ''}`, icon: it?.icon, sub: dim(it?.typeName ?? ''), detail: () => itemLines(app, it), act: () => app.result(g.companionBankWithdrawStack(id, slot), `${it?.name} comes back out.`) });
|
|
195
195
|
});
|
|
196
196
|
const bagSlots = comp.bankBags ?? [];
|
|
197
|
-
rows.push({ text: dim('Buy them another bank bag slot'), detail: () => [bold('Bank bag slot'), '', 'Paid from your purse; the price climbs with each slot like your own.'], act: () => { const r = g.buyCompanionBankSlot(id); app.result(r, r.ok ? `A new bank bag slot for ${comp.name}: ${
|
|
197
|
+
rows.push({ text: dim('Buy them another bank bag slot'), detail: () => [bold('Bank bag slot'), '', 'Paid from your purse; the price climbs with each slot like your own.'], act: () => { const r = g.buyCompanionBankSlot(id); app.result(r, r.ok ? `A new bank bag slot for ${comp.name}: ${coinText(r.cost)}.` : ''); } });
|
|
198
198
|
const bagsInBags = g.state.inventory.stacks.map((s) => ds.item(s.item)).filter((it) => it?.slot === 'bag');
|
|
199
199
|
if (bagsInBags.length) {
|
|
200
200
|
const free = bagSlots.map((b, i) => (b == null ? i : -1)).filter((i) => i >= 0);
|