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'), 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'), 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'), }; const UI = { elements: { 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'), nextTrainer: document.querySelector('#status [data-template-slot="nextTrainer"]'), nextArea: document.querySelector('#status [data-template-slot="nextArea"]'), 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; }, /** * @returns {HTMLElement} */ createPopup () { const popup = UI.createTemplate(Template.popup); 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); }, /* 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'); } 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.openStatsMenu(Memory.state.player.activeMonster); }); 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 () { 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.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'); }, /** * @param {Area} area */ drawArea (area) { UI.elements.battle.style.backgroundImage = `url(/modules/tuxemon/mods/tuxemon/gfx/ui/combat/${area.environment.battle_graphics.background})`; }, 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.elements.battleOpponentSprite.classList.remove('damaged'), UI.damageHighlightClickDuration * 1000); }, /** * @param {HTMLElement} damageNode */ drawDamageMiss (damageNode) { UI.drawActionFeedback(damageNode); }, /* Menu */ partySelectionMode: 'select', inventorySelectionMode: 'use', /** * @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', (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; }, 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="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 ( Memory.state.opponent.type === 'monster' && currentArea.monsterProgress >= currentArea.requiredEncounters && currentArea.trainerProgress < currentArea.trainers.length ) { nextTrainerButton.disabled = false; } else { nextTrainerButton.disabled = true; } const nextAreaButton = UI.elements.nextArea; if ( currentArea.monsterProgress >= currentArea.requiredEncounters && currentArea.trainerProgress === currentArea.trainers.length ) { nextAreaButton.disabled = false; } else { nextAreaButton.disabled = true; } }, openPartyMenu () { const popup = UI.createPopup(); const party = UI.createTemplate(Template.party); party.id = 'party'; for (const monsterIdx in Memory.state.player.monsters) { const monster = Memory.state.player.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; } 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); } UI.events.dispatchEvent(new CustomEvent('party:monsterSelected', { detail: { monster: monster, mode: UI.partySelectionMode, }, })); }); 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', () => { selectionModesNode.querySelector(`[data-party-selection-mode="${UI.partySelectionMode}"]`).removeAttribute('selected'); UI.partySelectionMode = node.dataset.partySelectionMode; node.setAttribute('selected', true); }); }); popup.querySelector('.popup').appendChild(party); 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.slug}.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: 'Heal', items: [], }, stats: { heading: 'Stats', items: [], }, balls: { heading: 'Balls', items: [], }, techniques: { heading: 'Techniques', items: [], }, other: { heading: 'Other', items: [], }, keyItems: { heading: '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', () => { 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(); const journal = UI.createTemplate(Template.menuJournal); journal.querySelector('[data-template-slot="save"]').addEventListener('click', () => { UI.openSaveDialog(); }); journal.querySelector('[data-template-slot="load"]').addEventListener('click', () => { UI.openLoadDialog(); }); popup.querySelector('.popup').appendChild(journal); UI.drawPopup(popup); }, 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', async () => { const selected = [...languageSelectNode.children].find((node) => node.selected === true); Memory.state.Settings.language = selected.value; await fetchTranslation(Memory.state.Settings.language); UI.drawOpponentMonster(); UI.drawActiveMonster(); UI.drawActiveTechniques(); }); /* 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', 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.drawStatus(); }); template.querySelector('[data-template-slot="currency.lastUpdated"]').textContent = DB.currencies.last_updated; popup.querySelector('.popup').appendChild(template); UI.drawPopup(popup); }, /* 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="hpText"]').textContent = `${monster.hp} / ${monster.stats.hp}`; return partyMonster; }, /* Menu - Monster */ /** * @param {Monster} monster * * @returns {HTMLElement} */ createStatsMenu (monster) { // TODO 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="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.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', () => { 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.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 = `/modules/tuxemon/mods/tuxemon/${item.sprite}`; inventoryItemNode.querySelector('[data-template-slot="name"]').textContent = item.name; inventoryItemNode.querySelector('[data-template-slot="quantity"]').textContent = item.quantity; inventoryItemNode.addEventListener('click', async () => { if (UI.inventorySelectionMode === 'use') { if (item.category === 'potion') { UI.openItemMonsterSelection(item); } else if (item.category === 'capture') { Game.useItem(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) { Game.removeItemFromInventory(Memory.state.player.inventory, item); 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('No applicable monsters.'); 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.textContent = item.conditions + ' -- ' + item.effects + ' -- ' + item.description; popup.querySelector('.popup').appendChild(template); UI.drawPopup(popup); }, /* Menu - Journal */ openSaveDialog () { const popup = UI.createPopup(); const dialog = UI.createTemplate(Template.dialogSave); const saveData = Memory.save(); dialog.querySelector('[data-template-slot="saveData"]').value = saveData; dialog.querySelector('[data-template-slot="saveClipboard"]').addEventListener('click', 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', () => { Memory.load( dialog.querySelector('[data-template-slot="saveData"]').value.trim() ); document.querySelectorAll('.popup__overlay').forEach((element) => element.remove()) }); popup.querySelector('.popup').appendChild(dialog); UI.drawPopup(popup); }, }; // UI element click bindings UI.elements.menuParty.addEventListener('click', UI.openPartyMenu); UI.elements.menuInventory.addEventListener('click', UI.openInventoryMenu); UI.elements.menuLog.addEventListener('click', UI.toggleLog); UI.elements.menuJournal.addEventListener('click', UI.openJournalMenu); UI.elements.menuSettings.addEventListener('click', UI.openSettingsMenu);