feat(web): announce offers a trade just unlocked
This commit is contained in:
@@ -30,6 +30,8 @@ const ERROR_MESSAGES: Readonly<Record<string, string>> = {
|
||||
SHOP_OFFER_LOCKED: 'You have not earned the right to buy this yet.',
|
||||
SHOP_INSUFFICIENT_SILVER: 'You cannot afford that.',
|
||||
SHOP_INVALID_QUANTITY: 'That quantity cannot be bought.',
|
||||
MERCHANT_REPUTATION_TOO_LOW: 'You have not earned enough standing for this yet.',
|
||||
SHOP_BAG_ALREADY_OWNED: 'You already carry that.',
|
||||
CHARACTER_NOT_FOUND: 'Your character could not be found.',
|
||||
};
|
||||
|
||||
@@ -59,6 +61,7 @@ export class MerchantStore {
|
||||
private readonly lastTradeState = signal<ExchangeResult | null>(null);
|
||||
private readonly lastPurchaseState = signal<ShopPurchaseResult | null>(null);
|
||||
private readonly selectionState = signal<Record<string, number>>({});
|
||||
private readonly newlyUnlockedState = signal<string[]>([]);
|
||||
|
||||
readonly interaction = this.interactionState.asReadonly();
|
||||
readonly exchange = this.exchangeState.asReadonly();
|
||||
@@ -71,6 +74,7 @@ export class MerchantStore {
|
||||
readonly lastTrade = this.lastTradeState.asReadonly();
|
||||
readonly lastPurchase = this.lastPurchaseState.asReadonly();
|
||||
readonly selection = this.selectionState.asReadonly();
|
||||
readonly newlyUnlocked = this.newlyUnlockedState.asReadonly();
|
||||
|
||||
/** True once anything is selected, so the trade button can enable. */
|
||||
readonly hasSelection = computed(() =>
|
||||
@@ -110,6 +114,7 @@ export class MerchantStore {
|
||||
this.lastTradeState.set(null);
|
||||
this.lastPurchaseState.set(null);
|
||||
this.selectionState.set({});
|
||||
this.newlyUnlockedState.set([]);
|
||||
this.panelState.set('DIALOGUE');
|
||||
|
||||
try {
|
||||
@@ -207,8 +212,13 @@ export class MerchantStore {
|
||||
// capacity, Silver and possibly renown at once, and the server is the
|
||||
// only place that knows all of it.
|
||||
this.exchangeState.set(await firstValueFrom(this.api.getTradeIn(npcKey)));
|
||||
this.shopState.set(
|
||||
this.shopState() ? await firstValueFrom(this.api.getShop(npcKey)) : null,
|
||||
const previousShop = this.shopState();
|
||||
const refreshedShop = previousShop
|
||||
? await firstValueFrom(this.api.getShop(npcKey))
|
||||
: null;
|
||||
this.shopState.set(refreshedShop);
|
||||
this.newlyUnlockedState.set(
|
||||
this.unlockedSince(previousShop, refreshedShop),
|
||||
);
|
||||
|
||||
// The purse in the top bar comes from the shared character state, so a
|
||||
@@ -230,6 +240,9 @@ export class MerchantStore {
|
||||
|
||||
this.pendingState.set(itemKey);
|
||||
this.actionErrorState.set(null);
|
||||
// A purchase re-reads the shop below, so a banner from an earlier trade
|
||||
// must not linger and be misread as caused by this buy.
|
||||
this.newlyUnlockedState.set([]);
|
||||
|
||||
try {
|
||||
this.lastPurchaseState.set(
|
||||
@@ -252,6 +265,37 @@ export class MerchantStore {
|
||||
this.lastPurchaseState.set(null);
|
||||
}
|
||||
|
||||
dismissUnlocked(): void {
|
||||
this.newlyUnlockedState.set([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Offer names that went from locked to open (slice §9).
|
||||
*
|
||||
* Derived from two shop reads the store already performs rather than from a
|
||||
* server event: the trade re-fetches the shop anyway, so the before/after
|
||||
* state is in hand, and a notification the server has to remember would be
|
||||
* more machinery than a one-line message is worth.
|
||||
*/
|
||||
private unlockedSince(
|
||||
before: ShopView | null,
|
||||
after: ShopView | null,
|
||||
): string[] {
|
||||
if (!before || !after) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const wasLocked = new Set(
|
||||
before.offers
|
||||
.filter((offer) => !offer.unlocked)
|
||||
.map((offer) => offer.itemKey),
|
||||
);
|
||||
|
||||
return after.offers
|
||||
.filter((offer) => offer.unlocked && wasLocked.has(offer.itemKey))
|
||||
.map((offer) => offer.itemName);
|
||||
}
|
||||
|
||||
private toMessage(error: unknown): string {
|
||||
if (error instanceof HttpErrorResponse) {
|
||||
const code = (error.error as { code?: string } | null)?.code;
|
||||
|
||||
Reference in New Issue
Block a user