X-Git-Url: https://git.localhorst.tv/?a=blobdiff_plain;f=resources%2Fjs%2Fhelpers%2Ftracker.js;h=c4b685c5b9ced45af45281b71e0728bae7aac022;hb=90922f8595a8d4fd7780a0b137eed66eaf7d6c49;hp=92508d46f1bc9434b97e641c8132a4fd94f0a095;hpb=4e24f36eb5d6907697c106ca15c2c405728d78a8;p=alttp.git diff --git a/resources/js/helpers/tracker.js b/resources/js/helpers/tracker.js index 92508d4..c4b685c 100644 --- a/resources/js/helpers/tracker.js +++ b/resources/js/helpers/tracker.js @@ -7,6 +7,7 @@ import { isBossDefeated, isChestOpen, } from './alttp-ram'; +import Logic from './logic'; export const BOOLEAN_STATES = [ 'blue-boomerang', @@ -42,7 +43,10 @@ export const BOOLEAN_STATES = [ ]; export const INTEGER_STATES = [ - 'bottle', + 'bottle-1', + 'bottle-2', + 'bottle-3', + 'bottle-4', 'heart-piece', 'lift', 'mail', @@ -54,6 +58,17 @@ export const INITIAL = { mail: 1, }; +export const BOTTLE_CONTENTS = [ + 'mushroom', + 'bottle', + 'red-potion', + 'green-potion', + 'blue-potion', + 'fairy', + 'bottle-bee', + 'bottle-good-bee', +]; + export const BOSSES = [ 'armos', 'lanmolas', @@ -68,11 +83,19 @@ export const BOSSES = [ ]; export const CONFIG = { + bossShuffle: false, + checkCalculation: 'room-data', + glitches: 'none', + mapLayout: 'horizontal', + showMap: 'situational', + showCompass: 'situational', + showSmall: 'always', + showBig: 'always', wildMap: false, wildCompass: false, wildSmall: false, wildBig: false, - bossShuffle: false, + worldState: 'open', }; export const DUNGEONS = [ @@ -1552,6 +1575,56 @@ export const UNDERWORLD_LOCATIONS = [ }, ]; +export const getConfigValue = (config, name, fallback) => + Object.prototype.hasOwnProperty.call(config, name) ? config[name] : fallback; + +export const configureDungeons = config => DUNGEONS.map(dungeon => { + const newDungeon = JSON.parse(JSON.stringify(dungeon)); + if (config.wildMap && dungeon.map) { + ++newDungeon.items; + } + if (config.wildCompass && dungeon.compass) { + ++newDungeon.items; + } + if (config.wildSmall) { + newDungeon.items += dungeon.sk; + } + if (config.wildBig && dungeon.bk && !dungeon.dropBk) { + ++newDungeon.items; + } + if (dungeon.boss) { + newDungeon.bosses = config.bossShuffle ? BOSSES : [dungeon.boss]; + } + return newDungeon; +}); + +export const applyLogic = (config, dungeons, state) => { + const logic = Logic[config.worldState]; + const map = {}; + for (const name in logic) { + try { + map[name] = logic[name](config, dungeons, state); + } catch (e) { + console.error('error evaluating', name, e); + } + } + return map; +}; + +export const shouldShowDungeonItem = (config, which) => { + const show = config[`show${which}`] || 'always'; + const wild = config[`wild${which}`] || false; + switch (show) { + default: + case 'always': + return true; + case 'situational': + return wild || (which === 'Compass' && config.bossShuffle); + case 'never': + return false; + } +}; + export const toggleBoolean = name => state => ({ ...state, [name]: !state[name], @@ -1598,24 +1671,106 @@ export const unclearAll = names => state => { return { ...state, ...changes }; }; -export const hasDungeonBoss = (state, dungeon) => !!state[`${dungeon.id}-boss-defeated`]; +export const countClearedLocations = (state, locations) => + locations.reduce((acc, cur) => state[cur] ? acc + 1 : acc, 0); + +export const hasClearedLocations = (state, locations) => + countClearedLocations(state, locations) === locations.length; + +export const getLocationStatus = (name, logic, state) => { + if (state[name]) return 'cleared'; + if (logic[name]) return logic[name]; + return logic.fallback; +}; + +export const getCombinedStatus = statuses => { + if (statuses.filter(s => s === 'cleared').length === statuses.length) { + return 'cleared'; + } + if (statuses.filter(s => ['available', 'cleared'].includes(s)).length === statuses.length) { + return 'available'; + } + if (statuses.filter(s => ['unavailable', 'cleared'].includes(s)).length === statuses.length) { + return 'unavailable'; + } + return 'partial'; +}; + +export const aggregateLocationStatus = (names, logic, state) => { + const statuses = names.map(name => getLocationStatus(name, logic, state)); + return getCombinedStatus(statuses); +}; + +export const countRemainingLocations = (state, locations) => + locations.reduce((acc, cur) => state[cur] ? acc : acc + 1, 0); + +export const getGanonCrystals = (config) => getConfigValue(config, 'ganon-crystals', 7); + +export const getGTCrystals = (config) => getConfigValue(config, 'gt-crystals', 7); + +export const getGTBoss = (state, which) => state[`gt-${which}-boss`]; + +export const hasDungeonBoss = (state, dungeon) => + !dungeon.boss || !!state[`${dungeon.id}-boss-defeated`]; export const getDungeonBoss = (state, dungeon) => - state[`${dungeon.id}-boss`] || dungeon.boss || null; + dungeon.bosses.length > 1 + ? state[`${dungeon.id}-boss`] || dungeon.boss || null + : dungeon.bosses[0]; -export const hasDungeonPrize = (state, dungeon) => !!state[`${dungeon.id}-prize-acquired`]; +export const hasDungeonPrize = (state, dungeon) => + !dungeon.prize || !!state[`${dungeon.id}-prize-acquired`]; export const getDungeonPrize = (state, dungeon) => state[`${dungeon.id}-prize`] || null; +export const getDungeonClearedItems = (state, dungeon) => state[`${dungeon.id}-checks`] || 0; + +export const getDungeonRemainingItems = (state, dungeon) => + Math.max(0, dungeon.items - getDungeonClearedItems(state, dungeon)); + +export const getDungeonAcquiredSKs = (state, dungeon) => state[`${dungeon.id}-small-key`] || 0; + export const isDungeonCleared = (state, dungeon) => { - const cleared = state[`${dungeon.id}-checks`] || 0; - const total = dungeon.items; - const hasItems = cleared >= total; - const hasBoss = !dungeon.boss || hasDungeonBoss(state, dungeon); - const hasPrize = !dungeon.porize || hasDungeonPrize(state, dungeon); + const hasItems = !getDungeonRemainingItems(state, dungeon); + const hasBoss = hasDungeonBoss(state, dungeon); + const hasPrize = hasDungeonPrize(state, dungeon); return hasItems && hasBoss && hasPrize; }; +export const aggregateDungeonStatus = (dungeon, logic, state) => { + if (isDungeonCleared(state, dungeon)) { + return 'cleared'; + } + if (logic[dungeon.id] === 'unavailable') { + return 'unavailable'; + } + const checks = [...dungeon.checks]; + if (['ct', 'gt'].includes(dungeon.id)) { + checks.push(`${dungeon.id}-boss-killable`); + } + const statuses = checks.map(name => getLocationStatus(name, logic, state)); + return getCombinedStatus(statuses); +}; + +export const toggleBossDefeated = dungeon => toggleBoolean(`${dungeon.id}-boss-defeated`); + +export const setBossDefeated = (dungeon, defeated) => + state => ({ ...state, [`${dungeon.id}-boss-defeated`]: !!defeated }); + +export const togglePrizeAcquired = dungeon => toggleBoolean(`${dungeon.id}-prize-acquired`); + +export const setPrizeAcquired = (dungeon, acquired) => + state => ({ ...state, [`${dungeon.id}-prize-acquired`]: !!acquired }); + +export const addDungeonCheck = dungeon => increment(`${dungeon.id}-checks`, dungeon.items); + +export const removeDungeonCheck = dungeon => decrement(`${dungeon.id}-checks`, dungeon.items); + +export const resetDungeonChecks = dungeon => state => ({ ...state, [`${dungeon.id}-checks`]: 0 }); + +export const completeDungeonChecks = dungeon => + state => ({ ...state, [`${dungeon.id}-checks`]: dungeon.items }); + export const makeEmptyState = () => { const state = {}; BOOLEAN_STATES.forEach(p => { @@ -1638,6 +1793,11 @@ export const makeEmptyState = () => { state[`${dungeon.id}-prize`] = 'crystal'; state[`${dungeon.id}-prize-acquired`] = false; } + if (dungeon.id === 'gt') { + state['gt-bot-boss'] = 'armos'; + state['gt-mid-boss'] = 'lanmolas'; + state['gt-top-boss'] = 'moldorm'; + } }); OVERWORLD_LOCATIONS.forEach(location => { state[location.id] = false; @@ -1672,19 +1832,10 @@ const collectInventory = (state, data, prizeMap) => { state.duck = !!(data[INV_ADDR.RANDO_FLUTE] & 0x01); state.bugnet = !!data[INV_ADDR.BUGNET]; state.book = !!data[INV_ADDR.BOOK]; - state.bottle = 0; - if (data[INV_ADDR.BOTTLE_1]) { - ++state.bottle; - } - if (data[INV_ADDR.BOTTLE_2]) { - ++state.bottle; - } - if (data[INV_ADDR.BOTTLE_3]) { - ++state.bottle; - } - if (data[INV_ADDR.BOTTLE_4]) { - ++state.bottle; - } + state['bottle-1'] = data[INV_ADDR.BOTTLE_1]; + state['bottle-2'] = data[INV_ADDR.BOTTLE_2]; + state['bottle-3'] = data[INV_ADDR.BOTTLE_3]; + state['bottle-4'] = data[INV_ADDR.BOTTLE_4]; state.somaria = !!data[INV_ADDR.SOMARIA]; state.byrna = !!data[INV_ADDR.BYRNA]; state.cape = !!data[INV_ADDR.CAPE]; @@ -1707,10 +1858,26 @@ const collectInventory = (state, data, prizeMap) => { state[`${dungeon.id}-compass`] = !!(compass & dungeon.mask); state[`${dungeon.id}-small-key`] = data[INV_ADDR.RANDO_KEY_START + dungeon.offset]; state[`${dungeon.id}-big-key`] = !!(bigKey & dungeon.mask); + state[`${dungeon.id}-checks-collected`] = + data[INV_ADDR.RANDO_CHECKS_START + dungeon.offset]; if (dungeon.prize) { const isCrystal = prizeMap[dungeon.offset].isCrystal; const prizeFlags = data[isCrystal ? INV_ADDR.CRYSTALS : INV_ADDR.PENDANTS]; - state[`${dungeon.id}-prize-acquired`] = !!(prizeFlags & prizeMap[dungeon.offset].mask); + const prizeAcquired = !!(prizeFlags & prizeMap[dungeon.offset].mask); + state[`${dungeon.id}-prize-acquired`] = prizeAcquired; + if (prizeAcquired) { + if (!isCrystal) { + if (prizeMap[dungeon.offset].mask === 1) { + state[`${dungeon.id}-prize`] = 'red-pendant'; + } else if (prizeMap[dungeon.offset].mask === 2) { + state[`${dungeon.id}-prize`] = 'blue-pendant'; + } else if (prizeMap[dungeon.offset].mask === 4) { + state[`${dungeon.id}-prize`] = 'green-pendant'; + } + } else { + state[`${dungeon.id}-prize`] = 'crystal'; + } + } } }); }; @@ -1730,24 +1897,20 @@ const collectUnderworld = (state, data) => { }); }; -export const computeState = (data, prizeMap) => { - const state = {}; - collectInventory(state, data.slice(SRAM_ADDR.INV_START), prizeMap); - collectOverworld(state, data); - collectUnderworld(state, data.slice(SRAM_ADDR.ROOM_DATA_START)); - return state; -}; - const getDungeonAmounts = (config, state) => { const amounts = {}; DUNGEONS.forEach(dungeon => { let amount = 0; let total = dungeon.checks.length; - dungeon.checks.forEach(check => { - if (state[check]) { - ++amount; - } - }); + if (config.checkCalculation === 'inventory') { + amount = state[`${dungeon.id}-checks-collected`]; + } else { + dungeon.checks.forEach(check => { + if (state[check]) { + ++amount; + } + }); + } if (!config.wildMap && state[`${dungeon.id}-map`]) { --amount; --total; @@ -1769,12 +1932,80 @@ const getDungeonAmounts = (config, state) => { return amounts; }; -export const mergeStates = (config, cur, inc) => { - const next = { ...cur, ...inc }; - const amounts = getDungeonAmounts(config, inc); +export const computeState = (config, data, prizeMap) => { + const state = {}; + collectInventory(state, data.slice(SRAM_ADDR.INV_START), prizeMap); + collectOverworld(state, data); + collectUnderworld(state, data.slice(SRAM_ADDR.ROOM_DATA_START)); + const amounts = getDungeonAmounts(config, state); + DUNGEONS.forEach(dungeon => { + state[`${dungeon.id}-checks`] = amounts[dungeon.id]; + }); + return state; +}; + +export const mergeStates = (autoState, manualState) => { + const next = { ...autoState }; + BOOLEAN_STATES.forEach(name => { + if (manualState[name]) { + next[name] = true; + } + }); + INTEGER_STATES.forEach(name => { + next[name] = Math.max(autoState[name] || 0, manualState[name] || 0); + }); DUNGEONS.forEach(dungeon => { - next[`${dungeon.id}-checks`] = amounts[dungeon.id]; + next[`${dungeon.id}-small-key`] += manualState[`${dungeon.id}-small-key`] || 0; + next[`${dungeon.id}-checks`] += manualState[`${dungeon.id}-checks`] || 0; + if (manualState[`${dungeon.id}-big-key`]) { + next[`${dungeon.id}-big-key`] = true; + } + if (manualState[`${dungeon.id}-compass`]) { + next[`${dungeon.id}-compass`] = true; + } + if (manualState[`${dungeon.id}-map`]) { + next[`${dungeon.id}-map`] = true; + } + if (manualState[`${dungeon.id}-boss`]) { + next[`${dungeon.id}-boss`] = manualState[`${dungeon.id}-boss`]; + } + if (manualState[`${dungeon.id}-boss-defeated`]) { + next[`${dungeon.id}-boss-defeated`] = true; + } + if (manualState[`${dungeon.id}-prize`] && + manualState[`${dungeon.id}-prize`] !== 'crystal' + ) { + next[`${dungeon.id}-prize`] = manualState[`${dungeon.id}-prize`]; + } else if (!next[`${dungeon.id}-prize`]) { + next[`${dungeon.id}-prize`] = 'crystal'; + } + if (manualState[`${dungeon.id}-prize-acquired`]) { + next[`${dungeon.id}-prize-acquired`] = true; + } + }); + OVERWORLD_LOCATIONS.forEach(loc => { + if (manualState[loc.id]) { + next[loc.id] = true; + } + }); + UNDERWORLD_LOCATIONS.forEach(loc => { + if (manualState[loc.id]) { + next[loc.id] = true; + } }); + // prefer auto + next['bottle-1'] = autoState['bottle-1'] || manualState['bottle-1'] || 0; + next['bottle-2'] = autoState['bottle-2'] || manualState['bottle-2'] || 0; + next['bottle-3'] = autoState['bottle-3'] || manualState['bottle-3'] || 0; + next['bottle-4'] = autoState['bottle-4'] || manualState['bottle-4'] || 0; + // force manual + next['mm-medallion'] = manualState['mm-medallion']; + next['tr-medallion'] = manualState['tr-medallion']; + next['gt-crystals'] = manualState['gt-crystals']; + next['ganon-crystals'] = manualState['ganon-crystals']; + next['gt-bot-boss'] = manualState['gt-bot-boss']; + next['gt-mid-boss'] = manualState['gt-mid-boss']; + next['gt-top-boss'] = manualState['gt-top-boss']; //console.log(next); return next; };