const Game = { phases: { preTurnBegin: [], preTurn: [], battle: { preAction: { opponent: [], player: [], }, action: { opponent: [], player: [], }, postAction: { opponent: [], player: [], }, }, postTurn: [], postTurnEnd: [], }, logMessages: [], isLoadingArea: false, isProgressingTurn: false, playerIsChoosingNextMonster: false, isStoryBattle: false, didWinStoryBattle: true, doBattleAnimation: true, opponentActionTimeout: null, didTechniqueHit: false, /* Battle */ async progressTurn () { Game.isProgressingTurn = true; Memory.state.turn++; // status effects await Game.applyStatusEffect(Memory.state.opponent.activeMonster); await Game.applyStatusEffect(Memory.state.player.activeMonster); Game.logTurn('begin'); // Phases for (const event of Game.phases.preTurn) { event(); } Game.phases.preTurn = []; for (const phaseKey of Object.keys(Game.phases.battle)) { for (const event of Game.phases.battle[phaseKey].player) { event(); await Game.handleDefeatOpponent(); if (!Game.playerIsChoosingNextMonster) await Game.handleDefeatPlayer(); } Game.phases.battle[phaseKey].player = []; Game.doBattleAnimation = false; for (const event of Game.phases.battle[phaseKey].opponent) { event(); await Game.handleDefeatOpponent(); if (!Game.playerIsChoosingNextMonster) await Game.handleDefeatPlayer(); } Game.doBattleAnimation = true; Game.phases.battle[phaseKey].opponent = []; } for (const event of Game.phases.postTurn) { event(); } Game.phases.postTurn = []; Game.logTurn('end'); for (const event of Game.phases.postTurnEnd) { event(); } Game.phases.postTurnEnd = []; UI.progressTurn(); Game.isProgressingTurn = false; }, async handleDefeatOpponent () { if (Memory.state.opponent.activeMonster.hp <= 0) { clearTimeout(Game.opponentActionTimeout); Game.opponentActionTimeout = null; for (const phase of Object.keys(Game.phases.battle)) { Game.removeBattlePhaseEvents(phase, 'opponent'); } Game.removeBattlePhaseEvents('action', 'player'); // money const money = calculateAwardedMoney(Memory.state.opponent.activeMonster); Memory.state.money += money; Game.addPhaseEvent('postTurn', () => { Game.log(translate('game:battle:money:get').replace('{amount}', money)); }); // item if (Memory.state.currentArea.items.length > 0) { const itemDropChance = Memory.state.currentArea.monsterProgress * 0.1; const itemDropCheck = Math.random() * 100; if (itemDropChance > itemDropCheck) { const itemDropRatioMax = Memory.state.currentArea.items.reduce((accumulator, itemData) => accumulator + itemData.dropRatio, 0); const itemDropRatioCheck = Math.random() * itemDropRatioMax; let accumulator = 0; for (const itemData of Memory.state.currentArea.items) { const lowerBounds = accumulator; accumulator += itemData.dropRatio; const upperBounds = accumulator; if (itemDropRatioCheck >= lowerBounds && itemDropRatioCheck <= upperBounds) { await Game.addItemToInventory(Memory.state.player.inventory, itemData.slug, 1); const droppedItem = await fetchItem(itemData.slug); Game.addPhaseEvent('postTurn', () => { Game.log(translate('game:battle:item:get').replace('{name}', droppedItem.name)); }); break; } } } } // exp Memory.state.player.activeMonster.exp += calculateAwardedExperience(Memory.state.opponent.activeMonster, [Memory.state.player.activeMonster])[0]; if (Memory.state.player.activeMonster.canLevelUp()) { Memory.state.player.activeMonster.levelUp(); } if (Memory.state.player.activeMonster.canEvolve('standard')) { await Game.evolveMonster(Memory.state.player.activeMonster); } Memory.state.Game.isInBattle = false; if (Memory.state.opponent.type === 'monster') { if ( Memory.state.currentArea.monsterProgress < Memory.state.currentArea.requiredEncounters || Memory.state.currentArea.isCompleted ) { Memory.state.currentArea.monsterProgress++; } 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]) { Memory.state.currentArea.trainerProgress++; if (Memory.state.currentArea.encounters.length > 0) { await Game.encounterWildMonster(); } else if (Memory.state.currentArea.trainers.length > 0) { await Game.encounterTrainer(); } else { UI.showMap(); } Game.didWinStoryBattle = true; Game.isStoryBattle = false; } else { await Game.encounterNextTrainerMonster(); } } Memory.saveToLocalStorage(); } }, async handleDefeatPlayer () { if (Memory.state.player.activeMonster.hp <= 0) { clearTimeout(Game.opponentActionTimeout); Game.opponentActionTimeout = null; for (const phase of Object.keys(Game.phases.battle)) { Game.removeBattlePhaseEvents(phase, 'player'); } 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)) { Memory.state.Game.isInBattle = false; Game.didWinStoryBattle = false; Game.isStoryBattle = false; if (Memory.state.currentArea.monsterProgress < Memory.state.currentArea.requiredEncounters) { Memory.state.currentArea.monsterProgress = 0; } // go to last visited town await Game.goToArea(Memory.state.lastVisitedTown); // heal all monsters full Game.healParty(); const healingCenterPrice = Object.values(Memory.state.currentArea.locations).find((location) => location.type === 'healingCenter').price; const totalHealingCenterPrice = healingCenterPrice * Memory.state.player.monsters.length; Memory.state.money -= totalHealingCenterPrice; Game.addPhaseEvent('postTurnEnd', () => { Game.log(translate('game:battle:defeat')); Game.log( translate('game:battle:defeat:recovery_price') .replace('{amount}', formatPrice(totalHealingCenterPrice)) .replace('{name}', Memory.state.currentArea.name) ); }); Memory.saveToLocalStorage(); } // party members still left else { Game.playerIsChoosingNextMonster = true; const monsterSelectionNode = UI.createPlayerDefeatedMonsterSelection(); monsterSelectionNode.addEventListener('monster:selected', UI.wrapCallback(() => Memory.saveToLocalStorage())); UI.openPlayerDefeatedMonsterSelection(monsterSelectionNode); } } }, /** * @param {('preAction' | 'action' | 'postAction')} phase * @param {Monster} monster * @param {Function} event */ addBattlePhaseEvent (phase, monster, event) { if (monster === Memory.state.player.activeMonster) { Game.phases.battle[phase].player.push(event); } else { Game.phases.battle[phase].opponent.push(event); } }, /** * @param {('preAction' | 'action' | 'postAction')} phase * @param {('player' | 'opponent')} type */ removeBattlePhaseEvents (phase, type) { Game.phases.battle[phase][type] = []; }, /** * @param {('preTurnBegin' | 'preTurn' | 'postTurn' | 'postTurnEnd')} phase * @param {Function} event */ addPhaseEvent (phase, event) { Game.phases[phase].push(event); }, clearAllPhaseEvents () { Game.removeBattlePhaseEvents('preAction', 'player'); Game.removeBattlePhaseEvents('preAction', 'opponent'); Game.removeBattlePhaseEvents('action', 'player'); Game.removeBattlePhaseEvents('action', 'opponent'); Game.removeBattlePhaseEvents('postAction', 'player'); Game.removeBattlePhaseEvents('postAction', 'opponent'); }, clearCurrentTurn () { Game.clearAllPhaseEvents(); clearTimeout(Game.opponentActionTimeout); Game.opponentActionTimeout = null; }, /** * @param {Technique} technique * @param {Monster} user * @param {Monster} target */ async tryUseTechnique (technique, user, target) { let canUse = true; const log = (message, indentation) => { Game.addBattlePhaseEvent('preAction', user, () => { Game.log(message, indentation); }); }; log( translate('game:battle:technique:attempt') .replace('{monster.name}', user.name) .replace('{technique.name}', technique.name) ); // recharge if (technique.isRecharging()) { if (Game.doBattleAnimation) { const feedbackNode = UI.createActionFeedback('recharge'); feedbackNode.classList.add('recharge'); UI.drawActionFeedback(feedbackNode); } log(translate('game:battle:technique:attempt:fail:recharge'), 1); canUse = false; } // noddingoff if (user.statusEffect && user.statusEffect.slug === StatusEffectType.noddingoff) { if (Game.doBattleAnimation) { const feedbackNode = UI.createActionFeedback('noddingoff'); UI.drawActionFeedback(feedbackNode); } log(translate('game:battle:technique:attempt:fail:noddingoff'), 1); canUse = false; } if (canUse) { // hit? const accuracy = Math.random(); Game.didTechniqueHit = technique.accuracy >= accuracy; if (!Game.didTechniqueHit) { technique.use(); Game.doBattleAnimation && UI.drawDamageMiss(UI.createDamageMiss()); log(translate('game:battle:technique:attempt:fail:miss'), 1); return; } await Game.useTechnique(technique, user, target); } }, /** * @param {Technique} technique * @param {Monster} user * @param {Monster} target */ async useTechnique (technique, user, target) { technique.use(); Game.addBattlePhaseEvent('action', user, () => { Game.log( translate('game:battle:technique:use') .replace('{monster.name}', user.name) .replace('{technique.name}', technique.name) ); }); for (const techniqueEffectCode of technique.effects) { const techniqueEffect = new TechniqueEffect(techniqueEffectCode); techniqueEffect.setUser(user); techniqueEffect.setTarget(target); // damage if (['damage', 'splash', 'area'].includes(techniqueEffect.type)) { Game.addBattlePhaseEvent('action', user, () => { const damage = simpleDamageCalculation(technique, user, target); target.hp -= damage; if (Game.doBattleAnimation) { const damageNode = UI.createDamage(damage); UI.applyMultiplierToDamage(damageNode, simpleDamageMultiplier(technique.types, target.types)); UI.applyTechniqueToDamage(damageNode, technique); UI.drawDamage(damageNode); UI.drawTechniqueAnimation(technique); } Game.log( translate('game:battle:technique:use:damage') .replace('{amount}', damage) .replace('{name}', target.name), 1 ); }); } // money else if (techniqueEffect.type === 'money') { Game.addBattlePhaseEvent('action', user, () => { const money = Math.max(1, Math.floor(Math.random() * target.level)); Memory.state.money += money; if (Game.doBattleAnimation) { const damageNode = UI.createDamage(`${money} €`); UI.applyTechniqueToDamage(damageNode, technique); UI.drawDamage(damageNode); UI.drawTechniqueAnimation(technique); } }); } // healing else if (techniqueEffect.type === 'healing') { for (const recipient of techniqueEffect.recipients) { Game.addBattlePhaseEvent('action', user, () => { const heal = (user.level + 7) * technique.healingPower; recipient.hp += heal; Game.log( translate('game:battle:technique:use:healing') .replace('{amount}', heal), 1 ); }); } } // switch else if (techniqueEffect.type === 'switch') { techniqueEffect.recipient.types = [techniqueEffect.switchType]; } // enhance else if (techniqueEffect.type === 'enhance') { Game.doBattleAnimation && UI.drawTechniqueAnimation(technique); } // status effect else if (techniqueEffect.type === 'status') { const statusEffect = await fetchStatusEffect(techniqueEffect.statusEffect); if (statusEffect.slug === 'lifeleech') { statusEffect.issuer = user; } Game.addBattlePhaseEvent('action', user, () => { // add status effect const potency = Math.random(); const success = technique.potency >= potency; if (success) { for (const recipient of techniqueEffect.recipients) { // TODO: check replace if (recipient.statusEffect) continue; recipient.statusEffect = statusEffect; Game.log( translate('game:battle:technique:use:status') .replace('{monster.name}', recipient.name) .replace('{status_effect.name}', translate(`status_${statusEffect.slug}`)), 1 ); } } }); Game.doBattleAnimation && UI.drawTechniqueAnimation(technique); } } }, /** * @param {Monster} monster */ async applyStatusEffect (monster) { if (!monster.statusEffect) { return; } if (monster.statusEffect.turnsLeft === 0) { Game.addBattlePhaseEvent('preAction', monster, () => { monster.statusEffect.onRemove && monster.statusEffect.onRemove(); // if still 0 turns left after remove action if (monster.statusEffect.turnsLeft === 0) { Game.log( translate('game:battle:status_effect:removed') .replace('{monster.name}', monster.name) .replace('{status_effect.name}', translate(`status_${monster.statusEffect.slug}`)) ); monster.statusEffect = null; } else { Game.applyStatusEffect(monster); } }); return; } const logStatusIs = () => { Game.log( translate('game:battle:status_effect:is') .replace('{monster.name}', monster.name) .replace('{status_effect.name}', translate(`status_${monster.statusEffect.slug}`)) ); } // poison / burn if (monster.statusEffect.slug === 'poison' || monster.statusEffect.slug === 'burn') { const statusEffectDamage = Math.floor(monster.stats.hp / 8); Game.addBattlePhaseEvent('postAction', monster, () => { monster.hp -= statusEffectDamage; if (Game.doBattleAnimation) { const damageNode = UI.createDamage(statusEffectDamage); UI.applyStatusEffectToDamage(damageNode, monster.statusEffect); UI.drawDamage(damageNode); } logStatusIs(); }); } // lifeleech else if (monster.statusEffect.slug === 'lifeleech') { const statusEffectLeech = Math.floor(monster.stats.hp / 16); Game.addBattlePhaseEvent('postAction', monster, () => { // if issuer is defeated => don't if (monster.statusEffect.issuer.hp <= 0) { return; } monster.hp -= statusEffectLeech; monster.statusEffect.issuer.hp += statusEffectLeech; if (Game.doBattleAnimation) { const damageNode = UI.createDamage(statusEffectLeech); UI.applyStatusEffectToDamage(damageNode, monster.statusEffect); UI.drawDamage(damageNode); } logStatusIs(); }); } // recover else if (monster.statusEffect.slug === 'recover') { const statusEffectHeal = Math.floor(monster.stats.hp / 16); Game.addBattlePhaseEvent('postAction', monster, () => { monster.hp += statusEffectHeal; if (Game.doBattleAnimation) { const feedbackNode = UI.createActionFeedback(statusEffectHeal); UI.applyStatusEffectToDamage(feedbackNode, monster.statusEffect); UI.drawActionFeedback(feedbackNode); } logStatusIs(); }); } // 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) { if ([TechniqueRange.melee, TechniqueRange.touch].includes(technique.range)) { Game.addBattlePhaseEvent('preAction', monster, () => { technique.potency = technique.stats.potency * 0.5; technique.power = technique.stats.power * 0.5; logStatusIs(); }); 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.addBattlePhaseEvent('preAction', monster, () => { technique.potency = technique.stats.potency * 0.5; technique.power = technique.stats.power * 0.5; logStatusIs(); }); monster.statusEffect.onRemove = () => { technique.resetStats(); }; } } } // charging else if (monster.statusEffect.slug === 'charging') { const nextStatusEffect = await fetchStatusEffect('chargedup'); Game.addBattlePhaseEvent('preAction', monster, () => { logStatusIs(); }); 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.addBattlePhaseEvent('preAction', monster, () => { monster.setStatModifier(statType, modifiedValue); logStatusIs(); }); } monster.statusEffect.onRemove = () => { monster.resetStatModifiers(); }; } Game.addBattlePhaseEvent('postAction', monster, () => { monster.statusEffect.turnsLeft--; }); }, 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 > -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(UI.wrapCallback(async () => { if (opponentActiveMonster.hp <= 0) { Game.opponentActionTimeout = null; return; } // technique Game.doBattleAnimation = false; await Game.tryUseTechnique( await fetchTechnique(Memory.state.opponent.activeMonster.getLearnableTechniques()[Math.floor(Math.random() * Memory.state.opponent.activeMonster.getLearnableTechniques().length)].technique), Memory.state.opponent.activeMonster, Memory.state.player.activeMonster ); Game.doBattleAnimation = true; await Game.progressTurn(); // item if (Memory.state.opponent.inventory.length > 0) { } Game.opponentActionTimeout = null; }), 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) - (levelDifference * 100) ); */ } }, /** * @param {MouseEvent} event */ async battleClick (event) { if (Game.isLoadingArea || Game.isProgressingTurn) { return; } Memory.state.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(); }, /** * @param {MouseEvent} event */ techniqueClick (event) { if (event.target === UI.elements.techniques) { return; } let target = event.target; while (target.dataset.gameElementType !== 'menuBattleTechniquesTechnique') { target = target.parentNode; } const idx = [...UI.elements.techniques.children].indexOf(target); Memory.state.activeTechnique = Memory.state.player.activeMonster.activeTechniques[idx]; // trigger battle click const rect = UI.elements.battleOpponent.getBoundingClientRect(); const xMin = rect.left + 64; const xMax = rect.right - 64; const yMin = rect.top + 32; const yMax = rect.bottom - 32; UI.elements.battleOpponent.dispatchEvent(new MouseEvent('click', { clientX: Math.random() * (xMax - xMin) + xMin, clientY: Math.random() * (yMax - yMin) + yMin, })); }, /** * @param {string} message * @param {number} indentation * @param {string} style */ log (message, indentation = 0, style) { Game.logMessages.push({ message: message, indentation: indentation, style: style, }); UI.drawLog(); }, /** * @param {('begin' | 'end')} */ logTurn (state) { Game.log( '- '.repeat(8) + `Turn ${Memory.state.turn} · ${slugToName(state)}` + ' -'.repeat(8), 0, { textAlign: 'center', } ); }, /* Progression */ async encounterWildMonster () { let randomMonster = null; const randomNumber = Math.random() * Memory.state.currentArea.encounterPercentTotal; let accumulator = 0; for (const encounter of Memory.state.currentArea.encounters) { const lowerBounds = accumulator; accumulator += encounter.encounter_percent; const upperBounds = accumulator; if (randomNumber >= lowerBounds && randomNumber <= upperBounds) { randomMonster = encounter; break; } } const randomLevel = Math.floor(Math.random() * (randomMonster.level_range[1] - randomMonster.level_range[0]) + randomMonster.level_range[0]); const monster = await fetchMonster(randomMonster.monster); monster.level = randomLevel; Game.encounterMonster(monster); }, /** * @param {Monster} monster */ async encounterMonster (monster) { const encounterMonster = new Trainer({ monsters: [monster] }); encounterMonster.type = 'monster'; await encounterMonster.initialize(); Memory.state.opponent = encounterMonster; UI.drawOpponentMonster(); }, async encounterTrainer () { Game.clearCurrentTurn(); let nextTrainerIdx = Memory.state.currentArea.trainerProgress; while (nextTrainerIdx > Memory.state.currentArea.trainers.length - 1) { nextTrainerIdx -= Memory.state.currentArea.trainers.length; } const nextTrainer = Memory.state.currentArea.trainers[nextTrainerIdx]; const trainer = new Trainer(nextTrainer); await trainer.initialize() Memory.state.opponent = trainer; UI.drawOpponentMonster(); UI.drawStatus(); }, async encounterNextTrainerMonster () { const activeMonsterIdx = Memory.state.opponent.monsters.indexOf(Memory.state.opponent.activeMonster); Memory.state.opponent.activeMonster = Memory.state.opponent.monsters[activeMonsterIdx + 1]; UI.drawOpponentMonster(); }, /** * @param {string} areaSlug */ async goToArea (areaSlug) { Game.isLoadingArea = true; Game.clearCurrentTurn(); // on leave let onLeaveStoryIsDone = true; if (Memory.state.currentArea.events?.onLeave?.length > 0) { Game.isLoadingArea = false; for (const event of Memory.state.currentArea.events.onLeave) { if (event.type === 'story') { UI.showMap(); UI.drawTown(); onLeaveStoryIsDone = await Story.progress(event.story); } } } if (!onLeaveStoryIsDone) { return; } Game.isLoadingArea = true; // after on leave events resolved if (Memory.state.currentArea) { Memory.state.areaProgress[Memory.state.currentArea.slug] = Memory.state.currentArea; } Memory.state.currentArea = await fetchArea(areaSlug); // on enter let onEnterStoryIsDone = true; if (Memory.state.currentArea.events?.onEnter?.length > 0) { Game.isLoadingArea = false; for (const event of Memory.state.currentArea.events.onEnter) { if (event.type === 'story') { UI.showMap(); UI.drawTown(); onEnterStoryIsDone = await Story.progress(event.story); } } } if (!onEnterStoryIsDone) { return; } Game.isLoadingArea = true; // after on enter events resolved if (Game.isTown(Memory.state.currentArea)) { if (Object.values(Memory.state.currentArea.locations).some((location) => location.type === 'healingCenter')) { Memory.state.lastVisitedTown = areaSlug; } } else { if (Memory.state.currentArea.encounters.length > 0) { await Game.encounterWildMonster(); } else if (Memory.state.currentArea.trainers.length > 0) { await Game.encounterTrainer(); } } UI.drawArea(); Memory.saveToLocalStorage(); Game.isLoadingArea = false; }, /* Menu - Inventory */ /** * @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 === 'status') { conditionIsApplicable = monster.statusEffect && monster.statusEffect.slug === itemCondition.value.replace('status_', ''); } else if (itemCondition.what === 'wild_monster') { conditionIsApplicable = Game.isBattleType('monster'); } else if (itemCondition.what === 'threat') { conditionIsApplicable = Memory.state.opponent.activeMonster.category === 'threat'; } else if (itemCondition.what === 'level') { const value = parseInt(itemCondition.value); conditionIsApplicable = eval(`${monster.level} ${itemCondition.comparator} ${value}`); } else if (itemCondition.what === 'has_path') { conditionIsApplicable = monster.evolutions.some((evolution) => evolution.path === 'item' && evolution.item === item.slug) } else { conditionIsApplicable = false; } if (itemCondition.is === 'not') { conditionIsApplicable = !conditionIsApplicable; } isApplicable = isApplicable && conditionIsApplicable; } return isApplicable; }, /** * @param {InventoryItem} * @param {Monster} */ async useItem (item, monster) { let useLowersQuantity = false; for (const itemEffectCode of item.effects) { const itemEffect = new ItemEffect(itemEffectCode); if (itemEffect.type === 'heal') { monster.hp += itemEffect.amount; useLowersQuantity = true; UI.drawActiveMonster(); } else if (itemEffect.type === 'revive') { monster.hp = itemEffect.amount; monster.statusEffect = null; useLowersQuantity = true; UI.drawActiveMonster(); } else if (itemEffect.type === 'capture') { Memory.state.activeBall = item.slug; UI.drawActiveBall(); } else if (itemEffect.type === 'evolve') { const evolution = monster.evolutions.find((evolution) => evolution.path === 'item' && evolution.item === item.slug); if (evolution) { await fetchMonster(evolution.monster_slug); monster.evolve(evolution); useLowersQuantity = true; UI.drawActiveMonster(); } } } // decrease quantity if (useLowersQuantity) { item.quantity--; // remove from inventory if (item.quantity === 0) { Game.removeItemFromInventory(Memory.state.player.inventory, item.slug); } } Memory.saveToLocalStorage(); }, /** * @param {InventoryItem[]} inventory * @param {ItemSlug} itemSlug * * @returns {(InventoryItem|undefined)} */ getItemFromInventory (inventory, itemSlug) { return inventory.find((inventoryItem) => inventoryItem.slug === itemSlug); }, /** * @param {InventoryItem[]} inventory * @param {ItemSlug} itemSlug * @param {number} quantity */ async addItemToInventory (inventory, itemSlug, quantity = 1) { const inventoryItem = Game.getItemFromInventory(inventory, itemSlug); if (inventoryItem) { inventoryItem.quantity += quantity; } else { inventory.push(new InventoryItem(await fetchItem(itemSlug), quantity)); } }, /** * @param {InventoryItem[]} inventory * @param {ItemSlug} itemSlug */ removeItemFromInventory (inventory, itemSlug) { inventory.splice(inventory.findIndex((inventoryItem) => inventoryItem.slug === itemSlug), 1); }, /* Menu - Catch */ /** * @returns {boolean} */ canCatchMonster () { return !Game.isTown(Memory.state.currentArea) && Game.isBattleType('monster') && Memory.state.activeBall; }, async catchMonster () { if (!Game.canCatchMonster()) { return; } const playerMonster = Memory.state.player.activeMonster; const opposingMonster = Memory.state.opponent.activeMonster; const activeBall = Game.getItemFromInventory(Memory.state.player.inventory, Memory.state.activeBall); // remove ball Game.log(translate('game:catch:ball:throw').replace('{ball}', activeBall.name)); activeBall.quantity--; if (activeBall.quantity === 0) { Game.removeItemFromInventory(Memory.state.player.inventory, Memory.state.activeBall); Memory.state.activeBall = ''; UI.drawActiveBall(); } // attempt capture Game.log(translate('game:catch:attempt'), 1); let success = true; let attempts = 1; const maxAttempts = 4; while (success && attempts <= maxAttempts) { success = checkCapture(playerMonster, opposingMonster, activeBall); if (!success) { Game.log(translate('game:catch:attempt:nr:success').replace('{nr}', attempts), 2); Game.log(translate('game:catch:broke_free').replace('{monster}', opposingMonster.name), 1); Memory.state.Game.isInBattle = true; UI.drawStatus(); Game.opponentTryUseTechnique(); return; // can't catch } Game.log(translate('game:catch:attempt:nr:fail').replace('{nr}', attempts), 2); attempts++; } Game.clearCurrentTurn(); 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; Game.log(translate('game:catch:caught').replace('{monster}', caughtMonster.name), 1); if (Memory.state.player.monsters.length < 6) { Memory.state.player.monsters.push(caughtMonster); Game.log(translate('game:catch:to_party').replace('{monster}', caughtMonster.name), 2); } else { Memory.state.monsters.push(caughtMonster); Game.log(translate('game:catch:to_box').replace('{monster}', caughtMonster.name), 2); } await Game.encounterWildMonster(); Memory.saveToLocalStorage(); }, /* Helper */ /** * @param {Monster} monster */ setActivePlayerMonster (monster) { Memory.state.player.activeMonster = monster; Memory.state.activeTechnique = Memory.state.player.activeMonster.activeTechniques[0]; UI.drawActiveMonster(); UI.drawActiveTechniques(); }, /** * @param {Monster} monster */ async evolveMonster (monster) { await fetchMonster(monster.evolutions[0].monster_slug); monster.evolve(monster.evolutions[0]); }, healParty () { for (const monster of Memory.state.player.monsters) { monster.hp = monster.stats.hp; monster.statusEffect = null; } }, /** * @param {string} type * * @returns {boolean} */ isBattleType (type) { return Memory.state.opponent.type === type; }, /** * @param {Area} area * * @returns {boolean} */ isTown (area) { return area.encounters.length === 0 && area.trainers.length === 0; }, getStageOfDaySimple () { const hours = (new Date()).getHours(); if (hours >= 6 && hours < 18) { return 'day'; } else { return 'night'; } }, }; // Game click bindings UI.elements.nextTrainer.addEventListener('click', UI.wrapCallback(Game.encounterTrainer)); UI.elements.battleOpponent.addEventListener('click', UI.wrapCallback(Game.battleClick)); UI.elements.techniques.addEventListener('click', UI.wrapCallback(Game.techniqueClick)); UI.elements.menuCatch.addEventListener('click', UI.wrapCallback(Game.catchMonster));