d9404a31-1970-4e79-ac9a-f4f9db60350b

This commit is contained in:
2026-06-23 12:47:49 +00:00
parent e193530f24
commit f73b96026c
101 changed files with 12685 additions and 2 deletions
+13
View File
@@ -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
+307
View File
@@ -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";
}
+265
View File
@@ -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);
}
+172
View File
@@ -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 };
+126
View File
@@ -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;
}
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}