Update 3D game 2026-07-10 21:20

This commit is contained in:
Warren H
2026-07-10 21:20:17 -04:00
parent 141ec64963
commit e0be0458aa
720 changed files with 366857 additions and 0 deletions
+173
View File
@@ -0,0 +1,173 @@
import { create } from "zustand";
import { DEFAULT_SETTINGS, normalizeHunterName } from "./data";
import { SaveRepository } from "./saveRepository";
import type { AppScreen, GameModeId, GameSettings, HunterSave, SaveSlotId, SaveSlotState } from "./types";
import type { BossId, HealerClassId, InventoryItem } from "../game/types";
const repository = new SaveRepository();
const SETTINGS_KEY = "i-want-to-heal:settings:v1";
function loadSettings(): GameSettings {
try {
const saved = localStorage.getItem(SETTINGS_KEY);
return saved ? { ...DEFAULT_SETTINGS, ...JSON.parse(saved) } : DEFAULT_SETTINGS;
} catch {
return DEFAULT_SETTINGS;
}
}
function persistSettings(settings: GameSettings) {
try {
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
} catch {
// Settings remain active for this session when storage is unavailable.
}
}
interface FrontendState {
screen: AppScreen;
accountId: string | null;
slots: SaveSlotState[];
selectedSlotId: SaveSlotId;
activeSlotId: SaveSlotId | null;
selectedMode: GameModeId;
selectedBossId: BossId;
settings: GameSettings;
notice: string;
signIn: (accountId: string) => void;
continueOffline: () => void;
signOut: () => void;
navigate: (screen: AppScreen) => void;
selectSlot: (slotId: SaveSlotId) => void;
createSlot: (slotId: SaveSlotId, hunterName: string) => boolean;
playSlot: (slotId: SaveSlotId) => void;
deleteSlot: (slotId: SaveSlotId) => void;
copySlot: (sourceId: SaveSlotId, targetId: SaveSlotId) => void;
uploadSlot: (slotId: SaveSlotId) => void;
downloadSlot: (slotId: SaveSlotId) => void;
selectMode: (mode: GameModeId) => void;
selectBoss: (bossId: BossId) => void;
selectHealerClass: (classId: HealerClassId) => void;
updateActiveHealerInventory: (inventory: InventoryItem[]) => void;
updateSetting: <K extends keyof GameSettings>(key: K, value: GameSettings[K]) => void;
touchActiveSave: () => void;
recordBossVictory: (bossName: string) => void;
clearNotice: () => void;
}
function activeSave(slots: SaveSlotState[], activeSlotId: SaveSlotId | null): HunterSave | null {
return slots.find((slot) => slot.id === activeSlotId)?.local ?? null;
}
export const useFrontendStore = create<FrontendState>((set, get) => ({
screen: "login",
accountId: null,
slots: repository.list(null),
selectedSlotId: 1,
activeSlotId: null,
selectedMode: "roguelike-pve",
selectedBossId: "bulldrome",
settings: loadSettings(),
notice: "",
signIn: (rawAccountId) => {
const accountId = rawAccountId.trim() || "wayfinder";
set({ accountId, slots: repository.list(accountId), screen: "saves", notice: `Online sync connected as ${accountId}.` });
},
continueOffline: () => set({ accountId: null, slots: repository.list(null), screen: "saves", notice: "Offline saves ready." }),
signOut: () => set({ accountId: null, slots: repository.list(null), activeSlotId: null, screen: "login", notice: "Signed out. Offline saves remain on this device." }),
navigate: (screen) => set({ screen, notice: "" }),
selectSlot: (selectedSlotId) => set({ selectedSlotId, notice: "" }),
createSlot: (slotId, rawHunterName) => {
const hunterName = normalizeHunterName(rawHunterName);
if (!hunterName) {
set({ notice: "Enter a hunter name before creating the save." });
return false;
}
repository.create(slotId, hunterName);
set((state) => ({ slots: repository.list(state.accountId), selectedSlotId: slotId, notice: `${hunterName} created in offline slot ${slotId}.` }));
return true;
},
playSlot: (slotId) => {
const local = repository.touch(slotId);
if (!local) return;
set((state) => ({ activeSlotId: slotId, selectedSlotId: slotId, slots: repository.list(state.accountId), screen: "home", notice: "Offline save loaded." }));
},
deleteSlot: (slotId) => {
repository.deleteLocal(slotId);
set((state) => ({
slots: repository.list(state.accountId),
activeSlotId: state.activeSlotId === slotId ? null : state.activeSlotId,
notice: `Local slot ${slotId} deleted. Online copy preserved.`,
}));
},
copySlot: (sourceId, targetId) => {
const copy = repository.copyLocal(sourceId, targetId);
if (!copy) return;
set((state) => ({ slots: repository.list(state.accountId), selectedSlotId: targetId, notice: `Slot ${sourceId} copied to slot ${targetId}.` }));
},
uploadSlot: (slotId) => {
const { accountId } = get();
if (!accountId) return set({ notice: "Sign in before syncing online." });
const uploaded = repository.upload(slotId, accountId);
set({ slots: repository.list(accountId), notice: uploaded ? `Slot ${slotId} synced to server.` : "No offline save to sync." });
},
downloadSlot: (slotId) => {
const { accountId } = get();
if (!accountId) return set({ notice: "Sign in before downloading an online save." });
const downloaded = repository.download(slotId, accountId);
set({ slots: repository.list(accountId), notice: downloaded ? `Slot ${slotId} overwritten with online version.` : "No online version exists for this slot." });
},
selectMode: (selectedMode) => set({ selectedMode, screen: "mode", notice: "" }),
selectBoss: (selectedBossId) => set({ selectedBossId, notice: "" }),
selectHealerClass: (classId) => {
const { activeSlotId, accountId } = get();
if (!activeSlotId) return;
const updated = repository.updateLocal(activeSlotId, (save) => ({ ...save, activeClassId: classId }));
if (!updated) return;
set({ slots: repository.list(accountId), notice: `${updated.healers[classId].level > 1 ? "Level " + updated.healers[classId].level + " " : ""}${classId[0].toUpperCase() + classId.slice(1)} selected.` });
},
updateActiveHealerInventory: (inventory) => {
const { activeSlotId, accountId } = get();
if (!activeSlotId) return;
repository.updateLocal(activeSlotId, (save) => ({
...save,
healers: {
...save.healers,
[save.activeClassId]: { ...save.healers[save.activeClassId], inventory: structuredClone(inventory) },
},
}));
set({ slots: repository.list(accountId) });
},
updateSetting: (key, value) => set((state) => {
const settings = { ...state.settings, [key]: value };
persistSettings(settings);
return { settings, notice: "Settings saved offline." };
}),
touchActiveSave: () => {
const { activeSlotId, accountId } = get();
if (!activeSlotId) return;
repository.touch(activeSlotId);
set({ slots: repository.list(accountId) });
},
recordBossVictory: (bossName) => {
const { activeSlotId, accountId } = get();
if (!activeSlotId) return;
repository.updateLocal(activeSlotId, (save) => {
const bossKills = { ...save.stats.bossKills, [bossName]: (save.stats.bossKills[bossName] ?? 0) + 1 };
return {
...save,
stats: { ...save.stats, totalBossKills: save.stats.totalBossKills + 1, flawlessClears: save.stats.flawlessClears + 1, bossKills },
collections: save.collections.map((boss) => boss.bossName === bossName
? { ...boss, defeated: true, drops: boss.drops.map((drop, index) => index === 0 ? { ...drop, count: drop.count + 1 } : drop) }
: boss),
};
});
set({ slots: repository.list(accountId), notice: `${bossName} clear saved offline.` });
},
clearNotice: () => set({ notice: "" }),
}));
export function useActiveHunter(): HunterSave | null {
return useFrontendStore((state) => activeSave(state.slots, state.activeSlotId));
}