summaryrefslogtreecommitdiff
path: root/resources/js/ui.js
diff options
context:
space:
mode:
Diffstat (limited to 'resources/js/ui.js')
-rw-r--r--resources/js/ui.js643
1 files changed, 643 insertions, 0 deletions
diff --git a/resources/js/ui.js b/resources/js/ui.js
new file mode 100644
index 0000000..f6a36b8
--- /dev/null
+++ b/resources/js/ui.js
@@ -0,0 +1,643 @@
+const Template = {
+ popup: document.querySelector('#tpl___popup'),
+
+ battleMonster: document.querySelector('#tpl___battle__monster'),
+ battleHpBar: document.querySelector('#tpl___battle__hp-bar'),
+ battleExpBar: document.querySelector('#tpl___battle__exp-bar'),
+ battleDamage: document.querySelector('#tpl___battle__damage'),
+
+ movesetList: document.querySelector('#tpl___moveset__list'),
+ movesetItem: document.querySelector('#tpl___moveset__item'),
+
+ techniques: document.querySelector('#tpl___techniques'),
+ technique: document.querySelector('#tpl___technique'),
+
+ party: document.querySelector('#tpl___party'),
+ partyMonster: document.querySelector('#tpl___party__monster'),
+
+ menuJournal: document.querySelector('#tpl___menu__journal'),
+};
+
+const UI = {
+ elements: {
+ battleEnemy: document.querySelector('#battle__enemy'),
+ battleEnemySprite: null,
+ battleEnemyAnimation: document.querySelector('.battle__monster-sprite__animation'),
+ battlePlayer: document.querySelector('#battle__player'),
+
+ techniques: document.querySelector('#techniques'),
+
+ money: document.querySelector('#money'),
+
+ menuParty: document.querySelector('#menu__party'),
+ menuInventory: document.querySelector('#menu__inventory'),
+ menuCatch: document.querySelector('#menu__catch'),
+ menuJournal: document.querySelector('#menu__journal'),
+ },
+
+ events: document.createElement('div'),
+
+
+ /**
+ * @param {HTMLElement} template
+ */
+ createTemplate (template) {
+ const templateBase = document.createElement('div');
+ templateBase.innerHTML = template.innerHTML.trim();
+
+ return templateBase.firstChild;
+ },
+
+ /**
+ * @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;
+ },
+
+ /**
+ * @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 */
+
+ /**
+ * @type {MouseEvent}
+ */
+ battleClickEvent: null,
+
+ 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} 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 = 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"]').textContent = monster.gender === 'male' ? '♂' : monster.gender === 'female' ? '♀' : '⚲';
+ 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}
+ */
+ createEnemyMonster () {
+ const battleMonsterNode = UI.createBattleMonster(state.enemy.monster);
+
+ battleMonsterNode.classList.add('battle__monster--enemy');
+
+ return battleMonsterNode;
+ },
+
+ /**
+ * @returns {HTMLElement}
+ */
+ createActiveMonster () {
+ const battleMonsterNode = UI.createBattleMonster(state.activeMonster);
+
+ UI.replaceTemplateSlot(
+ battleMonsterNode.querySelector('[data-template-slot="expBar"]'),
+ UI.createExpBar(state.activeMonster)
+ );
+
+ battleMonsterNode.classList.add('battle__monster--player');
+
+ battleMonsterNode.querySelector('[data-template-slot="sprite"]').addEventListener('click', () => {
+ UI.openStatsMenu(state.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="types"]').innerHTML = technique.types.map((type) => UI.createElementTypeIcon(type).outerHTML).join('');
+ techniqueNode.querySelector('[data-template-slot="power"]').textContent = technique.power;
+ techniqueNode.querySelector('[data-template-slot="accuracy"]').textContent = technique.accuracy;
+ techniqueNode.querySelector('[data-template-slot="range"]').textContent = technique.range;
+
+ template.appendChild(techniqueNode);
+ }
+
+ return template;
+ },
+
+ /**
+ * @param {HTMLElement} battleMonsterNode
+ */
+ drawEnemyMonster () {
+ const battleMonsterNode = UI.createEnemyMonster();
+
+ UI.elements.battleEnemySprite = battleMonsterNode.querySelector('[data-template-slot="sprite"]');
+ UI.elements.battleEnemySprite.style.transitionDuration = `${UI.damageHighlightClickDuration}s`;
+
+ const previousBattleMonsterNode = UI.elements.battleEnemy.querySelector('.battle__monster');
+ if (previousBattleMonsterNode) {
+ UI.elements.battleEnemySprite.classList = previousBattleMonsterNode.querySelector('[data-template-slot="sprite"]').classList;
+
+ UI.elements.battleEnemy.removeChild(previousBattleMonsterNode);
+ }
+ UI.elements.battleEnemy.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(state.activeMonster);
+ activeTechniques.id = 'techniques';
+
+ document.querySelector('#techniques').innerHTML = activeTechniques.innerHTML;
+ },
+
+ /**
+ * @returns {void}
+ */
+ drawTechniqueAnimation () {
+ if (!UI.techniqueAnimationIsRunning && state.activeTechnique.animation && DB.allAnimations[state.activeTechnique.animation]) {
+ UI.techniqueAnimationIsRunning = true;
+
+ const techniqueAnimationLoop = () => {
+ UI.elements.battleEnemyAnimation.src = `/modules/tuxemon/mods/tuxemon/animations/technique/${state.activeTechnique.animation}_${("00" + UI.techniqueAnimationNumber).slice(-2)}.png`;
+ UI.elements.battleEnemyAnimation.style.top = UI.battleClickEvent.clientY - (UI.elements.battleEnemyAnimation.clientHeight / 2);
+ UI.elements.battleEnemyAnimation.style.left = UI.battleClickEvent.clientX - (UI.elements.battleEnemyAnimation.clientWidth / 2);
+ // console.log(UI.elements.battleEnemyAnimation.src);
+
+ UI.techniqueAnimationNumber++;
+
+ if (UI.techniqueAnimationNumber >= DB.allAnimations[state.activeTechnique.animation].length) {
+ UI.techniqueAnimationIsRunning = false;
+ UI.techniqueAnimationNumber = 0;
+ UI.elements.battleEnemyAnimation.src = '';
+ return;
+ }
+
+ setTimeout(() => requestAnimationFrame(techniqueAnimationLoop), 1000 / UI.techniqueAnimationFps);
+ };
+
+ requestAnimationFrame(techniqueAnimationLoop);
+ }
+ },
+
+
+ /* Battle - Damage */
+
+ damageHighlightClickDuration: 0.1,
+ damageHighlightClickTimeout: null,
+
+ /**
+ * @param {number|string} damage
+ *
+ * @returns {HTMLElement}
+ */
+ createDamage (damage) {
+ const damageNode = UI.createTemplate(Template.battleDamage);
+ damageNode.innerHTML = damage;
+
+ damageNode.style.top = UI.battleClickEvent.pageY - UI.elements.battleEnemy.offsetTop + (Math.random() * 40 - 20);
+ damageNode.style.left = UI.battleClickEvent.pageX - UI.elements.battleEnemy.offsetLeft + (Math.random() * 40 - 20);
+
+ damageNode.dataset.duration = 2;
+ damageNode.style.animationDuration = `${damageNode.dataset.duration}s`;
+
+ return damageNode;
+ },
+
+ /**
+ * @returns {HTMLElement}
+ */
+ createDamageMiss () {
+ return UI.createDamage('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
+ */
+ drawDamage (damageNode) {
+ UI.elements.battleEnemy.appendChild(damageNode);
+ setTimeout(() => damageNode.remove(), (damageNode.dataset.duration * 1000) - 500);
+
+ UI.elements.battleEnemySprite.classList.add('damaged');
+ clearTimeout(UI.damageHighlightClickTimeout);
+ UI.damageHighlightClickTimeout = setTimeout(() => UI.elements.battleEnemySprite.classList.remove('damaged'), UI.damageHighlightClickDuration * 1000);
+
+ UI.drawTechniqueAnimation();
+ },
+
+ /**
+ * @param {HTMLElement} damageNode
+ */
+ drawDamageMiss (damageNode) {
+ UI.elements.battleEnemy.appendChild(damageNode);
+ setTimeout(() => damageNode.remove(), (damageNode.dataset.duration * 1000) - 500);
+ },
+
+
+
+ /* Menu */
+
+ partySelectionMode: 'select',
+
+
+ openPartyMenu () {
+ const popup = UI.createPopup();
+
+ const party = UI.createTemplate(Template.party);
+ party.id = 'party';
+ for (const monsterIdx in state.partyMonsters) {
+ const monster = state.partyMonsters[monsterIdx];
+ 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.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') {
+ state.activeMonster = monster;
+ state.activeTechnique = state.activeMonster.activeTechniques[0];
+
+ UI.drawActiveMonster();
+ UI.drawActiveTechniques();
+
+ 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);
+ },
+
+ openInventoryMenu () { // TODO
+ const popup = UI.createPopup();
+
+ const inventory = document.createElement('div');
+ inventory.id = 'inventory';
+ for (const item of state.inventory) {
+ }
+
+ popup.querySelector('.popup').appendChild(inventory);
+ UI.drawPopup(popup);
+ },
+
+ openJournalMenu () { // TODO
+ 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);
+ },
+
+ openSaveDialog () { // TODO
+ const popup = UI.createPopup();
+
+ const textarea = document.createElement('textarea');
+ textarea.value = Game.save();
+
+ popup.querySelector('.popup').appendChild(textarea);
+ UI.drawPopup(popup);
+ },
+
+ openLoadDialog () { // TODO
+ const popup = UI.createPopup();
+
+ const textarea = document.createElement('textarea');
+
+ const loadButton = document.createElement('button');
+ loadButton.textContent = "Load";
+ loadButton.addEventListener('click', () => Game.load(textarea.value.trim()));
+
+ popup.querySelector('.popup').appendChild(textarea);
+ popup.querySelector('.popup').appendChild(loadButton);
+
+ UI.drawPopup(popup);
+ },
+
+
+ /* Menu - Monster */
+
+ /**
+ * @param {Monster} monster
+ *
+ * @returns {HTMLElement}
+ */
+ createStatsMenu (monster) { // TODO
+ const template = document.createElement('div');
+ template.textContent = "select moves for " + monster.name;
+ template.style.width = '90vw';
+ template.style.height = '90vh';
+
+ template.addEventListener('click', () => UI.openMovesetSelection(monster));
+
+ return template;
+ },
+
+ /**
+ * @param {Monster} monster
+ *
+ * @returns {Promise<HTMLElement>}
+ */
+ 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.querySelector('[data-template-slot="name"]').textContent = slugToName(technique.slug);
+ 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;
+ }
+
+ // 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;
+ },
+
+ 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);
+ },
+};
+
+// UI element click bindings
+UI.elements.menuParty.addEventListener('click', UI.openPartyMenu);
+UI.elements.menuInventory.addEventListener('click', UI.openInventoryMenu);
+UI.elements.menuJournal.addEventListener('click', UI.openJournalMenu);