Add WIP gallery page and custom card editor

This commit is contained in:
Andrio Celos 2024-02-04 22:32:09 +11:00
parent 329993b15d
commit f193477ce3
13 changed files with 498 additions and 41 deletions

View File

@ -57,6 +57,7 @@
</div>
<p>
<a id="preGameDeckEditorButton" href="deckeditor">Edit decks</a> |
<a id="preGameGalleryButton" href="cardlist">Card list</a> |
<a id="preGameReplayButton" href="replay">Replay</a> |
<a id="preGameSettingsButton" href="settings">Settings</a> |
<a id="preGameHelpButton" href="help">Help</a>
@ -584,6 +585,35 @@
</section>
<div id="deckEditorCardListBackdrop"></div>
</div>
<div id="galleryPage" hidden>
<header>
<a id="galleryBackButton" href=".">Back</a>
<label for="gallerySortBox">
Sort by
<select id="gallerySortBox" autocomplete="off"></select>
</label>
<input type="text" id="galleryFilterBox" placeholder="Filter"/>
<button id="galleryNewCustomCardButton">New custom card</button>
<label for="galleryChecklistBox"><input type="checkbox" id="galleryChecklistBox" autocomplete="off"/>Checklist</label>
Bits to complete: <span id="bitsToCompleteField"></span>
</header>
<main id="galleryCardList">
</main>
<dialog id="galleryCardDialog">
<div id="galleryCardEditor" hidden>
<textarea type="text" id="galleryCardEditorName" placeholder="Card name"></textarea>
<div id="galleryCardEditorGrid"></div>
<div id="galleryCardEditorSpecialCost">
<label for="galleryCardEditorSpecialCostDefaultBox"><input type="checkbox" id="galleryCardEditorSpecialCostDefaultBox" checked/> Default</label>
</div>
</div>
<form method="dialog">
<button type="button" id="galleryCardEditorEditButton">Edit</button>
<button type="submit" id="galleryCardEditorSubmitButton">Save</button>
<button type="submit" id="galleryCardEditorCancelButton">Cancel</button>
</form>
</dialog>
</div>
<dialog id="testStageSelectionDialog">
<h3>Select a stage.</h3>
<form id="testStageSelectionForm" method="dialog">

View File

