More custom card UI

This commit is contained in:
Andrio Celos 2024-02-05 16:55:27 +11:00
parent f193477ce3
commit 935d3de744
11 changed files with 233 additions and 93 deletions

View File

@ -609,9 +609,17 @@
</div>
<form method="dialog">
<button type="button" id="galleryCardEditorEditButton">Edit</button>
<button type="submit" id="galleryCardEditorSubmitButton">Save</button>
<button type="button" id="galleryCardEditorSubmitButton">Save</button>
<button type="button" id="galleryCardEditorDeleteButton">Delete</button>
<button type="submit" id="galleryCardEditorCancelButton">Cancel</button>
</form>
<dialog id="galleryCardDeleteDialog">
<div></div>
<form method="dialog">
<button type="submit" id="galleryCardEditorDeleteYesButton">Delete</button>
<button type="submit">Cancel</button>
</form>
</dialog>
</dialog>
</div>
<dialog id="testStageSelectionDialog">

View File

@ -11,7 +11,7 @@ class Card {
inkColour2: Colour;
rarity: Rarity;
specialCost: number;
grid: readonly (readonly Space[])[];
grid: Space[][];
size: number;
isVariantOf?: number | null;
@ -20,8 +20,8 @@ class Card {
private maxX: number;
private maxY: number;
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 };
static DEFAULT_INK_COLOUR_1: Colour = { r: 116, g: 96, b: 240 };
static DEFAULT_INK_COLOUR_2: Colour = { r: 224, g: 242, b: 104 };
constructor(number: number, name: string, textScale: number, inkColour1: Colour, inkColour2: Colour, rarity: Rarity, specialCost: number, grid: Space[][]) {
this.number = number;
this.name = name;

View File

@ -11,7 +11,8 @@ class CardButton extends CheckButton implements ICardElement {
button.type = 'button';
button.classList.add('cardButton');
button.classList.add([ 'common', 'rare', 'fresh' ][card.rarity]);
if (card.number < 0) button.classList.add('upcoming');
if (card.number <= CUSTOM_CARD_START) button.classList.add('custom');
else if (card.number < 0) button.classList.add('upcoming');
button.dataset.cardNumber = card.number.toString();
super(button);
this.element = button;
@ -36,7 +37,7 @@ class CardButton extends CheckButton implements ICardElement {
let el2 = document.createElement('div');
el2.classList.add('cardNumber');
el2.innerText = card.number >= 0 ? `No. ${card.number}` : 'Upcoming';
el2.innerText = card.number >= 0 ? `No. ${card.number}` : card.number <= CUSTOM_CARD_START ? 'Custom' : 'Upcoming';
row.appendChild(el2);
el2 = document.createElement('div');

View File

@ -10,6 +10,9 @@ const cardDatabase = {
if (number > 0) {
number--;
if (number < cardDatabase.lastOfficialCardNumber) return cardDatabase.cards[number];
} else if (number <= CUSTOM_CARD_START) {
const card = customCards[CUSTOM_CARD_START - number];
if (card) return card;
} else if (number < 0) {
const card = cardDatabase._byAltNumber[-number];
if (card) return card;

View File

@ -21,7 +21,8 @@ class CardDisplay implements ICardElement {
this.svg = svg;
element.appendChild(svg);
if (card.number < 0) svg.classList.add('upcoming');
if (card.number <= CUSTOM_CARD_START) svg.classList.add('custom');
else if (card.number < 0) svg.classList.add('upcoming');
svg.dataset.cardNumber = card.number.toString();
svg.style.setProperty("--number", card.number.toString());

View File

@ -72,6 +72,31 @@ class CardList<T extends ICardElement> {
this.listElement.appendChild(button.element);
}
update(button: T, card: Card) {
const i = this.cardButtons.findIndex(c => c.card.number == card.number);
if (i < 0) throw new Error('The card to update was not found in the list.');
const existingButton = this.cardButtons[i];
this.cardButtons.splice(i, 1, button);
this.listElement.replaceChild(button.element, existingButton.element);
}
remove(card: Card) {
const i = this.cardButtons.findIndex(b => b.card.number == card.number);
if (i < 0) return;
this.listElement.removeChild(this.cardButtons[i].element);
this.cardButtons.splice(i, 1);
}
removeAllCustomCards() {
for (let i = this.cardButtons.length - 1; i >= 0; i--) {
const button = this.cardButtons[i];
if (button.card.number <= CUSTOM_CARD_START) {
this.listElement.removeChild(button.element);
this.cardButtons.splice(i, 1);
}
}
}
setSortOrder(sortOrder: string) {
this.sortBox.value = sortOrder;
this.updateSort();

View File

@ -56,3 +56,7 @@ function saveSettings() {
function saveChecklist() {
localStorage.setItem('checklist', JSON.stringify(ownedCards));
}
function saveCustomCards() {
localStorage.setItem('customCards', JSON.stringify(customCards));
}

View File

@ -24,45 +24,49 @@ let draggingCardButton: Element | null = null;
function deckEditInitCardDatabase(cards: Card[]) {
for (const card of cards) {
const button = new CardButton(card);
cardList.add(button);
cardListButtonGroup.add(button, card);
button.buttonElement.addEventListener('click', () => {
if (!button.enabled) return;
for (const button2 of cardList.cardButtons) {
if (button2 != button)
button2.checked = false;
}
const index = deckEditCardButtons.entries.findIndex(el => el.button.checked);
if (index < 0) return;
const oldEntry = deckEditCardButtons.entries[index];
const oldCardNumber = oldEntry.value;
if (oldCardNumber != 0)
cardListButtonGroup.entries.find(e => e.value.number == oldCardNumber || e.value.altNumber == oldCardNumber)!.button.enabled = true;
cardListButtonGroup.entries.find(e => e.value.number == card.number)!.button.enabled = false;
const button3 = createDeckEditCardButton(card.number);
button3.checked = true;
deckEditCardButtons.replace(index, button3, card.number);
deckEditUpdateSize();
cardList.listElement.parentElement!.classList.remove('selecting');
if (!deckModified) {
deckModified = true;
window.addEventListener('beforeunload', onBeforeUnload_deckEditor);
}
selectFirstEmptySlot();
});
addCardToDeckEditor(card);
addTestCard(card);
}
cardList.setSortOrder('size');
testAllCardsList.setSortOrder('size');
}
function addCardToDeckEditor(card: Card) {
const button = new CardButton(card);
cardList.add(button);
cardListButtonGroup.add(button, card);
button.buttonElement.addEventListener('click', () => {
if (!button.enabled) return;
for (const button2 of cardList.cardButtons) {
if (button2 != button)
button2.checked = false;
}
const index = deckEditCardButtons.entries.findIndex(el => el.button.checked);
if (index < 0) return;
const oldEntry = deckEditCardButtons.entries[index];
const oldCardNumber = oldEntry.value;
if (oldCardNumber != 0)
cardListButtonGroup.entries.find(e => e.value.number == oldCardNumber || e.value.altNumber == oldCardNumber)!.button.enabled = true;
cardListButtonGroup.entries.find(e => e.value.number == card.number)!.button.enabled = false;
const button3 = createDeckEditCardButton(card.number);
button3.checked = true;
deckEditCardButtons.replace(index, button3, card.number);
deckEditUpdateSize();
cardList.listElement.parentElement!.classList.remove('selecting');
if (!deckModified) {
deckModified = true;
window.addEventListener('beforeunload', onBeforeUnload_deckEditor);
}
selectFirstEmptySlot();
});
}
function deckEditInitStageDatabase(stages: Stage[]) {
for (const stage of stages) {
const button = new StageButton(stage);
@ -111,6 +115,7 @@ function editDeck() {
for (const entry of cardListButtonGroup.entries)
entry.button.enabled = !selectedDeck.cards.includes(entry.value.number);
reloadCustomCards();
deckEditUpdateSize();
cardList.clearFilter();
editingDeck = true;
@ -118,6 +123,16 @@ function editDeck() {
selectFirstEmptySlot();
}
function reloadCustomCards() {
if (!customCardsModified) return;
cardList.removeAllCustomCards();
testAllCardsList.removeAllCustomCards();
for (const card of customCards) {
addCardToDeckEditor(card);
addTestCard(card);
}
}
function selectFirstEmptySlot() {
let found = false;
for (const el of deckEditCardButtons.entries) {

View File

@ -1,23 +1,29 @@
const galleryCardList = CardList.fromId<CardDisplay>('galleryCardList', 'gallerySortBox', 'galleryFilterBox');
const galleryBackButton = document.getElementById('galleryBackButton') as HTMLLinkElement;
const galleryCardDialog = document.getElementById('galleryCardDialog') as HTMLDialogElement;
const galleryCardDeleteDialog = document.getElementById('galleryCardDeleteDialog') as HTMLDialogElement;
const galleryNewCustomCardButton = document.getElementById('galleryNewCustomCardButton') as HTMLButtonElement;
const galleryChecklistBox = document.getElementById('galleryChecklistBox') as HTMLInputElement;
const bitsToCompleteField = document.getElementById('bitsToCompleteField') as HTMLElement;
let galleryCardDisplay: CardDisplay | null = null;
let gallerySelectedCardDisplay: CardDisplay | null = null;
const galleryCardEditor = document.getElementById('galleryCardEditor') as HTMLButtonElement;
const galleryCardEditorName = document.getElementById('galleryCardEditorName') as HTMLTextAreaElement;
const galleryCardEditorGridButtons: HTMLButtonElement[][] = [ ];
const galleryCardEditorSpecialCostButtons: HTMLButtonElement[] = [ ];
const galleryCardEditorSpecialCost = document.getElementById('galleryCardEditorSpecialCost') as HTMLElement;
const galleryCardEditorSpecialCostDefaultBox = document.getElementById('galleryCardEditorSpecialCostDefaultBox') as HTMLInputElement;
const galleryCardEditorEditButton = document.getElementById('galleryCardEditorEditButton') as HTMLButtonElement;
const galleryCardEditorSubmitButton = document.getElementById('galleryCardEditorSubmitButton') as HTMLButtonElement;
const galleryCardEditorDeleteButton = document.getElementById('galleryCardEditorDeleteButton') as HTMLButtonElement;
const galleryCardEditorCancelButton = document.getElementById('galleryCardEditorCancelButton') as HTMLButtonElement;
const galleryCardEditorDeleteYesButton = document.getElementById('galleryCardEditorDeleteYesButton') as HTMLButtonElement;
const ownedCards: {[key: number]: number} = { 6: 0, 34: 0, 159: 0, 13: 0, 45: 0, 137: 0, 22: 0, 52: 0, 141: 0, 28: 0, 55: 0, 103: 0, 40: 0, 56: 0, 92: 0 };
let lastGridButton: HTMLButtonElement | null = null;
let customCardSize = 0;
let customCardSpecialCost = 0;
function showCardList() {
@ -26,49 +32,86 @@ function showCardList() {
function galleryInitCardDatabase(cards: Card[]) {
for (const card of cards.concat(customCards)) {
const display = new CardDisplay(card, 1, 'button');
const cardNumber = document.createElement('div');
cardNumber.className = 'cardNumber';
cardNumber.innerText = card.number >= 0 ? `No. ${card.number}` : 'Upcoming';
display.element.insertBefore(cardNumber, display.element.firstChild);
galleryCardList.add(display);
display.element.addEventListener('click', () => {
if (galleryChecklistBox.checked) {
if (card.number in ownedCards) {
delete ownedCards[card.number];
display.element.classList.add('unowned');
} else {
ownedCards[card.number] = 0;
display.element.classList.remove('unowned');
}
updateBitsToComplete();
saveChecklist();
} else {
const existingEl = galleryCardDialog.firstElementChild;
if (existingEl && existingEl.tagName != 'FORM')
galleryCardDialog.removeChild(existingEl);
const display = new CardDisplay(card, 1);
galleryCardDisplay = display;
galleryCardDialog.insertBefore(display.element, galleryCardDialog.firstChild);
galleryCardEditor.parentElement?.removeChild(galleryCardEditor);
display.element.appendChild(galleryCardEditor);
galleryCardEditor.hidden = true;
display.element.classList.remove('editing');
galleryCardEditorEditButton.hidden = false;
galleryCardEditorSubmitButton.hidden = true;
galleryCardEditorCancelButton.innerText = 'Close';
galleryCardDialog.showModal();
}
});
addCardToGallery(card);
}
updateBitsToComplete();
}
function addCardToGallery(card: Card) {
const display = createGalleryCardDisplay(card);
galleryCardList.add(display);
}
function createGalleryCardDisplay(card: Card) {
const display = new CardDisplay(card, 1, 'button');
const cardNumber = document.createElement('div');
cardNumber.className = 'cardNumber';
cardNumber.innerText = card.number >= 0 ? `No. ${card.number}` : card.number <= CUSTOM_CARD_START ? 'Custom' : 'Upcoming';
display.element.insertBefore(cardNumber, display.element.firstChild);
display.element.addEventListener('click', () => {
if (galleryChecklistBox.checked) {
if (card.number <= 0) return;
if (card.number in ownedCards) {
delete ownedCards[card.number];
display.element.classList.add('unowned');
} else {
ownedCards[card.number] = 0;
display.element.classList.remove('unowned');
}
updateBitsToComplete();
saveChecklist();
} else {
gallerySelectedCardDisplay = display;
openGalleryCardView(card);
}
});
return display;
}
function updateCardInGallery(card: Card) {
const display = createGalleryCardDisplay(card);
galleryCardList.update(display, card);
}
function openGalleryCardView(card: Card) {
const existingEl = galleryCardDialog.firstElementChild;
if (existingEl && existingEl.tagName != 'FORM')
galleryCardDialog.removeChild(existingEl);
const display = new CardDisplay(card, 1);
galleryCardDisplay = display;
galleryCardDialog.insertBefore(display.element, galleryCardDialog.firstChild);
galleryCardEditor.parentElement?.removeChild(galleryCardEditor);
display.element.appendChild(galleryCardEditor);
galleryCardEditor.hidden = true;
display.element.classList.remove('editing');
galleryCardEditorEditButton.hidden = card.number > CUSTOM_CARD_START;
galleryCardEditorDeleteButton.hidden = card.number > CUSTOM_CARD_START;
galleryCardEditorSubmitButton.hidden = true;
galleryCardEditorCancelButton.innerText = 'Close';
for (let y = 0; y < 8; y++) {
for (let x = 0; x < 8; x++) {
galleryCardEditorGridButtons[y][x].dataset.state = card.grid[y][x].toString();
}
}
updateCustomCardSize();
galleryCardDialog.showModal();
}
function startEditingCustomCard() {
galleryCardEditor.hidden = false;
galleryCardDisplay?.element.classList.add('editing');
galleryCardEditorEditButton.hidden = true;
galleryCardEditorDeleteButton.hidden = true;
galleryCardEditorSubmitButton.hidden = false;
galleryCardEditorCancelButton.innerText = 'Cancel';
}
galleryBackButton.addEventListener('click', e => {
e.preventDefault();
showPage('preGame');
@ -113,13 +156,6 @@ function updateBitsToComplete() {
}
{
const customCardsString = localStorage.getItem('customCards');
if (customCardsString) {
for (const card of JSON.parse(customCardsString)) {
customCards.push(Card.fromJson(card));
}
}
for (let x = 0; x < 8; x++) {
const row = [ ];
for (let y = 0; y < 8; y++) {
@ -168,12 +204,20 @@ function updateBitsToComplete() {
}
}
// Load the saved checklist.
// Load the saved checklist and custom cards.
const checklistString = localStorage.getItem('checklist');
if (checklistString) {
const cards = JSON.parse(checklistString);
Object.assign(ownedCards, cards);
}
const customCardsString = localStorage.getItem('customCards');
if (customCardsString) {
for (const cardJson of JSON.parse(customCardsString)) {
customCards.push(Card.fromJson(cardJson));
}
customCardsModified = customCards.length > 0;
}
}
for (let i = 0; i < 10; i++) {
@ -205,6 +249,7 @@ function updateCustomCardSize() {
}
}
customCardSize = size;
galleryCardDisplay!.setSize(size);
if (galleryCardEditorSpecialCostDefaultBox.checked) {
customCardSpecialCost =
@ -236,10 +281,41 @@ galleryCardEditorSpecialCostDefaultBox.addEventListener('change', () => {
updateCustomCardSize();
});
galleryCardEditorEditButton.addEventListener('click', () => {
galleryCardEditor.hidden = false;
galleryCardDisplay?.element.classList.add('editing');
galleryCardEditorEditButton.hidden = true;
galleryCardEditorSubmitButton.hidden = false;
galleryCardEditorCancelButton.innerText = 'Cancel';
galleryCardEditorEditButton.addEventListener('click', () => startEditingCustomCard());
galleryNewCustomCardButton.addEventListener('click', () => {
const card = new Card(UNSAVED_CUSTOM_CARD_INDEX, 'New card', 1, Card.DEFAULT_INK_COLOUR_1, Card.DEFAULT_INK_COLOUR_2, Rarity.Common, 1, Array.from({ length: 8 }, () => [ 0, 0, 0, 0, 0, 0, 0, 0]) );
openGalleryCardView(card);
startEditingCustomCard();
});
galleryCardEditorSubmitButton.addEventListener('click', () => {
const card = galleryCardDisplay!.card;
card.grid = Array.from(galleryCardEditorGridButtons, r => Array.from(r, b => parseInt(b.dataset.state!)));
card.name = galleryCardEditorName.value;
card.size = customCardSize;
card.specialCost = customCardSpecialCost;
if (card.number == UNSAVED_CUSTOM_CARD_INDEX) {
card.number = CUSTOM_CARD_START - customCards.length;
customCards.push(card);
addCardToGallery(card);
} else {
updateCardInGallery(card);
}
customCardsModified = true;
saveCustomCards();
});
galleryCardEditorDeleteButton.addEventListener('click', () => {
const label = galleryCardDeleteDialog.firstElementChild as HTMLElement;
label.innerText = `Are you sure you want to delete the custom card ${galleryCardDisplay!.card.name}?\nThis cannot be undone!`;
galleryCardDeleteDialog.showModal();
});
galleryCardEditorDeleteYesButton.addEventListener('click', () => {
const card = galleryCardDisplay!.card;
galleryCardList.remove(card);
galleryCardDialog.close();
customCards.splice(customCards.indexOf(card), 1);
saveCustomCards();
});

View File

@ -1,5 +1,7 @@
declare var baseUrl: string;
const CUSTOM_CARD_START = -1000; // TODO: Card numbers in replays shall be expanded to 2 bytes.
const UNSAVED_CUSTOM_CARD_INDEX = CUSTOM_CARD_START + 1;
const defaultColours = [
[ { r: 236, g: 249, b: 1 }, { r: 250, g: 158, b: 0 }, { r: 249, g: 249, b: 31 } ],
[ { r: 74, g: 92, b: 252 }, { r: 1, g: 237, b: 254 }, { r: 213, g: 225, b: 225 } ],
@ -19,6 +21,7 @@ let canPushState = isSecureContext && location.protocol != 'file:';
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).fill(1), true) ];
const customCards: Card[] = [ ];
let customCardsModified = false;
let selectedDeck: SavedDeck | null = null;
let editingDeck = false;
let deckModified = false;

View File

@ -1876,6 +1876,10 @@ button.dragging {
text-align: center;
}
.card.common #galleryCardEditorName { color: rgb(89, 49, 255); }
.card.rare #galleryCardEditorName { color: rgb(231, 180, 39); }
.card.fresh #galleryCardEditorName { color: white; }
.card.editing :is(.cardDisplayName, .cardGrid) {
display: none;
}