3d8566bc-063f-41e8-8934-68c9ac5b8211

This commit is contained in:
2026-06-23 13:38:23 +00:00
parent 674775be0e
commit da18f32df2
36 changed files with 849 additions and 34 deletions
+59 -7
View File
@@ -41,6 +41,7 @@ import {
computeEnergyRegen,
EXPEDITION_CONFIG,
} from "@/lib/game/expedition";
import { ACHIEVEMENTS, type Achievement } from "@/lib/game/achievements";
interface GameActions {
// 生命周期
@@ -73,6 +74,10 @@ interface GameActions {
// 飞升
doPrestige: () => { newBp: number } | null;
// 成就
checkAchievements: () => Achievement[];
consumeAchievementQueue: () => Achievement[];
// 设置
toggleTheme: () => void;
toggleSound: () => void;
@@ -86,6 +91,7 @@ type Store = GameState & GameActions & {
_lastSpawn: number;
_combo: number;
_lastPulse: number;
_achievementQueue: Achievement[];
};
/** 计算并写回产能字段 */
@@ -122,6 +128,7 @@ export const useGameStore = create<Store>()(
_lastSpawn: Date.now(),
_combo: 0,
_lastPulse: 0,
_achievementQueue: [],
init: () => {
const s = get();
@@ -141,6 +148,8 @@ export const useGameStore = create<Store>()(
pendingCrystals: [...s.pendingCrystals, crystal].slice(-CRYSTAL_SPAWN.maxPending),
});
}
// 兼容旧存档:补全 achievements 字段
const achievements = s.achievements ?? {};
// 首次进入:补发离线收益
const elapsed = Math.max(0, (now - s.lastTick) / 1000);
if (elapsed > 5) {
@@ -151,20 +160,21 @@ export const useGameStore = create<Store>()(
crystals: Math.min(s.crystalCap, s.crystals + gain),
lastTick: now,
activePuzzle,
...syncStats({ tech: s.tech, blueprints: s.blueprints }),
achievements,
...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements }),
});
} else {
set({ lastTick: now, activePuzzle, ...syncStats({ tech: s.tech, blueprints: s.blueprints }) });
set({ lastTick: now, activePuzzle, achievements, ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements }) });
}
},
loadOnline: () => {
const s = get();
set({ ...syncStats({ tech: s.tech, blueprints: s.blueprints }) });
set({ ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements }) });
},
hardReset: () => {
set({ ...createInitialState(), _lastAutoDecode: Date.now(), _lastSpawn: Date.now(), _combo: 0, _lastPulse: 0 });
set({ ...createInitialState(), _lastAutoDecode: Date.now(), _lastSpawn: Date.now(), _combo: 0, _lastPulse: 0, _achievementQueue: [] });
},
tick: (now) => {
@@ -466,7 +476,7 @@ export const useGameStore = create<Store>()(
const next = performPrestige(s);
set({
...next,
...syncStats({ tech: next.tech, blueprints: next.blueprints }),
...syncStats({ tech: next.tech, blueprints: next.blueprints, achievements: next.achievements }),
_lastAutoDecode: Date.now(),
_lastSpawn: Date.now(),
_combo: 0,
@@ -475,6 +485,48 @@ export const useGameStore = create<Store>()(
return { newBp };
},
checkAchievements: () => {
const s = get();
const newlyUnlocked: Achievement[] = [];
const updated = { ...s.achievements };
let crystals = s.crystals;
let insights = s.insights;
let contact = s.contact;
let statsDirty = false;
for (const a of ACHIEVEMENTS) {
if (updated[a.id]) continue;
if (a.check(s)) {
updated[a.id] = true;
newlyUnlocked.push(a);
// 发放即时奖励
if (a.reward.crystals) crystals += a.reward.crystals;
if (a.reward.insights) insights += a.reward.insights;
if (a.reward.contact) contact = Math.min(100, contact + a.reward.contact);
if (a.reward.crystalsPerSecPct || a.reward.insightPct) statsDirty = true;
}
}
if (newlyUnlocked.length === 0) return [];
set({
achievements: updated,
crystals,
insights,
contact,
...(statsDirty
? syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: updated })
: {}),
_achievementQueue: [...s._achievementQueue, ...newlyUnlocked],
});
return newlyUnlocked;
},
consumeAchievementQueue: () => {
const s = get();
if (s._achievementQueue.length === 0) return [];
const items = s._achievementQueue;
set({ _achievementQueue: [] });
return items;
},
canPrestige: () => get().contact >= CONTACT.prestigeMin,
toggleTheme: () => set({ theme: get().theme === "dark" ? "light" : "dark" }),
@@ -485,8 +537,8 @@ export const useGameStore = create<Store>()(
storage: createJSONStorage(() => localStorage),
// 不持久化临时字段
partialize: (s) => {
const { _lastAutoDecode, _lastSpawn, _combo, _lastPulse, ...rest } = s;
void _lastAutoDecode; void _lastSpawn; void _combo; void _lastPulse;
const { _lastAutoDecode, _lastSpawn, _combo, _lastPulse, _achievementQueue, ...rest } = s;
void _lastAutoDecode; void _lastSpawn; void _combo; void _lastPulse; void _achievementQueue;
return rest as GameState;
},
}