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.
- package/CHANGELOG.md +53 -0
- package/README.md +43 -10
- package/bin/azsh.mjs +15 -1
- package/package.json +7 -3
- package/src/cli/hdArt.mjs +1 -1
- package/src/cli/iconArt.mjs +11 -1
- package/src/cli/modelArt.mjs +7 -4
- package/src/cli/oocPrep.mjs +32 -0
- package/src/cli/repl.mjs +16 -0
- package/src/cli/sixel.mjs +26 -14
- package/src/cli/text.mjs +16 -0
- package/src/cli/tui.mjs +40 -13
- package/src/cli/tuiAutopilot.mjs +17 -8
- package/src/cli/tuiBags.mjs +37 -4
- package/src/cli/tuiBulk.mjs +83 -4
- package/src/cli/tuiCheck.mjs +328 -19
- package/src/cli/tuiCity.mjs +31 -31
- package/src/cli/tuiCompanionKit.mjs +26 -8
- package/src/cli/tuiCompanions.mjs +189 -29
- package/src/cli/tuiCreate.mjs +7 -0
- package/src/cli/tuiDungeon.mjs +113 -33
- package/src/cli/tuiFight.mjs +63 -21
- package/src/cli/tuiFightPanes.mjs +15 -6
- package/src/cli/tuiFloor.mjs +20 -8
- package/src/cli/tuiGear.mjs +9 -10
- package/src/cli/tuiLoot.mjs +5 -6
- package/src/cli/tuiMap.mjs +214 -0
- package/src/cli/tuiPaperdoll.mjs +6 -5
- package/src/cli/tuiPartyStage.mjs +36 -7
- package/src/cli/tuiPet.mjs +16 -8
- package/src/cli/tuiProfessions.mjs +23 -23
- package/src/cli/tuiSaves.mjs +14 -0
- package/src/cli/tuiSearch.mjs +4 -5
- package/src/cli/tuiSettings.mjs +3 -4
- package/src/cli/tuiStage.mjs +72 -16
- package/src/cli/tuiTalents.mjs +3 -4
- package/src/cli/tuiViews.mjs +374 -85
- package/src/cli/tuiWiki.mjs +2 -3
- package/src/cli/tuiWorld.mjs +88 -5
- package/src/data/gear-art.bin.br +0 -0
- package/src/data/hd-art.json +36 -36
- package/src/data/model-art-worn.bin.br +0 -0
- package/src/data/model-art.bin.gz +0 -0
- 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/character/character.ts +6 -0
- package/src/domain/pet/pet.ts +31 -0
- package/src/engine/combat/pickAction.ts +3494 -3487
- package/src/engine/combat/sessionChase.ts +12 -1
- package/src/engine/game/loadReconcile.ts +21 -1
- package/src/engine/game/pets.ts +33 -1
- package/src/engine/game/professions.ts +4 -0
- package/src/engine/game/squadQuestShare.ts +13 -1
- package/src/engine/game/types.ts +4 -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, inverse, vis, useColor, 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
|
});
|
|
@@ -146,7 +146,7 @@ function ahCategory(it) {
|
|
|
146
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
147
|
* under the cursor is drawn inverse whole, so there the pick is bracketed instead.
|
|
148
148
|
*/
|
|
149
|
-
function tabStrip(labels, at, width, focused = false) {
|
|
149
|
+
export function tabStrip(labels, at, width, focused = false) {
|
|
150
150
|
const cells = labels.map((l, i) => (i === at ? (useColor && !focused ? inverse(` ${l} `) : focused ? bold(`[${l}]`) : `[${l}]`) : ` ${l} `));
|
|
151
151
|
let from = 0, to = cells.length;
|
|
152
152
|
const len = () => cells.slice(from, to).reduce((n, c) => n + vis(c), 0) + (from > 0 ? 2 : 0) + (to < cells.length ? 2 : 0);
|
|
@@ -213,10 +213,10 @@ export function auctionDialog(app) {
|
|
|
213
213
|
b: { hint: 'bid', fn: (a, row) => {
|
|
214
214
|
if (mode !== 'browse' || !row?.data?.auction) return;
|
|
215
215
|
const au = row.data.auction, min = minNextBid(au);
|
|
216
|
-
a.prompt(`Bid on ${a.itemLabel(au.item)} (at least ${
|
|
216
|
+
a.prompt(`Bid on ${a.itemLabel(au.item)} (at least ${coinText(min)}): `, (t) => {
|
|
217
217
|
const amount = parseMoney(t);
|
|
218
218
|
if (amount == null) { a.say(red('Say a price like 2g 50s or 250.')); a.render(); return; }
|
|
219
|
-
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)}.`);
|
|
220
220
|
a.afterAction();
|
|
221
221
|
}, moneyText(min));
|
|
222
222
|
} },
|
|
@@ -236,7 +236,7 @@ export function auctionDialog(app) {
|
|
|
236
236
|
const L = a.layout(), width = (L.detailW ? L.listW : L.W) - 2;
|
|
237
237
|
const modeLabels = MODES.map((m) => ({ browse: 'Browse', sell: 'Sell', mine: 'My auctions' }[m]));
|
|
238
238
|
const rows = [
|
|
239
|
-
{ text: `${bold('Auction House')} ${tabStrip(modeLabels, MODES.indexOf(mode), 40)} ${
|
|
239
|
+
{ text: `${bold('Auction House')} ${tabStrip(modeLabels, MODES.indexOf(mode), 40)} ${coinText(a.ch.gold)}${unread ? yellow(` ${unread} new mail`) : ''}`, header: true },
|
|
240
240
|
];
|
|
241
241
|
if (mode === 'browse') {
|
|
242
242
|
const b = browse(a, now);
|
|
@@ -261,10 +261,10 @@ export function auctionDialog(app) {
|
|
|
261
261
|
const can = au.buyoutCopper <= a.ch.gold;
|
|
262
262
|
const name = `${qualityPaint(it?.quality)(it?.name ?? au.item)}${au.quantity > 1 ? ` x${au.quantity}` : ''}`;
|
|
263
263
|
rows.push({
|
|
264
|
-
text: `${can ? name : dim(name)} ${
|
|
265
|
-
data: { auction: au },
|
|
266
|
-
detail: () => itemLines(a, it, ['', `Buyout: ${
|
|
267
|
-
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)}.`)),
|
|
268
268
|
});
|
|
269
269
|
}
|
|
270
270
|
} else if (mode === 'sell') {
|
|
@@ -277,7 +277,7 @@ export function auctionDialog(app) {
|
|
|
277
277
|
const soulbound = !!s.bound, quest = !!it?.questItem;
|
|
278
278
|
const name = `${qualityPaint(it?.quality)(it?.name ?? s.item)}${s.count > 1 ? ` x${s.count}` : ''}`;
|
|
279
279
|
rows.push({
|
|
280
|
-
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 ?? ''),
|
|
281
281
|
detail: () => itemLines(a, it, ['', dim('Enter: set prices and list it')]),
|
|
282
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); },
|
|
283
283
|
});
|
|
@@ -292,9 +292,9 @@ export function auctionDialog(app) {
|
|
|
292
292
|
for (const au of own) {
|
|
293
293
|
const it = ds.item(au.item);
|
|
294
294
|
rows.push({
|
|
295
|
-
text: `${qualityPaint(it?.quality)(it?.name ?? au.item)}${au.quantity > 1 ? ` x${au.quantity}` : ''} ${dim('bid')} ${
|
|
296
|
-
data: { auction: au },
|
|
297
|
-
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)')]),
|
|
298
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.`)),
|
|
299
299
|
});
|
|
300
300
|
}
|
|
@@ -302,7 +302,7 @@ export function auctionDialog(app) {
|
|
|
302
302
|
if (!bids.length) rows.push({ text: dim(' You hold no high bids.'), header: true });
|
|
303
303
|
for (const au of bids) {
|
|
304
304
|
const it = ds.item(au.item);
|
|
305
|
-
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 });
|
|
306
306
|
}
|
|
307
307
|
}
|
|
308
308
|
if (toList) {
|
|
@@ -322,11 +322,11 @@ function sellFlow(app, s, it) {
|
|
|
322
322
|
const finish = (qty, bid, buyout) => {
|
|
323
323
|
const opts = Object.entries(AH_DURATIONS_MS).map(([k, ms]) => {
|
|
324
324
|
const dep = auctionDeposit(it?.sellPrice ?? 0, qty, ms / 1000);
|
|
325
|
-
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 };
|
|
326
326
|
});
|
|
327
327
|
app.pick('How long should it run?', opts, (dur) => {
|
|
328
328
|
const r = app.g.listAuction(s.item, qty, bid, buyout, dur, Date.now());
|
|
329
|
-
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)}.` : '');
|
|
330
330
|
app.afterAction();
|
|
331
331
|
});
|
|
332
332
|
};
|
|
@@ -335,7 +335,7 @@ function sellFlow(app, s, it) {
|
|
|
335
335
|
app.prompt(`Starting bid for ${itemText(it)}${qty > 1 ? ` x${qty}` : ''} (e.g. 2g 50s): `, (t1) => {
|
|
336
336
|
const bid = parseMoney(t1);
|
|
337
337
|
if (bid == null || bid < 1) { app.say(red('Say a price like 2g 50s or 250.')); app.render(); return; }
|
|
338
|
-
app.prompt(`Buyout (at least ${
|
|
338
|
+
app.prompt(`Buyout (at least ${coinText(bid)}): `, (t2) => {
|
|
339
339
|
const buyout = parseMoney(t2);
|
|
340
340
|
if (buyout == null || buyout < bid) { app.say(red('The buyout must be at least the starting bid.')); app.render(); return; }
|
|
341
341
|
finish(qty, bid, buyout);
|
|
@@ -375,16 +375,16 @@ export function mailDialog(app) {
|
|
|
375
375
|
rows(a) {
|
|
376
376
|
const now = Date.now();
|
|
377
377
|
const mail = a.g.listMail(now);
|
|
378
|
-
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 }];
|
|
379
379
|
if (!mail.length) rows.push({ text: dim('No mail.'), header: true });
|
|
380
380
|
for (const m of mail) {
|
|
381
381
|
const atts = m.attachments.map((x) => `${a.itemLabel(x.entry)}${x.quantity > 1 ? ` x${x.quantity}` : ''}`);
|
|
382
|
-
const bits = [...(m.money ? [
|
|
382
|
+
const bits = [...(m.money ? [coinText(m.money)] : []), ...atts];
|
|
383
383
|
const read = (m.checked ?? 0) & 1;
|
|
384
384
|
rows.push({
|
|
385
|
-
text: `${read ? dim(m.subject) : bold(m.subject)} ${dim(`from ${m.sender}`)}${bits.length ? ` ${bits.join(', ')}` : ''}${m.cod ? red(` COD ${
|
|
386
|
-
data: { mail: m },
|
|
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: ${
|
|
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))}`)],
|
|
388
388
|
act: () => takeMail(a, m),
|
|
389
389
|
});
|
|
390
390
|
}
|
|
@@ -396,7 +396,7 @@ export function mailDialog(app) {
|
|
|
396
396
|
function takeMail(app, m) {
|
|
397
397
|
app.g.markMailRead(m.id);
|
|
398
398
|
let took = 0;
|
|
399
|
-
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)); }
|
|
400
400
|
for (let i = 0; i < 16; i++) {
|
|
401
401
|
const cur = app.g.listMail(Date.now()).find((x) => x.id === m.id);
|
|
402
402
|
if (!cur?.attachments.length) break;
|
|
@@ -465,7 +465,7 @@ export function composeDialog(app) {
|
|
|
465
465
|
dest.state.mailbox = r.destMailbox;
|
|
466
466
|
dest.updatedAt = Date.now();
|
|
467
467
|
await a.repo.save(dest);
|
|
468
|
-
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)}.`));
|
|
469
469
|
await a.persist();
|
|
470
470
|
if (a.top()?.id === 'mail-compose') a.pop(); else a.render();
|
|
471
471
|
};
|
|
@@ -476,7 +476,7 @@ export function composeDialog(app) {
|
|
|
476
476
|
const rows = [{ text: bold('Send mail'), header: true }, { text: '', header: true }];
|
|
477
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; }); } });
|
|
478
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) });
|
|
479
|
-
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(); }) });
|
|
480
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; } });
|
|
481
481
|
rows.push({ text: '', header: true }, { text: `Enclose ${dim(`${m.attach.size}/${MAX_MAIL_ITEMS}`)}`, header: true });
|
|
482
482
|
a.g.state.inventory.stacks.forEach((s, slot) => {
|
|
@@ -487,7 +487,7 @@ export function composeDialog(app) {
|
|
|
487
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.`); },
|
|
488
488
|
});
|
|
489
489
|
});
|
|
490
|
-
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); } });
|
|
491
491
|
return rows;
|
|
492
492
|
},
|
|
493
493
|
};
|
|
@@ -1,15 +1,15 @@
|
|
|
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';
|
|
12
11
|
import { petKnownSpells, petAutocastOn } from '../domain/pet/pet.ts';
|
|
12
|
+
import { MAX_QUEST_SLOTS } from '../domain/quests/questlog.ts';
|
|
13
13
|
import { SHARE_CATEGORIES, SHARE_CATEGORY_LABEL, SHARE_GROUPS } from '../domain/bots/consumableShare.ts';
|
|
14
14
|
|
|
15
15
|
const H = (text) => ({ text, header: true });
|
|
@@ -115,7 +115,23 @@ export function questRows(app, comp) {
|
|
|
115
115
|
const offers = g.companionQuestOffers(id);
|
|
116
116
|
const log = (comp.questLog?.quests ?? []).filter((q) => q.status !== 'turnedIn');
|
|
117
117
|
if (!offers.length && !log.length) return [];
|
|
118
|
-
|
|
118
|
+
// Ones you already handed in: their copy never completed (the items never reached their bags), so it
|
|
119
|
+
// only holds a slot now.
|
|
120
|
+
const yoursDone = new Set(g.state.questlog.quests.filter((q) => q.status === 'turnedIn').map((q) => q.entry));
|
|
121
|
+
const stale = log.filter((qs) => qs.status !== 'complete' && yoursDone.has(qs.entry));
|
|
122
|
+
const rows = [H(''), H(`Their quests ${dim(`${log.length}/${MAX_QUEST_SLOTS} in the log x: abandon`)}`)];
|
|
123
|
+
if (stale.length) {
|
|
124
|
+
const titles = stale.map((qs) => ds.quest(qs.entry)?.title ?? qs.entry);
|
|
125
|
+
rows.push({
|
|
126
|
+
text: yellow(`Drop the ${plural(stale.length, 'quest')} you already handed in`),
|
|
127
|
+
detail: () => [bold('Handed in without them'), '', 'You turned these in, but their copy never completed, usually because the quest item never reached their bags. They hold log slots, and a full log refuses shared quests.', '', ...titles.map((t) => ` ${t}`)],
|
|
128
|
+
act: () => app.confirm(`${comp.name} abandons ${plural(stale.length, 'quest')}: ${titles.join(', ')}?`, () => {
|
|
129
|
+
let n = 0;
|
|
130
|
+
for (const qs of stale) if (g.abandonCompanionQuest(id, qs.entry).ok) n++;
|
|
131
|
+
app.say(dim(`${comp.name} abandons ${plural(n, 'quest')}.`));
|
|
132
|
+
}),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
119
135
|
for (const o of offers) {
|
|
120
136
|
rows.push({
|
|
121
137
|
text: o.kind === 'turn-in' ? `${green('?')} Hand in ${o.title}` : `${yellow('!')} Take ${o.title}${o.itemEntry ? dim(` from ${app.itemLabel(o.itemEntry)}`) : ''}`,
|
|
@@ -123,7 +139,7 @@ export function questRows(app, comp) {
|
|
|
123
139
|
act: () => {
|
|
124
140
|
if (o.kind === 'accept') { app.result(g.acceptCompanionQuest(id, o.quest), `${comp.name} takes ${o.title}.`); return; }
|
|
125
141
|
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 ? `, ${
|
|
142
|
+
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
143
|
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
144
|
else finish(0);
|
|
129
145
|
},
|
|
@@ -131,7 +147,8 @@ export function questRows(app, comp) {
|
|
|
131
147
|
}
|
|
132
148
|
for (const qs of log) {
|
|
133
149
|
const q = ds.quest(qs.entry);
|
|
134
|
-
|
|
150
|
+
const done = qs.status === 'complete' ? green('(Complete)') : yoursDone.has(qs.entry) ? dim('(you handed it in)') : '';
|
|
151
|
+
rows.push({ text: ` ${q?.title ?? qs.entry} ${done}`, detail: () => [bold(q?.title ?? ''), '', dim('x: abandon')], keys: { x: (a) => a.confirm(`${comp.name} abandons ${q?.title}?`, () => a.result(g.abandonCompanionQuest(id, qs.entry), `${comp.name} abandons ${q?.title}.`)) } });
|
|
135
152
|
}
|
|
136
153
|
return rows;
|
|
137
154
|
}
|
|
@@ -165,12 +182,13 @@ export function bagRows(app, comp, cap) {
|
|
|
165
182
|
const name = `${itemText(it, 'it')}${s.count > 1 ? ` x${s.count}` : ''}`;
|
|
166
183
|
rows.push({
|
|
167
184
|
text: `${qualityPaint(it?.quality)(it?.name ?? s.item)}${s.count > 1 ? ` x${s.count}` : ''}`,
|
|
185
|
+
icon: it?.icon, sub: dim(`${it?.typeName ?? ''}${it?.sellPrice ? ` sells ${coinText(it.sellPrice * s.count)}` : ''}`),
|
|
168
186
|
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
187
|
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
188
|
keys: {
|
|
171
189
|
u: (a) => companionUseWithPicker(a, comp, s.item),
|
|
172
190
|
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 ${
|
|
191
|
+
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
192
|
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
193
|
b: (a) => a.result(g.companionBankDepositStack(id, slot), `${name} goes into ${comp.name}'s bank.`),
|
|
176
194
|
m: (a) => {
|
|
@@ -191,10 +209,10 @@ export function bankRows(app, comp) {
|
|
|
191
209
|
const rows = [H(''), H(`Their bank ${dim(`${bank.length} stacks Enter: back to their bags`)}`)];
|
|
192
210
|
bank.forEach((s, slot) => {
|
|
193
211
|
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.`) });
|
|
212
|
+
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
213
|
});
|
|
196
214
|
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}: ${
|
|
215
|
+
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
216
|
const bagsInBags = g.state.inventory.stacks.map((s) => ds.item(s.item)).filter((it) => it?.slot === 'bag');
|
|
199
217
|
if (bagsInBags.length) {
|
|
200
218
|
const free = bagSlots.map((b, i) => (b == null ? i : -1)).filter((i) => i >= 0);
|