summaryrefslogtreecommitdiff
path: root/resources/js
diff options
context:
space:
mode:
authorDaniel Weipert <code@drogueronin.de>2023-08-23 20:29:07 +0200
committerDaniel Weipert <code@drogueronin.de>2023-08-23 20:29:07 +0200
commit7b1c251fcb085dc37de439ea1137373f1905d82e (patch)
tree32e3f2cd4367507726af6d0172e9621a37dff576 /resources/js
parent4dd1a344c6474087a3f8782dd54f5c7b4acc67ed (diff)
areas and capture and more
Diffstat (limited to 'resources/js')
-rw-r--r--resources/js/classes/Area.js3
-rw-r--r--resources/js/classes/Item.js8
-rw-r--r--resources/js/classes/Monster.js15
-rw-r--r--resources/js/classes/State.js5
-rw-r--r--resources/js/classes/StatusEffect.js3
-rw-r--r--resources/js/definitions.js1
-rw-r--r--resources/js/formula.js82
-rw-r--r--resources/js/game.js153
-rw-r--r--resources/js/helpers.js20
-rw-r--r--resources/js/main.js44
-rw-r--r--resources/js/memory.js31
-rw-r--r--resources/js/ui.js399
12 files changed, 617 insertions, 147 deletions
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
@@ -26,6 +26,11 @@ class State {
currentArea = null;
/**
+ * @type {AreaSlug}
+ */
+ lastVisitedTown = '';
+
+ /**
* @type {number}
*/
turn = 0;
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 = `<img src="/modules/tuxemon/mods/tuxemon/${item.sprite}" />`;
- 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);