Commit 7bcca2f4 authored by Jason Heard's avatar Jason Heard
Browse files

Merge branch 'finish-ui' into 'master'

Finish initial UI

See merge request !13
parents 073632e6 d08326e5
......@@ -17,7 +17,18 @@ export class Assets {
images: Record<Asset, HTMLImageElement | undefined>;
constructor(requestDraw: () => void) {
this.missingTexture.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgAQMAAABJtOi3AAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV8/pEUqDhYp4pChOlkQFXHUKhShQqgVWnUwufQLmjQkKS6OgmvBwY/FqoOLs64OroIg+AHi5Oik6CIl/i8ptIjx4Lgf7+497t4B/maVqWZwHFA1y8ikkkIuvyqEXhFEGAHEMCgxU58TxTQ8x9c9fHy9S/As73N/jj6lYDLAJxDPMt2wiDeIpzctnfM+cZSVJYX4nHjMoAsSP3JddvmNc8lhP8+MGtnMPHGUWCh1sdzFrGyoxFPEcUXVKN+fc1nhvMVZrdZZ+578hZGCtrLMdZrDSGERSxAhQEYdFVRhIUGrRoqJDO0nPfxDjl8kl0yuChg5FlCDCsnxg//B727N4uSEmxRJAj0vtv0xAoR2gVbDtr+Pbbt1AgSegSut4681gZlP0hsdLX4E9G8DF9cdTd4DLneA2JMuGZIjBWj6i0Xg/Yy+KQ8M3AK9a25v7X2cPgBZ6ip9AxwcAqMlyl73eHe4u7d/z7T7+wECdHJ6DIyFCQAAAAZQTFRFAAAA/wD/Nv1irwAAAAlwSFlzAAAuIwAALiMBeKU/dgAAAAd0SU1FB+QECAMbF1mWjR8AAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAFUlEQVQI12P4/5+BgYGKBFUN+/8fAO29P8FEXAQgAAAAAElFTkSuQmCC";
this.images = {};
this.images = {
cardBackground: undefined,
coalPlant: undefined,
oilPlant: undefined,
gasPlant: undefined,
nuclearPlant: undefined,
freePlant: undefined,
coalSymbol: undefined,
oilSymbol: undefined,
gasSymbol: undefined,
nuclearSymbol: undefined,
};
for (const assetName of assetList) {
this.images[assetName] = this.missingTexture;
const loadImage = new Image();
......
......@@ -2,9 +2,10 @@ import { ClickZone } from "./ClickZone";
import { RenderTarget } from "./RenderTarget";
export class Confirmation {
constructor(prompt: string, confirmText: string, action: () => boolean, cleanup: () => void) {
constructor(prompt: string, confirmText: string, showCancel: boolean, action: () => boolean, cleanup: () => void) {
this._prompt = prompt;
this._confirmText = confirmText;
this._showCancel = showCancel;
this._action = action;
this._cleanup = cleanup;
}
......@@ -22,7 +23,9 @@ export class Confirmation {
target.context.fillText(this._prompt, innerLeft, innerTop);
clickZones.push(target.drawButton(buttonStart, innerTop, spacer * 0.5, buttonWidth, innerHeight, this._confirmText, (): void => this.selectConfirm()));
clickZones.push(target.drawButton(buttonStart + buttonWidth + spacer, innerTop, spacer * 0.5, buttonWidth, innerHeight, "Cancel", (): void => this.cancel()));
if (this._showCancel) {
clickZones.push(target.drawButton(buttonStart + buttonWidth + spacer, innerTop, spacer * 0.5, buttonWidth, innerHeight, "Cancel", (): void => this.cancel()));
}
return clickZones;
}
......@@ -35,4 +38,5 @@ export class Confirmation {
private _cleanup: () => void;
private _confirmText: string;
private _prompt: string;
private _showCancel: boolean;
}
import { ServerMessage, ClientMessage } from "./messages";
export class Connection {
socket: WebSocket;
onReady: () => void;
onMessage: (message: ServerMessage) => void;
constructor(url: string, onReady: () => void, onMessage: (message: ServerMessage) => void) {
this.socket = new WebSocket(url);
this.onReady = onReady;
this.onMessage = onMessage;
this.socket.onopen = onReady;
this.socket.onmessage = (message: MessageEvent): void => {
this.onMessage(JSON.parse(message.data) as ServerMessage);
};
this._url = url;
this._onReady = onReady;
this._onMessage = onMessage;
this._createSocket();
}
send(message: ClientMessage): void {
this.socket.send(JSON.stringify(message));
this._socket.send(JSON.stringify(message));
}
private _createSocket(): void {
this._socket = new WebSocket(this._url);
this._socket.onopen = (): void => {
this._reconnectDelay = 100;
this._onReady();
};
this._socket.onmessage = (message: MessageEvent): void => {
this._onMessage(JSON.parse(message.data) as ServerMessage);
};
this._socket.onclose = (): void => {
console.warn("Disconnected; trying to reconnect...");
setTimeout((): void => this._createSocket(), this._reconnectDelay);
this._reconnectDelay *= 2;
};
}
private readonly _onReady: () => void;
private readonly _onMessage: (message: ServerMessage) => void;
private _reconnectDelay = 100;
private _socket!: WebSocket;
private readonly _url: string;
}
This diff is collapsed.
......@@ -10,7 +10,47 @@ const colours = {
"free": "#00FF00",
};
export function drawPlant(target: RenderTarget, plant: PlantCard, isSelected: boolean, tileSize: number, cardX: number, cardY: number): void {
export enum DeckType {
PlantDark,
PlantLight,
Resource,
ResourcePlus,
ResourcePlusPlus,
}
export function drawDeck(target: RenderTarget, deckType: DeckType, deckCount: number, tileSize: number, cardX: number, cardY: number): void {
target.drawAsset(Asset.cardBackground, cardX, cardY, tileSize, tileSize);
// TODO deck back assets!
// target.drawAsset((fuel.fuelType + "Symbol") as Asset, cardX + fuelOffset, cardY + fuelOffset, fuelSize, fuelSize);
const gap = tileSize * 0.1;
const topWord = deckType === DeckType.PlantDark ? "Dark"
: deckType === DeckType.PlantLight ? "Light"
: deckType === DeckType.Resource ? "Resource"
: "";
const middleWord = deckType === DeckType.PlantDark || deckType === DeckType.PlantLight ? "Plant"
: deckType === DeckType.Resource ? "Deck"
: deckType === DeckType.ResourcePlus ? "+"
: "++";
target.context.font = `${tileSize * 0.2}px Arial`;
target.context.textBaseline = "top";
target.context.textAlign = "center";
target.context.fillStyle = "#000000";
target.context.fillText(topWord, cardX + tileSize / 2, cardY + gap);
target.context.textBaseline = "middle";
target.context.textAlign = "center";
target.context.fillText(middleWord, cardX + tileSize / 2, cardY + tileSize / 2);
target.context.font = `${tileSize * 0.15}px Arial`;
target.context.textBaseline = "bottom";
target.context.textAlign = "center";
target.context.fillText(`(${deckCount})`, cardX + tileSize / 2, cardY + tileSize - gap);
}
export function drawPlant(target: RenderTarget, plant: PlantCard, isSelected: boolean, isDiscounted: boolean, tileSize: number, cardX: number, cardY: number): void {
if (isSelected) {
target.context.strokeStyle = "#ffff00";
target.context.strokeRect(cardX - 2, cardY - 2, tileSize + 4, tileSize + 4);
......@@ -19,7 +59,19 @@ export function drawPlant(target: RenderTarget, plant: PlantCard, isSelected: bo
target.drawAsset(Asset.cardBackground, cardX, cardY, tileSize, tileSize);
if (plant.plantType === "one-more-round") {
// TODO render one more round card
const gap = tileSize * 0.1;
target.context.font = `${tileSize * 0.25}px Arial`;
target.context.textBaseline = "top";
target.context.textAlign = "center";
target.context.fillStyle = "#000000";
target.context.fillText("One", cardX + tileSize / 2, cardY + gap);
target.context.textBaseline = "middle";
target.context.textAlign = "center";
target.context.fillText("More", cardX + tileSize / 2, cardY + tileSize / 2);
target.context.textBaseline = "bottom";
target.context.textAlign = "center";
target.context.fillText("Round", cardX + tileSize / 2, cardY + tileSize - gap);
return;
}
......@@ -55,7 +107,20 @@ export function drawPlant(target: RenderTarget, plant: PlantCard, isSelected: bo
target.context.fillText(plant.cost.toString(), cardX + textOppositeOffset, cardY + textOffset);
target.context.textBaseline = "bottom";
target.context.fillStyle = "#000000";
target.context.fillText("" + plant.power.toString(), cardX + textOppositeOffset, cardY + textOppositeOffset);
target.context.fillText(`€${plant.power}`, cardX + textOppositeOffset, cardY + textOppositeOffset);
if (isDiscounted) {
target.context.font = `${tileSize * 0.8}px Arial`;
target.context.fillStyle = "#66ff00";
target.context.textBaseline = "middle";
target.context.textAlign = "center";
target.context.save();
target.context.translate(cardX + tileSize / 2, cardY + tileSize / 2);
target.context.rotate(Math.PI / 6);
target.context.fillText("1", 0, 0);
target.context.restore();
}
}
export function drawFuel(target: RenderTarget, fuel: FuelCard, isSelected: boolean, tileSize: number, cardX: number, cardY: number): void {
......
......@@ -50,17 +50,22 @@ function init(): void {
}
const assets = new Assets(requestDraw);
const target = new RenderTarget(document.getElementById("mainCanvas") as HTMLCanvasElement, assets);
const canvas = document.getElementById("mainCanvas") as HTMLCanvasElement;
const target = new RenderTarget(canvas, assets);
const localID = localStorage.getItem("localID") || newUUID();
localStorage.setItem("localID", localID);
const playerName = localStorage.getItem("playerName") || "Player";
let playerName = localStorage.getItem("playerName") || "Player";
if (playerName === "Player") {
playerName = window.prompt("Enter player name:", "Player") ?? "Player";
}
localStorage.setItem("playerName", playerName);
let game: Game | undefined;
const queryParameters = getQuerryParameters();
const socket = new Connection("ws://" + window.location.host + "/ws", (): void => {
const secureUrl = window.location.protocol === "https:";
const socket = new Connection(`${secureUrl ? "wss:" : "ws:"}//${window.location.host}/ws`, (): void => {
game = new Game(localID, socket);
const gameId = queryParameters.gameid ? queryParameters.gameid.toString() : undefined;
game.join(gameId, playerName);
......@@ -79,7 +84,7 @@ function init(): void {
requestDraw();
window.onresize = requestDraw;
document.addEventListener("click", (event: MouseEvent): void => handleClick(event, clickZones, requestDraw));
canvas.addEventListener("click", (event: MouseEvent): void => handleClick(event, clickZones, requestDraw));
const loadingTag = document.getElementById("hideOnLoad");
if (loadingTag !== null) {
......
......@@ -32,7 +32,7 @@ interface PlayerFuelPosition {
export type FuelPosition = ShopFuelPosition | PlayerFuelPosition | DeckPosition;
type FuelType = "coal" | "oil" | "gas" | "nuclear";
export type FuelType = "coal" | "oil" | "gas" | "nuclear";
type PlantType = FuelType | "hybrid" | "free" | "one-more-round";
export interface PlantCard {
......@@ -62,6 +62,14 @@ export type FuelSlot = FuelCard | undefined;
type FuelColumn = [FuelSlot, FuelSlot, FuelSlot, FuelSlot, FuelSlot, FuelSlot];
export type FuelStore = [FuelColumn, FuelColumn, FuelColumn, FuelColumn];
export interface DeckInformation {
nextPowerPlantDark: boolean;
powerPlantDeckCount: number;
resourceDeckCount: number;
resourcePlusUsed: boolean;
resourcePlusPlusUsed: boolean;
}
// Players //
export type Player = string;
......@@ -91,6 +99,7 @@ interface ScrapPowerPlantPlayerAction {
actionType: "scrap-power-plant";
player: Player;
newPlant: PlantCard;
newPlantLocation: number;
}
interface GenericPlayerAction {
......@@ -102,11 +111,25 @@ interface AnyPlayerAction {
actionType: "join-game" | "join-or-start-game" | "start-game";
}
interface EndGameStats {
player: Player;
lastPowerupAmount: number;
victoryPoints: number;
remainingMoney: number;
biggestPowerPlant: number;
}
interface GameOverAction {
actionType: "game-over";
winner: Player;
endGameStats: Array<EndGameStats>;
}
export type PlayerActionWithPlayer = BidPowerPlantPlayerAction |
CounterBidPowerPlantPlayerAction|
ScrapPowerPlantPlayerAction |
GenericPlayerAction;
export type PlayerAction = BidPowerPlantPlayerAction |
CounterBidPowerPlantPlayerAction|
ScrapPowerPlantPlayerAction |
......@@ -150,7 +173,8 @@ export interface BuyFuelMessage {
export interface PowerUpMessage {
type: "power-up";
toUse: FuelAmounts;
fuelToUse: FuelAmounts;
plantsToUse: Array<number>;
}
export interface PingMessage {
......@@ -164,6 +188,8 @@ interface GameStateMessage {
gameUuid: string;
fuelStore: FuelStore;
plantStore: Array<PlantCard>;
plantDiscountAvailable: boolean;
deckInformation: DeckInformation;
playerOrdering: Array<Player>;
players: Array<PlayerState>;
nextAction: PlayerAction;
......@@ -230,6 +256,7 @@ interface PongMessage {
interface ErrorMessage {
type: "error";
message: string;
joinError: boolean;
}
export type ClientMessage =
......
import { PlayerState, FuelAmounts } from "./messages";
import { PlayerState, FuelAmounts, FuelType } from "./messages";
import { RenderTarget } from "./RenderTarget";
import { drawPlant } from "./cardRenderer";
import { ClickZone } from "./ClickZone";
import { Asset } from "./Assets";
const colours = [
"#FFC040",
"#FFFFFF",
"#0080F0",
"#FFFF00",
];
const fancyFuels = [
"Coal",
"Oil",
"Natural Gas",
"Uranium",
];
export const fuelColours = {
"coal": "#FFC040",
"oil": "#FFFFFF",
"gas": "#0080F0",
"nuclear": "#FFFF00",
};
const amountNames: (keyof FuelAmounts)[] = [
"coalAmount",
"oilAmount",
"gasAmount",
"nuclearAmount",
];
const fuelTypes: FuelType[] = [
"coal",
"oil",
"gas",
"nuclear",
];
export function drawPlayer(
target: RenderTarget,
......@@ -56,18 +57,14 @@ export function drawPlayer(
target.context.fillText(player.name + (isSelf ? " (you)" : "") + (extraPlayerText ? ` (${extraPlayerText})` : ""), x + margin + fontSize, y + margin);
target.context.font = `${fontSize}px Arial`;
target.context.textAlign = "right";
target.context.fillText("Balance: €" + player.money.toString(), x + width - margin, y + margin);
target.context.textBaseline = "bottom";
target.context.textAlign = "left";
target.context.fillText(`€${player.money}`, x + width - margin, y + margin);
for (let i = 0; i < 4; i++) {
target.context.fillStyle = colours[i];
target.context.fillText(fancyFuels[i] + ":", x + width / 4 * i + margin, y + height - margin - fontSize / 2);
target.context.fillText(player.resources[amountNames[i]].toString(), x + width / 4 * i + margin, y + height - margin + fontSize / 2);
drawPlayerFuel(target, fuelTypes[i], player.resources[amountNames[i]], fontSize * 2, x + (width - margin) / 4 * i + margin, y + height - margin - fontSize * 2);
}
for (let i = 0; i < player.plants.length; i++) {
const cardX = x + width / 2 + (cardSize + cardSize * cardGap) * (i - 1) - cardSize / 2;
const cardY = y + height / 2 - cardSize / 2 - fontSize / 2;
drawPlant(target, player.plants[i], isPlayerPlantSelected ? isPlayerPlantSelected(i) : false, cardSize, cardX, cardY);
drawPlant(target, player.plants[i], isPlayerPlantSelected ? isPlayerPlantSelected(i) : false, false, cardSize, cardX, cardY);
if (selectPlayerPlant) {
clickZones.push({
top: cardY,
......@@ -83,3 +80,20 @@ export function drawPlayer(
return clickZones;
}
export function drawPlayerFuel(target: RenderTarget, fuelType: FuelType, amount: number, height: number, left: number, top: number): void {
const fuelSize = height * 0.7;
const fontSize = height * 0.8;
const textOffsetX = height * 0.1;
const textOffsetY = height * 0.2;
const fuelOffset = (height - fuelSize) / 2;
target.drawAsset((fuelType + "Symbol") as Asset, left + fuelOffset, top + fuelOffset, fuelSize, fuelSize);
target.context.fillStyle = fuelColours[fuelType];
target.context.font = `${fontSize}px Arial`;
target.context.textBaseline = "top";
target.context.textAlign = "left";
target.context.fillText(`${amount}`, left + height + textOffsetX, top + textOffsetY);
}
const audioCtx: AudioContext = new (window.AudioContext || (window as any).webkitAudioContext || (window as any).audioContext);
/**
* Play a ping sound.
*/
export function ping(): void {
const startTime = audioCtx.currentTime;
const gainNode = audioCtx.createGain();
const attackTime = startTime + 0.025;
const sustainTime = attackTime + 0.05;
const releaseTime = sustainTime + 0.75;
gainNode.gain.linearRampToValueAtTime(0, startTime);
gainNode.gain.linearRampToValueAtTime(1.0, attackTime);
gainNode.gain.linearRampToValueAtTime(1.0, sustainTime);
gainNode.gain.exponentialRampToValueAtTime(0.001, releaseTime);
const oscillator = audioCtx.createOscillator();
oscillator.frequency.linearRampToValueAtTime(1108.73, startTime);
oscillator.frequency.linearRampToValueAtTime(1108.73, attackTime);
oscillator.frequency.linearRampToValueAtTime(1108.73, sustainTime);
oscillator.frequency.exponentialRampToValueAtTime(1046.50, releaseTime);
oscillator.type = "triangle";
oscillator.connect(gainNode);
gainNode.connect(audioCtx.destination);
oscillator.start(startTime);
oscillator.stop(releaseTime);
}
/**
* Play a boop sound.
*/
export function boop(): void {
const startTime = audioCtx.currentTime;
const gainNode = audioCtx.createGain();
const attackTime = startTime + 0.025;
const sustainTime = attackTime + 0.05;
const releaseTime = sustainTime + 0.25;
gainNode.gain.linearRampToValueAtTime(0, startTime);
gainNode.gain.linearRampToValueAtTime(1.0, attackTime);
gainNode.gain.linearRampToValueAtTime(1.0, sustainTime);
gainNode.gain.exponentialRampToValueAtTime(0.001, releaseTime);
const oscillator = audioCtx.createOscillator();
oscillator.frequency.linearRampToValueAtTime(349.23, startTime);
oscillator.frequency.linearRampToValueAtTime(349.23, attackTime);
oscillator.frequency.linearRampToValueAtTime(329.63, sustainTime);
oscillator.frequency.linearRampToValueAtTime(329.63, releaseTime);
oscillator.type = "triangle";
oscillator.connect(gainNode);
gainNode.connect(audioCtx.destination);
oscillator.start(startTime);
oscillator.stop(releaseTime);
}
......@@ -2,4 +2,4 @@
SCRIPTPATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd )
docker build -t powergrid-card-game "${SCRIPTPATH}"
docker build -t powergrid-cardgame-online "${SCRIPTPATH}"
......@@ -5,6 +5,6 @@ if [ -z "$1" ]; then
exit -1
fi
docker tag powergrid-card-game:latest 101100/powergrid-card-game:latest
docker tag powergrid-card-game:latest 101100/powergrid-card-game:$1
docker push 101100/powergrid-card-game
docker tag powergrid-cardgame-online:latest 101100/powergrid-cardgame-online:latest
docker tag powergrid-cardgame-online:latest 101100/powergrid-cardgame-online:$1
docker push 101100/powergrid-cardgame-online
......@@ -23,7 +23,7 @@
"lint": "tslint --project tsconfig.json client/sourse/*.ts",
"start": "concurrently \"npm:start-client\" \"npm:start-server\"",
"start-client": "parcel watch client/index.html --out-dir client/dist",
"start-server": "cargo run",
"start-server": "env RUST_BACKTRACE=1 cargo run",
"test": "npm run lint && npm run build-typescript"
}
}
This diff is collapsed.
......@@ -92,28 +92,42 @@ async fn main() {
let routes = warp::get().and(web_socket.or(static_files.or(main_files)));
register_ctrl_c_handler();
register_signal_handlers();
println!("Serving on port 3030...");
warp::serve(routes).run(([0, 0, 0, 0], 3030)).await;
}
fn register_ctrl_c_handler() {
fn register_signal_handlers() {
fn on_sigint() {
eprintln!("SIGINT caught - exiting");
std::process::exit(128 + signal_hook::SIGINT);
}
fn on_sigterm() {
eprintln!("SIGTERM caught - exiting");
std::process::exit(128 + signal_hook::SIGTERM);
}
let signal_result;
let sigint_result;
unsafe {
signal_result = signal_hook::register(signal_hook::SIGINT, || on_sigint());
sigint_result = signal_hook::register(signal_hook::SIGINT, || on_sigint());
}
match signal_result {
match sigint_result {
Ok(_) => eprintln!("Registered for SIGINT"),
Err(e) => eprintln!("Failed to register for SIGINT {:?}", e)
}
let sigterm_result;
unsafe {
sigterm_result = signal_hook::register(signal_hook::SIGTERM, || on_sigterm());
}
match sigterm_result {
Ok(_) => eprintln!("Registered for SIGTERM"),
Err(e) => eprintln!("Failed to register for SIGTERM {:?}", e)
}
}
......@@ -235,9 +249,9 @@ async fn handle_message(
eprintln!("(cid={}, gid={}): player is buying {} fuel card(s)", player_game_state.connection_id, &game_uuid, fuel.len());
game_state.buy_fuel(player_uuid, fuel)
},
ClientMessage::PowerUp{ to_use } => {
eprintln!("(cid={}, gid={}): player is powering up", player_game_state.connection_id, &game_uuid);
game_state.power_up(player_uuid, to_use)
ClientMessage::PowerUp{ fuel_to_use, plants_to_use } => {
eprintln!("(cid={}, gid={}): player is powering up {} plants", player_game_state.connection_id, &game_uuid, plants_to_use.len());
game_state.power_up(player_uuid, fuel_to_use, plants_to_use)
},
ClientMessage::Ping => {
reply_to_ping(&player_game_state, server_state).await;
......@@ -249,7 +263,8 @@ async fn handle_message(
Err(e) => {
eprintln!("(cid={}, gid={}): error from game: {}", player_game_state.connection_id, &game_uuid, e);
messages = Some(vec![ServerMessage::Error{
message: e
message: e,
join_error: false
}]);
recipient = Some(player_uuid.clone());
}
......@@ -269,6 +284,7 @@ async fn handle_message(
recipient = Some(game_state.game_uuid.clone());
} else {
eprintln!("(cid={}): got unknown game UUID for rejoin: {}", player_game_state.connection_id, game_uuid);
send_join_error(&player_game_state, server_state, String::from("The given game ID is unknown")).await;
}
} else {
eprintln!("(cid={}): creating a new game", player_game_state.connection_id);
......@@ -360,7 +376,7 @@ async fn join_game(player_game_state: &mut PlayerGameState, game_state: &mut Gam
}
Err(e) => {
eprintln!("(cid={}): error joining game: {}", player_game_state.connection_id, e);
// TODO: send error to user?
send_join_error(&player_game_state, server_state, e).await;
None
}
}
......@@ -368,7 +384,6 @@ async fn join_game(player_game_state: &mut PlayerGameState, game_state: &mut Gam
async fn reply_to_ping(player_game_state: &PlayerGameState, server_state: &ServerState) {
// TODO add message to queue instead?
if let Ok(message) = serde_json::to_string(&ServerMessage::Pong) {
if let Some(player_connection) = server_state.player_connections.lock().await.get(&player_game_state.connection_id) {
if let Err(e) = player_connection.send(Ok(Message::text(message.clone()))) {
......@@ -381,6 +396,19 @@ async fn reply_to_ping(player_game_state: &PlayerGameState, server_state: &Serve
}
async fn send_join_error(player_game_state: &PlayerGameState, server_state: &ServerState, error: String) {
if let Ok(message) = serde_json::to_string(&ServerMessage::Error{ message: error, join_error: true }) {
if let Some(player_connection) = server_state.player_connections.lock().await.get(&player_game_state.connection_id) {
if let Err(e) = player_connection.send(Ok(Message::text(message.clone()))) {
eprintln!("(cid={}): websocket error: {}", player_game_state.connection_id, e);
}
} else {
eprintln!("(cid={}): odd, I couldn't get the user's connection...", player_game_state.connection_id);
}
}
}
async fn user_disconnected(player_game_state: &PlayerGameState, server_state: &ServerState) {
eprintln!("(cid={}): good bye!", player_game_state.connection_id);
......