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 => {
|
const applyAdventurerUnlocks = (state: GameState): number => {
|
||||||
let count = 0;
|
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>();
|
const earnedAdventurerIds = new Set<string>();
|
||||||
|
|
||||||
for (const quest of state.quests) {
|
for (const questDefinition of defaultQuests) {
|
||||||
if (quest.status !== "completed") {
|
if (!completedQuestIds.has(questDefinition.id)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (const reward of quest.rewards) {
|
for (const reward of questDefinition.rewards) {
|
||||||
if (reward.type === "adventurer" && reward.targetId !== undefined) {
|
if (reward.type === "adventurer" && reward.targetId !== undefined) {
|
||||||
earnedAdventurerIds.add(reward.targetId);
|
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
|
* 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.
|
* @param state - The player's current game state.
|
||||||
* @returns A set of earned upgrade IDs.
|
* @returns A set of earned upgrade IDs.
|
||||||
*/
|
*/
|
||||||
const collectEarnedUpgradeIds = (state: GameState): Set<string> => {
|
const collectEarnedUpgradeIds = (state: GameState): Set<string> => {
|
||||||
const earnedIds = new Set<string>();
|
const earnedIds = new Set<string>();
|
||||||
for (const boss of state.bosses) {
|
const defeatedBossIds = new Set(
|
||||||
if (boss.status === "defeated") {
|
state.bosses.
|
||||||
for (const upgradeId of boss.upgradeRewards) {
|
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 upgradeId of bossDefinition.upgradeRewards) {
|
||||||
earnedIds.add(upgradeId);
|
earnedIds.add(upgradeId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
for (const quest of state.quests) {
|
for (const questDefinition of defaultQuests) {
|
||||||
if (quest.status !== "completed") {
|
if (!completedQuestIds.has(questDefinition.id)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (const reward of quest.rewards) {
|
for (const reward of questDefinition.rewards) {
|
||||||
if (reward.type === "upgrade" && reward.targetId !== undefined) {
|
if (reward.type === "upgrade" && reward.targetId !== undefined) {
|
||||||
earnedIds.add(reward.targetId);
|
earnedIds.add(reward.targetId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return earnedIds;
|
return earnedIds;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -348,13 +379,22 @@ const applyUpgradeUnlocks = (state: GameState): number => {
|
|||||||
*/
|
*/
|
||||||
const applyEquipmentUnlocks = (state: GameState): number => {
|
const applyEquipmentUnlocks = (state: GameState): number => {
|
||||||
let count = 0;
|
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>();
|
const earnedEquipmentIds = new Set<string>();
|
||||||
|
|
||||||
for (const boss of state.bosses) {
|
for (const bossDefinition of defaultBosses) {
|
||||||
if (boss.status !== "defeated") {
|
if (!defeatedBossIds.has(bossDefinition.id)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (const equipmentId of boss.equipmentRewards) {
|
for (const equipmentId of bossDefinition.equipmentRewards) {
|
||||||
earnedEquipmentIds.add(equipmentId);
|
earnedEquipmentIds.add(equipmentId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -366,6 +366,161 @@ describe("debug route", () => {
|
|||||||
expect(body.explorationUnlocked).toBe(0);
|
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 () => {
|
it("computes HMAC signature when ANTI_CHEAT_SECRET is set", async () => {
|
||||||
process.env.ANTI_CHEAT_SECRET = "test_secret";
|
process.env.ANTI_CHEAT_SECRET = "test_secret";
|
||||||
const state = makeState();
|
const state = makeState();
|
||||||
|
|||||||
Reference in New Issue
Block a user