From 0a93c96bb7ea429f5cbe3b436c0fc4efa594500c Mon Sep 17 00:00:00 2001 From: Daniel Weipert Date: Tue, 15 Aug 2023 22:48:11 +0200 Subject: status effects --- script.js | 245 +++++++++++++++++++++++++++++++++++++++++++++++++++++++------- style.css | 23 +++++- 2 files changed, 237 insertions(+), 31 deletions(-) diff --git a/script.js b/script.js index 9eecc44..d66f2bb 100644 --- a/script.js +++ b/script.js @@ -5,6 +5,7 @@ const DB = { shapes: {}, elements: {}, techniques: {}, + statusEffects: {}, }; const ElementType = { @@ -81,6 +82,40 @@ const StatusType = { dodge: 'dodge', speed: 'speed', }; +const StatusEffectType = { + blinded: 'blinded', + burn: 'burn', + chargedup: 'chargedup', + charging: 'charging', + confused: 'confused', + diehard: 'diehard', + dozing: 'dozing', + elementalshield: 'elementalshield', + eliminated: 'eliminated', + enraged: 'enraged', + exhausted: 'exhausted', + faint: 'faint', + feedback: 'feedback', + festering: 'festering', + flinching: 'flinching', + focused: 'focused', + grabbed: 'grabbed', + hardshell: 'hardshell', + harpooned: 'harpooned', + lifeleech: 'lifeleech', + lockdown: 'lockdown', + noddingoff: 'noddingoff', + poison: 'poison', + prickly: 'prickly', + recover: 'recover', + slow: 'slow', + sniping: 'sniping', + softened: 'softened', + stuck: 'stuck', + tired: 'tired', + wasting: 'wasting', + wild: 'wild', +}; class State { money = 0; @@ -100,8 +135,10 @@ class State { class Monster { #level = 1; + #status = null; + #hp = 0; + exp = 1; - hp = 0; tasteWarm = TasteWarm.tasteless; tasteCold = TasteCold.tasteless; @@ -111,7 +148,7 @@ class Monster { heldItem = null; statusEffect = ''; - statusModifiers = { + statusModifier = { hp: 0, melee: 0, armour: 0, @@ -176,6 +213,14 @@ class Monster { } } + get hp () { + return this.#hp; + } + + set hp (hp) { + this.#hp = Math.max(0, Math.min(hp, this.status.hp)); + } + get name () { return slugToName(this.slug); } @@ -219,6 +264,33 @@ class Monster { this.hp = statusPostEvolve.hp - (statusPreEvolve.hp - hpPreEvolve); } + async useTechnique(technique, enemy) { + const additionalEffects = technique.effects + .filter((effect) => !['damage', 'splash', 'area'].includes(effect)) + .map((effect) => new TechniqueEffect(effect)); + + for (const effect of additionalEffects) { + let recipient; + + if (effect.recipient === 'user') { + recipient = this; + } else { + recipient = enemy; + } + + if (effect.type === 'status') { + if (effect.application === 'give') { + recipient.statusEffect = await fetchStatusEffect(effect.effect); + } + else if (effect.application === 'remove') { + if (recipient.statusEffect.slug === effect.effect) { + recipient.statusEffect = ''; + } + } + } + } + } + getTasteStatusModifier (statusName, baseStatus) { let positive = 0; let negative = 0; @@ -257,13 +329,17 @@ class Monster { } get status () { + if (this.#status) { + return this.#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; + let hp = (this.shape.hp * multiplier) + this.statusModifier.hp; + let melee = (this.shape.melee * multiplier) + this.statusModifier.melee; + let armour = (this.shape.armour * multiplier) + this.statusModifier.armour; + let ranged = (this.shape.ranged * multiplier) + this.statusModifier.ranged; + let dodge = (this.shape.dodge * multiplier) + this.statusModifier.dodge; + let speed = (this.shape.speed * multiplier) + this.statusModifier.speed; // Tastes melee += this.getTasteStatusModifier('melee', melee); @@ -272,7 +348,7 @@ class Monster { dodge += this.getTasteStatusModifier('dodge', melee); speed += this.getTasteStatusModifier('speed', melee); - return { + this.#status = { hp, melee, armour, @@ -280,6 +356,17 @@ class Monster { dodge, speed, }; + + return this.#status; + } + + set status (status) { + this.#status = status; + } + + resetStatus () { + this.#status = null; + this.status; } }; @@ -307,10 +394,48 @@ class Technique { get sfx () { return DB.techniques[this.slug].sfx; } + + get effects () { + return DB.techniques[this.slug].effects; + } } class Item {} +class TechniqueEffect { + constructor (str) { + this.recipient = str.split(',')[1]; + this.application = str.split(' ')[0]; + this.type = str.split(',')[0].split(' ')[1].split('_')[0]; + this.effect = str.split(',')[0].split(' ')[1].split('_')[1]; + } +} + +class StatusEffect { + constructor (slug) { + this.slug = slug; + } + + get effects () { + return DB.statusEffects[this.slug].effects; + } + + get name () { + return slugToName(this.slug); + } + + get status () { + const status = {}; + + const statusChangeKeys = Object.keys(DB.statusEffects[this.slug]).filter((key) => key.startsWith('stat')); + for (const key of statusChangeKeys) { + status[key.replace('stat', '')] = DB.statusEffects[this.slug][key]; + } + + return status; + } +} + function simpleDamageMultiplier (techniqueTypes, targetTypes) { let multiplier = 1; @@ -360,7 +485,7 @@ function simpleDamageCalculation (technique, user, target) { const moveStrength = technique.power * multiplier; const damage = Math.floor((userStrength * moveStrength) / targetResist); - return damage; + return Math.max(damage, 1); } function calculateAwardedExperience (playerMonster, enemyMonster) { @@ -386,6 +511,13 @@ async function fetchTechnique (slug) { return new Technique(slug); } +async function fetchStatusEffect (slug) { + if (! DB.statusEffects[slug]) { + DB.statusEffects[slug] = await fetch(`/modules/tuxemon/mods/tuxemon/db/technique/status_${slug}.json`).then((response) => response.json()); + } + + return new StatusEffect(slug); +} function standardizeColor (color) { var ctx = document.createElement('canvas').getContext('2d'); @@ -433,6 +565,7 @@ function slugToName (slug) { await fetchMonster('lunight'), await fetchMonster('prophetoise'), await fetchMonster('drashimi'), + await fetchMonster('glombroc'), ]; state.activeMonster = state.partyMonsters[0]; state.activeTechnique = await fetchTechnique(state.activeMonster.moveset[0].technique); @@ -453,6 +586,12 @@ function slugToName (slug) { const UI = { activeMonster: null, + enemyImg: null, + damageHighlightClickDuration: 0.1, + damageHighlightClickTimeout: null, + imgClickDuration: 0.1, + damageClickTimeout: null, + damageAnimationIsRunning: false, damageAnimationNumber: 0, @@ -509,15 +648,12 @@ function slugToName (slug) { 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); + this.enemyImg.classList.add('damaged'); + clearTimeout(this.damageHighlightClickTimeout); + this.damageHighlightClickTimeout = setTimeout(() => this.enemyImg.classList.remove('damaged'), this.damageHighlightClickDuration * 1000); var enemyAnimation = battleEnemy.querySelector('.battle__monster-sprite__animation'); - if (!this.damageAnimationIsRunning && state.activeTechnique.animation) { + if (!this.damageAnimationIsRunning && state.activeTechnique.animation && DB.allAnimations[state.activeTechnique.animation]) { this.damageAnimationIsRunning = true; const damageAnimationLoop = () => { @@ -528,7 +664,7 @@ function slugToName (slug) { this.damageAnimationNumber++; - if (this.damageAnimationNumber === DB.allAnimations[state.activeTechnique.animation].length) { + if (this.damageAnimationNumber >= DB.allAnimations[state.activeTechnique.animation].length) { this.damageAnimationIsRunning = false; this.damageAnimationNumber = 0; enemyAnimation.src = ''; @@ -555,7 +691,7 @@ function slugToName (slug) { 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; - battleMonster.querySelector('.battle__monster-info__status').innerHTML = UI.createStatusIcon(monster.statusEffect); + battleMonster.querySelector('.battle__monster-info__status').innerHTML = UI.createStatusEffectIcon(monster.statusEffect); battleMonster.querySelector('.battle__monster-img').src = `/modules/tuxemon/mods/tuxemon/gfx/sprites/battle/${monster.slug}-front.png`; UI.setHp(monster, battleMonster.querySelector('.hp')); @@ -572,7 +708,17 @@ function slugToName (slug) { battlePlayer.appendChild(battleMonster); } else { battleMonster.classList.add('battle__monster--enemy'); - battleEnemy.querySelector('.battle__monster') && battleEnemy.removeChild(battleEnemy.querySelector('.battle__monster')); + + this.enemyImg = battleMonster.querySelector('.battle__monster-img'); + this.enemyImg.style.transitionDuration = this.damageHighlightClickDuration + 's'; + + const previousBattleMonster = battleEnemy.querySelector('.battle__monster'); + if (previousBattleMonster) { + this.enemyImg.classList = previousBattleMonster.querySelector('.battle__monster-img').classList; + + battleEnemy.removeChild(previousBattleMonster); + } + battleEnemy.appendChild(battleMonster); } }, @@ -581,7 +727,11 @@ function slugToName (slug) { UI.setBattleMonster(state.enemy.monster, 'enemy'); }, - async setActiveMonster () { + setActiveMonster () { + UI.setBattleMonster(state.activeMonster, 'player'); + }, + + async setActiveTechnique () { let activeMoveIndex = 0; while ((await fetchTechnique(state.activeMonster.moveset[activeMoveIndex].technique)).power === 0) { activeMoveIndex++; @@ -647,12 +797,12 @@ function slugToName (slug) { return img.outerHTML; }, - createStatusIcon (status) { - if (!status) return ''; + createStatusEffectIcon (statusEffect) { + if (!statusEffect) return ''; var img = document.createElement('img'); - img.src = `/modules/tuxemon/mods/tuxemon/gfx/ui/icons/status/icon_${status}.png`; - img.title = slugToName(status); + img.src = `/modules/tuxemon/mods/tuxemon/gfx/ui/icons/status/icon_${statusEffect.slug}.png`; + img.title = statusEffect.name; return img.outerHTML; }, @@ -682,6 +832,7 @@ function slugToName (slug) { } state.activeMonster = state.partyMonsters[Array.prototype.indexOf.call(document.querySelector('#party').children, target)]; + UI.setActiveTechnique(); UI.setActiveMonster(); popup.remove(); @@ -707,14 +858,50 @@ function slugToName (slug) { }, }; + const Game = { + applyEffect (monster) { + if (!monster.statusEffect) { + return; + } + + if (monster.statusEffect.slug === 'poison' || monster.statusEffect.slug === 'burn') { + const statusEffectDamage = Math.floor(monster.status.hp / 8); + UI.createDamage(clickEvent, statusEffectDamage); + monster.hp -= statusEffectDamage; + } + + if (monster.statusEffect.slug === 'recover') { + const statusEffectHeal = Math.floor(monster.status.hp / 16); + monster.hp += statusEffectHeal; + } + + if (monster.statusEffect.effects.includes('statchange')) { + monster.resetStatus(); + + for (const statusType in monster.statusEffect.status) { + const statusChange = monster.statusEffect.status[statusType]; + + monster.status[statusType] = Math.floor(eval(`${monster.status[statusType]} ${statusChange.operation} ${statusChange.value}`)); + } + } + }, + }; + UI.setActiveMonster(); UI.setEnemyMonster(); - var clickTimeout; + let clickEvent; document.querySelector('#battle__enemy').addEventListener('click', async (event) => { + clickEvent = event; + + Game.applyEffect(state.activeMonster); + Game.applyEffect(state.enemy.monster); + const damage = simpleDamageCalculation(state.activeTechnique, state.activeMonster, state.enemy.monster); UI.createDamage(event, damage); + await state.activeMonster.useTechnique(state.activeTechnique, state.enemy.monster); + state.enemy.monster.hp -= damage; if (state.enemy.monster.hp <= 0) { const faintedMonster = state.enemy.monster; @@ -728,10 +915,14 @@ function slugToName (slug) { if (state.activeMonster.canEvolve()) { await fetchMonster(state.activeMonster.evolutions[0].monster_slug); state.activeMonster.evolve(); - UI.setActiveMonster(); } } + + UI.setActiveMonster(); + UI.setEnemyMonster(); + UI.setHp(state.enemy.monster, battleEnemy); + money.textContent = state.money; }); diff --git a/style.css b/style.css index c0bf5f4..5c4b2f6 100644 --- a/style.css +++ b/style.css @@ -86,8 +86,13 @@ img { /* align-items: center; */ } .battle__monster-info__status { - flex-basis: 5%; - text-align: center; + /* flex-basis: 5%; */ + flex-basis: calc(1rem + 0.25rem); + /* text-align: center; */ +} +.battle__monster-info__status img { + width: 1rem; + height: 1rem; } .battle__monster-info-exp { @@ -111,10 +116,10 @@ img { .battle__monster-sprite { margin-bottom: 0.25rem; } -.battle__monster-sprite img { +.battle__monster-img { transition-property: filter; } -.battle__monster-sprite img.damaged { +.battle__monster-img.damaged { filter: brightness(2); } .battle__monster-sprite__animation { @@ -129,6 +134,8 @@ img { display: inline-flex; align-items: center; + + cursor: pointer; } .battle__monster-technique__name {} .battle__monster-technique__types {} @@ -190,10 +197,18 @@ img { } +#status { + color: #fff; + background-color: #000; + padding: 0.25rem; +} + + #menu { display: grid; grid-template-columns: 1fr 1fr 1fr; text-align: center; + background-color: beige; } #menu > div { cursor: pointer; -- cgit v1.2.3