mirror of
https://github.com/AndrioCelos/TableturfBattleApp.git
synced 2026-03-21 17:34:28 -05:00
Card art and sleeves update
This commit is contained in:
parent
a9a7249f59
commit
deb8a673ab
3
.gitmodules
vendored
3
.gitmodules
vendored
|
|
@ -4,3 +4,6 @@
|
|||
[submodule "TableturfBattleClient/mobile-drag-drop"]
|
||||
path = TableturfBattleClient/mobile-drag-drop
|
||||
url = https://github.com/timruffles/mobile-drag-drop.git
|
||||
[submodule "TableturfBattleClient/js-untar"]
|
||||
path = TableturfBattleClient/js-untar
|
||||
url = https://github.com/InvokIT/js-untar
|
||||
|
|
|
|||
BIN
TableturfBattleClient/assets/CardBackground-0-0.webp
Normal file
BIN
TableturfBattleClient/assets/CardBackground-0-0.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
BIN
TableturfBattleClient/assets/CardBackground-0-1.webp
Normal file
BIN
TableturfBattleClient/assets/CardBackground-0-1.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
BIN
TableturfBattleClient/assets/CardBackground-1-0.webp
Normal file
BIN
TableturfBattleClient/assets/CardBackground-1-0.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
BIN
TableturfBattleClient/assets/CardBackground-1-1.webp
Normal file
BIN
TableturfBattleClient/assets/CardBackground-1-1.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
BIN
TableturfBattleClient/assets/CardBackground-2-0.webp
Normal file
BIN
TableturfBattleClient/assets/CardBackground-2-0.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
BIN
TableturfBattleClient/assets/CardBackground-2-1.webp
Normal file
BIN
TableturfBattleClient/assets/CardBackground-2-1.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
|
|
@ -16,6 +16,7 @@
|
|||
<script src="config/config.js"></script>
|
||||
<script src="qrcodejs/qrcode.js"></script>
|
||||
<script src="mobile-drag-drop/release/index.min.js"></script>
|
||||
<script src="js-untar/build/dist/untar.js"></script>
|
||||
<script>
|
||||
// Enable a polyfill for drag/drop support on mobile Firefox.
|
||||
MobileDragDrop.polyfill({ dragImageTranslateOverride: MobileDragDrop.scrollBehaviourDragImageTranslateOverride });
|
||||
|
|
@ -429,6 +430,7 @@
|
|||
<button type="button" id="deckListTestButton" disabled>Test</button>
|
||||
<button type="button" id="deckExportButton" disabled>Export</button>
|
||||
<button type="button" id="deckCopyButton" disabled>Copy</button>
|
||||
<button type="button" id="deckSleevesButton" disabled>Sleeves</button>
|
||||
<button type="button" id="deckEditButton" disabled>Edit</button>
|
||||
<button type="button" id="deckRenameButton" disabled>Rename</button>
|
||||
<button type="button" id="deckDeleteButton" class="danger" disabled>Delete</button>
|
||||
|
|
@ -437,6 +439,41 @@
|
|||
<div id="deckCardListView">
|
||||
</div>
|
||||
</section>
|
||||
<dialog id="deckSleevesDialog">
|
||||
<form id="deckSleevesForm" method="dialog">
|
||||
<section id="deckSleevesList">
|
||||
<input type="radio" name="deckSleeves" id="deckSleeve0" value="0"/><label for="deckSleeve0"></label>
|
||||
<input type="radio" name="deckSleeves" id="deckSleeve1" value="1"/><label for="deckSleeve1"></label>
|
||||
<input type="radio" name="deckSleeves" id="deckSleeve2" value="2"/><label for="deckSleeve2"></label>
|
||||
<input type="radio" name="deckSleeves" id="deckSleeve3" value="3"/><label for="deckSleeve3"></label>
|
||||
<input type="radio" name="deckSleeves" id="deckSleeve4" value="4"/><label for="deckSleeve4"></label>
|
||||
<input type="radio" name="deckSleeves" id="deckSleeve5" value="5"/><label for="deckSleeve5"></label>
|
||||
<input type="radio" name="deckSleeves" id="deckSleeve6" value="6"/><label for="deckSleeve6"></label>
|
||||
<input type="radio" name="deckSleeves" id="deckSleeve7" value="7"/><label for="deckSleeve7"></label>
|
||||
<input type="radio" name="deckSleeves" id="deckSleeve8" value="8"/><label for="deckSleeve8"></label>
|
||||
<input type="radio" name="deckSleeves" id="deckSleeve9" value="9"/><label for="deckSleeve9"></label>
|
||||
<input type="radio" name="deckSleeves" id="deckSleeve10" value="10"/><label for="deckSleeve10"></label>
|
||||
<input type="radio" name="deckSleeves" id="deckSleeve11" value="11"/><label for="deckSleeve11"></label>
|
||||
<input type="radio" name="deckSleeves" id="deckSleeve12" value="12"/><label for="deckSleeve12"></label>
|
||||
<input type="radio" name="deckSleeves" id="deckSleeve13" value="13"/><label for="deckSleeve13"></label>
|
||||
<input type="radio" name="deckSleeves" id="deckSleeve14" value="14"/><label for="deckSleeve14"></label>
|
||||
<input type="radio" name="deckSleeves" id="deckSleeve15" value="15"/><label for="deckSleeve15"></label>
|
||||
<input type="radio" name="deckSleeves" id="deckSleeve16" value="16"/><label for="deckSleeve16"></label>
|
||||
<input type="radio" name="deckSleeves" id="deckSleeve17" value="17"/><label for="deckSleeve17"></label>
|
||||
<input type="radio" name="deckSleeves" id="deckSleeve18" value="18"/><label for="deckSleeve18"></label>
|
||||
<input type="radio" name="deckSleeves" id="deckSleeve19" value="19"/><label for="deckSleeve19"></label>
|
||||
<input type="radio" name="deckSleeves" id="deckSleeve20" value="20"/><label for="deckSleeve20"></label>
|
||||
<input type="radio" name="deckSleeves" id="deckSleeve21" value="21"/><label for="deckSleeve21"></label>
|
||||
<input type="radio" name="deckSleeves" id="deckSleeve22" value="22"/><label for="deckSleeve22"></label>
|
||||
<input type="radio" name="deckSleeves" id="deckSleeve23" value="23"/><label for="deckSleeve23"></label>
|
||||
<input type="radio" name="deckSleeves" id="deckSleeve24" value="24"/><label for="deckSleeve24"></label>
|
||||
</section>
|
||||
<section id="deckSleevesFormButtons">
|
||||
<button type="submit" id="deckSleevesOkButton">Select</button>
|
||||
<button type="submit" id="deckSleevesCancelButton">Cancel</button>
|
||||
</section>
|
||||
</form>
|
||||
</dialog>
|
||||
<dialog id="deckImportDialog">
|
||||
<form id="deckImportForm" method="dialog">
|
||||
<label for="deckImportTextButton">
|
||||
|
|
|
|||
1
TableturfBattleClient/js-untar
Submodule
1
TableturfBattleClient/js-untar
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 49e639cf82e8d58dccb3458cbd08768afee8b41c
|
||||
|
|
@ -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"> </div>'.repeat(Math.max(currentGame?.players?.length ?? 0, 2))}.`;
|
||||
return `It cannot be over${' <div class="playHintSpecial" aria-label="Special space"> </div>'.repeat(Math.max(currentGame?.game.players.length ?? 0, 2))}.`;
|
||||
if (space != Space.Empty && !isSpecialAttack)
|
||||
return 'It cannot be over inked spaces.';
|
||||
if (!isAnchored) {
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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}`));
|
||||
}
|
||||
|
|
|
|||
165
TableturfBattleClient/src/CardDisplay.ts
Normal file
165
TableturfBattleClient/src/CardDisplay.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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})`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ interface Player {
|
|||
specialColour: Colour;
|
||||
specialAccentColour: Colour;
|
||||
uiBaseColourIsSpecialColour?: boolean;
|
||||
sleeves: number;
|
||||
totalSpecialPoints: number;
|
||||
passes: number;
|
||||
gamesWon: number;
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
interface PlayerData {
|
||||
playerIndex: number;
|
||||
hand: Card[] | null;
|
||||
deck: Card[] | null;
|
||||
deck: Deck | null;
|
||||
cardsUsed: number[];
|
||||
move: Move | null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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
36
TableturfBattleClient/src/js-untar.d.ts
vendored
Normal 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>;
|
||||
|
|
@ -212,6 +212,73 @@ dialog::backdrop {
|
|||
border: 1em solid white;
|
||||
}
|
||||
|
||||
.deckList {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
}
|
||||
|
||||
.deckList button, #addDeckControls button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
border: inherit;
|
||||
text-shadow: 0 0 4px black;
|
||||
}
|
||||
|
||||
.deckButton, #addDeckControls {
|
||||
width: 20rem;
|
||||
height: 4rem;
|
||||
margin: 0.5em;
|
||||
}
|
||||
.deckButton {
|
||||
position: relative;
|
||||
background: linear-gradient(#00000040, #00000040), url('assets/external/sleeves.webp') 0 11%/800%;
|
||||
outline: 0.25em solid grey;
|
||||
border-radius: 0.5em;
|
||||
outline-offset: -0.2em;
|
||||
}
|
||||
|
||||
.deckButton[data-sleeves="0"] { background-position: 0 11%; }
|
||||
.deckButton[data-sleeves="1"] { background-position: 14.3% 11%; }
|
||||
.deckButton[data-sleeves="2"] { background-position: 28.6% 11%; }
|
||||
.deckButton[data-sleeves="3"] { background-position: 42.9% 11%; }
|
||||
.deckButton[data-sleeves="4"] { background-position: 57.1% 11%; }
|
||||
.deckButton[data-sleeves="5"] { background-position: 71.4% 11%; }
|
||||
.deckButton[data-sleeves="6"] { background-position: 85.7% 11%; }
|
||||
.deckButton[data-sleeves="7"] { background-position: 100% 11%; }
|
||||
.deckButton[data-sleeves="8"] { background-position: 0 37%; }
|
||||
.deckButton[data-sleeves="9"] { background-position: 14.3% 37%; }
|
||||
.deckButton[data-sleeves="10"] { background-position: 28.6% 37%; }
|
||||
.deckButton[data-sleeves="11"] { background-position: 42.9% 37%; }
|
||||
.deckButton[data-sleeves="12"] { background-position: 57.1% 37%; }
|
||||
.deckButton[data-sleeves="13"] { background-position: 71.4% 37%; }
|
||||
.deckButton[data-sleeves="14"] { background-position: 85.7% 37%; }
|
||||
.deckButton[data-sleeves="15"] { background-position: 100% 37%; }
|
||||
.deckButton[data-sleeves="16"] { background-position: 0 63%; }
|
||||
.deckButton[data-sleeves="17"] { background-position: 14.3% 63%; }
|
||||
.deckButton[data-sleeves="18"] { background-position: 28.6% 63%; }
|
||||
.deckButton[data-sleeves="19"] { background-position: 42.9% 63%; }
|
||||
.deckButton[data-sleeves="20"] { background-position: 57.1% 63%; }
|
||||
.deckButton[data-sleeves="21"] { background-position: 71.4% 63%; }
|
||||
.deckButton[data-sleeves="22"] { background-position: 85.7% 63%; }
|
||||
.deckButton[data-sleeves="23"] { background-position: 100% 63%; }
|
||||
.deckButton[data-sleeves="24"] { background-position: 0 89%; }
|
||||
|
||||
.deckButton:is(:active, .checked) {
|
||||
outline-color: lightgrey;
|
||||
}
|
||||
.deckButton:hover {
|
||||
outline-color: white;
|
||||
}
|
||||
.deckButton:focus-within {
|
||||
outline-color: white;
|
||||
}
|
||||
.deckButton.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Stages */
|
||||
|
||||
#stageList {
|
||||
|
|
@ -268,7 +335,7 @@ dialog::backdrop {
|
|||
|
||||
/* Cards */
|
||||
|
||||
.card, .cardBack {
|
||||
.cardButton, .cardBack {
|
||||
box-sizing: border-box;
|
||||
width: 10em;
|
||||
height: 12em;
|
||||
|
|
@ -278,22 +345,51 @@ dialog::backdrop {
|
|||
|
||||
.cardBack {
|
||||
--colour: grey;
|
||||
background: #000000C0;
|
||||
background: url(assets/external/sleeves.webp) 0 0/800%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 14em;
|
||||
animation: 0.25s ease-out forwards cardBackFadeIn;
|
||||
}
|
||||
.cardBack.waiting::before {
|
||||
content: '';
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
background: url(assets/external/IconUp_00.webp) center/cover;
|
||||
animation: 2s infinite linear loadingSpinner;
|
||||
opacity: 0.333;
|
||||
.cardBack[data-sleeves="0"] { background-position: 0% 0%; }
|
||||
.cardBack[data-sleeves="1"] { background-position: 14.3% 0%; }
|
||||
.cardBack[data-sleeves="2"] { background-position: 28.6% 0%; }
|
||||
.cardBack[data-sleeves="3"] { background-position: 42.9% 0%; }
|
||||
.cardBack[data-sleeves="4"] { background-position: 57.1% 0%; }
|
||||
.cardBack[data-sleeves="5"] { background-position: 71.4% 0%; }
|
||||
.cardBack[data-sleeves="6"] { background-position: 85.7% 0%; }
|
||||
.cardBack[data-sleeves="7"] { background-position: 100% 0%; }
|
||||
.cardBack[data-sleeves="8"] { background-position: 0% 33.3% }
|
||||
.cardBack[data-sleeves="9"] { background-position: 14.3% 33.3% }
|
||||
.cardBack[data-sleeves="10"] { background-position: 28.6% 33.3% }
|
||||
.cardBack[data-sleeves="11"] { background-position: 42.9% 33.3% }
|
||||
.cardBack[data-sleeves="12"] { background-position: 57.1% 33.3% }
|
||||
.cardBack[data-sleeves="13"] { background-position: 71.4% 33.3% }
|
||||
.cardBack[data-sleeves="14"] { background-position: 85.7% 33.3% }
|
||||
.cardBack[data-sleeves="15"] { background-position: 100% 33.3% }
|
||||
.cardBack[data-sleeves="16"] { background-position: 0% 66.7% }
|
||||
.cardBack[data-sleeves="17"] { background-position: 14.3% 66.7% }
|
||||
.cardBack[data-sleeves="18"] { background-position: 28.6% 66.7% }
|
||||
.cardBack[data-sleeves="19"] { background-position: 42.9% 66.7% }
|
||||
.cardBack[data-sleeves="20"] { background-position: 57.1% 66.7% }
|
||||
.cardBack[data-sleeves="21"] { background-position: 71.4% 66.7% }
|
||||
.cardBack[data-sleeves="22"] { background-position: 85.7% 66.7% }
|
||||
.cardBack[data-sleeves="23"] { background-position: 100% 66.7% }
|
||||
.cardBack[data-sleeves="24"] { background-position: 0% 100%; }
|
||||
|
||||
@keyframes cardBackFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-4rem);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
.cardButton {
|
||||
position: relative;
|
||||
color: currentColor; /* Override disabled colour */
|
||||
background: black;
|
||||
|
|
@ -302,15 +398,27 @@ dialog::backdrop {
|
|||
font-weight: 599;
|
||||
margin: 0.5em;
|
||||
}
|
||||
.card:not([hidden]) {
|
||||
.cardButton:not([hidden]) {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
:is(#deckCardListView, #deckCardListEdit, .cardListGrid) .card.upcoming { background: midnightblue; }
|
||||
:is(#deckCardListView, #deckCardListEdit, .cardListGrid) .cardButton.upcoming { background: midnightblue; }
|
||||
|
||||
.card.common { --colour: rgb(89, 49, 255); }
|
||||
.card.rare { --colour: rgb(231, 180, 39); }
|
||||
.card.fresh { --colour: white; }
|
||||
.cardButton.common { --colour: rgb(89, 49, 255); }
|
||||
.cardButton.rare { --colour: rgb(231, 180, 39); }
|
||||
.cardButton.fresh { --colour: white; }
|
||||
|
||||
.cardButton .cardArt {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-size: contain;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
opacity: 0.333;
|
||||
}
|
||||
|
||||
.cardHeader {
|
||||
height: 2.5em;
|
||||
|
|
@ -325,7 +433,7 @@ dialog::backdrop {
|
|||
display: none;
|
||||
}
|
||||
|
||||
.cardListGrid .card:hover .cardNumber {
|
||||
.cardListGrid .cardButton:hover .cardNumber {
|
||||
display: block;
|
||||
position: absolute;
|
||||
background: grey;
|
||||
|
|
@ -342,25 +450,25 @@ dialog::backdrop {
|
|||
line-height: 1.25em;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.card:is([data-card-number="163"], [data-card-number="166"], [data-card-number="196"], [data-card-number="197"], [data-card-number="199"],
|
||||
[data-card-number="202"], [data-card-number="-3"], [data-card-number="-5"], [data-card-number="-12"]) .cardName {
|
||||
.cardButton:is([data-card-number="163"], [data-card-number="166"], [data-card-number="196"], [data-card-number="197"], [data-card-number="199"],
|
||||
[data-card-number="202"], [data-card-number="216"]) .cardName {
|
||||
position: absolute;
|
||||
left: -1em;
|
||||
right: -1em;
|
||||
transform: scaleX(0.8);
|
||||
}
|
||||
|
||||
.card.common .cardName {
|
||||
.cardButton.common .cardName {
|
||||
color: var(--colour);
|
||||
}
|
||||
.card.rare .cardName {
|
||||
.cardButton.rare .cardName {
|
||||
background: var(--colour);
|
||||
background: linear-gradient(90deg, rgb(255, 242, 129) 0%, rgb(255, 255, 224) 15%, rgb(231, 180, 39) 50%, rgb(255, 255, 224) 85%, rgb(255, 242, 129) 100%);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
.card.fresh .cardName {
|
||||
.cardButton.fresh .cardName {
|
||||
background: var(--colour);
|
||||
background: linear-gradient(120deg, rgba(253, 217, 169, 1) 0%, rgba(200, 58, 141, 1) 50%, rgba(55, 233, 207, 1) 100%);
|
||||
background-clip: text;
|
||||
|
|
@ -368,8 +476,7 @@ dialog::backdrop {
|
|||
color: transparent;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.cardGrid {
|
||||
.cardButton .cardGrid {
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
border-spacing: 0;
|
||||
|
|
@ -380,6 +487,7 @@ dialog::backdrop {
|
|||
outline: 1px solid #80808080;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
.cardGrid td { background: #00000080; }
|
||||
.cardGrid td.ink { background: url('assets/InkOverlay.png') center/cover, var(--player-primary-colour); outline: none; }
|
||||
.cardGrid td.special { background: url('assets/SpecialOverlay.png') center/cover, var(--player-special-colour); outline: none; }
|
||||
|
||||
|
|
@ -423,7 +531,55 @@ dialog::backdrop {
|
|||
.playHintSpecial:nth-of-type(3) { background: url('assets/SpecialOverlay.png') center/cover, var(--special-colour-3); }
|
||||
.playHintSpecial:nth-of-type(4) { background: url('assets/SpecialOverlay.png') center/cover, var(--special-colour-4); }
|
||||
|
||||
:is(.card, .stage, .stageRandom)::before {
|
||||
.card {
|
||||
position: relative;
|
||||
color: currentColor; /* Override disabled colour */
|
||||
font-family: 'Splatoon 1', 'Arial Black', sans-serif;
|
||||
font-size: 2rem;
|
||||
font-weight: 599;
|
||||
aspect-ratio: 5/7;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
.playContainer .card {
|
||||
animation: 0.1s ease-out forwards flipCardIn;
|
||||
}
|
||||
|
||||
@keyframes flipCardOut {
|
||||
from { transform: scaleX(1); }
|
||||
to { transform: scaleX(0); }
|
||||
}
|
||||
@keyframes flipCardIn {
|
||||
from { transform: scaleX(0); }
|
||||
to { transform: scaleX(1); }
|
||||
}
|
||||
|
||||
svg.card {
|
||||
display: block;
|
||||
}
|
||||
|
||||
svg.card text {
|
||||
text-anchor: middle;
|
||||
}
|
||||
|
||||
svg.card text.cardDisplayName {
|
||||
transform: scaleX(var(--scale));
|
||||
}
|
||||
|
||||
rect.empty {
|
||||
fill: #00000080;
|
||||
stroke: #60606080;
|
||||
stroke-width: 6;
|
||||
}
|
||||
|
||||
rect.ink {
|
||||
fill: var(--player-primary-colour);
|
||||
}
|
||||
|
||||
rect.special, g.specialCost rect {
|
||||
fill: var(--player-special-colour);
|
||||
}
|
||||
|
||||
:is(.cardButton, .stage, .stageRandom)::before {
|
||||
/* If it exists */
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
|
@ -434,19 +590,19 @@ dialog::backdrop {
|
|||
opacity: 0.25;
|
||||
z-index: 0;
|
||||
}
|
||||
.card.checked, .card:is(:hover, :focus-within):not([disabled]) {
|
||||
.cardButton.checked, .cardButton:is(:hover, :focus-within):not([disabled]) {
|
||||
transform: rotate(-2deg);
|
||||
}
|
||||
.card:is(:hover, :focus-within):not(.checked, .disabled, [disabled])::before {
|
||||
.cardButton:is(:hover, :focus-within):not(.checked, .disabled, [disabled])::before {
|
||||
content: '';
|
||||
background: grey;
|
||||
}
|
||||
.card.checked::before {
|
||||
.cardButton.checked::before {
|
||||
content: '';
|
||||
background: var(--colour);
|
||||
}
|
||||
|
||||
.card.disabled {
|
||||
.cardButton.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
|
|
@ -828,7 +984,25 @@ dialog::backdrop {
|
|||
bottom: 0;
|
||||
}
|
||||
|
||||
.playContainer { margin: 8em 0; }
|
||||
.playContainer:not([hidden]) {
|
||||
position: relative;
|
||||
margin: 8em 0;
|
||||
width: 10em;
|
||||
height: 14em;
|
||||
border: 1px solid grey;
|
||||
border-radius: 1em;
|
||||
background: #00000080;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.playContainer .waiting {
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
background: url(assets/external/IconUp_00.webp) center/cover;
|
||||
animation: 2s infinite linear loadingSpinner;
|
||||
opacity: 0.333;
|
||||
}
|
||||
|
||||
.playContainer[data-index="2"] { grid-column: play-column-2; grid-row: 2 / -1; align-self: end; }
|
||||
.playContainer[data-index="1"] { grid-column: play-column-2; grid-row: 1 / 4; align-self: start; }
|
||||
|
|
@ -840,7 +1014,7 @@ dialog::backdrop {
|
|||
#gamePage.boardFlipped .playContainer[data-index="1"] { grid-column: play-column-1; grid-row: 2 / -1; align-self: end; }
|
||||
#gamePage.boardFlipped .playContainer[data-index="2"] { grid-column: play-column-1; grid-row: 1 / 4; align-self: start; }
|
||||
|
||||
.playContainer .card {
|
||||
.playContainer .cardButton {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
|
@ -1264,13 +1438,13 @@ dialog::backdrop {
|
|||
}
|
||||
|
||||
|
||||
.specialAttack, .specialAttack::after {
|
||||
.specialAttack, .specialAttackLabel {
|
||||
box-shadow: 0px 0 8px 4px var(--player-colour);
|
||||
}
|
||||
|
||||
.specialAttack::after {
|
||||
content: 'Special Attack!';
|
||||
.specialAttackLabel {
|
||||
position: absolute;
|
||||
font-family: 'Splatoon 1', sans-serif;
|
||||
left: -2em;
|
||||
top: 4em;
|
||||
right: -2em;
|
||||
|
|
@ -1333,33 +1507,10 @@ dialog::backdrop {
|
|||
gap: 3em;
|
||||
}
|
||||
|
||||
.deckList {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
}
|
||||
|
||||
.deckList button, #addDeckControls button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
border: inherit;
|
||||
text-shadow: 0 0 4px black;
|
||||
}
|
||||
button.dragging {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.deckList > button, #addDeckControls {
|
||||
width: 20rem;
|
||||
height: 3rem;
|
||||
margin: 0.5em;
|
||||
}
|
||||
.deckList > button { background: var(--primary-colour-2); position: relative;}
|
||||
.deckList > button:hover { background: var(--special-colour-2); }
|
||||
.deckList > button:focus-within { outline: 2px solid var(--special-accent-colour-2); }
|
||||
.deckList > button:is(:active, .checked) { background: var(--special-accent-colour-2); }
|
||||
.deckList > button.disabled { background: grey; }
|
||||
|
||||
#addDeckControls {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
|
@ -1388,7 +1539,7 @@ button.dragging {
|
|||
.touchmode .deckList .handle {
|
||||
width: 2em;
|
||||
}
|
||||
.card .handle {
|
||||
.cardButton .handle {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
|
@ -1396,7 +1547,7 @@ button.dragging {
|
|||
height: 3em;
|
||||
}
|
||||
|
||||
.card.emptySlot {
|
||||
.cardButton.emptySlot {
|
||||
--colour: dimgrey;
|
||||
background: url('assets/plus-circle.svg') center/2em no-repeat, black;
|
||||
}
|
||||
|
|
@ -1501,6 +1652,60 @@ button.dragging {
|
|||
text-align: start;
|
||||
}
|
||||
|
||||
#deckSleevesList {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: center;
|
||||
max-width: 80em;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
#deckSleevesList input {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
}
|
||||
#deckSleevesList label {
|
||||
display: inline-block;
|
||||
width: 10em;
|
||||
height: 14em;
|
||||
border-radius: 0.75em;
|
||||
background: url('assets/external/sleeves.webp') 0px 0px/800%;
|
||||
}
|
||||
#deckSleevesList input:hover+label {
|
||||
outline: 0.25em solid grey;
|
||||
}
|
||||
#deckSleevesList input:focus+label {
|
||||
outline: 0.25em solid grey;
|
||||
}
|
||||
#deckSleevesList input:checked+label {
|
||||
outline: 0.25em solid yellow;
|
||||
}
|
||||
#deckSleevesList label:nth-of-type(1) { background-position: 0% 0%; }
|
||||
#deckSleevesList label:nth-of-type(2) { background-position: -100% 0%; }
|
||||
#deckSleevesList label:nth-of-type(3) { background-position: -200% 0%; }
|
||||
#deckSleevesList label:nth-of-type(4) { background-position: -300% 0%; }
|
||||
#deckSleevesList label:nth-of-type(5) { background-position: -400% 0%; }
|
||||
#deckSleevesList label:nth-of-type(6) { background-position: -500% 0%; }
|
||||
#deckSleevesList label:nth-of-type(7) { background-position: -600% 0%; }
|
||||
#deckSleevesList label:nth-of-type(8) { background-position: -700% 0%; }
|
||||
#deckSleevesList label:nth-of-type(9) { background-position: 0% -100%; }
|
||||
#deckSleevesList label:nth-of-type(10) { background-position: -100% -100%; }
|
||||
#deckSleevesList label:nth-of-type(11) { background-position: -200% -100%; }
|
||||
#deckSleevesList label:nth-of-type(12) { background-position: -300% -100%; }
|
||||
#deckSleevesList label:nth-of-type(13) { background-position: -400% -100%; }
|
||||
#deckSleevesList label:nth-of-type(14) { background-position: -500% -100%; }
|
||||
#deckSleevesList label:nth-of-type(15) { background-position: -600% -100%; }
|
||||
#deckSleevesList label:nth-of-type(16) { background-position: -700% -100%; }
|
||||
#deckSleevesList label:nth-of-type(17) { background-position: 0% -200%; }
|
||||
#deckSleevesList label:nth-of-type(18) { background-position: -100% -200%; }
|
||||
#deckSleevesList label:nth-of-type(19) { background-position: -200% -200%; }
|
||||
#deckSleevesList label:nth-of-type(20) { background-position: -300% -200%; }
|
||||
#deckSleevesList label:nth-of-type(21) { background-position: -400% -200%; }
|
||||
#deckSleevesList label:nth-of-type(22) { background-position: -500% -200%; }
|
||||
#deckSleevesList label:nth-of-type(23) { background-position: -600% -200%; }
|
||||
#deckSleevesList label:nth-of-type(24) { background-position: -700% -200%; }
|
||||
#deckSleevesList label:nth-of-type(25) { background-position: 0% -300%; }
|
||||
|
||||
/* Help */
|
||||
|
||||
#helpControls {
|
||||
|
|
@ -1549,22 +1754,22 @@ button.dragging {
|
|||
#gamePage.boardFlipped .playerBar[data-index="3"] { grid-row: player-row-0; }
|
||||
|
||||
#gamePage:not(.boardFlipped):not([data-players="2"]) .playerBar:is([data-index="1"], [data-index="2"]) .wins,
|
||||
#gamePage.boardFlipped:not([data-players="2"]) .playerBar:is([data-index="0"], [data-index="3"]) .wins {
|
||||
float: initial;
|
||||
#gamePage.boardFlipped:not([data-players="2"]) .playerBar:is([data-index="0"], [data-index="3"]) .wins {
|
||||
float: initial;
|
||||
justify-content: end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 40rem) {
|
||||
.card {
|
||||
.cardButton {
|
||||
margin: 0;
|
||||
}
|
||||
.cardListGrid:not([hidden]) {
|
||||
justify-items: stretch;
|
||||
}
|
||||
.cardListGrid .card {
|
||||
width: auto;
|
||||
justify-self: stretch;
|
||||
.cardListGrid .cardButton {
|
||||
width: auto;
|
||||
justify-self: stretch;
|
||||
}
|
||||
|
||||
/* Lobby - Mobile layout */
|
||||
|
|
@ -1676,7 +1881,7 @@ button.dragging {
|
|||
grid-column: 1 / -1;
|
||||
grid-row: 2 / -1;
|
||||
}
|
||||
#showDeckList .card {
|
||||
#showDeckList .cardButton {
|
||||
width: 30vw;
|
||||
}
|
||||
|
||||
|
|
@ -1724,17 +1929,17 @@ button.dragging {
|
|||
display: block;
|
||||
}
|
||||
|
||||
:is(#deckListPage, #deckEditPage) section {
|
||||
:is(#deckListPage, #deckEditPage) > section {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
#deckListPage:not(.showingDeck) #deckEditorDeckViewSection {
|
||||
#deckListPage:not(.showingDeck) #deckEditorDeckViewSection {
|
||||
display: none;
|
||||
}
|
||||
#deckListPage.showingDeck #deckEditorDeckListSection {
|
||||
#deckListPage.showingDeck #deckEditorDeckListSection {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
|
@ -1804,7 +2009,7 @@ button.dragging {
|
|||
font-size: 55%;
|
||||
}
|
||||
|
||||
#cardList .card {
|
||||
#cardList .cardButton {
|
||||
height: 13em;
|
||||
}
|
||||
|
||||
|
|
@ -1846,6 +2051,12 @@ button.dragging {
|
|||
#testPlacementList div {
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
|
||||
#deckSleevesList label {
|
||||
width: 20%;
|
||||
height: auto;
|
||||
aspect-ratio: 5 / 7;
|
||||
}
|
||||
}
|
||||
|
||||
/* Loading spinner */
|
||||
|
|
|
|||
|
|
@ -2,32 +2,44 @@
|
|||
|
||||
namespace TableturfBattleServer;
|
||||
public class Card {
|
||||
[JsonProperty("number")]
|
||||
public int Number { get; }
|
||||
[JsonProperty("altNumber")]
|
||||
public int? AltNumber { get; }
|
||||
[JsonProperty("name")]
|
||||
public int? AltNumber { get; init; }
|
||||
public string Name { get; }
|
||||
[JsonProperty("rarity")]
|
||||
public Rarity Rarity { get; }
|
||||
[JsonProperty("specialCost")]
|
||||
public int SpecialCost { get; }
|
||||
[JsonIgnore]
|
||||
public int Size { get; }
|
||||
|
||||
[JsonProperty("grid")]
|
||||
public string? Line1 { get; init; }
|
||||
public string? Line2 { get; init; }
|
||||
public string? ArtFileName { get; init; }
|
||||
public float TextScale { get; init; }
|
||||
public Colour? InkColour1 { get; init; }
|
||||
public Colour? InkColour2 { get; init; }
|
||||
|
||||
[JsonProperty]
|
||||
private readonly Space[,] grid;
|
||||
|
||||
internal Card(int number, string name, Rarity rarity, Space[,] grid) : this(number, null, name, rarity, null, grid) { }
|
||||
internal Card(int number, int? altNumber, string name, Rarity rarity, Space[,] grid) : this(number, altNumber, name, rarity, null, grid) { }
|
||||
internal Card(int number, string name, Rarity rarity, int? specialCost, Space[,] grid) : this(number, null, name, rarity, specialCost, grid) { }
|
||||
internal Card(int number, int? altNumber, string name, Rarity rarity, int? specialCost, Space[,] grid) {
|
||||
internal Card(int number, string name, Rarity rarity, float textScale, string? artFileName, Space[,] grid) : this(number, null, name, rarity, null, textScale, artFileName, grid) { }
|
||||
internal Card(int number, int? altNumber, string name, Rarity rarity, float textScale, string? artFileName, Space[,] grid) : this(number, altNumber, name, rarity, null, textScale, artFileName, grid) { }
|
||||
internal Card(int number, string name, Rarity rarity, int? specialCost, float textScale, string? artFileName, Space[,] grid) : this(number, null, name, rarity, specialCost, textScale, artFileName, grid) { }
|
||||
internal Card(int number, int? altNumber, string name, Rarity rarity, int? specialCost, float textScale, string? artFileName, Space[,] grid) {
|
||||
this.Number = number;
|
||||
this.AltNumber = altNumber;
|
||||
this.Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||
this.Rarity = rarity;
|
||||
this.TextScale = textScale;
|
||||
this.ArtFileName = artFileName;
|
||||
this.grid = grid ?? throw new ArgumentNullException(nameof(grid));
|
||||
|
||||
var pos = (name ?? throw new ArgumentNullException(nameof(name))).IndexOf('\n');
|
||||
if (pos < 0)
|
||||
this.Name = name;
|
||||
else {
|
||||
this.Name = name[pos - 1] == '-' ? name.Remove(pos, 1) : name.Replace('\n', ' ');
|
||||
this.Line1 = name[0..pos];
|
||||
this.Line2 = name[(pos + 1)..];
|
||||
}
|
||||
|
||||
var size = 0;
|
||||
if (grid.GetUpperBound(0) != 7 || grid.GetUpperBound(1) != 7)
|
||||
throw new ArgumentException("Grid must be 8 × 8.", nameof(grid));
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,12 +1,7 @@
|
|||
using Newtonsoft.Json;
|
||||
|
||||
namespace TableturfBattleServer;
|
||||
namespace TableturfBattleServer;
|
||||
public struct Colour {
|
||||
[JsonProperty("r")]
|
||||
public int R;
|
||||
[JsonProperty("g")]
|
||||
public int G;
|
||||
[JsonProperty("b")]
|
||||
public int B;
|
||||
|
||||
public Colour(int r, int g, int b) {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ internal class WebSocketPayload<T> {
|
|||
}
|
||||
}
|
||||
internal class WebSocketPayloadWithPlayerData<T> : WebSocketPayload<T> {
|
||||
[JsonProperty("playerData")]
|
||||
public PlayerData? PlayerData;
|
||||
|
||||
public WebSocketPayloadWithPlayerData(string eventName, T payload, PlayerData? playerData) : base(eventName, payload)
|
||||
|
|
@ -22,18 +21,13 @@ internal class WebSocketPayloadWithPlayerData<T> : WebSocketPayload<T> {
|
|||
}
|
||||
|
||||
public class PlayerData {
|
||||
[JsonProperty("playerIndex")]
|
||||
public int PlayerIndex;
|
||||
[JsonProperty("hand")]
|
||||
public Card[]? Hand;
|
||||
[JsonProperty("deck")]
|
||||
public Card[]? Deck;
|
||||
[JsonProperty("move")]
|
||||
public Deck? Deck;
|
||||
public Move? Move;
|
||||
[JsonProperty("cardsUsed")]
|
||||
public List<int>? CardsUsed;
|
||||
|
||||
public PlayerData(int playerIndex, Card[]? hand, Card[]? deck, Move? move, List<int>? cardsUsed) {
|
||||
public PlayerData(int playerIndex, Card[]? hand, Deck? deck, Move? move, List<int>? cardsUsed) {
|
||||
this.PlayerIndex = playerIndex;
|
||||
this.Hand = hand;
|
||||
this.Deck = deck;
|
||||
|
|
|
|||
35
TableturfBattleServer/Deck.cs
Normal file
35
TableturfBattleServer/Deck.cs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
using Newtonsoft.Json;
|
||||
|
||||
namespace TableturfBattleServer;
|
||||
public class Deck : IEquatable<Deck> {
|
||||
[JsonProperty]
|
||||
internal string Name;
|
||||
[JsonProperty]
|
||||
internal int Sleeves;
|
||||
[JsonProperty]
|
||||
internal Card[] Cards;
|
||||
[JsonProperty]
|
||||
internal int[] Upgrades;
|
||||
|
||||
public Deck(string name, int sleeves, Card[] cards, int[] levels) {
|
||||
this.Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||
this.Sleeves = sleeves;
|
||||
this.Cards = cards ?? throw new ArgumentNullException(nameof(cards));
|
||||
this.Upgrades = levels ?? throw new ArgumentNullException(nameof(levels));
|
||||
}
|
||||
|
||||
public bool Equals(Deck? other)
|
||||
=> other is not null && this.Name == other.Name && this.Sleeves == other.Sleeves && this.Cards.SequenceEqual(other.Cards) && this.Upgrades.SequenceEqual(other.Upgrades);
|
||||
public override bool Equals(object? other) => other is Deck deck && this.Equals(deck);
|
||||
|
||||
public override int GetHashCode() {
|
||||
var hashCode = new HashCode();
|
||||
hashCode.Add(this.Name);
|
||||
hashCode.Add(this.Sleeves);
|
||||
foreach (var card in this.Cards)
|
||||
hashCode.Add(card.Number);
|
||||
foreach (var n in this.Upgrades)
|
||||
hashCode.Add(n);
|
||||
return hashCode.ToHashCode();
|
||||
}
|
||||
}
|
||||
|
|
@ -6,9 +6,7 @@ namespace TableturfBattleServer;
|
|||
public struct Error {
|
||||
[JsonIgnore]
|
||||
public HttpStatusCode HttpStatusCode { get; }
|
||||
[JsonProperty("code")]
|
||||
public string Code { get; }
|
||||
[JsonProperty("description")]
|
||||
public string Description { get; }
|
||||
|
||||
public Error(HttpStatusCode httpStatusCode, string code, string description) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
|
@ -10,31 +9,25 @@ public class Game {
|
|||
[JsonIgnore]
|
||||
public Guid ID { get; } = Guid.NewGuid();
|
||||
|
||||
[JsonProperty("state")]
|
||||
public GameState State { get; set; }
|
||||
[JsonProperty("turnNumber")]
|
||||
public int TurnNumber { get; set; }
|
||||
[JsonProperty("players")]
|
||||
public List<Player> Players { get; } = new(4);
|
||||
[JsonProperty("maxPlayers")]
|
||||
public int MaxPlayers { get; set; }
|
||||
[JsonProperty("stage")]
|
||||
public string? StageName { get; private set; }
|
||||
[JsonProperty("board")]
|
||||
public Space[,]? Board { get; private set; }
|
||||
[JsonProperty("startSpaces")]
|
||||
public Point[]? StartSpaces;
|
||||
|
||||
[JsonProperty("goalWinCount")]
|
||||
public int? GoalWinCount { get; set; }
|
||||
|
||||
[JsonProperty("turnTimeLimit")]
|
||||
public int? TurnTimeLimit { get; set; }
|
||||
[JsonProperty("turnTimeLeft")]
|
||||
public int? TurnTimeLeft { get; set; }
|
||||
[JsonIgnore]
|
||||
internal DateTime abandonedSince = DateTime.UtcNow;
|
||||
|
||||
[JsonIgnore]
|
||||
internal List<Deck> deckCache = new();
|
||||
[JsonIgnore]
|
||||
internal List<string> setStages = new();
|
||||
|
||||
public Game(int maxPlayers) => this.MaxPlayers = maxPlayers;
|
||||
|
|
@ -77,6 +70,10 @@ public class Game {
|
|||
return false;
|
||||
}
|
||||
|
||||
public Deck GetDeck(string name, int sleeves, IEnumerable<int> cardNumbers, IEnumerable<int> cardUpgrades)
|
||||
=> this.deckCache.FirstOrDefault(d => d.Name == name && d.Sleeves == sleeves && cardNumbers.SequenceEqual(from c in d.Cards select c.Number) && cardUpgrades.SequenceEqual(d.Upgrades))
|
||||
?? new(name, sleeves, (from i in cardNumbers select CardDatabase.GetCard(i)).ToArray(), cardUpgrades.ToArray());
|
||||
|
||||
public bool CanPlay(int playerIndex, Card card, int x, int y, int rotation, bool isSpecialAttack) {
|
||||
if (card is null) throw new ArgumentNullException(nameof(card));
|
||||
if (this.Board is null || this.Players[playerIndex].CurrentGameData is not SingleGameData gameData) return false;
|
||||
|
|
@ -331,7 +328,7 @@ public class Game {
|
|||
foreach (var player in this.Players) {
|
||||
var index = player.GetHandIndex(player.Move!.Card.Number);
|
||||
var draw = player.CurrentGameData.drawOrder![this.TurnNumber + 2];
|
||||
player.Hand![index] = player.CurrentGameData.Deck![draw];
|
||||
player.Hand![index] = player.CurrentGameData.Deck!.Cards[draw];
|
||||
player.ClearMoves();
|
||||
}
|
||||
this.SendEvent("turn", new { game = this, moves, placements, specialSpacesActivated }, true);
|
||||
|
|
@ -372,9 +369,7 @@ public class Game {
|
|||
}
|
||||
}
|
||||
|
||||
internal void SendPlayerReadyEvent(int playerIndex, bool isTimeout) {
|
||||
this.SendEvent("playerReady", new { playerIndex, isTimeout }, false);
|
||||
}
|
||||
internal void SendPlayerReadyEvent(int playerIndex, bool isTimeout) => this.SendEvent("playerReady", new { playerIndex, isTimeout }, false);
|
||||
|
||||
internal void SendEvent<T>(string eventType, T data, bool includePlayerData) {
|
||||
foreach (var session in Program.httpServer!.WebSocketServices.Hosts.First().Sessions.Sessions) {
|
||||
|
|
@ -388,22 +383,24 @@ public class Game {
|
|||
break;
|
||||
}
|
||||
}
|
||||
behaviour.SendInternal(JsonConvert.SerializeObject(new DTO.WebSocketPayloadWithPlayerData<T>(eventType, data, playerData)));
|
||||
behaviour.SendInternal(JsonUtils.Serialise(new DTO.WebSocketPayloadWithPlayerData<T>(eventType, data, playerData)));
|
||||
} else {
|
||||
behaviour.SendInternal(JsonConvert.SerializeObject(new DTO.WebSocketPayload<T>(eventType, data)));
|
||||
behaviour.SendInternal(JsonUtils.Serialise(new DTO.WebSocketPayload<T>(eventType, data)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void WriteReplayData(Stream stream) {
|
||||
const int VERSION = 2;
|
||||
const int VERSION = 3;
|
||||
|
||||
if (this.State < GameState.SetEnded)
|
||||
throw new InvalidOperationException("Can't save a replay until the set has ended.");
|
||||
|
||||
using var writer = new BinaryWriter(stream, Encoding.UTF8, true);
|
||||
writer.Write((byte) VERSION);
|
||||
|
||||
// Players
|
||||
writer.Write((byte) (this.Players.Count | (this.GoalWinCount ?? 0) << 4));
|
||||
foreach (var player in this.Players) {
|
||||
writer.Write((byte) player.Colour.R);
|
||||
|
|
@ -420,14 +417,29 @@ public class Game {
|
|||
writer.Write((byte) (nameBytes.Length | (player.UIBaseColourIsSpecialColour ? 0x80 : 0)));
|
||||
writer.Write(nameBytes);
|
||||
}
|
||||
|
||||
// Deck cache
|
||||
writer.Write7BitEncodedInt(this.deckCache.Count);
|
||||
foreach (var deck in this.deckCache) {
|
||||
writer.Write(deck.Name);
|
||||
writer.Write((byte) deck.Sleeves);
|
||||
foreach (var card in deck.Cards)
|
||||
writer.Write((byte) card.Number);
|
||||
|
||||
int upgradesPacked = 0;
|
||||
for (var i = 0; i < 15; i++)
|
||||
upgradesPacked |= deck.Upgrades[i] << (i * 2);
|
||||
writer.Write(upgradesPacked);
|
||||
}
|
||||
|
||||
// Games
|
||||
for (int i = 0; i < this.Players[0].Games.Count; i++) {
|
||||
var stageNumber = Enumerable.Range(0, StageDatabase.Stages.Count).First(j => this.setStages[i] == StageDatabase.Stages[j].Name);
|
||||
writer.Write((byte) stageNumber);
|
||||
|
||||
foreach (var player in this.Players) {
|
||||
var gameData = player.Games[i];
|
||||
foreach (var card in gameData.Deck!)
|
||||
writer.Write((byte) card.Number);
|
||||
writer.Write7BitEncodedInt(this.deckCache.IndexOf(gameData.Deck!));
|
||||
for (int j = 0; j < 4; j += 2)
|
||||
writer.Write((byte) (gameData.initialDrawOrder![j] | gameData.initialDrawOrder[j + 1] << 4));
|
||||
for (int j = 0; j < 15; j += 2)
|
||||
|
|
|
|||
9
TableturfBattleServer/JsonUtils.cs
Normal file
9
TableturfBattleServer/JsonUtils.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace TableturfBattleServer;
|
||||
internal class JsonUtils {
|
||||
private static readonly JsonSerializerSettings serializerSettings = new() { ContractResolver = new CamelCasePropertyNamesContractResolver() };
|
||||
|
||||
internal static string Serialise(object? o) => JsonConvert.SerializeObject(o, serializerSettings);
|
||||
}
|
||||
|
|
@ -1,22 +1,13 @@
|
|||
using System.ComponentModel;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace TableturfBattleServer;
|
||||
public class Move {
|
||||
[JsonProperty("card")]
|
||||
public Card Card { get; }
|
||||
[JsonProperty("isPass")]
|
||||
public bool IsPass { get; }
|
||||
[JsonProperty("x")]
|
||||
public int X { get; }
|
||||
[JsonProperty("y")]
|
||||
public int Y { get; }
|
||||
[JsonProperty("rotation")]
|
||||
public int Rotation { get; }
|
||||
[JsonProperty("isSpecialAttack")]
|
||||
public bool IsSpecialAttack { get; }
|
||||
[JsonProperty("isTimeout")]
|
||||
public bool IsTimeout { get; }
|
||||
|
||||
public Move(Card card, bool isPass, int x, int y, int rotation, bool isSpecialAttack, bool isTimeout) {
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@
|
|||
|
||||
namespace TableturfBattleServer;
|
||||
public class Placement {
|
||||
[JsonProperty("players")]
|
||||
public List<int> Players { get; } = new();
|
||||
[JsonProperty("spacesAffected"), JsonConverter(typeof(SpacesAffectedDictionaryConverter))]
|
||||
[JsonConverter(typeof(SpacesAffectedDictionaryConverter))]
|
||||
public Dictionary<Point, Space> SpacesAffected { get; } = new();
|
||||
|
||||
internal class SpacesAffectedDictionaryConverter : JsonConverter<Dictionary<Point, Space>> {
|
||||
|
|
|
|||
|
|
@ -2,17 +2,12 @@
|
|||
|
||||
namespace TableturfBattleServer;
|
||||
public class Player {
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; }
|
||||
[JsonIgnore]
|
||||
public Guid Token { get; }
|
||||
[JsonProperty("colour")]
|
||||
public Colour Colour { get; set; }
|
||||
[JsonProperty("specialColour")]
|
||||
public Colour SpecialColour { get; set; }
|
||||
[JsonProperty("specialAccentColour")]
|
||||
public Colour SpecialAccentColour { get; set; }
|
||||
[JsonProperty("uiBaseColourIsSpecialColour")]
|
||||
public bool UIBaseColourIsSpecialColour { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
|
|
@ -26,7 +21,6 @@ public class Player {
|
|||
[JsonIgnore]
|
||||
internal Move? ProvisionalMove;
|
||||
|
||||
[JsonProperty("gamesWon")]
|
||||
public int GamesWon { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
|
|
@ -35,15 +29,12 @@ public class Player {
|
|||
[JsonIgnore]
|
||||
public SingleGameData CurrentGameData => this.Games[^1];
|
||||
|
||||
[JsonProperty("specialPoints")]
|
||||
public int SpecialPoints => this.CurrentGameData.SpecialPoints;
|
||||
|
||||
[JsonProperty("totalSpecialPoints")]
|
||||
public int TotalSpecialPoints => this.CurrentGameData.TotalSpecialPoints;
|
||||
[JsonProperty("passes")]
|
||||
public int Passes => this.CurrentGameData.Passes;
|
||||
public int? Sleeves => this.CurrentGameData.Deck?.Sleeves;
|
||||
|
||||
[JsonProperty("isReady")]
|
||||
public bool IsReady => this.game.State switch {
|
||||
GameState.WaitingForPlayers or GameState.ChoosingStage => this.selectedStageIndex != null,
|
||||
GameState.ChoosingDeck => this.CurrentGameData.Deck != null,
|
||||
|
|
@ -75,7 +66,7 @@ public class Player {
|
|||
if (this.CurrentGameData.Deck != null) {
|
||||
this.Hand = new Card[4];
|
||||
for (int i = 0; i < 4; i++) {
|
||||
this.Hand[i] = this.CurrentGameData.Deck[this.CurrentGameData.drawOrder[i]];
|
||||
this.Hand[i] = this.CurrentGameData.Deck.Cards[this.CurrentGameData.drawOrder[i]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
using Newtonsoft.Json;
|
||||
|
||||
namespace TableturfBattleServer;
|
||||
namespace TableturfBattleServer;
|
||||
public struct Point {
|
||||
[JsonProperty("x")]
|
||||
public int X;
|
||||
[JsonProperty("y")]
|
||||
public int Y;
|
||||
|
||||
public Point(int x, int y) {
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ internal class Program {
|
|||
if (!e.Request.RawUrl.StartsWith("/api/")) {
|
||||
var path = e.Request.RawUrl == "/" || e.Request.RawUrl.StartsWith("/deckeditor") || e.Request.RawUrl.StartsWith("/game/") || e.Request.RawUrl.StartsWith("/replay/")
|
||||
? "index.html"
|
||||
: e.Request.RawUrl[1..];
|
||||
: HttpUtility.UrlDecode(e.Request.RawUrl[1..]);
|
||||
if (e.TryReadFile(path, out var bytes))
|
||||
SetResponse(e.Response, HttpStatusCode.OK,
|
||||
Path.GetExtension(path) switch {
|
||||
|
|
@ -102,6 +102,7 @@ internal class Program {
|
|||
".css" => "text/css",
|
||||
".js" => "text/javascript",
|
||||
".png" => "image/png",
|
||||
".webp" => "image/webp",
|
||||
".woff" or ".woff2" => "font/woff",
|
||||
_ => "application/octet-stream"
|
||||
}, bytes);
|
||||
|
|
@ -163,7 +164,7 @@ internal class Program {
|
|||
games.Add(game.ID, game);
|
||||
timer.Start();
|
||||
|
||||
SetResponse(e.Response, HttpStatusCode.OK, "application/json", JsonConvert.SerializeObject(new { gameID = game.ID, clientToken, maxPlayers }));
|
||||
SetResponse(e.Response, HttpStatusCode.OK, "application/json", JsonUtils.Serialise(new { gameID = game.ID, clientToken, maxPlayers }));
|
||||
Console.WriteLine($"New game started: {game.ID}; {games.Count} games active; {inactiveGames.Count} inactive");
|
||||
} catch (ArgumentException) {
|
||||
SetErrorResponse(e.Response, new(HttpStatusCode.BadRequest, "InvalidRequestData", "Invalid form data"));
|
||||
|
|
@ -193,7 +194,7 @@ internal class Program {
|
|||
SetErrorResponse(e.Response, new(HttpStatusCode.MethodNotAllowed, "MethodNotAllowed", "Invalid request method for this endpoint."));
|
||||
return;
|
||||
}
|
||||
SetResponse(e.Response, HttpStatusCode.OK, "application/json", JsonConvert.SerializeObject(game));
|
||||
SetResponse(e.Response, HttpStatusCode.OK, "application/json", JsonUtils.Serialise(game));
|
||||
break;
|
||||
}
|
||||
case "playerData": {
|
||||
|
|
@ -206,7 +207,7 @@ internal class Program {
|
|||
if (!Guid.TryParse(m.Groups[3].Value, out var clientToken))
|
||||
clientToken = Guid.Empty;
|
||||
|
||||
SetResponse(e.Response, HttpStatusCode.OK, "application/json", JsonConvert.SerializeObject(new {
|
||||
SetResponse(e.Response, HttpStatusCode.OK, "application/json", JsonUtils.Serialise(new {
|
||||
game,
|
||||
playerData = game.GetPlayer(clientToken, out var playerIndex, out var player)
|
||||
? new PlayerData(playerIndex, player)
|
||||
|
|
@ -256,7 +257,7 @@ internal class Program {
|
|||
game.SendEvent("join", new { playerIndex, player }, false);
|
||||
}
|
||||
// If they're already in the game, resend the original join response instead of an error.
|
||||
SetResponse(e.Response, HttpStatusCode.OK, "application/json", JsonConvert.SerializeObject(new { playerIndex, clientToken }));
|
||||
SetResponse(e.Response, HttpStatusCode.OK, "application/json", JsonUtils.Serialise(new { playerIndex, clientToken }));
|
||||
timer.Start();
|
||||
} catch (ArgumentException) {
|
||||
SetErrorResponse(e.Response, new(HttpStatusCode.BadRequest, "InvalidRequestData", "Invalid form data"));
|
||||
|
|
@ -375,6 +376,11 @@ internal class Program {
|
|||
SetErrorResponse(e.Response, new(HttpStatusCode.BadRequest, "InvalidDeckName", "Missing deck name."));
|
||||
return;
|
||||
}
|
||||
var deckSleeves = 0;
|
||||
if (d.TryGetValue("deckSleeves", out var deckSleevesString) && (!int.TryParse(deckSleevesString, out deckSleeves) || deckSleeves is < 0 or >= 25)) {
|
||||
SetErrorResponse(e.Response, new(HttpStatusCode.BadRequest, "InvalidDeckSleeves", "Invalid deck sleeves."));
|
||||
return;
|
||||
}
|
||||
if (!d.TryGetValue("deckCards", out var deckString)) {
|
||||
SetErrorResponse(e.Response, new(HttpStatusCode.BadRequest, "InvalidDeckCards", "Missing deck cards."));
|
||||
return;
|
||||
|
|
@ -384,8 +390,21 @@ internal class Program {
|
|||
SetErrorResponse(e.Response, new(HttpStatusCode.UnprocessableEntity, "InvalidDeckCards", "Invalid deck list."));
|
||||
return;
|
||||
}
|
||||
int[]? upgrades = null;
|
||||
if (d.TryGetValue("deckUpgrades", out var deckUpgradesString)) {
|
||||
upgrades = new int[15];
|
||||
var array2 = deckUpgradesString.Split(new[] { ',', '+', ' ' }, 15);
|
||||
for (var i = 0; i < 15; i++) {
|
||||
if (int.TryParse(array2[i], out var j) && i is >= 0 and <= 2)
|
||||
upgrades[i] = j;
|
||||
else {
|
||||
SetErrorResponse(e.Response, new(HttpStatusCode.UnprocessableEntity, "InvalidDeckUpgrades", "Invalid deck upgrade list."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
var cards = new int[15];
|
||||
for (int i = 0; i < 15; i++) {
|
||||
for (var i = 0; i < 15; i++) {
|
||||
if (!int.TryParse(array[i], out var cardNumber) || !CardDatabase.IsValidCardNumber(cardNumber)) {
|
||||
SetErrorResponse(e.Response, new(HttpStatusCode.UnprocessableEntity, "InvalidDeckCards", "Invalid deck list."));
|
||||
return;
|
||||
|
|
@ -406,7 +425,7 @@ internal class Program {
|
|||
}
|
||||
}
|
||||
|
||||
player.CurrentGameData.Deck = cards.Select(CardDatabase.GetCard).ToArray();
|
||||
player.CurrentGameData.Deck = game.GetDeck(deckName, deckSleeves, cards, upgrades ?? Enumerable.Repeat(0, 15));
|
||||
e.Response.StatusCode = (int) HttpStatusCode.NoContent;
|
||||
game.SendPlayerReadyEvent(playerIndex, false);
|
||||
timer.Start();
|
||||
|
|
@ -584,7 +603,7 @@ internal class Program {
|
|||
}
|
||||
|
||||
private static void SetErrorResponse(HttpListenerResponse response, Error error) {
|
||||
var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(error));
|
||||
var bytes = Encoding.UTF8.GetBytes(JsonUtils.Serialise(error));
|
||||
SetResponse(response, error.HttpStatusCode, "application/json", bytes);
|
||||
}
|
||||
private static void SetResponse(HttpListenerResponse response, HttpStatusCode statusCode, string contentType, string content) {
|
||||
|
|
|
|||
|
|
@ -2,12 +2,8 @@
|
|||
|
||||
namespace TableturfBattleServer;
|
||||
public class SingleGameData {
|
||||
[JsonProperty("specialPoints")]
|
||||
public int SpecialPoints { get; set; }
|
||||
|
||||
[JsonProperty("totalSpecialPoints")]
|
||||
public int TotalSpecialPoints { get; set; }
|
||||
[JsonProperty("passes")]
|
||||
public int Passes { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
|
|
@ -17,7 +13,7 @@ public class SingleGameData {
|
|||
internal List<ReplayTurn> turns = new(12);
|
||||
|
||||
[JsonIgnore]
|
||||
internal Card[]? Deck;
|
||||
internal Deck? Deck;
|
||||
[JsonIgnore]
|
||||
internal int[]? initialDrawOrder;
|
||||
[JsonIgnore]
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@
|
|||
|
||||
namespace TableturfBattleServer;
|
||||
public class Stage {
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; }
|
||||
[JsonProperty("grid")]
|
||||
[JsonProperty]
|
||||
internal readonly Space[,] grid;
|
||||
/// <summary>
|
||||
/// The lists of starting spaces on this stage.
|
||||
|
|
@ -13,7 +12,7 @@ public class Stage {
|
|||
/// The smallest list with at least as many spaces as players in the game will be used.
|
||||
/// For example, if there is a list of 3 and a list of 4, the list of 3 will be used for 2 or 3 players, and the list of 4 will be used for 4 players.
|
||||
/// </remarks>
|
||||
[JsonProperty("startSpaces")]
|
||||
[JsonProperty]
|
||||
internal readonly Point[][] startSpaces;
|
||||
|
||||
public Stage(string name, Space[,] grid, Point[][] startSpaces) {
|
||||
|
|
|
|||
|
|
@ -127,6 +127,6 @@ internal class StageDatabase {
|
|||
|
||||
static StageDatabase() {
|
||||
Stages = Array.AsReadOnly(stages);
|
||||
JSON = JsonConvert.SerializeObject(stages);
|
||||
JSON = JsonUtils.Serialise(stages);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,9 +30,9 @@ internal class TableturfWebSocketBehaviour : WebSocketBehavior {
|
|||
break;
|
||||
}
|
||||
}
|
||||
this.Send(JsonConvert.SerializeObject(new DTO.WebSocketPayloadWithPlayerData<Game?>("sync", game, playerData)));
|
||||
this.Send(JsonUtils.Serialise(new DTO.WebSocketPayloadWithPlayerData<Game?>("sync", game, playerData)));
|
||||
} else
|
||||
this.Send(JsonConvert.SerializeObject(new DTO.WebSocketPayloadWithPlayerData<Game?>("sync", null, null)));
|
||||
this.Send(JsonUtils.Serialise(new DTO.WebSocketPayloadWithPlayerData<Game?>("sync", null, null)));
|
||||
}
|
||||
|
||||
internal void SendInternal(string data) => this.Send(data);
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user