From 5214cdfbf26bc0bdee5d669a237fb8aefffb78d5 Mon Sep 17 00:00:00 2001 From: Daniel Weipert Date: Wed, 30 Aug 2023 21:35:28 +0200 Subject: story! --- resources/css/menu.css | 40 ++++ resources/css/page.css | 8 + resources/js/classes/Area.js | 2 - resources/js/classes/State.js | 12 + resources/js/db.js | 15 ++ resources/js/game.js | 10 +- resources/js/helpers.js | 9 + resources/js/main.js | 46 +--- resources/js/memory.js | 12 + resources/js/story.js | 163 ++++++++++++++ resources/js/ui.js | 512 ++++++++++++++++++++++++++---------------- 11 files changed, 594 insertions(+), 235 deletions(-) create mode 100644 resources/js/story.js (limited to 'resources') diff --git a/resources/css/menu.css b/resources/css/menu.css index 33ab7c3..9b0919f 100644 --- a/resources/css/menu.css +++ b/resources/css/menu.css @@ -359,6 +359,29 @@ grid-gap: 1rem; } +.menu__settings table { + border-collapse: collapse; +} + +.menu__settings table th { + font-weight: normal; + text-align: left; + vertical-align: top; +} + +.menu__settings table tr:not(:last-child) > * { + padding-bottom: 1rem; +} + +.menu__settings input, +.menu__settings select { + width: 100%; +} + +.menu__settings select { + text-align: center; +} + .setting-highlight { border: 2px solid yellow !important; } @@ -369,3 +392,20 @@ stroke: yellow; stroke-width: 2px; } + + + + +.story__popup { + padding: 1rem; + max-width: 300px; + + display: grid; + grid-gap: 1rem; + + text-align: center; +} + +.story__popup .text { + font-size: 1.2rem; +} diff --git a/resources/css/page.css b/resources/css/page.css index d16e542..2091829 100644 --- a/resources/css/page.css +++ b/resources/css/page.css @@ -34,8 +34,16 @@ svg { display: block; } +svg [data-interactable="true"], svg [data-location], svg [data-area], svg [data-encounter] { cursor: pointer; + stroke: red; + fill: transparent; +} + +svg [data-interactable="false"] { + stroke: transparent; + fill: transparent; } diff --git a/resources/js/classes/Area.js b/resources/js/classes/Area.js index 5a3a062..bafa949 100644 --- a/resources/js/classes/Area.js +++ b/resources/js/classes/Area.js @@ -8,8 +8,6 @@ class Area { this.slug = slug; } - async initialize () {} - get name () { return translate(this.alternateSlug) || slugToName(this.slug); } diff --git a/resources/js/classes/State.js b/resources/js/classes/State.js index e4820d3..0dc06aa 100644 --- a/resources/js/classes/State.js +++ b/resources/js/classes/State.js @@ -1,5 +1,7 @@ class State { Settings = { + name: '', + /** * @type {string} */ @@ -34,6 +36,16 @@ class State { */ lastVisitedTown = ''; + /** + * @type {Object.} + */ + storyProgress = {}; + + /** + * @type {string} + */ + currentStory = null; + /** * @type {number} */ diff --git a/resources/js/db.js b/resources/js/db.js index 54fc751..3c8be05 100644 --- a/resources/js/db.js +++ b/resources/js/db.js @@ -68,6 +68,8 @@ const DB = { */ items: {}, + npcs: {}, + areas: {}, translations: {}, @@ -162,6 +164,19 @@ async function fetchItem (slug) { return new Item(slug); } +/** + * @param {string} slug + * + * @returns {Promise} + */ +async function fetchNpc (slug) { + if (! DB.npcs[slug]) { + DB.npcs[slug] = await fetchDBData(`/modules/tuxemon/mods/tuxemon/db/npc/${slug}.json`).then((response) => response.json()); + } + + return DB.npcs[slug]; +} + /** * @param {string} locale * diff --git a/resources/js/game.js b/resources/js/game.js index 3ce2aed..0841729 100644 --- a/resources/js/game.js +++ b/resources/js/game.js @@ -25,6 +25,7 @@ const Game = { isLoadingArea: false, isProgressingTurn: false, playerIsChoosingNextMonster: false, + isStoryBattle: false, doBattleAnimation: true, opponentActionTimeout: null, didTechniqueHit: false, @@ -120,9 +121,13 @@ const Game = { Memory.state.currentArea.monsterProgress = 0; if (Memory.state.currentArea.encounters.length > 0) { await Game.encounterWildMonster(); - } else { + } else if (Memory.state.currentArea.trainers.length > 0) { await Game.encounterTrainer(); + } else { + UI.showMap(); } + + Game.isStoryBattle = false; } else { await Game.encounterNextTrainerMonster(); } @@ -146,6 +151,7 @@ const Game = { // whole party defeated if (!Memory.state.player.monsters.some((monster) => monster.hp > 0)) { Memory.state.Game.isInBattle = false; + Game.isStoryBattle = false; if (Memory.state.currentArea.monsterProgress < Memory.state.currentArea.requiredEncounters) { Memory.state.currentArea.monsterProgress = 0; @@ -1012,6 +1018,8 @@ const Game = { /** * @param {Area} area + * + * @returns {boolean} */ isTown (area) { return area.encounters.length === 0 && area.trainers.length === 0; diff --git a/resources/js/helpers.js b/resources/js/helpers.js index 58e2113..bceaf5e 100644 --- a/resources/js/helpers.js +++ b/resources/js/helpers.js @@ -13,6 +13,15 @@ function slugToName (slug) { .split('-').map((item) => ucfirst(item)).join(' '); } +/** + * @param {string} text + * + * @returns {string} + */ +function nl2br (text) { + return text.replace(new RegExp(/\\n/g), '
'); +} + /** * @param {string} color * diff --git a/resources/js/main.js b/resources/js/main.js index f472a14..1cabab5 100644 --- a/resources/js/main.js +++ b/resources/js/main.js @@ -8,50 +8,8 @@ UI.wrapCallback(async function () { // Start New Game else { - const possibleStarterMonsters = await Promise.all( - [ - 'budaye', - 'dollfin', - 'grintot', - 'ignibus', - 'memnomnom', - ].map(async (monsterSlug) => await fetchMonster(monsterSlug)) - ); + await initializeState(); - const monsterSelection = UI.openStarterMonsterSelection(possibleStarterMonsters); - monsterSelection.addEventListener('starter:monster:selected', UI.wrapCallback(async (event) => { - if (!confirm(`Select ${event.detail.monster.name}?`)) { - return; - } - - Memory.state.player = new Trainer({ - monsters: [ - event.detail.monster, - ], - inventory: [ - new InventoryItem(await fetchItem('tuxeball'), 5), - new InventoryItem(await fetchItem('potion')), - ] - }); - await Memory.state.player.initialize(); - - Game.setActivePlayerMonster(Memory.state.player.monsters[0]); - Memory.state.activeBall = 'tuxeball'; - - // set rival monster - possibleStarterMonsters.splice(possibleStarterMonsters.indexOf(event.detail.monster), 1); - const rivalMonster = possibleStarterMonsters[Math.round(Math.random() * (possibleStarterMonsters.length - 1))]; - Memory.state.opponent = new Trainer({ monsters: [ rivalMonster ] }); - await Memory.state.opponent.initialize(); - Memory.state.rivalMonster = rivalMonster.slug - - // go to starting area - await Game.goToArea('paper-town'); - - UI.drawActiveMonster(); - UI.drawActiveTechniques(); - - event.detail.popup.remove(); - })); + await Story.progress('start'); } })(); diff --git a/resources/js/memory.js b/resources/js/memory.js index 709a084..b403037 100644 --- a/resources/js/memory.js +++ b/resources/js/memory.js @@ -181,6 +181,7 @@ const Memory = { */ const loadedState = saveData; + Memory.state.Settings.name = loadedState.Settings.name; Memory.state.Settings.language = loadedState.Settings.language; await fetchTranslation(Memory.state.Settings.language); applyTranslation(); @@ -197,6 +198,8 @@ const Memory = { } Memory.state.currentArea = await loadArea(loadedState.currentArea); Memory.state.lastVisitedTown = loadedState.lastVisitedTown; + Memory.state.storyProgress = loadedState.storyProgress; + Memory.state.currentStory = loadedState.currentStory; Memory.state.turn = loadedState.turn; Memory.state.money = loadedState.money; @@ -227,6 +230,8 @@ const Memory = { UI.drawArea(); UI.drawStatus(); UI.closeAllPopups(); + + Story.progress(Memory.state.currentStory); }, /** @@ -240,3 +245,10 @@ const Memory = { Memory.loadFromString(localStorage.getItem('state')); }, }; + +async function initializeState () { + Memory.state.currentArea = await fetchArea('paper-town'); + Memory.state.player = new Trainer({ monsters: [] }); + Memory.state.opponent = new Trainer({ monsters: [] }); + Memory.state.activeTechnique = await fetchTechnique('all_in'); +} diff --git a/resources/js/story.js b/resources/js/story.js new file mode 100644 index 0000000..c5f7add --- /dev/null +++ b/resources/js/story.js @@ -0,0 +1,163 @@ +const Story = { + async start () { + const settingsPopup = UI.createPopup(); + settingsPopup.querySelector('[data-template-slot="content"]').append(UI.createSettingsMenu()); + UI.drawPopup(settingsPopup); + + await new Promise((resolve) => { + settingsPopup.addEventListener('close', UI.wrapCallback(async () => { + resolve(); + })); + }); + + await Story.progress('introduction'); + }, + + async introduction () { + const possibleStarterMonsters = await Promise.all( + [ + 'budaye', + 'dollfin', + 'grintot', + 'ignibus', + 'memnomnom', + ].map(async (monsterSlug) => await fetchMonster(monsterSlug)) + ); + + const monsterSelection = UI.openStarterMonsterSelection(possibleStarterMonsters, { title: translate('story:introduction:monster_selection:title', true) }); + await new Promise((resolve) => { + monsterSelection.addEventListener('starter:monster:selected', UI.wrapCallback(async (event) => { + if (!confirm(`Select ${event.detail.monster.name}?`)) { + return; + } + + event.detail.popup.remove(); + + await UI.buildAndShowStoryPopup({ speaker: await fetchNpc('spyder_dante'), text: translate('spyder_intro_shopkeeper4', true) }); + + // set rival monster + Memory.state.rivalMonster = event.detail.monster.slug; + + // initialize state variables + Memory.state.money = 250; + + Memory.state.opponent = new Trainer({ monsters: [] }); + await Memory.state.opponent.initialize(); + + Memory.state.player = new Trainer({ monsters: [] }); + await Memory.state.player.initialize(); + + // go to starting area + await Game.goToArea('paper-town'); + + resolve(); + })); + }); + }, + + async selectStarterMonster () { + await UI.buildAndShowStoryPopup({ speaker: await fetchNpc('spyder_dante'), text: translate('spyder_papertown_myfirstmon_notmet', true) }); + await UI.buildAndShowStoryPopup({ speaker: await fetchNpc('spyder_dante'), text: translate('spyder_papertown_myfirstmon2', true) }); + + const possibleStarterMonsters = await Promise.all( + [ + 'tweesher', + 'lambert', + 'nut', + 'agnite', + 'rockitten', + ].map(async (monsterSlug) => await fetchMonster(monsterSlug)) + ); + + const monsterSelection = UI.openStarterMonsterSelection(possibleStarterMonsters, { title: translate('story:select_starter_monster:monster_selection:title', true) }); + await new Promise((resolve) => { + monsterSelection.addEventListener('starter:monster:selected', UI.wrapCallback(async (event) => { + if (!confirm(`Select ${event.detail.monster.name}?`)) { + return; + } + + Memory.state.player = new Trainer({ + monsters: [ + event.detail.monster, + ] + }); + await Memory.state.player.initialize(); + + Game.setActivePlayerMonster(Memory.state.player.monsters[0]); + + // go to starting area + await Game.goToArea('paper-town'); + + UI.drawActiveMonster(); + UI.drawActiveTechniques(); + + event.detail.popup.remove(); + + resolve(); + })); + }); + + await Story.progress('battleRivalOne'); + }, + + async battleRivalOne () { + Memory.state.opponent = new Trainer({ monsters: [ await fetchMonster(Memory.state.rivalMonster) ] }); + await Memory.state.opponent.initialize(); + + await UI.buildAndShowStoryPopup({ speaker: await fetchNpc('spyder_rivalbillie'), text: translate('spyder_papertown_firstfight', true) }); + + await Story.battle(); + }, + + + // Helper + + /** + * @param {string} slug + * + * @returns {Promise} + */ + async progress (slug) { + if (!Story[slug]) { + return; + } + + Memory.state.currentStory = slug; + Memory.saveToLocalStorage(); + + await Story[slug](); + + Memory.state.storyProgress[slug] = true; + Memory.state.currentStory = null; + Memory.saveToLocalStorage(); + }, + + /** + * @returns {Promise} + */ + async battle () { + const previousArea = Object.assign({}, Memory.state.currentArea); + + Game.isStoryBattle = true; + Memory.state.Game.isInBattle = true; + Memory.saveToLocalStorage(); + + UI.drawBattle(); + UI.showBattle(); + + await new Promise((resolve) => { + const interval = setInterval(() => { + if (!Game.isStoryBattle) { + clearInterval(interval); + resolve(); + } + }, 100); + }); + + if (previousArea.slug === Memory.state.currentArea.slug) { + Memory.state.currentArea.trainerProgress = previousArea.trainerProgress; + + UI.drawStatus(); + } + }, +}; diff --git a/resources/js/ui.js b/resources/js/ui.js index ee33a64..5626048 100644 --- a/resources/js/ui.js +++ b/resources/js/ui.js @@ -39,6 +39,8 @@ const Template = { dialogLoad: document.querySelector('#tpl___dialog__load'), menuSettings: document.querySelector('#tpl___menu__settings'), + + storyPopup: document.querySelector('#tpl___story__popup'), }; const UI = { @@ -98,17 +100,26 @@ const UI = { }, /** + * @param {Object} options + * @param {boolean} [options.isClosable=true] + * * @returns {HTMLElement} */ - createPopup () { + createPopup (options) { + options = Object.assign({ + isClosable: true, + }, options); + const popup = UI.createTemplate(Template.popup); - popup.addEventListener('click', ({ target }) => { - if (target === popup) { - popup.dispatchEvent(new Event('close')); - popup.remove(); - } - }); + if (options.isClosable) { + popup.addEventListener('click', ({ target }) => { + if (target === popup) { + popup.dispatchEvent(new Event('close')); + popup.remove(); + } + }); + } return popup; }, @@ -534,14 +545,21 @@ const UI = { UI.drawTown(); UI.closeLog(); UI.showMap(); - } else { - UI.elements.battle.style.backgroundImage = `url(/modules/tuxemon/mods/tuxemon/gfx/ui/combat/${Memory.state.currentArea.environment.battle_graphics.background})`; - UI.showBattle(); - UI.drawOpponentMonster(); - UI.drawActiveMonster(); - UI.drawActiveTechniques(); + UI.drawStatus(); + UI.drawActiveBall(); + } else { + UI.drawBattle(); } + }, + + drawBattle () { + UI.elements.battle.style.backgroundImage = `url(/modules/tuxemon/mods/tuxemon/gfx/ui/combat/${Memory.state.currentArea.environment.battle_graphics.background})`; + + UI.showBattle(); + UI.drawOpponentMonster(); + UI.drawActiveMonster(); + UI.drawActiveTechniques(); UI.drawStatus(); UI.drawActiveBall(); @@ -736,6 +754,22 @@ const UI = { } }))); + template.querySelectorAll('[data-interactable="true"]').forEach((node) => { + if (node.dataset.story) { + if (node.dataset.storyOnce && Memory.state.storyProgress[node.dataset.story]) { + node.dataset.interactable = false; + return; + } + } + + node.addEventListener('click', UI.wrapCallback(async () => { + if (node.dataset.story) { + await Story.progress(node.dataset.story); + UI.drawTown(); + } + })); + }); + return template; }, @@ -833,6 +867,11 @@ const UI = { })); template.querySelector('[data-template-slot="box.view"]').addEventListener('click', UI.wrapCallback(() => { + if (Memory.state.monsters.length === 0) { + alert(translate('ui:healing_center:box:view:no_tuxemon_in_box', true)); + return; + } + const boxPopup = UI.createPopup(); const monsterSelection = UI.createMonsterSelection(Memory.state.monsters); monsterSelection.addEventListener('monster:selected', UI.wrapCallback((event) => { @@ -1044,7 +1083,10 @@ const UI = { } const changeAreaButton = UI.elements.changeArea; - if (!Game.isTown(currentArea)) { + if (Game.isStoryBattle) { + changeAreaButton.disabled = true; + } + else if (!Game.isTown(currentArea)) { if ( Memory.state.Game.isInBattle || (Memory.state.opponent && Memory.state.opponent.type === 'trainer' && Memory.state.opponent.activeMonster !== Memory.state.opponent.monsters[0]) @@ -1061,14 +1103,16 @@ const UI = { /** * @param {Monster[]} monsters */ - openStarterMonsterSelection (monsters) { + openStarterMonsterSelection (monsters, { title }) { const popup = UI.createPopup().cloneNode(true); // remove close event const template = UI.createPartySelection(monsters); - const title = document.createElement('h1'); - title.textContent = 'Select your Tuxemon!'; - title.style.textAlign = 'center'; - template.prepend(title); + const titleNode = document.createElement('h1'); + titleNode.textContent = title; + titleNode.style.textAlign = 'center'; + titleNode.style.margin = 0; + titleNode.style.padding = '1rem'; + template.prepend(titleNode); template.addEventListener('party:monster:selected', (event) => { const monster = event.detail.monster; @@ -1160,8 +1204,12 @@ const UI = { } } - else if (condition.startsWith('event.')) { - canGo = false; + else if (condition.startsWith('story.')) { + const storyCondition = condition.replace('story.', ''); + const storySlug = storyCondition; + const storyProgress = Memory.state.storyProgress[storySlug] || false; + + canGo = canGo && storyProgress; } } @@ -1334,175 +1382,12 @@ const UI = { openSettingsMenu () { const popup = UI.createPopup(); - const template = UI.createTemplate(Template.menuSettings); - - - /* Language */ - - const languageSelectNode = template.querySelector('[data-template-slot="language"]'); - - const languages = { - 'cs_CZ': 'Czech (Czech Republic)', - 'de_DE': 'German (Germany)', - 'en_US': 'English (United States)', - 'eo': 'Esperanto', - 'es_ES': 'Spanish (Spain)', - 'es_MX': 'Spanish (Mexico)', - 'fi': 'Finnish', - 'fr_FR': 'French (France)', - 'it_IT': 'Italian (Italy)', - 'ja': 'Japanese', - 'nb_NO': 'Norwegian Bokmål (Norway)', - 'pl': 'Polish', - 'pt_BR': 'Portuguese (Brazil)', - 'zh_CN': 'Chinese (China)', - }; - - for (const languageCode of Object.keys(languages)) { - const languageName = languages[languageCode]; - const languageOptionNode = document.createElement('option'); - - languageOptionNode.value = languageCode; - languageOptionNode.textContent = languageName; - - if (languageCode === Memory.state.Settings.language) { - languageOptionNode.selected = true; - } - - languageSelectNode.appendChild(languageOptionNode); - } - - languageSelectNode.addEventListener('change', UI.wrapCallback(async () => { - const selected = [...languageSelectNode.children].find((node) => node.selected === true); - Memory.state.Settings.language = selected.value; - - await fetchTranslation(Memory.state.Settings.language); - applyTranslation(); - - UI.drawArea(); - })); - - - /* Currency */ - - const currencySelectNode = template.querySelector('[data-template-slot="currency"]'); - const currencyCodeMap = Object.keys(DB.currencies.map).sort((a, b) => { - const nameA = DB.currencies.map[a].name; - const nameB = DB.currencies.map[b].name; - - return nameA > nameB ? 1 : -1; - }); - - for (const currencyCode of currencyCodeMap) { - const currency = DB.currencies.map[currencyCode]; - const currencyOptionNode = document.createElement('option'); - - currencyOptionNode.value = currencyCode, - currencyOptionNode.textContent = `${currency.symbol} - ${currency.name}`; - - if (currencyCode === Memory.state.Settings.currency) { - currencyOptionNode.selected = true; - } - - currencySelectNode.appendChild(currencyOptionNode); - } - - currencySelectNode.addEventListener('change', UI.wrapCallback(async () => { - const selected = [...currencySelectNode.children].find((node) => node.selected === true); - const previousCurrencyCode = Memory.state.Settings.currency; - const newCurrencyCode = selected.value; - - Memory.state.Settings.currency = newCurrencyCode; + const template = UI.createSettingsMenu(); - // re-calculate money - const previousCurrency = DB.currencies.map[previousCurrencyCode]; - const newCurrency = DB.currencies.map[newCurrencyCode]; - const baseRateMoney = Memory.state.money / previousCurrency.rate; - const exchangedMoney = baseRateMoney * newCurrency.rate; - Memory.state.money = Number(exchangedMoney.toFixed(newCurrency.decimals)); - - UI.drawArea(); - UI.drawStatus(); + popup.addEventListener('close', UI.wrapCallback(() => { + Memory.saveToLocalStorage(); })); - template.querySelector('[data-template-slot="currency.lastUpdated"]').textContent = DB.currencies.last_updated; - - - // Highlight - - template.querySelector('[data-template-slot="highlight"]').addEventListener('click', UI.wrapCallback(() => { - UI.isHighlighting = !UI.isHighlighting; - - const elements = [ - UI.elements.battleOpponent, - UI.elements.battlePlayer.querySelector('[data-template-slot="sprite"]'), - UI.elements.techniques, - UI.elements.showMap, - UI.elements.nextTrainer, - UI.elements.changeArea, - UI.elements.menuParty, - UI.elements.menuCatch, - UI.elements.menuInventory, - UI.elements.menuLog, - UI.elements.menuJournal, - UI.elements.menuSettings, - - Template.technique.content.firstElementChild, - Template.healingCenter.content.querySelector('[data-template-slot="heal"]'), - Template.shopItem.content.firstElementChild, - ...Template.monsterStats.content.querySelectorAll('button'), - Template.movesetItem.content.firstElementChild, - Template.tabHeading.content.querySelector('[data-template-slot="label"]'), - ...Template.party.content.querySelectorAll('[data-template-slot="modes"] button'), - Template.partyMonster.content.firstElementChild, - Template.inventoryItem.content.firstElementChild, - ...Template.inventory.content.querySelectorAll('[data-template-slot="modes"] button'), - ...Template.menuSettings.content.querySelectorAll('select, button'), - - ...document.querySelector('.menu__settings').querySelectorAll('select, button'), - ]; - - for (const element of elements) { - if (!element) { - continue; - } - - if (UI.isHighlighting) { - element.classList.add(UI.highlightClassName); - } else { - element.classList.remove(UI.highlightClassName); - } - } - - // map - const mapElements = [ - UI.elements.sceneTown.querySelector('[data-template-slot="map"]').firstElementChild, - Template.map.content.firstElementChild, - ]; - for (const element of mapElements) { - if (!element) { - continue; - } - - if (UI.isHighlighting) { - element.classList.add(`${UI.highlightClassName}--map`); - } else { - element.classList.remove(`${UI.highlightClassName}--map`); - } - } - })); - - - // Clear save data - - template.querySelector('[data-template-slot="clearLocalSaveData"]').addEventListener('click', UI.wrapCallback(() => { - if (confirm(translate('ui:settings:clear_local_save_data:confirm', true))) { - localStorage.removeItem('state'); - window.location.reload(); - } - })); - - popup.querySelector('.popup').appendChild(template); UI.drawPopup(popup); }, @@ -1536,7 +1421,7 @@ const UI = { * * @returns {HTMLElement} */ - createStatsMenu (monster) { // TODO + createStatsMenu (monster) { const template = UI.createTemplate(Template.monsterStats); template.querySelector('[data-template-slot="name"]').textContent = monster.name; @@ -1544,6 +1429,8 @@ const UI = { template.querySelector('[data-template-slot="level"]').textContent = monster.level; template.querySelector('[data-template-slot="types"]').innerHTML = monster.types.map((type) => UI.createElementTypeIcon(type).outerHTML).join(''); + template.querySelector('[data-template-slot="exp"]').innerHTML = `${monster.exp} / ${monster.getExperienceRequired(1)}`; + template.querySelector('[data-template-slot="stats.melee.name"]').textContent = translate(StatType.melee) || slugToName(StatType.melee); template.querySelector('[data-template-slot="stats.armour.name"]').textContent = translate(StatType.armour) || slugToName(StatType.armour); template.querySelector('[data-template-slot="stats.ranged.name"]').textContent = translate(StatType.ranged) || slugToName(StatType.ranged); @@ -1828,6 +1715,255 @@ const UI = { }, + /* Menu - Settings */ + + /** + * @returns {HTMLElement} + */ + createSettingsMenu () { + const template = UI.createTemplate(Template.menuSettings); + + + /* Name */ + + const nameNode = template.querySelector('[data-template-slot="name"]'); + nameNode.value = Memory.state.Settings.name; + nameNode.addEventListener('input', UI.wrapCallback((event) => { + Memory.state.Settings.name = event.target.value; + })); + + + /* Language */ + + const languageSelectNode = template.querySelector('[data-template-slot="language"]'); + + const languages = { + 'cs_CZ': 'Czech (Czech Republic)', + 'de_DE': 'German (Germany)', + 'en_US': 'English (United States)', + 'eo': 'Esperanto', + 'es_ES': 'Spanish (Spain)', + 'es_MX': 'Spanish (Mexico)', + 'fi': 'Finnish', + 'fr_FR': 'French (France)', + 'it_IT': 'Italian (Italy)', + 'ja': 'Japanese', + 'nb_NO': 'Norwegian Bokmål (Norway)', + 'pl': 'Polish', + 'pt_BR': 'Portuguese (Brazil)', + 'zh_CN': 'Chinese (China)', + }; + + for (const languageCode of Object.keys(languages)) { + const languageName = languages[languageCode]; + const languageOptionNode = document.createElement('option'); + + languageOptionNode.value = languageCode; + languageOptionNode.textContent = languageName; + + if (languageCode === Memory.state.Settings.language) { + languageOptionNode.selected = true; + } + + languageSelectNode.appendChild(languageOptionNode); + } + + languageSelectNode.addEventListener('change', UI.wrapCallback(async () => { + const selected = [...languageSelectNode.children].find((node) => node.selected === true); + Memory.state.Settings.language = selected.value; + + await fetchTranslation(Memory.state.Settings.language); + applyTranslation(); + + UI.drawArea(); + })); + + + /* Currency */ + + const currencySelectNode = template.querySelector('[data-template-slot="currency"]'); + const currencyCodeMap = Object.keys(DB.currencies.map).sort((a, b) => { + const nameA = DB.currencies.map[a].name; + const nameB = DB.currencies.map[b].name; + + return nameA > nameB ? 1 : -1; + }); + + for (const currencyCode of currencyCodeMap) { + const currency = DB.currencies.map[currencyCode]; + const currencyOptionNode = document.createElement('option'); + + currencyOptionNode.value = currencyCode, + currencyOptionNode.textContent = `${currency.symbol} - ${currency.name}`; + + if (currencyCode === Memory.state.Settings.currency) { + currencyOptionNode.selected = true; + } + + currencySelectNode.appendChild(currencyOptionNode); + } + + currencySelectNode.addEventListener('change', UI.wrapCallback(async () => { + const selected = [...currencySelectNode.children].find((node) => node.selected === true); + const previousCurrencyCode = Memory.state.Settings.currency; + const newCurrencyCode = selected.value; + + Memory.state.Settings.currency = newCurrencyCode; + + // re-calculate money + const previousCurrency = DB.currencies.map[previousCurrencyCode]; + const newCurrency = DB.currencies.map[newCurrencyCode]; + const baseRateMoney = Memory.state.money / previousCurrency.rate; + const exchangedMoney = baseRateMoney * newCurrency.rate; + Memory.state.money = Number(exchangedMoney.toFixed(newCurrency.decimals)); + + UI.drawArea(); + UI.drawStatus(); + })); + + template.querySelector('[data-template-slot="currency.lastUpdated"]').textContent = DB.currencies.last_updated; + + + // Highlight + + template.querySelector('[data-template-slot="highlight"]').addEventListener('click', UI.wrapCallback(() => { + UI.isHighlighting = !UI.isHighlighting; + + const elements = [ + UI.elements.battleOpponent, + UI.elements.battlePlayer.querySelector('[data-template-slot="sprite"]'), + UI.elements.techniques, + UI.elements.showMap, + UI.elements.nextTrainer, + UI.elements.changeArea, + UI.elements.menuParty, + UI.elements.menuCatch, + UI.elements.menuInventory, + UI.elements.menuLog, + UI.elements.menuJournal, + UI.elements.menuSettings, + + Template.technique.content.firstElementChild, + Template.healingCenter.content.querySelector('[data-template-slot="heal"]'), + Template.shopItem.content.firstElementChild, + ...Template.monsterStats.content.querySelectorAll('button'), + Template.movesetItem.content.firstElementChild, + Template.tabHeading.content.querySelector('[data-template-slot="label"]'), + ...Template.party.content.querySelectorAll('[data-template-slot="modes"] button'), + Template.partyMonster.content.firstElementChild, + Template.inventoryItem.content.firstElementChild, + ...Template.inventory.content.querySelectorAll('[data-template-slot="modes"] button'), + ...Template.menuSettings.content.querySelectorAll('select, button'), + + ...document.querySelector('.menu__settings').querySelectorAll('select, button'), + ]; + + for (const element of elements) { + if (!element) { + continue; + } + + if (UI.isHighlighting) { + element.classList.add(UI.highlightClassName); + } else { + element.classList.remove(UI.highlightClassName); + } + } + + // map + const mapElements = [ + UI.elements.sceneTown.querySelector('[data-template-slot="map"]').firstElementChild, + Template.map.content.firstElementChild, + ]; + for (const element of mapElements) { + if (!element) { + continue; + } + + if (UI.isHighlighting) { + element.classList.add(`${UI.highlightClassName}--map`); + } else { + element.classList.remove(`${UI.highlightClassName}--map`); + } + } + })); + + + // Clear save data + + template.querySelector('[data-template-slot="clearLocalSaveData"]').addEventListener('click', UI.wrapCallback(() => { + if (confirm(translate('ui:settings:clear_local_save_data:confirm', true))) { + localStorage.removeItem('state'); + window.location.reload(); + } + })); + + return template; + }, + + + // Story + + /** + * @returns {HTMLElement} + */ + createStoryPopup () { + const popup = UI.createPopup(); + + return popup; + }, + + /** + * @param {HTMLElement} popup + * + * @returns {HTMLElement} + */ + applyStoryPopupContent (popup, { speaker, text }) { + const template = UI.createTemplate(Template.storyPopup); + + template.querySelector('[data-template-slot="speaker.sprite"]').src = `/modules/tuxemon/mods/tuxemon/gfx/sprites/player/${speaker.template[0].sprite_name}.png`; + template.querySelector('[data-template-slot="speaker.name"]').textContent = slugToName(speaker.slug.replace('spyder_', '')); + template.querySelector('[data-template-slot="text"]').innerHTML = nl2br(text); + template.querySelector('[data-template-slot="next"]').addEventListener('click', UI.wrapCallback(() => { + popup.dispatchEvent(new Event('next')); + })); + + popup.querySelector('[data-template-slot="content"]').append(template); + + return popup; + }, + + /** + * @param {HTMLElement} popup + * + * @returns {Promise} + */ + drawStoryPopup (popup) { + UI.drawPopup(popup); + + return new Promise((resolve, _reject) => { + popup.addEventListener('next', () => { + popup.remove(); + resolve(); + }); + + popup.addEventListener('close', () => { + resolve(); + }); + }); + }, + + /** + * @returns {Promise} + */ + async buildAndShowStoryPopup ({ speaker, text }) { + let popup = UI.createStoryPopup(); + popup = UI.applyStoryPopupContent(popup, { speaker: speaker, text: text }); + + return await UI.drawStoryPopup(popup); + }, + + // Error /** -- cgit v1.2.3