diff options
author | Daniel Weipert <code@drogueronin.de> | 2023-08-17 02:53:14 +0200 |
---|---|---|
committer | Daniel Weipert <code@drogueronin.de> | 2023-08-17 17:42:15 +0200 |
commit | cc685bfe02b42b592987117fa008a4461785f53c (patch) | |
tree | 625c1c9573b178e574bb70cac042c35da4036cf1 | |
parent | 717fde1c48c7221da986ac02d2b806b2fee6f2d5 (diff) |
refactorrefactor
-rw-r--r-- | index.html | 122 | ||||
-rw-r--r-- | resources/css/battle.css (renamed from style.css) | 140 | ||||
-rw-r--r-- | resources/css/menu.css | 70 | ||||
-rw-r--r-- | resources/css/page.css | 24 | ||||
-rw-r--r-- | resources/css/variables.css | 3 | ||||
-rw-r--r-- | resources/js/classes/Item.js | 1 | ||||
-rw-r--r-- | resources/js/classes/Monster.js | 223 | ||||
-rw-r--r-- | resources/js/classes/State.js | 38 | ||||
-rw-r--r-- | resources/js/classes/StatusEffect.js | 55 | ||||
-rw-r--r-- | resources/js/classes/Technique.js | 77 | ||||
-rw-r--r-- | resources/js/db.js | 62 | ||||
-rw-r--r-- | resources/js/definitions.js | 136 | ||||
-rw-r--r-- | resources/js/formula.js | 93 | ||||
-rw-r--r-- | resources/js/game.js | 459 | ||||
-rw-r--r-- | resources/js/helpers.js | 45 | ||||
-rw-r--r-- | resources/js/ui.js | 643 | ||||
-rw-r--r-- | script.js | 1152 |
17 files changed, 2051 insertions, 1292 deletions
@@ -3,8 +3,12 @@ <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> - <link rel="stylesheet" href="style.css" /> + <link rel="stylesheet" href="/resources/css/variables.css" /> + <link rel="stylesheet" href="/resources/css/page.css" /> + <link rel="stylesheet" href="/resources/css/battle.css" /> + <link rel="stylesheet" href="/resources/css/menu.css" /> </head> + <body> <div class="wrap"> <div id="battle"> @@ -16,13 +20,20 @@ </div> <div id="techniques"></div> + <template id="tpl___techniques"> + <div></div> + </template> <template id="tpl___technique"> <div class="techniques__technique"> - <div class="techniques__technique__name"></div> - <div class="techniques__technique__types"></div> - <div class="techniques__technique__power"></div> - <div class="techniques__technique__accuracy"></div> - <div class="techniques__technique__range"></div> + <div> + <div data-template-slot="name"></div> + <div data-template-slot="types"></div> + </div> + <div> + <div data-template-slot="power"></div> + <div data-template-slot="accuracy"></div> + <div data-template-slot="range"></div> + </div> </div> </template> @@ -42,13 +53,31 @@ <div id="menu__catch"> <img src="/modules/tuxemon/mods/tuxemon/gfx/items/tuxeball.png" title="Catch" width="64" height="64" /> </div> + + <div id="menu__journal"> + <img src="/modules/tuxemon/mods/tuxemon/gfx/ui/menu/journal.png" title="Journal" width="64" height="64" /> + </div> </div> </div> + + <!-- Templates --> + + <template id="tpl___party"> + <div class="party"> + <div data-template-slot="monsters" class="party__monsters"></div> + <div data-template-slot="modes" class="party__selection-modes"> + <button data-template-slot="modeSelect" data-party-selection-mode="select">Select</button> + <button data-template-slot="modeStats" data-party-selection-mode="stats">Stats</button> + <button data-template-slot="modeTechniques" data-party-selection-mode="techniques">Techniques</button> + </div> + </div> + </template> + <template id="tpl___party__monster"> <div class="party__monster"> <div><div class="monster__level"></div></div> - <img class="monster__img" src="" /> + <img data-template-slot="sprite" class="monster__img" src="" /> <div class="monster__exp"> <div class="monster__exp-bar"></div> </div> @@ -66,43 +95,50 @@ <div class="battle__monster-info"> <div class="battle__monster-info-box"> <div> - <span class="battle__monster-info__name">{NAME}</span> - <span class="battle__monster-info__gender">{GENDER}</span> - Lv. <span class="battle__monster-info__level">{LEVEL}</span> + <span data-template-slot="name" class="battle__monster-info__name"></span> + <span data-template-slot="gender" class="battle__monster-info__gender"></span> + Lv. <span data-template-slot="level" class="battle__monster-info__level"></span> </div> <div class="battle__monster-info-box__status"> - <div class="battle__monster-info__status"></div> - <div class="hp"> - <div class="hp-bar-wrap"> - <div class="hp-bar"></div> - </div> - <div class="hp-text"></div> - </div> + <div data-template-slot="statusEffect" class="battle__monster-info__status"></div> + <div data-template-slot="hpBar"></div> </div> </div> <div class="battle__monster-info-exp"> <div class="exp-label">XP</div> - <div class="exp"> - <div class="exp-bar-wrap"> - <div class="exp-bar"></div> - </div> - <div class="exp-text"></div> - </div> + <div data-template-slot="expBar"></div> </div> </div> <div class="battle__monster-visual"> <div class="battle__monster-sprite"> - <img class="battle__monster-img" src="" draggable="false" /> - </div> - <div class="battle__monster-technique"> - <div class="battle__monster-technique__name"></div> - <div class="battle__monster-technique__types"></div> - <div class="battle__monster-technique__power"></div> + <img data-template-slot="sprite" class="battle__monster-img" src="" draggable="false" /> </div> </div> </div> </template> + <template id="tpl___battle__hp-bar"> + <div class="hp-bar"> + <div class="hp-bar__bar-wrap"> + <div data-template-slot="bar" class="hp-bar__bar"></div> + </div> + <div data-template-slot="text" class="hp-bar__text"></div> + </div> + </template> + + <template id="tpl___battle__exp-bar"> + <div class="exp-bar"> + <div class="exp-bar__bar-wrap"> + <div data-template-slot="bar" class="exp-bar__bar"></div> + </div> + <div data-template-slot="text" class="exp-bar__text"></div> + </div> + </template> + + <template id="tpl___battle__damage"> + <div data-template-slot="text" class="damage"></div> + </template> + <template id="tpl___popup"> <div class="popup__overlay"> <div class="popup"></div> @@ -114,13 +150,31 @@ </template> <template id="tpl___moveset__item"> <div class="moveset__item"> - <span class="moveset__item__name"></span> - <span class="moveset__item__type"></span> - <span class="moveset__item__power"></span> - <span class="moveset__item__level"></span> + <span data-template-slot="name" class="moveset__item__name"></span> + <span data-template-slot="types" class="moveset__item__type"></span> + <span data-template-slot="power" class="moveset__item__power"></span> + <span data-template-slot="level" class="moveset__item__level"></span> </div> </template> - <script type="text/javascript" src="script.js"></script> + <template id="tpl___menu__journal"> + <div> + <button data-template-slot="save">Save</button> + <button data-template-slot="load">Load</button> + </div> + </template> + + + <script type="text/javascript" src="/resources/js/definitions.js"></script> + <script type="text/javascript" src="/resources/js/helpers.js"></script> + <script type="text/javascript" src="/resources/js/classes/Monster.js"></script> + <script type="text/javascript" src="/resources/js/classes/Technique.js"></script> + <script type="text/javascript" src="/resources/js/classes/StatusEffect.js"></script> + <script type="text/javascript" src="/resources/js/classes/Item.js"></script> + <script type="text/javascript" src="/resources/js/classes/State.js"></script> + <script type="text/javascript" src="/resources/js/db.js"></script> + <script type="text/javascript" src="/resources/js/formula.js"></script> + <script type="text/javascript" src="/resources/js/ui.js"></script> + <script type="text/javascript" src="/resources/js/game.js"></script> </body> </html> diff --git a/style.css b/resources/css/battle.css index b344c8a..0f56234 100644 --- a/style.css +++ b/resources/css/battle.css @@ -1,48 +1,3 @@ -* { - box-sizing: border-box; -} - -html { - font-size: 14px; -} - -body { - margin: 0; -} - -img { - display: inline-block; - max-width: 100%; -} - -.wrap { - margin: 0 auto; - min-width: 360px; - max-width: 750px; - width: 100vw; - height: 100vh; -} - -.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: 0.25rem 0.5rem; - - background-image: url('/modules/tuxemon/mods/tuxemon/gfx/ui/background/spyder_empty.png'); - background-size: cover; - background-position: center; -} - #battle { user-select: none; min-height: 300px; @@ -153,34 +108,40 @@ img { display: none; } -.exp { +.exp-bar { width: 100%; } -.exp-bar-wrap { +.exp-bar__bar-wrap { width: 100%; border: 1px solid rgba(0, 0, 0, 0.5); background-color: lightgray; } -.exp-bar { +.exp-bar__bar { background-color: blue; height: 7px; transition: background-color; width: 0%; } -.hp { + +.hp-bar { flex-grow: 1; } -.hp-bar-wrap { +.hp-bar__bar-wrap { width: 100%; border: 1px solid rgba(0, 0, 0, 0.5); } -.hp-bar { +.hp-bar__bar { background-color: green; height: 10px; transition: background-color; } + +.moveset__list { + padding: 0 0.5rem; +} + .moveset__item { font-size: 1.5rem; border: 1px solid #000; @@ -188,7 +149,10 @@ img { margin: 0.5em 0; line-height: 1em; cursor: pointer; - background-color: rgba(255, 255, 255, 0.75); + background-color: rgba(255, 255, 255, 0.5); + + /* display: flex; */ + /* align-items: center; */ } .moveset__item:hover { @@ -201,8 +165,9 @@ img { } .moveset__item[selected] { - border-color: green; - color: green; + border-color: var(--color-success); + color: var(--color-success); + background-color: rgba(255, 255, 255, 0.75); } .moveset__item img { @@ -212,74 +177,37 @@ img { + #techniques { display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; -} -.techniques__technique {} - - - -#status { - color: #fff; - background-color: #000; - padding: 0.25rem; + background: beige; } - - -#menu { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - text-align: center; - background-color: beige; -} -#menu > div { +.techniques__technique { + user-select: none; + border: 1px solid #000; + padding: 0.25rem; cursor: pointer; } -#menu > div:hover { - background-color: rgba(0, 0, 0, 0.05); -} - - -#party { - display: grid; - grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr; -} - -.party__monster { - cursor: pointer; -} -.party__monster:hover { - background-color: rgba(0, 0, 0, 0.1); +.techniques__technique:hover, +.techniques__technique:active { + background-color: rgba(0, 0, 0, 0.05); } - -#enemy { - position: relative; - user-select: none; - cursor: pointer; - min-width: 750px; - min-height: 300px; +.techniques__technique > div { display: flex; - justify-content: center; + justify-content: space-between; 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; +.techniques__technique > div + div { + margin-top: 0.25rem; } -#enemy img.clicked { - filter: brightness(2); +.techniques__technique [data-template-slot="name"] { + font-size: 1.5rem; } diff --git a/resources/css/menu.css b/resources/css/menu.css new file mode 100644 index 0000000..41353b2 --- /dev/null +++ b/resources/css/menu.css @@ -0,0 +1,70 @@ +.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__overlay.popup--is-multiple { + background-color: rgba(0, 0, 0, 0.25); +} + +.popup { + background-image: url('/modules/tuxemon/mods/tuxemon/gfx/ui/background/spyder_empty.png'); + background-size: cover; + background-position: center; +} + + + + +#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; +} +#menu > div:hover { + background-color: rgba(0, 0, 0, 0.05); +} + + + +.party__monsters { + display: grid; + grid-template-columns: 1fr 1fr 1fr; +} + +.party__monster { + cursor: pointer; + padding: 1rem; +} +.party__monster:hover { + background-color: rgba(0, 0, 0, 0.1); +} + +.party__selection-modes { + display: flex; + justify-content: space-between; + padding: 1rem; +} + +.party__selection-modes button[selected] { + border-color: var(--color-success); +} diff --git a/resources/css/page.css b/resources/css/page.css new file mode 100644 index 0000000..96cefdf --- /dev/null +++ b/resources/css/page.css @@ -0,0 +1,24 @@ +* { + box-sizing: border-box; +} + +html { + font-size: 14px; +} + +body { + margin: 0; +} + +img { + display: inline-block; + max-width: 100%; +} + +.wrap { + margin: 0 auto; + min-width: 360px; + max-width: 750px; + width: 100vw; + height: 100vh; +} diff --git a/resources/css/variables.css b/resources/css/variables.css new file mode 100644 index 0000000..d5f11be --- /dev/null +++ b/resources/css/variables.css @@ -0,0 +1,3 @@ +:root { + --color-success: darkgreen; +} diff --git a/resources/js/classes/Item.js b/resources/js/classes/Item.js new file mode 100644 index 0000000..e274a20 --- /dev/null +++ b/resources/js/classes/Item.js @@ -0,0 +1 @@ +class Item {} diff --git a/resources/js/classes/Monster.js b/resources/js/classes/Monster.js new file mode 100644 index 0000000..9023f32 --- /dev/null +++ b/resources/js/classes/Monster.js @@ -0,0 +1,223 @@ +class Monster { + #level = 2; + #hp = 0; + + exp = 1; + + tasteWarm = TasteWarm.tasteless; + tasteCold = TasteCold.tasteless; + + gender = ''; + + heldItem = null; + + /** + * @type {StatusEffect} + */ + statusEffect = null; + + statModifiers = { + hp: 0, + melee: 0, + armour: 0, + ranged: 0, + dodge: 0, + speed: 0, + }; + + experienceModifier = 1; + moneyModifier = 1; + + /** + * @type {Technique[]} + */ + activeTechniques = []; + + 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.stats.hp; + + const possibleGenders = DB.monsters[this.slug].possible_genders; + this.gender = possibleGenders[Math.floor(Math.random() * possibleGenders.length)]; + + } + + async initialize () { + for (const move of this.getLearnableTechniques()) { + this.activeTechniques.push(await fetchTechnique(move.technique)); + } + } + + 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 statsPreLevelUp = this.stats; + const hpPreLevelUp = this.hp; + + this.#level = level; + + const statsPostLevelUp = this.stats; + + this.hp = statsPostLevelUp.hp - (statsPreLevelUp.hp - hpPreLevelUp); + + if (this.exp < this.getExperienceRequired(-1)) { + this.exp = this.getExperienceRequired(-1); + } + } + + get hp () { + return this.#hp; + } + + set hp (hp) { + this.#hp = Math.max(0, Math.min(hp, this.stats.hp)); + } + + get name () { + return slugToName(this.slug); + } + + getLearnableTechniques () { + return this.moveset.filter((move) => this.level >= move.level_learned); + } + + 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)); + return this.evolutions.filter((evolution) => evolution.path === 'standard' && this.level >= evolution.at_level); + } + + canEvolve () { + return this.getPossibleEvolutions().length > 0; + } + + evolve () { + const evolution = this.getPossibleEvolutions()[0]; + + const statsPreEvolve = this.stats; + const hpPreEvolve = this.hp; + + this.slug = evolution.monster_slug; + + const statsPostEvolve = this.stats; + + this.hp = statsPostEvolve.hp - (statsPreEvolve.hp - hpPreEvolve); + } + + getTasteStatModifier (statType, baseStat) { + let positive = 0; + let negative = 0; + + let isPositive = false; + let isNegative = false; + if (statType === StatType.melee) { + isPositive = this.tasteWarm === TasteWarm.salty; + isNegative = this.tasteCold === TasteCold.sweet; + } + else if (statType === StatType.armour) { + isPositive = this.tasteWarm === TasteWarm.hearty; + isNegative = this.tasteCold === TasteCold.soft; + } + else if (statType === StatType.ranged) { + isPositive = this.tasteWarm === TasteWarm.zesty; + isNegative = this.tasteCold === TasteCold.flakey; + } + else if (statType === StatType.dodge) { + isPositive = this.tasteWarm === TasteWarm.refined; + isNegative = this.tasteCold === TasteCold.dry; + } + else if (statType === StatType.speed) { + isPositive = this.tasteWarm === TasteWarm.peppy; + isNegative = this.tasteCold === TasteCold.mild; + } + + if (isPositive) { + positive = baseStat * 10 / 100; + } + if (isNegative) { + negative = baseStat * 10 / 100; + } + + return Math.floor(positive) - Math.floor(negative); + } + + get stats () { + const multiplier = this.level + 7; + let hp = (this.shape.hp * multiplier) + this.statModifiers.hp; + let melee = (this.shape.melee * multiplier) + this.statModifiers.melee; + let armour = (this.shape.armour * multiplier) + this.statModifiers.armour; + let ranged = (this.shape.ranged * multiplier) + this.statModifiers.ranged; + let dodge = (this.shape.dodge * multiplier) + this.statModifiers.dodge; + let speed = (this.shape.speed * multiplier) + this.statModifiers.speed; + + // Tastes + melee += this.getTasteStatModifier(StatType.melee, melee); + armour += this.getTasteStatModifier(StatType.armour, armour); + ranged += this.getTasteStatModifier(StatType.ranged, ranged); + dodge += this.getTasteStatModifier(StatType.dodge, dodge); + speed += this.getTasteStatModifier(StatType.speed, speed); + + return { + hp: hp, + [StatType.melee]: melee, + [StatType.armour]: armour, + [StatType.ranged]: ranged, + [StatType.dodge]: dodge, + [StatType.speed]: speed, + }; + } + + setStatModifier (statType, newAbsoluteValue) { + this.statModifiers[statType] = newAbsoluteValue - this.stats[statType]; + } + + resetStatModifiers () { + for (const m in this.statModifiers) { + this.statModifiers[m] = 0; + } + } +}; diff --git a/resources/js/classes/State.js b/resources/js/classes/State.js new file mode 100644 index 0000000..2384a85 --- /dev/null +++ b/resources/js/classes/State.js @@ -0,0 +1,38 @@ +class State { + /** + * @type {number} + */ + money = 0; + + /** + * @type {Monster[]} + */ + monsters = []; + + /** + * @type {Item[]} + */ + inventory = []; + + /** + * @type {Monster[]} + */ + partyMonsters = []; + + /** + * @type {Monster} + */ + activeMonster = null; + + /** + * @type {Technique} + */ + activeTechnique = null; + + enemy = { + /** + * @type {Monster} + */ + monster: null, + }; +}; diff --git a/resources/js/classes/StatusEffect.js b/resources/js/classes/StatusEffect.js new file mode 100644 index 0000000..ac6ae54 --- /dev/null +++ b/resources/js/classes/StatusEffect.js @@ -0,0 +1,55 @@ +class StatusEffect { + turnsLeft = 0; + onRemove = null; + + /** + * @type {Monster} + */ + issuer = null; + + constructor (slug) { + this.slug = slug; + + if (['recover', 'lifeleech'].includes(this.slug)) { + this.turnsLeft = 1; + } + else if (['charging'].includes(this.slug)) { + this.turnsLeft = 2; + } + else if (this.category === 'positive') { + this.turnsLeft = Math.ceil(Math.random() * 6) + 4; + } + else if (this.category === 'negative') { + this.turnsLeft = Math.ceil(Math.random() * 3) + 2; + } + else { + this.turnsLeft = Math.ceil(Math.random() * 3) + 2; + } + } + + /** + * @returns {string[]} + */ + get effects () { + return DB.statusEffects[this.slug].effects; + } + + get category () { + return DB.statusEffects[this.slug].category; + } + + get name () { + return slugToName(this.slug); + } + + get stats () { + const stats = {}; + + const statsChangeKeys = Object.keys(DB.statusEffects[this.slug]).filter((key) => key.startsWith('stat')); + for (const statChangeKey of statsChangeKeys) { + stats[statChangeKey.replace('stat', '')] = DB.statusEffects[this.slug][statChangeKey]; + } + + return stats; + } +} diff --git a/resources/js/classes/Technique.js b/resources/js/classes/Technique.js new file mode 100644 index 0000000..a24e094 --- /dev/null +++ b/resources/js/classes/Technique.js @@ -0,0 +1,77 @@ +class Technique { + #accuracy = 0; + #potency = 0; + #power = 0; + + constructor (slug) { + this.slug = slug; + + this.resetStats(); + } + + get name () { + return slugToName(this.slug); + } + + get types () { + return DB.techniques[this.slug].types; + } + + get range () { + return DB.techniques[this.slug].range; + } + + get animation () { + return DB.techniques[this.slug].animation; + } + + get sfx () { + return DB.techniques[this.slug].sfx; + } + + /** + * @returns {string[]} + */ + get effects () { + return DB.techniques[this.slug].effects; + } + + get accuracy () { + return this.#accuracy; + } + set accuracy (accuracy) { + this.#accuracy = accuracy; + } + + get potency () { + return this.#potency; + } + set potency (potency) { + this.#potency = potency; + } + + get power () { + return this.#power; + } + set power (power) { + this.#power = power; + } + + get stats () { + const accuracy = DB.techniques[this.slug].accuracy; + const potency = DB.techniques[this.slug].potency; + const power = DB.techniques[this.slug].power; + + return { + accuracy, + potency, + power, + }; + } + + resetStats () { + this.accuracy = this.stats.accuracy; + this.potency = this.stats.potency; + this.power = this.stats.power; + } +} diff --git a/resources/js/db.js b/resources/js/db.js new file mode 100644 index 0000000..96b971a --- /dev/null +++ b/resources/js/db.js @@ -0,0 +1,62 @@ +const DB = { + allMonsters: [], + allAnimations: {}, + monsters: {}, + shapes: {}, + elements: {}, + techniques: {}, + statusEffects: {}, +}; + +async function initializeDB () { + 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()); + } +} + +/** + * @param {MonsterSlug} slug + * + * @returns {Promise<Monster>} + */ +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()); + } + + const monster = new Monster(slug); + await monster.initialize(); + + return monster; +} + +/** + * @param {TechniqueSlug} slug + * + * @returns {Promise<Technique>} + */ +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); +} + +/** + * @param {string} slug + * + * @returns {Promise<StatusEffect>} + */ +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); +} diff --git a/resources/js/definitions.js b/resources/js/definitions.js new file mode 100644 index 0000000..b8c5071 --- /dev/null +++ b/resources/js/definitions.js @@ -0,0 +1,136 @@ +/** + * @typedef {string} MonsterSlug + * @typedef {string} TechniqueSlug + */ + +// + +const ElementType = { + aether: 'aether', + wood: 'wood', + fire: 'fire', + earth: 'earth', + metal: 'metal', + water: 'water', + + /* lightning: 'lightning', + frost: 'frost', + venom: 'venom', + //vermin: 'vermin', + cosmic: 'cosmic', + battle: 'battle', + psionic: 'psionic', + darkness: 'darkness', + heaven: 'heaven', + + combineTypes(typeA, typeB) { + if (typeA === ElementType.earth & typeB === ElementType.fire) { + return ElementType.lightning; + } + if (typeA === ElementType.earth & typeB === ElementType.water) { + return ElementType.frost; + } + if (typeA === ElementType.earth & typeB === ElementType.wood) { + return ElementType.venom; + } + // if (typeA === ElementType.earth & typeB === ElementType.metal) { + // return ElementType.vermin; + // } + if (typeA === ElementType.fire && typeB === ElementType.water) { + return ElementType.cosmic; + } + } */ +}; + +const ElementTypeColor = { + [ElementType.aether]: 'rgba(255, 255, 255, 1)', + [ElementType.wood]: '#3ca6a6', + [ElementType.fire]: '#ca3c3c', + [ElementType.earth]: '#eac93c', + [ElementType.metal]: '#e4e4e4', + [ElementType.water]: '#3c3c3c', +}; + + +/** + * @readonly + * @enum {string} + */ +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 StatType = { + // hp: 'hp', + melee: 'melee', + armour: 'armour', + ranged: 'ranged', + 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', +}; + +const StatusEffectTypeColor = { + [StatusEffectType.burn]: 'red', + [StatusEffectType.poison]: 'purple', + [StatusEffectType.recover]: 'white', +}; diff --git a/resources/js/formula.js b/resources/js/formula.js new file mode 100644 index 0000000..5a3433f --- /dev/null +++ b/resources/js/formula.js @@ -0,0 +1,93 @@ +/** + * @param {Technique} technique + * @param {Monster} user + * @param {Monster} target + * + * @returns {number} + */ +function simpleDamageCalculation (technique, user, target) { + if (technique.power === 0) { + return 0; + } + + let userBaseStrength = user.level + 7; + let userStrength = 1; + let targetResist = 1; + + if (technique.range === TechniqueRange.melee) { + userStrength = userBaseStrength * user.stats.melee; + targetResist = target.stats.armour; + } + else if (technique.range === TechniqueRange.touch) { + userStrength = userBaseStrength * user.stats.melee; + targetResist = target.stats.dodge; + } + else if (technique.range === TechniqueRange.ranged) { + userStrength = userBaseStrength * user.stats.ranged; + targetResist = target.stats.dodge; + } + else if (technique.range === TechniqueRange.reach) { + userStrength = userBaseStrength * user.stats.ranged; + targetResist = target.stats.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 Math.max(damage, 1); +} + +/** + * @param {(ElementType[]|string[])} techniqueTypes + * @param {(ElementType[]|string[])} targetTypes + * + * @returns {number} + */ +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)); +} + +/** + * @param {Monster} opposingMonster + * @param {Monster[]} participants + * + * @returns {number[]} + */ +function calculateAwardedExperience (opposingMonster, participants) { + const awardedExperienceDistribution = []; + + for (const participantIndex in participants) { + const participant = participants[participantIndex]; + const awardedExperience = Math.max( + Math.floor( + opposingMonster.getExperienceRequired(-1) / opposingMonster.level * opposingMonster.experienceModifier / participant.level + ), + 1 + ); + + awardedExperienceDistribution[participantIndex] = awardedExperience; + } + + return awardedExperienceDistribution; +} diff --git a/resources/js/game.js b/resources/js/game.js new file mode 100644 index 0000000..1f1f92b --- /dev/null +++ b/resources/js/game.js @@ -0,0 +1,459 @@ +const state = new State(); + + +const Game = { + phases: { + preAction: [], + action: [], + postAction: [], + }, + + didTechniqueHit: false, + + + async progressTurn () { + await Game.applyStatusEffect(state.enemy.monster); + await Game.applyStatusEffect(state.activeMonster); + + for (const event of Game.phases.preAction) { + event(); + } + Game.phases.preAction = []; + for (const event of Game.phases.action) { + event(); + } + Game.phases.action = []; + for (const event of Game.phases.postAction) { + event(); + } + Game.phases.postAction = []; + + UI.drawEnemyMonster(); + UI.drawActiveMonster(); + UI.drawActiveTechniques(); + + UI.elements.money.textContent = state.money; + }, + + /** + * @param {Technique} technique + * @param {Monster} user + * @param {Monster} target + */ + async useTechnique (technique, user, target) { + if (!Game.didTechniqueHit) { + UI.drawDamageMiss(UI.createDamageMiss()); + return; + } + + if (state.activeMonster.hp === state.activeMonster.stats.hp) { + state.activeMonster.hp = 1; + } + + for (const techniqueEffect of technique.effects) { + + // damage + if (['damage', 'splash', 'area'].includes(techniqueEffect)) { + Game.phases.action.push(() => { + const damage = simpleDamageCalculation(state.activeTechnique, state.activeMonster, state.enemy.monster); + + state.enemy.monster.hp -= damage; + const damageNode = UI.createDamage(damage); + UI.applyMultiplierToDamage(damageNode, simpleDamageMultiplier(state.activeTechnique.types, state.enemy.monster.types)); + UI.applyTechniqueToDamage(damageNode, state.activeTechnique); + UI.drawDamage(damageNode); + }); + } + + else if (techniqueEffect === 'money') { + state.money += Math.floor(Math.random() * target.level); + } + + else if (techniqueEffect === 'enhance') { + UI.drawDamage(UI.createDamage('!!ENHANCE!!')); + } + + // status effect + else if (techniqueEffect.includes('status_')) { + const statusEffect_recipient = techniqueEffect.split(',')[1]; + const statusEffect_application = techniqueEffect.split(' ')[0]; + const statusEffect_type = techniqueEffect.split(',')[0].split(' ')[1].split('_')[0]; + const statusEffect_effect = techniqueEffect.split(',')[0].split(' ')[1].split('_')[1]; + + const statusEffect = await fetchStatusEffect(statusEffect_effect); + statusEffect.issuer = user; + + let recipient; + if (statusEffect_recipient === 'user') { + recipient = user; + } else { + recipient = target; + } + + Game.phases.postAction.push(() => { + // add status effect + const potency = Math.random(); + const success = technique.potency >= potency; + + if (success) { + // TODO: check replace + if (recipient.statusEffect) return; + + recipient.statusEffect = statusEffect; + } + }); + + UI.drawTechniqueAnimation(); + } + } + }, + + /** + * @param {Monster} monster + */ + async applyStatusEffect (monster) { + if (!monster.statusEffect) { + return; + } + + if (monster.statusEffect.turnsLeft === 0) { + Game.phases.preAction.push(() => { + monster.statusEffect.onRemove && monster.statusEffect.onRemove(); + + // if still 0 turns left after remove action + if (monster.statusEffect.turnsLeft === 0) { + monster.statusEffect = null; + } else { + Game.applyStatusEffect(monster); + } + }); + + return; + } + + // poison / burn + if (monster.statusEffect.slug === 'poison' || monster.statusEffect.slug === 'burn') { + const statusEffectDamage = Math.floor(monster.stats.hp / 8); + + Game.phases.postAction.push(() => { + monster.hp -= statusEffectDamage; + + const damageNode = UI.createDamage(statusEffectDamage); + UI.applyStatusEffectToDamage(damageNode, monster.statusEffect); + UI.drawDamage(damageNode); + }); + } + + // lifeleech + else if (monster.statusEffect.slug === 'lifeleech') { + const statusEffectLeech = Math.floor(monster.stats.hp / 16); + + Game.phases.postAction.push(() => { + monster.hp -= statusEffectLeech; + monster.statusEffect.issuer.hp += statusEffectLeech; + + const damageNode = UI.createDamage(statusEffectLeech); + UI.applyStatusEffectToDamage(damageNode, monster.statusEffect); + UI.drawDamage(damageNode); + }); + } + + // recover + else if (monster.statusEffect.slug === 'recover') { + const statusEffectHeal = Math.floor(monster.stats.hp / 16); + + Game.phases.postAction.push(() => { + monster.hp += statusEffectHeal; + + const damageNode = UI.createDamage(statusEffectHeal); + UI.applyStatusEffectToDamage(damageNode, monster.statusEffect); + UI.drawDamage(damageNode); + }); + } + + // stuck + else if (monster.statusEffect.slug === 'stuck') { + for (const technique of monster.activeTechniques) { + if ([TechniqueRange.melee, TechniqueRange.touch].includes(technique.range)) { + Game.phases.preAction.push(() => { + technique.potency = technique.stats.potency * 0.5; + technique.power = technique.stats.power * 0.5; + }); + + monster.statusEffect.onRemove = () => { + technique.resetStats(); + }; + } + } + } + + // grabbed + else if (monster.statusEffect.slug === 'grabbed') { + for (const technique of monster.activeTechniques) { + if ([TechniqueRange.ranged, TechniqueRange.reach].includes(technique.range)) { + Game.phases.preAction.push(() => { + technique.potency = technique.stats.potency * 0.5; + technique.power = technique.stats.power * 0.5; + }); + + monster.statusEffect.onRemove = () => { + technique.resetStats(); + }; + } + } + } + + // charging + else if (monster.statusEffect.slug === 'charging') { + const nextStatusEffect = await fetchStatusEffect('chargedup'); + + monster.statusEffect.onRemove = () => { + monster.statusEffect = nextStatusEffect; + }; + } + + // statchange + else if (monster.statusEffect.effects.includes('statchange')) { + monster.resetStatModifiers(); + + for (const statType in monster.statusEffect.stats) { + const statChange = monster.statusEffect.stats[statType]; + const modifiedValue = Math.floor(eval(`${monster.stats[statType]} ${statChange.operation} ${statChange.value}`)); + + Game.phases.preAction.push(() => { + monster.setStatModifier(statType, modifiedValue); + }); + } + + monster.statusEffect.onRemove = () => { + monster.resetStatModifiers(); + }; + } + + Game.phases.postAction.push(() => { + monster.statusEffect.turnsLeft--; + }); + }, + + async spawnEnemyMonster () { + 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.drawEnemyMonster(); + }, + + /** + * @param {MouseEvent} event + */ + async battleClick (event) { + UI.battleClickEvent = event; + + // hit? + const accuracy = Math.random(); + Game.didTechniqueHit = state.activeTechnique.accuracy >= accuracy; + + await Game.useTechnique(state.activeTechnique, state.activeMonster, state.enemy.monster); + + Game.phases.postAction.push(async () => { + // enemy defeated + if (state.enemy.monster.hp <= 0) { + // money + state.money += state.enemy.monster.level * state.enemy.monster.moneyModifier; + + // exp + state.activeMonster.exp += calculateAwardedExperience(state.enemy.monster, [state.activeMonster])[0]; + + if (state.activeMonster.canLevelUp()) { + state.activeMonster.levelUp(); + } + if (state.activeMonster.canEvolve()) { + await fetchMonster(state.activeMonster.evolutions[0].monster_slug); + state.activeMonster.evolve(); + } + + await Game.spawnEnemyMonster(); + } + }); + + Game.progressTurn(); + }, + + /** + * @param {MouseEvent} event + */ + techniqueClick (event) { + if (event.target === UI.elements.techniques) { + return; + } + + let target = event.target; + while (!target.classList.contains('techniques__technique')) { + target = target.parentNode; + } + + const idx = [...UI.elements.techniques.children].indexOf(target); + state.activeTechnique = state.activeMonster.activeTechniques[idx]; + + // trigger battle click + const rect = UI.elements.battleEnemy.getBoundingClientRect(); + const xMin = rect.left + 64; + const xMax = rect.right - 64; + const yMin = rect.top + 32; + const yMax = rect.bottom - 32; + UI.elements.battleEnemy.dispatchEvent(new MouseEvent('click', { + clientX: Math.random() * (xMax - xMin) + xMin, + clientY: Math.random() * (yMax - yMin) + yMin, + })); + }, + + catchMonster () { + const caughtMonster = new Monster(state.enemy.monster.slug); + caughtMonster.initialize(); + caughtMonster.level = state.enemy.monster.level; + + state.partyMonsters.push(caughtMonster); + + Game.spawnEnemyMonster(); + }, + + /** + * @returns {string} + */ + save () { + const saveMonster = (monsterData, monsterState) => { + monsterData.level = monsterState.level; + monsterData.hp = monsterState.hp; + + return monsterData; + }; + + const saveData = JSON.parse(JSON.stringify(state)); + + for (const idx in saveData.monsters) { + saveData.monsters[idx] = saveMonster(saveData.monsters[idx], state.monsters[idx]); + } + + for (const idx in saveData.partyMonsters) { + saveData.partyMonsters[idx] = saveMonster(saveData.partyMonsters[idx], state.partyMonsters[idx]); + } + + saveData.activeMonsterIdx = state.partyMonsters.indexOf(state.activeMonster); + + saveData.enemy.monster = saveMonster(saveData.enemy.monster, state.enemy.monster); + + return btoa(JSON.stringify(saveData)); + }, + + /** + * @param {string} saveData + */ + async load (saveData) { + /** + * @param {Monster} monsterData + */ + const loadMonster = async (monsterData) => { + const monster = await fetchMonster(monsterData.slug); + + monster.level = monsterData.level; + monster.hp = monsterData.hp; + monster.exp = monsterData.exp; + monster.tasteWarm = monsterData.tasteWarm; + monster.tasteCold = monsterData.tasteCold; + monster.gender = monsterData.gender; + monster.heldItem = await loadItem(monsterData.heldItem); + monster.statusEffect = await loadStatusEffect(monsterData.statusEffect); + monster.statModifiers = monsterData.statModifiers; + monster.experienceModifier = monsterData.experienceModifier; + monster.moneyModifier = monsterData.moneyModifier; + monster.activeTechniques = await Promise.all(monsterData.activeTechniques.map(async (technique) => { + return loadTechnique(technique); + })); + + return monster; + }; + + /** + * @param {Item} itemData + */ + const loadItem = async (itemData) => {}; + + /** + * @param {StatusEffect} statusEffectData + */ + const loadStatusEffect = async (statusEffectData) => { + if (!statusEffectData) { + return null; + } + + const statusEffect = await fetchStatusEffect(statusEffectData.slug); + + statusEffect.turnsLeft = statusEffectData.turnsLeft; + + return statusEffect; + }; + + /** + * @param {Technique} techniqueData + */ + const loadTechnique = async (techniqueData) => { + const technique = await fetchTechnique(techniqueData.slug); + + return technique; + }; + + /** + * @type {State} + */ + const loadedState = JSON.parse(atob(saveData)); + + state.money = loadedState.money; + state.monsters = await Promise.all(loadedState.monsters.map(async (monsterData) => await loadMonster(monsterData))); + state.inventory = await Promise.all(loadedState.inventory.map(async (itemData) => await loadItem(itemData))); + state.partyMonsters = await Promise.all(loadedState.partyMonsters.map(async (monsterData) => await loadMonster(monsterData))); + state.activeMonster = state.partyMonsters[loadedState.activeMonsterIdx]; + state.activeTechnique = await loadTechnique(loadedState.activeTechnique); + state.enemy.monster = await loadMonster(loadedState.enemy.monster); + + UI.drawEnemyMonster(); + UI.drawActiveMonster(); + UI.drawActiveTechniques(); + }, +}; + +// Game click bindings +UI.elements.battleEnemy.addEventListener('click', Game.battleClick); +UI.elements.techniques.addEventListener('click', Game.techniqueClick); +UI.elements.menuCatch.addEventListener('click', Game.catchMonster); + + +(async function () { + await initializeDB(); + + const possibleStarterMonsters = ['budaye', 'dollfin', 'grintot', 'ignibus', 'memnomnom']; + + // state.enemy.monster = await fetchMonster('grintot'); + state.enemy.monster = await fetchMonster(possibleStarterMonsters[Math.round(Math.random() * (possibleStarterMonsters.length - 1))]); + + state.partyMonsters = [ + await fetchMonster(possibleStarterMonsters[Math.round(Math.random() * (possibleStarterMonsters.length - 1))]), + await fetchMonster('corvix'), + await fetchMonster('lunight'), + await fetchMonster('prophetoise'), + await fetchMonster('drashimi'), + await fetchMonster('glombroc'), + await fetchMonster('uneye'), + await fetchMonster('nostray'), + await fetchMonster('dragarbor'), + await fetchMonster('mk01_omega'), + ]; + + state.activeMonster = state.partyMonsters[0]; + state.activeTechnique = state.activeMonster.activeTechniques[0]; + + UI.drawEnemyMonster(); + UI.drawActiveMonster(); + UI.drawActiveTechniques(); +})(); diff --git a/resources/js/helpers.js b/resources/js/helpers.js new file mode 100644 index 0000000..019f822 --- /dev/null +++ b/resources/js/helpers.js @@ -0,0 +1,45 @@ +/** + * @param {string} slug + * + * @returns {(string|MonsterSlug|TechniqueSlug)} + */ +function slugToName (slug) { + return slug.split('_').map((item) => item.charAt(0).toUpperCase() + item.slice(1)).join(' '); +} + +/** + * @param {string} color + * + * @returns {string} + */ +function standardizeColor (color) { + var ctx = document.createElement('canvas').getContext('2d'); + ctx.fillStyle = color; + + return ctx.fillStyle; +} + +/** + * @param {...string} colors + * + * @returns {string} rgb + */ +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})`; +} diff --git a/resources/js/ui.js b/resources/js/ui.js new file mode 100644 index 0000000..f6a36b8 --- /dev/null +++ b/resources/js/ui.js @@ -0,0 +1,643 @@ +const Template = { + popup: document.querySelector('#tpl___popup'), + + battleMonster: document.querySelector('#tpl___battle__monster'), + battleHpBar: document.querySelector('#tpl___battle__hp-bar'), + battleExpBar: document.querySelector('#tpl___battle__exp-bar'), + battleDamage: document.querySelector('#tpl___battle__damage'), + + movesetList: document.querySelector('#tpl___moveset__list'), + movesetItem: document.querySelector('#tpl___moveset__item'), + + techniques: document.querySelector('#tpl___techniques'), + technique: document.querySelector('#tpl___technique'), + + party: document.querySelector('#tpl___party'), + partyMonster: document.querySelector('#tpl___party__monster'), + + menuJournal: document.querySelector('#tpl___menu__journal'), +}; + +const UI = { + elements: { + battleEnemy: document.querySelector('#battle__enemy'), + battleEnemySprite: null, + battleEnemyAnimation: document.querySelector('.battle__monster-sprite__animation'), + battlePlayer: document.querySelector('#battle__player'), + + techniques: document.querySelector('#techniques'), + + money: document.querySelector('#money'), + + menuParty: document.querySelector('#menu__party'), + menuInventory: document.querySelector('#menu__inventory'), + menuCatch: document.querySelector('#menu__catch'), + menuJournal: document.querySelector('#menu__journal'), + }, + + events: document.createElement('div'), + + + /** + * @param {HTMLElement} template + */ + createTemplate (template) { + const templateBase = document.createElement('div'); + templateBase.innerHTML = template.innerHTML.trim(); + + return templateBase.firstChild; + }, + + /** + * @returns {HTMLElement} + */ + createPopup () { + const popup = UI.createTemplate(Template.popup); + + popup.addEventListener('click', ({ target }) => { + if (target === popup) { + popup.dispatchEvent(new Event('close')); + popup.remove(); + } + }); + + return popup; + }, + + /** + * @param {HTMLElement} slotNode + * @param {HTMLElement} replacingNode + * + * @returns {HTMLElement} + */ + replaceTemplateSlot (slotNode, replacingNode) { + replacingNode.dataset.templateSlot = slotNode.dataset.templateSlot; + slotNode.replaceWith(replacingNode); + + return replacingNode; + }, + + /** + * @param {HTMLElement} popup + */ + drawPopup (popup) { + const otherPopupExists = document.querySelector('.popup__overlay'); + if (otherPopupExists) { + popup.classList.add('popup--is-multiple'); + } + + document.body.appendChild(popup); + }, + + + /* Battle */ + + /** + * @type {MouseEvent} + */ + battleClickEvent: null, + + techniqueAnimationIsRunning: false, + techniqueAnimationNumber: 0, + techniqueAnimationFps: 20, + + + /** + * @param {Monster} monster + * + * @returns {HTMLElement} + */ + createHpBar (monster) { + const template = UI.createTemplate(Template.battleHpBar); + const bar = template.querySelector('[data-template-slot="bar"]'); + const text = template.querySelector('[data-template-slot="text"]'); + + let barColor; + const percentHp = (monster.hp / monster.stats.hp) * 100; + if (percentHp > 60) { + barColor = 'green'; + } else if (percentHp > 15) { + barColor = 'rgb(240, 240, 100)'; + } else { + barColor = 'red'; + } + + bar.style.backgroundColor = barColor; + bar.style.width = `${percentHp}%`; + text.textContent = `${monster.hp} / ${monster.stats.hp}`; + + return template; + }, + + /** + * @param {Monster} monster + * + * @returns {HTMLElement} + */ + createExpBar (monster) { + const template = UI.createTemplate(Template.battleExpBar); + const bar = template.querySelector('[data-template-slot="bar"]'); + const text = template.querySelector('[data-template-slot="text"]'); + + const expToNextLevel = monster.getExperienceRequired() - monster.getExperienceRequired(-1); + const currentRelativeExp = monster.exp - monster.getExperienceRequired(-1); + const expPercent = (currentRelativeExp / expToNextLevel) * 100; + + bar.style.width = `${expPercent}%`; + text.textContent = `${monster.exp} / ${monster.getExperienceRequired()}`; + + return template; + }, + + /** + * @param {string} type + * + * @returns {HTMLElement} + */ + createElementTypeIcon (type) { + const img = document.createElement('img'); + img.src = `/modules/tuxemon/mods/tuxemon/gfx/ui/icons/element/${type}_type.png`; + img.title = slugToName(type); + + return img; + }, + + /** + * @param {StatusEffect} statusEffect + * + * @returns {HTMLElement} + */ + createStatusEffectIcon (statusEffect) { + if (!statusEffect) { + return document.createElement('i'); + } + + const img = document.createElement('img'); + img.src = `/modules/tuxemon/mods/tuxemon/gfx/ui/icons/status/icon_${statusEffect.slug}.png`; + img.title = statusEffect.name; + + return img; + }, + + /** + * @param {Monster} monster + */ + createBattleMonster (monster) { + const template = UI.createTemplate(Template.battleMonster); + + template.querySelector('[data-template-slot="name"]').textContent = monster.name; + template.querySelector('[data-template-slot="gender"]').textContent = monster.gender === 'male' ? '♂' : monster.gender === 'female' ? '♀' : '⚲'; + template.querySelector('[data-template-slot="level"]').textContent = monster.level; + template.querySelector('[data-template-slot="statusEffect"]').innerHTML = UI.createStatusEffectIcon(monster.statusEffect).outerHTML; + template.querySelector('[data-template-slot="sprite"]').src = `/modules/tuxemon/mods/tuxemon/gfx/sprites/battle/${monster.slug}-front.png`; + + UI.replaceTemplateSlot(template.querySelector('[data-template-slot="hpBar"]'), UI.createHpBar(monster)); + + return template; + }, + + /** + * @returns {HTMLElement} + */ + createEnemyMonster () { + const battleMonsterNode = UI.createBattleMonster(state.enemy.monster); + + battleMonsterNode.classList.add('battle__monster--enemy'); + + return battleMonsterNode; + }, + + /** + * @returns {HTMLElement} + */ + createActiveMonster () { + const battleMonsterNode = UI.createBattleMonster(state.activeMonster); + + UI.replaceTemplateSlot( + battleMonsterNode.querySelector('[data-template-slot="expBar"]'), + UI.createExpBar(state.activeMonster) + ); + + battleMonsterNode.classList.add('battle__monster--player'); + + battleMonsterNode.querySelector('[data-template-slot="sprite"]').addEventListener('click', () => { + UI.openStatsMenu(state.activeMonster); + }); + + return battleMonsterNode; + }, + + /** + * @param {Monster} monster + * + * @returns {HTMLElement} + */ + createActiveTechniques (monster) { + const template = UI.createTemplate(Template.techniques); + + for (const technique of monster.activeTechniques) { + const techniqueNode = UI.createTemplate(Template.technique); + + techniqueNode.querySelector('[data-template-slot="name"]').textContent = technique.name; + techniqueNode.querySelector('[data-template-slot="types"]').innerHTML = technique.types.map((type) => UI.createElementTypeIcon(type).outerHTML).join(''); + techniqueNode.querySelector('[data-template-slot="power"]').textContent = technique.power; + techniqueNode.querySelector('[data-template-slot="accuracy"]').textContent = technique.accuracy; + techniqueNode.querySelector('[data-template-slot="range"]').textContent = technique.range; + + template.appendChild(techniqueNode); + } + + return template; + }, + + /** + * @param {HTMLElement} battleMonsterNode + */ + drawEnemyMonster () { + const battleMonsterNode = UI.createEnemyMonster(); + + UI.elements.battleEnemySprite = battleMonsterNode.querySelector('[data-template-slot="sprite"]'); + UI.elements.battleEnemySprite.style.transitionDuration = `${UI.damageHighlightClickDuration}s`; + + const previousBattleMonsterNode = UI.elements.battleEnemy.querySelector('.battle__monster'); + if (previousBattleMonsterNode) { + UI.elements.battleEnemySprite.classList = previousBattleMonsterNode.querySelector('[data-template-slot="sprite"]').classList; + + UI.elements.battleEnemy.removeChild(previousBattleMonsterNode); + } + UI.elements.battleEnemy.appendChild(battleMonsterNode); + }, + + /** + * @returns {void} + */ + drawActiveMonster () { + const battleMonsterNode = UI.createActiveMonster(); + + const previousBattleMonsterNode = UI.elements.battlePlayer.querySelector('.battle__monster'); + if (previousBattleMonsterNode) { + UI.elements.battlePlayer.removeChild(previousBattleMonsterNode); + } + UI.elements.battlePlayer.appendChild(battleMonsterNode); + }, + + /** + * @returns {void} + */ + drawActiveTechniques () { + const activeTechniques = UI.createActiveTechniques(state.activeMonster); + activeTechniques.id = 'techniques'; + + document.querySelector('#techniques').innerHTML = activeTechniques.innerHTML; + }, + + /** + * @returns {void} + */ + drawTechniqueAnimation () { + if (!UI.techniqueAnimationIsRunning && state.activeTechnique.animation && DB.allAnimations[state.activeTechnique.animation]) { + UI.techniqueAnimationIsRunning = true; + + const techniqueAnimationLoop = () => { + UI.elements.battleEnemyAnimation.src = `/modules/tuxemon/mods/tuxemon/animations/technique/${state.activeTechnique.animation}_${("00" + UI.techniqueAnimationNumber).slice(-2)}.png`; + UI.elements.battleEnemyAnimation.style.top = UI.battleClickEvent.clientY - (UI.elements.battleEnemyAnimation.clientHeight / 2); + UI.elements.battleEnemyAnimation.style.left = UI.battleClickEvent.clientX - (UI.elements.battleEnemyAnimation.clientWidth / 2); + // console.log(UI.elements.battleEnemyAnimation.src); + + UI.techniqueAnimationNumber++; + + if (UI.techniqueAnimationNumber >= DB.allAnimations[state.activeTechnique.animation].length) { + UI.techniqueAnimationIsRunning = false; + UI.techniqueAnimationNumber = 0; + UI.elements.battleEnemyAnimation.src = ''; + return; + } + + setTimeout(() => requestAnimationFrame(techniqueAnimationLoop), 1000 / UI.techniqueAnimationFps); + }; + + requestAnimationFrame(techniqueAnimationLoop); + } + }, + + + /* Battle - Damage */ + + damageHighlightClickDuration: 0.1, + damageHighlightClickTimeout: null, + + /** + * @param {number|string} damage + * + * @returns {HTMLElement} + */ + createDamage (damage) { + const damageNode = UI.createTemplate(Template.battleDamage); + damageNode.innerHTML = damage; + + damageNode.style.top = UI.battleClickEvent.pageY - UI.elements.battleEnemy.offsetTop + (Math.random() * 40 - 20); + damageNode.style.left = UI.battleClickEvent.pageX - UI.elements.battleEnemy.offsetLeft + (Math.random() * 40 - 20); + + damageNode.dataset.duration = 2; + damageNode.style.animationDuration = `${damageNode.dataset.duration}s`; + + return damageNode; + }, + + /** + * @returns {HTMLElement} + */ + createDamageMiss () { + return UI.createDamage('MISS!'); + }, + + /** + * @param {HTMLElement} damageNode + * @param {number} multiplier + * + * @returns {HTMLElement} + */ + applyMultiplierToDamage (damageNode, multiplier) { + damageNode.style.fontSize = `${multiplier * 2}rem`; + + return damageNode; + }, + + /** + * @param {HTMLElement} damageNode + * @param {Technique} technique + * + * @returns {HTMLElement} + */ + applyTechniqueToDamage (damageNode, technique) { + damageNode.style.color = mixColors( + ...technique.types.map((type) => standardizeColor(ElementTypeColor[type])) + ); + + return damageNode; + }, + + /** + * @param {HTMLElement} damageNode + * @param {StatusEffect} statusEffect + * + * @returns {HTMLElement} + */ + applyStatusEffectToDamage (damageNode, statusEffect) { + damageNode.style.color = StatusEffectTypeColor[statusEffect.slug]; + + return damageNode; + }, + + /** + * @param {HTMLElement} damageNode + */ + drawDamage (damageNode) { + UI.elements.battleEnemy.appendChild(damageNode); + setTimeout(() => damageNode.remove(), (damageNode.dataset.duration * 1000) - 500); + + UI.elements.battleEnemySprite.classList.add('damaged'); + clearTimeout(UI.damageHighlightClickTimeout); + UI.damageHighlightClickTimeout = setTimeout(() => UI.elements.battleEnemySprite.classList.remove('damaged'), UI.damageHighlightClickDuration * 1000); + + UI.drawTechniqueAnimation(); + }, + + /** + * @param {HTMLElement} damageNode + */ + drawDamageMiss (damageNode) { + UI.elements.battleEnemy.appendChild(damageNode); + setTimeout(() => damageNode.remove(), (damageNode.dataset.duration * 1000) - 500); + }, + + + + /* Menu */ + + partySelectionMode: 'select', + + + openPartyMenu () { + const popup = UI.createPopup(); + + const party = UI.createTemplate(Template.party); + party.id = 'party'; + for (const monsterIdx in state.partyMonsters) { + const monster = state.partyMonsters[monsterIdx]; + const partyMonster = UI.createTemplate(Template.partyMonster); + + partyMonster.querySelector('[data-template-slot="sprite"]').src = `/modules/tuxemon/mods/tuxemon/gfx/sprites/battle/${monster.slug}-front.png`; + + partyMonster.addEventListener('click', async (event) => { + // bubble up to partyNode + let target = event.target; + while (target.parentNode.id !== party.id) { + target = target.parentNode; + } + + if (UI.partySelectionMode === 'select') { + state.activeMonster = monster; + state.activeTechnique = state.activeMonster.activeTechniques[0]; + + UI.drawActiveMonster(); + UI.drawActiveTechniques(); + + popup.remove(); + } + else if (UI.partySelectionMode === 'stats') { + UI.openStatsMenu(monster); + } + else if (UI.partySelectionMode === 'techniques') { + UI.openMovesetSelection(monster); + } + + UI.events.dispatchEvent(new CustomEvent('party:monsterSelected', { + detail: { + monster: monster, + mode: UI.partySelectionMode, + }, + })); + }); + + party.querySelector('[data-template-slot="monsters"]').appendChild(partyMonster); + } + + const selectionModesNode = party.querySelector('[data-template-slot="modes"]'); + const selectionModeNodes = selectionModesNode.querySelectorAll('[data-party-selection-mode]'); + selectionModeNodes.forEach((node) => { + if (node.dataset.partySelectionMode === UI.partySelectionMode) { + node.setAttribute('selected', true); + } + + node.addEventListener('click', () => { + selectionModesNode.querySelector(`[data-party-selection-mode="${UI.partySelectionMode}"]`).removeAttribute('selected'); + + UI.partySelectionMode = node.dataset.partySelectionMode; + + node.setAttribute('selected', true); + }); + }); + + popup.querySelector('.popup').appendChild(party); + UI.drawPopup(popup); + }, + + openInventoryMenu () { // TODO + const popup = UI.createPopup(); + + const inventory = document.createElement('div'); + inventory.id = 'inventory'; + for (const item of state.inventory) { + } + + popup.querySelector('.popup').appendChild(inventory); + UI.drawPopup(popup); + }, + + openJournalMenu () { // TODO + const popup = UI.createPopup(); + const journal = UI.createTemplate(Template.menuJournal); + + journal.querySelector('[data-template-slot="save"]').addEventListener('click', () => { + UI.openSaveDialog(); + }); + + journal.querySelector('[data-template-slot="load"]').addEventListener('click', () => { + UI.openLoadDialog(); + }); + + popup.querySelector('.popup').appendChild(journal); + UI.drawPopup(popup); + }, + + openSaveDialog () { // TODO + const popup = UI.createPopup(); + + const textarea = document.createElement('textarea'); + textarea.value = Game.save(); + + popup.querySelector('.popup').appendChild(textarea); + UI.drawPopup(popup); + }, + + openLoadDialog () { // TODO + const popup = UI.createPopup(); + + const textarea = document.createElement('textarea'); + + const loadButton = document.createElement('button'); + loadButton.textContent = "Load"; + loadButton.addEventListener('click', () => Game.load(textarea.value.trim())); + + popup.querySelector('.popup').appendChild(textarea); + popup.querySelector('.popup').appendChild(loadButton); + + UI.drawPopup(popup); + }, + + + /* Menu - Monster */ + + /** + * @param {Monster} monster + * + * @returns {HTMLElement} + */ + createStatsMenu (monster) { // TODO + const template = document.createElement('div'); + template.textContent = "select moves for " + monster.name; + template.style.width = '90vw'; + template.style.height = '90vh'; + + template.addEventListener('click', () => UI.openMovesetSelection(monster)); + + return template; + }, + + /** + * @param {Monster} monster + * + * @returns {Promise<HTMLElement>} + */ + async createMovesetSelection (monster) { + const movesetListNode = UI.createTemplate(Template.movesetList); + for (const move of monster.moveset) { + const technique = await fetchTechnique(move.technique); + const movesetItemNode = UI.createTemplate(Template.movesetItem); + + movesetItemNode.querySelector('[data-template-slot="name"]').textContent = slugToName(technique.slug); + movesetItemNode.querySelector('[data-template-slot="types"]').innerHTML = technique.types.map((type) => UI.createElementTypeIcon(type).outerHTML).join(''); + movesetItemNode.querySelector('[data-template-slot="power"]').textContent = technique.power; + movesetItemNode.querySelector('[data-template-slot="level"]').textContent = move.level_learned; + + // disabled? + if (monster.level < move.level_learned) { + movesetItemNode.setAttribute('disabled', true); + } + + // selected? + if (monster.activeTechniques.find((item) => item.slug == technique.slug)) { + movesetItemNode.toggleAttribute('selected'); + } + + // clicked + movesetItemNode.addEventListener('click', () => { + if (movesetItemNode.getAttribute('disabled')) { + return false; + } + + // un/select + movesetItemNode.toggleAttribute('selected'); + + const isSelected = movesetItemNode.hasAttribute('selected'); + if (isSelected) { + monster.activeTechniques.push(technique); + } else { + const idxTechniqueToRemove = monster.activeTechniques.findIndex((item) => item.slug == technique.slug); + if (idxTechniqueToRemove > -1) { + monster.activeTechniques.splice(idxTechniqueToRemove, 1); + } + } + + const event = new CustomEvent('movesetSelection:moveSelected', { + detail: { + isSelected: movesetItemNode.hasAttribute('selected'), + technique: technique, + }, + }); + UI.events.dispatchEvent(event); + }); + + movesetListNode.appendChild(movesetItemNode); + } + + return movesetListNode; + }, + + openStatsMenu (monster) { + const popup = UI.createPopup(); + const statusMenu = UI.createStatsMenu(monster); + + popup.querySelector('.popup').appendChild(statusMenu); + UI.drawPopup(popup); + }, + + /** + * @param {Monster} monster + */ + async openMovesetSelection (monster) { + const popup = UI.createPopup(); + const movesetSelection = await UI.createMovesetSelection(monster); + + popup.querySelector('.popup').appendChild(movesetSelection); + popup.addEventListener('close', () => UI.drawActiveTechniques()); + + UI.drawPopup(popup); + }, +}; + +// UI element click bindings +UI.elements.menuParty.addEventListener('click', UI.openPartyMenu); +UI.elements.menuInventory.addEventListener('click', UI.openInventoryMenu); +UI.elements.menuJournal.addEventListener('click', UI.openJournalMenu); diff --git a/script.js b/script.js deleted file mode 100644 index 2fab2fc..0000000 --- a/script.js +++ /dev/null @@ -1,1152 +0,0 @@ -const DB = { - allMonsters: [], - allAnimations: {}, - monsters: {}, - shapes: {}, - elements: {}, - techniques: {}, - statusEffects: {}, -}; - -const ElementType = { - aether: 'aether', - wood: 'wood', - fire: 'fire', - earth: 'earth', - metal: 'metal', - water: 'water', - - /* lightning: 'lightning', - frost: 'frost', - venom: 'venom', - //vermin: 'vermin', - cosmic: 'cosmic', - battle: 'battle', - psionic: 'psionic', - darkness: 'darkness', - heaven: 'heaven', - - combineTypes(typeA, typeB) { - if (typeA === ElementType.earth & typeB === ElementType.fire) { - return ElementType.lightning; - } - if (typeA === ElementType.earth & typeB === ElementType.water) { - return ElementType.frost; - } - if (typeA === ElementType.earth & typeB === ElementType.wood) { - return ElementType.venom; - } - // if (typeA === ElementType.earth & typeB === ElementType.metal) { - // return ElementType.vermin; - // } - if (typeA === ElementType.fire && typeB === ElementType.water) { - return ElementType.cosmic; - } - } */ -}; -const ElementTypeColor = { - [ElementType.aether]: 'rgba(255, 255, 255, 1)', - [ElementType.wood]: '#3ca6a6', - [ElementType.fire]: '#ca3c3c', - [ElementType.earth]: '#eac93c', - [ElementType.metal]: '#e4e4e4', - [ElementType.water]: '#3c3c3c', -}; -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', -}; -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; - monsters = []; - - inventory = []; - - partyMonsters = []; - - activeMonster = null; - activeTechnique = null; - - turn = 0; - currentArea = null; - - enemy = { - monster: null, - }; -}; - -class Monster { - #level = 1; - #hp = 0; - - exp = 1; - - tasteWarm = TasteWarm.tasteless; - tasteCold = TasteCold.tasteless; - - gender = ''; - - heldItem = null; - statusEffect = ''; - - statusModifier = { - hp: 0, - melee: 0, - armour: 0, - ranged: 0, - dodge: 0, - speed: 0, - }; - - experienceModifier = 1; - moneyModifier = 1; - - activeTechniques = []; - - 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 hp () { - return this.#hp; - } - - set hp (hp) { - this.#hp = Math.max(0, Math.min(hp, this.status.hp)); - } - - get name () { - return slugToName(this.slug); - } - - getLearnableTechniques () { - return this.moveset.filter((move) => this.level >= move.learned_at); - } - - 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)); - return this.evolutions.filter((evolution) => evolution.path === 'standard' && this.level >= evolution.at_level); - } - - 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.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); - 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, - }; - } - - setStatusModifier (statusType, newAbsoluteValue) { - this.statusModifier[statusType] = newAbsoluteValue - this.status[statusType]; - } - resetStatusModifier () { - for (const m in this.statusModifier) { - this.statusModifier[m] = 0; - } - } -}; - -class Technique { - #accuracy = 0; - #potency = 0; - #power = 0; - - combatEffects = []; - - constructor (slug) { - this.slug = slug; - - this.resetToBase(); - } - - get name () { - return slugToName(this.slug); - } - - get types () { - return DB.techniques[this.slug].types; - } - - get range () { - return DB.techniques[this.slug].range; - } - - get animation () { - return DB.techniques[this.slug].animation; - } - - get sfx () { - return DB.techniques[this.slug].sfx; - } - - get effects () { - return DB.techniques[this.slug].effects; - } - - get accuracy () { - return this.#accuracy; - } - set accuracy (accuracy) { - this.#accuracy = accuracy; - } - - get potency () { - return this.#potency; - } - set potency (potency) { - this.#potency = potency; - } - - get power () { - return this.#power; - } - set power (power) { - this.#power = power; - } - - get base () { - const accuracy = DB.techniques[this.slug].accuracy; - const potency = DB.techniques[this.slug].potency; - const power = DB.techniques[this.slug].power; - - return { - accuracy, - potency, - power, - }; - } - - resetToBase () { - this.accuracy = this.base.accuracy; - this.potency = this.base.potency; - this.power = this.base.power; - } -} - -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 { - turnsLeft = 0; - - constructor (slug) { - this.slug = slug; - - if (['recover', 'lifeleech'].includes(this.slug)) { - this.turnsLeft = 1; - } - else if (['charging'].includes(this.slug)) { - this.turnsLeft = 2; - } - else if (this.category === 'positive') { - this.turnsLeft = Math.ceil(Math.random() * 6) + 4; - } - else if (this.category === 'negative') { - this.turnsLeft = Math.ceil(Math.random() * 3) + 2; - } - else { - this.turnsLeft = Math.ceil(Math.random() * 3) + 2; - } - } - - get effects () { - return DB.statusEffects[this.slug].effects; - } - - get category () { - return DB.statusEffects[this.slug].category; - } - - 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; - - 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) { - if (technique.power === 0) { - return 0; - } - - 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 Math.max(damage, 1); -} - -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); -} -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'); - 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'), - await fetchMonster('glombroc'), - await fetchMonster('uneye'), - await fetchMonster('nostray'), - await fetchMonster('dragarbor'), - ]; - 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 templateTechnique = document.querySelector('#tpl___technique').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, - - enemyImg: null, - damageHighlightClickDuration: 0.1, - damageHighlightClickTimeout: null, - imgClickDuration: 0.1, - damageClickTimeout: null, - - damageAnimationIsRunning: false, - 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; - }, - - async 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*2 + '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); - - 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 && DB.allAnimations[state.activeTechnique.animation]) { - this.damageAnimationIsRunning = true; - - const damageAnimationLoop = () => { - enemyAnimation.src = `/modules/tuxemon/mods/tuxemon/animations/technique/${state.activeTechnique.animation}_${("00" + this.damageAnimationNumber).slice(-2)}.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) { - this.damageAnimationIsRunning = false; - this.damageAnimationNumber = 0; - enemyAnimation.src = ''; - return; - } - - setTimeout(() => requestAnimationFrame(damageAnimationLoop), 50); - }; - - requestAnimationFrame(damageAnimationLoop); - - // sfx - /* let sfx = state.activeTechnique.sfx.substr(4); - const audio = new Audio(`/modules/tuxemon/mods/tuxemon/sounds/technique/${sfx}.ogg`); - audio.play(); */ - } - }, - - 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; - 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')); - - if (where === 'player') { - UI.setExp(monster, battleMonster.querySelector('.exp')); - battleMonster.querySelector('.battle__monster-technique').addEventListener('click', UI.openMovesetSelection); - - battleMonster.classList.add('battle__monster--player'); - battlePlayer.querySelector('.battle__monster') && battlePlayer.removeChild(battlePlayer.querySelector('.battle__monster')); - battlePlayer.appendChild(battleMonster); - - UI.setActiveTechnique(); - - UI.setActiveTechniques(); - } else { - battleMonster.classList.add('battle__monster--enemy'); - - 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); - } - }, - - setEnemyMonster () { - UI.setBattleMonster(state.enemy.monster, 'enemy'); - }, - - setActiveMonster () { - UI.setBattleMonster(state.activeMonster, 'player'); - }, - - setActiveTechnique () { - battlePlayer.querySelector('.battle__monster-technique').innerHTML = - slugToName(state.activeTechnique.slug) + ' ' - + state.activeTechnique.types.map((type) => UI.createElementTypeIcon(type)).join(''); - }, - - setActiveTechniques () { - const techniquesNode = document.querySelector('#techniques'); - techniquesNode.replaceChildren(); - - for (const technique of state.activeMonster.activeTechniques) { - const techniqueNode = UI.getTemplate(templateTechnique); - techniqueNode.querySelector('.techniques__technique__name').innerHTML = technique.name; - techniquesNode.appendChild(techniqueNode); - } - }, - - async chooseActiveTechnique () { - let activeMoveIndex = 0; - while ((await fetchTechnique(state.activeMonster.moveset[activeMoveIndex].technique)).power === 0) { - activeMoveIndex++; - } - state.activeTechnique = await fetchTechnique(state.activeMonster.moveset[activeMoveIndex].technique); - - UI.setActiveTechnique(); - }, - - async openMovesetSelection () { - const popup = UI.createPopup(); - - const movesetList = UI.getTemplate(templateMovesetList); - for (const move of state.activeMonster.moveset) { - const technique = await fetchTechnique(move.technique); - const movesetItem = UI.getTemplate(templateMovesetItem); - - movesetItem.querySelector('.moveset__item__name').textContent = slugToName(technique.slug); - movesetItem.querySelector('.moveset__item__type').innerHTML = technique.types.map((type) => UI.createElementTypeIcon(type)).join(''); - movesetItem.querySelector('.moveset__item__power').innerHTML = technique.power; - movesetItem.querySelector('.moveset__item__level').innerHTML = move.level_learned; - - if (state.activeMonster.activeTechniques.find((item) => item.slug === technique.slug)) { - movesetItem.setAttribute('selected', true); - } - - movesetItem.addEventListener('click', () => { - if (movesetItem.getAttribute('disabled')) { - return false; - } - - state.activeTechnique = technique; - UI.setActiveTechnique(); - - if (movesetItem.getAttribute('selected')) { - movesetItem.removeAttribute('selected'); - state.activeMonster.activeTechniques.splice(state.activeMonster.activeTechniques.findIndex((item) => item.slug === technique.slug), 1); - } else { - movesetItem.setAttribute('selected', true); - state.activeMonster.activeTechniques.push(technique); - } - UI.setActiveTechniques(); - }); - - 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.setEnemyMonster(); - }, - - createElementTypeIcon (type) { - var img = document.createElement('img'); - img.src = `/modules/tuxemon/mods/tuxemon/gfx/ui/icons/element/${type}_type.png`; - img.title = slugToName(type); - - return img.outerHTML; - }, - - createStatusEffectIcon (statusEffect) { - if (!statusEffect) return ''; - - var img = document.createElement('img'); - img.src = `/modules/tuxemon/mods/tuxemon/gfx/ui/icons/status/icon_${statusEffect.slug}.png`; - img.title = statusEffect.name; - - return img.outerHTML; - }, - - createPopup () { - const popup = UI.getTemplate(templatePopup); - popup.addEventListener('click', ({ target }) => target === popup && popup.remove()); - - return popup; - }, - - openPartyMenu () { - const popup = UI.createPopup(); - - const party = document.createElement('div'); - party.id = 'party'; - for (const monster of state.partyMonsters) { - const partyMonster = UI.getTemplate(templatePartyMonster); - - partyMonster.dataset.slug = monster.slug; - partyMonster.querySelector('img').src = `/modules/tuxemon/mods/tuxemon/gfx/sprites/battle/${monster.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)]; - await UI.chooseActiveTechnique(); - UI.setActiveMonster(); - - popup.remove(); - }); - - party.appendChild(partyMonster); - } - - popup.querySelector('.popup').appendChild(party); - document.body.appendChild(popup); - }, - - openInventoryMenu () { - const popup = UI.createPopup(); - - const inventory = document.createElement('div'); - inventory.id = 'inventory'; - for (const item of state.inventory) { - } - - popup.querySelector('.popup').appendChild(inventory); - document.body.appendChild(popup); - }, - }; - - const Game = { - async useTechnique(technique, user, target) { - let statusEffects = []; - let combatEffects = []; - - for (const effect of technique.effects) { - if (effect.includes('status_')) { - statusEffects.push(new TechniqueEffect(effect)); - } - else if (!['damage', 'splash', 'area'].includes(effect)) { - combatEffects.push(effect); - } - } - - for (const effect of statusEffects) { - let recipient; - - if (effect.recipient === 'user') { - recipient = user; - } else { - recipient = target; - } - - if (effect.type === 'status') { - if (effect.application === 'give') { - if (recipient.statusEffect) { - continue; - } - - const potency = Math.random(); - const success = technique.potency >= potency; - if (success) { - recipient.statusEffect = await fetchStatusEffect(effect.effect); - } - } - else if (effect.application === 'remove') { - if (recipient.statusEffect.slug === effect.effect) { - recipient.statusEffect = ''; - } - } - } - } - - technique.combatEffects = combatEffects; - }, - - async applyStatusEffect (affectedMonster, opposingMonster) { - if (!affectedMonster.statusEffect) { - return; - } - - if (! (affectedMonster.statusEffect instanceof StatusEffect)) { - return; - } - - if (affectedMonster.statusEffect.slug === 'poison' || affectedMonster.statusEffect.slug === 'burn') { - const statusEffectDamage = Math.floor(affectedMonster.status.hp / 8); - affectedMonster.hp -= statusEffectDamage; - - UI.createDamage(clickEvent, statusEffectDamage); - } - - else if (affectedMonster.statusEffect.slug === 'recover') { - const statusEffectHeal = Math.floor(affectedMonster.status.hp / 16); - affectedMonster.hp += statusEffectHeal; - } - - else if (affectedMonster.statusEffect.slug === 'lifeleech') { - const statusEffectLeech = Math.floor(affectedMonster.status.hp / 16); - affectedMonster.hp -= statusEffectLeech; - opposingMonster.hp += statusEffectLeech; - - UI.createDamage(clickEvent, statusEffectLeech); - } - - else if (affectedMonster.statusEffect.slug === 'charging') { - turnEndPhaseEvents.push(async () => { - if (affectedMonster.statusEffect.turnsLeft === 0) { - affectedMonster.statusEffect = await fetchStatusEffect('chargedup'); - } - }); - } - - else if (affectedMonster.statusEffect.effects.includes('statchange')) { - affectedMonster.resetStatusModifier(); - - for (const statusType in affectedMonster.statusEffect.status) { - const statusChange = affectedMonster.statusEffect.status[statusType]; - - const modifiedValue = Math.floor(eval(`${affectedMonster.status[statusType]} ${statusChange.operation} ${statusChange.value}`)); - affectedMonster.setStatusModifier(statusType, modifiedValue); - } - } - - affectedMonster.statusEffect.turnsLeft--; - turnEndPhaseEvents.push(() => { - if (affectedMonster.statusEffect.turnsLeft === 0) { - affectedMonster.statusEffect = null; - affectedMonster.resetStatusModifier(); - } - }); - }, - - applyTechniqueEffect (technique, user, target) { - for (const effect of technique.combatEffects) { - if (effect === 'money') { - if (!techniqueHit) { - state.money += Math.floor(Math.random() * target.level); - } - } - } - - // modify technique stats - if (user.statusEffect) { - if (user.statusEffect.slug === 'grabbed') { - if ([TechniqueRange.ranged, TechniqueRange.reach].includes(technique.range)) { - technique.potency = technique.base.potency * 0.5; - technique.power = technique.base.power * 0.5; - } - } - - else if (user.statusEffect.slug === 'stuck') { - if ([TechniqueRange.melee, TechniqueRange.touch].includes(technique.range)) { - technique.potency = technique.base.potency * 0.5; - technique.power = technique.base.power * 0.5; - } - } - - // remove effect - if (user.statusEffect.turnsLeft === 0) { - turnEndPhaseEvents.push(() => { - technique.resetToBase(); - }); - } - } - }, - - damage () { - const damage = simpleDamageCalculation(state.activeTechnique, state.activeMonster, state.enemy.monster); - UI.createDamage(clickEvent, damage); - - state.enemy.monster.hp -= damage; - }, - }; - - UI.setActiveMonster(); - UI.setEnemyMonster(); - - let techniqueHit; - let clickEvent; - let turnEndPhaseEvents = []; - document.querySelector('#battle__enemy').addEventListener('click', async (event) => { - clickEvent = event; - - const accuracy = Math.random(); - techniqueHit = state.activeTechnique.accuracy >= accuracy; - - if (techniqueHit) { - await Game.useTechnique(state.activeTechnique, state.activeMonster, state.enemy.monster); - } - - await Game.applyStatusEffect(state.activeMonster, state.enemy.monster); - await Game.applyStatusEffect(state.enemy.monster, state.activeMonster); - - Game.applyTechniqueEffect(state.activeTechnique, state.activeMonster, state.enemy.monster); - - if (techniqueHit) { - Game.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); - state.activeMonster.levelUp(); - if (state.activeMonster.canEvolve()) { - await fetchMonster(state.activeMonster.evolutions[0].monster_slug); - state.activeMonster.evolve(); - } - } - } else { - UI.createDamage(event, 'MISS!'); - } - - UI.setActiveMonster(); - UI.setEnemyMonster(); - - UI.setHp(state.enemy.monster, battleEnemy); - - money.textContent = state.money; - - for (const turnEndPhaseEvent of turnEndPhaseEvents) { - const returnValue = turnEndPhaseEvent(); - if (returnValue instanceof Promise) { - await returnValue; - } - } - turnEndPhaseEvents = []; - }); - - document.querySelector('#menu__party').addEventListener('click', UI.openPartyMenu); - - document.querySelector('#menu__catch').addEventListener('click', async () => { - const caughtMonster = new Monster(state.enemy.monster.slug); - caughtMonster.level = state.enemy.monster.level; - - state.partyMonsters.push(caughtMonster); - - UI.createNewEnemyMonster(); - }); - - document.querySelector('#menu__inventory').addEventListener('click', UI.openInventoryMenu); -})(); |