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'); + }); });