优化:宗门4.2技能平衡
This commit is contained in:
1 parent
9843a79899
commit
f433cada32
37 files changed
+1472
-318
No files matched your search
@@ -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 胜率和回合门禁继续通过环境变量显式执行。
|
||||
该命令当前会按上述实测差距失败,避免未达标数据被误标为平衡验收完成。
|
||||
@@ -335,9 +335,7 @@ function PathDrawer({
|
||||
/>
|
||||
) : meridianFooterAction === 'save' ? (
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 text-sm">
|
||||
<p className="text-crimson">
|
||||
参悟方案{SLOT_NAMES[slot]}尚未保存。
|
||||
</p>
|
||||
<p className="text-crimson">参悟方案{SLOT_NAMES[slot]}尚未保存。</p>
|
||||
<InkButton
|
||||
variant="primary"
|
||||
disabled={busy}
|
||||
@@ -547,7 +545,11 @@ function PathDrawer({
|
||||
className={`flex h-full flex-col items-stretch justify-start p-3 text-left text-sm leading-6 ${selected ? 'bg-crimson/10 text-crimson' : 'bg-ink/5'} ${available ? '' : 'cursor-not-allowed opacity-50'}`}
|
||||
>
|
||||
<strong className="block">{node.name}</strong>
|
||||
<p className="mt-1">{node.description}</p>
|
||||
<p className="mt-1">
|
||||
{preview.nodes.find(
|
||||
(previewNode) => previewNode.id === node.id,
|
||||
)?.description ?? node.description}
|
||||
</p>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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点后收束。',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<SectCompiledBuild>,
|
||||
path: CultivatorSectPathState,
|
||||
features: HeavySwordFeatures,
|
||||
edgeCleansingLevel?: number,
|
||||
): Record<string, SectCompiledAbility> {
|
||||
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',
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -53,7 +53,7 @@ const SWIFT_SWORD_DEFINITION: SectPathDefinitionWithoutNodes = {
|
||||
id: 'counter',
|
||||
name: '回燕',
|
||||
description:
|
||||
'优先维持《藏锋听雷》与《踏雪无痕》,剑势达到5点后再行收束。',
|
||||
'优先补《藏锋听雷》;身法不占优且《踏雪无痕》缺失时,先踏雪再施展《剑荡山河》;剑势达到5点后收束。',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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点',
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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;
|
||||
@@ -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%',
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<NonNullable<AbilityConfig['modifiers']>>,
|
||||
listeners?: NonNullable<AbilityConfig['listeners']>,
|
||||
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<SectCompiledBuild>,
|
||||
path: CultivatorSectPathState,
|
||||
features: SwiftSwordFeatures,
|
||||
edgeCleansingLevel?: number,
|
||||
): Record<string, SectCompiledAbility> {
|
||||
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),
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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))}%`;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -65,7 +65,7 @@ const NODE_SET: Record<SwiftTactic | HeavyTactic, string[]> = {
|
||||
'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<SwiftTactic | HeavyTactic, string[]> = {
|
||||
],
|
||||
'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) => {
|
||||
|
||||
@@ -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<string, unknown> | 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<string, unknown>;
|
||||
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回合最多触发一次',
|
||||
]),
|
||||
);
|
||||
|
||||
@@ -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<DodgeEvent>({
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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',
|
||||
|
||||
@@ -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<Record<SectMethodId, number>>,
|
||||
): 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,
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
|
||||
export interface SectNodePlugin {
|
||||
readonly definition: import('../domain').SectMeridianNodeDefinition;
|
||||
describe?(context: SectProjectionContext): string;
|
||||
apply(context: SectNodeApplyContext, builder: SectBuildBuilder): void;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<number>();
|
||||
@@ -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)));
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user