summaryrefslogtreecommitdiff
path: root/script.js
diff options
context:
space:
mode:
authorDaniel Weipert <code@drogueronin.de>2023-08-15 13:18:57 +0200
committerDaniel Weipert <code@drogueronin.de>2023-08-15 13:18:57 +0200
commit8ab8988d01199f64151c532c59ff6c08735d4e37 (patch)
tree17d5bbdf36aa744119f37e0fffc6ede78d13fa70 /script.js
initial commit
Diffstat (limited to 'script.js')
-rw-r--r--script.js583
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;
+ });
+})();