]> git.localhorst.tv Git - alttp.git/blobdiff - resources/js/helpers/tracker.js
tracker layout
[alttp.git] / resources / js / helpers / tracker.js
index 1409b35e27ef6de1988309a973692ca5aebdcf35..c4b685c5b9ced45af45281b71e0728bae7aac022 100644 (file)
@@ -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,15 +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 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 => {
@@ -1629,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;
@@ -1663,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];
@@ -1698,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';
+                               }
+                       }
                }
        });
 };
@@ -1721,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;
@@ -1760,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;
 };