1
0

Added Activity feed

Fixed some more WebSocket timeouts

Changed Resource to support a label=true mode which puts a bubble lable instead of creating a stack

Signed-off-by: James Ketrenos <james_eikona@ketrenos.com>
This commit is contained in:
James Ketrenos 2022-03-01 20:19:48 -08:00
parent 2fa436081b
commit 4ff9ad015e
8 changed files with 214 additions and 81 deletions

View File

@ -18,12 +18,36 @@
.Activities .PlayerColor {
display: inline-flex;
width: 0.8em;
height: 0.8em;
width: 1rem;
height: 1rem;
padding: 0;
margin: 0;
margin: 0 0.2rem;
}
.Activities > div {
padding: 0.5em;
padding: 0.25rem 0.5rem;
display: flex;
align-items: center;
}
.Activity b,
.Activity .Dice {
margin-left: 0.25em;
}
.Activities > div:last-child {
border-top: 1px solid black;
}
.Activity.open{
opacity: 1;
}
.Activity.close{
animation: bounce-out 1s ease-in;
}
@keyframes bounce-out{
0% {opacity: 1; }
100% {opacity: 0; };
}

View File

@ -1,10 +1,58 @@
import React from "react";
import React, { useState, useCallback, useEffect } from "react";
import "./Activities.css";
import Paper from '@material-ui/core/Paper';
import Resource from './Resource.js';
import { getPlayerName } from './Common.js';
import PlayerColor from './PlayerColor.js';
import Dice from './Dice.js';
const Activity = ({ activity }) => {
const [animation, setAnimation] = useState('open');
const [display, setDisplay] = useState(true)
const hide = async (ms) => {
await new Promise(r => setTimeout(r, ms));
setAnimation('close')
await new Promise(r => setTimeout(r, 1000));
setDisplay(false)
};
if (display) {
setTimeout(() => hide(10000), 0);
}
let message;
/* If the date is in the future, set it to now */
const dice = activity.message.match(/^(.*rolled )([1-6])(, ([1-6]))?(.*)$/);
if (dice) {
if (dice[4]) {
const sum = parseInt(dice[2]) + parseInt(dice[4]);
message = <>{dice[1]}<b>{sum}</b>: <Dice pips={dice[2]}/>, <Dice pips={dice[4]}/>{dice[5]}</>;
} else {
message = <>{dice[1]}<Dice pips={dice[2]}/>{dice[5]}</>;
}
} else {
message = activity.message; /*
let start = activity.message;
while (start) {
const resource = start.match(/^(.*)(([0-9]+) (wood|sheep|wheat|stone|brick),?)(.*)$/);
if (resource) {
const count = resource[3] ? parseInt(resource[3]) : 1;
message = <><Resource count={count} type={resource[4]}/>{resource[5]}{message}</>;
start = resource[1];
} else {
message = <>{start}{message}</>;
start = '';
}
}*/
}
return <>{ display &&
<div className={`Activity ${animation}`}>
<PlayerColor color={activity.color}/>{message}
</div>
}</>;
}
const Activities = ({ table }) => {
if (!table.game) {
@ -14,20 +62,31 @@ const Activities = ({table }) => {
const
game = table.game,
isTurn = (game.turn && game.turn.color === game.color) ? true : false,
normalPlay = (game.state === 'initial-placement' || game.state === 'normal');
normalPlay = (game.state === 'initial-placement' || game.state === 'normal'),
mustDiscard = game.player ? (parseInt(game.player.mustDiscard ? game.player.mustDiscard : 0) !== 0) : false;
const list = game.activities
.filter(activity => game.timestamp - activity.date < 11000)
.map(activity => {
return <Activity key={activity.date} activity={activity}/>;
});
return (
<Paper className="Activities">
{ !isTurn && normalPlay && (!game.player || !game.player.mustDiscard) &&
<div>Waiting for {table.game.turn.name} to complete their turn.</div>
<div className="Activities">
{ list }
{ !isTurn && normalPlay && !mustDiscard &&
<div>Waiting for <PlayerColor color={table.game.turn.color}/> {table.game.turn.name} to complete their turn.</div>
}
{ isTurn && normalPlay && game.player && game.player.mustDiscard &&
{ isTurn && normalPlay && game.player && mustDiscard &&
<div>You must discard.</div>
}
{ isTurn && normalPlay && (!game.player || !game.player.mustDiscard) &&
<div>It is your turn.</div>
{ isTurn && normalPlay &&
<div><PlayerColor color={game.turn.color}/> It is your turn.</div>
}
</Paper>
</div>
);
};

View File

@ -35,6 +35,7 @@
.ChatList .MuiTypography-body1 {
font-size: 0.8rem;
display: flex;
flex-wrap: wrap;
}
.ChatList .System .MuiTypography-body1 {
@ -61,19 +62,32 @@
.ChatList .Resource {
display: inline-flex;
width: 3em;
height: 4.3em;
align-items: center;
justify-content: space-around;
height: 1.5rem;
width: 1.5rem;
min-width: 1.5rem;
min-height: 1.5rem;
pointer-events: none;
margin: 0 0.125rem;
background-size: 130%;
border: 2px solid #444;
border-radius: 2px;
margin-right: 0.5rem;
}
.ChatList .Stack {
margin-left: 0;
transition: none;
}
.ChatList .Stack > *:not(:first-child) {
margin-left: 0;
transition: none;
.ChatList .Resource > div {
position: absolute;
top: -0.625rem;
right: -0.625rem;
border-radius: 50%;
border: 1px solid white;
background-color: rgb(36, 148, 46);
font-size: 0.75rem;
width: 1rem;
height: 1rem;
text-align: center;
line-height: 1rem;
}
.ChatList .Dice {

View File

@ -96,7 +96,7 @@ const Chat = ({ table }) => {
const resource = start.match(/^(.*)(([0-9]+) (wood|sheep|wheat|stone|brick),?)(.*)$/);
if (resource) {
const count = resource[3] ? parseInt(resource[3]) : 1;
message = <><Resource count={count} type={resource[4]}/>{resource[5]}{message}</>;
message = <><Resource label={true} count={count} type={resource[4]}/>{resource[5]}{message}</>;
start = resource[1];
} else {
message = <>{start}{message}</>;

View File

@ -8,6 +8,11 @@
background-size: cover;
margin: 0.25em;
cursor: pointer;
display: inline-flex;
justify-content: space-around;
align-items: center;
color: white;
font-weight: bold;
}
.Resource:hover {

View File

@ -2,13 +2,23 @@ import React from "react";
import "./Resource.css";
import { assetsPath } from './Common.js';
const Resource = ({ type, select, disabled, count }) => {
const Resource = ({ type, select, disabled, count, label }) => {
const array = new Array(Number(count ? count : 0));
const click = select ? select : (event) => {
if (!disabled) {
event.target.classList.toggle('Selected');
}
};
if (label) {
return <div className="Resource"
data-type={type}
onClick={click}
style={{backgroundImage:`url(${assetsPath}/gfx/card-${type}.png)`}}>
<div>{count}</div>
</div>;
}
return (
<>
{ array.length > 0 &&

View File

@ -266,6 +266,7 @@ const Action = ({ table }) => {
const game = table.game,
inLobby = game.state === 'lobby',
inGame = game.state === 'normal',
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,
@ -279,7 +280,7 @@ const Action = ({ 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 || !inGame} 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 &&
@ -734,30 +735,33 @@ class Table extends React.Component {
}
resetKeepAlive(isDead) {
if (isDead) {
console.log(`Short circuiting keep-alive`);
} else {
console.log(`Resetting keep-alive`);
}
if (this.keepAlive) {
clearTimeout(this.keepAlive);
this.keepAlive = 0;
} else {
console.log(`No keep-alive active`);
}
this.keepAlive = setTimeout(() => {
console.error(`No server ping after 10 seconds (or connection closed by server)!`);
this.setState({ noNetwork: true });
if (this.ws) {
this.ws.close();
}
this.connectWebSocket();
}, isDead ? 3000 : 10000);
if (this.state.noNetwork !== false && !isDead) {
this.setState({ noNetwork: false });
} else if (this.state.noNetwork !== true && isDead) {
this.setState({ noNetwork: true });
}
this.keepAlive = setTimeout(() => {
console.error(`No server ping!`);
this.setState({ noNetwork: true });
if (this.ws) {
this.ws.close();
}
if (!this.websocketReconnect) {
this.websocketReconnect = setTimeout(() => {
delete this.websocketReconnect;
this.connectWebSocket();
}, 1000);
}
}, isDead ? 1000 : 5000);
}
connectWebSocket() {
@ -907,10 +911,6 @@ class Table extends React.Component {
clearTimeout(this.keepAlive);
this.keepAlive = 0;
}
if (this.websocketReconnect) {
clearTimeout(this.websocketReconnect);
this.websocketReconnect = 0;
}
if (this.updateSizeTimer) {
clearTimeout(this.updateSizeTimer);
this.updateSizeTimer = 0;

View File

@ -365,7 +365,7 @@ const processRoll = (game, dice) => {
return;
}
game.dice = dice;
addChatMessage(game, session, `${session.name} rolled ${game.dice[0]}, ${game.dice[1]}.`);
addActivity(game, session, `${session.name} rolled ${game.dice[0]}, ${game.dice[1]}.`);
game.turn.roll = game.dice[0] + game.dice[1];
if (game.turn.roll === 7) {
game.turn.robberInAction = true;
@ -388,7 +388,7 @@ const processRoll = (game, dice) => {
if (mustDiscard.length === 0) {
addChatMessage(game, null, `ROBBER! ${game.robberName} Robber Roberson has fled, and no one had to discard!`);
addChatMessage(game, null, `But drat! A new robber has arrived and must be placed by ${game.turn.name}.`);
addChatMessage(game, null, `A new robber has arrived and must be placed by ${game.turn.name}.`);
game.turn.actions = [ 'place-robber' ];
game.turn.limits = { pips: [] };
for (let i = 0; i < 19; i++) {
@ -677,7 +677,7 @@ const adminActions = (game, action, value) => {
case 'game-order':
game.dice = dice;
message = `${game.turn.name} rolled ${game.dice[0]}.`;
addChatMessage(game, session, message);
addActivity(game, session, message);
message = undefined;
processGameOrder(game, session.player, game.dice[0]);
break;
@ -717,7 +717,7 @@ const adminActions = (game, action, value) => {
continue;
}
console.log(`Kicking ${value} from ${game.id}.`);
const preamble = session.name ? `${session.name}, playing as ${color},` : color;
const preamble = session.name ? `${session.name}, playing as ${colorToWord(color)},` : colorToWord(color);
addChatMessage(game, null, `${preamble} was kicked from game by the Admin.`);
if (player) {
session.player = undefined;
@ -726,7 +726,7 @@ const adminActions = (game, action, value) => {
session.color = undefined;
return;
}
return `Unable to find active session for ${color} (${value})`;
return `Unable to find active session for ${colorToWord(color)} (${value})`;
default:
return `Invalid admin action ${action}.`;
@ -774,6 +774,17 @@ const setPlayerName = (game, session, name) => {
return undefined;
}
const colorToWord = (color) => {
switch (color) {
case 'O': return 'orange';
case 'W': return 'white';
case 'B': return 'blue';
case 'R': return 'red';
default:
return undefined;
}
}
const setPlayerColor = (game, session, color) => {
if (!game) {
return `No game found`;
@ -793,10 +804,10 @@ const setPlayerColor = (game, session, color) => {
/* Deselect currently active player for this session */
clearPlayer(player);
if (game.state !== 'lobby') {
message = `${name} has exited to the lobby and is no longer playing as ${session.color}.`
message = `${name} has exited to the lobby and is no longer playing as ${colorToWord(session.color)}.`
addChatMessage(game, null, message);
} else {
message = `${name} is no longer ${session.color}.`;
message = `${name} is no longer ${colorToWord(session.color)}.`;
}
session.player = undefined;
session.color = undefined;
@ -824,7 +835,7 @@ const setPlayerColor = (game, session, color) => {
for (let key in game.sessions) {
const tmp = game.sessions[key].player;
if (tmp && tmp.color === color) {
return `${game.sessions[key].name} already has ${color}`;
return `${game.sessions[key].name} already has ${colorToWord(color)}`;
}
}
@ -834,7 +845,7 @@ const setPlayerColor = (game, session, color) => {
session.player.status = `Active`;
session.player.lastActive = Date.now();
session.color = color;
addChatMessage(game, session, `${session.name} has chosen to play as ${color}.`);
addChatMessage(game, session, `${session.name} has chosen to play as ${colorToWord(color)}.`);
const afterActive = getActiveCount(game);
if (afterActive !== priorActive) {
@ -845,6 +856,14 @@ const setPlayerColor = (game, session, color) => {
}
};
const addActivity = (game, session, message) => {
let date = Date.now();
if (game.activities.length && game.activities[game.activities.length - 1].date === date) {
date++;
}
game.activities.push({ color: session.color, message, date });
}
const addChatMessage = (game, session, message) => {
game.chat.push({
from: session ? session.name : undefined,
@ -1521,7 +1540,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
game.players[key].gets = [];
delete game.players[key].offerRejected;
}
addChatMessage(game, session, `${name} requested to begin trading negotiations.`);
addActivity(game, session, `${name} requested to begin trading negotiations.`);
break;
}
@ -1534,7 +1553,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
}
game.turn.actions = [];
game.turn.limits = {};
addChatMessage(game, session, `${name} has cancelled trading negotiations.`);
addActivity(game, session, `${name} has cancelled trading negotiations.`);
break;
}
@ -1564,14 +1583,14 @@ router.put("/:id/:action/:value?", async (req, res) => {
}
game.turn.offer = offer;
}
addChatMessage(game, session, `${session.name} submitted an offer to give ${offerToString(offer)}.`);
// addActivity(game, session, `${session.name} submitted an offer to give ${offerToString(offer)}.`);
break;
}
/* Any player can reject an offer */
if (value === 'reject') {
session.player.offerRejected = true;
addChatMessage(game, session, `${session.name} rejected ${game.turn.name}'s offer.`);
addActivity(game, session, `${session.name} rejected ${game.turn.name}'s offer.`);
break;
}
@ -1699,7 +1718,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
color: getColorFromName(game, next)
};
game.turns++;
addChatMessage(game, session, `${name} passed their turn.`);
addActivity(game, session, `${name} passed their turn.`);
addChatMessage(game, null, `It is ${next}'s turn.`);
break;
@ -1774,7 +1793,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
debugChat(game, 'Before steal');
if (cards.length === 0) {
addChatMessage(game, session, `${playerNameFromColor(game, value)} did not have any cards to steal.`);
addActivity(game, session, `${playerNameFromColor(game, value)} did not have any cards to steal.`);
game.turn.actions = [];
game.turn.limits = {};
} else {
@ -1824,7 +1843,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
}
debugChat(game, 'Before development purchase');
addChatMessage(game, session, `${session.name} purchased a development card.`);
addActivity(game, session, `${session.name} purchased a development card.`);
player.stone--;
player.wheat--;
player.sheep--;
@ -1882,29 +1901,29 @@ router.put("/:id/:action/:value?", async (req, res) => {
error = `You can not play victory point cards until you can reach 10!`;
break;
}
addChatMessage(game, session, `${session.name} played a Victory Point card.`);
addActivity(game, session, `${session.name} played a Victory Point card.`);
}
if (card.type === 'progress') {
switch (card.card) {
case 'road-1':
case 'road-2':
addChatMessage(game, session, `${session.name} played a Road Building card. The server is giving them 2 brick and 2 wood to build those roads!`);
addActivity(game, session, `${session.name} played a Road Building card. The server is giving them 2 brick and 2 wood to build those roads!`);
player.brick += 2;
player.wood += 2;
break;
case 'monopoly':
game.turn.actions = [ 'select-resource' ];
game.turn.active = 'monopoly';
addChatMessage(game, session, `${session.name} played the Monopoly card, and is selecting their resource type to claim.`);
addActivity(game, session, `${session.name} played the Monopoly card, and is selecting their resource type to claim.`);
break;
case 'year-of-plenty':
game.turn.actions = [ 'select-resource' ];
game.turn.active = 'year-of-plenty';
addChatMessage(game, session, `${session.name} played the Year of Plenty card.`);
addActivity(game, session, `${session.name} played the Year of Plenty card.`);
break;
default:
addChatMessage(game, session, `Oh no! ${card.card} isn't impmented yet!`);
addActivity(game, session, `Oh no! ${card.card} isn't impmented yet!`);
break;
}
}
@ -1913,7 +1932,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
if (card.type === 'army') {
player.army++;
addChatMessage(game, session, `${session.name} played a Kaniget!`);
addActivity(game, session, `${session.name} played a Kaniget!`);
if (player.army > 2 &&
(!game.largestArmy || game.players[game.largestArmy].army < player.army)) {
@ -1967,7 +1986,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
if (error) {
break;
}
addChatMessage(game, session, `${session.name} has chosen ${type}!`);
addActivity(game, session, `${session.name} has chosen ${type}!`);
switch (game.turn.active) {
case 'monopoly':
@ -1992,7 +2011,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
if (gave.length) {
addChatMessage(game, session, `Players ${gave.join(', ')}. In total, ${session.name} received ${total} ${type}.`);
} else {
addChatMessage(game, session, 'No players had that resource. Wa-waaaa.');
addActivity(game, session, 'No players had that resource. Wa-waaaa.');
}
break;
@ -2038,7 +2057,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
break;
}
placeSettlement(game, corners);
addChatMessage(game, session, `${game.turn.name} is considering placing a settlement.`);
addActivity(game, session, `${game.turn.name} is considering placing a settlement.`);
break;
case 'place-settlement':
@ -2118,10 +2137,10 @@ router.put("/:id/:action/:value?", async (req, res) => {
game.turn.actions = [];
game.turn.limits = {};
if (bankType) {
addChatMessage(game, session,
addActivity(game, session,
`${name} placed a settlement by a maritime bank that trades ${bankType}.`);
} else {
addChatMessage(game, session, `${name} placed a settlement.`);
addActivity(game, session, `${name} placed a settlement.`);
}
calculateRoadLengths(game, session);
} else if (game.state === 'initial-placement') {
@ -2151,11 +2170,11 @@ router.put("/:id/:action/:value?", async (req, res) => {
}
player.settlements--;
if (bankType) {
addChatMessage(game, session,
addActivity(game, session,
`${name} placed a settlement by a maritime bank that trades ${bankType}. ` +
`Next, they need to place a road.`);
} else {
addChatMessage(game, session, `${name} placed a settlement. ` +
addActivity(game, session, `${name} placed a settlement. ` +
`Next, they need to place a road.`);
}
placeRoad(game, layout.corners[index].roads);
@ -2195,7 +2214,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
break;
}
placeCity(game, corners);
addChatMessage(game, session, `${game.turn.name} is considering upgrading a settlement to a city.`);
addActivity(game, session, `${game.turn.name} is considering upgrading a settlement to a city.`);
break;
case 'place-city':
@ -2252,7 +2271,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
debugChat(game, 'After city purchase');
game.turn.actions = [];
game.turn.limits = {};
addChatMessage(game, session, `${name} upgraded a settlement to a city!`);
addActivity(game, session, `${name} upgraded a settlement to a city!`);
break;
case 'buy-road':
@ -2288,7 +2307,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
break;
}
placeRoad(game, roads);
addChatMessage(game, session, `${game.turn.name} is considering building a road.`);
addActivity(game, session, `${game.turn.name} is considering building a road.`);
break;
case 'place-road':
@ -2340,12 +2359,12 @@ router.put("/:id/:action/:value?", async (req, res) => {
road.color = session.color;
game.turn.actions = [];
game.turn.limits = {};
addChatMessage(game, session, `${name} placed a road.`);
addActivity(game, session, `${name} placed a road.`);
calculateRoadLengths(game, session);
} else if (game.state === 'initial-placement') {
road.color = session.color;
addChatMessage(game, session, `${name} placed a road.`);
addActivity(game, session, `${name} placed a road.`);
calculateRoadLengths(game, session);
let next;
@ -2402,7 +2421,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
player[type] += receives[type];
message.push(`${receives[type]} ${type}`);
}
addChatMessage(game, session, `${session.name} receives ${message.join(', ')}.`);
addActivity(game, session, `${session.name} receives ${message.join(', ')}.`);
}
}
addChatMessage(game, null, `It is ${name}'s turn.`);
@ -2491,6 +2510,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
const ping = (session) => {
session.ping = Date.now();
console.log(`Sending ping to ${session.name}`);
session.ws.send(JSON.stringify({ type: 'ping', ping: session.ping }));
if (session.keepAlive) {
clearTimeout(session.keepAlive);
@ -2783,6 +2803,7 @@ const resetGame = (game) => {
},
developmentCards: [],
chat: [],
activities: [],
pipOrder: game.pipOrder,
borderOrder: game.borderOrder,
tileOrder: game.tileOrder,