From 8ab8988d01199f64151c532c59ff6c08735d4e37 Mon Sep 17 00:00:00 2001 From: Daniel Weipert Date: Tue, 15 Aug 2023 13:18:57 +0200 Subject: initial commit --- .gitignore | 0 .gitmodules | 3 + Readme.md | 1 + all-monsters.php | 10 + db/all-monsters.json | 1 + index.html | 81 +++++++ modules/tuxemon | 1 + script.js | 583 +++++++++++++++++++++++++++++++++++++++++++++++++++ style.css | 190 +++++++++++++++++ 9 files changed, 870 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 Readme.md create mode 100644 all-monsters.php create mode 100644 db/all-monsters.json create mode 100644 index.html create mode 160000 modules/tuxemon create mode 100644 script.js create mode 100644 style.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..c3786a0 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "modules/tuxemon"] + path = modules/tuxemon + url = https://github.com/Tuxemon/Tuxemon diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..7de2ce4 --- /dev/null +++ b/Readme.md @@ -0,0 +1 @@ +- https://wiki.tuxemon.org/Category:Monster diff --git a/all-monsters.php b/all-monsters.php new file mode 100644 index 0000000..cf687c7 --- /dev/null +++ b/all-monsters.php @@ -0,0 +1,10 @@ + + + + + +
+
Money:
+
+ +
+
+ +
+
+
+ + + + + + + + + + + + + diff --git a/modules/tuxemon b/modules/tuxemon new file mode 160000 index 0000000..136c50b --- /dev/null +++ b/modules/tuxemon @@ -0,0 +1 @@ +Subproject commit 136c50bff1fb5b04a1cb0032030fb868c0bca9ee 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; + }); +})(); diff --git a/style.css b/style.css new file mode 100644 index 0000000..8ffcee1 --- /dev/null +++ b/style.css @@ -0,0 +1,190 @@ +* { + box-sizing: border-box; +} + +body { + margin: 0; +} + +img { + display: inline-block; + max-width: 100%; +} + +.wrap { + margin: 0 auto; + width: 1200px; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; +} + +.popup__overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background-color: rgba(0, 0, 0, 0.8); + display: flex; + justify-content: center; + align-items: center; +} +.popup { + background-color: #fff; + padding: 1rem; +} + +#battle { + user-select: none; + min-width: 750px; + min-height: 300px; + padding: 1rem; + + display: flex; + flex-direction: column; + justify-content: space-between; + + background-image: url('/modules/tuxemon/mods/tuxemon/gfx/backgrounds/test/back02.png'); + background-image: url('https://wiki.tuxemon.org/images/9/9f/Sea_background.png'); + background-size: cover; +} + +#battle__enemy { + position: relative; + cursor: pointer; + flex-grow: 1; +} + +.battle__monster { + display: flex; + justify-content: space-between; + align-items: center; +} +.battle__monster-info { + border: 2px solid #000; + background-color: beige; + + flex-grow: 1; + flex-basis: 50%; +} +.battle__monster-info-box { + display: flex; + flex-direction: column; + + padding: 0.25rem; +} +.battle__monster-info__gender { + line-height: 1em; +} +.battle__monster-info-exp { + display: flex; + align-items: center; + + padding: 0.25rem; + + background-color: #000; + color: yellow; +} +.exp-label { + margin-right: 0.5rem; +} +.battle__monster-visual { + flex-grow: 1; + flex-basis: 50%; + text-align: center; +} + +.battle__monster-sprite { + margin-bottom: 0.25rem; +} +.battle__monster-sprite img { + transition-property: filter; +} +.battle__monster-sprite img.damaged { + filter: brightness(2); +} + +.battle__monster-technique { + background-color: beige; + border: 2px solid #000; + display: inline; + padding: 0.25rem; +} + +.battle__monster--player { + flex-direction: row-reverse; +} +.battle__monster--enemy .battle__monster-info-exp, +.battle__monster--enemy .battle__monster-technique { + display: none; +} + +.exp { + width: 100%; +} +.exp-bar-wrap { + width: 100%; + border: 1px solid rgba(0, 0, 0, 0.5); + background-color: lightgray; +} +.exp-bar { + background-color: blue; + height: 7px; + transition: background-color; + width: 0%; +} +.hp-bar-wrap { + width: 100%; + border: 1px solid rgba(0, 0, 0, 0.5); +} +.hp-bar { + background-color: green; + height: 10px; + transition: background-color; +} + +#enemy { + position: relative; + user-select: none; + cursor: pointer; + min-width: 750px; + min-height: 300px; + display: flex; + justify-content: center; + align-items: center; + border: 1px solid #000; + flex-direction: column; + + background-image: url('/modules/tuxemon/mods/tuxemon/gfx/backgrounds/test/back02.png'); + background-image: url('https://wiki.tuxemon.org/images/9/9f/Sea_background.png'); + background-size: cover; +} + +#enemy img { + transition-property: filter; +} +#enemy img.clicked { + filter: brightness(2); +} + + +.damage { + position: absolute; + color: red; + + animation: float-up-and-disappear; +} + +@keyframes float-up-and-disappear { + from { + opacity: 1; + } + + to { + top: 0; + opacity: 0; + font-size: 0; + } +} -- cgit v1.2.3