1165 lines
33 KiB
JavaScript
Executable File
1165 lines
33 KiB
JavaScript
Executable File
import React, { useState, useEffect } from "react";
|
|
import "./Table.css";
|
|
import history from "./history.js";
|
|
import Paper from '@material-ui/core/Paper';
|
|
import Button from '@material-ui/core/Button';
|
|
import TextField from '@material-ui/core/TextField';
|
|
import List from '@material-ui/core/List';
|
|
import ListItem from '@material-ui/core/ListItem';
|
|
import ListItemText from '@material-ui/core/ListItemText';
|
|
import Moment from 'react-moment';
|
|
import Board from './Board.js';
|
|
import Trade from './Trade.js';
|
|
import { assetsPath, base, getPlayerName, gamesPath } from './Common.js';
|
|
import PlayerColor from './PlayerColor.js';
|
|
import Dice from './Dice.js';
|
|
import Resource from './Resource.js';
|
|
import ViewCard from './ViewCard.js';
|
|
import Winner from './Winner.js';
|
|
import ChooseCard from './ChooseCard.js';
|
|
import { CircularProgress } from "@material-ui/core";
|
|
import 'moment-timezone';
|
|
|
|
/* Start of withRouter polyfill */
|
|
// https://reactrouter.com/docs/en/v6/faq#what-happened-to-withrouter-i-need-it
|
|
import {
|
|
useLocation,
|
|
useNavigate,
|
|
useParams
|
|
} from "react-router-dom";
|
|
|
|
function withRouter(Component) {
|
|
function ComponentWithRouterProp(props) {
|
|
let location = useLocation();
|
|
let navigate = useNavigate();
|
|
let params = useParams();
|
|
return (
|
|
<Component
|
|
{...props}
|
|
router={{ location, navigate, params }}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return ComponentWithRouterProp;
|
|
}
|
|
/* end of withRouter polyfill */
|
|
|
|
const Placard = ({table, type, active}) => {
|
|
const dismissClicked = (event) => {
|
|
table.setState({ buildActive: false });
|
|
}
|
|
|
|
const buildClicked = (event) => {
|
|
if (!type.match(/^l.*/)) {
|
|
if (!table.state.buildActive) {
|
|
table.setState({ buildActive: true });
|
|
}
|
|
}
|
|
};
|
|
|
|
const roadClicked = (event) => {
|
|
table.buyRoad();
|
|
table.setState({ buildActive: false });
|
|
};
|
|
|
|
const settlementClicked = (event) => {
|
|
table.buySettlement();
|
|
table.setState({ buildActive: false });
|
|
};
|
|
|
|
const cityClicked = (event) => {
|
|
table.buyCity();
|
|
table.setState({ buildActive: false });
|
|
};
|
|
|
|
const developmentClicked = (event) => {
|
|
table.buyDevelopment();
|
|
table.setState({ buildActive: false });
|
|
};
|
|
|
|
let buttons;
|
|
switch (active ? type : undefined) {
|
|
case 'orange':
|
|
case 'red':
|
|
case 'white':
|
|
case 'blue':
|
|
buttons = <>
|
|
<div onClick={dismissClicked}/>
|
|
<div onClick={roadClicked}/>
|
|
<div onClick={settlementClicked}/>
|
|
<div onClick={cityClicked}/>
|
|
<div onClick={developmentClicked}/>
|
|
<div onClick={dismissClicked}/>
|
|
</>;
|
|
break;
|
|
default:
|
|
buttons = <></>;
|
|
break;
|
|
}
|
|
|
|
return (
|
|
<div className={`Placard${active ? ' Selected' : ''}`}
|
|
onClick={buildClicked}
|
|
data-type={type}
|
|
style={{
|
|
backgroundImage:`url(${assetsPath}/gfx/placard-${type}.png)`
|
|
}}
|
|
>{buttons}</div>
|
|
);
|
|
};
|
|
|
|
const Development = ({table, type, card, onClick}) => {
|
|
return (
|
|
<div className={`Development ${card.played ? 'Selected' : ''}`}
|
|
onClick={onClick}
|
|
style={{
|
|
backgroundImage:`url(${assetsPath}/gfx/card-${type}.png)`
|
|
}}/>
|
|
);
|
|
};
|
|
|
|
const Chat = ({ table }) => {
|
|
const [lastTop, setLastTop] = useState(0),
|
|
[autoScroll, setAutoscroll] = useState(true),
|
|
[latest, setLatest] = useState(''),
|
|
[scrollTime, setScrollTime] = useState(0);
|
|
|
|
const chatInput = (event) => {
|
|
};
|
|
|
|
const chatKeyPress = (event) => {
|
|
if (event.key === "Enter") {
|
|
if (!autoScroll) {
|
|
setAutoscroll(true);
|
|
}
|
|
|
|
table.sendChat(event.target.value);
|
|
|
|
event.target.value = "";
|
|
}
|
|
};
|
|
|
|
const chatScroll = (event) => {
|
|
const chatList = event.target,
|
|
fromBottom = Math.round(Math.abs((chatList.scrollHeight - chatList.offsetHeight) - chatList.scrollTop));
|
|
|
|
/* If scroll is within 20 pixels of the bottom, turn on auto-scroll */
|
|
const shouldAutoscroll = (fromBottom < 20);
|
|
|
|
if (shouldAutoscroll !== autoScroll) {
|
|
setAutoscroll(shouldAutoscroll);
|
|
}
|
|
|
|
/* If the list should not auto scroll, then cache the current
|
|
* top of the list and record when we did this so we honor
|
|
* the auto-scroll for at least 500ms */
|
|
if (!shouldAutoscroll) {
|
|
const target = Math.round(chatList.scrollTop);
|
|
if (target !== lastTop) {
|
|
setLastTop(target);
|
|
setScrollTime(Date.now());
|
|
}
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
const chatList = document.getElementById("ChatList"),
|
|
currentTop = Math.round(chatList.scrollTop);
|
|
|
|
if (autoScroll) {
|
|
/* Auto-scroll to the bottom of the chat window */
|
|
const target = Math.round(chatList.scrollHeight - chatList.offsetHeight);
|
|
if (currentTop !== target) {
|
|
chatList.scrollTop = target;
|
|
}
|
|
return;
|
|
}
|
|
|
|
/* Maintain current position in scrolled view if the user hasn't
|
|
* been scrolling in the past 0.5s */
|
|
if ((Date.now() - scrollTime) > 500 && currentTop !== lastTop) {
|
|
chatList.scrollTop = lastTop;
|
|
}
|
|
});
|
|
|
|
//const timeDelta = game.timestamp - Date.now();
|
|
if (!table.game) {
|
|
console.log("Why no game?");
|
|
}
|
|
|
|
const messages = table.game && table.game.chat.map((item, index) => {
|
|
let message;
|
|
/* If the date is in the future, set it to now */
|
|
const dice = item.message.match(/^(.*rolled )([1-6])(, ([1-6]))?(.*)$/);
|
|
if (dice) {
|
|
if (dice[4]) {
|
|
message = <>{dice[1]}<Dice pips={dice[2]}/>, <Dice pips={dice[4]}/>{dice[5]}</>;
|
|
} else {
|
|
message = <>{dice[1]}<Dice pips={dice[2]}/>{dice[5]}</>;
|
|
}
|
|
} else {
|
|
message = item.message;
|
|
}
|
|
return (
|
|
<ListItem key={`msg-${item.date}`} className={item.color ? '' : 'System'}>
|
|
{ item.color &&
|
|
<PlayerColor color={item.color}/>
|
|
}
|
|
<ListItemText primary={message}
|
|
secondary={item.color && <Moment fromNow date={item.date > Date.now() ?
|
|
Date.now() : item.date} interval={1000}/>} />
|
|
</ListItem>
|
|
);
|
|
});
|
|
|
|
if (table.game && table.game.chat &&
|
|
table.game.chat[table.game.chat.length - 1].date !== latest) {
|
|
setLatest(table.game.chat[table.game.chat.length - 1].date);
|
|
setAutoscroll(true);
|
|
}
|
|
|
|
const name = table.game ? table.game.name : "Why no game?";
|
|
const elapsed = table.game ? (table.game.timestamp - table.game.startTime) : undefined;
|
|
return (
|
|
<Paper className="Chat">
|
|
<List className="ChatList" id="ChatList" onScroll={chatScroll}>
|
|
{ messages }
|
|
</List>
|
|
<TextField className="ChatInput"
|
|
disabled={!name}
|
|
onChange={chatInput}
|
|
onKeyPress={chatKeyPress}
|
|
label={elapsed && <Moment tz={"Etc/GMT"} format="h:mm:ss" durationFromNow interval={1000} date={table.game.startTime}></Moment>} variant="outlined"/>
|
|
</Paper>
|
|
);
|
|
}
|
|
|
|
const StartButton = ({ table }) => {
|
|
const startClick = (event) => {
|
|
table.setGameState("game-order").then((state) => {
|
|
table.game.state = state;
|
|
});
|
|
};
|
|
|
|
return (
|
|
<Button disabled={!table.game.color || table.game.active < 2} onClick={startClick}>Start game</Button>
|
|
);
|
|
};
|
|
|
|
const WaitingForPlayer = ({table}) => {
|
|
return (
|
|
<div className="WaitingForPlayer">
|
|
{ table.game && table.game.turn && <Paper>
|
|
<div className="Title">Waiting for {table.game.turn.name} to complete their turn.</div>
|
|
</Paper> }
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const GameOrder = ({table}) => {
|
|
|
|
const rollClick = (event) => {
|
|
table.throwDice();
|
|
}
|
|
|
|
if (!table.game) {
|
|
return (<></>);
|
|
}
|
|
|
|
let players = [], hasRolled = true;
|
|
for (let color in table.game.players) {
|
|
const item = table.game.players[color],
|
|
name = getPlayerName(table.game.sessions, color);
|
|
if (color === table.game.color) {
|
|
hasRolled = item.orderRoll !== 0;
|
|
}
|
|
if (name) {
|
|
if (!item.orderRoll) {
|
|
item.orderRoll = 0;
|
|
}
|
|
players.push({ name: name, color: color, ...item });
|
|
}
|
|
}
|
|
|
|
players.sort((A, B) => {
|
|
if (A.order === B.order) {
|
|
if (A.orderRoll === B.orderRoll) {
|
|
return A.name.localeCompare(B.name);
|
|
}
|
|
return B.orderRoll - A.orderRoll;
|
|
}
|
|
return B.order - A.order;
|
|
});
|
|
|
|
players = players.map(item =>
|
|
<div className="GameOrderPlayer" key={`player-${item.color}`}>
|
|
<PlayerColor color={item.color}/>
|
|
<div>{item.name}</div>
|
|
{ item.orderRoll !== 0 && <>rolled <Dice pips={item.orderRoll}/>. {item.orderStatus}</> }
|
|
{ item.orderRoll === 0 && <>has not rolled yet. {item.orderStatus}</>}
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className="GameOrder">
|
|
{ table.game && <Paper>
|
|
<div className="Title">Game Order</div>
|
|
<div className="PlayerList">
|
|
{ players }
|
|
</div>
|
|
<Button disabled={hasRolled} onClick={rollClick}>Roll Dice</Button>
|
|
</Paper> }
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const SelectPlayer = ({table, players}) => {
|
|
const playerClick = (event) => {
|
|
table.stealResource(event.currentTarget.getAttribute('data-color'));
|
|
}
|
|
|
|
if (!table.game) {
|
|
return (<></>);
|
|
}
|
|
|
|
let list = players.map(color => {
|
|
let item = {
|
|
color: color,
|
|
name: getPlayerName(table.game.sessions, color)
|
|
};
|
|
return <div className="SelectPlayerItem"
|
|
onClick={playerClick}
|
|
data-color={color}
|
|
key={`player-${item.color}`}>
|
|
<PlayerColor color={item.color}/>
|
|
<div>{item.name}</div>
|
|
</div>;
|
|
});
|
|
|
|
return (
|
|
<div className="SelectPlayer">
|
|
{ table.game && <Paper>
|
|
<div className="Title">Select Player to Steal From</div>
|
|
<div className="SelectPlayerList">
|
|
{ list }
|
|
</div>
|
|
</Paper> }
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const Action = ({ table }) => {
|
|
const buildClicked = (event) => {
|
|
table.buildClicked(event);
|
|
};
|
|
|
|
const discardClick = (event) => {
|
|
const nodes = document.querySelectorAll('.Hand .Resource.Selected'),
|
|
discarding = { wheat: 0, brick: 0, sheep: 0, stone: 0, wood: 0 };
|
|
for (let i = 0; i < nodes.length; i++) {
|
|
discarding[nodes[i].getAttribute("data-type")]++;
|
|
nodes[i].classList.remove('Selected');
|
|
}
|
|
return table.discard(discarding);
|
|
}
|
|
|
|
const newTableClick = (event) => {
|
|
return table.shuffleTable();
|
|
};
|
|
|
|
const tradeClick = (event) => {
|
|
table.startTrading();
|
|
}
|
|
|
|
const rollClick = (event) => {
|
|
table.throwDice();
|
|
}
|
|
|
|
const passClick = (event) => {
|
|
return table.passTurn();
|
|
}
|
|
|
|
const quitClick = (event) => {
|
|
table.setSelected("");
|
|
}
|
|
|
|
if (!table.game) {
|
|
console.log("Why no game?");
|
|
return (<Paper className="Action"/>);
|
|
}
|
|
|
|
const game = table.game,
|
|
inLobby = game.state === 'lobby',
|
|
player = game ? game.player : undefined,
|
|
hasRolled = (game && game.turn && game.turn.roll) ? true : false,
|
|
isTurn = (game && game.turn && game.turn.color === game.color) ? true : false,
|
|
robberActions = (game && game.turn && game.turn.robberInAction),
|
|
haveResources = player ? player.haveResources : false;
|
|
|
|
return (
|
|
<Paper className="Action">
|
|
{ inLobby && <>
|
|
<StartButton table={table}/>
|
|
<Button disabled={game.color ? false : true} onClick={newTableClick}>New table</Button>
|
|
<Button disabled={game.color ? true : false} onClick={() => {table.setState({ pickName: true})}}>Change name</Button> </> }
|
|
{ !inLobby && <>
|
|
<Button disabled={robberActions || !isTurn || hasRolled} onClick={rollClick}>Roll Dice</Button>
|
|
<Button disabled={robberActions || !isTurn || !hasRolled || !haveResources} onClick={tradeClick}>Trade</Button>
|
|
<Button disabled={robberActions || !isTurn || !hasRolled || !haveResources} onClick={buildClicked}>Build</Button>
|
|
{ game.turn.roll === 7 && player && player.mustDiscard > 0 &&
|
|
<Button onClick={discardClick}>Discard</Button>
|
|
}
|
|
<Button disabled={robberActions || !isTurn || !hasRolled} onClick={passClick}>Done</Button>
|
|
</> }
|
|
{ /* inLobby &&
|
|
<Button onClick={quitClick}>Quit</Button>
|
|
*/ }
|
|
</Paper>
|
|
);
|
|
}
|
|
|
|
const PlayerName = ({table}) => {
|
|
const [name, setName] = useState((table && table.game && table.game.name) ? table.game.name : "");
|
|
|
|
const nameChange = (event) => {
|
|
setName(event.target.value);
|
|
}
|
|
|
|
const sendName = () => {
|
|
if (name !== table.game.name) {
|
|
table.setPlayerName(name);
|
|
} else {
|
|
table.setError("");
|
|
table.setState({ pickName: false });
|
|
}
|
|
}
|
|
|
|
const nameKeyPress = (event) => {
|
|
if (event.key === "Enter") {
|
|
sendName();
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Paper className="PlayerName">
|
|
<TextField className="nameInput"
|
|
onChange={nameChange}
|
|
onKeyPress={nameKeyPress}
|
|
label="Enter your name"
|
|
variant="outlined"
|
|
value={name}
|
|
/>
|
|
<Button onClick={() => sendName()}>Set</Button>
|
|
</Paper>
|
|
);
|
|
};
|
|
|
|
/* This needs to take in a mechanism to declare the
|
|
* player's active item in the game */
|
|
const Players = ({ table }) => {
|
|
const toggleSelected = (key) => {
|
|
table.setSelected(table.game.color === key ? "" : key);
|
|
}
|
|
|
|
const players = [];
|
|
|
|
if (!table.game) {
|
|
return (<></>);
|
|
}
|
|
for (let color in table.game.players) {
|
|
const item = table.game.players[color], inLobby = table.game.state === 'lobby';
|
|
if (!inLobby && item.status === 'Not active') {
|
|
continue;
|
|
}
|
|
const name = getPlayerName(table.game.sessions, color),
|
|
selectable = table.game.state === 'lobby' && (item.status === 'Not active' || table.game.color === color);
|
|
let toggleText;
|
|
if (name) {
|
|
toggleText = `${name} has ${item.points} VP`;
|
|
if (item.unplayed) {
|
|
toggleText += ` and ${item.unplayed} unplayed DCs`;
|
|
}
|
|
} else {
|
|
toggleText = "Available";
|
|
}
|
|
players.push((
|
|
<div
|
|
data-selectable={selectable}
|
|
data-selected={table.game.color === color}
|
|
className="PlayerEntry"
|
|
onClick={() => { inLobby && selectable && toggleSelected(color) }}
|
|
key={`player-${color}`}>
|
|
<PlayerColor color={color}/>
|
|
<ListItemText primary={toggleText} secondary={(
|
|
<>
|
|
{ item.status + ' ' }
|
|
{ item.status !== 'Not active' && <Moment fromNow date={item.lastActive > Date.now() ? Date.now() : item.lastActive}/>}
|
|
</>)} />
|
|
</div>
|
|
));
|
|
}
|
|
|
|
return (
|
|
<Paper className="Players">
|
|
<List className="PlayerSelector">
|
|
{ players }
|
|
</List>
|
|
</Paper>
|
|
);
|
|
}
|
|
|
|
console.log("TODO: Convert this to a function component!!!!");
|
|
|
|
class Table extends React.Component {
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = {
|
|
total: 0,
|
|
wood: 0,
|
|
sheep: 0,
|
|
brick: 0,
|
|
stone: 0,
|
|
wheat: 0,
|
|
game: null,
|
|
message: "",
|
|
error: "",
|
|
signature: "",
|
|
buildActive: false,
|
|
cardActive: undefined,
|
|
loading: 0,
|
|
noNetwork: false
|
|
};
|
|
this.componentDidMount = this.componentDidMount.bind(this);
|
|
this.throwDice = this.throwDice.bind(this);
|
|
this.rollDice = this.rollDice.bind(this);
|
|
this.setGameState = this.setGameState.bind(this);
|
|
this.shuffleTable = this.shuffleTable.bind(this);
|
|
this.startTrading = this.startTrading.bind(this);
|
|
this.offerTrade = this.offerTrade.bind(this);
|
|
this.acceptTrade = this.acceptTrade.bind(this);
|
|
this.rejectTrade = this.rejectTrade.bind(this);
|
|
this.cancelTrading = this.cancelTrading.bind(this);
|
|
this.discard = this.discard.bind(this);
|
|
this.passTurn = this.passTurn.bind(this);
|
|
this.updateGame = this.updateGame.bind(this);
|
|
this.setPlayerName = this.setPlayerName.bind(this);
|
|
this.setSelected = this.setSelected.bind(this);
|
|
this.updateMessage = this.updateMessage.bind(this);
|
|
this.sendAction = this.sendAction.bind(this);
|
|
this.buildClicked = this.buildClicked.bind(this);
|
|
this.closeCard = this.closeCard.bind(this);
|
|
this.playCard = this.playCard.bind(this);
|
|
this.selectResource = this.selectResource.bind(this);
|
|
|
|
this.mouse = { x: 0, y: 0 };
|
|
this.radius = 0.317;
|
|
|
|
this.loadTimer = null;
|
|
|
|
this.game = null;
|
|
this.pips = [];
|
|
this.tiles = [];
|
|
this.borders = [];
|
|
this.tabletop = null;
|
|
this.closest = {
|
|
info: {},
|
|
tile: null,
|
|
road: null,
|
|
tradeToken: null,
|
|
settlement: null
|
|
};
|
|
|
|
this.id = (props.router && props.router.params.id) ? props.router.params.id : 0;
|
|
}
|
|
|
|
closeCard() {
|
|
this.setState({cardActive: undefined});
|
|
}
|
|
|
|
sendAction(action, value, extra) {
|
|
if (this.loadTimer) {
|
|
window.clearTimeout(this.loadTimer);
|
|
this.loadTimer = null;
|
|
}
|
|
|
|
if (value === undefined || value === null) {
|
|
value = '';
|
|
}
|
|
|
|
this.setState({ loading: this.state.loading + 1 });
|
|
|
|
return window.fetch(`${base}/api/v1/games/${this.state.game.id}/${action}/${value}`, {
|
|
method: "PUT",
|
|
cache: 'no-cache',
|
|
credentials: 'same-origin',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: extra ? JSON.stringify(extra) : undefined
|
|
}).then((res) => {
|
|
if (res.status >= 400) {
|
|
throw new Error(`Unable to perform ${action}!`);
|
|
}
|
|
return res.json();
|
|
}).then((game) => {
|
|
const error = (game.status !== 'success') ? game.status : undefined;
|
|
this.updateGame(game);
|
|
this.updateMessage();
|
|
this.setError(error);
|
|
}).catch((error) => {
|
|
console.error(error);
|
|
this.setError(error.message);
|
|
}).then(() => {
|
|
this.setState({ loading: this.state.loading - 1 });
|
|
});
|
|
}
|
|
|
|
setSelected(key) {
|
|
return this.sendAction('player-selected', key);
|
|
}
|
|
|
|
sendChat(message) {
|
|
return this.sendAction('chat', undefined, {message: message});
|
|
}
|
|
|
|
selectResource(card) {
|
|
return this.sendAction('select-resource', card);
|
|
}
|
|
|
|
playCard(card) {
|
|
this.setState({ cardActive: undefined });
|
|
return this.sendAction('play-card', undefined, card);
|
|
}
|
|
|
|
setPlayerName(name) {
|
|
return this.sendAction('player-name', name)
|
|
.then(() => {
|
|
this.setState({ pickName: false });
|
|
});
|
|
}
|
|
|
|
shuffleTable() {
|
|
return this.sendAction('shuffle')
|
|
.then(() => {
|
|
this.setError("Table shuffled!");
|
|
});
|
|
}
|
|
|
|
startTrading() {
|
|
return this.sendAction('trade');
|
|
}
|
|
|
|
cancelTrading() {
|
|
return this.sendAction('trade', 'cancel');
|
|
}
|
|
|
|
offerTrade(trade) {
|
|
return this.sendAction('trade', 'offer', trade);
|
|
}
|
|
|
|
acceptTrade(trade) {
|
|
return this.sendAction('trade', 'accept', trade);
|
|
}
|
|
|
|
rejectTrade(trade) {
|
|
return this.sendAction('trade', 'reject', trade);
|
|
}
|
|
|
|
discard(resources) {
|
|
return this.sendAction('discard', undefined, resources);
|
|
}
|
|
|
|
passTurn() {
|
|
return this.sendAction('pass');
|
|
};
|
|
|
|
rollDice() {
|
|
return this.sendAction('roll');
|
|
}
|
|
|
|
setError(error) {
|
|
if (!error) {
|
|
return;
|
|
}
|
|
if (this.errorTimeout) {
|
|
clearTimeout(this.errorTimeout);
|
|
}
|
|
setTimeout(() => this.setState({error: undefined}), 3000);
|
|
if (this.state.error !== error) {
|
|
this.setState({ error });
|
|
}
|
|
}
|
|
|
|
setGameState(state) {
|
|
if (this.loadTimer) {
|
|
window.clearTimeout(this.loadTimer);
|
|
this.loadTimer = null;
|
|
}
|
|
|
|
this.setState({ loading: this.state.loading + 1 });
|
|
return window.fetch(`${base}/api/v1/games/${this.state.game.id}/state/${state}`, {
|
|
method: "PUT",
|
|
cache: 'no-cache',
|
|
credentials: 'same-origin',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
}).then((res) => {
|
|
if (res.status >= 400) {
|
|
console.log(res);
|
|
throw new Error(`Unable to set state to ${state}`);
|
|
}
|
|
return res.json();
|
|
}).then((game) => {
|
|
console.log (`Game state set to ${game.state}!`);
|
|
this.updateGame(game);
|
|
this.updateMessage();
|
|
}).catch((error) => {
|
|
console.error(error);
|
|
this.setError(error.message);
|
|
}).then(() => {
|
|
this.setState({ loading: this.state.loading - 1 });
|
|
return this.game.state;
|
|
});
|
|
}
|
|
|
|
buildClicked(event) {
|
|
console.log("Build clicked");
|
|
this.setState({ buildActive: this.state.buildActive ? false : true });
|
|
};
|
|
|
|
placeRobber(robber) {
|
|
return this.sendAction('place-robber', robber);
|
|
};
|
|
|
|
buyDevelopment() {
|
|
return this.sendAction('buy-development');
|
|
}
|
|
|
|
buySettlement() {
|
|
return this.sendAction('buy-settlement');
|
|
}
|
|
|
|
placeSettlement(settlement) {
|
|
return this.sendAction('place-settlement', settlement);
|
|
}
|
|
|
|
buyCity() {
|
|
return this.sendAction('buy-city');
|
|
}
|
|
placeCity(city) {
|
|
return this.sendAction('place-city', city);
|
|
}
|
|
|
|
buyRoad() {
|
|
return this.sendAction('buy-road');
|
|
}
|
|
placeRoad(road) {
|
|
return this.sendAction('place-road', road);
|
|
}
|
|
|
|
stealResource(color) {
|
|
return this.sendAction('steal-resource', color);
|
|
}
|
|
|
|
throwDice() {
|
|
return this.rollDice();
|
|
}
|
|
|
|
updateGame(game) {
|
|
if (this.state.signature !== game.signature) {
|
|
this.setState( { signature: game.signature });
|
|
}
|
|
// console.log("Update Game", game);
|
|
this.setState( { game });
|
|
this.game = game;
|
|
}
|
|
|
|
updateMessage() {
|
|
const player = (this.game && this.game.color) ? this.game.players[this.game.color] : undefined,
|
|
name = this.game ? this.game.name : "";
|
|
|
|
let message = <></>;
|
|
if (this.state.pickName || !name) {
|
|
message = <>{message}Enter the name you would like to be known by, then press <b>ENTER</b> or select <b>SET</b>.</>;
|
|
} else {
|
|
switch (this.game && this.game.state) {
|
|
case 'lobby':
|
|
message = <>{message}You are in the lobby as <b>{name}</b>.</>;
|
|
if (!this.game.color) {
|
|
message = <>{message}You select one of the <b>Available</b> colors below.</>;
|
|
} else {
|
|
message = <>{message}You have selected <PlayerColor color={this.game.color}/>.</>;
|
|
}
|
|
message = <>{message}You can chat with other players below.</>;
|
|
if (this.game.active < 2) {
|
|
message = <>{message}Once there are two or more players, you can select <StartButton table={this}/>.</>;
|
|
} else {
|
|
message = <>{message}There are enough players to start the game. Select <StartButton table={this}/> when ready.</>;
|
|
}
|
|
break;
|
|
case 'game-order':
|
|
if (!player) {
|
|
message = <>{message}You are an observer in this game as <b>{name}</b>.</>;
|
|
message = <>{message}You can chat with other players below as <b>{this.game.name}</b>, but cannot play unless players go back to the Lobby.</>;
|
|
} else {
|
|
if (!player.order) {
|
|
message = <>{message}You need to roll for game order. Click <b>Roll Dice</b> below.</>;
|
|
} else {
|
|
message = <>{message}You rolled <Dice pips={player.order}/> for game order. Waiting for all players to roll.</>;
|
|
}
|
|
}
|
|
break;
|
|
case 'initial-placement':
|
|
message = <>{message}It is time for all the players to place their initial two settlements, with one road connected to each settlement.</>;
|
|
break;
|
|
case 'active':
|
|
if (!player) {
|
|
message = <>{message}This game is no longer in the lobby.<br/><b>TODO: Override game state to allow Lobby mode while in-game</b></>;
|
|
}
|
|
break;
|
|
case null:
|
|
case undefined:
|
|
case '':
|
|
message = <>{message}The game is in a wonky state. Sorry :(</>;
|
|
break;
|
|
case 'normal':
|
|
if (this.game && this.game.turn) {
|
|
if (this.game.turn.roll === 7) {
|
|
message = <>{message}Robber was rolled!</>;
|
|
let move = true;
|
|
for (let color in this.game.players) {
|
|
let name = '';
|
|
for (let i = 0; i < this.game.sessions.length; i++) {
|
|
if (this.game.sessions[i].color === color) {
|
|
name = this.game.sessions[i].name;
|
|
}
|
|
}
|
|
const discard = this.game.players[color].mustDiscard;
|
|
if (discard) {
|
|
move = false;
|
|
message = <>{message}<PlayerColor color={color}/> {name} needs to discard {discard} resources.</>;
|
|
}
|
|
}
|
|
if (move && (this.game.turn && !this.game.turn.placedRobber)) {
|
|
message = <>{message}<PlayerColor color={this.game.turn.color}/> {this.game.turn.name} needs to move the robber.</>
|
|
}
|
|
} else {
|
|
message = <>It is <PlayerColor color={this.game.turn.color}/> {this.game.turn.name}'s turn.</>;
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
message = <>{message}Game state is: {this.game.state}</>;
|
|
break;
|
|
}
|
|
}
|
|
this.setState({ message: message });
|
|
}
|
|
|
|
resetKeepAlive() {
|
|
if (this.keepAlive) {
|
|
clearTimeout(this.keepAlive);
|
|
}
|
|
this.keepAlive = setTimeout(() => {
|
|
console.error(`No server ping for 5 seconds!`);
|
|
this.setState({ noNetwork: true });
|
|
}, 5000);
|
|
}
|
|
|
|
connectWebSocket() {
|
|
let loc = window.location, new_uri;
|
|
if (loc.protocol === "https:") {
|
|
new_uri = "wss";
|
|
} else {
|
|
new_uri = "ws";
|
|
}
|
|
new_uri = `${new_uri}://${loc.host}${base}/api/v1/games/ws/${this.id}`;
|
|
console.log(`Attempting WebSocket connection to ${new_uri}`);
|
|
|
|
this.ws = new WebSocket(new_uri);
|
|
|
|
this.ws.onopen = (event) => {
|
|
console.log(`WebSocket open:`, event);
|
|
this.setState({ noNetwork: false });
|
|
this.resetKeepAlive();
|
|
};
|
|
|
|
this.ws.onmessage = (event) => {
|
|
let data;
|
|
try {
|
|
data = JSON.parse(event.data);
|
|
} catch (error) {
|
|
this.setError(error);
|
|
return;
|
|
}
|
|
let update;
|
|
switch (data.type) {
|
|
case 'game-update':
|
|
update = data.update;
|
|
const error = (update.status !== 'success') ? update.status : undefined;
|
|
this.updateGame(update);
|
|
this.updateMessage();
|
|
this.setError(error);
|
|
break;
|
|
case 'ping':
|
|
this.ws.send(JSON.stringify({ type: 'pong', timestamp: data.ping }));
|
|
break;
|
|
default:
|
|
console.log(`Unknown event type: ${data.type}`);
|
|
break;
|
|
}
|
|
|
|
this.resetKeepAlive();
|
|
this.setState({ noNetwork: false });
|
|
}
|
|
|
|
this.ws.onerror = (event) => {
|
|
this.setState({ error: event.message });
|
|
console.error(`WebSocket error:`, event);
|
|
if (!this.websocketReconnect) {
|
|
this.websocketReconnect = setTimeout(() => {
|
|
delete this.websocketReconnect;
|
|
this.connectWebSocket();
|
|
}, 1000);
|
|
}
|
|
};
|
|
|
|
this.ws.onclose = (event) => {
|
|
console.error(`WebSocket close:`, event);
|
|
this.setState({ noNetowrk: true, error: event.message });
|
|
if (!this.websocketReconnect) {
|
|
this.websocketReconnect = setTimeout(() => {
|
|
delete this.websocketReconnect;
|
|
this.connectWebSocket();
|
|
}, 1000);
|
|
}
|
|
};
|
|
}
|
|
|
|
componentDidMount() {
|
|
this.start = new Date();
|
|
|
|
console.log(`Mounted: ${base}`);
|
|
this.connectWebSocket();
|
|
|
|
const params = {};
|
|
if (this.id) {
|
|
console.log(`Loading game: ${this.id}`);
|
|
params.url = `${base}/api/v1/games/${this.id}`;
|
|
params.method = "GET"
|
|
} else {
|
|
console.log("Requesting new game.");
|
|
params.url = `${base}/api/v1/games/`;
|
|
params.method = "POST";
|
|
}
|
|
|
|
this.setState({ loading: this.state.loading + 1 });
|
|
window.fetch(params.url, {
|
|
method: params.method,
|
|
cache: 'no-cache',
|
|
credentials: 'same-origin',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
// body: JSON.stringify(data) // body data type must match "Content-Type" header
|
|
}).then((res) => {
|
|
if (res.status < 400) {
|
|
return res;
|
|
}
|
|
let error;
|
|
if (!this.id) {
|
|
error = `Unable to create new game.`;
|
|
throw new Error(error);
|
|
}
|
|
|
|
error = `Unable to find game ${this.id}. Starting new game.`
|
|
console.log(error);
|
|
this.setError(error);
|
|
|
|
params.url = `${base}/api/v1/games/${this.id}`;
|
|
params.method = "POST";
|
|
|
|
return window.fetch(params.url, {
|
|
method: params.method,
|
|
cache: 'no-cache',
|
|
credentials: 'same-origin',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
}).then((res) => {
|
|
return res.json();
|
|
}).then((game) => {
|
|
if (!this.id) {
|
|
history.push(`${gamesPath}/${game.id}`);
|
|
}
|
|
|
|
this.updateGame(game);
|
|
this.updateMessage();
|
|
|
|
this.setState({ error: "" });
|
|
}).catch((error) => {
|
|
console.error(error);
|
|
this.setError(error.message);
|
|
}).then(() => {
|
|
this.setState({ loading: this.state.loading - 1 });
|
|
});
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
if (this.loadTimer) {
|
|
clearTimeout(this.loadTimer);
|
|
}
|
|
if (this.keepAlive) {
|
|
clearTimeout(this.keepAlive);
|
|
}
|
|
if (this.updateSizeTimer) {
|
|
clearTimeout(this.updateSizeTimer);
|
|
this.updateSizeTimer = 0;
|
|
}
|
|
if (this.errorTimeout) {
|
|
clearTimeout(this.errorTimeout);
|
|
}
|
|
}
|
|
|
|
cardClicked(card) {
|
|
const game = this.state.game;
|
|
if (!game) {
|
|
return;
|
|
}
|
|
this.setState({cardActive: card });
|
|
}
|
|
|
|
render() {
|
|
const game = this.state.game,
|
|
player = game ? game.player : undefined,
|
|
isTurn = (game && game.turn && game.turn.color === game.color) ? true : false;
|
|
|
|
let color;
|
|
switch (game ? game.color : undefined) {
|
|
case "O": color = "orange"; break;
|
|
case "R": color = "red"; break;
|
|
case "B": color = "blue"; break;
|
|
default: case "W": color = "white"; break;
|
|
}
|
|
let development;
|
|
if (player) {
|
|
let stacks = {};
|
|
game.player.development.forEach(card =>
|
|
(card.type in stacks)
|
|
? stacks[card.type].push(card)
|
|
: stacks[card.type] = [card]);
|
|
|
|
development = [];
|
|
for (let type in stacks) {
|
|
const cards = stacks[type].map(card => <Development
|
|
onClick={() => this.cardClicked(card)}
|
|
card={card}
|
|
table={this}
|
|
key={`${type}-${card.card}`}
|
|
type={`${type}-${card.card}`}/>);
|
|
development.push(<div key={type} className="Stack">{ cards }</div>);
|
|
}
|
|
} else {
|
|
development = <>/</>;
|
|
}
|
|
|
|
return (
|
|
<div className="Table">
|
|
{ this.state.loading > 0 && <CircularProgress className='Loading'/> }
|
|
|
|
{ this.state.noNetwork && <div className='NoNetwork'/> }
|
|
|
|
<div style={{display: "inline-flex", flex: 1, flexDirection: "column"}}>
|
|
<Board table={this} game={game}/>
|
|
{ player !== undefined &&
|
|
<div className="BottomBar">
|
|
<div className="Hand">
|
|
<Resource type="wood" count={player.wood}/>
|
|
<Resource type="wheat" count={player.wheat}/>
|
|
<Resource type="stone" count={player.stone}/>
|
|
<Resource type="brick" count={player.brick}/>
|
|
<Resource type="sheep" count={player.sheep}/>
|
|
</div>
|
|
<div className="Hand">
|
|
{ development }
|
|
</div>
|
|
{ game.longestRoad && game.longestRoad === game.color &&
|
|
<Placard
|
|
active={false}
|
|
type='longest-road'
|
|
table={this}
|
|
/>
|
|
}
|
|
{ game.largestArmy && game.largestArmy === game.color &&
|
|
<Placard
|
|
active={false}
|
|
type='largest-army'
|
|
table={this}
|
|
/>
|
|
}
|
|
<Placard
|
|
active={this.state.buildActive}
|
|
disabled={!game || !game.turn || !game.turn.roll}
|
|
table={this} type={`${color}`}/>
|
|
</div>
|
|
}
|
|
{ player === undefined && <div className="BottomBar"></div>}
|
|
</div>
|
|
|
|
{ game && <div className={'Game ' + game.state}>
|
|
<Paper className="Message">{ this.state.message }</Paper>
|
|
{(this.state.pickName || !game.name) && <PlayerName table={this}/> }
|
|
{(!this.state.pickName && game.name) && <>
|
|
<Players table={this}/>
|
|
<Chat table={this}/>
|
|
<Action table={this}/>
|
|
</> }
|
|
</div> }
|
|
|
|
{ game && game.state === 'winner' &&
|
|
<Winner table={this} color={game.winner}/>
|
|
}
|
|
|
|
{ this.state.cardActive &&
|
|
<ViewCard table={this} card={this.state.cardActive}/>
|
|
}
|
|
|
|
{ game && game.state === 'game-order' &&
|
|
<GameOrder table={this}/>
|
|
}
|
|
|
|
{ game && game.state === 'normal' &&
|
|
game.turn.actions && game.turn.actions.indexOf('trade') !== -1 &&
|
|
<Trade table={this}/>
|
|
}
|
|
|
|
{ game
|
|
&& isTurn
|
|
&& game.turn.actions
|
|
&& game.turn.actions.indexOf('select-resource') !== -1 &&
|
|
<ChooseCard table={this} type={game.turn.active}/>
|
|
}
|
|
|
|
{ game && game.state === 'normal' &&
|
|
game.turn &&
|
|
isTurn &&
|
|
game.turn.actions && game.turn.actions.indexOf('steal-resource') !== -1 &&
|
|
<SelectPlayer table={this} players={game.turn.limits.players}/>
|
|
}
|
|
|
|
{ game && game.turn && !isTurn &&
|
|
(game.state === 'initial-placement' || game.state === 'normal') &&
|
|
(!game.player || !game.player.mustDiscard) && <WaitingForPlayer table={this}/>
|
|
}
|
|
|
|
{ this.state.error && <Paper onClick={() => this.setState({ error: undefined })} className="Error"><div>{this.state.error}</div></Paper> }
|
|
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
export default withRouter(props => <Table {...props}/>);
|