Add a basic deck editor page. #1

This commit is contained in:
Andrio Celos 2022-10-14 13:44:09 +11:00
parent e3fd48b45e
commit 191a2982f7
9 changed files with 564 additions and 61 deletions

View File

@ -42,6 +42,9 @@
<p><button type="submit" id="joinGameButton2">Join game</button></p>
<a id="preGameBackButton" href="../..">Create or join a different room</a>
</div>
<p>
<a id="preGameDeckEditorButton" href="deckeditor">Edit decks</a>
</p>
</form>
<footer>
<p>This website is not affiliated with Nintendo. All product names, logos, and brands are property of their respective owners.</p>
@ -70,10 +73,8 @@
</section>
<div id="lobbyDeckSection" hidden>
<h3>Choose your deck.</h3>
<p><span id="countLabel">0</span>/15 cards chosen</p>
<p><button type="button" id="submitDeckButton" disabled>Submit</button></p>
<p id="cardListLoadingSection">Loading cards...</p>
<div id="cardList"></div>
<div id="lobbyDeckList"></div>
</div>
</div>
<div id="gameSection" hidden>
@ -185,6 +186,52 @@
<div id="errorModal" hidden>
<div id="errorModalBox">A communication error has occurred.</div>
</div>
<div id="deckListSection" hidden>
<section id="deckEditorDeckListSection">
<a id="deckListBackButton" href="..">Back</a>
<h3>Deck list</h3>
<div id="deckList">
<button id="newDeckButton">New deck</label>
</div>
</section>
<section id="deckEditorDeckViewSection" hidden>
<h3 id="deckName">Deck</h3>
<div>
<button type="button" id="deckEditButton">Edit</button>
<button type="button" id="deckRenameButton">Rename</button>
<button type="button" id="deckCopyButton">Copy</button>
<button type="button" id="deckDeleteButton" class="danger">Delete</button>
</div>
<div class="deckSizeContainer">Total: <div id="deckViewSize">0</div></div>
<div id="deckCardListView">
</div>
</section>
</div>
<div id="deckEditSection" hidden>
<section id="deckEditorDeckEditSection">
<h3 id="deckName2">Deck</h3>
<div>
<button type="button" id="deckTestButton">Test</button>
<button type="button" id="deckSaveButton">Save</button>
<button type="button" id="deckCancelButton">Cancel</button>
</div>
<div class="deckSizeContainer">Total: <div id="deckEditSize">0</div></div>
<div id="deckCardListEdit">
</div>
</section>
<section id="deckEditorCardListSection">
<label for="cardListSortBox">
Sort by
<select id="cardListSortBox" autocomplete="off">
<option value="number">number</option>
<option value="name">name</option>
<option value="size">size</option>
<option value="rarity">rarity</option>
</select>
</label>
<div id="cardList"></div>
</section>
</div>
<script src="build/tsbuild.js"></script>
</body>
</html>

View File

@ -0,0 +1,21 @@
class Deck {
name: string;
cards: number[];
isReadOnly: boolean;
constructor(name: string, cards: number[], isReadOnly: boolean) {
this.name = name;
this.cards = cards;
this.isReadOnly = isReadOnly;
}
get isValid() {
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 (this.cards.indexOf(this.cards[i], i + 1) >= 0) return false; // Duplicate cards
}
return true;
}
}

View File

