Skip to content

Commit

Permalink
feat(app-board): connect game board to state
Browse files Browse the repository at this point in the history
  • Loading branch information
rams23 committed Jan 18, 2021
1 parent c4a4e61 commit d258839
Show file tree
Hide file tree
Showing 13 changed files with 229 additions and 67 deletions.
2 changes: 1 addition & 1 deletion packages/game-app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ function App() {
<Suspense fallback={null}>
<Switch>
<PrivateRoute path={RoutingPath.Dashboard} component={Dashboard} />
<PrivateRoute path={`${RoutingPath.Game}/:id`} component={GameView} />
<PrivateRoute path={`${RoutingPath.Game}/:gameId`} component={GameView} />
<PrivateRoute path={RoutingPath.CreateGame} component={CreateGameView} />
{renderAuthRoutes(user)}
</Switch>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ export interface RequestsKeys {
'auth.login': null;
'auth.logout': null;
'game.loadCards': null;
'game.loadGame': null;
createGame: null;
}
9 changes: 9 additions & 0 deletions packages/game-app/src/gameView/apis/callLoadCardsForDeck.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import firebase from 'firebase/app';
import 'firebase/firestore';
import { CardEntity, FirebaseCollection } from '@pipeline/common';

export default async function loadCardsForDeck(deckId: string): Promise<CardEntity[]> {
const cards = await firebase.firestore().collection(FirebaseCollection.Cards).where('deckId', '==', deckId).get();

return cards.docs.map(d => ({ id: d.id, ...d.data() } as CardEntity));
}
9 changes: 9 additions & 0 deletions packages/game-app/src/gameView/apis/callLoadGame.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import firebase from 'firebase/app';
import 'firebase/firestore';
import { FirebaseCollection, Game } from '@pipeline/common';

export default async function loadGame(gameId: string): Promise<Game> {
const cardDoc = await firebase.firestore().collection(FirebaseCollection.Games).doc(gameId).get();

return cardDoc.data() as Game;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { RectEntry, ViewRect } from '@dnd-kit/core/dist/types';
import { createPortal } from 'react-dom';
import { Transform } from '@dnd-kit/utilities';
import { GameEvent, GameEventType } from '../../types/gameEvents';
import { GameState } from '../../types/gameState';
import { GameUIState } from '../../types/gameUIState';

const DEBUG_ENABLED = false;

Expand All @@ -20,7 +20,7 @@ type Props = {
/**
* Current game state containing cards positions and placement (board or panel).
*/
currentGameState: GameState;
currentGameState: GameUIState;
/**
* Current game board scale (used for target coordinates calculation).
*/
Expand Down Expand Up @@ -66,7 +66,7 @@ let movementStart = 0;
* Wrap panel and game with this
*/
const CardsGameListeners: React.FC<Props> = ({ onEvent, children, currentGameState, boardScale, panAmount }) => {
const gameStateRef = useRef<GameState>(currentGameState);
const gameStateRef = useRef<GameUIState>(currentGameState);
const translationDeltaRef = useRef<TranslationDeltas>({});
const absoluteItemPositionWithResectToWindowRef = useRef<AbsoluteWindowPositions>({});
const panScaleRef = useRef(boardScale);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import React from 'react';
import { useDraggable } from '@dnd-kit/core';
import { useSelector } from 'react-redux';
import { selectors } from '../../slice';

type Props = {
id: string;
position?: { x: number; y: number } | null;
};

/**
* A card enhanced with dragging capability
*/
const DraggableCard: React.FC<Props> = ({ id, position = null }) => {
const DraggableCard: React.FC<Props> = ({ id }) => {
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
id,
});

const position = useSelector(selectors.getCardPosition(id));

const style =
position?.x || position?.y
? {
Expand Down
97 changes: 41 additions & 56 deletions packages/game-app/src/gameView/components/GameView/GameView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,64 +5,42 @@ import Board from '../Board';
import Panel from '../Panel';
import { TansformRenderProps } from '../../types/tansformRenderProps';
import DraggableCard from '../DraggableCard';
import { GameState } from '../../types/gameState';
import { GameUIState } from '../../types/gameUIState';
import { GameEvent, GameEventType } from '../../types/gameEvents';
import { useParams } from 'react-router-dom';
import useGameState from '../../hooks/useGameState';
import { useSelector } from 'react-redux';
import { selectors } from '../../slice';
import useCardEventHandler from '../../hooks/useCardEventHandler';

const cardsNum = 10;
const Game: React.FC<{ pan: { x: number; y: number }; scale: number; gameId: string }> = React.memo(
({ pan, scale, gameId }) => {
const state = useSelector(selectors.getCardStateForUI);

const initialState = new Array(cardsNum)
.fill(null)
.map((value, index) => index)
.reduce((previousValue, currentValue) => {
return {
...previousValue,
[currentValue]: {
placedIn: 'panel',
},
};
}, {} as GameState);
const { deckCardsIds, placedCardsIds } = useGameState(gameId);

const Game: React.FC<{ pan: { x: number; y: number }; scale: number }> = React.memo(({ pan, scale }) => {
const [state, setState] = useState<GameState>(initialState);
const { onCardEvent } = useCardEventHandler();

const onCardEvent = useCallback((event: GameEvent) => {
if (event.type === GameEventType.CardMovingEnd) {
const { cardId, target } = event;
setState(currentState => {
return {
...currentState,
[cardId]: {
placedIn: target,
position: event.position,
},
};
});
}
}, []);

return (
<CardsGameListeners onEvent={onCardEvent} boardScale={scale} panAmount={pan} currentGameState={state}>
<div className="board-wrapper">
<TransformComponent ref={ref => console.debug('ref', ref)}>
<Board>
{Object.entries(state)
.filter(([key, state]) => state.placedIn === 'board')
.map(([key, state]) => (
<DraggableCard key={key} id={key} position={state.position} />
return (
<CardsGameListeners onEvent={onCardEvent} boardScale={scale} panAmount={pan} currentGameState={state}>
<div className="board-wrapper">
<TransformComponent ref={ref => console.debug('ref', ref)}>
<Board>
{placedCardsIds.map(id => (
<DraggableCard key={id} id={id} />
))}
</Board>
</TransformComponent>
</div>
<Panel>
{Object.entries(state)
.filter(([key, state]) => state.placedIn === 'panel')
.map(([key, state]) => (
<DraggableCard key={key} id={key} />
</Board>
</TransformComponent>
</div>
<Panel>
{deckCardsIds.map(id => (
<DraggableCard key={id} id={id} />
))}
</Panel>
</CardsGameListeners>
);
});
</Panel>
</CardsGameListeners>
);
},
);

const wheelConfig = { step: 70 };
const transformOptions: React.ComponentProps<typeof TransformWrapper>['options'] = {
Expand All @@ -71,11 +49,18 @@ const transformOptions: React.ComponentProps<typeof TransformWrapper>['options']
} as any;

const GameView: React.FC = () => {
const renderGame = useCallback((transformProps: TansformRenderProps) => {
const scale = transformProps.scale;
const pan = { x: transformProps.positionX, y: transformProps.positionY };
return <Game pan={pan} scale={scale} />;
}, []);
const params = useParams<{ gameId: string }>();

const gameId = params.gameId;

const renderGame = useCallback(
(transformProps: TansformRenderProps) => {
const scale = transformProps.scale;
const pan = { x: transformProps.positionX, y: transformProps.positionY };
return <Game pan={pan} scale={scale} gameId={gameId} />;
},
[gameId],
);

return (
<TransformWrapper defaultScale={1} options={transformOptions} wheel={wheelConfig}>
Expand Down
22 changes: 22 additions & 0 deletions packages/game-app/src/gameView/hooks/useCardEventHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useCallback } from 'react';
import { GameEvent, GameEventType } from '../types/gameEvents';
import { useDispatch } from 'react-redux';
import { actions } from '../slice';

export default function useCardEventHandler() {
const dispatch = useDispatch();

const onCardEvent = useCallback(
(event: GameEvent) => {
if (event.type === GameEventType.CardMovingEnd) {
const { cardId, target, position } = event;
dispatch(actions.updateCardPosition({ cardId, position, target }));
}
},
[dispatch],
);

return {
onCardEvent,
};
}
22 changes: 22 additions & 0 deletions packages/game-app/src/gameView/hooks/useGameState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useDispatch, useSelector } from 'react-redux';
import { selectors, actions } from '../slice';
import { useEffect } from 'react';

export default function useGameState(currentGame: string) {
const placedCardsIds = useSelector(selectors.getPlacedCards);
const deckCardsIds = useSelector(selectors.getDeckCardsIds);
const selectedGameId = useSelector(selectors.getSelectedGameId);

const dispatch = useDispatch();

useEffect(() => {
if ((!placedCardsIds && !deckCardsIds) || selectedGameId !== currentGame) {
dispatch(actions.loadGame(currentGame));
}
}, [placedCardsIds, deckCardsIds, dispatch, currentGame, selectedGameId]);

return {
placedCardsIds: placedCardsIds || [],
deckCardsIds: deckCardsIds || [],
};
}
5 changes: 3 additions & 2 deletions packages/game-app/src/gameView/sagas/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { all } from 'redux-saga/effects';
import loadGards from './loadCards';
import loadCards from './loadCards';
import loadGame from './loadGame';

export default function* gameSaga() {
yield all([loadGards()]);
yield all([loadCards(), loadGame()]);
}
24 changes: 24 additions & 0 deletions packages/game-app/src/gameView/sagas/loadGame.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { call, put, takeEvery } from 'redux-saga/effects';
import { actions, GameState } from '../slice';
import { addRequestStatusManagement } from '@pipeline/requests-status';
import { CardEntity, Game } from '@pipeline/common';
import loadGame from '../apis/callLoadGame';
import loadCardsForDeck from '../apis/callLoadCardsForDeck';

function* executeLoadGame(action: ReturnType<typeof actions.loadGame>) {
const game: Game = yield call(loadGame, action.payload);

const cards: CardEntity[] = yield call(loadCardsForDeck, game.deckId);
yield put(actions.saveCards(cards));
// TODO load actual game state from firestore
const gameState: GameState = {
boardCards: [],
deckCards: cards.map(c => c.id),
cardsState: {},
};
yield put(actions.setInitialGameState({ state: gameState, gameId: action.payload }));
}

export default function* loadGameSaga() {
yield takeEvery(actions.loadGame, addRequestStatusManagement(executeLoadGame, 'game.loadGame'));
}
Loading

0 comments on commit d258839

Please sign in to comment.