Edit page details, start integrating socket

This commit is contained in:
snen 2021-09-24 00:47:45 -04:00
parent 4bcb0e947d
commit 6d3343d064
6 changed files with 104 additions and 42 deletions

View File

@ -1,6 +1,6 @@
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import useWebSocket from 'react-use-websocket'
import useSocket from './useSocket.ts'
import {CardInstance} from './types.ts' import {CardInstance} from './types.ts'
interface GameActionsContextValue {} interface GameActionsContextValue {}
@ -39,19 +39,13 @@ export default function Game(props: GameProps): JSX.Element {
const [state, setState] = React.useState() const [state, setState] = React.useState()
// ensure this is stable wrt state so that onMessage does not have to be constantly reattached // ensure this is stable wrt state so that onMessage does not have to be constantly reattached
const onMessage = React.useCallback(() => {}, []) // const onMessage = React.useCallback(() => {}, [])
const WS_URL = `ws://localhost:7636/ws` // const handle = useSocket()
const socket = useWebSocket(WS_URL, {onMessage})
useEffect(() => {
setInterval(() => console.log(socket.getWebSocket()), 3000)
}, [socket])
const gameActions = React.useMemo<GameActionsContextValue>(() => ({}), [socket.sendJsonMessage])
return ( return (
<GameActionsContext.Provider value={gameActions}> <GameActionsContext.Provider value={{}}>
<div /> <div>Hello world!</div>
</GameActionsContext.Provider> </GameActionsContext.Provider>
) )
} }

View File

