diff --git a/src/components/ActionButton/index.js b/src/components/ActionButton/index.js index b733820..14b9825 100644 --- a/src/components/ActionButton/index.js +++ b/src/components/ActionButton/index.js @@ -3,6 +3,7 @@ import { bool, func, string } from 'prop-types'; import { checkForNoiseOpeningDoor, useStateWithLabel } from '../../utils'; import { SOUNDS } from '../../assets/sounds'; import { + BLITZ_ACTION, CANNOT_BE_USED, CAR_ATTACK_ACTION, CAR_ENTER_ACTION, @@ -13,6 +14,7 @@ import { EXPLOSION_ACTION, GIVE_ORDERS_ACTION, HEAL_ACTION, + HIT_N_RUN_ACTION, LEAVE_GAME_ACTION, LOCK_ACTION, MAKE_NOISE_ACTION, @@ -118,6 +120,11 @@ const ActionButton = ({ useEffect(() => { switch (actionType) { + case BLITZ_ACTION: + setIconType2('fas fa-skull-crossbones'); + setIconType('fas fa-running'); + sound.current = new Audio(SOUNDS[`move-${type}`]); + break; case CAR_ATTACK_ACTION: setIconSize('medium'); setIconType('fas fa-child'); @@ -158,6 +165,12 @@ const ActionButton = ({ case HEAL_ACTION: setIconType('fas fa-hand-holding-medical'); break; + case HIT_N_RUN_ACTION: + setIconType('fas fa-skull-crossbones'); + setIconType2('fas fa-running'); + sound.current = new Audio(SOUNDS[`move-${type}`]); + break; + case LEAVE_GAME_ACTION: setIconType2('fas fa-running'); setIconType('fas fa-sign-out-alt'); diff --git a/src/components/ActionButton/styles.js b/src/components/ActionButton/styles.js index ae79280..9162323 100644 --- a/src/components/ActionButton/styles.js +++ b/src/components/ActionButton/styles.js @@ -1,6 +1,7 @@ import styled from '@emotion/styled'; import { css } from '@emotion/core'; import { + BLITZ_ACTION, CAR_ATTACK_ACTION, CAR_ENTER_ACTION, CAR_EXIT_ACTION, @@ -9,6 +10,7 @@ import { END_TURN_ACTION, EXPLOSION_ACTION, GIVE_ORDERS_ACTION, + HIT_N_RUN_ACTION, LEAVE_GAME_ACTION, MOVE_ACTION, OBJECTIVE_ACTION, @@ -342,6 +344,19 @@ export const PrimaryIcon = styled.i` transform: rotateY(180deg); `; } + if (actionType === BLITZ_ACTION) { + return css` + font-size: 1.5rem; + color: gray; + `; + } + if (actionType === HIT_N_RUN_ACTION) { + return css` + font-size: 2.2rem; + margin-left: -2px; + transform: rotateY(180deg); + `; + } return null; }} @@ -396,6 +411,18 @@ export const SecondaryIcon = styled.i` margin-left: -2px; `; } + if (actionType === BLITZ_ACTION) { + return css` + font-size: 2.2rem; + margin-left: -2px; + `; + } + if (actionType === HIT_N_RUN_ACTION) { + return css` + font-size: 1.3rem; + color: maroon; + `; + } return null; }} diff --git a/src/components/Fog/index.js b/src/components/Fog/index.js index 5ae762e..549cae6 100644 --- a/src/components/Fog/index.js +++ b/src/components/Fog/index.js @@ -16,7 +16,7 @@ const FogEffect = ({ inChar }) => { const canvasHeight = 200; const pCollection = []; const puffs = 1; - const particlesPerPuff = 1000; + const particlesPerPuff = 500; const smokeImage = new Image(); let pCount = 0; diff --git a/src/components/Items/ItemsArea/index.js b/src/components/Items/ItemsArea/index.js index 001c4e2..0f636d0 100644 --- a/src/components/Items/ItemsArea/index.js +++ b/src/components/Items/ItemsArea/index.js @@ -10,7 +10,7 @@ import { import SoundBlock from '../../SoundBlock'; import ActionButton from '../../ActionButton'; import ZombieFace from '../../../assets/images/zombieFace.png'; -import { BonusDicesType } from '../../../interfaces/types'; +import { BonusDiceType } from '../../../interfaces/types'; import { AppButton } from '../../Sections/PlayersSection/styles'; import { DROP, @@ -43,10 +43,9 @@ import { } from './styles'; const ItemsArea = ({ - actionsLeft, activateDualEffect, allSlotsAreEmpty, - bonusDices, + bonusDice, canAttack, canBeDeflected, canCombine, @@ -102,16 +101,16 @@ const ItemsArea = ({ if (ALL_WEAPONS[item].dice === SPECIAL) { gainCustomXp(BURNEM_ALL); } else { - const totalDices = calculateTotalDices(); + const totalDice = calculateTotalDice(); const currentPool = killButtons.length; - const newArray = [...Array(totalDices).keys()].map( + const newArray = [...Array(totalDice).keys()].map( value => value + currentPool ); toggleFiredDual(true); if (dualWeaponEffect) { - activateDualEffect(totalDices); + activateDualEffect(totalDice); } clearTimeout(killButtonsTimer.current); @@ -126,20 +125,20 @@ const ItemsArea = ({ } }; - const calculateTotalDices = () => { - const { combat, melee, ranged } = bonusDices; - let totalDices; + const calculateTotalDice = () => { + const { combat, melee, ranged } = bonusDice; + let totalDice; - totalDices = dice + combat; + totalDice = dice + combat; if (ALL_WEAPONS[item].attack === MELEE) { - totalDices += melee; + totalDice += melee; } else if (ALL_WEAPONS[item].attack === RANGED) { - totalDices += ranged; + totalDice += ranged; } else if (ALL_WEAPONS[item].attack === MELEE_RANGED) { - totalDices = totalDices + ranged + melee; + totalDice = totalDice + ranged + melee; } - return totalDices; + return totalDice; }; const checkIfReloadIsNeeded = () => ALL_WEAPONS[item] && ALL_WEAPONS[item].needsReloading; @@ -247,11 +246,17 @@ const ItemsArea = ({ }, [forcedKillButtons]); useEffect(() => { + clearTimeout(killButtonsTimer.current); + clearTimeout(dualTimer.current); + toggleFiredDual(); + changeKillButtons([]); return () => { clearTimeout(killButtonsTimer.current); clearTimeout(dualTimer.current); + toggleFiredDual(); + changeKillButtons([]); }; - }, []); + }, [changeKillButtons, charName, item, toggleFiredDual]); return ( null, allSlotsAreEmpty: false, - bonusDices: null, + bonusDice: null, canAttack: false, canBeDeflected: false, canCombine: null, diff --git a/src/components/Sections/PlayersSection/index.js b/src/components/Sections/PlayersSection/index.js index e21ce6a..b2f4eae 100644 --- a/src/components/Sections/PlayersSection/index.js +++ b/src/components/Sections/PlayersSection/index.js @@ -2,7 +2,7 @@ import React, { useContext, useEffect, useRef } from 'react'; import { useHistory } from 'react-router-dom'; import { cloneDeep, isEqual } from 'lodash'; import { arrayOf, bool, func, number, oneOfType, string } from 'prop-types'; -import { ABILITIES_S1 } from '../../../setup/abilities'; +import { ABILITIES_S1, ALL_ABILITIES } from '../../../setup/abilities'; import { blueThreatThresold, calculateXpBar, @@ -24,7 +24,8 @@ import { orangeThreatThresold, useStateWithLabel, useTurnsCounter, - yellowThreatThresold + yellowThreatThresold, + totalActions } from '../../../utils'; import ActionsModal from '../../ActionsModal'; import { SOUNDS } from '../../../assets/sounds'; @@ -45,6 +46,8 @@ import { ADD_CHARACTER, ADD_NEW_CHAR, ADVANCE_LEVEL, + BLITZ_ACTION, + BLITZ_LABEL, BLOCKED, BLOCKED_ONE, BONUS_ACTION, @@ -82,6 +85,8 @@ import { FINISH_SETUP, FIRST_PLAYER_TOKEN, FREE_ATTACK, + FREE_ATTACK_MELEE, + FREE_ATTACK_RANGED, FREE_MOVE, FREE_SEARCH, GAIN_XP, @@ -96,6 +101,8 @@ import { HEAL_CHOOSE, HEAL_WOUND, HIT, + HIT_N_RUN, + HIT_N_RUN_ACTION, INITIAL, IN_HAND, IN_RESERVE, @@ -491,9 +498,9 @@ const PlayersSection = ({ toggleActionsModal('orange'); } else if (char.abilities.length !== 3) { updatedChar.abilities = []; - updatedChar.actions = [3, 0, 0, 0, 0]; - updatedChar.actionsLeft = [3, 0, 0, 0, 0]; - updatedChar.bonusDices = { combat: 0, melee: 0, ranged: 0 }; + updatedChar.actions = [3, 0, [0, 0, 0], 0, 0]; + updatedChar.actionsLeft = [3, 0, [0, 0, 0], 0, 0]; + updatedChar.bonusDice = { combat: 0, melee: 0, ranged: 0 }; updatedChar = handlePromotionEffects( updatedChar, 'blue', @@ -522,9 +529,9 @@ const PlayersSection = ({ ]); } else if (char.abilities.length !== 2) { updatedChar.abilities = []; - updatedChar.actions = [3, 0, 0, 0, 0]; - updatedChar.actionsLeft = [3, 0, 0, 0, 0]; - updatedChar.bonusDices = { combat: 0, melee: 0, ranged: 0 }; + updatedChar.actions = [3, 0, [0, 0, 0], 0, 0]; + updatedChar.actionsLeft = [3, 0, [0, 0, 0], 0, 0]; + updatedChar.bonusDice = { combat: 0, melee: 0, ranged: 0 }; updatedChar = handlePromotionEffects( updatedChar, 'blue', @@ -551,12 +558,12 @@ const PlayersSection = ({ (char.actionsLeft && [...char.actionsLeft]) || [...char.actions] ); } else if (updatedChar.abilities.length === 1) { - return null; + return updatedChar; } else { updatedChar.abilities = []; - updatedChar.actions = [3, 0, 0, 0, 0]; - updatedChar.actionsLeft = [3, 0, 0, 0, 0]; - updatedChar.bonusDices = { combat: 0, melee: 0, ranged: 0 }; + updatedChar.actions = [3, 0, [0, 0, 0], 0, 0]; + updatedChar.actionsLeft = [3, 0, [0, 0, 0], 0, 0]; + updatedChar.bonusDice = { combat: 0, melee: 0, ranged: 0 }; updatedChar = handlePromotionEffects( updatedChar, 'blue', @@ -623,7 +630,7 @@ const PlayersSection = ({ characters.forEach(char => { if (char.name === name) { hasPlayed = !checkIfHasAnyActionLeft( - char.actionsLeft || [3, 0, 0, 0, 0] + char.actionsLeft || [3, 0, [0, 0, 0], 0, 0] ); } }); @@ -764,7 +771,9 @@ const PlayersSection = ({ const actions = { gen: (actionsLeft && actionsLeft[0]) || generalActions, mov: (actionsLeft && actionsLeft[1]) || extraMovementActions, - att: (actionsLeft && actionsLeft[2]) || extraAttackActions, + att: (actionsLeft && actionsLeft[2][0]) || extraAttackActions[0], + mAtt: (actionsLeft && actionsLeft[2][1]) || extraAttackActions[1], + rAtt: (actionsLeft && actionsLeft[2][2]) || extraAttackActions[2], sea: (actionsLeft && actionsLeft[3]) || searchActions, bon: (actionsLeft && actionsLeft[4]) || bonusActions }; @@ -778,6 +787,12 @@ const PlayersSection = ({ for (let i = 1; i <= actions.att; i++) { count.push(FREE_ATTACK); } + for (let i = 1; i <= actions.mAtt; i++) { + count.push(FREE_ATTACK_MELEE); + } + for (let i = 1; i <= actions.rAtt; i++) { + count.push(FREE_ATTACK_RANGED); + } for (let i = 1; i <= actions.sea; i++) { count.push(FREE_SEARCH); } @@ -817,7 +832,11 @@ const PlayersSection = ({ toggleHasKilledZombie(true); updatedCharacter.experience = newXp; - updateData(updatedCharacter); + + updateXpCounter( + calculateXpBar(updatedCharacter.experience, highestXp.xp, device.current) + ); + updateData(advancingLevel(updatedCharacter.experience, updatedCharacter)); }; const getMainButtonText = () => { @@ -867,6 +886,8 @@ const PlayersSection = ({ const setCustomXp = (newXp, prevXp, nextXp) => { const updatedCharacter = cloneDeep(character); let updatedXp = newXp; + let updHighestXp = highestXp.xp; + if (newXp === '...') { if (prevXp === 19 && nextXp === 43) { updatedXp = 20; @@ -886,12 +907,17 @@ const PlayersSection = ({ if (updatedXp > highestXp.xp || highestXp.name === character.name) { updateHighestXp({ name: character.name, xp: updatedXp }); + updHighestXp = updatedXp; } logger(LOG_TYPE_EXTENDED, GAIN_XP, `xp: ${prevXp} newXp: ${updatedXp}`); toggleHasKilledZombie(true); - updateData(updatedCharacter); + + updateXpCounter( + calculateXpBar(updatedCharacter.experience, updHighestXp, device.current) + ); + updateData(advancingLevel(updatedCharacter.experience, updatedCharacter)); }; const setNoise = (noise = 1) => { @@ -999,7 +1025,7 @@ const PlayersSection = ({ toggleChangedCharManually(true); setTimeout(() => toggleChangedCharManually(false), 2000); logger(LOG_TYPE_EXTENDED, CLICK_END_TURN); - updatedCharacter.actionsLeft = [0, 0, 0, 0, 0]; + updatedCharacter.actionsLeft = [0, 0, [0, 0, 0], 0, 0]; updateData(updatedCharacter); if (charsStillToAct.length > 0) { setTimeout(() => changeToAnotherPlayer(NEXT), 1200); @@ -1249,7 +1275,7 @@ const PlayersSection = ({ char => char.name !== character.name && !char.hasLeft ); updChar.hasLeft = true; - updChar.actionsLeft = [0, 0, 0, 0, 0]; + updChar.actionsLeft = [0, 0, [0, 0, 0], 0, 0]; updateCharSaved([...charsSaved, updChar]); updateData(updChar); @@ -1325,6 +1351,11 @@ const PlayersSection = ({ updChar.actionsLeft = newActionsLeft; updateData(updChar); selectSlot(emptySlot); + toggleHasKilledZombie(false); + }; + + const onUsePostKillSkill = () => { + toggleHasKilledZombie(false); }; const setNewChar = updatedCharacters => { @@ -1615,6 +1646,8 @@ const PlayersSection = ({ const updatedCharacter = charBackup ? cloneDeep(charBackup) : cloneDeep(character); + const actionsArray = generateActionsCountArray(); + if (charBackup) { backupChar(); } @@ -1625,10 +1658,25 @@ const PlayersSection = ({ searchActions, bonusActions ]; + + // updatedCharacter.actionsLeft.forEach((action, index) => { + // // Avoid clash of data loop due to async update of actions + // if ( + // index === 2 && + // totalActions(action) > totalActions(updatedCharacter.actions[2]) + // ) { + // updatedCharacter.actions[2] = [...action]; + // } + // if (action > updatedCharacter.actions[index]) { + // updatedCharacter.actions[index] = action; + // } + // }); + changeCharacter(updatedCharacter); - const actionsArray = generateActionsCountArray(); if (!isEqual(actionsArray, actionsCount)) { - updateActionsCount(actionsArray); + updateActionsCount( + generateActionsCountArray(updatedCharacter.actionsLeft) + ); updateData(updatedCharacter); } } @@ -1636,7 +1684,9 @@ const PlayersSection = ({ }, [ generalActions, extraMovementActions, - extraAttackActions, + extraAttackActions[0], + extraAttackActions[1], + extraAttackActions[2], searchActions, bonusActions ]); @@ -1701,7 +1751,9 @@ const PlayersSection = ({ toggleSomeoneIsWounded(characters.some(char => char.wounded)); changeCharacter(nextChar); - + updateXpCounter( + calculateXpBar(nextChar.experience, highestXp.xp, device.current) + ); checkIfCharHasDualEffect([...nextChar.inHand]); setCanOpenDoor(openDoors); toggleCanCombine(charCanCombineItems); @@ -1713,22 +1765,6 @@ const PlayersSection = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [charIndex, characters]); - useEffect(() => { - if (character.experience >= 0) { - const charClone = cloneDeep(character); - const newXpBar = calculateXpBar( - charClone.experience, - highestXp.xp, - device.current - ); - const updatedChar = advancingLevel(charClone.experience, charClone); - - updateXpCounter(newXpBar); - updateData(updatedChar); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [character.experience, updateXpCounter]); - useEffect(() => { if (zombiesArePlaying && extraActivation) { toggleExtraActivation(false); @@ -1895,7 +1931,12 @@ const PlayersSection = ({ )} {character.wounded !== KILLED && ( <> - {character.name} + + {device.current === DESKTOP && character.nickname + ? `"${character.nickname}"` + : ''}{' '} + {character.name} + {character.player} )} @@ -1925,16 +1966,29 @@ const PlayersSection = ({ /> )} - {!!generalActions && - context.rules.winGame && - round >= 5 && ( + {context.rules.winGame && round >= 5 && ( + + )} + + {hasKilledZombie && + character.abilities.includes( + ALL_ABILITIES.BLITZ.name + ) && ( )} @@ -1948,8 +2002,23 @@ const PlayersSection = ({ manyButtons={context.rules.cars} /> )} + {hasKilledZombie && + character.abilities.includes( + ALL_ABILITIES.HIT_N_RUN.name + ) && ( + + )} {!!generalActions && + canSearch && character.abilities.includes( ABILITIES_S1.HOLD_YOUR_NOSE.name ) && @@ -2049,6 +2118,17 @@ const PlayersSection = ({ /> )} + {!!generalActions && context.rules.explosion && ( + + )} + {!finishedTurn && context.rules.objectives && !!generalActions && ( @@ -2259,7 +2339,7 @@ const PlayersSection = ({ ...character.inHand, ...character.inReserve ])} - bonusDices={character.bonusDices} + bonusDice={character.bonusDice} canAttack={canAttack} canBeDeflected={ (character.abilities.includes( @@ -2292,6 +2372,7 @@ const PlayersSection = ({ inHandItem === ALL_ITEMS.PoliceRiotShield.name ) } + charName={character.name} charVoice={character.voice} damageMode={damageMode} device={device.current} @@ -2559,8 +2640,16 @@ const PlayersSection = ({ character.abilities[0] === character.promotions.blue.name } + textLength={character.promotions.blue.name.length} > - + {character.promotions.blue.name} @@ -2571,8 +2660,16 @@ const PlayersSection = ({ character.abilities[1] === character.promotions.yellow.name } + textLength={character.promotions.yellow.name.length} > - + {character.promotions.yellow.name} @@ -2584,8 +2681,15 @@ const PlayersSection = ({ character.abilities[2] === promo.name } key={`promo-orange-${promo.name.replace(' ', '-')}`} + textLength={promo.name.length} > - + {promo.name} ))} @@ -2598,8 +2702,15 @@ const PlayersSection = ({ character.abilities[3] === promo.name } key={`promo-orange-${promo.name.replace(' ', '-')}`} + textLength={promo.name.length} > - + {promo.name} ))} diff --git a/src/components/Sections/PlayersSection/styles.js b/src/components/Sections/PlayersSection/styles.js index 3a9ab83..3a7d3eb 100644 --- a/src/components/Sections/PlayersSection/styles.js +++ b/src/components/Sections/PlayersSection/styles.js @@ -1238,6 +1238,12 @@ export const LevelIndicator = styled.div` } }}; + ${({ active }) => + !active && + css` + opacity: 0.3; + `} + @media all and (min-width: 1200px) { margin: 0 10px 0 0; transform: translate(0, -2px); @@ -1374,9 +1380,11 @@ export const MovementIcon = styled.div` background: ${({ color }) => color}; color: ${({ type }) => (typeof type === 'number' ? 'white' : 'black')}; font-weight: 700; - line-height: 1.1; - font-size: 0.7rem; - font-family: 'Cairo', sans-serif; + line-height: 1.3; + letter-spacing: 0.001rem; + font-size: 0.8rem; + font-family: 'Grandstander', cursive; + text-transform: uppercase; &:not(:first-of-type) { margin-left: 15px; @@ -1685,6 +1693,12 @@ export const PromoWrapper = styled.div` letter-spacing: 0.1rem; font-size: 0.9rem; + ${({ textLength }) => + textLength > 15 && + css` + letter-spacing: 0.01rem; + `} + ${({ active }) => active && css` diff --git a/src/components/SoundBlock/index.js b/src/components/SoundBlock/index.js index ce3bbc1..eaf5828 100644 --- a/src/components/SoundBlock/index.js +++ b/src/components/SoundBlock/index.js @@ -7,14 +7,18 @@ import { ACTIVATE, ACTIVATIONS, ATTACK, + ATTACK_MELEE, + ATTACK_RANGED, + ATTACK_SURVIVOR, COMBINE_ACTION, - IN_RESERVE, IN_HAND, + IN_RESERVE, ITEMS, KILL, + MELEE, + RANGED, WEAPONS, - WOUND, - ATTACK_SURVIVOR + WOUND } from '../../constants'; import { Action, @@ -22,6 +26,7 @@ import { ZombieImageForMobile } from '../Sections/ZombiesSection/styles'; import { Block, PlayImageButton, PlayIcon, PlayText, ItemIcon } from './styles'; +import { ALL_WEAPONS } from '../../setup/weapons'; const SoundBlock = ({ activateKillButtons, @@ -30,6 +35,7 @@ const SoundBlock = ({ canBeDeflected, canCombine, charCanDeflect, + charName, combineItemSelected, combinePair, damageMode, @@ -129,7 +135,11 @@ const SoundBlock = ({ quickAttackDebounce.current = false; }, 1000); activateKillButtons(); - callback(ATTACK); + if (ALL_WEAPONS[name].attack === MELEE) { + callback(ATTACK_MELEE); + } else if (ALL_WEAPONS[name].attack === RANGED) { + callback(ATTACK_RANGED); + } } if (filename && type === WEAPONS && canAttack && useAlternativeSound) { @@ -173,6 +183,7 @@ const SoundBlock = ({ }, 4000); } }; + useEffect(() => { if (!sound.current || currentRound.current !== round) { sound.current = new Audio( @@ -186,12 +197,17 @@ const SoundBlock = ({ }, [filename, differentSounds, round, toggleAlternativeSound]); useEffect(() => { + clearTimeout(activateTimeout.current); + clearTimeout(attackButtonsTimeout.current); + clearTimeout(quickAttackDebounceTimeout.current); + activate(false); return () => { clearTimeout(activateTimeout.current); clearTimeout(attackButtonsTimeout.current); clearTimeout(quickAttackDebounceTimeout.current); + activate(false); }; - }, []); + }, [activate, charName]); return ( `ROUNDS: ${rounds} (${time})`; export const GAME_RULES_TITLE = 'Game rules'; export const GAME_SETS = 'Game sets'; diff --git a/src/interfaces/types.js b/src/interfaces/types.js index 3146628..02c75c6 100644 --- a/src/interfaces/types.js +++ b/src/interfaces/types.js @@ -32,7 +32,7 @@ export const ModalContentType = shape({ buttons: arrayOf(ModalButtonType) }); -export const BonusDicesType = shape({ +export const BonusDiceType = shape({ combat: number.isRequired, melee: number.isRequired, ranged: number.isRequired diff --git a/src/setup/abilities.js b/src/setup/abilities.js index d0e921e..16cd7d1 100644 --- a/src/setup/abilities.js +++ b/src/setup/abilities.js @@ -1,353 +1,417 @@ -const ACTION = { - name: '+1 Action', - description: 'The Survivor has an extra Action he may use as he pleases.', - effect: ([gen, mov, att, sea, bon]) => [gen + 1, mov, att, sea, bon] -}; - -const DAMAGE_MELEE = { - name: '+1 Damage: Melee', - description: 'The Survivor gets a +1 Damage bonus with Melee attacks.' -}; - -const DAMAGE_RANGED = { - name: '+1 Damage: ranged', - description: 'The Survivor gets a +1 Damage bonus with Ranged attacks.' -}; - -const DICE_ROLL_COMBAT = { - name: '+1 to dice: Combat', - description: - 'The Survivor adds 1 to the result of each die he rolls on a Combat Action (Melee or Ranged). The maximum result is always 6.' -}; - -const DICE_ROLL_MEELEE = { - name: '+1 to dice: Melee', - description: - 'The Survivor adds 1 to the result of each die he rolls in Melee Combat. The maximum result is always 6.' -}; -const DICE_ROLL_RANGED = { - name: '+1 to dice: Ranged', - description: - 'The Survivor adds 1 to the result of each die he rolls in Ranged Combat. The maximum result is always 6.' -}; -const DIE_COMBAT = { - name: '+1 die: Combat', - description: - 'The Survivor’s weapons roll an extra die in Combat (Melee or Ranged). Dual weapons gain a die each, for a total of +2 dice per Dual Combat Action.', - effect: ({ combat, melee, ranged }) => ({ combat: combat + 1, melee, ranged }) -}; -const DIE_MELEE = { - name: '+1 die: Melee', - description: - 'The Survivor’s Melee weapons rolls an extra die in Combat. Dual melee weapons gain a die each, for a total of +2 dice per Dual Melee Combat Action.', - effect: ({ combat, melee, ranged }) => ({ combat, melee: melee + 1, ranged }) -}; -const DIE_RANGED = { - name: '+1 die: Ranged', - description: - 'The Survivor’s Ranged weapons roll an extra die in Combat. Dual ranged weapons gain a die each, for a total of +2 dice per Dual Ranged Combat Action.', - effect: ({ combat, melee, ranged }) => ({ combat, melee, ranged: ranged + 1 }) -}; -const COMBAT_ACTION = { - name: '+1 free Combat Action', - description: - 'The Survivor has one free extra Combat Action. This Action may only be used for Melee or Ranged Combat.', - effect: ([gen, mov, att, sea, bon]) => [gen, mov, att + 1, sea, bon] -}; -const MOVE_ACTION = { - name: '+1 free Move Action', - description: - 'The Survivor has one free extra Move Action. This Action may only be used as a Move Action.', - effect: ([gen, mov, att, sea, bon]) => [gen, mov + 1, att, sea, bon] -}; -const SEARCH_ACTION = { - name: '+1 free Search Action', - description: - 'The Survivor has one free extra Search Action. This Action may only be used to Search and the Survivor can still only Search once per turn.', - effect: ([gen, mov, att, sea, bon]) => [gen, mov, att, sea + 1, bon] -}; -const MAX_RANGE = { - name: '+1 max Range', - description: 'The Survivor’s Ranged weapons’ maximum Range is increased by 1.' -}; -const ZONE_PER_MOVE = { - name: '+1 Zone per Move', - description: - 'The Survivor can move through one extra Zone each time he performs a Move Action. This Skill stacks with other effects benefitting Move Actions.' -}; -const RE_ROLL = { - name: '1 re-roll / turn', - description: - 'Once per turn, you can re-roll all the dice related to the resolution of an Action made by the Survivor. The new result takes the place of the previous one. This Skill stacks with the effects of Equipment that allow re-rolls.' -}; -const TWO_COCKTAILS = { - name: '2 cocktails are better than 1', - description: - 'The Survivor gets two Molotov cards instead of one when he creates a Molotov.' -}; -const TWO_ZONES_MOVE = { - name: '2 Zones per Move Action', - description: - 'When the Survivor spends one Action to Move, he can move one or two Zones instead of one.' -}; -const AMBIDEXTROUS = { - name: 'Ambidextrous', - description: - 'The Survivor treats all Melee and Ranged weapons as if they had the Dual symbol ' -}; -const BORN_LEADER = { - name: 'Born leader', - description: - 'During the Survivor’s turn, he may give one free Action to another Survivor, to use as he pleases. This Action must be used during the recipient’s next turn or it is lost.' -}; -const DESTINY = { - name: 'Destiny', - description: - 'The Survivor can use this Skill once per turn when he reveals an Equipment card he drew. Discard that card and draw another Equipment card.' -}; -const GUNSLINGER = { - name: 'Gunslinger', - description: - ' The Survivor treats all Ranged weapons as if they had the Dual symbol' -}; -const HOARD = { - name: 'Hoard', - description: ' The Survivor can carry one extra Equipment card in reserve', - effect: itemsInReserve => [...itemsInReserve, null] -}; -const HOLD_YOUR_NOSE = { - name: 'Hold your nose', - description: - 'This Skill can be used once per turn. The Survivor gets a free Search Action in the Zone if he has eliminated a Zombie (even outside a building) the very same turn. This Action may only be used to Search and the Survivor can still only Search once per turn.' -}; -const ALL_YOUVE_GOT = { - name: "Is that all you've got?", - description: - 'You can use this Skill any time the Survivor is about to get Wounded cards. Discard one Equipment card in your Survivor’s inventory for each Wound he’s about to receive. Negate a Wounded card per discarded Equipment card.' -}; -const LOCK_IT_DOWN = { - name: 'Lock it down', - description: - 'At the cost of one Action, the Survivor can close an open door. Opening it again later does not trigger a new Zombie Spawn.' -}; -const LOUD = { - name: 'Loud', - description: - 'Once per turn, the Survivor can make a huge amount of noise! Until this Survivor’s next turn, the Zone he used this Skill in is considered to have the highest number of Noise tokens on the entire map. If different Survivors have this Skill, only the last one who used it applies the effects.' -}; -const LUCKY = { - name: 'Lucky', - description: - 'The Survivor can re-roll once all the dice of each Action he takes. The new result takes the place of the previous one. This Skill stacks with the effects of other Skills (“1 re-roll per turn”, for example) and Equipment that allows re-rolls. ' -}; -const MATCHING_SET = { - name: 'Matching Set', - description: - 'When a Survivor performs a Search Action and draws a weapon card with the Dual symbol, he can immediately take a second card of the same type from the Equipment deck. Shuffle the deck afterward.', - effect: () => 'matchingSet' -}; -const MEDIC = { - name: 'Medic', - description: - 'Once per turn, the Survivor can freely remove one Wounded card from a Survivor in the same Zone. He may also heal himself' -}; -const NINJA = { - name: 'Ninja', - description: - ' The Survivor makes no Noise. At all. His miniature does not count as a Noise token, and his use of Equipment or weapons produces no Noise tokens either! The Survivor may choose not to use this Skill at any time, if he wishes to be noisy.' -}; -const SLIPPERY = { - name: 'Slippery', - description: - ' The Survivor does not spend extra Actions when he performs a Move Action through a Zone where there are Zombies' -}; -const SNIPER = { - name: 'Sniper', - description: - 'The Survivor may freely choose the targets of all his Ranged Combat Actions.' -}; - -const STARTS_WITH = equipment => ({ - name: `Starts with ${equipment}`, - description: - ' The Survivor begins the game with the indicated Equipment; its card is automatically assigned to him before the beginning of the game.', - effect: () => equipment -}); - -const SWORDMASTER = { - name: 'Swordmaster', - description: - 'The Survivor treats all Melee weapons as if they had the Dual symbol ' -}; -const TOUGH = { - name: 'Tough', - description: - 'The Survivor ignores the first Attack he receives from a single Zombie every Zombies’ Phase' -}; -const TRICK_SHOT = { - name: 'Trick shot', - description: - 'When the Survivor is equipped with Dual Ranged weapons, he can aim at targets in different Zones with each weapon in the same Action.' -}; - export const ABILITIES_S1 = { - ACTION, - DAMAGE_MELEE, - DAMAGE_RANGED, - DICE_ROLL_COMBAT, - DICE_ROLL_MEELEE, - DICE_ROLL_RANGED, - DIE_COMBAT, - DIE_MELEE, - DIE_RANGED, - COMBAT_ACTION, - MOVE_ACTION, - SEARCH_ACTION, - MAX_RANGE, - ZONE_PER_MOVE, - RE_ROLL, - TWO_COCKTAILS, - TWO_ZONES_MOVE, - AMBIDEXTROUS, - BORN_LEADER, - DESTINY, - GUNSLINGER, - HOARD, - HOLD_YOUR_NOSE, - ALL_YOUVE_GOT, - LOCK_IT_DOWN, - LOUD, - LUCKY, - MATCHING_SET, - MEDIC, - NINJA, - SLIPPERY, - SNIPER, - STARTS_WITH, - SWORDMASTER, - TOUGH, - TRICK_SHOT -}; - -const ACTION_MELEE = { - name: '+1 free Melee Action', - description: - 'The Survivor has one extra free Melee Combat Action. This Action may only be used for Melee Combat.', - effect: ([gen, mov, att, sea, bon]) => [ - gen, - mov, - [att[0], att[1] + 1, att[2]], - sea, - bon - ] -}; - -const ACTION_RANGED = { - name: '+1 free Ranged Action', - description: - 'The Survivor has one extra, free Ranged Combat Action. This Action can only be used for Ranged Combat.' -}; - -const SUPER_STRENGTH = { - name: 'Super strength', - description: - 'Consider the Damage value of Melee weapons used by the Survivor to be 3.' -}; - -const WEBBING = { - name: 'Webbing', - description: - 'All equipment in the Survivor’s inventory is considered equipped in hand.' -}; - -const ZOMBIE_LINK = { - name: 'Zombie link', - description: - 'The Survivor plays an extra turn each time an extra activation card is drawn in the Zombie pile.' + ACTION: { + name: '+1 Action', + description: 'The Survivor has an extra Action he may use as he pleases.', + effect: ([gen, mov, att, sea, bon]) => [gen + 1, mov, att, sea, bon] + }, + DAMAGE_MELEE: { + name: '+1 Damage: Melee', + description: 'The Survivor gets a +1 Damage bonus with Melee attacks.' + }, + DAMAGE_RANGED: { + name: '+1 Damage: ranged', + description: 'The Survivor gets a +1 Damage bonus with Ranged attacks.' + }, + DICE_ROLL_COMBAT: { + name: '+1 to dice: Combat', + description: + 'The Survivor adds 1 to the result of each die he rolls on a Combat Action (Melee or Ranged). The maximum result is always 6.' + }, + DICE_ROLL_MEELEE: { + name: '+1 to dice: Melee', + description: + 'The Survivor adds 1 to the result of each die he rolls in Melee Combat. The maximum result is always 6.' + }, + DICE_ROLL_RANGED: { + name: '+1 to dice: Ranged', + description: + 'The Survivor adds 1 to the result of each die he rolls in Ranged Combat. The maximum result is always 6.' + }, + DIE_COMBAT: { + name: '+1 die: Combat', + description: + 'The Survivor’s weapons roll an extra die in Combat (Melee or Ranged). Dual weapons gain a die each, for a total of +2 dice per Dual Combat Action.', + effect: ({ combat, melee, ranged }) => ({ + combat: combat + 1, + melee, + ranged + }) + }, + DIE_MELEE: { + name: '+1 die: Melee', + description: + 'The Survivor’s Melee weapons rolls an extra die in Combat. Dual melee weapons gain a die each, for a total of +2 dice per Dual Melee Combat Action.', + effect: ({ combat, melee, ranged }) => ({ + combat, + melee: melee + 1, + ranged + }) + }, + DIE_RANGED: { + name: '+1 die: Ranged', + description: + 'The Survivor’s Ranged weapons roll an extra die in Combat. Dual ranged weapons gain a die each, for a total of +2 dice per Dual Ranged Combat Action.', + effect: ({ combat, melee, ranged }) => ({ + combat, + melee, + ranged: ranged + 1 + }) + }, + COMBAT_ACTION: { + name: '+1 free Combat Action', + description: + 'The Survivor has one free extra Combat Action. This Action may only be used for Melee or Ranged Combat.', + effect: ([gen, mov, att, sea, bon]) => [ + gen, + mov, + [att[0] + 1, att[1], att[2]], + sea, + bon + ] + }, + MOVE_ACTION: { + name: '+1 free Move Action', + description: + 'The Survivor has one free extra Move Action. This Action may only be used as a Move Action.', + effect: ([gen, mov, att, sea, bon]) => [gen, mov + 1, att, sea, bon] + }, + SEARCH_ACTION: { + name: '+1 free Search Action', + description: + 'The Survivor has one free extra Search Action. This Action may only be used to Search and the Survivor can still only Search once per turn.', + effect: ([gen, mov, att, sea, bon]) => [gen, mov, att, sea + 1, bon] + }, + MAX_RANGE: { + name: '+1 max Range', + description: + 'The Survivor’s Ranged weapons’ maximum Range is increased by 1.' + }, + ZONE_PER_MOVE: { + name: '+1 Zone per Move', + description: + 'The Survivor can move through one extra Zone each time he performs a Move Action. This Skill stacks with other effects benefitting Move Actions.' + }, + RE_ROLL: { + name: '1 re-roll / turn', + description: + 'Once per turn, you can re-roll all the dice related to the resolution of an Action made by the Survivor. The new result takes the place of the previous one. This Skill stacks with the effects of Equipment that allow re-rolls.' + }, + TWO_COCKTAILS: { + name: '2 cocktails are better than 1', + description: + 'The Survivor gets two Molotov cards instead of one when he creates a Molotov.' + }, + TWO_ZONES_MOVE: { + name: '2 Zones per Move Action', + description: + 'When the Survivor spends one Action to Move, he can move one or two Zones instead of one.' + }, + AMBIDEXTROUS: { + name: 'Ambidextrous', + description: + 'The Survivor treats all Melee and Ranged weapons as if they had the Dual symbol ' + }, + BORN_LEADER: { + name: 'Born leader', + description: + 'During the Survivor’s turn, he may give one free Action to another Survivor, to use as he pleases. This Action must be used during the recipient’s next turn or it is lost.' + }, + DESTINY: { + name: 'Destiny', + description: + 'The Survivor can use this Skill once per turn when he reveals an Equipment card he drew. Discard that card and draw another Equipment card.' + }, + GUNSLINGER: { + name: 'Gunslinger', + description: + ' The Survivor treats all Ranged weapons as if they had the Dual symbol' + }, + HOARD: { + name: 'Hoard', + description: ' The Survivor can carry one extra Equipment card in reserve', + effect: itemsInReserve => [...itemsInReserve, null] + }, + HOLD_YOUR_NOSE: { + name: 'Hold your nose', + description: + 'This Skill can be used once per turn. The Survivor gets a free Search Action in the Zone if he has eliminated a Zombie (even outside a building) the very same turn. This Action may only be used to Search and the Survivor can still only Search once per turn.' + }, + ALL_YOUVE_GOT: { + name: "Is that all you've got?", + description: + 'You can use this Skill any time the Survivor is about to get Wounded cards. Discard one Equipment card in your Survivor’s inventory for each Wound he’s about to receive. Negate a Wounded card per discarded Equipment card.' + }, + LOCK_IT_DOWN: { + name: 'Lock it down', + description: + 'At the cost of one Action, the Survivor can close an open door. Opening it again later does not trigger a new Zombie Spawn.' + }, + LOUD: { + name: 'Loud', + description: + 'Once per turn, the Survivor can make a huge amount of noise! Until this Survivor’s next turn, the Zone he used this Skill in is considered to have the highest number of Noise tokens on the entire map. If different Survivors have this Skill, only the last one who used it applies the effects.' + }, + LUCKY: { + name: 'Lucky', + description: + 'The Survivor can re-roll once all the dice of each Action he takes. The new result takes the place of the previous one. This Skill stacks with the effects of other Skills (“1 re-roll per turn”, for example) and Equipment that allows re-rolls. ' + }, + MATCHING_SET: { + name: 'Matching Set', + description: + 'When a Survivor performs a Search Action and draws a weapon card with the Dual symbol, he can immediately take a second card of the same type from the Equipment deck. Shuffle the deck afterward.', + effect: () => 'matchingSet' + }, + MEDIC: { + name: 'Medic', + description: + 'Once per turn, the Survivor can freely remove one Wounded card from a Survivor in the same Zone. He may also heal himself' + }, + NINJA: { + name: 'Ninja', + description: + ' The Survivor makes no Noise. At all. His miniature does not count as a Noise token, and his use of Equipment or weapons produces no Noise tokens either! The Survivor may choose not to use this Skill at any time, if he wishes to be noisy.' + }, + SLIPPERY: { + name: 'Slippery', + description: + ' The Survivor does not spend extra Actions when he performs a Move Action through a Zone where there are Zombies' + }, + SNIPER: { + name: 'Sniper', + description: + 'The Survivor may freely choose the targets of all his Ranged Combat Actions.' + }, + STARTS_WITH: equipment => ({ + name: `Starts with ${equipment}`, + description: + ' The Survivor begins the game with the indicated Equipment; its card is automatically assigned to him before the beginning of the game.', + effect: () => equipment + }), + SWORDMASTER: { + name: 'Swordmaster', + description: + 'The Survivor treats all Melee weapons as if they had the Dual symbol ' + }, + TOUGH: { + name: 'Tough', + description: + 'The Survivor ignores the first Attack he receives from a single Zombie every Zombies’ Phase' + }, + TRICK_SHOT: { + name: 'Trick shot', + description: + 'When the Survivor is equipped with Dual Ranged weapons, he can aim at targets in different Zones with each weapon in the same Action.' + } }; export const ABILITIES_MALL = { - ACTION_MELEE, - ACTION_RANGED, - SUPER_STRENGTH, - WEBBING, - ZOMBIE_LINK + ACTION_MELEE: { + name: '+1 free Melee Action', + description: + 'The Survivor has one extra free Melee Combat Action. This Action may only be used for Melee Combat.', + effect: ([gen, mov, att, sea, bon]) => [ + gen, + mov, + [att[0], att[1] + 1, att[2]], + sea, + bon + ] + }, + ACTION_RANGED: { + name: '+1 free Ranged Action', + description: + 'The Survivor has one extra, free Ranged Combat Action. This Action can only be used for Ranged Combat.', + effect: ([gen, mov, att, sea, bon]) => [ + gen, + mov, + [att[0], att[1], att[2] + 1], + sea, + bon + ] + }, + LOW_PROFILE: { + name: 'Low Profile', + description: + 'The Survivor can’t be targeted by Survivors’ Ranged Attacks and can’t be hit by car attacks (in both case, even by rival Survivors’ Attacks). Ignore him when shooting in or driving through the Zone he stands in. Weapons that kill everything in the targeted Zone, like the Molotov, still kill him, though.' + }, + SUPER_STRENGTH: { + name: 'Super strength', + description: + 'Consider the Damage value of Melee weapons used by the Survivor to be 3.' + }, + WEBBING: { + name: 'Webbing', + description: + 'All equipment in the Survivor’s inventory is considered equipped in hand.' + }, + ZOMBIE_LINK: { + name: 'Zombie link', + description: + 'The Survivor plays an extra turn each time an extra activation card is drawn in the Zombie pile.' + } }; -const REAPER_RANGED = { - name: 'Reaper: Ranged', - description: - 'Use this Skill when assigning hits while resolving a Ranged Action. One of these hits can freely kill an additional identical Zombie in the same Zone. Only a single additional Zombie can be killed per Action when using this Skill.' -}; -const BLITZ = { - name: 'Blitz', - description: - 'Each time your Survivor kills the last Zombie of a Zone, he gets 1 free Move Action to use immediately' -}; -const BLOODLUST_MELEE = { - name: 'Bloodlust: Melee', - description: - 'Spend one Action with the Survivor: He moves up to two Zones to a Zone containing at least one Zombie (or rival Survivor). He then gains one free Melee Action' -}; -const REAPER_COMBAT = { - name: 'Reaper: Combat', - description: - 'Use this Skill when assigning hits while resolving a Combat Action. One of these hits can freely kill an additional identical Zombie in the same Zone. Only a single additional Zombie can be killed per Action when using this Skill..', - effect: ({ combat, melee, ranged }) => ({ combat: combat + 1, melee, ranged }) -}; -const REAPER_MELEE = { - name: 'Reaper: Melee', - description: - 'Use this Skill when assigning hits while resolving a Ranged Action. One of these hits can freely kill an additional identical Zombie in the same Zone. Only a single additional Zombie can be killed per Action when using this Skill.', - effect: ({ combat, melee, ranged }) => ({ combat, melee: melee + 1, ranged }) -}; -const REAPER_RANGED = { - name: 'Reaper: Ranged', - description: - 'Use this Skill when assigning hits while resolving a Ranged Action. One of these hits can freely kill an additional identical Zombie in the same Zone. Only a single additional Zombie can be killed per Action when using this Skill.', - effect: ({ combat, melee, ranged }) => ({ combat, melee, ranged: ranged + 1 }) -}; -const TAUNT = { - name: 'Taunt', - description: - 'The Survivor can use this Skill, for free, once during each of his Activations. Select a Zone your Survivor can see. All Zombies standing in the selected Zone immediately gain an extra Activation: They try to reach the taunting Survivor by any means available. Taunted Zombies ignore all other Survivors. They do not attack them and cross the Zone they stand in if needed to reach the taunting Survivor.' -}; -const TACTICIAN = { - name: 'Tactician', - description: - 'The Survivor’s turn can be resolved anytime during the Players’ Phase, before or after any other Survivor’s turn. If several teammates benefit from this Skill at the same time, the team’s players choose their activation order.' -}; export const ABILITIES_S2 = { - BLITZ, - BLOODLUST_MELEE, - REAPER_COMBAT, - REAPER_MELEE, - REAPER_RANGED, - TAUNT, - TACTICIAN + BLITZ: { + name: 'Blitz', + description: + 'Each time your Survivor kills the last Zombie of a Zone, he gets 1 free Move Action to use immediately' + }, + BLOODLUST_MELEE: { + name: 'Bloodlust: Melee', + description: + 'Spend one Action with the Survivor: He moves up to two Zones to a Zone containing at least one Zombie (or rival Survivor). He then gains one free Melee Action' + }, + FRENZY_COMBAT: { + name: 'Frenzy: Combat', + description: + 'All weapons the Survivor carries gain +1 die per Wound the Survivor suffers. Dual weapons gain a die each, for a total of +2 dice per Wound and per Dual Combat Action.' + }, + FRENZY_MELEE: { + name: 'Frenzy: Melee', + description: + 'All weapons the Survivor carries gain +1 die per Wound the Survivor suffers. Dual Melee weapons gain a die each, for a total of +2 dice per Wound and per Dual Melee Action.' + }, + FRENZY_RANGED: { + name: 'Frenzy: Ranged', + description: + 'All weapons the Survivor carries gain +1 die per Wound the Survivor suffers. Dual Ranged weapons gain a die each, for a total of +2 dice per Wound and per Dual Ranged Action.' + }, + LIFE_SAVER: { + name: 'Life Saver', + description: + 'The Survivor can use this Skill, for free, once during each of his Activations. Select a Zone containing at least one Zombie at Range 1 from your Survivor. Choose Survivors in the selected Zone to be dragged to your Survivor’s Zone without penalty. This is not a Move Action. A Survivor can decline the rescue and stay in the selected Zone if his controller chooses. Both Zones need to share a clear path.' + }, + POINT_BLANK: { + name: 'Point Blank', + description: + 'When firing at Range 0, the Survivor freely chooses the targets of his Ranged Combat Actions and can kill any type of Zombies (including Berserker Zombies). His Ranged weapons still need to inflict enough Damage to kill his targets.' + }, + REAPER_COMBAT: { + name: 'Reaper: Combat', + description: + 'Use this Skill when assigning hits while resolving a Combat Action. One of these hits can freely kill an additional identical Zombie in the same Zone. Only a single additional Zombie can be killed per Action when using this Skill..', + effect: ({ combat, melee, ranged }) => ({ + combat: combat + 1, + melee, + ranged + }) + }, + REAPER_MELEE: { + name: 'Reaper: Melee', + description: + 'Use this Skill when assigning hits while resolving a Ranged Action. One of these hits can freely kill an additional identical Zombie in the same Zone. Only a single additional Zombie can be killed per Action when using this Skill.', + effect: ({ combat, melee, ranged }) => ({ + combat, + melee: melee + 1, + ranged + }) + }, + REAPER_RANGED: { + name: 'Reaper: Ranged', + description: + 'Use this Skill when assigning hits while resolving a Ranged Action. One of these hits can freely kill an additional identical Zombie in the same Zone. Only a single additional Zombie can be killed per Action when using this Skill.', + effect: ({ combat, melee, ranged }) => ({ + combat, + melee, + ranged: ranged + 1 + }) + }, + SHOVE: { + name: 'Shove', + description: + 'The Survivor can use this Skill, for free, once during each of his Activations. Select a Zone at Range 1 from your Survivor. All Zombies standing in your Survivor’s Zone are pushed to the selected Zone. This is not a Movement. Both Zones need to share a clear path. A Zombie can’t cross barricades (see barricades), fences, closed doors, or walls but can be shoved out of a hole.' + }, + TAUNT: { + name: 'Taunt', + description: + 'The Survivor can use this Skill, for free, once during each of his Activations. Select a Zone your Survivor can see. All Zombies standing in the selected Zone immediately gain an extra Activation: They try to reach the taunting Survivor by any means available. Taunted Zombies ignore all other Survivors. They do not attack them and cross the Zone they stand in if needed to reach the taunting Survivor.' + }, + TACTICIAN: { + name: 'Tactician', + description: + 'The Survivor’s turn can be resolved anytime during the Players’ Phase, before or after any other Survivor’s turn. If several teammates benefit from this Skill at the same time, the team’s players choose their activation order.' + } }; -const HIT_N_RUN = { - name: 'Hit & Run', - description: - 'The Survivor can use this Skill for free, just after he resolved Melee or Ranged Combat Action resulting in at least a Zombie kill (or a rival Survivor kill). He can then resolve a free Move Action. The Survivor does not spend extra Actions to perform this free Move Action if Zombies are standing in his Zone.' -}; -const SEARCH_PLUS_ONE = { - name: 'Search: +1 card', - description: 'Draw an extra card when Searching with the Survivor.' -}; export const ABILITIES_S3 = { - HIT_N_RUN, - SEARCH_PLUS_ONE + FREE_RELOAD: { + name: 'Free Reload', + description: + 'The Survivor reloads reloadable weapons (Double Barrel, Mac-10, Sawed-Off, etc.) for free.' + }, + HIT_N_RUN: { + name: 'Hit & Run', + description: + 'The Survivor can use this Skill for free, just after he resolved Melee or Ranged Combat Action resulting in at least a Zombie kill (or a rival Survivor kill). He can then resolve a free Move Action. The Survivor does not spend extra Actions to perform this free Move Action if Zombies are standing in his Zone.' + }, + SCAVENGER: { + name: 'Scavenger', + description: + 'The Survivor can Search in any Zone. This includes street Zones, indoor alleys, hospital Zones, helipads, tents, etc.' + }, + SEARCH_PLUS_ONE: { + name: 'Search: +1 card', + description: 'Draw an extra card when Searching with the Survivor.' + }, + SPRINT: { + name: 'Sprint', + description: + 'The Survivor can use this Skill once during each of his Activations. Spend one Move Action with the Survivor: He may move one, two, or three Zones instead of one. Entering a Zone containing Zombies ends the Survivor’s Move Action.' + } }; -const CANT_BE_THAT_UNLUCKY = { - name: "Can't be THAT unlucy", - description: - 'Unlucky with dice results? Not anymore... Re-roll once any 1 on dice rolls.' -}; export const NIGHT_SHIFT = { - CANT_BE_THAT_UNLUCKY + CANT_BE_THAT_UNLUCKY: { + name: "Can't be THAT unlucy", + description: + 'Unlucky with dice results? Not anymore... Re-roll once any 1 on dice rolls.' + }, + PEEK_OUT_THE_WINDOW: { + name: 'Peek out the window', + description: + 'Unlucky with dice results? Not anymore... Re-roll once any 1 on dice rolls.' + }, + AGILITY: { + name: 'Agility', + description: + 'Character can dodge incoming melee attacks with 4+ on one die.' + }, + CHARISMATIC: { + name: 'Charismatic', + description: + 'Civilians found by this character are always friendly and calmed down, and will never leave the group behind. Civilians following a charismatic leader will not run away, even if they get scared. Gain +1 progression point per mission.' + }, + INFLUENCER: { + name: 'Influencer', + description: + 'One on a round, the character can spend one action to give a free use of one owned skill to another. The influenced character can use once the skill as it was their own. Note that any skills with specific requirements still apply (for example, you cannot influence someone with only melee weapons to do a ranged action).' + }, + NIGHT_VISION: { + name: 'Night vision', + description: + "The character's eyes gets very easily used to darkness and so they're not affected by any penalties applied on dark environments." + }, + PICKPOCKETING: { + name: 'Pickpocketing', + description: 'The character can take for free any items from other playes.' + }, + RUN_TO_DAYLIGHT: { + name: 'Run to daylight', + description: + 'The character can spend one action point and run two zones through an area infested with zombies. A die is rolled for each zombie. Any 4+ knocks down a zombie, but if the character rolls three 1s on the same dice roll, that means that the zombies have blocked the character advance and he is pinned on the zombies zone. Also throw a dice for fatties and Abominations, but they cannot be knocked down.' + }, + SHOOT_FROM_THE_HIP: { + name: 'Shoot from the hip', + description: + 'Any handgun can be quickly discharged without taking time to aim. It gives an additional +2 dice to roll but raises the difficult by one. With dual guns, can be used with both.' + }, + URBAN_SURVIVOR: { + name: 'Urban survivor', + description: + 'The characters knows where people use to hide their stuff. Thus, on any search, he can state exactly what kind of item he is looking for (weapons/ammo, items, civilians, food). He then may discard any search card that is not of the desired type (except for Aahh cards).' + } }; export const ALL_ABILITIES = { diff --git a/src/setup/characters.js b/src/setup/characters.js index 9d47420..54b96bc 100644 --- a/src/setup/characters.js +++ b/src/setup/characters.js @@ -1,48 +1,57 @@ /* eslint-disable no-unused-vars */ import Amy from '../assets/images/survivors/amy.png'; +import Anya from '../assets/images/survivors/anya.png'; import Ben from '../assets/images/survivors/ben.png'; +import BillCash from '../assets/images/survivors/billcash.png'; +import Bob from '../assets/images/survivors/bob.png'; import Clara from '../assets/images/survivors/clara.png'; import Debra from '../assets/images/survivors/debra.png'; import Doug from '../assets/images/survivors/doug.png'; -import Duke from '../assets/images/survivors/duke.png'; import Josh from '../assets/images/survivors/josh.png'; +import Krys from '../assets/images/survivors/krys.png'; import Mary from '../assets/images/survivors/mary.png'; import Ned from '../assets/images/survivors/ned.png'; import Phil from '../assets/images/survivors/phil.png'; import Ruiz from '../assets/images/survivors/ruiz.png'; -import Tommy from '../assets/images/survivors/tommy.png'; +import Sternkova from '../assets/images/survivors/sternkova.png'; import Wanda from '../assets/images/survivors/wanda.png'; -import AmyFace from '../assets/images/survivors/amy-face.png'; -import BenFace from '../assets/images/survivors/ben-face.png'; -import ClaraFace from '../assets/images/survivors/clara-face.png'; -import DebraFace from '../assets/images/survivors/debra-face.png'; -import DougFace from '../assets/images/survivors/doug-face.png'; -import DukeFace from '../assets/images/survivors/duke-face.png'; -import JoshFace from '../assets/images/survivors/josh-face.png'; -import MaryFace from '../assets/images/survivors/mary-face.png'; -import NedFace from '../assets/images/survivors/ned-face.png'; -import PhilFace from '../assets/images/survivors/phil-face.png'; -import RuizFace from '../assets/images/survivors/ruiz-face.png'; -import TommyFace from '../assets/images/survivors/tommy-face.png'; -import WandaFace from '../assets/images/survivors/wanda-face.png'; +import AmyFace from '../assets/images/survivors/faces/amy-face.png'; +import AnyaFace from '../assets/images/survivors/faces/anya-face.png'; +import BenFace from '../assets/images/survivors/faces/ben-face.png'; +import BillCashFace from '../assets/images/survivors/faces/billcash-face.png'; +import BobFace from '../assets/images/survivors/faces/bob-face.png'; +import ClaraFace from '../assets/images/survivors/faces/clara-face.png'; +import DebraFace from '../assets/images/survivors/faces/debra-face.png'; +import DougFace from '../assets/images/survivors/faces/doug-face.png'; +import JoshFace from '../assets/images/survivors/faces/josh-face.png'; +import KrysFace from '../assets/images/survivors/faces/krys-face.png'; +import MaryFace from '../assets/images/survivors/faces/mary-face.png'; +import NedFace from '../assets/images/survivors/faces/ned-face.png'; +import PhilFace from '../assets/images/survivors/faces/phil-face.png'; +import RuizFace from '../assets/images/survivors/faces/ruiz-face.png'; +import SternkovaFace from '../assets/images/survivors/faces/sternkova-face.png'; +import WandaFace from '../assets/images/survivors/faces/wanda-face.png'; -import SelectorAmy from '../assets/images/selectors/selector-amy.png'; -import SelectorBen from '../assets/images/selectors/selector-ben.png'; -import SelectorClara from '../assets/images/selectors/selector-clara.png'; -import SelectorDebra from '../assets/images/selectors/selector-debra.png'; -import SelectorDoug from '../assets/images/selectors/selector-doug.png'; -import SelectorDuke from '../assets/images/selectors/selector-duke.png'; -import SelectorJosh from '../assets/images/selectors/selector-josh.png'; -import SelectorMary from '../assets/images/selectors/selector-mary.png'; -import SelectorNed from '../assets/images/selectors/selector-ned.png'; -import SelectorPhil from '../assets/images/selectors/selector-phil.png'; -import SelectorRuiz from '../assets/images/selectors/selector-ruiz.png'; -import SelectorTommy from '../assets/images/selectors/selector-tommy.png'; -import SelectorWanda from '../assets/images/selectors/selector-wanda.png'; +import SelectorAmy from '../assets/images/survivors/selectors/selector-amy.png'; +import SelectorAnya from '../assets/images/survivors/selectors/selector-anya.png'; +import SelectorBen from '../assets/images/survivors/selectors/selector-ben.png'; +import SelectorBillCash from '../assets/images/survivors/selectors/selector-billcash.png'; +import SelectorBob from '../assets/images/survivors/selectors/selector-bob.png'; +import SelectorClara from '../assets/images/survivors/selectors/selector-clara.png'; +import SelectorDebra from '../assets/images/survivors/selectors/selector-debra.png'; +import SelectorDoug from '../assets/images/survivors/selectors/selector-doug.png'; +import SelectorJosh from '../assets/images/survivors/selectors/selector-josh.png'; +import SelectorKrys from '../assets/images/survivors/selectors/selector-krys.png'; +import SelectorMary from '../assets/images/survivors/selectors/selector-mary.png'; +import SelectorNed from '../assets/images/survivors/selectors/selector-ned.png'; +import SelectorPhil from '../assets/images/survivors/selectors/selector-phil.png'; +import SelectorRuiz from '../assets/images/survivors/selectors/selector-ruiz.png'; +import SelectorSternkova from '../assets/images/survivors/selectors/selector-sternkova.png'; +import SelectorWanda from '../assets/images/survivors/selectors/selector-wanda.png'; import { FEMALE, MALE } from '../constants'; -import { ABILITIES_S1, ABILITIES_S2, ABILITIES_MALL } from './abilities'; +import { ALL_ABILITIES } from './abilities'; const { ACTION, @@ -95,16 +104,35 @@ const { TWO_ZONES_MOVE, WEBBING, ZOMBIE_LINK, - ZONE_PER_MOVE + ZONE_PER_MOVE, + PEEK_OUT_THE_WINDOW, + AGILITY, + CHARISMATIC, + INFLUENCER, + NIGHT_VISION, + PICKPOCKETING, + RUN_TO_DAYLIGHT, + SHOOT_FROM_THE_HIP, + URBAN_SURVIVOR, + FREE_RELOAD, + SPRINT, + POINT_BLANK, + SCAVENGER, + LOW_PROFILE, + SHOVE, + LIFE_SAVER, + FRENZY_COMBAT, + FRENZY_MELEE, + FRENZY_RANGED } = ALL_ABILITIES; export const CHARACTERS_S1 = { Amy: { abilities: [], abilitiesUsed: [], - actions: [3, 0, 0, 0, 0], - actionsLeft: [3, 0, 0, 0, 0], - bonusDices: { combat: 0, melee: 0, ranged: 0 }, + actions: [3, 0, [0, 0, 0], 0, 0], + actionsLeft: [3, 0, [0, 0, 0], 0, 0], + bonusDice: { combat: 0, melee: 0, ranged: 0 }, color: '#a015a3', experience: 0, face: AmyFace, @@ -129,9 +157,9 @@ export const CHARACTERS_S1 = { Doug: { abilities: [], abilitiesUsed: [], - actions: [3, 0, 0, 0, 0], - actionsLeft: [3, 0, 0, 0, 0], - bonusDices: { combat: 0, melee: 0, ranged: 0 }, + actions: [3, 0, [0, 0, 0], 0, 0], + actionsLeft: [3, 0, [0, 0, 0], 0, 0], + bonusDice: { combat: 0, melee: 0, ranged: 0 }, color: '#3566c6', experience: 0, face: DougFace, @@ -156,9 +184,9 @@ export const CHARACTERS_S1 = { Josh: { abilities: [], abilitiesUsed: [], - actions: [3, 0, 0, 0, 0], - actionsLeft: [3, 0, 0, 0, 0], - bonusDices: { combat: 0, melee: 0, ranged: 0 }, + actions: [3, 0, [0, 0, 0], 0, 0], + actionsLeft: [3, 0, [0, 0, 0], 0, 0], + bonusDice: { combat: 0, melee: 0, ranged: 0 }, color: '#ba761d', experience: 0, face: JoshFace, @@ -183,9 +211,9 @@ export const CHARACTERS_S1 = { Ned: { abilities: [], abilitiesUsed: [], - actions: [3, 0, 0, 0, 0], - actionsLeft: [3, 0, 0, 0, 0], - bonusDices: { combat: 0, melee: 0, ranged: 0 }, + actions: [3, 0, [0, 0, 0], 0, 0], + actionsLeft: [3, 0, [0, 0, 0], 0, 0], + bonusDice: { combat: 0, melee: 0, ranged: 0 }, color: '#b52929', experience: 0, face: NedFace, @@ -210,9 +238,9 @@ export const CHARACTERS_S1 = { Phil: { abilities: [], abilitiesUsed: [], - actions: [3, 0, 0, 0, 0], - actionsLeft: [3, 0, 0, 0, 0], - bonusDices: { combat: 0, melee: 0, ranged: 0 }, + actions: [3, 0, [0, 0, 0], 0, 0], + actionsLeft: [3, 0, [0, 0, 0], 0, 0], + bonusDice: { combat: 0, melee: 0, ranged: 0 }, color: '#565656', experience: 0, face: PhilFace, @@ -236,9 +264,9 @@ export const CHARACTERS_S1 = { Wanda: { abilities: [], abilitiesUsed: [], - actions: [3, 0, 0, 0, 0], - actionsLeft: [3, 0, 0, 0, 0], - bonusDices: { combat: 0, melee: 0, ranged: 0 }, + actions: [3, 0, [0, 0, 0], 0, 0], + actionsLeft: [3, 0, [0, 0, 0], 0, 0], + bonusDice: { combat: 0, melee: 0, ranged: 0 }, color: '#339b35', experience: 0, face: WandaFace, @@ -266,9 +294,9 @@ export const CHARACTERS_KOPINSKI = { Ben: { abilities: [], abilitiesUsed: [], - actions: [3, 0, 0, 0, 0], - actionsLeft: [3, 0, 0, 0, 0], - bonusDices: { combat: 0, melee: 0, ranged: 0 }, + actions: [3, 0, [0, 0, 0], 0, 0], + actionsLeft: [3, 0, [0, 0, 0], 0, 0], + bonusDice: { combat: 0, melee: 0, ranged: 0 }, color: '#537c6f', experience: 0, face: BenFace, @@ -278,6 +306,7 @@ export const CHARACTERS_KOPINSKI = { location: null, movement: 'confident', name: 'Ben', + nickname: 'Redcap', noise: 0, player: null, promotions: { @@ -293,9 +322,9 @@ export const CHARACTERS_KOPINSKI = { Mary: { abilities: [], abilitiesUsed: [], - actions: [3, 0, 0, 0, 0], - actionsLeft: [3, 0, 0, 0, 0], - bonusDices: { combat: 0, melee: 0, ranged: 0 }, + actions: [3, 0, [0, 0, 0], 0, 0], + actionsLeft: [3, 0, [0, 0, 0], 0, 0], + bonusDice: { combat: 0, melee: 0, ranged: 0 }, color: '#3e4c84', experience: 0, face: MaryFace, @@ -305,6 +334,7 @@ export const CHARACTERS_KOPINSKI = { location: null, movement: 'normal', name: 'Mary', + nickname: 'Angry', noise: 0, player: null, promotions: { @@ -316,8 +346,11 @@ export const CHARACTERS_KOPINSKI = { selector: SelectorMary, voice: FEMALE, wounded: false - }, - Duke: { + } +}; + +export const CHARACTERS_NIGHT_SHIFT = { + Anya: { abilities: [], abilitiesUsed: [], actions: [3, 0, [0, 0, 0], 0, 0], @@ -325,13 +358,68 @@ export const CHARACTERS_KOPINSKI = { bonusDice: { combat: 0, melee: 0, ranged: 0 }, color: '#537c6f', experience: 0, - face: DukeFace, - img: Duke, + face: AnyaFace, + img: Anya, + inReserve: [null, null, null], + inHand: [null, null], + location: null, + movement: 'tactical', + name: 'Anya', + noise: 0, + player: null, + promotions: { + blue: HIT_N_RUN, + yellow: ACTION, + orange: [ZOMBIE_LINK, SNIPER], + red: [MAX_RANGE, DIE_COMBAT, CANT_BE_THAT_UNLUCKY] + }, + selector: SelectorAnya, + voice: FEMALE, + wounded: false + }, + BillCash: { + abilities: [], + abilitiesUsed: [], + actions: [3, 0, [0, 0, 0], 0, 0], + actionsLeft: [3, 0, [0, 0, 0], 0, 0], + bonusDice: { combat: 0, melee: 0, ranged: 0 }, + color: '#3e4c84', + experience: 0, + face: BillCashFace, + img: BillCash, inReserve: [null, null, null], inHand: [null, null], location: null, movement: 'confident', - name: 'Duke', + name: 'Bill Cash', + noise: 0, + player: null, + promotions: { + blue: SHOOT_FROM_THE_HIP, + yellow: ACTION, + orange: [NIGHT_VISION, FREE_RELOAD], + red: [DIE_RANGED, ACTION_RANGED, DICE_ROLL_RANGED] + }, + selector: SelectorBillCash, + voice: MALE, + wounded: false + }, + Bob: { + abilities: [], + abilitiesUsed: [], + actions: [3, 0, [0, 0, 0], 0, 0], + actionsLeft: [3, 0, [0, 0, 0], 0, 0], + bonusDice: { combat: 0, melee: 0, ranged: 0 }, + color: '#537c6f', + experience: 0, + face: BobFace, + img: Bob, + inReserve: [null, null, null], + inHand: [null, null], + location: null, + movement: 'heavy', + name: 'Bob', + nickname: 'Bluefoot', noise: 0, player: null, promotions: { @@ -340,11 +428,11 @@ export const CHARACTERS_KOPINSKI = { orange: [TAUNT, HOARD], red: [REAPER_MELEE, COMBAT_ACTION, ALL_YOUVE_GOT] }, - selector: SelectorDuke, + selector: SelectorBob, voice: MALE, wounded: false }, - Ruiz: { + Clara: { abilities: [], abilitiesUsed: [], actions: [3, 0, [0, 0, 0], 0, 0], @@ -352,29 +440,54 @@ export const CHARACTERS_KOPINSKI = { bonusDice: { combat: 0, melee: 0, ranged: 0 }, color: '#3e4c84', experience: 0, - face: RuizFace, - img: Ruiz, + face: ClaraFace, + img: Clara, inReserve: [null, null, null], inHand: [null, null], location: null, - movement: 'normal', - name: 'Ruiz', + movement: 'skating', + name: 'Clara', + nickname: 'Icy', noise: 0, player: null, promotions: { - blue: WEBBING, + blue: SPRINT, yellow: ACTION, - orange: [SEARCH_PLUS_ONE, BORN_LEADER], - red: [DIE_RANGED, TACTICIAN, BLOODLUST_MELEE] + orange: [POINT_BLANK, MEDIC], + red: [ACTION, LIFE_SAVER, DIE_RANGED] }, - selector: SelectorRuiz, + selector: SelectorClara, + voice: FEMALE, + wounded: false + }, + Debra: { + abilities: [], + abilitiesUsed: [], + actions: [3, 0, [0, 0, 0], 0, 0], + actionsLeft: [3, 0, [0, 0, 0], 0, 0], + bonusDice: { combat: 0, melee: 0, ranged: 0 }, + color: '#3e4c84', + experience: 0, + face: DebraFace, + img: Debra, + inReserve: [null, null, null], + inHand: [null, null], + location: null, + movement: 'quick', + name: 'Debra', + noise: 0, + player: null, + promotions: { + blue: LOW_PROFILE, + yellow: ACTION, + orange: [PICKPOCKETING, PEEK_OUT_THE_WINDOW], + red: [INFLUENCER, ACTION, FRENZY_COMBAT] + }, + selector: SelectorDebra, voice: MALE, wounded: false - } -}; - -export const CHARACTERS_NIGHT_SHIFT = { - Duke: { + }, + Krys: { abilities: [], abilitiesUsed: [], actions: [3, 0, [0, 0, 0], 0, 0], @@ -382,23 +495,24 @@ export const CHARACTERS_NIGHT_SHIFT = { bonusDice: { combat: 0, melee: 0, ranged: 0 }, color: '#537c6f', experience: 0, - face: DukeFace, - img: Duke, + face: KrysFace, + img: Krys, inReserve: [null, null, null], inHand: [null, null], location: null, - movement: 'confident', - name: 'Duke', + movement: 'furtive', + name: 'Krys', + nickname: 'Rainbow', noise: 0, player: null, promotions: { - blue: SUPER_STRENGTH, + blue: SCAVENGER, yellow: ACTION, - orange: [TAUNT, HOARD], - red: [REAPER_MELEE, COMBAT_ACTION, ALL_YOUVE_GOT] + orange: [MAX_RANGE, SLIPPERY], + red: [SNIPER, DICE_ROLL_COMBAT, WEBBING] }, - selector: SelectorDuke, - voice: MALE, + selector: SelectorKrys, + voice: FEMALE, wounded: false }, Ruiz: { @@ -416,6 +530,7 @@ export const CHARACTERS_NIGHT_SHIFT = { location: null, movement: 'normal', name: 'Ruiz', + nickname: 'Detective', noise: 0, player: null, promotions: { @@ -427,5 +542,32 @@ export const CHARACTERS_NIGHT_SHIFT = { selector: SelectorRuiz, voice: MALE, wounded: false + }, + Sternkova: { + abilities: [], + abilitiesUsed: [], + actions: [3, 0, [0, 0, 0], 0, 0], + actionsLeft: [3, 0, [0, 0, 0], 0, 0], + bonusDice: { combat: 0, melee: 0, ranged: 0 }, + color: '#537c6f', + experience: 0, + face: SternkovaFace, + img: Sternkova, + inReserve: [null, null, null], + inHand: [null, null], + location: null, + movement: 'tactical', + name: 'Sternkova', + noise: 0, + player: null, + promotions: { + blue: TACTICIAN, + yellow: ACTION, + orange: [BLOODLUST_MELEE, AGILITY], + red: [COMBAT_ACTION, REAPER_MELEE, DIE_COMBAT] + }, + selector: SelectorSternkova, + voice: FEMALE, + wounded: false } }; diff --git a/src/setup/sets.js b/src/setup/sets.js index 3e3f83b..26d31a5 100644 --- a/src/setup/sets.js +++ b/src/setup/sets.js @@ -2,7 +2,11 @@ import Season1 from '../assets/images/sets/season1.png'; import DogZ from '../assets/images/sets/dogZ.png'; import Kopinski from '../assets/images/sets/kopinski.png'; import NightShift from '../assets/images/sets/nightShift.png'; -import { CHARACTERS_KOPINSKI, CHARACTERS_S1 } from './characters'; +import { + CHARACTERS_KOPINSKI, + CHARACTERS_NIGHT_SHIFT, + CHARACTERS_S1 +} from './characters'; import { WEAPONS_S1, WEAPONS_NIGHT_SHIFT } from './weapons'; import { DOGZ, ZOMBIES_S1 } from './zombies'; import { ITEMS_S1, ITEMS_NIGHT_SHIFT } from './items'; @@ -38,6 +42,7 @@ export const EXPANSIONS = { name: 'kopinski' }, nightShift: { + characters: CHARACTERS_NIGHT_SHIFT, cover: NightShift, coverSize: 'small', deselectable: true, diff --git a/src/utils/actions.js b/src/utils/actions.js index ac92b88..f7e3e9a 100644 --- a/src/utils/actions.js +++ b/src/utils/actions.js @@ -1,6 +1,8 @@ import { BONUS_ACTION, FREE_ATTACK, + FREE_ATTACK_MELEE, + FREE_ATTACK_RANGED, FREE_MOVE, FREE_SEARCH } from '../constants'; @@ -10,6 +12,8 @@ export const getActionColor = action => { case FREE_MOVE: return '#33cc33'; case FREE_ATTACK: + case FREE_ATTACK_MELEE: + case FREE_ATTACK_RANGED: return '#ff0000'; case FREE_SEARCH: return '#ffa100'; @@ -20,11 +24,18 @@ export const getActionColor = action => { } }; +export const totalActions = actionsArray => { + return actionsArray.reduce((a, b) => a + b); +}; + export const checkIfHasAnyActionLeft = actionsArray => actionsArray && actionsArray.reduce((a, b, index) => { if (index === 4) { return a; } + if (index === 2) { + return a + totalActions(b); + } return a + (typeof b === 'number' ? Math.max(0, b) : Number(!!b)); }, 0); diff --git a/src/utils/hooks.js b/src/utils/hooks.js index 87271b9..e1c7285 100644 --- a/src/utils/hooks.js +++ b/src/utils/hooks.js @@ -1,7 +1,16 @@ import { useState, useDebugValue, useEffect } from 'react'; -import { checkIfHasAnyActionLeft } from './actions'; +import { checkIfHasAnyActionLeft, totalActions } from './actions'; import { logger } from './logger'; -import { GENERAL, LOG_TYPE_EXTENDED, TURNS_HOOK_UPDATED } from '../constants'; +import { + ATTACK, + ATTACK_MELEE, + ATTACK_RANGED, + GENERAL, + LOG_TYPE_EXTENDED, + MOVE, + SEARCH, + TURNS_HOOK_UPDATED +} from '../constants'; export const useStateWithLabel = (initialValue, displayName) => { const [value, setValue] = useState(initialValue); @@ -15,7 +24,7 @@ export const useTurnsCounter = ( [ numOfActions = 3, movements = 0, - attacks = 0, + attacks = [0, 0, 0], searches = 0, numOfBonusActions = 0 ] @@ -38,7 +47,7 @@ export const useTurnsCounter = ( // if (act === 0 && searchActions === 0) { // setSearchActions(searchActions - 1); // } - if (!act && !mov && !att && sea <= 0 && !bon) { + if (!act && !mov && !totalActions(att) && sea <= 0 && !bon) { changeMessage(`${character} used all actions.`); setSearchActions(searchActions - 1); finishTurn(true); @@ -46,7 +55,6 @@ export const useTurnsCounter = ( } return false; }; - const spendAction = (type = GENERAL) => { if (bonusActions) { changeMessage(`${character} used 1 bonus action to ${type}.`); @@ -56,7 +64,7 @@ export const useTurnsCounter = ( } return hasUsedAllActions({ bon: bonusActions - 1 }); } - if (type === 'move' && extraMovementActions > 0) { + if (type === MOVE && extraMovementActions > 0) { changeMessage( `${character} used 1 extra move of ${extraMovementActions}.` ); @@ -64,15 +72,34 @@ export const useTurnsCounter = ( return hasUsedAllActions({ mov: extraMovementActions - 1 }); } - if (type === 'attack' && extraAttackActions > 0) { - changeMessage( - `${character} used 1 extra attack of ${extraAttackActions}.` - ); - setExtraAttackActions(extraAttackActions - 1); - return hasUsedAllActions({ att: extraAttackActions - 1 }); + if (type.includes(ATTACK) && totalActions(extraAttackActions) > 0) { + let bonusAttackUsed = false; + if (type === ATTACK_MELEE && extraAttackActions[1] > 0) { + changeMessage( + `${character} used 1 extra melee attack of ${extraAttackActions[1]}.` + ); + extraAttackActions[1] -= 1; + bonusAttackUsed = true; + } else if (type === ATTACK_RANGED && extraAttackActions[2] > 0) { + changeMessage( + `${character} used 1 extra ranged attack of ${extraAttackActions[2]}.` + ); + extraAttackActions[2] -= 1; + bonusAttackUsed = true; + } else if (extraAttackActions[0] > 0) { + changeMessage( + `${character} used 1 extra attack of ${extraAttackActions[0]}.` + ); + extraAttackActions[0] -= 1; + bonusAttackUsed = true; + } + if (bonusAttackUsed) { + setExtraAttackActions(extraAttackActions); + return hasUsedAllActions({ att: extraAttackActions }); + } } - if (type === 'search') { + if (type === SEARCH) { if (searchActions < 0) { changeMessage(`${character} has no ${type} actions left.`); return null; @@ -92,7 +119,7 @@ export const useTurnsCounter = ( } left.` ); setGeneralActions(generalActions - 1); - if (type === 'search') { + if (type === SEARCH) { setSearchActions(-1); return hasUsedAllActions({ act: generalActions - 1 }); } @@ -123,7 +150,9 @@ export const useTurnsCounter = ( changeMessage(''); } }, [ - attacks, + attacks[0], + attacks[1], + attacks[2], character, movements, numOfActions, @@ -157,7 +186,7 @@ export const useTurnsCounter = ( spendAction, finishedTurn, canMove: generalActions > 0 || extraMovementActions > 0, - canAttack: generalActions > 0 || extraAttackActions > 0, + canAttack: generalActions > 0 || totalActions(extraAttackActions) > 0, canSearch: (generalActions > 0 && searchActions >= 0) || searchActions > 0, message }; diff --git a/src/utils/promotions.js b/src/utils/promotions.js index fff7838..e58161f 100644 --- a/src/utils/promotions.js +++ b/src/utils/promotions.js @@ -1,8 +1,10 @@ import { cloneDeep } from 'lodash'; -import { ABILITIES_S1 } from '../setup/abilities'; +import { ALL_ABILITIES } from '../setup/abilities'; const { ACTION, + ACTION_MELEE, + ACTION_RANGED, MOVE_ACTION, SEARCH_ACTION, COMBAT_ACTION, @@ -10,8 +12,11 @@ const { DIE_MELEE, DIE_RANGED, STARTS_WITH, - HOARD -} = ABILITIES_S1; + HOARD, + REAPER_COMBAT, + REAPER_MELEE, + REAPER_RANGED +} = ALL_ABILITIES; export const handlePromotionEffects = (char, level, actionsLeft, index) => { const updatedChar = cloneDeep(char); @@ -22,7 +27,9 @@ export const handlePromotionEffects = (char, level, actionsLeft, index) => { (updatedChar.promotions.blue.name === ACTION.name || updatedChar.promotions.blue.name === MOVE_ACTION.name || updatedChar.promotions.blue.name === SEARCH_ACTION.name || - updatedChar.promotions.blue.name === COMBAT_ACTION.name) && + updatedChar.promotions.blue.name === COMBAT_ACTION.name || + updatedChar.promotions.blue.name === ACTION_MELEE.name || + updatedChar.promotions.blue.name === ACTION_RANGED.name) && updatedChar.promotions.blue.effect ) { updatedChar.actions = updatedChar.promotions.blue.effect( @@ -34,11 +41,14 @@ export const handlePromotionEffects = (char, level, actionsLeft, index) => { } else if ( (updatedChar.promotions.blue.name === DIE_COMBAT.name || updatedChar.promotions.blue.name === DIE_MELEE.name || - updatedChar.promotions.blue.name === DIE_RANGED.name) && + updatedChar.promotions.blue.name === DIE_RANGED.name || + updatedChar.promotions.blue.name === REAPER_COMBAT.name || + updatedChar.promotions.blue.name === REAPER_MELEE.name || + updatedChar.promotions.blue.name === REAPER_RANGED.name) && updatedChar.promotions.blue.effect ) { - updatedChar.bonusDices = updatedChar.promotions.red[index].effect( - updatedChar.bonusDices + updatedChar.bonusDice = updatedChar.promotions.blue.effect( + updatedChar.bonusDice ); } else if ( updatedChar.promotions.blue.name.includes( @@ -57,7 +67,9 @@ export const handlePromotionEffects = (char, level, actionsLeft, index) => { (updatedChar.promotions.yellow.name === ACTION.name || updatedChar.promotions.yellow.name === MOVE_ACTION.name || updatedChar.promotions.yellow.name === SEARCH_ACTION.name || - updatedChar.promotions.blue.name === COMBAT_ACTION.name) && + updatedChar.promotions.yellow.name === COMBAT_ACTION.name || + updatedChar.promotions.yellow.name === ACTION_MELEE.name || + updatedChar.promotions.yellow.name === ACTION_RANGED.name) && updatedChar.promotions.yellow.effect ) { updatedChar.actions = updatedChar.promotions.yellow.effect( @@ -69,13 +81,16 @@ export const handlePromotionEffects = (char, level, actionsLeft, index) => { } else if ( (updatedChar.promotions.yellow.name === DIE_COMBAT.name || updatedChar.promotions.yellow.name === DIE_MELEE.name || - updatedChar.promotions.yellow.name === DIE_RANGED.name) && + updatedChar.promotions.yellow.name === DIE_RANGED.name || + updatedChar.promotions.yellow.name === REAPER_COMBAT.name || + updatedChar.promotions.yellow.name === REAPER_MELEE.name || + updatedChar.promotions.yellow.name === REAPER_RANGED.name) && updatedChar.promotions.yellow.effect ) { - updatedChar.bonusDices = updatedChar.promotions.yellow.effect( - updatedChar.bonusDices + updatedChar.bonusDice = updatedChar.promotions.yellow.effect( + updatedChar.bonusDice ); - } else if (updatedChar.promotions.blue.name.includes(HOARD.name)) { + } else if (updatedChar.promotions.yellow.name.includes(HOARD.name)) { updatedChar.inReserve = updatedChar.promotions.yellow.effect( updatedChar.inReserve ); @@ -86,7 +101,9 @@ export const handlePromotionEffects = (char, level, actionsLeft, index) => { (updatedChar.promotions.orange[index].name === ACTION.name || updatedChar.promotions.orange[index].name === MOVE_ACTION.name || updatedChar.promotions.orange[index].name === SEARCH_ACTION.name || - updatedChar.promotions.orange[index].name === COMBAT_ACTION.name) && + updatedChar.promotions.orange[index].name === COMBAT_ACTION.name || + updatedChar.promotions.orange[index].name === ACTION_MELEE.name || + updatedChar.promotions.orange[index].name === ACTION_RANGED.name) && updatedChar.promotions.orange[index].effect ) { updatedChar.actions = updatedChar.promotions.orange[index].effect( @@ -98,13 +115,16 @@ export const handlePromotionEffects = (char, level, actionsLeft, index) => { } else if ( (updatedChar.promotions.orange[index].name === DIE_COMBAT.name || updatedChar.promotions.orange[index].name === DIE_MELEE.name || - updatedChar.promotions.orange[index].name === DIE_RANGED.name) && + updatedChar.promotions.orange[index].name === DIE_RANGED.name || + updatedChar.promotions.orange[index].name === REAPER_COMBAT.name || + updatedChar.promotions.orange[index].name === REAPER_MELEE.name || + updatedChar.promotions.orange[index].name === REAPER_RANGED.name) && updatedChar.promotions.orange[index].effect ) { - updatedChar.bonusDices = updatedChar.promotions.orange[index].effect( - updatedChar.bonusDices + updatedChar.bonusDice = updatedChar.promotions.orange[index].effect( + updatedChar.bonusDice ); - } else if (updatedChar.promotions.blue.name.includes(HOARD.name)) { + } else if (updatedChar.promotions.orange[index].name.includes(HOARD.name)) { updatedChar.inReserve = updatedChar.promotions.orange[index].effect( updatedChar.inReserve ); @@ -115,7 +135,9 @@ export const handlePromotionEffects = (char, level, actionsLeft, index) => { (updatedChar.promotions.red[index].name === ACTION.name || updatedChar.promotions.red[index].name === MOVE_ACTION.name || updatedChar.promotions.red[index].name === SEARCH_ACTION.name || - updatedChar.promotions.red[index].name === COMBAT_ACTION.name) && + updatedChar.promotions.red[index].name === COMBAT_ACTION.name || + updatedChar.promotions.red[index].name === ACTION_MELEE.name || + updatedChar.promotions.red[index].name === ACTION_RANGED.name) && updatedChar.promotions.red[index].effect ) { updatedChar.actions = updatedChar.promotions.red[index].effect( @@ -127,13 +149,16 @@ export const handlePromotionEffects = (char, level, actionsLeft, index) => { } else if ( (updatedChar.promotions.red[index].name === DIE_COMBAT.name || updatedChar.promotions.red[index].name === DIE_MELEE.name || - updatedChar.promotions.red[index].name === DIE_RANGED.name) && + updatedChar.promotions.red[index].name === DIE_RANGED.name || + updatedChar.promotions.red[index].name === REAPER_COMBAT.name || + updatedChar.promotions.red[index].name === REAPER_MELEE.name || + updatedChar.promotions.red[index].name === REAPER_RANGED.name) && updatedChar.promotions.red[index].effect ) { - updatedChar.bonusDices = updatedChar.promotions.red[index].effect( - updatedChar.bonusDices + updatedChar.bonusDice = updatedChar.promotions.red[index].effect( + updatedChar.bonusDice ); - } else if (updatedChar.promotions.blue.name.includes(HOARD.name)) { + } else if (updatedChar.promotions.red[index].name.includes(HOARD.name)) { updatedChar.inReserve = updatedChar.promotions.red[index].effect( updatedChar.inReserve );