优化:幽都第三轮优化
This commit is contained in:
1 parent
6e5d5428e0
commit
bb975aef74
16 files changed
+485
-50
No files matched your search
@@ -39,7 +39,9 @@ function SectAlchemyBody() {
|
||||
sectContext={{
|
||||
facilityLevel: level,
|
||||
discountPercent,
|
||||
facilityLabel: presentation.facilityLabels.workshop,
|
||||
facilityLabel:
|
||||
presentation.facilityLabels.alchemy ??
|
||||
presentation.facilityLabels.workshop,
|
||||
scene,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -39,7 +39,9 @@ function SectRefineryBody() {
|
||||
sectContext={{
|
||||
facilityLevel: level,
|
||||
discountPercent,
|
||||
facilityLabel: presentation.facilityLabels.workshop,
|
||||
facilityLabel:
|
||||
presentation.facilityLabels.refinery ??
|
||||
presentation.facilityLabels.workshop,
|
||||
scene,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -52,6 +52,7 @@ function getDefaultScope(eventType: string): ListenerScope {
|
||||
return 'owner_as_actor';
|
||||
case 'SkillCastEvent':
|
||||
case 'SkillPreCastEvent':
|
||||
case 'HitCheckEvent':
|
||||
case 'DamageRequestEvent':
|
||||
return 'owner_as_caster';
|
||||
case 'RoundPreEvent':
|
||||
|
||||
@@ -155,6 +155,10 @@ export class DamageSystem {
|
||||
* ⑦ 最小伤害保证 + 四舍五入
|
||||
*/
|
||||
private _onDamageRequest(event: DamageRequestEvent): void {
|
||||
if (!event.target.isAlive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { target } = event;
|
||||
|
||||
const damageType = this._resolveDamageType(event);
|
||||
@@ -456,7 +460,7 @@ export class DamageSystem {
|
||||
|
||||
// 最终判定:在所有 DamageTakenEvent 监听器执行完后,重新检查存活状态
|
||||
// 如果免死效果生效,target.currentHp 会变为 1,从而跳过此处的阵亡发布
|
||||
if (target.getCurrentHp() <= 0) {
|
||||
if (beforeHp > 0 && target.getCurrentHp() <= 0) {
|
||||
EventBus.instance.publish<UnitDeadEvent>({
|
||||
type: 'UnitDeadEvent',
|
||||
timestamp: Date.now(),
|
||||
|
||||
@@ -90,6 +90,46 @@ function percentage(value: number): string {
|
||||
return `${Math.round(value * 100)}%`;
|
||||
}
|
||||
|
||||
function debuffTags(...tags: string[]): string[] {
|
||||
return [GameplayTags.BUFF.TYPE.DEBUFF, ...tags];
|
||||
}
|
||||
|
||||
function controlTags(...tags: string[]): string[] {
|
||||
return [
|
||||
GameplayTags.BUFF.TYPE.DEBUFF,
|
||||
GameplayTags.BUFF.TYPE.CONTROL,
|
||||
...tags,
|
||||
];
|
||||
}
|
||||
|
||||
function layerCurveDetail(label: string, values: readonly number[]): string {
|
||||
const groups: Array<{ start: number; end: number; value: number }> = [];
|
||||
values.forEach((value, index) => {
|
||||
const layer = index + 1;
|
||||
const previous = groups[groups.length - 1];
|
||||
if (previous?.value === value) {
|
||||
previous.end = layer;
|
||||
} else {
|
||||
groups.push({ start: layer, end: layer, value });
|
||||
}
|
||||
});
|
||||
return `${label}:${groups.map(({ start, end, value }) => {
|
||||
const range = start === end ? `${start}层` : `${start}~${end}层`;
|
||||
return `${range}${percentage(value)}`;
|
||||
}).join(',')}`;
|
||||
}
|
||||
|
||||
function erosionDetailRows(settings: YouduBuildSettings): string[] {
|
||||
return [
|
||||
layerCurveDetail(
|
||||
'物攻、法攻、物防、法防、身法',
|
||||
settings.erosionAttributeCurve,
|
||||
),
|
||||
layerCurveDetail('受治疗削弱', settings.erosionHealCurve),
|
||||
'普通驱散每次只移除1层蚀魂',
|
||||
];
|
||||
}
|
||||
|
||||
function soulFireDetail(settings: YouduBuildSettings): string {
|
||||
return `拥有3点魂火时:本次魂伤提高${percentage(settings.soulFireBonus)},伤害后消耗全部魂火`;
|
||||
}
|
||||
@@ -227,6 +267,7 @@ function temporaryPenalty(
|
||||
type: BuffType.DEBUFF,
|
||||
duration: 1,
|
||||
stackRule: StackRule.REFRESH_DURATION,
|
||||
tags: debuffTags(),
|
||||
modifiers: [
|
||||
{ attrType: AttributeType.ATK, type: ModifierType.ADD, value: attack },
|
||||
{ attrType: AttributeType.MAGIC_ATK, type: ModifierType.ADD, value: attack },
|
||||
@@ -278,10 +319,10 @@ function soulLostBuff(settings: YouduBuildSettings): BuffConfig {
|
||||
duration: 1,
|
||||
stackRule: StackRule.IGNORE,
|
||||
dispelPolicy: 'protected',
|
||||
tags: [
|
||||
tags: controlTags(
|
||||
lostTag,
|
||||
GameplayTags.STATUS.CONTROL.NO_ACTION,
|
||||
],
|
||||
),
|
||||
statusTags: [GameplayTags.STATUS.CONTROL.NO_ACTION],
|
||||
modifiers: [{
|
||||
attrType: AttributeType.HEAL_RECEIVED_REDUCTION,
|
||||
@@ -337,7 +378,7 @@ function forgetfulRiverBuff(settings: YouduBuildSettings): BuffConfig {
|
||||
type: BuffType.DEBUFF,
|
||||
duration: settings.forgetDuration,
|
||||
stackRule: StackRule.REFRESH_DURATION,
|
||||
tags: [forgetTag, GameplayTags.BUFF.DOT.ROOT],
|
||||
tags: debuffTags(forgetTag, GameplayTags.BUFF.DOT.ROOT),
|
||||
statusTags: [stateTag('forgetful-river')],
|
||||
modifiers: [
|
||||
{
|
||||
@@ -470,6 +511,7 @@ function soulErosionBuff(settings: YouduBuildSettings): BuffConfig {
|
||||
condition('has_not_tag', { scope: 'target', tag: returningTag }),
|
||||
],
|
||||
effects: [applyBuff(soulLostBuff(settings), 'target', {
|
||||
controlHitBonus: settings.highLayerControlHitBonus,
|
||||
onResistEffects: convergeLostSoul(settings, 'resist'),
|
||||
})],
|
||||
},
|
||||
@@ -654,7 +696,7 @@ function soulErosionBuff(settings: YouduBuildSettings): BuffConfig {
|
||||
stackRule: StackRule.STACK_LAYER,
|
||||
maxLayers: 5,
|
||||
dispelMode: 'one_layer',
|
||||
tags: [erosionTag],
|
||||
tags: debuffTags(erosionTag),
|
||||
statusTags: [stateTag('soul-erosion')],
|
||||
modifiers: [
|
||||
...[
|
||||
@@ -695,7 +737,7 @@ function noReturnBuff(settings: YouduBuildSettings): BuffConfig {
|
||||
type: BuffType.DEBUFF,
|
||||
duration: 2,
|
||||
stackRule: StackRule.REFRESH_DURATION,
|
||||
tags: [noReturnTag],
|
||||
tags: debuffTags(noReturnTag),
|
||||
statusTags: [stateTag('no-return')],
|
||||
modifiers: [
|
||||
{
|
||||
@@ -721,7 +763,7 @@ function shadowBuff(settings: YouduBuildSettings): BuffConfig {
|
||||
type: BuffType.DEBUFF,
|
||||
duration: settings.shadowDuration,
|
||||
stackRule: StackRule.REFRESH_DURATION,
|
||||
tags: [buffTag('shadow-revealed')],
|
||||
tags: debuffTags(buffTag('shadow-revealed')),
|
||||
statusTags: [shadowTag],
|
||||
modifiers: [{
|
||||
attrType: AttributeType.EVASION_RATE,
|
||||
@@ -740,7 +782,10 @@ function pinBuff(duration: number): BuffConfig {
|
||||
type: BuffType.CONTROL,
|
||||
duration,
|
||||
stackRule: StackRule.REFRESH_DURATION,
|
||||
tags: [buffTag('soul-pinning-nail'), GameplayTags.STATUS.CONTROL.NO_SKILL],
|
||||
tags: controlTags(
|
||||
buffTag('soul-pinning-nail'),
|
||||
GameplayTags.STATUS.CONTROL.NO_SKILL,
|
||||
),
|
||||
statusTags: [GameplayTags.STATUS.CONTROL.NO_SKILL],
|
||||
modifiers: [{
|
||||
attrType: AttributeType.HEAL_RECEIVED_REDUCTION,
|
||||
@@ -873,17 +918,17 @@ function compileRuntime(
|
||||
if (settings.firstShadowExtraLayer) {
|
||||
listeners.push({
|
||||
id: 'sect.youdu.first-shadow-extra-layer',
|
||||
eventType: GameplayTags.EVENT.DAMAGE_TAKEN,
|
||||
eventType: GameplayTags.EVENT.HIT_CHECK,
|
||||
scope: GameplayTags.SCOPE.OWNER_AS_CASTER,
|
||||
priority: EventPriorityLevel.DAMAGE_TAKEN,
|
||||
priority: EventPriorityLevel.POST_SETTLE,
|
||||
mapping: { caster: 'owner', target: 'event.target' },
|
||||
budget: { maxTriggers: 1, reset: 'battle' },
|
||||
conditions: [
|
||||
condition('is_hit', {}),
|
||||
condition('ability_has_tag', {
|
||||
tag: GameplayTags.ABILITY.SECT.namespace(YOUDU_SECT_ID),
|
||||
}),
|
||||
condition('has_tag', { scope: 'target', tag: shadowTag }),
|
||||
condition('damage_source_is', { damageSource: DamageSource.DIRECT }),
|
||||
],
|
||||
effects: [applyErosion(1, settings)],
|
||||
});
|
||||
@@ -899,19 +944,21 @@ function compileRuntime(
|
||||
listeners,
|
||||
detailRows: [
|
||||
`控制抗性 +${percentage(0.30 + settings.extraControlResistance)}`,
|
||||
'本场首次成功受控后立即解除',
|
||||
'本场首次成功受控后立即解除,并短暂免疫同类控制',
|
||||
...(settings.heartSoulFireGain > 0
|
||||
? [
|
||||
`首次解控时获得${settings.heartSoulFireGain}点魂火;每回合首次成功抵抗控制时获得1点魂火`,
|
||||
]
|
||||
: []),
|
||||
...(settings.heartShield
|
||||
? ['首次解控后获得10%最大气血护盾']
|
||||
: []),
|
||||
...(settings.firstShadowExtraLayer
|
||||
? ['每场首次命中照影目标的幽都神通额外增加1层蚀魂']
|
||||
: []),
|
||||
],
|
||||
}));
|
||||
|
||||
for (const id of ['tide-runtime', 'decree-runtime'] as const) {
|
||||
const passive = definition(id);
|
||||
if (passive.kind !== 'passive') throw new Error(`${id}定义错误`);
|
||||
builder.setAbility(id, factory.passive({
|
||||
definition: passive,
|
||||
pathId: settings.pathId,
|
||||
listeners: [],
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
function consumeSoulFire(): EffectConfig {
|
||||
@@ -956,6 +1003,7 @@ function compileAbilities(
|
||||
? [`目标有忘川时伤害提高${percentage(settings.sighForgetBonus)}`]
|
||||
: []),
|
||||
'命中后增加1层蚀魂',
|
||||
...erosionDetailRows(settings),
|
||||
],
|
||||
}));
|
||||
|
||||
@@ -1043,6 +1091,19 @@ function compileAbilities(
|
||||
`${settings.forgetDirectCoefficient.toFixed(2)} × 法术攻击(魂伤)`,
|
||||
`忘川持续${settings.forgetDuration}回合,每次行动前造成${coefficient(settings.forgetDotCoefficient)} × 法术攻击(魂伤)`,
|
||||
`受治疗效果额外降低${Math.round(settings.forgetHealReduction * 100)}%`,
|
||||
...(settings.forgetSpeedReduction < 0
|
||||
? [`忘川期间速度降低${percentage(-settings.forgetSpeedReduction)}`]
|
||||
: []),
|
||||
...(settings.forgetHighLayerBonus > 0
|
||||
? [`至少3层蚀魂时持续魂伤提高${percentage(settings.forgetHighLayerBonus)}`]
|
||||
: []),
|
||||
...(settings.forgetFourLayerBonus > 0
|
||||
? [`至少4层蚀魂时持续魂伤额外提高${percentage(settings.forgetFourLayerBonus)}`]
|
||||
: []),
|
||||
...(settings.crossingEcho
|
||||
? ['每回合首次对至少4层目标结算忘川时,追加0.12 × 法术攻击魂伤']
|
||||
: []),
|
||||
'命中后增加1层蚀魂',
|
||||
],
|
||||
}));
|
||||
|
||||
@@ -1054,6 +1115,7 @@ function compileAbilities(
|
||||
type: BuffType.DEBUFF,
|
||||
duration: settings.seizeDuration,
|
||||
stackRule: StackRule.REFRESH_DURATION,
|
||||
tags: debuffTags(),
|
||||
modifiers: [
|
||||
{ attrType: AttributeType.ATK, type: ModifierType.ADD, value: -0.20 },
|
||||
{ attrType: AttributeType.MAGIC_ATK, type: ModifierType.ADD, value: -0.20 },
|
||||
@@ -1091,7 +1153,7 @@ function compileAbilities(
|
||||
const pinHighEffects: EffectConfig[] = [
|
||||
{
|
||||
...applyBuff(pinBuff(2), 'target', {
|
||||
controlHitBonus: settings.pinControlHitBonus,
|
||||
controlHitBonus: settings.highLayerControlHitBonus,
|
||||
}),
|
||||
conditions: [condition('hp_above', { scope: 'target', value: 0 })],
|
||||
},
|
||||
@@ -1103,6 +1165,7 @@ function compileAbilities(
|
||||
type: BuffType.DEBUFF,
|
||||
duration: 2,
|
||||
stackRule: StackRule.REFRESH_DURATION,
|
||||
tags: debuffTags(),
|
||||
modifiers: [{
|
||||
attrType: AttributeType.SPEED,
|
||||
type: ModifierType.ADD,
|
||||
@@ -1157,7 +1220,11 @@ function compileAbilities(
|
||||
detailRows: [
|
||||
`术伤与魂伤各${coefficient(0.20 * settings.mixedDamageMultiplier)} × 法术攻击`,
|
||||
'命中后增加2层蚀魂',
|
||||
`封印1回合;施法前至少4层时延长至2回合${settings.pinControlHitBonus > 0 ? `,本次控制命中提高${percentage(settings.pinControlHitBonus)}` : ''}`,
|
||||
`封印1回合;施法前至少4层时延长至2回合${settings.highLayerControlHitBonus > 0 ? `,本次控制命中提高${percentage(settings.highLayerControlHitBonus)}` : ''}`,
|
||||
'镇魂钉期间无法施展主动技能,且受到的气血治疗降低100%',
|
||||
...(settings.pinHighLayerSlow
|
||||
? ['施法前至少4层时,目标速度降低20%,持续2回合']
|
||||
: []),
|
||||
soulFireDetail(settings),
|
||||
],
|
||||
}));
|
||||
@@ -1252,8 +1319,19 @@ function compileAbilities(
|
||||
extraTags: [soulDamageTag, soulFireConsumerTag],
|
||||
detailRows: [
|
||||
`(${settings.finishBaseCoefficient.toFixed(2)} + ${settings.finishPerLayerCoefficient.toFixed(2)} × 蚀魂层数) × 法术攻击(魂伤)`,
|
||||
'施放条件:目标至少4层蚀魂',
|
||||
settings.finishRetainedLayers > 0 ? '结算后保留2层蚀魂' : '伤害后清除全部蚀魂',
|
||||
'施加不归2回合',
|
||||
`不归期间受到的气血治疗降低80%,速度降低${percentage(-settings.noReturnSpeedReduction)}`,
|
||||
...(settings.finishAddsForget
|
||||
? [`结算后施加忘川,持续${settings.forgetDuration}回合`]
|
||||
: []),
|
||||
...(settings.oneNameOneJudgment
|
||||
? ['目标每场首次进入4层时获得标记;下一次终结命中后返还20点已支付法力并消耗标记']
|
||||
: []),
|
||||
...(settings.nameInYoudu
|
||||
? ['结算后若目标气血低于20%,获得3点魂火并减少2回合冷却,每场一次']
|
||||
: []),
|
||||
soulFireDetail(settings),
|
||||
],
|
||||
}));
|
||||
|
||||
@@ -4,10 +4,8 @@ import {
|
||||
} from '@shared/engine/battle-v5/core/types';
|
||||
import type { SectDefinitionWithoutPaths } from '../../core';
|
||||
import {
|
||||
YOUDU_DECREE_PATH_ID,
|
||||
YOUDU_SECT_ID,
|
||||
YOUDU_SOUL_FIRE,
|
||||
YOUDU_TIDE_PATH_ID,
|
||||
} from './ids';
|
||||
|
||||
export const YOUDU_BASE_DEFINITION: SectDefinitionWithoutPaths = {
|
||||
@@ -141,16 +139,6 @@ export const YOUDU_BASE_DEFINITION: SectDefinitionWithoutPaths = {
|
||||
description: '常驻控制韧性,并在每场第一次受控后自行解脱。', role: 'defensive',
|
||||
unlock: { type: 'method', methodId: 'dead-heart-living-spirit', level: 1 }, visibility: 'internal',
|
||||
},
|
||||
{
|
||||
id: 'tide-runtime', kind: 'passive', baseName: '长夜回潮',
|
||||
description: '忘川在深蚀之魂上回潮,并从有效潮伤中收拢魂火。', role: 'combo',
|
||||
unlock: { type: 'active_path', pathId: YOUDU_TIDE_PATH_ID }, visibility: 'internal',
|
||||
},
|
||||
{
|
||||
id: 'decree-runtime', kind: 'passive', baseName: '司命断章',
|
||||
description: '以照影定名,以镇魄断章,将层数化为裁断。', role: 'finisher',
|
||||
unlock: { type: 'active_path', pathId: YOUDU_DECREE_PATH_ID }, visibility: 'internal',
|
||||
},
|
||||
],
|
||||
onboarding: {
|
||||
initialContribution: 30,
|
||||
|
||||
@@ -28,7 +28,7 @@ export const YOUDU_DECREE_NODES = [
|
||||
node('decree-dead-heart-counter', '3', '心寂反照', '心死神活首次解控触发时获得2点魂火。', (f) => f.deepenHeartReflection()),
|
||||
node('decree-four-gates-closed', '4', '四门皆闭', '镇魂命中施法前至少4层目标时,令其速度降低20%两回合。', (f) => f.enableFourGatesSlow()),
|
||||
node('decree-punishment-measured', '4', '魂刑有度', '失魂被抵抗时,目标攻击与速度仍降低20%一回合。', (f) => f.enableMeasuredPunishment()),
|
||||
node('decree-iron-law', '4', '幽都铁律', '对至少4层目标施加镇魂时,控制命中提高15个百分点。', (f) => f.enforceIronLaw()),
|
||||
node('decree-iron-law', '4', '幽都铁律', '对至少4层蚀魂目标施加控制时,控制命中提高15个百分点。', (f) => f.enforceIronLaw()),
|
||||
node('decree-five-souls-scattered', '5', '五魄俱散', '目标从失魂回落后,攻击额外降低15%一回合。', (f) => f.enableFiveSoulsPenalty()),
|
||||
node('decree-returning-barrier', '5', '神归有垣', '心死神活首次解控后,获得10%最大气血护盾。', (f) => f.enableReturningBarrier()),
|
||||
node('decree-one-name-one-judgment', '5', '一名一判', '目标首次进入4层后获得标记;下一次终结命中返还20法力。', (f) => f.enableOneNameJudgment()),
|
||||
@@ -36,4 +36,3 @@ export const YOUDU_DECREE_NODES = [
|
||||
node('decree-seven-inch-severance', 'ultimate', '七寸断魂', '魂兮不归每层追加魂伤由0.20提高到0.25。', (f) => f.severSevenInches()),
|
||||
node('decree-name-in-youdu', 'ultimate', '名落幽都', '终结后目标低于20%气血时获得3魂火并返还2回合冷却,每场一次。', (f) => f.enableNameInYoudu()),
|
||||
] as const;
|
||||
|
||||
@@ -82,6 +82,7 @@ export const YOUDU_SECT_PRESENTATION: SectPresentationTheme = {
|
||||
},
|
||||
facilityLabels: {
|
||||
archive: '三魂阁', cultivation_room: '返照室', workshop: '镇铁炉',
|
||||
alchemy: '还魂药庐', refinery: '镇铁炉',
|
||||
spirit_vein: '黑水阴脉', herb_garden: '彼岸圃', formation: '万魂归窍阵',
|
||||
},
|
||||
lockedFacilities: ['formation'],
|
||||
@@ -106,4 +107,3 @@ export const YOUDU_SECT_PRESENTATION: SectPresentationTheme = {
|
||||
sweepActivity: '巡灯照影', sweepCanvasLabel: '巡灯照影游戏画布',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -34,7 +34,7 @@ export interface YouduBuildSettings {
|
||||
heartSoulFireGain: number;
|
||||
pinHighLayerSlow: boolean;
|
||||
lostResistPenalty: boolean;
|
||||
pinControlHitBonus: number;
|
||||
highLayerControlHitBonus: number;
|
||||
lostAfterPenalty: boolean;
|
||||
heartShield: boolean;
|
||||
oneNameOneJudgment: boolean;
|
||||
@@ -78,7 +78,7 @@ export function createYouduBuildSettings(pathId?: SectPathId): YouduBuildSetting
|
||||
heartSoulFireGain: pathId === 'decree' ? 1 : 0,
|
||||
pinHighLayerSlow: false,
|
||||
lostResistPenalty: false,
|
||||
pinControlHitBonus: 0,
|
||||
highLayerControlHitBonus: 0,
|
||||
lostAfterPenalty: false,
|
||||
heartShield: false,
|
||||
oneNameOneJudgment: false,
|
||||
@@ -133,7 +133,7 @@ export class DecreeBuildFacade {
|
||||
deepenHeartReflection(): void { this.settings.heartSoulFireGain = 2; }
|
||||
enableFourGatesSlow(): void { this.settings.pinHighLayerSlow = true; }
|
||||
enableMeasuredPunishment(): void { this.settings.lostResistPenalty = true; }
|
||||
enforceIronLaw(): void { this.settings.pinControlHitBonus = 0.15; }
|
||||
enforceIronLaw(): void { this.settings.highLayerControlHitBonus = 0.15; }
|
||||
enableFiveSoulsPenalty(): void { this.settings.lostAfterPenalty = true; }
|
||||
enableReturningBarrier(): void { this.settings.heartShield = true; }
|
||||
enableOneNameJudgment(): void { this.settings.oneNameOneJudgment = true; }
|
||||
@@ -141,4 +141,3 @@ export class DecreeBuildFacade {
|
||||
severSevenInches(): void { this.settings.finishPerLayerCoefficient = 0.25; }
|
||||
enableNameInYoudu(): void { this.settings.nameInYoudu = true; }
|
||||
}
|
||||
|
||||
@@ -31,6 +31,18 @@ function hasImminentHealing(context: AbilitySelectionContext): boolean {
|
||||
) ?? false;
|
||||
}
|
||||
|
||||
function hasImminentControlOrHealing(
|
||||
context: AbilitySelectionContext,
|
||||
): boolean {
|
||||
return context.opponent?.abilities.getAllAbilities().some(
|
||||
(ability) =>
|
||||
ability instanceof ActiveSkill &&
|
||||
ability.currentCooldown <= 1 &&
|
||||
(ability.selectionProfile?.intents?.includes('control') ||
|
||||
ability.selectionProfile?.intents?.includes('heal_hp')),
|
||||
) ?? false;
|
||||
}
|
||||
|
||||
abstract class YouduSelectionStrategy implements AbilitySelectionStrategy {
|
||||
constructor(protected readonly tacticId: SectTacticId) {}
|
||||
abstract select(context: AbilitySelectionContext): AbilitySelectionResult | null;
|
||||
@@ -106,9 +118,18 @@ export class YouduDecreeSelectionStrategy extends YouduSelectionStrategy {
|
||||
const erosion = layer(context);
|
||||
const hasShadow = hasBuff(context, YOUDU_SHADOW_REVEALED);
|
||||
const fire = context.caster.combatResources.getCurrent(YOUDU_SOUL_FIRE);
|
||||
const targetHp = context.opponent?.getHpPercent() ?? 1;
|
||||
|
||||
if (this.tacticId === 'pin-the-caster' && !hasShadow) {
|
||||
return this.pick(context, ['reveal-shadow', 'soul-severing-call'], 800);
|
||||
if (this.tacticId === 'pin-the-caster') {
|
||||
if (!hasShadow) {
|
||||
return this.pick(context, ['reveal-shadow', 'soul-severing-call'], 800);
|
||||
}
|
||||
if (hasImminentControlOrHealing(context)) {
|
||||
return this.pick(context, ['pin-soul', 'soul-severing-call'], 810);
|
||||
}
|
||||
if (erosion >= 4 && (fire >= 3 || targetHp < 0.45)) {
|
||||
return this.pick(context, ['soul-shall-not-return', 'pin-soul'], 820);
|
||||
}
|
||||
}
|
||||
if (this.tacticId === 'judge-at-four' && erosion >= 4) {
|
||||
return this.pick(context, ['soul-shall-not-return', 'pin-soul'], 820);
|
||||
|
||||
@@ -60,6 +60,10 @@ describe('幽都战斗与展示投影', () => {
|
||||
expect(YOUDU_SECT_PRESENTATION.map?.hotspots).toHaveLength(15);
|
||||
expect(YOUDU_SECT_PRESENTATION.map?.hotspots?.find((spot) => spot.id === 'formation'))
|
||||
.toMatchObject({ locked: true, facility: 'formation' });
|
||||
expect(YOUDU_SECT_PRESENTATION.facilityLabels).toMatchObject({
|
||||
alchemy: '还魂药庐',
|
||||
refinery: '镇铁炉',
|
||||
});
|
||||
});
|
||||
|
||||
it.each([YOUDU_TIDE_PATH_ID, YOUDU_DECREE_PATH_ID])(
|
||||
@@ -72,6 +76,9 @@ describe('幽都战斗与展示投影', () => {
|
||||
id: YOUDU_SOUL_FIRE, name: '魂火', icon: '◈', initial: 0, max: 3,
|
||||
}]);
|
||||
expect(projection.selectionStrategy).toBeDefined();
|
||||
expect(projection.abilities.map((ability) => ability.slug)).not.toContain(
|
||||
`sect.youdu.${pathId}-runtime`,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -128,6 +135,11 @@ describe('幽都战斗与展示投影', () => {
|
||||
realm: '化神',
|
||||
abilityId: 'youdu-runtime',
|
||||
}).detailRows.join(';');
|
||||
const slowPin = resolveSectAbility({
|
||||
sect: youduState(YOUDU_DECREE_PATH_ID, ['decree-four-gates-closed']),
|
||||
realm: '化神',
|
||||
abilityId: 'pin-soul',
|
||||
}).detailRows.join(';');
|
||||
|
||||
expect(sever).toContain('施法前至少3层蚀魂时魂伤提高70%');
|
||||
expect(sever).toContain('命中后增加2层蚀魂');
|
||||
@@ -136,12 +148,44 @@ describe('幽都战斗与展示投影', () => {
|
||||
expect(seize).toContain('术伤与魂伤各0.22 × 法术攻击');
|
||||
expect(pin).toContain('命中后增加2层蚀魂');
|
||||
expect(pin).toContain('本次控制命中提高15%');
|
||||
expect(pin).toContain('受到的气血治疗降低100%');
|
||||
expect(slowPin).toContain('目标速度降低20%');
|
||||
expect(heart).toContain('控制抗性 +40%');
|
||||
expect([sever, seize, pin].join(';')).not.toMatch(
|
||||
/佛相|sever-high|sever-low|pin-high|pin-low/,
|
||||
);
|
||||
});
|
||||
|
||||
it('终结与忘川详情完整展示节点条件,且不生成无条件或重复事实', () => {
|
||||
const finish = resolveSectAbility({
|
||||
sect: youduState(YOUDU_DECREE_PATH_ID, [
|
||||
'decree-one-name-one-judgment',
|
||||
'decree-name-in-youdu',
|
||||
]),
|
||||
realm: '化神',
|
||||
abilityId: 'soul-shall-not-return',
|
||||
}).detailRows.join(';');
|
||||
const forget = resolveSectAbility({
|
||||
sect: youduState(YOUDU_TIDE_PATH_ID, [
|
||||
'tide-black-water',
|
||||
'tide-no-return-current',
|
||||
]),
|
||||
realm: '化神',
|
||||
abilityId: 'forgetful-river-tide',
|
||||
}).detailRows.join(';');
|
||||
|
||||
expect(finish).toContain('目标至少4层蚀魂');
|
||||
expect(finish).toContain('受到的气血治疗降低80%');
|
||||
expect(finish).toContain('速度降低30%');
|
||||
expect(finish).toContain('目标每场首次进入4层时获得标记');
|
||||
expect(finish).toContain('目标气血低于20%');
|
||||
expect(finish).toContain('每场一次');
|
||||
expect(finish).not.toContain('伤害:相当于70%法攻');
|
||||
expect(finish).not.toContain('施展后:魂火:获得3点');
|
||||
expect(forget).toContain('忘川期间速度降低8%');
|
||||
expect(forget).toContain('至少4层蚀魂时持续魂伤额外提高30%');
|
||||
});
|
||||
|
||||
it('蚀魂详情展示完整五层曲线、逐层驱散且不出现零值占位', () => {
|
||||
const sigh = resolveSectAbility({
|
||||
sect: youduState(),
|
||||
|
||||
@@ -3,7 +3,9 @@ import { EventBus } from '@shared/engine/battle-v5/core/EventBus';
|
||||
import type {
|
||||
DamageRequestEvent,
|
||||
ResourceDrainEvent,
|
||||
UnitDeadEvent,
|
||||
} from '@shared/engine/battle-v5/core/events';
|
||||
import { EventPriorityLevel } from '@shared/engine/battle-v5/core/events';
|
||||
import {
|
||||
beginRuntimeAction,
|
||||
shouldTickBuffDuration,
|
||||
@@ -80,6 +82,36 @@ function setOverride(owner: Unit, attrType: AttributeType, value: number): void
|
||||
owner.updateDerivedStats();
|
||||
}
|
||||
|
||||
function addBuffImmunity(owner: Unit, tags: string[]): void {
|
||||
owner.abilities.addAbility(AbilityFactory.create({
|
||||
slug: `test.buff-immunity.${owner.id}.${tags.join('.')}`,
|
||||
name: '测试状态免疫',
|
||||
type: AbilityType.PASSIVE_SKILL,
|
||||
tags: [GameplayTags.ABILITY.KIND.PASSIVE],
|
||||
listeners: [{
|
||||
eventType: GameplayTags.EVENT.BUFF_ADD,
|
||||
scope: GameplayTags.SCOPE.OWNER_AS_TARGET,
|
||||
priority: 1_000,
|
||||
effects: [{ type: 'buff_immunity', params: { tags } }],
|
||||
}],
|
||||
}));
|
||||
}
|
||||
|
||||
function addDamageImmunity(owner: Unit, tags: string[]): void {
|
||||
owner.abilities.addAbility(AbilityFactory.create({
|
||||
slug: `test.damage-immunity.${owner.id}.${tags.join('.')}`,
|
||||
name: '测试伤害免疫',
|
||||
type: AbilityType.PASSIVE_SKILL,
|
||||
tags: [GameplayTags.ABILITY.KIND.PASSIVE],
|
||||
listeners: [{
|
||||
eventType: GameplayTags.EVENT.DAMAGE,
|
||||
scope: GameplayTags.SCOPE.OWNER_AS_TARGET,
|
||||
priority: EventPriorityLevel.DAMAGE_APPLY,
|
||||
effects: [{ type: 'damage_immunity', params: { tags } }],
|
||||
}],
|
||||
}));
|
||||
}
|
||||
|
||||
describe('幽都核心机制实际结算', () => {
|
||||
beforeEach(() => {
|
||||
EventBus.instance.reset();
|
||||
@@ -352,6 +384,119 @@ describe('幽都核心机制实际结算', () => {
|
||||
expect(youdu.buffs.getAllBuffIds()).toContain('test.control.no-skill');
|
||||
});
|
||||
|
||||
it('通用减益免疫阻止蚀魂与忘川,控制免疫只阻止失魂和镇魂钉', () => {
|
||||
const debuffCaster = unit('debuff-caster');
|
||||
const debuffTarget = unit('debuff-target');
|
||||
addBuffImmunity(debuffTarget, [GameplayTags.BUFF.TYPE.DEBUFF]);
|
||||
|
||||
ability('one-sigh').execute({ caster: debuffCaster, target: debuffTarget });
|
||||
ability('forgetful-river-tide').execute({
|
||||
caster: debuffCaster,
|
||||
target: debuffTarget,
|
||||
});
|
||||
|
||||
expect(erosion(debuffTarget)).toBeUndefined();
|
||||
expect(debuffTarget.buffs.getAllBuffIds()).not.toContain(
|
||||
YOUDU_FORGETFUL_RIVER,
|
||||
);
|
||||
|
||||
const controlCaster = unit('control-caster');
|
||||
const controlTarget = unit('control-target');
|
||||
addBuffImmunity(controlTarget, [GameplayTags.BUFF.TYPE.CONTROL]);
|
||||
setOverride(controlCaster, AttributeType.CONTROL_HIT, 1);
|
||||
setOverride(controlTarget, AttributeType.CONTROL_RESISTANCE, 0);
|
||||
ability('one-sigh').execute({ caster: controlCaster, target: controlTarget });
|
||||
ability('soul-severing-call').execute({
|
||||
caster: controlCaster,
|
||||
target: controlTarget,
|
||||
});
|
||||
ability('one-sigh').execute({ caster: controlCaster, target: controlTarget });
|
||||
expect(erosion(controlTarget)?.getLayer()).toBe(4);
|
||||
|
||||
ability('pin-soul').execute({ caster: controlCaster, target: controlTarget });
|
||||
|
||||
expect(erosion(controlTarget)).toBeDefined();
|
||||
expect(controlTarget.buffs.getAllBuffIds()).not.toContain(YOUDU_SOUL_LOST);
|
||||
expect(controlTarget.buffs.getAllBuffIds()).not.toContain(
|
||||
'sect.youdu.soul-pinning-nail',
|
||||
);
|
||||
});
|
||||
|
||||
it('先定其形按成功命中而非实际伤害触发,闪避不会消耗次数', () => {
|
||||
const system = new DamageSystem();
|
||||
const caster = unit('caster');
|
||||
const target = unit('target');
|
||||
const nodes = ['decree-fix-form-first'];
|
||||
installRuntime(caster, 'decree', nodes);
|
||||
ability('reveal-shadow', 'decree', nodes).execute({ caster, target });
|
||||
const sever = ability('soul-severing-call', 'decree', nodes);
|
||||
|
||||
EventBus.instance.publish({
|
||||
type: 'HitCheckEvent',
|
||||
timestamp: Date.now(),
|
||||
caster,
|
||||
target,
|
||||
ability: sever,
|
||||
isHit: false,
|
||||
isDodged: true,
|
||||
isResisted: false,
|
||||
});
|
||||
expect(erosion(target)).toBeUndefined();
|
||||
|
||||
EventBus.instance.publish({
|
||||
type: 'HitCheckEvent',
|
||||
timestamp: Date.now(),
|
||||
caster,
|
||||
target,
|
||||
ability: sever,
|
||||
isHit: true,
|
||||
isDodged: false,
|
||||
isResisted: false,
|
||||
});
|
||||
expect(erosion(target)?.getLayer()).toBe(1);
|
||||
|
||||
EventBus.instance.publish({
|
||||
type: 'HitCheckEvent',
|
||||
timestamp: Date.now(),
|
||||
caster,
|
||||
target,
|
||||
ability: sever,
|
||||
isHit: true,
|
||||
isDodged: false,
|
||||
isResisted: false,
|
||||
});
|
||||
expect(erosion(target)?.getLayer()).toBe(1);
|
||||
|
||||
addDamageImmunity(target, [GameplayTags.ABILITY.CHANNEL.TRUE]);
|
||||
const beforeHp = target.getCurrentHp();
|
||||
sever.execute({ caster, target });
|
||||
expect(target.getCurrentHp()).toBe(beforeHp);
|
||||
expect(erosion(target)?.getLayer()).toBe(3);
|
||||
system.destroy();
|
||||
});
|
||||
|
||||
it('幽都铁律同时提高五层失魂的控制命中', () => {
|
||||
const run = (nodes: string[]) => {
|
||||
const caster = unit(`caster-${nodes.length}`);
|
||||
const target = unit(`target-${nodes.length}`);
|
||||
setOverride(caster, AttributeType.CONTROL_HIT, 0);
|
||||
setOverride(target, AttributeType.CONTROL_RESISTANCE, 0.6);
|
||||
const sigh = ability('one-sigh', 'decree', nodes);
|
||||
for (let index = 0; index < 5; index += 1) {
|
||||
sigh.execute({ caster, target });
|
||||
}
|
||||
return target;
|
||||
};
|
||||
|
||||
const withoutNode = run([]);
|
||||
expect(withoutNode.buffs.getAllBuffIds()).not.toContain(YOUDU_SOUL_LOST);
|
||||
expect(erosion(withoutNode)?.getLayer()).toBe(3);
|
||||
|
||||
const withNode = run(['decree-iron-law']);
|
||||
expect(withNode.buffs.getAllBuffIds()).toContain(YOUDU_SOUL_LOST);
|
||||
expect(erosion(withNode)?.getLayer()).toBe(5);
|
||||
});
|
||||
|
||||
it('镇魂控制被抵抗时保留伤害与蚀魂,但不产生镇魂钉禁疗', () => {
|
||||
const caster = unit('caster');
|
||||
const target = unit('target');
|
||||
@@ -419,6 +564,26 @@ describe('幽都核心机制实际结算', () => {
|
||||
expect(erosion(target)?.getLayer()).toBe(2);
|
||||
});
|
||||
|
||||
it('混合伤害首段致死时只发布一次死亡事件', () => {
|
||||
const system = new DamageSystem();
|
||||
const caster = unit('caster');
|
||||
const target = unit('target');
|
||||
const deaths: UnitDeadEvent[] = [];
|
||||
target.setHp(1);
|
||||
EventBus.instance.subscribe<UnitDeadEvent>(
|
||||
'UnitDeadEvent',
|
||||
(event) => deaths.push(event),
|
||||
-1_000,
|
||||
);
|
||||
|
||||
ability('seize-soul').execute({ caster, target });
|
||||
|
||||
expect(target.isAlive()).toBe(false);
|
||||
expect(deaths).toHaveLength(1);
|
||||
expect(deaths[0]).toMatchObject({ unit: target, killer: caster });
|
||||
system.destroy();
|
||||
});
|
||||
|
||||
it('法术伤害回蓝只响应混合技能的术伤包,魂伤仍不可暴击与吸血', () => {
|
||||
const system = new DamageSystem();
|
||||
const caster = unit('caster');
|
||||
|
||||
@@ -10,10 +10,14 @@ import { describe, expect, it } from 'vitest';
|
||||
import { resolveSectAbility } from '../..';
|
||||
import {
|
||||
YOUDU_FORGETFUL_RIVER,
|
||||
YOUDU_SHADOW_REVEALED,
|
||||
YOUDU_SOUL_EROSION,
|
||||
YOUDU_SOUL_FIRE,
|
||||
} from '..';
|
||||
import { YouduTideSelectionStrategy } from '../strategy';
|
||||
import {
|
||||
YouduDecreeSelectionStrategy,
|
||||
YouduTideSelectionStrategy,
|
||||
} from '../strategy';
|
||||
import { youduState } from './testState';
|
||||
|
||||
function unit(id: string): Unit {
|
||||
@@ -49,6 +53,8 @@ function addTargetState(target: Unit, erosionLayers: number): void {
|
||||
function selectionContext(
|
||||
abilityIds: string[],
|
||||
erosionLayers: number,
|
||||
pathId: 'tide' | 'decree' = 'tide',
|
||||
withShadow = false,
|
||||
): AbilitySelectionContext {
|
||||
const caster = unit('caster');
|
||||
const opponent = unit('opponent');
|
||||
@@ -59,12 +65,21 @@ function selectionContext(
|
||||
max: 3,
|
||||
});
|
||||
addTargetState(opponent, erosionLayers);
|
||||
if (withShadow) {
|
||||
opponent.buffs.addBuff(BuffFactory.create({
|
||||
id: YOUDU_SHADOW_REVEALED,
|
||||
name: '照影',
|
||||
type: BuffType.DEBUFF,
|
||||
duration: 3,
|
||||
stackRule: StackRule.REFRESH_DURATION,
|
||||
}));
|
||||
}
|
||||
return {
|
||||
caster,
|
||||
opponent,
|
||||
candidates: abilityIds.map((abilityId, order) => ({
|
||||
ability: AbilityFactory.create(resolveSectAbility({
|
||||
sect: youduState('tide'),
|
||||
sect: youduState(pathId),
|
||||
realm: '化神',
|
||||
abilityId,
|
||||
}).config) as ActiveSkill,
|
||||
@@ -93,6 +108,34 @@ function addHealingAbility(opponent: Unit, cooldown: number): ActiveSkill {
|
||||
return heal;
|
||||
}
|
||||
|
||||
function addControlAbility(opponent: Unit, cooldown: number): ActiveSkill {
|
||||
const control = AbilityFactory.create({
|
||||
slug: `test.control.${cooldown}`,
|
||||
name: '测试控制',
|
||||
type: AbilityType.ACTIVE_SKILL,
|
||||
cooldown,
|
||||
targetPolicy: { team: 'enemy', scope: 'single' },
|
||||
selectionProfile: { intents: ['control'] },
|
||||
tags: [GameplayTags.ABILITY.FUNCTION.CONTROL],
|
||||
effects: [{
|
||||
type: 'apply_buff',
|
||||
params: {
|
||||
buffConfig: {
|
||||
id: `test.control.buff.${cooldown}`,
|
||||
name: '测试控制状态',
|
||||
type: BuffType.CONTROL,
|
||||
duration: 1,
|
||||
stackRule: StackRule.REFRESH_DURATION,
|
||||
tags: [GameplayTags.BUFF.TYPE.CONTROL],
|
||||
},
|
||||
},
|
||||
}],
|
||||
}) as ActiveSkill;
|
||||
opponent.abilities.addAbility(control);
|
||||
if (cooldown > 0) control.startCooldown();
|
||||
return control;
|
||||
}
|
||||
|
||||
describe('幽都自动战术', () => {
|
||||
it('溺疗只在敌方治疗冷却不超过1回合时优先镇魂', () => {
|
||||
const strategy = new YouduTideSelectionStrategy('healer-drown');
|
||||
@@ -130,4 +173,68 @@ describe('幽都自动战术', () => {
|
||||
'sect.youdu.soul-shall-not-return',
|
||||
);
|
||||
});
|
||||
|
||||
it('钉法者先补照影,并在敌方控制或治疗进入关键窗口时镇魂', () => {
|
||||
const strategy = new YouduDecreeSelectionStrategy('pin-the-caster');
|
||||
const withoutShadow = selectionContext(
|
||||
['reveal-shadow', 'soul-severing-call', 'pin-soul'],
|
||||
3,
|
||||
'decree',
|
||||
);
|
||||
expect(strategy.select(withoutShadow)?.ability.id).toBe(
|
||||
'sect.youdu.reveal-shadow',
|
||||
);
|
||||
|
||||
const context = selectionContext(
|
||||
['soul-severing-call', 'pin-soul'],
|
||||
3,
|
||||
'decree',
|
||||
true,
|
||||
);
|
||||
const control = addControlAbility(context.opponent!, 2);
|
||||
expect(strategy.select(context)?.ability.id).toBe(
|
||||
'sect.youdu.soul-severing-call',
|
||||
);
|
||||
control.tickCooldown();
|
||||
expect(strategy.select(context)?.ability.id).toBe('sect.youdu.pin-soul');
|
||||
});
|
||||
|
||||
it('钉法者在无关键窗口的四层击杀线优先终结', () => {
|
||||
const strategy = new YouduDecreeSelectionStrategy('pin-the-caster');
|
||||
const context = selectionContext(
|
||||
['pin-soul', 'soul-shall-not-return'],
|
||||
4,
|
||||
'decree',
|
||||
true,
|
||||
);
|
||||
context.caster.combatResources.set(YOUDU_SOUL_FIRE, 3);
|
||||
|
||||
expect(strategy.select(context)?.ability.id).toBe(
|
||||
'sect.youdu.soul-shall-not-return',
|
||||
);
|
||||
});
|
||||
|
||||
it('四层判决立即终结,取其第五则优先叠到五层', () => {
|
||||
const judge = new YouduDecreeSelectionStrategy('judge-at-four');
|
||||
const judgeContext = selectionContext(
|
||||
['soul-shall-not-return', 'pin-soul'],
|
||||
4,
|
||||
'decree',
|
||||
true,
|
||||
);
|
||||
expect(judge.select(judgeContext)?.ability.id).toBe(
|
||||
'sect.youdu.soul-shall-not-return',
|
||||
);
|
||||
|
||||
const fifth = new YouduDecreeSelectionStrategy('take-the-fifth');
|
||||
const fifthContext = selectionContext(
|
||||
['soul-severing-call', 'soul-shall-not-return'],
|
||||
4,
|
||||
'decree',
|
||||
true,
|
||||
);
|
||||
expect(fifth.select(fifthContext)?.ability.id).toBe(
|
||||
'sect.youdu.soul-severing-call',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -238,10 +238,11 @@ export class SectCompiler {
|
||||
const modifierFacts = (build.abilityPresentationModifiers ?? [])
|
||||
.filter((modifier) => modifier.abilityId === abilityId)
|
||||
.flatMap((modifier) => modifier.factRows);
|
||||
const authoredFacts = ability.detailRows ?? [];
|
||||
const detailRows = Array.from(
|
||||
new Set([
|
||||
...(ability.detailRows ?? []),
|
||||
...configFacts,
|
||||
...authoredFacts,
|
||||
...(authoredFacts.length === 0 ? configFacts : []),
|
||||
...modifierFacts,
|
||||
].filter(Boolean)),
|
||||
);
|
||||
|
||||
@@ -24,6 +24,7 @@ const ATTRIBUTE_LABELS: Partial<Record<AttributeType, string>> = {
|
||||
[AttributeType.SPEED]: '身法',
|
||||
[AttributeType.EVASION_RATE]: '闪避',
|
||||
[AttributeType.CONTROL_RESISTANCE]: '控制抗性',
|
||||
[AttributeType.HEAL_RECEIVED_REDUCTION]: '受治疗削弱',
|
||||
};
|
||||
|
||||
const DAMAGE_SOURCE_LABELS: Partial<Record<DamageSource, string>> = {
|
||||
@@ -87,6 +88,13 @@ function conditionText(
|
||||
) {
|
||||
return `目标状态层数${condition.type === 'buff_layer_at_least' ? '至少' : '少于'}${condition.params.value}层`;
|
||||
}
|
||||
if (
|
||||
(condition.type === 'hp_above' || condition.type === 'hp_below') &&
|
||||
condition.params.value !== undefined
|
||||
) {
|
||||
const subject = condition.params.scope === 'caster' ? '自身' : '目标';
|
||||
return `${subject}气血${condition.type === 'hp_below' ? '低于' : '高于'}${percent(condition.params.value)}`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -135,6 +143,9 @@ function modifierText(modifier: AttributeModifierConfig): string {
|
||||
if (modifier.type === ModifierType.ADD) {
|
||||
return `${label}${modifier.value >= 0 ? '+' : ''}${percent(modifier.value)}`;
|
||||
}
|
||||
if (modifier.attrType === AttributeType.HEAL_RECEIVED_REDUCTION) {
|
||||
return `${label}${modifier.value >= 0 ? '+' : ''}${percent(modifier.value)}`;
|
||||
}
|
||||
return `${label}${modifier.value >= 0 ? '+' : ''}${number(modifier.value)}`;
|
||||
}
|
||||
|
||||
@@ -160,6 +171,16 @@ function damageEffectsRow(
|
||||
if (damageEffects.length === 0) return [];
|
||||
|
||||
if (damageEffects.length === 1) {
|
||||
const layerScalar = damageEffects[0].params.buffLayerScalar;
|
||||
if (layerScalar) {
|
||||
const attribute = ATTRIBUTE_LABELS[layerScalar.attribute] ?? layerScalar.attribute;
|
||||
const range = layerScalar.maxLayers === undefined
|
||||
? `${layerScalar.minLayers ?? 0}层以上`
|
||||
: `${layerScalar.minLayers ?? 0}~${layerScalar.maxLayers}层`;
|
||||
return [
|
||||
`伤害:基础相当于${scalableValue(damageEffects[0].params.value)},每层目标状态追加${number(layerScalar.coefficientPerLayer * 100)}%${attribute}(${range})`,
|
||||
];
|
||||
}
|
||||
const dynamic = damageEffects[0].params.dynamicScalars?.find(
|
||||
(scalar) => scalar.source === 'target_missing_hp_ratio',
|
||||
);
|
||||
@@ -453,6 +474,8 @@ function describeEffect(
|
||||
for (const modifier of buff.modifiers ?? []) {
|
||||
if (modifier.scaleByLayer)
|
||||
rows.push(`每层:${modifierText(modifier)}`);
|
||||
else if (!modifier.valueByLayer)
|
||||
rows.push(`${buff.name}:${modifierText(modifier)}`);
|
||||
}
|
||||
rows.push(...formatBuffValueByLayerModifiers(buff.modifiers ?? []));
|
||||
if (buff.dispelMode === 'one_layer') {
|
||||
|
||||
@@ -197,6 +197,7 @@ export const GameplayTags = {
|
||||
ROUND_START: 'RoundStartEvent',
|
||||
SKILL_PRE_CAST: 'SkillPreCastEvent',
|
||||
SKILL_CAST: 'SkillCastEvent',
|
||||
HIT_CHECK: 'HitCheckEvent',
|
||||
DODGE: 'DodgeEvent',
|
||||
BUFF_ADD: 'BuffAddEvent',
|
||||
BUFF_APPLIED: 'BuffAppliedEvent',
|
||||
|
||||
Reference in new issue
Block a user