优化:宗门4.2技能平衡

This commit is contained in:
taojiacheng committed 2026-07-17 18:24:54 +08:00
1 parent 9843a79899
commit f433cada32
37 files changed
+1472 -318

No files matched your search

+14 -9
View File
@@ -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>
);
})}
+14 -3
View File
@@ -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');
});
});