@ -0,0 +1,161 @@
const deckNameLabel2 = document.getElementById('deckName2')!;
const deckEditSize = document.getElementById('deckEditSize')!;
const deckCardListEdit = document.getElementById('deckCardListEdit')!;
const cardList = document.getElementById('cardList')!;
const deckTestButton = document.getElementById('deckTestButton') as HTMLButtonElement;
const deckSaveButton = document.getElementById('deckSaveButton') as HTMLButtonElement;
const deckCancelButton = document.getElementById('deckCancelButton') as HTMLButtonElement;
const cardListSortBox = document.getElementById('cardListSortBox') as HTMLSelectElement;
const cardButtons: CardButton[] = [ ];
const deckEditCardButtons: (CardButton | HTMLLabelElement)[] = [ ];
let selectedDeckCardIndex: number | null = null;
function editDeck() {
if (selectedDeck == null) return;
deckNameLabel2.innerText = selectedDeck.name;
clearChildren(deckCardListEdit);
deckEditCardButtons.splice(0);
selectedDeckCardIndex = null;
for (let i = 0; i < 15; i++) {
if (selectedDeck.cards[i]) {
const button = createDeckEditCardButton(i, selectedDeck.cards[i]);
deckCardListEdit.appendChild(button.element);
deckEditCardButtons.push(button);
} else {
const element = createDeckEditEmptySlotButton(i);
deckCardListEdit.appendChild(element);
deckEditCardButtons.push(element);
}
}
deckEditUpdateSize();
showSection('deckEdit');
}
function createDeckEditCardButton(index: number, card: number) {
const button = new CardButton('radio', cardDatabase.cards![card - 1]);
button.inputElement.name = 'deckEditorSelectedCard'
deckCardListEdit.appendChild(button.element);
button.inputElement.addEventListener('input', () => {
if (button.inputElement.checked) {
for (const o of deckEditCardButtons) {
if (o != button) {
if ((o as CardButton).card)
(o as CardButton).element.classList.remove('checked');
else
(o as HTMLElement).classList.remove('checked');
}
}
selectedDeckCardIndex = index;
for (const button2 of cardButtons) {
button2.checked = button2.card.number == card;
}
}
});
return button;
}
function createDeckEditEmptySlotButton(index: number) {
const element = document.createElement('label');
element.className = 'card emptySlot';
const input = document.createElement('input');
input.type = 'radio';
input.name = 'deckEditorSelectedCard'
input.addEventListener('input', () => {
if (input.checked) {
for (const o of deckEditCardButtons) {
if (o != element) {
if ((o as CardButton).card)
(o as CardButton).element.classList.remove('checked');
else
(o as HTMLElement).classList.remove('checked');
}
}
selectedDeckCardIndex = index;
for (const button2 of cardButtons) {
button2.checked = false;
}
}
});
element.appendChild(input);
return element;
}
deckSaveButton.addEventListener('click', () => {
if (selectedDeck == null) return;
selectedDeck.cards = deckEditCardButtons.map(o => (o as CardButton).card?.number ?? 0);
saveDecks();
selectDeck();
showSection('deckList');
});
deckCancelButton.addEventListener('click', () => {
if (selectedDeck == null) return;
if (!confirm('Are you sure you want to stop editing this deck without saving?')) return;
showSection('deckList');
});
function deckEditUpdateSize() {
let size = 0;
for (const o of deckEditCardButtons) {
const card = (o as CardButton).card;
if (card) size += card.size;
}
deckEditSize.innerText = size.toString();
}
function initCardDatabase(cards: Card[]) {
for (const card of cards) {
const button = new CardButton('radio', card);
button.inputElement.name = 'deckEditorCardList';
cardButtons.push(button);
button.inputElement.addEventListener('input', () => {
if (button.inputElement.checked) {
for (const button2 of cardButtons) {
if (button2 != button)
button2.checked = false;
}
if (selectedDeckCardIndex == null) return;
const oldButton = deckEditCardButtons[selectedDeckCardIndex];
const button3 = createDeckEditCardButton(selectedDeckCardIndex, card.number);
button3.checked = true;
const oldElement = (oldButton as CardButton).element ?? (oldButton as Element);
deckCardListEdit.insertBefore(button3.element, oldElement);
deckCardListEdit.removeChild(oldElement);
deckEditCardButtons[selectedDeckCardIndex] = button3;
deckEditUpdateSize();
}
});
cardList.appendChild(button.element);
}
}
const cardSortOrders: { [key: string]: ((a: Card, b: Card) => number) | undefined } = {
'number': (a, b) => a.number - b.number,
'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,
}
cardListSortBox.addEventListener('change', () => {
const sortOrder = cardSortOrders[cardListSortBox.value];
if (sortOrder) {
clearChildren(cardList);
cardButtons.sort((a, b) => sortOrder(a.card, b.card));
for (const button of cardButtons)
cardList.appendChild(button.element);
}
});

