feat(web): announce offers a trade just unlocked
This commit is contained in:
@@ -6,6 +6,7 @@ import type {
|
||||
ExchangeResult,
|
||||
ExchangeView,
|
||||
NpcInteraction,
|
||||
ShopOfferView,
|
||||
ShopView,
|
||||
} from '../../core/api/game-api.models';
|
||||
import { GameApiService } from '../../core/api/game-api.service';
|
||||
@@ -74,26 +75,35 @@ function exchangeView(overrides: Partial<ExchangeView> = {}): ExchangeView {
|
||||
};
|
||||
}
|
||||
|
||||
function shopView(): ShopView {
|
||||
/** A single shop offer, defaulting to the fields no test in this file varies. */
|
||||
function offer(
|
||||
itemKey: string,
|
||||
itemName: string,
|
||||
unlocked: boolean,
|
||||
): ShopOfferView {
|
||||
return {
|
||||
itemKey,
|
||||
itemName,
|
||||
itemDescription: 'A bitter draught.',
|
||||
iconPath: '/images/items/potion.png',
|
||||
currencyType: 'SILVER',
|
||||
price: 12,
|
||||
quantity: 1,
|
||||
effectSummary: null,
|
||||
requirements: [],
|
||||
unlocked,
|
||||
affordable: true,
|
||||
};
|
||||
}
|
||||
|
||||
function shopView(offers?: ShopOfferView[]): ShopView {
|
||||
return {
|
||||
shopKey: 'borin-supplies',
|
||||
shopName: "Quartermaster's Supplies",
|
||||
npcKey: 'borin-quartermaster',
|
||||
silver: 100,
|
||||
offers: [
|
||||
{
|
||||
itemKey: 'small-healing-potion',
|
||||
itemName: 'Small Healing Potion',
|
||||
itemDescription: 'A bitter draught.',
|
||||
iconPath: '/images/items/potion.png',
|
||||
currencyType: 'SILVER',
|
||||
price: 12,
|
||||
quantity: 1,
|
||||
effectSummary: null,
|
||||
requirements: [],
|
||||
unlocked: true,
|
||||
affordable: true,
|
||||
},
|
||||
offers: offers ?? [
|
||||
offer('small-healing-potion', 'Small Healing Potion', true),
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -111,11 +121,23 @@ function tradeResult(): ExchangeResult {
|
||||
};
|
||||
}
|
||||
|
||||
function createApi(overrides: Partial<Record<string, unknown>> = {}) {
|
||||
function createApi(
|
||||
overrides: Partial<Record<string, unknown>> & { shopSequence?: ShopView[] } = {},
|
||||
) {
|
||||
// `shopSequence` lets a test hand back a different shop view on each call to
|
||||
// `getShop`, so before/after `unlocked` flags can be observed across a trade
|
||||
// without needing a real server round-trip.
|
||||
const { shopSequence, ...rest } = overrides;
|
||||
let shopCallIndex = 0;
|
||||
|
||||
return {
|
||||
getNpcInteraction: vi.fn(() => of(interaction())),
|
||||
getTradeIn: vi.fn(() => of(exchangeView())),
|
||||
getShop: vi.fn(() => of(shopView())),
|
||||
getShop: vi.fn(() =>
|
||||
shopSequence
|
||||
? of(shopSequence[Math.min(shopCallIndex++, shopSequence.length - 1)])
|
||||
: of(shopView()),
|
||||
),
|
||||
tradeIn: vi.fn(() => of(tradeResult())),
|
||||
getCharacter: vi.fn(() =>
|
||||
of({
|
||||
@@ -137,7 +159,7 @@ function createApi(overrides: Partial<Record<string, unknown>> = {}) {
|
||||
silverBalance: 88,
|
||||
}),
|
||||
),
|
||||
...overrides,
|
||||
...rest,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -361,4 +383,75 @@ describe('MerchantStore', () => {
|
||||
store.showPanel('EXCHANGE');
|
||||
expect(store.panel()).toBe('EXCHANGE');
|
||||
});
|
||||
|
||||
it('announces an offer that a trade just unlocked', async () => {
|
||||
// Reputation earned by the trade opened the pouch. The player should learn
|
||||
// that without hunting for it (slice §9).
|
||||
const api = createApi({
|
||||
shopSequence: [
|
||||
shopView([offer('basic-trophy-pouch', 'Basic Trophy Pouch', false)]),
|
||||
shopView([offer('basic-trophy-pouch', 'Basic Trophy Pouch', true)]),
|
||||
],
|
||||
});
|
||||
const store = createStore(api);
|
||||
|
||||
await store.load('borin-quartermaster');
|
||||
store.setQuantity('ash-pelt', 1);
|
||||
await store.tradeSelected();
|
||||
|
||||
expect(store.newlyUnlocked()).toEqual(['Basic Trophy Pouch']);
|
||||
});
|
||||
|
||||
it('says nothing when a trade unlocks nothing', async () => {
|
||||
const api = createApi({
|
||||
shopSequence: [
|
||||
shopView([offer('basic-trophy-pouch', 'Basic Trophy Pouch', false)]),
|
||||
shopView([offer('basic-trophy-pouch', 'Basic Trophy Pouch', false)]),
|
||||
],
|
||||
});
|
||||
const store = createStore(api);
|
||||
|
||||
await store.load('borin-quartermaster');
|
||||
store.setQuantity('ash-pelt', 1);
|
||||
await store.tradeSelected();
|
||||
|
||||
expect(store.newlyUnlocked()).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not re-announce an offer that was already open', async () => {
|
||||
const api = createApi({
|
||||
shopSequence: [
|
||||
shopView([offer('small-healing-potion', 'Small Healing Potion', true)]),
|
||||
shopView([offer('small-healing-potion', 'Small Healing Potion', true)]),
|
||||
],
|
||||
});
|
||||
const store = createStore(api);
|
||||
|
||||
await store.load('borin-quartermaster');
|
||||
store.setQuantity('ash-pelt', 1);
|
||||
await store.tradeSelected();
|
||||
|
||||
expect(store.newlyUnlocked()).toEqual([]);
|
||||
});
|
||||
|
||||
it('clears the unlock banner on a fresh load and at the start of a purchase', async () => {
|
||||
// A stale banner from a previous merchant visit, or from before a buy
|
||||
// click resolves, would misattribute an unlock to the wrong action.
|
||||
const api = createApi({
|
||||
shopSequence: [
|
||||
shopView([offer('basic-trophy-pouch', 'Basic Trophy Pouch', false)]),
|
||||
shopView([offer('basic-trophy-pouch', 'Basic Trophy Pouch', true)]),
|
||||
],
|
||||
});
|
||||
const store = createStore(api);
|
||||
|
||||
await store.load('borin-quartermaster');
|
||||
store.setQuantity('ash-pelt', 1);
|
||||
await store.tradeSelected();
|
||||
expect(store.newlyUnlocked()).toEqual(['Basic Trophy Pouch']);
|
||||
|
||||
await store.buy('basic-trophy-pouch');
|
||||
|
||||
expect(store.newlyUnlocked()).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user