v0.7: CrystalOrb Canvas 粒子系统升级 + 角色属性系统(四维)
CrystalOrb 升级(P0 视觉震撼): - 从 CSS 动画重写为 Canvas 2D 粒子系统 - 环绕能量粒子(3层32个,emerald/fuchsia/rose,带拖尾) - 环境星尘(40个,闪烁漂移) - 点击爆发粒子 + 冲击波环(连击解锁更多层) - 晶核辉光呼吸 + 3层六边形纹理 + 鼠标视差 - VLM 视觉评分 8/10 角色属性系统(P1 RPG 深度): - 新增 attributes.ts 逻辑层 + AttributesPanel UI - 四维属性:探索力/智慧/勇气/灵感(emerald/fuchsia/amber/rose) - 两区加成公式(0-50线性,50-100递减) - 飞升获得属性点(ascensions×2+1),手动分配 - 完成活动获得属性经验,自动升级 - 接入 pulse/clickNode/resolveCurrentNode/grantCruiseReward/tickTide - 旧存档兼容(migrateAttributes) - 新增 2 项成就 + 第 8 个标签页「角色」 - VLM 视觉评分 7-8/10 lint 零错误;HTTP 200
This commit is contained in:
+244
-18
@@ -67,6 +67,17 @@ import {
|
||||
type BeaconDailyProgress,
|
||||
} from "@/lib/game/beacon";
|
||||
import { setPendingOfflineReport } from "@/lib/game/offlineReport";
|
||||
import {
|
||||
ATTRIBUTE_HARD_CAP,
|
||||
migrateAttributes,
|
||||
levelUpCheck,
|
||||
getAllBonuses,
|
||||
createInitialAttributes,
|
||||
createInitialAttributeProgress,
|
||||
type AttributeKey,
|
||||
type CharacterAttributes,
|
||||
type AttributeProgress,
|
||||
} from "@/lib/game/attributes";
|
||||
|
||||
interface GameActions {
|
||||
// 生命周期
|
||||
@@ -121,6 +132,10 @@ interface GameActions {
|
||||
// 深空巡航奖励发放(v0.6 — 实时 Canvas 玩法)
|
||||
grantCruiseReward: (rewards: { crystals?: number; insights?: number; contact?: number }) => void;
|
||||
|
||||
// 角色属性(v0.7 P1)
|
||||
allocateAttribute: (attr: AttributeKey, points?: number) => { ok: boolean; leveledUp?: number };
|
||||
gainAttributeExp: (attr: AttributeKey, amount: number) => { leveledUp: number; newLevel: number };
|
||||
|
||||
// 派生
|
||||
canPrestige: () => boolean;
|
||||
}
|
||||
@@ -220,6 +235,8 @@ export const useGameStore = create<Store>()(
|
||||
const pendingPerkChoices = s.pendingPerkChoices ?? null;
|
||||
// v0.4 编年史兼容:补全 chronicle / runStart / bossKills / starTidesEncountered
|
||||
const migrated = migrateChronicleFields(s);
|
||||
// v0.7 角色属性兼容:补全 attributes / attributeProgress / pendingAttrPoints
|
||||
const attrMigrated = migrateAttributes(s);
|
||||
// 星图「能量共振」天赋 +1 能量上限
|
||||
const cm = constellationBonuses(constellation);
|
||||
const energyMax = (INITIAL_STATE.energyMax) + cm.energyMaxBonus;
|
||||
@@ -261,16 +278,19 @@ export const useGameStore = create<Store>()(
|
||||
runStart: migrated.runStart,
|
||||
bossKills: migrated.bossKills,
|
||||
starTidesEncountered: migrated.starTidesEncountered,
|
||||
...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements, activeTide: tide, constellation }),
|
||||
attributes: attrMigrated.attributes,
|
||||
attributeProgress: attrMigrated.attributeProgress,
|
||||
pendingAttrPoints: attrMigrated.pendingAttrPoints,
|
||||
...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements, activeTide: tide, constellation, attributes: attrMigrated.attributes }),
|
||||
});
|
||||
} else {
|
||||
set({ lastTick: now, activePuzzle, achievements, activeTide: tide, lastTideEnd: tide ? lastTideEndRaw : lastTideEndRaw, constellation, pendingPerkChoices, energyMax, chronicle: migrated.chronicle, runStart: migrated.runStart, bossKills: migrated.bossKills, starTidesEncountered: migrated.starTidesEncountered, ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements, activeTide: tide, constellation }) });
|
||||
set({ lastTick: now, activePuzzle, achievements, activeTide: tide, lastTideEnd: tide ? lastTideEndRaw : lastTideEndRaw, constellation, pendingPerkChoices, energyMax, chronicle: migrated.chronicle, runStart: migrated.runStart, bossKills: migrated.bossKills, starTidesEncountered: migrated.starTidesEncountered, attributes: attrMigrated.attributes, attributeProgress: attrMigrated.attributeProgress, pendingAttrPoints: attrMigrated.pendingAttrPoints, ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements, activeTide: tide, constellation, attributes: attrMigrated.attributes }) });
|
||||
}
|
||||
},
|
||||
|
||||
loadOnline: () => {
|
||||
const s = get();
|
||||
set({ ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: s.constellation }) });
|
||||
set({ ...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: s.constellation, attributes: s.attributes }) });
|
||||
},
|
||||
|
||||
hardReset: () => {
|
||||
@@ -282,7 +302,10 @@ export const useGameStore = create<Store>()(
|
||||
const tide = s.activeTide;
|
||||
// 星图「星潮引导」减少间隙
|
||||
const cm = constellationBonuses(s.constellation ?? []);
|
||||
const gap = Math.max(15000, TIDE_CONFIG.gap + cm.tideGapDeltaSec * 1000);
|
||||
// v0.7 灵感:星潮触发概率 +X%(缩短间隙)
|
||||
const am = getAllBonuses(s.attributes ?? {});
|
||||
const tideGapReduction = Math.min(0.3, am.tideTriggerBonus);
|
||||
const gap = Math.max(15000, (TIDE_CONFIG.gap + cm.tideGapDeltaSec * 1000) * (1 - tideGapReduction));
|
||||
// 1) 检查当前星潮是否结束
|
||||
if (tide && now >= tide.endsAt) {
|
||||
const endedType = tide.type;
|
||||
@@ -299,7 +322,7 @@ export const useGameStore = create<Store>()(
|
||||
lastTideEnd: now,
|
||||
insights: newInsights,
|
||||
// 星潮结束后重算 stats(移除 contactRate/insight 修饰)
|
||||
...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: null, constellation: s.constellation }),
|
||||
...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: null, constellation: s.constellation, attributes: s.attributes }),
|
||||
_tideEvents: [...s._tideEvents, event],
|
||||
});
|
||||
return event;
|
||||
@@ -327,7 +350,7 @@ export const useGameStore = create<Store>()(
|
||||
activeTide: newTide,
|
||||
starTidesEncountered: newTidesAll,
|
||||
// 星潮开始后重算 stats(应用 contactRate/insight 修饰)
|
||||
...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: newTide, constellation: s.constellation }),
|
||||
...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: newTide, constellation: s.constellation, attributes: s.attributes }),
|
||||
_tideEvents: [...s._tideEvents, event],
|
||||
});
|
||||
return event;
|
||||
@@ -412,7 +435,10 @@ export const useGameStore = create<Store>()(
|
||||
const mult = 1 + (combo - 1) * 0.15;
|
||||
// 星潮脉冲威力修饰
|
||||
const tideMod = getTideModifiers(s.activeTide);
|
||||
const gain = s.pulsePower * mult * tideMod.pulsePowerMult;
|
||||
// v0.7 灵感:脉冲连击加成 +X%
|
||||
const am = getAllBonuses(s.attributes ?? {});
|
||||
const comboBonusMult = 1 + am.pulseComboBonus * Math.max(0, combo - 1);
|
||||
const gain = s.pulsePower * mult * tideMod.pulsePowerMult * comboBonusMult;
|
||||
set({
|
||||
crystals: Math.min(s.crystalCap, s.crystals + gain),
|
||||
_combo: combo,
|
||||
@@ -420,6 +446,30 @@ export const useGameStore = create<Store>()(
|
||||
});
|
||||
// 深空信标:脉冲任务进度 +1
|
||||
trackBeacon("pulse", 1);
|
||||
// v0.7 角色属性:连击 ≥3 给灵感经验
|
||||
if (combo >= 3) {
|
||||
const expGain = 1 + Math.floor(combo / 2); // 3 连击=2, 5 连击=3, 10 连击=6
|
||||
// 内联经验获取(避免递归调用 set)
|
||||
const prog = s.attributeProgress?.inspiration ?? { exp: 0, level: s.attributes?.inspiration ?? 0 };
|
||||
const nextExp = prog.exp + expGain;
|
||||
const lvlResult = levelUpCheck(
|
||||
{ exp: nextExp, level: s.attributes?.inspiration ?? 0 },
|
||||
ATTRIBUTE_HARD_CAP
|
||||
);
|
||||
const newAttributes: CharacterAttributes = {
|
||||
...(s.attributes ?? createInitialAttributes()),
|
||||
inspiration: lvlResult.newProgress.level,
|
||||
};
|
||||
const newProgress: AttributeProgress = {
|
||||
...(s.attributeProgress ?? createInitialAttributeProgress()),
|
||||
inspiration: lvlResult.newProgress,
|
||||
};
|
||||
set({
|
||||
attributes: newAttributes,
|
||||
attributeProgress: newProgress,
|
||||
...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: s.constellation, attributes: newAttributes }),
|
||||
});
|
||||
}
|
||||
return { gain, combo };
|
||||
},
|
||||
|
||||
@@ -459,6 +509,23 @@ export const useGameStore = create<Store>()(
|
||||
// 解锁碎片
|
||||
const tentative: GameState = { ...s, totalDecoded: newTotal, fragments: { ...s.fragments } };
|
||||
const unlocked = checkFragments(tentative);
|
||||
// v0.7 角色属性:完成解码给智慧经验(tier 越高经验越多)
|
||||
const wisdomExpGain = puzzle.tier * 2;
|
||||
const curAttrs = s.attributes ?? createInitialAttributes();
|
||||
const curProg = s.attributeProgress ?? createInitialAttributeProgress();
|
||||
const progEntry = curProg.wisdom ?? { exp: 0, level: curAttrs.wisdom };
|
||||
const wisdomLvl = levelUpCheck(
|
||||
{ exp: progEntry.exp + wisdomExpGain, level: curAttrs.wisdom },
|
||||
ATTRIBUTE_HARD_CAP
|
||||
);
|
||||
const newAttributes: CharacterAttributes = {
|
||||
...curAttrs,
|
||||
wisdom: wisdomLvl.newProgress.level,
|
||||
};
|
||||
const newProgress: AttributeProgress = {
|
||||
...curProg,
|
||||
wisdom: wisdomLvl.newProgress,
|
||||
};
|
||||
set({
|
||||
activePuzzle: null,
|
||||
crystals: newCrystals,
|
||||
@@ -466,6 +533,9 @@ export const useGameStore = create<Store>()(
|
||||
contact: newContact,
|
||||
totalDecoded: newTotal,
|
||||
fragments: tentative.fragments,
|
||||
attributes: newAttributes,
|
||||
attributeProgress: newProgress,
|
||||
...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: s.constellation, attributes: newAttributes }),
|
||||
});
|
||||
// 深空信标:解码 +1,洞见累计
|
||||
trackBeacon("decode", 1);
|
||||
@@ -520,7 +590,10 @@ export const useGameStore = create<Store>()(
|
||||
const now = Date.now();
|
||||
// 星图「自动校准」减少自动解码周期
|
||||
const cm = constellationBonuses(s.constellation ?? []);
|
||||
const interval = Math.max(5000, 12000 + cm.autoDecodeIntervalDeltaSec * 1000);
|
||||
// v0.7 智慧:自动解码周期 -X%
|
||||
const am = getAllBonuses(s.attributes ?? {});
|
||||
const baseInterval = 12000 + cm.autoDecodeIntervalDeltaSec * 1000;
|
||||
const interval = Math.max(5000, baseInterval * am.autoDecodeIntervalMult);
|
||||
if (now - s._lastAutoDecode < interval) return;
|
||||
// 找一颗 T1 晶体自动解码
|
||||
const idx = s.pendingCrystals.findIndex((c) => c.tier === 1);
|
||||
@@ -537,6 +610,22 @@ export const useGameStore = create<Store>()(
|
||||
const newTotal = s.totalDecoded + 1;
|
||||
const tentative: GameState = { ...s, totalDecoded: newTotal, fragments: { ...s.fragments } };
|
||||
checkFragments(tentative);
|
||||
// v0.7 角色属性:自动解码给智慧经验(少量)
|
||||
const curAttrs = s.attributes ?? createInitialAttributes();
|
||||
const curProg = s.attributeProgress ?? createInitialAttributeProgress();
|
||||
const progEntry = curProg.wisdom ?? { exp: 0, level: curAttrs.wisdom };
|
||||
const wisdomLvl = levelUpCheck(
|
||||
{ exp: progEntry.exp + 1, level: curAttrs.wisdom },
|
||||
ATTRIBUTE_HARD_CAP
|
||||
);
|
||||
const newAttributes: CharacterAttributes = {
|
||||
...curAttrs,
|
||||
wisdom: wisdomLvl.newProgress.level,
|
||||
};
|
||||
const newProgress: AttributeProgress = {
|
||||
...curProg,
|
||||
wisdom: wisdomLvl.newProgress,
|
||||
};
|
||||
set({
|
||||
pendingCrystals: s.pendingCrystals.filter((c) => c.id !== crystal.id),
|
||||
crystals: s.crystals + rewards.crystals,
|
||||
@@ -545,6 +634,9 @@ export const useGameStore = create<Store>()(
|
||||
totalDecoded: newTotal,
|
||||
fragments: tentative.fragments,
|
||||
_lastAutoDecode: now,
|
||||
attributes: newAttributes,
|
||||
attributeProgress: newProgress,
|
||||
...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: s.constellation, attributes: newAttributes }),
|
||||
});
|
||||
// 深空信标:自动解码也算进度
|
||||
trackBeacon("decode", 1);
|
||||
@@ -562,7 +654,7 @@ export const useGameStore = create<Store>()(
|
||||
set({
|
||||
insights: s.insights - node.cost,
|
||||
tech: newTech,
|
||||
...syncStats({ tech: newTech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: s.constellation }),
|
||||
...syncStats({ tech: newTech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: s.constellation, attributes: s.attributes }),
|
||||
});
|
||||
return true;
|
||||
},
|
||||
@@ -577,8 +669,12 @@ export const useGameStore = create<Store>()(
|
||||
return { ok: false, reason: "能量不足" };
|
||||
}
|
||||
const tideMod = getTideModifiers(s.activeTide);
|
||||
const power = computeExpeditionPower(s) + tideMod.expeditionPowerBonus;
|
||||
const hp = computeExpeditionHp(s) + tideMod.expeditionHpBonus;
|
||||
// v0.7 角色属性:探索力 +X% 探险力,勇气 +X 探险生命
|
||||
const am = getAllBonuses(s.attributes ?? {});
|
||||
const basePower = computeExpeditionPower(s) + tideMod.expeditionPowerBonus;
|
||||
const baseHp = computeExpeditionHp(s) + tideMod.expeditionHpBonus;
|
||||
const power = Math.round(basePower * am.expeditionPowerMult);
|
||||
const hp = baseHp + am.expeditionHpBonus;
|
||||
const seed = Math.floor(Math.random() * 1e9);
|
||||
const expedition = generateExpedition(seed, power, hp);
|
||||
set({
|
||||
@@ -594,7 +690,18 @@ export const useGameStore = create<Store>()(
|
||||
if (!s.activeExpedition || s.activeExpedition.finished) return null;
|
||||
// 深拷贝
|
||||
const exp = JSON.parse(JSON.stringify(s.activeExpedition));
|
||||
const result = resolveNode(exp);
|
||||
// v0.7 勇气:BOSS 战胜率 +X%(动态提高 RNG 阈值)
|
||||
const nodeBefore = exp.nodes[exp.currentNode];
|
||||
const isBossNode = nodeBefore?.type === "boss";
|
||||
const am = getAllBonuses(s.attributes ?? {});
|
||||
const result = isBossNode
|
||||
? resolveNode(exp, () => {
|
||||
// 单次 rng() 调用:B% 概率返回 0(必胜),其余情况返回 r-B(保持均匀分布)
|
||||
const r = Math.random();
|
||||
const b = Math.min(0.95, am.bossWinRateBonus);
|
||||
return r < b ? 0 : Math.min(1, r - b);
|
||||
})
|
||||
: resolveNode(exp);
|
||||
// 累计奖励
|
||||
if (result.crystals) exp.rewards.crystals += result.crystals;
|
||||
if (result.insights) exp.rewards.insights += result.insights;
|
||||
@@ -613,7 +720,7 @@ export const useGameStore = create<Store>()(
|
||||
// 日志
|
||||
const logEntry = {
|
||||
expeditionId: exp.id,
|
||||
nodeType: exp.nodes[exp.currentNode]?.type || "combat",
|
||||
nodeType: nodeBefore?.type || "combat",
|
||||
result: result.log,
|
||||
rewards: [
|
||||
result.crystals ? `+${result.crystals}晶体` : "",
|
||||
@@ -632,12 +739,48 @@ export const useGameStore = create<Store>()(
|
||||
|
||||
// v0.4 编年史:击破 BOSS 时累计计数
|
||||
let bossKills = s.bossKills ?? 0;
|
||||
let bossKilledThisNode = false;
|
||||
if (
|
||||
result.ended &&
|
||||
result.endReason === "victory" &&
|
||||
exp.nodes[exp.currentNode]?.type === "boss"
|
||||
nodeBefore?.type === "boss"
|
||||
) {
|
||||
bossKills = bossKills + 1;
|
||||
bossKilledThisNode = true;
|
||||
}
|
||||
|
||||
// v0.7 角色属性:战斗胜利给勇气+探索力经验;BOSS 额外奖励
|
||||
let newAttributes = s.attributes ?? createInitialAttributes();
|
||||
let newProgress = s.attributeProgress ?? createInitialAttributeProgress();
|
||||
let statsNeedResync = false;
|
||||
// 战斗类节点(combat/boss)且胜利:勇气 + 探索力经验
|
||||
const isCombatVictory =
|
||||
(nodeBefore?.type === "combat" || nodeBefore?.type === "boss") &&
|
||||
!result.ended; // 中途战斗胜利(未结束探险)
|
||||
const isExpeditionVictory =
|
||||
result.ended && result.endReason === "victory";
|
||||
if (isCombatVictory || bossKilledThisNode || isExpeditionVictory) {
|
||||
const courageGain = bossKilledThisNode ? 8 : 2;
|
||||
const explorationGain = bossKilledThisNode ? 6 : isExpeditionVictory ? 4 : 1;
|
||||
const courageLvl = levelUpCheck(
|
||||
{ exp: newProgress.courage.exp + courageGain, level: newAttributes.courage },
|
||||
ATTRIBUTE_HARD_CAP
|
||||
);
|
||||
const explLvl = levelUpCheck(
|
||||
{ exp: newProgress.exploration.exp + explorationGain, level: newAttributes.exploration },
|
||||
ATTRIBUTE_HARD_CAP
|
||||
);
|
||||
newAttributes = {
|
||||
...newAttributes,
|
||||
courage: courageLvl.newProgress.level,
|
||||
exploration: explLvl.newProgress.level,
|
||||
};
|
||||
newProgress = {
|
||||
...newProgress,
|
||||
courage: courageLvl.newProgress,
|
||||
exploration: explLvl.newProgress,
|
||||
};
|
||||
statsNeedResync = true;
|
||||
}
|
||||
|
||||
set({
|
||||
@@ -648,11 +791,16 @@ export const useGameStore = create<Store>()(
|
||||
fragments: newFragments,
|
||||
expeditionLog: newLog,
|
||||
bossKills,
|
||||
attributes: newAttributes,
|
||||
attributeProgress: newProgress,
|
||||
...(statsNeedResync
|
||||
? syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: s.constellation, attributes: newAttributes })
|
||||
: {}),
|
||||
});
|
||||
// 深空信标:探险完成(胜负都计)+ 洞见累计 + BOSS 击破
|
||||
if (result.ended) {
|
||||
trackBeacon("expedition", 1);
|
||||
if (result.endReason === "victory" && exp.nodes[exp.currentNode]?.type === "boss") {
|
||||
if (bossKilledThisNode) {
|
||||
trackBeacon("boss", 1);
|
||||
}
|
||||
}
|
||||
@@ -700,7 +848,7 @@ export const useGameStore = create<Store>()(
|
||||
set({
|
||||
...next,
|
||||
energyMax,
|
||||
...syncStats({ tech: next.tech, blueprints: next.blueprints, achievements: next.achievements, activeTide: next.activeTide, constellation: next.constellation }),
|
||||
...syncStats({ tech: next.tech, blueprints: next.blueprints, achievements: next.achievements, activeTide: next.activeTide, constellation: next.constellation, attributes: next.attributes }),
|
||||
_lastAutoDecode: Date.now(),
|
||||
_lastSpawn: Date.now(),
|
||||
_combo: 0,
|
||||
@@ -732,7 +880,7 @@ export const useGameStore = create<Store>()(
|
||||
pendingPerkChoices: null,
|
||||
energyMax,
|
||||
chronicle: newChronicle,
|
||||
...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: newConstellation }),
|
||||
...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: newConstellation, attributes: s.attributes }),
|
||||
});
|
||||
return true;
|
||||
},
|
||||
@@ -771,7 +919,7 @@ export const useGameStore = create<Store>()(
|
||||
insights,
|
||||
contact,
|
||||
...(statsDirty
|
||||
? syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: updated, activeTide: s.activeTide, constellation: s.constellation })
|
||||
? syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: updated, activeTide: s.activeTide, constellation: s.constellation, attributes: s.attributes })
|
||||
: {}),
|
||||
_achievementQueue: [...s._achievementQueue, ...newlyUnlocked],
|
||||
});
|
||||
@@ -806,12 +954,90 @@ export const useGameStore = create<Store>()(
|
||||
const addCrystals = rewards.crystals ?? 0;
|
||||
const addInsights = rewards.insights ?? 0;
|
||||
const addContact = rewards.contact ?? 0;
|
||||
// v0.7 角色属性:巡航通关给探索力+勇气经验(按晶体奖励量缩放)
|
||||
const totalReward = addCrystals + addInsights * 10 + addContact * 10;
|
||||
const expBase = Math.max(2, Math.floor(totalReward / 30));
|
||||
const curAttrs = s.attributes ?? createInitialAttributes();
|
||||
const curProg = s.attributeProgress ?? createInitialAttributeProgress();
|
||||
const explLvl = levelUpCheck(
|
||||
{ exp: curProg.exploration.exp + expBase, level: curAttrs.exploration },
|
||||
ATTRIBUTE_HARD_CAP
|
||||
);
|
||||
const courageLvl = levelUpCheck(
|
||||
{ exp: curProg.courage.exp + Math.floor(expBase * 0.6), level: curAttrs.courage },
|
||||
ATTRIBUTE_HARD_CAP
|
||||
);
|
||||
const newAttributes: CharacterAttributes = {
|
||||
...curAttrs,
|
||||
exploration: explLvl.newProgress.level,
|
||||
courage: courageLvl.newProgress.level,
|
||||
};
|
||||
const newProgress: AttributeProgress = {
|
||||
...curProg,
|
||||
exploration: explLvl.newProgress,
|
||||
courage: courageLvl.newProgress,
|
||||
};
|
||||
set({
|
||||
crystals: Math.min(s.crystalCap, s.crystals + addCrystals),
|
||||
insights: s.insights + Math.round(addInsights),
|
||||
contact: Math.min(100, s.contact + addContact),
|
||||
attributes: newAttributes,
|
||||
attributeProgress: newProgress,
|
||||
...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: s.constellation, attributes: newAttributes }),
|
||||
});
|
||||
},
|
||||
|
||||
// ============ 角色属性系统(v0.7 P1) ============
|
||||
allocateAttribute: (attr, points = 1) => {
|
||||
const s = get();
|
||||
const cur = s.attributes ?? createInitialAttributes();
|
||||
const curVal = cur[attr] ?? 0;
|
||||
if (curVal >= ATTRIBUTE_HARD_CAP) {
|
||||
return { ok: false, leveledUp: 0 };
|
||||
}
|
||||
if ((s.pendingAttrPoints ?? 0) < points) {
|
||||
return { ok: false, leveledUp: 0 };
|
||||
}
|
||||
const alloc = Math.min(points, ATTRIBUTE_HARD_CAP - curVal, s.pendingAttrPoints);
|
||||
const newVal = curVal + alloc;
|
||||
const newAttributes: CharacterAttributes = { ...cur, [attr]: newVal };
|
||||
// 同步经验进度 level 字段(保持一致)
|
||||
const curProg = s.attributeProgress ?? createInitialAttributeProgress();
|
||||
const oldProg = curProg[attr] ?? { exp: 0, level: curVal };
|
||||
const newProgress: AttributeProgress = {
|
||||
...curProg,
|
||||
[attr]: { exp: oldProg.exp, level: newVal },
|
||||
};
|
||||
set({
|
||||
attributes: newAttributes,
|
||||
attributeProgress: newProgress,
|
||||
pendingAttrPoints: s.pendingAttrPoints - alloc,
|
||||
...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: s.constellation, attributes: newAttributes }),
|
||||
});
|
||||
return { ok: true, leveledUp: alloc };
|
||||
},
|
||||
|
||||
gainAttributeExp: (attr, amount) => {
|
||||
const s = get();
|
||||
const cur = s.attributes ?? createInitialAttributes();
|
||||
const curProg = s.attributeProgress ?? createInitialAttributeProgress();
|
||||
const progEntry = curProg[attr] ?? { exp: 0, level: cur[attr] };
|
||||
const result = levelUpCheck(
|
||||
{ exp: progEntry.exp + amount, level: cur[attr] },
|
||||
ATTRIBUTE_HARD_CAP
|
||||
);
|
||||
const newAttributes: CharacterAttributes = { ...cur, [attr]: result.newProgress.level };
|
||||
const newProgress: AttributeProgress = {
|
||||
...curProg,
|
||||
[attr]: result.newProgress,
|
||||
};
|
||||
set({
|
||||
attributes: newAttributes,
|
||||
attributeProgress: newProgress,
|
||||
...syncStats({ tech: s.tech, blueprints: s.blueprints, achievements: s.achievements, activeTide: s.activeTide, constellation: s.constellation, attributes: newAttributes }),
|
||||
});
|
||||
return { leveledUp: result.levelsGained, newLevel: result.newProgress.level };
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "echo-nexus-save-v1",
|
||||
|
||||
Reference in New Issue
Block a user