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'),
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'),
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');
}
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');
},
drawArea () {
UI.elements.battle.style.backgroundImage = `url(/modules/tuxemon/mods/tuxemon/gfx/ui/combat/${Memory.state.currentArea.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);
},
/* Town */
async drawTown () {
const currentArea = Memory.state.currentArea;
UI.elements.sceneTown.innerHTML = Memory.state.currentArea.map;
for (const locationId of Object.keys(currentArea.locations)) {
const location = currentArea.locations[locationId];
UI.elements.sceneTown.querySelector(`[data-location="${locationId}"]`).addEventListener('click', () => {
if (location.type === 'healingCenter') {
UI.openHealingCenter(location);
}
else if (location.type === 'shop') {
UI.openShop(location);
}
});
}
},
openHealingCenter (healingCenter) {},
async openShop (shop) {
const popup = UI.createPopup();
const template = document.createElement('div');
for (const itemData of shop.items) {
const item = await fetchItem(itemData.item_name);
const itemNode = document.createElement('div');
itemNode.innerHTML = ``;
itemNode.innerHTML += `${item.name}`;
itemNode.innerHTML += `${itemData.price} ${DB.currencies.map[Memory.state.Settings.currency].symbol}`;
template.appendChild(itemNode);
}
popup.querySelector('.popup').appendChild(template);
UI.drawPopup(popup);
},
/* 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="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 (
Memory.state.opponent.type === 'monster' &&
currentArea.monsterProgress >= currentArea.requiredEncounters &&
currentArea.trainerProgress < currentArea.trainers.length
) {
nextTrainerButton.disabled = false;
} else {
nextTrainerButton.disabled = true;
}
},
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 = connection.name;
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('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 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.drawTown();
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.changeArea.addEventListener('click', UI.openAreaSelection);
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);