View File

@ -0,0 +1,162 @@
const deckListBackButton = document.getElementById('deckListBackButton') as HTMLLinkElement;
const deckEditorDeckViewSection = document.getElementById('deckEditorDeckViewSection')!;
const deckNameLabel = document.getElementById('deckName')!;
const deckViewSize = document.getElementById('deckViewSize')!;
const deckList = document.getElementById('deckList')!;
const deckCardListView = document.getElementById('deckCardListView')!;
const newDeckButton = document.getElementById('newDeckButton') as HTMLButtonElement;
const deckEditButton = document.getElementById('deckEditButton') as HTMLButtonElement;
const deckRenameButton = document.getElementById('deckRenameButton') as HTMLButtonElement;
const deckCopyButton = document.getElementById('deckCopyButton') as HTMLButtonElement;
const deckDeleteButton = document.getElementById('deckDeleteButton') as HTMLButtonElement;
function showDeckList() {
showSection('deckList');
selectedDeck = null;
for (const el of deckList.getElementsByTagName('input')) {
(el as HTMLInputElement).checked = false;
}
}
deckListBackButton.addEventListener('click', e => {
e.preventDefault();
showSection('preGame');
if (canPushState) {
try {
history.pushState(null, '', '..');
} catch {
canPushState = false;
}
}
if (location.hash)
location.hash = '';
});
function saveDecks() {
const json = JSON.stringify(decks.filter(d => !d.isReadOnly), [ 'name', 'cards' ]);
localStorage.setItem('decks', json);
}
{
const decksString = localStorage.getItem('decks');
if (decksString) {
for (const deck of JSON.parse(decksString)) {
decks.push(new Deck(deck.name, deck.cards, 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));
saveDecks();
}
localStorage.removeItem('lastDeck');
}
for (let i = 0; i < decks.length; i++) {
createDeckButton(i, decks[i]);
}
}
function createDeckButton(index: number, deck: Deck) {
const label = document.createElement('label');
const button = document.createElement('input');
button.name = 'selectedDeck';
button.type = 'radio';
button.dataset.index = index.toString();
button.addEventListener('click', e => {
selectedDeck = decks[parseInt((e.target as HTMLInputElement).dataset.index!)];
selectDeck();
});
label.appendChild(button);
label.appendChild(document.createTextNode(deck.name));
deckList.insertBefore(label, newDeckButton);
return label;
}
newDeckButton.addEventListener('click', () => {
selectedDeck = new Deck(`Deck ${decks.length}`, [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], false);
createDeckButton(decks.length, selectedDeck);
decks.push(selectedDeck);
editDeck();
});
deckEditButton.addEventListener('click', editDeck);
function selectDeck() {
clearChildren(deckCardListView);
if (selectedDeck == null) return;
let size = 0;
deckNameLabel.innerText = selectedDeck.name;
for (const cardNumber of selectedDeck.cards) {
if (cardNumber) {
const card = cardDatabase.cards![cardNumber - 1];
size += card.size;
const button = new CardButton('radio', card);
button.inputElement.disabled = true;
button.inputElement.hidden = true;
deckCardListView.appendChild(button.element);
}
}
deckEditButton.disabled = selectedDeck.isReadOnly;
deckRenameButton.disabled = selectedDeck.isReadOnly;
deckCopyButton.disabled = false;
deckDeleteButton.disabled = selectedDeck.isReadOnly;
deckViewSize.innerText = size.toString();
deckEditorDeckViewSection.hidden = false;
}
deckRenameButton.addEventListener('click', () => {
if (selectedDeck == null) return;
const name = prompt(`What will you rename ${selectedDeck.name} to?`, selectedDeck.name)?.trim();
if (name) {
selectedDeck.name = name;
deckNameLabel.innerText = selectedDeck.name;
saveDecks();
}
});
deckCopyButton.addEventListener('click', () => {
if (selectedDeck == null) return;
selectedDeck = new Deck(selectedDeck.name, Array.from(selectedDeck.cards), false);
const button = createDeckButton(decks.length, selectedDeck);
decks.push(selectedDeck);
(button.getElementsByTagName('input')[0] as HTMLInputElement).checked = true;
selectDeck();
saveDecks();
});
deckDeleteButton.addEventListener('click', () => {
if (selectedDeck == null) return;
const index = decks.indexOf(selectedDeck);
if (index < 0) return;
if (!confirm(`Are you sure you want to delete ${selectedDeck.name}?`)) return;
decks.splice(index, 1);
let removed = false;
for (const el of Array.from(deckList.getElementsByTagName('label'))) {
const input = el.getElementsByTagName('input')[0];
if (removed) {
input.dataset.index = (parseInt(input.dataset.index!) - 1).toString();
} else if (parseInt(input.dataset.index!) == index) {
deckList.removeChild(el);
removed = true;
}
}
selectedDeck = null;
deckEditorDeckViewSection.hidden = true;
saveDecks();
});
if (!canPushState)
deckListBackButton.href = '#';

