diff --git a/.yarn/cache/@babel-runtime-npm-7.18.6-6a59ef0d54-8b707b64ae.zip b/.yarn/cache/@babel-runtime-npm-7.18.6-6a59ef0d54-8b707b64ae.zip new file mode 100644 index 00000000..afffbf6d Binary files /dev/null and b/.yarn/cache/@babel-runtime-npm-7.18.6-6a59ef0d54-8b707b64ae.zip differ diff --git a/.yarn/cache/@reduxjs-toolkit-npm-1.8.3-7a05ac0aba-2c932ac6fa.zip b/.yarn/cache/@reduxjs-toolkit-npm-1.8.3-7a05ac0aba-2c932ac6fa.zip new file mode 100644 index 00000000..c0523a35 Binary files /dev/null and b/.yarn/cache/@reduxjs-toolkit-npm-1.8.3-7a05ac0aba-2c932ac6fa.zip differ diff --git a/.yarn/cache/@types-hoist-non-react-statics-npm-3.3.1-c0081332b2-2c0778570d.zip b/.yarn/cache/@types-hoist-non-react-statics-npm-3.3.1-c0081332b2-2c0778570d.zip new file mode 100644 index 00000000..6803d574 Binary files /dev/null and b/.yarn/cache/@types-hoist-non-react-statics-npm-3.3.1-c0081332b2-2c0778570d.zip differ diff --git a/.yarn/cache/@types-react-redux-npm-7.1.24-31c5f19b33-6582246581.zip b/.yarn/cache/@types-react-redux-npm-7.1.24-31c5f19b33-6582246581.zip new file mode 100644 index 00000000..9c4b90cb Binary files /dev/null and b/.yarn/cache/@types-react-redux-npm-7.1.24-31c5f19b33-6582246581.zip differ diff --git a/.yarn/cache/@types-use-sync-external-store-npm-0.0.3-875a91a914-161ddb8eec.zip b/.yarn/cache/@types-use-sync-external-store-npm-0.0.3-875a91a914-161ddb8eec.zip new file mode 100644 index 00000000..65f8d4f4 Binary files /dev/null and b/.yarn/cache/@types-use-sync-external-store-npm-0.0.3-875a91a914-161ddb8eec.zip differ diff --git a/.yarn/cache/react-redux-npm-8.0.2-0855b9ffc2-44c1739c45.zip b/.yarn/cache/react-redux-npm-8.0.2-0855b9ffc2-44c1739c45.zip new file mode 100644 index 00000000..1eacd0a0 Binary files /dev/null and b/.yarn/cache/react-redux-npm-8.0.2-0855b9ffc2-44c1739c45.zip differ diff --git a/.yarn/cache/redux-npm-4.2.0-4688cc8d65-75f3955c89.zip b/.yarn/cache/redux-npm-4.2.0-4688cc8d65-75f3955c89.zip new file mode 100644 index 00000000..6ecf8c2c Binary files /dev/null and b/.yarn/cache/redux-npm-4.2.0-4688cc8d65-75f3955c89.zip differ diff --git a/.yarn/cache/redux-thunk-npm-2.4.1-2ba08bf615-af5abb425f.zip b/.yarn/cache/redux-thunk-npm-2.4.1-2ba08bf615-af5abb425f.zip new file mode 100644 index 00000000..03a3656c Binary files /dev/null and b/.yarn/cache/redux-thunk-npm-2.4.1-2ba08bf615-af5abb425f.zip differ diff --git a/.yarn/cache/reselect-npm-4.1.6-869f318cc3-3ea1058422.zip b/.yarn/cache/reselect-npm-4.1.6-869f318cc3-3ea1058422.zip new file mode 100644 index 00000000..e0fa67b4 Binary files /dev/null and b/.yarn/cache/reselect-npm-4.1.6-869f318cc3-3ea1058422.zip differ diff --git a/.yarn/cache/use-sync-external-store-npm-1.2.0-44f75d2564-5c639e0f8d.zip b/.yarn/cache/use-sync-external-store-npm-1.2.0-44f75d2564-5c639e0f8d.zip new file mode 100644 index 00000000..d737a8fc Binary files /dev/null and b/.yarn/cache/use-sync-external-store-npm-1.2.0-44f75d2564-5c639e0f8d.zip differ diff --git a/client/src/BoardGame.tsx b/client/src/BoardGame.tsx index 6c0090a6..a2d89c57 100644 --- a/client/src/BoardGame.tsx +++ b/client/src/BoardGame.tsx @@ -3,15 +3,15 @@ import { Client, BoardProps } from 'boardgame.io/react'; import { Local } from 'boardgame.io/multiplayer'; import { OpenStarTerVillageType } from 'packages/game/src/types'; import Table from './features/Table/Table'; -import Players from './Players/Players'; -import CurrentPlayer from './CurrentPlayer/CurrentPlayer'; +import ActionBoard from './features/ActionBoard/ActionBoard'; +import PlayerHand from './features/PlayerHand/PlayerHand'; const Board: React.FC> = (props) => { return (
+ - - + ); } diff --git a/client/src/app/hooks.ts b/client/src/app/hooks.ts new file mode 100644 index 00000000..6fefa5b1 --- /dev/null +++ b/client/src/app/hooks.ts @@ -0,0 +1,7 @@ +import { useDispatch, useSelector } from 'react-redux' +import type { TypedUseSelectorHook } from 'react-redux' +import type { RootState, AppDispatch } from './store' + +export const useAppDispatch: () => AppDispatch = useDispatch + +export const useAppSelector: TypedUseSelectorHook = useSelector diff --git a/client/src/app/store.ts b/client/src/app/store.ts new file mode 100644 index 00000000..ed8cee45 --- /dev/null +++ b/client/src/app/store.ts @@ -0,0 +1,12 @@ +import { configureStore } from '@reduxjs/toolkit' +import actionBoardSlice from '../features/ActionBoard/actionBoardSlice'; + +export const store = configureStore({ + reducer: { + actionBoard: actionBoardSlice.reducer + }, +}) + +export type RootState = ReturnType + +export type AppDispatch = typeof store.dispatch diff --git a/client/src/features/ActionBoard/ActionBoard.tsx b/client/src/features/ActionBoard/ActionBoard.tsx new file mode 100644 index 00000000..64140be7 --- /dev/null +++ b/client/src/features/ActionBoard/ActionBoard.tsx @@ -0,0 +1,56 @@ +import { useCallback, useEffect } from 'react'; +import { BoardProps } from 'boardgame.io/react'; +import { OpenStarTerVillageType as Type } from 'packages/game/src/types'; +import { Box, Heading, ButtonGroup, Button } from '@chakra-ui/react' +import { useAppDispatch, useAppSelector } from '../../app/hooks'; +import ActionBoardSlice from './actionBoardSlice'; + +const ActionBoard: React.FC> = (props) => { + const dispatch = useAppDispatch(); + const playerID = props.playerID; + const currentPlayer = props.ctx.currentPlayer; + const currentMove = useAppSelector(state => + state.actionBoard.moves.length + ? state.actionBoard.moves[state.actionBoard.moves.length - 1] + : null + ); + const isCreateProjectMoveActive = currentMove?.move === 'createProject'; + + useEffect(() => { + if (playerID === currentPlayer) { + dispatch(ActionBoardSlice.actions.playerTurnInited(currentPlayer)); + } + }, [playerID, currentPlayer, dispatch]); + + const handleCreateProjectButtonClick = useCallback(() => { + if (isCreateProjectMoveActive) { + return; + } + + dispatch(ActionBoardSlice.actions.moveInited({ moveType: 'createProject' })); + }, [isCreateProjectMoveActive, dispatch]); + + return ( + <> + + Current Player: {currentPlayer} + + + {playerID === currentPlayer && ( + + + + + + )} + + + ); +}; + +export default ActionBoard; diff --git a/client/src/features/ActionBoard/ActionBoard.types.ts b/client/src/features/ActionBoard/ActionBoard.types.ts new file mode 100644 index 00000000..79f44433 --- /dev/null +++ b/client/src/features/ActionBoard/ActionBoard.types.ts @@ -0,0 +1,13 @@ +import { OpenStarTerVillageType as Type } from 'packages/game/src/types'; + +export type MoveType = keyof Type.Move.AllMoves; + +export interface Move { + move: MoveType; + selections: { + tableProject: boolean[]; + tableJobs: boolean[]; + handProjects: boolean[]; + handForces: boolean[]; + }; +} diff --git a/client/src/features/ActionBoard/actionBoardSlice.tsx b/client/src/features/ActionBoard/actionBoardSlice.tsx new file mode 100644 index 00000000..425be871 --- /dev/null +++ b/client/src/features/ActionBoard/actionBoardSlice.tsx @@ -0,0 +1,69 @@ +import { createSlice } from '@reduxjs/toolkit' +import type { PayloadAction } from '@reduxjs/toolkit' +import { PlayerID } from 'boardgame.io'; +import { MoveType, Move } from './ActionBoard.types'; + +export interface ActionBoardState { + currentPlayer: PlayerID | null; + moves: Move[]; +} + +const initialState: ActionBoardState = { + currentPlayer: null, + moves: [] +} + +export const ActionBoardSlice = createSlice({ + name: 'actionBoard', + initialState, + reducers: { + playerTurnInited: (state, action: PayloadAction) => { + const playerId = action.payload; + + state.currentPlayer = playerId; + state.moves = []; + }, + moveInited: (state, action: PayloadAction<{ moveType: MoveType }>) => { + const { moveType } = action.payload; + + state.moves.push({ + move: moveType, + selections: { + tableProject: [], + tableJobs: [], + handProjects: [], + handForces: [], + } + }); + }, + tableProjectToggled: (state, action: PayloadAction<{ moveIndex: number, tableProjectIndex: number }>) => { + const { moveIndex, tableProjectIndex } = action.payload; + + state.moves[moveIndex].selections.tableProject[tableProjectIndex] = + !state.moves[moveIndex].selections.tableProject[tableProjectIndex]; + }, + tableJobsToggled: (state, action: PayloadAction<{ moveIndex: number, tableJobsIndex: number }>) => { + const { moveIndex, tableJobsIndex } = action.payload; + + state.moves[moveIndex].selections.tableJobs[tableJobsIndex] = + !state.moves[moveIndex].selections.tableJobs[tableJobsIndex]; + }, + handProjectsToggled: (state, action: PayloadAction<{ moveIndex: number, handProjectsIndex: number }>) => { + const { moveIndex, handProjectsIndex } = action.payload; + + state.moves[moveIndex].selections.handProjects[handProjectsIndex] = + !state.moves[moveIndex].selections.handProjects[handProjectsIndex]; + }, + handForcesToggled: (state, action: PayloadAction<{ moveIndex: number, handForcesIndex: number }>) => { + const { moveIndex, handForcesIndex } = action.payload; + + state.moves[moveIndex].selections.handForces[handForcesIndex] = + !state.moves[moveIndex].selections.handForces[handForcesIndex]; + }, + moveSubmitted: (state, action: PayloadAction<{ moveIndex: number }>) => { + // TODO: call api + } + } +}) + +export default ActionBoardSlice diff --git a/client/src/features/Job/ActiveJob.tsx b/client/src/features/Job/ActiveJob.tsx new file mode 100644 index 00000000..1d11438b --- /dev/null +++ b/client/src/features/Job/ActiveJob.tsx @@ -0,0 +1,26 @@ +import { useCallback } from 'react'; +import { Box, Text } from '@chakra-ui/react'; +import { OpenStarTerVillageType as Type } from 'packages/game/src/types'; + +export interface IActiveJobProps { + job: Type.Card.Job; + jobIndex: number; + onClick: (e: { job: IActiveJobProps['job'], jobIndex: IActiveJobProps['jobIndex'] }) => void; + selected?: boolean; +} + +const ActiveJob: React.FC = (props) => { + const { job, jobIndex, onClick, selected } = props; + const boarderColor = selected ? "red.300" : "gray.100"; + const handleClick = useCallback(() => { + onClick({ job, jobIndex }); + }, [job, jobIndex, onClick]) + + return ( + + {job.name} + + ); +} + +export default ActiveJob; diff --git a/client/src/features/PlayerHand/PlayerHand.tsx b/client/src/features/PlayerHand/PlayerHand.tsx new file mode 100644 index 00000000..fe7b421f --- /dev/null +++ b/client/src/features/PlayerHand/PlayerHand.tsx @@ -0,0 +1,56 @@ +import { BoardProps } from 'boardgame.io/react'; +import { OpenStarTerVillageType as Type } from 'packages/game/src/types'; +import { Box, Heading, HStack, Table, TableCaption, TableContainer, Tbody, Td, Tfoot, Th, Thead, Tr } from '@chakra-ui/react'; + +const PlayerHand: React.FC> = (props) => { + const { G, playerID } = props; + + const headRow = ( + + + + + ); + + const requirementRows = (requirements: Record) => { + return Object.entries(requirements).map(([jobName, contribution]) => { + return ( + + + + + ) + }) + }; + + const renderHandProjectCards = () => ( + + { + playerID && G.players[playerID].hand.projects.map((project, index) => + + +
職業貢獻
{jobName}{contribution}
+ {project.name} + + {headRow} + + + {requirementRows(project.requirements)} + +
+ + ) + } + + ); + + return ( + <> + Player Hand + Project Cards + {renderHandProjectCards()} + + ); +} + +export default PlayerHand; diff --git a/client/src/features/Table/Table.tsx b/client/src/features/Table/Table.tsx index c7b64f33..d27e7edc 100644 --- a/client/src/features/Table/Table.tsx +++ b/client/src/features/Table/Table.tsx @@ -1,23 +1,63 @@ +import { useCallback } from 'react'; import { BoardProps } from 'boardgame.io/react'; import { OpenStarTerVillageType as Type } from 'packages/game/src/types'; import { Box, Stack } from '@chakra-ui/react'; import ActiveProject from '../Project/ActiveProject'; +import ActiveJob from '../Job/ActiveJob'; +import { useAppSelector, useAppDispatch } from '../../app/hooks'; +import ActionBoardSlice from '../ActionBoard/actionBoardSlice'; type Props = BoardProps; const Table: React.FC = (props) => { + const dispatch = useAppDispatch(); const activeProjects = [...props.G.table.activeProjects, ...Array(6)].slice(0, 6); + const activeJobs = props.G.table.activeJobs; + const isCurrentPlayer = props.playerID === props.ctx.currentPlayer; + const currentMoveIndex = useAppSelector(state => + state.actionBoard.moves.length - 1 + ); + const currentMove = useAppSelector(state => + state.actionBoard.moves.length + ? state.actionBoard.moves[state.actionBoard.moves.length - 1] + : null + ); + const handleActiveJobClick = useCallback(({ jobIndex }) => { + if (!isCurrentPlayer) { + return; + } + + dispatch(ActionBoardSlice.actions.tableJobsToggled({ moveIndex: currentMoveIndex, tableJobsIndex: jobIndex })); + }, [isCurrentPlayer, currentMoveIndex, dispatch]) return ( - {activeProjects.map((p, pIndex) => ( - - - - ))} - - + + + {activeProjects.map((p, pIndex) => ( + + + + ))} + + + + + {activeJobs.map((job, jobIndex) => ( + + + + ))} + + + + ) } diff --git a/client/src/index.tsx b/client/src/index.tsx index 77cfed30..dbc33e3d 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -4,12 +4,16 @@ import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; +import { store } from './app/store' +import { Provider as ReduxProvider } from 'react-redux' ReactDOM.render( - - - + + + + + , document.getElementById('root') ); diff --git a/package.json b/package.json index 66a8942a..6fe16f58 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,12 @@ "test": "yarn workspaces foreach -p run test" }, "devDependencies": { - "@types/koa-static": "^4.0.2" + "@types/koa-static": "^4.0.2", + "@types/react-redux": "^7.1.24" }, "dependencies": { - "koa-static": "^5.0.0" + "@reduxjs/toolkit": "^1.8.3", + "koa-static": "^5.0.0", + "react-redux": "^8.0.2" } } diff --git a/yarn.lock b/yarn.lock index 134661f8..48a0dab2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1568,6 +1568,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.12.1": + version: 7.18.6 + resolution: "@babel/runtime@npm:7.18.6" + dependencies: + regenerator-runtime: ^0.13.4 + checksum: 8b707b64ae0524db617d0c49933b258b96376a38307dc0be8fb42db5697608bcc1eba459acce541e376cff5ed5c5287d24db5780bd776b7c75ba2c2e26ff8a2c + languageName: node + linkType: hard + "@babel/template@npm:^7.16.7, @babel/template@npm:^7.3.3": version: 7.16.7 resolution: "@babel/template@npm:7.16.7" @@ -3521,6 +3530,26 @@ __metadata: languageName: node linkType: hard +"@reduxjs/toolkit@npm:^1.8.3": + version: 1.8.3 + resolution: "@reduxjs/toolkit@npm:1.8.3" + dependencies: + immer: ^9.0.7 + redux: ^4.1.2 + redux-thunk: ^2.4.1 + reselect: ^4.1.5 + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 + react-redux: ^7.2.1 || ^8.0.2 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + checksum: 2c932ac6fa430b982108829a6150ca0c15ff617eb121b8055b91a89ab2124e8d93a1277ccdc96ff8af680fbde9d4c7021b1f924f11e28004ee61c6a7d39915bc + languageName: node + linkType: hard + "@rollup/plugin-babel@npm:^5.2.0": version: 5.3.1 resolution: "@rollup/plugin-babel@npm:5.3.1" @@ -4148,6 +4177,16 @@ __metadata: languageName: node linkType: hard +"@types/hoist-non-react-statics@npm:^3.3.0, @types/hoist-non-react-statics@npm:^3.3.1": + version: 3.3.1 + resolution: "@types/hoist-non-react-statics@npm:3.3.1" + dependencies: + "@types/react": "*" + hoist-non-react-statics: ^3.3.0 + checksum: 2c0778570d9a01d05afabc781b32163f28409bb98f7245c38d5eaf082416fdb73034003f5825eb5e21313044e8d2d9e1f3fe2831e345d3d1b1d20bcd12270719 + languageName: node + linkType: hard + "@types/html-minifier-terser@npm:^6.0.0": version: 6.1.0 resolution: "@types/html-minifier-terser@npm:6.1.0" @@ -4422,6 +4461,18 @@ __metadata: languageName: node linkType: hard +"@types/react-redux@npm:^7.1.24": + version: 7.1.24 + resolution: "@types/react-redux@npm:7.1.24" + dependencies: + "@types/hoist-non-react-statics": ^3.3.0 + "@types/react": "*" + hoist-non-react-statics: ^3.3.0 + redux: ^4.0.0 + checksum: 6582246581331ac7fbbd44aa1f1c136c8a9c8febbcf462432ac81302263308c21e1a2e7868beb7f73bbcb52a8e67935d133cb37f5bdcb6564eaff3a811805101 + languageName: node + linkType: hard + "@types/react@npm:*, @types/react@npm:^17.0.0": version: 17.0.39 resolution: "@types/react@npm:17.0.39" @@ -4527,6 +4578,13 @@ __metadata: languageName: node linkType: hard +"@types/use-sync-external-store@npm:^0.0.3": + version: 0.0.3 + resolution: "@types/use-sync-external-store@npm:0.0.3" + checksum: 161ddb8eec5dbe7279ac971531217e9af6b99f7783213566d2b502e2e2378ea19cf5e5ea4595039d730aa79d3d35c6567d48599f69773a02ffcff1776ec2a44e + languageName: node + linkType: hard + "@types/warning@npm:^3.0.0": version: 3.0.0 resolution: "@types/warning@npm:3.0.0" @@ -9184,7 +9242,7 @@ __metadata: languageName: node linkType: hard -"hoist-non-react-statics@npm:^3.3.1": +"hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.1, hoist-non-react-statics@npm:^3.3.2": version: 3.3.2 resolution: "hoist-non-react-statics@npm:3.3.2" dependencies: @@ -12626,8 +12684,11 @@ __metadata: version: 0.0.0-use.local resolution: "open-star-ter-village@workspace:." dependencies: + "@reduxjs/toolkit": ^1.8.3 "@types/koa-static": ^4.0.2 + "@types/react-redux": ^7.1.24 koa-static: ^5.0.0 + react-redux: ^8.0.2 languageName: unknown linkType: soft @@ -14253,6 +14314,38 @@ __metadata: languageName: node linkType: hard +"react-redux@npm:^8.0.2": + version: 8.0.2 + resolution: "react-redux@npm:8.0.2" + dependencies: + "@babel/runtime": ^7.12.1 + "@types/hoist-non-react-statics": ^3.3.1 + "@types/use-sync-external-store": ^0.0.3 + hoist-non-react-statics: ^3.3.2 + react-is: ^18.0.0 + use-sync-external-store: ^1.0.0 + peerDependencies: + "@types/react": ^16.8 || ^17.0 || ^18.0 + "@types/react-dom": ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + react-native: ">=0.59" + redux: ^4 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + react-dom: + optional: true + react-native: + optional: true + redux: + optional: true + checksum: 44c1739c45dad04ecc65a290897c73828ff0bf43f2b7618ed5ef6d4ceecedae38e76cecd189a5ecedf579c28ead05427bc000fb45ad30b9fcd5c2be27cd3ac73 + languageName: node + linkType: hard + "react-refresh@npm:^0.11.0": version: 0.11.0 resolution: "react-refresh@npm:0.11.0" @@ -14466,6 +14559,24 @@ __metadata: languageName: node linkType: hard +"redux-thunk@npm:^2.4.1": + version: 2.4.1 + resolution: "redux-thunk@npm:2.4.1" + peerDependencies: + redux: ^4 + checksum: af5abb425fb9dccda02e5f387d6f3003997f62d906542a3d35fc9420088f550dc1a018bdc246c7d23ee852b4d4ab8b5c64c5be426e45a328d791c4586a3c6b6e + languageName: node + linkType: hard + +"redux@npm:^4.0.0, redux@npm:^4.1.2": + version: 4.2.0 + resolution: "redux@npm:4.2.0" + dependencies: + "@babel/runtime": ^7.9.2 + checksum: 75f3955c89b3f18edf5411e5fb482aa2e4f41a416183e8802a6bf6472c4fc3d47675b8b321d147f8af8e0f616436ac507bf5a25f1c4d6180e797b549c7db2c1d + languageName: node + linkType: hard + "redux@npm:^4.1.0": version: 4.1.2 resolution: "redux@npm:4.1.2" @@ -14660,6 +14771,13 @@ __metadata: languageName: node linkType: hard +"reselect@npm:^4.1.5": + version: 4.1.6 + resolution: "reselect@npm:4.1.6" + checksum: 3ea1058422904063ec93c8f4693fe33dcb2178bbf417ace8db5b2c797a5875cf357d9308d11ed3942ee22507dd34ecfbf1f3a21340a4f31c206cab1d36ceef31 + languageName: node + linkType: hard + "resolve-cwd@npm:^3.0.0": version: 3.0.0 resolution: "resolve-cwd@npm:3.0.0" @@ -16724,6 +16842,15 @@ resolve@^2.0.0-next.3: languageName: node linkType: hard +"use-sync-external-store@npm:^1.0.0": + version: 1.2.0 + resolution: "use-sync-external-store@npm:1.2.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 5c639e0f8da3521d605f59ce5be9e094ca772bd44a4ce7322b055a6f58eeed8dda3c94cabd90c7a41fb6fa852210092008afe48f7038792fd47501f33299116a + languageName: node + linkType: hard + "use@npm:^3.1.0": version: 3.1.1 resolution: "use@npm:3.1.1"