From 3abf60b4efb093a92759c9e1c7c4dd4b4788837d Mon Sep 17 00:00:00 2001 From: lbabyuk Date: Wed, 21 Jun 2023 22:09:50 +0300 Subject: [PATCH] + Added functionality --- .eslintrc | 13 +- src/javascript/components/arena.js | 11 +- src/javascript/components/fight.js | 204 +++++++++++++++++++ src/javascript/components/fighterPreview.js | 28 ++- src/javascript/components/fighterSelector.js | 12 ++ src/javascript/components/modal/winner.js | 11 + src/javascript/helpers/apiHelper.js | 6 +- src/javascript/services/fightersService.js | 13 +- src/styles/arena.css | 87 +++++++- src/styles/fighterPreview.css | 31 +++ 10 files changed, 409 insertions(+), 7 deletions(-) diff --git a/.eslintrc b/.eslintrc index 07b128f..5ee7402 100644 --- a/.eslintrc +++ b/.eslintrc @@ -9,9 +9,20 @@ }, "plugins": ["prettier"], "rules": { - "prettier/prettier": "error", + "prettier/prettier": ["error", { + "endOfLine": "auto" + }], "arrow-body-style": 0, + "no-use-before-define": [ + "error", + { + "functions": false, + "classes": false, + "variables": false + } + ], "no-console": ["error", { "allow": ["warn", "error"] }], + "no-restricted-globals": ["error", "event", "fdescribe"], "no-underscore-dangle": 0, "no-useless-catch": 0 }, diff --git a/src/javascript/components/arena.js b/src/javascript/components/arena.js index e020caf..6a46600 100644 --- a/src/javascript/components/arena.js +++ b/src/javascript/components/arena.js @@ -1,5 +1,7 @@ import createElement from '../helpers/domHelper'; import { createFighterImage } from './fighterPreview'; +import { fight } from './fight'; +import showWinnerModal from './modal/winner'; function createFighter(fighter, position) { const imgElement = createFighterImage(fighter); @@ -59,7 +61,7 @@ function createArena(selectedFighters) { return arena; } -export default function renderArena(selectedFighters) { +export default async function renderArena(selectedFighters) { const root = document.getElementById('root'); const arena = createArena(selectedFighters); @@ -69,4 +71,11 @@ export default function renderArena(selectedFighters) { // todo: // - start the fight // - when fight is finished show winner + try { + const [firstFighter, secondFighter] = selectedFighters; + const winner = await fight(firstFighter, secondFighter); + showWinnerModal(winner); + } catch (error) { + throw error; + } } diff --git a/src/javascript/components/fight.js b/src/javascript/components/fight.js index 3707dcf..0c2a7b5 100644 --- a/src/javascript/components/fight.js +++ b/src/javascript/components/fight.js @@ -1,19 +1,223 @@ import controls from '../../constants/controls'; +const GAME_KEYS = Object.values(controls).flat(2); +const POINTS = 10; +const POSITIONS = { + LEFT: 'left', + RIGHT: 'right' +}; +const ATTACK_TYPES = { + PUNCH: 'punch', + FIREBALL: 'fireball' +}; + +function updateHealthIndicator(currentHealth, health, position) { + const healthIndicator = document.getElementById(`${position}-fighter-indicator`); + const indicatorWidth = Math.max(0, (currentHealth * 100) / health); + healthIndicator.style.width = `${indicatorWidth}%`; +} + +function showAttack(position, attack) { + const attackElement = document.getElementById(`${position}-${attack}`); + attackElement.classList.add(`arena___${position}-${attack}-show`); + setTimeout(() => { + attackElement.classList.remove(`arena___${position}-${attack}-show`); + }, 300); +} + +function toggleShield(show, position) { + const shield = document.getElementById(`${position}-shield`); + if (show) { + shield.style.visibility = 'visible'; + } else { + shield.style.visibility = 'hidden'; + } +} + +function toggleCritSignal(show, position) { + const indicator = document.getElementById(`${position}-crit-signal`); + if (show) { + indicator.style.visibility = 'visible'; + } else { + indicator.style.visibility = 'hidden'; + } +} + +function updateRageIndicator(position, width) { + const rageIndicator = document.getElementById(`${position}-rage-indicator`); + rageIndicator.style.width = `${width}%`; +} + +const createCritPointsUpdatedHandler = position => (currentPoints, canCrit) => { + if (currentPoints === 0) { + toggleCritSignal(false, position); + } + + if (canCrit) { + toggleCritSignal(true, position); + } + + updateRageIndicator(position, currentPoints * 10); +}; + +const createIsBlockingChangedHandler = position => isBlocking => { + toggleShield(isBlocking, position); +}; + +const createIsDamageReceivedHandler = position => (currentHealth, health) => { + updateHealthIndicator(currentHealth, health, position); +}; + +const createIsAttackingHandler = position => attack => { + showAttack(position, attack); +}; + +export const createFighterConfigs = position => ({ + onPointsUpdated: createCritPointsUpdatedHandler(position), + onIsBlockingChanged: createIsBlockingChangedHandler(position), + onDamageReceived: createIsDamageReceivedHandler(position), + onAttacking: createIsAttackingHandler(position) +}); + +function getRandomNumberFromRange(min, max) { + return Math.random() * (max - min) + min; +} + +function createArenaFighter(fighter, configs) { + const { onPointsUpdated, onIsBlockingChanged, onDamageReceived, onAttacking } = configs; + + return { + ...fighter, + currentHealth: fighter.health, + currentCritPoints: 0, + isBlocking: false, + timerId: null, + receiveDamage(value) { + this.currentHealth -= value; + onDamageReceived(this.currentHealth, this.health); + }, + setIsBlocking(value) { + this.isBlocking = value; + onIsBlockingChanged(value); + }, + doAttack(defender, damage) { + defender.receiveDamage(damage); + onAttacking(ATTACK_TYPES.PUNCH); + }, + doCritAttack(defender) { + if (!this.isCanDoCrit()) return; + + this.restartCritPoints(); + defender.receiveDamage(this.attack * 2); + onAttacking(ATTACK_TYPES.FIREBALL); + }, + isCanDoCrit() { + return this.currentCritPoints === POINTS; + }, + restartCritPoints() { + this.currentCritPoints = 0; + onPointsUpdated(this.currentCritPoints, false); + + this.timerId = setInterval(() => { + this.currentCritPoints += 1; + + const canDoCrit = this.isCanDoCrit(); + + onPointsUpdated(this.currentCritPoints, canDoCrit); + + if (canDoCrit) { + clearInterval(this.timerId); + } + }, 1000); + } + }; +} + +function applyFighterAttack(attacker, defender) { + if (attacker.isBlocking) { + return; + } + + if (defender.isBlocking) { + attacker.doAttack(defender, 0); + return; + } + + attacker.doAttack(defender, getDamage(attacker, defender)); +} + +function processFightAction(firstFighter, secondFighter, keyMap, currentCode) { + if (currentCode === controls.PlayerOneBlock) { + firstFighter.setIsBlocking(true); + } + if (currentCode === controls.PlayerTwoBlock) { + secondFighter.setIsBlocking(true); + } + if (currentCode === controls.PlayerOneAttack) { + applyFighterAttack(firstFighter, secondFighter, keyMap); + return; + } + if (currentCode === controls.PlayerTwoAttack) { + applyFighterAttack(secondFighter, firstFighter, keyMap); + return; + } + if (controls.PlayerOneCriticalHitCombination.every(code => keyMap.has(code))) { + firstFighter.doCritAttack(secondFighter); + return; + } + if (controls.PlayerTwoCriticalHitCombination.every(code => keyMap.has(code))) { + secondFighter.doCritAttack(firstFighter); + } +} + export async function fight(firstFighter, secondFighter) { return new Promise(resolve => { // resolve the promise with the winner when fight is over + const firstArenaFighter = createArenaFighter(firstFighter, createFighterConfigs(POSITIONS.LEFT)); + const secondArenaFighter = createArenaFighter(secondFighter, createFighterConfigs(POSITIONS.RIGHT)); + firstArenaFighter.restartCritPoints(); + secondArenaFighter.restartCritPoints(); + + const pressedKeys = new Map(); + document.addEventListener('keydown', e => { + if (e.repeat || !GAME_KEYS.some(key => key === e.code)) { + return; + } + pressedKeys.set(e.code, true); + processFightAction(firstArenaFighter, secondArenaFighter, pressedKeys, e.code); + + if (firstArenaFighter.currentHealth <= 0) { + resolve(secondFighter); + } else if (secondArenaFighter.currentHealth <= 0) { + resolve(firstFighter); + } + }); + document.addEventListener('keyup', e => { + if (e.code === controls.PlayerOneBlock) { + firstArenaFighter.setIsBlocking(false); + } + if (e.code === controls.PlayerTwoBlock) { + secondArenaFighter.setIsBlocking(false); + } + pressedKeys.delete(e.code); + }); }); } export function getDamage(attacker, defender) { // return damage + const damage = getHitPower(attacker) - getBlockPower(defender); + return damage > 0 ? damage : 0; } export function getHitPower(fighter) { // return hit power + const criticalHitChance = getRandomNumberFromRange(1, 2); + return fighter.attack * criticalHitChance; } export function getBlockPower(fighter) { // return block power + const dodgeChance = getRandomNumberFromRange(1, 2); + return fighter.defense * dodgeChance; } diff --git a/src/javascript/components/fighterPreview.js b/src/javascript/components/fighterPreview.js index f1fcfe4..8e22706 100644 --- a/src/javascript/components/fighterPreview.js +++ b/src/javascript/components/fighterPreview.js @@ -6,9 +6,35 @@ export function createFighterPreview(fighter, position) { tagName: 'div', className: `fighter-preview___root ${positionClassName}` }); - // todo: show fighter info (image, name, health, etc.) + if (fighter) { + // eslint-disable-next-line no-use-before-define + const fighterImage = createFighterImage(fighter); + const fighterName = createElement({ tagName: 'h3' }); + const fighterDetails = createElement({ + tagName: 'div', + className: 'fighter-details' + }); + const fighterDetailsWrapper = createElement({ + tagName: 'div', + className: 'fighter-details-wrapper' + }); + fighterDetails.innerHTML = ` +
+

${fighter.attack}

+
+
+

${fighter.defense}

+
+
+

${fighter.health}

+
+ `; + fighterName.innerText = fighter.name; + fighterDetailsWrapper.append(fighterName, fighterDetails); + fighterElement.append(fighterImage, fighterDetailsWrapper); + } return fighterElement; } diff --git a/src/javascript/components/fighterSelector.js b/src/javascript/components/fighterSelector.js index c48df96..c1b06b7 100644 --- a/src/javascript/components/fighterSelector.js +++ b/src/javascript/components/fighterSelector.js @@ -2,11 +2,23 @@ import createElement from '../helpers/domHelper'; import renderArena from './arena'; import versusImg from '../../../resources/versus.png'; import { createFighterPreview } from './fighterPreview'; +import fighterService from '../services/fightersService'; const fighterDetailsMap = new Map(); export async function getFighterInfo(fighterId) { // get fighter info from fighterDetailsMap or from service and write it to fighterDetailsMap + if (fighterDetailsMap) { + return fighterDetailsMap.get(fighterId); + } + + try { + const fighterDetails = await fighterService.getFighterDetails(fighterId); + fighterDetailsMap.set(fighterId, fighterDetails); + return fighterDetails; + } catch (error) { + throw error; + } } function startFight(selectedFighters) { diff --git a/src/javascript/components/modal/winner.js b/src/javascript/components/modal/winner.js index d890831..cd196d0 100644 --- a/src/javascript/components/modal/winner.js +++ b/src/javascript/components/modal/winner.js @@ -1,5 +1,16 @@ import showModal from './modal'; +import { createFighterImage } from '../fighterPreview'; export default function showWinnerModal(fighter) { // call showModal function + + const imageElement = createFighterImage(fighter); + const modalElement = { + title: `${fighter.name.toUpperCase()} You are the winner!!!`, + bodyElement: imageElement, + onClose: () => { + location.reload(); + } + }; + showModal(modalElement); } diff --git a/src/javascript/helpers/apiHelper.js b/src/javascript/helpers/apiHelper.js index e09cc07..34591ff 100644 --- a/src/javascript/helpers/apiHelper.js +++ b/src/javascript/helpers/apiHelper.js @@ -9,11 +9,13 @@ const SECURITY_HEADERS = { * To test the application against the real dataset set useMockAPI=false. * But to test the application you don't need to extend the GitHub REST API rate limit to 5000 requests with the token */ - // authorization: 'token %github_token%' + authorization: 'token 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' } }; +const responsePromise = fetch(BASE_API_URL, SECURITY_HEADERS); +responsePromise.then(response => response.json()); -const useMockAPI = true; +const useMockAPI = false; function getFighterById(endpoint) { const start = endpoint.lastIndexOf('/'); diff --git a/src/javascript/services/fightersService.js b/src/javascript/services/fightersService.js index b9839bc..b0d2b5d 100644 --- a/src/javascript/services/fightersService.js +++ b/src/javascript/services/fightersService.js @@ -3,6 +3,10 @@ import callApi from '../helpers/apiHelper'; class FighterService { #endpoint = 'fighters.json'; + constructor() { + this.getFighterDetails = this.getFighterDetails.bind(this); + } + async getFighters() { try { const apiResult = await callApi(this.#endpoint); @@ -12,12 +16,19 @@ class FighterService { } } + // eslint-disable-next-line class-methods-use-this async getFighterDetails(id) { + try { + const apiResult = await callApi(`details/fighter/${id}.json`); + return apiResult; + } catch (error) { + throw error; + } // todo: implement this method // endpoint - `details/fighter/${id}.json`; } } const fighterService = new FighterService(); - +fighterService.getFighterDetails(); export default fighterService; diff --git a/src/styles/arena.css b/src/styles/arena.css index 23d6e1d..2aa01c3 100644 --- a/src/styles/arena.css +++ b/src/styles/arena.css @@ -47,7 +47,7 @@ margin: 0 30px; } -.arena___health-indicator { +.arena___health-indicator, .arena___fighter-rage { width: 100%; height: 25px; border: 2px solid; @@ -61,3 +61,88 @@ width: 100%; background-color: #ebd759; } + +.arena___shields-container { + top: 40%; + width: 100%; + position: absolute; + display: flex; + justify-content: space-around; +} + +.arena___right-shield, .arena___left-shield { + visibility: hidden; +} + +.shield-img { + height: 25vh; +} + +.arena___punches-container, .arena___fireballs-container { + top: 40%; + left: 20%; + width: 60%; + position: absolute; + display: flex; + justify-content: space-around; +} + +.punch-img{ + height: 7vh; +} + +.arena___right-punch, .arena___left-punch, .arena___right-fireball, .arena___left-fireball { + opacity: 0; +} + +.arena___left-punch-show, .arena___left-fireball-show { + opacity: 1; + animation-name: moveToTheRight; + animation-duration: 0.3s; +} +.arena___right-punch-show, .arena___right-fireball-show { + opacity: 1; + animation-name: moveToTheLeft; + animation-duration: 0.3s; +} + +.arena___left-punch img { + transform: rotate(90deg) scaleX(-1); +} + +.arena___right-punch img { + transform: rotate(-90deg); +} + +.arena___right-fireball img, .arena___right-crit-indicator img { + transform: scaleX(-1); +} + +.arena___fireballs-container .fireball-img { + height: 25vh; +} + +.arena___crit-signals-container { + position: absolute; + top: 2%; + left: 5%; + display: flex; + width: 90%; + justify-content: space-between; +} + +.arena___crit-signals-container .fireball-img { + height: 10vh; +} + +@keyframes moveToTheRight { + 0% {opacity: 1;margin-left:0} + 90% {opacity: 0.9;margin-left:23vw} + 100% {opacity: 0;margin-left:20vw} +} + +@keyframes moveToTheLeft { + 0% {opacity: 1;margin-right:0} + 90% {opacity: 0.9;margin-right:23vw} + 100% {opacity: 0;margin-right:25vw} +} diff --git a/src/styles/fighterPreview.css b/src/styles/fighterPreview.css index 8d70854..7cc6d03 100644 --- a/src/styles/fighterPreview.css +++ b/src/styles/fighterPreview.css @@ -11,3 +11,34 @@ .fighter-preview___left { align-items: flex-start; } + +.fighter-preview___left img, .fighter-preview___right img { + max-height: 11rem; +} + +.fighter-preview___left, .fighter-preview___right { + width: 40%; +} + +.fighter-details-wrapper { + background-color: #ffdfa7; + border: 5px solid #ca9650; + padding: 10px; + text-align: center; +} + +.fighter-details { + display: flex; + flex-wrap: wrap; + justify-content: space-evenly; +} + +.icon { + height: 1.1rem; + width: 1.1rem; + display: inline; +} + +.fighter-detail-cell { + width: 20px; +} \ No newline at end of file