diff options
author | Daniel Weipert <code@drogueronin.de> | 2023-08-20 20:48:29 +0200 |
---|---|---|
committer | Daniel Weipert <code@drogueronin.de> | 2023-08-20 20:48:29 +0200 |
commit | 43a28ad77190f2e55e2e6ba65a9a7b5b1f5dea6c (patch) | |
tree | 759b4f27a68e729df313b9f8370117d4a6fbae37 | |
parent | b227fc9c2ef361f8aa5ee2f24aa1ade6bc972a73 (diff) |
area progression, item usage
-rw-r--r-- | db/_generated/areas/route1.json | 2 | ||||
-rw-r--r-- | db/areas/route1.json | 1 | ||||
-rw-r--r-- | index.html | 19 | ||||
-rw-r--r-- | resources/css/battle.css | 34 | ||||
-rw-r--r-- | resources/css/menu.css | 139 | ||||
-rw-r--r-- | resources/js/classes/InventoryItem.js | 3 | ||||
-rw-r--r-- | resources/js/classes/Monster.js | 4 | ||||
-rw-r--r-- | resources/js/classes/State.js | 5 | ||||
-rw-r--r-- | resources/js/classes/Trainer.js | 36 | ||||
-rw-r--r-- | resources/js/game.js | 131 | ||||
-rw-r--r-- | resources/js/main.js | 15 | ||||
-rw-r--r-- | resources/js/ui.js | 329 |
12 files changed, 552 insertions, 166 deletions
diff --git a/db/_generated/areas/route1.json b/db/_generated/areas/route1.json index 11eaf46..3956a3c 100644 --- a/db/_generated/areas/route1.json +++ b/db/_generated/areas/route1.json @@ -1 +1 @@ -{"encounter_slug":"route1","encounters":[{"monster":"pairagrin","encounter_rate":3.5,"daytime":true,"exp_req_mod":1,"level_range":[2,4]},{"monster":"aardorn","encounter_rate":3.5,"daytime":true,"exp_req_mod":1,"level_range":[2,4]},{"monster":"cataspike","encounter_rate":3.5,"daytime":true,"exp_req_mod":1,"level_range":[2,4]},{"monster":"pairagrin","encounter_rate":3.5,"daytime":false,"exp_req_mod":1,"level_range":[3,5]},{"monster":"aardorn","encounter_rate":3.5,"daytime":false,"exp_req_mod":1,"level_range":[3,5]},{"monster":"cataspike","encounter_rate":3.5,"daytime":false,"exp_req_mod":1,"level_range":[3,5]}],"requiredEncounters":10,"trainers":[{"name":"Bruder Mikki","monsters":[{"slug":"memnomnom","level":5},{"slug":"jelillow","level":5}],"inventory":[{"slug":"potion","quantity":2}]}],"environment_slug":"forest","previousArea":"paper-town","nextArea":"route2","environment":{"slug":"forest","battle_graphics":{"island_back":"woodland_island.png","island_front":"woodland_island.png","background":"forest_background.png"},"battle_music":"music_battle_loop"}}
\ No newline at end of file +{"encounter_slug":"route1","encounters":[{"monster":"pairagrin","encounter_rate":3.5,"daytime":true,"exp_req_mod":1,"level_range":[2,4]},{"monster":"aardorn","encounter_rate":3.5,"daytime":true,"exp_req_mod":1,"level_range":[2,4]},{"monster":"cataspike","encounter_rate":3.5,"daytime":true,"exp_req_mod":1,"level_range":[2,4]},{"monster":"pairagrin","encounter_rate":3.5,"daytime":false,"exp_req_mod":1,"level_range":[3,5]},{"monster":"aardorn","encounter_rate":3.5,"daytime":false,"exp_req_mod":1,"level_range":[3,5]},{"monster":"cataspike","encounter_rate":3.5,"daytime":false,"exp_req_mod":1,"level_range":[3,5]}],"requiredEncounters":10,"trainers":[{"name":"Bruder Mikki","sprite":"dragonrider.png","monsters":[{"slug":"memnomnom","level":5},{"slug":"jelillow","level":5}],"inventory":[{"slug":"potion","quantity":2}]}],"environment_slug":"forest","previousArea":"paper-town","nextArea":"route2","environment":{"slug":"forest","battle_graphics":{"island_back":"woodland_island.png","island_front":"woodland_island.png","background":"forest_background.png"},"battle_music":"music_battle_loop"}}
\ No newline at end of file diff --git a/db/areas/route1.json b/db/areas/route1.json index 54cc345..3d1cf23 100644 --- a/db/areas/route1.json +++ b/db/areas/route1.json @@ -8,6 +8,7 @@ "trainers": [ { "name": "Bruder Mikki", + "sprite": "dragonrider.png", "monsters": [ { "slug": "memnomnom", @@ -14,7 +14,8 @@ <div class="wrap"> <div id="battle"> <div id="battle__opponent"> - <img class="battle__monster-sprite__animation" src="" draggable="false" /> + <img class="battle__technique-animation" src="" draggable="false" /> + <img class="battle__opponent__trainer-sprite" src="" draggable="false" /> </div> <div id="battle__player"></div> @@ -25,10 +26,10 @@ <div id="status"> <div> Money: <span data-template-slot="money"></span> - Monster Progress: <span data-template-slot="monster-progress"></span> - Trainer Progress: <span data-template-slot="trainer-progress"></span> - <button data-template-slot="next-trainer">⇒ Next Trainer</button> - <button data-template-slot="next-area">⇒ Next Area</button> + Monster Progress: <span data-template-slot="monsterProgress"></span> + Trainer Progress: <span data-template-slot="trainerProgress"></span> + <button data-template-slot="nextTrainer">⇒ Next Trainer</button> + <button data-template-slot="nextArea">⇒ Next Area</button> </div> </div> @@ -226,7 +227,13 @@ </template> <template id="tpl___inventory"> - <div class="inventory"></div> + <div class="inventory"> + <div data-template-slot="items"></div> + <div data-template-slot="modes" class="inventory__selection-modes"> + <button data-template-slot="modeUse" data-selection-mode="use" class="menu-button">Use</button> + <button data-template-slot="modeInfo" data-selection-mode="info" class="menu-button">Info</button> + </div> + </div> </template> <template id="tpl___inventory__item"> diff --git a/resources/css/battle.css b/resources/css/battle.css index 2c09e5a..0052072 100644 --- a/resources/css/battle.css +++ b/resources/css/battle.css @@ -78,15 +78,25 @@ .battle__monster-img { cursor: pointer; transition-property: filter; + + width: 90px; } .battle__monster-img.damaged { filter: brightness(2); } -.battle__monster-sprite__animation { + +.battle__technique-animation { position: fixed; z-index: 10; } +.battle__opponent__trainer-sprite { + position: absolute; + top: 0; + right: 0; + height: 120px; +} + .battle__monster-technique { background-color: beige; border: 2px solid #000; @@ -97,18 +107,27 @@ cursor: pointer; } -.battle__monster-technique__name {} -.battle__monster-technique__types {} -.battle__monster-technique__power {} -.battle__monster--player { - flex-direction: row-reverse; + + + +.battle__monster--opponent { + align-items: flex-start; } + .battle__monster--opponent .battle__monster-info-exp, .battle__monster--opponent .battle__monster-technique { display: none; } +.battle__monster--player { + flex-direction: row-reverse; + align-items: flex-end; +} + + + + .exp-bar { width: 100%; } @@ -124,6 +143,9 @@ width: 0%; } + + + .hp-bar { flex-grow: 1; } diff --git a/resources/css/menu.css b/resources/css/menu.css index 38f4189..16a9d02 100644 --- a/resources/css/menu.css +++ b/resources/css/menu.css @@ -25,6 +25,8 @@ } + + .menu-button { font-size: 1.5rem; border: 1px solid #000; @@ -44,6 +46,8 @@ } + + .tabs { display: grid; } @@ -81,13 +85,6 @@ -.gender-icon { - line-height: 1em; -} - - - - #status { color: #fff; background-color: #000; @@ -96,6 +93,7 @@ + #menu { display: grid; grid-template-columns: 1fr 1fr; @@ -117,7 +115,64 @@ -.inventory {} + +.party__monsters { + display: grid; + grid-template-columns: 1fr 1fr 1fr; +} + +.party__monster { + cursor: pointer; + padding: 0.75rem; + text-align: center; + font-size: 0.75rem; +} +.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); + color: var(--color-success); +} + + + + +.monster-stats { + padding: 0.5rem; +} + +.monster-stats__identity { + display: flex; + justify-content: space-between; + align-items: center; +} + +.monster-stats table { + width: 100%; +} + +.monster-stats table th { + text-align: left; +} + +.monster-stats table td { + text-align: right; +} + +.gender-icon { + line-height: 1em; +} + + + .inventory__popup { align-items: start; @@ -140,6 +195,22 @@ text-align: center; } +.inventory__selection-modes { + display: flex; + padding: 1rem; +} + +.inventory__selection-modes button[selected] { + border-color: var(--color-success); + color: var(--color-success); +} + +.inventory__monster-selection { + display: grid; + grid-template-columns: 1fr 1fr 1fr; +} + + .menu__journal { @@ -192,55 +263,3 @@ font-size: 2rem; width: 80vw; } - - - -.party__monsters { - display: grid; - grid-template-columns: 1fr 1fr 1fr; -} - -.party__monster { - cursor: pointer; - padding: 0.75rem; - text-align: center; - font-size: 0.75rem; -} -.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); - color: var(--color-success); -} - - - -.monster-stats { - padding: 0.5rem; -} - -.monster-stats__identity { - display: flex; - justify-content: space-between; - align-items: center; -} - -.monster-stats table { - width: 100%; -} - -.monster-stats table th { - text-align: left; -} - -.monster-stats table td { - text-align: right; -} diff --git a/resources/js/classes/InventoryItem.js b/resources/js/classes/InventoryItem.js index d6eb311..9db42ff 100644 --- a/resources/js/classes/InventoryItem.js +++ b/resources/js/classes/InventoryItem.js @@ -12,8 +12,9 @@ class InventoryItem { /** * @param {Item} item */ - constructor (item) { + constructor (item, quantity = 1) { this.item = item; + this.quantity = quantity; } /* Item */ diff --git a/resources/js/classes/Monster.js b/resources/js/classes/Monster.js index 5c930f8..69bb654 100644 --- a/resources/js/classes/Monster.js +++ b/resources/js/classes/Monster.js @@ -66,6 +66,10 @@ class Monster { return DB.monsters[this.slug].types; } + get category () { + return DB.monsters[this.slug].category; + } + get moveset () { return DB.monsters[this.slug].moveset; } diff --git a/resources/js/classes/State.js b/resources/js/classes/State.js index 5d3cfff..6180c52 100644 --- a/resources/js/classes/State.js +++ b/resources/js/classes/State.js @@ -48,4 +48,9 @@ class State { * @type {Technique} */ activeTechnique = null; + + /** + * @type {InventoryItem} + */ + activeBall = null; }; diff --git a/resources/js/classes/Trainer.js b/resources/js/classes/Trainer.js index f2c2df9..d63e52d 100644 --- a/resources/js/classes/Trainer.js +++ b/resources/js/classes/Trainer.js @@ -7,6 +7,9 @@ class Trainer { */ type = 'trainer'; + name = ''; + sprite = ''; + /** * @type {Monster[]} */ @@ -22,28 +25,41 @@ class Trainer { */ activeMonster = []; - constructor (monsters, inventory = []) { - this.#monsters = monsters; - this.#inventory = inventory; + /** + * @param {(Monster[]|Object[])} monsters + * @param {(InventoryItem[]|Object[])} inventory + */ + constructor (trainerData) { + this.#monsters = trainerData.monsters; + this.#inventory = trainerData.inventory || []; + this.name = trainerData.name; + this.sprite = trainerData.sprite; } async initialize () { for (const monsterData of this.#monsters) { - const monster = await fetchMonster(monsterData.slug); + if (monsterData instanceof Monster) { + this.monsters.push(monsterData); + } else { + const monster = await fetchMonster(monsterData.slug); - monster.level = monsterData.level || monster.level; + monster.level = monsterData.level || monster.level; - this.monsters.push(monster); + this.monsters.push(monster); + } } this.activeMonster = this.monsters[0]; for (const itemData of this.#inventory) { - const item = new InventoryItem(await fetchItem(itemData.slug)); - - item.amount = itemData.amount || 1; + if (itemData instanceof InventoryItem) { + this.inventory.push(itemData); + } else { + const item = new InventoryItem(await fetchItem(itemData.slug)); - this.inventory.push(item); + item.quantity = itemData.quantity || 1; + this.inventory.push(item); + } } } } diff --git a/resources/js/game.js b/resources/js/game.js index 1a91f06..b233ef2 100644 --- a/resources/js/game.js +++ b/resources/js/game.js @@ -5,6 +5,7 @@ const Game = { postAction: [], }, + isLoadingArea: false, isInBattle: false, didTechniqueHit: false, @@ -49,7 +50,12 @@ const Game = { await Game.encounterWildMonster(); } else if (Memory.state.opponent.type === 'trainer') { if (Memory.state.opponent.activeMonster === Memory.state.opponent.monsters[Memory.state.opponent.monsters.length - 1]) { - await Game.progressToArea(await fetchArea(Memory.state.currentArea.nextArea)); + Memory.state.currentArea.trainerProgress++; + if (Memory.state.currentArea.encounters.length > 0) { + await Game.encounterWildMonster(); + } else { + await Game.progressToNextArea(); + } } else { await Game.encounterNextTrainerMonster(); } @@ -312,12 +318,16 @@ const Game = { * @param {MouseEvent} event */ async battleClick (event) { + if (Game.isLoadingArea) { + return; + } + Game.isInBattle = true; UI.battleClickEvent = event; await Game.tryUseTechnique(Memory.state.activeTechnique, Memory.state.player.activeMonster, Memory.state.opponent.activeMonster); - Game.progressTurn(); + await Game.progressTurn(); }, /** @@ -349,24 +359,94 @@ const Game = { }, /** + * @param {InventoryItem} item + * @param {Monster} monster + * + * @returns {boolean} + */ + canUseItem (item, monster = null) { + let isApplicable = true; + + for (const itemConditionCode of item.conditions) { + const itemCondition = new ItemCondition(itemConditionCode); + let conditionIsApplicable = true; + + if (itemCondition.what === 'current_hp') { + const value = parseInt(itemCondition.value) * monster.stats.hp; + conditionIsApplicable = eval(`${monster.hp} ${itemCondition.comparator} ${value}`); + } + + else if (itemCondition.what === 'wild_monster') { + conditionIsApplicable = Game.isBattleType('monster'); + } + + else if (itemCondition.what === 'threat') { + conditionIsApplicable = Memory.state.opponent.activeMonster.category === 'threat'; + } + + else { + conditionIsApplicable = false; + } + + if (itemCondition.is === 'not') { + conditionIsApplicable = !conditionIsApplicable; + } + + isApplicable = isApplicable && conditionIsApplicable; + } + + return isApplicable; + }, + + /** * @param {InventoryItem} + * @param {Monster} */ - async useItem (item) { + async useItem (item, monster) { for (const itemEffectCode of item.effects) { const itemEffect = new ItemEffect(itemEffectCode); - if (itemEffect.type === 'evolve') { + if (itemEffect.type === 'heal') { + monster.hp += itemEffect.amount; + item.quantity--; + } + + else if (itemEffect.type === 'capture') { + Memory.state.activeBall = item; + UI.drawActiveBall(); + } + + else if (itemEffect.type === 'evolve') { const evolution = Memory.state.player.activeMonster.getPossibleEvolutions('item')[0]; if (evolution) { await fetchMonster(evolution.monster_slug); Memory.state.player.activeMonster.evolve(evolution); UI.drawActiveMonster(); + + item.quantity--; } } } }, /** + * @param {Array} inventory + * @param {InventoryItem} item + */ + removeItemFromInventory (inventory, item) { + inventory.splice(inventory.indexOf(item), 1); + }, + + /** + * @param {string} type + * + * @returns {boolean} + */ + isBattleType (type) { + return Memory.state.opponent.type === type; + }, + + /** * @param {Monster} monster */ async evolveMonster (monster) { @@ -374,14 +454,29 @@ const Game = { monster.evolve(monster.evolutions[0]); }, + /** + * @returns {boolean} + */ + canCatchMonster () { + return Game.isBattleType('monster') && Memory.state.activeBall; + }, + async catchMonster () { - if (Memory.state.opponent.type === 'trainer') { + if (!Game.canCatchMonster()) { return; } + Memory.state.activeBall.quantity--; + if (Memory.state.activeBall.quantity === 0) { + Game.removeItemFromInventory(Memory.state.player.inventory, Memory.state.activeBall); + Memory.state.activeBall = null; + UI.drawActiveBall(); + } + const caughtMonster = new Monster(Memory.state.opponent.activeMonster.slug); caughtMonster.initialize(); caughtMonster.level = Memory.state.opponent.activeMonster.level; + caughtMonster.hp = Memory.state.opponent.activeMonster.hp; Memory.state.player.monsters.push(caughtMonster); @@ -401,17 +496,29 @@ const Game = { /** * @param {Area} area */ - async progressToArea (area) { + async jumpToArea (area) { Memory.state.currentArea = area; UI.drawArea(area); - UI.drawStatus(); + }, + + async progressToNextArea () { + Game.isLoadingArea = true; - if (area.encounters.length > 0) { + const currentArea = Memory.state.currentArea; + const nextArea = await fetchArea(currentArea.nextArea); + + await Game.jumpToArea(nextArea); + + if (nextArea.encounters.length > 0) { await Game.encounterWildMonster(); } else { await Game.encounterTrainer(); } + + UI.drawStatus(); + + Game.isLoadingArea = false; }, async encounterWildMonster () { @@ -421,7 +528,7 @@ const Game = { const monster = await fetchMonster(randomMonster.monster); monster.level = randomLevel; - const wildMonster = new Trainer([monster]); + const wildMonster = new Trainer({ monsters: [monster] }); wildMonster.type = 'monster'; await wildMonster.initialize(); Memory.state.opponent = wildMonster; @@ -432,7 +539,7 @@ const Game = { async encounterTrainer () { const nextTrainer = Memory.state.currentArea.trainers[Memory.state.currentArea.trainerProgress]; - const trainer = new Trainer(nextTrainer.monsters, nextTrainer.inventory); + const trainer = new Trainer(nextTrainer); if (nextTrainer.name === 'Rival') { trainer.monsters.push(Memory.state.rivalMonster); } @@ -440,6 +547,7 @@ const Game = { Memory.state.opponent = trainer; UI.drawOpponentMonster(); + UI.drawStatus(); }, async encounterNextTrainerMonster () { @@ -451,7 +559,8 @@ const Game = { }; // Game click bindings -UI.elements.status.querySelector('[data-template-slot="next-trainer"]').addEventListener('click', Game.encounterTrainer); +UI.elements.nextTrainer.addEventListener('click', Game.encounterTrainer); +UI.elements.nextArea.addEventListener('click', Game.progressToNextArea); UI.elements.battleOpponent.addEventListener('click', Game.battleClick); UI.elements.techniques.addEventListener('click', Game.techniqueClick); UI.elements.menuCatch.addEventListener('click', Game.catchMonster); diff --git a/resources/js/main.js b/resources/js/main.js index 815d734..8622731 100644 --- a/resources/js/main.js +++ b/resources/js/main.js @@ -4,23 +4,26 @@ // Start Game const possibleStarterMonsters = ['budaye', 'dollfin', 'grintot', 'ignibus', 'memnomnom']; - Memory.state.player = new Trainer( - [ + Memory.state.player = new Trainer({ + monsters: [ await fetchMonster(possibleStarterMonsters[Math.round(Math.random() * (possibleStarterMonsters.length - 1))]), ], - [ - new InventoryItem(await fetchItem('tuxeball')), + inventory: [ + new InventoryItem(await fetchItem('tuxeball'), 5), new InventoryItem(await fetchItem('potion')), ] - ); + }); await Memory.state.player.initialize(); Memory.state.activeTechnique = Memory.state.player.activeMonster.activeTechniques[0]; + Memory.state.activeBall = Memory.state.player.inventory[0]; // tuxeball Memory.state.rivalMonster = await fetchMonster(possibleStarterMonsters[Math.round(Math.random() * (possibleStarterMonsters.length - 1))]); const area = await fetchArea('paper-town'); - await Game.progressToArea(area); + await Game.jumpToArea(area); + await Game.encounterTrainer(); + UI.drawStatus(); UI.drawActiveMonster(); UI.drawActiveTechniques(); diff --git a/resources/js/ui.js b/resources/js/ui.js index 0d6a8f9..dbec9b2 100644 --- a/resources/js/ui.js +++ b/resources/js/ui.js @@ -37,12 +37,15 @@ const UI = { battle: document.querySelector('#battle'), battleOpponent: document.querySelector('#battle__opponent'), battleOpponentSprite: null, - battleOpponentAnimation: document.querySelector('.battle__monster-sprite__animation'), + battleOpponentTrainerSprite: document.querySelector('.battle__opponent__trainer-sprite'), + battleOpponentAnimation: document.querySelector('.battle__technique-animation'), battlePlayer: document.querySelector('#battle__player'), techniques: document.querySelector('#techniques'), status: document.querySelector('#status'), + nextTrainer: document.querySelector('#status [data-template-slot="nextTrainer"]'), + nextArea: document.querySelector('#status [data-template-slot="nextArea"]'), menuParty: document.querySelector('#menu__party'), menuInventory: document.querySelector('#menu__inventory'), @@ -355,13 +358,15 @@ const UI = { UI.elements.battleOpponentSprite = battleMonsterNode.querySelector('[data-template-slot="sprite"]'); UI.elements.battleOpponentSprite.style.transitionDuration = `${UI.damageHighlightClickDuration}s`; - // en/disable catch - if (Memory.state.opponent.type === 'trainer') { - UI.elements.menuCatch.setAttribute('disabled', true); + if (Game.isBattleType('trainer') && Memory.state.opponent.sprite) { + UI.elements.battleOpponentTrainerSprite.src = `/modules/tuxemon/mods/tuxemon/gfx/sprites/player/${Memory.state.opponent.sprite}`; } else { - UI.elements.menuCatch.removeAttribute('disabled'); + UI.elements.battleOpponentTrainerSprite.src = ''; } + // en/disable catch + UI.drawActiveBall(); + const previousBattleMonsterNode = UI.elements.battleOpponent.querySelector('.battle__monster'); if (previousBattleMonsterNode) { UI.elements.battleOpponentSprite.classList = previousBattleMonsterNode.querySelector('[data-template-slot="sprite"]').classList; @@ -564,19 +569,36 @@ const UI = { /* Menu */ partySelectionMode: 'select', + inventorySelectionMode: 'use', drawStatus () { - UI.elements.status.querySelector('[data-template-slot="money"]').textContent = `${Memory.state.money} €`; - UI.elements.status.querySelector('[data-template-slot="monster-progress"]').textContent = `${Memory.state.currentArea.monsterProgress} / ${'10'}`; - UI.elements.status.querySelector('[data-template-slot="trainer-progress"]').textContent = `${Memory.state.currentArea.trainerProgress} / ${Memory.state.currentArea.trainers.length}`; + const currentArea = Memory.state.currentArea; - const nextTrainerButton = UI.elements.status.querySelector('[data-template-slot="next-trainer"]'); - if (Memory.state.currentArea.monsterProgress >= Memory.state.currentArea.requiredEncounters) { + UI.elements.status.querySelector('[data-template-slot="money"]').textContent = `${Memory.state.money} €`; + UI.elements.status.querySelector('[data-template-slot="monsterProgress"]').textContent = `${currentArea.monsterProgress} / ${currentArea.requiredEncounters}`; + UI.elements.status.querySelector('[data-template-slot="trainerProgress"]').textContent = `${currentArea.trainerProgress} / ${currentArea.trainers.length}`; + + const nextTrainerButton = UI.elements.nextTrainer; + if ( + Memory.state.opponent.type === 'monster' && + currentArea.monsterProgress >= currentArea.requiredEncounters && + currentArea.trainerProgress < currentArea.trainers.length + ) { nextTrainerButton.disabled = false; } else { nextTrainerButton.disabled = true; } + + const nextAreaButton = UI.elements.nextArea; + if ( + currentArea.monsterProgress >= currentArea.requiredEncounters && + currentArea.trainerProgress === currentArea.trainers.length + ) { + nextAreaButton.disabled = false; + } else { + nextAreaButton.disabled = true; + } }, openPartyMenu () { @@ -586,13 +608,7 @@ const UI = { party.id = 'party'; for (const monsterIdx in Memory.state.player.monsters) { const monster = Memory.state.player.monsters[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.querySelector('[data-template-slot="name"]').textContent = monster.name; - partyMonster.querySelector('[data-template-slot="gender"]').innerHTML = UI.createGenderIcon(monster.gender).outerHTML; - partyMonster.querySelector('[data-template-slot="level"]').textContent = monster.level; - partyMonster.querySelector('[data-template-slot="hpText"]').textContent = `${monster.hp} / ${monster.stats.hp}`; + const partyMonster = UI.createPartyMonster(monster); partyMonster.addEventListener('click', async (event) => { // bubble up to partyNode @@ -648,9 +664,24 @@ const UI = { UI.drawPopup(popup); }, + drawActiveBall () { + if (Game.canCatchMonster()) { + UI.elements.menuCatch.removeAttribute('disabled'); + } else { + UI.elements.menuCatch.setAttribute('disabled', true); + } + + if (Memory.state.activeBall) { + UI.elements.menuCatch.querySelector('img').src = `/modules/tuxemon/mods/tuxemon/gfx/items/${Memory.state.activeBall.slug}.png`; + } else { + UI.elements.menuCatch.querySelector('img').src = `/modules/tuxemon/mods/tuxemon/gfx/items/tuxeball.png`; + } + }, + openInventoryMenu () { const popup = UI.createPopup(); const inventory = UI.createTemplate(Template.inventory); + inventory.id = 'inventory'; const tabs = { heal: { @@ -680,17 +711,7 @@ const UI = { }; for (const item of Memory.state.player.inventory) { - const inventoryItemNode = UI.createTemplate(Template.inventoryItem); - - inventoryItemNode.title = item.description; - - inventoryItemNode.querySelector('[data-template-slot="sprite"]').src = `/modules/tuxemon/mods/tuxemon/${item.sprite}`; - inventoryItemNode.querySelector('[data-template-slot="name"]').textContent = item.name; - inventoryItemNode.querySelector('[data-template-slot="quantity"]').textContent = item.quantity; - - inventoryItemNode.addEventListener('click', async () => { - Game.useItem(item); - }); + const inventoryItemNode = UI.createInventoryItem(item); if (['potion', 'revive'].includes(item.category)) { tabs['heal'].items.push(inventoryItemNode); @@ -726,7 +747,23 @@ const UI = { })); tabsNode.style.gridTemplateColumns = '1fr 1fr 1fr'; - inventory.appendChild(tabsNode); + inventory.querySelector('[data-template-slot="items"]').appendChild(tabsNode); + + const selectionModesNode = inventory.querySelector('[data-template-slot="modes"]'); + const selectionModeNodes = selectionModesNode.querySelectorAll('[data-selection-mode]'); + selectionModeNodes.forEach((node) => { + if (node.dataset.selectionMode === UI.inventorySelectionMode) { + node.setAttribute('selected', true); + } + + node.addEventListener('click', () => { + selectionModesNode.querySelector(`[data-selection-mode="${UI.inventorySelectionMode}"]`).removeAttribute('selected'); + + UI.inventorySelectionMode = node.dataset.selectionMode; + + node.setAttribute('selected', true); + }); + }); popup.querySelector('.popup').appendChild(inventory); popup.classList.add('inventory__popup'); @@ -750,42 +787,6 @@ const UI = { UI.drawPopup(popup); }, - openSaveDialog () { - const popup = UI.createPopup(); - const dialog = UI.createTemplate(Template.dialogSave); - - const saveData = Memory.save(); - - dialog.querySelector('[data-template-slot="saveData"]').value = saveData; - dialog.querySelector('[data-template-slot="saveClipboard"]').addEventListener('click', async () => { - if (navigator.clipboard) { - await navigator.clipboard.writeText(saveData); - alert('Saved to clipboard!'); - } else { - alert('ERROR: Browser can\'t copy to clipboard! You have to do it manually.'); - } - }); - - popup.querySelector('.popup').appendChild(dialog); - UI.drawPopup(popup); - }, - - openLoadDialog () { - const popup = UI.createPopup(); - const dialog = UI.createTemplate(Template.dialogLoad); - - dialog.querySelector('[data-template-slot="load"]').addEventListener('click', () => { - Memory.load( - dialog.querySelector('[data-template-slot="saveData"]').value.trim() - ); - - document.querySelectorAll('.popup__overlay').forEach((element) => element.remove()) - }); - - popup.querySelector('.popup').appendChild(dialog); - UI.drawPopup(popup); - }, - openSettingsMenu () { const popup = UI.createPopup(); const template = UI.createTemplate(Template.menuSettings); @@ -842,6 +843,26 @@ const UI = { }, + /* Menu - Party */ + + /** + * @param {Monster} + * + * @returns {HTMLElement} + */ + createPartyMonster (monster) { + 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.querySelector('[data-template-slot="name"]').textContent = monster.name; + partyMonster.querySelector('[data-template-slot="gender"]').innerHTML = UI.createGenderIcon(monster.gender).outerHTML; + partyMonster.querySelector('[data-template-slot="level"]').textContent = monster.level; + partyMonster.querySelector('[data-template-slot="hpText"]').textContent = `${monster.hp} / ${monster.stats.hp}`; + + return partyMonster; + }, + + /* Menu - Monster */ /** @@ -940,6 +961,9 @@ const UI = { return movesetListNode; }, + /** + * @param {Monster} monster + */ openStatsMenu (monster) { const popup = UI.createPopup(); const statusMenu = UI.createStatsMenu(monster); @@ -960,6 +984,181 @@ const UI = { UI.drawPopup(popup); }, + + + /* Menu - Inventory */ + + /** + * @param {InventoryItem} item + * + * @returns {HTMLElement} + */ + createInventoryItem (item) { + const inventoryItemNode = UI.createTemplate(Template.inventoryItem); + + inventoryItemNode.title = item.description; + inventoryItemNode.dataset.templateItem = item.slug; + + inventoryItemNode.querySelector('[data-template-slot="sprite"]').src = `/modules/tuxemon/mods/tuxemon/${item.sprite}`; + inventoryItemNode.querySelector('[data-template-slot="name"]').textContent = item.name; + inventoryItemNode.querySelector('[data-template-slot="quantity"]').textContent = item.quantity; + + inventoryItemNode.addEventListener('click', async () => { + if (UI.inventorySelectionMode === 'use') { + if (item.category === 'potion') { + UI.openItemMonsterSelection(item); + } + + else if (item.category === 'capture') { + Game.useItem(item); + } + } + else if (UI.inventorySelectionMode === 'info') { + UI.openItemInfo(item); + } + }); + + return inventoryItemNode; + }, + + /** + * @param {InventoryItem} item + */ + redrawInventoryItem (item) { + const itemNode = document.querySelector(`#inventory *[data-template-item="${item.slug}"]`); + if (item.quantity === 0) { + itemNode.remove(); + } + + const newNode = UI.createInventoryItem(item); + itemNode.replaceWith(newNode); + }, + + /** + * @param {InventoryItem} item + * @param {Monster[]} monsters + * + * @returns {HTMLElement} + */ + createItemMonsterSelection (item, monsters) { + const template = document.createElement('div'); + + /** + * @param {Monster} monster + * + * @returns {(HTMLElement|null)} + */ + const createMonsterNode = (monster) => { + if (!Game.canUseItem(item, monster)) { + return null; + } + + const monsterNode = UI.createPartyMonster(monster); + + monsterNode.addEventListener('click', async () => { + await Game.useItem(item, monster); + + if (item.quantity === 0) { + Game.removeItemFromInventory(Memory.state.player.inventory, item); + template.dispatchEvent(new Event('item:isExhausted')); + } else { + const replacingMonsterNode = createMonsterNode(monster); + if (replacingMonsterNode) { + monsterNode.replaceWith(replacingMonsterNode); + } else { + monsterNode.remove(); + template.dispatchEvent(new Event('monster:lostCondition')); + } + } + + UI.redrawInventoryItem(item); + }); + + return monsterNode; + }; + + for (const monster of monsters) { + const monsterNode = createMonsterNode(monster); + + monsterNode && template.appendChild(monsterNode); + } + + return template; + }, + + /** + * @param {InventoryItem} item + */ + openItemMonsterSelection (item) { + const popup = UI.createPopup(); + const template = UI.createItemMonsterSelection(item, Memory.state.player.monsters); + template.classList.add('inventory__monster-selection'); + + if (template.children.length === 0) { + alert('No applicable monsters.'); + return; + } + + template.addEventListener('item:isExhausted', () => popup.remove()); + template.addEventListener('monster:lostCondition', () => popup.remove()); + + popup.querySelector('.popup').appendChild(template); + + UI.drawPopup(popup); + }, + + /** + * @param {InventoryItem} item + */ + openItemInfo (item) { + const popup = UI.createPopup(); + const template = document.createElement('div'); + + template.textContent = item.conditions + ' -- ' + item.effects + ' -- ' + item.description; + + popup.querySelector('.popup').appendChild(template); + + UI.drawPopup(popup); + }, + + + /* Menu - Journal */ + + openSaveDialog () { + const popup = UI.createPopup(); + const dialog = UI.createTemplate(Template.dialogSave); + + const saveData = Memory.save(); + + dialog.querySelector('[data-template-slot="saveData"]').value = saveData; + dialog.querySelector('[data-template-slot="saveClipboard"]').addEventListener('click', async () => { + if (navigator.clipboard) { + await navigator.clipboard.writeText(saveData); + alert('Saved to clipboard!'); + } else { + alert('ERROR: Browser can\'t copy to clipboard! You have to do it manually.'); + } + }); + + popup.querySelector('.popup').appendChild(dialog); + UI.drawPopup(popup); + }, + + openLoadDialog () { + const popup = UI.createPopup(); + const dialog = UI.createTemplate(Template.dialogLoad); + + dialog.querySelector('[data-template-slot="load"]').addEventListener('click', () => { + Memory.load( + dialog.querySelector('[data-template-slot="saveData"]').value.trim() + ); + + document.querySelectorAll('.popup__overlay').forEach((element) => element.remove()) + }); + + popup.querySelector('.popup').appendChild(dialog); + UI.drawPopup(popup); + }, }; // UI element click bindings |