summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Weipert <code@drogueronin.de>2023-08-17 02:53:14 +0200
committerDaniel Weipert <code@drogueronin.de>2023-08-17 17:42:15 +0200
commitcc685bfe02b42b592987117fa008a4461785f53c (patch)
tree625c1c9573b178e574bb70cac042c35da4036cf1
parent717fde1c48c7221da986ac02d2b806b2fee6f2d5 (diff)
refactorrefactor
-rw-r--r--index.html122
-rw-r--r--resources/css/battle.css (renamed from style.css)140
-rw-r--r--resources/css/menu.css70
-rw-r--r--resources/css/page.css24
-rw-r--r--resources/css/variables.css3
-rw-r--r--resources/js/classes/Item.js1
-rw-r--r--resources/js/classes/Monster.js223
-rw-r--r--resources/js/classes/State.js38
-rw-r--r--resources/js/classes/StatusEffect.js55
-rw-r--r--resources/js/classes/Technique.js77
-rw-r--r--resources/js/db.js62
-rw-r--r--resources/js/definitions.js136
-rw-r--r--resources/js/formula.js93
-rw-r--r--resources/js/game.js459
-rw-r--r--resources/js/helpers.js45
-rw-r--r--resources/js/ui.js643
-rw-r--r--script.js1152
17 files changed, 2051 insertions, 1292 deletions
diff --git a/index.html b/index.html
index e569d47..9a27020 100644
--- a/index.html
+++ b/index.html
@@ -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) + ' &nbsp; '
- + 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);
-})();