generated from nhcarrigan/template
fix: adventurer unlocks not applied by force-unlock tool #93
@@ -269,13 +269,22 @@ const applyBossUnlocks = (state: GameState): number => {
|
||||
*/
|
||||
const applyAdventurerUnlocks = (state: GameState): number => {
|
||||
let count = 0;
|
||||
const completedQuestIds = new Set(
|
||||
state.quests.
|
||||
filter((q) => {
|
||||
return q.status === "completed";
|
||||
}).
|
||||
map((q) => {
|
||||
return q.id;
|
||||
}),
|
||||
);
|
||||
const earnedAdventurerIds = new Set<string>();
|
||||
|
||||
for (const quest of state.quests) {
|
||||
if (quest.status !== "completed") {
|
||||
for (const questDefinition of defaultQuests) {
|
||||
if (!completedQuestIds.has(questDefinition.id)) {
|
||||
continue;
|
||||
}
|
||||
for (const reward of quest.rewards) {
|
||||
for (const reward of questDefinition.rewards) {
|
||||
if (reward.type === "adventurer" && reward.targetId !== undefined) {
|
||||
earnedAdventurerIds.add(reward.targetId);
|
||||
}
|
||||
@@ -294,29 +303,51 @@ const applyAdventurerUnlocks = (state: GameState): number => {
|
||||
|
||||
/**
|
||||
* Collects all upgrade IDs the player has legitimately earned via boss defeats
|
||||
* and completed quest rewards.
|
||||
* and completed quest rewards, sourcing reward data from game definitions.
|
||||
* @param state - The player's current game state.
|
||||
* @returns A set of earned upgrade IDs.
|
||||
*/
|
||||
const collectEarnedUpgradeIds = (state: GameState): Set<string> => {
|
||||
const earnedIds = new Set<string>();
|
||||
for (const boss of state.bosses) {
|
||||
if (boss.status === "defeated") {
|
||||
for (const upgradeId of boss.upgradeRewards) {
|
||||
earnedIds.add(upgradeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const quest of state.quests) {
|
||||
if (quest.status !== "completed") {
|
||||
const defeatedBossIds = new Set(
|
||||
state.bosses.
|
||||
filter((b) => {
|
||||
return b.status === "defeated";
|
||||
}).
|
||||
map((b) => {
|
||||
return b.id;
|
||||
}),
|
||||
);
|
||||
const completedQuestIds = new Set(
|
||||
state.quests.
|
||||
filter((q) => {
|
||||
return q.status === "completed";
|
||||
}).
|
||||
map((q) => {
|
||||
return q.id;
|
||||
}),
|
||||
);
|
||||
|
||||
for (const bossDefinition of defaultBosses) {
|
||||
if (!defeatedBossIds.has(bossDefinition.id)) {
|
||||
continue;
|
||||
}
|
||||
for (const reward of quest.rewards) {
|
||||
for (const upgradeId of bossDefinition.upgradeRewards) {
|
||||
earnedIds.add(upgradeId);
|
||||
}
|
||||
}
|
||||
|
||||
for (const questDefinition of defaultQuests) {
|
||||
if (!completedQuestIds.has(questDefinition.id)) {
|
||||
continue;
|
||||
}
|
||||
for (const reward of questDefinition.rewards) {
|
||||
if (reward.type === "upgrade" && reward.targetId !== undefined) {
|
||||
earnedIds.add(reward.targetId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return earnedIds;
|
||||
};
|
||||
|
||||
@@ -348,13 +379,22 @@ const applyUpgradeUnlocks = (state: GameState): number => {
|
||||
*/
|
||||
const applyEquipmentUnlocks = (state: GameState): number => {
|
||||
let count = 0;
|
||||
const defeatedBossIds = new Set(
|
||||
state.bosses.
|
||||
filter((b) => {
|
||||
return b.status === "defeated";
|
||||
}).
|
||||
map((b) => {
|
||||
return b.id;
|
||||
}),
|
||||
);
|
||||
const earnedEquipmentIds = new Set<string>();
|
||||
|
||||
for (const boss of state.bosses) {
|
||||
if (boss.status !== "defeated") {
|
||||
for (const bossDefinition of defaultBosses) {
|
||||
if (!defeatedBossIds.has(bossDefinition.id)) {
|
||||
continue;
|
||||
}
|
||||
for (const equipmentId of boss.equipmentRewards) {
|
||||
for (const equipmentId of bossDefinition.equipmentRewards) {
|
||||
earnedEquipmentIds.add(equipmentId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,6 +366,161 @@ describe("debug route", () => {
|
||||
expect(body.explorationUnlocked).toBe(0);
|
||||
});
|
||||
|
||||
it("unlocks adventurer tier when its quest has been completed", async () => {
|
||||
const state = makeState({
|
||||
adventurers: [ { id: "scout", unlocked: false } ] as GameState["adventurers"],
|
||||
quests: [ { id: "haunted_mine", status: "completed" } ] as GameState["quests"],
|
||||
});
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
const res = await forceUnlocks();
|
||||
const body = await res.json() as { adventurersUnlocked: number };
|
||||
expect(body.adventurersUnlocked).toBe(1);
|
||||
});
|
||||
|
||||
it("does not unlock adventurer tier when it is already unlocked", async () => {
|
||||
const state = makeState({
|
||||
adventurers: [ { id: "scout", unlocked: true } ] as GameState["adventurers"],
|
||||
quests: [ { id: "haunted_mine", status: "completed" } ] as GameState["quests"],
|
||||
});
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
const res = await forceUnlocks();
|
||||
const body = await res.json() as { adventurersUnlocked: number };
|
||||
expect(body.adventurersUnlocked).toBe(0);
|
||||
});
|
||||
|
||||
it("unlocks upgrade when its boss has been defeated", async () => {
|
||||
const state = makeState({
|
||||
bosses: [ { id: "troll_king", status: "defeated" } ] as GameState["bosses"],
|
||||
upgrades: [ { id: "click_2", unlocked: false } ] as GameState["upgrades"],
|
||||
});
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
const res = await forceUnlocks();
|
||||
const body = await res.json() as { upgradesUnlocked: number };
|
||||
expect(body.upgradesUnlocked).toBe(1);
|
||||
});
|
||||
|
||||
it("does not unlock upgrade when boss is not defeated", async () => {
|
||||
const state = makeState({
|
||||
bosses: [ { id: "troll_king", status: "available" } ] as GameState["bosses"],
|
||||
upgrades: [ { id: "click_2", unlocked: false } ] as GameState["upgrades"],
|
||||
});
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
const res = await forceUnlocks();
|
||||
const body = await res.json() as { upgradesUnlocked: number };
|
||||
expect(body.upgradesUnlocked).toBe(0);
|
||||
});
|
||||
|
||||
it("does not unlock upgrade when it is already unlocked", async () => {
|
||||
const state = makeState({
|
||||
bosses: [ { id: "troll_king", status: "defeated" } ] as GameState["bosses"],
|
||||
upgrades: [ { id: "click_2", unlocked: true } ] as GameState["upgrades"],
|
||||
});
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
const res = await forceUnlocks();
|
||||
const body = await res.json() as { upgradesUnlocked: number };
|
||||
expect(body.upgradesUnlocked).toBe(0);
|
||||
});
|
||||
|
||||
it("unlocks upgrade granted as a quest reward", async () => {
|
||||
const state = makeState({
|
||||
quests: [ { id: "haunted_mine", status: "completed" } ] as GameState["quests"],
|
||||
upgrades: [ { id: "global_1", unlocked: false } ] as GameState["upgrades"],
|
||||
});
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
const res = await forceUnlocks();
|
||||
const body = await res.json() as { upgradesUnlocked: number };
|
||||
expect(body.upgradesUnlocked).toBe(1);
|
||||
});
|
||||
|
||||
it("marks equipment as owned when its boss has been defeated", async () => {
|
||||
const state = makeState({
|
||||
bosses: [ { id: "troll_king", status: "defeated" } ] as GameState["bosses"],
|
||||
equipment: [ { id: "iron_sword", owned: false } ] as GameState["equipment"],
|
||||
});
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
const res = await forceUnlocks();
|
||||
const body = await res.json() as { equipmentUnlocked: number };
|
||||
expect(body.equipmentUnlocked).toBe(1);
|
||||
});
|
||||
|
||||
it("does not mark equipment as owned when boss is not defeated", async () => {
|
||||
const state = makeState({
|
||||
bosses: [ { id: "troll_king", status: "available" } ] as GameState["bosses"],
|
||||
equipment: [ { id: "iron_sword", owned: false } ] as GameState["equipment"],
|
||||
});
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
const res = await forceUnlocks();
|
||||
const body = await res.json() as { equipmentUnlocked: number };
|
||||
expect(body.equipmentUnlocked).toBe(0);
|
||||
});
|
||||
|
||||
it("does not mark equipment as owned when it is already owned", async () => {
|
||||
const state = makeState({
|
||||
bosses: [ { id: "troll_king", status: "defeated" } ] as GameState["bosses"],
|
||||
equipment: [ { id: "iron_sword", owned: true } ] as GameState["equipment"],
|
||||
});
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
const res = await forceUnlocks();
|
||||
const body = await res.json() as { equipmentUnlocked: number };
|
||||
expect(body.equipmentUnlocked).toBe(0);
|
||||
});
|
||||
|
||||
it("returns storyUnlocked=0 when story is undefined", async () => {
|
||||
const state = makeState({
|
||||
story: undefined as unknown as GameState["story"],
|
||||
});
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
const res = await forceUnlocks();
|
||||
const body = await res.json() as { storyUnlocked: number };
|
||||
expect(body.storyUnlocked).toBe(0);
|
||||
});
|
||||
|
||||
it("unlocks story chapter when its boss has been defeated", async () => {
|
||||
const state = makeState({
|
||||
bosses: [ { id: "forest_giant", status: "defeated" } ] as GameState["bosses"],
|
||||
story: { completedChapters: [], unlockedChapterIds: [] },
|
||||
});
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
const res = await forceUnlocks();
|
||||
const body = await res.json() as { storyUnlocked: number };
|
||||
expect(body.storyUnlocked).toBe(1);
|
||||
});
|
||||
|
||||
it("does not unlock story chapter when boss is not defeated", async () => {
|
||||
const state = makeState({
|
||||
bosses: [ { id: "forest_giant", status: "available" } ] as GameState["bosses"],
|
||||
story: { completedChapters: [], unlockedChapterIds: [] },
|
||||
});
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
const res = await forceUnlocks();
|
||||
const body = await res.json() as { storyUnlocked: number };
|
||||
expect(body.storyUnlocked).toBe(0);
|
||||
});
|
||||
|
||||
it("does not unlock story chapter when it is already unlocked", async () => {
|
||||
const state = makeState({
|
||||
bosses: [ { id: "forest_giant", status: "defeated" } ] as GameState["bosses"],
|
||||
story: { completedChapters: [], unlockedChapterIds: [ "story_ch_01" ] },
|
||||
});
|
||||
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
|
||||
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
|
||||
const res = await forceUnlocks();
|
||||
const body = await res.json() as { storyUnlocked: number };
|
||||
expect(body.storyUnlocked).toBe(0);
|
||||
});
|
||||
|
||||
it("computes HMAC signature when ANTI_CHEAT_SECRET is set", async () => {
|
||||
process.env.ANTI_CHEAT_SECRET = "test_secret";
|
||||
const state = makeState();
|
||||
|
||||
Reference in New Issue
Block a user