d9404a31-1970-4e79-ac9a-f4f9db60350b
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const globalForPrisma = globalThis as unknown as {
|
||||
prisma: PrismaClient | undefined
|
||||
}
|
||||
|
||||
export const db =
|
||||
globalForPrisma.prisma ??
|
||||
new PrismaClient({
|
||||
log: ['query'],
|
||||
})
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db
|
||||
@@ -0,0 +1,307 @@
|
||||
// 回响星核 / Echo Nexus — 数值配置(数据驱动,便于平衡与工单迭代)
|
||||
import type {
|
||||
CrystalTier,
|
||||
ResonanceColor,
|
||||
TechNode,
|
||||
Fragment,
|
||||
} from "./types";
|
||||
|
||||
/** 初始状态 */
|
||||
export const INITIAL_STATE = {
|
||||
crystals: 0,
|
||||
insights: 0,
|
||||
energy: 10,
|
||||
contact: 0,
|
||||
crystalsPerSec: 0.4,
|
||||
crystalCap: 50,
|
||||
pulsePower: 2,
|
||||
offlineEff: 0.5,
|
||||
insightMult: 1,
|
||||
contactRateMult: 1,
|
||||
autoDecode: false,
|
||||
totalDecoded: 0,
|
||||
ascensions: 0,
|
||||
blueprints: [] as string[],
|
||||
pendingCrystals: [],
|
||||
activePuzzle: null,
|
||||
theme: "dark" as const,
|
||||
soundOn: true,
|
||||
};
|
||||
|
||||
/** 主动脉冲:连击窗口(毫秒)与加成 */
|
||||
export const PULSE = {
|
||||
comboWindowMs: 1500,
|
||||
comboMultStep: 0.15, // 每连击 +15%
|
||||
comboMax: 10,
|
||||
};
|
||||
|
||||
/** 离线收益上限(小时) */
|
||||
export const OFFLINE_CAP_HOURS = 8;
|
||||
|
||||
/** 接触进度:每次解码贡献 = base * tier * contactRateMult */
|
||||
export const CONTACT = {
|
||||
basePerDecode: 0.6,
|
||||
// 飞升所需最低接触进度
|
||||
prestigeMin: 100,
|
||||
};
|
||||
|
||||
/** 晶体生成:每 N 秒自动产出一颗待解码晶体(受技术影响) */
|
||||
export const CRYSTAL_SPAWN = {
|
||||
baseIntervalSec: 8, // 每 8 秒产一颗
|
||||
maxPending: 6,
|
||||
/** tier 概率分布 */
|
||||
tierWeights: { 1: 0.7, 2: 0.25, 3: 0.05 } as Record<CrystalTier, number>,
|
||||
};
|
||||
|
||||
/** 晶体价值(解码奖励基础,× tier) */
|
||||
export const CRYSTAL_VALUE = {
|
||||
1: { crystals: 6, insights: 1 },
|
||||
2: { crystals: 22, insights: 4 },
|
||||
3: { crystals: 90, insights: 16 },
|
||||
} as const;
|
||||
|
||||
/** 解码阵列配置(按 tier) */
|
||||
export const DECODE_CONFIG: Record<
|
||||
CrystalTier,
|
||||
{ rows: number; cols: number; targetLen: number; stepLimitBase: number }
|
||||
> = {
|
||||
1: { rows: 3, cols: 3, targetLen: 3, stepLimitBase: 5 },
|
||||
2: { rows: 4, cols: 4, targetLen: 4, stepLimitBase: 7 },
|
||||
3: { rows: 5, cols: 4, targetLen: 5, stepLimitBase: 9 },
|
||||
};
|
||||
|
||||
/** 共振颜色(4 色全息色谱,避开蓝/靛) */
|
||||
export const RESONANCE_COLORS: ResonanceColor[] = [
|
||||
"emerald",
|
||||
"rose",
|
||||
"amber",
|
||||
"fuchsia",
|
||||
];
|
||||
|
||||
/** 颜色可视化(用于 Canvas / CSS) */
|
||||
export const COLOR_VISUAL: Record<
|
||||
ResonanceColor,
|
||||
{ hex: string; glow: string; label: string }
|
||||
> = {
|
||||
emerald: { hex: "#34d399", glow: "rgba(52,211,153,0.55)", label: "翠" },
|
||||
rose: { hex: "#fb7185", glow: "rgba(251,113,133,0.55)", label: "玫" },
|
||||
amber: { hex: "#fbbf24", glow: "rgba(251,191,36,0.55)", label: "琥" },
|
||||
fuchsia: { hex: "#e879f9", glow: "rgba(232,121,249,0.55)", label: "紫" },
|
||||
};
|
||||
|
||||
/** 技术树:4 分支 × 3 级 = 12 节点 */
|
||||
export const TECH_TREE: TechNode[] = [
|
||||
// 采矿分支
|
||||
{
|
||||
id: "min_1",
|
||||
branch: "mining",
|
||||
level: 1,
|
||||
name: "谐振钻头",
|
||||
desc: "晶体/秒 +0.6",
|
||||
cost: 5,
|
||||
effect: { kind: "crystalsPerSec", value: 0.6 },
|
||||
},
|
||||
{
|
||||
id: "min_2",
|
||||
branch: "mining",
|
||||
level: 2,
|
||||
name: "深层矿脉",
|
||||
desc: "晶体/秒 +1.6,仓库上限 +100",
|
||||
cost: 24,
|
||||
effect: { kind: "crystalsPerSec", value: 1.6 },
|
||||
},
|
||||
{
|
||||
id: "min_3",
|
||||
branch: "mining",
|
||||
level: 3,
|
||||
name: "自治机群",
|
||||
desc: "晶体/秒 +5,仓库上限 +300",
|
||||
cost: 120,
|
||||
effect: { kind: "crystalsPerSec", value: 5 },
|
||||
},
|
||||
// 解码分支
|
||||
{
|
||||
id: "dec_1",
|
||||
branch: "decoding",
|
||||
level: 1,
|
||||
name: "谐振校准",
|
||||
desc: "解码步数上限 +2",
|
||||
cost: 6,
|
||||
effect: { kind: "decodeSteps", value: 2 },
|
||||
},
|
||||
{
|
||||
id: "dec_2",
|
||||
branch: "decoding",
|
||||
level: 2,
|
||||
name: "脉冲增幅",
|
||||
desc: "主动脉冲产出 +3",
|
||||
cost: 30,
|
||||
effect: { kind: "pulsePower", value: 3 },
|
||||
},
|
||||
{
|
||||
id: "dec_3",
|
||||
branch: "decoding",
|
||||
level: 3,
|
||||
name: "自动解码阵列",
|
||||
desc: "自动解码 T1 晶体(每 12 秒一颗)",
|
||||
cost: 200,
|
||||
effect: { kind: "autoDecode", value: 1 },
|
||||
},
|
||||
// 探险分支(v0.1 仅产能效果,探险系统留 v0.2)
|
||||
{
|
||||
id: "exp_1",
|
||||
branch: "expedition",
|
||||
level: 1,
|
||||
name: "远征推进器",
|
||||
desc: "晶体/秒 +1.2(探险预备)",
|
||||
cost: 12,
|
||||
effect: { kind: "crystalsPerSec", value: 1.2 },
|
||||
},
|
||||
{
|
||||
id: "exp_2",
|
||||
branch: "expedition",
|
||||
level: 2,
|
||||
name: "遗迹图谱",
|
||||
desc: "仓库上限 +200,晶体/秒 +2",
|
||||
cost: 60,
|
||||
effect: { kind: "crystalCap", value: 200 },
|
||||
},
|
||||
{
|
||||
id: "exp_3",
|
||||
branch: "expedition",
|
||||
level: 3,
|
||||
name: "维度信标",
|
||||
desc: "接触进度转化率 +50%",
|
||||
cost: 180,
|
||||
effect: { kind: "contactRate", value: 0.5 },
|
||||
},
|
||||
// 叙事分支
|
||||
{
|
||||
id: "nar_1",
|
||||
branch: "narrative",
|
||||
level: 1,
|
||||
name: "记忆校音",
|
||||
desc: "洞见获取 +25%",
|
||||
cost: 8,
|
||||
effect: { kind: "insightMult", value: 0.25 },
|
||||
},
|
||||
{
|
||||
id: "nar_2",
|
||||
branch: "narrative",
|
||||
level: 2,
|
||||
name: "深空回响",
|
||||
desc: "接触进度转化率 +35%",
|
||||
cost: 45,
|
||||
effect: { kind: "contactRate", value: 0.35 },
|
||||
},
|
||||
{
|
||||
id: "nar_3",
|
||||
branch: "narrative",
|
||||
level: 3,
|
||||
name: "以太共鸣",
|
||||
desc: "洞见获取 +60%",
|
||||
cost: 160,
|
||||
effect: { kind: "insightMult", value: 0.6 },
|
||||
},
|
||||
];
|
||||
|
||||
/** 技术分支元信息 */
|
||||
export const TECH_BRANCH_META: Record<
|
||||
string,
|
||||
{ name: string; icon: string; color: string; desc: string }
|
||||
> = {
|
||||
mining: { name: "采矿", icon: "Pickaxe", color: "emerald", desc: "提升晶体产能与仓储" },
|
||||
decoding: { name: "解码", icon: "ScanLine", color: "rose", desc: "强化解码与脉冲" },
|
||||
expedition: { name: "探险", icon: "Rocket", color: "amber", desc: "远征与维度信标" },
|
||||
narrative: { name: "叙事", icon: "BookOpen", color: "fuchsia", desc: "洞见与接触进度" },
|
||||
};
|
||||
|
||||
/** 记忆碎片(首纪元 8 个,v0.1) */
|
||||
export const FRAGMENTS: Fragment[] = [
|
||||
{
|
||||
id: "f1_1",
|
||||
era: 1,
|
||||
title: "第一缕谐振",
|
||||
echo:
|
||||
"在被称作「寂灭前夜」的年代,以太族第一次捕捉到来自虚空深处的谐振——那不是声音,而是存在的回声。他们将之记录在第一颗记忆晶体里。",
|
||||
threshold: 1,
|
||||
},
|
||||
{
|
||||
id: "f1_2",
|
||||
era: 1,
|
||||
title: "晶体之始",
|
||||
echo:
|
||||
"「我们为何要留下?」一位长者问。「因为后来的生命会需要方向。」于是他们熔铸星核,封存记忆,让晶体成为跨越时间的信笺。",
|
||||
threshold: 3,
|
||||
},
|
||||
{
|
||||
id: "f1_3",
|
||||
era: 1,
|
||||
title: "谐振法则",
|
||||
echo:
|
||||
"他们发现:思维可以被编码为共振序列。同色的节点相邻,便构成一句低语;一句句连成回响,回响连成文明。",
|
||||
threshold: 6,
|
||||
},
|
||||
{
|
||||
id: "f1_4",
|
||||
era: 1,
|
||||
title: "飞升之议",
|
||||
echo:
|
||||
"议会表决:是留下,还是离去?「留下意味着停滞;离去意味着信任。」最终,他们选择了信任——信任素未谋面的后来者。",
|
||||
threshold: 10,
|
||||
},
|
||||
{
|
||||
id: "f1_5",
|
||||
era: 1,
|
||||
title: "面包屑",
|
||||
echo:
|
||||
"他们没有带走一切。他们将知识拆成碎片,藏入晶体,散布星海。「让解码本身,成为资格的证明。」",
|
||||
threshold: 15,
|
||||
},
|
||||
{
|
||||
id: "f1_6",
|
||||
era: 1,
|
||||
title: "回响之名",
|
||||
echo:
|
||||
"舰载 AI 被命名为「回响」——意为:你是他们留下的回声,也是他们等待的回应。当你读懂这句话时,接触便已开始。",
|
||||
threshold: 22,
|
||||
},
|
||||
{
|
||||
id: "f1_7",
|
||||
era: 1,
|
||||
title: "第一个纪元的终焉",
|
||||
echo:
|
||||
"以太族集体褪去形体,如潮水退去。星海寂静了数万年。直到——你的脉冲扫描,唤醒了第一颗沉睡的晶体。",
|
||||
threshold: 32,
|
||||
},
|
||||
{
|
||||
id: "f1_8",
|
||||
era: 1,
|
||||
title: "致后来者",
|
||||
echo:
|
||||
"「如果你读到这里,说明你已具备跨越维度的资格。继续解码,继续拼凑。当进度满溢,我们将在更高处相会。」——以太族·第一纪元·绝笔",
|
||||
threshold: 45,
|
||||
},
|
||||
];
|
||||
|
||||
/** 飞升:保留蓝图数 = floor(totalDecoded / 20),最多 6 */
|
||||
export const PRESTIGE = {
|
||||
blueprintsPerDecode: 20,
|
||||
maxBlueprints: 6,
|
||||
/** 每个蓝图提供 +5% 产能、+3% 洞见、+4% 接触率 */
|
||||
perBlueprint: {
|
||||
crystalsPerSecMult: 0.05,
|
||||
insightMult: 0.03,
|
||||
contactRateMult: 0.04,
|
||||
},
|
||||
};
|
||||
|
||||
/** 数值格式化:大数字友好显示 */
|
||||
export function formatNum(n: number): string {
|
||||
if (!isFinite(n)) return "∞";
|
||||
if (n < 1000) return n.toFixed(n < 10 && n % 1 !== 0 ? 1 : 0);
|
||||
if (n < 1e6) return (n / 1e3).toFixed(2) + "K";
|
||||
if (n < 1e9) return (n / 1e6).toFixed(2) + "M";
|
||||
if (n < 1e12) return (n / 1e9).toFixed(2) + "B";
|
||||
return (n / 1e12).toFixed(2) + "T";
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
// 回响星核 / Echo Nexus — 解码共振谜题逻辑(原创核心玩法)
|
||||
//
|
||||
// 谜题规则「谐振序列 / Harmonic Sequence」:
|
||||
// 1. 一个 rows×cols 的发光节点阵列,每个节点带一种共振色(4 色全息色谱)。
|
||||
// 2. 中央给出「目标谐振序列」target: ResonanceColor[](长度由 tier 决定)。
|
||||
// 3. 玩家依次点击节点,重建该序列:
|
||||
// - 第 1 次点击可任意节点,但该节点颜色须 === target[0]。
|
||||
// - 第 k 次点击的节点颜色须 === target[k-1],且与上一次点击节点**四邻接**(上下左右)。
|
||||
// - 每个节点只能用一次(点击后变暗)。
|
||||
// 4. 步数上限 = stepLimitBase + 解码技术加成;超出则谐振崩塌,可重试(晶体不损耗)。
|
||||
// 5. 成功 → 释放晶体奖励 + 洞见 + 接触进度 + 可能触发叙事碎片。
|
||||
//
|
||||
// 设计意图:把放置游戏的「等待」变成有节奏的空间规划小谜题,兼具策略与爽快感。
|
||||
|
||||
import type {
|
||||
CrystalTier,
|
||||
DecodeNode,
|
||||
DecodePuzzle,
|
||||
ResonanceColor,
|
||||
} from "./types";
|
||||
import { DECODE_CONFIG, RESONANCE_COLORS } from "./config";
|
||||
|
||||
/** 简单可复现随机(mulberry32) */
|
||||
function makeRng(seed: number) {
|
||||
let a = seed >>> 0;
|
||||
return () => {
|
||||
a |= 0;
|
||||
a = (a + 0x6d2b79f5) | 0;
|
||||
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
||||
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成一个**保证有解**的解码谜题。
|
||||
*
|
||||
* 算法(路径构造法):
|
||||
* 1. 先随机生成目标序列 target。
|
||||
* 2. 在网格上**主动构造一条合法路径**:随机起点,每步走向一个未使用的四邻接节点,
|
||||
* 长度 = target.length。构造成功后,把路径上第 i 个节点颜色**强制设为 target[i]**。
|
||||
* 3. 其余非路径节点随机填色(偏向目标色,提升视觉丰富度与多解性)。
|
||||
*
|
||||
* 这样至少存在一条解(即构造的路径),玩家不会遇到「怎么都过不了」的死局。
|
||||
* 同时由于其余节点随机,玩家可能发现别的解路径,保留探索乐趣。
|
||||
*/
|
||||
export function generatePuzzle(tier: CrystalTier, seed?: number): DecodePuzzle {
|
||||
const cfg = DECODE_CONFIG[tier];
|
||||
const s = seed ?? Math.floor(Math.random() * 1e9);
|
||||
const rng = makeRng(s);
|
||||
|
||||
const { rows, cols, targetLen } = cfg;
|
||||
const total = rows * cols;
|
||||
|
||||
// 目标序列:从 4 色中随机选 targetLen 个(允许重复,但保证至少 2 种色以增加策略)
|
||||
const target: ResonanceColor[] = [];
|
||||
for (let i = 0; i < targetLen; i++) {
|
||||
target.push(RESONANCE_COLORS[Math.floor(rng() * RESONANCE_COLORS.length)]);
|
||||
}
|
||||
if (new Set(target).size < 2) {
|
||||
target[target.length - 1] =
|
||||
RESONANCE_COLORS[(RESONANCE_COLORS.indexOf(target[0]) + 1) % RESONANCE_COLORS.length];
|
||||
}
|
||||
|
||||
// 邻居计算
|
||||
const neighborsOf = (id: number): number[] => {
|
||||
const r = Math.floor(id / cols);
|
||||
const c = id % cols;
|
||||
const ns: number[] = [];
|
||||
if (r > 0) ns.push((r - 1) * cols + c);
|
||||
if (r < rows - 1) ns.push((r + 1) * cols + c);
|
||||
if (c > 0) ns.push(r * cols + (c - 1));
|
||||
if (c < cols - 1) ns.push(r * cols + (c + 1));
|
||||
return ns;
|
||||
};
|
||||
|
||||
// 构造一条保证可解的路径(多次尝试)
|
||||
let pathIds: number[] | null = null;
|
||||
for (let attempt = 0; attempt < 300 && !pathIds; attempt++) {
|
||||
const used = new Set<number>();
|
||||
const path: number[] = [];
|
||||
const start = Math.floor(rng() * total);
|
||||
used.add(start);
|
||||
path.push(start);
|
||||
let cur = start;
|
||||
let ok = true;
|
||||
for (let i = 1; i < target.length; i++) {
|
||||
const ns = neighborsOf(cur).filter((n) => !used.has(n));
|
||||
if (ns.length === 0) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
cur = ns[Math.floor(rng() * ns.length)];
|
||||
used.add(cur);
|
||||
path.push(cur);
|
||||
}
|
||||
if (ok) pathIds = path;
|
||||
}
|
||||
// 极端兜底(理论不会触发):线性路径
|
||||
if (!pathIds) {
|
||||
pathIds = Array.from({ length: target.length }, (_, i) => i);
|
||||
}
|
||||
|
||||
// 路径节点颜色映射
|
||||
const pathColor = new Map<number, ResonanceColor>();
|
||||
pathIds.forEach((id, i) => pathColor.set(id, target[i]));
|
||||
|
||||
// 生成网格
|
||||
const grid: DecodeNode[] = [];
|
||||
for (let r = 0; r < rows; r++) {
|
||||
for (let c = 0; c < cols; c++) {
|
||||
const id = r * cols + c;
|
||||
let color: ResonanceColor;
|
||||
if (pathColor.has(id)) {
|
||||
color = pathColor.get(id)!;
|
||||
} else {
|
||||
// 非路径节点:60% 偏向目标色(提升多解性),40% 随机全色
|
||||
if (rng() < 0.6) {
|
||||
color = target[Math.floor(rng() * target.length)];
|
||||
} else {
|
||||
color = RESONANCE_COLORS[Math.floor(rng() * RESONANCE_COLORS.length)];
|
||||
}
|
||||
}
|
||||
grid.push({ id, row: r, col: c, color, used: false });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tier,
|
||||
grid,
|
||||
rows,
|
||||
cols,
|
||||
target,
|
||||
path: [],
|
||||
stepLimit: cfg.stepLimitBase,
|
||||
seed: s,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 精确判断当前局面是否仍可解。
|
||||
* 从当前路径末端出发,DFS 搜索是否存在一条按 target 剩余顺序、四邻接、不重复的完成路径。
|
||||
* 用于玩家点击后检测「此路不通」,提示撤销。
|
||||
*/
|
||||
export function isSolvable(puzzle: DecodePuzzle): boolean {
|
||||
const remaining = puzzle.target.slice(puzzle.path.length);
|
||||
if (remaining.length === 0) return true;
|
||||
|
||||
const used = new Set(puzzle.path);
|
||||
const neighborsOf = (id: number): number[] => {
|
||||
const node = puzzle.grid.find((n) => n.id === id);
|
||||
if (!node) return [];
|
||||
const ns: number[] = [];
|
||||
if (node.row > 0) ns.push((node.row - 1) * puzzle.cols + node.col);
|
||||
if (node.row < puzzle.rows - 1) ns.push((node.row + 1) * puzzle.cols + node.col);
|
||||
if (node.col > 0) ns.push(node.row * puzzle.cols + (node.col - 1));
|
||||
if (node.col < puzzle.cols - 1) ns.push(node.row * puzzle.cols + (node.col + 1));
|
||||
return ns;
|
||||
};
|
||||
|
||||
// 若尚未落子,任意颜色匹配 remaining[0] 的未使用节点都可作为起点
|
||||
const starts: number[] =
|
||||
puzzle.path.length === 0
|
||||
? puzzle.grid.filter((n) => !used.has(n.id) && n.color === remaining[0]).map((n) => n.id)
|
||||
: [puzzle.path[puzzle.path.length - 1]];
|
||||
|
||||
const dfs = (curId: number, idx: number): boolean => {
|
||||
if (idx >= remaining.length) return true;
|
||||
const nextColor = remaining[idx];
|
||||
for (const nid of neighborsOf(curId)) {
|
||||
if (used.has(nid)) continue;
|
||||
const n = puzzle.grid.find((x) => x.id === nid);
|
||||
if (!n || n.color !== nextColor) continue;
|
||||
// 起点情况:第一步 idx=0 时,curId 本身要匹配 nextColor
|
||||
if (idx === 0 && puzzle.path.length === 0) {
|
||||
const cur = puzzle.grid.find((x) => x.id === curId);
|
||||
if (!cur || cur.color !== nextColor) continue;
|
||||
}
|
||||
used.add(nid);
|
||||
if (dfs(nid, idx + 1)) return true;
|
||||
used.delete(nid);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
for (const st of starts) {
|
||||
if (puzzle.path.length === 0) {
|
||||
// 起点自身需匹配 remaining[0]
|
||||
const cur = puzzle.grid.find((n) => n.id === st);
|
||||
if (!cur || cur.color !== remaining[0]) continue;
|
||||
used.add(st);
|
||||
if (dfs(st, 1)) return true;
|
||||
used.delete(st);
|
||||
} else {
|
||||
if (dfs(st, 0)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 两节点是否四邻接 */
|
||||
export function isAdjacent(a: DecodeNode, b: DecodeNode): boolean {
|
||||
return Math.abs(a.row - b.row) + Math.abs(a.col - b.col) === 1;
|
||||
}
|
||||
|
||||
/** 尝试点击节点。返回 { ok, finished, failReason } */
|
||||
export function tryClickNode(
|
||||
puzzle: DecodePuzzle,
|
||||
nodeId: number
|
||||
): { ok: boolean; finished: boolean; failReason?: string } {
|
||||
const node = puzzle.grid.find((n) => n.id === nodeId);
|
||||
if (!node) return { ok: false, finished: false, failReason: "无效节点" };
|
||||
if (node.used) return { ok: false, finished: false, failReason: "节点已使用" };
|
||||
|
||||
const stepIdx = puzzle.path.length;
|
||||
// 颜色须匹配目标序列当前位置
|
||||
if (node.color !== puzzle.target[stepIdx]) {
|
||||
return { ok: false, finished: false, failReason: "谐振不匹配" };
|
||||
}
|
||||
// 非首步须邻接
|
||||
if (stepIdx > 0) {
|
||||
const prev = puzzle.grid.find((n) => n.id === puzzle.path[stepIdx - 1])!;
|
||||
if (!isAdjacent(prev, node)) {
|
||||
return { ok: false, finished: false, failReason: "节点不相邻" };
|
||||
}
|
||||
}
|
||||
// 步数上限
|
||||
if (stepIdx >= puzzle.stepLimit) {
|
||||
return { ok: false, finished: false, failReason: "步数耗尽" };
|
||||
}
|
||||
|
||||
node.used = true;
|
||||
puzzle.path.push(nodeId);
|
||||
|
||||
const finished = puzzle.path.length >= puzzle.target.length;
|
||||
return { ok: true, finished };
|
||||
}
|
||||
|
||||
/** 是否已无解 — 复用精确的 isSolvable 判定 */
|
||||
export function isStuck(puzzle: DecodePuzzle): boolean {
|
||||
if (puzzle.path.length >= puzzle.target.length) return false;
|
||||
if (puzzle.path.length >= puzzle.stepLimit) return true;
|
||||
return !isSolvable(puzzle);
|
||||
}
|
||||
|
||||
/**
|
||||
* 假设从某节点作为第一步落子,局面是否仍可解。
|
||||
* 用于在选起点阶段高亮「可行起点」,避免玩家选到死起点。
|
||||
*/
|
||||
export function canStartFrom(puzzle: DecodePuzzle, nodeId: number): boolean {
|
||||
if (puzzle.path.length !== 0) return false;
|
||||
const node = puzzle.grid.find((n) => n.id === nodeId);
|
||||
if (!node || node.used) return false;
|
||||
if (node.color !== puzzle.target[0]) return false;
|
||||
const cp: DecodePuzzle = JSON.parse(JSON.stringify(puzzle));
|
||||
const n = cp.grid.find((x) => x.id === nodeId)!;
|
||||
n.used = true;
|
||||
cp.path.push(nodeId);
|
||||
return isSolvable(cp);
|
||||
}
|
||||
|
||||
/** 重置谜题(保留同一晶体,重新挑战) */
|
||||
export function resetPuzzle(puzzle: DecodePuzzle): DecodePuzzle {
|
||||
return generatePuzzle(puzzle.tier, puzzle.seed + 1);
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
// 回响星核 / Echo Nexus — 核心引擎:产能聚合、技术效果、飞升
|
||||
import type { GameState, PrestigeBonus, ResonanceColor, CrystalTier } from "./types";
|
||||
import {
|
||||
INITIAL_STATE,
|
||||
TECH_TREE,
|
||||
PRESTIGE,
|
||||
CRYSTAL_VALUE,
|
||||
CONTACT,
|
||||
} from "./config";
|
||||
|
||||
/** 由技术树 + 飞升蓝图聚合计算产能字段 */
|
||||
export function recomputeStats(state: Partial<GameState>): {
|
||||
crystalsPerSec: number;
|
||||
crystalCap: number;
|
||||
pulsePower: number;
|
||||
offlineEff: number;
|
||||
insightMult: number;
|
||||
contactRateMult: number;
|
||||
autoDecode: boolean;
|
||||
decodeStepsBonus: number;
|
||||
} {
|
||||
const tech = state.tech ?? {};
|
||||
const bp = state.blueprints?.length ?? 0;
|
||||
|
||||
let crystalsPerSec = INITIAL_STATE.crystalsPerSec;
|
||||
let crystalCap = INITIAL_STATE.crystalCap;
|
||||
let pulsePower = INITIAL_STATE.pulsePower;
|
||||
let offlineEff = INITIAL_STATE.offlineEff;
|
||||
let insightMult = INITIAL_STATE.insightMult;
|
||||
let contactRateMult = 1;
|
||||
let autoDecode = false;
|
||||
let decodeStepsBonus = 0;
|
||||
|
||||
for (const node of TECH_TREE) {
|
||||
const lvl = tech[node.id] ?? 0;
|
||||
if (!lvl) continue;
|
||||
const eff = node.effect;
|
||||
switch (eff.kind) {
|
||||
case "crystalsPerSec":
|
||||
crystalsPerSec += eff.value * lvl;
|
||||
break;
|
||||
case "crystalCap":
|
||||
crystalCap += eff.value * lvl;
|
||||
break;
|
||||
case "pulsePower":
|
||||
pulsePower += eff.value * lvl;
|
||||
break;
|
||||
case "offlineEff":
|
||||
offlineEff = Math.min(1, offlineEff + eff.value * lvl);
|
||||
break;
|
||||
case "insightMult":
|
||||
insightMult += eff.value * lvl;
|
||||
break;
|
||||
case "contactRate":
|
||||
contactRateMult += eff.value * lvl;
|
||||
break;
|
||||
case "decodeSteps":
|
||||
decodeStepsBonus += eff.value * lvl;
|
||||
break;
|
||||
case "autoDecode":
|
||||
autoDecode = lvl > 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 飞升蓝图加成
|
||||
crystalsPerSec *= 1 + bp * PRESTIGE.perBlueprint.crystalsPerSecMult;
|
||||
insightMult *= 1 + bp * PRESTIGE.perBlueprint.insightMult;
|
||||
contactRateMult *= 1 + bp * PRESTIGE.perBlueprint.contactRateMult;
|
||||
|
||||
return {
|
||||
crystalsPerSec,
|
||||
crystalCap,
|
||||
pulsePower,
|
||||
offlineEff,
|
||||
insightMult,
|
||||
contactRateMult,
|
||||
autoDecode,
|
||||
decodeStepsBonus,
|
||||
};
|
||||
}
|
||||
|
||||
/** 计算飞升加成概览(展示用) */
|
||||
export function computePrestigeBonus(state: GameState): PrestigeBonus {
|
||||
const bp = state.blueprints.length;
|
||||
return {
|
||||
crystalsPerSecMult: bp * PRESTIGE.perBlueprint.crystalsPerSecMult,
|
||||
insightMult: bp * PRESTIGE.perBlueprint.insightMult,
|
||||
contactRateMult: bp * PRESTIGE.perBlueprint.contactRateMult,
|
||||
};
|
||||
}
|
||||
|
||||
/** 本次飞升可获得的蓝图数 */
|
||||
export function computeNewBlueprints(state: GameState): number {
|
||||
const earned = Math.floor(state.totalDecoded / PRESTIGE.blueprintsPerDecode);
|
||||
return Math.max(0, Math.min(PRESTIGE.maxBlueprints, earned) - state.blueprints.length);
|
||||
}
|
||||
|
||||
/** 执行飞升:重置数值,保留蓝图与图谱与部分技术 */
|
||||
export function performPrestige(state: GameState): GameState {
|
||||
const newBp = computeNewBlueprints(state);
|
||||
const blueprints = [
|
||||
...state.blueprints,
|
||||
...Array.from({ length: newBp }, (_, i) => `bp_${Date.now()}_${i}`),
|
||||
].slice(0, PRESTIGE.maxBlueprints);
|
||||
|
||||
// 保留:fragments, totalDecoded(累计), ascensions+1, blueprints, theme/sound
|
||||
// 重置:资源、技术、产能、pendingCrystals、activePuzzle、contact、lastTick
|
||||
const fresh = createInitialState();
|
||||
const stats = recomputeStats({ tech: {}, blueprints });
|
||||
return {
|
||||
...fresh,
|
||||
fragments: state.fragments,
|
||||
totalDecoded: state.totalDecoded,
|
||||
ascensions: state.ascensions + 1,
|
||||
blueprints,
|
||||
theme: state.theme,
|
||||
soundOn: state.soundOn,
|
||||
createdAt: state.createdAt,
|
||||
lastTick: Date.now(),
|
||||
...stats,
|
||||
};
|
||||
}
|
||||
|
||||
/** 创建初始状态 */
|
||||
export function createInitialState(): GameState {
|
||||
return {
|
||||
...INITIAL_STATE,
|
||||
tech: {},
|
||||
fragments: {},
|
||||
pendingCrystals: [],
|
||||
activePuzzle: null,
|
||||
createdAt: Date.now(),
|
||||
lastTick: Date.now(),
|
||||
} as GameState;
|
||||
}
|
||||
|
||||
/** 计算解码奖励 */
|
||||
export function decodeRewards(
|
||||
tier: CrystalTier,
|
||||
insightMult: number,
|
||||
contactRateMult: number
|
||||
): { crystals: number; insights: number; contact: number } {
|
||||
const base = CRYSTAL_VALUE[tier];
|
||||
return {
|
||||
crystals: base.crystals,
|
||||
insights: Math.ceil(base.insights * insightMult),
|
||||
contact: +(CONTACT.basePerDecode * tier * contactRateMult).toFixed(2),
|
||||
};
|
||||
}
|
||||
|
||||
/** 离线收益计算 */
|
||||
export function computeOfflineGain(
|
||||
state: GameState,
|
||||
now: number
|
||||
): { gain: number; seconds: number; capped: boolean } {
|
||||
const elapsed = Math.max(0, (now - state.lastTick) / 1000);
|
||||
const cap = 8 * 3600; // 8h
|
||||
const seconds = Math.min(elapsed, cap);
|
||||
const gain = state.crystalsPerSec * seconds * state.offlineEff;
|
||||
return { gain, seconds, capped: elapsed > cap };
|
||||
}
|
||||
|
||||
/** 生成一颗晶体(按 tier 权重) */
|
||||
export function rollCrystalTier(rng: () => number = Math.random): CrystalTier {
|
||||
const r = rng();
|
||||
if (r < 0.7) return 1;
|
||||
if (r < 0.95) return 2;
|
||||
return 3;
|
||||
}
|
||||
|
||||
export type { ResonanceColor };
|
||||
@@ -0,0 +1,126 @@
|
||||
// 回响星核 / Echo Nexus — 核心类型定义
|
||||
|
||||
/** 资源类型 */
|
||||
export type ResourceType = "crystals" | "insights" | "energy";
|
||||
|
||||
/** 晶体稀有度(决定解码阵列规模与奖励) */
|
||||
export type CrystalTier = 1 | 2 | 3;
|
||||
|
||||
/** 共振颜色(解码谜题用)— 全息色谱,刻意避开蓝/靛 */
|
||||
export type ResonanceColor = "emerald" | "rose" | "amber" | "fuchsia";
|
||||
|
||||
/** 记忆晶体(待解码) */
|
||||
export interface Crystal {
|
||||
id: string;
|
||||
tier: CrystalTier;
|
||||
/** 解码奖励倍率 */
|
||||
value: number;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
/** 技术分支 */
|
||||
export type TechBranch = "mining" | "decoding" | "expedition" | "narrative";
|
||||
|
||||
/** 技术节点定义 */
|
||||
export interface TechNode {
|
||||
id: string;
|
||||
branch: TechBranch;
|
||||
level: number;
|
||||
name: string;
|
||||
desc: string;
|
||||
cost: number; // insights
|
||||
effect: TechEffect;
|
||||
}
|
||||
|
||||
export interface TechEffect {
|
||||
kind:
|
||||
| "crystalsPerSec"
|
||||
| "crystalCap"
|
||||
| "offlineEff"
|
||||
| "decodeSteps"
|
||||
| "autoDecode"
|
||||
| "pulsePower"
|
||||
| "contactRate"
|
||||
| "insightMult";
|
||||
value: number;
|
||||
}
|
||||
|
||||
/** 记忆碎片(叙事) */
|
||||
export interface Fragment {
|
||||
id: string;
|
||||
era: number; // 1-5 纪元
|
||||
title: string;
|
||||
/** 拼出后的回响文本 */
|
||||
echo: string;
|
||||
/** 解锁所需晶体解码次数门槛 */
|
||||
threshold: number;
|
||||
}
|
||||
|
||||
/** 飞升(Prestige)加成 */
|
||||
export interface PrestigeBonus {
|
||||
crystalsPerSecMult: number;
|
||||
insightMult: number;
|
||||
contactRateMult: number;
|
||||
}
|
||||
|
||||
/** 解码阵列节点 */
|
||||
export interface DecodeNode {
|
||||
id: number;
|
||||
row: number;
|
||||
col: number;
|
||||
color: ResonanceColor;
|
||||
used: boolean;
|
||||
}
|
||||
|
||||
/** 解码谜题状态 */
|
||||
export interface DecodePuzzle {
|
||||
tier: CrystalTier;
|
||||
grid: DecodeNode[];
|
||||
rows: number;
|
||||
cols: number;
|
||||
/** 目标共振序列(玩家需按此顺序点击同色相邻节点) */
|
||||
target: ResonanceColor[];
|
||||
/** 已点击节点序列 */
|
||||
path: number[];
|
||||
stepLimit: number;
|
||||
seed: number;
|
||||
}
|
||||
|
||||
/** 完整游戏状态 */
|
||||
export interface GameState {
|
||||
// 资源
|
||||
crystals: number;
|
||||
insights: number;
|
||||
energy: number;
|
||||
contact: number; // 0-100 接触进度
|
||||
|
||||
// 产能(由技术树 + 飞升计算缓存)
|
||||
crystalsPerSec: number;
|
||||
crystalCap: number;
|
||||
pulsePower: number;
|
||||
offlineEff: number; // 0-1
|
||||
insightMult: number;
|
||||
contactRateMult: number;
|
||||
autoDecode: boolean;
|
||||
|
||||
// 技术
|
||||
tech: Record<string, number>; // techId -> level
|
||||
|
||||
// 记忆
|
||||
fragments: Record<string, boolean>;
|
||||
totalDecoded: number;
|
||||
|
||||
// 飞升
|
||||
ascensions: number;
|
||||
blueprints: string[];
|
||||
|
||||
// 解码
|
||||
pendingCrystals: Crystal[];
|
||||
activePuzzle: DecodePuzzle | null;
|
||||
|
||||
// 元
|
||||
lastTick: number;
|
||||
createdAt: number;
|
||||
theme: "dark" | "light";
|
||||
soundOn: boolean;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
Reference in New Issue
Block a user