From 4ff9ad015edab28d10319bd8ce0b185c81237af3 Mon Sep 17 00:00:00 2001 From: James Ketrenos Date: Tue, 1 Mar 2022 20:19:48 -0800 Subject: [PATCH] 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 --- client/src/Activities.css | 32 ++++++++++++-- client/src/Activities.js | 81 +++++++++++++++++++++++++++++----- client/src/Chat.css | 34 ++++++++++----- client/src/Chat.js | 2 +- client/src/Resource.css | 5 +++ client/src/Resource.js | 12 +++++- client/src/Table.js | 38 ++++++++-------- server/routes/games.js | 91 ++++++++++++++++++++++++--------------- 8 files changed, 214 insertions(+), 81 deletions(-) diff --git a/client/src/Activities.css b/client/src/Activities.css index 94e0ec9..69d9108 100644 --- a/client/src/Activities.css +++ b/client/src/Activities.css @@ -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; }; } \ No newline at end of file diff --git a/client/src/Activities.js b/client/src/Activities.js index f52e0b6..4d3ea37 100644 --- a/client/src/Activities.js +++ b/client/src/Activities.js @@ -1,12 +1,60 @@ -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 Activities = ({table }) => { + 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]}{sum}: , {dice[5]}; + } else { + message = <>{dice[1]}{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[5]}{message}; + start = resource[1]; + } else { + message = <>{start}{message}; + start = ''; + } + }*/ + } + + return <>{ display && +
+ {message} +
+ }; +} + +const Activities = ({ table }) => { if (!table.game) { return <>; } @@ -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 ; + }); + return ( - - { !isTurn && normalPlay && (!game.player || !game.player.mustDiscard) && -
Waiting for {table.game.turn.name} to complete their turn.
+
+ { list } + + { !isTurn && normalPlay && !mustDiscard && +
Waiting for {table.game.turn.name} to complete their turn.
} - { isTurn && normalPlay && game.player && game.player.mustDiscard && + + { isTurn && normalPlay && game.player && mustDiscard &&
You must discard.
} - { isTurn && normalPlay && (!game.player || !game.player.mustDiscard) && -
It is your turn.
+ + { isTurn && normalPlay && +
It is your turn.
} - +
); }; diff --git a/client/src/Chat.css b/client/src/Chat.css index f33c6fb..5da0f32 100644 --- a/client/src/Chat.css +++ b/client/src/Chat.css @@ -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 { diff --git a/client/src/Chat.js b/client/src/Chat.js index 6cbc775..851f28d 100644 --- a/client/src/Chat.js +++ b/client/src/Chat.js @@ -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[5]}{message}; + message = <>{resource[5]}{message}; start = resource[1]; } else { message = <>{start}{message}; diff --git a/client/src/Resource.css b/client/src/Resource.css index cb8dae7..ca28cd3 100644 --- a/client/src/Resource.css +++ b/client/src/Resource.css @@ -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 { diff --git a/client/src/Resource.js b/client/src/Resource.js index 3755b39..95b71e3 100644 --- a/client/src/Resource.js +++ b/client/src/Resource.js @@ -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
+
{count}
+
; + } + return ( <> { array.length > 0 && diff --git a/client/src/Table.js b/client/src/Table.js index 642bb96..989af1a 100755 --- a/client/src/Table.js +++ b/client/src/Table.js @@ -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 }) => { } { !inLobby && <> - + { 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; diff --git a/server/routes/games.js b/server/routes/games.js index 29a7575..e52a3fc 100755 --- a/server/routes/games.js +++ b/server/routes/games.js @@ -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,