-
▶
-
{goal}
+
+
+ ▶
+
+
+ {goal}
+
|
产能 {formatNum(crystalsPerSec)}/s
@@ -234,6 +274,7 @@ export default function Page() {
+
);
}
diff --git a/src/components/game/StarTide.tsx b/src/components/game/StarTide.tsx
new file mode 100644
index 000000000..a5e3d4fdc
--- /dev/null
+++ b/src/components/game/StarTide.tsx
@@ -0,0 +1,130 @@
+"use client";
+// 回响星核 / Echo Nexus — 星潮事件 UI(横幅 + 通知 + 背景叠层)
+import { useEffect, useState } from "react";
+import { useGameStore } from "@/store/gameStore";
+import { useToast } from "@/hooks/use-toast";
+import { sfx } from "@/hooks/useAudio";
+import { TIDE_EVENTS, TIDE_CONFIG } from "@/lib/game/starTide";
+
+/** 星潮事件通知消费者:监听 _tideEvents 队列,弹 Toast + 播音效 */
+export function StarTideNotifier() {
+ const events = useGameStore((s) => s._tideEvents);
+ const consume = useGameStore((s) => s.consumeTideEvents);
+ const { toast } = useToast();
+
+ useEffect(() => {
+ if (events.length === 0) return;
+ const items = consume();
+ for (const ev of items) {
+ if (ev.started) {
+ const meta = TIDE_EVENTS[ev.started];
+ sfx("tideStart");
+ toast({
+ title: `${meta.icon} 星潮降临:${meta.name}`,
+ description: meta.desc,
+ });
+ }
+ if (ev.ended) {
+ const meta = TIDE_EVENTS[ev.ended];
+ if (ev.silenceCompensation && ev.silenceCompensation > 0) {
+ sfx("techBuy");
+ toast({
+ title: `${meta.icon} 寂静期消散`,
+ description: `虚空回赠 ${ev.silenceCompensation} 洞见。`,
+ });
+ } else {
+ sfx("tideEnd");
+ toast({
+ title: `${meta.icon} ${meta.name}结束`,
+ description: "星潮退去,物理法则恢复。",
+ });
+ }
+ }
+ }
+ }, [events, consume, toast]);
+
+ return null;
+}
+
+/** 顶部星潮指示器(活跃时显示倒计时芯片) */
+export function StarTideIndicator() {
+ const activeTide = useGameStore((s) => s.activeTide);
+ const [, force] = useState(0);
+
+ // 每秒刷新倒计时
+ useEffect(() => {
+ if (!activeTide) return;
+ const id = setInterval(() => force((n) => n + 1), 500);
+ return () => clearInterval(id);
+ }, [activeTide]);
+
+ if (!activeTide) return null;
+ const meta = TIDE_EVENTS[activeTide.type];
+ const remaining = Math.max(0, activeTide.endsAt - Date.now());
+ const secs = Math.ceil(remaining / 1000);
+ const progress = 1 - remaining / TIDE_CONFIG.duration;
+
+ return (
+
+ {/* 进度环背景 */}
+
+
{meta.icon}
+
{meta.name}
+
{secs}s
+
+ );
+}
+
+/** 全屏背景叠层:活跃星潮时给页面罩一层对应色的微光 */
+export function StarTideOverlay() {
+ const activeTide = useGameStore((s) => s.activeTide);
+ if (!activeTide) return null;
+ const meta = TIDE_EVENTS[activeTide.type];
+
+ return (
+
+ {/* 顶部色带 */}
+
+ {/* 底部色带 */}
+
+ {/* 边缘呼吸光 */}
+
+
+
+ );
+}
diff --git a/src/hooks/useGameLoop.ts b/src/hooks/useGameLoop.ts
index 8fc185845..3fad177f3 100644
--- a/src/hooks/useGameLoop.ts
+++ b/src/hooks/useGameLoop.ts
@@ -3,10 +3,11 @@
import { useEffect, useRef } from "react";
import { useGameStore } from "@/store/gameStore";
-/** 250ms tick:产能 + 自动产晶体 + 自动解码 + 成就检测 */
+/** 250ms tick:产能 + 自动产晶体 + 自动解码 + 星潮 + 成就检测 */
export function useGameLoop() {
const tick = useGameStore((s) => s.tick);
const autoDecodeTick = useGameStore((s) => s.autoDecodeTick);
+ const tickTide = useGameStore((s) => s.tickTide);
const checkAchievements = useGameStore((s) => s.checkAchievements);
const init = useGameStore((s) => s.init);
const inited = useRef(false);
@@ -16,30 +17,33 @@ export function useGameLoop() {
init();
inited.current = true;
}
- let achCounter = 0;
+ let counter = 0;
const id = setInterval(() => {
const now = Date.now();
tick(now);
autoDecodeTick();
+ // 星潮检测每 tick 都查(结束/触发判定需及时)
+ tickTide(now);
+ counter++;
// 成就检测每秒执行一次(降低开销)
- achCounter++;
- if (achCounter % 4 === 0) {
+ if (counter % 4 === 0) {
checkAchievements();
}
}, 250);
return () => clearInterval(id);
- }, [tick, autoDecodeTick, checkAchievements, init]);
+ }, [tick, autoDecodeTick, tickTide, checkAchievements, init]);
- // 页面可见性:切回时补 tick + 立即检测成就
+ // 页面可见性:切回时补 tick + 星潮 + 成就
useEffect(() => {
const onVis = () => {
if (document.visibilityState === "visible") {
tick(Date.now());
+ tickTide(Date.now());
checkAchievements();
}
};
document.addEventListener("visibilitychange", onVis);
return () => document.removeEventListener("visibilitychange", onVis);
- }, [tick, checkAchievements]);
+ }, [tick, tickTide, checkAchievements]);
}
diff --git a/src/lib/game/audio.ts b/src/lib/game/audio.ts
index 683e75749..1ce92a5e9 100644
--- a/src/lib/game/audio.ts
+++ b/src/lib/game/audio.ts
@@ -17,6 +17,8 @@ type SfxName =
| "expeditionDefeat" // 探险失败(下行低音)
| "prestige" // 飞升(宏大扫频)
| "achievement" // 成就解锁(亮丽琶音)
+ | "tideStart" // 星潮降临(神秘扫频)
+ | "tideEnd" // 星潮结束(柔和消退)
| "uiHover" // 界面悬停(极轻)
| "uiClick"; // 界面点击(轻确认)
@@ -249,6 +251,21 @@ class AudioEngine {
this.tone(1318.51, 0.4, "triangle", 0.18, 0.24);
break;
}
+ case "tideStart": {
+ // 神秘扫频 + 泛音列
+ this.sweep(330, 660, 0.8, "sine", 0.16);
+ this.sweep(440, 880, 0.8, "triangle", 0.08, 0.04);
+ this.tone(523.25, 0.5, "sine", 0.1, 0.2);
+ this.tone(783.99, 0.6, "sine", 0.08, 0.3);
+ break;
+ }
+ case "tideEnd": {
+ // 柔和下行消退
+ this.tone(659.25, 0.3, "sine", 0.14, 0);
+ this.tone(523.25, 0.35, "sine", 0.12, 0.1);
+ this.tone(392, 0.5, "triangle", 0.1, 0.22);
+ break;
+ }
case "uiHover": {
this.tone(880, 0.05, "sine", 0.05);
break;
diff --git a/src/lib/game/config.ts b/src/lib/game/config.ts
index 9a126066d..a460e4658 100644
--- a/src/lib/game/config.ts
+++ b/src/lib/game/config.ts
@@ -30,6 +30,8 @@ export const INITIAL_STATE = {
lastEnergyTick: Date.now(),
totalExpeditions: 0,
achievements: {},
+ activeTide: null,
+ lastTideEnd: 0,
theme: "dark" as const,
soundOn: true,
};
diff --git a/src/lib/game/engine.ts b/src/lib/game/engine.ts
index f6b5e261b..5c3aa9f9e 100644
--- a/src/lib/game/engine.ts
+++ b/src/lib/game/engine.ts
@@ -8,8 +8,9 @@ import {
CONTACT,
} from "./config";
import { achievementBonuses } from "./achievements";
+import { getTideModifiers, type StarTide } from "./starTide";
-/** 由技术树 + 飞升蓝图 + 成就聚合计算产能字段 */
+/** 由技术树 + 飞升蓝图 + 成就 + 星潮聚合计算产能字段 */
export function recomputeStats(state: Partial
): {
crystalsPerSec: number;
crystalCap: number;
@@ -23,6 +24,7 @@ export function recomputeStats(state: Partial): {
const tech = state.tech ?? {};
const bp = state.blueprints?.length ?? 0;
const ach = achievementBonuses(state.achievements ?? {});
+ const tideMod = getTideModifiers((state.activeTide as StarTide | null) ?? null);
let crystalsPerSec = INITIAL_STATE.crystalsPerSec;
let crystalCap = INITIAL_STATE.crystalCap;
@@ -74,6 +76,10 @@ export function recomputeStats(state: Partial): {
crystalsPerSec *= 1 + ach.crystalsPerSecPct / 100;
insightMult *= 1 + ach.insightPct / 100;
+ // 星潮瞬时修饰(contactRateMult 与 insightMult 进缓存;产能/脉冲/探险在用点处即时乘)
+ insightMult += tideMod.insightMultAdd;
+ contactRateMult *= tideMod.contactRateMult;
+
return {
crystalsPerSec,
crystalCap,
@@ -111,7 +117,7 @@ export function performPrestige(state: GameState): GameState {
].slice(0, PRESTIGE.maxBlueprints);
// 保留:fragments, totalDecoded(累计), ascensions+1, blueprints, achievements, theme/sound, expeditionLog
- // 重置:资源、技术、产能、pendingCrystals、activePuzzle、activeExpedition、contact、lastTick、energy
+ // 重置:资源、技术、产能、pendingCrystals、activePuzzle、activeExpedition、contact、lastTick、energy、星潮
const fresh = createInitialState();
const stats = recomputeStats({ tech: {}, blueprints, achievements: state.achievements });
return {
@@ -128,6 +134,9 @@ export function performPrestige(state: GameState): GameState {
lastEnergyTick: Date.now(),
expeditionLog: state.expeditionLog,
totalExpeditions: state.totalExpeditions,
+ // 星潮:飞升后清空,lastTideEnd 设为现在,使首次星潮在 firstDelay 后触发
+ activeTide: null,
+ lastTideEnd: Date.now(),
...stats,
};
}
@@ -139,6 +148,8 @@ export function createInitialState(): GameState {
tech: {},
fragments: {},
achievements: {},
+ activeTide: null,
+ lastTideEnd: Date.now(),
pendingCrystals: [],
activePuzzle: null,
activeExpedition: null,
diff --git a/src/lib/game/starTide.ts b/src/lib/game/starTide.ts
new file mode 100644
index 000000000..a9e735a25
--- /dev/null
+++ b/src/lib/game/starTide.ts
@@ -0,0 +1,209 @@
+// 回响星核 / Echo Nexus — 星潮事件系统(周期性全局玩法修饰)
+//
+// 深空中偶发的「星潮」会短暂改变物理法则,给放置循环注入动态变化。
+// 每 ~40s 间隙后触发一次持续 ~75s 的星潮,6 种类型各有正负效果。
+
+export type TideType =
+ | "crystal" // 晶体潮:产能 ×2
+ | "resonance" // 谐振风暴:解码奖励 +60%
+ | "ruins" // 遗迹共振:探险力 +5、生命 +30
+ | "void" // 虚空低语:洞见 ×2
+ | "core" // 星核悸动:接触率 ×3、脉冲 ×2
+ | "silence"; // 寂静期:产能 ×0.5,但结束时补偿洞见
+
+export interface StarTide {
+ type: TideType;
+ startedAt: number;
+ endsAt: number;
+ /** 唯一 id,用于 React key */
+ id: string;
+}
+
+export interface TideModifiers {
+ crystalsPerSecMult: number;
+ decodeRewardMult: number;
+ insightMultAdd: number; // 加到 insightMult(百分比,0.5 = +50%)
+ contactRateMult: number;
+ pulsePowerMult: number;
+ expeditionPowerBonus: number;
+ expeditionHpBonus: number;
+ /** 寂静期结束时补偿的洞见(按持续秒数) */
+ silenceInsightPerSec?: number;
+}
+
+export const TIDE_CONFIG = {
+ /** 星潮持续时间(毫秒) */
+ duration: 75000,
+ /** 星潮之间的间隙(毫秒) */
+ gap: 40000,
+ /** 首次星潮触发延迟(毫秒,从 init 起) */
+ firstDelay: 30000,
+};
+
+export const TIDE_EVENTS: Record<
+ TideType,
+ {
+ name: string;
+ desc: string;
+ color: string;
+ glow: string;
+ icon: string;
+ /** 是否负面事件(影响 UI 提示语气) */
+ negative: boolean;
+ }
+> = {
+ crystal: {
+ name: "晶体潮",
+ desc: "深空晶体涌动,采矿产能翻倍",
+ color: "#34d399",
+ glow: "rgba(52,211,153,0.5)",
+ icon: "✺",
+ negative: false,
+ },
+ resonance: {
+ name: "谐振风暴",
+ desc: "共振序列被放大,解码奖励 +60%",
+ color: "#fb7185",
+ glow: "rgba(251,113,133,0.5)",
+ icon: "❖",
+ negative: false,
+ },
+ ruins: {
+ name: "遗迹共振",
+ desc: "远古遗迹苏醒,探险力 +5、生命 +30",
+ color: "#fbbf24",
+ glow: "rgba(251,191,36,0.5)",
+ icon: "⬢",
+ negative: false,
+ },
+ void: {
+ name: "虚空低语",
+ desc: "虚空传来回响,洞见获取翻倍",
+ color: "#e879f9",
+ glow: "rgba(232,121,249,0.5)",
+ icon: "✷",
+ negative: false,
+ },
+ core: {
+ name: "星核悸动",
+ desc: "星核震荡,接触率 ×3、脉冲威力 ×2",
+ color: "#a5f3fc",
+ glow: "rgba(165,243,252,0.5)",
+ icon: "✦",
+ negative: false,
+ },
+ silence: {
+ name: "寂静期",
+ desc: "虚空沉寂,产能减半;结束时按时长补偿洞见",
+ color: "#94a3b8",
+ glow: "rgba(148,163,184,0.5)",
+ icon: "◌",
+ negative: true,
+ },
+};
+
+/** 权重表(寂静期概率略低) */
+const TIDE_WEIGHTS: Record = {
+ crystal: 22,
+ resonance: 20,
+ ruins: 16,
+ void: 18,
+ core: 14,
+ silence: 10,
+};
+
+export function rollTide(rng: () => number = Math.random): TideType {
+ const total = Object.values(TIDE_WEIGHTS).reduce((a, b) => a + b, 0);
+ let r = rng() * total;
+ for (const k of Object.keys(TIDE_WEIGHTS) as TideType[]) {
+ r -= TIDE_WEIGHTS[k];
+ if (r <= 0) return k;
+ }
+ return "crystal";
+}
+
+/** 计算当前星潮的修饰器 */
+export function getTideModifiers(tide: StarTide | null): TideModifiers {
+ if (!tide) {
+ return {
+ crystalsPerSecMult: 1,
+ decodeRewardMult: 1,
+ insightMultAdd: 0,
+ contactRateMult: 1,
+ pulsePowerMult: 1,
+ expeditionPowerBonus: 0,
+ expeditionHpBonus: 0,
+ };
+ }
+ switch (tide.type) {
+ case "crystal":
+ return {
+ crystalsPerSecMult: 2,
+ decodeRewardMult: 1,
+ insightMultAdd: 0,
+ contactRateMult: 1,
+ pulsePowerMult: 1,
+ expeditionPowerBonus: 0,
+ expeditionHpBonus: 0,
+ };
+ case "resonance":
+ return {
+ crystalsPerSecMult: 1,
+ decodeRewardMult: 1.6,
+ insightMultAdd: 0,
+ contactRateMult: 1,
+ pulsePowerMult: 1,
+ expeditionPowerBonus: 0,
+ expeditionHpBonus: 0,
+ };
+ case "ruins":
+ return {
+ crystalsPerSecMult: 1,
+ decodeRewardMult: 1,
+ insightMultAdd: 0,
+ contactRateMult: 1,
+ pulsePowerMult: 1,
+ expeditionPowerBonus: 5,
+ expeditionHpBonus: 30,
+ };
+ case "void":
+ return {
+ crystalsPerSecMult: 1,
+ decodeRewardMult: 1,
+ insightMultAdd: 1, // insightMult 翻倍(+100%)
+ contactRateMult: 1,
+ pulsePowerMult: 1,
+ expeditionPowerBonus: 0,
+ expeditionHpBonus: 0,
+ };
+ case "core":
+ return {
+ crystalsPerSecMult: 1,
+ decodeRewardMult: 1,
+ insightMultAdd: 0,
+ contactRateMult: 3,
+ pulsePowerMult: 2,
+ expeditionPowerBonus: 0,
+ expeditionHpBonus: 0,
+ };
+ case "silence":
+ return {
+ crystalsPerSecMult: 0.5,
+ decodeRewardMult: 1,
+ insightMultAdd: 0,
+ contactRateMult: 1,
+ pulsePowerMult: 1,
+ expeditionPowerBonus: 0,
+ expeditionHpBonus: 0,
+ silenceInsightPerSec: 0.4,
+ };
+ }
+}
+
+/** 寂静期结束时的洞见补偿 */
+export function computeSilenceCompensation(tide: StarTide): number {
+ const mod = getTideModifiers(tide);
+ if (!mod.silenceInsightPerSec) return 0;
+ const secs = (tide.endsAt - tide.startedAt) / 1000;
+ return Math.round(mod.silenceInsightPerSec * secs);
+}
diff --git a/src/lib/game/types.ts b/src/lib/game/types.ts
index 38284bf38..db31c7331 100644
--- a/src/lib/game/types.ts
+++ b/src/lib/game/types.ts
@@ -128,6 +128,10 @@ export interface GameState {
// 成就
achievements: Record; // achievementId -> unlocked
+ // 星潮事件
+ activeTide: import("./starTide").StarTide | null;
+ lastTideEnd: number;
+
// 元
lastTick: number;
createdAt: number;
diff --git a/src/store/gameStore.ts b/src/store/gameStore.ts
index 1ddd08e28..a7acae38f 100644
--- a/src/store/gameStore.ts
+++ b/src/store/gameStore.ts
@@ -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()(
_combo: 0,
_lastPulse: 0,
_achievementQueue: [],
+ _tideEvents: [],
init: () => {
const s = get();
@@ -148,8 +162,13 @@ export const useGameStore = create()(
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()(
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()(
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()(
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()(
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()(
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()(
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()(
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()(
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()(
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()(
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;
},
}
diff --git a/worklog.md b/worklog.md
index d7dc453a9..b480e0e55 100644
--- a/worklog.md
+++ b/worklog.md
@@ -51,16 +51,27 @@
- **验证**:agent-browser 全流程通过;成就 3/14 解锁;lint 零错误;HTTP 200
- 详见 docs/repo/docs/06-音频与成就系统-v0.2.1.md
+### v0.3 星潮事件系统(本轮完成)
+- **动机**:回应 Issue #1「玩法单一」反馈,为放置循环注入动态变化
+- **6 种星潮**(`src/lib/game/starTide.ts`):晶体潮(产能×2)/谐振风暴(解码×1.6)/遗迹共振(探险+5力+30血)/虚空低语(洞见×2)/星核悸动(接触×3脉冲×2)/寂静期(产能×0.5,结束补偿洞见)
+- **触发**:首次 30s,常规间隙 40s,持续 75s,权重抽取,飞升重置
+- **修饰器应用**:产能/脉冲/解码奖励即时乘;洞见/接触率进 recomputeStats 缓存;探险力/血量即时加
+- **UI**(`src/components/game/StarTide.tsx`):顶部指示器芯片(图标+名称+倒计时) + 全屏背景叠层(色带+呼吸光) + Toast通知 + Footer联动
+- **音效**:tideStart(神秘扫频) + tideEnd(柔和消退),程序化合成
+- **QA**:agent-browser 全流程通过(触发/指示器/叠层/寂静期补偿 50→81 洞见);VLM 视觉确认;lint 零错误
+- 详见 docs/repo/docs/07-星潮事件系统-v0.3.md
+
### 进行中
-- [ ] 持续迭代:socket 全局「星潮」事件(v0.3 剩余)、云存档+排行榜(v0.4)、全5纪元叙事(v0.5)
+- [ ] 持续迭代:云存档+排行榜(v0.4)、全5纪元叙事(v0.5)、socket 多人同步星潮(后续)
## 未解决问题或风险 / 下一阶段优先事项
-- Issue #1 玩家反馈「玩法单一」,后续可考虑:更多探险事件变体、限时星潮事件、成就奖励多样化
+- v0.3 星潮为单机版(原计划 socket 全局事件),后续可扩展为多人同步
+- Issue #1 玩家反馈已通过星潮系统部分回应,后续可加更多探险事件变体
- v0.2 探险 BOSS 战胜率较低(基础探索力 10 vs BOSS 难度 5-7,胜率约 25%),需玩家投资探险技术
- 探险能量恢复较慢(45s/点),后续可加技术提升恢复速度
- 需持续关注 Gitea 工单(仓库 Issues)获取额外需求
-- 下一阶段优先:socket 全局「星潮」实时事件、云存档+排行榜
+- 下一阶段优先:云存档+排行榜、全5纪元叙事
## 定时任务
- 已设置:每 15 分钟 webDevReview(自动 QA + 迭代开发,job_id: 227581)