import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd' import { Link } from 'react-router-dom' import GAME_CONFIG from '../../../GAME_CONFIG.json' import React, { useContext, useState, useEffect, useLayoutEffect, useCallback, } from 'react' import styled, { css } from 'styled-components' import { BoardCard } from './BoardCard' import { store } from '../store/Store' import { FaHeart, FaBolt } from 'react-icons/fa' const FIELD_SIZE = GAME_CONFIG.maxCardsInField const reorder = (list, startIndex, endIndex) => { const result = Array.from(list) const [removed] = result.splice(startIndex, 1) result.splice(endIndex, 0, removed) return result } /** *@param {source} - source *@param {destination} - destination *@param {droppableSource} - source *@param {droppableDestination} - destination **/ const move = (source, destination, droppableSource, droppableDestination) => { const sourceClone = Array.from(source) const destClone = Array.from(destination) const [removed] = sourceClone.splice(droppableSource.index, 1) destClone.splice(droppableDestination.index, 0, removed) const result = {} result[droppableSource.droppableId] = sourceClone result[droppableDestination.droppableId] = destClone return result } const ALLY_TYPES = { hand: 'allyHand', field: 'allyField', } export const Board = (props) => { const { state, dispatch } = useContext(store) const isGamePaused = () => state.game && state.game.gamePaused const [allyCards, setAllyCards] = useState({ [ALLY_TYPES.hand]: [], [ALLY_TYPES.field]: [], }) useLayoutEffect(() => { if (state.game) { if (state.playerNumber === 1) { setAllyCards({ [ALLY_TYPES.hand]: [...state.game.player1.hand], [ALLY_TYPES.field]: [...state.game.player1.field], }) } else { setAllyCards({ [ALLY_TYPES.hand]: [...state.game.player2.hand], [ALLY_TYPES.field]: [...state.game.player2.field], }) } } }, [state.game]) const getAllyStateType = (droppableId) => { return allyCards[droppableId] } const invokeCard = (card) => { console.log('invoke card', card) if (!card.isInvoked) { let me if (state.playerNumber === 1) { me = state.game.player1 } else { me = state.game.player2 } // Invokes a card into the field and updates ally hand with a new deep copy if (me.field.length >= FIELD_SIZE) { return dispatch({ type: 'SET_ERROR', payload: { error: 'The field is full', }, }) } if (card.cost > me.energy) { return dispatch({ type: 'SET_ERROR', payload: { error: "You don't have enough energy to invoke this card", }, }) } card.isInvoked = true state.socket.emit('invoke-card', { game: state.game, card, }) } } const onDragEnd = useCallback( (result) => { const { source, destination } = result if (!destination) { return } let allyState = getAllyStateType(source.droppableId) const isEnoughEnergyToInvoke = state.playerNumber === 1 ? state.game.player1.energy : state.game.player2.energy console.log( isEnoughEnergyToInvoke, allyState.cost, 'energy', state.playerNumber, state.game.player1.energy, state.game.player2.energy, ) if (isEnoughEnergyToInvoke < allyState[source.index].cost) { return } if (source.droppableId === destination.droppableId) { const items = reorder( allyState, source.index, destination.index, ) setAllyCards({ ...allyCards, [source.droppableId]: items }) } else { //invoke card invokeCard(allyCards[ALLY_TYPES.hand][source.index]) const result = move( getAllyStateType(source.droppableId), getAllyStateType(destination.droppableId), source, destination, ) setAllyCards({ [ALLY_TYPES.hand]: result[ALLY_TYPES.hand], [ALLY_TYPES.field]: result[ALLY_TYPES.field], }) } }, [state.game, allyCards], ) return ( <Page> <ResultMsg winner={state.gameOver && state.areYouTheWinner} loser={state.gameOver && !state.areYouTheWinner} > {state.gameOver && state.areYouTheWinner ? 'Congratulations! You are the winner!' : state.gameOver && !state.areYouTheWinner ? 'You lost! Better luck next time!' : null} </ResultMsg> {/* <p>Turn: {state.game ? state.game.currentTurnNumber : 0}</p> <p>Timer: {props.turnCountdownTimer}</p> */} <ExitLink hidden={!state.gameOver} to="/"> Exit </ExitLink> {state.game ? ( <Game className="game"> <EnemyDeck>Enemy's <br /> Deck</EnemyDeck> <YourDeck>Your <br /> Deck</YourDeck> <EnemyStatsBox className={ state.isAttackMode ? 'enemy-stats attack-mode' : 'enemy-stats' } onClick={() => { if (state.isAttackMode) props.attackDirectly() }} > <p>Enemy</p> <p> {state.playerNumber === 1 ? state.game.player2.life : state.game.player1.life} <FaHeart /> </p> <p> {state.playerNumber === 1 ? state.game.player2.energy : state.game.player1.energy} <FaBolt /> </p> </EnemyStatsBox> <AllyStatsBox className="my-stats"> <p>You</p> <p> {state.playerNumber === 1 ? state.game.player1.life : state.game.player2.life} <FaHeart /> </p> <p> {state.playerNumber === 1 ? state.game.player1.energy : state.game.player2.energy} <FaBolt /> </p> </AllyStatsBox> <CardContainer className="cards-container enemy-cards-container"> {state.visualEnemyHand} </CardContainer> <Field className="field"> <DragDropContext onDragEnd={onDragEnd}> <EnemyField className={ state.isAttackMode ? 'enemy-field attack-mode' : 'enemy-field' } > {state.enemyFieldHtml} </EnemyField> <FieldContainer top > <Droppable droppableId={`${ALLY_TYPES.field}`} direction="horizontal" > {(provided, snapshot) => ( <CardPanel ref={provided.innerRef} isDraggingOver={snapshot.isDraggingOver} > {allyCards[ALLY_TYPES.field].map( (allyFieldCard, index) => ( <Draggable key={index} draggableId={`allyFieldCard${index}`} index={index} isDragDisabled={true} > {( provided, snapshot, ) => ( <div ref={ provided.innerRef } {...provided.draggableProps} {...provided.dragHandleProps} > <BoardCard {...allyFieldCard} /> </div> )} </Draggable> ), )} {provided.placeholder} </CardPanel> )} </Droppable> </FieldContainer> <FieldContainer bottom> <Droppable droppableId={`${ALLY_TYPES.hand}`} direction="horizontal" > {(provided, snapshot) => ( <CardPanel ref={provided.innerRef} // isDraggingOver={snapshot.isDraggingOver} outter > {allyCards[ALLY_TYPES.hand].map( (allyHandCard, index) => ( <Draggable key={index} draggableId={`allyHand${index}`} index={index} isDragDisabled={ state.playerNumber !== state.game .currentPlayerTurn } > {( provided, snapshot, ) => ( <div ref={ provided.innerRef } {...provided.draggableProps} {...provided.dragHandleProps} > <BoardCard {...allyHandCard} /> </div> )} </Draggable> ), )} {provided.placeholder} </CardPanel> )} </Droppable> </FieldContainer> </DragDropContext> </Field> <Button className="end-turn" disabled={state.isOtherPlayerTurn || isGamePaused()} onClick={() => { props.endTurn() }} > End Turn </Button> <Button className="surrender" style={{ backgroundColor: 'red', }} disabled={isGamePaused()} onClick={() => { props.surrender() }} > Surrender </Button> </Game> ) : ( <p>Game loading...</p> )} </Page> ) } const Page = styled.div` border-radius: 10px; text-align: center; margin: 0 auto; width: 80%; height: 90%; color: #fff; background-color: #1f1f1f; display: flex; align-items: center; > h1 { margin-top: 0; } @media(max-width: 891px){ height: 80%; margin: 0 auto 10%; } @media(max-width: 578px){ width: 95%; margin: 0 auto 20%; } ` const Game = styled.div` width: 95%; margin: 0 auto; position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 90%; ` const CardContainer = styled.div` display: flex; flex: 1; justify-content: center; align-items: center; margin: 0 auto; width: 95%; height: 20%; > div { background-color: rgb(105, 102, 102); height: 100%; position: relative; border-radius: 0.3rem; &::before{ content: "?"; position: absolute; width: 20px; height: 20px; left: 50%; margin-left: -10px; top: 50%; margin-top: -10px; } } ` const Field = styled.div` width: 60%; margin: 0 auto; display: flex; flex-direction: column; justify-content: center; align-items: center; flex: 4; @media(max-width: 891px){ height: 100%; width: 100%; } ` const EnemyField = styled.div` display: flex; width: 70%; flex: 1; margin: 10px 0; overflow: auto; >* + * { margin-left: 10px; } > * { border-radius: 0.3rem; height: 100% !important; } &.attack-mode div:not(.empty-item) { background-color: tomato; cursor: pointer; &:hover { opacity: 0.7; } } @media(max-width: 891px){ width: 95%; } ` const FieldContainer = styled.div` background-color: lightgrey; flex: 1; width: 70%; ${(props) => props.width} ${({ top }) => top && css` border-radius: 0.3rem; overflow: hidden; @media(max-width: 891px){ width: 95%; } `} ${({ bottom }) => bottom && css` > div { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; } @media(max-width: 891px){ width: 95%; } `} margin: 0 auto; @media(max-width: 891px){ width: 95%; } ` const StatsBox = styled.div` position: absolute; background-color: white; border-radius: 0.3rem; min-width: 120px; overflow: hidden; @media(max-width: 891px){ width: 300px; margin-left: -150px; } @media(max-width: 425px){ width: 200px; margin-left: -100px; } ` const EnemyStatsBox = styled(StatsBox)` top: 15px; left: 15px; display: flex; flex-direction: column; > p { color: #fff; font-size: 20px; font-weight: 900; flex: 1; margin: 0; padding: 10px 20px; text-align: center; display: flex; justify-content: space-around; align-items: center; &:first-child{ position: absolute; bottom: -50px; text-transform: uppercase; @media(max-width: 891px){ display: none; } } &:nth-child(2){ background-color: #df2230; } &:last-child{ background-color: #7c18e0; } } &.attack-mode { background-color: tomato; cursor: pointer; color: white; &:hover { opacity: 0.7; } } @media(max-width: 891px){ top: -55px; left: 50%; flex-direction: row; } ` const AllyStatsBox = styled(StatsBox)` bottom: 15px; right: 15px; display: flex; flex-direction: column; > p { color: #fff; font-size: 20px; font-weight: 900; flex: 1; margin: 0; padding: 10px 20px; text-align: center; display: flex; justify-content: space-around; align-items: center; &:first-child{ position: absolute; top: -50px; text-transform: uppercase; @media(max-width: 891px){ display: none; } } &:nth-child(2){ background-color: #df2230; } &:last-child{ background-color: #7c18e0; } } @media(max-width: 891px){ bottom: -55px; flex-direction: row; left: 50%; } ` const EnemyDeck = styled.div` position: absolute; right: 15px; top: 15px; height: 100px; width:150px; text-align: center; display: flex; align-items: center; justify-content: center; font-weight: 900; background-color: rgb(105, 102, 102); border-radius: 0.2rem; @media(max-width: 891px){ display: none; } ` const YourDeck = styled.div` position: absolute; left: 15px; bottom: 15px; height: 100px; width:150px; display: flex; align-items: center; justify-content: center; font-weight: 900; background-color: #ff8a32; border-radius: 0.2rem; color: #000; @media(max-width: 891px){ display: none; } ` const ExitLink = styled(Link)` background-color: rgb(42, 90, 162); border: none; border-radius: 10px; padding: 20px; color: white; cursor: pointer; display: inline-block; text-decoration: none; min-width: 200px; margin-bottom: 20px; display: ${(props) => (props.hidden ? 'none' : 'block')}; &:hover { opacity: 0.7; } &:active { background-color: rgb(0, 98, 139); } &:disabled { background-color: rgb(105, 102, 102); opacity: 0.7; cursor: not-allowed; } ` const ResultMsg = styled.h1` font-size: ${(props) => (props.winner || props.loser ? '18pt' : '32px')}; color: ${(props) => props.winner ? 'green' : props.loser ? 'tomato' : 'black'}; ` const Button = styled.button` background-color: rgb(42, 90, 162); border: none; border-radius: 50%; width: 80px; height: 80px; position: absolute; right: 5%; bottom: 35%; color: white; cursor: pointer; text-decoration: none; &.end-turn{ top: 35%; @media(max-width: 891px){ top: 111.5%; left: 35% } @media(max-width: 630px){ left: 19%; } } &:hover { opacity: 0.7; } &:active { background-color: rgb(0, 98, 139); } &:disabled { background-color: rgb(105, 102, 102); opacity: 0.7; cursor: not-allowed; } @media(max-width: 891px){ top: 111.5%; right: 35%; width: 70px; height: 70px; } @media(max-width: 630px){ right: 25%; } ` const CardPanel = styled.div` background: ${({ outter }) => (outter ? '#1f1f1f' : 'lightgrey')}; display: flex; justify-content: center; align-items: center; height: 100%; overflow: auto; `