diff options
author | Daniel Weipert <code@drogueronin.de> | 2023-08-15 13:18:57 +0200 |
---|---|---|
committer | Daniel Weipert <code@drogueronin.de> | 2023-08-15 13:18:57 +0200 |
commit | 8ab8988d01199f64151c532c59ff6c08735d4e37 (patch) | |
tree | 17d5bbdf36aa744119f37e0fffc6ede78d13fa70 /script.js |
initial commit
Diffstat (limited to 'script.js')
-rw-r--r-- | script.js | 583 |
1 files changed, 583 insertions, 0 deletions
diff --git a/script.js b/script.js new file mode 100644 index 0000000..c5c2028 --- /dev/null +++ b/script.js @@ -0,0 +1,583 @@ +const DB = { + allMonsters: [], + 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; + + name = ''; + 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 + ); + } + + canEvolve () { + if (this.evolutions.length === 0) { + return; + } + + return this.level >= this.evolutions[0].at_level; + } + + evolve () { + const evolution = this.evolutions[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; + } +} + +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.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('drashimi'); + + 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, + + 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.left = event.pageX - battleEnemy.offsetLeft; + damageNode.style.top = event.pageY - battleEnemy.offsetTop; + + 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('img'); + const imgClickDuration = 0.1; + enemyImg.style.transitionDuration = imgClickDuration + 's'; + enemyImg.classList.add('damaged'); + clearTimeout(clickTimeout); + clickTimeout = setTimeout(() => enemyImg.classList.remove('damaged'), imgClickDuration * 1000); + }, + + 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', async (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)]; + state.activeTechnique = await fetchTechnique(state.activeMonster.moveset[0].technique); + + UI.setBattleMonster(state.activeMonster, 'player'); + }); + partyMonster.style.cursor = 'pointer'; + + party.appendChild(partyMonster); + }, + + 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); + const movesetItem = UI.getTemplate(templateMovesetItem); + + movesetItem.textContent = slugToName(technique.slug) + ' - ' + technique.types + ' - ' + technique.power; + movesetItem.addEventListener('click', () => { + state.activeTechnique = technique; + UI.setBattleMonster(state.activeMonster, 'player'); + + popup.remove(); + }); + + movesetList.appendChild(movesetItem); + } + + popup.querySelector('.popup').appendChild(movesetList); + document.body.appendChild(popup); + }, + }; + + for (const monster of state.partyMonsters) { + UI.addPartyMonster(monster.slug); + } + + UI.setBattleMonster(state.activeMonster, 'player'); + UI.setBattleMonster(state.enemy.monster, 'enemy'); + + 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; + + 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.moneyModifier = state.enemy.monster.level; + UI.setBattleMonster(state.enemy.monster, 'enemy'); + + state.money += faintedMonster.level * faintedMonster.moneyModifier; + + state.activeMonster.exp += calculateAwardedExperience(state.activeMonster, faintedMonster); + state.activeMonster.levelUp(); + if (state.activeMonster.canEvolve()) { + await fetchMonster(state.activeMonster.evolutions[0].monster_slug); + state.activeMonster.evolve(); + state.activeTechnique = await fetchTechnique(state.activeMonster.moveset[0].technique); + } + + UI.setBattleMonster(state.activeMonster, 'player'); + } + UI.setHp(state.enemy.monster, battleEnemy); + money.textContent = state.money; + }); +})(); |