Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
Expand Down
11 changes: 10 additions & 1 deletion src/javascript/components/arena.js
Original file line number Diff line number Diff line change
@@ -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);
Expand Down Expand Up @@ -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);

Expand All @@ -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;
}
}
204 changes: 204 additions & 0 deletions src/javascript/components/fight.js
Original file line number Diff line number Diff line change
@@ -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;
}
28 changes: 27 additions & 1 deletion src/javascript/components/fighterPreview.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
<div class="fighter-detail-cell">
<p>${fighter.attack}</p>
</div>
<div class="fighter-detail-cell">
<p>${fighter.defense}</p>
</div>
<div class="fighter-detail-cell">
<p>${fighter.health}</p>
</div>
`;
fighterName.innerText = fighter.name;
fighterDetailsWrapper.append(fighterName, fighterDetails);
fighterElement.append(fighterImage, fighterDetailsWrapper);
}
return fighterElement;
}

Expand Down
12 changes: 12 additions & 0 deletions src/javascript/components/fighterSelector.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
11 changes: 11 additions & 0 deletions src/javascript/components/modal/winner.js
Original file line number Diff line number Diff line change
@@ -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);
}
6 changes: 4 additions & 2 deletions src/javascript/helpers/apiHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('/');
Expand Down
Loading