├── .gitignore ├── packager ├── suffix.js ├── bump.sh ├── package.json ├── bump.js ├── prefix.js ├── run.js └── moduleRegistry.js ├── features ├── authToast.js ├── versionWarning.js ├── scriptRegistry.js ├── questDisabler.js ├── actionEnabler.js ├── petHighlighter.js ├── debugService.js ├── recipeClickthrough.js ├── marketListingLimitWarning.js ├── dropChanceDisplay.js ├── conversionHider.js ├── petRenamer.js ├── estimatorActivity.js ├── changelog.js ├── targetAmountCrafting.js ├── _marketCompetition.js ├── estimatorOutskirts.js ├── petFilter.js ├── marketPriceButtons.js ├── targetAmountMarket.js ├── marksGrouper.js ├── dataForwarder.js ├── craftCheatSheet.js ├── petStatHighlighter.js ├── ui.js ├── petStatRedesign.js ├── configurationPage.js ├── itemHover.js ├── _skillOverviewPage.js └── guildSorts.js ├── caches ├── actionCache.js ├── monsterCache.js ├── skillSetCache.js ├── structuresCache.js ├── recipeCache.js ├── expeditionCache.js ├── traitCache.js ├── masteryCache.js ├── expeditionDropCache.js ├── ingredientCache.js ├── petPassiveCache.js ├── skillCache.js ├── statNameCache.js ├── petCache.js └── dropCache.js ├── stores ├── localConfigurationStore.js ├── expStateStore.js ├── configurationStore.js ├── variousStateStore.js ├── lootStore.js ├── abstractStateStore.js ├── equipmentStateStore.js ├── customItemPriceStore.js ├── masteryStateStore.js └── petStateStore.js ├── libraries ├── interceptor.js ├── colorMapper.js ├── logService.js ├── assetUtil.js ├── polyfill.js ├── events.js ├── hotkey.js ├── itemUtil.js ├── elementCreator.js ├── configuration.js ├── Promise.js ├── toast.js ├── elementWatcher.js ├── modal.js ├── pageDetector.js ├── request.js ├── localDatabase.js └── EstimationGenerator.js ├── readers ├── settingsReader.js ├── lootReader.js ├── guildReader.js ├── structuresReader.js ├── enchantmentsReader.js ├── guildStructuresReader.js ├── equipmentReader.js ├── variousReader.js ├── guildEventReader.js ├── questReader.js ├── inventoryReader.js ├── marksReader.js ├── expReader.js ├── marketReader.js ├── traitsReader.js ├── masteryReader.js └── petReader.js ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules/* 2 | **/package-lock.json -------------------------------------------------------------------------------- /packager/suffix.js: -------------------------------------------------------------------------------- 1 | window.moduleRegistry.build(); 2 | -------------------------------------------------------------------------------- /features/authToast.js: -------------------------------------------------------------------------------- 1 | (toast) => { 2 | 3 | function initialise() { 4 | toast.create({ 5 | text: 'Pancake-Scripts initialised!', 6 | image: 'https://img.icons8.com/?size=48&id=1ODJ62iG96gX&format=png' 7 | }); 8 | } 9 | 10 | initialise(); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /packager/bump.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | git checkout main 4 | git merge development 5 | version=$(node.exe bump.js $1) 6 | node.exe run.js 7 | git add ../* 8 | git commit -m "Version $version" 9 | git tag "v$version" 10 | git push 11 | git push origin --tags 12 | git checkout development 13 | git merge main 14 | git push 15 | 16 | read -p "Press any key to resume ..." 17 | -------------------------------------------------------------------------------- /caches/actionCache.js: -------------------------------------------------------------------------------- 1 | (request) => { 2 | 3 | const exports = { 4 | list: [], 5 | byId: {}, 6 | byName: {} 7 | }; 8 | 9 | async function initialise() { 10 | const actions = await request.listActions(); 11 | for(const action of actions) { 12 | exports.list.push(action); 13 | exports.byId[action.id] = action; 14 | exports.byName[action.name] = action; 15 | } 16 | return exports; 17 | } 18 | 19 | return initialise(); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /caches/monsterCache.js: -------------------------------------------------------------------------------- 1 | (request) => { 2 | 3 | const exports = { 4 | list: [], 5 | byId: {}, 6 | byName: {} 7 | }; 8 | 9 | async function initialise() { 10 | const monsters = await request.listMonsters(); 11 | for(const monster of monsters) { 12 | exports.list.push(monster); 13 | exports.byId[monster.id] = monster; 14 | exports.byName[monster.name] = monster; 15 | } 16 | return exports; 17 | } 18 | 19 | return initialise(); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /caches/skillSetCache.js: -------------------------------------------------------------------------------- 1 | (request) => { 2 | 3 | const exports = { 4 | list: [], 5 | byId: {}, 6 | byName: {} 7 | }; 8 | 9 | async function initialise() { 10 | const skillSets = await request.listSkillSets(); 11 | for(const skillSet of skillSets) { 12 | exports.list.push(skillSet); 13 | exports.byId[skillSet.id] = skillSet; 14 | exports.byName[skillSet.name] = skillSet; 15 | } 16 | return exports; 17 | } 18 | 19 | return initialise(); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /caches/structuresCache.js: -------------------------------------------------------------------------------- 1 | (request) => { 2 | 3 | const exports = { 4 | list: [], 5 | byId: {}, 6 | byName: {} 7 | }; 8 | 9 | async function initialise() { 10 | const structures = await request.listStructures(); 11 | for(const structure of structures) { 12 | exports.list.push(structure); 13 | exports.byId[structure.id] = structure; 14 | exports.byName[structure.name] = structure; 15 | } 16 | return exports; 17 | } 18 | 19 | return initialise(); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /caches/recipeCache.js: -------------------------------------------------------------------------------- 1 | (request) => { 2 | 3 | const exports = { 4 | list: [], 5 | byId: {}, 6 | byName: {}, 7 | byImage: {} 8 | }; 9 | 10 | async function initialise() { 11 | exports.list = await request.listRecipes(); 12 | for(const recipe of exports.list) { 13 | exports.byId[recipe.id] = recipe; 14 | exports.byName[recipe.name] = recipe; 15 | const lastPart = recipe.image.split('/').at(-1); 16 | exports.byImage[lastPart] = recipe; 17 | } 18 | return exports; 19 | } 20 | 21 | return initialise(); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /stores/localConfigurationStore.js: -------------------------------------------------------------------------------- 1 | (localDatabase) => { 2 | 3 | const exports = { 4 | load, 5 | save 6 | }; 7 | 8 | const STORE_NAME = 'settings'; 9 | 10 | async function load() { 11 | const entries = await localDatabase.getAllEntries(STORE_NAME); 12 | const configurations = {}; 13 | for(const entry of entries) { 14 | configurations[entry.key] = entry.value; 15 | } 16 | return configurations; 17 | } 18 | 19 | async function save(key, value) { 20 | await localDatabase.saveEntry(STORE_NAME, {key, value}); 21 | } 22 | 23 | return exports; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /caches/expeditionCache.js: -------------------------------------------------------------------------------- 1 | (request) => { 2 | 3 | const exports = { 4 | list: [], 5 | byId: {}, 6 | byName: {}, 7 | byTier: {} 8 | }; 9 | 10 | async function initialise() { 11 | const expeditions = await request.listExpeditions(); 12 | for(const expedition of expeditions) { 13 | exports.list.push(expedition); 14 | exports.byId[expedition.id] = expedition; 15 | exports.byName[expedition.name] = expedition; 16 | exports.byTier[expedition.tier] = expedition; 17 | } 18 | return exports; 19 | } 20 | 21 | return initialise(); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /caches/traitCache.js: -------------------------------------------------------------------------------- 1 | (request) => { 2 | 3 | const exports = { 4 | list: [], 5 | byId: {}, 6 | byName: {}, 7 | byImage: {} 8 | }; 9 | 10 | async function initialise() { 11 | const traits = await request.listTraits(); 12 | for(const trait of traits) { 13 | exports.list.push(trait); 14 | exports.byId[trait.id] = trait; 15 | exports.byName[trait.name] = trait; 16 | const lastPart = trait.image.split('/').at(-1); 17 | exports.byImage[lastPart] = trait; 18 | } 19 | return exports; 20 | } 21 | 22 | return initialise(); 23 | 24 | } 25 | -------------------------------------------------------------------------------- /caches/masteryCache.js: -------------------------------------------------------------------------------- 1 | (request) => { 2 | 3 | const exports = { 4 | list: [], 5 | byId: {}, 6 | bySkill: {}, 7 | byImage: {} 8 | }; 9 | 10 | async function initialise() { 11 | const masteries = await request.listMasteries(); 12 | for(const mastery of masteries) { 13 | exports.list.push(mastery); 14 | exports.byId[mastery.id] = mastery; 15 | exports.bySkill[mastery.skill] = mastery; 16 | const lastPart = mastery.image.split('/').at(-1); 17 | exports.byImage[lastPart] = mastery; 18 | } 19 | return exports; 20 | } 21 | 22 | return initialise(); 23 | 24 | } 25 | -------------------------------------------------------------------------------- /libraries/interceptor.js: -------------------------------------------------------------------------------- 1 | (events) => { 2 | 3 | function initialise() { 4 | registerInterceptorUrlChange(); 5 | events.emit('url', window.location.href); 6 | } 7 | 8 | function registerInterceptorUrlChange() { 9 | const pushState = history.pushState; 10 | history.pushState = function() { 11 | pushState.apply(history, arguments); 12 | events.emit('url', arguments[2]); 13 | }; 14 | const replaceState = history.replaceState; 15 | history.replaceState = function() { 16 | replaceState.apply(history, arguments); 17 | events.emit('url', arguments[2]); 18 | } 19 | } 20 | 21 | initialise(); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /libraries/colorMapper.js: -------------------------------------------------------------------------------- 1 | () => { 2 | 3 | const colorMappings = { 4 | // https://colorswall.com/palette/3 5 | primary: '#0275d8', 6 | success: '#5cb85c', 7 | info: '#5bc0de', 8 | warning: '#f0ad4e', 9 | danger: '#d9534f', 10 | inverse: '#292b2c', 11 | // custom 12 | focus: '#fff021', 13 | // component styling 14 | componentLight: '#393532', 15 | componentRegular: '#28211b', 16 | componentDark: '#211a12', 17 | componentHover: '#3c2f26', 18 | componentSelected: '#1c1916' 19 | }; 20 | 21 | function mapColor(color) { 22 | return colorMappings[color] || color; 23 | } 24 | 25 | return mapColor; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /features/versionWarning.js: -------------------------------------------------------------------------------- 1 | (request, toast) => { 2 | 3 | function initialise() { 4 | setInterval(run, 1000 * 60 * 5); 5 | run(); 6 | } 7 | 8 | async function run() { 9 | const version = await request.getVersion(); 10 | if(!window.PANCAKE_VERSION || version === window.PANCAKE_VERSION) { 11 | return; 12 | } 13 | toast.create({ 14 | text: `Consider updating Pancake-Scripts to ${version}!
Click here to go to GreasyFork { 2 | 3 | const exports = { 4 | list: [], 5 | byExpedition: {}, 6 | byItem: {} 7 | }; 8 | 9 | async function initialise() { 10 | const drops = await request.listExpeditionDrops(); 11 | for(const drop of drops) { 12 | exports.list.push(drop); 13 | if(!exports.byExpedition[drop.expedition]) { 14 | exports.byExpedition[drop.expedition] = []; 15 | } 16 | exports.byExpedition[drop.expedition].push(drop); 17 | if(!exports.byItem[drop.item]) { 18 | exports.byItem[drop.item] = []; 19 | } 20 | exports.byItem[drop.item].push(drop); 21 | } 22 | return exports; 23 | } 24 | 25 | return initialise(); 26 | 27 | } 28 | -------------------------------------------------------------------------------- /features/scriptRegistry.js: -------------------------------------------------------------------------------- 1 | (Promise, elementCreator) => { 2 | 3 | const loaded = new Promise.Deferred('scriptRegistry'); 4 | 5 | const exports = { 6 | isLoaded 7 | }; 8 | 9 | async function initialise() { 10 | const promises = [ 11 | elementCreator.addScript('https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js'), 12 | elementCreator.addScript('https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.20.0/matter.min.js'), 13 | elementCreator.addScript('https://code.jquery.com/ui/1.14.1/jquery-ui.js'), 14 | ]; 15 | await window.Promise.all(promises); 16 | loaded.resolve(); 17 | } 18 | 19 | function isLoaded() { 20 | return loaded; 21 | } 22 | 23 | initialise(); 24 | 25 | return exports; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /packager/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "packager", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "run.js", 6 | "scripts": { 7 | "start": "node run.js", 8 | "watch": "nodemon -w ../ -i plugin.js", 9 | "bump-patch": "bump.sh patch", 10 | "bump-minor": "bump.sh minor", 11 | "bump-major": "bump.sh major" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/Boldy97/ironwood-scripts.git" 16 | }, 17 | "author": "", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/Boldy97/ironwood-scripts/issues" 21 | }, 22 | "homepage": "https://github.com/Boldy97/ironwood-scripts#readme", 23 | "dependencies": { 24 | "node-fetch": "^3.3.2", 25 | "nodemon": "^3.1.4", 26 | "semver": "^7.5.4" 27 | }, 28 | "type": "module" 29 | } 30 | -------------------------------------------------------------------------------- /libraries/logService.js: -------------------------------------------------------------------------------- 1 | () => { 2 | 3 | const exports = { 4 | error, 5 | get 6 | }; 7 | 8 | const errors = []; 9 | 10 | function initialise() { 11 | window.onerror = function(message, url, lineNumber, columnNumber, error) { 12 | errors.push({ 13 | time: Date.now(), 14 | message, 15 | url, 16 | lineNumber, 17 | columnNumber, 18 | error 19 | }); 20 | return false; 21 | }; 22 | } 23 | 24 | function error() { 25 | errors.push({ 26 | time: Date.now(), 27 | value: [...arguments] 28 | }); 29 | } 30 | 31 | function get() { 32 | return errors; 33 | } 34 | 35 | initialise(); 36 | 37 | return exports; 38 | 39 | } -------------------------------------------------------------------------------- /features/questDisabler.js: -------------------------------------------------------------------------------- 1 | (configuration, elementWatcher) => { 2 | 3 | function initialise() { 4 | configuration.registerCheckbox({ 5 | category: 'UI Features', 6 | key: 'quest-disabler', 7 | name: 'Quest Disabler', 8 | default: false, 9 | handler: toggle 10 | }); 11 | } 12 | 13 | async function toggle(state) { 14 | await elementWatcher.exists('nav-component button[routerLink="/quests"]'); 15 | $('nav-component button[routerLink="/quests"]') 16 | .attr('disabled', state) 17 | .css('pointer-events', state ? 'none' : '') 18 | .find('.name') 19 | .css('color', state ? '#db6565' : 'white') 20 | .css('text-decoration', state ? 'line-through' : ''); 21 | } 22 | 23 | initialise(); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /caches/ingredientCache.js: -------------------------------------------------------------------------------- 1 | (request) => { 2 | 3 | const exports = { 4 | list: [], 5 | byAction: {}, 6 | byItem: {} 7 | }; 8 | 9 | async function initialise() { 10 | const ingredients = await request.listIngredients(); 11 | for(const ingredient of ingredients) { 12 | exports.list.push(ingredient); 13 | if(!exports.byAction[ingredient.action]) { 14 | exports.byAction[ingredient.action] = []; 15 | } 16 | exports.byAction[ingredient.action].push(ingredient); 17 | if(!exports.byItem[ingredient.item]) { 18 | exports.byItem[ingredient.item] = []; 19 | } 20 | exports.byItem[ingredient.item].push(ingredient); 21 | } 22 | return exports; 23 | } 24 | 25 | return initialise(); 26 | 27 | } 28 | -------------------------------------------------------------------------------- /readers/settingsReader.js: -------------------------------------------------------------------------------- 1 | (events) => { 2 | 3 | const emitEvent = events.emit.bind(null, 'reader-settings'); 4 | 5 | function initialise() { 6 | events.register('page', update); 7 | window.setInterval(update, 1000); 8 | } 9 | 10 | function update() { 11 | const page = events.getLast('page'); 12 | if(!page) { 13 | return; 14 | } 15 | if(page.type === 'settings') { 16 | readScreen(); 17 | } 18 | } 19 | 20 | function readScreen() { 21 | const data = { 22 | name: $('settings-page .name:contains("Username")').next().text() 23 | }; 24 | if(!data.name) { 25 | return; 26 | } 27 | emitEvent({ 28 | type: 'full', 29 | value: data 30 | }); 31 | } 32 | 33 | initialise(); 34 | 35 | } 36 | -------------------------------------------------------------------------------- /libraries/assetUtil.js: -------------------------------------------------------------------------------- 1 | (itemCache) => { 2 | const loadedImages = new Map(); 3 | 4 | const exports = { 5 | loadImageFromUrl, 6 | loadItemImage 7 | }; 8 | 9 | async function loadImageFromUrl(url) { 10 | if (loadedImages.has(url)) { 11 | return loadedImages.get(url); 12 | } 13 | 14 | return await new Promise((res) => { 15 | const img = new Image(); 16 | img.onload = () => { 17 | loadedImages.set(url, img); 18 | res(img); 19 | }; 20 | img.onerror = () => res(null); 21 | img.src = url; 22 | }); 23 | } 24 | 25 | async function loadItemImage(itemId) { 26 | const item = itemCache.byId[itemId]; 27 | if (!item) return null; 28 | return await loadImageFromUrl('assets/' + item.image); 29 | } 30 | 31 | return exports; 32 | } 33 | -------------------------------------------------------------------------------- /readers/lootReader.js: -------------------------------------------------------------------------------- 1 | (events, itemUtil) => { 2 | 3 | function initialise() { 4 | events.register('page', update); 5 | window.setInterval(update, 500); 6 | } 7 | 8 | function update() { 9 | const page = events.getLast('page'); 10 | if(!page || page.type !== 'action') { 11 | return; 12 | } 13 | const lootCard = $('skill-page .card:not(:first-child) .header > .name:contains("Loot")') 14 | .closest('.card'); 15 | if(!lootCard.length) { 16 | return; 17 | } 18 | const loot = {}; 19 | lootCard.find('.row').each((i,element) => { 20 | itemUtil.extractItem(element, loot); 21 | }); 22 | events.emit('reader-loot', { 23 | skill: page.skill, 24 | action: page.action, 25 | loot 26 | }); 27 | } 28 | 29 | initialise(); 30 | 31 | } 32 | -------------------------------------------------------------------------------- /readers/guildReader.js: -------------------------------------------------------------------------------- 1 | (events, util) => { 2 | 3 | const emitEvent = events.emit.bind(null, 'reader-guild'); 4 | 5 | function initialise() { 6 | events.register('page', update); 7 | window.setInterval(update, 1000); 8 | } 9 | 10 | function update() { 11 | const page = events.getLast('page'); 12 | if(!page) { 13 | return; 14 | } 15 | if(page.type === 'guild') { 16 | readScreen(); 17 | } 18 | } 19 | 20 | function readScreen() { 21 | const data = { 22 | name: $('guild-page .tracker .name').text(), 23 | level: util.parseNumber($('guild-page .tracker .level').text()) 24 | }; 25 | if(!data.name) { 26 | return; 27 | } 28 | emitEvent({ 29 | type: 'full', 30 | value: data 31 | }); 32 | } 33 | 34 | initialise(); 35 | 36 | } 37 | -------------------------------------------------------------------------------- /caches/petPassiveCache.js: -------------------------------------------------------------------------------- 1 | (util, request) => { 2 | 3 | const exports = { 4 | list: [], 5 | byId: {}, 6 | byName: {}, 7 | idToIndex: {} 8 | }; 9 | 10 | async function initialise() { 11 | const petPassives = await request.listPetPassives(); 12 | for(const petPassive of petPassives) { 13 | exports.list.push(petPassive); 14 | exports.byId[petPassive.id] = petPassive; 15 | exports.byName[petPassive.name] = petPassive; 16 | exports.idToIndex[petPassive.id] = exports.list.length-1; 17 | petPassive.stats = { 18 | name: petPassive.statName, 19 | value: petPassive.statValue, 20 | level: util.parseNumber(petPassive.name) 21 | }; 22 | delete petPassive.statName; 23 | delete petPassive.statValue; 24 | } 25 | return exports; 26 | } 27 | 28 | return initialise(); 29 | 30 | } 31 | -------------------------------------------------------------------------------- /features/actionEnabler.js: -------------------------------------------------------------------------------- 1 | (configuration, events) => { 2 | 3 | let enabled = false; 4 | 5 | function initialise() { 6 | configuration.registerCheckbox({ 7 | category: 'UI Features', 8 | key: 'action-enabler', 9 | name: 'Action Enabler', 10 | default: true, 11 | handler: handleConfigStateChange 12 | }); 13 | events.register('page', handlePage); 14 | } 15 | 16 | function handleConfigStateChange(state) { 17 | enabled = state; 18 | } 19 | 20 | function handlePage(page) { 21 | if(!enabled || page.type !== 'action') { 22 | return; 23 | } 24 | $('skill-page .header > .name:contains("Actions")') 25 | .closest('.card') 26 | .find('button[disabled]') 27 | .not('.container > button') 28 | .removeAttr('disabled') 29 | .find('.level') 30 | .css('color', '#db6565'); 31 | } 32 | 33 | initialise(); 34 | 35 | } 36 | -------------------------------------------------------------------------------- /stores/expStateStore.js: -------------------------------------------------------------------------------- 1 | (events, util) => { 2 | 3 | const emitEvent = events.emit.bind(null, 'state-exp'); 4 | const state = {}; 5 | 6 | function initialise() { 7 | events.register('reader-exp', handleExpReader); 8 | } 9 | 10 | function handleExpReader(event) { 11 | let updated = false; 12 | for(const skill of event) { 13 | if(!state[skill.id]) { 14 | state[skill.id] = { 15 | id: skill.id, 16 | exp: 0, 17 | level: 1 18 | }; 19 | } 20 | const level = util.expToLevel(skill.exp); 21 | if(skill.exp > state[skill.id].exp || level !== state[skill.id].level) { 22 | updated = true; 23 | state[skill.id].exp = skill.exp; 24 | state[skill.id].level = level; 25 | } 26 | } 27 | if(updated) { 28 | emitEvent(state); 29 | } 30 | } 31 | 32 | initialise(); 33 | 34 | } 35 | -------------------------------------------------------------------------------- /caches/skillCache.js: -------------------------------------------------------------------------------- 1 | (request) => { 2 | 3 | const exports = { 4 | list: [], 5 | byId: {}, 6 | byName: {}, 7 | byTechnicalName: {}, 8 | match, 9 | }; 10 | 11 | async function initialise() { 12 | const skills = await request.listSkills(); 13 | for(const skill of skills) { 14 | exports.list.push(skill); 15 | exports.byId[skill.id] = skill; 16 | exports.byName[skill.displayName] = skill; 17 | exports.byTechnicalName[skill.technicalName] = skill; 18 | } 19 | return exports; 20 | } 21 | 22 | function match(name) { 23 | name = name.toLowerCase(); 24 | for(let skill of exports.list) { 25 | if(name === skill.displayName.toLowerCase()) { 26 | return skill; 27 | } 28 | if(name === skill.technicalName.toLowerCase()) { 29 | return skill; 30 | } 31 | } 32 | } 33 | 34 | return initialise(); 35 | 36 | } 37 | -------------------------------------------------------------------------------- /packager/bump.js: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs'; 2 | import path, { dirname } from 'path'; 3 | import { fileURLToPath } from 'url'; 4 | import semverInc from 'semver/functions/inc.js'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = dirname(__filename); 8 | 9 | async function run() { 10 | let content = await readFile('prefix.js'); 11 | const from = /@version\W+(.*)/.exec(content)[1]; 12 | const to = semverInc(from, process.argv[2]); 13 | content = content 14 | .split('[\r\n]+') 15 | .map(line => !line.toLowerCase().includes('version') ? line : line.replaceAll(from, to)) 16 | .join('\n'); 17 | writeFile('prefix.js', content.replaceAll(from, to)); 18 | process.stdout.write(to); 19 | } 20 | 21 | async function readFile(filename) { 22 | return await fs.readFile(path.resolve(__dirname, filename), 'utf8'); 23 | } 24 | 25 | async function writeFile(filename, content) { 26 | fs.writeFile(path.resolve(__dirname, filename), content); 27 | } 28 | 29 | run(); 30 | -------------------------------------------------------------------------------- /stores/configurationStore.js: -------------------------------------------------------------------------------- 1 | (Promise, localConfigurationStore, _remoteConfigurationStore) => { 2 | 3 | const initialised = new Promise.Expiring(2000, 'configurationStore'); 4 | let configs = null; 5 | 6 | const exports = { 7 | save, 8 | getConfigs 9 | }; 10 | 11 | const configurationStore = _remoteConfigurationStore || localConfigurationStore; 12 | 13 | async function initialise() { 14 | configs = await configurationStore.load(); 15 | for (const key in configs) { 16 | try { 17 | configs[key] = JSON.parse(configs[key]); 18 | } catch(e){ 19 | console.error(e); 20 | } 21 | } 22 | initialised.resolve(exports); 23 | } 24 | 25 | async function save(key, value) { 26 | await configurationStore.save(key, value); 27 | configs[key] = value; 28 | } 29 | 30 | function getConfigs() { 31 | return configs; 32 | } 33 | 34 | initialise(); 35 | 36 | return initialised; 37 | 38 | } 39 | -------------------------------------------------------------------------------- /features/petHighlighter.js: -------------------------------------------------------------------------------- 1 | (events) => { 2 | 3 | const exports = { 4 | highlight 5 | }; 6 | 7 | let currentColor = null; 8 | let currentNames = null; 9 | 10 | function initialise() { 11 | events.register('page', update); 12 | events.register('state-pet', update); 13 | } 14 | 15 | function highlight(color, names) { 16 | currentColor = color; 17 | currentNames = names; 18 | } 19 | 20 | function update() { 21 | if(!currentColor || !currentNames || !currentNames.length) { 22 | return; 23 | } 24 | const page = events.getLast('page'); 25 | if(page?.type === 'taming' && page.menu === 'pets') { 26 | events.getLast('state-pet') 27 | .filter(pet => currentNames.includes(pet.name) && pet.element) 28 | .forEach(pet => { 29 | $(pet.element).css('box-shadow', `inset 0px 0px 8px 0px ${currentColor}`) 30 | }); 31 | } 32 | } 33 | 34 | initialise(); 35 | 36 | return exports; 37 | 38 | } 39 | -------------------------------------------------------------------------------- /features/debugService.js: -------------------------------------------------------------------------------- 1 | (request, toast, statsStore, EstimationGenerator, logService, events, util) => { 2 | 3 | const exports = { 4 | submit 5 | }; 6 | 7 | async function submit() { 8 | const data = get(); 9 | try { 10 | await forward(data); 11 | } catch(e) { 12 | exportToClipboard(data); 13 | } 14 | } 15 | 16 | function get() { 17 | return { 18 | stats: statsStore.get(), 19 | state: (new EstimationGenerator()).export(), 20 | logs: logService.get(), 21 | events: events.getLastCache() 22 | }; 23 | } 24 | 25 | async function forward(data) { 26 | await request.report(data); 27 | toast.create({ 28 | text: 'Forwarded debug data', 29 | image: 'https://img.icons8.com/?size=48&id=13809' 30 | }); 31 | } 32 | 33 | function exportToClipboard(data) { 34 | toast.copyToClipboard(JSON.stringify(data), 'Failed to forward, exported to clipboard instead'); 35 | } 36 | 37 | return exports; 38 | 39 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Boldy97 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packager/prefix.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Ironwood RPG - Pancake-Scripts 3 | // @namespace http://tampermonkey.net/ 4 | // @version 6.3.0 5 | // @description A collection of scripts to enhance Ironwood RPG - https://github.com/Boldy97/ironwood-scripts 6 | // @author Pancake 7 | // @match https://ironwoodrpg.com/* 8 | // @icon https://www.google.com/s2/favicons?sz=64&domain=ironwoodrpg.com 9 | // @grant none 10 | // @require https://code.jquery.com/jquery-3.6.4.min.js 11 | // ==/UserScript== 12 | 13 | window.PANCAKE_ROOT = 'https://iwrpg.vectordungeon.com'; 14 | window.PANCAKE_VERSION = '6.3.0'; 15 | Object.defineProperty(Array.prototype, '_groupBy', { 16 | enumerable: false, 17 | value: function(selector) { 18 | return Object.values(this.reduce(function(rv, x) { 19 | (rv[selector(x)] = rv[selector(x)] || []).push(x); 20 | return rv; 21 | }, {})); 22 | } 23 | }); 24 | Object.defineProperty(Array.prototype, '_distinct', { 25 | enumerable: false, 26 | value: function() { 27 | return [...new Set(this)]; 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /libraries/polyfill.js: -------------------------------------------------------------------------------- 1 | () => { 2 | 3 | const exports = { 4 | requestIdleCallback 5 | }; 6 | 7 | function requestIdleCallback() { 8 | if(!window.requestIdleCallback) { 9 | window.requestIdleCallback = function(callback, options) { 10 | var options = options || {}; 11 | var relaxation = 1; 12 | var timeout = options.timeout || relaxation; 13 | var start = performance.now(); 14 | return setTimeout(function () { 15 | callback({ 16 | get didTimeout() { 17 | return options.timeout ? false : (performance.now() - start) - relaxation > timeout; 18 | }, 19 | timeRemaining: function () { 20 | return Math.max(0, relaxation + (performance.now() - start)); 21 | }, 22 | }); 23 | }, relaxation); 24 | }; 25 | } 26 | return window.requestIdleCallback(...arguments); 27 | } 28 | 29 | return exports; 30 | 31 | } 32 | -------------------------------------------------------------------------------- /caches/statNameCache.js: -------------------------------------------------------------------------------- 1 | (request) => { 2 | 3 | const exports = { 4 | list: [], 5 | byName: {}, 6 | validate 7 | }; 8 | 9 | async function initialise() { 10 | const stats = await request.listItemStats(); 11 | // frontend only 12 | stats.push('MAX_AMOUNT'); 13 | stats.push('MASTERY_PET_PASSIVE'); 14 | stats.push('MASTERY_DUNGEON_RUNE'); 15 | stats.push('MASTERY_AUTOMATION'); // currently not used 16 | stats.push('MASTERY_BOUNTIFUL_HARVEST'); 17 | stats.push('MASTERY_OPULENT_CRAFTING'); 18 | stats.push('MASTERY_SAVAGE_LOOTING'); 19 | stats.push('MASTERY_INSATIABLE_POWER'); 20 | stats.push('MASTERY_POTENT_CONCOCTION'); 21 | stats.push('MASTERY_RUNIC_WISDOM'); 22 | for(const stat of stats) { 23 | exports.list.push(stat); 24 | exports.byName[stat] = stat; 25 | } 26 | return exports; 27 | } 28 | 29 | function validate(name) { 30 | if(!exports.byName[name]) { 31 | throw `Unsupported stat usage : ${name}`; 32 | } 33 | } 34 | 35 | return initialise(); 36 | 37 | } -------------------------------------------------------------------------------- /stores/variousStateStore.js: -------------------------------------------------------------------------------- 1 | (events) => { 2 | 3 | const emitEvent = events.emit.bind(null, 'state-various'); 4 | const state = {}; 5 | 6 | function initialise() { 7 | events.register('reader-various', handleReader); 8 | } 9 | 10 | function handleReader(event) { 11 | const updated = merge(state, event); 12 | if(updated) { 13 | emitEvent(state); 14 | } 15 | } 16 | 17 | function merge(target, source) { 18 | let updated = false; 19 | for(const key in source) { 20 | if(!(key in target)) { 21 | target[key] = source[key]; 22 | updated = true; 23 | continue; 24 | } 25 | if(typeof target[key] === 'object' && typeof source[key] === 'object') { 26 | updated |= merge(target[key], source[key]); 27 | continue; 28 | } 29 | if(target[key] !== source[key]) { 30 | target[key] = source[key]; 31 | updated = true; 32 | continue; 33 | } 34 | } 35 | return updated; 36 | } 37 | 38 | initialise(); 39 | 40 | } 41 | -------------------------------------------------------------------------------- /caches/petCache.js: -------------------------------------------------------------------------------- 1 | (request) => { 2 | 3 | const exports = { 4 | list: [], 5 | byId: {}, 6 | byName: {}, 7 | byImage: {}, 8 | idToIndex: {} 9 | }; 10 | 11 | async function initialise() { 12 | const pets = await request.listPets(); 13 | for(const pet of pets) { 14 | exports.list.push(pet); 15 | exports.byId[pet.id] = pet; 16 | exports.byName[pet.name] = pet; 17 | exports.idToIndex[pet.id] = exports.list.length-1; 18 | const lastPart = pet.image.split('/').at(-1); 19 | exports.byImage[lastPart] = pet; 20 | pet.abilities = [{ 21 | [pet.abilityName1]: pet.abilityValue1 22 | }]; 23 | if(pet.abilityName2) { 24 | pet.abilities.push({ 25 | [pet.abilityName2]: pet.abilityValue2 26 | }); 27 | } 28 | delete pet.abilityName1; 29 | delete pet.abilityValue1; 30 | delete pet.abilityName2; 31 | delete pet.abilityValue2; 32 | } 33 | return exports; 34 | } 35 | 36 | return initialise(); 37 | 38 | } 39 | -------------------------------------------------------------------------------- /stores/lootStore.js: -------------------------------------------------------------------------------- 1 | (events, util) => { 2 | 3 | let state = null; 4 | 5 | function initialise() { 6 | events.register('reader-loot', handle); 7 | } 8 | 9 | function handle(event) { 10 | // first time 11 | if(state == null) { 12 | return emit(event, false); 13 | } 14 | // compare action and skill 15 | if(state.skill !== event.skill || state.action !== event.action) { 16 | return emit(event, false); 17 | } 18 | // check updated amounts 19 | if(Object.keys(event.loot).length !== Object.keys(state.loot).length) { 20 | return emit(event, true); 21 | } 22 | for(const key in event.loot) { 23 | if(event.loot[key] !== state.loot[key] || event.loot[key] !== state.loot[key]) { 24 | return emit(event, true); 25 | } 26 | } 27 | } 28 | 29 | function emit(event, includePartialDelta) { 30 | if(includePartialDelta) { 31 | event.delta = util.deltaObjects(state.loot, event.loot); 32 | } else { 33 | event.delta = event.loot; 34 | } 35 | state = event; 36 | events.emit('state-loot', state); 37 | } 38 | 39 | initialise(); 40 | 41 | } 42 | -------------------------------------------------------------------------------- /readers/structuresReader.js: -------------------------------------------------------------------------------- 1 | (events, util, structuresCache) => { 2 | 3 | const emitEvent = events.emit.bind(null, 'reader-structures'); 4 | 5 | function initialise() { 6 | events.register('page', update); 7 | window.setInterval(update, 1000); 8 | } 9 | 10 | function update() { 11 | const page = events.getLast('page'); 12 | if(!page) { 13 | return; 14 | } 15 | if(page.type === 'structure' && $('home-page .categories .category-active').text() === 'Build') { 16 | readStructuresScreen(); 17 | } 18 | } 19 | 20 | function readStructuresScreen() { 21 | const structures = {}; 22 | $('home-page .categories + .card button').each((i,element) => { 23 | element = $(element); 24 | const name = element.find('.name').text(); 25 | const structure = structuresCache.byName[name]; 26 | if(!structure) { 27 | return; 28 | } 29 | const level = util.parseNumber(element.find('.level').text()); 30 | structures[structure.id] = level; 31 | }); 32 | emitEvent({ 33 | type: 'full', 34 | value: structures 35 | }); 36 | } 37 | 38 | initialise(); 39 | 40 | } 41 | -------------------------------------------------------------------------------- /libraries/events.js: -------------------------------------------------------------------------------- 1 | () => { 2 | 3 | const exports = { 4 | register, 5 | emit, 6 | getLast, 7 | getLastCache 8 | }; 9 | 10 | const handlers = {}; 11 | const lastCache = {}; 12 | 13 | function register(name, handler) { 14 | if(!handlers[name]) { 15 | handlers[name] = []; 16 | } 17 | handlers[name].push(handler); 18 | if(lastCache[name]) { 19 | handle(handler, lastCache[name], name); 20 | } 21 | } 22 | 23 | // options = { skipCache } 24 | function emit(name, data, options) { 25 | if(!options?.skipCache) { 26 | lastCache[name] = data; 27 | } 28 | if(!handlers[name]) { 29 | return; 30 | } 31 | for(const handler of handlers[name]) { 32 | handle(handler, data, name); 33 | } 34 | } 35 | 36 | function handle(handler, data, name) { 37 | try { 38 | handler(data, name); 39 | } catch(e) { 40 | console.error('Something went wrong', e); 41 | } 42 | } 43 | 44 | function getLast(name) { 45 | return lastCache[name]; 46 | } 47 | 48 | function getLastCache() { 49 | return lastCache; 50 | } 51 | 52 | return exports; 53 | 54 | } 55 | -------------------------------------------------------------------------------- /readers/enchantmentsReader.js: -------------------------------------------------------------------------------- 1 | (events, util, structuresCache) => { 2 | 3 | const emitEvent = events.emit.bind(null, 'reader-enchantments'); 4 | 5 | function initialise() { 6 | events.register('page', update); 7 | window.setInterval(update, 1000); 8 | } 9 | 10 | function update() { 11 | const page = events.getLast('page'); 12 | if(!page) { 13 | return; 14 | } 15 | if(page.type === 'enchantment' && $('home-page .categories .category-active').text() === 'Enchant') { 16 | readEnchantmentsScreen(); 17 | } 18 | } 19 | 20 | function readEnchantmentsScreen() { 21 | const enchantments = {}; 22 | $('home-page .categories + .card button').each((i,element) => { 23 | element = $(element); 24 | const name = element.find('.name').text(); 25 | const structure = structuresCache.byName[name]; 26 | if(!structure) { 27 | return; 28 | } 29 | const level = util.parseNumber(element.find('.level').text()); 30 | enchantments[structure.id] = level; 31 | }); 32 | emitEvent({ 33 | type: 'full', 34 | value: enchantments 35 | }); 36 | } 37 | 38 | initialise(); 39 | 40 | } 41 | -------------------------------------------------------------------------------- /readers/guildStructuresReader.js: -------------------------------------------------------------------------------- 1 | (events, util, structuresCache) => { 2 | 3 | const emitEvent = events.emit.bind(null, 'reader-structures-guild'); 4 | 5 | function initialise() { 6 | events.register('page', update); 7 | window.setInterval(update, 1000); 8 | } 9 | 10 | function update() { 11 | const page = events.getLast('page'); 12 | if(!page) { 13 | return; 14 | } 15 | if(page.type === 'guild' && $('guild-page .tracker ~ div button.row-active .name').text() === 'Buildings') { 16 | readGuildStructuresScreen(); 17 | } 18 | } 19 | 20 | function readGuildStructuresScreen() { 21 | const structures = {}; 22 | $('guild-page .card').first().find('button').each((i,element) => { 23 | element = $(element); 24 | const name = element.find('.name').text(); 25 | const structure = structuresCache.byName[name]; 26 | if(!structure) { 27 | return; 28 | } 29 | const level = util.parseNumber(element.find('.amount').text()); 30 | structures[structure.id] = level; 31 | }); 32 | emitEvent({ 33 | type: 'full', 34 | value: structures 35 | }); 36 | } 37 | 38 | initialise(); 39 | 40 | } 41 | -------------------------------------------------------------------------------- /features/recipeClickthrough.js: -------------------------------------------------------------------------------- 1 | (recipeCache, configuration, util) => { 2 | 3 | let enabled = false; 4 | 5 | function initialise() { 6 | configuration.registerCheckbox({ 7 | category: 'UI Features', 8 | key: 'recipe-click', 9 | name: 'Recipe clickthrough', 10 | default: true, 11 | handler: handleConfigStateChange 12 | }); 13 | $(document).on('click', 'div.image > img', handleClick); 14 | } 15 | 16 | function handleConfigStateChange(state) { 17 | enabled = state; 18 | } 19 | 20 | function handleClick(event) { 21 | if(!enabled) { 22 | return; 23 | } 24 | if($(event.currentTarget).closest('button').length) { 25 | return; 26 | } 27 | event.stopPropagation(); 28 | const name = $(event.relatedTarget).find('.name').text(); 29 | const nameMatch = recipeCache.byName[name]; 30 | if(nameMatch) { 31 | return followRecipe(nameMatch); 32 | } 33 | 34 | const parts = event.target.src.split('/'); 35 | const lastPart = parts[parts.length-1]; 36 | const imageMatch = recipeCache.byImage[lastPart]; 37 | if(imageMatch) { 38 | return followRecipe(imageMatch); 39 | } 40 | } 41 | 42 | function followRecipe(recipe) { 43 | util.goToPage(recipe.url); 44 | } 45 | 46 | initialise(); 47 | 48 | } 49 | -------------------------------------------------------------------------------- /stores/abstractStateStore.js: -------------------------------------------------------------------------------- 1 | (events, util) => { 2 | 3 | const SOURCES = [ 4 | 'inventory', 5 | 'equipment-runes', 6 | 'equipment-tomes', 7 | 'structures', 8 | 'enchantments', 9 | 'structures-guild', 10 | 'marks', 11 | 'traits' 12 | ]; 13 | 14 | const stateBySource = {}; 15 | 16 | function initialise() { 17 | for(const source of SOURCES) { 18 | stateBySource[source] = {}; 19 | events.register(`reader-${source}`, handleReader.bind(null, source)); 20 | } 21 | } 22 | 23 | function handleReader(source, event) { 24 | let updated = false; 25 | if(event.type === 'full' || event.type === 'cache') { 26 | if(util.compareObjects(stateBySource[source], event.value)) { 27 | return; 28 | } 29 | updated = true; 30 | stateBySource[source] = event.value; 31 | } 32 | if(event.type === 'partial') { 33 | for(const key of Object.keys(event.value)) { 34 | if(stateBySource[source][key] === event.value[key]) { 35 | continue; 36 | } 37 | updated = true; 38 | stateBySource[source][key] = event.value[key]; 39 | } 40 | } 41 | if(updated) { 42 | events.emit(`state-${source}`, stateBySource[source]); 43 | } 44 | } 45 | 46 | initialise(); 47 | 48 | } 49 | -------------------------------------------------------------------------------- /stores/equipmentStateStore.js: -------------------------------------------------------------------------------- 1 | (events, util, itemCache) => { 2 | 3 | let state = {}; 4 | 5 | function initialise() { 6 | events.register('reader-equipment-equipment', handleEquipmentReader); 7 | } 8 | 9 | function handleEquipmentReader(event) { 10 | let updated = false; 11 | if(event.type === 'full' || event.type === 'cache') { 12 | if(util.compareObjects(state, event.value)) { 13 | return; 14 | } 15 | updated = true; 16 | state = event.value; 17 | } 18 | if(event.type === 'partial') { 19 | for(const key of Object.keys(event.value)) { 20 | if(state[key] === event.value[key]) { 21 | continue; 22 | } 23 | updated = true; 24 | // remove items of similar type 25 | for(const itemType in itemCache.specialIds) { 26 | if(Array.isArray(itemCache.specialIds[itemType]) && itemCache.specialIds[itemType].includes(+key)) { 27 | for(const itemId of itemCache.specialIds[itemType]) { 28 | delete state[itemId]; 29 | } 30 | } 31 | } 32 | state[key] = event.value[key]; 33 | } 34 | } 35 | if(updated) { 36 | events.emit('state-equipment-equipment', state); 37 | } 38 | } 39 | 40 | initialise(); 41 | 42 | } 43 | -------------------------------------------------------------------------------- /readers/equipmentReader.js: -------------------------------------------------------------------------------- 1 | (events, itemUtil) => { 2 | 3 | function initialise() { 4 | events.register('page', update); 5 | window.setInterval(update, 1000); 6 | } 7 | 8 | function update() { 9 | const page = events.getLast('page'); 10 | if(!page) { 11 | return; 12 | } 13 | if(page.type === 'equipment') { 14 | readEquipmentScreen(); 15 | } 16 | if(page.type === 'action') { 17 | readActionScreen(); 18 | } 19 | } 20 | 21 | function readEquipmentScreen() { 22 | const equipment = {}; 23 | const activeTab = $('equipment-page .categories button[disabled]').text().toLowerCase(); 24 | $('equipment-page .header + .items > .item > .description').parent().each((i,element) => { 25 | itemUtil.extractItem(element, equipment); 26 | }); 27 | events.emit(`reader-equipment-${activeTab}`, { 28 | type: 'full', 29 | value: equipment 30 | }); 31 | } 32 | 33 | function readActionScreen() { 34 | const equipment = {}; 35 | $('skill-page .header > .name:contains("Consumables")').closest('.card').find('button > .name:not(.placeholder)').parent().each((i,element) => { 36 | itemUtil.extractItem(element, equipment); 37 | }); 38 | events.emit('reader-equipment-equipment', { 39 | type: 'partial', 40 | value: equipment 41 | }); 42 | } 43 | 44 | initialise(); 45 | 46 | } 47 | -------------------------------------------------------------------------------- /libraries/hotkey.js: -------------------------------------------------------------------------------- 1 | () => { 2 | const keyHandlers = new Map(); 3 | 4 | const exports = { 5 | attach, 6 | detach, 7 | detachAll 8 | }; 9 | 10 | function initialise() { 11 | $(window).on('keydown._globalKeyManager', onKeydown); 12 | } 13 | 14 | function onKeydown(e) { 15 | const el = document.activeElement; 16 | const isUserFocusable = 17 | el.tagName === 'INPUT' || 18 | el.tagName === 'TEXTAREA' || 19 | el.tagName === 'BUTTON' || 20 | el.isContentEditable; 21 | 22 | const key = e.key.toLowerCase(); 23 | const handler = keyHandlers.get(key); 24 | 25 | if (!handler) return; 26 | 27 | if (isUserFocusable && !handler.override) return; 28 | 29 | e.preventDefault(); 30 | handler.callback(e); 31 | } 32 | 33 | function attach(key, callback, override = false) { 34 | if (typeof key !== 'string' || typeof callback !== 'function' || key.trim() === '') return; 35 | 36 | const normalizedKey = key.trim().toLowerCase(); 37 | keyHandlers.set(normalizedKey, { callback, override }); 38 | } 39 | 40 | function detach(key) { 41 | if (typeof key !== 'string' || key.trim() === '') return; 42 | 43 | const normalizedKey = key.trim().toLowerCase(); 44 | keyHandlers.delete(normalizedKey); 45 | } 46 | 47 | function detachAll() { 48 | keyHandlers.clear(); 49 | } 50 | 51 | initialise(); 52 | 53 | return exports; 54 | } 55 | -------------------------------------------------------------------------------- /features/marketListingLimitWarning.js: -------------------------------------------------------------------------------- 1 | (events, configuration, colorMapper) => { 2 | 3 | const LISTING_LIMIT = 250; 4 | let enabled = false; 5 | 6 | function initialise() { 7 | configuration.registerCheckbox({ 8 | category: 'Market', 9 | key: 'market-listing-limit-warning', 10 | name: 'Listing limit warning', 11 | default: true, 12 | handler: handleConfigStateChange 13 | }); 14 | events.register('reader-market', update); 15 | } 16 | 17 | function handleConfigStateChange(state) { 18 | enabled = state; 19 | } 20 | 21 | function update(marketData) { 22 | $('.market-listing-limit-warning').remove(); 23 | if(!enabled) { 24 | return; 25 | } 26 | if(marketData.type === 'OWN') { 27 | return; 28 | } 29 | if(marketData.count <= LISTING_LIMIT) { 30 | return; 31 | } 32 | if(marketData.listings.length < LISTING_LIMIT) { 33 | return; 34 | } 35 | $('market-page .count').before(` 36 |
37 | 38 | Not all listings visible 39 |
40 | `); 41 | } 42 | 43 | initialise(); 44 | 45 | } 46 | -------------------------------------------------------------------------------- /features/dropChanceDisplay.js: -------------------------------------------------------------------------------- 1 | (configuration, events, dropCache, itemCache, util) => { 2 | 3 | let enabled = false; 4 | 5 | function initialise() { 6 | configuration.registerCheckbox({ 7 | category: 'UI Features', 8 | key: 'drop-chance-display', 9 | name: 'Drop Chance Display', 10 | default: true, 11 | handler: handleConfigStateChange 12 | }); 13 | events.register('page', handlePage); 14 | } 15 | 16 | function handleConfigStateChange(state) { 17 | enabled = state; 18 | } 19 | 20 | function handlePage(page) { 21 | if(!enabled || page.type !== 'action') { 22 | return; 23 | } 24 | const drops = dropCache.byAction[page.action]; 25 | let list = $('action-drops-component .item') 26 | .toArray() 27 | .map(element => ({ 28 | element, 29 | name: $(element).find('.name').text() 30 | })); 31 | list.forEach(a => { 32 | a.item = itemCache.byName[a.name]; 33 | a.drop = drops.find(b => b.item === a.item.id); 34 | }); 35 | list = list.filter(a => a.drop); 36 | $('.pancakeChance').remove(); 37 | for(const a of list) { 38 | $(a.element).find('.chance').after( 39 | $(`
 (${util.formatNumber(100 * a.drop.chance)}%)
`) 40 | .css('color', '#aaa') 41 | ); 42 | } 43 | } 44 | 45 | initialise(); 46 | 47 | } 48 | -------------------------------------------------------------------------------- /features/conversionHider.js: -------------------------------------------------------------------------------- 1 | (configuration, elementWatcher) => { 2 | 3 | let enabled = false; 4 | 5 | function initialise() { 6 | configuration.registerCheckbox({ 7 | category: 'UI Features', 8 | key: 'conversion-hider', 9 | name: 'Hide unavailable conversions', 10 | default: true, 11 | handler: handleConfigStateChange 12 | }); 13 | const chains = [ 14 | ['app-component > div.scroll div.wrapper', 'skill-page'], 15 | ['app-component > div.scroll div.wrapper', 'taming-page'], 16 | ['app-component > div.scroll div.wrapper', 'home-page', '.groups', '.group', 'automate-component'] 17 | ]; 18 | for(const chain of chains) { 19 | elementWatcher.addRecursiveObserver(onSelection, ...chain, 'charcoal-component'); 20 | elementWatcher.addRecursiveObserver(onSelection, ...chain, 'compost-component'); 21 | elementWatcher.addRecursiveObserver(onSelection, ...chain, 'arcane-powder-component'); 22 | elementWatcher.addRecursiveObserver(onSelection, ...chain, 'pet-snacks-component'); 23 | elementWatcher.addRecursiveObserver(onSelection, ...chain, 'metal-parts-component'); 24 | elementWatcher.addRecursiveObserver(onSelection, ...chain, 'sigil-pieces-component'); 25 | } 26 | } 27 | 28 | function handleConfigStateChange(state) { 29 | enabled = state; 30 | } 31 | 32 | function onSelection(screen) { 33 | if(!enabled) { 34 | return; 35 | } 36 | $(screen).find('button[disabled]').remove(); 37 | } 38 | 39 | initialise(); 40 | 41 | } 42 | -------------------------------------------------------------------------------- /features/petRenamer.js: -------------------------------------------------------------------------------- 1 | (configuration, events, petUtil, elementCreator, toast) => { 2 | 3 | let enabled = false; 4 | let lastSeenPet; 5 | let pasteButton; 6 | 7 | function initialise() { 8 | configuration.registerCheckbox({ 9 | category: 'Pets', 10 | key: 'pet-rename', 11 | name: 'Name suggestions', 12 | default: false, 13 | handler: handleConfigStateChange 14 | }); 15 | events.register('reader-pet', handlePetReader); 16 | $(document).on('click', 'modal-component .header .heading', onRename); 17 | pasteButton = elementCreator.getButton('Paste encoded name', pasteName); 18 | } 19 | 20 | function handleConfigStateChange(state) { 21 | enabled = state; 22 | } 23 | 24 | function handlePetReader(event) { 25 | if(event.type === 'single') { 26 | lastSeenPet = event.value; 27 | } 28 | } 29 | 30 | function onRename() { 31 | if(!enabled) { 32 | return; 33 | } 34 | const page = events.getLast('page'); 35 | if(!page || page.type !== 'taming') { 36 | return; 37 | } 38 | $('modal-component .header > .name').append(pasteButton); 39 | } 40 | 41 | function pasteName() { 42 | const text = petUtil.petToText(lastSeenPet); 43 | const input = $('modal-component input'); 44 | input.val(text); 45 | input[0].dispatchEvent(new Event('input')); 46 | toast.create({ 47 | text: 'Pasted encoded name', 48 | image: 'https://img.icons8.com/?size=48&id=22244' 49 | }); 50 | } 51 | 52 | initialise(); 53 | 54 | } -------------------------------------------------------------------------------- /readers/variousReader.js: -------------------------------------------------------------------------------- 1 | (events, util) => { 2 | 3 | const emitEvent = events.emit.bind(null, 'reader-various'); 4 | 5 | function initialise() { 6 | events.register('page', update); 7 | window.setInterval(update, 1000); 8 | } 9 | 10 | function update() { 11 | const page = events.getLast('page'); 12 | if(!page) { 13 | return; 14 | } 15 | const various = {}; 16 | if(page.type === 'action') { 17 | readActionScreen(various, page.skill); 18 | emitEvent(various); 19 | } 20 | if(page.type === 'settings') { 21 | readSettingsScreen(various); 22 | emitEvent(various); 23 | } 24 | } 25 | 26 | function readActionScreen(various, skillId) { 27 | const amountText = $('skill-page .header > .name:contains("Loot")').parent().find('.amount').text(); 28 | const amountValue = !amountText ? null : util.parseNumber(amountText.split(' / ')[1]) - util.parseNumber(amountText.split(' / ')[0]); 29 | various.maxAmount = { 30 | [skillId]: amountValue 31 | }; 32 | const opulenceMode = $('skill-page .header > .name:contains("Consumables")').closest('.card').find('.row > .name:contains("Stardust")').closest('.row').find('.use').text(); 33 | if(opulenceMode) { 34 | various.opulenceMode = opulenceMode; 35 | } 36 | } 37 | 38 | function readSettingsScreen(various) { 39 | const username = $('settings-page .row:contains("Username") :last-child').text(); 40 | if(username) { 41 | various.username = username; 42 | } 43 | } 44 | 45 | initialise(); 46 | 47 | } 48 | -------------------------------------------------------------------------------- /readers/guildEventReader.js: -------------------------------------------------------------------------------- 1 | (events, util) => { 2 | 3 | const emitEvent = events.emit.bind(null, 'reader-guild-event'); 4 | const ONE_MINUTE = 1000 * 60; 5 | const TWO_DAYS = 1000 * 60 * 60 * 24 * 2; 6 | 7 | function initialise() { 8 | events.register('page', update); 9 | window.setInterval(update, 1000); 10 | } 11 | 12 | function update() { 13 | const page = events.getLast('page'); 14 | if(!page) { 15 | return; 16 | } 17 | if(page.type === 'guild' && $('guild-page .tracker ~ div button.row-active .name').text() === 'Events') { 18 | readScreen(); 19 | } 20 | } 21 | 22 | function readScreen() { 23 | const eventRunning = $('guild-page .header:contains("Event")').parent().text().includes('Guild Credits'); 24 | let eventStartMillis = null; 25 | let eventType = null; 26 | if(eventRunning) { 27 | const time = []; 28 | $('guild-page .header:contains("Event")').parent().find('.date').children().each((index, element) => time.push($(element).text())); 29 | const eventSecondsRemaining = util.parseDuration(time.join(' ')); 30 | eventStartMillis = Date.now() - TWO_DAYS + 1000 * eventSecondsRemaining; 31 | eventStartMillis = util.roundToMultiple(eventStartMillis, ONE_MINUTE); 32 | eventType = $('guild-page .header:contains("Event")').parent().find('.date').prev().text().split(' Event')[0]; 33 | } 34 | const data = { 35 | eventRunning, 36 | eventStartMillis, 37 | eventType 38 | }; 39 | emitEvent({ 40 | type: 'full', 41 | value: data 42 | }); 43 | } 44 | 45 | initialise(); 46 | 47 | } 48 | -------------------------------------------------------------------------------- /libraries/itemUtil.js: -------------------------------------------------------------------------------- 1 | (util, itemCache) => { 2 | 3 | const exports = { 4 | extractItem 5 | }; 6 | 7 | function extractItem(element, target, ignoreMissing) { 8 | element = $(element); 9 | const name = element.find('.name').text(); 10 | let item = itemCache.byName[name]; 11 | if(!item) { 12 | const src = element.find('img').attr('src'); 13 | if(src) { 14 | const image = src.split('/').at(-1); 15 | item = itemCache.byImage[image]; 16 | } 17 | } 18 | if(!item) { 19 | if(!ignoreMissing) { 20 | console.warn(`Could not find item with name [${name}]`); 21 | } 22 | return false; 23 | } 24 | let amount = 1; 25 | let amountElements = element.find('.amount, .value'); 26 | let uses = 0; 27 | if(amountElements.length) { 28 | var amountText = amountElements.text(); 29 | if(!amountText) { 30 | return false; 31 | } 32 | if(amountText.includes(' / ')) { 33 | amountText = amountText.split(' / ')[0]; 34 | } 35 | amount = util.parseNumber(amountText); 36 | if(amountText.includes('&')) { 37 | const usesText = amountText.split('&')[1]; 38 | uses = util.parseNumber(usesText); 39 | } 40 | } 41 | if(!uses) { 42 | const usesText = element.find('.uses, .use').text(); 43 | if(usesText && !usesText.endsWith('HP')) { 44 | uses = util.parseNumber(usesText); 45 | } 46 | } 47 | amount += uses; 48 | target[item.id] = (target[item.id] || 0) + amount; 49 | return item; 50 | } 51 | 52 | return exports; 53 | 54 | } 55 | -------------------------------------------------------------------------------- /readers/questReader.js: -------------------------------------------------------------------------------- 1 | (events, util) => { 2 | 3 | const emitEvent = events.emit.bind(null, 'reader-quests'); 4 | 5 | function initialise() { 6 | events.register('page', update); 7 | window.setInterval(update, 1000); 8 | } 9 | 10 | function update() { 11 | const page = events.getLast('page'); 12 | if (!page) { 13 | return; 14 | } 15 | if (page.type === 'quests') { 16 | readScreen(); 17 | } 18 | } 19 | 20 | function readScreen() { 21 | const statsCard = $('quests-page .card:has(.header .name:contains("Stats"))'); 22 | 23 | const data = { 24 | currentCompletedQuests: util.parseNumber($('quests-page .header > .amount').text().split(' / ')[0]), 25 | maxCompletedQuests: util.parseNumber($('quests-page .header > .amount').text().split(' / ')[1]), 26 | currentAutoCompletes: util.parseNumber(statsCard.find('.row:has(.name:contains("Auto Quest Completes")) div:last').text().split(' / ')[0]), 27 | maxAutoCompletes: util.parseNumber(statsCard.find('.row:has(.name:contains("Auto Quest Completes")) div:last').text().split(' / ')[1]), 28 | resetTime: util.parseDuration( 29 | statsCard.find('.row:has(.name:contains("Daily Quest Reset")) .time') 30 | .children() 31 | .get() 32 | .map(a => a.textContent) 33 | .join(' ') 34 | ), 35 | totalQuestsCompleted: util.parseNumber(statsCard.find('.row:has(.name:contains("Quests Completed")) div:last').text()), 36 | missingQuestPoints: util.parseNumber(statsCard.find('.row:has(.name:contains("Missing QP")) div:last').text().replace(' QP', '')), 37 | }; 38 | emitEvent(data); 39 | } 40 | 41 | initialise(); 42 | 43 | } 44 | -------------------------------------------------------------------------------- /features/estimatorActivity.js: -------------------------------------------------------------------------------- 1 | (skillCache, actionCache, estimatorAction, statsStore) => { 2 | 3 | const exports = { 4 | get 5 | }; 6 | 7 | function get(skillId, actionId) { 8 | const skill = skillCache.byId[skillId]; 9 | const action = actionCache.byId[actionId]; 10 | const speed = getSpeed(skill.technicalName, action); 11 | const actionCount = estimatorAction.LOOPS_PER_HOUR / speed; 12 | const actualActionCount = actionCount * (1 + statsStore.get('EFFICIENCY_CHANCE', skill.technicalName) / 100); 13 | const dropCount = actualActionCount * (1 + statsStore.get('DOUBLE_DROP_CHANCE', skill.technicalName) / 100); 14 | const ingredientCount = actualActionCount * (1 - statsStore.get('PRESERVATION_CHANCE', skill.technicalName) / 100); 15 | const exp = actualActionCount * action.exp * (1 + statsStore.get('DOUBLE_EXP_CHANCE', skill.technicalName) / 100); 16 | const drops = estimatorAction.getDrops(skillId, actionId, false, dropCount, actualActionCount); 17 | const ingredients = estimatorAction.getIngredients(skillId, actionId, ingredientCount); 18 | const equipments = estimatorAction.getEquipmentUses(skillId, actionId, actualActionCount); 19 | 20 | return { 21 | type: 'ACTIVITY', 22 | skill: skillId, 23 | action: actionId, 24 | speed, 25 | actionsPerHour: dropCount, 26 | productionSpeed: speed * actionCount / dropCount, 27 | exp, 28 | drops, 29 | ingredients, 30 | equipments 31 | }; 32 | } 33 | 34 | function getSpeed(skillName, action) { 35 | const speedBonus = statsStore.get('SKILL_SPEED', skillName); 36 | return Math.round(action.speed * 1000 / (100 + speedBonus)) + 1; 37 | } 38 | 39 | return exports; 40 | 41 | } 42 | -------------------------------------------------------------------------------- /stores/customItemPriceStore.js: -------------------------------------------------------------------------------- 1 | (localDatabase, itemCache, Promise) => { 2 | 3 | const STORE_NAME = 'item-price'; 4 | let prices = {}; 5 | 6 | const exports = { 7 | get, 8 | set 9 | }; 10 | 11 | const initialised = new Promise.Expiring(2000, 'customItemPriceStore'); 12 | 13 | async function initialise() { 14 | const entries = await localDatabase.getAllEntries(STORE_NAME); 15 | prices = {}; 16 | for(const entry of entries) { 17 | prices[entry.key] = entry.value; 18 | } 19 | initialised.resolve(exports); 20 | } 21 | 22 | function get(id) { 23 | if(prices[id]) { 24 | return prices[id]; 25 | } 26 | return getDefault(+id); 27 | } 28 | 29 | function getDefault(id) { 30 | if(id === itemCache.specialIds.coins) { 31 | return 1; 32 | } 33 | if(id === itemCache.specialIds.charcoal) { 34 | return get(itemCache.byName['Pine Log'].id); 35 | } 36 | if(id === itemCache.specialIds.stardust) { 37 | return 2; 38 | } 39 | if(id === itemCache.specialIds.masteryContract) { 40 | return 2; 41 | } 42 | const item = itemCache.byId[id]; 43 | if(item.attributes['UNTRADEABLE']) { 44 | return item.attributes.SELL_PRICE; 45 | } 46 | return item.attributes.MIN_MARKET_PRICE; 47 | } 48 | 49 | async function set(id, price) { 50 | if(!price || price === getDefault(id)) { 51 | await localDatabase.removeEntry(STORE_NAME, id); 52 | delete prices[id]; 53 | return; 54 | } 55 | await localDatabase.saveEntry(STORE_NAME, { 56 | key: id, 57 | value: price 58 | }); 59 | prices[id] = price; 60 | } 61 | 62 | initialise(); 63 | 64 | return initialised; 65 | 66 | } 67 | -------------------------------------------------------------------------------- /readers/inventoryReader.js: -------------------------------------------------------------------------------- 1 | (events, itemUtil) => { 2 | 3 | const emitEvent = events.emit.bind(null, 'reader-inventory'); 4 | 5 | function initialise() { 6 | events.register('page', update); 7 | window.setInterval(update, 1000); 8 | } 9 | 10 | function update() { 11 | const page = events.getLast('page'); 12 | if(!page) { 13 | return; 14 | } 15 | if(page.type === 'inventory') { 16 | readInventoryScreen(); 17 | } 18 | if(page.type === 'action') { 19 | readActionScreen(); 20 | } 21 | if(page.type === 'taming' && page.menu === 'expeditions') { 22 | readExpeditionsScreen(); 23 | } 24 | } 25 | 26 | function readInventoryScreen() { 27 | const inventory = {}; 28 | $('inventory-page .items > .item').each((_i,element) => { 29 | itemUtil.extractItem(element, inventory, true); 30 | }); 31 | emitEvent({ 32 | type: 'full', 33 | value: inventory 34 | }); 35 | } 36 | 37 | function readActionScreen() { 38 | const inventory = {}; 39 | $('skill-page .header > .name:contains("Materials")').closest('.card').find('.row').each((_i,element) => { 40 | itemUtil.extractItem(element, inventory); 41 | }); 42 | const stardustRow = $('skill-page .header > .name:contains("Consumables")').closest('.card').find('.row > .name:contains("Stardust")').closest('.row')[0]; 43 | if(stardustRow) { 44 | itemUtil.extractItem(stardustRow, inventory); 45 | } 46 | emitEvent({ 47 | type: 'partial', 48 | value: inventory 49 | }); 50 | } 51 | 52 | function readExpeditionsScreen() { 53 | const inventory = {}; 54 | $('taming-page .heading:contains("Materials") + button').each((_i,element) => { 55 | itemUtil.extractItem(element, inventory); 56 | }); 57 | emitEvent({ 58 | type: 'partial', 59 | value: inventory 60 | }); 61 | } 62 | 63 | initialise(); 64 | 65 | } 66 | -------------------------------------------------------------------------------- /stores/masteryStateStore.js: -------------------------------------------------------------------------------- 1 | (events, localDatabase) => { 2 | 3 | const emitEvent = events.emit.bind(null, 'state-mastery'); 4 | const DATABASE_KEY = 'masteries'; 5 | let state = { 6 | materials: {}, 7 | points: {} 8 | }; 9 | 10 | async function initialise() { 11 | await loadSavedData(); 12 | events.register('reader-mastery', handleReader); 13 | } 14 | 15 | async function loadSavedData() { 16 | const savedData = await localDatabase.getVariousEntry(DATABASE_KEY); 17 | if(savedData) { 18 | state = savedData; 19 | emitEvent(state); 20 | } 21 | } 22 | 23 | async function handleReader(event) { 24 | let updated = false; 25 | if(event.type === 'material') { 26 | updated |= handleMaterialReader(event); 27 | } 28 | if(event.type === 'full') { 29 | updated |= handlePointsReader(event); 30 | } 31 | if(updated) { 32 | await localDatabase.saveVariousEntry(DATABASE_KEY, state); 33 | emitEvent(state); 34 | } 35 | } 36 | 37 | function handleMaterialReader(event) { 38 | if(!state.materials[event.skill]) { 39 | state.materials[event.skill] = {}; 40 | } 41 | let updated = false; 42 | for(const item in event.materials) { 43 | if(state.materials[event.skill][item] === undefined || state.materials[event.skill][item] !== event.materials[item]) { 44 | updated = true; 45 | } 46 | state.materials[event.skill][item] = event.materials[item]; 47 | } 48 | return updated; 49 | } 50 | 51 | function handlePointsReader(event) { 52 | let updated = false; 53 | const newPoints = {}; 54 | for(const passive of event.passives) { 55 | updated |= !state.points[passive]; // additions 56 | newPoints[passive] = 1; 57 | } 58 | for(const key of Object.keys(state.points)) { 59 | updated |= !newPoints[key]; // removal 60 | } 61 | state.points = newPoints; 62 | return updated; 63 | } 64 | 65 | initialise(); 66 | 67 | } 68 | -------------------------------------------------------------------------------- /readers/marksReader.js: -------------------------------------------------------------------------------- 1 | (events, util, skillCache, skillSetCache) => { 2 | 3 | const emitEvent = events.emit.bind(null, 'reader-marks'); 4 | const sets = ['Forest', 'Mountain', 'Ocean', 'All'].map(name => skillSetCache.byName[name]); 5 | 6 | function initialise() { 7 | events.register('page', update); 8 | window.setInterval(update, 1000); 9 | } 10 | 11 | function update() { 12 | const page = events.getLast('page'); 13 | if(!page) { 14 | return; 15 | } 16 | if(page.type === 'marks' && page.menu === 'skill marks') { 17 | readMarksScreen(); 18 | } 19 | } 20 | 21 | function readMarksScreen() { 22 | const marks = { 23 | exp: {}, 24 | eff: {} 25 | }; 26 | // singles 27 | $('marks-page .header:contains("Skill Marks")').parent().find('.row').each((i,element) => { 28 | element = $(element); 29 | const name = element.find('.name').text().replace(/ Mark$/, ''); 30 | const amount = util.parseNumber(element.find('.amount').text()); 31 | if(amount) { 32 | const skill = skillCache.match(name); 33 | marks.exp[skill.id] = 4; 34 | } 35 | }); 36 | // sets 37 | let oneSetUnlocked = false; 38 | for(const set of sets) { 39 | if(containsAllKeys(marks.exp, set.skills)) { 40 | oneSetUnlocked = true; 41 | for(const skillId of set.skills) { 42 | marks.eff[skillId] = (marks.eff[skillId] || 0) + 2; 43 | } 44 | } 45 | } 46 | const tamingSkill = skillCache.byName['Taming']; 47 | if(oneSetUnlocked && marks.exp[tamingSkill.id]) { 48 | marks.eff[tamingSkill.id] = (marks.eff[tamingSkill.id] || 0) + 2; 49 | } 50 | emitEvent({ 51 | type: 'full', 52 | value: marks 53 | }); 54 | } 55 | 56 | function containsAllKeys(object, keys) { 57 | for(const key of keys) { 58 | if(!object[key]) { 59 | return false; 60 | } 61 | } 62 | return true; 63 | } 64 | 65 | initialise(); 66 | 67 | } 68 | -------------------------------------------------------------------------------- /readers/expReader.js: -------------------------------------------------------------------------------- 1 | (events, skillCache, util) => { 2 | 3 | const emitEvent = events.emit.bind(null, 'reader-exp'); 4 | 5 | function initialise() { 6 | events.register('page', update); 7 | window.setInterval(update, 1000); 8 | } 9 | 10 | function update() { 11 | const page = events.getLast('page'); 12 | if(!page) { 13 | return; 14 | } 15 | if(page.type === 'action') { 16 | readActionScreen(page.skill); 17 | } 18 | if(page.type === 'taming') { 19 | readTamingScreen(); 20 | } 21 | readSidebar(); 22 | } 23 | 24 | function readActionScreen(id) { 25 | const text = $('skill-page .tabs > button:contains("Stats")') 26 | .closest('.card') 27 | .find('.row > .name:contains("Total"):contains("XP")') 28 | .closest('.row') 29 | .find('.value') 30 | .text(); 31 | const exp = text ? util.parseNumber(text) : readActionScreenFallback(); 32 | emitEvent([{ id, exp }]); 33 | } 34 | 35 | function readActionScreenFallback() { 36 | const level = util.parseNumber($('tracker-component .level').text()); 37 | const exp = util.parseNumber($('tracker-component .exp').text()); 38 | return util.levelToExp(level) + exp; 39 | } 40 | 41 | function readTamingScreen() { 42 | const text = $('taming-page .header > .name:contains("Stats")') 43 | .closest('.card') 44 | .find('.row > .name:contains("Total"):contains("XP")') 45 | .closest('.row') 46 | .find('.amount') 47 | .text(); 48 | const exp = util.parseNumber(text); 49 | emitEvent([{ 50 | exp, 51 | id: skillCache.byName['Taming'].id 52 | }]); 53 | } 54 | 55 | function readSidebar() { 56 | const levels = []; 57 | $('nav-component button.skill').each((i,element) => { 58 | element = $(element); 59 | const name = element.find('.name').text(); 60 | const id = skillCache.byName[name].id; 61 | const level = +(/\d+/.exec(element.find('.level').text())?.[0]); 62 | const exp = util.levelToExp(level); 63 | levels.push({ id, exp }); 64 | }); 65 | emitEvent(levels); 66 | } 67 | 68 | initialise(); 69 | 70 | } 71 | -------------------------------------------------------------------------------- /features/changelog.js: -------------------------------------------------------------------------------- 1 | (Promise, pages, components, request, configuration) => { 2 | 3 | const PAGE_NAME = 'Plugin changelog'; 4 | const loaded = new Promise.Deferred('changelog'); 5 | 6 | let changelogs = null; 7 | 8 | async function initialise() { 9 | await pages.register({ 10 | category: 'Skills', 11 | after: 'Changelog', 12 | name: PAGE_NAME, 13 | image: 'https://ironwoodrpg.com/assets/misc/changelog.png', 14 | render: renderPage 15 | }); 16 | configuration.registerCheckbox({ 17 | category: 'Pages', 18 | key: 'changelog-enabled', 19 | name: 'Changelog', 20 | default: true, 21 | handler: handleConfigStateChange 22 | }); 23 | load(); 24 | } 25 | 26 | function handleConfigStateChange(state, name) { 27 | if(state) { 28 | pages.show(PAGE_NAME); 29 | } else { 30 | pages.hide(PAGE_NAME); 31 | } 32 | } 33 | 34 | async function load() { 35 | changelogs = await request.getChangelogs(); 36 | loaded.resolve(); 37 | } 38 | 39 | async function renderPage() { 40 | await loaded; 41 | const header = components.search(componentBlueprint, 'header'); 42 | const list = components.search(componentBlueprint, 'list'); 43 | for(const index in changelogs) { 44 | componentBlueprint.componentId = `changelogComponent_${index}`; 45 | header.title = changelogs[index].title; 46 | header.textRight = new Date(changelogs[index].time).toLocaleDateString(); 47 | list.entries = changelogs[index].entries; 48 | components.addComponent(componentBlueprint); 49 | } 50 | } 51 | 52 | const componentBlueprint = { 53 | componentId: 'changelogComponent', 54 | dependsOn: 'custom-page', 55 | parent: '.column0', 56 | selectedTabIndex: 0, 57 | tabs: [{ 58 | title: 'tab', 59 | rows: [{ 60 | id: 'header', 61 | type: 'header', 62 | title: '', 63 | textRight: '' 64 | },{ 65 | id: 'list', 66 | type: 'list', 67 | entries: [] 68 | }] 69 | }] 70 | }; 71 | 72 | initialise(); 73 | 74 | } 75 | -------------------------------------------------------------------------------- /libraries/elementCreator.js: -------------------------------------------------------------------------------- 1 | (Promise, colorMapper) => { 2 | 3 | const exports = { 4 | addStyles, 5 | addScript, 6 | getButton, 7 | getTag 8 | }; 9 | 10 | function initialise() { 11 | addStyles(styles); 12 | } 13 | 14 | function addStyles(css) { 15 | const head = document.getElementsByTagName('head')[0] 16 | if(!head) { 17 | console.error('Could not add styles, missing head'); 18 | return; 19 | } 20 | const style = document.createElement('style'); 21 | style.innerHTML = css; 22 | head.appendChild(style); 23 | } 24 | 25 | function addScript(url) { 26 | const result = new Promise.Deferred('script-' + url); 27 | $('