From 7b1c251fcb085dc37de439ea1137373f1905d82e Mon Sep 17 00:00:00 2001 From: Daniel Weipert Date: Wed, 23 Aug 2023 20:29:07 +0200 Subject: areas and capture and more --- resources/css/battle.css | 1 - resources/css/menu.css | 28 ++- resources/css/page.css | 10 + resources/css/town.css | 24 ++- resources/js/classes/Area.js | 3 + resources/js/classes/Item.js | 8 +- resources/js/classes/Monster.js | 15 ++ resources/js/classes/State.js | 5 + resources/js/classes/StatusEffect.js | 3 + resources/js/definitions.js | 1 + resources/js/formula.js | 82 ++++++- resources/js/game.js | 153 +++++++++----- resources/js/helpers.js | 20 ++ resources/js/main.js | 44 ++-- resources/js/memory.js | 31 ++- resources/js/ui.js | 399 ++++++++++++++++++++++++++++------- 16 files changed, 672 insertions(+), 155 deletions(-) (limited to 'resources') diff --git a/resources/css/battle.css b/resources/css/battle.css index 748dad1..681b2e6 100644 --- a/resources/css/battle.css +++ b/resources/css/battle.css @@ -7,7 +7,6 @@ flex-direction: column; justify-content: space-between; - background-image: url('/modules/tuxemon/mods/tuxemon/gfx/ui/combat/sea_background.png'); background-size: cover; } diff --git a/resources/css/menu.css b/resources/css/menu.css index 8b0cb0b..82ba18e 100644 --- a/resources/css/menu.css +++ b/resources/css/menu.css @@ -112,15 +112,24 @@ .status__area > span > span { margin: 0 0.5rem; } +.status__area img { + cursor: pointer; +} .status__numbers { - display: flex; - justify-content: space-between; + display: grid; + grid-template-columns: 1fr 1fr 1fr; } .status__numbers > span { display: flex; align-items: center; } +.status__numbers > span:nth-child(2) { + justify-content: center; +} +.status__numbers > span:nth-child(3) { + justify-content: flex-end; +} .status__numbers > span > span { margin-left: 0.5rem; } @@ -325,3 +334,18 @@ font-size: 2rem; width: 80vw; } + + + + +.menu__settings { + padding: 1rem; + display: grid; + grid-gap: 1rem; +} + +.setting-highlight { + border: 2px solid yellow; + stroke: yellow; + stroke-width: 2px; +} diff --git a/resources/css/page.css b/resources/css/page.css index 57c46a5..635a160 100644 --- a/resources/css/page.css +++ b/resources/css/page.css @@ -27,3 +27,13 @@ img { .hidden { display: none; } + +svg { + width: 100%; + height: 100%; + display: block; +} + +svg [data-location] { + cursor: pointer; +} diff --git a/resources/css/town.css b/resources/css/town.css index b8be144..832c3d8 100644 --- a/resources/css/town.css +++ b/resources/css/town.css @@ -1,11 +1,25 @@ #scene__town {} -#scene__town svg { - width: 100%; - height: 100%; - display: block; + + + +.healing-center { + padding: 1rem; +} + + + + +.shop { + padding: 1rem; + + display: grid; + grid-gap: 1rem; } -svg [data-location] { +.shop__item { + display: flex; + justify-content: space-between; + align-items: center; cursor: pointer; } diff --git a/resources/js/classes/Area.js b/resources/js/classes/Area.js index b0bd710..e8d7acc 100644 --- a/resources/js/classes/Area.js +++ b/resources/js/classes/Area.js @@ -35,6 +35,9 @@ class Area { return DB.areas[this.slug].map; } + /** + * @returns {Object[]} + */ get locations () { return DB.areas[this.slug].locations; } diff --git a/resources/js/classes/Item.js b/resources/js/classes/Item.js index 9da6a92..40089f2 100644 --- a/resources/js/classes/Item.js +++ b/resources/js/classes/Item.js @@ -20,7 +20,13 @@ class Item { } get sprite () { - return DB.items[this.slug].sprite; + const sprite = DB.items[this.slug].sprite; + + if (sprite) { + return '/modules/tuxemon/mods/tuxemon/' + sprite; + } + + return ''; } get conditions () { diff --git a/resources/js/classes/Monster.js b/resources/js/classes/Monster.js index 84ea5da..c058a75 100644 --- a/resources/js/classes/Monster.js +++ b/resources/js/classes/Monster.js @@ -62,6 +62,9 @@ class Monster { } } + /** + * @returns {string[]} + */ get types () { return DB.monsters[this.slug].types; } @@ -74,6 +77,18 @@ class Monster { return DB.monsters[this.slug].moveset; } + get catch_rate () { + return DB.monsters[this.slug].catch_rate; + } + + get upper_catch_resistance () { + return DB.monsters[this.slug].upper_catch_resistance; + } + + get lower_catch_resistance () { + return DB.monsters[this.slug].lower_catch_resistance; + } + /** * @returns {DB_Evolution[]} */ diff --git a/resources/js/classes/State.js b/resources/js/classes/State.js index 085d1c7..1a65efe 100644 --- a/resources/js/classes/State.js +++ b/resources/js/classes/State.js @@ -25,6 +25,11 @@ class State { */ currentArea = null; + /** + * @type {AreaSlug} + */ + lastVisitedTown = ''; + /** * @type {number} */ diff --git a/resources/js/classes/StatusEffect.js b/resources/js/classes/StatusEffect.js index e1ae9c9..d161761 100644 --- a/resources/js/classes/StatusEffect.js +++ b/resources/js/classes/StatusEffect.js @@ -18,6 +18,9 @@ class StatusEffect { else if (['charging'].includes(this.slug)) { this.turnsLeft = 2; } + else if (['faint'].includes(this.slug)) { + this.turnsLeft = Number.MAX_SAFE_INTEGER; + } else if (this.category === 'positive') { this.turnsLeft = Math.ceil(Math.random() * 6) + 4; } diff --git a/resources/js/definitions.js b/resources/js/definitions.js index 338fdf7..d399a3c 100644 --- a/resources/js/definitions.js +++ b/resources/js/definitions.js @@ -1,6 +1,7 @@ /** * @typedef {string} MonsterSlug * @typedef {string} TechniqueSlug + * @typedef {string} AreaSlug */ // diff --git a/resources/js/formula.js b/resources/js/formula.js index 7223cb8..f28ce2d 100644 --- a/resources/js/formula.js +++ b/resources/js/formula.js @@ -95,13 +95,89 @@ function calculateAwardedExperience (opposingMonster, participants) { /** * @param {Monster} opposingMonster * - * @returns {number[]} + * @returns {number} */ function calculateAwardedMoney (opposingMonster) { let money = opposingMonster.level * opposingMonster.moneyModifier; - const baseDecimalDiff = 2 - DB.currencies.map[Memory.state.Settings.currency].decimals; - money = money * Math.pow(10, baseDecimalDiff); + money = convertToCurrencyBase(money); return money; } + +/** + * @param {Monster} playerMonster + * @param {Monster} opposingMonster + * @param {Item} ball + * + * @returns {boolean} + */ +function checkCapture (playerMonster, opposingMonster, ball) { + const MAX_CATCH_RATE = 255; + const MAX_ATTEMPT_RATE = 65536; + const ATTEMPT_CONSTANT = 524325; + + // status effect + let STATUS_MODIFER = 1.0; + if (opposingMonster.statusEffect && opposingMonster.statusEffect.category === 'negative') { + STATUS_MODIFER = 1.2; + } + + // ball + let BALL_MODIFIER = 1.0; + if (ball.slug === 'tuxeball_wood') { + if (opposingMonster.types.includes(ElementType.wood)) { + BALL_MODIFIER = 1.5 + } else { + BALL_MODIFIER = 0.2; + } + } + else if (ball.slug === 'tuxeball_fire') { + if (opposingMonster.types.includes(ElementType.fire)) { + BALL_MODIFIER = 1.5 + } else { + BALL_MODIFIER = 0.2; + } + } + else if (ball.slug === 'tuxeball_earth') { + if (opposingMonster.types.includes(ElementType.earth)) { + BALL_MODIFIER = 1.5 + } else { + BALL_MODIFIER = 0.2; + } + } + else if (ball.slug === 'tuxeball_metal') { + if (opposingMonster.types.includes(ElementType.metal)) { + BALL_MODIFIER = 1.5 + } else { + BALL_MODIFIER = 0.2; + } + } + else if (ball.slug === 'tuxeball_water') { + if (opposingMonster.types.includes(ElementType.water)) { + BALL_MODIFIER = 1.5 + } else { + BALL_MODIFIER = 0.2; + } + } + + // calculate + const catchCheck = Math.max( + (3 * opposingMonster.hp - 2 * playerMonster.hp) + * opposingMonster.catch_rate + * STATUS_MODIFER + * BALL_MODIFIER + / (3 * opposingMonster.hp), + 1 + ); + + let attemptCheck = ATTEMPT_CONSTANT / ( + Math.sqrt(Math.sqrt(MAX_CATCH_RATE / catchCheck)) * 8 + ) + + const catchResistance = Math.random() * (opposingMonster.upper_catch_resistance - opposingMonster.lower_catch_resistance) + opposingMonster.lower_catch_resistance; + + attemptCheck = attemptCheck * catchResistance; + + return Math.random() * MAX_ATTEMPT_RATE > Math.round(attemptCheck); +} diff --git a/resources/js/game.js b/resources/js/game.js index 9370382..663e6b0 100644 --- a/resources/js/game.js +++ b/resources/js/game.js @@ -1,5 +1,6 @@ const Game = { phases: { + preTurnBegin: [], preTurn: [], battle: { preAction: { @@ -16,6 +17,7 @@ const Game = { }, }, postTurn: [], + postTurnEnd: [], }, logMessages: [], @@ -73,6 +75,11 @@ const Game = { Game.logTurn('end'); + for (const event of Game.phases.postTurnEnd) { + event(); + } + Game.phases.postTurnEnd = []; + UI.progressTurn(); Game.isProgressingTurn = false; }, @@ -133,29 +140,33 @@ const Game = { } Game.removeBattlePhaseEvents('action', 'opponent'); + Memory.state.player.activeMonster.statusEffect = await fetchStatusEffect('faint'); + // whole party defeated if (!Memory.state.player.monsters.some((monster) => monster.hp > 0)) { - if (Game.isBattleType('trainer')) { - if (Memory.state.currentArea.encounters.length > 0) { - await Game.encounterWildMonster(); - } else { - await Game.encounterTrainer(); - } - } + Game.isInBattle = false; + Memory.state.currentArea.monsterProgress = 0; - else if (Game.isBattleType('monster')) { - if (Memory.state.currentArea.monsterProgress < Memory.state.currentArea.requiredEncounters) { - Memory.state.currentArea.monsterProgress = 0; - UI.drawStatus(); - } - - await Game.encounterWildMonster(); - } + // go to last visited town + await Game.goToArea(Memory.state.lastVisitedTown); // heal all monsters full + let totalHealingCenterPrice = 0; for (const monster of Memory.state.player.monsters) { monster.hp = monster.stats.hp; + monster.statusEffect = null; + + // pay healing center + const healingCenterPrice = Object.values(Memory.state.currentArea.locations).find((location) => location.type === 'healingCenter').price; + totalHealingCenterPrice += healingCenterPrice; } + + Memory.state.money -= totalHealingCenterPrice; + + Game.addPhaseEvent('postTurnEnd', () => { + Game.log(`Whited out!`); + Game.log(`Payed ${formatPrice(totalHealingCenterPrice)} for full recovery at ${Memory.state.currentArea.name}!`); + }); } // party members still left @@ -189,7 +200,7 @@ const Game = { }, /** - * @param {('preTurn' | 'postTurn')} phase + * @param {('preTurnBegin' | 'preTurn' | 'postTurn' | 'postTurnEnd')} phase * @param {Function} event */ addPhaseEvent (phase, event) { @@ -459,6 +470,14 @@ const Game = { }); } + // confused + else if (monster.statusEffect.slug === 'confused') { + Game.addBattlePhaseEvent('preAction', monster, () => { + // TODO + logStatusIs(); + }); + } + // stuck else if (monster.statusEffect.slug === 'stuck') { for (const technique of monster.activeTechniques) { @@ -533,25 +552,15 @@ const Game = { }); }, - /** - * @param {MouseEvent} event - */ - async battleClick (event) { - if (Game.isLoadingArea || Game.isProgressingTurn) { - return; - } - - Game.isInBattle = true; - UI.battleClickEvent = event; - - // player - await Game.tryUseTechnique(Memory.state.activeTechnique, Memory.state.player.activeMonster, Memory.state.opponent.activeMonster); - - // opponent + async opponentTryUseTechnique () { if (!Game.opponentActionTimeout) { let speedDifference = Memory.state.opponent.activeMonster.stats.speed - Memory.state.player.activeMonster.stats.speed; if (speedDifference > 0) speedDifference = speedDifference / 2; - else if (speedDifference < 0) speedDifference = speedDifference * 2; + else if (speedDifference < 0 && speedDifference > -100) speedDifference = speedDifference * 2; + let levelDifference = Memory.state.opponent.activeMonster.level - Memory.state.player.activeMonster.level; + if (levelDifference >= 5) levelDifference = levelDifference * 2; + else if (levelDifference < 0 && levelDifference > -10) levelDifference = 0; + else if (levelDifference <= -10) levelDifference = levelDifference / 10; const opponentActiveMonster = Memory.state.opponent.activeMonster; Game.opponentActionTimeout = setTimeout(async () => { @@ -576,13 +585,31 @@ const Game = { } Game.opponentActionTimeout = null; - }, Math.max(500, 2000 - (speedDifference * 10))); + }, Math.max(levelDifference < 10 ? 500 : 50, Math.min(2000 - (speedDifference * 10) - (levelDifference * 100), 3000))); console.log( 'Opponent Attack Timeout', Memory.state.opponent.activeMonster.stats.speed, Memory.state.player.activeMonster.stats.speed, - 2000 - (speedDifference * 10) + 2000 - (speedDifference * 10) - (levelDifference * 100) ); } + }, + + /** + * @param {MouseEvent} event + */ + async battleClick (event) { + if (Game.isLoadingArea || Game.isProgressingTurn) { + return; + } + + Game.isInBattle = true; + UI.battleClickEvent = event; + + // player + await Game.tryUseTechnique(Memory.state.activeTechnique, Memory.state.player.activeMonster, Memory.state.opponent.activeMonster); + + // opponent + await Game.opponentTryUseTechnique(); await Game.progressTurn(); }, @@ -669,7 +696,7 @@ const Game = { } const nextTrainer = Memory.state.currentArea.trainers[nextTrainerIdx]; - if (nextTrainer.name === 'Rival') { + if (nextTrainer.name.startsWith('Rival')) { for (const idx in nextTrainer.monsters) { if (nextTrainer.monsters[idx].slug === 'STARTER') { nextTrainer.monsters[idx].slug = Memory.state.rivalMonster; @@ -704,16 +731,12 @@ const Game = { } Memory.state.currentArea = await fetchArea(areaSlug); - UI.drawArea(); if (Game.isTown(Memory.state.currentArea)) { - UI.elements.sceneBattle.classList.add('hidden'); - UI.elements.sceneTown.classList.remove('hidden'); - - UI.drawTown(); + if (Object.values(Memory.state.currentArea.locations).some((location) => location.type === 'healingCenter')) { + Memory.state.lastVisitedTown = areaSlug; + } } else { - UI.elements.sceneTown.classList.add('hidden'); - UI.elements.sceneBattle.classList.remove('hidden'); if (Memory.state.currentArea.encounters.length > 0) { await Game.encounterWildMonster(); } else if (Memory.state.currentArea.trainers.length > 0) { @@ -721,8 +744,7 @@ const Game = { } } - UI.drawStatus(); - UI.drawActiveBall(); + UI.drawArea(); Game.isLoadingArea = false; }, @@ -748,6 +770,10 @@ const Game = { conditionIsApplicable = eval(`${monster.hp} ${itemCondition.comparator} ${value}`); } + else if (itemCondition.what === 'status') { + conditionIsApplicable = monster.statusEffect && monster.statusEffect.slug === itemCondition.value.replace('status_', ''); + } + else if (itemCondition.what === 'wild_monster') { conditionIsApplicable = Game.isBattleType('monster'); } @@ -784,6 +810,13 @@ const Game = { UI.drawActiveMonster(); } + if (itemEffect.type === 'revive') { + monster.hp = itemEffect.amount; + monster.statusEffect = null; + item.quantity--; + UI.drawActiveMonster(); + } + else if (itemEffect.type === 'capture') { Memory.state.activeBall = item; UI.drawActiveBall(); @@ -825,15 +858,41 @@ const Game = { return; } - Game.clearCurrentTurn(); + const playerMonster = Memory.state.player.activeMonster; + const opposingMonster = Memory.state.opponent.activeMonster; + const activeBall = Memory.state.activeBall; - Memory.state.activeBall.quantity--; - if (Memory.state.activeBall.quantity === 0) { + // remove ball + activeBall.quantity--; + if (activeBall.quantity === 0) { Game.removeItemFromInventory(Memory.state.player.inventory, Memory.state.activeBall); Memory.state.activeBall = null; UI.drawActiveBall(); } + // attempt capture + Game.log('Attempting capture!'); + let success = true; + let attempts = 1; + const maxAttempts = 4; + while (success && attempts <= maxAttempts) { + success = checkCapture(playerMonster, opposingMonster, activeBall); + + if (!success) { + Game.log(`Escape attempt ${attempts}: succeeded!`); + Game.log(`${opposingMonster.name} broke free!`); + + Game.opponentTryUseTechnique(); + return; // can't catch + } + + Game.log(`Escape attempt ${attempts}: failed!`); + + attempts++; + } + + Game.clearCurrentTurn(); + const caughtMonster = new Monster(Memory.state.opponent.activeMonster.slug); caughtMonster.initialize(); caughtMonster.level = Memory.state.opponent.activeMonster.level; diff --git a/resources/js/helpers.js b/resources/js/helpers.js index e9cb37d..adf0bb7 100644 --- a/resources/js/helpers.js +++ b/resources/js/helpers.js @@ -63,3 +63,23 @@ function randomString () { function translate (msgid) { return DB.translations[Memory.state.Settings.language][msgid]; } + +/** + * @param {number} amount + * + * @returns {number} + */ +function convertToCurrencyBase (amount) { + const baseDecimalDiff = 2 - DB.currencies.map[Memory.state.Settings.currency].decimals; + + return amount * Math.pow(10, baseDecimalDiff); +} + +/** + * @param {number} price + * + * @returns {number} + */ +function formatPrice (price) { + return `${price} ${DB.currencies.map[Memory.state.Settings.currency].symbol}`; +} diff --git a/resources/js/main.js b/resources/js/main.js index 24eafec..6ea94e2 100644 --- a/resources/js/main.js +++ b/resources/js/main.js @@ -4,24 +4,36 @@ // Start Game const possibleStarterMonsters = ['budaye', 'dollfin', 'grintot', 'ignibus', 'memnomnom']; - Memory.state.player = new Trainer({ - monsters: [ - await fetchMonster(possibleStarterMonsters[Math.round(Math.random() * (possibleStarterMonsters.length - 1))]), - ], - inventory: [ - new InventoryItem(await fetchItem('tuxeball'), 5), - new InventoryItem(await fetchItem('potion')), - ] - }); - await Memory.state.player.initialize(); + const monsterSelection = UI.openStarterMonsterSelection( + await Promise.all(possibleStarterMonsters.map(async (monsterSlug) => await fetchMonster(monsterSlug))) + ); + monsterSelection.addEventListener('starter:monster:selected', async (event) => { + if (!confirm(`Select ${event.detail.monster.name}?`)) { + return; + } + + Memory.state.player = new Trainer({ + monsters: [ + event.detail.monster, + ], + inventory: [ + new InventoryItem(await fetchItem('tuxeball'), 5), + new InventoryItem(await fetchItem('potion')), + ] + }); + await Memory.state.player.initialize(); - Game.setActivePlayerMonster(Memory.state.player.monsters[0]); - Memory.state.activeBall = Memory.state.player.inventory[0]; // tuxeball + Game.setActivePlayerMonster(Memory.state.player.monsters[0]); + Memory.state.activeBall = Memory.state.player.inventory[0]; // tuxeball - Memory.state.rivalMonster = possibleStarterMonsters[Math.round(Math.random() * (possibleStarterMonsters.length - 1))]; + possibleStarterMonsters.splice(possibleStarterMonsters.indexOf(event.detail.monster), 1); + Memory.state.rivalMonster = possibleStarterMonsters[Math.round(Math.random() * (possibleStarterMonsters.length - 1))]; - await Game.goToArea('paper-town'); + await Game.goToArea('paper-town'); - UI.drawActiveMonster(); - UI.drawActiveTechniques(); + UI.drawActiveMonster(); + UI.drawActiveTechniques(); + + event.detail.popup.remove(); + }); })(); diff --git a/resources/js/memory.js b/resources/js/memory.js index 47cde88..40be4df 100644 --- a/resources/js/memory.js +++ b/resources/js/memory.js @@ -9,21 +9,39 @@ const Memory = { * @returns {string} */ save () { + const prepareSaveData = (saveData) => { + const prepareMonster = (monster) => { + if (monster.statusEffect && monster.statusEffect.slug === 'lifeleech') { + monster.statusEffect = null; + } + + return monster; + }; + + for (const idx in saveData.monsters) { + saveData.monsters[idx] = prepareMonster(saveData.monsters[idx]); + } + for (const idx in saveData.player.monsters) { + saveData.player.monsters[idx] = prepareMonster(saveData.player.monsters[idx]); + } + for (const idx in saveData.opponent.monsters) { + saveData.opponent.monsters[idx] = prepareMonster(saveData.opponent.monsters[idx]); + } + + return JSON.parse(JSON.stringify(saveData)); + }; + const saveMonster = (monsterData, monsterState) => { monsterData.level = monsterState.level; monsterData.hp = monsterState.hp; - if (monsterData.statusEffect && monsterData.statusEffect.slug === 'lifeleech') { - monsterData.statusEffect = null; - } - return monsterData; }; /** * @type {State} */ - const saveData = JSON.parse(JSON.stringify(Memory.state)); + const saveData = prepareSaveData(Object.assign({}, Memory.state)); // monsters for (const idx in saveData.monsters) { @@ -163,6 +181,7 @@ const Memory = { Memory.state.areaProgress[areaSlug] = await loadArea(areaData); } Memory.state.currentArea = await loadArea(loadedState.currentArea); + Memory.state.lastVisitedTown = loadedState.lastVisitedTown; Memory.state.turn = loadedState.turn; Memory.state.money = loadedState.money; @@ -179,7 +198,7 @@ const Memory = { Memory.state.activeTechnique = await loadTechnique(loadedState.activeTechnique); Memory.state.activeBall = await loadInventoryItem(loadedState.activeBall); - UI.drawArea(Memory.state.currentArea); + UI.drawArea(); UI.drawStatus(); UI.drawOpponentMonster(); UI.drawActiveMonster(); diff --git a/resources/js/ui.js b/resources/js/ui.js index 3940574..6f38867 100644 --- a/resources/js/ui.js +++ b/resources/js/ui.js @@ -14,6 +14,10 @@ const Template = { techniques: document.querySelector('#tpl___techniques'), technique: document.querySelector('#tpl___technique'), + healingCenter: document.querySelector('#tpl___healing-center'), + shop: document.querySelector('#tpl___shop'), + shopItem: document.querySelector('#tpl___shop__item'), + party: document.querySelector('#tpl___party'), partyMonster: document.querySelector('#tpl___party__monster'), @@ -51,6 +55,7 @@ const UI = { log: document.querySelector('#log'), status: document.querySelector('#status'), + showMap: document.querySelector('#status [data-template-slot="showMap"]'), nextTrainer: document.querySelector('#status [data-template-slot="nextTrainer"]'), changeArea: document.querySelector('#status [data-template-slot="changeArea"]'), @@ -274,6 +279,14 @@ const UI = { return document.createElement('i'); } + if (statusEffect.slug === 'faint') { + const node = document.createElement('b'); + node.innerHTML = 'X'; + node.title = statusEffect.name; + + return node; + } + const img = document.createElement('img'); img.src = `/modules/tuxemon/mods/tuxemon/gfx/ui/icons/status/icon_${statusEffect.slug}.png`; img.title = statusEffect.name; @@ -420,6 +433,10 @@ const UI = { * @returns {void} */ drawActiveTechniques () { + if (!Memory.state.player) { // on starter selection screen only + return; + } + const activeTechniques = UI.createActiveTechniques(Memory.state.player.activeMonster); activeTechniques.id = 'techniques'; @@ -495,8 +512,35 @@ const UI = { UI.elements.log.classList.toggle('log--is-hidden'); }, + openLog () { + UI.elements.log.classList.remove('log--is-hidden'); + }, + + closeLog () { + UI.elements.log.classList.add('log--is-hidden'); + }, + drawArea () { - UI.elements.battle.style.backgroundImage = `url(/modules/tuxemon/mods/tuxemon/gfx/ui/combat/${Memory.state.currentArea.environment.battle_graphics.background})`; + if (Game.isTown(Memory.state.currentArea)) { + UI.elements.sceneTown.querySelector('[data-template-slot="map"]').replaceChildren(UI.createMap()); + + UI.closeLog(); + + UI.elements.sceneBattle.classList.add('hidden'); + UI.elements.sceneTown.classList.remove('hidden'); + } else { + UI.elements.battle.style.backgroundImage = `url(/modules/tuxemon/mods/tuxemon/gfx/ui/combat/${Memory.state.currentArea.environment.battle_graphics.background})`; + + UI.elements.sceneTown.classList.add('hidden'); + UI.elements.sceneBattle.classList.remove('hidden'); + + UI.drawOpponentMonster(); + UI.drawActiveMonster(); + UI.drawActiveTechniques(); + } + + UI.drawStatus(); + UI.drawActiveBall(); }, progressTurn () { @@ -633,39 +677,120 @@ const UI = { /* Town */ - async drawTown () { + drawTown () {}, + + + /* Map */ + + /** + * @returns {HTMLElement} + */ + createMap () { + const template = document.createElement('div'); const currentArea = Memory.state.currentArea; - UI.elements.sceneTown.innerHTML = Memory.state.currentArea.map; + template.innerHTML = currentArea.map; + template.style.width = '100vw'; + template.style.maxWidth = '750px'; + + if (currentArea.locations) { + for (const locationId of Object.keys(currentArea.locations)) { + const location = currentArea.locations[locationId]; + + template.querySelector(`[data-location="${locationId}"]`).addEventListener('click', () => { + if (location.type === 'healingCenter') { + UI.openHealingCenter(location); + } + + else if (location.type === 'shop') { + UI.openShop(location); + } + }); + } + } + + return template; + }, - for (const locationId of Object.keys(currentArea.locations)) { - const location = currentArea.locations[locationId]; + /** + * @param {Object} healingCenter + */ + openHealingCenter (healingCenter) { + const popup = UI.createPopup(); + const template = UI.createTemplate(Template.healingCenter); + + const price = convertToCurrencyBase(healingCenter.price); + template.querySelector('[data-template-slot="price"]').innerHTML = formatPrice(price); - UI.elements.sceneTown.querySelector(`[data-location="${locationId}"]`).addEventListener('click', () => { - if (location.type === 'healingCenter') { - UI.openHealingCenter(location); + template.querySelector('[data-template-slot="heal"]').addEventListener('click', () => { + const applicableMonsters = Memory.state.player.monsters.filter((monster) => monster.hp < monster.stats.hp || monster.statusEffect); + if (applicableMonsters.length === 0) { + alert('No applicable monsters.'); + return; + } + + const monsterSelectionPopup = UI.createPopup(); + const monsterSelection = UI.createMonsterSelection(applicableMonsters); + + monsterSelection.addEventListener('monster:selected', (event) => { + if (Memory.state.money < price) { + alert(`Not enough ${DB.currencies.map[Memory.state.Settings.currency].symbol}.`); + return; } - else if (location.type === 'shop') { - UI.openShop(location); + Memory.state.money -= price; + event.detail.monster.hp = event.detail.monster.stats.hp; + event.detail.monster.statusEffect = null; + event.detail.node.remove(); + + if (monsterSelection.children.length === 0) { + monsterSelectionPopup.remove(); } + + UI.drawStatus(); }); - } - }, - openHealingCenter (healingCenter) {}, + monsterSelectionPopup.querySelector('.popup').appendChild(monsterSelection); + UI.drawPopup(monsterSelectionPopup); + }); + + popup.querySelector('.popup').appendChild(template); + UI.drawPopup(popup); + }, + /** + * @param {Object} shop + */ async openShop (shop) { const popup = UI.createPopup(); - const template = document.createElement('div'); + const template = UI.createTemplate(Template.shop); for (const itemData of shop.items) { + const price = convertToCurrencyBase(itemData.price); const item = await fetchItem(itemData.item_name); - const itemNode = document.createElement('div'); + const itemNode = UI.createTemplate(Template.shopItem); + + itemNode.querySelector('[data-template-slot="sprite"]').src = item.sprite; + itemNode.querySelector('[data-template-slot="name"]').innerHTML = item.name; + itemNode.querySelector('[data-template-slot="price"]').innerHTML = formatPrice(price); + + itemNode.addEventListener('click', () => { + if (Memory.state.money < price) { + alert(`Not enough ${DB.currencies.map[Memory.state.Settings.currency].symbol}.`); + return; + } + + Memory.state.money -= price; - itemNode.innerHTML = ``; - itemNode.innerHTML += `${item.name}`; - itemNode.innerHTML += `${itemData.price} ${DB.currencies.map[Memory.state.Settings.currency].symbol}`; + const itemInInventory = Memory.state.player.inventory.find((inventoryItem) => inventoryItem.slug === item.slug); + if (itemInInventory) { + itemInInventory.quantity++; + } else { + Memory.state.player.inventory.push(new InventoryItem(item, 1)); + } + + UI.drawStatus(); + }); template.appendChild(itemNode); } @@ -679,6 +804,7 @@ const UI = { partySelectionMode: 'select', inventorySelectionMode: 'use', + isHighlighting: false, /** @@ -727,6 +853,56 @@ const UI = { return template; }, + /** + * @param {Monster[]} monsters + * + * @returns {HTMLElement} + */ + createPartySelection (monsters) { + const party = UI.createTemplate(Template.party); + party.id = 'party'; + for (const monsterIdx in monsters) { + const monster = monsters[monsterIdx]; + const partyMonster = UI.createPartyMonster(monster); + + partyMonster.addEventListener('click', async (event) => { + // bubble up to partyNode + let target = event.target; + while (target.parentNode.id !== party.id) { + target = target.parentNode; + } + + party.dispatchEvent(new CustomEvent('party:monster:selected', { + detail: { + monster: monster, + mode: UI.partySelectionMode, + node: partyMonster, + }, + })); + }); + + 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); + }); + }); + + return party; + }, + drawStatus () { const currentArea = Memory.state.currentArea; @@ -739,15 +915,84 @@ const UI = { 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; + if (!Game.isTown(currentArea)) { + if ( + Memory.state.opponent.type === 'monster' && + currentArea.monsterProgress >= currentArea.requiredEncounters && + currentArea.trainerProgress < currentArea.trainers.length && + !Game.isInBattle + ) { + nextTrainerButton.disabled = false; + } else { + nextTrainerButton.disabled = true; + } } else { nextTrainerButton.disabled = true; } + + const changeAreaButton = UI.elements.changeArea; + if (!Game.isTown(currentArea)) { + if ( + Game.isInBattle || + (Memory.state.opponent && Memory.state.opponent.type === 'trainer') + ) { + changeAreaButton.disabled = true; + } else { + changeAreaButton.disabled = false; + } + } else { + changeAreaButton.disabled = false; + } + }, + + /** + * @param {Monster[]} monsters + */ + openStarterMonsterSelection (monsters) { + const popup = UI.createPopup().cloneNode(true); // remove close event + const template = UI.createPartySelection(monsters); + + const title = document.createElement('h1'); + title.textContent = 'Select your Tuxemon!'; + title.style.textAlign = 'center'; + template.prepend(title); + + template.addEventListener('party:monster:selected', (event) => { + const monster = event.detail.monster; + + if (UI.partySelectionMode === 'select') { + template.dispatchEvent(new CustomEvent('starter:monster:selected', { + detail: { + monster: monster, + node: event.detail.node, + popup: popup, + }, + })); + } + else if (UI.partySelectionMode === 'stats') { + UI.openStatsMenu(monster); + } + else if (UI.partySelectionMode === 'techniques') { + UI.openMovesetSelection(monster); + } + }); + + popup.querySelector('.popup').appendChild(template); + UI.drawPopup(popup); + + return template; + }, + + openMap () { + if (Game.isInBattle || Game.isTown(Memory.state.currentArea)) { + return; + } + + const popup = UI.createPopup(); + const template = UI.createMap(); + + popup.querySelector('.popup').appendChild(template); + UI.drawPopup(popup); }, openAreaSelection () { @@ -771,6 +1016,10 @@ const UI = { canGo = canGo && currentArea.trainerProgress >= currentArea.trainers.length; } + else if (condition.startsWith('area.')) { + canGo = Memory.state.areaProgress.hasOwnProperty(condition.replace('area.', '')); + } + else if (condition.startsWith('event.')) { canGo = false; } @@ -798,60 +1047,25 @@ const UI = { openPartyMenu () { const popup = UI.createPopup(); + const template = UI.createPartySelection(Memory.state.player.monsters); - const party = UI.createTemplate(Template.party); - party.id = 'party'; - for (const monsterIdx in Memory.state.player.monsters) { - const monster = Memory.state.player.monsters[monsterIdx]; - const partyMonster = UI.createPartyMonster(monster); - - 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') { - Game.setActivePlayerMonster(monster); - - 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, - }, - })); - }); + template.addEventListener('party:monster:selected', (event) => { + const monster = event.detail.monster; - party.querySelector('[data-template-slot="monsters"]').appendChild(partyMonster); - } + if (UI.partySelectionMode === 'select') { + Game.setActivePlayerMonster(monster); - 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); + popup.remove(); + } + else if (UI.partySelectionMode === 'stats') { + UI.openStatsMenu(monster); + } + else if (UI.partySelectionMode === 'techniques') { + UI.openMovesetSelection(monster); } - - 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); + popup.querySelector('.popup').appendChild(template); UI.drawPopup(popup); }, @@ -1067,13 +1281,44 @@ const UI = { const exchangedMoney = baseRateMoney * newCurrency.rate; Memory.state.money = Number(exchangedMoney.toFixed(newCurrency.decimals)); - UI.drawTown(); + UI.drawArea(); UI.drawStatus(); }); template.querySelector('[data-template-slot="currency.lastUpdated"]').textContent = DB.currencies.last_updated; + // Highlight + + template.querySelector('[data-template-slot="highlight"]').addEventListener('click', () => { + UI.isHighlighting = !UI.isHighlighting; + + const elements = [ + UI.elements.battleOpponent, + UI.elements.battlePlayer.querySelector('[data-template-slot="sprite"]'), + UI.elements.techniques, + ...UI.elements.sceneTown.querySelectorAll('[data-location]'), + UI.elements.showMap, + UI.elements.nextTrainer, + UI.elements.changeArea, + UI.elements.menuParty, + UI.elements.menuCatch, + UI.elements.menuInventory, + UI.elements.menuLog, + UI.elements.menuJournal, + UI.elements.menuSettings, + ]; + + for (const element of elements) { + if (UI.isHighlighting) { + element.classList.add('setting-highlight'); + } else { + element.classList.remove('setting-highlight'); + } + } + }); + + popup.querySelector('.popup').appendChild(template); UI.drawPopup(popup); }, @@ -1093,6 +1338,7 @@ const UI = { 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="statusEffect"]').innerHTML = UI.createStatusEffectIcon(monster.statusEffect).outerHTML; partyMonster.querySelector('[data-template-slot="hpText"]').textContent = `${monster.hp} / ${monster.stats.hp}`; return partyMonster; @@ -1235,7 +1481,7 @@ const UI = { inventoryItemNode.title = item.description; inventoryItemNode.dataset.inventoryItem = item.slug; - inventoryItemNode.querySelector('[data-template-slot="sprite"]').src = `/modules/tuxemon/mods/tuxemon/${item.sprite}`; + inventoryItemNode.querySelector('[data-template-slot="sprite"]').src = item.sprite; inventoryItemNode.querySelector('[data-template-slot="name"]').textContent = item.name; inventoryItemNode.querySelector('[data-template-slot="quantity"]').textContent = item.quantity; @@ -1245,6 +1491,10 @@ const UI = { UI.openItemMonsterSelection(item); } + else if (item.category === 'revive') { + UI.openItemMonsterSelection(item); + } + else if (item.category === 'capture') { Game.useItem(item); } @@ -1389,6 +1639,7 @@ const UI = { }; // UI element click bindings +UI.elements.showMap.addEventListener('click', UI.openMap); UI.elements.changeArea.addEventListener('click', UI.openAreaSelection); UI.elements.menuParty.addEventListener('click', UI.openPartyMenu); UI.elements.menuInventory.addEventListener('click', UI.openInventoryMenu); -- cgit v1.2.3