286 lines
8.2 KiB
TypeScript
286 lines
8.2 KiB
TypeScript
import React, { useState, useContext, useMemo, useEffect, useRef } from "react";
|
|
import equal from "fast-deep-equal";
|
|
import "./Activities.css";
|
|
import { PlayerColor } from "./PlayerColor";
|
|
import { Dice } from "./Dice";
|
|
import { GlobalContext } from "./GlobalContext";
|
|
|
|
type ActivityData = {
|
|
message: string;
|
|
color: string;
|
|
date: number;
|
|
};
|
|
|
|
type PlayerData = {
|
|
name: string;
|
|
mustDiscard?: number;
|
|
};
|
|
|
|
type TurnData = {
|
|
color?: string;
|
|
name?: string;
|
|
placedRobber?: boolean;
|
|
robberInAction?: boolean;
|
|
active?: string;
|
|
actions?: string[];
|
|
select?: Record<string, unknown>;
|
|
};
|
|
|
|
type ActivityProps = {
|
|
keep: boolean;
|
|
activity: ActivityData;
|
|
};
|
|
|
|
const Activity: React.FC<ActivityProps> = ({ keep, 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 && !keep) {
|
|
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;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{display && (
|
|
<div className={`Activity ${animation}`}>
|
|
<PlayerColor color={activity.color} />
|
|
{message}
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
const Activities: React.FC = () => {
|
|
const { lastJsonMessage, sendJsonMessage } = useContext(GlobalContext);
|
|
const [activities, setActivities] = useState<ActivityData[]>([]);
|
|
const [turn, setTurn] = useState<TurnData | undefined>(undefined);
|
|
const [color, setColor] = useState<string | undefined>(undefined);
|
|
const [players, setPlayers] = useState<Record<string, PlayerData>>({});
|
|
const [timestamp, setTimestamp] = useState<number>(0);
|
|
const [state, setState] = useState<string>("");
|
|
|
|
const fields = useMemo(() => ["activities", "turn", "players", "timestamp", "color", "state"], []);
|
|
const requestUpdate = (fields: string | string[]) => {
|
|
let request: string[];
|
|
if (!Array.isArray(fields)) {
|
|
request = [fields];
|
|
} else {
|
|
request = fields;
|
|
}
|
|
sendJsonMessage({
|
|
type: "get",
|
|
fields: request,
|
|
});
|
|
};
|
|
useEffect(() => {
|
|
if (!lastJsonMessage) {
|
|
return;
|
|
}
|
|
const data = lastJsonMessage;
|
|
switch (data.type) {
|
|
case "game-update": {
|
|
const ignoring: string[] = [],
|
|
processing: string[] = [];
|
|
if (data.update) {
|
|
for (const field in data.update) {
|
|
if (fields.indexOf(field) === -1) {
|
|
ignoring.push(field);
|
|
} else {
|
|
processing.push(field);
|
|
}
|
|
}
|
|
}
|
|
console.log(`activities - game update`, data.update);
|
|
console.log(`activities - ignoring ${ignoring.join(",")}`);
|
|
console.log(`activities - processing ${processing.join(",")}`);
|
|
|
|
if (data.update && "state" in data.update && data.update.state !== state) {
|
|
requestUpdate("turn");
|
|
setState(data.update.state as string);
|
|
}
|
|
if (data.update && "activities" in data.update && !equal(data.update.activities, activities)) {
|
|
setActivities(data.update.activities as ActivityData[]);
|
|
}
|
|
if (data.update && "turn" in data.update && !equal(data.update.turn, turn)) {
|
|
setTurn(data.update.turn as TurnData);
|
|
}
|
|
if (data.update && "players" in data.update && !equal(data.update.players, players)) {
|
|
setPlayers(data.update.players as Record<string, PlayerData>);
|
|
}
|
|
if (data.update && "timestamp" in data.update && data.update.timestamp !== timestamp) {
|
|
setTimestamp(data.update.timestamp as number);
|
|
}
|
|
if (data.update && "color" in data.update && data.update.color !== color) {
|
|
setColor(data.update.color as string);
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}, [lastJsonMessage, activities, turn, players, timestamp, color, state, fields]);
|
|
|
|
useEffect(() => {
|
|
if (!sendJsonMessage) {
|
|
return;
|
|
}
|
|
sendJsonMessage({
|
|
type: "get",
|
|
fields,
|
|
});
|
|
}, [sendJsonMessage, fields]);
|
|
|
|
if (!timestamp) {
|
|
return <></>;
|
|
}
|
|
|
|
const isTurn = turn && turn.color === color ? true : false,
|
|
normalPlay = ["initial-placement", "normal", "volcano"].indexOf(state) !== -1,
|
|
mustPlaceRobber = turn && !turn.placedRobber && turn.robberInAction,
|
|
placement = state === "initial-placement" || (turn && turn.active === "road-building"),
|
|
placeRoad = placement && turn && turn.actions && turn.actions.indexOf("place-road") !== -1,
|
|
mustStealResource = turn && turn.actions && turn.actions.indexOf("steal-resource") !== -1,
|
|
rollForVolcano = state === "volcano" && turn && !turn.select,
|
|
rollForOrder = state === "game-order",
|
|
selectResources = turn && turn.actions && turn.actions.indexOf("select-resources") !== -1;
|
|
|
|
const discarders: React.ReactElement[] = [];
|
|
let mustDiscard = false;
|
|
for (const key in players) {
|
|
const player = players[key];
|
|
if (!player.mustDiscard) {
|
|
continue;
|
|
}
|
|
mustDiscard = true;
|
|
const name = color === key ? "You" : player.name;
|
|
discarders.push(
|
|
<div key={name} className="Requirement">
|
|
{name} must discard <b>{player.mustDiscard}</b> cards.
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const list: React.ReactElement[] = activities
|
|
.filter((activity, index) => activities.length - 1 === index || timestamp - activity.date < 11000)
|
|
.map((activity, index, filtered) => {
|
|
return <Activity keep={filtered.length - 1 === index} key={activity.date} activity={activity} />;
|
|
});
|
|
|
|
let who: string | React.ReactElement;
|
|
if (turn && turn.select) {
|
|
const selecting: { color: string; name: string }[] = [];
|
|
for (const key in turn.select) {
|
|
selecting.push({
|
|
color: key,
|
|
name: color === key ? "You" : players[key]?.name || "",
|
|
});
|
|
}
|
|
who = (
|
|
<>
|
|
{selecting.map((player, index) => (
|
|
<div className="Who" key={index}>
|
|
<PlayerColor color={player.color} />
|
|
{player.name}
|
|
{index !== selecting.length - 1 ? ", " : ""}
|
|
</div>
|
|
))}
|
|
</>
|
|
);
|
|
} else {
|
|
if (isTurn) {
|
|
who = "You";
|
|
} else {
|
|
if (!turn || !turn.name) {
|
|
who = "Everyone";
|
|
} else {
|
|
who = (
|
|
<>
|
|
<PlayerColor color={turn.color} />
|
|
{turn.name}
|
|
</>
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="Activities">
|
|
{list}
|
|
{normalPlay && !mustDiscard && mustPlaceRobber && <div className="Requirement">{who} must move the Robber.</div>}
|
|
|
|
{placement && (
|
|
<div className="Requirement">
|
|
{who} must place {
|
|
(turn && (turn as any).active === "road-building" && (turn as any).freeRoads)
|
|
? `${(turn as any).freeRoads} roads`
|
|
: placeRoad
|
|
? "a road"
|
|
: "a settlement"
|
|
}.
|
|
</div>
|
|
)}
|
|
|
|
{mustStealResource && <div className="Requirement">{who} must select a player to steal from.</div>}
|
|
|
|
{rollForOrder && <div className="Requirement">{who} must roll for game order.</div>}
|
|
|
|
{rollForVolcano && <div className="Requirement">{who} must roll for Volcano devastation!</div>}
|
|
|
|
{selectResources && <div className="Requirement">{who} must select resources!</div>}
|
|
|
|
{normalPlay && mustDiscard && <> {discarders} </>}
|
|
|
|
{!isTurn && normalPlay && turn && (
|
|
<div>
|
|
It is <PlayerColor color={turn.color} /> {turn.name}
|
|
{"'"}s turn.
|
|
</div>
|
|
)}
|
|
|
|
{isTurn && normalPlay && turn && (
|
|
<div className="Go">
|
|
<PlayerColor color={turn.color} /> It is your turn.
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export { Activities };
|