const Template = { popup: document.querySelector('#tpl___popup'), tabs: document.querySelector('#tpl___tabs'), tabHeading: document.querySelector('#tpl___tabs__tab-heading'), tabPanels: document.querySelector('#tpl___tabs__panels'), tabPanel: document.querySelector('#tpl___tabs__tab-panel'), battleMonster: document.querySelector('#tpl___battle__monster'), battleHpBar: document.querySelector('#tpl___battle__hp-bar'), battleExpBar: document.querySelector('#tpl___battle__exp-bar'), battleActionFeedback: document.querySelector('#tpl___battle__action-feedback'), techniques: document.querySelector('#tpl___techniques'), technique: document.querySelector('#tpl___technique'), map: document.querySelector('#tpl___map'), healingCenter: document.querySelector('#tpl___healing-center'), shop: document.querySelector('#tpl___shop'), shopItem: document.querySelector('#tpl___shop__item'), party: document.querySelector('#tpl___party'), partyMonster: document.querySelector('#tpl___party__monster'), monsterStats: document.querySelector('#tpl___monster-stats'), movesetList: document.querySelector('#tpl___moveset__list'), movesetItem: document.querySelector('#tpl___moveset__item'), areaSelection: document.querySelector('#tpl___area-selection'), areaSelectionItem: document.querySelector('#tpl___area-selection__item'), inventory: document.querySelector('#tpl___inventory'), inventoryItem: document.querySelector('#tpl___inventory__item'), menuJournal: document.querySelector('#tpl___menu__journal'), dialogSave: document.querySelector('#tpl___dialog__save'), dialogLoad: document.querySelector('#tpl___dialog__load'), menuSettings: document.querySelector('#tpl___menu__settings'), loadPopup: document.querySelector('#tpl___load-popup'), storyPopup: document.querySelector('#tpl___story__popup'), }; const UI = { elements: { sceneBattle: document.querySelector('#scene__battle'), sceneTown: document.querySelector('#scene__town'), battle: document.querySelector('#battle'), battleOpponent: document.querySelector('#battle__opponent'), battleOpponentSprite: null, battleOpponentAnimation: document.querySelector('.battle__technique-animation'), battlePlayer: document.querySelector('#battle__player'), techniques: document.querySelector('#techniques'), log: document.querySelector('#log'), status: document.querySelector('#status'), showMap: document.querySelector('#status [data-template-slot="showMap"]'), nextTrainer: document.querySelector('#status [data-template-slot="nextTrainer"]'), changeArea: document.querySelector('#status [data-template-slot="changeArea"]'), menuParty: document.querySelector('#menu__party'), menuInventory: document.querySelector('#menu__inventory'), menuCatch: document.querySelector('#menu__catch'), menuLog: document.querySelector('#menu__log'), menuJournal: document.querySelector('#menu__journal'), menuSettings: document.querySelector('#menu__settings'), }, events: document.createElement('div'), /** * @param {HTMLElement} template */ createTemplate (template) { const templateBase = document.createElement('div'); templateBase.innerHTML = template.innerHTML.trim(); const templateNode = templateBase.firstChild; /** * @param {HTMLElement} targetElement */ templateNode.appendTo = function (targetElement) { if (templateNode.dataset.templateType && templateNode.dataset.templateType === 'multiple') { for (const child of [...this.children]) { targetElement.appendChild(child); } } else { targetElement.appendChild(this); } }; return templateNode; }, /** * @param {Object} options * @param {boolean} [options.isClosable=true] * * @returns {HTMLElement} */ createPopup (options) { options = Object.assign({ isClosable: true, }, options); const popup = UI.createTemplate(Template.popup); if (options.isClosable) { popup.addEventListener('click', ({ target }) => { if (target === popup) { popup.dispatchEvent(new Event('close')); popup.remove(); } }); } return popup; }, /** * @typedef {Object} Tab * @property {HTMLElement} heading * @property {HTMLElement} content * @inner * * @param {Tab[]} tabs * * @returns {HTMLElement} */ createTabs (tabs) { const wrap = UI.createTemplate(Template.tabs); const panelsNode = UI.createTemplate(Template.tabPanels); wrap.style.gridTemplateColumns = '1fr '.repeat(tabs.length); const name = randomString(); for (const idx in tabs) { const tab = tabs[idx]; const tabHeading = UI.createTemplate(Template.tabHeading); const tabPanel = UI.createTemplate(Template.tabPanel); const inputId = `${name}_${idx}`; const panelId = randomString(); const tabHeadingInput = tabHeading.querySelector('[data-template-slot="input"]'); tabHeadingInput.name = name; tabHeadingInput.id = inputId; tabHeadingInput.setAttribute('aria-controls', panelId); if (idx == 0) { tabHeadingInput.checked = true; } const tabHeadingLabel = tabHeading.querySelector('[data-template-slot="label"]'); tabHeadingLabel.setAttribute('for', inputId); tabHeadingLabel.appendChild(tab.heading); tabPanel.id = panelId; tabPanel.appendChild(tab.content); tabHeading.appendTo(wrap); panelsNode.appendChild(tabPanel); } wrap.appendChild(panelsNode); return wrap; }, /** * @param {HTMLElement} slotNode * @param {HTMLElement} replacingNode * * @returns {HTMLElement} */ replaceTemplateSlot (slotNode, replacingNode) { replacingNode.dataset.templateSlot = slotNode.dataset.templateSlot; slotNode.replaceWith(replacingNode); return replacingNode; }, /** * @param {HTMLElement} popup */ drawPopup (popup) { const otherPopupExists = document.querySelector('.popup__overlay'); if (otherPopupExists) { popup.classList.add('popup--is-multiple'); } document.body.appendChild(popup); }, closeAllPopups () { document.querySelectorAll('.popup__overlay').forEach((popup) => popup.remove()); }, /* Battle */ techniqueAnimationIsRunning: false, techniqueAnimationNumber: 0, techniqueAnimationFps: 20, /** * @param {Monster} monster * * @returns {HTMLElement} */ createHpBar (monster) { const template = UI.createTemplate(Template.battleHpBar); const bar = template.querySelector('[data-template-slot="bar"]'); const text = template.querySelector('[data-template-slot="text"]'); let barColor; const percentHp = (monster.hp / monster.stats.hp) * 100; if (percentHp > 60) { barColor = 'green'; } else if (percentHp > 15) { barColor = 'rgb(240, 240, 100)'; } else { barColor = 'red'; } bar.style.backgroundColor = barColor; bar.style.width = `${percentHp}%`; text.textContent = `${monster.hp} / ${monster.stats.hp}`; return template; }, /** * @param {Monster} monster * * @returns {HTMLElement} */ createExpBar (monster) { const template = UI.createTemplate(Template.battleExpBar); const bar = template.querySelector('[data-template-slot="bar"]'); const text = template.querySelector('[data-template-slot="text"]'); const expToNextLevel = monster.getExperienceRequired() - monster.getExperienceRequired(-1); const currentRelativeExp = monster.exp - monster.getExperienceRequired(-1); const expPercent = (currentRelativeExp / expToNextLevel) * 100; bar.style.width = `${expPercent}%`; text.textContent = `${monster.exp} / ${monster.getExperienceRequired()}`; return template; }, /** * @param {string} gender * * @returns {HTMLElement} */ createGenderIcon (gender) { const icon = document.createElement('span'); icon.textContent = gender === 'male' ? '♂' : gender === 'female' ? '♀' : '⚲'; icon.title = slugToName(gender); icon.classList.add('gender-icon'); return icon; }, /** * @param {string} type * * @returns {HTMLElement} */ createElementTypeIcon (type) { const img = document.createElement('img'); img.src = `/modules/tuxemon/mods/tuxemon/gfx/ui/icons/element/${type}_type.png`; img.title = translate(type) || slugToName(type); return img; }, /** * @param {StatusEffect} statusEffect * * @returns {HTMLElement} */ createStatusEffectIcon (statusEffect) { if (!statusEffect) { return document.createElement('i'); } if (statusEffect.slug === 'faint') { const node = document.createElement('b'); node.innerHTML = 'X'; node.title = statusEffect.name; return node; } const img = document.createElement('img'); img.src = `/modules/tuxemon/mods/tuxemon/gfx/ui/icons/status/icon_${statusEffect.slug}.png`; img.title = statusEffect.name; return img; }, /** * @param {Monster} monster */ createBattleMonster (monster) { const template = UI.createTemplate(Template.battleMonster); template.querySelector('[data-template-slot="name"]').textContent = monster.name; template.querySelector('[data-template-slot="gender"]').innerHTML = UI.createGenderIcon(monster.gender).outerHTML; template.querySelector('[data-template-slot="level"]').textContent = monster.level; template.querySelector('[data-template-slot="statusEffect"]').innerHTML = UI.createStatusEffectIcon(monster.statusEffect).outerHTML; template.querySelector('[data-template-slot="sprite"]').src = `/modules/tuxemon/mods/tuxemon/gfx/sprites/battle/${monster.slug}-front.png`; UI.replaceTemplateSlot(template.querySelector('[data-template-slot="hpBar"]'), UI.createHpBar(monster)); return template; }, /** * @returns {HTMLElement} */ createOpponentMonster () { const battleMonsterNode = UI.createBattleMonster(Memory.state.opponent.activeMonster); battleMonsterNode.classList.add('battle__monster--opponent'); if (Game.isBattleType('trainer')) { battleMonsterNode.classList.add('battle__monster--is-trainer'); battleMonsterNode.querySelector('[data-template-slot="trainerName"]').textContent = Memory.state.opponent.name; if (Memory.state.opponent.sprite) { battleMonsterNode.classList.add('battle__monster--has-trainer-sprite'); battleMonsterNode.querySelector('[data-template-slot="trainerSprite"]').src = `/modules/tuxemon/mods/tuxemon/gfx/sprites/player/${Memory.state.opponent.sprite}`; battleMonsterNode.querySelector('[data-template-slot="trainerSprite"]').title = Memory.state.opponent.name; } } return battleMonsterNode; }, /** * @returns {HTMLElement} */ createActiveMonster () { const battleMonsterNode = UI.createBattleMonster(Memory.state.player.activeMonster); UI.replaceTemplateSlot( battleMonsterNode.querySelector('[data-template-slot="expBar"]'), UI.createExpBar(Memory.state.player.activeMonster) ); battleMonsterNode.classList.add('battle__monster--player'); battleMonsterNode.querySelector('[data-template-slot="sprite"]').addEventListener('click', UI.wrapCallback(() => { UI.openStatsMenu(Memory.state.player.activeMonster); })); if (UI.isHighlighting) { battleMonsterNode.querySelector('[data-template-slot="sprite"]').classList.add(UI.highlightClassName); } return battleMonsterNode; }, /** * @param {Monster} monster * * @returns {HTMLElement} */ createActiveTechniques (monster) { const template = UI.createTemplate(Template.techniques); for (const technique of monster.activeTechniques) { const techniqueNode = UI.createTemplate(Template.technique); techniqueNode.querySelector('[data-template-slot="name"]').textContent = technique.name; techniqueNode.querySelector('[data-template-slot="recharge"]').textContent = technique.rechargeLength; techniqueNode.querySelector('[data-template-slot="types"]').innerHTML = technique.types.map((type) => UI.createElementTypeIcon(type).outerHTML).join(''); techniqueNode.querySelector('[data-template-slot="range"]').textContent = translate(technique.range) || slugToName(technique.range); techniqueNode.querySelector('[data-template-slot="power"]').textContent = technique.power; techniqueNode.querySelector('[data-template-slot="accuracy"]').textContent = technique.accuracy; if (technique.isRecharging()) { techniqueNode.setAttribute('disabled', true); } template.appendChild(techniqueNode); } return template; }, /** * @returns {HTMLElement} */ createPlayerDefeatedMonsterSelection () { const monsterSelectionNode = UI.createMonsterSelection(Memory.state.player.monsters.filter((monster) => monster.hp > 0)); monsterSelectionNode.addEventListener('monster:selected', (event) => { Game.setActivePlayerMonster(event.detail.monster); Game.playerIsChoosingNextMonster = false; }); return monsterSelectionNode; }, /** * @param {HTMLElement} battleMonsterNode */ drawOpponentMonster () { const battleMonsterNode = UI.createOpponentMonster(); UI.elements.battleOpponentSprite = battleMonsterNode.querySelector('[data-template-slot="sprite"]'); UI.elements.battleOpponentSprite.style.transitionDuration = `${UI.damageHighlightClickDuration}s`; // en/disable catch UI.drawActiveBall(); const previousBattleMonsterNode = UI.elements.battleOpponent.querySelector('.battle__monster'); if (previousBattleMonsterNode) { UI.elements.battleOpponentSprite.classList = previousBattleMonsterNode.querySelector('[data-template-slot="sprite"]').classList; UI.elements.battleOpponent.removeChild(previousBattleMonsterNode); } UI.elements.battleOpponent.appendChild(battleMonsterNode); }, /** * @returns {void} */ drawActiveMonster () { const battleMonsterNode = UI.createActiveMonster(); const previousBattleMonsterNode = UI.elements.battlePlayer.querySelector('.battle__monster'); if (previousBattleMonsterNode) { UI.elements.battlePlayer.removeChild(previousBattleMonsterNode); } UI.elements.battlePlayer.appendChild(battleMonsterNode); }, /** * @returns {void} */ drawActiveTechniques () { if (!Memory.state.player.activeMonster) { // on starter selection screen only return; } const activeTechniques = UI.createActiveTechniques(Memory.state.player.activeMonster); activeTechniques.id = 'techniques'; document.querySelector('#techniques').innerHTML = activeTechniques.innerHTML; }, /** * @param {Technique} technique * * @returns {void} */ drawTechniqueAnimation (technique) { const animation = technique.animation; if (!(animation && DB.allAnimations[animation])) { return; } if (UI.techniqueAnimationIsRunning) { return; } UI.techniqueAnimationIsRunning = true; const x = UI.battleClickEvent.clientX; const y = UI.battleClickEvent.clientY; const techniqueAnimationLoop = UI.wrapCallback(() => { UI.elements.battleOpponentAnimation.src = `/modules/tuxemon/mods/tuxemon/animations/technique/${animation}_${("00" + UI.techniqueAnimationNumber).slice(-2)}.png`; UI.elements.battleOpponentAnimation.style.top = y - (UI.elements.battleOpponentAnimation.clientHeight / 2) + 'px'; UI.elements.battleOpponentAnimation.style.left = x - (UI.elements.battleOpponentAnimation.clientWidth / 2) + 'px'; // console.log(UI.elements.battleOpponentAnimation.src); UI.techniqueAnimationNumber++; if (UI.techniqueAnimationNumber >= DB.allAnimations[animation].length) { UI.techniqueAnimationIsRunning = false; UI.techniqueAnimationNumber = 0; UI.elements.battleOpponentAnimation.src = ''; return; } setTimeout(() => requestAnimationFrame(techniqueAnimationLoop), 1000 / UI.techniqueAnimationFps); }); requestAnimationFrame(techniqueAnimationLoop); }, drawLog () { if (UI.elements.log.children.length > Memory.state.Settings.logMaxLength) { UI.elements.log.innerHTML = ''; } for (const message of Game.logMessages) { const textNode = document.createElement('div'); textNode.innerHTML = '> '.repeat(message.indentation) + message.message; if (message.style) { for (const property of Object.keys(message.style)) { const value = message.style[property]; textNode.style[property] = value; } } UI.elements.log.appendChild(textNode); UI.elements.log.scrollTop = UI.elements.log.scrollHeight; } Game.logMessages = []; }, toggleLog () { UI.elements.log.classList.toggle('log--is-hidden'); }, openLog () { UI.elements.log.classList.remove('log--is-hidden'); }, closeLog () { UI.elements.log.classList.add('log--is-hidden'); }, drawArea () { if (Game.isTown(Memory.state.currentArea)) { UI.drawTown(); UI.closeLog(); UI.showMap(); UI.drawStatus(); UI.drawActiveBall(); } else { UI.drawBattle(); } }, drawBattle () { UI.elements.battle.style.backgroundImage = `url(${Memory.state.currentArea.environment.battle_graphics.background})`; UI.elements.battle.style.backgroundPosition = `${Memory.state.currentArea.environment.battle_graphics.background_position || 'top'}`; UI.showBattle(); UI.drawOpponentMonster(); UI.drawActiveMonster(); UI.drawActiveTechniques(); UI.drawStatus(); UI.drawActiveBall(); }, progressTurn () { UI.drawOpponentMonster(); UI.drawActiveMonster(); UI.drawActiveTechniques(); UI.drawStatus(); }, /** * @param {HTMLElement} monsterSelectionNode */ openPlayerDefeatedMonsterSelection (monsterSelectionNode) { const popup = UI.createPopup().cloneNode(true); // remove event listeners monsterSelectionNode.addEventListener('monster:selected', () => { popup.remove(); }); popup.querySelector('[data-template-slot="content"]').appendChild(monsterSelectionNode); UI.drawPopup(popup); }, /* Battle - Action Feedback */ /** * @type {MouseEvent} */ battleClickEvent: null, damageHighlightClickDuration: 0.1, damageHighlightClickTimeout: null, /** * @param {any} feedback * * @returns {HTMLElement} */ createActionFeedback (feedback) { const feedbackNode = UI.createTemplate(Template.battleActionFeedback); feedbackNode.innerHTML = feedback; feedbackNode.style.top = UI.battleClickEvent.pageY - UI.elements.battleOpponent.offsetTop + (Math.random() * 40 - 20) + 'px'; feedbackNode.style.left = UI.battleClickEvent.pageX - UI.elements.battleOpponent.offsetLeft + (Math.random() * 40 - 20) + 'px'; feedbackNode.dataset.duration = 2; feedbackNode.style.animationDuration = `${feedbackNode.dataset.duration}s`; return feedbackNode; }, /** * @param {number|string} damage * * @returns {HTMLElement} */ createDamage (damage) { return UI.createActionFeedback(damage); }, /** * @returns {HTMLElement} */ createDamageMiss () { return UI.createActionFeedback('MISS!'); }, /** * @param {HTMLElement} damageNode * @param {number} multiplier * * @returns {HTMLElement} */ applyMultiplierToDamage (damageNode, multiplier) { damageNode.style.fontSize = `${multiplier * 2}rem`; return damageNode; }, /** * @param {HTMLElement} damageNode * @param {Technique} technique * * @returns {HTMLElement} */ applyTechniqueToDamage (damageNode, technique) { damageNode.style.color = mixColors( ...technique.types.map((type) => standardizeColor(ElementTypeColor[type])) ); return damageNode; }, /** * @param {HTMLElement} damageNode * @param {StatusEffect} statusEffect * * @returns {HTMLElement} */ applyStatusEffectToDamage (damageNode, statusEffect) { damageNode.style.color = StatusEffectTypeColor[statusEffect.slug]; return damageNode; }, /** * @param {HTMLElement} damageNode */ drawActionFeedback (node) { UI.elements.battleOpponent.appendChild(node); setTimeout(() => node.remove(), (node.dataset.duration * 1000) - 500); }, /** * @param {HTMLElement} damageNode */ drawDamage (damageNode) { UI.drawActionFeedback(damageNode); UI.elements.battleOpponentSprite.classList.add('damaged'); clearTimeout(UI.damageHighlightClickTimeout); UI.damageHighlightClickTimeout = setTimeout( UI.wrapCallback(() => UI.elements.battleOpponentSprite.classList.remove('damaged')), UI.damageHighlightClickDuration * 1000 ); }, /** * @param {HTMLElement} damageNode */ drawDamageMiss (damageNode) { UI.drawActionFeedback(damageNode); }, /* Town */ drawTown () { UI.elements.sceneTown.querySelector('[data-template-slot="map"]').replaceChildren(UI.createMap()); }, /* Map */ shopSelectionMode: 'buy', /** * @returns {HTMLElement} */ createMap () { const template = UI.createTemplate(Template.map); const currentArea = Memory.state.currentArea; template.innerHTML = currentArea.map; if (currentArea.locations) { for (const locationId of Object.keys(currentArea.locations)) { const location = currentArea.locations[locationId]; template.querySelector(`[data-location="${locationId}"]`).addEventListener('click', UI.wrapCallback(() => { if (location.type === 'healingCenter') { UI.openHealingCenter(location); } else if (location.type === 'shop') { UI.openShop(location); } })); } } template.querySelectorAll('[data-area]').forEach((node) => node.addEventListener('click', UI.wrapCallback(async () => { await Game.goToArea(node.dataset.area); UI.closeAllPopups(); }))); template.querySelectorAll('[data-encounter]').forEach((node) => node.addEventListener('click', UI.wrapCallback(async () => { if (!Memory.state.Game.isInBattle) { const monster = await fetchMonster(node.dataset.encounter); monster.level = parseInt(node.dataset.encounterLevel); Game.encounterMonster(monster); Memory.state.Game.isInBattle = true; UI.showBattle(); UI.closeAllPopups(); } }))); 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; }, /** * @param {Object} healingCenter */ openHealingCenter (healingCenter) { const popup = UI.createPopup(); const template = UI.createTemplate(Template.healingCenter); const price = convertToCurrencyBase(healingCenter.price); template.querySelector('[data-template-slot="price"]').innerHTML = formatPrice(price); template.querySelector('[data-template-slot="heal"]').addEventListener('click', UI.wrapCallback(() => { const applicableMonsters = Memory.state.player.monsters.filter((monster) => { return monster.hp < monster.stats.hp || (monster.statusEffect && monster.statusEffect.category === 'negative') }); if (applicableMonsters.length === 0) { alert(translate('ui:no_applicable_monsters', true)); return; } const monsterSelectionPopup = UI.createPopup(); const monsterSelection = UI.createMonsterSelection(applicableMonsters); monsterSelection.addEventListener('monster:selected', (event) => { if (Memory.state.money < price) { alert(`Not enough ${DB.currencies.map[Memory.state.Settings.currency].symbol}.`); return; } Memory.state.money -= price; event.detail.monster.hp = event.detail.monster.stats.hp; event.detail.monster.statusEffect = null; event.detail.node.remove(); if (monsterSelection.children.length === 0) { monsterSelectionPopup.remove(); } UI.drawStatus(); }); monsterSelectionPopup.querySelector('.popup').appendChild(monsterSelection); UI.drawPopup(monsterSelectionPopup); })); template.querySelector('[data-template-slot="box.withdraw"]').addEventListener('click', UI.wrapCallback(() => { if (Memory.state.monsters.length === 0) { alert(translate('ui:healing_center:box:withdraw:no_tuxemon_in_box', true)); return; } if (Memory.state.player.monsters.length === 6) { alert(translate('ui:healing_center:box:withdraw:no_space_in_party', true)); return; } const boxPopup = UI.createPopup(); const monsterSelection = UI.createMonsterSelection(Memory.state.monsters); monsterSelection.addEventListener('monster:selected', UI.wrapCallback((event) => { Memory.state.player.monsters.push(event.detail.monster); Memory.state.monsters.splice(Memory.state.monsters.findIndex((monster) => monster === event.detail.monster), 1); event.detail.node.remove(); if (Memory.state.player.monsters.length === 6) { boxPopup.remove(); } })) boxPopup.querySelector('.popup').appendChild(monsterSelection); UI.drawPopup(boxPopup); })); template.querySelector('[data-template-slot="box.deposit"]').addEventListener('click', UI.wrapCallback(() => { if (Memory.state.player.monsters.length === 1) { alert(translate('ui:healing_center:box:deposit:last_tuxemon', true)); return; } const boxPopup = UI.createPopup(); const monsterSelection = UI.createMonsterSelection(Memory.state.player.monsters); monsterSelection.addEventListener('monster:selected', UI.wrapCallback((event) => { Memory.state.monsters.push(event.detail.monster); Memory.state.player.monsters.splice(Memory.state.player.monsters.findIndex((monster) => monster === event.detail.monster), 1); event.detail.node.remove(); if (Memory.state.player.monsters.length === 1) { boxPopup.remove(); } })) boxPopup.querySelector('.popup').appendChild(monsterSelection); UI.drawPopup(boxPopup); })); 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) => { UI.openStatsMenu(event.detail.monster); })) boxPopup.querySelector('.popup').appendChild(monsterSelection); UI.drawPopup(boxPopup); })); popup.querySelector('.popup').appendChild(template); UI.drawPopup(popup); }, /** * @param {Object} shop */ async openShop (shop) { const popup = UI.createPopup(); const template = UI.createTemplate(Template.shop); for (const itemData of shop.items) { const price = convertToCurrencyBase(itemData.price); const item = await fetchItem(itemData.item_name); const itemNode = UI.createTemplate(Template.shopItem); itemNode.querySelector('[data-template-slot="sprite"]').src = item.sprite; itemNode.querySelector('[data-template-slot="name"]').innerHTML = item.name; itemNode.querySelector('[data-template-slot="price"]').innerHTML = formatPrice(price); itemNode.addEventListener('click', UI.wrapCallback(() => { if (UI.shopSelectionMode === 'buy') { if (Memory.state.money < price) { alert(`Not enough ${DB.currencies.map[Memory.state.Settings.currency].symbol}.`); return; } Memory.state.money -= price; const itemInInventory = Memory.state.player.inventory.find((inventoryItem) => inventoryItem.slug === item.slug); if (itemInInventory) { itemInInventory.quantity++; } else { Memory.state.player.inventory.push(new InventoryItem(item, 1)); } UI.drawStatus(); } else if (UI.shopSelectionMode === 'info') { UI.openItemInfo(item); } })); template.querySelector('[data-template-slot="items"]').appendChild(itemNode); } const selectionModesNode = template.querySelector('[data-template-slot="modes"]'); const selectionModeNodes = selectionModesNode.querySelectorAll('[data-selection-mode]'); selectionModeNodes.forEach((node) => { if (node.dataset.selectionMode === UI.shopSelectionMode) { node.setAttribute('selected', true); } node.addEventListener('click', UI.wrapCallback(() => { selectionModesNode.querySelector(`[data-selection-mode="${UI.shopSelectionMode}"]`).removeAttribute('selected'); UI.shopSelectionMode = node.dataset.selectionMode; node.setAttribute('selected', true); })); }); popup.querySelector('.popup').appendChild(template); UI.drawPopup(popup); }, /* Menu */ partySelectionMode: 'select', inventorySelectionMode: 'use', isHighlighting: false, highlightClassName: 'setting-highlight', /** * @param {Monster[]} monsters * * @returns {HTMLElement} */ createMonsterSelection (monsters) { const template = document.createElement('div'); template.classList.add('monster-selection'); for (const monster of monsters) { const monsterNode = UI.createMonsterSelectionMonster(monster); monsterNode.addEventListener('monster:selected', UI.wrapCallback((event) => { template.dispatchEvent(new CustomEvent('monster:selected', { detail: { node: monsterNode, monster: event.detail.monster, }, })); })); template.appendChild(monsterNode); } return template; }, /** * @param {Monster} monster * * @returns {HTMLElement} */ createMonsterSelectionMonster (monster) { const template = UI.createPartyMonster(monster); template.addEventListener('click', () => { template.dispatchEvent(new CustomEvent('monster:selected', { detail: { monster: monster, }, })); }); return template; }, /** * @param {Monster[]} monsters * * @returns {HTMLElement} */ createPartySelection (monsters) { const party = UI.createTemplate(Template.party); party.id = 'party'; for (const monsterIdx in monsters) { const monster = monsters[monsterIdx]; const partyMonster = UI.createPartyMonster(monster); partyMonster.addEventListener('click', async (event) => { // bubble up to partyNode let target = event.target; while (target.parentNode.id !== party.id) { target = target.parentNode; } party.dispatchEvent(new CustomEvent('party:monster:selected', { detail: { monster: monster, mode: UI.partySelectionMode, node: partyMonster, }, })); }); party.querySelector('[data-template-slot="monsters"]').appendChild(partyMonster); } const selectionModesNode = party.querySelector('[data-template-slot="modes"]'); const selectionModeNodes = selectionModesNode.querySelectorAll('[data-party-selection-mode]'); selectionModeNodes.forEach((node) => { if (node.dataset.partySelectionMode === UI.partySelectionMode) { node.setAttribute('selected', true); } node.addEventListener('click', UI.wrapCallback(() => { selectionModesNode.querySelector(`[data-party-selection-mode="${UI.partySelectionMode}"]`).removeAttribute('selected'); UI.partySelectionMode = node.dataset.partySelectionMode; node.setAttribute('selected', true); })); }); return party; }, drawStatus () { const currentArea = Memory.state.currentArea; UI.elements.status.querySelector('[data-template-slot="money"]').textContent = `${Memory.state.money.toFixed(DB.currencies.map[Memory.state.Settings.currency].decimals)}` + ' ' + `${DB.currencies.map[Memory.state.Settings.currency].symbol}`; UI.elements.status.querySelector('[data-template-slot="area"]').textContent = currentArea.name; UI.elements.status.querySelector('[data-template-slot="monsterProgress"]').textContent = `${currentArea.monsterProgress} / ${currentArea.requiredEncounters}`; UI.elements.status.querySelector('[data-template-slot="trainerProgress"]').textContent = `${currentArea.trainerProgress} / ${currentArea.trainers.length}`; const nextTrainerButton = UI.elements.nextTrainer; if (!Game.isTown(currentArea)) { if ( Memory.state.opponent.type === 'monster' && currentArea.monsterProgress >= currentArea.requiredEncounters && currentArea.trainerProgress < currentArea.trainers.length && !Memory.state.Game.isInBattle ) { nextTrainerButton.disabled = false; } else { nextTrainerButton.disabled = true; } } else { nextTrainerButton.disabled = true; } const changeAreaButton = UI.elements.changeArea; 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]) ) { changeAreaButton.disabled = true; } else { changeAreaButton.disabled = false; } } else { changeAreaButton.disabled = false; } }, /** * @param {Monster[]} monsters */ openStarterMonsterSelection (monsters, { title }) { const popup = UI.createPopup().cloneNode(true); // remove close event const template = UI.createPartySelection(monsters); 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; if (UI.partySelectionMode === 'select') { template.dispatchEvent(new CustomEvent('starter:monster:selected', { detail: { monster: monster, node: event.detail.node, popup: popup, }, })); } else if (UI.partySelectionMode === 'stats') { UI.openStatsMenu(monster); } else if (UI.partySelectionMode === 'techniques') { UI.openMovesetSelection(monster); } }); popup.querySelector('.popup').appendChild(template); UI.drawPopup(popup); return template; }, showMap () { UI.elements.sceneBattle.classList.add('hidden'); UI.elements.sceneTown.classList.remove('hidden'); }, toggleMap () { if (Memory.state.Game.isInBattle || Game.isTown(Memory.state.currentArea)) { return; } UI.drawOpponentMonster(); UI.drawActiveMonster(); UI.drawActiveTechniques(); UI.drawTown(); UI.elements.sceneBattle.classList.toggle('hidden'); UI.elements.sceneTown.classList.toggle('hidden'); }, showBattle () { UI.elements.sceneBattle.classList.remove('hidden'); UI.elements.sceneTown.classList.add('hidden'); }, openAreaSelection () { const popup = UI.createPopup(); const template = UI.createTemplate(Template.areaSelection); const currentArea = Memory.state.currentArea; for (const connectionSlug in currentArea.connections) { const connection = currentArea.connections[connectionSlug]; const connectionNode = UI.createTemplate(Template.areaSelectionItem); connectionNode.querySelector('[data-template-slot="text"]').textContent = translate(connection['modules/tuxemon.slug']) || slugToName(connection['modules/tuxemon.slug']); let canGo = true; for (const condition of connection.conditions) { if (condition === 'encounters') { canGo = canGo && currentArea.monsterProgress >= currentArea.requiredEncounters; } else if (condition === 'trainers') { canGo = canGo && currentArea.trainerProgress >= currentArea.trainers.length; } else if (condition.startsWith('area.')) { const areaCondition = condition.replace('area.', ''); const areaSlug = areaCondition.split('.')[0]; const area = Memory.state.areaProgress[areaSlug] || null; if (!area) { canGo = false; continue; } if (areaCondition.includes('completed')) { canGo = canGo && area.isCompleted; } else { // doesn't have any extra properties and exists in progress => visited canGo = canGo && true; } } else if (condition.startsWith('story.')) { const storyCondition = condition.replace('story.', ''); const storySlug = storyCondition; const storyProgress = Memory.state.storyProgress[storySlug] || false; canGo = canGo && storyProgress; } } if (!canGo) { connectionNode.setAttribute('disabled', true); } connectionNode.addEventListener('click', UI.wrapCallback(() => { if (canGo) { Game.goToArea(connectionSlug); popup.remove(); } else { alert("Can\'t go there yet!"); } })); template.appendChild(connectionNode); } popup.querySelector('.popup').appendChild(template); UI.drawPopup(popup); }, openPartyMenu () { const popup = UI.createPopup(); const template = UI.createPartySelection(Memory.state.player.monsters); template.addEventListener('party:monster:selected', (event) => { const monster = event.detail.monster; if (UI.partySelectionMode === 'select') { Game.setActivePlayerMonster(monster); popup.remove(); } else if (UI.partySelectionMode === 'stats') { UI.openStatsMenu(monster); } else if (UI.partySelectionMode === 'techniques') { UI.openMovesetSelection(monster); } }); popup.querySelector('.popup').appendChild(template); UI.drawPopup(popup); }, drawActiveBall () { if (Game.canCatchMonster()) { UI.elements.menuCatch.removeAttribute('disabled'); } else { UI.elements.menuCatch.setAttribute('disabled', true); } if (Memory.state.activeBall) { UI.elements.menuCatch.querySelector('img').src = `/modules/tuxemon/mods/tuxemon/gfx/items/${Memory.state.activeBall}.png`; } else { UI.elements.menuCatch.querySelector('img').src = `/modules/tuxemon/mods/tuxemon/gfx/items/tuxeball.png`; } }, openInventoryMenu () { const popup = UI.createPopup(); const inventory = UI.createTemplate(Template.inventory); inventory.id = 'inventory'; const tabs = { heal: { heading: translate('ui:inventory:tab:heal'), items: [], }, stats: { heading: translate('ui:inventory:tab:stats'), items: [], }, balls: { heading: translate('ui:inventory:tab:balls'), items: [], }, techniques: { heading: translate('ui:inventory:tab:techniques'), items: [], }, other: { heading: translate('ui:inventory:tab:other'), items: [], }, keyItems: { heading: translate('ui:inventory:tab:key_items'), items: [], }, }; for (const item of Memory.state.player.inventory) { const inventoryItemNode = UI.createInventoryItem(item); if (['potion', 'revive'].includes(item.category)) { tabs['heal'].items.push(inventoryItemNode); } else if (['stats'].includes(item.category)) { tabs['stats'].items.push(inventoryItemNode); } else if (['capture'].includes(item.category)) { tabs['balls'].items.push(inventoryItemNode); } else if (['technique'].includes(item.category)) { tabs['techniques'].items.push(inventoryItemNode); } else if (['KeyItem'].includes(item.type)) { tabs['keyItems'].items.push(inventoryItemNode); } else { tabs['other'].items.push(inventoryItemNode); } } const tabsNode = UI.createTabs(Object.values(tabs).map((tab) => { const content = document.createElement('div'); content.classList.add('inventory__tab'); for (const item of tab.items) { content.appendChild(item); } return { heading: document.createTextNode(tab.heading), content: content, }; })); tabsNode.style.gridTemplateColumns = '1fr 1fr 1fr'; inventory.querySelector('[data-template-slot="items"]').appendChild(tabsNode); const selectionModesNode = inventory.querySelector('[data-template-slot="modes"]'); const selectionModeNodes = selectionModesNode.querySelectorAll('[data-selection-mode]'); selectionModeNodes.forEach((node) => { if (node.dataset.selectionMode === UI.inventorySelectionMode) { node.setAttribute('selected', true); } node.addEventListener('click', UI.wrapCallback(() => { selectionModesNode.querySelector(`[data-selection-mode="${UI.inventorySelectionMode}"]`).removeAttribute('selected'); UI.inventorySelectionMode = node.dataset.selectionMode; node.setAttribute('selected', true); })); }); popup.querySelector('.popup').appendChild(inventory); popup.classList.add('inventory__popup'); UI.drawPopup(popup); }, openJournalMenu () { const popup = UI.createPopup(); popup.id = 'popup___journal'; const journal = UI.createTemplate(Template.menuJournal); journal.querySelector('[data-template-slot="save"]').addEventListener('click', UI.wrapCallback(() => { UI.openSaveDialog(); })); journal.querySelector('[data-template-slot="load"]').addEventListener('click', UI.wrapCallback(() => { UI.openLoadDialog(); })); popup.querySelector('.popup').appendChild(journal); UI.drawPopup(popup); }, openSettingsMenu () { const popup = UI.createPopup(); const template = UI.createSettingsMenu(); popup.addEventListener('close', UI.wrapCallback(() => { Memory.saveToLocalStorage(); })); popup.querySelector('.popup').appendChild(template); UI.drawPopup(popup); }, /** * @param {Function[]} loadActions */ async showLoading (loadActions) { const popup = UI.createPopup(); const template = UI.createTemplate(Template.loadPopup); popup.querySelector('[data-template-slot="content"]').append(template); UI.drawPopup(popup); for (const idx in loadActions) { await loadActions[idx](); const percent = ((parseInt(idx) + 1) / loadActions.length) * 100; template.querySelector('[data-template-slot="progress"]').value = percent; template.querySelector('[data-template-slot="progress.text"]').textContent = `${percent.toFixed(2)}%`; } popup.remove(); }, /* Menu - Party */ /** * @param {Monster} * * @returns {HTMLElement} */ createPartyMonster (monster) { const partyMonster = UI.createTemplate(Template.partyMonster); partyMonster.querySelector('[data-template-slot="sprite"]').src = `/modules/tuxemon/mods/tuxemon/gfx/sprites/battle/${monster.slug}-front.png`; partyMonster.querySelector('[data-template-slot="name"]').textContent = monster.name; partyMonster.querySelector('[data-template-slot="gender"]').innerHTML = UI.createGenderIcon(monster.gender).outerHTML; partyMonster.querySelector('[data-template-slot="level"]').textContent = monster.level; partyMonster.querySelector('[data-template-slot="statusEffect"]').innerHTML = UI.createStatusEffectIcon(monster.statusEffect).outerHTML; partyMonster.querySelector('[data-template-slot="hpText"]').textContent = `${monster.hp} / ${monster.stats.hp}`; return partyMonster; }, /* Menu - Monster */ /** * @param {Monster} monster * * @returns {HTMLElement} */ createStatsMenu (monster) { const template = UI.createTemplate(Template.monsterStats); template.querySelector('[data-template-slot="name"]').textContent = monster.name; template.querySelector('[data-template-slot="gender"]').innerHTML = UI.createGenderIcon(monster.gender).outerHTML; 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); template.querySelector('[data-template-slot="stats.dodge.name"]').textContent = translate(StatType.dodge) || slugToName(StatType.dodge); template.querySelector('[data-template-slot="stats.speed.name"]').textContent = translate(StatType.speed) || slugToName(StatType.speed); template.querySelector('[data-template-slot="stats.melee.value"]').textContent = monster.stats.melee; template.querySelector('[data-template-slot="stats.armour.value"]').textContent = monster.stats.armour; template.querySelector('[data-template-slot="stats.ranged.value"]').textContent = monster.stats.ranged; template.querySelector('[data-template-slot="stats.dodge.value"]').textContent = monster.stats.dodge; template.querySelector('[data-template-slot="stats.speed.value"]').textContent = monster.stats.speed; template.querySelector('[data-template-slot="techniques"]').addEventListener('click', UI.wrapCallback(() => UI.openMovesetSelection(monster))); return template; }, /** * @param {Monster} monster * * @returns {Promise} */ async createMovesetSelection (monster) { const movesetListNode = UI.createTemplate(Template.movesetList); for (const move of monster.moveset) { const technique = await fetchTechnique(move.technique); const movesetItemNode = UI.createTemplate(Template.movesetItem); movesetItemNode.title = technique.description; movesetItemNode.querySelector('[data-template-slot="name"]').textContent = technique.name; movesetItemNode.querySelector('[data-template-slot="types"]').innerHTML = technique.types.map((type) => UI.createElementTypeIcon(type).outerHTML).join(''); movesetItemNode.querySelector('[data-template-slot="power"]').textContent = technique.power; movesetItemNode.querySelector('[data-template-slot="level"]').textContent = move.level_learned; // disabled? if (monster.level < move.level_learned) { movesetItemNode.setAttribute('disabled', true); } // selected? if (monster.activeTechniques.find((item) => item.slug == technique.slug)) { movesetItemNode.toggleAttribute('selected'); } // clicked movesetItemNode.addEventListener('click', UI.wrapCallback(() => { if (movesetItemNode.getAttribute('disabled')) { return false; } if (monster.activeTechniques.length === 4 && !movesetItemNode.hasAttribute('selected')) { return; } // un/select movesetItemNode.toggleAttribute('selected'); const isSelected = movesetItemNode.hasAttribute('selected'); if (isSelected) { monster.activeTechniques.push(technique); } else { const idxTechniqueToRemove = monster.activeTechniques.findIndex((item) => item.slug == technique.slug); if (idxTechniqueToRemove > -1) { monster.activeTechniques.splice(idxTechniqueToRemove, 1); } } const event = new CustomEvent('movesetSelection:moveSelected', { detail: { isSelected: movesetItemNode.hasAttribute('selected'), technique: technique, }, }); UI.events.dispatchEvent(event); })); movesetListNode.appendChild(movesetItemNode); } return movesetListNode; }, /** * @param {Monster} monster */ openStatsMenu (monster) { const popup = UI.createPopup(); const statusMenu = UI.createStatsMenu(monster); popup.querySelector('.popup').appendChild(statusMenu); UI.drawPopup(popup); }, /** * @param {Monster} monster */ async openMovesetSelection (monster) { const popup = UI.createPopup(); const movesetSelection = await UI.createMovesetSelection(monster); popup.querySelector('.popup').appendChild(movesetSelection); popup.addEventListener('close', UI.wrapCallback(() => UI.drawActiveTechniques())); UI.drawPopup(popup); }, /* Menu - Inventory */ /** * @param {InventoryItem} item * * @returns {HTMLElement} */ createInventoryItem (item) { const inventoryItemNode = UI.createTemplate(Template.inventoryItem); inventoryItemNode.title = item.description; inventoryItemNode.dataset.inventoryItem = item.slug; inventoryItemNode.querySelector('[data-template-slot="sprite"]').src = item.sprite; inventoryItemNode.querySelector('[data-template-slot="name"]').textContent = item.name; inventoryItemNode.querySelector('[data-template-slot="quantity"]').textContent = item.quantity; inventoryItemNode.addEventListener('click', UI.wrapCallback(async () => { if (UI.inventorySelectionMode === 'use') { if (item.category === 'potion') { UI.openItemMonsterSelection(item); } else if (item.category === 'revive') { UI.openItemMonsterSelection(item); } else if (item.category === 'capture') { Game.useItem(item); } else if (item.category === 'booster') { UI.openItemMonsterSelection(item); } } else if (UI.inventorySelectionMode === 'info') { UI.openItemInfo(item); } })); return inventoryItemNode; }, /** * @param {InventoryItem} item */ redrawInventoryItem (item) { const itemNode = document.querySelector(`#inventory *[data-inventory-item="${item.slug}"]`); if (item.quantity === 0) { itemNode.remove(); } const newNode = UI.createInventoryItem(item); itemNode.replaceWith(newNode); }, /** * @param {InventoryItem} item * @param {Monster[]} monsters * * @returns {HTMLElement} */ createItemMonsterSelection (item, monsters) { const template = UI.createMonsterSelection( monsters.filter((monster) => Game.canUseItem(item, monster)) ); const onMonsterSelectd = async (event) => { const monster = event.detail.monster; const monsterNode = event.detail.node || event.target; await Game.useItem(item, monster); if (item.quantity === 0) { template.dispatchEvent(new Event('item:isExhausted')); } else { const canStillUseItem = Game.canUseItem(item, monster); if (canStillUseItem) { const replacingMonsterNode = UI.createMonsterSelectionMonster(monster); replacingMonsterNode.addEventListener('monster:selected', onMonsterSelectd); monsterNode.replaceWith(replacingMonsterNode); } else { monsterNode.remove(); template.dispatchEvent(new Event('monster:lostCondition')); } } UI.redrawInventoryItem(item); }; template.addEventListener('monster:selected', onMonsterSelectd); return template; }, /** * @param {InventoryItem} item */ openItemMonsterSelection (item) { const popup = UI.createPopup(); const template = UI.createItemMonsterSelection(item, Memory.state.player.monsters); template.classList.add('inventory__monster-selection'); if (template.children.length === 0) { alert(translate('ui:no_applicable_monsters', true)); return; } template.addEventListener('item:isExhausted', () => popup.remove()); template.addEventListener('monster:lostCondition', () => popup.remove()); popup.querySelector('.popup').appendChild(template); UI.drawPopup(popup); }, /** * @param {InventoryItem} item */ openItemInfo (item) { const popup = UI.createPopup(); const template = document.createElement('div'); template.classList.add('item-info'); template.textContent = item.description; popup.querySelector('.popup').appendChild(template); UI.drawPopup(popup); }, /* Menu - Journal */ openSaveDialog () { const popup = UI.createPopup(); const dialog = UI.createTemplate(Template.dialogSave); Memory.saveToLocalStorage(); const saveData = Memory.saveToString(); dialog.querySelector('[data-template-slot="saveData"]').value = saveData; dialog.querySelector('[data-template-slot="saveClipboard"]').addEventListener('click', UI.wrapCallback(async () => { if (navigator.clipboard) { await navigator.clipboard.writeText(saveData); alert('Saved to clipboard!'); } else { alert('ERROR: Browser can\'t copy to clipboard! You have to do it manually.'); } })); popup.querySelector('.popup').appendChild(dialog); UI.drawPopup(popup); }, openLoadDialog () { const popup = UI.createPopup(); const dialog = UI.createTemplate(Template.dialogLoad); dialog.querySelector('[data-template-slot="load"]').addEventListener('click', UI.wrapCallback(() => { Memory.loadFromString( dialog.querySelector('[data-template-slot="saveData"]').value.trim() ); popup.remove(); document.querySelector('#popup___journal').remove(); })); popup.querySelector('.popup').appendChild(dialog); UI.drawPopup(popup); }, /* 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 = speaker.sprite; template.querySelector('[data-template-slot="speaker.name"]').textContent = speaker.name; 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 /** * @param {Function} callback * * @returns {Function} */ wrapCallback (callback) { return async (...arguments) => { try { await callback(...arguments); } catch (exception) { console.log(exception); UI.showErrorMessage( `${exception}\n\n${exception.stack}\n` + 'Please report at https://gitlab.com/dweipert.de/games/tuxemonclicker!' ); } }; }, /** * @param {string} message */ showErrorMessage (message) { alert(message); }, }; // UI element click bindings UI.elements.showMap.addEventListener('click', UI.wrapCallback(UI.toggleMap)); UI.elements.changeArea.addEventListener('click', UI.wrapCallback(UI.openAreaSelection)); UI.elements.menuParty.addEventListener('click', UI.wrapCallback(UI.openPartyMenu)); UI.elements.menuInventory.addEventListener('click', UI.wrapCallback(UI.openInventoryMenu)); UI.elements.menuLog.addEventListener('click', UI.wrapCallback(UI.toggleLog)); UI.elements.menuJournal.addEventListener('click', UI.wrapCallback(UI.openJournalMenu)); UI.elements.menuSettings.addEventListener('click', UI.wrapCallback(UI.openSettingsMenu));