diff options
Diffstat (limited to 'resources/js/ui.js')
-rw-r--r-- | resources/js/ui.js | 643 |
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); |