Add upcoming cards

Upcoming cards are identified by a negative number. The pre-release number will be preserved as an alt number after release to avoid breaking saved deck and replay data that references it.
This commit is contained in:
Andrio Celos
2023-08-18 21:36:19 +10:00
parent 235d3ac25a
commit d366d955af
14 changed files with 229 additions and 38 deletions

View File

@@ -287,6 +287,7 @@ class Board {
clearChildren(this.table);
this.cells.splice(0);
this.highlightedCells.splice(0);
this.animatedCells.splice(0);
this.specialAnimatedCells.splice(0);
const boardWidth = this.grid.length;

View File

@@ -1,5 +1,6 @@
class Card {
number: number;
altNumber: number | null;
name: string;
rarity: Rarity;
specialCost: number;
@@ -11,8 +12,9 @@ class Card {
private maxX: number;
private maxY: number;
constructor(number: number, name: string, rarity: Rarity, specialCost: number, grid: Space[][]) {
constructor(number: number, altNumber: number | null, name: string, rarity: Rarity, specialCost: number, grid: Space[][]) {
this.number = number;
this.altNumber = altNumber;
this.name = name;
this.rarity = rarity;
this.specialCost = specialCost;
@@ -38,9 +40,11 @@ class Card {
}
static fromJson(obj: any) {
return new Card(obj.number, obj.name, obj.rarity, obj.specialCost, obj.grid);
return new Card(obj.number, obj.altNumber ?? null, obj.name, obj.rarity, obj.specialCost, obj.grid);
}
get isUpcoming() { return this.number < 0; }
getSpace(x: number, y: number, rotation: number) {
switch (rotation & 3) {
case 0: return this.grid[x][y];

View File

@@ -10,6 +10,7 @@ class CardButton extends CheckButton {
button.type = 'button';
button.classList.add('card');
button.classList.add([ 'common', 'rare', 'fresh' ][card.rarity]);
if (card.number < 0) button.classList.add('upcoming');
button.dataset.cardNumber = card.number.toString();
super(button);
@@ -40,7 +41,7 @@ class CardButton extends CheckButton {
let el2 = document.createElement('div');
el2.classList.add('cardNumber');
el2.innerText = card.number.toString();
el2.innerText = card.number >= 0 ? `No. ${card.number}` : 'Upcoming';
row.appendChild(el2);
el2 = document.createElement('div');

View File

@@ -1,9 +1,23 @@
const cardDatabase = {
cards: null as Card[] | null,
lastOfficialCardNumber: 0,
_byAltNumber: [ ] as Card[],
// Upcoming cards are identified with a negative number, as their actual numbers aren't known until their release.
// The placeholder numbers will be kept as alternate numbers then, to avoid breaking replay and saved deck data that uses them.
get(number: number) {
if (cardDatabase.cards == null) throw new Error('Card database not loaded');
if (number <= 0 || number > cardDatabase.cards.length) throw new RangeError(`No card with number ${number}`);
return cardDatabase.cards[number - 1];
if (number > 0) {
number--;
if (number < cardDatabase.lastOfficialCardNumber) return cardDatabase.cards[number];
} else if (number < 0) {
const card = cardDatabase._byAltNumber[-number];
if (card) return card;
}
throw new RangeError(`No card with number ${number}`);
},
isValidCardNumber(number: number) {
return number > 0 ? number <= cardDatabase.lastOfficialCardNumber : cardDatabase._byAltNumber[-number] != undefined;
},
loadAsync() {
return new Promise<Card[]>((resolve, reject) => {
@@ -19,7 +33,11 @@ const cardDatabase = {
const s = cardListRequest.responseText;
const response = JSON.parse(s) as object[];
for (const o of response) {
cards.push(Card.fromJson(o));
const card = Card.fromJson(o);
cards.push(card);
cardDatabase.lastOfficialCardNumber = Math.max(cardDatabase.lastOfficialCardNumber, card.number);
if (card.number < 0) cardDatabase._byAltNumber[-card.number] = card;
else if (card.altNumber != null && card.altNumber < 0) cardDatabase._byAltNumber[-card.altNumber] = card;
}
cardDatabase.cards = cards;
resolve(cards);

View File

@@ -4,11 +4,11 @@ class CardList {
readonly filterBox: HTMLInputElement;
readonly cardButtons: CardButton[] = [ ];
static readonly cardSortOrders: { [key: string]: ((a: Card, b: Card) => number) | undefined } = {
'number': (a, b) => a.number - b.number,
static readonly cardSortOrders: { [key: string]: (a: Card, b: Card) => number } = {
'number': (a, b) => compareCardNumbers(a, b),
'name': (a, b) => a.name.localeCompare(b.name),
'size': (a, b) => a.size != b.size ? a.size - b.size : a.number - b.number,
'rarity': (a, b) => a.rarity != b.rarity ? a.rarity - b.rarity : a.number - b.number,
'size': (a, b) => a.size != b.size ? a.size - b.size : compareCardNumbers(a, b),
'rarity': (a, b) => a.rarity != b.rarity ? a.rarity - b.rarity : compareCardNumbers(a, b),
}
constructor(listElement: HTMLElement, sortBox: HTMLSelectElement, filterBox: HTMLInputElement) {
@@ -55,3 +55,7 @@ class CardList {
button.buttonElement.hidden = false;
}
}
function compareCardNumbers(a: Card, b: Card) {
// Sort upcoming cards after released cards.
return a.number >= 0 ? (b.number >= 0 ? a.number - b.number : -1) : (b.number >= 0 ? 1 : b.number - a.number);
}

View File

@@ -13,7 +13,7 @@ class Deck {
if (!cardDatabase.cards) throw new Error('Card database must be loaded to validate decks.');
if (this.cards.length != 15) return false;
for (let i = 0; i < 15; i++) {
if (this.cards[i] <= 0 || this.cards[i] > cardDatabase.cards.length) return false;
if (!cardDatabase.isValidCardNumber(this.cards[i])) return false;
if (this.cards.indexOf(this.cards[i], i + 1) >= 0) return false; // Duplicate cards
}
return true;

View File

@@ -68,9 +68,8 @@ function deckEditInitStageDatabase(stages: Stage[]) {
}
} else if (selectedDeck) {
for (const cardNumber of selectedDeck.cards) {
if (cardNumber > 0 && cardNumber <= cardDatabase.cards!.length) {
if (cardDatabase.isValidCardNumber(cardNumber))
addTestDeckCard(cardDatabase.get(cardNumber));
}
}
}
@@ -105,12 +104,12 @@ function editDeck() {
showPage('deckEdit');
}
function createDeckEditCardButton(index: number, card: number) {
const button = new CardButton(cardDatabase.cards![card - 1]);
function createDeckEditCardButton(index: number, cardNumber: number) {
const button = new CardButton(cardDatabase.get(cardNumber));
button.buttonElement.addEventListener('click', () => {
selectedDeckCardIndex = index;
for (const button2 of cardList.cardButtons) {
button2.checked = button2.card.number == card;
button2.checked = button2.card.number == cardNumber;
}
cardList.listElement.parentElement!.classList.add('selecting');
});

View File

@@ -158,7 +158,7 @@ deckImportForm.addEventListener('submit', e => {
const data = JSON.parse(deckImportTextBox.value);
const decks = (data instanceof Array ? data : [ data ]) as Deck[];
for (const deck of decks) {
if (typeof(deck) != 'object' || !Array.isArray(deck.cards) || deck.cards.length != 15 || deck.cards.find(i => i < 0 || i > cardDatabase.cards!.length))
if (typeof(deck) != 'object' || !Array.isArray(deck.cards) || deck.cards.length != 15 || deck.cards.find(i => !cardDatabase.isValidCardNumber(i)))
throw new SyntaxError('Invalid JSON deck');
}
importDecks(decks);
@@ -187,7 +187,7 @@ function selectDeck() {
deckNameLabel.innerText = selectedDeck.name;
for (const cardNumber of selectedDeck.cards) {
if (cardNumber) {
const card = cardDatabase.cards![cardNumber - 1];
const card = cardDatabase.get(cardNumber);
size += card.size;
const button = new CardButton(card);

View File

@@ -38,7 +38,7 @@ function loadReplay(base64: string) {
const initialDrawOrder = [ ];
const drawOrder = [ ];
for (let j = 0; j < 15; j++) {
deck.push(cardDatabase.get(dataView.getUint8(pos + 9 + j)));
deck.push(loadCardFromReplay(dataView, pos + 9 + j));
}
for (let j = 0; j < 2; j++) {
initialDrawOrder.push(dataView.getUint8(pos + 24 + j) & 0xF);
@@ -59,14 +59,14 @@ function loadReplay(base64: string) {
for (let i = 0; i < 12; i++) {
const turn = [ ];
for (let j = 0; j < numPlayers; j++) {
const cardNumber = dataView.getUint8(pos);
const card = loadCardFromReplay(dataView, pos);
const b = dataView.getUint8(pos + 1);
const x = dataView.getInt8(pos + 2);
const y = dataView.getInt8(pos + 3);
if (b & 0x80)
turn.push({ card: cardDatabase.get(cardNumber), isPass: true, isTimeout: (b & 0x20) != 0 });
turn.push({ card, isPass: true, isTimeout: (b & 0x20) != 0 });
else {
const move: PlayMove = { card: cardDatabase.get(cardNumber), isPass: false, isTimeout: (b & 0x20) != 0, x, y, rotation: b & 0x03, isSpecialAttack: (b & 0x40) != 0 };
const move: PlayMove = { card, isPass: false, isTimeout: (b & 0x20) != 0, x, y, rotation: b & 0x03, isSpecialAttack: (b & 0x40) != 0 };
turn.push(move);
}
pos += 4;
@@ -113,7 +113,7 @@ function loadReplay(base64: string) {
const drawOrder = [ ];
let won = false;
for (let j = 0; j < 15; j++) {
deck.push(cardDatabase.get(dataView.getUint8(pos + j)));
deck.push(loadCardFromReplay(dataView, pos + j));
}
for (let j = 0; j < 2; j++) {
initialDrawOrder.push(dataView.getUint8(pos + 15 + j) & 0xF);
@@ -162,14 +162,14 @@ function replayLoadTurns(dataView: DataView, numPlayers: number, pos: number) {
for (let i = 0; i < 12; i++) {
const turn = [ ];
for (let j = 0; j < numPlayers; j++) {
const cardNumber = dataView.getUint8(pos);
const card = loadCardFromReplay(dataView, pos);
const b = dataView.getUint8(pos + 1);
const x = dataView.getInt8(pos + 2);
const y = dataView.getInt8(pos + 3);
if (b & 0x80)
turn.push({ card: cardDatabase.get(cardNumber), isPass: true, isTimeout: (b & 0x20) != 0 });
turn.push({ card, isPass: true, isTimeout: (b & 0x20) != 0 });
else {
const move: PlayMove = { card: cardDatabase.get(cardNumber), isPass: false, isTimeout: (b & 0x20) != 0, x, y, rotation: b & 0x03, isSpecialAttack: (b & 0x40) != 0 };
const move: PlayMove = { card, isPass: false, isTimeout: (b & 0x20) != 0, x, y, rotation: b & 0x03, isSpecialAttack: (b & 0x40) != 0 };
turn.push(move);
}
pos += 4;
@@ -178,3 +178,5 @@ function replayLoadTurns(dataView: DataView, numPlayers: number, pos: number) {
}
return turns;
}
function loadCardFromReplay(dataView: DataView, index: number) { return cardDatabase.get(dataView.getUint8(index) > cardDatabase.lastOfficialCardNumber ? dataView.getInt8(index) : dataView.getUint8(index)); }

View File

@@ -347,6 +347,7 @@ function setupWebSocket(gameID: string) {
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');