10 import Logic from './logic';
12 export const BOOLEAN_STATES = [
45 export const INTEGER_STATES = [
57 export const INITIAL = {
61 export const BOTTLE_CONTENTS = [
72 export const BOSSES = [
85 export const CONFIG = {
87 checkCalculation: 'room-data',
88 compactKeysanity: true,
90 mapLayout: 'horizontal',
91 showMap: 'situational',
92 showCompass: 'situational',
102 export const DUNGEONS = [
115 offset: DUNGEON_IDS.HC,
116 mask: DUNGEON_MASKS.HC,
139 offset: DUNGEON_IDS.CT,
140 mask: DUNGEON_MASKS.CT,
157 offset: DUNGEON_IDS.GT,
158 mask: DUNGEON_MASKS.GT,
202 offset: DUNGEON_IDS.EP,
203 mask: DUNGEON_MASKS.EP,
221 bosses: ['lanmolas'],
226 offset: DUNGEON_IDS.DP,
227 mask: DUNGEON_MASKS.DP,
250 offset: DUNGEON_IDS.TH,
251 mask: DUNGEON_MASKS.TH,
273 offset: DUNGEON_IDS.PD,
274 mask: DUNGEON_MASKS.PD,
277 'pd-stalfos-basement',
285 'pd-harmless-hellway',
304 offset: DUNGEON_IDS.SP,
305 mask: DUNGEON_MASKS.SP,
331 offset: DUNGEON_IDS.SW,
332 mask: DUNGEON_MASKS.SW,
356 offset: DUNGEON_IDS.TT,
357 mask: DUNGEON_MASKS.TT,
377 bosses: ['kholdstare'],
381 offset: DUNGEON_IDS.IP,
382 mask: DUNGEON_MASKS.IP,
402 bosses: ['vitreous'],
406 offset: DUNGEON_IDS.MM,
407 mask: DUNGEON_MASKS.MM,
431 offset: DUNGEON_IDS.TR,
432 mask: DUNGEON_MASKS.TR,
441 'tr-laser-bridge-top',
442 'tr-laser-bridge-left',
443 'tr-laser-bridge-right',
444 'tr-laser-bridge-bottom',
450 export const OVERWORLD_LOCATIONS = [
492 id: 'floating-island',
507 id: 'lake-hylia-island',
577 id: 'sunken-treasure',
598 export const UNDERWORLD_LOCATIONS = [
605 id: 'blinds-hut-top',
610 id: 'blinds-hut-left',
615 id: 'blinds-hut-right',
620 id: 'blinds-hut-far-left',
625 id: 'blinds-hut-far-right',
689 id: 'dp-big-key-chest',
695 id: 'dp-compass-chest',
719 id: 'ep-big-key-chest',
731 id: 'ep-compass-chest',
748 id: 'graveyard-ledge',
897 id: 'gt-helma-right',
903 id: 'gt-pre-moldorm',
909 id: 'gt-post-moldorm',
938 id: 'hookshot-cave-br',
943 id: 'hookshot-cave-tr',
948 id: 'hookshot-cave-tl',
953 id: 'hookshot-cave-bl',
963 id: 'hype-cave-left',
968 id: 'hype-cave-right',
973 id: 'hype-cave-bottom',
988 id: 'ip-compass-chest',
994 id: 'ip-big-key-chest',
1006 id: 'ip-spike-chest',
1012 id: 'ip-freezor-chest',
1035 id: 'kak-well-left',
1045 id: 'kak-well-right',
1050 id: 'kak-well-bottom',
1065 id: 'lost-woods-hideout',
1080 id: 'mini-moldorm-far-left',
1085 id: 'mini-moldorm-left',
1090 id: 'mini-moldorm-right',
1095 id: 'mini-moldorm-far-right',
1100 id: 'mini-moldorm-npc',
1105 id: 'mm-bridge-chest',
1110 id: 'mm-spike-chest',
1115 id: 'mm-lobby-chest',
1120 id: 'mm-compass-chest',
1125 id: 'mm-big-key-chest',
1140 id: 'mire-shed-left',
1145 id: 'mire-shed-right',
1150 id: 'paradox-lower-far-left',
1155 id: 'paradox-lower-left',
1160 id: 'paradox-lower-right',
1165 id: 'paradox-lower-far-right',
1170 id: 'paradox-lower-mid',
1175 id: 'paradox-upper-left',
1180 id: 'paradox-upper-right',
1185 id: 'pd-shooter-room',
1190 id: 'pd-stalfos-basement',
1195 id: 'pd-big-key-chest',
1200 id: 'pd-arena-bridge',
1205 id: 'pd-arena-ledge',
1220 id: 'pd-compass-chest',
1225 id: 'pd-harmless-hellway',
1235 id: 'pd-maze-bottom',
1240 id: 'pd-basement-left',
1245 id: 'pd-basement-right',
1250 id: 'pyramid-fairy-left',
1255 id: 'pyramid-fairy-right',
1281 id: 'secret-passage',
1322 id: 'sp-compass-chest',
1328 id: 'sp-west-chest',
1334 id: 'sp-big-key-chest',
1340 id: 'sp-flooded-left',
1346 id: 'sp-flooded-right',
1358 id: 'spec-rock-cave',
1373 id: 'super-bunny-top',
1378 id: 'super-bunny-bottom',
1395 id: 'sw-compass-chest',
1401 id: 'sw-big-key-chest',
1407 id: 'sw-pot-prison',
1413 id: 'sw-pinball-room',
1419 id: 'sw-bridge-chest',
1430 id: 'th-basement-cage',
1436 id: 'th-big-key-chest',
1454 id: 'th-compass-chest',
1460 id: 'tr-roller-left',
1466 id: 'tr-roller-right',
1472 id: 'tr-compass-chest',
1484 id: 'tr-big-key-chest',
1496 id: 'tr-crysta-roller',
1502 id: 'tr-laser-bridge-top',
1508 id: 'tr-laser-bridge-left',
1514 id: 'tr-laser-bridge-right',
1520 id: 'tr-laser-bridge-bottom',
1532 id: 'tt-big-key-chest',
1538 id: 'tt-ambush-chest',
1544 id: 'tt-compass-chest',
1568 id: 'waterfall-fairy-left',
1573 id: 'waterfall-fairy-right',
1579 export const getConfigValue = (config, name, fallback) =>
1580 Object.prototype.hasOwnProperty.call(config, name) ? config[name] : fallback;
1582 export const configureDungeons = config => DUNGEONS.map(dungeon => {
1583 const newDungeon = JSON.parse(JSON.stringify(dungeon));
1584 if (config.wildMap && dungeon.map) {
1587 if (config.wildCompass && dungeon.compass) {
1590 if (config.wildSmall) {
1591 newDungeon.items += dungeon.sk;
1593 if (config.wildBig && dungeon.bk && !dungeon.dropBk) {
1597 newDungeon.bosses = config.bossShuffle ? BOSSES : [dungeon.boss];
1602 export const applyLogic = (config, dungeons, state) => {
1603 const logic = Logic[config.worldState];
1605 for (const name in logic) {
1607 map[name] = logic[name](config, dungeons, state);
1609 console.error('error evaluating', name, e);
1615 export const shouldShowDungeonItem = (config, which) => {
1616 const show = config[`show${which}`] || 'always';
1617 const wild = config[`wild${which}`] || false;
1623 return wild || (which === 'Compass' && config.bossShuffle);
1629 export const showsFullKeysanity = (config) =>
1630 shouldShowDungeonItem(config, 'Map') &&
1631 shouldShowDungeonItem(config, 'Compass') &&
1632 shouldShowDungeonItem(config, 'Small') &&
1633 shouldShowDungeonItem(config, 'Big');
1635 export const shouldCompactKeysanity = (config) =>
1636 config.compactKeysanity && showsFullKeysanity(config);
1638 export const toggleBoolean = name => state => ({
1640 [name]: !state[name],
1643 export const increment = (name, max, skipZero) => state => {
1644 let newValue = ((state[name] || 0) + 1) % (max + 1);
1645 if (skipZero && !newValue) {
1654 export const decrement = (name, max, skipZero) => state => {
1655 let newValue = ((state[name] || 0) + max) % (max + 1);
1656 if (skipZero && !newValue) {
1665 export const highestActive = (state, names) => {
1666 for (let i = names.length; i >= 0; --i) {
1667 if (state[names[i]]) {
1674 export const clearAll = names => state => {
1675 const changes = names.reduce((acc, cur) => ({ ...acc, [cur]: true }), {});
1676 return { ...state, ...changes };
1679 export const unclearAll = names => state => {
1680 const changes = names.reduce((acc, cur) => ({ ...acc, [cur]: false }), {});
1681 return { ...state, ...changes };
1684 export const countClearedLocations = (state, locations) =>
1685 locations.reduce((acc, cur) => state[cur] ? acc + 1 : acc, 0);
1687 export const hasClearedLocations = (state, locations) =>
1688 countClearedLocations(state, locations) === locations.length;
1690 export const getLocationStatus = (name, logic, state) => {
1691 if (state[name]) return 'cleared';
1692 if (logic[name]) return logic[name];
1693 return logic.fallback;
1696 export const getCombinedStatus = statuses => {
1697 if (statuses.filter(s => s === 'cleared').length === statuses.length) {
1700 if (statuses.filter(s => ['available', 'cleared'].includes(s)).length === statuses.length) {
1703 if (statuses.filter(s => ['unavailable', 'cleared'].includes(s)).length === statuses.length) {
1704 return 'unavailable';
1709 export const aggregateLocationStatus = (names, logic, state) => {
1710 const statuses = names.map(name => getLocationStatus(name, logic, state));
1711 return getCombinedStatus(statuses);
1714 export const countRemainingLocations = (state, locations) =>
1715 locations.reduce((acc, cur) => state[cur] ? acc : acc + 1, 0);
1717 export const getGanonCrystals = (config) => getConfigValue(config, 'ganon-crystals', 7);
1719 export const getGTCrystals = (config) => getConfigValue(config, 'gt-crystals', 7);
1721 export const getGTBoss = (state, which) => state[`gt-${which}-boss`];
1723 export const hasDungeonBoss = (state, dungeon) =>
1724 !dungeon.boss || !!state[`${dungeon.id}-boss-defeated`];
1726 export const getDungeonBoss = (state, dungeon) =>
1727 dungeon.bosses.length > 1
1728 ? state[`${dungeon.id}-boss`] || dungeon.boss || null
1729 : dungeon.bosses[0];
1731 export const hasDungeonPrize = (state, dungeon) =>
1732 !dungeon.prize || !!state[`${dungeon.id}-prize-acquired`];
1734 export const getDungeonPrize = (state, dungeon) => state[`${dungeon.id}-prize`] || null;
1736 export const getDungeonClearedItems = (state, dungeon) => state[`${dungeon.id}-checks`] || 0;
1738 export const getDungeonRemainingItems = (state, dungeon) =>
1739 Math.max(0, dungeon.items - getDungeonClearedItems(state, dungeon));
1741 export const getDungeonAcquiredSKs = (state, dungeon) => state[`${dungeon.id}-small-key`] || 0;
1743 export const isDungeonCleared = (state, dungeon) => {
1744 const hasItems = !getDungeonRemainingItems(state, dungeon);
1745 const hasBoss = hasDungeonBoss(state, dungeon);
1746 const hasPrize = hasDungeonPrize(state, dungeon);
1747 return hasItems && hasBoss && hasPrize;
1750 export const aggregateDungeonStatus = (dungeon, logic, state) => {
1751 if (isDungeonCleared(state, dungeon)) {
1754 if (logic[dungeon.id] === 'unavailable') {
1755 return 'unavailable';
1757 const checks = [...dungeon.checks];
1758 if (['ct', 'gt'].includes(dungeon.id)) {
1759 checks.push(`${dungeon.id}-boss-killable`);
1761 const statuses = checks.map(name => getLocationStatus(name, logic, state));
1762 return getCombinedStatus(statuses);
1765 export const toggleBossDefeated = dungeon => toggleBoolean(`${dungeon.id}-boss-defeated`);
1767 export const setBossDefeated = (dungeon, defeated) =>
1768 state => ({ ...state, [`${dungeon.id}-boss-defeated`]: !!defeated });
1770 export const togglePrizeAcquired = dungeon => toggleBoolean(`${dungeon.id}-prize-acquired`);
1772 export const setPrizeAcquired = (dungeon, acquired) =>
1773 state => ({ ...state, [`${dungeon.id}-prize-acquired`]: !!acquired });
1775 export const addDungeonCheck = dungeon => increment(`${dungeon.id}-checks`, dungeon.items);
1777 export const removeDungeonCheck = dungeon => decrement(`${dungeon.id}-checks`, dungeon.items);
1779 export const resetDungeonChecks = dungeon => state => ({ ...state, [`${dungeon.id}-checks`]: 0 });
1781 export const completeDungeonChecks = dungeon =>
1782 state => ({ ...state, [`${dungeon.id}-checks`]: dungeon.items });
1784 export const makeEmptyState = () => {
1786 BOOLEAN_STATES.forEach(p => {
1787 state[p] = INITIAL[p] || false;
1789 INTEGER_STATES.forEach(p => {
1790 state[p] = INITIAL[p] || 0;
1792 DUNGEONS.forEach(dungeon => {
1793 state[`${dungeon.id}-map`] = false;
1794 state[`${dungeon.id}-compass`] = false;
1795 state[`${dungeon.id}-small-key`] = 0;
1796 state[`${dungeon.id}-big-key`] = false;
1797 state[`${dungeon.id}-checks`] = 0;
1799 state[`${dungeon.id}-boss`] = dungeon.boss;
1800 state[`${dungeon.id}-boss-defeated`] = false;
1802 if (dungeon.prize) {
1803 state[`${dungeon.id}-prize`] = 'crystal';
1804 state[`${dungeon.id}-prize-acquired`] = false;
1806 if (dungeon.id === 'gt') {
1807 state['gt-bot-boss'] = 'armos';
1808 state['gt-mid-boss'] = 'lanmolas';
1809 state['gt-top-boss'] = 'moldorm';
1812 OVERWORLD_LOCATIONS.forEach(location => {
1813 state[location.id] = false;
1815 UNDERWORLD_LOCATIONS.forEach(location => {
1816 state[location.id] = false;
1818 state['mm-medallion'] = null;
1819 state['tr-medallion'] = null;
1823 const collectInventory = (state, data, prizeMap) => {
1824 state.bow = !!(data[INV_ADDR.RANDO_BOW] & 0x80);
1825 state.silvers = (data[INV_ADDR.RANDO_BOW] & 0xC0) == 0xC0;
1826 state['bowless-silvers'] = (data[INV_ADDR.RANDO_BOW] & 0xC0) == 0x40;
1827 state['blue-boomerang'] = !!(data[INV_ADDR.RANDO_BOOM] & 0x40);
1828 state['red-boomerang'] = !!(data[INV_ADDR.RANDO_BOOM] & 0x80);
1829 state.hookshot = !!data[INV_ADDR.HOOK];
1830 state.bomb = data[INV_ADDR.BOMB];
1831 state.mushroom = !!(data[INV_ADDR.RANDO_POWDER] & 0x20);
1832 state.powder = !!(data[INV_ADDR.RANDO_POWDER] & 0x10);
1833 state['fire-rod'] = !!data[INV_ADDR.FROD];
1834 state['ice-rod'] = !!data[INV_ADDR.IROD];
1835 state.bombos = !!data[INV_ADDR.BOMBOS];
1836 state.ether = !!data[INV_ADDR.ETHER];
1837 state.quake = !!data[INV_ADDR.QUAKE];
1838 state.lamp = !!data[INV_ADDR.LAMP];
1839 state.hammer = !!data[INV_ADDR.HAMMER];
1840 state.shovel = !!(data[INV_ADDR.RANDO_FLUTE] & 0x04);
1841 state.flute = !!(data[INV_ADDR.RANDO_FLUTE] & 0x03);
1842 state.duck = !!(data[INV_ADDR.RANDO_FLUTE] & 0x01);
1843 state.bugnet = !!data[INV_ADDR.BUGNET];
1844 state.book = !!data[INV_ADDR.BOOK];
1845 state['bottle-1'] = data[INV_ADDR.BOTTLE_1];
1846 state['bottle-2'] = data[INV_ADDR.BOTTLE_2];
1847 state['bottle-3'] = data[INV_ADDR.BOTTLE_3];
1848 state['bottle-4'] = data[INV_ADDR.BOTTLE_4];
1849 state.somaria = !!data[INV_ADDR.SOMARIA];
1850 state.byrna = !!data[INV_ADDR.BYRNA];
1851 state.cape = !!data[INV_ADDR.CAPE];
1852 state.mirror = !!data[INV_ADDR.MIRROR];
1853 state.lift = data[INV_ADDR.GLOVE];
1854 state.boots = !!data[INV_ADDR.BOOTS];
1855 state.flippers = !!data[INV_ADDR.FLIPPERS];
1856 state.moonpearl = !!data[INV_ADDR.MOONPEARL];
1857 state.sword = data[INV_ADDR.SWORD];
1858 state.shield = data[INV_ADDR.SHIELD];
1859 state.mail = data[INV_ADDR.ARMOR] + 1;
1860 state['heart-piece'] = data[INV_ADDR.HEART_PIECE];
1861 state['half-magic'] = data[INV_ADDR.MAGIC_USE] > 0;
1862 state['quarter-magic'] = data[INV_ADDR.MAGIC_USE] > 1;
1863 const map = getShort(data, INV_ADDR.MAP);
1864 const compass = getShort(data, INV_ADDR.COMPASS);
1865 const bigKey = getShort(data, INV_ADDR.BIG_KEY);
1866 DUNGEONS.forEach(dungeon => {
1867 state[`${dungeon.id}-map`] = !!(map & dungeon.mask);
1868 state[`${dungeon.id}-compass`] = !!(compass & dungeon.mask);
1869 state[`${dungeon.id}-small-key`] = data[INV_ADDR.RANDO_KEY_START + dungeon.offset];
1870 state[`${dungeon.id}-big-key`] = !!(bigKey & dungeon.mask);
1871 state[`${dungeon.id}-checks-collected`] =
1872 data[INV_ADDR.RANDO_CHECKS_START + dungeon.offset];
1873 if (dungeon.prize) {
1874 const isCrystal = prizeMap[dungeon.offset].isCrystal;
1875 const prizeFlags = data[isCrystal ? INV_ADDR.CRYSTALS : INV_ADDR.PENDANTS];
1876 const prizeAcquired = !!(prizeFlags & prizeMap[dungeon.offset].mask);
1877 state[`${dungeon.id}-prize-acquired`] = prizeAcquired;
1878 if (prizeAcquired) {
1880 if (prizeMap[dungeon.offset].mask === 1) {
1881 state[`${dungeon.id}-prize`] = 'red-pendant';
1882 } else if (prizeMap[dungeon.offset].mask === 2) {
1883 state[`${dungeon.id}-prize`] = 'blue-pendant';
1884 } else if (prizeMap[dungeon.offset].mask === 4) {
1885 state[`${dungeon.id}-prize`] = 'green-pendant';
1888 state[`${dungeon.id}-prize`] = 'crystal';
1895 const collectOverworld = (state, data) => {
1896 OVERWORLD_LOCATIONS.forEach(location => {
1897 state[location.id] = !!(data[location.address] & location.mask);
1901 const collectUnderworld = (state, data) => {
1902 UNDERWORLD_LOCATIONS.forEach(location => {
1903 state[location.id] = isChestOpen(data, location.room, location.chest);
1905 DUNGEONS.forEach(dungeon => {
1906 state[`${dungeon.id}-boss-defeated`] = isBossDefeated(data, dungeon.bossRoom);
1910 const getDungeonAmounts = (config, state) => {
1912 DUNGEONS.forEach(dungeon => {
1914 let total = dungeon.checks.length;
1915 if (config.checkCalculation === 'inventory') {
1916 amount = state[`${dungeon.id}-checks-collected`];
1918 dungeon.checks.forEach(check => {
1924 if (!config.wildMap && state[`${dungeon.id}-map`]) {
1928 if (!config.wildCompass && state[`${dungeon.id}-compass`]) {
1932 if (!config.wildSmall) {
1933 amount -= Math.min(state[`${dungeon.id}-small-key`], dungeon.sk);
1934 total -= dungeon.sk;
1936 if (!config.wildBig && !dungeon.dropBk && state[`${dungeon.id}-big-key`]) {
1940 amounts[dungeon.id] = Math.min(total, amount);
1945 export const computeState = (config, data, prizeMap) => {
1947 collectInventory(state, data.slice(SRAM_ADDR.INV_START), prizeMap);
1948 collectOverworld(state, data);
1949 collectUnderworld(state, data.slice(SRAM_ADDR.ROOM_DATA_START));
1950 const amounts = getDungeonAmounts(config, state);
1951 DUNGEONS.forEach(dungeon => {
1952 state[`${dungeon.id}-checks`] = amounts[dungeon.id];
1957 export const mergeStates = (autoState, manualState) => {
1958 const next = { ...autoState };
1959 BOOLEAN_STATES.forEach(name => {
1960 if (manualState[name]) {
1964 INTEGER_STATES.forEach(name => {
1965 next[name] = Math.max(autoState[name] || 0, manualState[name] || 0);
1967 DUNGEONS.forEach(dungeon => {
1968 next[`${dungeon.id}-small-key`] += manualState[`${dungeon.id}-small-key`] || 0;
1969 next[`${dungeon.id}-checks`] += manualState[`${dungeon.id}-checks`] || 0;
1970 if (manualState[`${dungeon.id}-big-key`]) {
1971 next[`${dungeon.id}-big-key`] = true;
1973 if (manualState[`${dungeon.id}-compass`]) {
1974 next[`${dungeon.id}-compass`] = true;
1976 if (manualState[`${dungeon.id}-map`]) {
1977 next[`${dungeon.id}-map`] = true;
1979 if (manualState[`${dungeon.id}-boss`]) {
1980 next[`${dungeon.id}-boss`] = manualState[`${dungeon.id}-boss`];
1982 if (manualState[`${dungeon.id}-boss-defeated`]) {
1983 next[`${dungeon.id}-boss-defeated`] = true;
1985 if (manualState[`${dungeon.id}-prize`] &&
1986 manualState[`${dungeon.id}-prize`] !== 'crystal'
1988 next[`${dungeon.id}-prize`] = manualState[`${dungeon.id}-prize`];
1989 } else if (!next[`${dungeon.id}-prize`]) {
1990 next[`${dungeon.id}-prize`] = 'crystal';
1992 if (manualState[`${dungeon.id}-prize-acquired`]) {
1993 next[`${dungeon.id}-prize-acquired`] = true;
1996 OVERWORLD_LOCATIONS.forEach(loc => {
1997 if (manualState[loc.id]) {
1998 next[loc.id] = true;
2001 UNDERWORLD_LOCATIONS.forEach(loc => {
2002 if (manualState[loc.id]) {
2003 next[loc.id] = true;
2007 next['bottle-1'] = autoState['bottle-1'] || manualState['bottle-1'] || 0;
2008 next['bottle-2'] = autoState['bottle-2'] || manualState['bottle-2'] || 0;
2009 next['bottle-3'] = autoState['bottle-3'] || manualState['bottle-3'] || 0;
2010 next['bottle-4'] = autoState['bottle-4'] || manualState['bottle-4'] || 0;
2012 next['mm-medallion'] = manualState['mm-medallion'];
2013 next['tr-medallion'] = manualState['tr-medallion'];
2014 next['gt-crystals'] = manualState['gt-crystals'];
2015 next['ganon-crystals'] = manualState['ganon-crystals'];
2016 next['gt-bot-boss'] = manualState['gt-bot-boss'];
2017 next['gt-mid-boss'] = manualState['gt-mid-boss'];
2018 next['gt-top-boss'] = manualState['gt-top-boss'];
2019 //console.log(next);