diff --git a/src/components/ChatCards.js b/src/components/ChatCards.js index 8bdc211..a68ae6b 100644 --- a/src/components/ChatCards.js +++ b/src/components/ChatCards.js @@ -5,7 +5,7 @@ import Typography from "@material-ui/core/Typography"; import { makeStyles } from "@material-ui/core/styles"; import { cardsFromEvent, checkSetUltra, conjugateCard, modes } from "../game"; -import { formatTime } from "../util"; +import { formatCount, formatTime } from "../util"; import SetCard from "./SetCard"; import User from "./User"; @@ -35,40 +35,52 @@ const useStyles = makeStyles((theme) => ({ function ChatCards({ item, gameMode, startedAt }) { const classes = useStyles(); const setType = modes[gameMode].setType; - let cards = cardsFromEvent(item); - if (setType === "UltraSet") { - // Arrange cards in pairs and add the 5th card - cards = checkSetUltra(...cards) || cards.slice(); - cards.splice(2, 0, conjugateCard(cards[0], cards[1]), null); + let cards; + if (!item.kind) { + cards = cardsFromEvent(item); + if (setType === "UltraSet") { + // Arrange cards in pairs and add the 5th card + cards = checkSetUltra(...cards) || cards.slice(); + cards.splice(2, 0, conjugateCard(cards[0], cards[1]), null); + } } return (
- - {setType} found by + + {item.kind === "board_done" + ? `Board clear: ${formatCount(item.count, setType)}` + : `${setType} found by`} - -
-
- {setType === "Set" && - cards.map((card) => )} - {(setType === "UltraSet" || setType === "GhostSet") && - Array.from(Array(3), (_, i) => ( -
- - {cards[i * 2 + 1] && ( - - )} -
- ))} + {item.user && ( + + )}
+ {cards && ( +
+ {setType === "Set" && + cards.map((card) => ( + + ))} + {(setType === "UltraSet" || setType === "GhostSet") && + Array.from(Array(3), (_, i) => ( +
+ + {cards[i * 2 + 1] && ( + + )} +
+ ))} +
+ )}
); diff --git a/src/components/Game.js b/src/components/Game.js index cab552b..4ecc789 100644 --- a/src/components/Game.js +++ b/src/components/Game.js @@ -1,4 +1,4 @@ -import { useContext, useMemo } from "react"; +import { useContext, useEffect, useMemo, useState } from "react"; import Divider from "@material-ui/core/Divider"; import Paper from "@material-ui/core/Paper"; @@ -31,6 +31,7 @@ function Game({ selected, answer, lastSet, + lastKeptSet, faceDown, showShortcuts, remaining = -1, @@ -40,6 +41,16 @@ function Game({ const isHorizontal = cardOrientation === "horizontal"; const isLandscape = layoutOrientation === "landscape"; const [gameDimensions, gameEl] = useDimensions(); + const [highlightCards, setHighlightCards] = useState(null); + + const lastKeptCards = lastKeptSet?.join("|"); + useEffect(() => { + setHighlightCards(lastKeptCards?.split("|")); + if (lastKeptCards) { + const timer = setTimeout(() => setHighlightCards(null), 500); + return () => clearTimeout(timer); + } + }, [lastKeptCards]); [board, lastSet] = useMemo( () => addLastSet(board, lastSet), @@ -249,6 +260,7 @@ function Game({ width={cardWidth} hinted={answer?.includes(card)} active={selected?.includes(card)} + highlight={highlightCards?.includes(card)} faceDown={ faceDown === "all" || (faceDown && diff --git a/src/components/ResponsiveSetCard.js b/src/components/ResponsiveSetCard.js index e8f5207..6bbe462 100644 --- a/src/components/ResponsiveSetCard.js +++ b/src/components/ResponsiveSetCard.js @@ -18,7 +18,8 @@ const useStyles = makeStyles((theme) => ({ justifyContent: "center", flexShrink: 0, backgroundColor: theme.setCard.background, - transition: "box-shadow 0.15s", + transition: + "box-shadow 0.15s, width 0.5s, height 0.5s, background-color 0.3s", }, clickable: { cursor: "pointer", @@ -31,6 +32,9 @@ const useStyles = makeStyles((theme) => ({ active: { boxShadow: "0px 0px 5px 3px #4b9e9e !important", }, + highlight: { + backgroundColor: theme.setCard.highlight, + }, hintedOverlay: { position: "absolute", inset: 0, @@ -71,7 +75,7 @@ function ResponsiveSetCard(props) { const theme = useTheme(); // Black magic below to scale cards given any width - const { width, value, onClick, hinted, active, faceDown } = props; + const { width, value, onClick, hinted, active, highlight, faceDown } = props; const height = Math.round(width / 1.6); const margin = Math.round(width * 0.035); const contentWidth = width - 2 * margin; @@ -109,6 +113,7 @@ function ResponsiveSetCard(props) { className={clsx(classes.card, { [classes.clickable]: onClick, [classes.active]: active, + [classes.highlight]: highlight, })} style={{ ...extraStyle, @@ -118,7 +123,6 @@ function ResponsiveSetCard(props) { margin: margin, borderRadius: margin, border: faceDown ? undefined : BORDERS[border], - transition: "width 0.5s, height 0.5s", }} onClick={onClick} > diff --git a/src/game.js b/src/game.js index 3d80019..2a571b8 100644 --- a/src/game.js +++ b/src/game.js @@ -1,3 +1,5 @@ +import { formatCount } from "./util"; + export const BASE_RATING = 1200; export const SCALING_FACTOR = 800; @@ -108,11 +110,10 @@ export function addCard(deck, card, gameMode, findState) { if (doChain) { const fromLast = cards.reduce((s, c) => s + lastSet.includes(c), 0); if (fromLast !== chain) { - const noun = chain > 1 ? "cards" : "card"; return { kind: "error", cards, - error: `${chain} ${noun} must be from the previous ${setType}`, + error: `${formatCount(chain, "card")} must be from the previous ${setType}`, }; } } @@ -295,7 +296,7 @@ function processValidEvent(internalGameState, event, cards) { history.push(event); } -function updateBoard(internalGameState, cards) { +function updateBoard(internalGameState, event, cards) { const { current, gameMode, puzzle, boardSize, minBoardSize, findState } = internalGameState; // in puzzle modes only advance after all sets were found @@ -304,6 +305,17 @@ function updateBoard(internalGameState, cards) { if (findSet(board, gameMode, findState)) return; findState.foundSets.clear(); cards = board; + // add a synthetic event to show in chat that the board changed + const { history } = internalGameState; + let i; + for (i = history.length - 2; i >= 0; i--) { + if (history[i].kind === "board_done") break; + } + history.push({ + time: event.time, + kind: "board_done", + count: history.length - 1 - i, + }); } // remove cards, preserving positions when possible removeCards(internalGameState, cards); @@ -333,7 +345,7 @@ function processEvent(internalGameState, event) { findState.lastSet = allCards; } processValidEvent(internalGameState, event, cards); - updateBoard(internalGameState, cards); + updateBoard(internalGameState, event, cards); } export function computeState(gameData, gameMode) { diff --git a/src/pages/GamePage.js b/src/pages/GamePage.js index 1ca3cef..55a0a21 100644 --- a/src/pages/GamePage.js +++ b/src/pages/GamePage.js @@ -24,10 +24,12 @@ import { SettingsContext, UserContext } from "../context"; import firebase, { createGame, finishGame } from "../firebase"; import { addCard, + cardsFromEvent, computeState, eventFromCards, findSet, generateDeck, + modes, removeCard, } from "../game"; import useFirebaseRef from "../hooks/useFirebaseRef"; @@ -142,15 +144,29 @@ function GamePage({ match }) { ); }, [gameMode, gameData?.deck, gameData?.seed]); - const { current, scores, lastEvents, history, board, answer, findState } = - useMemo(() => { - if (!gameData) return {}; - const state = computeState({ ...gameData, deck }, gameMode); - const { current, boardSize, findState } = state; - const board = current.slice(0, boardSize); - const answer = findSet(board, gameMode, findState); - return { ...state, board, answer }; - }, [gameMode, gameData, deck]); + const { + current, + scores, + lastEvents, + history, + board, + answer, + findState, + lastKeptSet, + } = useMemo(() => { + if (!gameData) return {}; + const state = computeState({ ...gameData, deck }, gameMode); + const { current, boardSize, findState, history } = state; + const board = current.slice(0, boardSize); + const answer = findSet(board, gameMode, findState); + const lastKeptSet = + modes[gameMode].puzzle && + history.length && + !history[history.length - 1].kind + ? cardsFromEvent(history[history.length - 1]) + : null; + return { ...state, board, answer, lastKeptSet }; + }, [gameMode, gameData, deck]); if (redirect) return ; @@ -414,6 +430,7 @@ function GamePage({ match }) { onClick={handleClick} onClear={handleClear} lastSet={findState.lastSet} + lastKeptSet={lastKeptSet} answer={hint} remaining={current.length - board.length} faceDown={paused ? "all" : gameMode === "memory" ? "deal" : ""} diff --git a/src/themes.js b/src/themes.js index 10a1484..9b8ae22 100644 --- a/src/themes.js +++ b/src/themes.js @@ -1,6 +1,4 @@ -import { grey } from "@material-ui/core/colors"; -import { indigo } from "@material-ui/core/colors"; -import { red } from "@material-ui/core/colors"; +import { grey, indigo, red } from "@material-ui/core/colors"; import { createTheme } from "@material-ui/core/styles"; export const darkTheme = createTheme({ @@ -48,6 +46,7 @@ export const darkTheme = createTheme({ red: "#ffb047", background: "#262626", hinted: "rgba(41, 182, 246, 0.25)", + highlight: "rgba(75, 158, 158, 0.3)", backColors: [ grey[900], grey[800], @@ -96,6 +95,7 @@ export const lightTheme = createTheme({ red: "#ff0101", background: "#fff", hinted: "rgba(3, 169, 244, 0.2)", + highlight: "rgba(255, 234, 0, 0.3)", backColors: [ indigo[600], indigo[300], diff --git a/src/util.js b/src/util.js index 6b2ef5a..2358a6e 100644 --- a/src/util.js +++ b/src/util.js @@ -112,6 +112,11 @@ export function formatTime(t, hideSubsecond) { return (hours ? `${hours}:` : "") + moment.utc(rest).format(format); } +export function formatCount(count, singular, plural = null) { + const noun = count === 1 ? singular : (plural ?? singular + "s"); + return `${count} ${noun}`; +} + export function censorText(text) { return censor.applyTo(text, badWords.getAllMatches(text)); }