Release v0.1.7 2026-07-12

This commit is contained in:
Warren H
2026-07-12 23:39:57 -04:00
parent 122f159b94
commit 77fd434226
31 changed files with 545 additions and 113 deletions
+1 -1
View File
@@ -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");
});
+3 -2
View File
@@ -75,8 +75,8 @@ export const MODE_COPY: Record<GameModeId, { eyebrow: string; title: string; des
"rogue-trials": {
eyebrow: "14 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(),
+5 -1
View File
@@ -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();
+3 -1
View File
@@ -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);
});
+1
View File
@@ -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
View File
@@ -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
+1
View File
@@ -42,6 +42,7 @@ export interface HunterStats {
healingDone: number;
bossKills: Record<string, number>;
highestRoguelikeRound: number;
highestRogueTrialsEndlessKills: number;
}
export interface HealerProgress {