diff options
Diffstat (limited to 'resources/js')
| -rw-r--r-- | resources/js/classes/Area.js | 2 | ||||
| -rw-r--r-- | resources/js/classes/State.js | 12 | ||||
| -rw-r--r-- | resources/js/db.js | 15 | ||||
| -rw-r--r-- | resources/js/game.js | 10 | ||||
| -rw-r--r-- | resources/js/helpers.js | 9 | ||||
| -rw-r--r-- | resources/js/main.js | 46 | ||||
| -rw-r--r-- | resources/js/memory.js | 12 | ||||
| -rw-r--r-- | resources/js/story.js | 163 | ||||
| -rw-r--r-- | resources/js/ui.js | 512 | 
9 files changed, 546 insertions, 235 deletions
| 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}       */ @@ -35,6 +37,16 @@ class State {    lastVisitedTown = '';    /** +   * @type {Object.<string, boolean>} +   */ +  storyProgress = {}; + +  /** +   * @type {string} +   */ +  currentStory = null; + +  /**     * @type {number}     */    turn = 0; 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: {}, @@ -163,6 +165,19 @@ async function fetchItem (slug) {  }  /** + * @param {string} slug + * + * @returns {Promise<Object>} + */ +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   *   * @returns {Promise<Object>} 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 @@ -14,6 +14,15 @@ function slugToName (slug) {  }  /** + * @param {string} text + * + * @returns {string} + */ +function nl2br (text) { +  return text.replace(new RegExp(/\\n/g), '<br>'); +} + +/**   * @param {string} color   *   * @returns {string} 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<any>} +   */ +  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<any>} +   */ +  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<any>} +   */ +  drawStoryPopup (popup) { +    UI.drawPopup(popup); + +    return new Promise((resolve, _reject) => { +      popup.addEventListener('next', () => { +        popup.remove(); +        resolve(); +      }); + +      popup.addEventListener('close', () => { +        resolve(); +      }); +    }); +  }, + +  /** +   * @returns {Promise<any>} +   */ +  async buildAndShowStoryPopup ({ speaker, text }) { +    let popup = UI.createStoryPopup(); +    popup = UI.applyStoryPopupContent(popup, { speaker: speaker, text: text }); + +    return await UI.drawStoryPopup(popup); +  }, + +    // Error    /** | 
