Card art and sleeves update

This commit is contained in:
Andrio Celos
2023-10-10 09:31:50 +11:00
parent a9a7249f59
commit deb8a673ab
43 changed files with 1314 additions and 681 deletions

View File

@@ -196,7 +196,7 @@ class Board {
if (space == Space.Wall || space == Space.OutOfBounds)
return 'It cannot be over a wall.';
if (space >= Space.SpecialInactive1)
return `It cannot be over${' <div class="playHintSpecial" aria-label="Special space">&nbsp;</div>'.repeat(Math.max(currentGame?.players?.length ?? 0, 2))}.`;
return `It cannot be over${' <div class="playHintSpecial" aria-label="Special space">&nbsp;</div>'.repeat(Math.max(currentGame?.game.players.length ?? 0, 2))}.`;
if (space != Space.Empty && !isSpecialAttack)
return 'It cannot be over inked spaces.';
if (!isAnchored) {

View File

@@ -2,6 +2,13 @@ class Card {
number: number;
altNumber: number | null;
name: string;
line1: string | null;
line2: string | null;
artFileName: string | null;
imageUrl?: string;
textScale: number;
inkColour1: Colour;
inkColour2: Colour;
rarity: Rarity;
specialCost: number;
grid: readonly (readonly Space[])[];
@@ -12,10 +19,19 @@ class Card {
private maxX: number;
private maxY: number;
constructor(number: number, altNumber: number | null, name: string, rarity: Rarity, specialCost: number, grid: Space[][]) {
private static DEFAULT_INK_COLOUR_1: Colour = { r: 116, g: 96, b: 240 };
private static DEFAULT_INK_COLOUR_2: Colour = { r: 224, g: 242, b: 104 };
constructor(number: number, altNumber: number | null, name: string, line1: string | null, line2: string | null, artFileName: string | null, textScale: number, inkColour1: Colour, inkColour2: Colour, rarity: Rarity, specialCost: number, grid: Space[][]) {
this.number = number;
this.altNumber = altNumber;
this.name = name;
this.line1 = line1;
this.line2 = line2;
this.artFileName = artFileName;
this.textScale = textScale;
this.inkColour1 = inkColour1;
this.inkColour2 = inkColour2;
this.rarity = rarity;
this.specialCost = specialCost;
this.grid = grid;
@@ -40,7 +56,9 @@ class Card {
}
static fromJson(obj: any) {
return new Card(obj.number, obj.altNumber ?? null, obj.name, obj.rarity, obj.specialCost, obj.grid);
return cardDatabase.cards && cardDatabase.isValidCardNumber(obj.number)
? cardDatabase.get(obj.number)
: new Card(obj.number, obj.altNumber ?? null, obj.name, obj.line1 ?? null, obj.line2 ?? null, obj.artFileName ?? null, obj.textScale ?? 1, obj.inkColour1 ?? this.DEFAULT_INK_COLOUR_1, obj.inkColour2 ?? this.DEFAULT_INK_COLOUR_2, obj.rarity, obj.specialCost, obj.grid);
}
get isUpcoming() { return this.number < 0; }

View File

@@ -8,7 +8,7 @@ class CardButton extends CheckButton {
constructor(card: Card) {
let button = document.createElement('button');
button.type = 'button';
button.classList.add('card');
button.classList.add('cardButton');
button.classList.add([ 'common', 'rare', 'fresh' ][card.rarity]);
if (card.number < 0) button.classList.add('upcoming');
button.dataset.cardNumber = card.number.toString();
@@ -35,6 +35,13 @@ class CardButton extends CheckButton {
}
}
if (card.imageUrl) {
const bgDiv = document.createElement('div');
bgDiv.setAttribute('class', 'cardArt');
bgDiv.style.backgroundImage = `url(${card.imageUrl})`;
button.appendChild(bgDiv);
}
let row = document.createElement('div');
row.className = 'cardHeader';
button.appendChild(row);

View File

@@ -28,7 +28,7 @@ const cardDatabase = {
const cardListRequest = new XMLHttpRequest();
cardListRequest.open('GET', `${config.apiBaseUrl}/cards`);
cardListRequest.addEventListener('load', e => {
const cards = [ ];
const cards: Card[] = [ ];
if (cardListRequest.status == 200) {
const s = cardListRequest.responseText;
const response = JSON.parse(s) as object[];
@@ -40,7 +40,38 @@ const cardDatabase = {
else if (card.altNumber != null && card.altNumber < 0) cardDatabase._byAltNumber[-card.altNumber] = card;
}
cardDatabase.cards = cards;
resolve(cards);
if (window.location.protocol == 'file:') {
// If debugging locally, just read the files from the assets directory.
for (const card of cardDatabase.cards!) {
card.imageUrl = `assets/external/card/${card.artFileName}.webp`
}
resolve(cards);
return;
}
// Otherwise, download and extract card images from a .tar package.
const imagesRequest = new XMLHttpRequest();
imagesRequest.responseType = 'arraybuffer';
imagesRequest.open('GET', 'assets/external/card.tar');
imagesRequest.addEventListener('load', () => {
if (imagesRequest.status == 200 && imagesRequest.response) {
const buffer = imagesRequest.response as ArrayBuffer;
untar(buffer).then(files => {
for (const tarFile of files) {
const card = cardDatabase.cards!.find(c => tarFile.name == `${c.artFileName}.webp`);
if (!card) continue;
card.imageUrl = tarFile.getBlobUrl();
}
resolve(cards);
});
} else {
reject(new Error(`Error downloading card images: response was ${imagesRequest.status}`));
}
});
imagesRequest.addEventListener('error', () => {
reject(new Error('Error downloading card images: no further information.'))
});
imagesRequest.send();
} else {
reject(new Error(`Error downloading card database: response was ${cardListRequest.status}`));
}

View File

@@ -0,0 +1,165 @@
class CardDisplay {
readonly card: Card;
readonly element: SVGSVGElement;
constructor(card: Card, level: number) {
let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 635 885');
svg.setAttribute('alt', card.name);
this.element = svg;
svg.classList.add('card');
svg.classList.add([ 'common', 'rare', 'fresh' ][card.rarity]);
if (card.number < 0) svg.classList.add('upcoming');
svg.dataset.cardNumber = card.number.toString();
svg.style.setProperty("--number", card.number.toString());
// Background
const image = document.createElementNS('http://www.w3.org/2000/svg', 'image');
image.setAttribute('href', `assets/CardBackground-${card.rarity}-${level > 0 ? '1' : '0'}.webp`);
image.setAttribute('width', '100%');
image.setAttribute('height', '100%');
svg.appendChild(image);
if (level == 0) {
svg.insertAdjacentHTML('beforeend', `<image href="assets/external/CardInk.webp" width="635" height="885" clip-path="url(#myClip)"/>`);
} else {
svg.insertAdjacentHTML('beforeend', `
<filter id="ink1-${card.number}" color-interpolation-filters="sRGB"><feColorMatrix type="matrix" values="${card.inkColour1.r / 255} 0 0 0 0 0 ${card.inkColour1.g / 255} 0 0 0 0 0 ${card.inkColour1.b / 255} 0 0 0 0 0 0.88 0"/></filter>
<image href="assets/external/CardInk-1.webp" width="635" height="885" clip-path="url(#myClip)" filter="url(#ink1-${card.number})"/>
<filter id="ink2-${card.number}" color-interpolation-filters="sRGB"><feColorMatrix type="matrix" values="${card.inkColour2.r / 255} 0 0 0 0 0 ${card.inkColour2.g / 255} 0 0 0 0 0 ${card.inkColour2.b / 255} 0 0 0 0 0 0.88 0"/></filter>
<image href="assets/external/CardInk-2.webp" width="635" height="885" clip-path="url(#myClip)" filter="url(#ink2-${card.number})"/>
`);
}
// Art
if (card.imageUrl) {
const image2 = document.createElementNS('http://www.w3.org/2000/svg', 'image');
image2.setAttribute('class', 'cardArt');
image2.setAttribute('href', card.imageUrl);
image2.setAttribute('width', '100%');
image2.setAttribute('height', '100%');
svg.appendChild(image2);
}
// Grid
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
g.setAttribute('transform', 'translate(380 604) rotate(6.5) scale(0.283)');
svg.appendChild(g);
let size = 0;
for (var y = 0; y < 8; y++) {
for (var x = 0; x < 8; x++) {
if (card.grid[x][y] == Space.Empty) {
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.classList.add('empty');
rect.setAttribute('x', (100 * x + 3).toString());
rect.setAttribute('y', (100 * y + 3).toString());
rect.setAttribute('width', '94');
rect.setAttribute('height', '94');
g.appendChild(rect);
} else {
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
const elements: Element[] = [ rect ];
size++;
rect.classList.add(card.grid[x][y] == Space.SpecialInactive1 ? 'special' : 'ink');
const image = document.createElementNS('http://www.w3.org/2000/svg', 'image');
image.setAttribute('href', card.grid[x][y] == Space.SpecialInactive1 ? 'assets/SpecialOverlay.png' : 'assets/InkOverlay.png');
elements.push(image);
for (const el of elements) {
el.setAttribute('x', (100 * x).toString());
el.setAttribute('y', (100 * y).toString());
el.setAttribute('width', '100');
el.setAttribute('height', '100');
g.appendChild(el);
}
}
}
}
// Name
const text1 = document.createElementNS('http://www.w3.org/2000/svg', 'text');
text1.setAttribute('class', 'cardDisplayName');
text1.setAttribute('x', '50%');
text1.setAttribute('y', '168');
text1.setAttribute('font-size', '76');
text1.setAttribute('font-weight', 'bold');
text1.setAttribute('stroke', 'black');
text1.setAttribute('stroke-width', '15');
text1.setAttribute('stroke-linejoin', 'round');
text1.setAttribute('paint-order', 'stroke');
text1.setAttribute('word-spacing', '-10');
text1.setAttribute('transform-origin', 'center');
text1.setAttribute('transform', `scale(${card.textScale} 1)`);
switch (card.rarity) {
case Rarity.Common:
text1.setAttribute('fill', '#6038FF');
break;
case Rarity.Rare:
svg.insertAdjacentHTML('beforeend', `
<linearGradient id='rareGradient' y1='25%' spreadMethod='reflect'>
<stop offset='0%' stop-color='#FEF9C6'/>
<stop offset='50%' stop-color='#DFAF17'/>
<stop offset='100%' stop-color='#FEF9C6'/>
</linearGradient>
`);
text1.setAttribute('fill', 'url("#rareGradient")');
break;
case Rarity.Fresh:
svg.insertAdjacentHTML('beforeend', `
<linearGradient id='freshGradient' y2='25%'>
<stop offset='0%' stop-color='#FF8EDD'/>
<stop offset='25%' stop-color='#FFEC9F'/>
<stop offset='50%' stop-color='#B84386'/>
<stop offset='75%' stop-color='#2BEFC8'/>
<stop offset='100%' stop-color='#FF8EDD'/>
</linearGradient>
`);
text1.setAttribute('fill', 'url("#freshGradient")');
break;
}
if (card.line1 && card.line2) {
const tspan1 = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
tspan1.setAttribute('y', '122');
tspan1.appendChild(document.createTextNode(card.line1));
text1.appendChild(tspan1);
const tspan2 = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
tspan2.setAttribute('x', '50%');
tspan2.setAttribute('y', '216');
tspan2.appendChild(document.createTextNode(card.line2));
text1.appendChild(tspan2);
} else
text1.innerHTML = card.name;
svg.appendChild(text1);
// Size
svg.insertAdjacentHTML('beforeend', `<image href='assets/external/Game Assets/CardCost_0${card.rarity}.png' width='80' height='80' transform='translate(12 798) rotate(-45) scale(1.33)'/>`);
svg.insertAdjacentHTML('beforeend', `<text fill='white' stroke='${card.rarity == Rarity.Common ? '#482BB4' : card.rarity == Rarity.Rare ? '#8B7E25' : '#481EF9'}' paint-order='stroke' stroke-width='5' font-size='48' y='816' x='87'>${size}</text>`);
// Special cost
const g2 = document.createElementNS('http://www.w3.org/2000/svg', 'g');
g2.setAttribute('class', 'specialCost');
g2.setAttribute('transform', 'translate(170 806) scale(0.32)');
svg.appendChild(g2);
for (let i = 0; i < card.specialCost; i++) {
let rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
const image = document.createElementNS('http://www.w3.org/2000/svg', 'image');
image.setAttribute('href', 'assets/SpecialOverlay.png');
for (const el of [ rect, image ]) {
el.setAttribute('x', (110 * (i % 5)).toString());
el.setAttribute('y', (-125 * Math.floor(i / 5)).toString());
el.setAttribute('width', '95');
el.setAttribute('height', '95');
g2.appendChild(el);
}
}
this.card = card;
}
}

View File

@@ -1,11 +1,15 @@
class Deck {
class SavedDeck {
name: string;
sleeves: number;
cards: number[];
isReadOnly: boolean;
upgrades: number[];
isReadOnly?: boolean;
constructor(name: string, cards: number[], isReadOnly: boolean) {
constructor(name: string, sleeves: number, cards: number[], upgrades: number[], isReadOnly: boolean) {
this.name = name;
this.sleeves = sleeves;
this.cards = cards;
this.upgrades = upgrades;
this.isReadOnly = isReadOnly;
}
@@ -19,3 +23,17 @@ class Deck {
return true;
}
}
class Deck {
name: string;
sleeves: number;
cards: Card[];
upgrades: number[];
constructor(name: string, sleeves: number, cards: Card[], upgrades: number[]) {
this.name = name;
this.sleeves = sleeves;
this.cards = cards;
this.upgrades = upgrades;
}
}

View File

@@ -1,19 +1,27 @@
/** A UUID used to identify the client. */
let clientToken = window.localStorage.getItem('clientToken') || '';
/** The data of the current game, or null if not in a game. */
let currentGame: {
id: string,
interface Game {
state: GameState,
/** The list of players in the current game. */
players: Player[],
/** The maximum number of players in the game. */
maxPlayers: number,
/** The current one-based turn number, or 0 if redraw decisions are being made. */
turnNumber: number,
/** The total turn time limit in seconds. */
turnTimeLimit: number | null,
/** The time remaining in the current turn in seconds. */
turnTimeLeft: number | null,
/** The number of game wins needed to win the set, or null if no goal win count is set. */
goalWinCount: number | null,
}
/** A UUID used to identify the client. */
let clientToken = window.localStorage.getItem('clientToken') || '';
/** The data of the current game, or null if not in a game. */
let currentGame: {
id: string,
game: Game,
/** The user's player data, or null if they are spectating. */
me: PlayerData | null,
turnNumber: number,
turnTimeLimit: number | null,
turnTimeLeft: number | null,
goalWinCount: number | null,
/** The WebSocket used for receiving game events, or null if not yet connected. */
webSocket: WebSocket | null
} | null = null;
@@ -24,7 +32,7 @@ let currentReplay: {
games: {
stage: Stage,
playerData: {
deck: Card[],
deck: Deck,
initialDrawOrder: number[],
drawOrder: number[],
won: boolean

View File

@@ -9,6 +9,7 @@ const deckCardListView = document.getElementById('deckCardListView')!;
const addDeckControls = document.getElementById('addDeckControls')!;
const newDeckButton = document.getElementById('newDeckButton') as HTMLButtonElement;
const importDeckButton = document.getElementById('importDeckButton') as HTMLButtonElement;
const deckSleevesButton = document.getElementById('deckSleevesButton') as HTMLButtonElement;
const deckEditButton = document.getElementById('deckEditButton') as HTMLButtonElement;
const deckListTestButton = document.getElementById('deckListTestButton') as HTMLButtonElement;
const deckExportButton = document.getElementById('deckExportButton') as HTMLButtonElement;
@@ -22,6 +23,11 @@ const deckExportTextBox = document.getElementById('deckExportTextBox') as HTMLTe
const deckExportAllButton = document.getElementById('deckExportAllButton') as HTMLButtonElement;
const deckSleevesDialog = document.getElementById('deckSleevesDialog') as HTMLDialogElement;
const deckSleevesForm = document.getElementById('deckSleevesForm') as HTMLFormElement;
const deckSleevesButtons = deckSleevesForm.getElementsByTagName('input');
const deckSleevesOkButton = document.getElementById('deckSleevesOkButton') as HTMLButtonElement;
const deckImportDialog = document.getElementById('deckImportDialog') as HTMLDialogElement;
const deckImportForm = document.getElementById('deckImportForm') as HTMLFormElement;
const deckImportTextBox = document.getElementById('deckImportTextBox') as HTMLTextAreaElement;
@@ -39,7 +45,7 @@ const deckImportFileBox = document.getElementById('deckImportFileBox') as HTMLIn
const deckImportErrorBox = document.getElementById('deckImportErrorBox')!;
const deckImportOkButton = document.getElementById('deckImportOkButton') as HTMLButtonElement;
const deckButtons = new CheckButtonGroup<Deck>(deckList);
const deckButtons = new CheckButtonGroup<SavedDeck>(deckList);
let deckListTouchMode = false;
let draggingDeckButton: Element | null = null;
@@ -85,7 +91,7 @@ deckViewBackButton.addEventListener('click', e => {
});
function saveDecks() {
const json = JSON.stringify(decks.filter(d => !d.isReadOnly), [ 'name', 'cards' ]);
const json = JSON.stringify(decks.filter(d => !d.isReadOnly), [ 'name', 'cards', 'sleeves' ]);
localStorage.setItem('decks', json);
}
@@ -93,13 +99,13 @@ function saveDecks() {
const decksString = localStorage.getItem('decks');
if (decksString) {
for (const deck of JSON.parse(decksString)) {
decks.push(new Deck(deck.name, deck.cards, false));
decks.push(new SavedDeck(deck.name, deck.sleeves ?? 0, deck.cards, deck.upgrades ?? new Array(15), false));
}
} else {
const lastDeckString = localStorage.getItem('lastDeck');
const lastDeck = lastDeckString?.split(/\+/)?.map(s => parseInt(s));
if (lastDeck && lastDeck.length == 15) {
decks.push(new Deck('Custom Deck', lastDeck, false));
decks.push(new SavedDeck('Custom Deck', 0, lastDeck, new Array(15), false));
saveDecks();
}
localStorage.removeItem('lastDeck');
@@ -110,9 +116,11 @@ function saveDecks() {
}
}
function createDeckButton(deck: Deck) {
function createDeckButton(deck: SavedDeck) {
const buttonElement = document.createElement('button');
buttonElement.className = 'deckButton';
buttonElement.type = 'button';
buttonElement.dataset.sleeves = deck.sleeves.toString();
const button = new CheckButton(buttonElement);
deckButtons.add(button, deck);
buttonElement.addEventListener('click', () => {
@@ -199,14 +207,17 @@ function deckButton_drop(e: DragEvent) {
}
}
function importDecks(decksToImport: (Deck | number[])[]) {
let newSelectedDeck: Deck | null = null;
function importDecks(decksToImport: (SavedDeck | number[])[]) {
let newSelectedDeck: SavedDeck | null = null;
for (const el of decksToImport) {
let deck;
if (el instanceof Array)
deck = new Deck(`Imported Deck ${decks.length + 1}`, el, false);
deck = new SavedDeck(`Imported Deck ${decks.length + 1}`, 0, el, new Array(15), false);
else {
deck = el;
deck.sleeves ??= 0;
deck.upgrades ??= new Array(15);
deck.isReadOnly = false;
if (!deck.name) deck.name = `Imported Deck ${decks.length + 1}`;
}
createDeckButton(deck);
@@ -223,7 +234,7 @@ function importDecks(decksToImport: (Deck | number[])[]) {
}
newDeckButton.addEventListener('click', () => {
selectedDeck = new Deck(`Deck ${decks.length + 1}`, [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], false);
selectedDeck = new SavedDeck(`Deck ${decks.length + 1}`, 0, new Array(15), new Array(15), false);
createDeckButton(selectedDeck);
decks.push(selectedDeck);
editDeck();
@@ -287,9 +298,29 @@ function parseDecksForImport(s: string) {
// TODO: add support for tblturf.ink
}
deckSleevesButton.addEventListener('click', () => {
if (selectedDeck == null) return;
deckSleevesButtons[selectedDeck.sleeves].checked = true;
deckSleevesDialog.showModal();
});
deckSleevesForm.addEventListener('submit', e => {
if (e.submitter == deckSleevesOkButton && selectedDeck != null) {
let i = 0;
for (const button of deckSleevesButtons) {
if (button.checked) {
selectedDeck.sleeves = i;
deckButtons.entries[decks.indexOf(selectedDeck)].button.buttonElement.dataset.sleeves = i.toString();
saveDecks();
return;
}
i++;
}
}
});
deckEditButton.addEventListener('click', editDeck);
deckListTestButton.addEventListener('click', _ => {
deckListTestButton.addEventListener('click', () => {
testStageButtons.deselect();
testStageSelectionDialog.showModal();
});
@@ -315,9 +346,10 @@ function selectDeck() {
deckListTestButton.disabled = false;
deckExportButton.disabled = false;
deckCopyButton.disabled = false;
deckEditButton.disabled = selectedDeck.isReadOnly;
deckRenameButton.disabled = selectedDeck.isReadOnly;
deckDeleteButton.disabled = selectedDeck.isReadOnly;
deckSleevesButton.disabled = selectedDeck.isReadOnly ?? false;
deckEditButton.disabled = selectedDeck.isReadOnly ?? false;
deckRenameButton.disabled = selectedDeck.isReadOnly ?? false;
deckDeleteButton.disabled = selectedDeck.isReadOnly ?? false;
deckViewSize.innerText = size.toString();
deckListPage.classList.add('showingDeck');
}
@@ -331,6 +363,7 @@ function deselectDeck() {
deckListTestButton.disabled = true;
deckExportButton.disabled = true;
deckCopyButton.disabled = true;
deckSleevesButton.disabled = true;
deckEditButton.disabled = true;
deckRenameButton.disabled = true;
deckDeleteButton.disabled = true;
@@ -362,7 +395,7 @@ deckRenameButton.addEventListener('click', () => {
deckCopyButton.addEventListener('click', () => {
if (selectedDeck == null) return;
importDecks([ new Deck(`${selectedDeck.name} - Copy`, Array.from(selectedDeck.cards), false) ]);
importDecks([ new SavedDeck(`${selectedDeck.name} - Copy`, selectedDeck.sleeves, Array.from(selectedDeck.cards), Array.from(selectedDeck.upgrades), false) ]);
});
deckDeleteButton.addEventListener('click', () => {

View File

@@ -153,7 +153,7 @@ function initTest(stage: Stage) {
clear();
testMode = true;
gamePage.classList.add('deckTest');
currentGame = { id: 'test', state: GameState.Ongoing, maxPlayers: 2, players: [ ], webSocket: null, turnNumber: 1, turnTimeLimit: null, turnTimeLeft: null, goalWinCount: null, me: { playerIndex: 0, move: null, deck: null, hand: null, cardsUsed: [ ] } };
currentGame = { id: 'test', game: { state: GameState.Ongoing, maxPlayers: 2, players: [ ], turnNumber: 1, turnTimeLimit: null, turnTimeLeft: null, goalWinCount: null }, me: { playerIndex: 0, move: null, deck: null, hand: null, cardsUsed: [ ] }, webSocket: null };
board.resize(stage.copyGrid());
const startSpaces = stage.getStartSpaces(2);
board.startSpaces = startSpaces;
@@ -163,6 +163,8 @@ function initTest(stage: Stage) {
for (var o of playerBars)
o.element.hidden = true;
for (var el of playContainers)
el.hidden = true;
testPlacements.splice(0);
testUndoButton.enabled = false;
clearChildren(testPlacementList);
@@ -184,79 +186,60 @@ function initTest(stage: Stage) {
}
replayNextButton.buttonElement.addEventListener('click', _ => {
if (currentGame == null || currentReplay == null || currentGame.state == GameState.GameEnded || currentGame.state == GameState.SetEnded)
if (currentGame == null || currentReplay == null || currentGame.game.state == GameState.GameEnded || currentGame.game.state == GameState.SetEnded)
return;
if (replayAnimationAbortController) {
replayUpdateHand();
replayAnimationAbortController.abort();
replayAnimationAbortController = null;
turnNumberLabel.turnNumber = currentGame.turnNumber;
turnNumberLabel.turnNumber = currentGame.game.turnNumber;
board.refresh();
const scores = board.getScores();
for (let i = 0; i < currentGame.players.length; i++) {
for (let i = 0; i < currentGame.game.players.length; i++) {
updateStats(i, scores);
}
}
clearPlayContainers();
if (currentGame.turnNumber == 0) {
if (currentGame.game.turnNumber == 0) {
// Show redraw decisions.
replayPreviousButton.enabled = true;
currentGame.state = GameState.Ongoing;
currentGame.turnNumber++;
turnNumberLabel.turnNumber = currentGame.turnNumber;
currentGame.game.state = GameState.Ongoing;
currentGame.game.turnNumber++;
turnNumberLabel.turnNumber = currentGame.game.turnNumber;
replayUpdateHand();
} else if (currentGame.turnNumber > 12) {
currentGame.state = currentReplay.gameNumber + 1 >= currentReplay.games.length ? GameState.SetEnded : GameState.GameEnded;
} else if (currentGame.game.turnNumber > 12) {
currentGame.game.state = currentReplay.gameNumber + 1 >= currentReplay.games.length ? GameState.SetEnded : GameState.GameEnded;
gameButtonsContainer.hidden = true;
gamePage.classList.add('gameEnded');
showResult();
} else {
const moves = currentReplay.turns[currentGame.turnNumber - 1];
const moves = currentReplay.turns[currentGame.game.turnNumber - 1];
const result = board.makePlacements(moves);
currentReplay.placements.push(result);
let anySpecialAttacks = false;
// Show the cards that were played.
const entry = handButtons.entries.find(b => b.value.number == moves[currentReplay!.watchingPlayer].card.number);
if (entry) entry.button.checked = true;
for (let i = 0; i < currentGame.players.length; i++) {
const player = currentGame.players[i];
const move = moves[i];
const button = new CardButton(move.card);
button.buttonElement.disabled = true;
if ((move as PlayMove).isSpecialAttack) {
anySpecialAttacks = true;
player.specialPoints -= (move as PlayMove).card.specialCost;
button.buttonElement.classList.add('specialAttack');
} else if (move.isPass) {
player.passes++;
player.specialPoints++;
const el = document.createElement('div');
el.className = 'passLabel';
el.innerText = 'Pass';
button.buttonElement.appendChild(el);
}
playContainers[i].append(button.buttonElement);
}
for (const p of result.specialSpacesActivated) {
const space = board.grid[p.x][p.y];
const player2 = currentGame.players[space & 3];
const player2 = currentGame.game.players[space & 3];
player2.specialPoints++;
player2.totalSpecialPoints++;
}
currentGame.turnNumber++;
currentGame.game.turnNumber++;
for (let i = 0; i < currentGame.game.players.length; i++)
showReady(i);
replayAnimationAbortController = new AbortController();
(async () => {
await playInkAnimations({ game: { state: GameState.Ongoing, board: null, turnNumber: currentGame.turnNumber, players: currentGame.players }, moves, placements: result.placements, specialSpacesActivated: result.specialSpacesActivated }, anySpecialAttacks, replayAnimationAbortController.signal);
turnNumberLabel.turnNumber = currentGame.turnNumber;
await playInkAnimations({ game: { state: GameState.Ongoing, board: null, turnNumber: currentGame.game.turnNumber, players: currentGame.game.players }, moves, placements: result.placements, specialSpacesActivated: result.specialSpacesActivated }, replayAnimationAbortController.signal);
turnNumberLabel.turnNumber = currentGame.game.turnNumber;
clearPlayContainers();
if (currentGame.turnNumber > 12) {
currentGame.state = currentReplay.gameNumber + 1 >= currentReplay.games.length ? GameState.SetEnded : GameState.GameEnded;
if (currentGame.game.turnNumber > 12) {
currentGame.game.state = currentReplay.gameNumber + 1 >= currentReplay.games.length ? GameState.SetEnded : GameState.GameEnded;
gameButtonsContainer.hidden = true;
gamePage.classList.add('gameEnded');
showResult();
@@ -267,52 +250,52 @@ replayNextButton.buttonElement.addEventListener('click', _ => {
});
replayPreviousButton.buttonElement.addEventListener('click', _ => {
if (currentGame == null || currentReplay == null || currentGame.turnNumber == 0) return;
if (currentGame == null || currentReplay == null || currentGame.game.turnNumber == 0) return;
replayAnimationAbortController?.abort();
replayAnimationAbortController = null;
if (currentGame.state == GameState.GameEnded || currentGame.state == GameState.SetEnded) {
for (let i = 0; i < currentGame.players.length; i++) {
if (currentGame.game.state == GameState.GameEnded || currentGame.game.state == GameState.SetEnded) {
for (let i = 0; i < currentGame.game.players.length; i++) {
if (currentReplay.games[currentReplay.gameNumber].playerData[i].won)
currentGame.players[i].gamesWon--;
playerBars[i].winCounter.wins = currentGame.players[i].gamesWon;
currentGame.game.players[i].gamesWon--;
playerBars[i].winCounter.wins = currentGame.game.players[i].gamesWon;
}
}
if (currentGame.turnNumber > 1) {
if (currentGame.game.turnNumber > 1) {
const result = currentReplay.placements.pop();
if (!result) return;
clearPlayContainers();
for (let i = 0; i < currentGame.players.length; i++) {
for (let i = 0; i < currentGame.game.players.length; i++) {
const el = playerBars[i].resultElement;
el.innerText = '';
}
undoTurn(result);
}
currentGame.turnNumber--;
currentGame.game.turnNumber--;
replayNextButton.enabled = true;
gamePage.classList.remove('gameEnded');
handContainer.hidden = false;
gameButtonsContainer.hidden = false;
if (currentGame.turnNumber > 0) {
currentGame.state = GameState.Ongoing;
turnNumberLabel.turnNumber = currentGame.turnNumber;
if (currentGame.game.turnNumber > 0) {
currentGame.game.state = GameState.Ongoing;
turnNumberLabel.turnNumber = currentGame.game.turnNumber;
const scores = board.getScores();
for (let i = 0; i < currentGame.players.length; i++) {
const move = currentReplay.turns[currentGame.turnNumber - 1][i];
for (let i = 0; i < currentGame.game.players.length; i++) {
const move = currentReplay.turns[currentGame.game.turnNumber - 1][i];
if (move.isPass) {
currentGame.players[i].passes--;
currentGame.players[i].specialPoints--;
currentGame.game.players[i].passes--;
currentGame.game.players[i].specialPoints--;
} else if ((move as PlayMove).isSpecialAttack)
currentGame.players[i].specialPoints += (move as PlayMove).card.specialCost;
currentGame.game.players[i].specialPoints += (move as PlayMove).card.specialCost;
updateStats(i, scores);
}
} else {
currentGame.state = GameState.Redraw;
currentGame.game.state = GameState.Redraw;
replayPreviousButton.enabled = false;
turnNumberLabel.turnNumber = null;
}
@@ -324,7 +307,7 @@ replayPreviousButton.buttonElement.addEventListener('click', _ => {
function undoTurn(turn: PlacementResults) {
for (const p of turn.specialSpacesActivated) {
const space = board.grid[p.x][p.y];
const player2 = currentGame!.players[space & 3];
const player2 = currentGame!.game.players[space & 3];
if (player2) {
player2.specialPoints--;
player2.totalSpecialPoints--;
@@ -352,23 +335,23 @@ function replaySwitchGame(gameNumber: number) {
replayAnimationAbortController?.abort();
replayAnimationAbortController = null;
for (let i = 0; i < currentGame.players.length; i++) {
currentGame.players[i].specialPoints = 0;
currentGame.players[i].totalSpecialPoints = 0;
currentGame.players[i].passes = 0;
currentGame.players[i].gamesWon = 0;
for (let i = 0; i < currentGame.game.players.length; i++) {
currentGame.game.players[i].specialPoints = 0;
currentGame.game.players[i].totalSpecialPoints = 0;
currentGame.game.players[i].passes = 0;
currentGame.game.players[i].gamesWon = 0;
for (let j = 0; j < gameNumber; j++) {
if (currentReplay.games[j].playerData[i].won)
currentGame.players[i].gamesWon++;
currentGame.game.players[i].gamesWon++;
}
playerBars[i].winCounter.wins = currentGame.players[i].gamesWon;
playerBars[i].winCounter.wins = currentGame.game.players[i].gamesWon;
}
currentReplay.gameNumber = gameNumber;
currentReplay.turns = currentReplay.games[gameNumber].turns;
currentReplay.placements.splice(0);
currentGame.state = GameState.Ongoing;
currentGame.turnNumber = 0;
currentGame.game.state = GameState.Ongoing;
currentGame.game.turnNumber = 0;
clearPlayContainers();
gamePage.classList.remove('gameEnded');
replayPreviousGameButton.enabled = gameNumber > 0;
@@ -381,8 +364,8 @@ function replaySwitchGame(gameNumber: number) {
const stage = currentReplay.games[gameNumber].stage;
board.resize(stage.copyGrid());
const startSpaces = stage.getStartSpaces(currentGame.players.length);
for (let i = 0; i < currentGame.players.length; i++) {
const startSpaces = stage.getStartSpaces(currentGame.game.players.length);
for (let i = 0; i < currentGame.game.players.length; i++) {
board.grid[startSpaces[i].x][startSpaces[i].y] = Space.SpecialInactive1 | i;
playerBars[i].points = 1;
playerBars[i].pointsDelta = null;
@@ -400,18 +383,18 @@ flipButton.addEventListener('click', () => {
replayAnimationAbortController.abort();
replayAnimationAbortController = null;
clearPlayContainers();
turnNumberLabel.turnNumber = currentGame.turnNumber;
turnNumberLabel.turnNumber = currentGame.game.turnNumber;
const scores = board.getScores();
for (let i = 0; i < currentGame.players.length; i++) {
for (let i = 0; i < currentGame.game.players.length; i++) {
updateStats(i, scores);
}
}
if (currentReplay) {
currentReplay.watchingPlayer++;
if (currentReplay.watchingPlayer >= currentGame.players.length)
if (currentReplay.watchingPlayer >= currentGame.game.players.length)
currentReplay.watchingPlayer = 0;
gamePage.dataset.myPlayerIndex = currentReplay.watchingPlayer.toString();
gamePage.dataset.uiBaseColourIsSpecialColour = currentGame.players[currentReplay.watchingPlayer].uiBaseColourIsSpecialColour?.toString();
gamePage.dataset.uiBaseColourIsSpecialColour = currentGame.game.players[currentReplay.watchingPlayer].uiBaseColourIsSpecialColour?.toString();
board.flip = currentReplay.watchingPlayer % 2 != 0;
clearShowDeck();
replayUpdateHand();
@@ -510,7 +493,7 @@ function loadPlayers(players: Player[]) {
const scores = board.getScores();
for (let i = 0; i < players.length; i++) {
const player = players[i];
currentGame!.players[i] = players[i];
currentGame!.game.players[i] = players[i];
playerBars[i].name = player.name;
playerBars[i].winCounter.wins = players[i].gamesWon;
updateStats(i, scores);
@@ -522,6 +505,7 @@ function loadPlayers(players: Player[]) {
}
for (let i = 0; i < playerBars.length; i++) {
playerBars[i].visible = i < players.length;
playContainers[i].hidden = i >= players.length;
}
}
@@ -530,15 +514,15 @@ function updateStats(playerIndex: number, scores: number[]) {
playerBars[playerIndex].points = scores[playerIndex];
playerBars[playerIndex].pointsDelta = 0;
playerBars[playerIndex].pointsTo = 0;
playerBars[playerIndex].specialPoints = currentGame.players[playerIndex].specialPoints;
playerBars[playerIndex].statSpecialPointsElement.innerText = currentGame.players[playerIndex].totalSpecialPoints.toString();
playerBars[playerIndex].statPassesElement.innerText = currentGame.players[playerIndex].passes.toString();
playerBars[playerIndex].specialPoints = currentGame.game.players[playerIndex].specialPoints;
playerBars[playerIndex].statSpecialPointsElement.innerText = currentGame.game.players[playerIndex].totalSpecialPoints.toString();
playerBars[playerIndex].statPassesElement.innerText = currentGame.game.players[playerIndex].passes.toString();
}
/** Shows the waiting indication for the specified player. */
function showWaiting(playerIndex: number) {
const el = document.createElement('div');
el.className = 'cardBack waiting';
el.className = 'waiting';
playContainers[playerIndex].appendChild(el);
}
@@ -547,7 +531,7 @@ function showReady(playerIndex: number) {
clearChildren(playContainers[playerIndex]);
const el = document.createElement('div');
el.className = 'cardBack';
el.innerText = 'Ready';
el.dataset.sleeves = (currentGame?.game.players[playerIndex].sleeves ?? 0).toString();
playContainers[playerIndex].appendChild(el);
}
@@ -566,7 +550,7 @@ function resetPlayControls() {
if (canPlay && currentGame?.me?.hand != null) {
for (let i = 0; i < 4; i++) {
canPlayCard[i] = board.canPlayCard(currentGame.me.playerIndex, currentGame.me.hand[i], false);
canPlayCardAsSpecialAttack[i] = currentGame.players[currentGame.me.playerIndex].specialPoints >= currentGame.me.hand[i].specialCost
canPlayCardAsSpecialAttack[i] = currentGame.game.players[currentGame.me.playerIndex].specialPoints >= currentGame.me.hand[i].specialCost
&& board.canPlayCard(currentGame.me.playerIndex, currentGame.me.hand[i], true);
handButtons.entries[i].button.enabled = canPlayCard[i];
}
@@ -602,7 +586,9 @@ async function playInkAnimations(data: {
moves: Move[],
placements: Placement[],
specialSpacesActivated: Point[]
}, anySpecialAttacks: boolean, abortSignal?: AbortSignal) {
}, abortSignal?: AbortSignal) {
if (!currentGame) return;
const inkPlaced = new Set<number>();
const placements = data.placements;
board.clearHighlight();
@@ -611,6 +597,46 @@ async function playInkAnimations(data: {
board.specialAttack = false;
canPlay = false;
timeLabel.faded = true;
// Show the cards that were played.
let anySpecialAttacks = false;
for (let i = 0; i < currentGame.game.players.length; i++) {
const player = currentGame.game.players[i];
const move = data.moves[i];
if ((move as PlayMove).isSpecialAttack)
anySpecialAttacks = true;
function addCardDisplay() {
clearChildren(playContainers[i]);
const display = new CardDisplay(move.card, 1);
playContainers[i].append(display.element);
if ((move as PlayMove).isSpecialAttack) {
if (currentReplay) player.specialPoints -= (move as PlayMove).card.specialCost;
const el = document.createElement('div');
el.className = 'specialAttackLabel';
el.innerText = 'Special Attack!';
playContainers[i].appendChild(el);
display.element.classList.add('specialAttack');
} else if (move.isPass) {
if (currentReplay) {
player.passes++;
player.specialPoints++;
}
const el = document.createElement('div');
el.className = 'passLabel';
el.innerText = 'Pass';
playContainers[i].appendChild(el);
}
}
const back = playContainers[i].firstElementChild as HTMLElement;
if (back) {
back.style.setProperty('animation', '0.1s ease-in forwards flipCardOut');
back.addEventListener('animationend', addCardDisplay);
} else
addCardDisplay();
}
await delay(anySpecialAttacks ? 3000 : 1000, abortSignal);
for (let i = 0; i < data.game.players.length; i++) {
if ((data.moves[i] as PlayMove).isSpecialAttack)
@@ -661,7 +687,7 @@ function showResult() {
turnNumberLabel.turnNumber = null;
let winners = [ 0 ]; let maxPoints = playerBars[0].points;
for (let i = 1; i < currentGame.players.length; i++) {
for (let i = 1; i < currentGame.game.players.length; i++) {
if (playerBars[i].points > maxPoints) {
winners.splice(0);
winners.push(i);
@@ -670,7 +696,7 @@ function showResult() {
winners.push(i);
}
for (let i = 0; i < currentGame.players.length; i++) {
for (let i = 0; i < currentGame.game.players.length; i++) {
const el = playerBars[i].resultElement;
if (winners.includes(i)) {
if (winners.length == 1) {
@@ -678,8 +704,10 @@ function showResult() {
el.classList.remove('lose');
el.classList.remove('draw');
el.innerText = 'Victory';
if (currentReplay) currentGame.players[i].gamesWon++;
playerBars[i].winCounter.wins++;
if (currentReplay) {
currentGame.game.players[i].gamesWon++;
playerBars[i].winCounter.wins++;
}
} else {
el.classList.remove('win');
el.classList.remove('lose');
@@ -704,9 +732,9 @@ function showResult() {
replayNextButton.buttonElement.hidden = true;
replayNextGameButton.buttonElement.hidden = true;
flipButton.hidden = true;
if (currentGame.state == GameState.SetEnded) {
if (currentGame.game.state == GameState.SetEnded) {
leaveButton.hidden = false;
nextGameButton.buttonElement.hidden = currentGame.goalWinCount != null;
nextGameButton.buttonElement.hidden = currentGame.game.goalWinCount != null;
shareReplayLinkButton.hidden = false;
canShareReplay = navigator.canShare && navigator.canShare({ url: window.location.href, title: 'Tableturf Battle Replay' });
shareReplayLinkButton.innerText = canShareReplay ? 'Share replay link' : 'Copy replay link';
@@ -714,7 +742,7 @@ function showResult() {
leaveButton.hidden = currentGame.me != null;
nextGameButton.buttonElement.hidden = currentGame.me == null;
}
nextGameButton.enabled = currentGame.me != null && !currentGame.players[currentGame.me.playerIndex].isReady;
nextGameButton.enabled = currentGame.me != null && !currentGame.game.players[currentGame.me.playerIndex].isReady;
nextGameButton.buttonElement.innerHTML = nextGameButton.enabled ? 'Next game' : '<div class="loadingSpinner"></div> Waiting for other player';
}
}
@@ -726,9 +754,9 @@ function clearShowDeck() {
showDeckButtons.splice(0);
}
function populateShowDeck(deck: Card[]) {
function populateShowDeck(deck: Deck) {
if (showDeckButtons.length == 0) {
for (const card of deck) {
for (const card of deck.cards) {
const button = new CardButton(card);
button.buttonElement.disabled = true;
showDeckButtons.push(button);
@@ -764,7 +792,7 @@ function updateHandAndDeck(playerData: PlayerData) {
handButtons.add(button, card);
button.buttonElement.addEventListener('click', e => {
if (!button.enabled) {
if (specialButton.checked && currentGame!.players[currentGame!.me!.playerIndex].specialPoints < card.specialCost)
if (specialButton.checked && currentGame!.game.players[currentGame!.me!.playerIndex].specialPoints < card.specialCost)
cardHint.showError('Not enough special points.');
else if (!(specialButton.checked ? canPlayCardAsSpecialAttack : canPlayCard)[i])
cardHint.showError('No place to play this card.');
@@ -832,13 +860,13 @@ function replayUpdateHand() {
b.buttonElement.parentElement!.className = '';
let indices;
if (currentGame.turnNumber == 0) {
if (currentGame.game.turnNumber == 0) {
indices = playerData.initialDrawOrder;
} else {
indices = playerData.drawOrder.slice(0, 4);
for (let i = 0; i < currentGame.turnNumber - 1; i++) {
for (let i = 0; i < currentGame.game.turnNumber - 1; i++) {
const move = currentReplay.turns[i][currentReplay.watchingPlayer];
let j = indices.findIndex(k => playerData.deck[k].number == move.card.number);
let j = indices.findIndex(k => playerData.deck.cards[k].number == move.card.number);
if (j < 0) j = indices.findIndex(k => k < 0 || k >= 15);
if (j >= 0) {
showDeckButtons[indices[j]].buttonElement.parentElement!.className = 'used';
@@ -856,7 +884,7 @@ function replayUpdateHand() {
for (let i = 0; i < 4; i++) {
if (indices[i] >= 15) continue; // Accounts for an old bug in the server that corrupted the initialDrawOrder replay fields.
const card = playerData.deck[indices[i]];
const card = playerData.deck.cards[indices[i]];
const button = new CardButton(card);
button.buttonElement.disabled = true;
handButtons.add(button, card);
@@ -906,7 +934,7 @@ passButton.buttonElement.addEventListener('click', passButton_click);
function specialButton_click(e: Event) {
if (!specialButton.enabled) {
if (currentGame!.me!.hand!.every(c => currentGame!.players[currentGame!.me!.playerIndex].specialPoints < c.specialCost))
if (currentGame!.me!.hand!.every(c => currentGame!.game.players[currentGame!.me!.playerIndex].specialPoints < c.specialCost))
cardHint.showError('Not enough special points.');
else if (!canPlayCardAsSpecialAttack.includes(true))
cardHint.showError('No place to play a special attack.');
@@ -1041,7 +1069,7 @@ board.onhighlightchange = dScores => {
timeLabel.ontimeout = () => {
if (currentGame == null || !canPlay) return;
if (currentGame.turnNumber == 0) {
if (currentGame.game.turnNumber == 0) {
let req = new XMLHttpRequest();
req.open('POST', `${config.apiBaseUrl}/games/${currentGame!.id}/redraw`);
req.addEventListener('error', () => communicationError());
@@ -1263,15 +1291,15 @@ function updateRGB(playerIndex: number, colourIndex: number) {
setColour(playerIndex, colourIndex, rgb);
}
function setColour(playerIndex: number, colourIndex: number, colour: Colour) {
if (!currentGame || playerIndex >= currentGame.players.length) return;
if (!currentGame || playerIndex >= currentGame.game.players.length) return;
if (colourIndex == 0) {
currentGame.players[playerIndex].colour = colour;
currentGame.game.players[playerIndex].colour = colour;
document.body.style.setProperty(`--primary-colour-${playerIndex + 1}`, `rgb(${colour.r}, ${colour.g}, ${colour.b})`);
} else if (colourIndex == 1) {
currentGame.players[playerIndex].specialColour = colour;
currentGame.game.players[playerIndex].specialColour = colour;
document.body.style.setProperty(`--special-colour-${playerIndex + 1}`, `rgb(${colour.r}, ${colour.g}, ${colour.b})`);
} else {
currentGame.players[playerIndex].specialAccentColour = colour;
currentGame.game.players[playerIndex].specialAccentColour = colour;
document.body.style.setProperty(`--special-accent-colour-${playerIndex + 1}`, `rgb(${colour.r}, ${colour.g}, ${colour.b})`);
}
}

View File

@@ -15,7 +15,7 @@ const lobbyStageSection = document.getElementById('lobbyStageSection')!;
const lobbyStageSubmitButton = document.getElementById('submitStageButton') as HTMLButtonElement;
const lobbyDeckSection = document.getElementById('lobbyDeckSection')!;
const lobbyDeckList = document.getElementById('lobbyDeckList')!;
const lobbyDeckButtons = new CheckButtonGroup<Deck>(lobbyDeckList);
const lobbyDeckButtons = new CheckButtonGroup<SavedDeck>(lobbyDeckList);
const lobbyDeckSubmitButton = document.getElementById('submitDeckButton') as HTMLButtonElement;
const lobbyTimeLimitBox = document.getElementById('lobbyTimeLimitBox') as HTMLInputElement;
@@ -94,7 +94,7 @@ function lobbyResetSlots() {
playerListItems.splice(0);
lobbyWinCounters.splice(0);
for (let i = 0; i < currentGame.maxPlayers; i++) {
for (let i = 0; i < currentGame.game.maxPlayers; i++) {
var el = document.createElement('li');
el.className = 'empty';
el.innerText = 'Waiting...';
@@ -113,8 +113,8 @@ function clearReady() {
if (!currentGame) throw new Error('No current game');
stageSelectionFormSubmitButton.disabled = false;
stageSelectionFormLoadingSection.hidden = true;
for (var i = 0; i < currentGame.players.length; i++) {
currentGame.players[i].isReady = false;
for (var i = 0; i < currentGame.game.players.length; i++) {
currentGame.game.players[i].isReady = false;
playerListItems[i].className = 'filled';
}
}
@@ -122,7 +122,7 @@ function clearReady() {
function lobbyAddPlayer(playerIndex: number) {
if (!currentGame) throw new Error('No current game');
const listItem = playerListItems[playerIndex];
const player = currentGame.players[playerIndex];
const player = currentGame.game.players[playerIndex];
listItem.innerText = player.name;
listItem.className = player.isReady ? 'filled ready' : 'filled';
@@ -131,7 +131,7 @@ function lobbyAddPlayer(playerIndex: number) {
el.title = 'Battles won';
listItem.appendChild(el);
const winCounter = new WinCounter(el);
winCounter.wins = currentGame.players[playerIndex].gamesWon;
winCounter.wins = currentGame.game.players[playerIndex].gamesWon;
lobbyWinCounters.push(winCounter);
}
@@ -151,7 +151,9 @@ function initDeckSelection() {
const buttonElement = document.createElement('button');
buttonElement.type = 'button';
buttonElement.className = 'deckButton';
buttonElement.innerText = deck.name;
buttonElement.dataset.sleeves = deck.sleeves.toString();
const button = new CheckButton(buttonElement);
lobbyDeckButtons.add(button, deck);
@@ -202,6 +204,7 @@ deckSelectionForm.addEventListener('submit', e => {
data.append('clientToken', clientToken);
data.append('deckName', selectedDeck.name);
data.append('deckCards', selectedDeck.cards.join('+'));
data.append('deckSleeves', selectedDeck.sleeves.toString());
req.send(data.toString());
localStorage.setItem('lastDeckName', selectedDeck.name);

View File

@@ -6,6 +6,7 @@ interface Player {
specialColour: Colour;
specialAccentColour: Colour;
uiBaseColourIsSpecialColour?: boolean;
sleeves: number;
totalSpecialPoints: number;
passes: number;
gamesWon: number;

View File

@@ -55,7 +55,7 @@ class PlayerBar {
get specialPoints() { return this.specialPointsContainer.getElementsByClassName('specialPoint').length; }
set specialPoints(value: number) {
const oldList = this.specialPointsContainer.getElementsByClassName('specialPoint');
const oldList = Array.from(this.specialPointsContainer.getElementsByClassName('specialPoint'));
if (value < oldList.length) {
for (let i = oldList.length - 1; i >= value; i--)
this.specialPointsContainer.removeChild(oldList[i]);

View File

@@ -1,7 +1,7 @@
interface PlayerData {
playerIndex: number;
hand: Card[] | null;
deck: Card[] | null;
deck: Deck | null;
cardsUsed: number[];
move: Move | null;
}

View File

@@ -28,17 +28,18 @@ function loadReplay(base64: string) {
specialColour: { r: dataView.getUint8(pos + 3), g: dataView.getUint8(pos + 4), b: dataView.getUint8(pos + 5) },
specialAccentColour: { r: dataView.getUint8(pos + 6), g: dataView.getUint8(pos + 7), b: dataView.getUint8(pos + 8) },
uiBaseColourIsSpecialColour: false,
sleeves: 0,
totalSpecialPoints: 0,
passes: 0,
gamesWon: 0
};
players.push(player);
const deck = [ ];
const cards = [ ];
const initialDrawOrder = [ ];
const drawOrder = [ ];
for (let j = 0; j < 15; j++) {
deck.push(loadCardFromReplay(dataView, pos + 9 + j));
cards.push(loadCardFromReplay(dataView, pos + 9 + j));
}
for (let j = 0; j < 2; j++) {
initialDrawOrder.push(dataView.getUint8(pos + 24 + j) & 0xF);
@@ -51,7 +52,7 @@ function loadReplay(base64: string) {
else
drawOrder.push(dataView.getUint8(pos + 26 + j) >> 4 & 0xF);
}
playerData.push({ deck, initialDrawOrder, drawOrder, won: false });
playerData.push({ deck: new Deck("Deck", 0, cards, new Array(15)), initialDrawOrder, drawOrder, won: false });
pos += 35 + len;
}
@@ -95,6 +96,7 @@ function loadReplay(base64: string) {
specialColour: { r: dataView.getUint8(pos + 3), g: dataView.getUint8(pos + 4), b: dataView.getUint8(pos + 5) },
specialAccentColour: { r: dataView.getUint8(pos + 6), g: dataView.getUint8(pos + 7), b: dataView.getUint8(pos + 8) },
uiBaseColourIsSpecialColour: (n2 & 0x80) != 0,
sleeves: 0,
totalSpecialPoints: 0,
passes: 0,
gamesWon: 0
@@ -108,12 +110,12 @@ function loadReplay(base64: string) {
const playerData = [ ];
pos++;
for (let i = 0; i < numPlayers; i++) {
const deck = [ ];
const cards = [ ];
const initialDrawOrder = [ ];
const drawOrder = [ ];
let won = false;
for (let j = 0; j < 15; j++) {
deck.push(loadCardFromReplay(dataView, pos + j));
cards.push(loadCardFromReplay(dataView, pos + j));
}
for (let j = 0; j < 2; j++) {
initialDrawOrder.push(dataView.getUint8(pos + 15 + j) & 0xF);
@@ -126,7 +128,7 @@ function loadReplay(base64: string) {
else
drawOrder.push(dataView.getUint8(pos + 17 + j) >> 4 & 0xF);
}
playerData.push({ deck, initialDrawOrder, drawOrder, won });
playerData.push({ deck: new Deck("Deck", 0, cards, new Array(15)), initialDrawOrder, drawOrder, won });
pos += 25;
}
const turns = replayLoadTurns(dataView, numPlayers, pos);
@@ -141,14 +143,16 @@ function loadReplay(base64: string) {
currentGame = {
id: 'replay',
state: GameState.Redraw,
game: {
state: GameState.Redraw,
players: players,
maxPlayers: players.length,
turnNumber: 0,
turnTimeLimit: null,
turnTimeLeft: null,
goalWinCount: goalWinCount
},
me: null,
players: players,
maxPlayers: players.length,
turnNumber: 0,
turnTimeLimit: null,
turnTimeLeft: null,
goalWinCount: goalWinCount,
webSocket: null
};

View File

@@ -8,8 +8,8 @@ let initialised = false;
let initialiseCallback: (() => void) | null = null;
let canPushState = isSecureContext && location.protocol != 'file:';
const decks: Deck[] = [ new Deck('Starter Deck', [ 6, 34, 159, 13, 45, 137, 22, 52, 141, 28, 55, 103, 40, 56, 92 ], true) ];
let selectedDeck: Deck | null = null;
const decks = [ new SavedDeck('Starter Deck', 0, [ 6, 34, 159, 13, 45, 137, 22, 52, 141, 28, 55, 103, 40, 56, 92 ], new Array(15), true) ];
let selectedDeck: SavedDeck | null = null;
let editingDeck = false;
let deckModified = false;
let shouldConfirmLeavingGame = false;
@@ -97,15 +97,15 @@ function clearUrlFromGame() {
function onGameSettingsChange() {
if (currentGame == null) return;
if (lobbyTimeLimitBox.value != currentGame.turnTimeLimit?.toString() ?? '')
lobbyTimeLimitBox.value = currentGame.turnTimeLimit?.toString() ?? '';
if (lobbyTimeLimitBox.value != currentGame.game.turnTimeLimit?.toString() ?? '')
lobbyTimeLimitBox.value = currentGame.game.turnTimeLimit?.toString() ?? '';
}
function onGameStateChange(game: any, playerData: PlayerData | null) {
if (currentGame == null)
throw new Error('currentGame is null');
clearPlayContainers();
currentGame.state = game.state;
currentGame.game.state = game.state;
if (game.board) {
board.flip = playerData != null && playerData.playerIndex % 2 != 0;
@@ -119,7 +119,7 @@ function onGameStateChange(game: any, playerData: PlayerData | null) {
gamePage.dataset.myPlayerIndex = playerData ? playerData.playerIndex.toString() : '';
gamePage.dataset.uiBaseColourIsSpecialColour = playerData && game.players[playerData.playerIndex].uiBaseColourIsSpecialColour ? 'true' : 'false';
if (game.stage != GameState.WaitingForPlayers)
if (game.state != GameState.WaitingForPlayers)
lobbyLockSettings(true);
redrawModal.hidden = true;
@@ -159,25 +159,25 @@ function onGameStateChange(game: any, playerData: PlayerData | null) {
} else
initSpectator();
currentGame.turnNumber = game.turnNumber;
currentGame.game.turnNumber = game.turnNumber;
gameButtonsContainer.hidden = currentGame.me == null || game.state == GameState.GameEnded || game.state == GameState.SetEnded;
switch (game.state) {
case GameState.Redraw:
if (currentGame.me) setConfirmLeavingGame();
redrawModal.hidden = currentGame.me == null || currentGame.players[currentGame.me.playerIndex].isReady;
redrawModal.hidden = currentGame.me == null || currentGame.game.players[currentGame.me.playerIndex].isReady;
turnNumberLabel.turnNumber = null;
canPlay = false;
timeLabel.faded = redrawModal.hidden;
timeLabel.paused = false;
break;
case GameState.Ongoing:
for (let i = 0; i < currentGame.players.length; i++)
for (let i = 0; i < currentGame.game.players.length; i++)
showWaiting(i);
if (currentGame.me) setConfirmLeavingGame();
turnNumberLabel.turnNumber = game.turnNumber;
board.autoHighlight = true;
canPlay = currentGame.me != null && !currentGame.players[currentGame.me.playerIndex].isReady;
canPlay = currentGame.me != null && !currentGame.game.players[currentGame.me.playerIndex].isReady;
timeLabel.faded = !canPlay;
timeLabel.paused = false;
resetPlayControls();
@@ -214,7 +214,7 @@ errorDialog.addEventListener('close', e => {
function playerDataReviver(key: string, value: any) {
return !value ? value
: key == 'hand' || key == 'deck'
: key == 'hand' || key == 'cards'
? (value as (Card | number)[]).map(v => typeof v == 'number' ? cardDatabase.get(v) : Card.fromJson(v))
: key == 'card'
? typeof value == 'number' ? cardDatabase.get(value) : Card.fromJson(value)
@@ -247,33 +247,35 @@ function setupWebSocket(gameID: string) {
} else {
currentGame = {
id: gameID,
state: payload.data.state,
game: {
state: payload.data.state,
players: payload.data.players,
maxPlayers: payload.data.maxPlayers,
turnNumber: payload.data.turnNumber,
turnTimeLimit: payload.data.turnTimeLimit,
turnTimeLeft: payload.data.turnTimeLeft,
goalWinCount: payload.data.goalWinCount,
},
me: payload.playerData,
players: payload.data.players,
maxPlayers: payload.data.maxPlayers,
turnNumber: payload.data.turnNumber,
turnTimeLimit: payload.data.turnTimeLimit,
turnTimeLeft: payload.data.turnTimeLeft,
goalWinCount: payload.data.goalWinCount,
webSocket: webSocket
};
lobbyResetSlots();
for (let i = 0; i < currentGame.players.length; i++)
for (let i = 0; i < currentGame.game.players.length; i++)
lobbyAddPlayer(i);
onGameSettingsChange();
for (let i = 0; i < playerBars.length; i++) {
playerBars[i].visible = i < currentGame.maxPlayers;
playerBars[i].visible = i < currentGame.game.maxPlayers;
}
for (const button of stageButtons.buttons)
(button as StageButton).setStartSpaces(currentGame.maxPlayers);
(button as StageButton).setStartSpaces(currentGame.game.maxPlayers);
onGameStateChange(payload.data, payload.playerData);
for (let i = 0; i < currentGame.players.length; i++) {
if (currentGame.players[i].isReady) showReady(i);
for (let i = 0; i < currentGame.game.players.length; i++) {
if (currentGame.game.players[i].isReady) showReady(i);
}
if (currentGame.me) {
@@ -294,8 +296,8 @@ function setupWebSocket(gameID: string) {
}
timeLabel.paused = false;
if (currentGame.turnTimeLeft) {
timeLabel.setTime(currentGame.turnTimeLeft);
if (currentGame.game.turnTimeLeft) {
timeLabel.setTime(currentGame.game.turnTimeLeft);
timeLabel.show();
} else
timeLabel.hide();
@@ -307,19 +309,19 @@ function setupWebSocket(gameID: string) {
}
switch (payload.event) {
case 'settingsChange':
currentGame.turnTimeLimit = payload.data.turnTimeLimit;
currentGame.game.turnTimeLimit = payload.data.turnTimeLimit;
onGameSettingsChange();
break;
case 'join':
if (payload.data.playerIndex == currentGame.players.length) {
currentGame.players.push(payload.data.player);
if (payload.data.playerIndex == currentGame.game.players.length) {
currentGame.game.players.push(payload.data.player);
lobbyAddPlayer(payload.data.playerIndex);
}
else
communicationError();
break;
case 'playerReady':
currentGame.players[payload.data.playerIndex].isReady = true;
currentGame.game.players[payload.data.playerIndex].isReady = true;
lobbySetReady(payload.data.playerIndex);
if (payload.data.playerIndex == currentGame.me?.playerIndex) {
@@ -343,46 +345,28 @@ function setupWebSocket(gameID: string) {
clearReady();
board.autoHighlight = false;
showPage('game');
currentGame.turnNumber = payload.data.game.turnNumber;
currentGame.game.turnNumber = payload.data.game.turnNumber;
let anySpecialAttacks = false;
// Show the cards that were played.
clearPlayContainers();
for (let i = 0; i < currentGame.players.length; i++) {
const player = currentGame.players[i];
for (let i = 0; i < currentGame.game.players.length; i++) {
const player = currentGame.game.players[i];
player.specialPoints = payload.data.game.players[i].specialPoints;
player.totalSpecialPoints = payload.data.game.players[i].totalSpecialPoints;
player.passes = payload.data.game.players[i].passes;
player.gamesWon = payload.data.game.players[i].gamesWon;
player.isReady = payload.data.game.players[i].isReady;
lobbyWinCounters[i].wins = player.gamesWon;
const move = payload.data.moves[i];
const button = new CardButton(move.card);
button.buttonElement.disabled = true;
if (move.isSpecialAttack) {
anySpecialAttacks = true;
button.buttonElement.classList.add('specialAttack');
} else if (move.isPass) {
const el = document.createElement('div');
el.className = move.isTimeout ? 'passLabel timeout' : 'passLabel';
el.innerText = 'Pass';
button.buttonElement.appendChild(el);
}
button.buttonElement.disabled = false;
playContainers[i].append(button.buttonElement);
}
timeLabel.paused = true;
if (payload.data.game.turnTimeLeft)
timeLabel.setTime(payload.data.game.turnTimeLeft);
(async () => {
await playInkAnimations(payload.data, anySpecialAttacks);
await playInkAnimations(payload.data);
if (payload.playerData) updateHandAndDeck(payload.playerData);
turnNumberLabel.turnNumber = payload.data.game.turnNumber;
clearPlayContainers();
if (payload.event == 'gameEnd') {
currentGame.state = payload.data.game.state;
currentGame.game.state = payload.data.game.state;
clearConfirmLeavingGame();
gameButtonsContainer.hidden = true;
passButton.enabled = false;
@@ -398,7 +382,7 @@ function setupWebSocket(gameID: string) {
timeLabel.paused = false;
if (payload.data.game.turnTimeLeft)
timeLabel.show();
for (let i = 0; i < currentGame.players.length; i++)
for (let i = 0; i < currentGame.game.players.length; i++)
showWaiting(i);
}
})();
@@ -553,7 +537,6 @@ function resetAnimation(el: HTMLElement) {
el.style.animation = '';
}
document.getElementById('noJSPage')!.innerText = 'Loading client...';
if (config.discordUrl) {

36
TableturfBattleClient/src/js-untar.d.ts vendored Normal file
View File

@@ -0,0 +1,36 @@
declare class TarFile {
name: string;
mode: string;
uid: number;
gid: number;
size: number;
mtime: number;
checksum: number;
type: string;
linkname: string;
ustarFormat: string;
version?: string;
uname?: string;
gname?: string;
devmajor?: number;
devminor?: number;
namePrefix?: string;
buffer: ArrayBuffer;
blob: Blob;
getBlobUrl(): string;
readAsString(): string;
readAsJSON(): object;
}
declare class ProgressivePromise<TResult, TProgress> extends Promise<TResult> {
progress(cb: ((value: TProgress) => void)): ProgressivePromise<TResult, TProgress>;
then<TResult1 = TResult, TResult2 = never>(
onfulfilled?: ((value: TResult) => TResult1 | PromiseLike<TResult1>) | undefined | null,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null,
onProgress?: ((value: TProgress) => void) | undefined | null)
: ProgressivePromise<TResult1 | TResult2, TProgress>;
}
declare function untar(arrayBuffer: ArrayBuffer): ProgressivePromise<TarFile[], TarFile>;