Implement the rest of gameplay, no game status yet
This commit is contained in:
parent
7e0ba5431a
commit
23285b89b0
@ -8,19 +8,31 @@ const EMPTY_SPACE = '-'
|
|||||||
interface CardTokenProps {
|
interface CardTokenProps {
|
||||||
card: Card | null
|
card: Card | null
|
||||||
onSelect: () => void
|
onSelect: () => void
|
||||||
|
isSelected: boolean
|
||||||
|
disabled: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BoardSlot(props: CardTokenProps): JSX.Element {
|
export default function BoardSlot(props: CardTokenProps): JSX.Element {
|
||||||
const {onSelect, card} = props
|
const {onSelect, card, isSelected, disabled} = props
|
||||||
|
|
||||||
if (card == null) {
|
if (card === null || card.type === -1) {
|
||||||
return (
|
return (
|
||||||
<button>
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`board-slot${isSelected ? ' selected' : ''}`}
|
||||||
|
onClick={onSelect}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
<span>{EMPTY_SPACE}</span>
|
<span>{EMPTY_SPACE}</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<CardToken onSelect={onSelect} cardKey={card.id} />
|
<CardToken
|
||||||
|
onSelect={onSelect}
|
||||||
|
cardKey={card.type}
|
||||||
|
isSelected={isSelected}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -3,34 +3,44 @@ import React from 'react'
|
|||||||
import type { GameHandle, Selection } from './types.ts'
|
import type { GameHandle, Selection } from './types.ts'
|
||||||
import BoardSlot from './BoardSlot.tsx'
|
import BoardSlot from './BoardSlot.tsx'
|
||||||
import CardToken from './cards/Card.tsx'
|
import CardToken from './cards/Card.tsx'
|
||||||
import {isAttackSelection, isMoveSelection, isPlayCardSelection} from './selection.tsx'
|
import {
|
||||||
|
isAttackSelection,
|
||||||
|
isMoveSelection,
|
||||||
|
isPlayCardSelection,
|
||||||
|
isAlly,
|
||||||
|
isBoard,
|
||||||
|
isHand,
|
||||||
|
} from './selection.tsx'
|
||||||
|
|
||||||
type GameProps = GameHandle
|
type GameProps = GameHandle
|
||||||
|
|
||||||
export default function Game({
|
export default function Game(props: GameProps): JSX.Element {
|
||||||
team,
|
const {
|
||||||
board,
|
team,
|
||||||
player,
|
board,
|
||||||
deck,
|
player,
|
||||||
enemyLife,
|
deck,
|
||||||
enemyDeckSize,
|
enemyLife,
|
||||||
enemyHandSize,
|
enemyDeckSize,
|
||||||
currentTurn,
|
enemyHandSize,
|
||||||
canDraw,
|
currentTurn,
|
||||||
hasDrawn,
|
canDraw,
|
||||||
gameStatus,
|
hasDrawn,
|
||||||
startTurn,
|
gameStatus,
|
||||||
endTurn,
|
drawChoices,
|
||||||
startDraw,
|
startTurn,
|
||||||
commitDraw,
|
endTurn,
|
||||||
attackCard,
|
startDraw,
|
||||||
moveCard,
|
commitDraw,
|
||||||
playCard,
|
attackCard,
|
||||||
}: GameProps): JSX.Element {
|
moveCard,
|
||||||
|
playCard,
|
||||||
|
getView,
|
||||||
|
} = props
|
||||||
const [selection, setSelection] = React.useState<Selection | null>(null)
|
const [selection, setSelection] = React.useState<Selection | null>(null)
|
||||||
|
|
||||||
function selectCard(nextSelection: Selection): void {
|
function selectCard(nextSelection: Selection): void {
|
||||||
if (!selection) {
|
if (selection === null) {
|
||||||
setSelection(nextSelection)
|
setSelection(nextSelection)
|
||||||
} else {
|
} else {
|
||||||
if (isAttackSelection(selection, nextSelection)) {
|
if (isAttackSelection(selection, nextSelection)) {
|
||||||
@ -44,11 +54,27 @@ export default function Game({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectTurnButton(): void {
|
React.useEffect(() => {
|
||||||
}
|
if (currentTurn === team) {
|
||||||
|
startTurn()
|
||||||
|
}
|
||||||
|
}, [currentTurn, startTurn])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const intervalId = setInterval(
|
||||||
|
() => {
|
||||||
|
getView()
|
||||||
|
},
|
||||||
|
1000,
|
||||||
|
)
|
||||||
|
return () => {
|
||||||
|
clearInterval(intervalId)
|
||||||
|
}
|
||||||
|
}, [getView])
|
||||||
|
|
||||||
const enemyBoard = team === 1 ? board.scourge : board.sentinal
|
const enemyBoard = team === 1 ? board.scourge : board.sentinal
|
||||||
const allyBoard = team === 1 ? board.sentinal : board.scourge
|
const allyBoard = team === 1 ? board.sentinal : board.scourge
|
||||||
|
const isMyTurn = currentTurn === team
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="game-container">
|
<div className="game-container">
|
||||||
@ -58,19 +84,20 @@ export default function Game({
|
|||||||
<p>Life: {enemyLife}</p>
|
<p>Life: {enemyLife}</p>
|
||||||
<p>Deck: {enemyDeckSize}</p>
|
<p>Deck: {enemyDeckSize}</p>
|
||||||
</div>
|
</div>
|
||||||
<p>Turn: {currentTurn}</p>
|
<div>
|
||||||
{canDraw && <p>Drawing phase...</p>}
|
<p>{isMyTurn ? 'My' : 'Enemy'} Turn</p>
|
||||||
{hasDrawn && <p>Action phase...</p>}
|
{isMyTurn && canDraw && <button type="button" onClick={startDraw}>Start Draw</button>}
|
||||||
<button onClick={selectTurnButton}>End/Start Turn</button>
|
{isMyTurn && hasDrawn && <button type="button" onClick={endTurn}>End Turn</button>}
|
||||||
|
</div>
|
||||||
<div className="player-info">
|
<div className="player-info">
|
||||||
<p>Life: {player.life}</p>
|
<p>Life: {player.life}</p>
|
||||||
<p>Deck: {deck.length}</p>
|
<p>Deck: {deck.cards.length}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="game-board">
|
<div className="game-board">
|
||||||
<div className="hand">
|
<div className="hand">
|
||||||
{Array(enemyHandSize).fill(null).map(() => (
|
{Array(enemyHandSize).fill(null).map(() => (
|
||||||
<CardToken cardKey={null} />
|
<CardToken cardKey={null} isSelected={false} disabled />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="fighter-area enemy">
|
<div className="fighter-area enemy">
|
||||||
@ -78,6 +105,12 @@ export default function Game({
|
|||||||
<BoardSlot
|
<BoardSlot
|
||||||
card={card ?? null}
|
card={card ?? null}
|
||||||
onSelect={() => selectCard({target: 'opponent', type: 'board', index})}
|
onSelect={() => selectCard({target: 'opponent', type: 'board', index})}
|
||||||
|
isSelected={
|
||||||
|
selection
|
||||||
|
? !isAlly(selection) && isBoard(selection) && selection.index === index
|
||||||
|
: false
|
||||||
|
}
|
||||||
|
disabled={!(currentTurn === team && hasDrawn)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -86,14 +119,55 @@ export default function Game({
|
|||||||
<BoardSlot
|
<BoardSlot
|
||||||
card={card ?? null}
|
card={card ?? null}
|
||||||
onSelect={() => selectCard({target: 'ally', type: 'board', index})}
|
onSelect={() => selectCard({target: 'ally', type: 'board', index})}
|
||||||
|
isSelected={
|
||||||
|
selection
|
||||||
|
? isAlly(selection) && isBoard(selection) && selection.index === index
|
||||||
|
: false
|
||||||
|
}
|
||||||
|
disabled={!(currentTurn === team && hasDrawn)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{drawChoices.length > 0 && (
|
||||||
|
<React.Fragment>
|
||||||
|
<div className="draw-choices">
|
||||||
|
{drawChoices.map((card, index) => (
|
||||||
|
<CardToken
|
||||||
|
cardKey={card.type === -1 ? null : card.type}
|
||||||
|
onSelect={() => selectCard({target: 'ally', type: 'draws', index})}
|
||||||
|
isSelected={
|
||||||
|
selection
|
||||||
|
? selection.type === 'draws' && selection.index === index
|
||||||
|
: false
|
||||||
|
}
|
||||||
|
disabled={false}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={
|
||||||
|
() => selection && selection.type === 'draws' && commitDraw(selection.index)
|
||||||
|
}
|
||||||
|
disabled={selection?.type !== 'draws'}
|
||||||
|
>
|
||||||
|
Draw Card
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
<div className="hand">
|
<div className="hand">
|
||||||
{player.hand.map((card, index) => (
|
{player.hand.map((card, index) => (
|
||||||
<CardToken
|
<CardToken
|
||||||
cardKey={card.id}
|
cardKey={card.type === -1 ? null : card.type}
|
||||||
onSelect={() => selectCard({target: 'ally', type: 'hand', index})}
|
onSelect={() => selectCard({target: 'ally', type: 'hand', index})}
|
||||||
|
isSelected={
|
||||||
|
selection
|
||||||
|
? isAlly(selection) && isHand(selection) && selection.index === index
|
||||||
|
: false
|
||||||
|
}
|
||||||
|
disabled={!(isMyTurn && hasDrawn)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,6 +6,7 @@ import Loading from '../components/Loading.tsx'
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
AsyncHandle,
|
AsyncHandle,
|
||||||
|
Card,
|
||||||
FighterArea,
|
FighterArea,
|
||||||
GameState,
|
GameState,
|
||||||
GameAction,
|
GameAction,
|
||||||
@ -14,19 +15,69 @@ import {
|
|||||||
import useServerSocket from './useServerSocket.ts'
|
import useServerSocket from './useServerSocket.ts'
|
||||||
import Game from './Game.tsx'
|
import Game from './Game.tsx'
|
||||||
|
|
||||||
function reducer(state: GameState, action: GameAction): GameState {
|
interface GameClientState extends GameState {
|
||||||
|
isDrawing: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type GameClientAction = GameAction
|
||||||
|
|
||||||
|
function reducer(state: GameClientState, action: GameClientAction): GameClientState {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'update-state': {
|
case 'update-state': {
|
||||||
return {...action.state, team: state.team}
|
return {...state, ...action.state}
|
||||||
}
|
}
|
||||||
case 'set-player': {
|
case 'set-player-team': {
|
||||||
return {...state, team: action.team}
|
return {...state, team: action.team}
|
||||||
}
|
}
|
||||||
|
case 'play-card': {
|
||||||
|
const nextHand = [
|
||||||
|
...state.player.hand.slice(0, action.handIndex),
|
||||||
|
...state.player.hand.slice(action.handIndex + 1),
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
player: { ...state.player, hand: nextHand },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'receive-cards': {
|
||||||
|
// first, i can draw and i am not yet drawing
|
||||||
|
// scry N cards from the deck
|
||||||
|
if (state.canDraw && !state.isDrawing) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isDrawing: true,
|
||||||
|
drawChoices: action.cards,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// then, i am drawing, i can still draw, and I haven't yet drawn.
|
||||||
|
// i draw a card into my hand
|
||||||
|
if (state.canDraw && state.isDrawing && !state.hasDrawn) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
drawChoices: [],
|
||||||
|
player: {
|
||||||
|
...state.player,
|
||||||
|
hand: action.cards
|
||||||
|
},
|
||||||
|
isDrawing: false,
|
||||||
|
hasDrawn: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// then, i am no longer drawing and cannot draw. this is the board.
|
||||||
|
const team = state.team === 1 ? 'sentinal' : 'scourge' as const
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
board: {
|
||||||
|
...state.board,
|
||||||
|
[team]: action.cards,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
default: return state
|
default: return state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: GameState = {
|
const initialState: GameClientState = {
|
||||||
board: {
|
board: {
|
||||||
sentinal: Array(4).fill(undefined) as FighterArea,
|
sentinal: Array(4).fill(undefined) as FighterArea,
|
||||||
scourge: Array(4).fill(undefined) as FighterArea,
|
scourge: Array(4).fill(undefined) as FighterArea,
|
||||||
@ -38,7 +89,9 @@ const initialState: GameState = {
|
|||||||
life: 0,
|
life: 0,
|
||||||
ready: false,
|
ready: false,
|
||||||
},
|
},
|
||||||
deck: [],
|
deck: {
|
||||||
|
cards: []
|
||||||
|
},
|
||||||
team: 1,
|
team: 1,
|
||||||
enemyLife: 0,
|
enemyLife: 0,
|
||||||
enemyDeckSize: 0,
|
enemyDeckSize: 0,
|
||||||
@ -47,17 +100,20 @@ const initialState: GameState = {
|
|||||||
canDraw: false,
|
canDraw: false,
|
||||||
hasDrawn: false,
|
hasDrawn: false,
|
||||||
gameStatus: 0,
|
gameStatus: 0,
|
||||||
|
isDrawing: false,
|
||||||
|
drawChoices: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function GameClient(): JSX.Element {
|
export default function GameClient(): JSX.Element {
|
||||||
const [state, dispatch] = React.useReducer(reducer, initialState)
|
const [state, dispatch] = React.useReducer(reducer, initialState)
|
||||||
|
|
||||||
const handleGameUpdate = React.useCallback((data: Record<string, unknown>) => {
|
const handleGameUpdate = React.useCallback((action: GameAction) => {
|
||||||
console.log(data)
|
dispatch(action)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const socketHandle = useServerSocket(handleGameUpdate)
|
const socketHandle = useServerSocket(handleGameUpdate)
|
||||||
|
|
||||||
|
const {team} = state
|
||||||
const gameHandle = React.useMemo<AsyncHandle<GameCommandAPI>>(
|
const gameHandle = React.useMemo<AsyncHandle<GameCommandAPI>>(
|
||||||
() => {
|
() => {
|
||||||
if (socketHandle.status !== 'connected') return socketHandle
|
if (socketHandle.status !== 'connected') return socketHandle
|
||||||
@ -69,6 +125,7 @@ export default function GameClient(): JSX.Element {
|
|||||||
socketHandle.handle.sendGameCommand({
|
socketHandle.handle.sendGameCommand({
|
||||||
type: 's',
|
type: 's',
|
||||||
cmd: 'b',
|
cmd: 'b',
|
||||||
|
player_id: team,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
startTurn: () => {
|
startTurn: () => {
|
||||||
@ -76,6 +133,7 @@ export default function GameClient(): JSX.Element {
|
|||||||
socketHandle.handle.sendGameCommand({
|
socketHandle.handle.sendGameCommand({
|
||||||
type: 's',
|
type: 's',
|
||||||
cmd: 's',
|
cmd: 's',
|
||||||
|
player_id: team,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
endTurn: () => {
|
endTurn: () => {
|
||||||
@ -83,6 +141,7 @@ export default function GameClient(): JSX.Element {
|
|||||||
socketHandle.handle.sendGameCommand({
|
socketHandle.handle.sendGameCommand({
|
||||||
type: 's',
|
type: 's',
|
||||||
cmd: 'e',
|
cmd: 'e',
|
||||||
|
player_id: team,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
getView: () => {
|
getView: () => {
|
||||||
@ -90,6 +149,7 @@ export default function GameClient(): JSX.Element {
|
|||||||
socketHandle.handle.sendGameCommand({
|
socketHandle.handle.sendGameCommand({
|
||||||
type: 's',
|
type: 's',
|
||||||
cmd: 'g',
|
cmd: 'g',
|
||||||
|
player_id: team,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
startDraw: () => {
|
startDraw: () => {
|
||||||
@ -97,20 +157,25 @@ export default function GameClient(): JSX.Element {
|
|||||||
socketHandle.handle.sendGameCommand({
|
socketHandle.handle.sendGameCommand({
|
||||||
type: 'a',
|
type: 'a',
|
||||||
cmd: 's',
|
cmd: 's',
|
||||||
|
player_id: team,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
commitDraw: (cardIndex: number) => {
|
commitDraw: (cardIndex: number) => {
|
||||||
if (socketHandle.status !== 'connected') return
|
if (socketHandle.status !== 'connected') return
|
||||||
|
// dispatch({type})
|
||||||
socketHandle.handle.sendGameCommand({
|
socketHandle.handle.sendGameCommand({
|
||||||
type: 'a',
|
type: 'a',
|
||||||
cmd: `d ${cardIndex}`,
|
cmd: `d ${cardIndex}`,
|
||||||
|
player_id: team,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
playCard: (handIndex: number, positionIndex: number) => {
|
playCard: (handIndex: number, positionIndex: number) => {
|
||||||
if (socketHandle.status !== 'connected') return
|
if (socketHandle.status !== 'connected') return
|
||||||
|
dispatch({ type: 'play-card', handIndex })
|
||||||
socketHandle.handle.sendGameCommand({
|
socketHandle.handle.sendGameCommand({
|
||||||
type: 'a',
|
type: 'a',
|
||||||
cmd: `p ${handIndex} ${positionIndex}`,
|
cmd: `p ${handIndex} ${positionIndex}`,
|
||||||
|
player_id: team,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
moveCard: (positionFrom: number, positionTo: number) => {
|
moveCard: (positionFrom: number, positionTo: number) => {
|
||||||
@ -118,6 +183,7 @@ export default function GameClient(): JSX.Element {
|
|||||||
socketHandle.handle.sendGameCommand({
|
socketHandle.handle.sendGameCommand({
|
||||||
type: 'a',
|
type: 'a',
|
||||||
cmd: `m ${positionFrom} ${positionTo}`,
|
cmd: `m ${positionFrom} ${positionTo}`,
|
||||||
|
player_id: team,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
attackCard: (positionFrom: number, positionTo: number) => {
|
attackCard: (positionFrom: number, positionTo: number) => {
|
||||||
@ -125,23 +191,22 @@ export default function GameClient(): JSX.Element {
|
|||||||
socketHandle.handle.sendGameCommand({
|
socketHandle.handle.sendGameCommand({
|
||||||
type: 'a',
|
type: 'a',
|
||||||
cmd: `a ${positionFrom} ${positionTo}`,
|
cmd: `a ${positionFrom} ${positionTo}`,
|
||||||
|
player_id: team,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[socketHandle, state],
|
[socketHandle, team],
|
||||||
)
|
)
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (gameHandle.status !== 'connected') return
|
if (gameHandle.status !== 'connected') return
|
||||||
gameHandle.handle.readyPlayer()
|
gameHandle.handle.readyPlayer()
|
||||||
if (state?.team === 1) gameHandle.handle.startTurn()
|
}, [gameHandle, team])
|
||||||
}, [gameHandle, state?.team])
|
|
||||||
|
|
||||||
switch (gameHandle.status) {
|
switch (gameHandle.status) {
|
||||||
case 'connected': {
|
case 'connected': {
|
||||||
if (!state) return <Loading />
|
|
||||||
return <Game {...state} {...gameHandle.handle} />
|
return <Game {...state} {...gameHandle.handle} />
|
||||||
}
|
}
|
||||||
case 'not-connected':
|
case 'not-connected':
|
||||||
|
@ -9,21 +9,30 @@ const EMPTY_SPACE = '-'
|
|||||||
interface CardTokenProps {
|
interface CardTokenProps {
|
||||||
cardKey: CardKey | null
|
cardKey: CardKey | null
|
||||||
onSelect?: () => void
|
onSelect?: () => void
|
||||||
|
isSelected: boolean
|
||||||
|
disabled: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CardToken(props: CardTokenProps): JSX.Element {
|
export default function CardToken(props: CardTokenProps): JSX.Element {
|
||||||
const {onSelect, cardKey} = props
|
const {onSelect, cardKey, isSelected, disabled} = props
|
||||||
|
|
||||||
if (cardKey == null) {
|
if (cardKey == null) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={`card-button card-back${isSelected ? ' selected' : ''}`}>
|
||||||
<span>{EMPTY_SPACE}</span>
|
<div>
|
||||||
|
<span>{EMPTY_SPACE}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<button onClick={onSelect}>
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`card-button${isSelected ? ' selected' : ''}`}
|
||||||
|
onClick={onSelect}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
|
className="card-image"
|
||||||
src={getCardSrc(cardKey)}
|
src={getCardSrc(cardKey)}
|
||||||
alt={getCardAlt(cardKey)}
|
alt={getCardAlt(cardKey)}
|
||||||
/>
|
/>
|
||||||
|
@ -2,8 +2,8 @@ import type {CardKey} from '../types.ts'
|
|||||||
|
|
||||||
const cardPaths: Record<CardKey, string> = {
|
const cardPaths: Record<CardKey, string> = {
|
||||||
0: 'joker.png',
|
0: 'joker.png',
|
||||||
1: 'sp_2.png',
|
1: 'sp_14.png',
|
||||||
2: 'sp_14.png',
|
2: 'sp_2.png',
|
||||||
3: 'sp_3.png',
|
3: 'sp_3.png',
|
||||||
4: 'sp_4.png',
|
4: 'sp_4.png',
|
||||||
5: 'sp_5.png',
|
5: 'sp_5.png',
|
||||||
|
@ -1,17 +1,21 @@
|
|||||||
import type {Selection} from './types.ts'
|
import type {Selection} from './types.ts'
|
||||||
|
|
||||||
function isAlly(selection: Selection): boolean {
|
export function isAlly(selection: Selection): boolean {
|
||||||
return selection.target === 'ally'
|
return selection.target === 'ally'
|
||||||
}
|
}
|
||||||
|
|
||||||
function isHand(selection: Selection): boolean {
|
export function isHand(selection: Selection): boolean {
|
||||||
return selection.type === 'hand'
|
return selection.type === 'hand'
|
||||||
}
|
}
|
||||||
|
|
||||||
function isBoard(selection: Selection): boolean {
|
export function isBoard(selection: Selection): boolean {
|
||||||
return selection.type === 'board'
|
return selection.type === 'board'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isDrawSelection(selection: Selection): boolean {
|
||||||
|
return selection.type === 'draws'
|
||||||
|
}
|
||||||
|
|
||||||
export function isPlayCardSelection(first: Selection, second: Selection): boolean {
|
export function isPlayCardSelection(first: Selection, second: Selection): boolean {
|
||||||
const isMyHand = isAlly(first) && isHand(first)
|
const isMyHand = isAlly(first) && isHand(first)
|
||||||
const targetsMyBoard = isAlly(second) && isBoard(second)
|
const targetsMyBoard = isAlly(second) && isBoard(second)
|
||||||
|
@ -11,10 +11,10 @@ export type AsyncHandle<T> =
|
|||||||
export type CardKey = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8
|
export type CardKey = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8
|
||||||
|
|
||||||
export interface Card {
|
export interface Card {
|
||||||
type: number
|
type: CardKey | -1
|
||||||
basePower: number
|
basePower: number
|
||||||
power: number
|
power: number
|
||||||
id: CardKey
|
id: string
|
||||||
counters: number
|
counters: number
|
||||||
owner: number
|
owner: number
|
||||||
position: number
|
position: number
|
||||||
@ -36,6 +36,7 @@ export interface Player {
|
|||||||
life: number
|
life: number
|
||||||
ready: boolean
|
ready: boolean
|
||||||
}
|
}
|
||||||
|
export type Team = 1 | 2
|
||||||
|
|
||||||
interface TeamEnumMap {
|
interface TeamEnumMap {
|
||||||
1: 'sentinal'
|
1: 'sentinal'
|
||||||
@ -55,8 +56,10 @@ interface GameStatusMap {
|
|||||||
export interface GameState {
|
export interface GameState {
|
||||||
board: Board
|
board: Board
|
||||||
player: Player
|
player: Player
|
||||||
deck: Card[]
|
deck: {
|
||||||
team: 1 | 2
|
cards: Card[]
|
||||||
|
}
|
||||||
|
team: Team
|
||||||
enemyLife: number
|
enemyLife: number
|
||||||
enemyDeckSize: number
|
enemyDeckSize: number
|
||||||
enemyHandSize: number
|
enemyHandSize: number
|
||||||
@ -64,11 +67,14 @@ export interface GameState {
|
|||||||
canDraw: boolean
|
canDraw: boolean
|
||||||
hasDrawn: boolean
|
hasDrawn: boolean
|
||||||
gameStatus: keyof GameStatusMap
|
gameStatus: keyof GameStatusMap
|
||||||
|
drawChoices: Card[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GameAction =
|
export type GameAction =
|
||||||
| { type: 'set-player'; team: 1 | 2}
|
| { type: 'set-player-team'; team: Team}
|
||||||
| { type: 'update-state'; state: Omit<GameState, 'team'>}
|
| { type: 'receive-cards'; cards: Card[]}
|
||||||
|
| { type: 'update-state'; state: Omit<GameState, 'team'> }
|
||||||
|
| { type: 'play-card', handIndex: number }
|
||||||
|
|
||||||
export interface GameCommandAPI {
|
export interface GameCommandAPI {
|
||||||
readyPlayer: () => void
|
readyPlayer: () => void
|
||||||
@ -106,13 +112,13 @@ export interface CommandVariantParamMap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface GameCommand {
|
export interface GameCommand {
|
||||||
playerId: string
|
player_id: Team
|
||||||
type: GameCommandEnum,
|
type: GameCommandEnum
|
||||||
cmd: string // "<Variant> <VariantParam1> <VariantParam2> ..."
|
cmd: string // "<Variant> <VariantParam1> <VariantParam2> ..."
|
||||||
}
|
}
|
||||||
|
|
||||||
type SelectionTarget = 'ally' | 'opponent'
|
type SelectionTarget = 'ally' | 'opponent'
|
||||||
type SelectionType = 'hand' | 'board'
|
type SelectionType = 'hand' | 'board' | 'draws'
|
||||||
|
|
||||||
export interface Selection {
|
export interface Selection {
|
||||||
target: SelectionTarget
|
target: SelectionTarget
|
||||||
|
@ -75,7 +75,7 @@ function reducer(state: GameSocketSessionState, action: Action): GameSocketSessi
|
|||||||
const initialState: GameSocketSessionState = {status: 'connecting'}
|
const initialState: GameSocketSessionState = {status: 'connecting'}
|
||||||
|
|
||||||
interface SocketHandle {
|
interface SocketHandle {
|
||||||
sendGameCommand: (command: Omit<GameCommand, 'playerId'>) => void
|
sendGameCommand: (command: GameCommand) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function useServerSocket(
|
export default function useServerSocket(
|
||||||
@ -98,18 +98,49 @@ export default function useServerSocket(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
case 'joined p1': {
|
case 'joined p1': {
|
||||||
onUpdate({ type: 'set-player', team: 1 })
|
onUpdate({ type: 'set-player-team', team: 1 })
|
||||||
dispatch ({type: 'join-game'})
|
dispatch ({ type: 'join-game' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case 'joined p2': {
|
case 'joined p2': {
|
||||||
onUpdate({ type: 'set-player', team: 2 })
|
onUpdate({ type: 'set-player-team', team: 2 })
|
||||||
dispatch ({type: 'join-game' })
|
dispatch ({ type: 'join-game' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
default: break;
|
case 'played': {
|
||||||
|
switch (data.game_result.result_type) {
|
||||||
|
case 'a': {
|
||||||
|
const result = data.game_result.action_result
|
||||||
|
if (result) onUpdate({
|
||||||
|
type: 'receive-cards',
|
||||||
|
cards: result.cards,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case 's': {
|
||||||
|
const result = data.game_result.state_result
|
||||||
|
if (result) onUpdate({
|
||||||
|
type: 'update-state',
|
||||||
|
state: {
|
||||||
|
board: result.board,
|
||||||
|
player: result.player,
|
||||||
|
deck: result.deck,
|
||||||
|
enemyLife: result.enemy_life,
|
||||||
|
enemyDeckSize: result.enemy_deck_size,
|
||||||
|
enemyHandSize: result.enemy_hand_size,
|
||||||
|
currentTurn: result.current_turn,
|
||||||
|
canDraw: result.can_draw,
|
||||||
|
hasDrawn: result.has_drawn,
|
||||||
|
gameStatus: result.game_status,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
default: return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default: return;
|
||||||
}
|
}
|
||||||
onUpdate(data)
|
|
||||||
}, [onUpdate])
|
}, [onUpdate])
|
||||||
|
|
||||||
const onOpen = React.useCallback(() => {
|
const onOpen = React.useCallback(() => {
|
||||||
@ -141,16 +172,13 @@ export default function useServerSocket(
|
|||||||
sendJsonMessage(message)
|
sendJsonMessage(message)
|
||||||
}, [sendJsonMessage])
|
}, [sendJsonMessage])
|
||||||
|
|
||||||
const sendGameCommand = React.useCallback((gameCommand: Omit<GameCommand, 'playerId'>) => {
|
const sendGameCommand = React.useCallback((gameCommand: GameCommand) => {
|
||||||
if (state.status !== 'in-game') return
|
if (state.status !== 'in-game') return
|
||||||
sendJson({
|
sendJson({
|
||||||
player_id: MY_ID,
|
player_id: MY_ID,
|
||||||
match_id: state.matchId,
|
match_id: state.matchId,
|
||||||
command: 'play',
|
command: 'play',
|
||||||
game_command: {
|
game_command: gameCommand,
|
||||||
...gameCommand,
|
|
||||||
playerId: state.playerId,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}, [state, sendJson])
|
}, [state, sendJson])
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
* {
|
* {
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
@ -151,6 +152,7 @@ h2 {
|
|||||||
.game-sidebar {
|
.game-sidebar {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
width: 10em;
|
width: 10em;
|
||||||
|
padding: 1em;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -162,7 +164,6 @@ h2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.player-info {
|
.player-info {
|
||||||
margin: 1em;
|
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
background-color: #c4c4c4;
|
background-color: #c4c4c4;
|
||||||
color: #000000;
|
color: #000000;
|
||||||
@ -187,3 +188,40 @@ h2 {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.board-slot {
|
||||||
|
width: 5em;
|
||||||
|
height: 7em;
|
||||||
|
margin: 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-button {
|
||||||
|
width: 10em;
|
||||||
|
height: 14em;
|
||||||
|
margin: 0 1em;
|
||||||
|
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-button:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-back {
|
||||||
|
background: grey;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
box-shadow: 0px 0px 8px 4px rgba(245, 194, 10, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
import { Router } from 'oak'
|
|
||||||
|
|
||||||
function handleSocket(socket: WebSocket) {
|
|
||||||
console.log(socket)
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiRouter = new Router()
|
|
||||||
|
|
||||||
apiRouter
|
|
||||||
.get('/api/ws', async (context) => {
|
|
||||||
if (!context.isUpgradable) throw new Error('Context not upgradable.')
|
|
||||||
const ws = await context.upgrade()
|
|
||||||
handleSocket(ws)
|
|
||||||
})
|
|
||||||
.get('/api/hello', (context) => {
|
|
||||||
context.response.body = "Hello world!";
|
|
||||||
})
|
|
||||||
|
|
||||||
export default apiRouter
|
|
@ -1,9 +1,5 @@
|
|||||||
import { Application, Status } from 'oak'
|
import { Application, Status } from 'oak'
|
||||||
|
|
||||||
//// import any necessary wasm file here
|
|
||||||
// import init from '~/common/wasm.js'
|
|
||||||
|
|
||||||
import apiRouter from './routes/api.ts'
|
|
||||||
import staticRouter from './routes/static.tsx'
|
import staticRouter from './routes/static.tsx'
|
||||||
|
|
||||||
const app = new Application()
|
const app = new Application()
|
||||||
@ -28,9 +24,6 @@ app.use((ctx, next) => {
|
|||||||
return next()
|
return next()
|
||||||
})
|
})
|
||||||
|
|
||||||
app.use(apiRouter.routes())
|
|
||||||
app.use(apiRouter.allowedMethods())
|
|
||||||
|
|
||||||
app.use(staticRouter.routes())
|
app.use(staticRouter.routes())
|
||||||
app.use(staticRouter.allowedMethods())
|
app.use(staticRouter.allowedMethods())
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user