diff --git a/clients/design-system/src/game-assets/README.md b/clients/design-system/src/game-assets/README.md new file mode 100644 index 00000000..8c891a61 --- /dev/null +++ b/clients/design-system/src/game-assets/README.md @@ -0,0 +1,2 @@ +Game assets are intentionally not realistic to help players to suspend belief +due to the [uncanny valley](https://en.wikipedia.org/wiki/Uncanny_valley). diff --git a/clients/design-system/vite.config.ts b/clients/design-system/vite.config.ts index c357a6d4..ce870d70 100644 --- a/clients/design-system/vite.config.ts +++ b/clients/design-system/vite.config.ts @@ -55,5 +55,11 @@ export default defineConfig({ entryFileNames: '[name].js', }, }, + cssCodeSplit: false, + }, + css: { + modules: { + scopeBehaviour: 'global', + }, }, }); diff --git a/clients/player-client/package.json b/clients/player-client/package.json index de53b3b6..908094f8 100644 --- a/clients/player-client/package.json +++ b/clients/player-client/package.json @@ -23,6 +23,7 @@ "dependencies": { "@galaxyops/design-system": "workspace:*", "axios": "^1.6.7", + "luxon": "~3.3.0", "react": "18.2.0", "react-dom": "18.2.0", "react-router-dom": "6.22.1" @@ -34,6 +35,7 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.0.0", "@types/jest": "29.5.12", + "@types/luxon": "~3.2.0", "@types/react": "18.2.59", "@types/react-dom": "18.2.19", "@typescript-eslint/eslint-plugin": "~6.16.0", diff --git a/clients/player-client/src/context/Action.type.ts b/clients/player-client/src/context/Action.type.ts new file mode 100644 index 00000000..d72f5f30 --- /dev/null +++ b/clients/player-client/src/context/Action.type.ts @@ -0,0 +1,6 @@ +import { Actions } from './Actions.type'; + +export type Action = + | { type: Actions.SET_USER; payload: string } + | { type: Actions.LOGOUT } + | { type: Actions.PAGE_LOADING; payload: boolean }; diff --git a/clients/player-client/src/context/Actions.type.ts b/clients/player-client/src/context/Actions.type.ts new file mode 100644 index 00000000..95980307 --- /dev/null +++ b/clients/player-client/src/context/Actions.type.ts @@ -0,0 +1,5 @@ +export enum Actions { + PAGE_LOADING = 'PAGE_LOADING', + SET_USER = 'SET_USER', + LOGOUT = 'LOGOUT', +} diff --git a/clients/player-client/src/context/AppContext.ts b/clients/player-client/src/context/AppContext.ts new file mode 100644 index 00000000..dc7191d1 --- /dev/null +++ b/clients/player-client/src/context/AppContext.ts @@ -0,0 +1,11 @@ +import { createContext } from 'react'; +import { AppState } from './AppState.type'; +import { Action } from './Action.type'; + +export const AppContext = createContext< + | { + state: AppState; + dispatch: React.Dispatch; + } + | undefined +>(undefined); diff --git a/clients/player-client/src/context/AppProvider.tsx b/clients/player-client/src/context/AppProvider.tsx index da0dc4f7..eb0f93cc 100644 --- a/clients/player-client/src/context/AppProvider.tsx +++ b/clients/player-client/src/context/AppProvider.tsx @@ -1,23 +1,10 @@ -import React, { createContext, useReducer, useContext } from 'react'; - -enum Actions { - PAGE_LOADING = 'PAGE_LOADING', - SET_USER = 'SET_USER', - LOGOUT = 'LOGOUT', -} - -interface AppState { - user: string | null; - isLoading?: boolean; -} +import React, { useReducer } from 'react'; +import { Action } from './Action.type'; +import { Actions } from './Actions.type'; +import { AppState } from './AppState.type'; +import { AppContext } from './AppContext'; // TODO add footnotes - -type Action = - | { type: Actions.SET_USER; payload: string } - | { type: Actions.LOGOUT } - | { type: Actions.PAGE_LOADING; payload: boolean }; - const reducer = (state: AppState, action: Action): AppState => { switch (action.type) { case Actions.PAGE_LOADING: @@ -31,14 +18,6 @@ const reducer = (state: AppState, action: Action): AppState => { } }; -const AppContext = createContext< - | { - state: AppState; - dispatch: React.Dispatch; - } - | undefined ->(undefined); - export const AppProvider: React.FC<{ children: React.ReactNode }> = props => { const { children } = props; const [state, dispatch] = useReducer(reducer, { user: null }); @@ -49,11 +28,3 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = props => { ); }; - -export const useAppContext = () => { - const context = useContext(AppContext); - if (!context) { - throw new Error('useAppContext must be used within an AppProvider'); - } - return context; -}; diff --git a/clients/player-client/src/context/AppState.type.ts b/clients/player-client/src/context/AppState.type.ts new file mode 100644 index 00000000..5618d77c --- /dev/null +++ b/clients/player-client/src/context/AppState.type.ts @@ -0,0 +1,4 @@ +export interface AppState { + user: string | null; + isLoading?: boolean; +} diff --git a/clients/player-client/src/context/useContext.ts b/clients/player-client/src/context/useContext.ts new file mode 100644 index 00000000..40bbc7c1 --- /dev/null +++ b/clients/player-client/src/context/useContext.ts @@ -0,0 +1,10 @@ +import { useContext } from 'react'; +import { AppContext } from './AppContext'; + +export const useAppContext = () => { + const context = useContext(AppContext); + if (!context) { + throw new Error('useAppContext must be used within an AppProvider'); + } + return context; +}; diff --git a/clients/player-client/src/core/config/keyboard-bindings.ts b/clients/player-client/src/core/config/keyboard-bindings.ts new file mode 100644 index 00000000..f4deffb9 --- /dev/null +++ b/clients/player-client/src/core/config/keyboard-bindings.ts @@ -0,0 +1,16 @@ +// keyboard +export const keyboardBindings = { + up: 'w', + down: 's', + left: 'a', + right: 'd', + jump: ' ', + debug: 'p', + select: 'Enter', + back: 'u', + pause: 'Escape', +}; + +// other inputs will be touch and based +// may need to display buttons on screen for touch +// move by clicking a tile.. diff --git a/clients/player-client/src/core/types/index.ts b/clients/player-client/src/core/types/index.ts new file mode 100644 index 00000000..252f5201 --- /dev/null +++ b/clients/player-client/src/core/types/index.ts @@ -0,0 +1 @@ +export * from './player-input-record.type'; diff --git a/clients/player-client/src/core/types/player-input-record.type.ts b/clients/player-client/src/core/types/player-input-record.type.ts new file mode 100644 index 00000000..e3039862 --- /dev/null +++ b/clients/player-client/src/core/types/player-input-record.type.ts @@ -0,0 +1,4 @@ +export type PlayerInputRecord = { + key: string; + timestamp: string; +}; diff --git a/clients/player-client/src/core/useHandleInput.ts b/clients/player-client/src/core/useHandleInput.ts new file mode 100644 index 00000000..ae7fdac3 --- /dev/null +++ b/clients/player-client/src/core/useHandleInput.ts @@ -0,0 +1,46 @@ +import { useState, useEffect } from 'react'; +import { keyboardBindings } from './config/keyboard-bindings'; +import { DateTime } from 'luxon'; +import { PlayerInputRecord } from './types'; + +export default function useHandleInput(): PlayerInputRecord[] | null { + const [inputState, setInputState] = useState( + null, + ); + const maxRecords = 5; // Set the maximum number of records + + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + const newInputRecords: PlayerInputRecord[] = []; + + // Handle different key strokes + Object.entries(keyboardBindings).forEach(([direction, key]) => { + if (event.key === key) { + // Collect key press for the given direction + newInputRecords.push({ + key: direction, + timestamp: DateTime.now().toISO(), + }); + } + }); + + if (newInputRecords.length > 0) { + setInputState(prevState => { + const newState = prevState ? [...prevState] : []; + newInputRecords.forEach(record => { + newState.unshift(record); + }); + return newState.slice(0, maxRecords); + }); + } + }; + + window.addEventListener('keydown', handleKeyDown); + + return () => { + window.removeEventListener('keydown', handleKeyDown); + }; + }, []); + + return inputState; +} diff --git a/clients/player-client/src/pages/home.page.tsx b/clients/player-client/src/pages/home.page.tsx index f1712fcf..8c2a6364 100644 --- a/clients/player-client/src/pages/home.page.tsx +++ b/clients/player-client/src/pages/home.page.tsx @@ -3,10 +3,14 @@ import { IsometricCanvasProps, } from '@galaxyops/design-system/dist/main'; import gameState from './game-state.json'; +import useHandleInput from '../core/useHandleInput'; export default function HomePage() { + const input = useHandleInput(); + return ( <> + {input &&
Input: {input?.map(i => i.key).join(', ')}
} {/* @ts-expect-error needed for types temp */} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c9dc1755..972baa64 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -564,6 +564,9 @@ importers: axios: specifier: ^1.6.7 version: 1.7.7 + luxon: + specifier: ~3.3.0 + version: 3.3.0 react: specifier: 18.2.0 version: 18.2.0 @@ -592,6 +595,9 @@ importers: '@types/jest': specifier: 29.5.12 version: 29.5.12 + '@types/luxon': + specifier: ~3.2.0 + version: 3.2.2 '@types/react': specifier: 18.2.59 version: 18.2.59