diff --git a/src/arena/Constuructors/PhysConstructor.js b/src/arena/Constuructors/PhysConstructor.js index f869559d..f51032e5 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 { isSuccessResult } = 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.params.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 && isSuccessResult(result)) { + throw this.breaks(result.message, result); } - } + }); } /** @@ -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'); } /** @@ -204,12 +195,14 @@ class PhysConstructor { } /** - * @param {String} msg строка остановки атаки (причина) + * @param {string} message строка остановки атаки (причина) + * @param {import('./types').SuccessArgs} cause строка остановки атаки (причина) */ - breaks(msg) { + breaks(message, cause) { return { actionType: 'phys', - message: msg, + message, + cause, action: this.name, initiator: this.params.initiator.nick, target: this.params.target.nick, diff --git a/src/arena/Constuructors/PreAffect.ts b/src/arena/Constuructors/PreAffect.ts new file mode 100644 index 00000000..f21f5122 --- /dev/null +++ b/src/arena/Constuructors/PreAffect.ts @@ -0,0 +1,7 @@ +import type GameService from '../GameService'; +import type { Player } from '../PlayersService'; +import type { SuccessArgs } from './types'; + +export interface PreAffect { + check(params: { initiator: Player, target: Player, game: GameService}): SuccessArgs | void +} diff --git a/src/arena/Constuructors/SkillConstructor.ts b/src/arena/Constuructors/SkillConstructor.ts index 32cc57c3..cdd789c7 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'; @@ -39,6 +40,10 @@ export abstract class Skill { game: Game; }; + status = { + exp: 0, + }; + /** * Создание скила */ @@ -65,10 +70,11 @@ export abstract class Skill { this.getCost(); this.checkChance(); this.run(); - this.next(); - this.getExp(initiator); - } catch (failMsg) { - game.recordOrderResult(failMsg); + this.success(); + } catch (error) { + handleCastError(error, (error) => { + this.fail(error.message); + }); } } @@ -83,7 +89,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 +99,7 @@ export abstract class Skill { checkChance(): void { if (MiscService.rndm('1d100') > this.getChance()) { // скил сфейлился - throw this.breaks('SKILL_FAIL'); + throw this.getFailResult('SKILL_FAIL'); } } @@ -108,12 +114,16 @@ export abstract class Skill { } /** - * Успешное прохождение скила и отправка записи в BattleLog + * Рассчитываем полученный exp */ - next(): void { - const { initiator, target, game } = this.params; - const args: SkillNext = { - exp: this.baseExp, + getExp(initiator: Player): void { + this.status.exp = this.baseExp; + initiator.stats.up('exp', this.status.exp); + } + + getSuccessResult({ initiator, target } = this.params): SkillNext { + const result: SkillNext = { + exp: this.status.exp, action: this.displayName, actionType: 'skill', target: target.nick, @@ -121,26 +131,44 @@ export abstract class Skill { msg: this.customMessage?.bind(this), }; - game.recordOrderResult(args); + this.reset(); + + return result; + } + + getFailResult(message: BreaksMessage, { initiator, target } = this.params): Breaks { + const result: Breaks = { + action: this.displayName, + initiator: initiator.nick, + target: target.nick, + actionType: 'skill', + message, + }; + + this.reset(); + + return result; } /** - * Расчитываем полученный exp + * Успешное прохождение скила и отправка записи в BattleLog */ - getExp(initiator: Player): void { - initiator.stats.mode('up', 'exp', this.baseExp); + success({ initiator, target, game } = this.params): void { + 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); + } + + reset() { + this.status.exp = 0; } } diff --git a/src/arena/Constuructors/types.ts b/src/arena/Constuructors/types.ts index 415c6c87..23aaab3f 100644 --- a/src/arena/Constuructors/types.ts +++ b/src/arena/Constuructors/types.ts @@ -73,6 +73,7 @@ export type PhysNext = BaseNext & { export type PhysBreak = Omit & { actionType: 'phys'; + cause?: SuccessArgs; message: BreaksMessage; weapon: Item; expArr: ExpArr; @@ -94,6 +95,7 @@ export type ActionType = SuccessArgs['actionType']; export interface Breaks { actionType: ActionType; message: BreaksMessage; + cause?: SuccessArgs; action: string; initiator: string; target: string; diff --git a/src/arena/Constuructors/utils/index.ts b/src/arena/Constuructors/utils/index.ts index 5e2b0dbe..f08938ca 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,11 @@ export const isSuccessDamageResult = (result: SuccessArgs | FailArgs) => { return false; }; + +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/LogService/utils/format-cause.ts b/src/arena/LogService/utils/format-cause.ts new file mode 100644 index 00000000..3ae99077 --- /dev/null +++ b/src/arena/LogService/utils/format-cause.ts @@ -0,0 +1,10 @@ +import { SuccessArgs } from '@/arena/Constuructors/types'; + +export function formatCause(cause: SuccessArgs) { + switch (cause.actionType) { + case 'skill': + return `_${cause.action}_ *${cause.initiator}*: 📖${cause.exp}`; + default: + return `_${cause.action}_ *${cause.initiator}*`; + } +} diff --git a/src/arena/LogService/utils/format-error.ts b/src/arena/LogService/utils/format-error.ts index f3f260dc..1a1da433 100644 --- a/src/arena/LogService/utils/format-error.ts +++ b/src/arena/LogService/utils/format-error.ts @@ -1,5 +1,5 @@ import type { BreaksMessage, FailArgs } from '@/arena/Constuructors/types'; - +import { formatCause } from './format-cause'; /** * msg * @todo WIP, функция должна будет принимать как значения урона т.п так и @@ -9,12 +9,21 @@ import type { BreaksMessage, FailArgs } from '@/arena/Constuructors/types'; */ export function formatError(msgObj: FailArgs): string { const { - action, message, target, initiator, + action, actionType, message, target, initiator, cause, } = msgObj; const expString = 'expArr' in msgObj ? msgObj.expArr.map(({ name, exp }) => `${name}: 📖${exp}`).join(', ') : ''; const weapon = 'weapon' in msgObj ? msgObj.weapon.case : ''; + if (cause) { + switch (actionType) { + case 'phys': + return `*${initiator} пытался атаковать ${target}, но у него не получилось\n${formatCause(cause)}`; + default: + return `*${initiator} пытался использовать _${action}_ ${target}, но у него не получилось\n${formatCause(cause)}`; + } + } + const TEXT: Record> = { NO_INITIATOR: { ru: `Некто хотел использовать _${action}_ на игрока *${target}*, но исчез`, diff --git a/src/arena/LogService/utils/format-exp.ts b/src/arena/LogService/utils/format-exp.ts index d3b7f655..47d14bb4 100644 --- a/src/arena/LogService/utils/format-exp.ts +++ b/src/arena/LogService/utils/format-exp.ts @@ -26,6 +26,8 @@ export function formatExp(args: SuccessArgs): string { return expBrackets(args.expArr.map(({ name, exp, val }) => `${name}: 💖${val}/${args.hp} 📖${exp}`).join(', ')); case 'phys': return expBrackets(`💔-${args.dmg}/${args.hp} 📖${args.exp}`); + case 'skill': + return args.exp ? expBrackets(`📖${args.exp}`) : ''; default: return expBrackets(`📖${args.exp}`); } 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..86fa20a9 --- /dev/null +++ b/src/arena/skills/__snapshots__/disarm.test.ts.snap @@ -0,0 +1,57 @@ +// 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": undefined, + "target": "asperiores", + }, + { + "action": "attack", + "actionType": "phys", + "cause": { + "action": "🥊 Обезоруживание", + "actionType": "skill", + "exp": 0, + "initiator": "alias", + "msg": undefined, + "target": "asperiores", + }, + "initiator": "asperiores", + "message": undefined, + "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..32ac8cd9 --- /dev/null +++ b/src/arena/skills/__snapshots__/dodge.test.ts.snap @@ -0,0 +1,58 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dodge target should dodge attack if has initiator more dex 1`] = ` +[ + { + "action": "🐍 Увертка", + "actionType": "skill", + "exp": 0, + "initiator": "alias", + "msg": [Function], + "target": "alias", + }, + { + "action": "attack", + "actionType": "phys", + "cause": { + "action": "🐍 Увертка", + "actionType": "skill", + "exp": 50, + "initiator": "alias", + "msg": [Function], + "target": "asperiores", + }, + "initiator": "asperiores", + "message": undefined, + "target": "alias", + "weapon": { + "code": "a100", + }, + }, +] +`; + +exports[`dodge target should not dodge attack if target has more dex 1`] = ` +[ + { + "action": "🐍 Увертка", + "actionType": "skill", + "exp": 0, + "initiator": "explicabo", + "msg": [Function], + "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..100bafaf --- /dev/null +++ b/src/arena/skills/__snapshots__/parry.test.ts.snap @@ -0,0 +1,58 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`parry target should not parry attack if target has more dex 1`] = ` +[ + { + "action": "🤺 Парирование", + "actionType": "skill", + "exp": 0, + "initiator": "explicabo", + "msg": [Function], + "target": "explicabo", + }, + { + "action": "attack", + "actionType": "phys", + "dmg": 11, + "dmgType": "physical", + "exp": 88, + "hp": -3, + "initiator": "repellat", + "target": "explicabo", + "weapon": { + "code": "a100", + }, + }, +] +`; + +exports[`parry target should parry attack if initiator has more dex 1`] = ` +[ + { + "action": "🤺 Парирование", + "actionType": "skill", + "exp": 0, + "initiator": "alias", + "msg": [Function], + "target": "alias", + }, + { + "action": "attack", + "actionType": "phys", + "cause": { + "action": "🤺 Парирование", + "actionType": "skill", + "exp": 8, + "initiator": "alias", + "msg": [Function], + "target": "asperiores", + }, + "initiator": "asperiores", + "message": undefined, + "target": "alias", + "weapon": { + "code": "a100", + }, + }, +] +`; diff --git a/src/arena/skills/berserk.ts b/src/arena/skills/berserk.ts index 81c27eea..5ff250a6 100644 --- a/src/arena/skills/berserk.ts +++ b/src/arena/skills/berserk.ts @@ -1,5 +1,6 @@ -import { bold, italic } from '../../utils/formatString'; -import { Skill, SkillNext } from '../Constuructors/SkillConstructor'; +import { bold, italic } from '@/utils/formatString'; +import { Skill } from '../Constuructors/SkillConstructor'; +import { SuccessArgs } from '../Constuructors/types'; /** * Берсерк @@ -31,9 +32,11 @@ class Berserk extends Skill { initiator.stats.mul('hit', effect); initiator.stats.mul('patk', (1 / effect)); initiator.stats.mul('mgp', (1 / effect)); + + this.getExp(this.params.initiator); } - customMessage(args: SkillNext) { + customMessage(args: SuccessArgs) { return `${bold(args.initiator)} использовал ${italic(this.displayName)}`; } } 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..20b54eb5 100644 --- a/src/arena/skills/disarm.ts +++ b/src/arena/skills/disarm.ts @@ -1,16 +1,15 @@ -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', - displayName: '🥊 Обезаруживание', - desc: 'Обезаруживает противника, не давая ему совершить атаку оружием', + displayName: '🥊 Обезоруживание', + desc: 'Обезоруживает противника, не давая ему совершить атаку оружием', cost: [12, 13, 14, 15, 16, 17], proc: 10, baseExp: 20, @@ -33,11 +32,17 @@ class Disarm extends Skill { const tDex = target.stats.val('dex'); if (iDex >= tDex) { target.flags.isDisarmed = true; + + this.getExp(target); + } else { + throw this.getFailResult('SKILL_FAIL'); } } - customMessage(args: SuccessArgs) { - return `${bold(args.initiator)} использовал ${italic(this.displayName)} `; + check({ initiator, target, game } = this.params) { + if (initiator.flags.isDisarmed) { + return this.getSuccessResult({ initiator: target, target: initiator, game }); + } } } diff --git a/src/arena/skills/dodge.test.ts b/src/arena/skills/dodge.test.ts new file mode 100644 index 00000000..ffc7cbbc --- /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[1], 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[1], 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..07917110 100644 --- a/src/arena/skills/dodge.ts +++ b/src/arena/skills/dodge.ts @@ -1,11 +1,15 @@ -import { bold, italic } from '../../utils/formatString'; +import { floatNumber } from '@/utils/floatNumber'; +import { bold, italic } from '@/utils/formatString'; +import arena from '..'; +import type { PreAffect } from '../Constuructors/PreAffect'; import { Skill } from '../Constuructors/SkillConstructor'; -import type { SuccessArgs } from '../Constuructors/types'; +import { 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,28 @@ 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; + } + 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.getExp(target); + + return this.getSuccessResult({ initiator: target, target: initiator, 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..e40a9314 --- /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[1], 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[1], 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..003d9828 100644 --- a/src/arena/skills/parry.ts +++ b/src/arena/skills/parry.ts @@ -1,11 +1,12 @@ -import { bold, italic } from '../../utils/formatString'; +import { bold, italic } from '@/utils/formatString'; +import { PreAffect } from '../Constuructors/PreAffect'; import { Skill } from '../Constuructors/SkillConstructor'; -import type { SuccessArgs } from '../Constuructors/types'; +import { SuccessArgs } from '../Constuructors/types'; /** * Парирование */ -class Parry extends Skill { +class Parry extends Skill implements PreAffect { constructor() { super({ name: 'parry', @@ -32,6 +33,20 @@ 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.getExp(target); + + return this.getSuccessResult({ initiator: target, target: initiator, game }); + } + + target.flags.isParry -= +initiatorDex; + } + } + customMessage(args: SuccessArgs) { return `${bold(args.initiator)} использовал ${italic(this.displayName)}`; }