Release v0.1.7 2026-07-12
This commit is contained in:
@@ -7,7 +7,7 @@ import { AVAILABLE_BOSS_IDS, BOSS_GROUPS } from "../game/bossCatalog";
|
||||
describe("game mode configuration", () => {
|
||||
it("separates randomized PVE from selectable Dungeons", () => {
|
||||
expect(MODE_COPY["roguelike-pve"].title).toBe("PVE");
|
||||
expect(MODE_COPY["rogue-trials"].detail).toContain("unseen");
|
||||
expect(MODE_COPY["rogue-trials"].detail).toContain("Endless");
|
||||
expect(MODE_COPY.dungeons.title).toBe("Dungeons");
|
||||
});
|
||||
|
||||
|
||||
@@ -75,8 +75,8 @@ export const MODE_COPY: Record<GameModeId, { eyebrow: string; title: string; des
|
||||
"rogue-trials": {
|
||||
eyebrow: "1–4 hunters · five-round PVE trial",
|
||||
title: "Rogue Trials",
|
||||
description: "Build through four randomized dual-boss rounds, then face three bosses together in a final trial.",
|
||||
detail: "Round 5 trio always uses bosses unseen during that run",
|
||||
description: "Build through four randomized dual-boss rounds, defeat an unseen trio, then leave with the clear or continue into endless combat.",
|
||||
detail: "Endless mode replaces every fallen boss and tracks your best kill count",
|
||||
status: "Playable now",
|
||||
},
|
||||
dungeons: {
|
||||
@@ -139,6 +139,7 @@ export function createHunterSave(slotId: SaveSlotId, now: string, hunterName: st
|
||||
healingDone: 0,
|
||||
bossKills: {},
|
||||
highestRoguelikeRound: 0,
|
||||
highestRogueTrialsEndlessKills: 0,
|
||||
},
|
||||
materials: [] as MaterialStack[],
|
||||
collectionLog: createEmptyCollectionLog(),
|
||||
|
||||
@@ -22,7 +22,7 @@ export interface LeaderboardEntry {
|
||||
}
|
||||
|
||||
export interface LeaderboardResult {
|
||||
kind: "boss" | "roguelike";
|
||||
kind: "boss" | "roguelike" | "rogue-trials-endless";
|
||||
bossId?: BossId;
|
||||
top: LeaderboardEntry[];
|
||||
current: LeaderboardEntry | null;
|
||||
@@ -147,6 +147,10 @@ export class OnlineRepository {
|
||||
roguelikeLeaderboard(slotId: SaveSlotId): Promise<LeaderboardResult> {
|
||||
return this.request(`/api/leaderboards/roguelike?slot=${slotId}`);
|
||||
}
|
||||
|
||||
rogueTrialsEndlessLeaderboard(slotId: SaveSlotId): Promise<LeaderboardResult> {
|
||||
return this.request(`/api/leaderboards/rogue-trials-endless?slot=${slotId}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const onlineRepository = new OnlineRepository();
|
||||
|
||||
@@ -108,7 +108,7 @@ describe("SaveRepository", () => {
|
||||
expect(migrated.activeClassId).toBe("priest");
|
||||
expect(migrated.playSeconds).toBe(0);
|
||||
expect(Object.values(migrated.healers).every((healer) => healer.level === 1 && healer.inventory.length > 0)).toBe(true);
|
||||
expect(migrated.stats).toEqual({ totalBossKills: 0, flawlessClears: 0, alliesSaved: 0, healingDone: 0, bossKills: {}, highestRoguelikeRound: 0 });
|
||||
expect(migrated.stats).toEqual({ totalBossKills: 0, flawlessClears: 0, alliesSaved: 0, healingDone: 0, bossKills: {}, highestRoguelikeRound: 0, highestRogueTrialsEndlessKills: 0 });
|
||||
expect(migrated.materials).toEqual([]);
|
||||
expect(migrated.collectionLog).toEqual({ dropsFound: {}, petsFound: {} });
|
||||
expect(Object.values(migrated.gearProgress).every((owner) => owner.infusionAbilityId === null && owner.passiveInfusionId === null && Object.values(owner.slots).every((slot) => slot.level === 0))).toBe(true);
|
||||
@@ -122,6 +122,7 @@ describe("SaveRepository", () => {
|
||||
const drop = groupDrop("charge", "veteran");
|
||||
created.healers.priest.level = 8;
|
||||
created.stats = { ...created.stats, totalBossKills: 2, bossKills: { bulldrome: 2 } };
|
||||
created.stats.highestRogueTrialsEndlessKills = 14;
|
||||
created.materials = [{ id: drop.id, name: drop.name, quantity: 4, rarity: drop.rarity, itemLevel: drop.itemLevel, glyph: drop.glyph }];
|
||||
created.collectionLog = { dropsFound: { [drop.id]: 4 }, petsFound: { "bulldrome-pet": 1 } };
|
||||
storage.setItem("i-want-to-heal:saves:local:v1", JSON.stringify({ 1: created }));
|
||||
@@ -130,6 +131,7 @@ describe("SaveRepository", () => {
|
||||
expect(migrated.schemaVersion).toBe(5);
|
||||
expect(migrated.healers.priest.level).toBe(8);
|
||||
expect(migrated.stats.bossKills).toEqual({ bulldrome: 2 });
|
||||
expect(migrated.stats.highestRogueTrialsEndlessKills).toBe(14);
|
||||
expect(migrated.materials[0]).toMatchObject({ id: drop.id, quantity: 4 });
|
||||
expect(migrated.collectionLog).toEqual(created.collectionLog);
|
||||
});
|
||||
|
||||
@@ -150,6 +150,7 @@ function normalizeSave(value: unknown): HunterSave | null {
|
||||
healingDone: Math.max(0, candidate.stats?.healingDone ?? 0),
|
||||
bossKills,
|
||||
highestRoguelikeRound: Math.max(0, Math.floor(candidate.stats?.highestRoguelikeRound ?? 0)),
|
||||
highestRogueTrialsEndlessKills: Math.max(0, Math.floor(candidate.stats?.highestRogueTrialsEndlessKills ?? 0)),
|
||||
},
|
||||
materials: normalizeMaterials(candidate.materials, collectionLog),
|
||||
collectionLog,
|
||||
|
||||
+18
-2
@@ -13,7 +13,7 @@ import {
|
||||
infusionsForOwner,
|
||||
} from "../game/progression/infusions";
|
||||
import { normalizeDifficultySlug, rollBossReward, type BossRewardAward, type DifficultySlug } from "../game/progression/loot";
|
||||
import { highestRoguelikeRoundAfterDefeat } from "../game/progression/hunterStats";
|
||||
import { highestEndlessBossKillsAfterDefeat, highestRoguelikeRoundAfterDefeat } from "../game/progression/hunterStats";
|
||||
|
||||
const repository = new SaveRepository();
|
||||
const accounts = new AccountRepository();
|
||||
@@ -125,6 +125,7 @@ export interface FrontendState {
|
||||
touchActiveSave: () => void;
|
||||
recordBossVictory: (bossId: BossId, difficultySlug: DifficultySlug) => BossRewardAward | null;
|
||||
recordRoguelikeDefeat: (round: number) => void;
|
||||
recordRogueTrialsEndlessDefeat: (bossKills: number) => void;
|
||||
clearRecentRewards: () => void;
|
||||
clearNotice: () => void;
|
||||
}
|
||||
@@ -388,7 +389,7 @@ export const useFrontendStore = create<FrontendState>((set, get) => ({
|
||||
});
|
||||
set((state) => ({
|
||||
slots: refreshLocalSlots(state.slots),
|
||||
recentRewards: awarded ? [...state.recentRewards, awarded] : state.recentRewards,
|
||||
recentRewards: awarded ? [...state.recentRewards, awarded].slice(-12) : state.recentRewards,
|
||||
notice: awarded ? `${awarded.drop.name} x${awarded.quantity} saved.` : "Boss clear saved.",
|
||||
}));
|
||||
return awarded;
|
||||
@@ -406,6 +407,19 @@ export const useFrontendStore = create<FrontendState>((set, get) => ({
|
||||
if (!updated) return;
|
||||
set((state) => ({ slots: refreshLocalSlots(state.slots) }));
|
||||
},
|
||||
recordRogueTrialsEndlessDefeat: (bossKills) => {
|
||||
const { activeSlotId } = get();
|
||||
if (!activeSlotId) return;
|
||||
const updated = repository.updateLocal(activeSlotId, (save) => ({
|
||||
...save,
|
||||
stats: {
|
||||
...save.stats,
|
||||
highestRogueTrialsEndlessKills: highestEndlessBossKillsAfterDefeat(save.stats.highestRogueTrialsEndlessKills, bossKills),
|
||||
},
|
||||
}));
|
||||
if (!updated) return;
|
||||
set((state) => ({ slots: refreshLocalSlots(state.slots) }));
|
||||
},
|
||||
clearRecentRewards: () => set({ recentRewards: [] }),
|
||||
clearNotice: () => set({ notice: "" }),
|
||||
}));
|
||||
@@ -442,6 +456,7 @@ export type FrontendSnapshot = Omit<FrontendState,
|
||||
| "touchActiveSave"
|
||||
| "recordBossVictory"
|
||||
| "recordRoguelikeDefeat"
|
||||
| "recordRogueTrialsEndlessDefeat"
|
||||
| "clearRecentRewards"
|
||||
| "clearNotice"
|
||||
>;
|
||||
@@ -479,6 +494,7 @@ export function getFrontendSnapshot(): FrontendSnapshot {
|
||||
touchActiveSave: _touchActiveSave,
|
||||
recordBossVictory: _recordBossVictory,
|
||||
recordRoguelikeDefeat: _recordRoguelikeDefeat,
|
||||
recordRogueTrialsEndlessDefeat: _recordRogueTrialsEndlessDefeat,
|
||||
clearRecentRewards: _clearRecentRewards,
|
||||
clearNotice: _clearNotice,
|
||||
...snapshot
|
||||
|
||||
@@ -42,6 +42,7 @@ export interface HunterStats {
|
||||
healingDone: number;
|
||||
bossKills: Record<string, number>;
|
||||
highestRoguelikeRound: number;
|
||||
highestRogueTrialsEndlessKills: number;
|
||||
}
|
||||
|
||||
export interface HealerProgress {
|
||||
|
||||
Reference in New Issue
Block a user