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'), 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'), }; 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; }, /** * @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'); } 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.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 () { if (!Memory.state.player) { // 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.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.elements.sceneTown.querySelector('[data-template-slot="map"]').replaceChildren(UI.createMap()); UI.closeLog(); UI.elements.sceneBattle.classList.add('hidden'); UI.elements.sceneTown.classList.remove('hidden'); } else { UI.elements.battle.style.backgroundImage = `url(/modules/tuxemon/mods/tuxemon/gfx/ui/combat/${Memory.state.currentArea.environment.battle_graphics.background})`; UI.elements.sceneTown.classList.add('hidden'); UI.elements.sceneBattle.classList.remove('hidden'); 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.elements.battleOpponentSprite.classList.remove('damaged'), UI.damageHighlightClickDuration * 1000); }, /** * @param {HTMLElement} damageNode */ drawDamageMiss (damageNode) { UI.drawActionFeedback(damageNode); }, /* Town */ drawTown () {}, /* Map */ /** * @returns {HTMLElement} */ createMap () { const template = document.createElement('div'); const currentArea = Memory.state.currentArea; template.innerHTML = currentArea.map; template.style.width = '100vw'; template.style.maxWidth = '750px'; if (currentArea.locations) { for (const locationId of Object.keys(currentArea.locations)) { const location = currentArea.locations[locationId]; template.querySelector(`[data-location="${locationId}"]`).addEventListener('click', () => { if (location.type === 'healingCenter') { UI.openHealingCenter(location); } else if (location.type === 'shop') { UI.openShop(location); } }); } } 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', () => { const applicableMonsters = Memory.state.player.monsters.filter((monster) => monster.hp < monster.stats.hp || monster.statusEffect); 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); }); 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', () => { 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(); }); template.appendChild(itemNode); } popup.querySelector('.popup').appendChild(template); UI.drawPopup(popup); }, /* Menu */ partySelectionMode: 'select', inventorySelectionMode: 'use', isHighlighting: false, /** * @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; }, /** * @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', () => { 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 && !Game.isInBattle ) { nextTrainerButton.disabled = false; } else { nextTrainerButton.disabled = true; } } else { nextTrainerButton.disabled = true; } const changeAreaButton = UI.elements.changeArea; if (!Game.isTown(currentArea)) { if ( Game.isInBattle || (Memory.state.opponent && Memory.state.opponent.type === 'trainer') ) { changeAreaButton.disabled = true; } else { changeAreaButton.disabled = false; } } else { changeAreaButton.disabled = false; } }, /** * @param {Monster[]} monsters */ openStarterMonsterSelection (monsters) { 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); 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; }, openMap () { if (Game.isInBattle || Game.isTown(Memory.state.currentArea)) { return; } const popup = UI.createPopup(); const template = UI.createMap(); popup.querySelector('.popup').appendChild(template); UI.drawPopup(popup); }, 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.')) { canGo = Memory.state.areaProgress.hasOwnProperty(condition.replace('area.', '')); } else if (condition.startsWith('event.')) { canGo = false; } } if (!canGo) { connectionNode.setAttribute('disabled', true); } connectionNode.addEventListener('click', () => { 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.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: 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', () => { 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.createEventListener(() => { UI.openSaveDialog(); })); journal.querySelector('[data-template-slot="load"]').addEventListener('click', UI.createEventListener(() => { 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); 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', 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.isHighlighting = !UI.isHighlighting; const elements = [ UI.elements.battleOpponent, UI.elements.battlePlayer.querySelector('[data-template-slot="sprite"]'), UI.elements.techniques, ...UI.elements.sceneTown.querySelectorAll('[data-location]'), 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, ]; for (const element of elements) { if (UI.isHighlighting) { element.classList.add('setting-highlight'); } else { element.classList.remove('setting-highlight'); } } }); 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="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) { // 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 = 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 === 'revive') { 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(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.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', UI.createEventListenr(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.createEventListener(() => { 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); }, // Error /** * @param {Function} listener * * @returns {Function} */ createEventListener (listener) { return (event) => { try { listener(event); } catch (exception) { console.log(exception); UI.showErrorMessage(exception); } }; }, /** * @param {string} message */ showErrorMessage (message) { alert(message); }, }; // UI element click bindings UI.elements.showMap.addEventListener('click', UI.createEventListener(UI.openMap)); UI.elements.changeArea.addEventListener('click', UI.createEventListener(UI.createEventListener(UI.openAreaSelection))); UI.elements.menuParty.addEventListener('click', UI.createEventListener(UI.openPartyMenu)); UI.elements.menuInventory.addEventListener('click', UI.createEventListener(UI.openInventoryMenu)); UI.elements.menuLog.addEventListener('click', UI.createEventListener(UI.toggleLog)); UI.elements.menuJournal.addEventListener('click', UI.createEventListener(UI.openJournalMenu)); UI.elements.menuSettings.addEventListener('click', UI.createEventListener(UI.openSettingsMenu));