@ -1,6 +1,7 @@
/// <reference path="CheckButton.ts"/>
class CardButton extends CheckButton {
class CardButton extends CheckButton implements ICardElement {
readonly element: HTMLButtonElement;
private static idNumber = 0;
readonly card: Card;
@ -13,6 +14,7 @@ class CardButton extends CheckButton {
if (card.number < 0) button.classList.add('upcoming');
button.dataset.cardNumber = card.number.toString();
super(button);
this.element = button;
this.card = card;

View File

@ -1,15 +1,26 @@
class CardDisplay {
class CardDisplay implements ICardElement {
readonly card: Card;
readonly element: SVGSVGElement;
readonly element: HTMLElement;
readonly svg: SVGSVGElement;
private readonly sizeElement: SVGTextElement;
private readonly specialCostGroup: SVGGElement;
private level: number;
constructor(card: Card, level: number) {
let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
constructor(card: Card, level: number, elementType: string = 'div') {
this.card = card;
this.level = level;
const element = document.createElement(elementType);
element.classList.add('card');
element.classList.add([ 'common', 'rare', 'fresh' ][card.rarity]);
this.element = element;
const 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;
this.svg = svg;
element.appendChild(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());
@ -44,6 +55,7 @@ class CardDisplay {
// Grid
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
g.setAttribute('class', 'cardGrid');
g.setAttribute('transform', 'translate(380 604) rotate(6.5) scale(0.283)');
svg.appendChild(g);
@ -54,6 +66,7 @@ class CardDisplay {
text1.setAttribute('class', 'cardDisplayName');
text1.setAttribute('x', '50%');
text1.setAttribute('y', '168');
text1.setAttribute('text-anchor', 'middle');
text1.setAttribute('font-size', '76');
text1.setAttribute('font-weight', 'bold');
text1.setAttribute('stroke', 'black');
@ -108,28 +121,17 @@ class CardDisplay {
// 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'>${card.size}</text>`);
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' text-anchor='middle'>${card.size}</text>`);
this.sizeElement = svg.lastElementChild as SVGTextElement;
// Special cost
const g2 = document.createElementNS('http://www.w3.org/2000/svg', 'g');
this.specialCostGroup = g2;
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;
this.setSpecialCost(card.specialCost);
}
static CreateSvgCardGrid(card: Card, parent: SVGElement) {
@ -162,4 +164,24 @@ class CardDisplay {
}
}
}
setSpecialCost(value: number) {
clearChildren(this.specialCostGroup);
for (let i = 0; i < value; 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');
this.specialCostGroup.appendChild(el);
}
}
}
setSize(value: number) {
this.sizeElement.innerHTML = value.toString();
}
}

View File

@ -1,8 +1,8 @@
class CardList {
class CardList<T extends ICardElement> {
readonly listElement: HTMLElement;
readonly sortBox: HTMLSelectElement;
readonly filterBox: HTMLInputElement;
readonly cardButtons: CardButton[] = [ ];
readonly cardButtons: T[] = [ ];
static readonly cardSortOrders: { [key: string]: (a: Card, b: Card) => number } = {
'number': (a, b) => CardList.compareByNumber(a, b),
@ -42,7 +42,7 @@ class CardList {
filterBox.addEventListener('input', () => {
const s = filterBox.value.toLowerCase();
for (const button of this.cardButtons)
button.buttonElement.hidden = s != '' && !button.card.name.toLowerCase().includes(s);
button.element.hidden = s != '' && !button.card.name.toLowerCase().includes(s);
});
for (const label in CardList.cardSortOrders) {
@ -59,17 +59,17 @@ class CardList {
clearChildren(this.listElement);
this.cardButtons.sort((a, b) => sortOrder(a.card, b.card));
for (const button of this.cardButtons)
this.listElement.appendChild(button.buttonElement);
this.listElement.appendChild(button.element);
}
}
static fromId(id: string, sortBoxId: string, filterBoxId: string) {
return new CardList(document.getElementById(id)!, document.getElementById(sortBoxId) as HTMLSelectElement, document.getElementById(filterBoxId) as HTMLInputElement);
static fromId<T extends ICardElement>(id: string, sortBoxId: string, filterBoxId: string) {
return new CardList<T>(document.getElementById(id)!, document.getElementById(sortBoxId) as HTMLSelectElement, document.getElementById(filterBoxId) as HTMLInputElement);
}
add(button: CardButton) {
add(button: T) {
this.cardButtons.push(button);
this.listElement.appendChild(button.buttonElement);
this.listElement.appendChild(button.element);
}
setSortOrder(sortOrder: string) {
@ -80,6 +80,6 @@ class CardList {
clearFilter() {
this.filterBox.value = '';
for (const button of this.cardButtons)
button.buttonElement.hidden = false;
button.element.hidden = false;
}
}

View File

@ -52,3 +52,7 @@ let userConfig = new Config();
function saveSettings() {
localStorage.setItem('settings', JSON.stringify(userConfig));
}
function saveChecklist() {
localStorage.setItem('checklist', JSON.stringify(ownedCards));
}

View File

@ -0,0 +1,4 @@
interface ICardElement {
card: Card;
element: HTMLElement;
}

View File

@ -3,7 +3,7 @@
const deckNameLabel2 = document.getElementById('deckName2')!;
const deckEditSize = document.getElementById('deckEditSize')!;
const deckCardListEdit = document.getElementById('deckCardListEdit')!;
const cardList = CardList.fromId('cardList', 'cardListSortBox', 'cardListFilterBox');
const cardList = CardList.fromId<CardButton>('cardList', 'cardListSortBox', 'cardListFilterBox');
const cardListButtonGroup = new CheckButtonGroup<Card>();
const deckEditMenu = document.getElementById('deckEditMenu')!;

View File

@ -0,0 +1,245 @@
const galleryCardList = CardList.fromId<CardDisplay>('galleryCardList', 'gallerySortBox', 'galleryFilterBox');
const galleryBackButton = document.getElementById('galleryBackButton') as HTMLLinkElement;
const galleryCardDialog = document.getElementById('galleryCardDialog') 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;
const galleryCardEditor = document.getElementById('galleryCardEditor') as HTMLButtonElement;
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 galleryCardEditorCancelButton = document.getElementById('galleryCardEditorCancelButton') 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 customCardSpecialCost = 0;
function showCardList() {
showPage('gallery');
}
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();
}
});
}
updateBitsToComplete();
}
galleryBackButton.addEventListener('click', e => {
e.preventDefault();
showPage('preGame');
if (canPushState) {
try {
history.pushState(null, '', '.');
} catch {
canPushState = false;
}
}
if (location.hash)
location.hash = '';
});
galleryChecklistBox.addEventListener('change', () => {
if (galleryChecklistBox.checked) {
for (const cardDisplay of galleryCardList.cardButtons) {
if (cardDisplay.card.number in ownedCards)
cardDisplay.element.classList.remove('unowned');
else
cardDisplay.element.classList.add('unowned');
}
} else {
for (const cardDisplay of galleryCardList.cardButtons)
cardDisplay.element.classList.remove('unowned');
}
});
function updateBitsToComplete() {
if (!cardDatabase.cards) throw new Error('Card database not loaded');
let bitsRequired = 0;
for (const card of cardDatabase.cards) {
if (card.number in ownedCards) continue;
switch (card.rarity) {
case Rarity.Fresh: bitsRequired += 40; break;
case Rarity.Rare: bitsRequired += 15; break;
default: bitsRequired += 5; break;
}
}
bitsToCompleteField.innerText = bitsRequired.toString();
}
{
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++) {
const button = document.createElement('button');
button.type = 'button';
button.dataset.state = Space.Empty.toString();
button.dataset.x = x.toString();
button.dataset.y = y.toString();
button.addEventListener('click', () => {
const state = parseInt(button.dataset.state ?? '0');
switch (state) {
case Space.Empty:
button.dataset.state = Space.Ink1.toString();
break;
case Space.Ink1:
if (lastGridButton == button) {
// When a space is pressed twice, move the special space there.
for (const row of galleryCardEditorGridButtons) {
for (const button2 of row) {
if (button2 == button)
button2.dataset.state = Space.SpecialInactive1.toString();
else if (button2.dataset.state == Space.SpecialInactive1.toString())
button2.dataset.state = Space.Ink1.toString();
}
}
} else
button.dataset.state = Space.Empty.toString();
break;
default:
button.dataset.state = Space.Empty.toString();
break;
}
lastGridButton = button;
updateCustomCardSize();
});
row.push(button);
}
galleryCardEditorGridButtons.push(row);
}
const galleryCardEditorGrid = document.getElementById('galleryCardEditorGrid')!;
for (let y = 0; y < 8; y++) {
for (let x = 0; x < 8; x++) {
galleryCardEditorGrid.appendChild(galleryCardEditorGridButtons[x][y]);
}
}
// Load the saved checklist.
const checklistString = localStorage.getItem('checklist');
if (checklistString) {
const cards = JSON.parse(checklistString);
Object.assign(ownedCards, cards);
}
}
for (let i = 0; i < 10; i++) {
const button = document.createElement('button');
const n = i < 5 ? i + 6 : i - 4;
button.dataset.value = n.toString();
galleryCardEditorSpecialCost.appendChild(button);
galleryCardEditorSpecialCostButtons.push(button);
button.addEventListener('click', () => {
customCardSpecialCost = n;
galleryCardEditorSpecialCostDefaultBox.checked = false;
updateCustomCardSpecialCost();
});
}
function updateCustomCardSize() {
let size = 0, hasSpecialSpace = false;
for (const row of galleryCardEditorGridButtons) {
for (const button2 of row) {
switch (parseInt(button2.dataset.state!)) {
case Space.Ink1:
size++;
break;
case Space.SpecialInactive1:
size++;
hasSpecialSpace = true;
break;
}
}
}
galleryCardDisplay!.setSize(size);
if (galleryCardEditorSpecialCostDefaultBox.checked) {
customCardSpecialCost =
size <= 3 ? 1
: size <= 5 ? 2
: size <= 8 ? 3
: size <= 11 ? 4
: size <= 15 ? 5
: 6;
if (!hasSpecialSpace && customCardSpecialCost > 3)
customCardSpecialCost = 3;
updateCustomCardSpecialCost();
}
}
function updateCustomCardSpecialCost() {
galleryCardDisplay?.setSpecialCost(customCardSpecialCost);
for (let i = 0; i < galleryCardEditorSpecialCostButtons.length; i++) {
const button = galleryCardEditorSpecialCostButtons[i];
if (parseInt(button.dataset.value!) <= customCardSpecialCost)
button.classList.add('active');
else
button.classList.remove('active');
}
}
galleryCardEditorSpecialCostDefaultBox.addEventListener('change', () => {
if (galleryCardEditorSpecialCostDefaultBox.checked)
updateCustomCardSize();
});
galleryCardEditorEditButton.addEventListener('click', () => {
galleryCardEditor.hidden = false;
galleryCardDisplay?.element.classList.add('editing');
galleryCardEditorEditButton.hidden = true;
galleryCardEditorSubmitButton.hidden = false;
galleryCardEditorCancelButton.innerText = 'Cancel';
});

View File

@ -47,7 +47,7 @@ playContainers.sort((a, b) => parseInt(a.dataset.index || '0') - parseInt(b.data
const testControls = document.getElementById('testControls')!;
const testDeckList = document.getElementById('testDeckList')!;
const testAllCardsList = CardList.fromId('testAllCardsList', 'testAllCardsListSortBox', 'testAllCardsListFilterBox');
const testAllCardsList = CardList.fromId<CardButton>('testAllCardsList', 'testAllCardsListSortBox', 'testAllCardsListFilterBox');
const testPlacementList = document.getElementById('testPlacementList')!;
const testDeckButton = CheckButton.fromId('testDeckButton');
const testDeckContainer = document.getElementById('testDeckContainer')!;

View File

@ -5,6 +5,7 @@ const joinGameButton = document.getElementById('joinGameButton')!;
const nameBox = document.getElementById('nameBox') as HTMLInputElement;
const gameIDBox = document.getElementById('gameIDBox') as HTMLInputElement;
const preGameDeckEditorButton = document.getElementById('preGameDeckEditorButton') as HTMLLinkElement;
const preGameGalleryButton = document.getElementById('preGameGalleryButton') as HTMLLinkElement;
const preGameLoadingSection = document.getElementById('preGameLoadingSection')!;
const preGameLoadingLabel = document.getElementById('preGameLoadingLabel')!;
const preGameReplayButton = document.getElementById('preGameReplayButton') as HTMLLinkElement;
@ -301,6 +302,12 @@ preGameDeckEditorButton.addEventListener('click', e => {
setUrl('deckeditor');
});
preGameGalleryButton.addEventListener('click', e => {
e.preventDefault();
showCardList();
setUrl('cardlist');
});
preGameSettingsButton.addEventListener('click', e => {
e.preventDefault();
optionsColourGoodBox.value = userConfig.goodColour ?? 'yellow';
@ -419,6 +426,8 @@ window.addEventListener('popstate', () => {
}
}
if (!canPushState)
if (!canPushState) {
preGameDeckEditorButton.href = '#deckeditor';
preGameGalleryButton.href = '#cardlist';
}
setLoadingMessage('Loading game data...');

View File

@ -18,6 +18,7 @@ let initialiseCallback: (() => void) | null = null;
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 selectedDeck: SavedDeck | null = null;
let editingDeck = false;
let deckModified = false;
@ -50,6 +51,7 @@ function onInitialise(callback: () => void) {
function initCardDatabase(cards: Card[]) {
deckEditInitCardDatabase(cards);
galleryInitCardDatabase(cards);
if (!cards.find(c => c.number < 0)) {
gameSetupAllowUpcomingCardsBox.parentElement!.hidden = true;
lobbyAllowUpcomingCardsBox.parentElement!.hidden = true;
@ -63,7 +65,7 @@ function initStageDatabase(stages: Stage[]) {
// Pages
const pages = new Map<string, HTMLDivElement>();
for (var id of [ 'noJS', 'preGame', 'lobby', 'game', 'deckList', 'deckEdit' ]) {
for (var id of [ 'noJS', 'preGame', 'lobby', 'game', 'deckList', 'deckEdit', 'gallery' ]) {
let el = document.getElementById(`${id}Page`) as HTMLDivElement;
if (!el) throw new EvalError(`Element not found: ${id}Page`);
pages.set(id, el);
@ -484,6 +486,8 @@ function processUrl() {
clearGame();
if (location.pathname.endsWith('/deckeditor') || location.hash == '#deckeditor')
onInitialise(showDeckList);
else if (location.pathname.endsWith('/cardlist') || location.hash == '#cardlist')
onInitialise(showCardList);
else {
showPage('preGame');
if (location.pathname.endsWith('/help') || location.hash == '#help')

View File

@ -501,10 +501,6 @@ dialog::backdrop {
.cardNumber {
display: none;
}
.cardListGrid .cardButton:hover .cardNumber {
display: block;
position: absolute;
background: grey;
border: 1px solid black;
@ -515,6 +511,10 @@ dialog::backdrop {
z-index: 1;
}
.cardListGrid .cardButton:hover .cardNumber {
display: block;
}
.cardName {
text-align: center;
line-height: 1.25em;
@ -605,6 +605,7 @@ dialog::backdrop {
}
.playContainer .card {
animation: 0.1s ease-out forwards flipCardIn;
height: 100%;
}
.playContainer .card.preview {
animation: none;
@ -1810,6 +1811,142 @@ button.dragging {
#deckSleevesList label:nth-of-type(24) { background-position: -700% -200%; }
#deckSleevesList label:nth-of-type(25) { background-position: 0% -300%; }
/* Card list */
#galleryPage:not([hidden]) {
height: 100vh;
display: flex;
flex-flow: column;
}
#galleryCardList {
display: grid;
grid-template-columns: repeat(auto-fill, 10em);
grid-auto-rows: auto;
gap: 0.5em;
justify-content: space-evenly;
}
#galleryCardList .card {
height: 14rem;
border: none;
background: none;
padding: 0;
transition: transform 0.25s ease;
}
#galleryCardList .card.unowned svg {
opacity: 0.333;
}
#galleryCardList .card:hover {
transform: scale(1.1);
}
#galleryCardList .card:hover .cardNumber {
display: block;
font-size: 1rem;
top: -0.5em;
}
#galleryCardDialog > .card {
height: min(75vh, 100vw);
}
#galleryCardEditor {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
font-size: calc(min(75vh, 100vw) * 0.08);
}
#galleryCardEditorName {
position: absolute;
left: 5%;
top: 8%;
width: 90%;
height: 20%;
color: black;
background: none;
border: none;
font: inherit;
line-height: 1.25em;
text-align: center;
}
.card.editing :is(.cardDisplayName, .cardGrid) {
display: none;
}
#galleryCardEditorGrid {
position: absolute;
left: 20%;
right: 20%;
top: 30%;
bottom: 30%;
display: grid;
grid-template-columns: repeat(8, 1fr);
grid-template-rows: repeat(8, 1fr);
}
#galleryCardEditorGrid button {
position: relative;
border: none;
}
:is(#galleryCardEditorGrid, #galleryCardEditorSpecialCost) button:is(:hover, :focus)::before {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: #ffffff80;
content: '';
}
#galleryCardEditorGrid button[data-state="0"] {
border: 1px solid grey;
background: #00000080;
}
#galleryCardEditorGrid button[data-state="4"] {
border: 1px solid grey;
background: url('assets/InkOverlay.png') center/cover, var(--primary-colour-1);
}
#galleryCardEditorGrid button[data-state="8"] {
border: 1px solid grey;
background: url('assets/SpecialOverlay.png') center/cover, var(--special-colour-1);
}
#galleryCardEditorSpecialCost {
display: none;
position: absolute;
grid-template-columns: repeat(5, 1fr);
left: 26.8%;
top: 86.8%;
gap: 0.072em;
}
#galleryCardEditorSpecialCost button {
width: 1.7em;
height: 1.7em;
border: none;
background: #00000080;
position: relative;
}
#galleryCardEditorSpecialCost button.active {
background: url('assets/SpecialOverlay.png') center/cover, var(--special-colour-1);
}
#galleryCardEditorSpecialCost label {
position: absolute;
left: 0;
top: 105%;
font: 33% 'Splatoon 2', sans-serif;
background: grey;
padding: 0 1em;
}
/* Help */
#helpControls {

View File

@ -93,7 +93,7 @@ internal partial class Program {
private static void HttpServer_OnRequest(object? sender, HttpRequestEventArgs e) {
e.Response.AppendHeader("Access-Control-Allow-Origin", "*");
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/")
var path = e.Request.RawUrl == "/" || e.Request.RawUrl.StartsWith("/deckeditor") || e.Request.RawUrl.StartsWith("/cardlist") || e.Request.RawUrl.StartsWith("/game/") || e.Request.RawUrl.StartsWith("/replay/")
? "index.html"
: HttpUtility.UrlDecode(e.Request.RawUrl[1..]);
if (e.TryReadFile(path, out var bytes))