new rpg mode
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
MAX_ACTIVE_ROSTER,
|
||||
PARTY_RECRUITS_PER_WAVE,
|
||||
createRpgRoguelikeRun,
|
||||
createRunGearItem,
|
||||
moveRpgFocus,
|
||||
reduceRpgRoguelikeRun,
|
||||
rpgFocusId,
|
||||
rpgFocusItems,
|
||||
type RpgRoguelikeRunState,
|
||||
} from ".";
|
||||
|
||||
function reachSpellDraft(seed = 810): RpgRoguelikeRunState {
|
||||
let state = createRpgRoguelikeRun({ seed });
|
||||
while (state.phase === "party-draft") {
|
||||
const draft = state.partyDraft!;
|
||||
const tank = draft.offers.find((offer) => offer.role === "Tank");
|
||||
const ordered = tank ? [tank, ...draft.offers.filter((offer) => offer !== tank)] : draft.offers;
|
||||
for (const offer of ordered) {
|
||||
if (state.roster.length >= MAX_ACTIVE_ROSTER || state.partyDraft!.recruitedThisWaveIds.length >= PARTY_RECRUITS_PER_WAVE) break;
|
||||
state = reduceRpgRoguelikeRun(state, { type: "party-recruit", candidateId: offer.candidateId });
|
||||
}
|
||||
state = reduceRpgRoguelikeRun(state, { type: "party-next-wave" });
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
describe("RPG Roguelike semantic UI focus", () => {
|
||||
it("moves horizontally within card rows and vertically between action groups", () => {
|
||||
let state = createRpgRoguelikeRun({ seed: 120 });
|
||||
const offers = state.partyDraft!.offers;
|
||||
const first = rpgFocusId.partyOffer(offers[0].candidateId);
|
||||
const second = rpgFocusId.partyOffer(offers[1].candidateId);
|
||||
const last = rpgFocusId.partyOffer(offers[offers.length - 1].candidateId);
|
||||
expect(moveRpgFocus(state, first, "right")).toBe(second);
|
||||
expect(moveRpgFocus(state, first, "left")).toBe(last);
|
||||
expect(moveRpgFocus(state, second, "down")).toBe(rpgFocusId.partyContinue);
|
||||
|
||||
state = reduceRpgRoguelikeRun(state, { type: "party-recruit", candidateId: offers[0].candidateId });
|
||||
state = reduceRpgRoguelikeRun(state, { type: "party-next-wave" });
|
||||
const laterOffer = rpgFocusId.partyOffer(state.partyDraft!.offers[0].candidateId);
|
||||
const rosterAction = rpgFocusId.partyMember(offers[0].candidateId);
|
||||
expect(moveRpgFocus(state, laterOffer, "down")).toBe(rosterAction);
|
||||
expect(moveRpgFocus(state, rosterAction, "down")).toBe(rpgFocusId.partyContinue);
|
||||
});
|
||||
|
||||
it("orders party offers, later-wave roster removals, then Continue", () => {
|
||||
let state = createRpgRoguelikeRun({ seed: 12 });
|
||||
const firstOffer = state.partyDraft!.offers[0];
|
||||
expect(rpgFocusItems(state).map((item) => item.id)).toEqual([
|
||||
...state.partyDraft!.offers.map((offer) => rpgFocusId.partyOffer(offer.candidateId)),
|
||||
rpgFocusId.partyContinue,
|
||||
]);
|
||||
|
||||
state = reduceRpgRoguelikeRun(state, { type: "party-recruit", candidateId: firstOffer.candidateId });
|
||||
expect(rpgFocusItems(state).map((item) => item.id)).not.toContain(rpgFocusId.partyMember(firstOffer.candidateId));
|
||||
state = reduceRpgRoguelikeRun(state, { type: "party-next-wave" });
|
||||
expect(rpgFocusItems(state).map((item) => item.id)).toEqual([
|
||||
...state.partyDraft!.offers.map((offer) => rpgFocusId.partyOffer(offer.candidateId)),
|
||||
rpgFocusId.partyMember(firstOffer.candidateId),
|
||||
rpgFocusId.partyContinue,
|
||||
]);
|
||||
});
|
||||
|
||||
it("omits final-wave removals that cannot be refilled", () => {
|
||||
let state = createRpgRoguelikeRun({ seed: 91 });
|
||||
state = reduceRpgRoguelikeRun(state, { type: "party-next-wave" });
|
||||
const prior = state.partyDraft!.offers.find((offer) => offer.role === "Tank")!;
|
||||
state = reduceRpgRoguelikeRun(state, { type: "party-recruit", candidateId: prior.candidateId });
|
||||
state = reduceRpgRoguelikeRun(state, { type: "party-next-wave" });
|
||||
for (const offer of state.partyDraft!.offers.slice(0, 3)) {
|
||||
state = reduceRpgRoguelikeRun(state, { type: "party-recruit", candidateId: offer.candidateId });
|
||||
}
|
||||
|
||||
expect(state.roster).toHaveLength(4);
|
||||
expect(rpgFocusItems(state).map((item) => item.id)).not.toContain(rpgFocusId.partyMember(prior.candidateId));
|
||||
expect(reduceRpgRoguelikeRun(state, { type: "party-remove", memberId: prior.candidateId })).toBe(state);
|
||||
});
|
||||
|
||||
it("locks first-wave spell picks, then exposes selected removals before Continue", () => {
|
||||
let state = reachSpellDraft();
|
||||
const spellId = state.spellDraft!.offers[0];
|
||||
state = reduceRpgRoguelikeRun(state, { type: "spell-pick", spellId });
|
||||
expect(rpgFocusItems(state).map((item) => item.id)).not.toContain(rpgFocusId.spellSelected(spellId));
|
||||
|
||||
state = reduceRpgRoguelikeRun(state, { type: "spell-next-wave" });
|
||||
expect(rpgFocusItems(state).map((item) => item.id)).toEqual([
|
||||
...state.spellDraft!.offers.map((offer) => rpgFocusId.spellOffer(offer)),
|
||||
rpgFocusId.spellSelected(spellId),
|
||||
rpgFocusId.spellContinue,
|
||||
]);
|
||||
});
|
||||
|
||||
it("orders shop purchases, bag sales, Rest, Revive, then Leave and omits disabled actions", () => {
|
||||
const drafted = reachSpellDraft(44);
|
||||
const offerItem = createRunGearItem("shop-weapon", "player", "weapon", 1);
|
||||
const soldItem = createRunGearItem("sold-armor", "player", "armor", 1);
|
||||
const bagItem = createRunGearItem("bag-trinket", "player", "trinket", 0);
|
||||
const first = drafted.roster[0];
|
||||
const second = drafted.roster[1];
|
||||
const state: RpgRoguelikeRunState = {
|
||||
...drafted,
|
||||
phase: "shop",
|
||||
currency: 200,
|
||||
roster: drafted.roster.map((member) => member.instanceId === first.instanceId
|
||||
? { ...member, hp: 0 }
|
||||
: member.instanceId === second.instanceId
|
||||
? { ...member, hp: 1 }
|
||||
: member),
|
||||
bag: [bagItem],
|
||||
shop: {
|
||||
act: 1,
|
||||
offers: [
|
||||
{ id: "available", item: offerItem, price: 80, sold: false },
|
||||
{ id: "sold", item: soldItem, price: 20, sold: true },
|
||||
],
|
||||
restCost: 35,
|
||||
reviveCost: 60,
|
||||
},
|
||||
};
|
||||
expect(rpgFocusItems(state).map((item) => item.id)).toEqual([
|
||||
rpgFocusId.shopOffer("available"),
|
||||
rpgFocusId.shopSell(bagItem.id),
|
||||
rpgFocusId.shopRest,
|
||||
rpgFocusId.shopRevive(first.instanceId),
|
||||
rpgFocusId.shopLeave,
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user