diff --git a/TableturfBattleClient/src/Board.ts b/TableturfBattleClient/src/Board.ts index 7426f36..1a8d091 100644 --- a/TableturfBattleClient/src/Board.ts +++ b/TableturfBattleClient/src/Board.ts @@ -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; diff --git a/TableturfBattleClient/src/Card.ts b/TableturfBattleClient/src/Card.ts index f33076d..3c8bdd5 100644 --- a/TableturfBattleClient/src/Card.ts +++ b/TableturfBattleClient/src/Card.ts @@ -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]; diff --git a/TableturfBattleClient/src/CardButton.ts b/TableturfBattleClient/src/CardButton.ts index d79f188..e480ef9 100644 --- a/TableturfBattleClient/src/CardButton.ts +++ b/TableturfBattleClient/src/CardButton.ts @@ -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'); diff --git a/TableturfBattleClient/src/CardDatabase.ts b/TableturfBattleClient/src/CardDatabase.ts index 6cfe68d..dc440fc 100644 --- a/TableturfBattleClient/src/CardDatabase.ts +++ b/TableturfBattleClient/src/CardDatabase.ts @@ -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((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); diff --git a/TableturfBattleClient/src/CardList.ts b/TableturfBattleClient/src/CardList.ts index cc240a4..702f5d7 100644 --- a/TableturfBattleClient/src/CardList.ts +++ b/TableturfBattleClient/src/CardList.ts @@ -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); +} diff --git a/TableturfBattleClient/src/Deck.ts b/TableturfBattleClient/src/Deck.ts index 9e8c265..8fc14a8 100644 --- a/TableturfBattleClient/src/Deck.ts +++ b/TableturfBattleClient/src/Deck.ts @@ -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; diff --git a/TableturfBattleClient/src/Pages/DeckEditPage.ts b/TableturfBattleClient/src/Pages/DeckEditPage.ts index 10560f2..6257b57 100644 --- a/TableturfBattleClient/src/Pages/DeckEditPage.ts +++ b/TableturfBattleClient/src/Pages/DeckEditPage.ts @@ -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'); }); diff --git a/TableturfBattleClient/src/Pages/DeckListPage.ts b/TableturfBattleClient/src/Pages/DeckListPage.ts index 37c9f57..db1300c 100644 --- a/TableturfBattleClient/src/Pages/DeckListPage.ts +++ b/TableturfBattleClient/src/Pages/DeckListPage.ts @@ -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); diff --git a/TableturfBattleClient/src/ReplayLoader.ts b/TableturfBattleClient/src/ReplayLoader.ts index 90251ad..a1bcfe2 100644 --- a/TableturfBattleClient/src/ReplayLoader.ts +++ b/TableturfBattleClient/src/ReplayLoader.ts @@ -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)); } diff --git a/TableturfBattleClient/src/app.ts b/TableturfBattleClient/src/app.ts index 094c93e..7ce42b3 100644 --- a/TableturfBattleClient/src/app.ts +++ b/TableturfBattleClient/src/app.ts @@ -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'); diff --git a/TableturfBattleClient/tableturf.css b/TableturfBattleClient/tableturf.css index a0be81e..5adba16 100644 --- a/TableturfBattleClient/tableturf.css +++ b/TableturfBattleClient/tableturf.css @@ -306,6 +306,8 @@ dialog::backdrop { display: inline-block; } +:is(#deckCardListView, #deckCardListEdit, .cardListGrid) .card.upcoming { background: midnightblue; } + .card.common { --colour: rgb(89, 49, 255); } .card.rare { --colour: rgb(231, 180, 39); } .card.fresh { --colour: white; } @@ -323,12 +325,25 @@ dialog::backdrop { display: none; } +.cardListGrid .card:hover .cardNumber { + display: block; + position: absolute; + background: grey; + border: 1px solid black; + top: -1.5em; + left: -1em; + padding: 0 1em; + transform: rotate(-10deg); + z-index: 1; +} + .cardName { text-align: center; 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"]) .cardName { +.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 { position: absolute; left: -1em; right: -1em; @@ -415,10 +430,14 @@ dialog::backdrop { top: 0; right: 0; bottom: 0; + border-radius: 0.5em; opacity: 0.25; z-index: 0; } -.card:is(:hover, :focus-within):not(.checked, .disabled)::before { +.card:is(.checked, :hover, :focus-within):not([disabled]) { + transform: rotate(-3deg); +} +.card:is(:hover, :focus-within):not(.checked, .disabled, [disabled])::before { content: ''; background: grey; } @@ -1540,7 +1559,7 @@ dialog::backdrop { .playerBar[data-index="1"] { grid-column: 1 / span 4; grid-row: 1; } .playerBar[data-index="2"] { grid-column: 1 / span 4; grid-row: 3; } .playerBar[data-index="3"] { grid-column: 1 / span 4; grid-row: 1; } - + .wins { margin-right: 8em; } #gamePage.boardFlipped .playerBar[data-index="3"] { grid-column: 1 / span 4; grid-row: 3; } diff --git a/TableturfBattleServer/Card.cs b/TableturfBattleServer/Card.cs index d0759dd..d57bb54 100644 --- a/TableturfBattleServer/Card.cs +++ b/TableturfBattleServer/Card.cs @@ -4,6 +4,8 @@ namespace TableturfBattleServer; public class Card { [JsonProperty("number")] public int Number { get; } + [JsonProperty("altNumber")] + public int? AltNumber { get; } [JsonProperty("name")] public string Name { get; } [JsonProperty("rarity")] @@ -16,9 +18,12 @@ public class Card { [JsonProperty("grid")] private readonly Space[,] grid; - internal Card(int number, string name, Rarity rarity, Space[,] grid) : this(number, name, rarity, null, grid) { } - internal Card(int number, string name, Rarity rarity, int? specialCost, 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) { this.Number = number; + this.AltNumber = altNumber; this.Name = name ?? throw new ArgumentNullException(nameof(name)); this.Rarity = rarity; this.grid = grid ?? throw new ArgumentNullException(nameof(grid)); diff --git a/TableturfBattleServer/CardDatabase.cs b/TableturfBattleServer/CardDatabase.cs index bcb4877..49b9072 100644 --- a/TableturfBattleServer/CardDatabase.cs +++ b/TableturfBattleServer/CardDatabase.cs @@ -2097,21 +2097,158 @@ public static class CardDatabase { { 0, 0, 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0 } + }), + + // Upcoming Drizzle Season 2023 cards + new(-1, "Custom Goo Tuber", Rarity.Common, new Space[,] { + { 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, I, 0, 0, 0, 0, 0 }, + { 0, 0, I, I, 0, 0, 0, 0 }, + { 0, 0, I, 0, I, 0, 0, 0 }, + { 0, 0, I, S, 0, 0, 0, 0 }, + { 0, 0, I, I, 0, 0, 0, 0 }, + { 0, 0, I, I, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0 } + }), + new(-2, "Sloshing Machine Neo", Rarity.Common, new Space[,] { + { 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, I, I, I, 0, 0, 0 }, + { 0, 0, S, I, I, 0, 0, 0 }, + { 0, 0, I, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0 } + }), + new(-3, "Heavy Edit Splatling", Rarity.Common, new Space[,] { + { 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, I, I, 0, 0 }, + { 0, 0, 0, I, I, I, 0, 0 }, + { 0, 0, 0, I, I, S, 0, 0 }, + { 0, 0, 0, I, 0, I, 0, 0 }, + { 0, 0, 0, I, 0, 0, 0, 0 }, + { 0, 0, 0, I, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0 } + }), + new(-4, "Dread Wringer", Rarity.Common, new Space[,] { + { 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, I, 0, I, 0, 0 }, + { 0, 0, I, I, I, I, 0, 0 }, + { 0, 0, I, I, I, I, 0, 0 }, + { 0, 0, 0, 0, S, I, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0 } + }), + new(-5, "Octobrush Nouveau", Rarity.Common, new Space[,] { + { 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, I, 0, 0, 0, 0, 0 }, + { 0, I, I, I, 0, 0, 0, 0 }, + { 0, 0, S, I, 0, 0, 0, 0 }, + { 0, 0, 0, 0, I, 0, 0, 0 }, + { 0, 0, 0, 0, 0, I, 0, 0 }, + { 0, 0, 0, 0, 0, 0, I, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0 } + }), + new(-6, "Gold Dynamo Roller", Rarity.Common, new Space[,] { + { 0, 0, I, I, 0, 0, 0, 0 }, + { 0, 0, I, I, 0, 0, 0, 0 }, + { 0, 0, I, I, 0, 0, 0, 0 }, + { 0, 0, I, S, I, I, 0, 0 }, + { 0, 0, I, 0, 0, 0, 0, 0 }, + { 0, 0, I, 0, 0, 0, 0, 0 }, + { 0, 0, I, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0 } + }), + new(-7, "Tri-Stringer Collab", Rarity.Common, new Space[,] { + { 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, I, 0, 0, 0, 0, 0, 0 }, + { 0, I, 0, 0, 0, 0, 0, 0 }, + { 0, I, I, 0, 0, 0, 0, 0 }, + { 0, S, 0, I, 0, 0, 0, 0 }, + { 0, I, I, I, I, I, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0 } + }), + new(-8, "Bloblobber Deco", Rarity.Common, new Space[,] { + { 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, I, 0, 0, 0, 0 }, + { 0, 0, 0, I, I, 0, 0, 0 }, + { 0, 0, 0, I, I, 0, 0, 0 }, + { 0, 0, 0, I, I, 0, 0, 0 }, + { 0, 0, I, S, I, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0 } + }), + new(-9, "Horrorboros", Rarity.Fresh, new Space[,] { + { 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, S, 0, 0, 0, 0, 0 }, + { 0, I, 0, I, 0, 0, 0, 0 }, + { 0, I, I, 0, I, I, I, 0 }, + { I, 0, I, 0, I, 0, I, 0 }, + { 0, 0, I, I, I, 0, I, 0 }, + { 0, 0, 0, 0, 0, I, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0 } + }), + new(-10, "Cohozuna", Rarity.Fresh, new Space[,] { + { 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, I, 0, I, I, 0, 0, 0 }, + { 0, I, I, I, I, I, 0, 0 }, + { 0, 0, I, I, I, I, 0, 0 }, + { 0, 0, 0, I, I, I, 0, 0 }, + { 0, 0, 0, 0, 0, I, 0, 0 }, + { 0, 0, 0, 0, S, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0 } + }), + new(-11, "Sorella Brella", Rarity.Common, new Space[,] { + { 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, I, 0, 0, 0, 0 }, + { 0, 0, I, I, I, 0, 0, 0 }, + { 0, I, I, S, I, I, 0, 0 }, + { 0, 0, 0, I, 0, 0, 0, 0 }, + { 0, 0, 0, 0, I, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0 } + }), + new(-12, "Ballpoint Splatling Nouveau", Rarity.Common, new Space[,] { + { 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, I, 0, 0, 0 }, + { 0, 0, 0, I, S, 0, 0, 0 }, + { 0, 0, 0, I, I, 0, 0, 0 }, + { 0, 0, I, I, I, 0, 0, 0 }, + { 0, 0, 0, I, I, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0 } }) }; - public static Version Version { get; } = new(4, 1, 0, 0); - public static DateTime LastModified { get; } = new(2023, 8, 12, 9, 0, 0, DateTimeKind.Utc); + private static readonly Dictionary byAltNumber; + + public static int LastOfficialCardNumber { get; } + + public static Version Version { get; } = new(5, 0, 0, 0); + public static DateTime LastModified { get; } = new(2023, 8, 18, 1, 0, 0, DateTimeKind.Utc); public static string JSON { get; } public static ReadOnlyCollection Cards { get; } static CardDatabase() { Cards = Array.AsReadOnly(cards); JSON = JsonConvert.SerializeObject(cards); + LastOfficialCardNumber = cards.Last(c => c.Number > 0).Number; + byAltNumber = new(from c in cards where c.Number < 0 || c.AltNumber is not null select new KeyValuePair(c.Number < 0 ? c.Number : c.AltNumber ?? 0, c)); } - public static Card GetCard(int number) { - number--; - return number >= 0 && number < cards.Length ? cards[number] : throw new ArgumentOutOfRangeException(nameof(number)); - } + // 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. + public static Card GetCard(int number) => number switch { + > 0 => --number < LastOfficialCardNumber ? cards[number] : throw new ArgumentOutOfRangeException(nameof(number)), + < 0 => byAltNumber.TryGetValue(number, out var card) ? card : throw new ArgumentOutOfRangeException(nameof(number)), + _ => throw new ArgumentOutOfRangeException(nameof(number)) + }; + + public static bool IsValidCardNumber(int number) => number switch { + > 0 => number <= LastOfficialCardNumber, + < 0 => byAltNumber.ContainsKey(number), + _ => false + }; } diff --git a/TableturfBattleServer/Program.cs b/TableturfBattleServer/Program.cs index a309592..8bddffd 100644 --- a/TableturfBattleServer/Program.cs +++ b/TableturfBattleServer/Program.cs @@ -349,7 +349,7 @@ internal class Program { } var cards = new int[15]; for (int i = 0; i < 15; i++) { - if (!int.TryParse(array[i], out var cardNumber) || cardNumber < 0 || cardNumber > CardDatabase.Cards.Count) { + if (!int.TryParse(array[i], out var cardNumber) || !CardDatabase.IsValidCardNumber(cardNumber)) { SetErrorResponse(e.Response, new(HttpStatusCode.UnprocessableEntity, "InvalidDeckCards", "Invalid deck list.")); return; }