42773b7e-7147-4ad5-828d-da7fcf0290b8

This commit is contained in:
2026-06-23 13:52:13 +00:00
parent da18f32df2
commit f3d2e53e4f
16 changed files with 562 additions and 34 deletions
+116 -17
View File
@@ -42,6 +42,14 @@ import {
EXPEDITION_CONFIG,
} from "@/lib/game/expedition";
import { ACHIEVEMENTS, type Achievement } from "@/lib/game/achievements";
import {
TIDE_CONFIG,
rollTide,
getTideModifiers,
computeSilenceCompensation,
type StarTide,
type TideType,
} from "@/lib/game/starTide";
interface GameActions {
// 生命周期
@@ -53,6 +61,10 @@ interface GameActions {
tick: (now: number) => void;
pulse: () => { gain: number; combo: number } | null;
// 星潮
tickTide: (now: number) => { started?: TideType; ended?: TideType; silenceCompensation?: number } | null;
consumeTideEvents: () => { started?: TideType; ended?: TideType; silenceCompensation?: number }[];
// 解码
startDecode: (crystalId: string) => void;
clickNode: (nodeId: number) => { ok: boolean; finished: boolean; solvable?: boolean; failReason?: string };
@@ -92,6 +104,7 @@ type Store = GameState & GameActions & {
_combo: number;
_lastPulse: number;
_achievementQueue: Achievement[];
_tideEvents: { started?: TideType; ended?: TideType; silenceCompensation?: number }[];
};
/** 计算并写回产能字段 */
@@ -129,6 +142,7 @@ export const useGameStore = create<Store>()(
_combo: 0,
_lastPulse: 0,
_achievementQueue: [],
_tideEvents: [],
init: () => {
const s = get();
@@ -148,8 +162,13 @@ export const useGameStore = create<Store>()(
pendingCrystals: [...s.pendingCrystals, crystal].slice(-CRYSTAL_SPAWN.maxPending),
});
}
// 兼容旧存档:补全 achievements 字段
// 兼容旧存档:补全 achievements / 星潮 字段
const achievements = s.achievements ?? {};
const activeTide = s.activeTide ?? null;
// lastTideEnd:0 表示从未触发过星潮(首次游玩),保留 0 让 firstDelay 生效
const lastTideEndRaw = s.lastTideEnd ?? 0;
// 若旧存档有已过期的星潮,清掉
const tide = activeTide && activeTide.endsAt > now ? activeTide : null;
// 首次进入:补发离线收益
const elapsed = Math.max(0, (now - s.lastTick) / 1000);
if (elapsed > 5) {
@@ -161,20 +180,81 @@ export const useGameStore = create<Store>()(
lastTick: now,
activePuzzle,
achievements,
...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements }),
activeTide: tide,
lastTideEnd: tide ? lastTideEndRaw : lastTideEndRaw,
...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements, activeTide: tide }),
});
} else {
set({ lastTick: now, activePuzzle, achievements, ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements }) });
set({ lastTick: now, activePuzzle, achievements, activeTide: tide, lastTideEnd: tide ? lastTideEndRaw : lastTideEndRaw, ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements, activeTide: tide }) });
}
},
loadOnline: () => {
const s = get();
set({ ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements }) });
set({ ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide }) });
},
hardReset: () => {
set({ ...createInitialState(), _lastAutoDecode: Date.now(), _lastSpawn: Date.now(), _combo: 0, _lastPulse: 0, _achievementQueue: [] });
set({ ...createInitialState(), _lastAutoDecode: Date.now(), _lastSpawn: Date.now(), _combo: 0, _lastPulse: 0, _achievementQueue: [], _tideEvents: [] });
},
tickTide: (now) => {
const s = get();
const tide = s.activeTide;
// 1) 检查当前星潮是否结束
if (tide && now >= tide.endsAt) {
const endedType = tide.type;
// 寂静期补偿洞见
let silenceCompensation = 0;
if (tide.type === "silence") {
silenceCompensation = computeSilenceCompensation(tide);
}
const newInsights = s.insights + silenceCompensation;
const event: { started?: TideType; ended?: TideType; silenceCompensation?: number } = { ended: endedType };
if (silenceCompensation > 0) event.silenceCompensation = silenceCompensation;
set({
activeTide: null,
lastTideEnd: now,
insights: newInsights,
// 星潮结束后重算 stats(移除 contactRate/insight 修饰)
...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: null }),
_tideEvents: [...s._tideEvents, event],
});
return event;
}
// 2) 检查是否该触发新星潮(间隙已过)
if (!tide) {
const since = now - s.lastTideEnd;
// 首次:以 createdAt 起算 firstDelay;之后以 lastTideEnd 起算 gap
const firstStart = s.lastTideEnd === 0 || s.lastTideEnd <= s.createdAt + 1000;
const need = firstStart ? TIDE_CONFIG.firstDelay : TIDE_CONFIG.gap;
if (since >= need) {
const type = rollTide();
const newTide: StarTide = {
id: `tide_${now}_${Math.random().toString(36).slice(2, 7)}`,
type,
startedAt: now,
endsAt: now + TIDE_CONFIG.duration,
};
const event: { started?: TideType; ended?: TideType; silenceCompensation?: number } = { started: type };
set({
activeTide: newTide,
// 星潮开始后重算 stats(应用 contactRate/insight 修饰)
...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: newTide }),
_tideEvents: [...s._tideEvents, event],
});
return event;
}
}
return null;
},
consumeTideEvents: () => {
const s = get();
if (s._tideEvents.length === 0) return [];
const items = s._tideEvents;
set({ _tideEvents: [] });
return items;
},
tick: (now) => {
@@ -182,11 +262,14 @@ export const useGameStore = create<Store>()(
const dt = Math.max(0, (now - s.lastTick) / 1000);
if (dt <= 0) return;
// 星潮产能修饰(即时乘)
const tideMod = getTideModifiers(s.activeTide);
const effCps = s.crystalsPerSec * tideMod.crystalsPerSecMult;
// 产能累加(仅闲置产能受仓库上限;探险奖励可超出)
const newCrystals =
s.crystals >= s.crystalCap
? s.crystals // 已达/超上限,不再自动产出
: Math.min(s.crystalCap, s.crystals + s.crystalsPerSec * dt);
: Math.min(s.crystalCap, s.crystals + effCps * dt);
// 自动产出待解码晶体(每 baseInterval 秒一颗,受技术/飞升略提速)
const bpBoost = 1 + s.blueprints.length * 0.03;
@@ -238,7 +321,9 @@ export const useGameStore = create<Store>()(
combo = Math.min(10, s._combo + 1);
}
const mult = 1 + (combo - 1) * 0.15;
const gain = s.pulsePower * mult;
// 星潮脉冲威力修饰
const tideMod = getTideModifiers(s.activeTide);
const gain = s.pulsePower * mult * tideMod.pulsePowerMult;
set({
crystals: Math.min(s.crystalCap, s.crystals + gain),
_combo: combo,
@@ -266,8 +351,14 @@ export const useGameStore = create<Store>()(
const res = tryClickNode(puzzle, nodeId);
if (res.ok) {
if (res.finished) {
// 结算奖励
const rewards = decodeRewards(puzzle.tier, s.insightMult, s.contactRateMult);
// 结算奖励(星潮解码奖励修饰)
const tideMod = getTideModifiers(s.activeTide);
const base = decodeRewards(puzzle.tier, s.insightMult, s.contactRateMult);
const rewards = {
crystals: Math.round(base.crystals * tideMod.decodeRewardMult),
insights: Math.round(base.insights * tideMod.decodeRewardMult),
contact: +(base.contact * tideMod.decodeRewardMult).toFixed(2),
};
const newTotal = s.totalDecoded + 1;
const newContact = Math.min(100, s.contact + rewards.contact);
const newInsights = s.insights + rewards.insights;
@@ -336,7 +427,13 @@ export const useGameStore = create<Store>()(
const idx = s.pendingCrystals.findIndex((c) => c.tier === 1);
if (idx < 0) return;
const crystal = s.pendingCrystals[idx];
const rewards = decodeRewards(1, s.insightMult, s.contactRateMult);
const tideMod = getTideModifiers(s.activeTide);
const base = decodeRewards(1, s.insightMult, s.contactRateMult);
const rewards = {
crystals: Math.round(base.crystals * tideMod.decodeRewardMult),
insights: Math.round(base.insights * tideMod.decodeRewardMult),
contact: +(base.contact * tideMod.decodeRewardMult).toFixed(2),
};
const newTotal = s.totalDecoded + 1;
const tentative: GameState = { ...s, totalDecoded: newTotal, fragments: { ...s.fragments } };
checkFragments(tentative);
@@ -362,7 +459,7 @@ export const useGameStore = create<Store>()(
set({
insights: s.insights - node.cost,
tech: newTech,
...syncStats({ tech: newTech, blueprints: s.blueprints }),
...syncStats({ tech: newTech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide }),
});
return true;
},
@@ -376,8 +473,9 @@ export const useGameStore = create<Store>()(
if (s.energy < EXPEDITION_CONFIG.energyCost) {
return { ok: false, reason: "能量不足" };
}
const power = computeExpeditionPower(s);
const hp = computeExpeditionHp(s);
const tideMod = getTideModifiers(s.activeTide);
const power = computeExpeditionPower(s) + tideMod.expeditionPowerBonus;
const hp = computeExpeditionHp(s) + tideMod.expeditionHpBonus;
const seed = Math.floor(Math.random() * 1e9);
const expedition = generateExpedition(seed, power, hp);
set({
@@ -476,11 +574,12 @@ export const useGameStore = create<Store>()(
const next = performPrestige(s);
set({
...next,
...syncStats({ tech: next.tech, blueprints: next.blueprints, achievements: next.achievements }),
...syncStats({ tech: next.tech, blueprints: next.blueprints, achievements: next.achievements, activeTide: next.activeTide }),
_lastAutoDecode: Date.now(),
_lastSpawn: Date.now(),
_combo: 0,
_lastPulse: 0,
_tideEvents: [],
});
return { newBp };
},
@@ -512,7 +611,7 @@ export const useGameStore = create<Store>()(
insights,
contact,
...(statsDirty
? syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: updated })
? syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: updated, activeTide: s.activeTide })
: {}),
_achievementQueue: [...s._achievementQueue, ...newlyUnlocked],
});
@@ -537,8 +636,8 @@ export const useGameStore = create<Store>()(
storage: createJSONStorage(() => localStorage),
// 不持久化临时字段
partialize: (s) => {
const { _lastAutoDecode, _lastSpawn, _combo, _lastPulse, _achievementQueue, ...rest } = s;
void _lastAutoDecode; void _lastSpawn; void _combo; void _lastPulse; void _achievementQueue;
const { _lastAutoDecode, _lastSpawn, _combo, _lastPulse, _achievementQueue, _tideEvents, ...rest } = s;
void _lastAutoDecode; void _lastSpawn; void _combo; void _lastPulse; void _achievementQueue; void _tideEvents;
return rest as GameState;
},
}