Commit 5e20da61 authored by Jason Heard's avatar Jason Heard
Browse files

Merge branch 'ui-tweaks' into 'master'

UI Tweaks

See merge request !15
parents e33992e2 592ba0db
......@@ -6,7 +6,7 @@ import { ClickZone } from "./ClickZone";
import { drawPlayer } from "./playerRenderer";
import { AmountSelection } from "./AmmountSelection";
import { Confirmation } from "./Confirmation";
import { ping, boop } from "./sounds";
import { ping, boop, tada } from "./sounds";
export class Game {
constructor(playerUUID: Player, socket: Connection) {
......@@ -31,6 +31,7 @@ export class Game {
};
this._playerOrder = [];
this._players = [];
this._playerHuesByUuid = {};
this._playersByUuid = {};
}
draw(target: RenderTarget): Array<ClickZone> {
......@@ -80,19 +81,70 @@ export class Game {
return 0;
}
let orderCharacter: string | undefined;
let orderSpace = 0;
let highlightPlayer: Player | undefined;
const donePlayers: Record<Player, true> = {};
if (this._action
&& (this._action.actionType === "bid-power-plant"
|| this._action.actionType === "counter-bid"
|| this._action.actionType === "scrap-power-plant"
|| this._action.actionType === "power-up")) {
orderCharacter = "";
orderSpace = height * 0.5;
highlightPlayer = this._action.actionType === "counter-bid" || this._action.actionType === "scrap-power-plant"
? this._action.biddingPlayer
: this._action.player;
if (this._action.actionType === "bid-power-plant"
|| this._action.actionType === "counter-bid"
|| this._action.actionType === "scrap-power-plant") {
for (const donePlayer of this._action.doneBidding) {
donePlayers[donePlayer] = true;
}
}
} else if (this._action && this._action.actionType === "buy-fuel") {
orderCharacter = "";
orderSpace = height * 0.5;
highlightPlayer = this._action.player;
}
const innerTop = top + spacer;
const innerLeft = left + spacer;
const innerHeight = height - spacer * 2;
const innerWidth = this._players.length * (innerHeight + spacer) - spacer;
const actualWidth = innerWidth + spacer * 2;
const innerWidth = this._players.length * (innerHeight + spacer + orderSpace) - spacer - orderSpace;
const actualWidth = innerWidth + (spacer) * 2;
target.context.strokeStyle = "#ffffff";
target.context.strokeRect(left, top, actualWidth, height);
if (orderCharacter) {
target.context.font = `${height * 0.7}px Arial`;
target.context.textBaseline = "top";
target.context.fillStyle = "#ffffff";
target.context.textAlign = "left";
}
const orderTextSpacer = height * 0.6;
let gap = 0;
for (const player of this._playerOrder) {
target.drawIdenticon(player, innerLeft + gap, innerTop, innerHeight);
gap += innerHeight + spacer;
for (let i = 0; i < this._playerOrder.length; i += 1) {
const player = this._playerOrder[i];
const playerHue = this._playerHuesByUuid[player];
target.drawIdenticon(player, playerHue, innerLeft + gap, innerTop, innerHeight);
if (highlightPlayer === player) {
target.context.strokeStyle = "#ffff00";
target.context.strokeRect(innerLeft + gap - 2, innerTop - 2, innerHeight + 4, innerHeight + 4);
}
if (donePlayers[player]) {
target.context.fillStyle = "#000000";
target.context.globalAlpha = 0.75;
target.context.fillRect(innerLeft + gap, innerTop, innerHeight, innerHeight);
target.context.fillStyle = "#ffffff";
target.context.globalAlpha = 1.0;
}
if (orderCharacter && i < this._playerOrder.length - 1) {
target.context.fillText(orderCharacter, innerLeft + gap + orderTextSpacer, innerTop);
}
gap += innerHeight + spacer + orderSpace;
}
return actualWidth;
......@@ -153,7 +205,7 @@ export class Game {
const currentBid = this._action.currentBid;
const money = this._getMoney();
if (currentBid < money) {
target.context.fillText("Waiting for your counter bid...", innerLeft, innerTop);
target.context.fillText(`Current bid is €${currentBid}; counter bid?`, innerLeft, innerTop);
clickZones.push(target.drawButton(buttonStart, innerTop, spacer * 0.5, buttonWidth, innerHeight, "Counter Bid", (): void => this._counterBidAction(currentBid)));
clickZones.push(target.drawButton(buttonStart + buttonWidth + spacer, innerTop, spacer * 0.5, buttonWidth, innerHeight, "Skip Bidding", this._skipCounterBiddingAction));
} else {
......@@ -237,7 +289,7 @@ export class Game {
// draw players
let playerTop = top + spacer + tileSize;
for (const playerStats of this._action.endGameStats) {
target.drawIdenticon(playerStats.player, gridOriginX + spacer, playerTop, playerSize);
target.drawIdenticon(playerStats.player, this._playerHuesByUuid[playerStats.player], gridOriginX + spacer, playerTop, playerSize);
const textTop = playerTop + spacer * 0.5;
target.context.textAlign = "left";
target.context.fillText(this._playersByUuid[playerStats.player].name, gridOriginX + spacer + (playerSize + spacer) * 1, textTop);
......@@ -260,21 +312,25 @@ export class Game {
const cardY = gridOriginY + tileSize * y;
if (y < 2 && x < 4) {
// power plants are in the first two rows and the first four columns
const plantPos = this._store.plant.length > 6
? x + y * 4
: x - 1 + y * 3;
const finalRound = this._store.plant.length <= 6;
const plantPos = finalRound
? x - 1 + y * 3
: x + y * 4;
const plant = this._store.plant[plantPos];
if (plant && (this._store.plant.length > 6 || x > 0)) {
if (plant && (!finalRound || x > 0)) {
const isDiscounted = plantPos === 0 && this._store.plantDiscountAvailable;
const isPlantActive = this._getIsPlantActive(plant, plantPos);
drawPlant(target, plant, isPlantActive, isDiscounted, cardSize, cardX + cardOffset, cardY + cardOffset);
clickZones.push({
top: cardY + cardOffset,
bottom: cardY + cardOffset + cardSize,
left: cardX + cardOffset,
right: cardX + cardOffset + cardSize,
action: (): void => this._selectShopPlant(plant, plantPos),
});
const isAvailable = finalRound || y === 0;
drawPlant(target, plant, isPlantActive, isDiscounted, !isAvailable, cardSize, cardX + cardOffset, cardY + cardOffset);
if (isAvailable) {
clickZones.push({
top: cardY + cardOffset,
bottom: cardY + cardOffset + cardSize,
left: cardX + cardOffset,
right: cardX + cardOffset + cardSize,
action: (): void => this._selectShopPlant(plant, plantPos),
});
}
}
} else if (y === 0 && x === 4) {
// power plant deck is in the top right
......@@ -347,7 +403,7 @@ export class Game {
const player = this._players[x + y * 2];
const isSelf = player.uuid === this._self;
const extraPlayerText = this._getExtraPlayerText(player);
clickZones.push(...drawPlayer(target, player, isSelf, extraPlayerText, isSelf ? this._selectPlayerPlant : undefined, isSelf ? this._isPlayerPlantSelected : undefined, posX, posY, playerWidth, playerHeight));
clickZones.push(...drawPlayer(target, player, this._playerHuesByUuid[player.uuid], isSelf, extraPlayerText, isSelf ? this._selectPlayerPlant : undefined, isSelf ? this._isPlayerPlantSelected : undefined, posX, posY, playerWidth, playerHeight));
}
}
} else {
......@@ -363,7 +419,7 @@ export class Game {
const player = this._players[y];
const isSelf = player.uuid === this._self;
const extraPlayerText = this._getExtraPlayerText(player);
clickZones.push(...drawPlayer(target, player, isSelf, extraPlayerText, isSelf ? this._selectPlayerPlant : undefined, isSelf ? this._isPlayerPlantSelected : undefined, posX, posY, playerWidth, playerHeight));
clickZones.push(...drawPlayer(target, player, this._playerHuesByUuid[player.uuid], isSelf, extraPlayerText, isSelf ? this._selectPlayerPlant : undefined, isSelf ? this._isPlayerPlantSelected : undefined, posX, posY, playerWidth, playerHeight));
}
}
......@@ -448,16 +504,24 @@ export class Game {
case "bid-power-plant":
if (this._action.player === player.uuid) {
return "Bidding";
} else if (this._action.doneBidding.indexOf(player.uuid) !== -1) {
return "Done Bidding";
}
break;
case "counter-bid":
if (this._action.player === player.uuid) {
return "Counter-bidding";
} else if (this._action.doneBidding.indexOf(player.uuid) !== -1) {
return "Done Bidding";
} else if (this._action.alreadySkipped.indexOf(player.uuid) !== -1) {
return "Skipped";
}
break;
case "scrap-power-plant":
if (this._action.player === player.uuid) {
return "Scrapping";
} else if (this._action.doneBidding.indexOf(player.uuid) !== -1) {
return "Done Bidding";
}
break;
case "buy-fuel":
......@@ -477,7 +541,7 @@ export class Game {
break;
}
if (this._action.actionType === "counter-bid" && this._action.currentWinner === player.uuid) {
return `Current high bidder at €${this._action.currentBid}`;
return `High bidder at €${this._action.currentBid}`;
}
return undefined;
}
......@@ -677,17 +741,23 @@ export class Game {
&& this._action.player === this._self) {
const isDiscounted = plantPos === 0 && this._store.plantDiscountAvailable;
this._amountSelection = new AmountSelection("Please select a bid amount:", isDiscounted ? 1 : plant.cost, this._getMoney(), (amount: number): void => {
this._socket.send({
type: "bid-power-plant",
location: plantPos,
startingBid: amount,
const minimumCost = isDiscounted ? 1 : plant.cost;
const money = this._getMoney();
if (minimumCost > money) {
this._showErrorMessage(`That plant is €${minimumCost}, you can't afford it.`);
} else {
this._amountSelection = new AmountSelection("Please select a bid amount:", minimumCost, money, (amount: number): void => {
this._socket.send({
type: "bid-power-plant",
location: plantPos,
startingBid: amount,
});
}, (): void => {
this._amountSelection = undefined;
this._selectedPlant = undefined;
});
}, (): void => {
this._amountSelection = undefined;
this._selectedPlant = undefined;
});
this._selectedPlant = plantPos;
this._selectedPlant = plantPos;
}
} else {
console.debug("Ignoring plant click when it isn't our bid phase.");
}
......@@ -734,22 +804,41 @@ export class Game {
});
}
private _updateNextAction(nextAction: PlayerAction): void {
// make a sound if it is your turn to perform an action
const oldActionWithPlayer = Game._getActionWithPlayer(this._action);
const newActionWithPlayer = Game._getActionWithPlayer(nextAction);
if (newActionWithPlayer && newActionWithPlayer.player === this._self
&& (!oldActionWithPlayer || oldActionWithPlayer.player !== this._self || oldActionWithPlayer.actionType !== newActionWithPlayer.actionType)) {
ping();
}
// make a sound if the game is over
if ((!this._action || this._action.actionType !== "game-over") && nextAction && nextAction.actionType === "game-over") {
tada();
}
this._action = nextAction;
// initially select all power plants to power up if it is our turn to power up
if (this._action && this._action.actionType === "power-up" && this._action.player === this._self) {
this._selectPlantsToPowerUp();
}
// give a notification if this is the final round
if (this._action && this._action.actionType === "bid-power-plant" && this._action.lateGame && !this._seenLateGame) {
this._showErrorMessage("This is the final round.");
this._seenLateGame = true;
}
}
private _updatePlayers(players: Array<PlayerState>): void {
this._players = players;
this._playersByUuid = {};
for (const player of players) {
this._playerHuesByUuid = {};
const hueShift = 360 / this._players.length;
for (let i = 0; i < players.length; i += 1) {
const player = players[i];
this._playersByUuid[player.uuid] = player;
this._playerHuesByUuid[player.uuid] = hueShift * i;
}
}
private _amountSelection: AmountSelection | undefined;
......@@ -760,7 +849,9 @@ export class Game {
private _errorIsJoinError?: boolean;
private _playerOrder: Array<Player>;
private _players: Array<PlayerState>;
private _playerHuesByUuid: Record<string, number>;
private _playersByUuid: Record<string, PlayerState>;
private _seenLateGame: boolean = false;
private _self: Player;
private _selectedFuel: Array<{ price: number; slot: number; }> | undefined;
private _selectedPlant: number | undefined;
......
......@@ -28,17 +28,18 @@ export class RenderTarget {
const yAdjustment = (height - actualHeight) / 2;
this.context.drawImage(this.assets.get(assetName), posX + xAdjustment, posY + yAdjustment, actualWidth, actualHeight);
}
drawIdenticon(uuid: string, posX: number, posY: number, size: number): void {
drawIdenticon(uuid: string, hue: number, posX: number, posY: number, size: number): void {
this.context.save();
this.context.translate(posX, posY);
drawIcon(this.context, uuid, size, {
hues: [ hue ],
lightness: {
color: [0.50, 1.00],
grayscale: [0.51, 1.00],
color: [0.50, 0.50],
grayscale: [0.75, 0.75],
},
saturation: {
color: 1.00,
grayscale: 0.00,
grayscale: 0.75,
},
});
this.context.restore();
......
......@@ -50,7 +50,7 @@ export function drawDeck(target: RenderTarget, deckType: DeckType, deckCount: nu
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 {
export function drawPlant(target: RenderTarget, plant: PlantCard, isSelected: boolean, isDiscounted: boolean, isFaded: 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);
......@@ -121,6 +121,12 @@ export function drawPlant(target: RenderTarget, plant: PlantCard, isSelected: bo
target.context.fillText("1", 0, 0);
target.context.restore();
}
if (isFaded) {
target.context.fillStyle = "#000000";
target.context.globalAlpha = 0.5;
target.context.fillRect(cardX, cardY, tileSize, tileSize);
target.context.globalAlpha = 1.0;
}
}
export function drawFuel(target: RenderTarget, fuel: FuelCard, isSelected: boolean, tileSize: number, cardX: number, cardY: number): void {
......
......@@ -5,6 +5,7 @@ import { RenderTarget } from "./RenderTarget";
import { newUUID } from "./uuid";
import { Game } from "./Game";
import { Assets } from "./Assets";
import { woop } from "./sounds";
function getQuerryParameters(): Record<string, string | true> {
let queryParameters: Record<string, string | true> = {};
......@@ -91,6 +92,7 @@ function init(): void {
loadingTag.style.display = "none";
}
console.log("%cW%co%co%ct%c!", "color: red; font-size: 3em;", "color: yellow; font-size: 3em;", "color: green; font-size: 3em;", "color: blue; font-size: 3em;", "color: purple; font-size: 3em;");
woop();
}
window.onload = init;
......@@ -83,20 +83,26 @@ export interface PlayerState {
interface BidPowerPlantPlayerAction {
actionType: "bid-power-plant";
doneBidding: Array<Player>;
lateGame: boolean;
player: Player;
}
interface CounterBidPowerPlantPlayerAction {
actionType: "counter-bid";
alreadySkipped: Array<Player>;
biddingPlayer: Player;
currentBid: number;
currentWinner: Player;
doneBidding: Array<Player>;
location: number;
player: Player;
}
interface ScrapPowerPlantPlayerAction {
actionType: "scrap-power-plant";
biddingPlayer: Player;
doneBidding: Array<Player>;
player: Player;
newPlant: PlantCard;
newPlantLocation: number;
......
export const C0 = 16.35;
export const Db0 = 17.32;
export const Cs0 = Db0;
export const D0 = 18.35;
export const Eb0 = 19.45;
export const Ds0 = Eb0;
export const E0 = 20.60;
export const F0 = 21.83;
export const Gb0 = 23.12;
export const Fs0 = Gb0;
export const G0 = 24.50;
export const Ab0 = 25.96;
export const Gs0 = Ab0;
export const A0 = 27.50;
export const Bb0 = 29.14;
export const As0 = Bb0;
export const B0 = 30.87;
export const C1 = 32.70;
export const Db1 = 34.65;
export const Cs1 = Db1;
export const D1 = 36.71;
export const Eb1 = 38.89;
export const Ds1 = Eb1;
export const E1 = 41.20;
export const F1 = 43.65;
export const Gb1 = 46.25;
export const Fs1 = Gb1;
export const G1 = 49.00;
export const Ab1 = 51.91;
export const Gs1 = Ab1;
export const A1 = 55.00;
export const Bb1 = 58.27;
export const As1 = Bb1;
export const B1 = 61.74;
export const C2 = 65.41;
export const Db2 = 69.30;
export const Cs2 = Db2;
export const D2 = 73.42;
export const Eb2 = 77.78;
export const Ds2 = Eb2;
export const E2 = 82.41;
export const F2 = 87.31;
export const Gb2 = 92.50;
export const Fs2 = Gb2;
export const G2 = 98.00;
export const Ab2 = 103.83;
export const Gs2 = Ab2;
export const A2 = 110.00;
export const Bb2 = 116.54;
export const As2 = Bb2;
export const B2 = 123.47;
export const C3 = 130.81;
export const Db3 = 138.59;
export const Cs3 = Db3;
export const D3 = 146.83;
export const Eb3 = 155.56;
export const Ds3 = Eb3;
export const E3 = 164.81;
export const F3 = 174.61;
export const Gb3 = 185.00;
export const Fs3 = Gb3;
export const G3 = 196.00;
export const Ab3 = 207.65;
export const Gs3 = Ab3;
export const A3 = 220.00;
export const Bb3 = 233.08;
export const As3 = Bb3;
export const B3 = 246.94;
export const C4 = 261.63;
export const Db4 = 277.18;
export const Cs4 = Db4;
export const D4 = 293.66;
export const Eb4 = 311.13;
export const Ds4 = Eb4;
export const E4 = 329.63;
export const F4 = 349.23;
export const Gb4 = 369.99;
export const Fs4 = Gb4;
export const G4 = 392.00;
export const Ab4 = 415.30;
export const Gs4 = Ab4;
export const A4 = 440.00;
export const Bb4 = 466.16;
export const As4 = Bb4;
export const B4 = 493.88;
export const C5 = 523.25;
export const Db5 = 554.37;
export const Cs5 = Db5;
export const D5 = 587.33;
export const Eb5 = 622.25;
export const Ds5 = Eb5;
export const E5 = 659.25;
export const F5 = 698.46;
export const Gb5 = 739.99;
export const Fs5 = Gb5;
export const G5 = 783.99;
export const Ab5 = 830.61;
export const Gs5 = Ab5;
export const A5 = 880.00;
export const Bb5 = 932.33;
export const As5 = Bb5;
export const B5 = 987.77;
export const C6 = 1046.50;
export const Db6 = 1108.73;
export const Cs6 = Db6;
export const D6 = 1174.66;
export const Eb6 = 1244.51;
export const Ds6 = Eb6;
export const E6 = 1318.51;
export const F6 = 1396.91;
export const Gb6 = 1479.98;
export const Fs6 = Gb6;
export const G6 = 1567.98;
export const Ab6 = 1661.22;
export const Gs6 = Ab6;
export const A6 = 1760.00;
export const Bb6 = 1864.66;
export const As6 = Bb6;
export const B6 = 1975.53;
export const C7 = 2093.00;
export const Db7 = 2217.46;
export const Cs7 = Db7;
export const D7 = 2349.32;
export const Eb7 = 2489.02;
export const Ds7 = Eb7;
export const E7 = 2637.02;
export const F7 = 2793.83;
export const Gb7 = 2959.96;
export const Fs7 = Gb7;
export const G7 = 3135.96;
export const Ab7 = 3322.44;
export const Gs7 = Ab7;
export const A7 = 3520.00;
export const Bb7 = 3729.31;
export const As7 = Bb7;
export const B7 = 3951.07;
export const C8 = 4186.01;
export const Db8 = 4434.92;
export const Cs8 = Db8;
export const D8 = 4698.63;
export const Eb8 = 4978.03;
export const Ds8 = Eb8;
export const E8 = 5274.04;
export const F8 = 5587.65;
export const Gb8 = 5919.91;
export const Fs8 = Gb8;
export const G8 = 6271.93;
export const Ab8 = 6644.88;
export const Gs8 = Ab8;
export const A8 = 7040.00;
export const Bb8 = 7458.62;
export const As8 = Bb8;
export const B8 = 7902.13;
......@@ -26,6 +26,7 @@ const fuelTypes: FuelType[] = [
export function drawPlayer(
target: RenderTarget,
player: PlayerState,
playerHue: number,
isSelf: boolean,
extraPlayerText: string | undefined,
selectPlayerPlant: ((playerPlantLocation: number) => void) | undefined,
......@@ -48,7 +49,7 @@ export function drawPlayer(
}
target.context.strokeRect(x, y, width, height);
target.drawIdenticon(player.uuid, x + margin, y + margin * 0.9, fontSize);
target.drawIdenticon(player.uuid, playerHue, x + margin, y + margin * 0.9, fontSize);
target.context.font = `${fontSize}px Arial`;
target.context.textBaseline = "top";
......@@ -64,7 +65,7 @@ export function drawPlayer(
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, false, cardSize, cardX, cardY);
drawPlant(target, player.plants[i], isPlayerPlantSelected ? isPlayerPlantSelected(i) : false, false, false, cardSize, cardX, cardY);
if (selectPlayerPlant) {
clickZones.push({
top: cardY,
......
import { F4, E4, Db6, G4, C5, E5, C6, G5, E6, G6, C7 } from "./notes";
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);
let nextSoundStart = 0;