优化:心法升级趋势
This commit is contained in:
1 parent
e912485cf1
commit
3d8d63a6c2
8 files changed
+195
-56
No files matched your search
@@ -30,7 +30,7 @@ function getMethodDisabledReason(args: {
|
||||
};
|
||||
}) {
|
||||
const targetLevel = args.currentLevel + 1;
|
||||
if (targetLevel > args.data.methodLevelCap) return '已达当前境界上限';
|
||||
if (targetLevel > args.data.methodLevelCap) return '已达当前心法上限';
|
||||
const primary = args.data.definition?.methods.find(
|
||||
(method) => method.isPrimary,
|
||||
);
|
||||
|
||||
@@ -66,6 +66,35 @@ CultivatorSectState
|
||||
- 多个节点修改同一组神通时,建立流派私有 Build Facade,用语义特征组合变化;禁止恢复 `nodes.has(id)` 中央分派。
|
||||
- 节点每次应用必须造成可观察的最终运行时变化,空实现会在注册时失败。
|
||||
|
||||
## 标准心法成长
|
||||
|
||||
标准宗门心法覆盖 1~180 级。目标等级 `L` 的单级研习成本独立于境界突破经验表:
|
||||
|
||||
```text
|
||||
修为 = ceil((50 × 1.05^(L - 1)) / 10) × 10
|
||||
灵石 = ceil((修为 × 3) / 100) × 100
|
||||
道心感悟 = 0
|
||||
```
|
||||
|
||||
跨级研习逐级取整后累加。境界每个小阶段开放 5 级;有效上限取境界、弟子身份与藏经设施三者的最小值。
|
||||
|
||||
| 弟子身份 | 心法上限 |
|
||||
| --- | ---: |
|
||||
| 记名弟子 | 45 |
|
||||
| 外门弟子 | 90 |
|
||||
| 内门弟子 | 135 |
|
||||
| 真传弟子 | 180 |
|
||||
|
||||
| 藏经设施等级 | 心法上限 |
|
||||
| ---: | ---: |
|
||||
| 1 | 40 |
|
||||
| 2 | 75 |
|
||||
| 3 | 110 |
|
||||
| 4 | 145 |
|
||||
| 5 | 180 |
|
||||
|
||||
外门、内门、真传的最低晋升境界依次为炼气、筑基、金丹;贡献、任务与试炼仍由标准组织策略判定。
|
||||
|
||||
## 边界约束
|
||||
|
||||
- 每个宗门固定六本心法,每本至少拥有一个基础神通。
|
||||
|
||||
+22
-2
@@ -51,7 +51,20 @@ describe('LingxiaoOrganizationModule', () => {
|
||||
});
|
||||
|
||||
it('owns rank, economy, task, and construction content', () => {
|
||||
expect(LINGXIAO_ORGANIZATION.ranks.methodLevelCap('registered')).toBe(5);
|
||||
expect(
|
||||
(
|
||||
['registered', 'outer', 'inner', 'true'] as const
|
||||
).map((rank) => LINGXIAO_ORGANIZATION.ranks.methodLevelCap(rank)),
|
||||
).toEqual([45, 90, 135, 180]);
|
||||
expect(LINGXIAO_ORGANIZATION.ranks.requirement('outer')).toMatchObject({
|
||||
minRealm: '炼气',
|
||||
contribution: 100,
|
||||
dailyCompletions: 3,
|
||||
});
|
||||
expect(LINGXIAO_ORGANIZATION.ranks.requirement('inner')).toMatchObject({
|
||||
minRealm: '筑基',
|
||||
contribution: 500,
|
||||
});
|
||||
expect(LINGXIAO_ORGANIZATION.ranks.requirement('true')).toMatchObject({
|
||||
minRealm: '金丹',
|
||||
contribution: 3000,
|
||||
@@ -66,13 +79,20 @@ describe('LingxiaoOrganizationModule', () => {
|
||||
expect(LINGXIAO_ORGANIZATION.construction.facilityPriority[0]).toBe(
|
||||
'archive',
|
||||
);
|
||||
expect(
|
||||
[1, 2, 3, 4, 5].map((archiveLevel) =>
|
||||
LINGXIAO_ORGANIZATION.benefits.methodLevelCap(
|
||||
new Map([['archive', archiveLevel]]),
|
||||
),
|
||||
),
|
||||
).toEqual([40, 75, 110, 145, 180]);
|
||||
const levels = new Map([
|
||||
['archive', 5],
|
||||
['cultivation_room', 5],
|
||||
['workshop', 5],
|
||||
['spirit_vein', 5],
|
||||
]);
|
||||
expect(LINGXIAO_ORGANIZATION.benefits.methodLevelCap(levels)).toBe(100);
|
||||
expect(LINGXIAO_ORGANIZATION.benefits.methodLevelCap(levels)).toBe(180);
|
||||
expect(
|
||||
LINGXIAO_ORGANIZATION.benefits.retreatMultiplier(levels, 'outer'),
|
||||
).toBe(1.1);
|
||||
|
||||
@@ -28,12 +28,12 @@ export const SECT_RANK_LABELS: Record<SectDiscipleRank, string> = {
|
||||
true: '真传弟子',
|
||||
};
|
||||
|
||||
export const SECT_RANK_METHOD_CAP: Record<SectDiscipleRank, number> = {
|
||||
registered: 5,
|
||||
outer: 20,
|
||||
inner: 40,
|
||||
true: Number.MAX_SAFE_INTEGER,
|
||||
};
|
||||
export const SECT_RANK_METHOD_CAP = {
|
||||
registered: 45,
|
||||
outer: 90,
|
||||
inner: 135,
|
||||
true: 180,
|
||||
} as const satisfies Record<SectDiscipleRank, number>;
|
||||
|
||||
export interface SectFacilityState {
|
||||
key: SectFacilityKey;
|
||||
|
||||
@@ -4,7 +4,11 @@ import {
|
||||
} from '@shared/config/realmProgression';
|
||||
import type { PillSpec } from '@shared/types/consumable';
|
||||
import type { Attributes, Cultivator } from '@shared/types/cultivator';
|
||||
import type { SectDiscipleRank, SectRankRequirement } from '../domain';
|
||||
import {
|
||||
SECT_RANK_METHOD_CAP,
|
||||
type SectDiscipleRank,
|
||||
type SectRankRequirement,
|
||||
} from '../domain';
|
||||
import { StandardSectCapabilityPolicy } from './StandardSectCapabilityPolicy';
|
||||
import {
|
||||
SECT_CRAFT_CONTEXTS,
|
||||
@@ -50,6 +54,10 @@ const capabilities = new StandardSectCapabilityPolicy(
|
||||
new Set(['sect.formation.view']),
|
||||
);
|
||||
|
||||
export const STANDARD_SECT_ARCHIVE_METHOD_CAP = [
|
||||
0, 40, 75, 110, 145, 180,
|
||||
] as const;
|
||||
|
||||
function taskPresentation(
|
||||
title: string,
|
||||
description: string,
|
||||
@@ -824,12 +832,7 @@ class StandardSectRankPolicy implements SectRankPolicy {
|
||||
}
|
||||
|
||||
methodLevelCap(rank: SectDiscipleRank): number {
|
||||
return {
|
||||
registered: 5,
|
||||
outer: 20,
|
||||
inner: 40,
|
||||
true: Number.MAX_SAFE_INTEGER,
|
||||
}[rank];
|
||||
return SECT_RANK_METHOD_CAP[rank];
|
||||
}
|
||||
|
||||
requirement(
|
||||
@@ -1004,7 +1007,10 @@ class StandardSectBenefitPolicy implements SectBenefitPolicy {
|
||||
1,
|
||||
Math.min(5, Math.floor(this.archiveLevel(levels))),
|
||||
);
|
||||
return [0, 20, 40, 60, 80, 100][level] ?? 20;
|
||||
return (
|
||||
STANDARD_SECT_ARCHIVE_METHOD_CAP[level] ??
|
||||
STANDARD_SECT_ARCHIVE_METHOD_CAP[1]
|
||||
);
|
||||
}
|
||||
|
||||
gardenLevel(levels: ReadonlyMap<string, number>): number {
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { EXP_CAP_TABLE } from '@shared/config/cultivationProgress';
|
||||
import { getRealmStageRank } from '@shared/config/realmProgression';
|
||||
import {
|
||||
REALM_STAGE_VALUES,
|
||||
REALM_VALUES,
|
||||
type RealmStage,
|
||||
type RealmType,
|
||||
} from '@shared/types/constants';
|
||||
import type { RealmStage, RealmType } from '@shared/types/constants';
|
||||
import type {
|
||||
SectMethodId,
|
||||
SectPathDefinition,
|
||||
@@ -22,10 +17,6 @@ export interface SectPathProgressProjection {
|
||||
|
||||
export interface SectProgressionPolicy {
|
||||
methodLevelCap(realm: RealmType, stage: RealmStage): number;
|
||||
minimumRealmStageForMethodLevel(level: number): {
|
||||
realm: RealmType;
|
||||
stage: RealmStage;
|
||||
};
|
||||
methodTrainingCost(fromLevel: number, targetLevel: number): SectTrainingCost;
|
||||
pathProgress(args: {
|
||||
path: SectPathDefinition;
|
||||
@@ -44,6 +35,35 @@ export interface SectProgressionPolicy {
|
||||
}): SectPathLayerDefinition;
|
||||
}
|
||||
|
||||
export const STANDARD_SECT_METHOD_COST_CURVE = {
|
||||
baseCultivationExp: 50,
|
||||
growthRate: 1.05,
|
||||
cultivationRoundUnit: 10,
|
||||
spiritStoneMultiplier: 3,
|
||||
spiritStoneRoundUnit: 100,
|
||||
} as const;
|
||||
|
||||
function roundUpToUnit(value: number, unit: number): number {
|
||||
return Math.ceil(value / unit) * unit;
|
||||
}
|
||||
|
||||
function methodLevelTrainingCost(level: number): SectTrainingCost {
|
||||
const cultivationExp = roundUpToUnit(
|
||||
STANDARD_SECT_METHOD_COST_CURVE.baseCultivationExp *
|
||||
STANDARD_SECT_METHOD_COST_CURVE.growthRate ** (level - 1),
|
||||
STANDARD_SECT_METHOD_COST_CURVE.cultivationRoundUnit,
|
||||
);
|
||||
return {
|
||||
cultivationExp,
|
||||
comprehensionInsight: 0,
|
||||
spiritStones: roundUpToUnit(
|
||||
cultivationExp *
|
||||
STANDARD_SECT_METHOD_COST_CURVE.spiritStoneMultiplier,
|
||||
STANDARD_SECT_METHOD_COST_CURVE.spiritStoneRoundUnit,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function pathLayerCost(
|
||||
realm: RealmType,
|
||||
stage: RealmStage,
|
||||
@@ -143,18 +163,6 @@ export class StandardSectProgressionPolicy implements SectProgressionPolicy {
|
||||
return (getRealmStageRank(realm, stage) + 1) * 5;
|
||||
}
|
||||
|
||||
minimumRealmStageForMethodLevel(level: number): {
|
||||
realm: RealmType;
|
||||
stage: RealmStage;
|
||||
} {
|
||||
for (const realm of REALM_VALUES) {
|
||||
for (const stage of REALM_STAGE_VALUES) {
|
||||
if (this.methodLevelCap(realm, stage) >= level) return { realm, stage };
|
||||
}
|
||||
}
|
||||
return { realm: '渡劫', stage: '圆满' };
|
||||
}
|
||||
|
||||
methodTrainingCost(fromLevel: number, targetLevel: number): SectTrainingCost {
|
||||
const total: SectTrainingCost = {
|
||||
cultivationExp: 0,
|
||||
@@ -162,10 +170,9 @@ export class StandardSectProgressionPolicy implements SectProgressionPolicy {
|
||||
spiritStones: 0,
|
||||
};
|
||||
for (let level = fromLevel + 1; level <= targetLevel; level += 1) {
|
||||
const { realm, stage } = this.minimumRealmStageForMethodLevel(level);
|
||||
const cultivationExp = Math.ceil(EXP_CAP_TABLE[realm][stage] * 0.01);
|
||||
total.cultivationExp += cultivationExp;
|
||||
total.spiritStones += Math.max(50, cultivationExp * 2);
|
||||
const cost = methodLevelTrainingCost(level);
|
||||
total.cultivationExp += cost.cultivationExp;
|
||||
total.spiritStones += cost.spiritStones;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,53 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
SECT_RANK_METHOD_CAP,
|
||||
getEffectiveSectMethodLevelCap,
|
||||
hasSectRank,
|
||||
} from '../domain';
|
||||
|
||||
describe('宗门组织成长', () => {
|
||||
it('四种弟子身份均匀开放至180级', () => {
|
||||
expect(SECT_RANK_METHOD_CAP).toEqual({
|
||||
registered: 45,
|
||||
outer: 90,
|
||||
inner: 135,
|
||||
true: 180,
|
||||
});
|
||||
});
|
||||
|
||||
it('按境界、弟子职阶和藏经阁取心法最低上限', () => {
|
||||
expect(getEffectiveSectMethodLevelCap({ realmCap: 80, rank: 'registered', facilityCap: 100 })).toBe(5);
|
||||
expect(getEffectiveSectMethodLevelCap({ realmCap: 80, rank: 'outer', facilityCap: 20 })).toBe(20);
|
||||
expect(getEffectiveSectMethodLevelCap({ realmCap: 35, rank: 'inner', facilityCap: 100 })).toBe(35);
|
||||
expect(getEffectiveSectMethodLevelCap({ realmCap: 120, rank: 'true', facilityCap: 60 })).toBe(60);
|
||||
expect(
|
||||
getEffectiveSectMethodLevelCap({
|
||||
realmCap: 180,
|
||||
rank: 'registered',
|
||||
facilityCap: 180,
|
||||
}),
|
||||
).toBe(45);
|
||||
expect(
|
||||
getEffectiveSectMethodLevelCap({
|
||||
realmCap: 80,
|
||||
rank: 'outer',
|
||||
facilityCap: 180,
|
||||
}),
|
||||
).toBe(80);
|
||||
expect(
|
||||
getEffectiveSectMethodLevelCap({
|
||||
realmCap: 180,
|
||||
rank: 'inner',
|
||||
facilityCap: 110,
|
||||
}),
|
||||
).toBe(110);
|
||||
expect(
|
||||
getEffectiveSectMethodLevelCap({
|
||||
realmCap: 180,
|
||||
rank: 'true',
|
||||
facilityCap: 145,
|
||||
}),
|
||||
).toBe(145);
|
||||
});
|
||||
|
||||
it('消费贡献不会参与职阶比较或造成降阶', () => {
|
||||
expect(hasSectRank('inner', 'outer')).toBe(true);
|
||||
expect(hasSectRank('outer', 'inner')).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -9,20 +9,64 @@ import {
|
||||
import { HEAVY_SWORD_PATH, SWIFT_SWORD_PATH } from '../../content/lingxiao';
|
||||
|
||||
describe('通用宗门成长', () => {
|
||||
it('按目标等级的最低境界阶段计算修为与灵石', () => {
|
||||
it('每个境界阶段开放五级并在渡劫圆满达到180级', () => {
|
||||
expect(getSectMethodLevelCap('炼气', '初期')).toBe(5);
|
||||
expect(getSectMethodLevelCap('筑基', '初期')).toBe(25);
|
||||
expect(getSectMethodTrainingCost(0, 1)).toEqual({
|
||||
cultivationExp: 5,
|
||||
comprehensionInsight: 0,
|
||||
spiritStones: 50,
|
||||
});
|
||||
expect(getSectMethodLevelCap('渡劫', '圆满')).toBe(180);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[1, 50, 200],
|
||||
[50, 550, 1_700],
|
||||
[100, 6_270, 18_900],
|
||||
[120, 16_620, 49_900],
|
||||
[150, 71_810, 215_500],
|
||||
[180, 310_360, 931_100],
|
||||
])(
|
||||
'%i级按独立指数曲线计算单级修为与灵石',
|
||||
(level, cultivationExp, spiritStones) => {
|
||||
expect(getSectMethodTrainingCost(level - 1, level)).toEqual({
|
||||
cultivationExp,
|
||||
comprehensionInsight: 0,
|
||||
spiritStones,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('逐级取整后累加跨级成本', () => {
|
||||
expect(getSectMethodTrainingCost(4, 6)).toEqual({
|
||||
cultivationExp: 11,
|
||||
cultivationExp: 140,
|
||||
comprehensionInsight: 0,
|
||||
spiritStones: 100,
|
||||
spiritStones: 600,
|
||||
});
|
||||
});
|
||||
|
||||
it('1至180级累计成本与分段累加一致', () => {
|
||||
const total = getSectMethodTrainingCost(0, 180);
|
||||
const first = getSectMethodTrainingCost(0, 120);
|
||||
const second = getSectMethodTrainingCost(120, 180);
|
||||
expect(total).toEqual({
|
||||
cultivationExp: 6_517_250,
|
||||
comprehensionInsight: 0,
|
||||
spiritStones: 19_560_400,
|
||||
});
|
||||
expect(total.cultivationExp).toBe(
|
||||
first.cultivationExp + second.cultivationExp,
|
||||
);
|
||||
expect(total.spiritStones).toBe(
|
||||
first.spiritStones + second.spiritStones,
|
||||
);
|
||||
expect(first.comprehensionInsight + second.comprehensionInsight).toBe(0);
|
||||
});
|
||||
|
||||
it('所有可用等级的灵石成本均高于修为成本', () => {
|
||||
for (let level = 1; level <= 180; level += 1) {
|
||||
const cost = getSectMethodTrainingCost(level - 1, level);
|
||||
expect(cost.spiritStones).toBeGreaterThan(cost.cultivationExp);
|
||||
expect(cost.comprehensionInsight).toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('参悟只允许选择已解锁层且同层互斥', () => {
|
||||
expect(() =>
|
||||
validateMeridianNodeIds({
|
||||
|
||||
Reference in new issue
Block a user