diff --git a/src/shared/engine/sect/content/tianyan/base/TianyanBaseCompiler.ts b/src/shared/engine/sect/content/tianyan/base/TianyanBaseCompiler.ts index 8df4811a..3ee76cdd 100644 --- a/src/shared/engine/sect/content/tianyan/base/TianyanBaseCompiler.ts +++ b/src/shared/engine/sect/content/tianyan/base/TianyanBaseCompiler.ts @@ -42,6 +42,7 @@ import { TIANYAN_SECRET_ART, TIANYAN_SECT_ID, TIANYAN_SHATTER_COOLDOWN, + TIANYAN_STRATEGY_ELEMENT_HISTORY, TIANYAN_TECHNIQUE, type TianyanLandingAbilityId, } from '../ids'; @@ -52,6 +53,7 @@ import { import { TIANYAN_ELEMENTS, TIANYAN_ELEMENT_ABILITY_TAGS, + TIANYAN_LANDING_BASE_DAMAGE, TIANYAN_ELEMENT_NAMES, getTianyanReaction, nextGeneratingElement, @@ -884,7 +886,7 @@ function commonReactionPrelude( effects.push({ type: 'element_history', params: { - key: 'sect.tianyan.strategy.recent-elements', + key: TIANYAN_STRATEGY_ELEMENT_HISTORY, threshold: 3, effects: [], }, @@ -1232,7 +1234,7 @@ function compileLandingAbility( mismatchImmunityTag, ], selectionProfile: { intents: ['damage'] }, - detailRows: [ + notes: [ `落印术·${TIANYAN_ELEMENT_NAMES[spec.element]}`, `法印持续:${settings.sealDuration}回合`, ], @@ -1331,6 +1333,7 @@ function compileUtilityAbilities( ], extraTags: [innerArtTag, TIANYAN_ELEMENT_ABILITY_TAGS.wood], selectionProfile: { intents: ['heal_hp'] }, + notes: ['内景法·木'], }), ); @@ -1367,6 +1370,7 @@ function compileUtilityAbilities( ], extraTags: [innerArtTag, TIANYAN_ELEMENT_ABILITY_TAGS.fire], selectionProfile: { intents: ['buff'] }, + notes: ['内景法·火'], }), ); @@ -1395,6 +1399,7 @@ function compileUtilityAbilities( ], extraTags: [innerArtTag, TIANYAN_ELEMENT_ABILITY_TAGS.earth], selectionProfile: { intents: ['defensive'] }, + notes: ['内景法·土'], }), ); @@ -1429,6 +1434,7 @@ function compileUtilityAbilities( ], extraTags: [innerArtTag, TIANYAN_ELEMENT_ABILITY_TAGS.water], selectionProfile: { intents: ['restore_mp', 'buff'] }, + notes: ['内景法·水'], }), ); } @@ -1487,6 +1493,7 @@ function compileSecrets( })), extraTags: [secretArtTag], selectionProfile: { intents: ['buff'] }, + notes: ['天衍秘法'], }), ); @@ -1546,6 +1553,7 @@ function compileSecrets( })), extraTags: [secretArtTag], selectionProfile: { intents: ['buff', 'heal_hp', 'restore_mp'] }, + notes: ['天衍秘法'], }), ); } @@ -1777,7 +1785,7 @@ const LANDING_SPECS: LandingSpec[] = [ { id: 'verdant-pulse', element: 'wood', - coefficient: 0.68, + coefficient: TIANYAN_LANDING_BASE_DAMAGE['verdant-pulse'], baseEffects: (_reaction, settings) => [ healHp( 0.03 * settings.woodHealingMultiplier, @@ -1789,7 +1797,7 @@ const LANDING_SPECS: LandingSpec[] = [ { id: 'flowing-flame', element: 'fire', - coefficient: 0.82, + coefficient: TIANYAN_LANDING_BASE_DAMAGE['flowing-flame'], baseEffects: () => [ targetBuff(periodicDamageBuff(TIANYAN_BURN, '灼烧', 0.16, 'fire')), ], @@ -1797,7 +1805,7 @@ const LANDING_SPECS: LandingSpec[] = [ { id: 'earth-bearing-seal', element: 'earth', - coefficient: 0.62, + coefficient: TIANYAN_LANDING_BASE_DAMAGE['earth-bearing-seal'], baseEffects: (_reaction, settings) => [ shieldMagic(0.32, undefined, settings.loadoutMultiplier), ], @@ -1805,7 +1813,7 @@ const LANDING_SPECS: LandingSpec[] = [ { id: 'metal-cloud-cutter', element: 'metal', - coefficient: 0.88, + coefficient: TIANYAN_LANDING_BASE_DAMAGE['metal-cloud-cutter'], baseEffects: () => [ targetBuff( buff('sect.tianyan.metal-cut', '破锋', BuffType.DEBUFF, 2, { @@ -1822,7 +1830,7 @@ const LANDING_SPECS: LandingSpec[] = [ { id: 'white-star-breaker', element: 'metal', - coefficient: 0.50, + coefficient: TIANYAN_LANDING_BASE_DAMAGE['white-star-breaker'], baseEffects: () => [ { type: 'dispel', params: { maxCount: 1, status: 'positive' } }, ], @@ -1830,7 +1838,7 @@ const LANDING_SPECS: LandingSpec[] = [ { id: 'dark-water-return', element: 'water', - coefficient: 0.72, + coefficient: TIANYAN_LANDING_BASE_DAMAGE['dark-water-return'], baseEffects: (reaction, settings) => { if (reaction?.id !== 'cold-spring') { return [ diff --git a/src/shared/engine/sect/content/tianyan/ids.ts b/src/shared/engine/sect/content/tianyan/ids.ts index ce485001..4c38659d 100644 --- a/src/shared/engine/sect/content/tianyan/ids.ts +++ b/src/shared/engine/sect/content/tianyan/ids.ts @@ -53,6 +53,8 @@ export const TIANYAN_CHAIN_CONTROL = 'sect.tianyan.chain-control'; export const TIANYAN_FIRST_CHANGE = 'sect.tianyan.first-change'; export const TIANYAN_SHATTER_COOLDOWN = 'sect.tianyan.shatter-cooldown'; export const TIANYAN_DISPEL_TRUTH_COOLDOWN = 'sect.tianyan.dispel-truth-cooldown'; +export const TIANYAN_STRATEGY_ELEMENT_HISTORY = + 'sect.tianyan.strategy.recent-elements'; export const TIANYAN_TECHNIQUE = 'landing-technique'; export const TIANYAN_INNER_ART = 'inner-art'; diff --git a/src/shared/engine/sect/content/tianyan/shared/reactions.ts b/src/shared/engine/sect/content/tianyan/shared/reactions.ts index 88338a66..688fc9cf 100644 --- a/src/shared/engine/sect/content/tianyan/shared/reactions.ts +++ b/src/shared/engine/sect/content/tianyan/shared/reactions.ts @@ -1,4 +1,8 @@ import { GameplayTags } from '@shared/engine/shared/tag-domain'; +import { + TIANYAN_SECT_ID, + type TianyanLandingAbilityId, +} from '../ids'; export const TIANYAN_ELEMENTS = [ 'wood', @@ -51,6 +55,18 @@ export const TIANYAN_ELEMENT_ABILITY_TAGS: Record = { water: GameplayTags.ABILITY.ELEMENT.WATER, }; +export const TIANYAN_LANDING_BASE_DAMAGE: Record< + TianyanLandingAbilityId, + number +> = { + 'verdant-pulse': 0.68, + 'flowing-flame': 0.82, + 'earth-bearing-seal': 0.62, + 'metal-cloud-cutter': 0.88, + 'white-star-breaker': 0.50, + 'dark-water-return': 0.72, +}; + export const TIANYAN_ELEMENT_BUFF_TAGS: Record = { wood: GameplayTags.BUFF.ELEMENT.WOOD, fire: GameplayTags.BUFF.ELEMENT.FIRE, @@ -60,15 +76,15 @@ export const TIANYAN_ELEMENT_BUFF_TAGS: Record = { }; export const TIANYAN_SEAL_STATE_TAGS: Record = { - wood: GameplayTags.STATUS.SECT.state('tianyan', 'ElementSeal.Wood'), - fire: GameplayTags.STATUS.SECT.state('tianyan', 'ElementSeal.Fire'), - earth: GameplayTags.STATUS.SECT.state('tianyan', 'ElementSeal.Earth'), - metal: GameplayTags.STATUS.SECT.state('tianyan', 'ElementSeal.Metal'), - water: GameplayTags.STATUS.SECT.state('tianyan', 'ElementSeal.Water'), + wood: GameplayTags.STATUS.SECT.state(TIANYAN_SECT_ID, 'ElementSeal.Wood'), + fire: GameplayTags.STATUS.SECT.state(TIANYAN_SECT_ID, 'ElementSeal.Fire'), + earth: GameplayTags.STATUS.SECT.state(TIANYAN_SECT_ID, 'ElementSeal.Earth'), + metal: GameplayTags.STATUS.SECT.state(TIANYAN_SECT_ID, 'ElementSeal.Metal'), + water: GameplayTags.STATUS.SECT.state(TIANYAN_SECT_ID, 'ElementSeal.Water'), }; export const TIANYAN_ANY_SEAL_STATE_TAG = GameplayTags.STATUS.SECT.state( - 'tianyan', + TIANYAN_SECT_ID, 'ElementSeal', ); diff --git a/src/shared/engine/sect/content/tianyan/strategy.ts b/src/shared/engine/sect/content/tianyan/strategy.ts index 06679861..83aa06a4 100644 --- a/src/shared/engine/sect/content/tianyan/strategy.ts +++ b/src/shared/engine/sect/content/tianyan/strategy.ts @@ -7,9 +7,14 @@ import { BuffType } from '@shared/engine/battle-v5/core/types'; import { readElementHistory } from '@shared/engine/battle-v5/core/runtimeState'; import { GameplayTags } from '@shared/engine/shared/tag-domain'; import { SectStrategyCandidates, type SectTacticId } from '../../core'; -import { TIANYAN_SECT_ID } from './ids'; import { + TIANYAN_SECT_ID, + TIANYAN_STRATEGY_ELEMENT_HISTORY, +} from './ids'; +import { + TIANYAN_ELEMENT_ABILITY_TAGS, TIANYAN_ELEMENTS, + TIANYAN_LANDING_BASE_DAMAGE, TIANYAN_SEAL_STATE_TAGS, getTianyanReaction, type TianyanElement, @@ -24,16 +29,6 @@ const ABILITY_ELEMENTS: Partial> = { 'dark-water-return': 'water', }; -const STRATEGY_ELEMENT_HISTORY = 'sect.tianyan.strategy.recent-elements'; - -const ELEMENT_TAGS: Record = { - wood: GameplayTags.ABILITY.ELEMENT.WOOD, - fire: GameplayTags.ABILITY.ELEMENT.FIRE, - earth: GameplayTags.ABILITY.ELEMENT.EARTH, - metal: GameplayTags.ABILITY.ELEMENT.METAL, - water: GameplayTags.ABILITY.ELEMENT.WATER, -}; - function contentAbilityId(runtimeId: string): string { const prefix = `sect.${TIANYAN_SECT_ID}.`; return runtimeId.startsWith(prefix) ? runtimeId.slice(prefix.length) : runtimeId; @@ -77,6 +72,21 @@ function abilityIdsByReaction( .filter((id): id is string => Boolean(id)); } +function expectedReactionDamage( + abilityId: string, + seal: TianyanElement, +): number { + const element = ABILITY_ELEMENTS[abilityId]; + const base = TIANYAN_LANDING_BASE_DAMAGE[ + abilityId as keyof typeof TIANYAN_LANDING_BASE_DAMAGE + ]; + if (!element || base === undefined) return 0; + const reaction = getTianyanReaction(seal, element); + return base * ( + 1 + (reaction.mainDamageBonus ?? reaction.followUpRatio ?? 0) + ); +} + abstract class TianyanSelectionStrategy implements AbilitySelectionStrategy { constructor(protected readonly tacticId: SectTacticId) {} @@ -164,11 +174,13 @@ export class HetuSelectionStrategy extends TianyanSelectionStrategy { if (reactions.length > 0) { const recentElements = readElementHistory( context.caster, - STRATEGY_ELEMENT_HISTORY, + TIANYAN_STRATEGY_ELEMENT_HISTORY, ); const missingElementReactions = reactions.filter((id) => { const element = ABILITY_ELEMENTS[id]; - return element ? !recentElements.has(ELEMENT_TAGS[element]) : false; + return element + ? !recentElements.has(TIANYAN_ELEMENT_ABILITY_TAGS[element]) + : false; }); if (missingElementReactions.length > 0) { return this.pick(context, missingElementReactions, 740); @@ -204,7 +216,13 @@ export class LuoshuSelectionStrategy extends TianyanSelectionStrategy { id === 'earth-bearing-seal', ); if (!controlImmune && lock.length > 0) return this.pick(context, lock, 780); - if (currentSeal(context)) { + const hasControlLanding = context.candidates.some((candidate) => { + const id = contentAbilityId(candidate.ability.id); + return id === 'earth-bearing-seal' || + id === 'metal-cloud-cutter' || + id === 'white-star-breaker'; + }); + if (currentSeal(context) && hasControlLanding) { return this.pick(context, ['shift-palace', ...anyReaction], 720); } } @@ -224,17 +242,22 @@ export class LuoshuSelectionStrategy extends TianyanSelectionStrategy { } if (this.tacticId === 'decisive-derivation') { if (anyReaction.length > 0) { - const expectedDamageOrder = [ - 'metal-cloud-cutter', - 'flowing-flame', - 'dark-water-return', - 'earth-bearing-seal', - 'verdant-pulse', - 'white-star-breaker', - ].filter((id) => anyReaction.includes(id)); + const seal = currentSeal(context)!; + const expectedDamageOrder = anyReaction + .map((id, order) => ({ + id, + order, + damage: expectedReactionDamage(id, seal), + })) + .sort((left, right) => + right.damage - left.damage || + left.order - right.order || + left.id.localeCompare(right.id), + ) + .map(({ id }) => id); return this.pick( context, - [...expectedDamageOrder, ...anyReaction], + expectedDamageOrder, 760, ); } diff --git a/src/shared/engine/sect/content/tianyan/tests/combatProjection.test.ts b/src/shared/engine/sect/content/tianyan/tests/combatProjection.test.ts index 2205cb6c..bce973e6 100644 --- a/src/shared/engine/sect/content/tianyan/tests/combatProjection.test.ts +++ b/src/shared/engine/sect/content/tianyan/tests/combatProjection.test.ts @@ -203,4 +203,18 @@ describe('天衍圣地战斗投影', () => { ability.id as (typeof TIANYAN_VISIBLE_ABILITY_IDS)[number], ))).toHaveLength(13); }); + + it('技能详情区分落印术、内景法与天衍秘法', () => { + const sect = tianyanState(TIANYAN_HETU_PATH_ID); + const notes = (abilityId: string) => + resolveSectAbility({ sect, realm: '化神', abilityId }).notes; + + expect(notes('verdant-pulse')).toContain('落印术·木'); + expect(notes('myriad-wood-renewal')).toContain('内景法·木'); + expect(notes('lotus-in-fire')).toContain('内景法·火'); + expect(notes('boundless-earth')).toContain('内景法·土'); + expect(notes('heavenly-river-cleansing')).toContain('内景法·水'); + expect(notes('shift-palace')).toContain('天衍秘法'); + expect(notes('five-qi-repository')).toContain('天衍秘法'); + }); }); diff --git a/src/shared/engine/sect/content/tianyan/tests/selectionStrategy.test.ts b/src/shared/engine/sect/content/tianyan/tests/selectionStrategy.test.ts index 4da921b0..949ea490 100644 --- a/src/shared/engine/sect/content/tianyan/tests/selectionStrategy.test.ts +++ b/src/shared/engine/sect/content/tianyan/tests/selectionStrategy.test.ts @@ -11,6 +11,7 @@ import { resolveSectAbility } from '../..'; import { TIANYAN_HETU_PATH_ID, TIANYAN_LUOSHU_PATH_ID, + TIANYAN_STRATEGY_ELEMENT_HISTORY, } from '../ids'; import { TIANYAN_HETU_PATH_MODULE, @@ -107,7 +108,7 @@ describe('天衍六套自动战术', () => { ); rememberElement( battle.caster, - 'sect.tianyan.strategy.recent-elements', + TIANYAN_STRATEGY_ELEMENT_HISTORY, GameplayTags.ABILITY.ELEMENT.WATER, ); @@ -211,6 +212,23 @@ describe('天衍六套自动战术', () => { expect(selectedId(result)).toBe('shift-palace'); }); + it('锁机未装配土金落印术时不为空转法印,改用当前合法反应', () => { + const battle = context(TIANYAN_LUOSHU_PATH_ID, [ + 'shift-palace', + 'flowing-flame', + ]); + battle.opponent.buffs.addBuff( + BuffFactory.create(createElementSeal('wood', 2)), + battle.caster, + ); + + const result = TIANYAN_LUOSHU_PATH_MODULE + .createSelectionStrategy('lock-meridian') + .select(battle); + + expect(selectedId(result)).toBe('flowing-flame'); + }); + it('断局只在实际可触发的反应中比较伤害,不误选无反应技能', () => { const battle = context(TIANYAN_LUOSHU_PATH_ID, [ 'metal-cloud-cutter', @@ -227,4 +245,21 @@ describe('天衍六套自动战术', () => { expect(selectedId(result)).toBe('earth-bearing-seal'); }); + + it('断局在水印下选择预期伤害更高的滋荣而非泥沼', () => { + const battle = context(TIANYAN_LUOSHU_PATH_ID, [ + 'earth-bearing-seal', + 'verdant-pulse', + ]); + battle.opponent.buffs.addBuff( + BuffFactory.create(createElementSeal('water', 2)), + battle.caster, + ); + + const result = TIANYAN_LUOSHU_PATH_MODULE + .createSelectionStrategy('decisive-derivation') + .select(battle); + + expect(selectedId(result)).toBe('verdant-pulse'); + }); });