feat(v0.3.1): 星图天文台元进程天赋系统 — 18 天赋 / 3 选 1 draft / Canvas 动态星图

- 新增 src/lib/game/constellation.ts:6 类别 × 3 天赋 = 18 个永久天赋,
  constellationBonuses() 聚合 19 项修饰器,rollPerkChoices() 保证不同类别
- 状态扩展:GameState.constellation / pendingPerkChoices
- engine.ts: recomputeStats 聚合三层加成(技术+蓝图+成就+星图),
  performPrestige 触发天赋选择 + 飞升礼包补偿,新增 rollCrystalTierWithBonus
- expedition.ts: 探险力/生命应用星座修饰器
- gameStore.ts: 新增 chooseConstellationPerk + rerollPerkChoices 动作,
  全链路接入(tickTide/autoDecodeTick/clickNode 等用点)
- 新增 ConstellationPanel.tsx: Canvas 动态星图(六边形 6 星座 × 3 星点,
  闪烁/光晕/十字光线/连接线/中心星核呼吸)+ Hover 提示 + 类别图例
- 新增 ConstellationDialog.tsx: 飞升后自动弹出,3 卡片 draft + 重新抽取
- PrestigeDialog: 增加「星图觉醒预告」卡片
- page.tsx: 6 列标签栏 + 顶部「觉醒」按钮 + 统计面板加星图天赋
- audio.ts: 新增 constellation SFX(上升琶音 + 高频闪光)
- achievements.ts: 新增「星图初绘」「六分星辉」2 项成就
- QA: agent-browser + VLM 全流程通过;lint 零错误;编译 < 200ms
- 二次回应 Issue #1:从「连连看」扩展为 6 大玩法层 + 元进程 draft
This commit is contained in:
2026-06-23 14:12:37 +00:00
parent f3d2e53e4f
commit 9f4d839c6b
18 changed files with 1002 additions and 36 deletions
+24
View File
@@ -167,6 +167,30 @@ export const ACHIEVEMENTS: Achievement[] = [
reward: { insights: 8 },
rewardText: "+8 洞见",
},
{
id: "ach_constellation_1",
name: "星图初绘",
desc: "觉醒第 1 道星图天赋",
icon: "✨",
color: "#e879f9",
check: (s) => (s.constellation?.length ?? 0) >= 1,
reward: { insights: 30, crystalsPerSecPct: 5 },
rewardText: "+30 洞见 · 产能 +5%",
},
{
id: "ach_constellation_6",
name: "六分星辉",
desc: "觉醒 6 道星图天赋(每类各 1 道)",
icon: "✺",
color: "#34d399",
check: (s) => {
const c = s.constellation ?? [];
const cats = new Set(c.map((id) => id.split("_")[1]));
return cats.size >= 6 && c.length >= 6;
},
reward: { crystalsPerSecPct: 12, insightPct: 12 },
rewardText: "产能 +12% · 洞见 +12%",
},
];
/** 计算成就提供的永久加成(跨周目保留) */
+11
View File
@@ -19,6 +19,7 @@ type SfxName =
| "achievement" // 成就解锁(亮丽琶音)
| "tideStart" // 星潮降临(神秘扫频)
| "tideEnd" // 星潮结束(柔和消退)
| "constellation" // 星图觉醒(空灵琶音 + 高频闪光)
| "uiHover" // 界面悬停(极轻)
| "uiClick"; // 界面点击(轻确认)
@@ -266,6 +267,16 @@ class AudioEngine {
this.tone(392, 0.5, "triangle", 0.1, 0.22);
break;
}
case "constellation": {
// 星图觉醒:上升琶音 + 高频闪光
this.tone(523.25, 0.18, "sine", 0.16, 0);
this.tone(659.25, 0.2, "sine", 0.16, 0.08);
this.tone(783.99, 0.22, "sine", 0.18, 0.16);
this.tone(1046.5, 0.35, "triangle", 0.16, 0.24);
// 高频闪光
this.tone(2093, 0.15, "sine", 0.08, 0.3);
break;
}
case "uiHover": {
this.tone(880, 0.05, "sine", 0.05);
break;
+2
View File
@@ -32,6 +32,8 @@ export const INITIAL_STATE = {
achievements: {},
activeTide: null,
lastTideEnd: 0,
constellation: [] as string[],
pendingPerkChoices: null as string[] | null,
theme: "dark" as const,
soundOn: true,
};
+215
View File
@@ -0,0 +1,215 @@
// 回响星核 / 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,
};
});
}
+44 -8
View File
@@ -9,8 +9,9 @@ import {
} from "./config";
import { achievementBonuses } from "./achievements";
import { getTideModifiers, type StarTide } from "./starTide";
import { constellationBonuses, rollPerkChoices } from "./constellation";
/** 由技术树 + 飞升蓝图 + 成就 + 星潮聚合计算产能字段 */
/** 由技术树 + 飞升蓝图 + 成就 + 星图天赋 + 星潮聚合计算产能字段 */
export function recomputeStats(state: Partial<GameState>): {
crystalsPerSec: number;
crystalCap: number;
@@ -25,6 +26,7 @@ export function recomputeStats(state: Partial<GameState>): {
const bp = state.blueprints?.length ?? 0;
const ach = achievementBonuses(state.achievements ?? {});
const tideMod = getTideModifiers((state.activeTide as StarTide | null) ?? null);
const cm = constellationBonuses(state.constellation ?? []);
let crystalsPerSec = INITIAL_STATE.crystalsPerSec;
let crystalCap = INITIAL_STATE.crystalCap;
@@ -67,15 +69,25 @@ export function recomputeStats(state: Partial<GameState>): {
}
}
// 飞升蓝图加成
crystalsPerSec *= 1 + bp * PRESTIGE.perBlueprint.crystalsPerSecMult;
insightMult *= 1 + bp * PRESTIGE.perBlueprint.insightMult;
contactRateMult *= 1 + bp * PRESTIGE.perBlueprint.contactRateMult;
// 飞升蓝图加成+ 星图「蓝图回响」额外加成)
const bpAllPct = cm.perBlueprintAllStatsPct / 100;
crystalsPerSec *= 1 + (bp * (PRESTIGE.perBlueprint.crystalsPerSecMult + bpAllPct));
insightMult *= 1 + (bp * (PRESTIGE.perBlueprint.insightMult + bpAllPct));
contactRateMult *= 1 + (bp * (PRESTIGE.perBlueprint.contactRateMult + bpAllPct));
// 成就永久加成(跨周目)
crystalsPerSec *= 1 + ach.crystalsPerSecPct / 100;
insightMult *= 1 + ach.insightPct / 100;
// 星图天赋永久加成(跨周目)
crystalsPerSec *= cm.crystalsPerSecMult * cm.allProductionMult;
crystalCap *= cm.crystalCapMult;
pulsePower *= cm.pulsePowerMult * cm.allProductionMult;
offlineEff = Math.min(1, offlineEff + cm.offlineEffBonus);
insightMult += cm.insightMultAdd;
contactRateMult *= cm.contactRateMult;
decodeStepsBonus += cm.decodeStepsBonus;
// 星潮瞬时修饰(contactRateMult 与 insightMult 进缓存;产能/脉冲/探险在用点处即时乘)
insightMult += tideMod.insightMultAdd;
contactRateMult *= tideMod.contactRateMult;
@@ -108,7 +120,7 @@ export function computeNewBlueprints(state: GameState): number {
return Math.max(0, Math.min(PRESTIGE.maxBlueprints, earned) - state.blueprints.length);
}
/** 执行飞升:重置数值,保留蓝图图谱成就与部分技术 */
/** 执行飞升:重置数值,保留蓝图/图谱/成就/星图天赋,并触发天赋选择 */
export function performPrestige(state: GameState): GameState {
const newBp = computeNewBlueprints(state);
const blueprints = [
@@ -116,10 +128,15 @@ export function performPrestige(state: GameState): GameState {
...Array.from({ length: newBp }, (_, i) => `bp_${Date.now()}_${i}`),
].slice(0, PRESTIGE.maxBlueprints);
// 保留:fragments, totalDecoded(累计), ascensions+1, blueprints, achievements, theme/sound, expeditionLog
// 星图天赋:飞升后获得一次 3 选 1 的机会
const choices = rollPerkChoices(state.constellation ?? []);
// 星图「飞升礼包」天赋的初始资源补偿
const cm = constellationBonuses(state.constellation ?? []);
// 保留:fragments, totalDecoded(累计), ascensions+1, blueprints, achievements, constellation, theme/sound, expeditionLog
// 重置:资源、技术、产能、pendingCrystals、activePuzzle、activeExpedition、contact、lastTick、energy、星潮
const fresh = createInitialState();
const stats = recomputeStats({ tech: {}, blueprints, achievements: state.achievements });
const stats = recomputeStats({ tech: {}, blueprints, achievements: state.achievements, constellation: state.constellation });
return {
...fresh,
fragments: state.fragments,
@@ -127,6 +144,11 @@ export function performPrestige(state: GameState): GameState {
ascensions: state.ascensions + 1,
blueprints,
achievements: state.achievements,
constellation: state.constellation,
pendingPerkChoices: choices.length > 0 ? choices : null,
// 飞升礼包补偿
crystals: cm.prestigeStartCrystals,
insights: cm.prestigeStartInsights,
theme: state.theme,
soundOn: state.soundOn,
createdAt: state.createdAt,
@@ -194,4 +216,18 @@ export function rollCrystalTier(rng: () => number = Math.random): CrystalTier {
return 3;
}
/** 生成一颗晶体(带 T2/T3 概率补偿,受星图「晶体富集」影响) */
export function rollCrystalTierWithBonus(t2t3Bonus: number, rng: () => number = Math.random): CrystalTier {
if (t2t3Bonus <= 0) return rollCrystalTier(rng);
const r = rng();
// 基础:T1=0.7, T2=0.25, T3=0.05
// 补偿:从 T1 中按 bonus 比例挪到 T2/T3
const shift = Math.min(0.5, t2t3Bonus); // 上限 50%
const t1 = 0.7 - 0.7 * shift;
const t2 = 0.25 + 0.25 * shift * 0.7;
if (r < t1) return 1;
if (r < t1 + t2) return 2;
return 3;
}
export type { ResonanceColor };
+8 -1
View File
@@ -9,6 +9,7 @@ import type {
ExpeditionResult,
GameState,
} from "./types";
import { constellationBonuses } from "./constellation";
/** 简单可复现随机(mulberry32 */
function makeRng(seed: number) {
@@ -173,7 +174,7 @@ export function generateExpedition(
};
}
/** 计算探险力(由技术树 + 飞升蓝图) */
/** 计算探险力(由技术树 + 飞升蓝图 + 星图天赋 */
export function computeExpeditionPower(state: GameState): number {
let power = EXPEDITION_CONFIG.basePower;
// 探险分支技术加成
@@ -185,6 +186,9 @@ export function computeExpeditionPower(state: GameState): number {
power *= 1 + (state.blueprints?.length ?? 0) * 0.08;
// 飞升周目加成
power *= 1 + (state.ascensions ?? 0) * 0.15;
// 星图「信标矩阵」加成
const cm = constellationBonuses(state.constellation ?? []);
power *= cm.expeditionPowerMult;
return Math.round(power);
}
@@ -193,6 +197,9 @@ export function computeExpeditionHp(state: GameState): number {
let hp = EXPEDITION_CONFIG.baseHp;
hp += (state.tech?.exp_2 ?? 0) * 20;
hp += (state.ascensions ?? 0) * 10;
// 星图「维生护盾」加成
const cm = constellationBonuses(state.constellation ?? []);
hp = Math.round(hp * cm.expeditionHpMult);
return hp;
}
+4
View File
@@ -132,6 +132,10 @@ export interface GameState {
activeTide: import("./starTide").StarTide | null;
lastTideEnd: number;
// 星图天文台(v0.3.1 元进程)
constellation: string[]; // 已解锁天赋 ID 列表
pendingPerkChoices: string[] | null; // 飞升后待选择(3 选 1
// 元
lastTick: number;
createdAt: number;