const DB = { allMonsters: [], allAnimations: {}, monsters: {}, shapes: {}, elements: {}, techniques: {}, }; const ElementType = { aether: 'aether', wood: 'wood', fire: 'fire', earth: 'earth', metal: 'metal', water: 'water', }; const ElementTypeColor = { [ElementType.aether]: 'rgba(255, 255, 255, 1)', [ElementType.wood]: 'rgb(0, 255, 0, 1)', [ElementType.fire]: 'red', [ElementType.earth]: 'brown', [ElementType.metal]: 'silver', [ElementType.water]: 'blue', }; const TasteWarm = { tasteless: 'tasteless', peppy: 'peppy', salty: 'salty', hearty: 'hearty', zesty: 'zesty', refined: 'refined', }; const TasteCold = { tasteless: 'tasteless', mild: 'mild', sweet: 'sweet', soft: 'soft', flakey: 'flakey', dry: 'dry', }; const TechniqueRange = { melee: 'melee', touch: 'touch', ranged: 'ranged', reach: 'reach', reliable: 'reliable', }; const StatusType = { melee: 'melee', armour: 'armour', ranged: 'ranged', dodge: 'dodge', speed: 'speed', }; class State { money = 0; monsters = []; partyMonsters = []; activeMonster = null; activeTechnique = null; enemy = { monster: null, }; }; class Monster { #level = 1; exp = 1; hp = 0; tasteWarm = TasteWarm.tasteless; tasteCold = TasteCold.tasteless; gender = ''; statusModifiers = { hp: 0, melee: 0, armour: 0, ranged: 0, dodge: 0, speed: 0, }; experienceModifier = 1; moneyModifier = 1; constructor (slug) { this.slug = slug; const tasteWarm = Object.keys(TasteWarm).slice(1); this.tasteWarm = tasteWarm[Math.floor(Math.random() * tasteWarm.length)]; const tasteCold = Object.keys(TasteCold).slice(1); this.tasteCold = tasteCold[Math.floor(Math.random() * tasteCold.length)]; this.hp = this.status.hp; const possibleGenders = DB.monsters[this.slug].possible_genders; this.gender = possibleGenders[Math.floor(Math.random() * possibleGenders.length)] } get shape () { for (const shapeData of DB.shapes) { if (shapeData.slug === DB.monsters[this.slug].shape) { return shapeData; } } } get types () { return DB.monsters[this.slug].types; } get moveset () { return DB.monsters[this.slug].moveset; } get evolutions () { return DB.monsters[this.slug].evolutions; } get level () { return this.#level; } set level (level) { const statusPreLevelUp = this.status; const hpPreLevelUp = this.hp; this.#level = level; const statusPostLevelUp = this.status; this.hp = statusPostLevelUp.hp - (statusPreLevelUp.hp - hpPreLevelUp); if (this.exp < this.getExperienceRequired(-1)) { this.exp = this.getExperienceRequired(-1); } } get name () { return slugToName(this.slug); } canLevelUp () { return this.exp >= this.getExperienceRequired(); } levelUp () { while (this.canLevelUp()) { this.level++; } } getExperienceRequired (levelOffset = 0) { return Math.max( Math.pow(this.level + levelOffset, 3), 1 ); } getPossibleEvolutions () { return this.evolutions.filter((evolution) => this.level >= evolution.at_level && (!evolution.item || this.heldItem === evolution.item)); } canEvolve () { return this.getPossibleEvolutions().length > 0; } evolve () { const evolution = this.getPossibleEvolutions[0]; const statusPreEvolve = this.status; const hpPreEvolve = this.hp; this.slug = evolution.monster_slug; const statusPostEvolve = this.status; this.hp = statusPostEvolve.hp - (statusPreEvolve.hp - hpPreEvolve); } getTasteStatusModifier (statusName, baseStatus) { let positive = 0; let negative = 0; let isPositive = false; let isNegative = false; if (statusName === 'melee') { isPositive = this.tasteWarm === TasteWarm.salty; isNegative = this.tasteCold === TasteCold.sweet; } else if (statusName === 'armour') { isPositive = this.tasteWarm === TasteWarm.hearty; isNegative = this.tasteCold === TasteCold.soft; } else if (statusName === 'ranged') { isPositive = this.tasteWarm === TasteWarm.zesty; isNegative = this.tasteCold === TasteCold.flakey; } else if (statusName === 'dodge') { isPositive = this.tasteWarm === TasteWarm.refined; isNegative = this.tasteCold === TasteCold.dry; } else if (statusName === 'speed') { isPositive = this.tasteWarm === TasteWarm.peppy; isNegative = this.tasteCold === TasteCold.mild; } if (isPositive) { positive = baseStatus * 10 / 100; } if (isNegative) { negative = baseStatus * 10 / 100; } return Math.floor(positive) - Math.floor(negative); } get status () { const multiplier = this.level + 7; let hp = (this.shape.hp * multiplier) + this.statusModifiers.hp; let melee = (this.shape.melee * multiplier) + this.statusModifiers.melee; let armour = (this.shape.armour * multiplier) + this.statusModifiers.armour; let ranged = (this.shape.ranged * multiplier) + this.statusModifiers.ranged; let dodge = (this.shape.dodge * multiplier) + this.statusModifiers.dodge; let speed = (this.shape.speed * multiplier) + this.statusModifiers.speed; // Tastes melee += this.getTasteStatusModifier('melee', melee); armour += this.getTasteStatusModifier('armour', melee); ranged += this.getTasteStatusModifier('ranged', melee); dodge += this.getTasteStatusModifier('dodge', melee); speed += this.getTasteStatusModifier('speed', melee); return { hp, melee, armour, ranged, dodge, speed, }; } }; class Technique { constructor (slug) { this.slug = slug; } get types () { return DB.techniques[this.slug].types; } get power () { return DB.techniques[this.slug].power; } get range () { return DB.techniques[this.slug].range; } get animation () { return DB.techniques[this.slug].animation; } } function simpleDamageMultiplier (techniqueTypes, targetTypes) { let multiplier = 1; for (const techniqueType of techniqueTypes) { if (techniqueType === ElementType.aether) { continue; } for (const targetType of targetTypes) { if (targetType === ElementType.aether) { continue; } multiplier *= DB.elements[techniqueType].types.find((type) => type.against === targetType).multiplier; } } return Math.max(0.25, Math.min(multiplier, 4)); } function simpleDamageCalculation (technique, user, target) { let userBaseStrength = user.level + 7; let userStrength = 1; let targetResist = 1; if (technique.range === TechniqueRange.melee) { userStrength = userBaseStrength * user.status.melee; targetResist = target.status.armour; } else if (technique.range === TechniqueRange.touch) { userStrength = userBaseStrength * user.status.melee; targetResist = target.status.dodge; } else if (technique.range === TechniqueRange.ranged) { userStrength = userBaseStrength * user.status.ranged; targetResist = target.status.dodge; } else if (technique.range === TechniqueRange.reach) { userStrength = userBaseStrength * user.status.ranged; targetResist = target.status.armour; } else if (technique.range === TechniqueRange.reliable) { userStrength = userBaseStrength; targetResist = 1; } const multiplier = simpleDamageMultiplier(technique.types, target.types); const moveStrength = technique.power * multiplier; const damage = Math.floor((userStrength * moveStrength) / targetResist); return damage; } function calculateAwardedExperience (playerMonster, enemyMonster) { return Math.max( Math.floor( enemyMonster.getExperienceRequired(-1) / enemyMonster.level * enemyMonster.experienceModifier / playerMonster.level ), 1 ); } async function fetchMonster (slug) { if (! DB.monsters[slug]) { DB.monsters[slug] = await fetch(`/modules/tuxemon/mods/tuxemon/db/monster/${slug}.json`).then((response) => response.json()); } return new Monster(slug); } async function fetchTechnique (slug) { if (! DB.techniques[slug]) { DB.techniques[slug] = await fetch(`/modules/tuxemon/mods/tuxemon/db/technique/${slug}.json`).then((response) => response.json()); } return new Technique(slug); } function standardizeColor (color) { var ctx = document.createElement('canvas').getContext('2d'); ctx.fillStyle = color; return ctx.fillStyle; } function mixColors(...colors) { let r = 0; let g = 0; let b = 0; for (const color of colors) { const [cr, cg, cb] = color.match(/\w\w/g).map((c) => parseInt(c, 16)); r += cr; g += cg; b += cb; } r = r / colors.length; g = g / colors.length; b = b / colors.length; return `rgb(${r}, ${g}, ${b})`; } function slugToName (slug) { return slug.split('_').map((item) => item.charAt(0).toUpperCase() + item.slice(1)).join(' '); } (async function () { DB.allMonsters = await fetch('/db/all-monsters.json').then((response) => response.json()); DB.allAnimations = await fetch('/db/animations.json').then((response) => response.json()); DB.shapes = await fetch('/modules/tuxemon/mods/tuxemon/db/shape/shapes.json').then((response) => response.json()); for (const element of Object.keys(ElementType)) { DB.elements[element] = await fetch(`/modules/tuxemon/mods/tuxemon/db/element/${element}.json`).then((response) => response.json()); } const state = new State(); state.enemy.monster = await fetchMonster('grintot'); state.partyMonsters = [ await fetchMonster('corvix'), await fetchMonster('lunight'), await fetchMonster('prophetoise'), await fetchMonster('drashimi'), ]; state.activeMonster = state.partyMonsters[0]; state.activeTechnique = await fetchTechnique(state.activeMonster.moveset[0].technique); const templatePartyMonster = document.querySelector('#tpl___party__monster').innerHTML; const templateBattleMonster = document.querySelector('#tpl___battle__monster').innerHTML; const templatePopup = document.querySelector('#tpl___popup').innerHTML; const templateMovesetList = document.querySelector('#tpl___moveset__list').innerHTML; const templateMovesetItem = document.querySelector('#tpl___moveset__item').innerHTML; const party = document.querySelector('#party'); const money = document.querySelector('#money'); const battle = document.querySelector('#battle'); const battleEnemy = document.querySelector('#battle__enemy'); const battlePlayer = document.querySelector('#battle__player'); const UI = { activeMonster: null, damageAnimationInterval: null, damageAnimationNumber: 0, getTemplate (template) { var tpl = document.createElement('div'); tpl.innerHTML = template.trim(); return tpl.firstChild; }, setExp (monster, parentNode) { const expBar = parentNode.querySelector('.exp-bar'); const expText = parentNode.querySelector('.exp-text'); const expToNextLevel = monster.getExperienceRequired() - monster.getExperienceRequired(-1); const currentExp = monster.exp - monster.getExperienceRequired(-1); expBar.style.width = (currentExp / expToNextLevel) * 100 + '%'; expText.textContent = monster.exp + ' / ' + monster.getExperienceRequired(); }, setHp (monster, parentNode) { const hpBar = parentNode.querySelector('.hp-bar'); const hpText = parentNode.querySelector('.hp-text'); const percentHp = (monster.hp / monster.status.hp) * 100; if (percentHp > 60) { hpBar.style.backgroundColor = 'green'; } else if (percentHp > 15) { hpBar.style.backgroundColor = 'rgb(240, 240, 100)'; } else { hpBar.style.backgroundColor = 'red'; } hpBar.style.width = percentHp + '%'; hpText.textContent = monster.hp + ' / ' + monster.status.hp; }, createDamage (event, damage) { const damageNode = document.createElement('div'); damageNode.classList.add('damage'); damageNode.textContent = damage; const damageMultiplier = simpleDamageMultiplier(state.activeTechnique.types, state.enemy.monster.types); damageNode.style.fontSize = damageMultiplier + 'rem'; damageNode.style.top = event.pageY - battleEnemy.offsetTop + (Math.random() * 40 - 20); damageNode.style.left = event.pageX - battleEnemy.offsetLeft + (Math.random() * 40 - 20); damageNode.style.color = mixColors(...state.activeTechnique.types.map((type) => standardizeColor(ElementTypeColor[type]))); const damageNodeDuration = 2; damageNode.style.animationDuration = damageNodeDuration + 's'; battleEnemy.appendChild(damageNode); setTimeout(() => damageNode.remove(), (damageNodeDuration * 1000) - 500); const enemyImg = battleEnemy.querySelector('.battle__monster-img'); const imgClickDuration = 0.1; enemyImg.style.transitionDuration = imgClickDuration + 's'; enemyImg.classList.add('damaged'); clearTimeout(clickTimeout); clickTimeout = setTimeout(() => enemyImg.classList.remove('damaged'), imgClickDuration * 1000); var enemyAnimation = battleEnemy.querySelector('.battle__monster-sprite__animation'); // enemyAnimation.style.top = enemyImg.getBoundingClientRect().top; // enemyAnimation.style.left = enemyImg.getBoundingClientRect().left; if (!this.damageAnimationInterval) { this.damageAnimationInterval = setInterval(() => { enemyAnimation.src = `/modules/tuxemon/mods/tuxemon/animations/technique/${state.activeTechnique.animation}_0${this.damageAnimationNumber}.png`; enemyAnimation.style.top = event.clientY - (enemyAnimation.clientHeight / 2); enemyAnimation.style.left = event.clientX - (enemyAnimation.clientWidth / 2); console.log(enemyAnimation.src); this.damageAnimationNumber++; if (this.damageAnimationNumber === DB.allAnimations[state.activeTechnique.animation].length) { clearInterval(this.damageAnimationInterval); this.damageAnimationInterval = null; this.damageAnimationNumber = 0; enemyAnimation.src = ''; } }, 50); } }, addPartyMonster (slug) { let partyMonster = document.createElement('div'); partyMonster.innerHTML = templatePartyMonster.trim(); partyMonster = partyMonster.firstChild; partyMonster.dataset.slug = slug; partyMonster.querySelector('img').src = `/modules/tuxemon/mods/tuxemon/gfx/sprites/battle/${slug}-front.png`; partyMonster.addEventListener('click', (event) => { let target = event.target; while (target.parentNode.id !== 'party') { target = target.parentNode; } state.activeMonster = state.partyMonsters[Array.prototype.indexOf.call(document.querySelector('#party').children, target)]; UI.setActiveMonster(); }); partyMonster.style.cursor = 'pointer'; party.appendChild(partyMonster); }, async setActiveMonster () { let activeMoveIndex = 0; while ((await fetchTechnique(state.activeMonster.moveset[activeMoveIndex].technique)).power === 0) { activeMoveIndex++; } state.activeTechnique = await fetchTechnique(state.activeMonster.moveset[activeMoveIndex].technique); UI.setBattleMonster(state.activeMonster, 'player'); }, setBattleMonster (monster, where) { let battleMonster = document.createElement('div'); battleMonster.innerHTML = templateBattleMonster.trim(); battleMonster = battleMonster.firstChild; battleMonster.querySelector('.battle__monster-info__name').textContent = monster.name; battleMonster.querySelector('.battle__monster-info__gender').textContent = monster.gender === 'male' ? '♂' : monster.gender === 'female' ? '♀' : '⚲'; battleMonster.querySelector('.battle__monster-info__level').textContent = monster.level; UI.setHp(monster, battleMonster.querySelector('.hp')); battleMonster.querySelector('img').src = `/modules/tuxemon/mods/tuxemon/gfx/sprites/battle/${monster.slug}-front.png`; if (where === 'player') { UI.setExp(monster, battleMonster.querySelector('.exp')); battleMonster.querySelector('.battle__monster-technique').textContent = state.activeTechnique.slug + ' - ' + state.activeTechnique.types + ' - ' + state.activeTechnique.power; battleMonster.querySelector('.battle__monster-technique').addEventListener('click', UI.openMovesetSelection); battleMonster.classList.add('battle__monster--player'); battlePlayer.replaceChildren(battleMonster); } else { battleMonster.classList.add('battle__monster--enemy'); // battleEnemy.replaceChildren(battleMonster); battleEnemy.querySelector('.battle__monster') && battleEnemy.removeChild(battleEnemy.querySelector('.battle__monster')); battleEnemy.appendChild(battleMonster); } }, async openMovesetSelection () { const popup = UI.getTemplate(templatePopup); popup.addEventListener('click', ({ target }) => target === popup && popup.remove()); const movesetList = UI.getTemplate(templateMovesetList); for (const move of state.activeMonster.moveset) { const technique = await fetchTechnique(move.technique); if (technique.power === 0) { continue; } const movesetItem = UI.getTemplate(templateMovesetItem); movesetItem.textContent = slugToName(technique.slug) + ' - ' + technique.types + ' - ' + technique.power + ' - ' + move.level_learned; movesetItem.addEventListener('click', () => { if (movesetItem.getAttribute('disabled')) { return false; } state.activeTechnique = technique; UI.setBattleMonster(state.activeMonster, 'player'); popup.remove(); }); if (state.activeMonster.level < move.level_learned) { movesetItem.setAttribute('disabled', true); } movesetList.appendChild(movesetItem); } popup.querySelector('.popup').appendChild(movesetList); document.body.appendChild(popup); }, async createNewEnemyMonster () { state.enemy.monster = await fetchMonster(DB.allMonsters[Math.floor(Math.random() * DB.allMonsters.length)]); state.enemy.monster.level = Math.ceil(Math.random() * state.activeMonster.level); // state.enemy.monster.experienceModifier = state.enemy.monster.level; state.enemy.monster.moneyModifier = state.enemy.monster.level; UI.setBattleMonster(state.enemy.monster, 'enemy'); }, }; for (const monster of state.partyMonsters) { UI.addPartyMonster(monster.slug); } UI.setBattleMonster(state.activeMonster, 'player'); UI.setBattleMonster(state.enemy.monster, 'enemy'); UI.setActiveMonster(); var clickTimeout; document.querySelector('#battle__enemy').addEventListener('click', async (event) => { const damage = simpleDamageCalculation(state.activeTechnique, state.activeMonster, state.enemy.monster); UI.createDamage(event, damage); state.enemy.monster.hp -= damage; if (state.enemy.monster.hp <= 0) { const faintedMonster = state.enemy.monster; await UI.createNewEnemyMonster(); state.money += faintedMonster.level * faintedMonster.moneyModifier; state.activeMonster.exp += calculateAwardedExperience(state.activeMonster, faintedMonster) * 50; state.activeMonster.levelUp(); if (state.activeMonster.canEvolve()) { await fetchMonster(state.activeMonster.evolutions[0].monster_slug); state.activeMonster.evolve(); UI.setActiveMonster(); } UI.setBattleMonster(state.activeMonster, 'player'); } UI.setHp(state.enemy.monster, battleEnemy); money.textContent = state.money; }); document.querySelector('#catch').addEventListener('click', async (event) => { const caughtMonster = new Monster(state.enemy.monster.slug); caughtMonster.level = state.enemy.monster.level; state.partyMonsters.push(caughtMonster); UI.createNewEnemyMonster(); UI.addPartyMonster(caughtMonster.slug); }); })();