Compare commits
8 Commits
602e4abece
...
a0e4942680
Author | SHA1 | Date | |
---|---|---|---|
a0e4942680 | |||
c4985162ce | |||
c6bb6c0ad5 | |||
dfce3aa2f4 | |||
dd8b930d0b | |||
260139b7a3 | |||
b541dd49e2 | |||
e092bd5d01 |
@ -4,7 +4,6 @@
|
|||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin: 0.25rem 0.25rem 0.25rem 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.PlayerList .Name {
|
.PlayerList .Name {
|
||||||
@ -53,6 +52,7 @@
|
|||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.PlayerList .PlayerSelector.MuiList-padding {
|
.PlayerList .PlayerSelector.MuiList-padding {
|
||||||
|
@ -162,7 +162,8 @@ const PlayerList: React.FC = () => {
|
|||||||
sx={{
|
sx={{
|
||||||
maxWidth: { xs: '100%', sm: 500 },
|
maxWidth: { xs: '100%', sm: 500 },
|
||||||
p: { xs: 1, sm: 2 },
|
p: { xs: 1, sm: 2 },
|
||||||
m: { xs: 0, sm: 2 },
|
mt: 0.5,
|
||||||
|
mb: 0.5,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MediaAgent {...{ session, peers, setPeers }} />
|
<MediaAgent {...{ session, peers, setPeers }} />
|
||||||
|
@ -225,13 +225,26 @@ const RoomView = (props: RoomProps) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state === "volcano") {
|
if (state === "volcano") {
|
||||||
if (!audioEffects.volcano) {
|
if (audio) {
|
||||||
audioEffects.volcano = loadAudio("volcano-eruption.mp3");
|
if (!audioEffects.volcano) {
|
||||||
audioEffects.volcano.volume = volume * volume;
|
audioEffects.volcano = loadAudio("volcano-eruption.mp3");
|
||||||
|
audioEffects.volcano.volume = volume * volume;
|
||||||
|
} else {
|
||||||
|
if (!audioEffects.volcano.hasPlayed && audioEffects.volcano.readyState >= 2) {
|
||||||
|
audioEffects.volcano.hasPlayed = true;
|
||||||
|
audioEffects.volcano.play().catch((e) => console.error("Audio play failed:", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!audioEffects.volcano.hasPlayed && audioEffects.volcano.readyState >= 2) {
|
// Audio disabled -> stop any currently playing volcano effect
|
||||||
audioEffects.volcano.hasPlayed = true;
|
if (audioEffects.volcano) {
|
||||||
audioEffects.volcano.play().catch((e) => console.error("Audio play failed:", e));
|
try {
|
||||||
|
audioEffects.volcano.pause();
|
||||||
|
audioEffects.volcano.currentTime = 0;
|
||||||
|
} catch (e) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
audioEffects.volcano.hasPlayed = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -242,58 +255,83 @@ const RoomView = (props: RoomProps) => {
|
|||||||
}, [state, volume]);
|
}, [state, volume]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (turn && turn.color === color && state !== "room") {
|
// When audio is enabled we may create/play effects; when disabled ensure
|
||||||
if (!audioEffects.yourTurn) {
|
// any existing effects are stopped and reset.
|
||||||
audioEffects.yourTurn = loadAudio("its-your-turn.mp3");
|
if (audio) {
|
||||||
audioEffects.yourTurn.volume = volume * volume;
|
if (turn && turn.color === color && state !== "room") {
|
||||||
} else {
|
if (!audioEffects.yourTurn) {
|
||||||
if (!audioEffects.yourTurn.hasPlayed && audioEffects.yourTurn.readyState >= 2) {
|
audioEffects.yourTurn = loadAudio("its-your-turn.mp3");
|
||||||
audioEffects.yourTurn.hasPlayed = true;
|
audioEffects.yourTurn.volume = volume * volume;
|
||||||
audioEffects.yourTurn.play().catch((e) => console.error("Audio play failed:", e));
|
} else {
|
||||||
|
if (!audioEffects.yourTurn.hasPlayed && audioEffects.yourTurn.readyState >= 2) {
|
||||||
|
audioEffects.yourTurn.hasPlayed = true;
|
||||||
|
audioEffects.yourTurn.play().catch((e) => console.error("Audio play failed:", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (turn) {
|
||||||
|
if (audioEffects.yourTurn) {
|
||||||
|
audioEffects.yourTurn.hasPlayed = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (turn) {
|
|
||||||
if (audioEffects.yourTurn) {
|
|
||||||
audioEffects.yourTurn.hasPlayed = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (turn && turn.roll === 7) {
|
if (turn && turn.roll === 7) {
|
||||||
if (!audioEffects.robber) {
|
if (!audioEffects.robber) {
|
||||||
audioEffects.robber = loadAudio("robber.mp3");
|
audioEffects.robber = loadAudio("robber.mp3");
|
||||||
audioEffects.robber.volume = volume * volume;
|
audioEffects.robber.volume = volume * volume;
|
||||||
} else {
|
} else {
|
||||||
if (!audioEffects.robber.hasPlayed && audioEffects.robber.readyState >= 2) {
|
if (!audioEffects.robber.hasPlayed && audioEffects.robber.readyState >= 2) {
|
||||||
audioEffects.robber.hasPlayed = true;
|
audioEffects.robber.hasPlayed = true;
|
||||||
audioEffects.robber.play().catch((e) => console.error("Audio play failed:", e));
|
audioEffects.robber.play().catch((e) => console.error("Audio play failed:", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (turn) {
|
||||||
|
if (audioEffects.robber) {
|
||||||
|
audioEffects.robber.hasPlayed = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (turn) {
|
|
||||||
if (audioEffects.robber) {
|
|
||||||
audioEffects.robber.hasPlayed = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (turn && turn.actions && turn.actions.indexOf("playing-knight") !== -1) {
|
if (turn && turn.actions && turn.actions.indexOf("playing-knight") !== -1) {
|
||||||
if (!audioEffects.knights) {
|
if (!audioEffects.knights) {
|
||||||
audioEffects.knights = loadAudio("the-knights-who-say-ni.mp3");
|
audioEffects.knights = loadAudio("the-knights-who-say-ni.mp3");
|
||||||
audioEffects.knights.volume = volume * volume;
|
audioEffects.knights.volume = volume * volume;
|
||||||
} else {
|
} else {
|
||||||
if (!audioEffects.knights.hasPlayed && audioEffects.knights.readyState >= 2) {
|
if (!audioEffects.knights.hasPlayed && audioEffects.knights.readyState >= 2) {
|
||||||
audioEffects.knights.hasPlayed = true;
|
audioEffects.knights.hasPlayed = true;
|
||||||
audioEffects.knights.play().catch((e) => console.error("Audio play failed:", e));
|
audioEffects.knights.play().catch((e) => console.error("Audio play failed:", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (turn && turn.actions && turn.actions.indexOf("playing-knight") === -1) {
|
||||||
|
if (audioEffects.knights) {
|
||||||
|
audioEffects.knights.hasPlayed = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (turn && turn.actions && turn.actions.indexOf("playing-knight") === -1) {
|
} else {
|
||||||
if (audioEffects.knights) {
|
// audio disabled: stop any currently playing effects and reset their state
|
||||||
audioEffects.knights.hasPlayed = false;
|
const stopIfPlaying = (ae?: AudioEffect) => {
|
||||||
}
|
if (!ae) return;
|
||||||
|
try {
|
||||||
|
ae.pause();
|
||||||
|
ae.currentTime = 0;
|
||||||
|
} catch (e) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
ae.hasPlayed = false;
|
||||||
|
};
|
||||||
|
stopIfPlaying(audioEffects.yourTurn);
|
||||||
|
stopIfPlaying(audioEffects.robber);
|
||||||
|
stopIfPlaying(audioEffects.knights);
|
||||||
}
|
}
|
||||||
}, [state, turn, color, volume]);
|
}, [state, turn, color, volume]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
for (const key in audioEffects) {
|
for (const key in audioEffects) {
|
||||||
audioEffects[key].volume = volume * volume;
|
if (audioEffects[key]) {
|
||||||
|
try {
|
||||||
|
audioEffects[key]!.volume = volume * volume;
|
||||||
|
} catch (e) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [volume]);
|
}, [volume]);
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Trade .PlayerList {
|
.Trade .TradeList {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background-color:rgba(224, 224, 224);
|
background-color:rgba(224, 224, 224);
|
||||||
margin: 0.25rem 0;
|
margin: 0.25rem 0;
|
||||||
@ -188,7 +188,10 @@
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
padding: 0.25rem;
|
||||||
border-bottom: 1px solid #ccc;
|
border-bottom: 1px solid #ccc;
|
||||||
|
min-height: fit-content;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Trade .TradeLine:last-child {
|
.Trade .TradeLine:last-child {
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
import React, { useState, useCallback, useEffect, useContext, useMemo, useRef } from "react";
|
import React, { useState, useCallback, useEffect, useContext, useMemo, useRef } from 'react';
|
||||||
import equal from "fast-deep-equal";
|
import equal from 'fast-deep-equal';
|
||||||
|
|
||||||
import Paper from "@mui/material/Paper";
|
import Paper from '@mui/material/Paper';
|
||||||
import Button from "@mui/material/Button";
|
import Button from '@mui/material/Button';
|
||||||
import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
|
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
|
||||||
import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward";
|
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
|
||||||
|
|
||||||
import { Resource } from "./Resource";
|
import { Resource } from './Resource';
|
||||||
import { PlayerColor } from "./PlayerColor";
|
import { PlayerColor } from './PlayerColor';
|
||||||
import { GlobalContext } from "./GlobalContext";
|
import { GlobalContext } from './GlobalContext';
|
||||||
|
import { assetsPath } from './Common';
|
||||||
|
|
||||||
import "./Trade.css";
|
import './Trade.css';
|
||||||
|
import { Box } from '@mui/material';
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
|
|
||||||
|
|
||||||
interface Resources {
|
interface Resources {
|
||||||
wheat: number;
|
wheat: number;
|
||||||
@ -51,7 +51,33 @@ const Trade: React.FC = () => {
|
|||||||
const [players, setPlayers] = useState<any>(undefined);
|
const [players, setPlayers] = useState<any>(undefined);
|
||||||
const [color, setColor] = useState<string | undefined>(undefined);
|
const [color, setColor] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
const fields = useMemo(() => ["turn", "players", "private", "color"], []);
|
// Viewer context: either the player's private info or a read-only observer stub.
|
||||||
|
// Observers (no `private` data) will use a read-only view but should be able
|
||||||
|
// to see active offers. Use `observerViewMode` to adjust offer visibility rules.
|
||||||
|
const isPrivObserver = !priv || !priv.name;
|
||||||
|
const observerViewMode = isPrivObserver;
|
||||||
|
|
||||||
|
// Default color for observers until the full flow is implemented
|
||||||
|
const defaultObserverColor = observerViewMode ? 'R' : undefined;
|
||||||
|
|
||||||
|
const viewer: any = priv
|
||||||
|
? priv
|
||||||
|
: {
|
||||||
|
name: '',
|
||||||
|
color: undefined,
|
||||||
|
gives: [],
|
||||||
|
gets: [],
|
||||||
|
offerRejected: {},
|
||||||
|
banks: [],
|
||||||
|
resources: 0,
|
||||||
|
};
|
||||||
|
const isObserver = !viewer || !viewer.name;
|
||||||
|
|
||||||
|
// Prefer a viewer-specified color. If none, observers default to `defaultObserverColor`, otherwise fall back to server `color`.
|
||||||
|
const effectiveColor =
|
||||||
|
viewer && viewer.color ? viewer.color : defaultObserverColor ? defaultObserverColor : color;
|
||||||
|
|
||||||
|
const fields = useMemo(() => ['turn', 'players', 'private', 'color'], []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!lastJsonMessage) {
|
if (!lastJsonMessage) {
|
||||||
@ -59,18 +85,18 @@ const Trade: React.FC = () => {
|
|||||||
}
|
}
|
||||||
const data = lastJsonMessage;
|
const data = lastJsonMessage;
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case "game-update":
|
case 'game-update':
|
||||||
console.log(`trade - game-update: `, data.update);
|
console.log(`trade - game-update: `, data.update);
|
||||||
if ("turn" in data.update && !equal(turn, data.update.turn)) {
|
if ('turn' in data.update && !equal(turn, data.update.turn)) {
|
||||||
setTurn(data.update.turn);
|
setTurn(data.update.turn);
|
||||||
}
|
}
|
||||||
if ("players" in data.update && !equal(players, data.update.players)) {
|
if ('players' in data.update && !equal(players, data.update.players)) {
|
||||||
setPlayers(data.update.players);
|
setPlayers(data.update.players);
|
||||||
}
|
}
|
||||||
if ("private" in data.update && !equal(priv, data.update.private)) {
|
if ('private' in data.update && !equal(priv, data.update.private)) {
|
||||||
setPriv(data.update.private);
|
setPriv(data.update.private);
|
||||||
}
|
}
|
||||||
if ("color" in data.update && color !== data.update.color) {
|
if ('color' in data.update && color !== data.update.color) {
|
||||||
setColor(data.update.color);
|
setColor(data.update.color);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -84,25 +110,30 @@ const Trade: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sendJsonMessage({
|
sendJsonMessage({
|
||||||
type: "get",
|
type: 'get',
|
||||||
fields,
|
fields,
|
||||||
});
|
});
|
||||||
}, [sendJsonMessage, fields]);
|
}, [sendJsonMessage, fields]);
|
||||||
|
|
||||||
const transfer = useCallback(
|
const transfer = useCallback(
|
||||||
(type: string, direction: string) => {
|
(type: string, direction: string) => {
|
||||||
if (direction === "give") {
|
if (direction === 'give') {
|
||||||
/* give clicked */
|
/* give clicked */
|
||||||
if (gets[type as keyof Resources]) {
|
if (gets[type as keyof Resources]) {
|
||||||
gets[type as keyof Resources]--;
|
gets[type as keyof Resources]--;
|
||||||
gives[type as keyof Resources] = 0;
|
gives[type as keyof Resources] = 0;
|
||||||
} else {
|
} else {
|
||||||
if (gives[type as keyof Resources] < priv[type]) {
|
// Only allow incrementing gives if we have a priv viewer with that resource
|
||||||
|
if (
|
||||||
|
viewer &&
|
||||||
|
viewer[type] !== undefined &&
|
||||||
|
gives[type as keyof Resources] < viewer[type]
|
||||||
|
) {
|
||||||
gives[type as keyof Resources]++;
|
gives[type as keyof Resources]++;
|
||||||
}
|
}
|
||||||
gets[type as keyof Resources] = 0;
|
gets[type as keyof Resources] = 0;
|
||||||
}
|
}
|
||||||
} else if (direction === "get") {
|
} else if (direction === 'get') {
|
||||||
/* get clicked */
|
/* get clicked */
|
||||||
if (gives[type as keyof Resources]) {
|
if (gives[type as keyof Resources]) {
|
||||||
gives[type as keyof Resources]--;
|
gives[type as keyof Resources]--;
|
||||||
@ -118,7 +149,7 @@ const Trade: React.FC = () => {
|
|||||||
setGets({ ...gets });
|
setGets({ ...gets });
|
||||||
setGives({ ...gives });
|
setGives({ ...gives });
|
||||||
},
|
},
|
||||||
[setGets, setGives, gets, gives, priv]
|
[setGets, setGives, gets, gives, viewer]
|
||||||
);
|
);
|
||||||
|
|
||||||
const createTransfer = useCallback(
|
const createTransfer = useCallback(
|
||||||
@ -126,7 +157,7 @@ const Trade: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div key={resource} className="Transfer">
|
<div key={resource} className="Transfer">
|
||||||
<Resource
|
<Resource
|
||||||
onClick={() => transfer(resource, "get")}
|
onClick={() => transfer(resource, 'get')}
|
||||||
label={true}
|
label={true}
|
||||||
type={resource}
|
type={resource}
|
||||||
disabled
|
disabled
|
||||||
@ -134,7 +165,7 @@ const Trade: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
<div className="Direction">
|
<div className="Direction">
|
||||||
{gets[resource as keyof Resources] === gives[resource as keyof Resources] ? (
|
{gets[resource as keyof Resources] === gives[resource as keyof Resources] ? (
|
||||||
""
|
''
|
||||||
) : gets[resource as keyof Resources] > gives[resource as keyof Resources] ? (
|
) : gets[resource as keyof Resources] > gives[resource as keyof Resources] ? (
|
||||||
<ArrowDownwardIcon />
|
<ArrowDownwardIcon />
|
||||||
) : (
|
) : (
|
||||||
@ -142,24 +173,28 @@ const Trade: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Resource
|
<Resource
|
||||||
onClick={() => transfer(resource, "give")}
|
onClick={() => transfer(resource, 'give')}
|
||||||
label={true}
|
label={true}
|
||||||
type={resource}
|
type={resource}
|
||||||
disabled
|
disabled
|
||||||
available={priv ? priv[resource] - gives[resource as keyof Resources] : undefined}
|
available={
|
||||||
|
viewer && viewer[resource] !== undefined
|
||||||
|
? viewer[resource] - gives[resource as keyof Resources]
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
count={gives[resource as keyof Resources]}
|
count={gives[resource as keyof Resources]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[gives, gets, transfer, priv]
|
[gives, gets, transfer, viewer]
|
||||||
);
|
);
|
||||||
|
|
||||||
const sendTrade = useCallback(
|
const sendTrade = useCallback(
|
||||||
(action: string, offer: any) => {
|
(action: string, offer: any) => {
|
||||||
if (sendJsonMessage) {
|
if (sendJsonMessage) {
|
||||||
sendJsonMessage({
|
sendJsonMessage({
|
||||||
type: "trade",
|
type: 'trade',
|
||||||
action,
|
action,
|
||||||
offer,
|
offer,
|
||||||
});
|
});
|
||||||
@ -192,7 +227,7 @@ const Trade: React.FC = () => {
|
|||||||
console.log(gives, gets);
|
console.log(gives, gets);
|
||||||
trade.gives.forEach((give: any) => (_gives[give.type as keyof Resources] = give.count));
|
trade.gives.forEach((give: any) => (_gives[give.type as keyof Resources] = give.count));
|
||||||
trade.gets.forEach((get: any) => (_gets[get.type as keyof Resources] = get.count));
|
trade.gets.forEach((get: any) => (_gets[get.type as keyof Resources] = get.count));
|
||||||
sendTrade("offer", trade);
|
sendTrade('offer', trade);
|
||||||
console.log(_gives, _gets);
|
console.log(_gives, _gets);
|
||||||
setGives(Object.assign({}, empty, _gives));
|
setGives(Object.assign({}, empty, _gives));
|
||||||
setGets(Object.assign({}, empty, _gets));
|
setGets(Object.assign({}, empty, _gets));
|
||||||
@ -200,15 +235,16 @@ const Trade: React.FC = () => {
|
|||||||
[setGives, setGets, gives, gets, sendTrade]
|
[setGives, setGets, gives, gets, sendTrade]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!priv || !turn || !turn.actions || turn.actions.indexOf("trade") === -1) {
|
// Allow observers (no `private` data) to view trades in read-only mode.
|
||||||
|
if (!turn || !turn.actions || turn.actions.indexOf('trade') === -1) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const transfers = ["brick", "wood", "wheat", "sheep", "stone"].map((resource) => {
|
const transfers = ['brick', 'wood', 'wheat', 'sheep', 'stone'].map(resource => {
|
||||||
return createTransfer(resource);
|
return createTransfer(resource);
|
||||||
});
|
});
|
||||||
|
|
||||||
priv.offerRejected = priv.offerRejected ? priv.offerRejected : {};
|
const viewerOfferRejected = viewer.offerRejected ? viewer.offerRejected : {};
|
||||||
|
|
||||||
const canMeetOffer = (player: any, offer: any) => {
|
const canMeetOffer = (player: any, offer: any) => {
|
||||||
if (offer.gets.length === 0 || offer.gives.length === 0) {
|
if (offer.gets.length === 0 || offer.gives.length === 0) {
|
||||||
@ -216,7 +252,7 @@ const Trade: React.FC = () => {
|
|||||||
}
|
}
|
||||||
for (let i = 0; i < offer.gets.length; i++) {
|
for (let i = 0; i < offer.gets.length; i++) {
|
||||||
const get = offer.gets[i];
|
const get = offer.gets[i];
|
||||||
if (offer.name === "The bank") {
|
if (offer.name === 'The bank') {
|
||||||
const _gives: any[] = [],
|
const _gives: any[] = [],
|
||||||
_gets: any[] = [];
|
_gets: any[] = [];
|
||||||
for (const type in gives) {
|
for (const type in gives) {
|
||||||
@ -238,7 +274,7 @@ const Trade: React.FC = () => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (get.type !== "bank") {
|
if (get.type !== 'bank') {
|
||||||
if (gives[get.type as keyof Resources] < get.count) {
|
if (gives[get.type as keyof Resources] < get.count) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -273,8 +309,9 @@ const Trade: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
valid =
|
valid =
|
||||||
offer.gives.find((item: any) => (item.type === get.type || item.type === "*") && item.count === get.count) !==
|
offer.gives.find(
|
||||||
undefined;
|
(item: any) => (item.type === get.type || item.type === '*') && item.count === get.count
|
||||||
|
) !== undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (valid)
|
if (valid)
|
||||||
@ -284,13 +321,14 @@ const Trade: React.FC = () => {
|
|||||||
}
|
}
|
||||||
valid =
|
valid =
|
||||||
offer.gets.find(
|
offer.gets.find(
|
||||||
(item: any) => (item.type === give.type || item.type === "bank") && item.count === give.count
|
(item: any) =>
|
||||||
|
(item.type === give.type || item.type === 'bank') && item.count === give.count
|
||||||
) !== undefined;
|
) !== undefined;
|
||||||
});
|
});
|
||||||
return valid;
|
return valid;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isTurn = turn && turn.color === color ? true : false;
|
const isTurn = turn && turn.color === effectiveColor ? true : false;
|
||||||
|
|
||||||
const offerClicked = () => {
|
const offerClicked = () => {
|
||||||
const trade = {
|
const trade = {
|
||||||
@ -307,20 +345,23 @@ const Trade: React.FC = () => {
|
|||||||
trade.gets.push({ type: key, count: gets[key as keyof Resources] });
|
trade.gets.push({ type: key, count: gets[key as keyof Resources] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sendTrade("offer", trade);
|
sendTrade('offer', trade);
|
||||||
};
|
};
|
||||||
|
|
||||||
const cancelOffer = (offer: any) => {
|
const cancelOffer = (offer: any) => {
|
||||||
sendTrade("cancel", offer);
|
sendTrade('cancel', offer);
|
||||||
};
|
};
|
||||||
|
|
||||||
const acceptClicked = (offer: any) => {
|
const acceptClicked = (offer: any) => {
|
||||||
if (offer.name === "The bank") {
|
if (offer.name === 'The bank') {
|
||||||
sendTrade("accept", Object.assign({}, { name: offer.name, gives: trade.gets, gets: trade.gives }));
|
sendTrade(
|
||||||
|
'accept',
|
||||||
|
Object.assign({}, { name: offer.name, gives: trade.gets, gets: trade.gives })
|
||||||
|
);
|
||||||
} else if (offer.self) {
|
} else if (offer.self) {
|
||||||
sendTrade("accept", offer);
|
sendTrade('accept', offer);
|
||||||
} else {
|
} else {
|
||||||
sendTrade("accept", Object.assign({}, offer, { gives: offer.gets, gets: offer.gives }));
|
sendTrade('accept', Object.assign({}, offer, { gives: offer.gets, gets: offer.gives }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -329,16 +370,18 @@ const Trade: React.FC = () => {
|
|||||||
/* Player has rejected the active player's bid or active player rejected
|
/* Player has rejected the active player's bid or active player rejected
|
||||||
* the other player's bid */
|
* the other player's bid */
|
||||||
const rejectClicked = (trade: any) => {
|
const rejectClicked = (trade: any) => {
|
||||||
sendTrade("reject", trade);
|
sendTrade('reject', trade);
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Create list of active trades */
|
/* Create list of active trades */
|
||||||
const activeTrades: any[] = [];
|
const activeTrades: any[] = [];
|
||||||
|
console.log('trade - building activeTrades', { turn, players, viewer, color });
|
||||||
for (const colorKey in players) {
|
for (const colorKey in players) {
|
||||||
const item = players[colorKey],
|
const item = players[colorKey],
|
||||||
name = item.name;
|
name = item.name;
|
||||||
|
console.log(`trade - processing player ${colorKey}`, { item, name, colorKey });
|
||||||
item.offerRejected = item.offerRejected ? item.offerRejected : {};
|
item.offerRejected = item.offerRejected ? item.offerRejected : {};
|
||||||
if (item.status !== "Active") {
|
if (item.status !== 'Active') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
/* Only list players with an offer, unless it is the active player (see
|
/* Only list players with an offer, unless it is the active player (see
|
||||||
@ -346,8 +389,8 @@ const Trade: React.FC = () => {
|
|||||||
* or the player explicitly rejected the player's offer */
|
* or the player explicitly rejected the player's offer */
|
||||||
if (
|
if (
|
||||||
turn.name !== name &&
|
turn.name !== name &&
|
||||||
priv.name !== name &&
|
viewer.name !== name &&
|
||||||
!(colorKey in priv.offerRejected) &&
|
!(colorKey in (viewer.offerRejected || {})) &&
|
||||||
(!item.gets || item.gets.length === 0 || !item.gives || item.gives.length === 0)
|
(!item.gets || item.gets.length === 0 || !item.gives || item.gives.length === 0)
|
||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
@ -355,7 +398,7 @@ const Trade: React.FC = () => {
|
|||||||
|
|
||||||
const tmp: TradeItem = {
|
const tmp: TradeItem = {
|
||||||
negotiator: turn.name === name,
|
negotiator: turn.name === name,
|
||||||
self: priv.name === name,
|
self: viewer.name === name,
|
||||||
name: name,
|
name: name,
|
||||||
color: colorKey,
|
color: colorKey,
|
||||||
valid: false,
|
valid: false,
|
||||||
@ -363,6 +406,24 @@ const Trade: React.FC = () => {
|
|||||||
gives: item.gives ? item.gives : [],
|
gives: item.gives ? item.gives : [],
|
||||||
offerRejected: item.offerRejected,
|
offerRejected: item.offerRejected,
|
||||||
};
|
};
|
||||||
|
console.log('trade - tmp before fallback', { tmp });
|
||||||
|
|
||||||
|
// If this is the active (negotiating) player but their gets/gives
|
||||||
|
// are not present in the players map (e.g. observers where private
|
||||||
|
// data isn't available), fall back to the offer on `turn` so the
|
||||||
|
// trade line shows the active player's current offer.
|
||||||
|
if (tmp.negotiator && turn && turn.offer) {
|
||||||
|
console.log('trade - negotiator detected; turn.offer present', { turnOffer: turn.offer });
|
||||||
|
if ((!tmp.gets || tmp.gets.length === 0) && turn.offer.gets) {
|
||||||
|
console.log('trade - applying turn.offer.gets fallback', { gets: turn.offer.gets });
|
||||||
|
tmp.gets = turn.offer.gets.slice();
|
||||||
|
}
|
||||||
|
if ((!tmp.gives || tmp.gives.length === 0) && turn.offer.gives) {
|
||||||
|
console.log('trade - applying turn.offer.gives fallback', { gives: turn.offer.gives });
|
||||||
|
tmp.gives = turn.offer.gives.slice();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('trade - tmp after fallback', { tmp });
|
||||||
|
|
||||||
tmp.canSubmit = !!(tmp.gets.length && tmp.gives.length);
|
tmp.canSubmit = !!(tmp.gets.length && tmp.gives.length);
|
||||||
|
|
||||||
@ -397,17 +458,17 @@ const Trade: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isOfferSubmitted = isCompatibleOffer(priv, trade),
|
const isOfferSubmitted = isCompatibleOffer(viewer, trade),
|
||||||
isNegiatorSubmitted = turn && turn.offer && isCompatibleOffer(priv, turn.offer),
|
isNegiatorSubmitted = turn && turn.offer && isCompatibleOffer(viewer, turn.offer),
|
||||||
isOfferValid = trade.gives.length && trade.gets.length ? true : false;
|
isOfferValid = trade.gives.length && trade.gets.length ? true : false;
|
||||||
|
|
||||||
if (isTurn && priv && priv.banks) {
|
if (isTurn && viewer && viewer.banks) {
|
||||||
priv.banks.forEach((bank: string) => {
|
viewer.banks.forEach((bank: string) => {
|
||||||
const count = bank === "bank" ? 3 : 2;
|
const count = bank === 'bank' ? 3 : 2;
|
||||||
activeTrades.push({
|
activeTrades.push({
|
||||||
name: `The bank`,
|
name: `The bank`,
|
||||||
color: undefined,
|
color: undefined,
|
||||||
gives: [{ count: 1, type: "*" }],
|
gives: [{ count: 1, type: '*' }],
|
||||||
gets: [{ count: count, type: bank }],
|
gets: [{ count: count, type: bank }],
|
||||||
valid: false,
|
valid: false,
|
||||||
offerRejected: {},
|
offerRejected: {},
|
||||||
@ -417,8 +478,8 @@ const Trade: React.FC = () => {
|
|||||||
activeTrades.push({
|
activeTrades.push({
|
||||||
name: `The bank`,
|
name: `The bank`,
|
||||||
color: undefined,
|
color: undefined,
|
||||||
gives: [{ count: 1, type: "*" }],
|
gives: [{ count: 1, type: '*' }],
|
||||||
gets: [{ count: 4, type: "bank" }],
|
gets: [{ count: 4, type: 'bank' }],
|
||||||
valid: false,
|
valid: false,
|
||||||
offerRejected: {},
|
offerRejected: {},
|
||||||
});
|
});
|
||||||
@ -426,38 +487,45 @@ const Trade: React.FC = () => {
|
|||||||
|
|
||||||
if (isTurn) {
|
if (isTurn) {
|
||||||
activeTrades.forEach((offer: any) => {
|
activeTrades.forEach((offer: any) => {
|
||||||
if (offer.name === "The bank") {
|
if (offer.name === 'The bank') {
|
||||||
/* offer has to be the second parameter for the bank to match */
|
/* offer has to be the second parameter for the bank to match */
|
||||||
offer.valid = isCompatibleOffer({ gives: trade.gets, gets: trade.gives }, offer);
|
offer.valid = isCompatibleOffer({ gives: trade.gets, gets: trade.gives }, offer);
|
||||||
} else {
|
} else {
|
||||||
offer.valid = !(turn.color in offer.offerRejected) && canMeetOffer(priv, offer);
|
// For observers, ignore the offerRejected map so the read-only viewer can
|
||||||
|
// see the active player's offer details.
|
||||||
|
offer.valid =
|
||||||
|
(observerViewMode ? true : !(turn.color in offer.offerRejected)) &&
|
||||||
|
canMeetOffer(viewer, offer);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const found = activeTrades.find((item: any) => item.name === turn.name);
|
const found = activeTrades.find((item: any) => item.name === turn.name);
|
||||||
if (found) {
|
if (found) {
|
||||||
found.valid = !(color! in found.offerRejected) && canMeetOffer(priv, found);
|
found.valid =
|
||||||
|
(observerViewMode ? true : !(effectiveColor! in found.offerRejected)) &&
|
||||||
|
canMeetOffer(viewer, found);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const tradeElements = activeTrades.map((item: any, index: number) => {
|
const tradeElements = activeTrades.map((item: any, index: number) => {
|
||||||
const youRejectedOffer = color! in item.offerRejected;
|
console.log(`trade - rendering trade element ${index}`, { item, index });
|
||||||
|
const youRejectedOffer = observerViewMode ? false : effectiveColor! in item.offerRejected;
|
||||||
let youWereRejected;
|
let youWereRejected;
|
||||||
if (isTurn) {
|
if (isTurn) {
|
||||||
youWereRejected = item.color && item.color in priv.offerRejected;
|
youWereRejected = item.color && item.color in (viewer.offerRejected || {});
|
||||||
} else {
|
} else {
|
||||||
youWereRejected = Object.getOwnPropertyNames(priv.offerRejected).length !== 0;
|
youWereRejected = Object.getOwnPropertyNames(viewer.offerRejected || {}).length !== 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isNewOffer = item.self && !isOfferSubmitted;
|
const isNewOffer = item.self && !isOfferSubmitted;
|
||||||
|
|
||||||
let isSameOffer;
|
let isSameOffer;
|
||||||
const isBank = item.name === "The bank";
|
const isBank = item.name === 'The bank';
|
||||||
|
|
||||||
if (isTurn) {
|
if (isTurn) {
|
||||||
isSameOffer = isCompatibleOffer(trade, { gets: item.gives, gives: item.gets });
|
isSameOffer = isCompatibleOffer(trade, { gets: item.gives, gives: item.gets });
|
||||||
} else {
|
} else {
|
||||||
isSameOffer = turn.offer && isCompatibleOffer(priv, turn.offer);
|
isSameOffer = turn.offer && isCompatibleOffer(viewer, turn.offer);
|
||||||
}
|
}
|
||||||
|
|
||||||
let source;
|
let source;
|
||||||
@ -472,46 +540,84 @@ const Trade: React.FC = () => {
|
|||||||
} else {
|
} else {
|
||||||
source = item;
|
source = item;
|
||||||
}
|
}
|
||||||
const _gets = source.gets.length
|
|
||||||
|
// Ensure arrays exist so .length checks are safe
|
||||||
|
source.gets = source.gets || [];
|
||||||
|
source.gives = source.gives || [];
|
||||||
|
|
||||||
|
// Build display elements and also capture snapshots for logging
|
||||||
|
const computed_gets = source.gets.length
|
||||||
? source.gets.map((get: any, index: number) => {
|
? source.gets.map((get: any, index: number) => {
|
||||||
if (get.type === "bank") {
|
if (get.type === 'bank') {
|
||||||
return (
|
return (
|
||||||
<span key={`get-bank-${index}`}>
|
<span key={`get-bank-${index}`}>
|
||||||
<b>{get.count}</b> of any resource{" "}
|
<b>{get.count}</b> of any resource{' '}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <Resource key={`get-${get.type}-${index}`} disabled label type={get.type} count={get.count} />;
|
return (
|
||||||
|
<Resource
|
||||||
|
key={`get-${get.type}-${index}`}
|
||||||
|
disabled
|
||||||
|
label
|
||||||
|
type={get.type}
|
||||||
|
count={get.count}
|
||||||
|
/>
|
||||||
|
);
|
||||||
})
|
})
|
||||||
: "nothing";
|
: 'nothing';
|
||||||
const _gives = source.gives.length
|
|
||||||
|
const computed_gives = source.gives.length
|
||||||
? source.gives.map((give: any, index: number) => {
|
? source.gives.map((give: any, index: number) => {
|
||||||
if (give.type === "*") {
|
if (give.type === '*') {
|
||||||
return (
|
return (
|
||||||
<span key={`give-bank-${index}`}>
|
<span key={`give-bank-${index}`}>
|
||||||
<b>1</b> of any resource{" "}
|
<b>1</b> of any resource{' '}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <Resource key={`give-${give.type}-${index}`} disabled label type={give.type} count={give.count} />;
|
return (
|
||||||
|
<Resource
|
||||||
|
key={`give-${give.type}-${index}`}
|
||||||
|
disabled
|
||||||
|
label
|
||||||
|
type={give.type}
|
||||||
|
count={give.count}
|
||||||
|
/>
|
||||||
|
);
|
||||||
})
|
})
|
||||||
: "nothing";
|
: 'nothing';
|
||||||
|
|
||||||
|
// Log JSON snapshots so the console shows values at log time (not live objects)
|
||||||
|
try {
|
||||||
|
console.log(`trade - computed for render ${index}`, {
|
||||||
|
item: JSON.parse(JSON.stringify(item)),
|
||||||
|
source: JSON.parse(JSON.stringify(source)),
|
||||||
|
computed_gets: source.gets.length ? JSON.parse(JSON.stringify(source.gets)) : 'nothing',
|
||||||
|
computed_gives: source.gives.length ? JSON.parse(JSON.stringify(source.gives)) : 'nothing',
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// Fallback if stringify fails
|
||||||
|
console.log(`trade - computed for render ${index}`, { item, source });
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="TradeLine" key={`player-${item.name}-${index}`}>
|
<Box className="TradeLine" key={`player-${item.name}-${index}`}>
|
||||||
<PlayerColor color={item.color} />
|
<PlayerColor color={item.color} />
|
||||||
<div className="TradeText">
|
<div className="TradeText">
|
||||||
{item.self && (
|
{item.self && !isObserver && (
|
||||||
<>
|
<>
|
||||||
{(_gets !== "nothing" || _gives !== "nothing") && (
|
{(computed_gets !== 'nothing' || computed_gives !== 'nothing') && (
|
||||||
<span>
|
<span>
|
||||||
You want {_gets} and will give {_gives}.
|
You want {computed_gets} and will give {computed_gives}.
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{youWereRejected && !isNewOffer && <span>{turn.name} rejected your offer.</span>}
|
{youWereRejected && !isNewOffer && !isObserver && (
|
||||||
|
<span>{turn.name} rejected your offer.</span>
|
||||||
|
)}
|
||||||
|
|
||||||
{!youWereRejected && _gets === "nothing" && _gives === "nothing" && (
|
{!youWereRejected && computed_gets === 'nothing' && computed_gives === 'nothing' && (
|
||||||
<span>You have not made a trade offer.</span>
|
<span>You have not made a trade offer.</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -519,9 +625,11 @@ const Trade: React.FC = () => {
|
|||||||
isSameOffer &&
|
isSameOffer &&
|
||||||
!youWereRejected &&
|
!youWereRejected &&
|
||||||
isOfferValid &&
|
isOfferValid &&
|
||||||
_gets !== "nothing" &&
|
computed_gets !== 'nothing' &&
|
||||||
_gives !== "nothing" && (
|
computed_gives !== 'nothing' && (
|
||||||
<span style={{ fontWeight: "bold" }}>Your submitted offer agrees with {turn.name}'s terms.</span>
|
<span style={{ fontWeight: 'bold' }}>
|
||||||
|
Your submitted offer agrees with {turn.name}'s terms.
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -530,10 +638,10 @@ const Trade: React.FC = () => {
|
|||||||
<>
|
<>
|
||||||
{(!isTurn || !isSameOffer || isBank) &&
|
{(!isTurn || !isSameOffer || isBank) &&
|
||||||
!youRejectedOffer &&
|
!youRejectedOffer &&
|
||||||
_gets !== "nothing" &&
|
computed_gets !== 'nothing' &&
|
||||||
_gives !== "nothing" && (
|
computed_gives !== 'nothing' && (
|
||||||
<span>
|
<span>
|
||||||
{item.name} wants {_gets} and will give {_gives}.
|
{item.name} wants {computed_gets} and will give {computed_gives}.
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -543,20 +651,29 @@ const Trade: React.FC = () => {
|
|||||||
!isSameOffer &&
|
!isSameOffer &&
|
||||||
isOfferValid &&
|
isOfferValid &&
|
||||||
!youRejectedOffer &&
|
!youRejectedOffer &&
|
||||||
_gets !== "nothing" &&
|
computed_gets !== 'nothing' &&
|
||||||
_gives !== "nothing" && <span style={{ fontWeight: "bold" }}>This is a counter offer.</span>}
|
computed_gives !== 'nothing' && (
|
||||||
|
<span style={{ fontWeight: 'bold' }}>This is a counter offer.</span>
|
||||||
|
)}
|
||||||
|
|
||||||
{isTurn && isSameOffer && !youRejectedOffer && _gets !== "nothing" && _gives !== "nothing" && (
|
{isTurn &&
|
||||||
<span>{item.name} will meet your terms.</span>
|
isSameOffer &&
|
||||||
|
!youRejectedOffer &&
|
||||||
|
computed_gets !== 'nothing' &&
|
||||||
|
computed_gives !== 'nothing' && <span>{item.name} will meet your terms.</span>}
|
||||||
|
|
||||||
|
{(!isTurn || !youWereRejected) &&
|
||||||
|
(computed_gets === 'nothing' || computed_gives === 'nothing') && (
|
||||||
|
<span>{item.name} has not submitted a trade offer.</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{youRejectedOffer && !isObserver && (
|
||||||
|
<span>You rejected {item.name}'s offer.</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(!isTurn || !youWereRejected) && (_gets === "nothing" || _gives === "nothing") && (
|
{isTurn && youWereRejected && !isObserver && (
|
||||||
<span>{item.name} has not submitted a trade offer.</span>
|
<span>{item.name} rejected your offer.</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{youRejectedOffer && <span>You rejected {item.name}'s offer.</span>}
|
|
||||||
|
|
||||||
{isTurn && youWereRejected && <span>{item.name} rejected your offer.</span>}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@ -570,45 +687,55 @@ const Trade: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!isTurn && item.color === turn.color && (
|
{!isTurn && item.color === turn.color && (
|
||||||
<Button disabled={!item.valid || isNegiatorSubmitted} onClick={() => agreeClicked(item)}>
|
<Button
|
||||||
|
disabled={!item.valid || isNegiatorSubmitted || observerViewMode}
|
||||||
|
onClick={() => agreeClicked(item)}
|
||||||
|
>
|
||||||
agree
|
agree
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{item.name !== "The bank" && !item.self && (isTurn || item.name === turn.name) && (
|
{item.name !== 'The bank' && !item.self && (isTurn || item.name === turn.name) && (
|
||||||
<Button
|
<Button
|
||||||
disabled={!item.gets.length || !item.gives.length || youRejectedOffer}
|
disabled={
|
||||||
|
!item.gets.length || !item.gives.length || youRejectedOffer || observerViewMode
|
||||||
|
}
|
||||||
onClick={() => rejectClicked(item)}
|
onClick={() => rejectClicked(item)}
|
||||||
>
|
>
|
||||||
reject
|
reject
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{item.self && (
|
{item.self && !isObserver && (
|
||||||
<Button disabled={isOfferSubmitted || !isOfferValid} onClick={offerClicked}>
|
<Button disabled={isOfferSubmitted || !isOfferValid} onClick={offerClicked}>
|
||||||
Offer
|
Offer
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{item.self && (
|
{item.self && !isObserver && (
|
||||||
<Button disabled onClick={() => cancelOffer(item)}>
|
<Button disabled onClick={() => cancelOffer(item)}>
|
||||||
cancel
|
cancel
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Box>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper className="Trade">
|
<Paper className="Trade">
|
||||||
<div className="PlayerList">{tradeElements}</div>
|
<div className="TradeList">{tradeElements}</div>
|
||||||
{priv.resources === 0 && (
|
{!priv && (
|
||||||
|
<div style={{ padding: '0.5rem' }}>
|
||||||
|
<b>Read-only: observers can view offers but cannot participate in trades.</b>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{priv && priv.resources === 0 && (
|
||||||
<div>
|
<div>
|
||||||
<b>You have no resources to participate in this trade.</b>
|
<b>You have no resources to participate in this trade.</b>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{priv.resources !== 0 && (
|
{priv && priv.resources !== 0 && (
|
||||||
<div className="Transfers">
|
<div className="Transfers">
|
||||||
<div className="GiveGet">
|
<div className="GiveGet">
|
||||||
<div>Get</div>
|
<div>Get</div>
|
||||||
|
@ -20,7 +20,14 @@ const Winner: React.FC<WinnerProps> = ({ winnerDismissed, setWinnerDismissed })
|
|||||||
const { lastJsonMessage, sendJsonMessage } = useContext(GlobalContext);
|
const { lastJsonMessage, sendJsonMessage } = useContext(GlobalContext);
|
||||||
const [winner, setWinner] = useState<any>(undefined);
|
const [winner, setWinner] = useState<any>(undefined);
|
||||||
const [state, setState] = useState<string | undefined>(undefined);
|
const [state, setState] = useState<string | undefined>(undefined);
|
||||||
const fields = useMemo(() => ["winner", "state"], []);
|
// Track the client's color so we can tell if the current user is an observer
|
||||||
|
// (observers don't have a color assigned). Observers should remain on the
|
||||||
|
// winner screen even if the server advances the global state to `lobby`.
|
||||||
|
const [color, setColor] = useState<string | undefined>(undefined);
|
||||||
|
// Include color/private so this component can determine whether the local
|
||||||
|
// session is an observer. We purposely request these minimal fields here
|
||||||
|
// to avoid coupling Winner to higher-level RoomView state handling.
|
||||||
|
const fields = useMemo(() => ["winner", "state", "color", "private"], []);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!lastJsonMessage) {
|
if (!lastJsonMessage) {
|
||||||
return;
|
return;
|
||||||
@ -33,11 +40,39 @@ const Winner: React.FC<WinnerProps> = ({ winnerDismissed, setWinnerDismissed })
|
|||||||
if ("winner" in data.update && !equal(data.update.winner, winner)) {
|
if ("winner" in data.update && !equal(data.update.winner, winner)) {
|
||||||
setWinner(data.update.winner);
|
setWinner(data.update.winner);
|
||||||
}
|
}
|
||||||
|
// Keep track of our client color so we can special-case observer behavior
|
||||||
|
// (observers have no color). This allows an observer to stay on the
|
||||||
|
// winner screen even when the server moves the game back to the lobby.
|
||||||
|
if ("color" in data.update && data.update.color !== color) {
|
||||||
|
setColor(data.update.color);
|
||||||
|
}
|
||||||
if ("state" in data.update && data.update.state !== state) {
|
if ("state" in data.update && data.update.state !== state) {
|
||||||
|
// If the server leaves the winner state we normally clear the
|
||||||
|
// winner data. However, if the local user is an observer (no
|
||||||
|
// `color`) and they were viewing the winner screen, we keep the
|
||||||
|
// winner visible until they explicitly click "Go back to Lobby".
|
||||||
|
// This lets observers continue to view final game stats and the
|
||||||
|
// chat even as the server advances global state to `lobby`.
|
||||||
|
const isObserver = typeof color === "undefined" || color === null;
|
||||||
|
const leavingWinner = state === "winner" && data.update.state !== "winner";
|
||||||
|
|
||||||
if (data.update.state !== "winner") {
|
if (data.update.state !== "winner") {
|
||||||
setWinner(undefined);
|
if (isObserver && leavingWinner) {
|
||||||
|
// OBSERVER SPECIAL CASE: don't clear `winner` here. Still update
|
||||||
|
// the internal `state` so other parts of the UI know the server
|
||||||
|
// state changed, but keep rendering the winner panel for the
|
||||||
|
// observer until they click the button.
|
||||||
|
} else {
|
||||||
|
setWinner(undefined);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setWinnerDismissed(false);
|
|
||||||
|
// Only reset the dismissed flag when the server re-enters winner
|
||||||
|
// (otherwise preserve user's dismissal choice).
|
||||||
|
if (data.update.state === "winner") {
|
||||||
|
setWinnerDismissed(false);
|
||||||
|
}
|
||||||
|
|
||||||
setState(data.update.state);
|
setState(data.update.state);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -58,8 +93,17 @@ const Winner: React.FC<WinnerProps> = ({ winnerDismissed, setWinnerDismissed })
|
|||||||
const quitClicked = useCallback(() => {
|
const quitClicked = useCallback(() => {
|
||||||
if (!winnerDismissed) {
|
if (!winnerDismissed) {
|
||||||
setWinnerDismissed(true);
|
setWinnerDismissed(true);
|
||||||
|
// Tell server we want to go back to the lobby. Also immediately
|
||||||
|
// request the latest room/game fields so the client receives the
|
||||||
|
// updated lobby/game state and can continue observing the current
|
||||||
|
// game id. This helps observers remain connected to the same game
|
||||||
|
// record after the server transitions them.
|
||||||
|
sendJsonMessage({ type: "goto-lobby" });
|
||||||
sendJsonMessage({
|
sendJsonMessage({
|
||||||
type: "goto-lobby",
|
type: "get",
|
||||||
|
// Request a conservative set of fields required to continue
|
||||||
|
// observing the current game/lobby state.
|
||||||
|
fields: ["id", "state", "color", "private", "players"],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [sendJsonMessage, winnerDismissed, setWinnerDismissed]);
|
}, [sendJsonMessage, winnerDismissed, setWinnerDismissed]);
|
||||||
|
943
server/ai/app.ts
943
server/ai/app.ts
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "peddlers-of-ketran-ai-bot",
|
"name": "peddlers-of-ketran-ai-bot",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "app.js",
|
"main": "app.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "export $(cat ../../.env | xargs) && node app.js"
|
"start": "node start.js",
|
||||||
|
"start:ts": "export $(cat ../../.env | xargs) && node -r ts-node/register app.ts"
|
||||||
},
|
},
|
||||||
"author": "James Ketrenos <james_settlers@ketrenos.com>",
|
"author": "James Ketrenos <james_settlers@ketrenos.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
28
server/ai/start.js
Normal file
28
server/ai/start.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Load env from ../../.env (best-effort)
|
||||||
|
try {
|
||||||
|
const envPath = path.resolve(__dirname, '..', '..', '.env');
|
||||||
|
if (fs.existsSync(envPath)) {
|
||||||
|
const content = fs.readFileSync(envPath, 'utf8');
|
||||||
|
content.split(/\n/).forEach(line => {
|
||||||
|
const m = line.match(/^([^#=]+)=([\s\S]*)$/);
|
||||||
|
if (m) process.env[m[1].trim()] = m[2].trim();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Prefer ts-node/register to allow requiring .ts directly
|
||||||
|
require.resolve('ts-node/register');
|
||||||
|
require('ts-node/register');
|
||||||
|
require('./app.ts');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('ts-node not found. Please run this inside the project container where dev dependencies are installed.');
|
||||||
|
console.error('Original error:', err && err.message ? err.message : err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
@ -2342,7 +2342,15 @@ const pass = (game: Game, session: Session): string | undefined => {
|
|||||||
if (!next || !next.player) {
|
if (!next || !next.player) {
|
||||||
return `Unable to find the next player to pass to.`;
|
return `Unable to find the next player to pass to.`;
|
||||||
}
|
}
|
||||||
session.player.totalTime += Date.now() - session.player.turnStart;
|
// Only accumulate totalTime if turnStart is a valid timestamp
|
||||||
|
if (session.player.turnStart && typeof session.player.turnStart === 'number' && session.player.turnStart > 0) {
|
||||||
|
const delta = Date.now() - session.player.turnStart;
|
||||||
|
if (!isNaN(delta) && delta > 0) {
|
||||||
|
session.player.totalTime += delta;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`${session.short}: pass() called but player.turnStart is not set; skipping time accumulation.`);
|
||||||
|
}
|
||||||
session.player.turnNotice = '';
|
session.player.turnNotice = '';
|
||||||
game.turn = newTurn(next.player);
|
game.turn = newTurn(next.player);
|
||||||
next.player.turnStart = Date.now();
|
next.player.turnStart = Date.now();
|
||||||
@ -3099,7 +3107,17 @@ const placeRoad = (game: Game, session: Session, index: number): string | undefi
|
|||||||
}
|
}
|
||||||
|
|
||||||
addChatMessage(game, null, `Everyone has placed their two settlements!`);
|
addChatMessage(game, null, `Everyone has placed their two settlements!`);
|
||||||
|
// Ensure the first player's turn timer and turnStart are initialized
|
||||||
|
if (game.turn && game.turn.color) {
|
||||||
|
const firstSession = sessionFromColor(game, game.turn.color);
|
||||||
|
if (firstSession && firstSession.player) {
|
||||||
|
// initialize turnStart if not already set
|
||||||
|
if (!firstSession.player.turnStart || firstSession.player.turnStart === 0) {
|
||||||
|
firstSession.player.turnStart = Date.now();
|
||||||
|
}
|
||||||
|
startTurnTimer(game, firstSession);
|
||||||
|
}
|
||||||
|
}
|
||||||
/* Figure out which players received which resources for their
|
/* Figure out which players received which resources for their
|
||||||
* initial (second) settlement placement. This mirrors the original
|
* initial (second) settlement placement. This mirrors the original
|
||||||
* behaviour where the player receives resources adjacent to their
|
* behaviour where the player receives resources adjacent to their
|
||||||
|
20
server/routes/games/turnFactory.ts
Normal file
20
server/routes/games/turnFactory.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Player, Turn } from "./types";
|
||||||
|
|
||||||
|
export const newTurn = (first: Player) : Turn => {
|
||||||
|
return {
|
||||||
|
name: first.name,
|
||||||
|
color: first.color,
|
||||||
|
actions: [],
|
||||||
|
limits: {},
|
||||||
|
roll: 0,
|
||||||
|
volcano: null,
|
||||||
|
free: false,
|
||||||
|
freeRoads: 0,
|
||||||
|
select: {},
|
||||||
|
active: null,
|
||||||
|
robberInAction: false,
|
||||||
|
placedRobber: false,
|
||||||
|
developmentPurchased: false,
|
||||||
|
offer: null,
|
||||||
|
};
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user