216 lines
9.4 KiB
TypeScript
Executable File
216 lines
9.4 KiB
TypeScript
Executable File
// 回响星核 / Echo Nexus — 星图天文台:飞升后元进程(Meta-Progression)
|
||
// 设计动机:回应 Issue #1「就一直连连看?」——飞升后玩家从 3 个随机星座天赋中选 1,
|
||
// 跨周目永久生效。增加策略选择 + 视觉变化(动态星图)。
|
||
import type { GameState } from "./types";
|
||
|
||
/** 星座天赋类别(6 类,每类 3 个天赋 = 18 个) */
|
||
export type ConstellationCategory =
|
||
| "mining" // 翠 - 采矿
|
||
| "decoding" // 玫 - 解码
|
||
| "expedition" // 琥 - 探险
|
||
| "contact" // 紫 - 接触
|
||
| "economy" // 灰 - 经济
|
||
| "cosmic"; // 金 - 宇宙
|
||
|
||
/** 天赋效果(聚合到 recomputeStats 或具体动作处) */
|
||
export interface ConstellationPerk {
|
||
id: string;
|
||
category: ConstellationCategory;
|
||
name: string;
|
||
desc: string;
|
||
/** 顺序序号(同类别 1/2/3,决定星座内的位置) */
|
||
order: number;
|
||
}
|
||
|
||
/** 类别可视化(用于 Canvas 与 CSS) */
|
||
export const CONSTELLATION_CATEGORY_META: Record<
|
||
ConstellationCategory,
|
||
{ name: string; color: string; glow: string; hex: string; icon: string; desc: string }
|
||
> = {
|
||
mining: { name: "永动矿脉", color: "emerald", glow: "rgba(52,211,153,0.55)", hex: "#34d399", icon: "Pickaxe", desc: "晶体产能与仓储" },
|
||
decoding: { name: "光谱矩阵", color: "rose", glow: "rgba(251,113,133,0.55)", hex: "#fb7185", icon: "ScanLine", desc: "解码效率与脉冲" },
|
||
expedition: { name: "远征星图", color: "amber", glow: "rgba(251,191,36,0.55)", hex: "#fbbf24", icon: "Rocket", desc: "探险力与生命" },
|
||
contact: { name: "接触回响", color: "fuchsia", glow: "rgba(232,121,249,0.55)", hex: "#e879f9", icon: "Sparkles", desc: "接触进度与飞升" },
|
||
economy: { name: "虚空市场", color: "slate", glow: "rgba(148,163,184,0.55)", hex: "#94a3b8", icon: "Coins", desc: "经济与离线" },
|
||
cosmic: { name: "宇宙回响", color: "amber", glow: "rgba(251,191,36,0.55)", hex: "#fcd34d", icon: "Star", desc: "全局综合增益" },
|
||
};
|
||
|
||
/** 18 个星座天赋(6 类 × 3) */
|
||
export const CONSTELLATION_PERKS: ConstellationPerk[] = [
|
||
// 翠 · 永动矿脉
|
||
{ id: "c_min_1", category: "mining", order: 1, name: "永动钻头", desc: "晶体/秒 +20%" },
|
||
{ id: "c_min_2", category: "mining", order: 2, name: "深井网络", desc: "仓库上限 +50%" },
|
||
{ id: "c_min_3", category: "mining", order: 3, name: "谐振熔炉", desc: "主动脉冲威力 +50%" },
|
||
// 玫 · 光谱矩阵
|
||
{ id: "c_dec_1", category: "decoding", order: 1, name: "光谱记忆", desc: "洞见倍率 +15%" },
|
||
{ id: "c_dec_2", category: "decoding", order: 2, name: "步幅延展", desc: "解码步数上限 +1" },
|
||
{ id: "c_dec_3", category: "decoding", order: 3, name: "自动校准", desc: "自动解码周期 -3s" },
|
||
// 琥 · 远征星图
|
||
{ id: "c_exp_1", category: "expedition", order: 1, name: "维生护盾", desc: "探险生命 +25%" },
|
||
{ id: "c_exp_2", category: "expedition", order: 2, name: "信标矩阵", desc: "探险力 +20%" },
|
||
{ id: "c_exp_3", category: "expedition", order: 3, name: "能量共振", desc: "能量上限 +1" },
|
||
// 紫 · 接触回响
|
||
{ id: "c_con_1", category: "contact", order: 1, name: "接触共鸣", desc: "接触进度率 +20%" },
|
||
{ id: "c_con_2", category: "contact", order: 2, name: "飞升加速", desc: "飞升所需接触 -10" },
|
||
{ id: "c_con_3", category: "contact", order: 3, name: "蓝图回响", desc: "每个蓝图额外 +2% 全属性" },
|
||
// 灰 · 虚空市场
|
||
{ id: "c_eco_1", category: "economy", order: 1, name: "离线缓存", desc: "离线效率 +15%" },
|
||
{ id: "c_eco_2", category: "economy", order: 2, name: "晶体富集", desc: "T2/T3 晶体出现率 +8%" },
|
||
{ id: "c_eco_3", category: "economy", order: 3, name: "星潮引导", desc: "星潮间隙 -10s" },
|
||
// 金 · 宇宙回响
|
||
{ id: "c_cos_1", category: "cosmic", order: 1, name: "飞升礼包", desc: "每次飞升后获得 +30 晶体 / +5 洞见" },
|
||
{ id: "c_cos_2", category: "cosmic", order: 2, name: "二周目经验", desc: "解码奖励 +10%" },
|
||
{ id: "c_cos_3", category: "cosmic", order: 3, name: "全息共振", desc: "所有产能与脉冲 +8%" },
|
||
];
|
||
|
||
/** 由 ID 取天赋 */
|
||
export function getPerk(id: string): ConstellationPerk | undefined {
|
||
return CONSTELLATION_PERKS.find((p) => p.id === id);
|
||
}
|
||
|
||
/** 聚合玩家已解锁天赋的总修饰器(供 recomputeStats / 具体动作使用) */
|
||
export interface ConstellationModifiers {
|
||
crystalsPerSecMult: number;
|
||
crystalCapMult: number;
|
||
pulsePowerMult: number;
|
||
insightMultAdd: number;
|
||
decodeStepsBonus: number;
|
||
autoDecodeIntervalDeltaSec: number;
|
||
expeditionHpMult: number;
|
||
expeditionPowerMult: number;
|
||
energyMaxBonus: number;
|
||
contactRateMult: number;
|
||
prestigeContactMinDelta: number;
|
||
perBlueprintAllStatsPct: number;
|
||
offlineEffBonus: number;
|
||
t2t3BonusRate: number;
|
||
tideGapDeltaSec: number;
|
||
prestigeStartCrystals: number;
|
||
prestigeStartInsights: number;
|
||
decodeRewardMult: number;
|
||
allProductionMult: number;
|
||
}
|
||
|
||
export function constellationBonuses(perks: string[]): ConstellationModifiers {
|
||
const owned = new Set(perks);
|
||
const m: ConstellationModifiers = {
|
||
crystalsPerSecMult: 1,
|
||
crystalCapMult: 1,
|
||
pulsePowerMult: 1,
|
||
insightMultAdd: 0,
|
||
decodeStepsBonus: 0,
|
||
autoDecodeIntervalDeltaSec: 0,
|
||
expeditionHpMult: 1,
|
||
expeditionPowerMult: 1,
|
||
energyMaxBonus: 0,
|
||
contactRateMult: 1,
|
||
prestigeContactMinDelta: 0,
|
||
perBlueprintAllStatsPct: 0,
|
||
offlineEffBonus: 0,
|
||
t2t3BonusRate: 0,
|
||
tideGapDeltaSec: 0,
|
||
prestigeStartCrystals: 0,
|
||
prestigeStartInsights: 0,
|
||
decodeRewardMult: 1,
|
||
allProductionMult: 1,
|
||
};
|
||
if (owned.has("c_min_1")) m.crystalsPerSecMult += 0.2;
|
||
if (owned.has("c_min_2")) m.crystalCapMult += 0.5;
|
||
if (owned.has("c_min_3")) m.pulsePowerMult += 0.5;
|
||
if (owned.has("c_dec_1")) m.insightMultAdd += 0.15;
|
||
if (owned.has("c_dec_2")) m.decodeStepsBonus += 1;
|
||
if (owned.has("c_dec_3")) m.autoDecodeIntervalDeltaSec -= 3;
|
||
if (owned.has("c_exp_1")) m.expeditionHpMult += 0.25;
|
||
if (owned.has("c_exp_2")) m.expeditionPowerMult += 0.2;
|
||
if (owned.has("c_exp_3")) m.energyMaxBonus += 1;
|
||
if (owned.has("c_con_1")) m.contactRateMult += 0.2;
|
||
if (owned.has("c_con_2")) m.prestigeContactMinDelta -= 10;
|
||
if (owned.has("c_con_3")) m.perBlueprintAllStatsPct += 2;
|
||
if (owned.has("c_eco_1")) m.offlineEffBonus += 0.15;
|
||
if (owned.has("c_eco_2")) m.t2t3BonusRate += 0.08;
|
||
if (owned.has("c_eco_3")) m.tideGapDeltaSec -= 10;
|
||
if (owned.has("c_cos_1")) {
|
||
m.prestigeStartCrystals += 30;
|
||
m.prestigeStartInsights += 5;
|
||
}
|
||
if (owned.has("c_cos_2")) m.decodeRewardMult += 0.1;
|
||
if (owned.has("c_cos_3")) m.allProductionMult += 0.08;
|
||
return m;
|
||
}
|
||
|
||
/** 生成 3 个可选天赋(从未拥有的中随机抽取,保证类别多样性) */
|
||
export function rollPerkChoices(
|
||
owned: string[],
|
||
rng: () => number = Math.random
|
||
): string[] {
|
||
const ownedSet = new Set(owned);
|
||
const pool = CONSTELLATION_PERKS.filter((p) => !ownedSet.has(p.id));
|
||
if (pool.length === 0) return [];
|
||
// 洗牌
|
||
const shuffled = [...pool].sort(() => rng() - 0.5);
|
||
// 取前 3 个,但尽量保证不同类别
|
||
const picked: ConstellationPerk[] = [];
|
||
const usedCats = new Set<ConstellationCategory>();
|
||
// 第一轮:每类最多 1 个
|
||
for (const p of shuffled) {
|
||
if (picked.length >= 3) break;
|
||
if (!usedCats.has(p.category)) {
|
||
picked.push(p);
|
||
usedCats.add(p.category);
|
||
}
|
||
}
|
||
// 第二轮:补齐
|
||
for (const p of shuffled) {
|
||
if (picked.length >= 3) break;
|
||
if (!picked.includes(p)) picked.push(p);
|
||
}
|
||
return picked.slice(0, 3).map((p) => p.id);
|
||
}
|
||
|
||
/** 给定类别取该类别内已解锁的天赋 order 数 */
|
||
export function countUnlockedInCategory(perks: string[], cat: ConstellationCategory): number {
|
||
const owned = new Set(perks);
|
||
return CONSTELLATION_PERKS.filter((p) => p.category === cat && owned.has(p.id)).length;
|
||
}
|
||
|
||
/** 计算总进度(已解锁 / 18) */
|
||
export function constellationProgress(perks: string[]): { unlocked: number; total: number } {
|
||
return { unlocked: perks.length, total: CONSTELLATION_PERKS.length };
|
||
}
|
||
|
||
/** 飞升所需接触进度(受天赋影响) */
|
||
export function computePrestigeContactMin(state: GameState): number {
|
||
const c = constellationBonuses(state.constellation ?? []);
|
||
return Math.max(60, 100 + c.prestigeContactMinDelta);
|
||
}
|
||
|
||
/** 星图布局:6 个星座中心点(极坐标转笛卡尔) */
|
||
export function getConstellationLayout(centerX: number, centerY: number, radius: number) {
|
||
const cats: ConstellationCategory[] = ["mining", "decoding", "expedition", "contact", "economy", "cosmic"];
|
||
return cats.map((cat, i) => {
|
||
// 六边形布局:每 60° 一个
|
||
const angle = (Math.PI / 3) * i - Math.PI / 2; // 顶部为第一个
|
||
return {
|
||
category: cat,
|
||
cx: centerX + radius * Math.cos(angle),
|
||
cy: centerY + radius * Math.sin(angle),
|
||
meta: CONSTELLATION_CATEGORY_META[cat],
|
||
};
|
||
});
|
||
}
|
||
|
||
/** 取某类别内 3 个星点的局部坐标(相对星座中心) */
|
||
export function getStarsInCategory(cat: ConstellationCategory, cx: number, cy: number, scale = 1) {
|
||
const perks = CONSTELLATION_PERKS.filter((p) => p.category === cat).sort((a, b) => a.order - b.order);
|
||
return perks.map((p, i) => {
|
||
// 三角形排布:order 1 顶点,2/3 底部
|
||
const localY = i === 0 ? -22 * scale : 14 * scale;
|
||
const localX = i === 0 ? 0 : (i === 1 ? -18 * scale : 18 * scale);
|
||
return {
|
||
perk: p,
|
||
x: cx + localX,
|
||
y: cy + localY,
|
||
};
|
||
});
|
||
}
|