summaryrefslogtreecommitdiff
path: root/resources/js
diff options
context:
space:
mode:
authorDaniel Weipert <code@drogueronin.de>2023-08-30 21:35:28 +0200
committerDaniel Weipert <code@drogueronin.de>2023-08-30 21:35:28 +0200
commit5214cdfbf26bc0bdee5d669a237fb8aefffb78d5 (patch)
tree7bec19278a0cf8fc1772e8eb6d985391d402c614 /resources/js
parent5e2e6753966c19616822915c14795bf62b589f68 (diff)
story!
Diffstat (limited to 'resources/js')
-rw-r--r--resources/js/classes/Area.js2
-rw-r--r--resources/js/classes/State.js12
-rw-r--r--resources/js/db.js15
-rw-r--r--resources/js/game.js10
-rw-r--r--resources/js/helpers.js9
-rw-r--r--resources/js/main.js46
-rw-r--r--resources/js/memory.js12
-rw-r--r--resources/js/story.js163
-rw-r--r--resources/js/ui.js512
9 files changed, 546 insertions, 235 deletions
diff --git a/resources/js/classes/Area.js b/resources/js/classes/Area.js
index 5a3a062..bafa949 100644
--- a/resources/js/classes/Area.js
+++ b/resources/js/classes/Area.js
@@ -8,8 +8,6 @@ class Area {
this.slug = slug;
}
- async initialize () {}
-
get name () {
return translate(this.alternateSlug) || slugToName(this.slug);
}
diff --git a/resources/js/classes/State.js b/resources/js/classes/State.js
index e4820d3..0dc06aa 100644
--- a/resources/js/classes/State.js
+++ b/resources/js/classes/State.js
@@ -1,5 +1,7 @@
class State {
Settings = {
+ name: '',
+
/**
* @type {string}
*/
@@ -35,6 +37,16 @@ class State {
lastVisitedTown = '';
/**
+ * @type {Object.<string, boolean>}
+ */
+ storyProgress = {};
+
+ /**
+ * @type {string}
+ */
+ currentStory = null;
+
+ /**
* @type {number}
*/
turn = 0;
diff --git a/resources/js/db.js b/resources/js/db.js
index 54fc751..3c8be05 100644
--- a/resources/js/db.js
+++ b/resources/js/db.js
@@ -68,6 +68,8 @@ const DB = {
*/
items: {},
+ npcs: {},
+
areas: {},
translations: {},
@@ -163,6 +165,19 @@ async function fetchItem (slug) {
}
/**
+ * @param {string} slug
+ *
+ * @returns {Promise<Object>}
+ */
+async function fetchNpc (slug) {
+ if (! DB.npcs[slug]) {
+ DB.npcs[slug] = await fetchDBData(`/modules/tuxemon/mods/tuxemon/db/npc/${slug}.json`).then((response) => response.json());
+ }
+
+ return DB.npcs[slug];
+}
+
+/**
* @param {string} locale
*
* @returns {Promise<Object>}
diff --git a/resources/js/game.js b/resources/js/game.js
index 3ce2aed..0841729 100644
--- a/resources/js/game.js
+++ b/resources/js/game.js
@@ -25,6 +25,7 @@ const Game = {
isLoadingArea: false,
isProgressingTurn: false,
playerIsChoosingNextMonster: false,
+ isStoryBattle: false,
doBattleAnimation: true,
opponentActionTimeout: null,
didTechniqueHit: false,
@@ -120,9 +121,13 @@ const Game = {
Memory.state.currentArea.monsterProgress = 0;
if (Memory.state.currentArea.encounters.length > 0) {
await Game.encounterWildMonster();
- } else {
+ } else if (Memory.state.currentArea.trainers.length > 0) {
await Game.encounterTrainer();
+ } else {
+ UI.showMap();
}
+
+ Game.isStoryBattle = false;
} else {
await Game.encounterNextTrainerMonster();
}
@@ -146,6 +151,7 @@ const Game = {
// whole party defeated
if (!Memory.state.player.monsters.some((monster) => monster.hp > 0)) {
Memory.state.Game.isInBattle = false;
+ Game.isStoryBattle = false;
if (Memory.state.currentArea.monsterProgress < Memory.state.currentArea.requiredEncounters) {
Memory.state.currentArea.monsterProgress = 0;
@@ -1012,6 +1018,8 @@ const Game = {
/**
* @param {Area} area
+ *
+ * @returns {boolean}
*/
isTown (area) {
return area.encounters.length === 0 && area.trainers.length === 0;
diff --git a/resources/js/helpers.js b/resources/js/helpers.js
index 58e2113..bceaf5e 100644
--- a/resources/js/helpers.js
+++ b/resources/js/helpers.js
@@ -14,6 +14,15 @@ function slugToName (slug) {
}
/**
+ * @param {string} text
+ *
+ * @returns {string}
+ */
+function nl2br (text) {
+ return text.replace(new RegExp(/\\n/g), '<br>');
+}
+
+/**
* @param {string} color
*
* @returns {string}
diff --git a/resources/js/main.js b/resources/js/main.js
index f472a14..1cabab5 100644
--- a/resources/js/main.js
+++ b/resources/js/main.js
@@ -8,50 +8,8 @@ UI.wrapCallback(async function () {
// Start New Game
else {
- const possibleStarterMonsters = await Promise.all(
- [
- 'budaye',
- 'dollfin',
- 'grintot',
- 'ignibus',
- 'memnomnom',
- ].map(async (monsterSlug) => await fetchMonster(monsterSlug))
- );
+ await initializeState();
- const monsterSelection = UI.openStarterMonsterSelection(possibleStarterMonsters);
- monsterSelection.addEventListener('starter:monster:selected', UI.wrapCallback(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 = 'tuxeball';
-
- // set rival monster
- possibleStarterMonsters.splice(possibleStarterMonsters.indexOf(event.detail.monster), 1);
- const rivalMonster = possibleStarterMonsters[Math.round(Math.random() * (possibleStarterMonsters.length - 1))];
- Memory.state.opponent = new Trainer({ monsters: [ rivalMonster ] });
- await Memory.state.opponent.initialize();
- Memory.state.rivalMonster = rivalMonster.slug
-
- // go to starting area
- await Game.goToArea('paper-town');
-
- UI.drawActiveMonster();
- UI.drawActiveTechniques();
-
- event.detail.popup.remove();
- }));
+ await Story.progress('start');
}
})();
diff --git a/resources/js/memory.js b/resources/js/memory.js
index 709a084..b403037 100644
--- a/resources/js/memory.js
+++ b/resources/js/memory.js
@@ -181,6 +181,7 @@ const Memory = {
*/
const loadedState = saveData;
+ Memory.state.Settings.name = loadedState.Settings.name;
Memory.state.Settings.language = loadedState.Settings.language;
await fetchTranslation(Memory.state.Settings.language);
applyTranslation();
@@ -197,6 +198,8 @@ const Memory = {
}
Memory.state.currentArea = await loadArea(loadedState.currentArea);
Memory.state.lastVisitedTown = loadedState.lastVisitedTown;
+ Memory.state.storyProgress = loadedState.storyProgress;
+ Memory.state.currentStory = loadedState.currentStory;
Memory.state.turn = loadedState.turn;
Memory.state.money = loadedState.money;
@@ -227,6 +230,8 @@ const Memory = {
UI.drawArea();
UI.drawStatus();
UI.closeAllPopups();
+
+ Story.progress(Memory.state.currentStory);
},
/**
@@ -240,3 +245,10 @@ const Memory = {
Memory.loadFromString(localStorage.getItem('state'));
},
};
+
+async function initializeState () {
+ Memory.state.currentArea = await fetchArea('paper-town');
+ Memory.state.player = new Trainer({ monsters: [] });
+ Memory.state.opponent = new Trainer({ monsters: [] });
+ Memory.state.activeTechnique = await fetchTechnique('all_in');
+}
diff --git a/resources/js/story.js b/resources/js/story.js
new file mode 100644
index 0000000..c5f7add
--- /dev/null
+++ b/resources/js/story.js
@@ -0,0 +1,163 @@
+const Story = {
+ async start () {
+ const settingsPopup = UI.createPopup();
+ settingsPopup.querySelector('[data-template-slot="content"]').append(UI.createSettingsMenu());
+ UI.drawPopup(settingsPopup);
+
+ await new Promise((resolve) => {
+ settingsPopup.addEventListener('close', UI.wrapCallback(async () => {
+ resolve();
+ }));
+ });
+
+ await Story.progress('introduction');
+ },
+
+ async introduction () {
+ const possibleStarterMonsters = await Promise.all(
+ [
+ 'budaye',
+ 'dollfin',
+ 'grintot',
+ 'ignibus',
+ 'memnomnom',
+ ].map(async (monsterSlug) => await fetchMonster(monsterSlug))
+ );
+
+ const monsterSelection = UI.openStarterMonsterSelection(possibleStarterMonsters, { title: translate('story:introduction:monster_selection:title', true) });
+ await new Promise((resolve) => {
+ monsterSelection.addEventListener('starter:monster:selected', UI.wrapCallback(async (event) => {
+ if (!confirm(`Select ${event.detail.monster.name}?`)) {
+ return;
+ }
+
+ event.detail.popup.remove();
+
+ await UI.buildAndShowStoryPopup({ speaker: await fetchNpc('spyder_dante'), text: translate('spyder_intro_shopkeeper4', true) });
+
+ // set rival monster
+ Memory.state.rivalMonster = event.detail.monster.slug;
+
+ // initialize state variables
+ Memory.state.money = 250;
+
+ Memory.state.opponent = new Trainer({ monsters: [] });
+ await Memory.state.opponent.initialize();
+
+ Memory.state.player = new Trainer({ monsters: [] });
+ await Memory.state.player.initialize();
+
+ // go to starting area
+ await Game.goToArea('paper-town');
+
+ resolve();
+ }));
+ });
+ },
+
+ async selectStarterMonster () {
+ await UI.buildAndShowStoryPopup({ speaker: await fetchNpc('spyder_dante'), text: translate('spyder_papertown_myfirstmon_notmet', true) });
+ await UI.buildAndShowStoryPopup({ speaker: await fetchNpc('spyder_dante'), text: translate('spyder_papertown_myfirstmon2', true) });
+
+ const possibleStarterMonsters = await Promise.all(
+ [
+ 'tweesher',
+ 'lambert',
+ 'nut',
+ 'agnite',
+ 'rockitten',
+ ].map(async (monsterSlug) => await fetchMonster(monsterSlug))
+ );
+
+ const monsterSelection = UI.openStarterMonsterSelection(possibleStarterMonsters, { title: translate('story:select_starter_monster:monster_selection:title', true) });
+ await new Promise((resolve) => {
+ monsterSelection.addEventListener('starter:monster:selected', UI.wrapCallback(async (event) => {
+ if (!confirm(`Select ${event.detail.monster.name}?`)) {
+ return;
+ }
+
+ Memory.state.player = new Trainer({
+ monsters: [
+ event.detail.monster,
+ ]
+ });
+ await Memory.state.player.initialize();
+
+ Game.setActivePlayerMonster(Memory.state.player.monsters[0]);
+
+ // go to starting area
+ await Game.goToArea('paper-town');
+
+ UI.drawActiveMonster();
+ UI.drawActiveTechniques();
+
+ event.detail.popup.remove();
+
+ resolve();
+ }));
+ });
+
+ await Story.progress('battleRivalOne');
+ },
+
+ async battleRivalOne () {
+ Memory.state.opponent = new Trainer({ monsters: [ await fetchMonster(Memory.state.rivalMonster) ] });
+ await Memory.state.opponent.initialize();
+
+ await UI.buildAndShowStoryPopup({ speaker: await fetchNpc('spyder_rivalbillie'), text: translate('spyder_papertown_firstfight', true) });
+
+ await Story.battle();
+ },
+
+
+ // Helper
+
+ /**
+ * @param {string} slug
+ *
+ * @returns {Promise<any>}
+ */
+ async progress (slug) {
+ if (!Story[slug]) {
+ return;
+ }
+
+ Memory.state.currentStory = slug;
+ Memory.saveToLocalStorage();
+
+ await Story[slug]();
+
+ Memory.state.storyProgress[slug] = true;
+ Memory.state.currentStory = null;
+ Memory.saveToLocalStorage();
+ },
+
+ /**
+ * @returns {Promise<any>}
+ */
+ async battle () {
+ const previousArea = Object.assign({}, Memory.state.currentArea);
+
+ Game.isStoryBattle = true;
+ Memory.state.Game.isInBattle = true;
+ Memory.saveToLocalStorage();
+
+ UI.drawBattle();
+ UI.showBattle();
+
+ await new Promise((resolve) => {
+ const interval = setInterval(() => {
+ if (!Game.isStoryBattle) {
+ clearInterval(interval);
+ resolve();
+ }
+ }, 100);
+ });
+
+ if (previousArea.slug === Memory.state.currentArea.slug) {
+ Memory.state.currentArea.trainerProgress = previousArea.trainerProgress;
+
+ UI.drawStatus();
+ }
+ },
+};
diff --git a/resources/js/ui.js b/resources/js/ui.js
index ee33a64..5626048 100644
--- a/resources/js/ui.js
+++ b/resources/js/ui.js
@@ -39,6 +39,8 @@ const Template = {
dialogLoad: document.querySelector('#tpl___dialog__load'),
menuSettings: document.querySelector('#tpl___menu__settings'),
+
+ storyPopup: document.querySelector('#tpl___story__popup'),
};
const UI = {
@@ -98,17 +100,26 @@ const UI = {
},
/**
+ * @param {Object} options
+ * @param {boolean} [options.isClosable=true]
+ *
* @returns {HTMLElement}
*/
- createPopup () {
+ createPopup (options) {
+ options = Object.assign({
+ isClosable: true,
+ }, options);
+
const popup = UI.createTemplate(Template.popup);
- popup.addEventListener('click', ({ target }) => {
- if (target === popup) {
- popup.dispatchEvent(new Event('close'));
- popup.remove();
- }
- });
+ if (options.isClosable) {
+ popup.addEventListener('click', ({ target }) => {
+ if (target === popup) {
+ popup.dispatchEvent(new Event('close'));
+ popup.remove();
+ }
+ });
+ }
return popup;
},
@@ -534,14 +545,21 @@ const UI = {
UI.drawTown();
UI.closeLog();
UI.showMap();
- } else {
- UI.elements.battle.style.backgroundImage = `url(/modules/tuxemon/mods/tuxemon/gfx/ui/combat/${Memory.state.currentArea.environment.battle_graphics.background})`;
- UI.showBattle();
- UI.drawOpponentMonster();
- UI.drawActiveMonster();
- UI.drawActiveTechniques();
+ UI.drawStatus();
+ UI.drawActiveBall();
+ } else {
+ UI.drawBattle();
}
+ },
+
+ drawBattle () {
+ UI.elements.battle.style.backgroundImage = `url(/modules/tuxemon/mods/tuxemon/gfx/ui/combat/${Memory.state.currentArea.environment.battle_graphics.background})`;
+
+ UI.showBattle();
+ UI.drawOpponentMonster();
+ UI.drawActiveMonster();
+ UI.drawActiveTechniques();
UI.drawStatus();
UI.drawActiveBall();
@@ -736,6 +754,22 @@ const UI = {
}
})));
+ template.querySelectorAll('[data-interactable="true"]').forEach((node) => {
+ if (node.dataset.story) {
+ if (node.dataset.storyOnce && Memory.state.storyProgress[node.dataset.story]) {
+ node.dataset.interactable = false;
+ return;
+ }
+ }
+
+ node.addEventListener('click', UI.wrapCallback(async () => {
+ if (node.dataset.story) {
+ await Story.progress(node.dataset.story);
+ UI.drawTown();
+ }
+ }));
+ });
+
return template;
},
@@ -833,6 +867,11 @@ const UI = {
}));
template.querySelector('[data-template-slot="box.view"]').addEventListener('click', UI.wrapCallback(() => {
+ if (Memory.state.monsters.length === 0) {
+ alert(translate('ui:healing_center:box:view:no_tuxemon_in_box', true));
+ return;
+ }
+
const boxPopup = UI.createPopup();
const monsterSelection = UI.createMonsterSelection(Memory.state.monsters);
monsterSelection.addEventListener('monster:selected', UI.wrapCallback((event) => {
@@ -1044,7 +1083,10 @@ const UI = {
}
const changeAreaButton = UI.elements.changeArea;
- if (!Game.isTown(currentArea)) {
+ if (Game.isStoryBattle) {
+ changeAreaButton.disabled = true;
+ }
+ else if (!Game.isTown(currentArea)) {
if (
Memory.state.Game.isInBattle ||
(Memory.state.opponent && Memory.state.opponent.type === 'trainer' && Memory.state.opponent.activeMonster !== Memory.state.opponent.monsters[0])
@@ -1061,14 +1103,16 @@ const UI = {
/**
* @param {Monster[]} monsters
*/
- openStarterMonsterSelection (monsters) {
+ openStarterMonsterSelection (monsters, { title }) {
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);
+ const titleNode = document.createElement('h1');
+ titleNode.textContent = title;
+ titleNode.style.textAlign = 'center';
+ titleNode.style.margin = 0;
+ titleNode.style.padding = '1rem';
+ template.prepend(titleNode);
template.addEventListener('party:monster:selected', (event) => {
const monster = event.detail.monster;
@@ -1160,8 +1204,12 @@ const UI = {
}
}
- else if (condition.startsWith('event.')) {
- canGo = false;
+ else if (condition.startsWith('story.')) {
+ const storyCondition = condition.replace('story.', '');
+ const storySlug = storyCondition;
+ const storyProgress = Memory.state.storyProgress[storySlug] || false;
+
+ canGo = canGo && storyProgress;
}
}
@@ -1334,175 +1382,12 @@ const UI = {
openSettingsMenu () {
const popup = UI.createPopup();
- const template = UI.createTemplate(Template.menuSettings);
-
-
- /* Language */
-
- const languageSelectNode = template.querySelector('[data-template-slot="language"]');
-
- const languages = {
- 'cs_CZ': 'Czech (Czech Republic)',
- 'de_DE': 'German (Germany)',
- 'en_US': 'English (United States)',
- 'eo': 'Esperanto',
- 'es_ES': 'Spanish (Spain)',
- 'es_MX': 'Spanish (Mexico)',
- 'fi': 'Finnish',
- 'fr_FR': 'French (France)',
- 'it_IT': 'Italian (Italy)',
- 'ja': 'Japanese',
- 'nb_NO': 'Norwegian Bokmål (Norway)',
- 'pl': 'Polish',
- 'pt_BR': 'Portuguese (Brazil)',
- 'zh_CN': 'Chinese (China)',
- };
-
- for (const languageCode of Object.keys(languages)) {
- const languageName = languages[languageCode];
- const languageOptionNode = document.createElement('option');
-
- languageOptionNode.value = languageCode;
- languageOptionNode.textContent = languageName;
-
- if (languageCode === Memory.state.Settings.language) {
- languageOptionNode.selected = true;
- }
-
- languageSelectNode.appendChild(languageOptionNode);
- }
-
- languageSelectNode.addEventListener('change', UI.wrapCallback(async () => {
- const selected = [...languageSelectNode.children].find((node) => node.selected === true);
- Memory.state.Settings.language = selected.value;
-
- await fetchTranslation(Memory.state.Settings.language);
- applyTranslation();
-
- UI.drawArea();
- }));
-
-
- /* Currency */
-
- const currencySelectNode = template.querySelector('[data-template-slot="currency"]');
- const currencyCodeMap = Object.keys(DB.currencies.map).sort((a, b) => {
- const nameA = DB.currencies.map[a].name;
- const nameB = DB.currencies.map[b].name;
-
- return nameA > nameB ? 1 : -1;
- });
-
- for (const currencyCode of currencyCodeMap) {
- const currency = DB.currencies.map[currencyCode];
- const currencyOptionNode = document.createElement('option');
-
- currencyOptionNode.value = currencyCode,
- currencyOptionNode.textContent = `${currency.symbol} - ${currency.name}`;
-
- if (currencyCode === Memory.state.Settings.currency) {
- currencyOptionNode.selected = true;
- }
-
- currencySelectNode.appendChild(currencyOptionNode);
- }
-
- currencySelectNode.addEventListener('change', UI.wrapCallback(async () => {
- const selected = [...currencySelectNode.children].find((node) => node.selected === true);
- const previousCurrencyCode = Memory.state.Settings.currency;
- const newCurrencyCode = selected.value;
-
- Memory.state.Settings.currency = newCurrencyCode;
+ const template = UI.createSettingsMenu();
- // re-calculate money
- const previousCurrency = DB.currencies.map[previousCurrencyCode];
- const newCurrency = DB.currencies.map[newCurrencyCode];
- const baseRateMoney = Memory.state.money / previousCurrency.rate;
- const exchangedMoney = baseRateMoney * newCurrency.rate;
- Memory.state.money = Number(exchangedMoney.toFixed(newCurrency.decimals));
-
- UI.drawArea();
- UI.drawStatus();
+ popup.addEventListener('close', UI.wrapCallback(() => {
+ Memory.saveToLocalStorage();
}));
- template.querySelector('[data-template-slot="currency.lastUpdated"]').textContent = DB.currencies.last_updated;
-
-
- // Highlight
-
- template.querySelector('[data-template-slot="highlight"]').addEventListener('click', UI.wrapCallback(() => {
- UI.isHighlighting = !UI.isHighlighting;
-
- const elements = [
- UI.elements.battleOpponent,
- UI.elements.battlePlayer.querySelector('[data-template-slot="sprite"]'),
- UI.elements.techniques,
- 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,
-
- Template.technique.content.firstElementChild,
- Template.healingCenter.content.querySelector('[data-template-slot="heal"]'),
- Template.shopItem.content.firstElementChild,
- ...Template.monsterStats.content.querySelectorAll('button'),
- Template.movesetItem.content.firstElementChild,
- Template.tabHeading.content.querySelector('[data-template-slot="label"]'),
- ...Template.party.content.querySelectorAll('[data-template-slot="modes"] button'),
- Template.partyMonster.content.firstElementChild,
- Template.inventoryItem.content.firstElementChild,
- ...Template.inventory.content.querySelectorAll('[data-template-slot="modes"] button'),
- ...Template.menuSettings.content.querySelectorAll('select, button'),
-
- ...document.querySelector('.menu__settings').querySelectorAll('select, button'),
- ];
-
- for (const element of elements) {
- if (!element) {
- continue;
- }
-
- if (UI.isHighlighting) {
- element.classList.add(UI.highlightClassName);
- } else {
- element.classList.remove(UI.highlightClassName);
- }
- }
-
- // map
- const mapElements = [
- UI.elements.sceneTown.querySelector('[data-template-slot="map"]').firstElementChild,
- Template.map.content.firstElementChild,
- ];
- for (const element of mapElements) {
- if (!element) {
- continue;
- }
-
- if (UI.isHighlighting) {
- element.classList.add(`${UI.highlightClassName}--map`);
- } else {
- element.classList.remove(`${UI.highlightClassName}--map`);
- }
- }
- }));
-
-
- // Clear save data
-
- template.querySelector('[data-template-slot="clearLocalSaveData"]').addEventListener('click', UI.wrapCallback(() => {
- if (confirm(translate('ui:settings:clear_local_save_data:confirm', true))) {
- localStorage.removeItem('state');
- window.location.reload();
- }
- }));
-
-
popup.querySelector('.popup').appendChild(template);
UI.drawPopup(popup);
},
@@ -1536,7 +1421,7 @@ const UI = {
*
* @returns {HTMLElement}
*/
- createStatsMenu (monster) { // TODO
+ createStatsMenu (monster) {
const template = UI.createTemplate(Template.monsterStats);
template.querySelector('[data-template-slot="name"]').textContent = monster.name;
@@ -1544,6 +1429,8 @@ const UI = {
template.querySelector('[data-template-slot="level"]').textContent = monster.level;
template.querySelector('[data-template-slot="types"]').innerHTML = monster.types.map((type) => UI.createElementTypeIcon(type).outerHTML).join('');
+ template.querySelector('[data-template-slot="exp"]').innerHTML = `${monster.exp} / ${monster.getExperienceRequired(1)}`;
+
template.querySelector('[data-template-slot="stats.melee.name"]').textContent = translate(StatType.melee) || slugToName(StatType.melee);
template.querySelector('[data-template-slot="stats.armour.name"]').textContent = translate(StatType.armour) || slugToName(StatType.armour);
template.querySelector('[data-template-slot="stats.ranged.name"]').textContent = translate(StatType.ranged) || slugToName(StatType.ranged);
@@ -1828,6 +1715,255 @@ const UI = {
},
+ /* Menu - Settings */
+
+ /**
+ * @returns {HTMLElement}
+ */
+ createSettingsMenu () {
+ const template = UI.createTemplate(Template.menuSettings);
+
+
+ /* Name */
+
+ const nameNode = template.querySelector('[data-template-slot="name"]');
+ nameNode.value = Memory.state.Settings.name;
+ nameNode.addEventListener('input', UI.wrapCallback((event) => {
+ Memory.state.Settings.name = event.target.value;
+ }));
+
+
+ /* Language */
+
+ const languageSelectNode = template.querySelector('[data-template-slot="language"]');
+
+ const languages = {
+ 'cs_CZ': 'Czech (Czech Republic)',
+ 'de_DE': 'German (Germany)',
+ 'en_US': 'English (United States)',
+ 'eo': 'Esperanto',
+ 'es_ES': 'Spanish (Spain)',
+ 'es_MX': 'Spanish (Mexico)',
+ 'fi': 'Finnish',
+ 'fr_FR': 'French (France)',
+ 'it_IT': 'Italian (Italy)',
+ 'ja': 'Japanese',
+ 'nb_NO': 'Norwegian Bokmål (Norway)',
+ 'pl': 'Polish',
+ 'pt_BR': 'Portuguese (Brazil)',
+ 'zh_CN': 'Chinese (China)',
+ };
+
+ for (const languageCode of Object.keys(languages)) {
+ const languageName = languages[languageCode];
+ const languageOptionNode = document.createElement('option');
+
+ languageOptionNode.value = languageCode;
+ languageOptionNode.textContent = languageName;
+
+ if (languageCode === Memory.state.Settings.language) {
+ languageOptionNode.selected = true;
+ }
+
+ languageSelectNode.appendChild(languageOptionNode);
+ }
+
+ languageSelectNode.addEventListener('change', UI.wrapCallback(async () => {
+ const selected = [...languageSelectNode.children].find((node) => node.selected === true);
+ Memory.state.Settings.language = selected.value;
+
+ await fetchTranslation(Memory.state.Settings.language);
+ applyTranslation();
+
+ UI.drawArea();
+ }));
+
+
+ /* Currency */
+
+ const currencySelectNode = template.querySelector('[data-template-slot="currency"]');
+ const currencyCodeMap = Object.keys(DB.currencies.map).sort((a, b) => {
+ const nameA = DB.currencies.map[a].name;
+ const nameB = DB.currencies.map[b].name;
+
+ return nameA > nameB ? 1 : -1;
+ });
+
+ for (const currencyCode of currencyCodeMap) {
+ const currency = DB.currencies.map[currencyCode];
+ const currencyOptionNode = document.createElement('option');
+
+ currencyOptionNode.value = currencyCode,
+ currencyOptionNode.textContent = `${currency.symbol} - ${currency.name}`;
+
+ if (currencyCode === Memory.state.Settings.currency) {
+ currencyOptionNode.selected = true;
+ }
+
+ currencySelectNode.appendChild(currencyOptionNode);
+ }
+
+ currencySelectNode.addEventListener('change', UI.wrapCallback(async () => {
+ const selected = [...currencySelectNode.children].find((node) => node.selected === true);
+ const previousCurrencyCode = Memory.state.Settings.currency;
+ const newCurrencyCode = selected.value;
+
+ Memory.state.Settings.currency = newCurrencyCode;
+
+ // re-calculate money
+ const previousCurrency = DB.currencies.map[previousCurrencyCode];
+ const newCurrency = DB.currencies.map[newCurrencyCode];
+ const baseRateMoney = Memory.state.money / previousCurrency.rate;
+ const exchangedMoney = baseRateMoney * newCurrency.rate;
+ Memory.state.money = Number(exchangedMoney.toFixed(newCurrency.decimals));
+
+ 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.wrapCallback(() => {
+ UI.isHighlighting = !UI.isHighlighting;
+
+ const elements = [
+ UI.elements.battleOpponent,
+ UI.elements.battlePlayer.querySelector('[data-template-slot="sprite"]'),
+ UI.elements.techniques,
+ 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,
+
+ Template.technique.content.firstElementChild,
+ Template.healingCenter.content.querySelector('[data-template-slot="heal"]'),
+ Template.shopItem.content.firstElementChild,
+ ...Template.monsterStats.content.querySelectorAll('button'),
+ Template.movesetItem.content.firstElementChild,
+ Template.tabHeading.content.querySelector('[data-template-slot="label"]'),
+ ...Template.party.content.querySelectorAll('[data-template-slot="modes"] button'),
+ Template.partyMonster.content.firstElementChild,
+ Template.inventoryItem.content.firstElementChild,
+ ...Template.inventory.content.querySelectorAll('[data-template-slot="modes"] button'),
+ ...Template.menuSettings.content.querySelectorAll('select, button'),
+
+ ...document.querySelector('.menu__settings').querySelectorAll('select, button'),
+ ];
+
+ for (const element of elements) {
+ if (!element) {
+ continue;
+ }
+
+ if (UI.isHighlighting) {
+ element.classList.add(UI.highlightClassName);
+ } else {
+ element.classList.remove(UI.highlightClassName);
+ }
+ }
+
+ // map
+ const mapElements = [
+ UI.elements.sceneTown.querySelector('[data-template-slot="map"]').firstElementChild,
+ Template.map.content.firstElementChild,
+ ];
+ for (const element of mapElements) {
+ if (!element) {
+ continue;
+ }
+
+ if (UI.isHighlighting) {
+ element.classList.add(`${UI.highlightClassName}--map`);
+ } else {
+ element.classList.remove(`${UI.highlightClassName}--map`);
+ }
+ }
+ }));
+
+
+ // Clear save data
+
+ template.querySelector('[data-template-slot="clearLocalSaveData"]').addEventListener('click', UI.wrapCallback(() => {
+ if (confirm(translate('ui:settings:clear_local_save_data:confirm', true))) {
+ localStorage.removeItem('state');
+ window.location.reload();
+ }
+ }));
+
+ return template;
+ },
+
+
+ // Story
+
+ /**
+ * @returns {HTMLElement}
+ */
+ createStoryPopup () {
+ const popup = UI.createPopup();
+
+ return popup;
+ },
+
+ /**
+ * @param {HTMLElement} popup
+ *
+ * @returns {HTMLElement}
+ */
+ applyStoryPopupContent (popup, { speaker, text }) {
+ const template = UI.createTemplate(Template.storyPopup);
+
+ template.querySelector('[data-template-slot="speaker.sprite"]').src = `/modules/tuxemon/mods/tuxemon/gfx/sprites/player/${speaker.template[0].sprite_name}.png`;
+ template.querySelector('[data-template-slot="speaker.name"]').textContent = slugToName(speaker.slug.replace('spyder_', ''));
+ template.querySelector('[data-template-slot="text"]').innerHTML = nl2br(text);
+ template.querySelector('[data-template-slot="next"]').addEventListener('click', UI.wrapCallback(() => {
+ popup.dispatchEvent(new Event('next'));
+ }));
+
+ popup.querySelector('[data-template-slot="content"]').append(template);
+
+ return popup;
+ },
+
+ /**
+ * @param {HTMLElement} popup
+ *
+ * @returns {Promise<any>}
+ */
+ drawStoryPopup (popup) {
+ UI.drawPopup(popup);
+
+ return new Promise((resolve, _reject) => {
+ popup.addEventListener('next', () => {
+ popup.remove();
+ resolve();
+ });
+
+ popup.addEventListener('close', () => {
+ resolve();
+ });
+ });
+ },
+
+ /**
+ * @returns {Promise<any>}
+ */
+ async buildAndShowStoryPopup ({ speaker, text }) {
+ let popup = UI.createStoryPopup();
+ popup = UI.applyStoryPopupContent(popup, { speaker: speaker, text: text });
+
+ return await UI.drawStoryPopup(popup);
+ },
+
+
// Error
/**