diff --git a/packages-user/data-state/test/enemyCombination.test.ts b/packages-user/data-state/test/enemyCombination.test.ts index 94a11f0..4b3d973 100644 --- a/packages-user/data-state/test/enemyCombination.test.ts +++ b/packages-user/data-state/test/enemyCombination.test.ts @@ -1,19 +1,27 @@ // 测试顶层伤害组合:单特殊属性 / 单光环基线,以及多特殊属性与系统层光环效果组合 import { describe, expect, it, vi } from 'vitest'; -import { type ITileLocator } from '@motajs/common'; +import { FullRange, logger, type ITileLocator } from '@motajs/common'; import { type IEnemyAttr, type IHeroAttr } from '@user/data-common'; import { Enemy, type IEnemy, + type IHeroAttribute, type IReadonlyHeroAttribute, type ISpecial } from '@user/data-base'; -import { EnemyContext, type IReadonlyEnemyHandler } from '@user/data-system'; +import { + EnemyContext, + type IAuraView, + type IEnemyCommonQueryEffect, + type IEnemyFinalEffect, + type IEnemySpecialQueryEffect, + type IReadonlyEnemyHandler +} from '@user/data-system'; import { createCoreState, type CoreState } from '../src/core'; import { MainDamageCalculator } from '../src/enemy/calculator'; import { CommonAuraConverter, GuardAuraConverter } from '../src/enemy/aura'; import { MainEnemyFinalEffect } from '../src/enemy/final'; -import { type IHaloValue } from '../src/enemy/special'; +import { type IHaloValue, type IVampireValue } from '../src/enemy/special'; vi.hoisted(() => { Map.prototype.getOrInsert ??= function ( @@ -108,7 +116,7 @@ function createEnemy(options: IEnemyOptions = {}): IEnemy { */ function createHero( overrides: Partial = {} -): IReadonlyHeroAttribute { +): IHeroAttribute { const values: IHeroAttr = { name: 'hero', hp: 100, @@ -127,7 +135,7 @@ function createHero( getFinalAttribute: (name: string) => (values as never)[name], getModifiableClone: () => hero }; - return hero as unknown as IReadonlyHeroAttribute; + return hero as unknown as IHeroAttribute; } /** @@ -265,3 +273,325 @@ describe('enemy combination stage 1 - component baselines', () => { ); }); }); + +/** + * 组装一个使用指定上下文(而非顶层默认上下文)的只读伤害信息对象 + * @param context 使用的怪物上下文 + * @param enemy 怪物对象 + * @param hero 勇士属性对象 + * @param locator 怪物定位符 + */ +function createContextHandler( + context: EnemyContext, + enemy: IEnemy, + hero: IReadonlyHeroAttribute, + locator: ITileLocator = { x: 0, y: 0 } +): IReadonlyEnemyHandler { + return { + enemy, + context, + locator, + hero, + state: context.state + } as IReadonlyEnemyHandler; +} + +/** + * 创建一个按优先级顺序记录自身施加、并给目标加攻击力的全局光环 + * @param priority 光环优先级 + * @param tag 记录到顺序数组的标记 + * @param order 记录施加顺序的数组 + */ +function createOrderAura( + priority: number, + tag: string, + order: string[] +): IAuraView { + return { + priority, + range: new FullRange(), + couldApplyBase: true, + couldApplySpecial: false, + getRangeParam: () => undefined, + apply: handler => { + order.push(tag); + handler.enemy.addAttribute('atk', 1); + }, + applySpecial: () => null + }; +} + +/** + * 创建一个只施加特殊属性(不修改基础属性)的全局光环 + * @param priority 光环优先级 + * @param code 要施加的特殊属性代码 + */ +function createSpecialAura( + priority: number, + code: number +): IAuraView { + return { + priority, + range: new FullRange(), + couldApplyBase: false, + couldApplySpecial: true, + getRangeParam: () => undefined, + apply: () => {}, + applySpecial: () => ({ + add: () => [createSpecial(code, undefined)], + delete: () => [], + modify: () => false + }) + }; +} + +describe('enemy combination stage 2 - multi-special and system pipelines', () => { + // 验证多个顶层特殊属性同时存在时按真实计算器组合出确定的伤害与回合数 + it('combines multiple top-level specials into one damage result', () => { + const state = createCoreState(); + state.enemyManager.addPrefab( + createEnemy({ + id: 'combo-a', + code: 1, + specials: [ + createSpecial(3, undefined), + createSpecial(4, undefined), + createSpecial(5, undefined), + createSpecial(22, 7) + ] + }) + ); + state.enemyManager.addPrefab( + createEnemy({ + id: 'combo-b', + code: 2, + specials: [ + createSpecial(2, undefined), + createSpecial(11, { + vampire: 10, + add: true + }), + createSpecial(7, 100), + createSpecial(8, 50) + ] + }) + ); + const solidCombo = state.enemyManager.createEnemy(1)!; + const vampireCombo = state.enemyManager.createEnemy(2)!; + const calculator = new MainDamageCalculator(); + + const first = calculator.calculate( + createHandler(state, solidCombo, createHero()) + ); + const second = calculator.calculate( + createHandler(state, vampireCombo, createHero({ hp: 200 })) + ); + + expect(first).toEqual({ damage: 25, turn: 2 }); + expect(second).toEqual({ damage: 61, turn: 3 }); + expect( + calculator.getCriticalLimit( + createHandler(state, solidCombo, createHero()), + 'atk' + ) + ).toBe(Infinity); + }); + + // 验证真实支援光环写入的支援坐标经计算器递归累加支援怪的回合与伤害 + it('recurses into a guard enemy written by the real guard aura', () => { + const state = createCoreState(); + const hero = createHero(); + const context = createContext(state, hero); + const guardSource = createEnemy({ + id: 'guard-source', + code: 26, + specials: [createSpecial(26, undefined)] + }); + const guardTarget = createEnemy({ id: 'guard-target', code: 1 }); + context.setEnemyAt({ x: 1, y: 0 }, guardSource); + context.setEnemyAt({ x: 0, y: 0 }, guardTarget); + context.buildup(); + + const targetView = context.getEnemyByLocator({ x: 0, y: 0 })!; + const target = targetView.getComputedEnemy(); + expect(target.getAttribute('guard').size).toBe(1); + + const info = new MainDamageCalculator().calculate( + createContextHandler(context, target as IEnemy, hero, { + x: 0, + y: 0 + }) + ); + + expect(info).toEqual({ damage: 12, turn: 4 }); + }); + + // 验证支援坐标处没有怪物时告警 137 并跳过该支援,不改变基础伤害 + it('warns 137 when a guard locator has no enemy', () => { + const state = createCoreState(); + const hero = createHero(); + const enemy = createEnemy({ + id: 'missing-guard', + code: 1, + attrs: { guard: new Set([{ x: 5, y: 5 }]) } + }); + + const result = logger.catch(() => + new MainDamageCalculator().calculate( + createHandler(state, enemy, hero) + ) + ); + + expect(result.ret).toEqual({ damage: 3, turn: 2 }); + expect(result.info.map(item => item.code)).toContain(137); + }); + + // 验证光环基础效果先于常规查询效果执行,查询阶段能看到光环加成后的属性 + it('runs the aura base stage before the common query stage', () => { + const state = createCoreState(); + const context = createContext(state); + const enemy = createEnemy({ + id: 'query-target', + code: 25, + specials: [ + createSpecial(25, { + haloRange: 0, + haloSquare: false, + hpBuff: 0, + atkBuff: 50, + defBuff: 0 + }) + ] + }); + context.setEnemyAt({ x: 0, y: 0 }, enemy); + const observed: number[] = []; + const effect: IEnemyCommonQueryEffect = { + priority: 5, + apply: (handler, _special, query) => { + query(); + observed.push(handler.enemy.getAttribute('atk')); + handler.enemy.addAttribute('atk', 1); + } + }; + context.registerCommonQueryEffect(25, effect); + + context.buildup(); + + const view = context.getEnemyByLocator({ x: 0, y: 0 })!; + expect(observed).toEqual([12]); + expect(view.getComputedEnemy().getAttribute('atk')).toBe(13); + }); + + // 验证同优先级链上高优先级全局光环先于低优先级施加 + it('applies higher-priority global auras first', () => { + const state = createCoreState(); + const context = createContext(state); + const enemy = createEnemy({ id: 'priority-target', code: 1 }); + context.setEnemyAt({ x: 0, y: 0 }, enemy); + const order: string[] = []; + context.addAura(createOrderAura(40, 'p40', order)); + context.addAura(createOrderAura(30, 'p30', order)); + + context.buildup(); + + const view = context.getEnemyByLocator({ x: 0, y: 0 })!; + expect(order).toEqual(['p40', 'p30']); + expect(view.getComputedEnemy().getAttribute('atk')).toBe(10); + }); + + // 验证最终效果阶段按优先级降序执行,真实最终效果在自定义效果之后生效 + it('runs final effects in descending priority before the real effect', () => { + const state = createCoreState(); + const context = createContext(state); + const enemy = createEnemy({ + id: 'final-target', + code: 3, + specials: [createSpecial(3, undefined)] + }); + context.setEnemyAt({ x: 0, y: 0 }, enemy); + const order: string[] = []; + const high: IEnemyFinalEffect = { + priority: 10, + apply: handler => { + order.push('p10'); + handler.enemy.addAttribute('atk', 1); + } + }; + const low: IEnemyFinalEffect = { + priority: 5, + apply: handler => { + order.push('p5'); + handler.enemy.addAttribute('atk', 1); + } + }; + context.registerFinalEffect(high); + context.registerFinalEffect(low); + + context.buildup(); + + const view = context.getEnemyByLocator({ x: 0, y: 0 })!; + expect(order).toEqual(['p10', 'p5']); + expect(view.getComputedEnemy().getAttribute('atk')).toBe(10); + expect(view.getComputedEnemy().getAttribute('def')).toBe(19); + }); + + // 验证高优先级光环施加的特殊属性可被更低优先级的特殊查询效果观测 + it('feeds a special query effect from a higher-priority aura special', () => { + const state = createCoreState(); + const context = createContext(state); + const enemy = createEnemy({ id: 'special-target', code: 1 }); + context.setEnemyAt({ x: 0, y: 0 }, enemy); + context.addAura(createSpecialAura(30, 99)); + let queried = false; + const effect: IEnemySpecialQueryEffect = { + priority: 20, + for: () => ({ + shouldQuery: handler => handler.enemy.hasSpecial(99), + add: () => [], + delete: () => [], + modify: () => { + queried = true; + return true; + } + }) + }; + context.registerSpecialQueryEffect(effect); + + context.buildup(); + + const view = context.getEnemyByLocator({ x: 0, y: 0 })!; + expect(queried).toBe(true); + expect(view.getComputedEnemy().hasSpecial(99)).toBe(true); + }); + + // 验证属性流水线结果经缓存的伤害系统刷新,markDirty/deleteEnemy/with 行为正确 + it('links the aura pipeline result to the cached damage system', () => { + const state = createCoreState(); + const enemy = createEnemy({ id: 'link-target', code: 1 }); + state.enemyManager.addPrefab(enemy); + state.enemyContext.setEnemyAt({ x: 0, y: 0 }, enemy); + state.enemyContext.buildup(); + + const system = state.enemyContext.getDamageSystem()!; + const view = state.enemyContext.getEnemyByLocator({ x: 0, y: 0 })!; + const first = system.getDamageInfo(view); + expect(system.getDamageInfo(view)).toBe(first); + expect(first?.turn).toBe(0); + + system.markDirty(view); + const afterDirty = system.getDamageInfo(view); + expect(afterDirty).not.toBe(first); + + system.deleteEnemy(view); + expect(system.getDamageInfo(view)).not.toBe(afterDirty); + + const hero = createHero({ atk: 20 }); + const scoped = system.with(hero); + expect(scoped).not.toBe(system); + const info = scoped.getDamageInfoByHandler( + createHandler(state, createEnemy({ code: 7 }), hero) + ); + expect(info?.damage).toBe(3); + expect(info?.turn).toBe(2); + }); +});