diff --git a/docs/lingxiao-v4.2-balance.md b/docs/lingxiao-v4.2-balance.md
index b6e60aa3..3090a233 100644
--- a/docs/lingxiao-v4.2-balance.md
+++ b/docs/lingxiao-v4.2-balance.md
@@ -11,9 +11,12 @@
- 神通固定蓝耗,不再随境界变化。
- 神通伤害、护盾、治疗、状态强度和显式声明的持续时间由所属心法统一投影。
+- 《一剑破妄》的驱散数量随《观微剑意》成长档由 1 个提高至 7 个,最终配置只保留普通整数。
- 剑痕与裂甲均为 3 层、3–6 回合状态;剑痕逐层增伤,裂甲逐层降低物防。
- 快剑每连续两次自身行动未造成直接伤害才衰减 1 点剑势;重剑以裂甲、护盾破裂和后发重击组织循环。
- 回燕代表构筑使用留痕,装配《剑荡山河》《藏锋听雷》《踏雪无痕》《剑破万法》;极势在裂甲不足 2 层时先补裂甲。
+- 普通敌对神通的效果链统一要求命中;基础与重剑《藏锋听雷》改为自身蓄势,施展时登记必中、不可打断的后发《听雷》。
+- 宗门节点卡片通过运行时 resolver 展示当前心法等级的实际数值,静态定义只保留机制摘要。
## 编译边界收尾
@@ -28,26 +31,28 @@
| 代表对局 | 筑基初期(25级) | 金丹圆满(60级) | 化神圆满(100级) |
|---|---:|---:|---:|
-| 追风快剑 vs 后发重剑 | 41% / 8.18 / 0% | 79% / 5.42 / 0% | 46% / 4.03 / 0% |
-| 连势快剑 vs 极势重剑 | 23% / 7.40 / 0% | 63% / 5.59 / 0% | 90% / 5.05 / 0% |
-| 回燕快剑 vs 守山重剑 | 26% / 13.41 / 0% | 9% / 11.73 / 0% | 0% / 5.46 / 0% |
+| 追风快剑 vs 后发重剑 | 52% / 8.25 / 0% | 79% / 5.58 / 0% | 20% / 4.36 / 0% |
+| 连势快剑 vs 极势重剑 | 17% / 7.56 / 0% | 29% / 5.09 / 0% | 88% / 4.72 / 0% |
+| 回燕快剑 vs 守山重剑 | 0% / 14.69 / 0% | 1% / 11.89 / 0% | 0% / 7.25 / 0% |
3×3 交叉矩阵实测范围:
| 境界 | 快剑胜率范围 | 平均回合范围 | 平局率 |
|---|---:|---:|---:|
-| 筑基初期 | 12%–98% | 7.40–13.41 | 0% |
-| 金丹圆满 | 9%–95% | 5.15–11.73 | 0% |
-| 化神圆满 | 0%–92% | 4.03–5.68 | 0% |
+| 筑基初期 | 0%–97% | 7.07–14.69 | 0% |
+| 金丹圆满 | 1%–96% | 4.64–11.89 | 0% |
+| 化神圆满 | 0%–88% | 4.06–7.25 | 0% |
## 门禁结论
-成长、蓝耗、状态分层、持续时间、authoring 元数据剥离、36 节点运行时矩阵和 729 套组合编译均已进入自动化测试。当前固定种子平衡门禁仍未通过:三境界代表对局均未同时满足胜率与回合要求,多数交叉对局仍超出范围,化神“回燕 vs 守山”仍为 0% 胜率,不能标记为平衡验收完成。稳定 modifier 标识消除了 Buff 挂载对随机序列的额外消耗,因此本表已使用治理后的固定种子重新实测,不能与治理前数据直接逐项比较。
+成长、驱散数量、蓝耗、命中语义、状态覆盖、被动嵌套成长、动态节点文案、authoring 元数据剥离、36 节点运行时矩阵和 729 套组合编译均已进入自动化测试。严格 3×3 门禁也已改为默认执行。
+
+当前固定种子平衡门禁仍未通过:只有筑基“追风 vs 后发”满足代表胜率与回合要求;其余代表对局及多数交叉对局越界,筑基和化神“回燕 vs 守山”仍为 0% 胜率。按本轮“保留九个流派基础神通倍率、只围绕指定身份节点以 5% 粒度校准”的边界,现有早期流派装配已经形成 0%–97% 的克制跨度,不能标记为平衡验收完成。继续收敛需要放开至少一项边界:代表装配/属性分配、流派基础神通数值,或接受更宽的交叉胜率区间。
严格门禁命令:
```bash
-LINGXIAO_BALANCE_GATE=1 bun run test src/shared/engine/sect/content/lingxiao/tests/balanceSimulation.test.ts
+bunx vitest run src/shared/engine/sect/content/lingxiao/tests/balanceSimulation.test.ts
```
-默认测试保留伤害构成、1/180 级边界冒烟等断言;严格 3×3 胜率和回合门禁继续通过环境变量显式执行。
+该命令当前会按上述实测差距失败,避免未达标数据被误标为平衡验收完成。
diff --git a/src/react-app/routes/game/sect/components/PathsTab.tsx b/src/react-app/routes/game/sect/components/PathsTab.tsx
index d37e4f40..43e157b8 100644
--- a/src/react-app/routes/game/sect/components/PathsTab.tsx
+++ b/src/react-app/routes/game/sect/components/PathsTab.tsx
@@ -335,9 +335,7 @@ function PathDrawer({
/>
) : meridianFooterAction === 'save' ? (
-
- 参悟方案{SLOT_NAMES[slot]}尚未保存。
-
+
参悟方案{SLOT_NAMES[slot]}尚未保存。
{node.name}
- {node.description}
+
+ {preview.nodes.find(
+ (previewNode) => previewNode.id === node.id,
+ )?.description ?? node.description}
+
);
})}
diff --git a/src/shared/engine/sect/content/index.ts b/src/shared/engine/sect/content/index.ts
index 0f4fc976..e55432fe 100644
--- a/src/shared/engine/sect/content/index.ts
+++ b/src/shared/engine/sect/content/index.ts
@@ -41,11 +41,12 @@ export const resolveSectPathPreview = (args: {
realm: RealmType;
pathId: string;
}): ResolvedSectPathPreview => {
- const definition = productionSectRuntime.registry.require(
- args.sect.sectId,
- ).definition;
+ const sectModule = productionSectRuntime.registry.require(args.sect.sectId);
+ const definition = sectModule.definition;
const path = definition.paths.find((entry) => entry.id === args.pathId);
if (!path) throw new Error(`未知宗门流派: ${args.pathId}`);
+ const pathModule = sectModule.paths.get(path.id);
+ if (!pathModule) throw new Error(`未知宗门流派模块: ${args.pathId}`);
const learnedPath = args.sect.paths.find(
(entry) => entry.pathId === args.pathId,
);
@@ -74,6 +75,16 @@ export const resolveSectPathPreview = (args: {
learned: Boolean(learnedPath),
active: args.sect.activePathId === path.id,
activeMeridianSlot: learnedPath?.activeMeridianSlot,
+ nodes: path.nodes.map((node) => {
+ const plugin = pathModule.nodes.get(node.id);
+ return {
+ id: node.id,
+ name: node.name,
+ description:
+ plugin?.describe?.({ sect: args.sect, realm: args.realm }) ??
+ node.description,
+ };
+ }),
abilities: definition.abilities.map((ability) => {
const baseline = productionSectRuntime.resolveAbility({
sect: baselineState,
diff --git a/src/shared/engine/sect/content/lingxiao/base/LingxiaoBaseCompiler.ts b/src/shared/engine/sect/content/lingxiao/base/LingxiaoBaseCompiler.ts
index b0d35b98..a138f800 100644
--- a/src/shared/engine/sect/content/lingxiao/base/LingxiaoBaseCompiler.ts
+++ b/src/shared/engine/sect/content/lingxiao/base/LingxiaoBaseCompiler.ts
@@ -117,6 +117,7 @@ export function compileLingxiaoBase(
],
});
active('turning-body', {
+ targetTeam: 'self',
effects: [],
castEffects: [
selfBuff(
@@ -167,10 +168,10 @@ export function compileLingxiaoBase(
active('breaking-edge', {
effects: [
sectEffects.physicalDamage(0.95),
- {
- type: 'dispel',
- params: { targetTag: GameplayTags.BUFF.TYPE.BUFF, maxCount: 1 },
- },
+ sectEffects.dispelPositiveBuffsByMethod(
+ 1,
+ context.sect.methods['edge-cleansing'],
+ ),
],
});
active('sword-aegis', {
diff --git a/src/shared/engine/sect/content/lingxiao/paths/heavy/HeavySwordBuildFacade.ts b/src/shared/engine/sect/content/lingxiao/paths/heavy/HeavySwordBuildFacade.ts
index 1e8d8a38..50d4db4a 100644
--- a/src/shared/engine/sect/content/lingxiao/paths/heavy/HeavySwordBuildFacade.ts
+++ b/src/shared/engine/sect/content/lingxiao/paths/heavy/HeavySwordBuildFacade.ts
@@ -1,11 +1,11 @@
+import { DamageSource } from '@shared/engine/battle-v5/core/types';
+import { GameplayTags } from '@shared/engine/shared/tag-domain';
import type {
SectBuildBuilder,
SectCompiledBuild,
SectPathCompileContext,
} from '../../../../core';
import { SectAbilityFactory, sectEffects } from '../../../../core';
-import { GameplayTags } from '@shared/engine/shared/tag-domain';
-import { DamageSource } from '@shared/engine/battle-v5/core/types';
import { LINGXIAO_SECT_ID } from '../../ids';
import { LINGXIAO_SWORD_MOMENTUM } from '../../shared/LingxiaoMechanics';
import {
@@ -41,6 +41,7 @@ export class HeavySwordBuildFacade {
this.baseBuild,
this.context.path,
this.features,
+ this.context.sect.methods['edge-cleansing'],
),
);
const resource = this.baseBuild.resources[0];
@@ -59,25 +60,32 @@ export function initializeHeavySwordBuild(
const facade = new HeavySwordBuildFacade(context, builder, builder.build());
builder.setExtension(HEAVY_BUILD_FACADE, facade);
const factory = new SectAbilityFactory(LINGXIAO_SECT_ID);
- builder.addPassive(factory.passive({
- id: 'heavy-shield-momentum',
- name: '大巧不工',
- pathId: context.path.pathId,
- listeners: [{
- id: 'sect.lingxiao.heavy.shield-momentum',
- eventType: GameplayTags.EVENT.DAMAGE_TAKEN,
- scope: GameplayTags.SCOPE.OWNER_AS_TARGET,
- priority: 0,
- mapping: { caster: 'owner', target: 'owner' },
- guard: { skipSecondaryDamageSource: true },
- budget: { maxTriggers: 1, reset: 'round' },
- conditions: [
- { type: 'damage_source_is', params: { damageSource: DamageSource.DIRECT } },
- { type: 'shield_absorbed_at_least', params: { value: 1 } },
+ builder.addPassive(
+ factory.passive({
+ id: 'heavy-shield-momentum',
+ name: '大巧不工',
+ pathId: context.path.pathId,
+ listeners: [
+ {
+ id: 'sect.lingxiao.heavy.shield-momentum',
+ eventType: GameplayTags.EVENT.DAMAGE_TAKEN,
+ scope: GameplayTags.SCOPE.OWNER_AS_TARGET,
+ priority: 0,
+ mapping: { caster: 'owner', target: 'owner' },
+ guard: { skipSecondaryDamageSource: true },
+ budget: { maxTriggers: 1, reset: 'round' },
+ conditions: [
+ {
+ type: 'damage_source_is',
+ params: { damageSource: DamageSource.DIRECT },
+ },
+ { type: 'shield_absorbed_at_least', params: { value: 1 } },
+ ],
+ effects: [sectEffects.modifyResource(LINGXIAO_SWORD_MOMENTUM, 1)],
+ },
],
- effects: [sectEffects.modifyResource(LINGXIAO_SWORD_MOMENTUM, 1)],
- }],
- }));
+ }),
+ );
}
export function heavySwordBuild(
diff --git a/src/shared/engine/sect/content/lingxiao/paths/heavy/HeavySwordPathModule.ts b/src/shared/engine/sect/content/lingxiao/paths/heavy/HeavySwordPathModule.ts
index a1279312..d182b68b 100644
--- a/src/shared/engine/sect/content/lingxiao/paths/heavy/HeavySwordPathModule.ts
+++ b/src/shared/engine/sect/content/lingxiao/paths/heavy/HeavySwordPathModule.ts
@@ -49,12 +49,14 @@ const HEAVY_SWORD_DEFINITION: SectPathDefinitionWithoutNodes = {
{
id: 'heavy-full',
name: '极势',
- description: '优先积蓄剑势与至少2层裂甲,达到6点剑势后以《剑破万法》全力收束。',
+ description:
+ '4点剑势起且裂甲不足2层时,提前准备《藏锋听雷》;6点剑势但裂甲仍不足时先补甲,满足后以《剑破万法》收束。',
},
{
id: 'heavy-guard',
name: '守山',
- description: '优先建立护盾并施展《剑心通明》,剑势达到5点后再行收束。',
+ description:
+ '无护盾时优先《踏雪无痕》;气血低于65%且《剑心通明》缺失时施展剑心;剑势达到5点后收束。',
},
],
};
diff --git a/src/shared/engine/sect/content/lingxiao/paths/heavy/nodes/layer1.ts b/src/shared/engine/sect/content/lingxiao/paths/heavy/nodes/layer1.ts
index be83e88c..50083550 100644
--- a/src/shared/engine/sect/content/lingxiao/paths/heavy/nodes/layer1.ts
+++ b/src/shared/engine/sect/content/lingxiao/paths/heavy/nodes/layer1.ts
@@ -1,5 +1,9 @@
import { GameplayTags } from '@shared/engine/shared/tag-domain';
-import { DAMAGE_MODIFIER_PRIORITY, DIRECT_DAMAGE_CONDITION, sectEffects } from '../../../../../core';
+import {
+ DAMAGE_MODIFIER_PRIORITY,
+ DIRECT_DAMAGE_CONDITION,
+ sectEffects,
+} from '../../../../../core';
import { createLingxiaoNode } from '../../../shared/createLingxiaoNode';
import { LINGXIAO_SWORD_MOMENTUM } from '../../../shared/LingxiaoMechanics';
import { addLingxiaoPassive } from '../../../shared/SwordNodePassives';
@@ -7,43 +11,86 @@ import { heavySwordBuild } from '../HeavySwordBuildFacade';
export const HEAVY_LAYER_1_NODES = [
createLingxiaoNode(
- { id: 'heavy-opening', layerId: '1', name: '立地', description: '战斗开始时获得1点剑势,并获得相当于35%物攻的护盾。' },
+ {
+ id: 'heavy-opening',
+ layerId: '1',
+ name: '立地',
+ description: '战斗开始时获得1点剑势,并获得相当于35%物攻的护盾。',
+ },
(context, builder) => {
heavySwordBuild(builder).enable('opening');
addLingxiaoPassive(context, builder, {
- id: 'heavy-opening', name: '立地', listeners: [{
- id: 'sect.lingxiao.heavy.opening', eventType: 'BattleInitEvent',
- scope: GameplayTags.SCOPE.GLOBAL, priority: 0,
- mapping: { caster: 'owner', target: 'owner' }, budget: { maxTriggers: 1, reset: 'battle' },
- effects: [sectEffects.shieldByAttack(0.35, undefined, 'caster')],
- }],
+ id: 'heavy-opening',
+ name: '立地',
+ listeners: [
+ {
+ id: 'sect.lingxiao.heavy.opening',
+ eventType: 'BattleInitEvent',
+ scope: GameplayTags.SCOPE.GLOBAL,
+ priority: 0,
+ mapping: { caster: 'owner', target: 'owner' },
+ budget: { maxTriggers: 1, reset: 'battle' },
+ effects: [sectEffects.shieldByAttack(0.35, undefined, 'caster')],
+ },
+ ],
});
},
),
createLingxiaoNode(
- { id: 'heavy-hidden-weight', layerId: '1', name: '承锋', description: '本场战斗首次受到直接伤害时,该次伤害降低15%,并获得2点剑势。' },
- (context, builder) => addLingxiaoPassive(context, builder, {
- id: 'heavy-hidden-weight', name: '承锋', listeners: [{
- id: 'sect.lingxiao.heavy.bearing-edge', eventType: GameplayTags.EVENT.DAMAGE_REQUEST,
- scope: GameplayTags.SCOPE.OWNER_AS_TARGET, priority: DAMAGE_MODIFIER_PRIORITY,
- mapping: { caster: 'owner', target: 'owner' }, budget: { maxTriggers: 1, reset: 'battle' },
- guard: { skipSecondaryDamageSource: true }, conditions: [DIRECT_DAMAGE_CONDITION],
- effects: [
- { type: 'percent_damage_modifier', params: { mode: 'reduce', value: 0.15 } },
- sectEffects.modifyResource(LINGXIAO_SWORD_MOMENTUM, 2),
+ {
+ id: 'heavy-hidden-weight',
+ layerId: '1',
+ name: '承锋',
+ description:
+ '本场战斗首次受到直接伤害时,该次伤害降低15%,并获得2点剑势。',
+ },
+ (context, builder) =>
+ addLingxiaoPassive(context, builder, {
+ id: 'heavy-hidden-weight',
+ name: '承锋',
+ listeners: [
+ {
+ id: 'sect.lingxiao.heavy.bearing-edge',
+ eventType: GameplayTags.EVENT.DAMAGE_REQUEST,
+ scope: GameplayTags.SCOPE.OWNER_AS_TARGET,
+ priority: DAMAGE_MODIFIER_PRIORITY,
+ mapping: { caster: 'owner', target: 'owner' },
+ budget: { maxTriggers: 1, reset: 'battle' },
+ guard: { skipSecondaryDamageSource: true },
+ conditions: [DIRECT_DAMAGE_CONDITION],
+ effects: [
+ {
+ type: 'percent_damage_modifier',
+ params: { mode: 'reduce', value: 0.15 },
+ },
+ sectEffects.modifyResource(LINGXIAO_SWORD_MOMENTUM, 2),
+ ],
+ },
],
- }],
- }),
+ }),
),
createLingxiaoNode(
- { id: 'heavy-testing-frame', layerId: '1', name: '守拙', description: '每回合首次护盾破裂时,额外获得1点剑势。' },
- (context, builder) => addLingxiaoPassive(context, builder, {
- id: 'heavy-testing-frame', name: '守拙', listeners: [{
- id: 'sect.lingxiao.heavy.simple-guard', eventType: 'ShieldBreakEvent',
- scope: GameplayTags.SCOPE.OWNER_AS_TARGET, priority: 0,
- mapping: { caster: 'owner', target: 'owner' }, budget: { maxTriggers: 1, reset: 'round' },
- effects: [sectEffects.modifyResource(LINGXIAO_SWORD_MOMENTUM, 1)],
- }],
- }),
+ {
+ id: 'heavy-testing-frame',
+ layerId: '1',
+ name: '守拙',
+ description: '每回合首次护盾破裂时,额外获得1点剑势。',
+ },
+ (context, builder) =>
+ addLingxiaoPassive(context, builder, {
+ id: 'heavy-testing-frame',
+ name: '守拙',
+ listeners: [
+ {
+ id: 'sect.lingxiao.heavy.simple-guard',
+ eventType: 'ShieldBreakEvent',
+ scope: GameplayTags.SCOPE.OWNER_AS_TARGET,
+ priority: 0,
+ mapping: { caster: 'owner', target: 'owner' },
+ budget: { maxTriggers: 1, reset: 'round' },
+ effects: [sectEffects.modifyResource(LINGXIAO_SWORD_MOMENTUM, 1)],
+ },
+ ],
+ }),
),
] as const;
diff --git a/src/shared/engine/sect/content/lingxiao/paths/heavy/nodes/layer2.ts b/src/shared/engine/sect/content/lingxiao/paths/heavy/nodes/layer2.ts
index dc368d53..3a8e0d72 100644
--- a/src/shared/engine/sect/content/lingxiao/paths/heavy/nodes/layer2.ts
+++ b/src/shared/engine/sect/content/lingxiao/paths/heavy/nodes/layer2.ts
@@ -1,17 +1,49 @@
import { createLingxiaoNode } from '../../../shared/createLingxiaoNode';
+import {
+ growthMagnitude,
+ growthStatusMagnitude,
+ nodePercent,
+} from '../../../shared/LingxiaoNodeDescription';
import { heavySwordBuild } from '../HeavySwordBuildFacade';
+import {
+ HEAVY_CHARGED_GUARD_SHIELD_COEFFICIENT,
+ HEAVY_CHARGED_REDUCTION,
+ HEAVY_CHARGED_STRIKE_COEFFICIENT,
+} from '../variants';
export const HEAVY_LAYER_2_NODES = [
createLingxiaoNode(
- { id: 'heavy-triple-ridge', layerId: '2', name: '蓄岳', description: '《藏锋听雷》的直接伤害减免由24%提高至32%。' },
+ {
+ id: 'heavy-triple-ridge',
+ layerId: '2',
+ name: '蓄岳',
+ description: '提高《藏锋听雷》蓄势期间随《凌虚步》成长的直接伤害减免。',
+ },
(_context, builder) => heavySwordBuild(builder).enable('chargedReduction'),
+ (context) =>
+ `《藏锋听雷》蓄势期间受到的直接伤害降低${nodePercent(growthStatusMagnitude(context, 'void-step', HEAVY_CHARGED_REDUCTION))}。`,
),
createLingxiaoNode(
- { id: 'heavy-shattering-armor', layerId: '2', name: '听雷', description: '《藏锋听雷》的后发攻击由176%物攻提高至208%物攻,并共施加2层裂甲。' },
+ {
+ id: 'heavy-shattering-armor',
+ layerId: '2',
+ name: '听雷',
+ description: '提高《藏锋听雷》的后发攻击,并使其共施加2层裂甲。',
+ },
(_context, builder) => heavySwordBuild(builder).enable('chargedStrike'),
+ (context) =>
+ `《藏锋听雷》的后发《听雷》造成相当于${nodePercent(growthMagnitude(context, 'void-step', HEAVY_CHARGED_STRIKE_COEFFICIENT))}物攻的伤害,并共施加2层裂甲。`,
),
createLingxiaoNode(
- { id: 'heavy-retained-frame', layerId: '2', name: '守心', description: '施展《藏锋听雷》开始蓄势时,获得相当于48%物攻的护盾。' },
- (_context, builder) => heavySwordBuild(builder).enable('chargedGuardShield'),
+ {
+ id: 'heavy-retained-frame',
+ layerId: '2',
+ name: '守心',
+ description: '施展《藏锋听雷》开始蓄势时,获得随《凌虚步》成长的护盾。',
+ },
+ (_context, builder) =>
+ heavySwordBuild(builder).enable('chargedGuardShield'),
+ (context) =>
+ `施展《藏锋听雷》开始蓄势时,获得相当于${nodePercent(growthMagnitude(context, 'void-step', HEAVY_CHARGED_GUARD_SHIELD_COEFFICIENT))}物攻的护盾。`,
),
] as const;
diff --git a/src/shared/engine/sect/content/lingxiao/paths/heavy/nodes/layer3.ts b/src/shared/engine/sect/content/lingxiao/paths/heavy/nodes/layer3.ts
index d46f68f5..5d859e6b 100644
--- a/src/shared/engine/sect/content/lingxiao/paths/heavy/nodes/layer3.ts
+++ b/src/shared/engine/sect/content/lingxiao/paths/heavy/nodes/layer3.ts
@@ -1,37 +1,94 @@
import { DamageSource } from '@shared/engine/battle-v5/core/types';
import { GameplayTags } from '@shared/engine/shared/tag-domain';
-import { DAMAGE_MODIFIER_PRIORITY, DIRECT_DAMAGE_CONDITION, sectEffects } from '../../../../../core';
+import {
+ DAMAGE_MODIFIER_PRIORITY,
+ DIRECT_DAMAGE_CONDITION,
+ sectEffects,
+} from '../../../../../core';
import { createLingxiaoNode } from '../../../shared/createLingxiaoNode';
+import {
+ growthMagnitude,
+ nodePercent,
+} from '../../../shared/LingxiaoNodeDescription';
import { addLingxiaoPassive } from '../../../shared/SwordNodePassives';
import { heavySwordBuild } from '../HeavySwordBuildFacade';
export const HEAVY_LAYER_3_NODES = [
createLingxiaoNode(
- { id: 'heavy-crossing-pass', layerId: '3', name: '山门', description: '《踏雪无痕》提供的护盾提高50%。' },
+ {
+ id: 'heavy-crossing-pass',
+ layerId: '3',
+ name: '山门',
+ description: '《踏雪无痕》提供的护盾提高50%。',
+ },
(_context, builder) => heavySwordBuild(builder).enable('mountainGate'),
+ (context) =>
+ `《踏雪无痕》提供的护盾提高50%,当前获得相当于${nodePercent(growthMagnitude(context, 'void-step', 0.52 * 1.5))}物攻的护盾。`,
),
createLingxiaoNode(
- { id: 'heavy-borrowed-weight', layerId: '3', name: '回澜', description: '护盾破裂时,反击造成相当于75%物攻的伤害,每回合最多触发一次。' },
- (context, builder) => addLingxiaoPassive(context, builder, {
- id: 'heavy-borrowed-weight', name: '回澜', listeners: [{
- id: 'sect.lingxiao.heavy.returning-wave', eventType: 'ShieldBreakEvent',
- scope: GameplayTags.SCOPE.OWNER_AS_TARGET, priority: 0,
- mapping: { caster: 'owner', target: 'event.caster' }, budget: { maxTriggers: 1, reset: 'round' },
- guard: { skipSecondaryDamageSource: true },
- effects: [sectEffects.physicalDamage(0.75, undefined, false, DamageSource.COUNTER)],
- }],
- }),
+ {
+ id: 'heavy-borrowed-weight',
+ layerId: '3',
+ name: '回澜',
+ description:
+ '护盾破裂时,反击造成相当于75%物攻的伤害,每回合最多触发一次。',
+ },
+ (context, builder) =>
+ addLingxiaoPassive(context, builder, {
+ id: 'heavy-borrowed-weight',
+ name: '回澜',
+ listeners: [
+ {
+ id: 'sect.lingxiao.heavy.returning-wave',
+ eventType: 'ShieldBreakEvent',
+ scope: GameplayTags.SCOPE.OWNER_AS_TARGET,
+ priority: 0,
+ mapping: { caster: 'owner', target: 'event.caster' },
+ budget: { maxTriggers: 1, reset: 'round' },
+ guard: { skipSecondaryDamageSource: true },
+ effects: [
+ sectEffects.physicalDamage(
+ 0.75,
+ undefined,
+ false,
+ DamageSource.COUNTER,
+ ),
+ ],
+ },
+ ],
+ }),
),
createLingxiaoNode(
- { id: 'heavy-unmoved', layerId: '3', name: '固守', description: '拥有护盾时,受到的直接伤害降低10%。' },
- (context, builder) => addLingxiaoPassive(context, builder, {
- id: 'heavy-unmoved', name: '固守', listeners: [{
- id: 'sect.lingxiao.heavy.solid-guard', eventType: GameplayTags.EVENT.DAMAGE_REQUEST,
- scope: GameplayTags.SCOPE.OWNER_AS_TARGET, priority: DAMAGE_MODIFIER_PRIORITY,
- mapping: { caster: 'owner', target: 'owner' }, guard: { skipSecondaryDamageSource: true },
- conditions: [DIRECT_DAMAGE_CONDITION, { type: 'has_shield', params: { scope: 'caster', value: 0 } }],
- effects: [{ type: 'percent_damage_modifier', params: { mode: 'reduce', value: 0.1 } }],
- }],
- }),
+ {
+ id: 'heavy-unmoved',
+ layerId: '3',
+ name: '固守',
+ description: '拥有护盾时,受到的直接伤害降低10%。',
+ },
+ (context, builder) =>
+ addLingxiaoPassive(context, builder, {
+ id: 'heavy-unmoved',
+ name: '固守',
+ listeners: [
+ {
+ id: 'sect.lingxiao.heavy.solid-guard',
+ eventType: GameplayTags.EVENT.DAMAGE_REQUEST,
+ scope: GameplayTags.SCOPE.OWNER_AS_TARGET,
+ priority: DAMAGE_MODIFIER_PRIORITY,
+ mapping: { caster: 'owner', target: 'owner' },
+ guard: { skipSecondaryDamageSource: true },
+ conditions: [
+ DIRECT_DAMAGE_CONDITION,
+ { type: 'has_shield', params: { scope: 'caster', value: 0 } },
+ ],
+ effects: [
+ {
+ type: 'percent_damage_modifier',
+ params: { mode: 'reduce', value: 0.1 },
+ },
+ ],
+ },
+ ],
+ }),
),
] as const;
diff --git a/src/shared/engine/sect/content/lingxiao/paths/heavy/nodes/layer4.ts b/src/shared/engine/sect/content/lingxiao/paths/heavy/nodes/layer4.ts
index 1faa3a73..0ab9388a 100644
--- a/src/shared/engine/sect/content/lingxiao/paths/heavy/nodes/layer4.ts
+++ b/src/shared/engine/sect/content/lingxiao/paths/heavy/nodes/layer4.ts
@@ -1,4 +1,7 @@
-import { AttributeType, DamageSource } from '@shared/engine/battle-v5/core/types';
+import {
+ AttributeType,
+ DamageSource,
+} from '@shared/engine/battle-v5/core/types';
import { GameplayTags } from '@shared/engine/shared/tag-domain';
import { sectEffects } from '../../../../../core';
import { createLingxiaoNode } from '../../../shared/createLingxiaoNode';
@@ -6,67 +9,140 @@ import { LINGXIAO_SWORD_MOMENTUM } from '../../../shared/LingxiaoMechanics';
import { addLingxiaoPassive } from '../../../shared/SwordNodePassives';
const shieldHitConditions = [
- { type: 'damage_source_is' as const, params: { damageSource: DamageSource.DIRECT } },
+ {
+ type: 'damage_source_is' as const,
+ params: { damageSource: DamageSource.DIRECT },
+ },
{ type: 'shield_absorbed_at_least' as const, params: { value: 1 } },
];
export const HEAVY_LAYER_4_NODES = [
createLingxiaoNode(
- { id: 'heavy-rending-mountain', layerId: '4', name: '横关', description: '每回合首次以护盾吸收直接伤害时,反击造成相当于75%物攻的伤害,并获得1点剑势。' },
- (context, builder) => addLingxiaoPassive(context, builder, {
- id: 'heavy-rending-mountain', name: '横关', listeners: [{
- id: 'sect.lingxiao.heavy.crossbar', eventType: GameplayTags.EVENT.DAMAGE_TAKEN,
- scope: GameplayTags.SCOPE.OWNER_AS_TARGET, priority: 0,
- mapping: { caster: 'owner', target: 'event.caster' }, budget: { maxTriggers: 1, reset: 'round' },
- guard: { skipSecondaryDamageSource: true }, conditions: shieldHitConditions,
- effects: [
- sectEffects.physicalDamage(0.75, undefined, false, DamageSource.COUNTER),
- sectEffects.modifyResource(LINGXIAO_SWORD_MOMENTUM, 1),
- ],
- }],
- }),
- ),
- createLingxiaoNode(
- { id: 'heavy-ending-life', layerId: '4', name: '借力', description: '返还本次护盾吸收量30%的伤害,反击伤害不超过相当于90%物攻。' },
- (context, builder) => addLingxiaoPassive(context, builder, {
- id: 'heavy-ending-life', name: '借力', listeners: [{
- id: 'sect.lingxiao.heavy.borrow-force', eventType: GameplayTags.EVENT.DAMAGE_TAKEN,
- scope: GameplayTags.SCOPE.OWNER_AS_TARGET, priority: 0,
- mapping: { caster: 'owner', target: 'event.caster' },
- guard: { skipSecondaryDamageSource: true }, conditions: shieldHitConditions,
- effects: [
+ {
+ id: 'heavy-rending-mountain',
+ layerId: '4',
+ name: '横关',
+ description:
+ '每回合首次以护盾吸收直接伤害时,反击造成相当于55%物攻的伤害,并获得1点剑势。',
+ },
+ (context, builder) =>
+ addLingxiaoPassive(context, builder, {
+ id: 'heavy-rending-mountain',
+ name: '横关',
+ listeners: [
{
- type: 'damage_memory',
- params: {
- key: 'sect.lingxiao.heavy.borrow-force', mode: 'record', event: 'shield_absorbed',
- target: 'caster', maxStoredValue: { attribute: AttributeType.ATK, coefficient: 3 },
- },
- },
- {
- type: 'damage_memory',
- params: {
- key: 'sect.lingxiao.heavy.borrow-force', mode: 'release', ratio: 0.3,
- releaseAs: 'counter', target: 'caster', consume: true,
- },
+ id: 'sect.lingxiao.heavy.crossbar',
+ eventType: GameplayTags.EVENT.DAMAGE_TAKEN,
+ scope: GameplayTags.SCOPE.OWNER_AS_TARGET,
+ priority: 0,
+ mapping: { caster: 'owner', target: 'event.caster' },
+ budget: { maxTriggers: 1, reset: 'round' },
+ guard: { skipSecondaryDamageSource: true },
+ conditions: shieldHitConditions,
+ effects: [
+ sectEffects.physicalDamage(
+ 0.55,
+ undefined,
+ false,
+ DamageSource.COUNTER,
+ ),
+ sectEffects.modifyResource(LINGXIAO_SWORD_MOMENTUM, 1),
+ ],
},
],
- }],
- }),
+ }),
),
createLingxiaoNode(
- { id: 'heavy-returning-peak', layerId: '4', name: '震锋', description: '每回合首次受到直接伤害时,反击造成相当于55%物攻的伤害,并驱散敌方1个正面状态。' },
- (context, builder) => addLingxiaoPassive(context, builder, {
- id: 'heavy-returning-peak', name: '震锋', listeners: [{
- id: 'sect.lingxiao.heavy.shocking-edge', eventType: GameplayTags.EVENT.DAMAGE_TAKEN,
- scope: GameplayTags.SCOPE.OWNER_AS_TARGET, priority: 0,
- mapping: { caster: 'owner', target: 'event.caster' }, budget: { maxTriggers: 1, reset: 'round' },
- guard: { skipSecondaryDamageSource: true },
- conditions: [{ type: 'damage_source_is', params: { damageSource: DamageSource.DIRECT } }],
- effects: [
- sectEffects.physicalDamage(0.55, undefined, false, DamageSource.COUNTER),
- { type: 'dispel', params: { targetTag: GameplayTags.BUFF.TYPE.BUFF, maxCount: 1 } },
+ {
+ id: 'heavy-ending-life',
+ layerId: '4',
+ name: '借力',
+ description: '返还本次护盾吸收量20%的伤害,反击伤害不超过相当于60%物攻。',
+ },
+ (context, builder) =>
+ addLingxiaoPassive(context, builder, {
+ id: 'heavy-ending-life',
+ name: '借力',
+ listeners: [
+ {
+ id: 'sect.lingxiao.heavy.borrow-force',
+ eventType: GameplayTags.EVENT.DAMAGE_TAKEN,
+ scope: GameplayTags.SCOPE.OWNER_AS_TARGET,
+ priority: 0,
+ mapping: { caster: 'owner', target: 'event.caster' },
+ guard: { skipSecondaryDamageSource: true },
+ conditions: shieldHitConditions,
+ effects: [
+ {
+ type: 'damage_memory',
+ params: {
+ key: 'sect.lingxiao.heavy.borrow-force',
+ mode: 'record',
+ event: 'shield_absorbed',
+ target: 'caster',
+ maxStoredValue: {
+ attribute: AttributeType.ATK,
+ coefficient: 3,
+ },
+ },
+ },
+ {
+ type: 'damage_memory',
+ params: {
+ key: 'sect.lingxiao.heavy.borrow-force',
+ mode: 'release',
+ ratio: 0.2,
+ releaseAs: 'counter',
+ target: 'caster',
+ consume: true,
+ },
+ },
+ ],
+ },
],
- }],
- }),
+ }),
+ ),
+ createLingxiaoNode(
+ {
+ id: 'heavy-returning-peak',
+ layerId: '4',
+ name: '震锋',
+ description:
+ '每回合首次受到直接伤害时,反击造成相当于40%物攻的伤害,并驱散敌方1个正面状态。',
+ },
+ (context, builder) =>
+ addLingxiaoPassive(context, builder, {
+ id: 'heavy-returning-peak',
+ name: '震锋',
+ listeners: [
+ {
+ id: 'sect.lingxiao.heavy.shocking-edge',
+ eventType: GameplayTags.EVENT.DAMAGE_TAKEN,
+ scope: GameplayTags.SCOPE.OWNER_AS_TARGET,
+ priority: 0,
+ mapping: { caster: 'owner', target: 'event.caster' },
+ budget: { maxTriggers: 1, reset: 'round' },
+ guard: { skipSecondaryDamageSource: true },
+ conditions: [
+ {
+ type: 'damage_source_is',
+ params: { damageSource: DamageSource.DIRECT },
+ },
+ ],
+ effects: [
+ sectEffects.physicalDamage(
+ 0.4,
+ undefined,
+ false,
+ DamageSource.COUNTER,
+ ),
+ {
+ type: 'dispel',
+ params: { targetTag: GameplayTags.BUFF.TYPE.BUFF, maxCount: 1 },
+ },
+ ],
+ },
+ ],
+ }),
),
] as const;
diff --git a/src/shared/engine/sect/content/lingxiao/paths/heavy/nodes/layer5.ts b/src/shared/engine/sect/content/lingxiao/paths/heavy/nodes/layer5.ts
index 38c2dee6..a25907a2 100644
--- a/src/shared/engine/sect/content/lingxiao/paths/heavy/nodes/layer5.ts
+++ b/src/shared/engine/sect/content/lingxiao/paths/heavy/nodes/layer5.ts
@@ -1,35 +1,77 @@
import { GameplayTags } from '@shared/engine/shared/tag-domain';
import { DAMAGE_MODIFIER_PRIORITY } from '../../../../../core';
import { createLingxiaoNode } from '../../../shared/createLingxiaoNode';
+import {
+ growthMagnitude,
+ nodePercent,
+} from '../../../shared/LingxiaoNodeDescription';
import { addLingxiaoPassive } from '../../../shared/SwordNodePassives';
import { heavySwordBuild } from '../HeavySwordBuildFacade';
+import { HEAVY_RETURNING_PEAK_SHIELD_COEFFICIENT } from '../variants';
export const HEAVY_LAYER_5_NODES = [
createLingxiaoNode(
- { id: 'heavy-aftershock', layerId: '5', name: '裂岳', description: '《剑破万法》获得15%穿防。' },
+ {
+ id: 'heavy-aftershock',
+ layerId: '5',
+ name: '裂岳',
+ description: '《剑破万法》获得15%穿防。',
+ },
(_context, builder) => heavySwordBuild(builder).enable('rendingMountain'),
),
createLingxiaoNode(
- { id: 'heavy-linked-mountains', layerId: '5', name: '断命', description: '目标气血低于25%时,《剑破万法》造成的伤害提高25%。' },
- (context, builder) => addLingxiaoPassive(context, builder, {
- id: 'heavy-linked-mountains', name: '断命', listeners: [{
- id: 'sect.lingxiao.heavy.life-ending', eventType: GameplayTags.EVENT.DAMAGE_REQUEST,
- scope: GameplayTags.SCOPE.OWNER_AS_CASTER, priority: DAMAGE_MODIFIER_PRIORITY,
- mapping: { caster: 'owner', target: 'event.target' },
- conditions: [
- { type: 'ability_has_tag', params: { tag: GameplayTags.ABILITY.SECT.FINISHER } },
- { type: 'hp_below', params: { value: 0.25, scope: 'target' } },
+ {
+ id: 'heavy-linked-mountains',
+ layerId: '5',
+ name: '断命',
+ description: '目标气血低于25%时,《剑破万法》造成的伤害提高15%。',
+ },
+ (context, builder) =>
+ addLingxiaoPassive(context, builder, {
+ id: 'heavy-linked-mountains',
+ name: '断命',
+ listeners: [
+ {
+ id: 'sect.lingxiao.heavy.life-ending',
+ eventType: GameplayTags.EVENT.DAMAGE_REQUEST,
+ scope: GameplayTags.SCOPE.OWNER_AS_CASTER,
+ priority: DAMAGE_MODIFIER_PRIORITY,
+ mapping: { caster: 'owner', target: 'event.target' },
+ conditions: [
+ {
+ type: 'ability_has_tag',
+ params: { tag: GameplayTags.ABILITY.SECT.FINISHER },
+ },
+ { type: 'hp_below', params: { value: 0.25, scope: 'target' } },
+ ],
+ effects: [
+ {
+ type: 'percent_damage_modifier',
+ params: { mode: 'increase', value: 0.15 },
+ },
+ ],
+ },
],
- effects: [{ type: 'percent_damage_modifier', params: { mode: 'increase', value: 0.25 } }],
- }],
- presentationModifiers: [{
- abilityId: 'sect-ultimate',
- factRows: ['参悟·断命:目标气血低于25%时,《剑破万法》造成的伤害提高25%'],
- }],
- }),
+ presentationModifiers: [
+ {
+ abilityId: 'sect-ultimate',
+ factRows: [
+ '参悟·断命:目标气血低于25%时,《剑破万法》造成的伤害提高15%',
+ ],
+ },
+ ],
+ }),
),
createLingxiaoNode(
- { id: 'heavy-steady-mountain', layerId: '5', name: '回峰', description: '《剑破万法》伤害降低15%,施展后返还2点剑势,并获得相当于48%物攻的护盾。' },
+ {
+ id: 'heavy-steady-mountain',
+ layerId: '5',
+ name: '回峰',
+ description:
+ '《剑破万法》伤害降低15%,施展后返还2点剑势,并获得随《问剑篇》成长的护盾。',
+ },
(_context, builder) => heavySwordBuild(builder).enable('returningPeak'),
+ (context) =>
+ `《剑破万法》伤害降低15%;施展后返还2点剑势,并获得相当于${nodePercent(growthMagnitude(context, 'lingxiao-canon', HEAVY_RETURNING_PEAK_SHIELD_COEFFICIENT))}物攻的护盾。`,
),
] as const;
diff --git a/src/shared/engine/sect/content/lingxiao/paths/heavy/nodes/ultimate.ts b/src/shared/engine/sect/content/lingxiao/paths/heavy/nodes/ultimate.ts
index 1cffe7c2..de4d4569 100644
--- a/src/shared/engine/sect/content/lingxiao/paths/heavy/nodes/ultimate.ts
+++ b/src/shared/engine/sect/content/lingxiao/paths/heavy/nodes/ultimate.ts
@@ -2,8 +2,20 @@ import { GameplayTags } from '@shared/engine/shared/tag-domain';
import { sectEffects } from '../../../../../core';
import { createLingxiaoNode } from '../../../shared/createLingxiaoNode';
import { HEAVY_ECHO_COOLDOWN } from '../../../shared/LingxiaoMechanics';
+import {
+ growthDuration,
+ growthMagnitude,
+ nodePercent,
+} from '../../../shared/LingxiaoNodeDescription';
import { addLingxiaoPassive } from '../../../shared/SwordNodePassives';
import { heavySwordBuild } from '../HeavySwordBuildFacade';
+import {
+ HEAVY_ECHO_HEAL_RATIO,
+ HEAVY_ECHO_SHIELD_COEFFICIENT,
+ HEAVY_HEAVEN_CLEAVING_TOTAL_COEFFICIENT,
+ HEAVY_IMMOVABLE_COUNTER_COEFFICIENT,
+ HEAVY_IMMOVABLE_SHIELD_COEFFICIENT,
+} from '../variants';
export const HEAVY_ULTIMATE_NODES = [
createLingxiaoNode(
@@ -12,9 +24,11 @@ export const HEAVY_ULTIMATE_NODES = [
layerId: 'ultimate',
name: '开天',
description:
- '《剑破万法》在6点剑势时的基础总倍率提高至280%物攻,并获得20%穿防;冷却增加1回合。其他伤害修正仍照常叠加。',
+ '《剑破万法》改为仅可在6点剑势时施展,提高随《问剑篇》成长的总倍率并获得20%穿防;冷却增加1回合。',
},
(_context, builder) => heavySwordBuild(builder).enable('heavenCleaving'),
+ (context) =>
+ `《剑破万法》仅可在6点剑势时施展,当前总倍率为${nodePercent(growthMagnitude(context, 'lingxiao-canon', HEAVY_HEAVEN_CLEAVING_TOTAL_COEFFICIENT))}物攻,并获得20%穿防;冷却增加1回合。`,
),
createLingxiaoNode(
{
@@ -22,9 +36,11 @@ export const HEAVY_ULTIMATE_NODES = [
layerId: 'ultimate',
name: '不动如山',
description:
- '《剑心通明》额外提供相当于80%物攻的护盾;持续期间每回合可反击一次,造成相当于48%物攻的伤害。',
+ '《剑心通明》额外提供随《万法不侵》成长的护盾;持续期间每回合可反击一次,造成随该心法成长的伤害。',
},
(_context, builder) => heavySwordBuild(builder).enable('immovableMountain'),
+ (context) =>
+ `《剑心通明》额外提供相当于${nodePercent(growthMagnitude(context, 'origin-returning', HEAVY_IMMOVABLE_SHIELD_COEFFICIENT))}物攻的护盾;未来${growthDuration(context, 'origin-returning', 3)}次自身行动内,每回合首次受到直接伤害时反击,造成相当于${nodePercent(growthMagnitude(context, 'origin-returning', HEAVY_IMMOVABLE_COUNTER_COEFFICIENT))}物攻的伤害。`,
),
createLingxiaoNode(
{
@@ -32,7 +48,7 @@ export const HEAVY_ULTIMATE_NODES = [
layerId: 'ultimate',
name: '山河回响',
description:
- '施展《剑破万法》后恢复4%最大气血,并获得相当于64%物攻的护盾,每3回合最多触发一次。',
+ '施展《剑破万法》后恢复气血并获得随《问剑篇》成长的护盾,每3回合最多触发一次。',
},
(context, builder) => {
heavySwordBuild(builder).enable('mountainRiverEcho');
@@ -61,5 +77,7 @@ export const HEAVY_ULTIMATE_NODES = [
],
});
},
+ (context) =>
+ `施展《剑破万法》后恢复${nodePercent(growthMagnitude(context, 'lingxiao-canon', HEAVY_ECHO_HEAL_RATIO))}最大气血,并获得相当于${nodePercent(growthMagnitude(context, 'lingxiao-canon', HEAVY_ECHO_SHIELD_COEFFICIENT))}物攻的护盾,每3回合最多触发一次。`,
),
] as const;
diff --git a/src/shared/engine/sect/content/lingxiao/paths/heavy/strategy.ts b/src/shared/engine/sect/content/lingxiao/paths/heavy/strategy.ts
index 1e7b367d..48f62cd3 100644
--- a/src/shared/engine/sect/content/lingxiao/paths/heavy/strategy.ts
+++ b/src/shared/engine/sect/content/lingxiao/paths/heavy/strategy.ts
@@ -19,10 +19,17 @@ export class LingxiaoHeavySelectionStrategy implements AbilitySelectionStrategy
const index = new SectStrategyCandidates(LINGXIAO_SECT_ID, candidates);
const momentum = caster.combatResources.getCurrent(LINGXIAO_SWORD_MOMENTUM);
const buffs = new Set(caster.buffs.getAllBuffIds());
- const finisherThreshold = this.tacticId === 'heavy-break' ? 3 : this.tacticId === 'heavy-guard' ? 5 : 6;
- const armorRend = opponent.buffs.getAllBuffs().find(
- (buff) => buff.id === LINGXIAO_ARMOR_REND_BUFF,
- )?.getLayer() ?? 0;
+ const finisherThreshold =
+ this.tacticId === 'heavy-break'
+ ? 3
+ : this.tacticId === 'heavy-guard'
+ ? 5
+ : 6;
+ const armorRend =
+ opponent.buffs
+ .getAllBuffs()
+ .find((buff) => buff.id === LINGXIAO_ARMOR_REND_BUFF)
+ ?.getLayer() ?? 0;
const mountainStep = index.find('shadow-step');
if (
@@ -41,12 +48,29 @@ export class LingxiaoHeavySelectionStrategy implements AbilitySelectionStrategy
) {
return index.resultForCandidate(sinking, 660);
}
+ const hidden = index.find('turning-body');
+ if (
+ this.tacticId === 'heavy-full' &&
+ momentum >= 4 &&
+ armorRend < 2 &&
+ hidden &&
+ !buffs.has('sect.lingxiao.heavy.hidden-edge')
+ ) {
+ return index.resultForCandidate(hidden, 650);
+ }
const heart = index.find('sword-aegis');
- if (heart && !buffs.has('sect.lingxiao.heavy.mountain-heart') && caster.getHpPercent() < 0.65) {
+ if (
+ heart &&
+ !buffs.has('sect.lingxiao.heavy.mountain-heart') &&
+ caster.getHpPercent() < 0.65
+ ) {
return index.resultForCandidate(heart, 620);
}
- const hidden = index.find('turning-body');
- if (hidden && this.tacticId === 'heavy-break' && !buffs.has('sect.lingxiao.heavy.hidden-edge')) {
+ if (
+ hidden &&
+ this.tacticId === 'heavy-break' &&
+ !buffs.has('sect.lingxiao.heavy.hidden-edge')
+ ) {
return index.resultForCandidate(hidden, 590);
}
const finisher = index.find('sect-ultimate');
@@ -55,13 +79,19 @@ export class LingxiaoHeavySelectionStrategy implements AbilitySelectionStrategy
momentum >= finisherThreshold &&
(this.tacticId !== 'heavy-full' || armorRend >= 2)
) {
- return index.resultForCandidate(finisher, opponent.getHpPercent() < 0.25 ? 570 : 520);
+ return index.resultForCandidate(
+ finisher,
+ opponent.getHpPercent() < 0.25 ? 570 : 520,
+ );
}
const heavyIntent = index.find('nurturing-sword');
if (heavyIntent && !buffs.has('sect.lingxiao.heavy.weightless-edge')) {
return index.resultForCandidate(heavyIntent, 400);
}
if (sinking) return index.resultForCandidate(sinking, 360);
- return index.resultForCandidate(index.find('guiding-sword') ?? candidates[0], 100);
+ return index.resultForCandidate(
+ index.find('guiding-sword') ?? candidates[0],
+ 100,
+ );
}
}
diff --git a/src/shared/engine/sect/content/lingxiao/paths/heavy/variants.ts b/src/shared/engine/sect/content/lingxiao/paths/heavy/variants.ts
index 6324356c..0e672ff2 100644
--- a/src/shared/engine/sect/content/lingxiao/paths/heavy/variants.ts
+++ b/src/shared/engine/sect/content/lingxiao/paths/heavy/variants.ts
@@ -56,6 +56,17 @@ export const EMPTY_HEAVY_FEATURES: HeavySwordFeatures = {
mountainRiverEcho: false,
};
+export const HEAVY_CHARGED_REDUCTION = 0.32;
+export const HEAVY_CHARGED_STRIKE_COEFFICIENT = 2.08;
+export const HEAVY_CHARGED_GUARD_SHIELD_COEFFICIENT = 0.36;
+export const HEAVY_RETURNING_PEAK_SHIELD_COEFFICIENT = 0.36;
+export const HEAVY_IMMOVABLE_SHIELD_COEFFICIENT = 0.56;
+export const HEAVY_IMMOVABLE_COUNTER_COEFFICIENT = 0.36;
+export const HEAVY_HEAVEN_CLEAVING_TOTAL_COEFFICIENT = 2.8;
+export const HEAVY_FINISHER_COEFFICIENT_PER_MOMENTUM = 0.29;
+export const HEAVY_ECHO_HEAL_RATIO = 0.04;
+export const HEAVY_ECHO_SHIELD_COEFFICIENT = 0.64;
+
const damage = (
coefficient: number,
damageSource: DamageSource = DamageSource.DIRECT,
@@ -119,6 +130,7 @@ export function buildHeavyAbilities(
baseBuild: Readonly,
path: CultivatorSectPathState,
features: HeavySwordFeatures,
+ edgeCleansingLevel?: number,
): Record {
const built = { ...baseBuild.abilities };
const factory = new SectAbilityFactory(LINGXIAO_SECT_ID);
@@ -173,6 +185,7 @@ export function buildHeavyAbilities(
id: 'turning-body',
cooldown: 3,
role: 'defensive',
+ targetTeam: 'self',
effects: [],
castEffects: [
selfBuff(
@@ -180,11 +193,19 @@ export function buildHeavyAbilities(
'藏锋听雷',
1,
[],
- reductionListeners(features.chargedReduction ? 0.32 : 0.24),
+ reductionListeners(
+ features.chargedReduction ? HEAVY_CHARGED_REDUCTION : 0.24,
+ ),
false,
),
...(features.chargedGuardShield
- ? [sectEffects.shieldByAttack(0.48, undefined, 'caster')]
+ ? [
+ sectEffects.shieldByAttack(
+ HEAVY_CHARGED_GUARD_SHIELD_COEFFICIENT,
+ undefined,
+ 'caster',
+ ),
+ ]
: []),
{
type: 'queue_action',
@@ -202,7 +223,9 @@ export function buildHeavyAbilities(
GameplayTags.ABILITY.TARGET.SINGLE,
],
effects: [
- damage(features.chargedStrike ? 2.08 : 1.76),
+ damage(
+ features.chargedStrike ? HEAVY_CHARGED_STRIKE_COEFFICIENT : 1.76,
+ ),
...createArmorRend(features.chargedStrike ? 2 : 1),
sectEffects.modifyResource(resourceId, 2),
],
@@ -235,10 +258,7 @@ export function buildHeavyAbilities(
role: 'utility',
effects: [
damage(1.04),
- {
- type: 'dispel',
- params: { targetTag: GameplayTags.BUFF.TYPE.BUFF, maxCount: 1 },
- },
+ sectEffects.dispelPositiveBuffsByMethod(1, edgeCleansingLevel),
],
});
built['sword-aegis'] = active({
@@ -248,7 +268,13 @@ export function buildHeavyAbilities(
targetTeam: 'self',
effects: [
...(features.immovableMountain
- ? [sectEffects.shieldByAttack(0.8, undefined, 'caster')]
+ ? [
+ sectEffects.shieldByAttack(
+ HEAVY_IMMOVABLE_SHIELD_COEFFICIENT,
+ undefined,
+ 'caster',
+ ),
+ ]
: []),
selfBuff(
'sect.lingxiao.heavy.mountain-heart',
@@ -277,7 +303,12 @@ export function buildHeavyAbilities(
guard: { skipSecondaryDamageSource: true },
budget: { maxTriggers: 1, reset: 'round' as const },
conditions: [DIRECT_DAMAGE_CONDITION],
- effects: [damage(0.48, DamageSource.COUNTER)],
+ effects: [
+ damage(
+ HEAVY_IMMOVABLE_COUNTER_COEFFICIENT,
+ DamageSource.COUNTER,
+ ),
+ ],
},
]
: []),
@@ -315,8 +346,13 @@ export function buildHeavyAbilities(
type: 'resource_scaled_damage',
params: {
resourceId,
- baseCoefficient: (heaven ? 1.06 : 0.88) * returnScale,
- coefficientPerPoint: 0.29 * returnScale,
+ baseCoefficient:
+ (heaven
+ ? HEAVY_HEAVEN_CLEAVING_TOTAL_COEFFICIENT -
+ HEAVY_FINISHER_COEFFICIENT_PER_MOMENTUM * 6
+ : 0.88) * returnScale,
+ coefficientPerPoint:
+ HEAVY_FINISHER_COEFFICIENT_PER_MOMENTUM * returnScale,
minPoints: heaven ? 6 : 3,
maxPoints: 6,
consume: 'all',
@@ -331,7 +367,11 @@ export function buildHeavyAbilities(
...(features.returningPeak
? [
sectEffects.modifyResource(resourceId, 2, undefined, 'refund'),
- sectEffects.shieldByAttack(0.48, undefined, 'caster'),
+ sectEffects.shieldByAttack(
+ HEAVY_RETURNING_PEAK_SHIELD_COEFFICIENT,
+ undefined,
+ 'caster',
+ ),
]
: []),
...(features.mountainRiverEcho
@@ -354,8 +394,12 @@ export function buildHeavyAbilities(
operation: 'set' as const,
amount: 3,
effects: [
- sectEffects.healMaxHp(0.04, 'caster'),
- sectEffects.shieldByAttack(0.64, undefined, 'caster'),
+ sectEffects.healMaxHp(HEAVY_ECHO_HEAL_RATIO, 'caster'),
+ sectEffects.shieldByAttack(
+ HEAVY_ECHO_SHIELD_COEFFICIENT,
+ undefined,
+ 'caster',
+ ),
],
},
},
diff --git a/src/shared/engine/sect/content/lingxiao/paths/swift/SwiftSwordBuildFacade.ts b/src/shared/engine/sect/content/lingxiao/paths/swift/SwiftSwordBuildFacade.ts
index c8e16b54..0dcea996 100644
--- a/src/shared/engine/sect/content/lingxiao/paths/swift/SwiftSwordBuildFacade.ts
+++ b/src/shared/engine/sect/content/lingxiao/paths/swift/SwiftSwordBuildFacade.ts
@@ -40,6 +40,7 @@ export class SwiftSwordBuildFacade {
this.baseBuild,
this.context.path,
this.features,
+ this.context.sect.methods['edge-cleansing'],
),
);
const resource = this.baseBuild.resources[0];
diff --git a/src/shared/engine/sect/content/lingxiao/paths/swift/SwiftSwordPathModule.ts b/src/shared/engine/sect/content/lingxiao/paths/swift/SwiftSwordPathModule.ts
index 9ed90825..37701768 100644
--- a/src/shared/engine/sect/content/lingxiao/paths/swift/SwiftSwordPathModule.ts
+++ b/src/shared/engine/sect/content/lingxiao/paths/swift/SwiftSwordPathModule.ts
@@ -53,7 +53,7 @@ const SWIFT_SWORD_DEFINITION: SectPathDefinitionWithoutNodes = {
id: 'counter',
name: '回燕',
description:
- '优先维持《藏锋听雷》与《踏雪无痕》,剑势达到5点后再行收束。',
+ '优先补《藏锋听雷》;身法不占优且《踏雪无痕》缺失时,先踏雪再施展《剑荡山河》;剑势达到5点后收束。',
},
],
};
diff --git a/src/shared/engine/sect/content/lingxiao/paths/swift/nodes/layer1.ts b/src/shared/engine/sect/content/lingxiao/paths/swift/nodes/layer1.ts
index 727bd687..37a6d43c 100644
--- a/src/shared/engine/sect/content/lingxiao/paths/swift/nodes/layer1.ts
+++ b/src/shared/engine/sect/content/lingxiao/paths/swift/nodes/layer1.ts
@@ -10,6 +10,11 @@ import {
LINGXIAO_SWORD_MOMENTUM,
createSwordMark,
} from '../../../shared/LingxiaoMechanics';
+import {
+ growthDuration,
+ growthStatusMagnitude,
+ nodePercent,
+} from '../../../shared/LingxiaoNodeDescription';
import {
addHiddenNodePassive,
addLingxiaoPassive,
@@ -71,7 +76,8 @@ export const SWIFT_LAYER_1_NODES = [
id: 'swift-hidden-edge',
layerId: '1',
name: '敛锋',
- description: '本场战斗首次受到直接伤害时,该次伤害降低10%,并获得3点剑势。',
+ description:
+ '本场战斗首次受到直接伤害时,该次伤害降低10%,并获得3点剑势。',
},
(context, builder) =>
addHiddenNodePassive(context, builder, {
@@ -85,7 +91,8 @@ export const SWIFT_LAYER_1_NODES = [
id: 'swift-probing-edge',
layerId: '1',
name: '探虚',
- description: '《基础剑式》每累计命中2次,额外获得1点剑势,并施加1层剑痕。',
+ description:
+ '《基础剑式》每累计命中2次,额外获得1点剑势,并施加1层随《问剑篇》成长的剑痕。',
},
(context, builder) =>
addProbingNodePassive(context, builder, {
@@ -95,5 +102,7 @@ export const SWIFT_LAYER_1_NODES = [
basicAbilityId: 'plain-sword',
statusEffect: createSwordMark(),
}),
+ (context) =>
+ `《基础剑式》每累计命中2次,额外获得1点剑势,并施加1层剑痕;每层使目标受到的直接、反击和追击伤害提高${nodePercent(growthStatusMagnitude(context, 'lingxiao-canon', 0.02))},持续目标未来${growthDuration(context, 'lingxiao-canon', 3)}次行动。`,
),
] as const;
diff --git a/src/shared/engine/sect/content/lingxiao/paths/swift/nodes/layer2.ts b/src/shared/engine/sect/content/lingxiao/paths/swift/nodes/layer2.ts
index 92b55453..43773f3b 100644
--- a/src/shared/engine/sect/content/lingxiao/paths/swift/nodes/layer2.ts
+++ b/src/shared/engine/sect/content/lingxiao/paths/swift/nodes/layer2.ts
@@ -1,17 +1,40 @@
import { createLingxiaoNode } from '../../../shared/createLingxiaoNode';
+import {
+ growthMagnitude,
+ nodePercent,
+} from '../../../shared/LingxiaoNodeDescription';
import { swiftSwordBuild } from '../SwiftSwordBuildFacade';
+import { SWIFT_SPLIT_LIGHT_HIT_COEFFICIENT } from '../variants';
export const SWIFT_LAYER_2_NODES = [
createLingxiaoNode(
- { id: 'swift-split-light', layerId: '2', name: '分光', description: '《剑荡山河》变为7段攻击,每段造成相当于23%物攻的伤害,并获得3点剑势。' },
+ {
+ id: 'swift-split-light',
+ layerId: '2',
+ name: '分光',
+ description:
+ '《剑荡山河》变为7段攻击并获得3点剑势;每段伤害随《剑气凌霄》成长。',
+ },
(_context, builder) => swiftSwordBuild(builder).enable('splitLight'),
+ (context) =>
+ `《剑荡山河》变为7段攻击,每段造成相当于${nodePercent(growthMagnitude(context, 'sword-guidance', SWIFT_SPLIT_LIGHT_HIT_COEFFICIENT))}物攻的伤害,并获得3点剑势。`,
),
createLingxiaoNode(
- { id: 'swift-stacking-waves', layerId: '2', name: '叠浪', description: '施展《剑荡山河》后,其当前冷却减少1回合。' },
+ {
+ id: 'swift-stacking-waves',
+ layerId: '2',
+ name: '叠浪',
+ description: '施展《剑荡山河》后,其当前冷却减少1回合。',
+ },
(_context, builder) => swiftSwordBuild(builder).enable('stackingWaves'),
),
createLingxiaoNode(
- { id: 'swift-retained-force', layerId: '2', name: '留痕', description: '《剑荡山河》额外施加1层剑痕,共施加2层。' },
+ {
+ id: 'swift-retained-force',
+ layerId: '2',
+ name: '留痕',
+ description: '《剑荡山河》额外施加1层剑痕,共施加2层。',
+ },
(_context, builder) => swiftSwordBuild(builder).enable('retainedForce'),
),
] as const;
diff --git a/src/shared/engine/sect/content/lingxiao/paths/swift/nodes/layer3.ts b/src/shared/engine/sect/content/lingxiao/paths/swift/nodes/layer3.ts
index 92d1aead..e5ea7e9b 100644
--- a/src/shared/engine/sect/content/lingxiao/paths/swift/nodes/layer3.ts
+++ b/src/shared/engine/sect/content/lingxiao/paths/swift/nodes/layer3.ts
@@ -5,11 +5,18 @@ import {
LINGXIAO_SWORD_MOMENTUM,
SWIFT_GUARDED_EDGE,
} from '../../../shared/LingxiaoMechanics';
+import {
+ growthDuration,
+ growthMagnitude,
+ growthStatusMagnitude,
+ nodePercent,
+} from '../../../shared/LingxiaoNodeDescription';
import {
addBorrowedNodePassive,
addLingxiaoPassive,
} from '../../../shared/SwordNodePassives';
import { swiftSwordBuild } from '../SwiftSwordBuildFacade';
+import { SWIFT_RETURNING_SWALLOW_COUNTER_COEFFICIENT } from '../variants';
export const SWIFT_LAYER_3_NODES = [
createLingxiaoNode(
@@ -17,9 +24,12 @@ export const SWIFT_LAYER_3_NODES = [
id: 'swift-returning-swallow',
layerId: '3',
name: '燕返',
- description: '《藏锋听雷》的反击伤害由40%物攻提高至60%物攻,命中后施加1层剑痕。',
+ description:
+ '提高《藏锋听雷》的首次闪避反击伤害,命中后施加1层随《问剑篇》成长的剑痕。',
},
(_context, builder) => swiftSwordBuild(builder).enable('returningSwallow'),
+ (context) =>
+ `《藏锋听雷》的首次闪避反击造成相当于${nodePercent(growthMagnitude(context, 'void-step', SWIFT_RETURNING_SWALLOW_COUNTER_COEFFICIENT))}物攻的伤害;命中后施加1层剑痕,每层使目标受到的直接、反击和追击伤害提高${nodePercent(growthStatusMagnitude(context, 'lingxiao-canon', 0.02))},持续目标未来${growthDuration(context, 'lingxiao-canon', 3)}次行动。`,
),
createLingxiaoNode(
{
@@ -40,7 +50,8 @@ export const SWIFT_LAYER_3_NODES = [
id: 'swift-guarded-edge',
layerId: '3',
name: '守锋',
- description: '被控制而跳过行动时剑势不衰减;下一次通过积势神通获得剑势时额外获得1点。',
+ description:
+ '被控制而跳过行动时剑势不衰减;下一次通过积势神通获得剑势时额外获得1点。',
},
(context, builder) => {
swiftSwordBuild(builder).enable('guardedEdge');
@@ -87,10 +98,14 @@ export const SWIFT_LAYER_3_NODES = [
],
},
],
- presentationModifiers: [{
- abilityId: 'guiding-sword',
- factRows: ['参悟·守锋:被控制而跳过行动时剑势不衰减;下一次通过积势神通获得剑势时额外获得1点'],
- }],
+ presentationModifiers: [
+ {
+ abilityId: 'guiding-sword',
+ factRows: [
+ '参悟·守锋:被控制而跳过行动时剑势不衰减;下一次通过积势神通获得剑势时额外获得1点',
+ ],
+ },
+ ],
});
},
),
diff --git a/src/shared/engine/sect/content/lingxiao/paths/swift/nodes/layer4.ts b/src/shared/engine/sect/content/lingxiao/paths/swift/nodes/layer4.ts
index e899a633..504053d5 100644
--- a/src/shared/engine/sect/content/lingxiao/paths/swift/nodes/layer4.ts
+++ b/src/shared/engine/sect/content/lingxiao/paths/swift/nodes/layer4.ts
@@ -1,8 +1,16 @@
import { GameplayTags } from '@shared/engine/shared/tag-domain';
import { DAMAGE_MODIFIER_PRIORITY } from '../../../../../core';
import { createLingxiaoNode } from '../../../shared/createLingxiaoNode';
+import {
+ growthMagnitude,
+ nodePercent,
+} from '../../../shared/LingxiaoNodeDescription';
import { addLingxiaoPassive } from '../../../shared/SwordNodePassives';
import { swiftSwordBuild } from '../SwiftSwordBuildFacade';
+import {
+ SWIFT_MOUNTAIN_BREAKING_COEFFICIENT,
+ SWIFT_SHEATHING_SHIELD_COEFFICIENT,
+} from '../variants';
export const SWIFT_LAYER_4_NODES = [
createLingxiaoNode(
@@ -10,16 +18,19 @@ export const SWIFT_LAYER_4_NODES = [
id: 'swift-mountain-breaking',
layerId: '4',
name: '破妄',
- description: '施展《剑破万法》时消耗全部剑痕;每消耗1层,追加一次相当于14.4%物攻且无视防御的伤害。',
+ description:
+ '施展《剑破万法》时消耗全部剑痕;每消耗1层,追加一次随《问剑篇》成长且无视防御的伤害。',
},
(_context, builder) => swiftSwordBuild(builder).enable('mountainBreaking'),
+ (context) =>
+ `施展《剑破万法》时消耗目标全部剑痕;每消耗1层,追加相当于${nodePercent(growthMagnitude(context, 'lingxiao-canon', SWIFT_MOUNTAIN_BREAKING_COEFFICIENT))}物攻且无视防御的伤害。`,
),
createLingxiaoNode(
{
id: 'swift-life-chasing',
layerId: '4',
name: '追命',
- description: '目标气血低于25%时,《剑破万法》造成的伤害提高25%。',
+ description: '目标气血低于25%时,《剑破万法》造成的伤害提高15%。',
},
(context, builder) =>
addLingxiaoPassive(context, builder, {
@@ -35,7 +46,7 @@ export const SWIFT_LAYER_4_NODES = [
effects: [
{
type: 'percent_damage_modifier',
- params: { mode: 'increase', value: 0.25 },
+ params: { mode: 'increase', value: 0.15 },
conditions: [
{
type: 'ability_has_tag',
@@ -50,10 +61,14 @@ export const SWIFT_LAYER_4_NODES = [
],
},
],
- presentationModifiers: [{
- abilityId: 'sect-ultimate',
- factRows: ['参悟·追命:目标气血低于25%时,《剑破万法》造成的伤害提高25%'],
- }],
+ presentationModifiers: [
+ {
+ abilityId: 'sect-ultimate',
+ factRows: [
+ '参悟·追命:目标气血低于25%时,《剑破万法》造成的伤害提高15%',
+ ],
+ },
+ ],
}),
),
createLingxiaoNode(
@@ -61,8 +76,11 @@ export const SWIFT_LAYER_4_NODES = [
id: 'swift-sheathing',
layerId: '4',
name: '归鞘',
- description: '《剑破万法》伤害降低15%,施展后返还1点剑势,并获得相当于40%物攻的护盾。',
+ description:
+ '《剑破万法》伤害降低15%,施展后返还2点剑势,并获得随《问剑篇》成长的护盾。',
},
(_context, builder) => swiftSwordBuild(builder).enable('sheathing'),
+ (context) =>
+ `《剑破万法》伤害降低15%;施展后返还2点剑势,并获得相当于${nodePercent(growthMagnitude(context, 'lingxiao-canon', SWIFT_SHEATHING_SHIELD_COEFFICIENT))}物攻的护盾。`,
),
] as const;
diff --git a/src/shared/engine/sect/content/lingxiao/paths/swift/nodes/layer5.ts b/src/shared/engine/sect/content/lingxiao/paths/swift/nodes/layer5.ts
index 30e91505..70671373 100644
--- a/src/shared/engine/sect/content/lingxiao/paths/swift/nodes/layer5.ts
+++ b/src/shared/engine/sect/content/lingxiao/paths/swift/nodes/layer5.ts
@@ -18,7 +18,8 @@ export const SWIFT_LAYER_5_NODES = [
id: 'swift-gapless',
layerId: '5',
name: '无隙',
- description: '施展《剑破万法》后,下一次《剑起沧澜》不消耗法力,并额外获得1点剑势。',
+ description:
+ '施展《剑破万法》后,下一次《剑起沧澜》不消耗法力,并额外获得1点剑势。',
},
(context, builder) => {
swiftSwordBuild(builder).enable('gapless');
@@ -58,10 +59,14 @@ export const SWIFT_LAYER_5_NODES = [
],
},
],
- presentationModifiers: [{
- abilityId: 'sect-ultimate',
- factRows: ['参悟·无隙:施展《剑破万法》后,下一次《剑起沧澜》不消耗法力,并额外获得1点剑势'],
- }],
+ presentationModifiers: [
+ {
+ abilityId: 'sect-ultimate',
+ factRows: [
+ '参悟·无隙:施展《剑破万法》后,下一次《剑起沧澜》不消耗法力,并额外获得1点剑势',
+ ],
+ },
+ ],
});
},
),
@@ -90,10 +95,14 @@ export const SWIFT_LAYER_5_NODES = [
],
},
],
- presentationModifiers: [{
- abilityId: 'linked-edge',
- factRows: ['参悟·连城:施展《剑荡山河》后,其他快剑神通的当前冷却减少1回合,每回合最多触发一次'],
- }],
+ presentationModifiers: [
+ {
+ abilityId: 'linked-edge',
+ factRows: [
+ '参悟·连城:施展《剑荡山河》后,其他快剑神通的当前冷却减少1回合,每回合最多触发一次',
+ ],
+ },
+ ],
});
},
),
@@ -102,7 +111,8 @@ export const SWIFT_LAYER_5_NODES = [
id: 'swift-still-tide',
layerId: '5',
name: '静潮',
- description: '连续两次自身行动未施展《剑破万法》后,暂停剑势衰减;下一次《剑破万法》伤害提高20%。',
+ description:
+ '连续两次自身行动未施展《剑破万法》后,暂停剑势衰减;下一次《剑破万法》伤害提高15%。',
},
(context, builder) => {
swiftSwordBuild(builder).enable('stillTide');
@@ -140,7 +150,7 @@ export const SWIFT_LAYER_5_NODES = [
effects: [
{
type: 'percent_damage_modifier',
- params: { mode: 'increase', value: 0.2 },
+ params: { mode: 'increase', value: 0.15 },
conditions: [
{
type: 'ability_has_tag',
@@ -152,10 +162,14 @@ export const SWIFT_LAYER_5_NODES = [
],
},
],
- presentationModifiers: [{
- abilityId: 'sect-ultimate',
- factRows: ['参悟·静潮:连续两次自身行动未施展《剑破万法》后暂停剑势衰减;下一次《剑破万法》伤害提高20%'],
- }],
+ presentationModifiers: [
+ {
+ abilityId: 'sect-ultimate',
+ factRows: [
+ '参悟·静潮:连续两次自身行动未施展《剑破万法》后暂停剑势衰减;下一次《剑破万法》伤害提高15%',
+ ],
+ },
+ ],
});
},
),
diff --git a/src/shared/engine/sect/content/lingxiao/paths/swift/nodes/ultimate.ts b/src/shared/engine/sect/content/lingxiao/paths/swift/nodes/ultimate.ts
index 3b5649fa..612681a0 100644
--- a/src/shared/engine/sect/content/lingxiao/paths/swift/nodes/ultimate.ts
+++ b/src/shared/engine/sect/content/lingxiao/paths/swift/nodes/ultimate.ts
@@ -2,8 +2,16 @@ import { GameplayTags } from '@shared/engine/shared/tag-domain';
import { sectEffects } from '../../../../../core';
import { createLingxiaoNode } from '../../../shared/createLingxiaoNode';
import { SWIFT_ENDLESS_COOLDOWN } from '../../../shared/LingxiaoMechanics';
+import {
+ growthMagnitude,
+ nodePercent,
+} from '../../../shared/LingxiaoNodeDescription';
import { addLingxiaoPassive } from '../../../shared/SwordNodePassives';
import { swiftSwordBuild } from '../SwiftSwordBuildFacade';
+import {
+ SWIFT_ENDLESS_FLOW_COEFFICIENT,
+ SWIFT_UNENDING_WIND_SHIELD_COEFFICIENT,
+} from '../variants';
export const SWIFT_ULTIMATE_NODES = [
createLingxiaoNode(
@@ -12,7 +20,7 @@ export const SWIFT_ULTIMATE_NODES = [
layerId: 'ultimate',
name: '无间',
description:
- '施展《剑破万法》后,追加相当于48%物攻的追击并获得1点剑势,每3回合最多触发一次。',
+ '施展《剑破万法》后,追加随《问剑篇》成长的追击并获得1点剑势,每3回合最多触发一次。',
},
(context, builder) => {
swiftSwordBuild(builder).enable('endlessFlow');
@@ -41,6 +49,8 @@ export const SWIFT_ULTIMATE_NODES = [
],
});
},
+ (context) =>
+ `施展《剑破万法》后,追加相当于${nodePercent(growthMagnitude(context, 'lingxiao-canon', SWIFT_ENDLESS_FLOW_COEFFICIENT))}物攻的追击并获得1点剑势,每3回合最多触发一次。`,
),
createLingxiaoNode(
{
@@ -48,7 +58,7 @@ export const SWIFT_ULTIMATE_NODES = [
layerId: 'ultimate',
name: '绝影',
description:
- '以6点剑势施展《剑破万法》时,全部伤害段必定暴击,冷却增加1回合。',
+ '以6点剑势施展《剑破万法》时,总伤害降低15%,全部伤害段必定暴击,冷却增加1回合。',
},
(_context, builder) => swiftSwordBuild(builder).enable('shadowLine'),
),
@@ -58,8 +68,10 @@ export const SWIFT_ULTIMATE_NODES = [
layerId: 'ultimate',
name: '回风',
description:
- '每次《藏锋听雷》持续期间首次闪避时,获得相当于32%物攻的护盾,并施加1层剑痕。',
+ '每次《藏锋听雷》持续期间首次闪避时,获得随《凌虚步》成长的护盾,并施加1层剑痕。',
},
(_context, builder) => swiftSwordBuild(builder).enable('unendingWind'),
+ (context) =>
+ `每次《藏锋听雷》持续期间首次闪避时,获得相当于${nodePercent(growthMagnitude(context, 'void-step', SWIFT_UNENDING_WIND_SHIELD_COEFFICIENT))}物攻的护盾,并施加1层剑痕。`,
),
] as const;
diff --git a/src/shared/engine/sect/content/lingxiao/paths/swift/strategy.ts b/src/shared/engine/sect/content/lingxiao/paths/swift/strategy.ts
index f0cdedd2..429006dd 100644
--- a/src/shared/engine/sect/content/lingxiao/paths/swift/strategy.ts
+++ b/src/shared/engine/sect/content/lingxiao/paths/swift/strategy.ts
@@ -20,22 +20,42 @@ export class LingxiaoSwiftSelectionStrategy implements AbilitySelectionStrategy
if (!opponent || candidates.length === 0) return null;
const index = new SectStrategyCandidates(LINGXIAO_SECT_ID, candidates);
const momentum = caster.combatResources.getCurrent(LINGXIAO_SWORD_MOMENTUM);
- const finisherThreshold = this.tacticId === 'aggressive' ? 3 : this.tacticId === 'counter' ? 5 : 6;
+ const finisherThreshold =
+ this.tacticId === 'aggressive' ? 3 : this.tacticId === 'counter' ? 5 : 6;
const buffs = new Set(caster.buffs.getAllBuffIds());
- const swordMarks = opponent.buffs.getAllBuffs().find(
- (buff) => buff.id === LINGXIAO_SWORD_MARK_BUFF,
- )?.getLayer() ?? 0;
-
+ const swordMarks =
+ opponent.buffs
+ .getAllBuffs()
+ .find((buff) => buff.id === LINGXIAO_SWORD_MARK_BUFF)
+ ?.getLayer() ?? 0;
const turning = index.find('turning-body');
- if (this.tacticId === 'counter' && turning && !buffs.has(LINGXIAO_RETURNING_SWALLOW_BUFF)) {
+ if (
+ this.tacticId === 'counter' &&
+ turning &&
+ !buffs.has(LINGXIAO_RETURNING_SWALLOW_BUFF)
+ ) {
return index.resultForCandidate(turning, 620);
}
+ const step = index.find('shadow-step');
+ if (
+ this.tacticId === 'counter' &&
+ step &&
+ !buffs.has('sect.lingxiao.swift.traceless-step') &&
+ caster.attributes.getValue(AttributeType.SPEED) <=
+ opponent.attributes.getValue(AttributeType.SPEED)
+ ) {
+ return index.resultForCandidate(step, 610);
+ }
const linked = index.find('linked-edge');
if (this.tacticId === 'steady' && linked && swordMarks < 2) {
return index.resultForCandidate(linked, 640);
}
const heart = index.find('sword-aegis');
- if (heart && !buffs.has('sect.lingxiao.swift.wind-heart') && caster.getHpPercent() < 0.6) {
+ if (
+ heart &&
+ !buffs.has('sect.lingxiao.swift.wind-heart') &&
+ caster.getHpPercent() < 0.6
+ ) {
return index.resultForCandidate(heart, 580);
}
const finisher = index.find('sect-ultimate');
@@ -44,7 +64,10 @@ export class LingxiaoSwiftSelectionStrategy implements AbilitySelectionStrategy
momentum >= finisherThreshold &&
(this.tacticId !== 'steady' || swordMarks >= 2)
) {
- return index.resultForCandidate(finisher, opponent.getHpPercent() < 0.25 ? 560 : 500);
+ return index.resultForCandidate(
+ finisher,
+ opponent.getHpPercent() < 0.25 ? 560 : 500,
+ );
}
if (linked) return index.resultForCandidate(linked, 400);
@@ -52,14 +75,17 @@ export class LingxiaoSwiftSelectionStrategy implements AbilitySelectionStrategy
if (lightBuff && !buffs.has('sect.lingxiao.swift.light-sword')) {
return index.resultForCandidate(lightBuff, 340);
}
- const step = index.find('shadow-step');
if (
step &&
!buffs.has('sect.lingxiao.swift.traceless-step') &&
- caster.attributes.getValue(AttributeType.SPEED) <= opponent.attributes.getValue(AttributeType.SPEED)
+ caster.attributes.getValue(AttributeType.SPEED) <=
+ opponent.attributes.getValue(AttributeType.SPEED)
) {
return index.resultForCandidate(step, 320);
}
- return index.resultForCandidate(index.find('guiding-sword') ?? candidates[0], 100);
+ return index.resultForCandidate(
+ index.find('guiding-sword') ?? candidates[0],
+ 100,
+ );
}
}
diff --git a/src/shared/engine/sect/content/lingxiao/paths/swift/variants.ts b/src/shared/engine/sect/content/lingxiao/paths/swift/variants.ts
index 43602e05..440160f4 100644
--- a/src/shared/engine/sect/content/lingxiao/paths/swift/variants.ts
+++ b/src/shared/engine/sect/content/lingxiao/paths/swift/variants.ts
@@ -68,6 +68,13 @@ export const EMPTY_SWIFT_FEATURES: SwiftSwordFeatures = {
unendingWind: false,
};
+export const SWIFT_SPLIT_LIGHT_HIT_COEFFICIENT = 0.21;
+export const SWIFT_RETURNING_SWALLOW_COUNTER_COEFFICIENT = 0.6;
+export const SWIFT_MOUNTAIN_BREAKING_COEFFICIENT = 0.12;
+export const SWIFT_SHEATHING_SHIELD_COEFFICIENT = 0.48;
+export const SWIFT_ENDLESS_FLOW_COEFFICIENT = 0.32;
+export const SWIFT_UNENDING_WIND_SHIELD_COEFFICIENT = 0.4;
+
const damage = (
coefficient: number,
conditions?: EffectConfig['conditions'],
@@ -93,6 +100,7 @@ const selfBuff = (
modifiers: NonNullable>,
listeners?: NonNullable,
growsWithMethod = true,
+ stackRule: StackRule = StackRule.REFRESH_DURATION,
): EffectConfig => ({
type: 'apply_buff',
params: {
@@ -103,7 +111,7 @@ const selfBuff = (
name,
type: BuffType.BUFF,
duration,
- stackRule: StackRule.REFRESH_DURATION,
+ stackRule,
tags: [GameplayTags.BUFF.TYPE.BUFF],
modifiers,
listeners,
@@ -117,6 +125,7 @@ export function buildSwiftAbilities(
baseBuild: Readonly,
path: CultivatorSectPathState,
features: SwiftSwordFeatures,
+ edgeCleansingLevel?: number,
): Record {
const built = { ...baseBuild.abilities };
const factory = new SectAbilityFactory(LINGXIAO_SECT_ID);
@@ -174,7 +183,9 @@ export function buildSwiftAbilities(
});
const hits = features.splitLight ? 7 : 5;
- const hitCoefficient = features.splitLight ? 0.23 : 0.29;
+ const hitCoefficient = features.splitLight
+ ? SWIFT_SPLIT_LIGHT_HIT_COEFFICIENT
+ : 0.29;
built['linked-edge'] = active({
id: 'linked-edge',
cooldown: 2,
@@ -247,7 +258,7 @@ export function buildSwiftAbilities(
{
attrType: AttributeType.EVASION_RATE,
type: ModifierType.FIXED,
- value: 0.2,
+ value: 0.24,
},
],
[
@@ -260,7 +271,9 @@ export function buildSwiftAbilities(
budget: { maxTriggers: 1, reset: 'buff_lifetime' },
effects: [
damage(
- features.returningSwallow ? 0.6 : 0.4,
+ features.returningSwallow
+ ? SWIFT_RETURNING_SWALLOW_COUNTER_COEFFICIENT
+ : 0.4,
undefined,
false,
DamageSource.COUNTER,
@@ -270,11 +283,19 @@ export function buildSwiftAbilities(
? [createSwordMark()]
: []),
...(features.unendingWind
- ? [sectEffects.shieldByAttack(0.32, undefined, 'caster')]
+ ? [
+ sectEffects.shieldByAttack(
+ SWIFT_UNENDING_WIND_SHIELD_COEFFICIENT,
+ undefined,
+ 'caster',
+ ),
+ ]
: []),
],
},
],
+ true,
+ StackRule.OVERRIDE,
),
],
});
@@ -312,6 +333,8 @@ export function buildSwiftAbilities(
effects: [sectEffects.modifyResource(resourceId, 1)],
},
],
+ true,
+ StackRule.OVERRIDE,
),
],
});
@@ -322,10 +345,7 @@ export function buildSwiftAbilities(
role: 'utility',
effects: [
damage(0.95),
- {
- type: 'dispel',
- params: { targetTag: GameplayTags.BUFF.TYPE.BUFF, maxCount: 1 },
- },
+ sectEffects.dispelPositiveBuffsByMethod(1, edgeCleansingLevel),
],
});
@@ -363,7 +383,8 @@ export function buildSwiftAbilities(
],
});
- const sheathingScale = features.sheathing ? 0.85 : 1;
+ const finisherScale =
+ (features.sheathing ? 0.85 : 1) * (features.shadowLine ? 0.85 : 1);
built['sect-ultimate'] = active({
id: 'sect-ultimate',
cooldown: 4 + (features.shadowLine ? 1 : 0),
@@ -380,7 +401,7 @@ export function buildSwiftAbilities(
],
effects: [
damage(
- 0.3 * sheathingScale,
+ 0.3 * finisherScale,
undefined,
false,
DamageSource.DIRECT,
@@ -388,7 +409,7 @@ export function buildSwiftAbilities(
),
...Array.from({ length: 6 }, (_, index) =>
damage(
- 0.33 * sheathingScale,
+ 0.33 * finisherScale,
[
{
type: 'combat_resource_at_least',
@@ -410,15 +431,21 @@ export function buildSwiftAbilities(
displayName: '剑痕',
consume: 'all' as const,
scaleEffectsByLayer: true,
- effects: [damage(0.144, undefined, true)],
+ effects: [
+ damage(SWIFT_MOUNTAIN_BREAKING_COEFFICIENT, undefined, true),
+ ],
},
},
]
: []),
...(features.sheathing
? [
- sectEffects.modifyResource(resourceId, 1, undefined, 'refund'),
- sectEffects.shieldByAttack(0.4, undefined, 'caster'),
+ sectEffects.modifyResource(resourceId, 2, undefined, 'refund'),
+ sectEffects.shieldByAttack(
+ SWIFT_SHEATHING_SHIELD_COEFFICIENT,
+ undefined,
+ 'caster',
+ ),
]
: []),
...(features.gapless
@@ -460,7 +487,12 @@ export function buildSwiftAbilities(
operation: 'set' as const,
amount: 3,
effects: [
- damage(0.48, undefined, false, DamageSource.FOLLOW_UP),
+ damage(
+ SWIFT_ENDLESS_FLOW_COEFFICIENT,
+ undefined,
+ false,
+ DamageSource.FOLLOW_UP,
+ ),
sectEffects.modifyResource(resourceId, 1),
],
},
diff --git a/src/shared/engine/sect/content/lingxiao/shared/LingxiaoNodeDescription.ts b/src/shared/engine/sect/content/lingxiao/shared/LingxiaoNodeDescription.ts
new file mode 100644
index 00000000..bd60afe0
--- /dev/null
+++ b/src/shared/engine/sect/content/lingxiao/shared/LingxiaoNodeDescription.ts
@@ -0,0 +1,49 @@
+import {
+ standardSectMethodGrowthPolicy,
+ type SectMethodId,
+ type SectProjectionContext,
+} from '../../../core';
+
+function level(
+ context: SectProjectionContext,
+ methodId: SectMethodId,
+): number | undefined {
+ return context.sect.methods[methodId];
+}
+
+export function growthMagnitude(
+ context: SectProjectionContext,
+ methodId: SectMethodId,
+ baseValue: number,
+): number {
+ return standardSectMethodGrowthPolicy.scaleMagnitude(
+ baseValue,
+ level(context, methodId),
+ );
+}
+
+export function growthStatusMagnitude(
+ context: SectProjectionContext,
+ methodId: SectMethodId,
+ baseValue: number,
+): number {
+ return standardSectMethodGrowthPolicy.scaleStatusMagnitude(
+ baseValue,
+ level(context, methodId),
+ );
+}
+
+export function growthDuration(
+ context: SectProjectionContext,
+ methodId: SectMethodId,
+ baseDuration: number,
+): number {
+ return standardSectMethodGrowthPolicy.growDuration(
+ baseDuration,
+ level(context, methodId),
+ );
+}
+
+export function nodePercent(value: number): string {
+ return `${Number((value * 100).toFixed(2))}%`;
+}
diff --git a/src/shared/engine/sect/content/lingxiao/shared/createLingxiaoNode.ts b/src/shared/engine/sect/content/lingxiao/shared/createLingxiaoNode.ts
index 4a912e31..291819c6 100644
--- a/src/shared/engine/sect/content/lingxiao/shared/createLingxiaoNode.ts
+++ b/src/shared/engine/sect/content/lingxiao/shared/createLingxiaoNode.ts
@@ -3,11 +3,13 @@ import {
type SectBuildBuilder,
type SectMeridianNodeDefinition,
type SectNodeApplyContext,
+ type SectProjectionContext,
} from '../../../core';
export function createLingxiaoNode(
definition: SectMeridianNodeDefinition,
apply: (context: SectNodeApplyContext, builder: SectBuildBuilder) => void,
+ describe?: (context: SectProjectionContext) => string,
): ConfiguredSectNodePlugin {
- return new ConfiguredSectNodePlugin(definition, apply);
+ return new ConfiguredSectNodePlugin(definition, apply, describe);
}
diff --git a/src/shared/engine/sect/content/lingxiao/tests/balanceSimulation.test.ts b/src/shared/engine/sect/content/lingxiao/tests/balanceSimulation.test.ts
index 613f0b17..9939f3c9 100644
--- a/src/shared/engine/sect/content/lingxiao/tests/balanceSimulation.test.ts
+++ b/src/shared/engine/sect/content/lingxiao/tests/balanceSimulation.test.ts
@@ -65,7 +65,7 @@ const NODE_SET: Record = {
'swift-endless-flow',
],
steady: [
- 'swift-probing-edge',
+ 'swift-opening',
'swift-retained-force',
'swift-guarded-edge',
'swift-mountain-breaking',
@@ -90,7 +90,7 @@ const NODE_SET: Record = {
],
'heavy-full': [
'heavy-opening',
- 'heavy-triple-ridge',
+ 'heavy-shattering-armor',
'heavy-crossing-pass',
'heavy-ending-life',
'heavy-aftershock',
@@ -376,7 +376,7 @@ describe('凌霄V4.2三境界固定种子平衡矩阵', () => {
EventBus.instance.reset();
});
- it.runIf(process.env.LINGXIAO_BALANCE_GATE === '1').each(REALMS)(
+ it.each(REALMS)(
'$label 的三组代表对局与3×3战术矩阵满足验收范围',
({ realm, stage, layerCount, methodLevel, realmIndex }) => {
const swiftTactics: SwiftTactic[] = ['aggressive', 'steady', 'counter'];
@@ -392,6 +392,11 @@ describe('凌霄V4.2三境界固定种子平衡矩阵', () => {
]);
const failures: string[] = [];
const reports: string[] = [];
+ const turnRange = [
+ { min: 8, max: 12 },
+ { min: 7, max: 11 },
+ { min: 6, max: 10 },
+ ][realmIndex];
for (const [swiftIndex, swiftTactic] of swiftTactics.entries()) {
for (const [heavyIndex, heavyTactic] of heavyTactics.entries()) {
const result = simulate({
@@ -413,9 +418,12 @@ describe('凌霄V4.2三境界固定种子平衡矩阵', () => {
if (
result.swiftWinRate < minWinRate ||
result.swiftWinRate > maxWinRate ||
+ result.swiftWinRate === 0 ||
+ result.swiftWinRate === 1 ||
result.drawRate > 0.05 ||
(isRepresentative &&
- (result.averageTurns < 8 || result.averageTurns > 12))
+ (result.averageTurns < turnRange.min ||
+ result.averageTurns > turnRange.max))
) {
failures.push(label);
}
@@ -436,8 +444,8 @@ describe('凌霄V4.2三境界固定种子平衡矩阵', () => {
heavyTactic: 'heavy-break',
seed: 0x4c58_4999,
});
- expect(result.swiftIdentityShare).toBeGreaterThan(0.5);
- expect(result.heavyIdentityShare).toBeGreaterThan(0.5);
+ expect(result.swiftIdentityShare).toBeGreaterThanOrEqual(0.5);
+ expect(result.heavyIdentityShare).toBeGreaterThanOrEqual(0.5);
}, 30_000);
it.each([1, 180])('%i级心法边界可完成代表对局冒烟', (methodLevel) => {
diff --git a/src/shared/engine/sect/content/lingxiao/tests/combatProjection.test.ts b/src/shared/engine/sect/content/lingxiao/tests/combatProjection.test.ts
index b2439364..78133b26 100644
--- a/src/shared/engine/sect/content/lingxiao/tests/combatProjection.test.ts
+++ b/src/shared/engine/sect/content/lingxiao/tests/combatProjection.test.ts
@@ -52,6 +52,33 @@ function state(
};
}
+function setMethodLevel(sect: CultivatorSectState, level: number): void {
+ sect.methods = Object.fromEntries(
+ Object.keys(sect.methods).map((methodId) => [methodId, level]),
+ );
+}
+
+function findObjectById(
+ value: unknown,
+ id: string,
+): Record | undefined {
+ if (!value || typeof value !== 'object') return undefined;
+ if (Array.isArray(value)) {
+ for (const entry of value) {
+ const found = findObjectById(entry, id);
+ if (found) return found;
+ }
+ return undefined;
+ }
+ const record = value as Record;
+ if (record.id === id) return record;
+ for (const child of Object.values(record)) {
+ const found = findObjectById(child, id);
+ if (found) return found;
+ }
+ return undefined;
+}
+
describe('宗门注册投影', () => {
it('九个神通跨境界保持固定蓝耗且基础剑式始终免费', () => {
const expected = new Map([
@@ -112,6 +139,44 @@ describe('宗门注册投影', () => {
},
);
+ it.each([
+ [0, 1],
+ [1, 1],
+ [29, 1],
+ [30, 2],
+ [60, 3],
+ [90, 4],
+ [120, 5],
+ [150, 6],
+ [180, 7],
+ [999, 7],
+ ])('一剑破妄在%i级驱散%i个正面状态', (level, expectedCount) => {
+ for (const pathId of [undefined, 'swift-sword', 'heavy-sword'] as const) {
+ const sect = state(pathId);
+ sect.methods['edge-cleansing'] = level;
+ sect.abilityLoadout = [
+ 'guiding-sword',
+ 'linked-edge',
+ null,
+ 'sect-ultimate',
+ ];
+ const ability = resolveSectAbility({
+ sect,
+ realm: '化神',
+ abilityId: 'breaking-edge',
+ });
+ const dispel = ability.config.effects?.find(
+ (effect) => effect.type === 'dispel',
+ );
+ expect(
+ dispel?.type === 'dispel' ? dispel.params.maxCount : undefined,
+ ).toBe(expectedCount);
+ expect(ability.detailRows).toContain(
+ `命中后:驱散:目标${expectedCount}个正面状态`,
+ );
+ }
+ });
+
it('炼气初期心法上限可解锁除绝式外全部基础神通,10级开放剑破万法', () => {
const early = state();
early.methods = Object.fromEntries(
@@ -199,7 +264,7 @@ describe('宗门注册投影', () => {
expect(detail.summary).toBe(
'剑锋纵横,如长河奔涌;所过之处,山河亦为之震荡。',
);
- expect(detail.detailRows).toContain('伤害:7段 × 28.75%物攻');
+ expect(detail.detailRows).toContain('伤害:7段 × 26.25%物攻');
expect(
projection.abilities.find(
(ability) => ability.slug === detail.config.slug,
@@ -274,6 +339,9 @@ describe('宗门注册投影', () => {
'__sectMethodGrowth',
);
}
+ for (const passive of compiled.passives) {
+ expect(JSON.stringify(passive)).not.toContain('__sectMethodGrowth');
+ }
compiledCount += 1;
return;
}
@@ -287,6 +355,127 @@ describe('宗门注册投影', () => {
30_000,
);
+ it.each([
+ [60, 4, 0.0227],
+ [120, 5, 0.0253],
+ [180, 6, 0.028],
+ ])(
+ '探虚被动在%i级投影为%i行动、每层%s增伤且不泄漏成长元数据',
+ (level, duration, value) => {
+ const sect = state('swift-sword', ['swift-probing-edge']);
+ setMethodLevel(sect, level);
+ const compiled = productionSectRuntime.compiler.compile(LINGXIAO_MODULE, {
+ sect,
+ realm: '化神',
+ });
+ const probing = compiled.passives.find((passive) =>
+ passive.slug.endsWith('.swift-probing-edge'),
+ );
+ const swordMark = findObjectById(probing, 'sect.lingxiao.sword-mark');
+ expect(swordMark?.duration).toBe(duration);
+ expect(JSON.stringify(swordMark?.listeners)).toContain(
+ `"value":${value}`,
+ );
+ expect(JSON.stringify(compiled)).not.toContain('__sectMethodGrowth');
+ },
+ );
+
+ it('基础与重剑藏锋为自身蓄势,快剑藏锋仍是命中后获得姿态', () => {
+ for (const pathId of [undefined, 'heavy-sword'] as const) {
+ const ability = resolveSectAbility({
+ sect: state(pathId),
+ realm: '化神',
+ abilityId: 'turning-body',
+ });
+ expect(ability.config.targetPolicy?.team).toBe('self');
+ expect(ability.detailRows).toEqual(
+ expect.arrayContaining([
+ '施展后:蓄势:下一次自身行动发动《听雷》',
+ '后发:必然命中',
+ '蓄势:除自身死亡外不可打断',
+ ]),
+ );
+ }
+
+ const swift = resolveSectAbility({
+ sect: state('swift-sword'),
+ realm: '化神',
+ abilityId: 'turning-body',
+ });
+ expect(swift.config.targetPolicy?.team).toBe('enemy');
+ expect(
+ swift.detailRows.some((row) => row.startsWith('命中后:状态:')),
+ ).toBe(true);
+ });
+
+ it.each([
+ [1, '60%', '36%'],
+ [60, '70%', '42%'],
+ [100, '75%', '45%'],
+ [120, '80%', '48%'],
+ [180, '90%', '54%'],
+ ])(
+ '%i级节点卡片展示当前实际成长数值',
+ (level, returningSwallow, guardedShield) => {
+ const swiftSect = state('swift-sword');
+ const heavySect = state('heavy-sword');
+ setMethodLevel(swiftSect, level);
+ setMethodLevel(heavySect, level);
+ if (level < 10) {
+ swiftSect.abilityLoadout = ['guiding-sword', null, null, null];
+ heavySect.abilityLoadout = ['guiding-sword', null, null, null];
+ }
+ const swiftNodes = resolveSectPathPreview({
+ sect: swiftSect,
+ realm: '化神',
+ pathId: 'swift-sword',
+ }).nodes;
+ const heavyNodes = resolveSectPathPreview({
+ sect: heavySect,
+ realm: '化神',
+ pathId: 'heavy-sword',
+ }).nodes;
+ expect(
+ swiftNodes.find((node) => node.id === 'swift-returning-swallow')
+ ?.description,
+ ).toContain(returningSwallow);
+ expect(
+ heavyNodes.find((node) => node.id === 'heavy-retained-frame')
+ ?.description,
+ ).toContain(guardedShield);
+ },
+ );
+
+ it('成长节点卡片复用当前心法实际值而非静态1级数值', () => {
+ const sect = state('swift-sword');
+ const heavyPath = state('heavy-sword').paths[0];
+ sect.paths.push(heavyPath);
+ const swiftNodes = resolveSectPathPreview({
+ sect,
+ realm: '化神',
+ pathId: 'swift-sword',
+ }).nodes;
+ const heavyNodes = resolveSectPathPreview({
+ sect,
+ realm: '化神',
+ pathId: 'heavy-sword',
+ }).nodes;
+ const descriptions = new Map(
+ [...swiftNodes, ...heavyNodes].map((node) => [node.id, node.description]),
+ );
+ expect(descriptions.get('swift-mountain-breaking')).toContain('15%物攻');
+ expect(descriptions.get('swift-sheathing')).toContain('60%物攻');
+ expect(descriptions.get('swift-endless-flow')).toContain('40%物攻');
+ expect(descriptions.get('swift-unending-wind')).toContain('50%物攻');
+ expect(descriptions.get('heavy-heaven-cleaving')).toContain('350%物攻');
+ expect(descriptions.get('heavy-immovable-mountain')).toContain('70%物攻');
+ expect(descriptions.get('heavy-immovable-mountain')).toContain('45%物攻');
+ expect(descriptions.get('heavy-mountain-river-echo')).toContain(
+ '5%最大气血',
+ );
+ expect(descriptions.get('heavy-mountain-river-echo')).toContain('80%物攻');
+ });
+
it('最终神通事实正确合并节点倍率、门槛与跨神通被动', () => {
const shadow = resolveSectAbility({
sect: state('swift-sword', ['swift-shadow-line']),
@@ -305,8 +494,8 @@ describe('宗门注册投影', () => {
expect(returningPeak.detailRows).toEqual(
expect.arrayContaining([
'伤害:基础相当于93.5%物攻,每点剑势增加30.81%物攻',
- '剑势:返还2点',
- '护盾:相当于60%物攻',
+ '命中后:剑势:返还2点',
+ '命中后:护盾:相当于45%物攻',
]),
);
@@ -331,7 +520,10 @@ describe('宗门注册投影', () => {
abilityId: 'sect-ultimate',
});
expect(mountainBreaking.detailRows).toEqual(
- expect.arrayContaining(['状态:消耗全部剑痕', '每层追加:相当于18%物攻']),
+ expect.arrayContaining([
+ '命中后:状态:消耗全部剑痕',
+ '命中后:每层追加:相当于15%物攻',
+ ]),
);
const passiveFacts = resolveSectAbility({
@@ -429,7 +621,7 @@ describe('宗门注册投影', () => {
).toEqual(
expect.arrayContaining([
'伤害:5段 × 36.25%物攻',
- '剑痕:向目标施加1层,持续目标未来4次行动',
+ '命中后:剑痕:向目标施加1层,持续目标未来4次行动',
]),
);
expect(sect).toEqual(before);
@@ -451,10 +643,16 @@ describe('宗门注册投影', () => {
expect(preview.learned).toBe(true);
expect(preview.active).toBe(false);
expect(preview.activeMeridianSlot).toBe(1);
- expect(shadow.baseline.detailRows).not.toContain('护盾:相当于65%物攻');
- expect(shadow.pathBase.detailRows).toContain('护盾:相当于65%物攻');
- expect(shadow.pathBase.detailRows).not.toContain('护盾:相当于97.5%物攻');
- expect(shadow.current?.detailRows).toContain('护盾:相当于97.5%物攻');
+ expect(shadow.baseline.detailRows).not.toContain(
+ '施展后:护盾:相当于65%物攻',
+ );
+ expect(shadow.pathBase.detailRows).toContain('施展后:护盾:相当于65%物攻');
+ expect(shadow.pathBase.detailRows).not.toContain(
+ '施展后:护盾:相当于97.5%物攻',
+ );
+ expect(shadow.current?.detailRows).toContain(
+ '施展后:护盾:相当于97.5%物攻',
+ );
});
it('动态详情保留条件、目标、触发次数与统一术语', () => {
@@ -478,7 +676,7 @@ describe('宗门注册投影', () => {
abilityId: 'turning-body',
}).detailRows,
).toContain(
- '触发:持续期间首次闪避时,反击造成相当于50%物攻的伤害,并获得1点剑势',
+ '命中后:触发:持续期间首次闪避时,反击造成相当于50%物攻的伤害,并获得1点剑势',
);
expect(
resolveSectAbility({
@@ -486,14 +684,14 @@ describe('宗门注册投影', () => {
realm: '化神',
abilityId: 'shadow-step',
}).detailRows,
- ).toContain('触发:持续期间首次闪避时,获得1点剑势');
+ ).toContain('施展后:触发:持续期间首次闪避时,获得1点剑势');
expect(
resolveSectAbility({
sect: swift,
realm: '化神',
abilityId: 'sect-ultimate',
}).detailRows,
- ).toContain('施放后:消耗全部剑势');
+ ).toContain('命中后:消耗全部剑势');
});
it('动态详情聚合剑痕并完整描述节点触发效果', () => {
@@ -507,14 +705,16 @@ describe('宗门注册投影', () => {
realm: '化神',
abilityId: 'linked-edge',
}).detailRows,
- ).toContain('剑痕:向目标施加2层,持续目标未来4次行动');
+ ).toContain('命中后:剑痕:向目标施加2层,持续目标未来4次行动');
const swordMarkRows = resolveSectAbility({
sect: swift,
realm: '化神',
abilityId: 'linked-edge',
}).detailRows;
- expect(swordMarkRows).toContain('剑痕:最多3层');
- expect(swordMarkRows).toContain('每层:受到的直接、反击和追击伤害提高2.4%');
+ expect(swordMarkRows).toContain('命中后:剑痕:最多3层');
+ expect(swordMarkRows).toContain(
+ '命中后:每层:受到的直接、反击和追击伤害提高2.4%',
+ );
expect(
resolveSectAbility({
sect: swift,
@@ -522,7 +722,7 @@ describe('宗门注册投影', () => {
abilityId: 'turning-body',
}).detailRows,
).toContain(
- '触发:持续期间首次闪避时,反击造成相当于50%物攻的伤害、获得1点剑势、向目标施加1层剑痕,持续目标未来4次行动,并获得相当于40%物攻的护盾',
+ '命中后:触发:持续期间首次闪避时,反击造成相当于50%物攻的伤害、获得1点剑势、向目标施加1层剑痕,持续目标未来4次行动,并获得相当于50%物攻的护盾',
);
const heavy = state('heavy-sword', ['heavy-immovable-mountain']);
@@ -531,8 +731,8 @@ describe('宗门注册投影', () => {
realm: '化神',
abilityId: 'linked-edge',
}).detailRows;
- expect(armorRendRows).toContain('裂甲:最多3层');
- expect(armorRendRows).toContain('每层:物防-3.6%');
+ expect(armorRendRows).toContain('命中后:裂甲:最多3层');
+ expect(armorRendRows).toContain('命中后:每层:物防-3.6%');
expect(
resolveSectAbility({
sect: heavy,
@@ -540,7 +740,7 @@ describe('宗门注册投影', () => {
abilityId: 'sword-aegis',
}).detailRows,
).toContain(
- '触发:持续期间每回合首次受到直接伤害时,反击造成相当于60%物攻的伤害',
+ '施展后:触发:持续期间每回合首次受到直接伤害时,反击造成相当于45%物攻的伤害',
);
});
@@ -552,7 +752,7 @@ describe('宗门注册投影', () => {
realm: '化神',
abilityId: 'linked-edge',
}).detailRows,
- ).toContain('冷却:当前冷却减少1回合');
+ ).toContain('命中后:冷却:当前冷却减少1回合');
const echo = state('heavy-sword', ['heavy-mountain-river-echo']);
const echoRows = resolveSectAbility({
@@ -562,8 +762,8 @@ describe('宗门注册投影', () => {
}).detailRows;
expect(echoRows).toEqual(
expect.arrayContaining([
- '恢复:5%自身最大气血',
- '护盾:相当于80%物攻',
+ '命中后:恢复:5%自身最大气血',
+ '命中后:护盾:相当于80%物攻',
'参悟·山河回响:每3回合最多触发一次',
]),
);
diff --git a/src/shared/engine/sect/content/lingxiao/tests/sectNodeRuntime.test.ts b/src/shared/engine/sect/content/lingxiao/tests/sectNodeRuntime.test.ts
index abab2287..cb6dd13a 100644
--- a/src/shared/engine/sect/content/lingxiao/tests/sectNodeRuntime.test.ts
+++ b/src/shared/engine/sect/content/lingxiao/tests/sectNodeRuntime.test.ts
@@ -1,18 +1,23 @@
import type { ActiveSkill } from '@shared/engine/battle-v5/abilities/ActiveSkill';
+import { StackRule } from '@shared/engine/battle-v5/buffs/Buff';
import { EventBus } from '@shared/engine/battle-v5/core/EventBus';
import type {
ActionPostEvent,
DamageRequestEvent,
DodgeEvent,
} from '@shared/engine/battle-v5/core/events';
+import { consumeQueuedAction } from '@shared/engine/battle-v5/core/runtimeState';
import {
AttributeType,
+ BuffType,
DamageSource,
DamageType,
} from '@shared/engine/battle-v5/core/types';
import { AbilityFactory } from '@shared/engine/battle-v5/factories/AbilityFactory';
+import { BuffFactory } from '@shared/engine/battle-v5/factories/BuffFactory';
import { DamageSystem } from '@shared/engine/battle-v5/systems/DamageSystem';
import { Unit } from '@shared/engine/battle-v5/units/Unit';
+import { GameplayTags } from '@shared/engine/shared/tag-domain';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { LINGXIAO_SWORD_MOMENTUM } from '..';
import { projectSectCombat, resolveSectAbility } from '../..';
@@ -93,19 +98,34 @@ describe('凌霄参悟运行时语义', () => {
it('留痕使剑荡山河施加两层剑痕', () => {
const { owner, enemy } = install('swift-sword', ['swift-retained-force']);
- const linked = owner.abilities.getAbility('sect.lingxiao.linked-edge') as ActiveSkill;
+ const linked = owner.abilities.getAbility(
+ 'sect.lingxiao.linked-edge',
+ ) as ActiveSkill;
linked.execute({ caster: owner, target: enemy });
- expect(enemy.buffs.getAllBuffs().find((buff) => buff.id === 'sect.lingxiao.sword-mark')?.getLayer()).toBe(2);
+ expect(
+ enemy.buffs
+ .getAllBuffs()
+ .find((buff) => buff.id === 'sect.lingxiao.sword-mark')
+ ?.getLayer(),
+ ).toBe(2);
});
- it('守心在开始蓄势时立即获得相当于60%物攻的护盾', () => {
+ it('守心在开始蓄势时立即获得成长后的护盾', () => {
const { sect } = install('heavy-sword', ['heavy-retained-frame']);
- const ability = resolveSectAbility({ sect, realm: '化神', abilityId: 'turning-body' }).config;
+ const ability = resolveSectAbility({
+ sect,
+ realm: '化神',
+ abilityId: 'turning-body',
+ }).config;
expect(ability.castEffects).toContainEqual(
expect.objectContaining({ type: 'shield' }),
);
- const queue = ability.castEffects?.find((effect) => effect.type === 'queue_action');
- expect(queue?.type === 'queue_action' ? queue.params : undefined).toMatchObject({
+ const queue = ability.castEffects?.find(
+ (effect) => effect.type === 'queue_action',
+ );
+ expect(
+ queue?.type === 'queue_action' ? queue.params : undefined,
+ ).toMatchObject({
interruptPolicy: 'uninterruptible',
hitPolicy: 'guaranteed',
});
@@ -180,7 +200,7 @@ describe('凌霄参悟运行时语义', () => {
};
EventBus.instance.publish(request);
- expect(request.damageIncreasePctBucket).toBeCloseTo(0.2);
+ expect(request.damageIncreasePctBucket).toBeCloseTo(0.15);
});
it('承锋只降低整场战斗第一次直接伤害并获得2点剑势', () => {
@@ -219,8 +239,139 @@ describe('凌霄参悟运行时语义', () => {
turning.execute({ caster: owner, target: enemy });
- expect(owner.buffs.getAllBuffIds()).toContain('sect.lingxiao.heavy.hidden-edge');
+ expect(owner.buffs.getAllBuffIds()).toContain(
+ 'sect.lingxiao.heavy.hidden-edge',
+ );
expect(owner.getCurrentShield()).toBe(0);
expect(enemy.getCurrentShield()).toBe(0);
});
+
+ it('快剑藏锋未命中时整条普通效果链失效', () => {
+ const { sect, owner, enemy } = install('swift-sword', []);
+ const turning = AbilityFactory.create(
+ resolveSectAbility({ sect, realm: '化神', abilityId: 'turning-body' })
+ .config,
+ ) as ActiveSkill;
+ const hpBefore = enemy.getCurrentHp();
+
+ turning.execute({
+ caster: owner,
+ target: enemy,
+ shouldApplyEffects: false,
+ });
+
+ expect(enemy.getCurrentHp()).toBe(hpBefore);
+ expect(owner.buffs.getAllBuffIds()).not.toContain(
+ 'sect.lingxiao.returning-swallow',
+ );
+ expect(owner.combatResources.getCurrent(LINGXIAO_SWORD_MOMENTUM)).toBe(0);
+ });
+
+ it('基础与重剑藏锋不依赖前置命中并正常登记后发', () => {
+ for (const pathId of ['heavy-sword'] as const) {
+ const { sect, owner, enemy } = install(pathId, []);
+ const turning = AbilityFactory.create(
+ resolveSectAbility({ sect, realm: '化神', abilityId: 'turning-body' })
+ .config,
+ ) as ActiveSkill;
+ turning.execute({
+ caster: owner,
+ target: enemy,
+ shouldApplyEffects: false,
+ });
+ expect(consumeQueuedAction(owner)?.ability.name).toBe('听雷');
+ expect(owner.buffs.getAllBuffIds()).toContain(
+ 'sect.lingxiao.heavy.hidden-edge',
+ );
+ }
+
+ const sect = sectState('heavy-sword', []);
+ sect.activePathId = undefined;
+ sect.paths = [];
+ const projection = projectSectCombat({ sect, realm: '化神' })!;
+ const owner = combatUnit('base-owner');
+ const enemy = combatUnit('base-enemy');
+ for (const resource of projection.resources)
+ owner.combatResources.define(resource);
+ const turning = AbilityFactory.create(
+ resolveSectAbility({ sect, realm: '化神', abilityId: 'turning-body' })
+ .config,
+ ) as ActiveSkill;
+ turning.execute({
+ caster: owner,
+ target: enemy,
+ shouldApplyEffects: false,
+ });
+ expect(consumeQueuedAction(owner)?.ability.name).toBe('听雷');
+ });
+
+ it('重剑攻击未命中时不获得护盾和剑势,一剑破妄也不驱散', () => {
+ const { sect, owner, enemy } = install('heavy-sword', []);
+ for (const abilityId of ['guiding-sword', 'linked-edge']) {
+ const ability = AbilityFactory.create(
+ resolveSectAbility({ sect, realm: '化神', abilityId }).config,
+ ) as ActiveSkill;
+ ability.execute({
+ caster: owner,
+ target: enemy,
+ shouldApplyEffects: false,
+ });
+ }
+ expect(owner.getCurrentShield()).toBe(0);
+ expect(owner.combatResources.getCurrent(LINGXIAO_SWORD_MOMENTUM)).toBe(0);
+
+ enemy.buffs.addBuff(
+ BuffFactory.create({
+ id: 'test.positive',
+ name: '测试增益',
+ type: BuffType.BUFF,
+ duration: 3,
+ stackRule: StackRule.OVERRIDE,
+ tags: [GameplayTags.BUFF.TYPE.BUFF],
+ }),
+ enemy,
+ );
+ const breaking = AbilityFactory.create(
+ resolveSectAbility({ sect, realm: '化神', abilityId: 'breaking-edge' })
+ .config,
+ ) as ActiveSkill;
+ breaking.execute({
+ caster: owner,
+ target: enemy,
+ shouldApplyEffects: false,
+ });
+ expect(enemy.buffs.getAllBuffIds()).toContain('test.positive');
+ });
+
+ it('快剑姿态提前重施会替换旧修改器并重置首次闪避预算', () => {
+ for (const abilityId of ['turning-body', 'shadow-step']) {
+ const { sect, owner, enemy } = install('swift-sword', []);
+ const ability = AbilityFactory.create(
+ resolveSectAbility({ sect, realm: '化神', abilityId }).config,
+ ) as ActiveSkill;
+ const target = abilityId === 'shadow-step' ? owner : enemy;
+ const dodge = (): void =>
+ EventBus.instance.publish({
+ type: 'DodgeEvent',
+ timestamp: Date.now(),
+ caster: enemy,
+ target: owner,
+ ability,
+ });
+
+ ability.execute({ caster: owner, target });
+ const modifiedEvasion = owner.attributes.getValue(
+ AttributeType.EVASION_RATE,
+ );
+ dodge();
+ expect(owner.combatResources.getCurrent(LINGXIAO_SWORD_MOMENTUM)).toBe(1);
+
+ ability.execute({ caster: owner, target });
+ expect(owner.attributes.getValue(AttributeType.EVASION_RATE)).toBeCloseTo(
+ modifiedEvasion,
+ );
+ dodge();
+ expect(owner.combatResources.getCurrent(LINGXIAO_SWORD_MOMENTUM)).toBe(2);
+ }
+ });
});
diff --git a/src/shared/engine/sect/core/authoring/SectEffectFactory.ts b/src/shared/engine/sect/core/authoring/SectEffectFactory.ts
index 0a9ca47c..f912d2b2 100644
--- a/src/shared/engine/sect/core/authoring/SectEffectFactory.ts
+++ b/src/shared/engine/sect/core/authoring/SectEffectFactory.ts
@@ -5,6 +5,8 @@ import {
DamageSource,
DamageType,
} from '@shared/engine/battle-v5/core/types';
+import { GameplayTags } from '@shared/engine/shared/tag-domain';
+import { standardSectMethodGrowthPolicy } from './StandardSectMethodGrowthPolicy';
export const DIRECT_DAMAGE_CONDITION = {
type: 'damage_source_is' as const,
@@ -78,6 +80,22 @@ export class SectEffectFactory {
};
}
+ dispelPositiveBuffsByMethod(
+ baseCount: number,
+ methodLevel: number | undefined,
+ ): EffectConfig {
+ return {
+ type: 'dispel',
+ params: {
+ targetTag: GameplayTags.BUFF.TYPE.BUFF,
+ maxCount: standardSectMethodGrowthPolicy.growCount(
+ baseCount,
+ methodLevel,
+ ),
+ },
+ };
+ }
+
modifyCounter(
key: string,
operation: 'add' | 'subtract' | 'set' | 'reset',
diff --git a/src/shared/engine/sect/core/authoring/StandardSectMethodGrowthPolicy.ts b/src/shared/engine/sect/core/authoring/StandardSectMethodGrowthPolicy.ts
index a6f97963..42de25f2 100644
--- a/src/shared/engine/sect/core/authoring/StandardSectMethodGrowthPolicy.ts
+++ b/src/shared/engine/sect/core/authoring/StandardSectMethodGrowthPolicy.ts
@@ -13,6 +13,7 @@ import { consumeSectBuffMethodGrowth } from './SectMethodGrowthAuthoring';
export interface SectMethodGrowthValues {
level: number;
+ tier: number;
magnitude: number;
statusMagnitude: number;
durationBonus: number;
@@ -31,11 +32,18 @@ export class StandardSectMethodGrowthPolicy {
rawLevel === undefined || Number.isNaN(rawLevel) ? 1 : rawLevel;
const level = Math.max(1, Math.min(180, Math.floor(normalized)));
if (level === 180) {
- return { level, magnitude: 1.5, statusMagnitude: 1.4, durationBonus: 3 };
+ return {
+ level,
+ tier: 6,
+ magnitude: 1.5,
+ statusMagnitude: 1.4,
+ durationBonus: 3,
+ };
}
if (level >= 150) {
return {
level,
+ tier: 5,
magnitude: 1.4167,
statusMagnitude: 1.3333,
durationBonus: 2,
@@ -44,17 +52,25 @@ export class StandardSectMethodGrowthPolicy {
if (level >= 120) {
return {
level,
+ tier: 4,
magnitude: 1.3333,
statusMagnitude: 1.2667,
durationBonus: 2,
};
}
if (level >= 90) {
- return { level, magnitude: 1.25, statusMagnitude: 1.2, durationBonus: 1 };
+ return {
+ level,
+ tier: 3,
+ magnitude: 1.25,
+ statusMagnitude: 1.2,
+ durationBonus: 1,
+ };
}
if (level >= 60) {
return {
level,
+ tier: 2,
magnitude: 1.1667,
statusMagnitude: 1.1333,
durationBonus: 1,
@@ -63,12 +79,19 @@ export class StandardSectMethodGrowthPolicy {
if (level >= 30) {
return {
level,
+ tier: 1,
magnitude: 1.0833,
statusMagnitude: 1.0667,
durationBonus: 0,
};
}
- return { level, magnitude: 1, statusMagnitude: 1, durationBonus: 0 };
+ return {
+ level,
+ tier: 0,
+ magnitude: 1,
+ statusMagnitude: 1,
+ durationBonus: 0,
+ };
}
scaleMagnitude(value: number, rawLevel: number | undefined): number {
@@ -85,6 +108,10 @@ export class StandardSectMethodGrowthPolicy {
: duration + this.resolve(rawLevel).durationBonus;
}
+ growCount(baseCount: number, rawLevel: number | undefined): number {
+ return Math.max(0, Math.floor(baseCount)) + this.resolve(rawLevel).tier;
+ }
+
projectAbility(
ability: SectCompiledAbility,
methodId: SectMethodId,
@@ -124,6 +151,29 @@ export class StandardSectMethodGrowthPolicy {
);
}
+ /**
+ * 被动效果默认保持固定数值,只解析其内部显式声明的心法成长内容。
+ */
+ projectPassives(
+ passives: readonly import('@shared/engine/battle-v5/core/configs').AbilityConfig[],
+ methodLevels: Partial>,
+ ): import('@shared/engine/battle-v5/core/configs').AbilityConfig[] {
+ const fixedGrowth = this.resolve(1);
+ return passives.map((passive) => {
+ const projected = structuredClone(passive);
+ projected.effects = projected.effects?.map((effect) =>
+ this.projectEffect(effect, fixedGrowth, methodLevels),
+ );
+ projected.castEffects = projected.castEffects?.map((effect) =>
+ this.projectEffect(effect, fixedGrowth, methodLevels),
+ );
+ projected.listeners = projected.listeners?.map((listener) =>
+ this.projectListener(listener, fixedGrowth, methodLevels),
+ );
+ return projected;
+ });
+ }
+
private projectEffect(
effect: EffectConfig,
growth: SectMethodGrowthValues,
diff --git a/src/shared/engine/sect/core/compilation/SectCompiler.ts b/src/shared/engine/sect/core/compilation/SectCompiler.ts
index ae273d5a..481fabbe 100644
--- a/src/shared/engine/sect/core/compilation/SectCompiler.ts
+++ b/src/shared/engine/sect/core/compilation/SectCompiler.ts
@@ -1,4 +1,5 @@
import { AbilityFactory } from '@shared/engine/battle-v5/factories/AbilityFactory';
+import { standardSectMethodGrowthPolicy } from '../authoring';
import type {
CultivatorSectPathState,
ResolvedSectAbility,
@@ -9,10 +10,11 @@ import type {
SectProjectionContext,
} from '../domain';
import type { SectModule } from '../plugin';
-import { projectSectMethodModifiers } from '../presentation';
-import { describeSectAbilityConfig } from '../presentation';
+import {
+ describeSectAbilityConfig,
+ projectSectMethodModifiers,
+} from '../presentation';
import { isAbilityUnlocked } from '../progression';
-import { standardSectMethodGrowthPolicy } from '../authoring';
function findActivePath(
context: SectProjectionContext,
@@ -76,6 +78,10 @@ export class SectCompiler {
rawBuild.abilities,
context.sect.methods,
),
+ passives: standardSectMethodGrowthPolicy.projectPassives(
+ rawBuild.passives,
+ context.sect.methods,
+ ),
});
this.assertCombatResourceContract(module.definition, build);
this.assertAbilityContracts(build);
@@ -183,7 +189,8 @@ export class SectCompiler {
{
...ability,
detailRows,
- summary: base?.description ?? ability.summary ?? ability.config.name,
+ summary:
+ base?.description ?? ability.summary ?? ability.config.name,
},
];
}),
@@ -204,7 +211,9 @@ export class SectCompiler {
): void {
const expected = definition.combatResource;
if (build.resources.length !== 1) {
- throw new Error(`宗门 ${definition.id} 编译结果必须且只能包含一个战斗资源`);
+ throw new Error(
+ `宗门 ${definition.id} 编译结果必须且只能包含一个战斗资源`,
+ );
}
const actual = build.resources[0];
if (
diff --git a/src/shared/engine/sect/core/domain/compilation.ts b/src/shared/engine/sect/core/domain/compilation.ts
index ec409605..deb79c30 100644
--- a/src/shared/engine/sect/core/domain/compilation.ts
+++ b/src/shared/engine/sect/core/domain/compilation.ts
@@ -78,10 +78,17 @@ export interface ResolvedSectPathAbilityPreview {
current?: ResolvedSectAbility;
}
+export interface ResolvedSectNodePreview {
+ id: string;
+ name: string;
+ description: string;
+}
+
export interface ResolvedSectPathPreview {
pathId: string;
learned: boolean;
active: boolean;
activeMeridianSlot?: 1 | 2 | 3;
+ nodes: ResolvedSectNodePreview[];
abilities: ResolvedSectPathAbilityPreview[];
}
diff --git a/src/shared/engine/sect/core/plugin/SectNodePlugin.ts b/src/shared/engine/sect/core/plugin/SectNodePlugin.ts
index 51ce611f..053dcc6f 100644
--- a/src/shared/engine/sect/core/plugin/SectNodePlugin.ts
+++ b/src/shared/engine/sect/core/plugin/SectNodePlugin.ts
@@ -2,6 +2,7 @@ import type { SectBuildBuilder } from '../compilation';
import type {
SectMeridianNodeDefinition,
SectNodeApplyContext,
+ SectProjectionContext,
} from '../domain';
import type { SectNodePlugin } from './contracts';
@@ -13,8 +14,15 @@ export class ConfiguredSectNodePlugin implements SectNodePlugin {
context: SectNodeApplyContext,
builder: SectBuildBuilder,
) => void,
+ private readonly descriptionResolver?: (
+ context: SectProjectionContext,
+ ) => string,
) {}
+ describe(context: SectProjectionContext): string {
+ return this.descriptionResolver?.(context) ?? this.definition.description;
+ }
+
apply(context: SectNodeApplyContext, builder: SectBuildBuilder): void {
this.behavior(context, builder);
}
diff --git a/src/shared/engine/sect/core/plugin/contracts.ts b/src/shared/engine/sect/core/plugin/contracts.ts
index 58f7ee19..6e3ef86e 100644
--- a/src/shared/engine/sect/core/plugin/contracts.ts
+++ b/src/shared/engine/sect/core/plugin/contracts.ts
@@ -15,6 +15,7 @@ import type {
export interface SectNodePlugin {
readonly definition: import('../domain').SectMeridianNodeDefinition;
+ describe?(context: SectProjectionContext): string;
apply(context: SectNodeApplyContext, builder: SectBuildBuilder): void;
}
diff --git a/src/shared/engine/sect/core/presentation/abilityFacts.ts b/src/shared/engine/sect/core/presentation/abilityFacts.ts
index ebb9d506..9f4cf5dc 100644
--- a/src/shared/engine/sect/core/presentation/abilityFacts.ts
+++ b/src/shared/engine/sect/core/presentation/abilityFacts.ts
@@ -430,7 +430,7 @@ function describeEffect(
const nested = effect.params.effects ?? [];
return [
...damageEffectsRow(nested, resources),
- ...describeEffectList(nested, resources),
+ ...nested.flatMap((child) => describeEffect(child, resources)),
];
}
case 'cooldown_modify':
@@ -445,6 +445,7 @@ function describeEffect(
function describeEffectList(
effects: readonly EffectConfig[],
resources: readonly CombatResourceDefinition[],
+ timing: 'hit' | 'cast',
): string[] {
const rows: string[] = [];
const handled = new Set();
@@ -464,15 +465,51 @@ function describeEffectList(
candidate.params.buffConfig.id === effect.params.buffConfig.id,
);
matches.forEach(({ candidateIndex }) => handled.add(candidateIndex));
- rows.push(...describeEffect(effect, resources, matches.length));
+ rows.push(
+ ...applyEffectTiming(
+ effect,
+ describeEffect(effect, resources, matches.length),
+ timing,
+ ),
+ );
return;
}
handled.add(index);
- rows.push(...describeEffect(effect, resources));
+ rows.push(
+ ...applyEffectTiming(effect, describeEffect(effect, resources), timing),
+ );
});
return rows;
}
+function applyEffectTiming(
+ effect: EffectConfig,
+ rows: readonly string[],
+ timing: 'hit' | 'cast',
+): string[] {
+ const timingLabel = timing === 'hit' ? '命中后' : '施展后';
+ return rows.map((row) => {
+ if (
+ row.startsWith('伤害:') ||
+ row.startsWith('暴击:') ||
+ row.startsWith('穿防:') ||
+ row.startsWith('施放条件:') ||
+ /点.+时总倍率:/.test(row)
+ ) {
+ return row;
+ }
+ if (effect.type === 'queue_action') {
+ if (row.startsWith('后发:') || row.startsWith('蓄势:除')) return row;
+ if (row.startsWith('蓄势:')) return `${timingLabel}:${row}`;
+ return `后发命中后:${row.replace(/^施放后:/, '')}`;
+ }
+ if (row.startsWith('施放后:')) {
+ return `${timingLabel}:${row.slice('施放后:'.length)}`;
+ }
+ return `${timingLabel}:${row}`;
+ });
+}
+
export function describeSectAbilityConfig(
config: AbilityConfig,
resources: readonly CombatResourceDefinition[],
@@ -490,7 +527,13 @@ export function describeSectAbilityConfig(
}
}
rows.push(...damageEffectsRow(config.effects ?? [], resources));
- rows.push(...describeEffectList(config.effects ?? [], resources));
- rows.push(...describeEffectList(config.castEffects ?? [], resources));
+ rows.push(
+ ...describeEffectList(
+ config.effects ?? [],
+ resources,
+ config.targetPolicy?.team === 'enemy' ? 'hit' : 'cast',
+ ),
+ );
+ rows.push(...describeEffectList(config.castEffects ?? [], resources, 'cast'));
return Array.from(new Set(rows.filter(Boolean)));
}
diff --git a/src/shared/engine/sect/core/tests/methodGrowth.test.ts b/src/shared/engine/sect/core/tests/methodGrowth.test.ts
index b0fb729f..28599765 100644
--- a/src/shared/engine/sect/core/tests/methodGrowth.test.ts
+++ b/src/shared/engine/sect/core/tests/methodGrowth.test.ts
@@ -71,24 +71,45 @@ function ability(): SectCompiledAbility {
describe('StandardSectMethodGrowthPolicy', () => {
it.each([
- [0, 1, 1, 0],
- [1, 1, 1, 0],
- [29, 1, 1, 0],
- [30, 1.0833, 1.0667, 0],
- [60, 1.1667, 1.1333, 1],
- [100, 1.25, 1.2, 1],
- [180, 1.5, 1.4, 3],
- [999, 1.5, 1.4, 3],
- [-10, 1, 1, 0],
- ])('等级 %i 使用标准成长档位', (level, magnitude, status, duration) => {
+ [0, 0, 1, 1, 0],
+ [1, 0, 1, 1, 0],
+ [29, 0, 1, 1, 0],
+ [30, 1, 1.0833, 1.0667, 0],
+ [60, 2, 1.1667, 1.1333, 1],
+ [100, 3, 1.25, 1.2, 1],
+ [180, 6, 1.5, 1.4, 3],
+ [999, 6, 1.5, 1.4, 3],
+ [-10, 0, 1, 1, 0],
+ ])('等级 %i 使用标准成长档位', (level, tier, magnitude, status, duration) => {
expect(policy.resolve(level)).toEqual({
level: Math.max(1, Math.min(180, level)),
+ tier,
magnitude,
statusMagnitude: status,
durationBonus: duration,
});
});
+ it.each([
+ [0, 1],
+ [1, 1],
+ [29, 1],
+ [30, 2],
+ [59, 2],
+ [60, 3],
+ [89, 3],
+ [90, 4],
+ [119, 4],
+ [120, 5],
+ [149, 5],
+ [150, 6],
+ [179, 6],
+ [180, 7],
+ [999, 7],
+ ])('等级 %i 将基础计数1成长为%i', (level, expected) => {
+ expect(policy.growCount(1, level)).toBe(expected);
+ });
+
it('组合完成后执行一次最终投影,并区分成长与固定持续时间', () => {
const once = policy.projectAbility(ability(), 'fixture-method', {
'fixture-method': 180,
@@ -109,4 +130,66 @@ describe('StandardSectMethodGrowthPolicy', () => {
});
expect(JSON.stringify(once.config)).not.toContain('__sectMethodGrowth');
});
+
+ it('被动固定数值不成长,但会解析内部显式声明的状态成长', () => {
+ const passive = {
+ slug: 'sect.fixture.passive-growth',
+ name: '被动成长测试',
+ type: 'passive_skill' as const,
+ listeners: [
+ {
+ id: 'fixture.listener',
+ eventType: 'ActionPostEvent',
+ scope: 'owner_as_actor',
+ priority: 0,
+ mapping: { caster: 'owner' as const, target: 'owner' as const },
+ effects: [
+ {
+ type: 'damage' as const,
+ params: {
+ value: { attribute: AttributeType.ATK, coefficient: 0.4 },
+ damageType: DamageType.PHYSICAL,
+ },
+ },
+ {
+ type: 'apply_buff' as const,
+ params: {
+ target: 'target' as const,
+ buffConfig: withSectBuffMethodGrowth(
+ {
+ id: 'fixture.passive-mark',
+ name: '被动印记',
+ type: BuffType.DEBUFF,
+ duration: 3,
+ stackRule: StackRule.STACK_LAYER,
+ modifiers: [
+ {
+ attrType: AttributeType.DEF,
+ type: ModifierType.ADD,
+ value: -0.03,
+ },
+ ],
+ },
+ { methodId: 'fixture-method', duration: true },
+ ),
+ },
+ },
+ ],
+ },
+ ],
+ };
+
+ const [projected] = policy.projectPassives([passive], {
+ 'fixture-method': 180,
+ });
+ expect(projected.listeners?.[0].effects[0]).toMatchObject({
+ params: { value: { coefficient: 0.4 } },
+ });
+ expect(projected.listeners?.[0].effects[1]).toMatchObject({
+ params: {
+ buffConfig: { duration: 6, modifiers: [{ value: -0.042 }] },
+ },
+ });
+ expect(JSON.stringify(projected)).not.toContain('__sectMethodGrowth');
+ });
});