@ -15,17 +15,50 @@ type AsyncHandle<T> =
| { status: 'connecting' } | { status: 'connecting' }
| { status: 'connected'; handle: T} | { status: 'connected'; handle: T}
interface SocketAPI {} interface SessionCommandAPI {
query: () => void
join: () => void
leave: () => void
play: () => void
poll: () => void
}
interface GameCommandAPI {
act: () => void,
getState: () => void,
debug: () => void,
}
// TODO
type GameCommandEnum = 'a' | 's' | 'd'
interface SocketMessage {
playerId?: string
matchId?: string
command: keyof SessionCommandAPI
gameCommand?: {
playerId: string
type: GameCommandEnum,
cmd: unknown
}
}
type State = type State =
| { status: 'not-connected' } | { status: 'not-connected' }
| { status: 'connecting' } | { status: 'connecting' }
| { status: 'connected'} | { status: 'finding-game' }
| { status: 'in-game'; playerId: string; matchId: string }
type Action =
| { type: 'open' }
| { type: 'close' }
| { type: 'error' }
| { type: 'game-found'; matchId: string; playerId: string }
type Action = 'open' | 'close' | 'error'
function reducer(_state: State, action: Action): State { function reducer(_state: State, action: Action): State {
switch (action) { switch (action.type) {
case 'open': return { status: 'connected' } case 'open': return { status: 'finding-game' }
case 'game-found': return { status: 'in-game', matchId: action.matchId, playerId: action.playerId }
case 'close': return { status: 'not-connected' } case 'close': return { status: 'not-connected' }
case 'error': return { status: 'connecting' } case 'error': return { status: 'connecting' }
default: return assertNever(action) default: return assertNever(action)
@ -34,8 +67,8 @@ function reducer(_state: State, action: Action): State {
const initialState: State = {status: 'not-connected'} const initialState: State = {status: 'not-connected'}
export default function useSocket(): AsyncHandle<SocketAPI> { export default function useSocket(): AsyncHandle<GameCommandAPI> {
const _user = useUser() const profile = useUser()
const [state, dispatch] = React.useReducer(reducer, initialState) const [state, dispatch] = React.useReducer(reducer, initialState)
const onMessage = React.useCallback((message: WebSocketEventMap['message']) => { const onMessage = React.useCallback((message: WebSocketEventMap['message']) => {
@ -45,36 +78,53 @@ export default function useSocket(): AsyncHandle<SocketAPI> {
const onOpen = React.useCallback(() => { const onOpen = React.useCallback(() => {
console.log('socket opened') console.log('socket opened')
dispatch('open') dispatch({type: 'open'})
}, []) }, [])
const onError = React.useCallback((event: WebSocketEventMap['error']) => { const onError = React.useCallback((event: WebSocketEventMap['error']) => {
console.error(event) console.error(event)
dispatch('error') dispatch({type: 'error'})
}, []) }, [])
const onClose = React.useCallback((_event: WebSocketEventMap['close']) => { const onClose = React.useCallback((_event: WebSocketEventMap['close']) => {
console.log('socket closed') console.log('socket closed')
dispatch('close') dispatch({type: 'close'})
}, []) }, [])
const url = React.useMemo(() => const url = React.useMemo(
`ws://${window.location.hostname}${window.location.port ? `:${window.location.port}` : ''}/api/ws?name=${name}`, // () => `ws://arcade.saintnet.tech:7636/ws?name=${profile.displayName}`,
[], () => `ws://arcade.saintnet.tech:7636/ws`,
[profile],
) )
const socket = useWebSocket( const socket = useWebSocket(
url, url,
{onMessage, onOpen, onError, onClose, shouldReconnect}, {onMessage, onOpen, onError, onClose, shouldReconnect},
) )
const sendJson = React.useCallback((message: SocketMessage) => {
socket.send(JSON.stringify(message))
}, [socket])
const handle = React.useMemo(() => ({ const handle = React.useMemo(() => ({
sendJson: (value: {}) => socket.send(JSON.stringify(value)), // session commands
}), [socket]) query: () => {},
join: () => {},
leave: () => {},
play: () => {},
poll: () => {},
// game commands
act: () => {},
getState: () => {},
debug: () => {},
}), [sendJson])
switch (state.status) { switch (state.status) {
case 'connected': { case 'in-game': {
return {status: 'connected', handle} return {status: 'connected', handle}
} }
case 'finding-game': {
return {status: 'connecting'}
}
case 'connecting': case 'connecting':
case 'not-connected': case 'not-connected':
return state return state

View File

@ -1,13 +1,33 @@
import React from 'react' import React from 'react'
import assertNever from '~/common/assertNever.ts'
import IntroPage from '../components/IntroPage.tsx' import IntroPage from '../components/IntroPage.tsx'
import Page from '../components/Page.tsx'
import Game from '../game/Game.tsx'
type MenuState = 'menu' | 'play'
export default function AppPage() { export default function AppPage() {
return ( const [menuState, setMenuState] = React.useState<MenuState>('menu')
<IntroPage>
<div> switch (menuState) {
<p>Hello world!</p> case 'menu': {
</div> return (
</IntroPage> <IntroPage>
) <button onClick={() => setMenuState('play')}>
Play
</button>
</IntroPage>
)
}
case 'play': {
return (
<Page>
<Game />
</Page>
)
}
default: return assertNever(menuState)
}
} }

View File

@ -98,10 +98,10 @@ h2 {
.header { .header {
height: 90px; height: 90px;
flex: 0 0 auto; flex: 0 0 auto;
background-color: #4a6670; background-color: #3454d1;
padding-left: 2em; padding-left: 2em;
padding-right: 2em; padding-right: 2em;
box-shadow: 0 1em 1em rgba(0, 0, 0, 0.25); box-shadow: 2px 8px 4px 1px rgba(0, 0, 0, 0.25);
display: flex; display: flex;
align-items: center; align-items: center;
@ -109,9 +109,8 @@ h2 {
} }
.header-text { .header-text {
font-size: 32px;
font-family: sans-serif; font-family: sans-serif;
color: #18212b; color: #ddf6fd;
} }
.main { .main {
@ -119,7 +118,6 @@ h2 {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background-color: #e6eaef;
} }
.frame { .frame {

View File

@ -1,3 +1,3 @@
export interface UserProfile { export interface UserProfile {
displayName: string
} }

View File

@ -40,8 +40,8 @@ const html = `
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Deno React Web Starter</title> <title>Tomecraft</title>
<meta name="description" content="Template for SSR React apps with Deno, Oak, and Firebase." /> <meta name="description" content="Tactical CCG" />
<link href="styles.css" rel="stylesheet" type="text/css"> <link href="styles.css" rel="stylesheet" type="text/css">
<style> <style>
body { body {