View File

@ -2,7 +2,6 @@
/// <reference path="../StageDatabase.ts"/>
const stageButtons: StageButton[] = [ ];
const cardButtons: CardButton[] = [ ];
const shareLinkButton = document.getElementById('shareLinkButton') as HTMLButtonElement;
const submitDeckButton = document.getElementById('submitDeckButton') as HTMLButtonElement;
let lobbyShareData: ShareData | null;
@ -13,6 +12,7 @@ const stageRandomButton = document.getElementById('stageRandomButton') as HTMLIn
const lobbySelectedStageSection = document.getElementById('lobbySelectedStageSection')!;
const lobbyStageSection = document.getElementById('lobbyStageSection')!;
const lobbyDeckSection = document.getElementById('lobbyDeckSection')!;
const lobbyDeckList = document.getElementById('lobbyDeckList')!;
let selectedStageButton = null as StageButton | null;
@ -64,7 +64,50 @@ function updateDeckCount() {
submitDeckButton.disabled = (count != 15);
}
submitDeckButton.addEventListener('click', e => {
function initDeckSelection() {
const lastDeckName = localStorage.getItem('lastDeckName');
selectedDeck = null;
if (currentGame?.me) {
clearChildren(lobbyDeckList);
for (let i = 0; i < decks.length; i++) {
const deck = decks[i];
const label = document.createElement('label');
const button = document.createElement('input');
button.name = 'gameSelectedDeck';
button.type = 'radio';
button.dataset.index = i.toString();
button.addEventListener('click', () => {
selectedDeck = deck;
submitDeckButton.disabled = false;
});
label.appendChild(button);
label.appendChild(document.createTextNode(deck.name));
if (!deck.isValid) {
label.classList.add('disabled');
button.disabled = true;
} else if (deck.name == lastDeckName) {
selectedDeck = deck;
button.checked = true;
}
lobbyDeckList.appendChild(label);
}
submitDeckButton.disabled = selectedDeck == null;
lobbyDeckSection.hidden = false;
} else {
lobbyDeckSection.hidden = true;
}
}
submitDeckButton.addEventListener('click', () => {
if (selectedDeck == null) return;
let req = new XMLHttpRequest();
req.open('POST', `${config.apiBaseUrl}/games/${currentGame!.id}/chooseDeck`);
req.addEventListener('load', e => {
@ -73,37 +116,13 @@ submitDeckButton.addEventListener('click', e => {
}
});
let data = new URLSearchParams();
let cardsString = '';
for (var el of cardButtons) {
if (el.inputElement.checked) {
if (cardsString != '') cardsString += '+';
cardsString += el.card.number.toString();
}
}
data.append('clientToken', clientToken);
data.append('deckName', 'Deck');
data.append('deckCards', cardsString);
data.append('deckName', selectedDeck.name);
data.append('deckCards', selectedDeck.cards.join('+'));
req.send(data.toString());
localStorage.setItem('lastDeck', cardsString);
localStorage.setItem('lastDeckName', selectedDeck.name);
});
const starterDeck = [ 6, 34, 159, 13, 45, 137, 22, 52, 141, 28, 55, 103, 40, 56, 92 ];
const lastDeckString = localStorage.getItem('lastDeck');
const lastDeck = lastDeckString?.split(/\+/)?.map(s => parseInt(s)) || starterDeck;
cardDatabase.loadAsync().then(cards => {
const cardList = document.getElementById('cardList')!;
for (const card of cards) {
const button = new CardButton('checkbox', card);
cardButtons.push(button);
button.checked = lastDeck != null && lastDeck.includes(card.number);
button.inputElement.addEventListener('input', updateDeckCount);
cardList.appendChild(button.element);
}
updateDeckCount();
document.getElementById('cardListLoadingSection')!.hidden = true;
}).catch(() => communicationError);
stageDatabase.loadAsync().then(stages => {
const stageList = document.getElementById('stageList')!;
for (const stage of stages) {

View File

@ -3,6 +3,7 @@ const joinGameButton = document.getElementById('joinGameButton')!;
const nameBox = document.getElementById('nameBox') as HTMLInputElement;
const gameIDBox = document.getElementById('gameIDBox') as HTMLInputElement;
const maxPlayersBox = document.getElementById('maxPlayersBox') as HTMLSelectElement;
const preGameDeckEditorButton = document.getElementById('preGameDeckEditorButton') as HTMLLinkElement;
let shownMaxPlayersWarning = false;
@ -47,17 +48,18 @@ maxPlayersBox.addEventListener('change', () => {
}
});
function setGameUrl(gameID: string | null) {
function setUrl(path: string) {
if (canPushState) {
try {
history.pushState(null, '', `game/${gameID}`);
history.pushState(null, '', path);
} catch {
canPushState = false;
location.hash = `#game/${gameID}`;
location.hash = `#${path}`;
}
} else
location.hash = `#game/${gameID}`;
location.hash = `#${path}`;
}
function setGameUrl(gameID: string) { setUrl(`game/${gameID}`); }
function tryJoinGame(name: string, idOrUrl: string, fromInitialLoad: boolean) {
const m = /(?:^|[#/])([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i.exec(idOrUrl);
@ -147,19 +149,29 @@ function presetGameID(url: string) {
tryJoinGame(playerName, url, true);
}
preGameDeckEditorButton.addEventListener('click', e => {
e.preventDefault();
showDeckList();
setUrl('deckeditor');
});
const playerName = localStorage.getItem('name');
(document.getElementById('nameBox') as HTMLInputElement).value = playerName || '';
function processUrl() {
const m = /^(.*)\/game\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/.exec(location.toString());
if (m)
presetGameID(m[2]);
else if (location.hash) {
canPushState = false;
presetGameID(location.hash);
} else {
clearPreGameForm(false);
showSection('preGame');
if (location.pathname.endsWith('/deckeditor') || location.hash == '#deckeditor')
showDeckList();
else {
const m = /^(.*)\/game\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/.exec(location.toString());
if (m)
presetGameID(m[2]);
else if (location.hash) {
canPushState = false;
presetGameID(location.hash);
} else {
clearPreGameForm(false);
showSection('preGame');
}
}
}
@ -167,3 +179,6 @@ window.addEventListener('popstate', () => {
processUrl();
});
processUrl();
if (!canPushState)
preGameDeckEditorButton.href = '#deckeditor';

View File

@ -1,10 +1,13 @@
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;
function delay(ms: number) { return new Promise(resolve => setTimeout(() => resolve(null), ms)); }
// Sections
const sections = new Map<string, HTMLDivElement>();
for (var id of [ 'noJS', 'preGame', 'lobby', 'game' ]) {
for (var id of [ 'noJS', 'preGame', 'lobby', 'game', 'deckList', 'deckEdit' ]) {
let el = document.getElementById(`${id}Section`) as HTMLDivElement;
if (!el) throw new EvalError(`Element not found: ${id}Section`);
sections.set(id, el);
@ -50,7 +53,7 @@ function onGameStateChange(game: any, playerData: any) {
lobbySelectedStageSection.appendChild(selectedStageButton.element);
lobbySelectedStageSection.hidden = false;
lobbyDeckSection.hidden = !playerData || game.players[playerData.playerIndex]?.isReady;
initDeckSelection();
break;
case GameState.Redraw:
case GameState.Ongoing:
@ -239,3 +242,11 @@ function isInternetExplorer() {
if (isInternetExplorer()) {
alert("You seem to be using an unsupported browser. Some layout or features of this app may not work correctly.");
}
function clearChildren(el: Element) {
let el2;
while (el2 = el.firstChild)
el.removeChild(el2);
}
cardDatabase.loadAsync().then(initCardDatabase).catch(() => communicationError);

View File

@ -10,9 +10,8 @@
src: url('assets/splatoon2.woff2') format('woff2');
}
/* Body */
body {
margin: 0;
color: white;
background: black;
font-family: 'Splatoon 2', sans-serif;
@ -75,16 +74,16 @@ footer {
}
#playerList {
padding-left: 0;
list-style: none;
padding-left: 0;
list-style: none;
}
#playerList li {
width: calc(100% - 3em);
margin: 0.5em 1em;
background: #111;
border-radius: 0.5em;
padding: 0.5em;
width: calc(100% - 3em);
margin: 0.5em 1em;
background: #111;
border-radius: 0.5em;
padding: 0.5em;
text-shadow: 1px 1px black;
}
@ -95,7 +94,7 @@ footer {
}
#playerList .ready::after {
content: '\2714';
content: '\2714';
position: absolute;
bottom: 0;
right: 0.5em;
@ -145,7 +144,7 @@ footer {
}
.stageGrid td {
width: 0.5em;
width: 0.5em;
height: 0.5em;
box-sizing: border-box;
}
@ -183,6 +182,7 @@ footer {
border: 1px solid var(--colour);
border-radius: 0.5em;
width: 10em;
height: 12em;
margin: 5px;
position: relative;
}
@ -360,6 +360,7 @@ footer {
#cardList {
display: flex;
flex-wrap: wrap;
overflow-y: scroll;
}
/* Game UI */
@ -649,7 +650,74 @@ footer {
font-size: large;
}
/* Deck editor */
:is(#deckListSection, #deckEditSection):not([hidden]) {
height: 100vh;
display: grid;
grid-template-columns: 1fr 1fr;
}
#deckList :is(label, button) {
width: 20rem;
height: 3rem;
background: dimgrey;
margin: 0.5em;
display: flex;
justify-content: center;
align-items: center;
font-family: inherit;
color: inherit;
font-size: inherit;
border: inherit;
}
/* Score section */
#deckList {
display: flex;
flex-flow: column;
}
#deckList input {
display: none;
}
.card.emptySlot {
--colour: dimgrey;
}
#deckCardListView, #deckCardListEdit {
display: grid;
grid-template-columns: auto auto auto;
justify-content: center;
grid-column: 2;
grid-row: 1 / 5;
overflow-y: scroll;
}
#deckEditorDeckViewSection:not([hidden]), #deckEditorDeckEditSection {
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: auto auto auto 1fr;
height: 100vh;
}
#deckName, #deckName2 {
grid-column: 1;
grid-row: 1;
}
:is(#deckEditorDeckViewSection, #deckEditorDeckEditSection) > h3 + div {
grid-row: 2;
grid-column: 1;
}
.deckSizeContainer {
grid-column: 1;
grid-row: 3;
}
#deckEditorCardListSection {
height: 100vh;
display: grid;
grid-template-rows: auto 1fr;
}

View File

@ -8,7 +8,6 @@
"strict": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noImplicitReturns": true
}
}