diff --git a/src/arena/Constuructors/PhysConstructor.js b/src/arena/Constuructors/PhysConstructor.js index f869559d..eddb602f 100644 --- a/src/arena/Constuructors/PhysConstructor.js +++ b/src/arena/Constuructors/PhysConstructor.js @@ -1,6 +1,8 @@ const { floatNumber } = require('../../utils/floatNumber'); const { default: arena } = require('../index'); const MiscService = require('../MiscService'); +const { dodge, parry, disarm } = require('../skills'); +const { isFailResult } = require('./utils'); /** * @typedef {import ('../GameService').default} game @@ -13,7 +15,10 @@ const MiscService = require('../MiscService'); * @todo Сейчас при отсутствие защиты на цели, не учитывается статик протект( * ???) Т.е если цель не защищается атака по ней на 95% удачна * */ -class PhysConstructor { +class PhysConstructor { /** + * @type {import('./PreAffect').PreAffect[]} + * */ + preAffects; /** * Конструктор атаки * @param {atkAct} atkAct имя actions @@ -23,14 +28,20 @@ class PhysConstructor { * @property {String} desc * @property {Number} lvl * @property {String} orderType + * + * @param {import('./PreAffect').PreAffect[]} preAffects */ - constructor(atkAct) { + constructor(atkAct, preAffects = [dodge, parry, disarm]) { this.name = atkAct.name; this.displayName = atkAct.displayName; this.desc = atkAct.desc; this.lvl = atkAct.lvl; this.orderType = atkAct.orderType; this.status = { hit: 0, exp: 0 }; + /** + * @type {import('./PreAffect').PreAffect[]} + * */ + this.preAffects = preAffects; } /** @@ -63,34 +74,15 @@ class PhysConstructor { * Проверка флагов влияющих на физический урон */ checkPreAffects() { - const { initiator, target, game } = this.params; - const iDex = initiator.stats.val('dex'); - // Глобальная проверка не весит ли затмение на арене - if (game.flags.global.isEclipsed) throw this.breaks('ECLIPSE'); - const weapon = arena.items[initiator.weapon.code]; - const hasDodgeableItems = MiscService.weaponTypes[weapon.wtype].dodge; - // Проверяем увёртку - if (target.flags.isDodging && hasDodgeableItems) { - /** @todo возможно следует состряпать static функцию tryDodge внутри скила - * уворота которая будет выполнять весь расчет а возвращать только bool - * значение. Сейчас эти проверки сильно раздувают PhysConstructor - */ - // проверяем имеет ли цель достаточно dex для того что бы уклониться + if (this.game.flags.global.isEclipsed) throw this.breaks('ECLIPSE'); - const at = floatNumber(Math.round(target.flags.isDodging / iDex)); - console.log('Dodging: ', at); - const r = MiscService.rndm('1d100'); - const c = Math.round(Math.sqrt(at) + (10 * at) + 5); - console.log('left:', c, ' right:', r, ' result:', c > r); - if (c > r) throw this.breaks('DODGED'); - } - if (target.flags.isParry) { - if (+(target.flags.isParry - iDex) > 0) { - throw this.breaks('PARRYED'); - } else { - target.flags.isParry -= +iDex; + this.preAffects.forEach((preAffect) => { + const result = preAffect.check(this.params); + + if (result && isFailResult(result)) { + throw this.breaks(result.message); } - } + }); } /** @@ -99,7 +91,6 @@ class PhysConstructor { fitsCheck() { const { initiator } = this.params; if (!initiator.weapon) throw this.breaks('NO_WEAPON'); - if (initiator.flags.isDisarmed) throw this.breaks('DISARM'); } /** diff --git a/src/arena/Constuructors/PreAffect.ts b/src/arena/Constuructors/PreAffect.ts new file mode 100644 index 00000000..bd5d2c2f --- /dev/null +++ b/src/arena/Constuructors/PreAffect.ts @@ -0,0 +1,7 @@ +import type GameService from '../GameService'; +import type { Player } from '../PlayersService'; +import type { Breaks } from './types'; + +export interface PreAffect { + check(params: { initiator: Player, target: Player, game: GameService}): Breaks | void +} diff --git a/src/arena/Constuructors/SkillConstructor.ts b/src/arena/Constuructors/SkillConstructor.ts index 32cc57c3..c44a23b1 100644 --- a/src/arena/Constuructors/SkillConstructor.ts +++ b/src/arena/Constuructors/SkillConstructor.ts @@ -3,8 +3,9 @@ import type Game from '../GameService'; import MiscService from '../MiscService'; import type { Player } from '../PlayersService'; import type { - CostType, OrderType, AOEType, Breaks, BreaksMessage, CustomMessage, BaseNext, + CostType, OrderType, AOEType, CustomMessage, BaseNext, Breaks, BreaksMessage, } from './types'; +import { handleCastError } from './utils'; export type SkillNext = BaseNext & { actionType: 'skill'; @@ -65,10 +66,10 @@ export abstract class Skill { this.getCost(); this.checkChance(); this.run(); - this.next(); - this.getExp(initiator); - } catch (failMsg) { - game.recordOrderResult(failMsg); + } catch (error) { + handleCastError(error, (error) => { + this.fail(error.message); + }); } } @@ -83,7 +84,7 @@ export abstract class Skill { if (remainingEnergy >= 0) { initiator.stats.set(this.costType, remainingEnergy); } else { - throw this.breaks('NO_ENERGY'); + throw this.getFailResult('NO_ENERGY'); } } @@ -93,7 +94,7 @@ export abstract class Skill { checkChance(): void { if (MiscService.rndm('1d100') > this.getChance()) { // скил сфейлился - throw this.breaks('SKILL_FAIL'); + throw this.getFailResult('SKILL_FAIL'); } } @@ -108,11 +109,24 @@ export abstract class Skill { } /** - * Успешное прохождение скила и отправка записи в BattleLog + * Рассчитываем полученный exp */ - next(): void { - const { initiator, target, game } = this.params; - const args: SkillNext = { + getExp(initiator: Player): void { + initiator.stats.up('exp', this.baseExp); + } + + getFailResult(message: BreaksMessage, { initiator, target } = this.params): Breaks { + return { + action: this.displayName, + initiator: initiator.nick, + target: target.nick, + actionType: 'skill', + message, + }; + } + + getSuccessResult({ initiator, target } = this.params): SkillNext { + return { exp: this.baseExp, action: this.displayName, actionType: 'skill', @@ -120,27 +134,25 @@ export abstract class Skill { initiator: initiator.nick, msg: this.customMessage?.bind(this), }; - - game.recordOrderResult(args); } /** - * Расчитываем полученный exp + * Успешное прохождение скила и отправка записи в BattleLog */ - getExp(initiator: Player): void { - initiator.stats.mode('up', 'exp', this.baseExp); + success({ initiator, target, game } = this.params): void { + this.getExp(initiator); + + const result = this.getSuccessResult({ initiator, target, game }); + + game.recordOrderResult(result); } /** * Обработка провала магии */ - breaks(e: BreaksMessage): Breaks { - return { - action: this.displayName, - initiator: this.params.initiator.nick, - target: this.params.target.nick, - actionType: 'skill', - message: e, - }; + fail(message: BreaksMessage, { initiator, target, game } = this.params): void { + const result = this.getFailResult(message, { initiator, target, game }); + + game.recordOrderResult(result); } } diff --git a/src/arena/Constuructors/utils/index.ts b/src/arena/Constuructors/utils/index.ts index 5e2b0dbe..3c1ffece 100644 --- a/src/arena/Constuructors/utils/index.ts +++ b/src/arena/Constuructors/utils/index.ts @@ -1,4 +1,4 @@ -import type { FailArgs, SuccessArgs } from '../types'; +import type { Breaks, FailArgs, SuccessArgs } from '../types'; export const isSuccessResult = (result: SuccessArgs | FailArgs): result is SuccessArgs => { return !('message' in result); @@ -11,3 +11,15 @@ export const isSuccessDamageResult = (result: SuccessArgs | FailArgs) => { return false; }; + +export const isFailResult = (result: SuccessArgs | FailArgs): result is FailArgs => { + return 'message' in result; +}; + +export const handleCastError = (error: unknown, onActionError: (error: Breaks) => void) => { + if (error instanceof Error) { + console.error(error); + } else { + onActionError(error as Breaks); + } +}; diff --git a/src/arena/actions/__snapshots__/attack.test.ts.snap b/src/arena/actions/__snapshots__/attack.test.ts.snap new file mode 100644 index 00000000..ebd07bad --- /dev/null +++ b/src/arena/actions/__snapshots__/attack.test.ts.snap @@ -0,0 +1,67 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`attack should dodge 1`] = ` +[ + 8, + 8, +] +`; + +exports[`attack should dodge 2`] = `0`; + +exports[`attack should dodge 3`] = ` +[ + { + "action": "🐍 Увертка", + "actionType": "skill", + "exp": 50, + "initiator": "magni", + "msg": [Function], + "target": "asperiores", + }, + { + "action": "attack", + "actionType": "phys", + "initiator": "asperiores", + "message": "DODGED", + "target": "magni", + "weapon": { + "code": "a100", + }, + }, +] +`; + +exports[`attack should not dodge 1`] = ` +[ + 8, + -2.3, +] +`; + +exports[`attack should not dodge 2`] = `82`; + +exports[`attack should not dodge 3`] = ` +[ + { + "action": "🐍 Увертка", + "actionType": "skill", + "initiator": "amet", + "message": "SKILL_FAIL", + "target": "ratione", + }, + { + "action": "attack", + "actionType": "phys", + "dmg": 10.3, + "dmgType": "physical", + "exp": 82, + "hp": -2.3, + "initiator": "ratione", + "target": "amet", + "weapon": { + "code": "a100", + }, + }, +] +`; diff --git a/src/arena/actions/attack.test.ts b/src/arena/actions/attack.test.ts new file mode 100644 index 00000000..621a2261 --- /dev/null +++ b/src/arena/actions/attack.test.ts @@ -0,0 +1,62 @@ +import casual from 'casual'; +import CharacterService from '@/arena/CharacterService'; +import GameService from '@/arena/GameService'; +import { profsData } from '@/data/profs'; +import TestUtils from '@/utils/testUtils'; +import attack from './attack'; + +// npm t src/arena/actions/attack.test.ts + +describe('attack', () => { + let game: GameService; + + beforeAll(() => { + casual.seed(1); + }); + + beforeEach(async () => { + const harks = { ...profsData.m.hark, wis: 20 }; + const initiator = await TestUtils.createCharacter({ prof: 'm', magics: { fireBall: 3 }, harks }, { withWeapon: true }); + const target = await TestUtils.createCharacter({ skills: { dodge: 3 } }); + + await Promise.all([initiator.id, target.id].map(CharacterService.getCharacterById)); + + game = new GameService([initiator.id, target.id]); + }); + + beforeEach(() => { + jest.spyOn(global.Math, 'random').mockReturnValue(0.15); + }); + + afterEach(() => { + jest.spyOn(global.Math, 'random').mockRestore(); + }); + + it('should dodge', () => { + game.players.players[0].proc = 1; + + game.players.players[1].flags.isDodging = Infinity; + attack.cast(game.players.players[0], game.players.players[1], game); + + expect( + game.players.players.map((player) => player.stats.val('hp')), + ).toMatchSnapshot(); + expect(game.players.players[0].stats.val('exp')).toMatchSnapshot(); + expect( TestUtils.normalizeRoundHistory(game.getRoundResults())).toMatchSnapshot(); + }); + + + + it('should not dodge', () => { + game.players.players[0].proc = 1; + + game.players.players[1].flags.isDodging = -Infinity; + attack.cast(game.players.players[0], game.players.players[1], game); + + expect( + game.players.players.map((player) => player.stats.val('hp')), + ).toMatchSnapshot(); + expect(game.players.players[0].stats.val('exp')).toMatchSnapshot(); + expect(TestUtils.normalizeRoundHistory(game.getRoundResults())).toMatchSnapshot(); + }); +}); diff --git a/src/arena/skills/__snapshots__/disarm.test.ts.snap b/src/arena/skills/__snapshots__/disarm.test.ts.snap new file mode 100644 index 00000000..3c0b7fa6 --- /dev/null +++ b/src/arena/skills/__snapshots__/disarm.test.ts.snap @@ -0,0 +1,49 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`disarm target should be disarmed if initiator has more dex 1`] = ` +[ + { + "action": "🥊 Обезаруживание", + "actionType": "skill", + "exp": 20, + "initiator": "alias", + "msg": [Function], + "target": "asperiores", + }, + { + "action": "attack", + "actionType": "phys", + "initiator": "asperiores", + "message": "DISARM", + "target": "alias", + "weapon": { + "code": "a100", + }, + }, +] +`; + +exports[`disarm target should not be disarmed if initiator has less dex 1`] = ` +[ + { + "action": "🥊 Обезаруживание", + "actionType": "skill", + "initiator": "explicabo", + "message": "SKILL_FAIL", + "target": "repellat", + }, + { + "action": "attack", + "actionType": "phys", + "dmg": 11, + "dmgType": "physical", + "exp": 88, + "hp": -3, + "initiator": "repellat", + "target": "explicabo", + "weapon": { + "code": "a100", + }, + }, +] +`; diff --git a/src/arena/skills/__snapshots__/dodge.test.ts.snap b/src/arena/skills/__snapshots__/dodge.test.ts.snap new file mode 100644 index 00000000..ba578790 --- /dev/null +++ b/src/arena/skills/__snapshots__/dodge.test.ts.snap @@ -0,0 +1,67 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dodge target should dodge attack if has more dex 1`] = ` +[ + 8, + 8, +] +`; + +exports[`dodge target should dodge attack if has more dex 2`] = `0`; + +exports[`dodge target should dodge attack if has more dex 3`] = ` +[ + { + "action": "🐍 Увертка", + "actionType": "skill", + "exp": 50, + "initiator": "alias", + "msg": [Function], + "target": "asperiores", + }, + { + "action": "attack", + "actionType": "phys", + "initiator": "asperiores", + "message": "DODGED", + "target": "alias", + "weapon": { + "code": "a100", + }, + }, +] +`; + +exports[`dodge target should not dodge attack if has more dex 1`] = ` +[ + 8, + -3, +] +`; + +exports[`dodge target should not dodge attack if has more dex 2`] = `88`; + +exports[`dodge target should not dodge attack if has more dex 3`] = ` +[ + { + "action": "🐍 Увертка", + "actionType": "skill", + "initiator": "repellat", + "message": "SKILL_FAIL", + "target": "explicabo", + }, + { + "action": "attack", + "actionType": "phys", + "dmg": 11, + "dmgType": "physical", + "exp": 88, + "hp": -3, + "initiator": "repellat", + "target": "explicabo", + "weapon": { + "code": "a100", + }, + }, +] +`; diff --git a/src/arena/skills/__snapshots__/parry.test.ts.snap b/src/arena/skills/__snapshots__/parry.test.ts.snap new file mode 100644 index 00000000..03281846 --- /dev/null +++ b/src/arena/skills/__snapshots__/parry.test.ts.snap @@ -0,0 +1,49 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`parry target should dodge attack if has more dex 1`] = ` +[ + { + "action": "🤺 Парирование", + "actionType": "skill", + "exp": 8, + "initiator": "alias", + "msg": [Function], + "target": "asperiores", + }, + { + "action": "attack", + "actionType": "phys", + "initiator": "asperiores", + "message": "PARRYED", + "target": "alias", + "weapon": { + "code": "a100", + }, + }, +] +`; + +exports[`parry target should not dodge attack if has more dex 1`] = ` +[ + { + "action": "🤺 Парирование", + "actionType": "skill", + "initiator": "repellat", + "message": "SKILL_FAIL", + "target": "explicabo", + }, + { + "action": "attack", + "actionType": "phys", + "dmg": 11, + "dmgType": "physical", + "exp": 88, + "hp": -3, + "initiator": "repellat", + "target": "explicabo", + "weapon": { + "code": "a100", + }, + }, +] +`; diff --git a/src/arena/skills/berserk.ts b/src/arena/skills/berserk.ts index 81c27eea..f5e60acc 100644 --- a/src/arena/skills/berserk.ts +++ b/src/arena/skills/berserk.ts @@ -31,6 +31,8 @@ class Berserk extends Skill { initiator.stats.mul('hit', effect); initiator.stats.mul('patk', (1 / effect)); initiator.stats.mul('mgp', (1 / effect)); + + this.success(this.params); } customMessage(args: SkillNext) { diff --git a/src/arena/skills/disarm.test.ts b/src/arena/skills/disarm.test.ts new file mode 100644 index 00000000..b7003f52 --- /dev/null +++ b/src/arena/skills/disarm.test.ts @@ -0,0 +1,53 @@ +import casual from 'casual'; +import CharacterService from '@/arena/CharacterService'; +import GameService from '@/arena/GameService'; +import TestUtils from '@/utils/testUtils'; +import attack from '../actions/attack'; +import disarm from './disarm'; + +// npm t src/arena/skills/disarm.test.ts + +describe('disarm', () => { + let game: GameService; + + beforeAll(() => { + casual.seed(1); + }); + + beforeEach(async () => { + const initiator = await TestUtils.createCharacter({ prof: 'w' }, { withWeapon: true }); + const target = await TestUtils.createCharacter({ prof: 'l', skills: { disarm: 1 } }); + + await Promise.all([initiator.id, target.id].map(CharacterService.getCharacterById)); + + game = new GameService([initiator.id, target.id]); + }); + + beforeEach(() => { + jest.spyOn(global.Math, 'random').mockReturnValue(0.15); + }); + + afterEach(() => { + jest.spyOn(global.Math, 'random').mockRestore(); + }); + + it('target should be disarmed if initiator has more dex', async () => { + game.players.players[0].proc = 1; + game.players.players[1].stats.set('dex', 9999); + + disarm.cast(game.players.players[1], game.players.players[0], game); + attack.cast(game.players.players[0], game.players.players[1], game); + + expect(TestUtils.normalizeRoundHistory(game.getRoundResults())).toMatchSnapshot(); + }); + + it('target should not be disarmed if initiator has less dex', async () => { + game.players.players[0].proc = 1; + game.players.players[0].stats.set('dex', 9999); + + disarm.cast(game.players.players[1], game.players.players[0], game); + attack.cast(game.players.players[0], game.players.players[1], game); + + expect(TestUtils.normalizeRoundHistory(game.getRoundResults())).toMatchSnapshot(); + }); +}); diff --git a/src/arena/skills/disarm.ts b/src/arena/skills/disarm.ts index 97f2ede3..9753cb6f 100644 --- a/src/arena/skills/disarm.ts +++ b/src/arena/skills/disarm.ts @@ -1,11 +1,12 @@ import { bold, italic } from '../../utils/formatString'; +import { PreAffect } from '../Constuructors/PreAffect'; import { Skill } from '../Constuructors/SkillConstructor'; import type { SuccessArgs } from '../Constuructors/types'; /** * Обезаруживание */ -class Disarm extends Skill { +class Disarm extends Skill implements PreAffect { constructor() { super({ name: 'disarm', @@ -33,6 +34,16 @@ class Disarm extends Skill { const tDex = target.stats.val('dex'); if (iDex >= tDex) { target.flags.isDisarmed = true; + + this.success(this.params); + } else { + this.fail('SKILL_FAIL', this.params); + } + } + + check({ initiator, target, game } = this.params) { + if (initiator.flags.isDisarmed) { + return this.getFailResult('DISARM', { initiator, target, game }); } } diff --git a/src/arena/skills/dodge.test.ts b/src/arena/skills/dodge.test.ts new file mode 100644 index 00000000..e04a0a12 --- /dev/null +++ b/src/arena/skills/dodge.test.ts @@ -0,0 +1,53 @@ +import casual from 'casual'; +import CharacterService from '@/arena/CharacterService'; +import GameService from '@/arena/GameService'; +import TestUtils from '@/utils/testUtils'; +import attack from '../actions/attack'; +import dodge from './dodge'; + +// npm t src/arena/skills/dodge.test.ts + +describe('dodge', () => { + let game: GameService; + + beforeAll(() => { + casual.seed(1); + }); + + beforeEach(async () => { + const initiator = await TestUtils.createCharacter({ prof: 'w' }, { withWeapon: true }); + const target = await TestUtils.createCharacter({ prof: 'l', skills: { dodge: 1 } }); + + await Promise.all([initiator.id, target.id].map(CharacterService.getCharacterById)); + + game = new GameService([initiator.id, target.id]); + }); + + beforeEach(() => { + jest.spyOn(global.Math, 'random').mockReturnValue(0.15); + }); + + afterEach(() => { + jest.spyOn(global.Math, 'random').mockRestore(); + }); + + it('target should dodge attack if has initiator more dex', async () => { + game.players.players[0].proc = 1; + game.players.players[1].stats.set('dex', 9999); + + dodge.cast(game.players.players[1], game.players.players[0], game); + attack.cast(game.players.players[0], game.players.players[1], game); + + expect(TestUtils.normalizeRoundHistory(game.getRoundResults())).toMatchSnapshot(); + }); + + it('target should not dodge attack if target has more dex', async () => { + game.players.players[0].proc = 1; + game.players.players[0].stats.set('dex', 9999); + + dodge.cast(game.players.players[1], game.players.players[0], game); + attack.cast(game.players.players[0], game.players.players[1], game); + + expect(TestUtils.normalizeRoundHistory(game.getRoundResults())).toMatchSnapshot(); + }); +}); diff --git a/src/arena/skills/dodge.ts b/src/arena/skills/dodge.ts index f5bd1bbd..968a2d60 100644 --- a/src/arena/skills/dodge.ts +++ b/src/arena/skills/dodge.ts @@ -1,11 +1,15 @@ +import { floatNumber } from '@/utils/floatNumber'; +import arena from '..'; import { bold, italic } from '../../utils/formatString'; +import type { PreAffect } from '../Constuructors/PreAffect'; import { Skill } from '../Constuructors/SkillConstructor'; import type { SuccessArgs } from '../Constuructors/types'; +import MiscService from '../MiscService'; /** * Увертка */ -class Dodge extends Skill { +class Dodge extends Skill implements PreAffect { constructor() { super({ name: 'dodge', @@ -30,6 +34,30 @@ class Dodge extends Skill { initiator.flags.isDodging = this.effect[initiatorSkillLvl - 1] * initiator.stats.val('dex'); } + check({ initiator, target, game } = this.params) { + const iDex = initiator.stats.val('dex'); + if (!initiator.weapon) { + return this.getFailResult('NO_WEAPON', { initiator, target, game }); + } + const weapon = arena.items[initiator.weapon.code]; + const isDodgeableWeapon = MiscService.weaponTypes[weapon.wtype].dodge; + + if (target.flags.isDodging && isDodgeableWeapon) { + const at = floatNumber(Math.round(target.flags.isDodging / iDex)); + console.log('Dodging: ', at); + const r = MiscService.rndm('1d100'); + const c = Math.round(Math.sqrt(at) + (10 * at) + 5); + console.log('left:', c, ' right:', r, ' result:', c > r); + if (c > r) { + this.success({ initiator: target, target: initiator, game }); + + return this.getFailResult('DODGED', { initiator, target, game }); + } + + this.fail('SKILL_FAIL', { initiator, target, game }); + } + } + customMessage(args: SuccessArgs) { return `${bold(args.initiator)} использовал ${italic(this.displayName)}`; } diff --git a/src/arena/skills/parry.test.ts b/src/arena/skills/parry.test.ts new file mode 100644 index 00000000..f62b08b3 --- /dev/null +++ b/src/arena/skills/parry.test.ts @@ -0,0 +1,53 @@ +import casual from 'casual'; +import CharacterService from '@/arena/CharacterService'; +import GameService from '@/arena/GameService'; +import TestUtils from '@/utils/testUtils'; +import attack from '../actions/attack'; +import parry from './parry'; + +// npm t src/arena/skills/parry.test.ts + +describe('parry', () => { + let game: GameService; + + beforeAll(() => { + casual.seed(1); + }); + + beforeEach(async () => { + const initiator = await TestUtils.createCharacter({ prof: 'w' }, { withWeapon: true }); + const target = await TestUtils.createCharacter({ prof: 'l', skills: { parry: 1 } }); + + await Promise.all([initiator.id, target.id].map(CharacterService.getCharacterById)); + + game = new GameService([initiator.id, target.id]); + }); + + beforeEach(() => { + jest.spyOn(global.Math, 'random').mockReturnValue(0.15); + }); + + afterEach(() => { + jest.spyOn(global.Math, 'random').mockRestore(); + }); + + it('target should parry attack if initiator has more dex', async () => { + game.players.players[0].proc = 1; + game.players.players[1].stats.set('dex', 9999); + + parry.cast(game.players.players[1], game.players.players[0], game); + attack.cast(game.players.players[0], game.players.players[1], game); + + expect(TestUtils.normalizeRoundHistory(game.getRoundResults())).toMatchSnapshot(); + }); + + it('target should not parry attack if target has more dex', async () => { + game.players.players[0].proc = 1; + game.players.players[0].stats.set('dex', 9999); + + parry.cast(game.players.players[1], game.players.players[0], game); + attack.cast(game.players.players[0], game.players.players[1], game); + + expect(TestUtils.normalizeRoundHistory(game.getRoundResults())).toMatchSnapshot(); + }); +}); diff --git a/src/arena/skills/parry.ts b/src/arena/skills/parry.ts index 01ceb5e5..ced3931b 100644 --- a/src/arena/skills/parry.ts +++ b/src/arena/skills/parry.ts @@ -1,11 +1,12 @@ import { bold, italic } from '../../utils/formatString'; +import { PreAffect } from '../Constuructors/PreAffect'; import { Skill } from '../Constuructors/SkillConstructor'; import type { SuccessArgs } from '../Constuructors/types'; /** * Парирование */ -class Parry extends Skill { +class Parry extends Skill implements PreAffect { constructor() { super({ name: 'parry', @@ -32,6 +33,21 @@ class Parry extends Skill { initiator.flags.isParry = initiator.stats.val('dex') * effect; } + check({ initiator, target, game } = this.params) { + const initiatorDex = initiator.stats.val('dex'); + + if (target.flags.isParry) { + if ((target.flags.isParry - initiatorDex) > 0) { + this.success({ initiator: target, target: initiator, game }); + + return this.getFailResult('PARRYED', { initiator, target, game }); + } + + target.flags.isParry -= +initiatorDex; + this.fail('SKILL_FAIL', { initiator, target, game }); + } + } + customMessage(args: SuccessArgs) { return `${bold(args.initiator)} использовал ${italic(this.displayName)}`; }