Update 3D game 2026-07-10 21:20
This commit is contained in:
@@ -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));
|
||||
}
|
||||
Reference in New Issue
Block a user