优化:提升流派感悟消耗优化
This commit is contained in:
1 parent
26e4d9c286
commit
6eb36d627c
14 files changed
+323
-67
No files matched your search
@@ -83,7 +83,7 @@ function ArchiveConversation({
|
||||
messages.push({
|
||||
id: 'catalog',
|
||||
speaker: actor.name,
|
||||
body: `阁中现有${data.definition.methods.map((method) => `《${method.name}》`).join('、')},都可逐卷查问。`,
|
||||
body: `阁中现有${data.definition.methods.map((method) => `${method.name}`).join('、')},都可逐卷查问。`,
|
||||
});
|
||||
if (topic === 'limit' && data)
|
||||
messages.push({
|
||||
|
||||
@@ -199,6 +199,7 @@ function PathDrawer({
|
||||
createMeridianDrafts(state),
|
||||
);
|
||||
const [confirmUnlock, setConfirmUnlock] = useState(false);
|
||||
const [confirmSave, setConfirmSave] = useState(false);
|
||||
const [discardConfirm, setDiscardConfirm] = useState(false);
|
||||
const draftsRef = useRef(drafts);
|
||||
const savedRef = useRef(saved);
|
||||
@@ -220,6 +221,7 @@ function PathDrawer({
|
||||
mergeFreshMeridianState(draftsRef.current, savedRef.current, fresh),
|
||||
);
|
||||
setSaved(fresh);
|
||||
setConfirmSave(false);
|
||||
if (state) setSlot((current) => current ?? state.activeMeridianSlot);
|
||||
}, [state, stateSignature]);
|
||||
|
||||
@@ -289,6 +291,7 @@ function PathDrawer({
|
||||
sectJsonRequest('PUT', { nodeIds: drafts[slot] }),
|
||||
);
|
||||
setSaved((current) => ({ ...current, [slot]: [...drafts[slot]] }));
|
||||
setConfirmSave(false);
|
||||
};
|
||||
|
||||
const footer = discardConfirm ? (
|
||||
@@ -349,6 +352,22 @@ function PathDrawer({
|
||||
busy={busy}
|
||||
onConfirm={() => void unlockLayer()}
|
||||
/>
|
||||
) : meridianFooterAction === 'save' && confirmSave ? (
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 text-sm">
|
||||
<p className="text-crimson">
|
||||
本次选定保存后暂不可更改,重置功能尚未开放。
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<InkButton onClick={() => setConfirmSave(false)}>返回检查</InkButton>
|
||||
<InkButton
|
||||
variant="primary"
|
||||
disabled={busy}
|
||||
onClick={() => void saveDraft()}
|
||||
>
|
||||
{busy ? '保存中' : '确认保存'}
|
||||
</InkButton>
|
||||
</div>
|
||||
</div>
|
||||
) : meridianFooterAction === 'save' ? (
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 text-sm">
|
||||
<p className="text-crimson">
|
||||
@@ -358,9 +377,9 @@ function PathDrawer({
|
||||
<InkButton
|
||||
variant="primary"
|
||||
disabled={busy}
|
||||
onClick={() => void saveDraft()}
|
||||
onClick={() => setConfirmSave(true)}
|
||||
>
|
||||
{busy ? '保存中' : `保存${terms.meridianLoadout}`}
|
||||
{`保存${terms.meridianLoadout}`}
|
||||
</InkButton>
|
||||
</div>
|
||||
) : meridianFooterAction === 'resolve-dirty' ? (
|
||||
@@ -405,6 +424,7 @@ function PathDrawer({
|
||||
onChange={(value) => {
|
||||
setTab(value as DrawerTab);
|
||||
setConfirmUnlock(false);
|
||||
setConfirmSave(false);
|
||||
setDiscardConfirm(false);
|
||||
}}
|
||||
/>
|
||||
@@ -520,18 +540,24 @@ function PathDrawer({
|
||||
)}
|
||||
</section>
|
||||
|
||||
{state ? (
|
||||
{state &&
|
||||
StandardSectRules.enabledMeridianLoadoutSlots.length > 1 ? (
|
||||
<InkTabs
|
||||
items={StandardSectRules.meridianLoadoutSlots.map((value) => ({
|
||||
value: String(value),
|
||||
label: `${terms.meridianLoadout}${value}${state.activeMeridianSlot === value ? ' · 当前' : ''}${isMeridianDraftDirty(drafts, saved, value) ? ' · 未保存' : ''}`,
|
||||
}))}
|
||||
items={StandardSectRules.enabledMeridianLoadoutSlots.map(
|
||||
(value) => ({
|
||||
value: String(value),
|
||||
label: `${terms.meridianLoadout}${value}${state.activeMeridianSlot === value ? ' · 当前' : ''}${isMeridianDraftDirty(drafts, saved, value) ? ' · 未保存' : ''}`,
|
||||
}),
|
||||
)}
|
||||
activeValue={String(slot)}
|
||||
onChange={(value) => setSlot(Number(value) as MeridianSlot)}
|
||||
onChange={(value) => {
|
||||
setConfirmSave(false);
|
||||
setSlot(Number(value) as MeridianSlot);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
) : !state ? (
|
||||
<InkNotice>参悟第一层后,方可配置参悟方案。</InkNotice>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
<div className="space-y-3">
|
||||
{[...path.layers]
|
||||
@@ -550,13 +576,23 @@ function PathDrawer({
|
||||
.filter((node) => node.layerId === layer.id)
|
||||
.map((node) => {
|
||||
const selected = drafts[slot].includes(node.id);
|
||||
const savedNodeId = saved[slot].find(
|
||||
(nodeId) =>
|
||||
path.nodes.find(
|
||||
(candidate) => candidate.id === nodeId,
|
||||
)?.layerId === layer.id,
|
||||
);
|
||||
const selectionLocked = Boolean(savedNodeId);
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={node.id}
|
||||
disabled={!available || busy}
|
||||
disabled={
|
||||
!available || busy || selectionLocked
|
||||
}
|
||||
onClick={() => {
|
||||
setDiscardConfirm(false);
|
||||
setConfirmSave(false);
|
||||
setDrafts((current) => ({
|
||||
...current,
|
||||
[slot]: toggleMeridianNode({
|
||||
@@ -566,9 +602,12 @@ function PathDrawer({
|
||||
}),
|
||||
}));
|
||||
}}
|
||||
className={`flex h-full flex-col items-stretch justify-start p-3 text-left text-sm leading-6 ${selected ? 'bg-crimson/10 text-crimson' : 'bg-ink/5'} ${available ? '' : 'cursor-not-allowed opacity-50'}`}
|
||||
className={`flex h-full flex-col items-stretch justify-start p-3 text-left text-sm leading-6 ${selected ? 'bg-crimson/10 text-crimson' : 'bg-ink/5'} ${available && !selectionLocked ? '' : 'cursor-not-allowed opacity-50'}`}
|
||||
>
|
||||
<strong className="block">{node.name}</strong>
|
||||
<strong className="block">
|
||||
{node.name}
|
||||
{savedNodeId === node.id ? ' · 已锁定' : ''}
|
||||
</strong>
|
||||
<p className="mt-1">
|
||||
{preview.nodes.find(
|
||||
(previewNode) => previewNode.id === node.id,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
MeridianLoadoutSpecification,
|
||||
MethodTrainingSpecification,
|
||||
SectTradition,
|
||||
StandardSectRules,
|
||||
type CultivatorSectState,
|
||||
type SectPathDefinition,
|
||||
type SectRuntime,
|
||||
@@ -197,15 +198,19 @@ export class SectTraditionApplicationService {
|
||||
slot: number,
|
||||
nodeIds: string[],
|
||||
) {
|
||||
const normalizedSlot = this.requireSlot(slot);
|
||||
const normalizedSlot = this.requireEnabledSlot(slot);
|
||||
const { sect, path, pathState } = await this.requirePath(
|
||||
cultivatorId,
|
||||
pathId,
|
||||
);
|
||||
let validated: string[];
|
||||
try {
|
||||
validated = this.meridianLoadout.validate({
|
||||
validated = this.meridianLoadout.validateUpdate({
|
||||
path,
|
||||
currentNodeIds:
|
||||
pathState.meridianLoadouts.find(
|
||||
(loadout) => loadout.slot === normalizedSlot,
|
||||
)?.nodeIds ?? [],
|
||||
nodeIds,
|
||||
unlockedLayerIds: pathState.unlockedLayerIds,
|
||||
methods: sect.methods,
|
||||
@@ -236,7 +241,7 @@ export class SectTraditionApplicationService {
|
||||
pathId: string,
|
||||
slot: number,
|
||||
) {
|
||||
const normalizedSlot = this.requireSlot(slot);
|
||||
const normalizedSlot = this.requireEnabledSlot(slot);
|
||||
const { sect } = await this.requirePath(cultivatorId, pathId);
|
||||
SectTradition.rehydrate(sect).activateMeridianLoadout(
|
||||
pathId,
|
||||
@@ -325,9 +330,19 @@ export class SectTraditionApplicationService {
|
||||
);
|
||||
}
|
||||
|
||||
private requireSlot(slot: number): 1 | 2 | 3 {
|
||||
private requireEnabledSlot(slot: number): 1 | 2 | 3 {
|
||||
if (slot !== 1 && slot !== 2 && slot !== 3)
|
||||
throw new SectError('SECT_INVALID_MERIDIAN', '经脉方案槽无效', 400);
|
||||
if (
|
||||
!(StandardSectRules.enabledMeridianLoadoutSlots as readonly number[]).includes(
|
||||
slot,
|
||||
)
|
||||
)
|
||||
throw new SectError(
|
||||
'SECT_INVALID_MERIDIAN',
|
||||
'参悟方案暂未开放',
|
||||
400,
|
||||
);
|
||||
return slot;
|
||||
}
|
||||
|
||||
|
||||
@@ -95,13 +95,34 @@ CultivatorSectState
|
||||
|
||||
外门、内门、真传的最低晋升境界依次为炼气、筑基、金丹;贡献、任务与试炼仍由标准组织策略判定。
|
||||
|
||||
## 标准流派参悟
|
||||
|
||||
标准流派共六层,参悟成本独立于境界突破经验表。第 `N` 层成本为:
|
||||
|
||||
```text
|
||||
修为 = 5,000 × 4^(N - 1)
|
||||
灵石 = 修为 × 5
|
||||
道心感悟 = 100
|
||||
```
|
||||
|
||||
| 层级 | 境界要求 | 修为 | 灵石 | 道心感悟 |
|
||||
| --- | --- | ---: | ---: | ---: |
|
||||
| 第一层 | 筑基中期 | 5,000 | 25,000 | 100 |
|
||||
| 第二层 | 金丹圆满 | 20,000 | 100,000 | 100 |
|
||||
| 第三层 | 化神中期 | 80,000 | 400,000 | 100 |
|
||||
| 第四层 | 炼虚圆满 | 320,000 | 1,600,000 | 100 |
|
||||
| 第五层 | 大乘中期 | 1,280,000 | 6,400,000 | 100 |
|
||||
| 终式 | 渡劫圆满 | 5,120,000 | 25,600,000 | 100 |
|
||||
|
||||
每个流派底层保留三套参悟方案,当前只开放方案一。节点首次保存后锁定;未选择节点的已解锁层级仍可继续补选,替换、移除和重置暂不开放。两个已习得流派可以随时免费切换。
|
||||
|
||||
## 边界约束
|
||||
|
||||
- 每个宗门固定六本心法,每本至少拥有一个基础神通。
|
||||
- 流派层数与每层节点数由内容声明;层 ID、顺序和节点归属必须稳定且可校验。
|
||||
- 玩家只保存已按顺序解锁的层 ID;流派编译器不得从等级推导倍率或层级。
|
||||
- 四个主动槽固定、不可重复,只能装配已解锁主动神通。
|
||||
- 每个流派独立保存等级、战术和三套参悟方案,只能激活一个流派。
|
||||
- 每个流派独立保存层级、战术和三套参悟方案,只能激活一个流派;当前开放方案由标准规则统一声明。
|
||||
- 未注册 ID、未知配置版本和非法持久化状态必须明确失败。
|
||||
- battle-v5 运行时标签只能通过 `GameplayTags` 生成。
|
||||
- 普通宗门不得复制组织模块,不得绕过 `SectAbilityFactory`/battle-v5 `AbilityFactory`,不得在流派编译器中按节点 ID 集中分派。
|
||||
|
||||
@@ -20,7 +20,7 @@ const HEAVY_SWORD_DEFINITION: SectPathDefinitionWithoutNodes = {
|
||||
description:
|
||||
'重剑不争一时之快,以身承势,以守养锋;剑意未足时稳住自身,剑意既成后,以一剑决定胜负。',
|
||||
minRealm: '筑基',
|
||||
minRealmStage: '初期',
|
||||
minRealmStage: '中期',
|
||||
layers: [...STANDARD_PATH_LAYERS],
|
||||
defaultTacticId: 'heavy-break',
|
||||
presentation: {
|
||||
|
||||
@@ -20,7 +20,7 @@ const SWIFT_SWORD_DEFINITION: SectPathDefinitionWithoutNodes = {
|
||||
description:
|
||||
'剑随身走,身随势变;以迅疾剑式连缀攻势,在交锋之间留下剑痕,最终将诸般剑影收束于《此剑平生》。',
|
||||
minRealm: '筑基',
|
||||
minRealmStage: '初期',
|
||||
minRealmStage: '中期',
|
||||
layers: [...STANDARD_PATH_LAYERS],
|
||||
defaultTacticId: 'aggressive',
|
||||
presentation: {
|
||||
|
||||
@@ -29,7 +29,7 @@ const hetuDefinition: SectPathDefinitionWithoutNodes = {
|
||||
description:
|
||||
'河图示其流,前法不灭,后法由此而生。三数成图之后,伤势、气血与法力都在同一轮转中得到续接。',
|
||||
minRealm: '筑基',
|
||||
minRealmStage: '初期',
|
||||
minRealmStage: '中期',
|
||||
layers: [...STANDARD_PATH_LAYERS],
|
||||
defaultTacticId: 'small-cycle',
|
||||
tactics: [
|
||||
@@ -55,7 +55,7 @@ const luoshuDefinition: SectPathDefinitionWithoutNodes = {
|
||||
description:
|
||||
'洛书定其位,不等万法自然流转。移一宫、断一势、藏一印,在敌势真正成形之前先改写它的去处。',
|
||||
minRealm: '筑基',
|
||||
minRealmStage: '初期',
|
||||
minRealmStage: '中期',
|
||||
layers: [...STANDARD_PATH_LAYERS],
|
||||
defaultTacticId: 'break-pattern',
|
||||
tactics: [
|
||||
|
||||
@@ -28,7 +28,7 @@ const mirrorDefinition: SectPathDefinitionWithoutNodes = {
|
||||
description:
|
||||
'以承受制造因,以魔相兑现果。来力不急于拒绝,只把每一道因果留在镜中;佛相立因,魔相现报,无相令因果同时照见。',
|
||||
minRealm: '筑基',
|
||||
minRealmStage: '初期',
|
||||
minRealmStage: '中期',
|
||||
layers: [...STANDARD_PATH_LAYERS],
|
||||
defaultTacticId: 'guard',
|
||||
tactics: [
|
||||
@@ -93,7 +93,7 @@ const demonDefinition: SectPathDefinitionWithoutNodes = {
|
||||
description:
|
||||
'以佛相主动沉血,以魔相强渡两息,以无相令燃血与渡厄同时显化。气血不是怒气的别名,而是每一门神通真正支付的渡河之资。',
|
||||
minRealm: '筑基',
|
||||
minRealmStage: '初期',
|
||||
minRealmStage: '中期',
|
||||
layers: [...STANDARD_PATH_LAYERS],
|
||||
defaultTacticId: 'trial-fire',
|
||||
tactics: [
|
||||
|
||||
@@ -23,7 +23,7 @@ const tideDefinition: SectPathDefinitionWithoutNodes = {
|
||||
id: YOUDU_TIDE_PATH_ID,
|
||||
name: '招魂渡夜',
|
||||
description: '一声唤名落入黑水,千里游魂都听见自己的回音。此道让忘川一寸寸漫过归路。',
|
||||
minRealm: '筑基', minRealmStage: '初期',
|
||||
minRealm: '筑基', minRealmStage: '中期',
|
||||
layers: [...STANDARD_PATH_LAYERS],
|
||||
defaultTacticId: 'tide-cycle',
|
||||
tactics: [
|
||||
@@ -48,7 +48,7 @@ const decreeDefinition: SectPathDefinitionWithoutNodes = {
|
||||
id: YOUDU_DECREE_PATH_ID,
|
||||
name: '镇魄司命',
|
||||
description: '见影而知魂,知魂而书名;铁钉落下之前,门人已量过每一道离身缝隙。',
|
||||
minRealm: '筑基', minRealmStage: '初期',
|
||||
minRealm: '筑基', minRealmStage: '中期',
|
||||
layers: [...STANDARD_PATH_LAYERS],
|
||||
defaultTacticId: 'pin-the-caster',
|
||||
tactics: [
|
||||
|
||||
@@ -4,6 +4,7 @@ export const StandardSectRules = Object.freeze({
|
||||
methodSlots: Object.freeze([1, 2, 3, 4, 5, 6] as const),
|
||||
activeAbilitySlotCount: 4,
|
||||
meridianLoadoutSlots: Object.freeze([1, 2, 3] as const),
|
||||
enabledMeridianLoadoutSlots: Object.freeze([1] as Array<1 | 2 | 3>),
|
||||
meridianNodeTransportLimit: 64,
|
||||
combatResourceCount: 1,
|
||||
});
|
||||
@@ -16,6 +16,7 @@ import { createAbilitySlots } from '../presentation/abilityLoadout';
|
||||
import {
|
||||
assertMethodTrainingTarget,
|
||||
isAbilityUnlocked,
|
||||
validateMeridianLoadoutUpdate,
|
||||
validateMeridianNodeIds,
|
||||
} from '../progression/progression';
|
||||
|
||||
@@ -160,6 +161,12 @@ export class MeridianLoadoutSpecification {
|
||||
validate(candidate: Parameters<typeof validateMeridianNodeIds>[0]): string[] {
|
||||
return validateMeridianNodeIds(candidate);
|
||||
}
|
||||
|
||||
validateUpdate(
|
||||
candidate: Parameters<typeof validateMeridianLoadoutUpdate>[0],
|
||||
): string[] {
|
||||
return validateMeridianLoadoutUpdate(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
export class AbilityLoadoutSpecification {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { EXP_CAP_TABLE } from '@shared/config/cultivationProgress';
|
||||
import { getRealmStageRank } from '@shared/config/realmProgression';
|
||||
import type { RealmStage, RealmType } from '@shared/types/constants';
|
||||
import type {
|
||||
@@ -43,6 +42,13 @@ export const STANDARD_SECT_METHOD_COST_CURVE = {
|
||||
spiritStoneRoundUnit: 100,
|
||||
} as const;
|
||||
|
||||
export const STANDARD_SECT_PATH_COST_CURVE = {
|
||||
baseCultivationExp: 5_000,
|
||||
growthMultiplier: 4,
|
||||
spiritStoneMultiplier: 5,
|
||||
comprehensionInsight: 100,
|
||||
} as const;
|
||||
|
||||
function roundUpToUnit(value: number, unit: number): number {
|
||||
return Math.ceil(value / unit) * unit;
|
||||
}
|
||||
@@ -64,16 +70,16 @@ function methodLevelTrainingCost(level: number): SectTrainingCost {
|
||||
};
|
||||
}
|
||||
|
||||
function pathLayerCost(
|
||||
realm: RealmType,
|
||||
stage: RealmStage,
|
||||
comprehensionInsight: number,
|
||||
): SectTrainingCost {
|
||||
const cultivationExp = Math.ceil(EXP_CAP_TABLE[realm][stage] * 0.5);
|
||||
function pathLayerCost(order: number): SectTrainingCost {
|
||||
const cultivationExp =
|
||||
STANDARD_SECT_PATH_COST_CURVE.baseCultivationExp *
|
||||
STANDARD_SECT_PATH_COST_CURVE.growthMultiplier ** (order - 1);
|
||||
return {
|
||||
cultivationExp,
|
||||
comprehensionInsight,
|
||||
spiritStones: cultivationExp * 10,
|
||||
comprehensionInsight:
|
||||
STANDARD_SECT_PATH_COST_CURVE.comprehensionInsight,
|
||||
spiritStones:
|
||||
cultivationExp * STANDARD_SECT_PATH_COST_CURVE.spiritStoneMultiplier,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -83,48 +89,48 @@ export const STANDARD_PATH_LAYERS: readonly SectPathLayerDefinition[] = [
|
||||
order: 1,
|
||||
label: '第一层',
|
||||
minRealm: '筑基',
|
||||
minRealmStage: '初期',
|
||||
cost: pathLayerCost('筑基', '初期', 10),
|
||||
minRealmStage: '中期',
|
||||
cost: pathLayerCost(1),
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
order: 2,
|
||||
label: '第二层',
|
||||
minRealm: '筑基',
|
||||
minRealm: '金丹',
|
||||
minRealmStage: '圆满',
|
||||
cost: pathLayerCost('筑基', '圆满', 15),
|
||||
cost: pathLayerCost(2),
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
order: 3,
|
||||
label: '第三层',
|
||||
minRealm: '金丹',
|
||||
minRealmStage: '圆满',
|
||||
cost: pathLayerCost('金丹', '圆满', 20),
|
||||
minRealm: '化神',
|
||||
minRealmStage: '中期',
|
||||
cost: pathLayerCost(3),
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
order: 4,
|
||||
label: '第四层',
|
||||
minRealm: '元婴',
|
||||
minRealm: '炼虚',
|
||||
minRealmStage: '圆满',
|
||||
cost: pathLayerCost('元婴', '圆满', 25),
|
||||
cost: pathLayerCost(4),
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
order: 5,
|
||||
label: '第五层',
|
||||
minRealm: '化神',
|
||||
minRealm: '大乘',
|
||||
minRealmStage: '中期',
|
||||
cost: pathLayerCost('化神', '中期', 30),
|
||||
cost: pathLayerCost(5),
|
||||
},
|
||||
{
|
||||
id: 'ultimate',
|
||||
order: 6,
|
||||
label: '终式',
|
||||
minRealm: '化神',
|
||||
minRealm: '渡劫',
|
||||
minRealmStage: '圆满',
|
||||
cost: pathLayerCost('化神', '圆满', 40),
|
||||
cost: pathLayerCost(6),
|
||||
},
|
||||
] as const;
|
||||
|
||||
|
||||
@@ -97,6 +97,21 @@ export function validateMeridianNodeIds(args: {
|
||||
return uniqueIds;
|
||||
}
|
||||
|
||||
export function validateMeridianLoadoutUpdate(args: {
|
||||
path: SectPathDefinition;
|
||||
currentNodeIds: string[];
|
||||
nodeIds: string[];
|
||||
unlockedLayerIds: string[];
|
||||
methods: Partial<Record<SectMethodId, number>>;
|
||||
}): string[] {
|
||||
const validated = validateMeridianNodeIds(args);
|
||||
const nextNodeIds = new Set(validated);
|
||||
if (args.currentNodeIds.some((nodeId) => !nextNodeIds.has(nodeId))) {
|
||||
throw new Error('已保存参悟节点不可更改,重置功能尚未开放');
|
||||
}
|
||||
return validated;
|
||||
}
|
||||
|
||||
export function assertMethodTrainingTarget(args: {
|
||||
definition: SectDefinition;
|
||||
methodId: SectMethodId;
|
||||
|
||||
@@ -3,7 +3,9 @@ import {
|
||||
getPathProgress,
|
||||
getSectMethodLevelCap,
|
||||
getSectMethodTrainingCost,
|
||||
StandardSectRules,
|
||||
standardSectProgression,
|
||||
validateMeridianLoadoutUpdate,
|
||||
validateMeridianNodeIds,
|
||||
} from '..';
|
||||
import { HEAVY_SWORD_PATH, SWIFT_SWORD_PATH } from '../../content/lingxiao';
|
||||
@@ -95,38 +97,65 @@ describe('通用宗门成长', () => {
|
||||
});
|
||||
|
||||
it('六层按顺序、境界和精确资源成本解锁', () => {
|
||||
expect(HEAVY_SWORD_PATH.layers.map((layer) => layer.cost)).toEqual([
|
||||
{ cultivationExp: 950, comprehensionInsight: 10, spiritStones: 9_500 },
|
||||
expect(
|
||||
HEAVY_SWORD_PATH.layers.map((layer) => ({
|
||||
realm: `${layer.minRealm}${layer.minRealmStage}`,
|
||||
cost: layer.cost,
|
||||
})),
|
||||
).toEqual([
|
||||
{
|
||||
cultivationExp: 2_500,
|
||||
comprehensionInsight: 15,
|
||||
spiritStones: 25_000,
|
||||
realm: '筑基中期',
|
||||
cost: {
|
||||
cultivationExp: 5_000,
|
||||
comprehensionInsight: 100,
|
||||
spiritStones: 25_000,
|
||||
},
|
||||
},
|
||||
{
|
||||
cultivationExp: 13_500,
|
||||
comprehensionInsight: 20,
|
||||
spiritStones: 135_000,
|
||||
realm: '金丹圆满',
|
||||
cost: {
|
||||
cultivationExp: 20_000,
|
||||
comprehensionInsight: 100,
|
||||
spiritStones: 100_000,
|
||||
},
|
||||
},
|
||||
{
|
||||
cultivationExp: 47_000,
|
||||
comprehensionInsight: 25,
|
||||
spiritStones: 470_000,
|
||||
realm: '化神中期',
|
||||
cost: {
|
||||
cultivationExp: 80_000,
|
||||
comprehensionInsight: 100,
|
||||
spiritStones: 400_000,
|
||||
},
|
||||
},
|
||||
{
|
||||
cultivationExp: 65_000,
|
||||
comprehensionInsight: 30,
|
||||
spiritStones: 650_000,
|
||||
realm: '炼虚圆满',
|
||||
cost: {
|
||||
cultivationExp: 320_000,
|
||||
comprehensionInsight: 100,
|
||||
spiritStones: 1_600_000,
|
||||
},
|
||||
},
|
||||
{
|
||||
cultivationExp: 125_000,
|
||||
comprehensionInsight: 40,
|
||||
spiritStones: 1_250_000,
|
||||
realm: '大乘中期',
|
||||
cost: {
|
||||
cultivationExp: 1_280_000,
|
||||
comprehensionInsight: 100,
|
||||
spiritStones: 6_400_000,
|
||||
},
|
||||
},
|
||||
{
|
||||
realm: '渡劫圆满',
|
||||
cost: {
|
||||
cultivationExp: 5_120_000,
|
||||
comprehensionInsight: 100,
|
||||
spiritStones: 25_600_000,
|
||||
},
|
||||
},
|
||||
]);
|
||||
const progress = getPathProgress({
|
||||
path: HEAVY_SWORD_PATH,
|
||||
unlockedLayerIds: ['1', '2', '3', '4'],
|
||||
realm: '化神',
|
||||
realm: '大乘',
|
||||
stage: '中期',
|
||||
});
|
||||
expect(progress.unlockedLayers.map((layer) => layer.id)).toEqual([
|
||||
@@ -148,4 +177,127 @@ describe('通用宗门成长', () => {
|
||||
}),
|
||||
).toThrow('按顺序解锁');
|
||||
});
|
||||
|
||||
it('六层在各自境界节点开放且不绑定心法', () => {
|
||||
const gates = [
|
||||
{
|
||||
layerId: '1',
|
||||
before: ['筑基', '初期'],
|
||||
reached: ['筑基', '中期'],
|
||||
},
|
||||
{
|
||||
layerId: '2',
|
||||
before: ['金丹', '后期'],
|
||||
reached: ['金丹', '圆满'],
|
||||
},
|
||||
{
|
||||
layerId: '3',
|
||||
before: ['化神', '初期'],
|
||||
reached: ['化神', '中期'],
|
||||
},
|
||||
{
|
||||
layerId: '4',
|
||||
before: ['炼虚', '后期'],
|
||||
reached: ['炼虚', '圆满'],
|
||||
},
|
||||
{
|
||||
layerId: '5',
|
||||
before: ['大乘', '初期'],
|
||||
reached: ['大乘', '中期'],
|
||||
},
|
||||
{
|
||||
layerId: 'ultimate',
|
||||
before: ['渡劫', '后期'],
|
||||
reached: ['渡劫', '圆满'],
|
||||
},
|
||||
] as const;
|
||||
|
||||
for (const [index, gate] of gates.entries()) {
|
||||
const unlockedLayerIds = HEAVY_SWORD_PATH.layers
|
||||
.slice(0, index)
|
||||
.map((layer) => layer.id);
|
||||
const before = getPathProgress({
|
||||
path: HEAVY_SWORD_PATH,
|
||||
unlockedLayerIds,
|
||||
realm: gate.before[0],
|
||||
stage: gate.before[1],
|
||||
methods: {},
|
||||
});
|
||||
const reached = getPathProgress({
|
||||
path: HEAVY_SWORD_PATH,
|
||||
unlockedLayerIds,
|
||||
realm: gate.reached[0],
|
||||
stage: gate.reached[1],
|
||||
methods: {},
|
||||
});
|
||||
expect(before.nextLayer?.id).toBe(gate.layerId);
|
||||
expect(before.nextLayerAvailable).toBe(false);
|
||||
expect(reached.nextLayer?.id).toBe(gate.layerId);
|
||||
expect(reached.nextLayerAvailable).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('流派指数成本累计正确且每层均消耗满额道心感悟', () => {
|
||||
const onePath = HEAVY_SWORD_PATH.layers.reduce(
|
||||
(total, layer) => ({
|
||||
cultivationExp: total.cultivationExp + layer.cost.cultivationExp,
|
||||
comprehensionInsight:
|
||||
total.comprehensionInsight + layer.cost.comprehensionInsight,
|
||||
spiritStones: total.spiritStones + layer.cost.spiritStones,
|
||||
}),
|
||||
{ cultivationExp: 0, comprehensionInsight: 0, spiritStones: 0 },
|
||||
);
|
||||
expect(onePath).toEqual({
|
||||
cultivationExp: 6_825_000,
|
||||
comprehensionInsight: 600,
|
||||
spiritStones: 34_125_000,
|
||||
});
|
||||
expect({
|
||||
cultivationExp: onePath.cultivationExp * 2,
|
||||
comprehensionInsight: onePath.comprehensionInsight * 2,
|
||||
spiritStones: onePath.spiritStones * 2,
|
||||
}).toEqual({
|
||||
cultivationExp: 13_650_000,
|
||||
comprehensionInsight: 1_200,
|
||||
spiritStones: 68_250_000,
|
||||
});
|
||||
for (const layer of HEAVY_SWORD_PATH.layers) {
|
||||
expect(layer.cost.comprehensionInsight).toBe(100);
|
||||
expect(layer.cost.spiritStones).toBeGreaterThan(
|
||||
layer.cost.cultivationExp,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('当前只开放方案一并保留三槽底层容量', () => {
|
||||
expect(StandardSectRules.meridianLoadoutSlots).toEqual([1, 2, 3]);
|
||||
expect(StandardSectRules.enabledMeridianLoadoutSlots).toEqual([1]);
|
||||
});
|
||||
|
||||
it('已保存节点不可移除或替换,但可为新层级追加选择', () => {
|
||||
const base = {
|
||||
path: HEAVY_SWORD_PATH,
|
||||
currentNodeIds: ['heavy-opening'],
|
||||
unlockedLayerIds: ['1', '2'],
|
||||
methods: {},
|
||||
};
|
||||
expect(
|
||||
validateMeridianLoadoutUpdate({
|
||||
...base,
|
||||
nodeIds: ['heavy-opening', 'heavy-triple-ridge'],
|
||||
}),
|
||||
).toEqual(['heavy-opening', 'heavy-triple-ridge']);
|
||||
expect(() =>
|
||||
validateMeridianLoadoutUpdate({
|
||||
...base,
|
||||
nodeIds: [],
|
||||
}),
|
||||
).toThrow('重置功能尚未开放');
|
||||
expect(() =>
|
||||
validateMeridianLoadoutUpdate({
|
||||
...base,
|
||||
nodeIds: ['heavy-hidden-weight'],
|
||||
}),
|
||||
).toThrow('重置功能尚未开放');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user