v0.9 工单修复: 4 bug + 4 玩法改进
Gitea Issue #2 修复: Bug: 1. 探险只能一次 → 能量恢复 45→20s, 初始能量 3→5, 上限 5→8 新增 lastExpeditionSummary 显示上次结果 新增 dismissExpedition action 2. 元素偏移/遮挡 → 标签页 grid-cols-8 → 响应式 grid-cols-4/sm:8 3. 成就UI遮挡 → max-h 440→520, min-h 提高 4. 提示框遮挡 → toast top-0→top-14 避开 header, TOAST_LIMIT 1→3 玩法改进: 1. 中央晶体球 → 下方新增 4 指标收益面板(产能/脉冲/连击/仓库) 2. 技术树 12→20 节点 (每分支 3→5 级) 3. 成就解锁 → 全屏居中特效通知 (framer-motion 弹窗) 4. 叙事融入 → 新建 narrative.ts, 脉冲/技术树注入世界观文本
This commit is contained in:
+128
-2
@@ -45,6 +45,10 @@ import {
|
||||
Radio,
|
||||
Navigation,
|
||||
User,
|
||||
Gem,
|
||||
Zap,
|
||||
Flame,
|
||||
Database,
|
||||
} from "lucide-react";
|
||||
import { TECH_TREE, FRAGMENTS, formatNum } from "@/lib/game/config";
|
||||
import { ACHIEVEMENTS } from "@/lib/game/achievements";
|
||||
@@ -275,6 +279,8 @@ export default function Page() {
|
||||
<div data-tut="crystal-orb" className="contents">
|
||||
<CrystalOrb />
|
||||
</div>
|
||||
{/* v0.9 工单 #2:全息收益信息面板 — 让晶体球价值可视化 */}
|
||||
<CrystalYieldPanel />
|
||||
</section>
|
||||
|
||||
{/* 右侧:解码 + 标签面板 */}
|
||||
@@ -284,9 +290,11 @@ export default function Page() {
|
||||
<DecodePanel />
|
||||
</div>
|
||||
{/* 标签面板:探险 / 技术 / 星图 / 图谱 / 成就 / 信标 / 统计 / 角色 */}
|
||||
<div className="glass rounded-2xl p-3 min-h-[280px] sm:min-h-[320px] max-h-[440px] flex flex-col">
|
||||
{/* v0.9 工单修复:移除 max-h 硬限制,改用 min-h + flex 自适应,避免成就/技术树内容被挤压遮挡 */}
|
||||
<div className="glass rounded-2xl p-3 min-h-[300px] sm:min-h-[360px] max-h-[520px] flex flex-col">
|
||||
<Tabs defaultValue={hasActiveExpedition ? "expedition" : hasPendingPerk ? "constellation" : "tech"} className="h-full flex flex-col">
|
||||
<TabsList className="grid grid-cols-8 h-9 bg-black/30 gap-0.5 p-1">
|
||||
{/* v0.9 工单修复:标签页响应式布局,小屏 4 列 2 行避免拥挤遮挡 */}
|
||||
<TabsList className="grid grid-cols-4 sm:grid-cols-8 h-auto sm:h-9 bg-black/30 gap-0.5 p-1">
|
||||
<TabsTrigger value="expedition" data-tut="tab-expedition" className="text-[11px] gap-1 relative px-0.5 flex-col h-7 data-[state=active]:bg-amber-500/15 data-[state=active]:shadow-[0_0_12px_rgba(251,191,36,0.3)] transition-all">
|
||||
<Rocket className="h-3.5 w-3.5" />
|
||||
<span className="leading-none">探险</span>
|
||||
@@ -428,6 +436,124 @@ export default function Page() {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* v0.9 工单 #2:全息收益信息面板
|
||||
* 在中央晶体球下方展示 4 项核心收益指标(产能 / 脉冲 / 连击 / 仓库),
|
||||
* 让玩家明确感知晶体球的价值,缓解"占地方又没用"的反馈。
|
||||
* 严格四色全息配色:emerald / rose / amber / fuchsia(禁止蓝色/靛色)。
|
||||
*/
|
||||
function CrystalYieldPanel() {
|
||||
const crystalsPerSec = useGameStore((s) => s.crystalsPerSec);
|
||||
const pulsePower = useGameStore((s) => s.pulsePower);
|
||||
const combo = useGameStore((s) => s._combo);
|
||||
const crystals = useGameStore((s) => s.crystals);
|
||||
const crystalCap = useGameStore((s) => s.crystalCap);
|
||||
|
||||
// 连击倍率:1 + (combo - 1) * 0.15(仅 combo≥1 时生效)
|
||||
const comboMult = combo >= 1 ? 1 + (combo - 1) * 0.15 : 1;
|
||||
const comboHot = combo >= 3; // 连击≥3 高亮闪烁
|
||||
|
||||
// 仓库容量百分比
|
||||
const fillPct = crystalCap > 0 ? (crystals / crystalCap) * 100 : 0;
|
||||
const warehouseWarn = fillPct >= 90; // ≥90% 警告色 + pulse
|
||||
|
||||
return (
|
||||
<div className="mt-3 grid grid-cols-2 sm:grid-cols-4 gap-2 w-full max-w-md pointer-events-none">
|
||||
{/* 晶体产能 — emerald */}
|
||||
<div className="rounded-lg border border-emerald-400/30 bg-black/30 backdrop-blur p-2 flex items-center gap-1.5">
|
||||
<Gem className="h-3.5 w-3.5 text-emerald-400 shrink-0" />
|
||||
<div className="min-w-0 leading-tight">
|
||||
<div className="text-[9px] uppercase tracking-wider text-emerald-300/70 truncate">
|
||||
晶体产能
|
||||
</div>
|
||||
<div className="text-sm font-mono tabular-nums text-emerald-200 truncate">
|
||||
{formatNum(crystalsPerSec)}
|
||||
<span className="text-[9px] text-emerald-300/60">/s</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 主动脉冲 — rose */}
|
||||
<div className="rounded-lg border border-rose-400/30 bg-black/30 backdrop-blur p-2 flex items-center gap-1.5">
|
||||
<Zap className="h-3.5 w-3.5 text-rose-400 shrink-0" />
|
||||
<div className="min-w-0 leading-tight">
|
||||
<div className="text-[9px] uppercase tracking-wider text-rose-300/70 truncate">
|
||||
主动脉冲
|
||||
</div>
|
||||
<div className="text-sm font-mono tabular-nums text-rose-200 truncate">
|
||||
{formatNum(pulsePower)}
|
||||
{combo >= 1 && (
|
||||
<span className="text-[9px] text-rose-300/80"> ×{comboMult.toFixed(2)}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 当前连击 — amber,≥3 时高亮闪烁 */}
|
||||
<div
|
||||
className={`rounded-lg border bg-black/30 backdrop-blur p-2 flex items-center gap-1.5 ${
|
||||
comboHot ? "border-amber-400/70 animate-pulse" : "border-amber-400/30"
|
||||
}`}
|
||||
>
|
||||
<Flame
|
||||
className={`h-3.5 w-3.5 text-amber-400 shrink-0 ${
|
||||
comboHot ? "drop-shadow-[0_0_4px_rgba(251,191,36,0.7)]" : ""
|
||||
}`}
|
||||
/>
|
||||
<div className="min-w-0 leading-tight">
|
||||
<div className="text-[9px] uppercase tracking-wider text-amber-300/70 truncate">
|
||||
当前连击
|
||||
</div>
|
||||
<div
|
||||
className={`text-sm font-mono tabular-nums truncate ${
|
||||
comboHot ? "text-amber-200" : "text-amber-300/80"
|
||||
}`}
|
||||
>
|
||||
{combo}
|
||||
<span className="text-[9px] text-amber-300/60">/10</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 仓库容量 — fuchsia,≥90% 切 rose 警告色 + pulse */}
|
||||
<div
|
||||
className={`rounded-lg border bg-black/30 backdrop-blur p-2 flex items-center gap-1.5 ${
|
||||
warehouseWarn ? "border-rose-400/70 animate-pulse" : "border-fuchsia-400/30"
|
||||
}`}
|
||||
>
|
||||
<Database
|
||||
className={`h-3.5 w-3.5 shrink-0 ${
|
||||
warehouseWarn ? "text-rose-400" : "text-fuchsia-400"
|
||||
}`}
|
||||
/>
|
||||
<div className="min-w-0 leading-tight">
|
||||
<div
|
||||
className={`text-[9px] uppercase tracking-wider truncate ${
|
||||
warehouseWarn ? "text-rose-300/80" : "text-fuchsia-300/70"
|
||||
}`}
|
||||
>
|
||||
仓库容量
|
||||
</div>
|
||||
<div
|
||||
className={`text-sm font-mono tabular-nums truncate ${
|
||||
warehouseWarn ? "text-rose-200" : "text-fuchsia-200"
|
||||
}`}
|
||||
>
|
||||
{formatNum(crystals)}
|
||||
<span
|
||||
className={`text-[9px] ${
|
||||
warehouseWarn ? "text-rose-300/70" : "text-fuchsia-300/60"
|
||||
}`}
|
||||
>
|
||||
/{formatNum(crystalCap)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatsPanel() {
|
||||
const s = useGameStore();
|
||||
const achCount = Object.values(s.achievements).filter(Boolean).length;
|
||||
|
||||
@@ -1,26 +1,168 @@
|
||||
"use client";
|
||||
// 回响星核 / Echo Nexus — 成就解锁通知消费者
|
||||
import { useEffect } from "react";
|
||||
// 回响星核 / Echo Nexus — 成就解锁通知(v0.9 全屏特效增强)
|
||||
// 工单 Issue #2 反馈"成就系统没看出来有什么用",改为居中全息弹窗 + 奖励数字弹出
|
||||
import { useEffect, useState } from "react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useGameStore } from "@/store/gameStore";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { sfx } from "@/hooks/useAudio";
|
||||
import type { Achievement } from "@/lib/game/achievements";
|
||||
|
||||
interface NotificationItem {
|
||||
achievement: Achievement;
|
||||
id: number;
|
||||
}
|
||||
|
||||
export function AchievementNotifier() {
|
||||
const queue = useGameStore((s) => s._achievementQueue);
|
||||
const consume = useGameStore((s) => s.consumeAchievementQueue);
|
||||
const { toast } = useToast();
|
||||
const [notifications, setNotifications] = useState<NotificationItem[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (queue.length === 0) return;
|
||||
const items = consume();
|
||||
for (const a of items) {
|
||||
sfx("achievement");
|
||||
// v0.9:全屏居中特效通知
|
||||
const id = Date.now() + Math.random();
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setNotifications((prev) => [...prev, { achievement: a, id }]);
|
||||
// 4 秒后自动移除
|
||||
setTimeout(() => {
|
||||
setNotifications((prev) => prev.filter((n) => n.id !== id));
|
||||
}, 4000);
|
||||
// 同时保留底部 toast 作为日志
|
||||
toast({
|
||||
title: `🏆 成就解锁:${a.name}`,
|
||||
title: `成就解锁:${a.name}`,
|
||||
description: `${a.desc} 奖励:${a.rewardText}`,
|
||||
});
|
||||
}
|
||||
}, [queue, consume, toast]);
|
||||
|
||||
return null;
|
||||
if (notifications.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[120] pointer-events-none flex items-center justify-center">
|
||||
<AnimatePresence>
|
||||
{notifications.map((n, idx) => {
|
||||
const a = n.achievement;
|
||||
// 多个同时出现时垂直堆叠偏移
|
||||
const offset = (idx - (notifications.length - 1) / 2) * 140;
|
||||
return (
|
||||
<motion.div
|
||||
key={n.id}
|
||||
initial={{ opacity: 0, scale: 0.4, y: 40, rotateX: -30 }}
|
||||
animate={{ opacity: 1, scale: 1, y: offset, rotateX: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.6, y: -60 }}
|
||||
transition={{ type: "spring", stiffness: 280, damping: 22 }}
|
||||
className="absolute"
|
||||
style={{ transformOrigin: "center center" }}
|
||||
>
|
||||
<div
|
||||
className="relative rounded-2xl border-2 px-8 py-5 backdrop-blur-xl overflow-hidden"
|
||||
style={{
|
||||
borderColor: a.color,
|
||||
background: `linear-gradient(135deg, ${a.color}22, rgba(5,4,16,0.92))`,
|
||||
boxShadow: `0 0 60px ${a.color}66, 0 0 120px ${a.color}33, inset 0 0 30px ${a.color}11`,
|
||||
minWidth: 320,
|
||||
maxWidth: 420,
|
||||
}}
|
||||
>
|
||||
{/* 背景径向辉光 */}
|
||||
<div
|
||||
className="absolute -top-20 -right-20 h-40 w-40 rounded-full blur-3xl opacity-50"
|
||||
style={{ background: a.color }}
|
||||
/>
|
||||
{/* 旋转光线 */}
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-2xl opacity-20"
|
||||
style={{
|
||||
background: `conic-gradient(from 0deg, transparent, ${a.color}, transparent, ${a.color}, transparent)`,
|
||||
}}
|
||||
animate={{ rotate: 360 }}
|
||||
transition={{ duration: 8, repeat: Infinity, ease: "linear" }}
|
||||
/>
|
||||
|
||||
<div className="relative flex items-center gap-4">
|
||||
{/* 图标徽章 */}
|
||||
<motion.div
|
||||
className="shrink-0 h-16 w-16 rounded-2xl flex items-center justify-center text-3xl"
|
||||
style={{
|
||||
background: `${a.color}33`,
|
||||
border: `2px solid ${a.color}`,
|
||||
boxShadow: `0 0 20px ${a.color}88, inset 0 0 12px ${a.color}44`,
|
||||
}}
|
||||
animate={{
|
||||
boxShadow: [
|
||||
`0 0 20px ${a.color}88, inset 0 0 12px ${a.color}44`,
|
||||
`0 0 40px ${a.color}cc, inset 0 0 20px ${a.color}66`,
|
||||
`0 0 20px ${a.color}88, inset 0 0 12px ${a.color}44`,
|
||||
],
|
||||
}}
|
||||
transition={{ duration: 2, repeat: Infinity }}
|
||||
>
|
||||
{a.icon}
|
||||
</motion.div>
|
||||
|
||||
{/* 文本 */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.15 }}
|
||||
className="text-[10px] font-mono uppercase tracking-[0.2em] mb-0.5"
|
||||
style={{ color: a.color }}
|
||||
>
|
||||
✦ Achievement Unlocked
|
||||
</motion.div>
|
||||
<motion.h3
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="text-lg font-bold truncate"
|
||||
style={{ color: a.color, textShadow: `0 0 12px ${a.color}66` }}
|
||||
>
|
||||
{a.name}
|
||||
</motion.h3>
|
||||
<motion.p
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
className="text-[11px] text-muted-foreground/90 truncate"
|
||||
>
|
||||
{a.desc}
|
||||
</motion.p>
|
||||
{/* 奖励条 */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.4 }}
|
||||
className="mt-1.5 inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-[10px] font-mono font-semibold"
|
||||
style={{
|
||||
background: `${a.color}22`,
|
||||
border: `1px solid ${a.color}55`,
|
||||
color: a.color,
|
||||
}}
|
||||
>
|
||||
▸ {a.rewardText}
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 进度条(倒计时) */}
|
||||
<motion.div
|
||||
className="absolute bottom-0 left-0 h-0.5"
|
||||
style={{ background: a.color }}
|
||||
initial={{ width: "100%" }}
|
||||
animate={{ width: "0%" }}
|
||||
transition={{ duration: 4, ease: "linear" }}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useGameStore } from "@/store/gameStore";
|
||||
import { formatNum } from "@/lib/game/config";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { sfx } from "@/hooks/useAudio";
|
||||
import { PULSE_WHISPERS, maybeWhisper } from "@/lib/game/narrative";
|
||||
|
||||
interface FloatNum {
|
||||
id: number;
|
||||
@@ -242,10 +243,18 @@ export function CrystalOrb() {
|
||||
setFloats((f) => f.filter((it) => it.id !== id));
|
||||
}, 900);
|
||||
if (res.combo >= 5 && res.combo % 5 === 0) {
|
||||
// v0.9 叙事融入:高连击时注入世界观低语
|
||||
const whisper = maybeWhisper(PULSE_WHISPERS, 0.6);
|
||||
toast({
|
||||
title: `×${res.combo} 连击!`,
|
||||
description: "谐振叠加,产能飙升。",
|
||||
description: whisper ? `「${whisper}」` : "谐振叠加,产能飙升。",
|
||||
});
|
||||
} else if (res.combo >= 3 && res.combo % 3 === 0) {
|
||||
// 3 连击时也有概率出现叙事
|
||||
const whisper = maybeWhisper(PULSE_WHISPERS, 0.3);
|
||||
if (whisper) {
|
||||
toast({ title: "低语", description: `「${whisper}」` });
|
||||
}
|
||||
}
|
||||
},
|
||||
[pulse, toast, triggerPulseEffect]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
// 回响星核 / Echo Nexus — 遗迹探险面板(v0.2 肉鸽系统)
|
||||
import { useState } from "react";
|
||||
// 回响星核 / Echo Nexus — 遗迹探险面板(v0.2 肉鸽系统,v0.9 工单修复)
|
||||
import { useState, useEffect } from "react";
|
||||
import { useGameStore } from "@/store/gameStore";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { sfx } from "@/hooks/useAudio";
|
||||
@@ -42,20 +42,30 @@ export function ExpeditionPanel() {
|
||||
const lastEnergyTick = useGameStore((s) => s.lastEnergyTick);
|
||||
const expeditionLog = useGameStore((s) => s.expeditionLog);
|
||||
const totalExpeditions = useGameStore((s) => s.totalExpeditions);
|
||||
const lastExpeditionSummary = useGameStore((s) => s.lastExpeditionSummary);
|
||||
// v0.8.1 动态能量恢复间隔(技术 + 探索力属性缩短)
|
||||
const regenIntervalSec = useGameStore((s) => computeEnergyRegenInterval(s));
|
||||
const startExpedition = useGameStore((s) => s.startExpedition);
|
||||
const resolveCurrentNode = useGameStore((s) => s.resolveCurrentNode);
|
||||
const advanceNode = useGameStore((s) => s.advanceNode);
|
||||
const abortExpedition = useGameStore((s) => s.abortExpedition);
|
||||
const dismissExpedition = useGameStore((s) => s.dismissExpedition);
|
||||
const { toast } = useToast();
|
||||
const [lastLog, setLastLog] = useState<string | null>(null);
|
||||
const [now, setNow] = useState(() => Date.now());
|
||||
// v0.9 工单修复:能量恢复实时倒计时(每秒刷新)
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setNow(Date.now()), 1000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
// 能量恢复进度(v0.8.1 动态间隔)
|
||||
const now = Date.now();
|
||||
// 能量恢复进度(v0.8.1 动态间隔,v0.9 实时刷新)
|
||||
const regenMs = regenIntervalSec * 1000;
|
||||
const regenProgress = energy >= energyMax ? 100 : Math.min(100, ((now - lastEnergyTick) / regenMs) * 100);
|
||||
const elapsed = now - lastEnergyTick;
|
||||
const regenProgress = energy >= energyMax ? 100 : Math.min(100, (elapsed / regenMs) * 100);
|
||||
const regenBoosted = regenIntervalSec < EXPEDITION_CONFIG.energyRegenSec;
|
||||
const nextEnergyInSec = energy >= energyMax ? 0 : Math.max(0, Math.ceil((regenMs - elapsed) / 1000));
|
||||
const canStart = energy >= EXPEDITION_CONFIG.energyCost;
|
||||
|
||||
const handleStart = () => {
|
||||
const res = startExpedition();
|
||||
@@ -100,6 +110,9 @@ export function ExpeditionPanel() {
|
||||
|
||||
// ===== 无活跃探险:展示入口 =====
|
||||
if (!activeExpedition || activeExpedition.finished) {
|
||||
// v0.9 工单修复:如果探险刚结束(finished 且有摘要),显示结算面板
|
||||
const justFinished = activeExpedition?.finished && lastExpeditionSummary &&
|
||||
Date.now() - lastExpeditionSummary.finishedAt < 60000;
|
||||
return (
|
||||
<div className="flex flex-col gap-3 h-full">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -110,6 +123,40 @@ export function ExpeditionPanel() {
|
||||
<span className="text-[10px] text-muted-foreground">已探索 {totalExpeditions} 次</span>
|
||||
</div>
|
||||
|
||||
{/* v0.9 工单修复:上次探险结果摘要 */}
|
||||
{lastExpeditionSummary && (
|
||||
<div
|
||||
className={`rounded-xl border p-2.5 relative overflow-hidden ${
|
||||
lastExpeditionSummary.result === "victory"
|
||||
? "border-emerald-400/40 bg-emerald-950/20"
|
||||
: lastExpeditionSummary.result === "defeat"
|
||||
? "border-rose-400/40 bg-rose-950/20"
|
||||
: "border-amber-400/30 bg-amber-950/10"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-[11px] font-semibold flex items-center gap-1">
|
||||
{lastExpeditionSummary.result === "victory" ? "✦ 上次探险:胜利" : lastExpeditionSummary.result === "defeat" ? "✕ 上次探险:失败" : "→ 上次探险:撤退"}
|
||||
</span>
|
||||
<span className="text-[9px] text-muted-foreground font-mono">{lastExpeditionSummary.nodeCount} 节点</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5 text-[10px]">
|
||||
{lastExpeditionSummary.rewards.crystals > 0 && (
|
||||
<span className="text-emerald-300">+{formatNum(lastExpeditionSummary.rewards.crystals)} 晶体</span>
|
||||
)}
|
||||
{lastExpeditionSummary.rewards.insights > 0 && (
|
||||
<span className="text-amber-300">+{lastExpeditionSummary.rewards.insights} 洞见</span>
|
||||
)}
|
||||
{lastExpeditionSummary.rewards.contact > 0 && (
|
||||
<span className="text-fuchsia-300">+{lastExpeditionSummary.rewards.contact.toFixed(1)} 接触</span>
|
||||
)}
|
||||
{lastExpeditionSummary.rewards.crystals === 0 && lastExpeditionSummary.rewards.insights === 0 && (
|
||||
<span className="text-muted-foreground/60">无奖励</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 能量条 */}
|
||||
<div className="rounded-xl border border-amber-400/20 bg-amber-950/20 p-3">
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
@@ -127,8 +174,10 @@ export function ExpeditionPanel() {
|
||||
value={regenProgress}
|
||||
className="h-1.5 bg-black/40 [&>div]:bg-gradient-to-r [&>div]:from-amber-500 [&>div]:to-yellow-300"
|
||||
/>
|
||||
<p className="text-[10px] text-muted-foreground/70 mt-1">
|
||||
下一点能量约 {Math.ceil((regenMs - (now - lastEnergyTick)) / 1000)}s 后恢复
|
||||
<p className="text-[10px] text-muted-foreground/70 mt-1 flex items-center gap-1">
|
||||
<span>下一点能量</span>
|
||||
<span className="text-amber-300 font-mono font-semibold">{nextEnergyInSec}s</span>
|
||||
<span>后恢复</span>
|
||||
{regenBoosted && (
|
||||
<span className="text-emerald-400/80 ml-1">⚡ {regenIntervalSec.toFixed(0)}s/点 (已加速)</span>
|
||||
)}
|
||||
@@ -140,35 +189,32 @@ export function ExpeditionPanel() {
|
||||
</div>
|
||||
|
||||
{/* 出发按钮 */}
|
||||
<div className="flex-1 flex flex-col items-center justify-center gap-4 rounded-2xl border border-dashed border-amber-400/20 bg-black/30 p-6 relative overflow-hidden">
|
||||
<div className="flex-1 flex flex-col items-center justify-center gap-3 rounded-2xl border border-dashed border-amber-400/20 bg-black/30 p-4 relative overflow-hidden">
|
||||
<div className="absolute -top-10 -right-10 h-32 w-32 rounded-full bg-amber-500/10 blur-2xl" />
|
||||
<div className="relative">
|
||||
<div className="h-20 w-20 rounded-full bg-gradient-to-br from-amber-500/30 to-rose-500/20 flex items-center justify-center mx-auto" style={{ boxShadow: "0 0 30px rgba(251,191,36,0.3)" }}>
|
||||
<Rocket className="h-9 w-9 text-amber-300" />
|
||||
<div className={`h-16 w-16 rounded-full bg-gradient-to-br from-amber-500/30 to-rose-500/20 flex items-center justify-center mx-auto ${canStart ? "animate-pulse" : ""}`} style={{ boxShadow: "0 0 30px rgba(251,191,36,0.3)" }}>
|
||||
<Rocket className="h-7 w-7 text-amber-300" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center space-y-1">
|
||||
<p className="text-sm font-medium text-amber-100">深入以太遗迹</p>
|
||||
<p className="text-[11px] text-muted-foreground/80 max-w-[220px]">
|
||||
派出探险队进入程序化生成的遗迹路径,沿途遭遇战斗、宝藏、抉择与解谜,击败终点 BOSS 获取丰厚奖励。
|
||||
程序化生成的遗迹路径,沿途遭遇战斗、宝藏、抉择与解谜,击败终点 BOSS 获取丰厚奖励。
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleStart}
|
||||
disabled={energy < EXPEDITION_CONFIG.energyCost}
|
||||
disabled={!canStart}
|
||||
className="bg-gradient-to-r from-amber-600 to-rose-500 hover:from-amber-500 hover:to-rose-400 text-white border-0"
|
||||
>
|
||||
<Rocket className="h-4 w-4 mr-1.5" />
|
||||
出发(消耗 {EXPEDITION_CONFIG.energyCost} 能量)
|
||||
{canStart ? `出发(消耗 ${EXPEDITION_CONFIG.energyCost} 能量)` : `能量不足(${nextEnergyInSec}s 后恢复)`}
|
||||
</Button>
|
||||
{energy < EXPEDITION_CONFIG.energyCost && (
|
||||
<p className="text-[10px] text-rose-300/80">能量不足,请等待恢复</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 探险日志 */}
|
||||
{expeditionLog.length > 0 && (
|
||||
<div className="rounded-xl border border-white/5 bg-black/20 p-2 max-h-32 overflow-y-auto echo-scroll">
|
||||
<div className="rounded-xl border border-white/5 bg-black/20 p-2 max-h-28 overflow-y-auto echo-scroll">
|
||||
<div className="text-[10px] text-muted-foreground/60 mb-1 px-1">最近探险记录</div>
|
||||
{expeditionLog.slice(0, 4).map((entry, i) => (
|
||||
<div key={i} className="text-[10px] text-muted-foreground/70 px-1 py-0.5 border-b border-white/5 last:border-0">
|
||||
@@ -178,6 +224,18 @@ export function ExpeditionPanel() {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* v0.9 工单修复:刚结束的探险,显示"返回"按钮 */}
|
||||
{justFinished && (
|
||||
<Button
|
||||
onClick={() => dismissExpedition()}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 text-[11px] border-amber-400/30 text-amber-200"
|
||||
>
|
||||
收起结算,准备下次出发
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useGameStore } from "@/store/gameStore";
|
||||
import { TECH_TREE, TECH_BRANCH_META, formatNum } from "@/lib/game/config";
|
||||
import { sfx } from "@/hooks/useAudio";
|
||||
import { TECH_LORE } from "@/lib/game/narrative";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -58,10 +59,14 @@ export function TechTree() {
|
||||
<span className="text-xs font-medium" style={{ color }}>
|
||||
{meta.name}
|
||||
</span>
|
||||
<span className="text-[10px] text-muted-foreground/70">
|
||||
<span className="text-[10px] text-muted-foreground/70 truncate flex-1">
|
||||
{meta.desc}
|
||||
</span>
|
||||
</div>
|
||||
{/* v0.9 叙事融入:每分支的以太族遗产描述 */}
|
||||
<p className="text-[9px] italic text-muted-foreground/50 px-1 mb-1.5 truncate">
|
||||
「{TECH_LORE[b] ?? ""}」
|
||||
</p>
|
||||
<div className="grid gap-1.5">
|
||||
{nodes.map((node) => {
|
||||
const owned = (tech[node.id] ?? 0) >= 1;
|
||||
|
||||
@@ -16,7 +16,7 @@ const ToastViewport = React.forwardRef<
|
||||
<ToastPrimitives.Viewport
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||
"fixed top-14 z-[90] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
ToastProps,
|
||||
} from "@/components/ui/toast"
|
||||
|
||||
const TOAST_LIMIT = 1
|
||||
const TOAST_LIMIT = 3
|
||||
const TOAST_REMOVE_DELAY = 1000000
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
|
||||
@@ -90,10 +90,10 @@ export const ACHIEVEMENTS: Achievement[] = [
|
||||
{
|
||||
id: "ach_tech_all",
|
||||
name: "全谱精通",
|
||||
desc: "解锁全部 12 项技术",
|
||||
desc: "解锁全部 20 项技术",
|
||||
icon: "⬡",
|
||||
color: "#e879f9",
|
||||
check: (s) => Object.values(s.tech).filter((v) => v > 0).length >= 12,
|
||||
check: (s) => Object.values(s.tech).filter((v) => v > 0).length >= 20,
|
||||
reward: { crystalsPerSecPct: 15, insightPct: 15 },
|
||||
rewardText: "产能 +15% · 洞见 +15%",
|
||||
},
|
||||
|
||||
+77
-4
@@ -10,7 +10,7 @@ import type {
|
||||
export const INITIAL_STATE = {
|
||||
crystals: 0,
|
||||
insights: 0,
|
||||
energy: 3,
|
||||
energy: 5,
|
||||
contact: 0,
|
||||
crystalsPerSec: 0.4,
|
||||
crystalCap: 50,
|
||||
@@ -26,10 +26,11 @@ export const INITIAL_STATE = {
|
||||
activePuzzle: null,
|
||||
activeExpedition: null,
|
||||
expeditionLog: [],
|
||||
energyMax: 5,
|
||||
energyMax: 8,
|
||||
lastEnergyTick: Date.now(),
|
||||
totalExpeditions: 0,
|
||||
bossKills: 0,
|
||||
lastExpeditionSummary: null as import("./types").GameState["lastExpeditionSummary"],
|
||||
achievements: {},
|
||||
activeTide: null,
|
||||
lastTideEnd: 0,
|
||||
@@ -124,7 +125,7 @@ export const COLOR_VISUAL: Record<
|
||||
fuchsia: { hex: "#e879f9", glow: "rgba(232,121,249,0.55)", label: "紫" },
|
||||
};
|
||||
|
||||
/** 技术树:4 分支 × 3 级 = 12 节点 */
|
||||
/** 技术树:4 分支 × 5 级 = 20 节点(v0.9 工单 #2 扩充:12→20) */
|
||||
export const TECH_TREE: TechNode[] = [
|
||||
// 采矿分支
|
||||
{
|
||||
@@ -154,6 +155,24 @@ export const TECH_TREE: TechNode[] = [
|
||||
cost: 120,
|
||||
effect: { kind: "crystalsPerSec", value: 5 },
|
||||
},
|
||||
{
|
||||
id: "min_4",
|
||||
branch: "mining",
|
||||
level: 4,
|
||||
name: "量子采掘矩阵",
|
||||
desc: "晶体/秒 +12,仓库上限 +800",
|
||||
cost: 400,
|
||||
effect: { kind: "crystalsPerSec", value: 12 },
|
||||
},
|
||||
{
|
||||
id: "min_5",
|
||||
branch: "mining",
|
||||
level: 5,
|
||||
name: "戴森星核",
|
||||
desc: "晶体/秒 +30,仓库上限 +2000",
|
||||
cost: 1200,
|
||||
effect: { kind: "crystalsPerSec", value: 30 },
|
||||
},
|
||||
// 解码分支
|
||||
{
|
||||
id: "dec_1",
|
||||
@@ -182,7 +201,25 @@ export const TECH_TREE: TechNode[] = [
|
||||
cost: 200,
|
||||
effect: { kind: "autoDecode", value: 1 },
|
||||
},
|
||||
// 探险分支(v0.2:探险力/生命/能量上限/产能)
|
||||
{
|
||||
id: "dec_4",
|
||||
branch: "decoding",
|
||||
level: 4,
|
||||
name: "谐振放大器",
|
||||
desc: "主动脉冲产出 +8",
|
||||
cost: 500,
|
||||
effect: { kind: "pulsePower", value: 8 },
|
||||
},
|
||||
{
|
||||
id: "dec_5",
|
||||
branch: "decoding",
|
||||
level: 5,
|
||||
name: "全息解码核心",
|
||||
desc: "自动解码扩展至 T2 晶体,解码步数 +3",
|
||||
cost: 1500,
|
||||
effect: { kind: "decodeSteps", value: 3 },
|
||||
},
|
||||
// 探险分支(v0.2:探险力/生命/能量上限/产能;v0.9:level 4/5 高级探险力+能量恢复)
|
||||
{
|
||||
id: "exp_1",
|
||||
branch: "expedition",
|
||||
@@ -210,6 +247,24 @@ export const TECH_TREE: TechNode[] = [
|
||||
cost: 180,
|
||||
effect: { kind: "contactRate", value: 0.5 },
|
||||
},
|
||||
{
|
||||
id: "exp_4",
|
||||
branch: "expedition",
|
||||
level: 4,
|
||||
name: "虚空航标",
|
||||
desc: "探险力 +12,探险生命 +30,能量恢复 +25%",
|
||||
cost: 450,
|
||||
effect: { kind: "crystalCap", value: 400 },
|
||||
},
|
||||
{
|
||||
id: "exp_5",
|
||||
branch: "expedition",
|
||||
level: 5,
|
||||
name: "维度跃迁引擎",
|
||||
desc: "探险力 +25,探险生命 +50,能量恢复 +30%",
|
||||
cost: 1400,
|
||||
effect: { kind: "crystalCap", value: 600 },
|
||||
},
|
||||
// 叙事分支
|
||||
{
|
||||
id: "nar_1",
|
||||
@@ -238,6 +293,24 @@ export const TECH_TREE: TechNode[] = [
|
||||
cost: 160,
|
||||
effect: { kind: "insightMult", value: 0.6 },
|
||||
},
|
||||
{
|
||||
id: "nar_4",
|
||||
branch: "narrative",
|
||||
level: 4,
|
||||
name: "以太低语",
|
||||
desc: "洞见获取 +100%",
|
||||
cost: 500,
|
||||
effect: { kind: "insightMult", value: 1.0 },
|
||||
},
|
||||
{
|
||||
id: "nar_5",
|
||||
branch: "narrative",
|
||||
level: 5,
|
||||
name: "文明回响",
|
||||
desc: "接触进度转化率 +80%,洞见获取 +150%",
|
||||
cost: 1600,
|
||||
effect: { kind: "insightMult", value: 1.5 },
|
||||
},
|
||||
];
|
||||
|
||||
/** 技术分支元信息 */
|
||||
|
||||
@@ -79,6 +79,15 @@ export function recomputeStats(state: Partial<GameState>): {
|
||||
}
|
||||
}
|
||||
|
||||
// v0.9 工单 #2:多效果节点的次效果(desc 中描述但 effect.kind 只能承载一个)
|
||||
// 采矿分支 desc 承诺的仓库上限加成(同时补齐 min_2/min_3 旧 desc 文案)
|
||||
if (tech.min_2) crystalCap += 100;
|
||||
if (tech.min_3) crystalCap += 300;
|
||||
if (tech.min_4) crystalCap += 800;
|
||||
if (tech.min_5) crystalCap += 2000;
|
||||
// 叙事分支 nar_5 文明回响:接触进度转化率 +80%(与 insightMult +150% 并存)
|
||||
if (tech.nar_5) contactRateMult += 0.8;
|
||||
|
||||
// 飞升蓝图加成(+ 星图「蓝图回响」额外加成)
|
||||
const bpAllPct = cm.perBlueprintAllStatsPct / 100;
|
||||
crystalsPerSec *= 1 + (bp * (PRESTIGE.perBlueprint.crystalsPerSecMult + bpAllPct));
|
||||
|
||||
@@ -39,10 +39,11 @@ export const EXPEDITION_CONFIG = {
|
||||
* v0.8.1 平衡:scale 8→4,floor 0.25→0.35,让基础探险力也能有合理胜率 */
|
||||
combatDifficultyScale: 4,
|
||||
combatWinRateFloor: 0.35,
|
||||
/** 能量恢复基础间隔(秒),可被技术/属性缩短 */
|
||||
energyRegenSec: 45,
|
||||
/** 能量恢复基础间隔(秒),可被技术/属性缩短
|
||||
* v0.9 工单修复:45→20,解决「探险只能一次」体验问题 */
|
||||
energyRegenSec: 20,
|
||||
/** 能量恢复最短间隔(秒,技术+属性全满时) */
|
||||
energyRegenMinSec: 12,
|
||||
energyRegenMinSec: 8,
|
||||
};
|
||||
|
||||
/** 节点类型权重(boss 固定末位,其余按权重随机) */
|
||||
@@ -181,11 +182,13 @@ export function generateExpedition(
|
||||
/** 计算探险力(由技术树 + 飞升蓝图 + 星图天赋) */
|
||||
export function computeExpeditionPower(state: GameState): number {
|
||||
let power = EXPEDITION_CONFIG.basePower;
|
||||
// 探险分支技术加成
|
||||
// 探险分支技术加成(v0.9 扩充:exp_4 +12,exp_5 +25)
|
||||
const exp1 = state.tech?.exp_1 ?? 0;
|
||||
const exp2 = state.tech?.exp_2 ?? 0;
|
||||
const exp3 = state.tech?.exp_3 ?? 0;
|
||||
power += exp1 * 3 + exp2 * 5 + exp3 * 8;
|
||||
const exp4 = state.tech?.exp_4 ?? 0;
|
||||
const exp5 = state.tech?.exp_5 ?? 0;
|
||||
power += exp1 * 3 + exp2 * 5 + exp3 * 8 + exp4 * 12 + exp5 * 25;
|
||||
// 飞升蓝图加成
|
||||
power *= 1 + (state.blueprints?.length ?? 0) * 0.08;
|
||||
// 飞升周目加成
|
||||
@@ -200,6 +203,9 @@ export function computeExpeditionPower(state: GameState): number {
|
||||
export function computeExpeditionHp(state: GameState): number {
|
||||
let hp = EXPEDITION_CONFIG.baseHp;
|
||||
hp += (state.tech?.exp_2 ?? 0) * 20;
|
||||
// v0.9 扩充:exp_4 +30,exp_5 +50
|
||||
hp += (state.tech?.exp_4 ?? 0) * 30;
|
||||
hp += (state.tech?.exp_5 ?? 0) * 50;
|
||||
hp += (state.ascensions ?? 0) * 10;
|
||||
// 星图「维生护盾」加成
|
||||
const cm = constellationBonuses(state.constellation ?? []);
|
||||
@@ -366,13 +372,16 @@ export function advanceExpedition(expedition: Expedition): ExpeditionResult {
|
||||
}
|
||||
|
||||
/** 计算实际能量恢复间隔(秒),由技术树 + 角色属性缩短
|
||||
* v0.8.1:exp_2 解锁 -30%,exp_3 解锁 -20%,探索力属性 -最高30%,下限 12s */
|
||||
* v0.8.1:exp_2 解锁 -30%,exp_3 解锁 -20%,探索力属性 -最高30%,下限 12s
|
||||
* v0.9 扩充:exp_4 加速 25%(×0.75),exp_5 加速 30%(×0.70),下限 8s */
|
||||
export function computeEnergyRegenInterval(state: GameState): number {
|
||||
const base = EXPEDITION_CONFIG.energyRegenSec;
|
||||
let mult = 1;
|
||||
// 技术:遗迹图谱(exp_2)+ 维度信标(exp_3)
|
||||
// 技术:遗迹图谱(exp_2)+ 维度信标(exp_3)+ 虚空航标(exp_4)+ 维度跃迁引擎(exp_5)
|
||||
if (state.tech?.exp_2) mult *= 0.7;
|
||||
if (state.tech?.exp_3) mult *= 0.8;
|
||||
if (state.tech?.exp_4) mult *= 0.75;
|
||||
if (state.tech?.exp_5) mult *= 0.7;
|
||||
// 角色属性:探索力(exploration)每点缩短少量,高探索力最高 -30%
|
||||
const exploration = state.attributes?.exploration ?? 0;
|
||||
const explorationBonus = exploration >= 50
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// 回响星核 / Echo Nexus — 叙事氛围文本池(v0.9 工单修复)
|
||||
// 工单 Issue #2 反馈"很多东西都没靠着世界观去写,像是为了做游戏而做的一样"
|
||||
// 本模块为各系统交互注入叙事碎片,让世界观渗透到每个角落
|
||||
|
||||
/** 脉冲扫描时随机触发的氛围低语(连击越高出现概率越大) */
|
||||
export const PULSE_WHISPERS: string[] = [
|
||||
"晶核深处传来一阵微弱的震颤…",
|
||||
"以太族的余响在谐振中回荡。",
|
||||
"你听见了一句无法辨认的低语。",
|
||||
"星海的寂静被你的脉冲短暂打破。",
|
||||
"晶体表面的纹路闪烁,像是某种回应。",
|
||||
"回响核心记录着每一次触碰。",
|
||||
"虚空深处,有什么在倾听。",
|
||||
"谐振频率与你的脉冲产生了共鸣。",
|
||||
"这是以太族留下的第一道印记。",
|
||||
"晶体内部的光,比星辰更古老。",
|
||||
"你的脉冲唤醒了一段沉睡的频率。",
|
||||
"维度薄膜因你的触碰而微微颤动。",
|
||||
"采矿不是掠夺,是聆听。",
|
||||
"每一颗晶体,都是一封跨越时间的信。",
|
||||
"以太族信任你,所以留下了这些。",
|
||||
];
|
||||
|
||||
/** 解码成功时的叙事碎片(按解码次数递进解锁更深层的叙事) */
|
||||
export const DECODE_ECHOES: string[] = [
|
||||
"一段记忆在你的解码中苏醒…",
|
||||
"谐振序列被还原,低语化为可读的符文。",
|
||||
"你触碰到了以太族思维的一角。",
|
||||
"解码完成的瞬间,你看见了他们的星海。",
|
||||
"记忆晶体释放出封存了万年的画面。",
|
||||
"这不仅是数据,是一段被编码的经历。",
|
||||
"以太族的语言不是文字,是共振本身。",
|
||||
"你读懂了这句话时,接触便已开始。",
|
||||
"晶体褪去外壳,露出内核中的回响。",
|
||||
"解码,是以太族留给后来者的资格证明。",
|
||||
"你的思维与晶体中的残响短暂重合。",
|
||||
"一段文明的碎片在你手中重组。",
|
||||
];
|
||||
|
||||
/** 技术解锁时的叙事(每分支一段通用叙事) */
|
||||
export const TECH_LORE: Record<string, string> = {
|
||||
mining: "以太族从不掠夺星海,他们与矿脉共振,让晶体自愿献出光芒。",
|
||||
decoding: "谐振法则是以太族的核心遗产——思维可被编码为共振序列。",
|
||||
expedition: "他们留下的遗迹不是废墟,是故意设置的试炼场。",
|
||||
narrative: "记忆校音,是以太族赋予后来者的第一项礼物。",
|
||||
};
|
||||
|
||||
/** 探险节点事件额外的叙事前缀 */
|
||||
export const EXPEDITION_NARRATIVE: Record<string, string> = {
|
||||
combat: "以太族的守卫残影仍在执行万年前的指令。",
|
||||
treasure: "这些宝藏是以太族特意留下的补给。",
|
||||
choice: "分岔甬道是他们对后来者的考验。",
|
||||
puzzle: "谐振锁的答案,藏在以太族的思维模式里。",
|
||||
rest: "这处休憩节点,是以太族留给疲惫旅人的礼物。",
|
||||
boss: "终焉之核是以太族离去前留下的最后一道试炼。",
|
||||
};
|
||||
|
||||
/** 飞升时的叙事 */
|
||||
export const PRESTIGE_ECHOES: string[] = [
|
||||
"你的意识与以太族的残响交汇,维度薄膜被穿透…",
|
||||
"飞升不是结束,是与更高处的他们相会。",
|
||||
"你褪去了这一周目的形态,回响却永存。",
|
||||
"以太族在更高处等你,已等候了万年。",
|
||||
"接触完成。你成为了他们等待的回应。",
|
||||
];
|
||||
|
||||
/** 随机取一条叙事文本 */
|
||||
export function pickWhisper(pool: string[], seed?: number): string {
|
||||
if (pool.length === 0) return "";
|
||||
const idx = seed !== undefined
|
||||
? Math.abs(Math.floor(seed)) % pool.length
|
||||
: Math.floor(Math.random() * pool.length);
|
||||
return pool[idx];
|
||||
}
|
||||
|
||||
/** 按概率返回一条叙事(probability 0-1) */
|
||||
export function maybeWhisper(pool: string[], probability: number): string | null {
|
||||
if (Math.random() < probability) {
|
||||
return pickWhisper(pool);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -125,6 +125,13 @@ export interface GameState {
|
||||
lastEnergyTick: number;
|
||||
totalExpeditions: number;
|
||||
bossKills: number; // 累计击败 BOSS 数(跨周目)
|
||||
/** v0.9 工单修复:上次探险结果摘要,用于入口界面展示 */
|
||||
lastExpeditionSummary: {
|
||||
result: "victory" | "defeat" | "abort";
|
||||
rewards: { crystals: number; insights: number; contact: number };
|
||||
nodeCount: number;
|
||||
finishedAt: number;
|
||||
} | null;
|
||||
|
||||
// 成就
|
||||
achievements: Record<string, boolean>; // achievementId -> unlocked
|
||||
|
||||
+37
-3
@@ -127,6 +127,8 @@ interface GameActions {
|
||||
resolveCurrentNode: () => ExpeditionResult | null;
|
||||
advanceNode: () => void;
|
||||
abortExpedition: () => void;
|
||||
/** v0.9 工单修复:手动关闭已结束的探险,回到入口 */
|
||||
dismissExpedition: () => void;
|
||||
|
||||
// 技术
|
||||
buyTech: (techId: string) => boolean;
|
||||
@@ -778,12 +780,17 @@ export const useGameStore = create<Store>()(
|
||||
const tideIntervalMult = tideModEarly.autoDecodeIntervalMult ?? 1;
|
||||
const interval = Math.max(5000, baseInterval * am.autoDecodeIntervalMult * tideIntervalMult);
|
||||
if (now - s._lastAutoDecode < interval) return;
|
||||
// 找一颗 T1 晶体自动解码
|
||||
const idx = s.pendingCrystals.findIndex((c) => c.tier === 1);
|
||||
// 找一颗 T1 晶体自动解码;v0.9 dec_5「全息解码核心」解锁后可自动解码 T2
|
||||
let autoTier: CrystalTier = 1;
|
||||
let idx = s.pendingCrystals.findIndex((c) => c.tier === 1);
|
||||
if (idx < 0 && s.tech?.dec_5) {
|
||||
idx = s.pendingCrystals.findIndex((c) => c.tier === 2);
|
||||
autoTier = 2;
|
||||
}
|
||||
if (idx < 0) return;
|
||||
const crystal = s.pendingCrystals[idx];
|
||||
const tideMod = getTideModifiers(s.activeTide);
|
||||
const base = decodeRewards(1, s.insightMult, s.contactRateMult);
|
||||
const base = decodeRewards(autoTier, s.insightMult, s.contactRateMult);
|
||||
const finalMult = tideMod.decodeRewardMult * cm.decodeRewardMult;
|
||||
const rewards = {
|
||||
crystals: Math.round(base.crystals * finalMult),
|
||||
@@ -923,6 +930,17 @@ export const useGameStore = create<Store>()(
|
||||
exp.finished = true;
|
||||
}
|
||||
|
||||
// v0.9 工单修复:探险结束时记录摘要,用于入口界面展示
|
||||
let lastExpeditionSummary = s.lastExpeditionSummary ?? null;
|
||||
if (result.ended && (result.endReason === "victory" || result.endReason === "defeat")) {
|
||||
lastExpeditionSummary = {
|
||||
result: result.endReason,
|
||||
rewards: { ...exp.rewards },
|
||||
nodeCount: exp.nodes.length,
|
||||
finishedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
// v0.4 编年史:击破 BOSS 时累计计数
|
||||
let bossKills = s.bossKills ?? 0;
|
||||
let bossKilledThisNode = false;
|
||||
@@ -979,6 +997,7 @@ export const useGameStore = create<Store>()(
|
||||
bossKills,
|
||||
attributes: newAttributes,
|
||||
attributeProgress: newProgress,
|
||||
lastExpeditionSummary,
|
||||
...(statsNeedResync
|
||||
? syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: s.constellation, attributes: newAttributes })
|
||||
: {}),
|
||||
@@ -1017,12 +1036,27 @@ export const useGameStore = create<Store>()(
|
||||
rewards: "",
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
// v0.9 工单修复:记录上次探险摘要,用于入口展示
|
||||
const summary = {
|
||||
result: "abort" as const,
|
||||
rewards: { ...exp.rewards },
|
||||
nodeCount: exp.nodes.length,
|
||||
finishedAt: Date.now(),
|
||||
};
|
||||
set({
|
||||
activeExpedition: exp,
|
||||
expeditionLog: [logEntry, ...s.expeditionLog].slice(0, 30),
|
||||
lastExpeditionSummary: summary,
|
||||
});
|
||||
},
|
||||
|
||||
// v0.9 工单修复:手动关闭已结束探险,回到入口
|
||||
dismissExpedition: () => {
|
||||
const s = get();
|
||||
if (!s.activeExpedition || !s.activeExpedition.finished) return;
|
||||
set({ activeExpedition: null });
|
||||
},
|
||||
|
||||
doPrestige: () => {
|
||||
const s = get();
|
||||
if (s.contact < CONTACT.prestigeMin) return null;
|
||||
|
||||
Reference in New Issue
Block a user