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',
89 mapLayout: 'horizontal',
90 showMap: 'situational',
91 showCompass: 'situational',
101 export const DUNGEONS = [
114 offset: DUNGEON_IDS.HC,
115 mask: DUNGEON_MASKS.HC,
138 offset: DUNGEON_IDS.CT,
139 mask: DUNGEON_MASKS.CT,
156 offset: DUNGEON_IDS.GT,
157 mask: DUNGEON_MASKS.GT,
201 offset: DUNGEON_IDS.EP,
202 mask: DUNGEON_MASKS.EP,
220 bosses: ['lanmolas'],
225 offset: DUNGEON_IDS.DP,
226 mask: DUNGEON_MASKS.DP,
249 offset: DUNGEON_IDS.TH,
250 mask: DUNGEON_MASKS.TH,
272 offset: DUNGEON_IDS.PD,
273 mask: DUNGEON_MASKS.PD,
276 'pd-stalfos-basement',
284 'pd-harmless-hellway',
303 offset: DUNGEON_IDS.SP,
304 mask: DUNGEON_MASKS.SP,
330 offset: DUNGEON_IDS.SW,
331 mask: DUNGEON_MASKS.SW,
355 offset: DUNGEON_IDS.TT,
356 mask: DUNGEON_MASKS.TT,
376 bosses: ['kholdstare'],
380 offset: DUNGEON_IDS.IP,
381 mask: DUNGEON_MASKS.IP,
401 bosses: ['vitreous'],
405 offset: DUNGEON_IDS.MM,
406 mask: DUNGEON_MASKS.MM,
430 offset: DUNGEON_IDS.TR,
431 mask: DUNGEON_MASKS.TR,
440 'tr-laser-bridge-top',
441 'tr-laser-bridge-left',
442 'tr-laser-bridge-right',
443 'tr-laser-bridge-bottom',
449 export const OVERWORLD_LOCATIONS = [
491 id: 'floating-island',
506 id: 'lake-hylia-island',
576 id: 'sunken-treasure',
597 export const UNDERWORLD_LOCATIONS = [
604 id: 'blinds-hut-top',
609 id: 'blinds-hut-left',
614 id: 'blinds-hut-right',
619 id: 'blinds-hut-far-left',
624 id: 'blinds-hut-far-right',
688 id: 'dp-big-key-chest',
694 id: 'dp-compass-chest',
718 id: 'ep-big-key-chest',
730 id: 'ep-compass-chest',
747 id: 'graveyard-ledge',
896 id: 'gt-helma-right',
902 id: 'gt-pre-moldorm',
908 id: 'gt-post-moldorm',
937 id: 'hookshot-cave-br',
942 id: 'hookshot-cave-tr',
947 id: 'hookshot-cave-tl',
952 id: 'hookshot-cave-bl',
962 id: 'hype-cave-left',
967 id: 'hype-cave-right',
972 id: 'hype-cave-bottom',
987 id: 'ip-compass-chest',
993 id: 'ip-big-key-chest',
1005 id: 'ip-spike-chest',
1011 id: 'ip-freezor-chest',
1034 id: 'kak-well-left',
1044 id: 'kak-well-right',
1049 id: 'kak-well-bottom',
1064 id: 'lost-woods-hideout',
1079 id: 'mini-moldorm-far-left',
1084 id: 'mini-moldorm-left',
1089 id: 'mini-moldorm-right',
1094 id: 'mini-moldorm-far-right',
1099 id: 'mini-moldorm-npc',
1104 id: 'mm-bridge-chest',
1109 id: 'mm-spike-chest',
1114 id: 'mm-lobby-chest',
1119 id: 'mm-compass-chest',
1124 id: 'mm-big-key-chest',
1139 id: 'mire-shed-left',
1144 id: 'mire-shed-right',
1149 id: 'paradox-lower-far-left',
1154 id: 'paradox-lower-left',
1159 id: 'paradox-lower-right',
1164 id: 'paradox-lower-far-right',
1169 id: 'paradox-lower-mid',
1174 id: 'paradox-upper-left',
1179 id: 'paradox-upper-right',
1184 id: 'pd-shooter-room',
1189 id: 'pd-stalfos-basement',
1194 id: 'pd-big-key-chest',
1199 id: 'pd-arena-bridge',
1204 id: 'pd-arena-ledge',
1219 id: 'pd-compass-chest',
1224 id: 'pd-harmless-hellway',
1234 id: 'pd-maze-bottom',
1239 id: 'pd-basement-left',
1244 id: 'pd-basement-right',
1249 id: 'pyramid-fairy-left',
1254 id: 'pyramid-fairy-right',
1280 id: 'secret-passage',
1321 id: 'sp-compass-chest',
1327 id: 'sp-west-chest',
1333 id: 'sp-big-key-chest',
1339 id: 'sp-flooded-left',
1345 id: 'sp-flooded-right',
1357 id: 'spec-rock-cave',
1372 id: 'super-bunny-top',
1377 id: 'super-bunny-bottom',
1394 id: 'sw-compass-chest',
1400 id: 'sw-big-key-chest',
1406 id: 'sw-pot-prison',
1412 id: 'sw-pinball-room',
1418 id: 'sw-bridge-chest',
1429 id: 'th-basement-cage',
1435 id: 'th-big-key-chest',
1453 id: 'th-compass-chest',
1459 id: 'tr-roller-left',
1465 id: 'tr-roller-right',
1471 id: 'tr-compass-chest',
1483 id: 'tr-big-key-chest',
1495 id: 'tr-crysta-roller',
1501 id: 'tr-laser-bridge-top',
1507 id: 'tr-laser-bridge-left',
1513 id: 'tr-laser-bridge-right',
1519 id: 'tr-laser-bridge-bottom',
1531 id: 'tt-big-key-chest',
1537 id: 'tt-ambush-chest',
1543 id: 'tt-compass-chest',
1567 id: 'waterfall-fairy-left',
1572 id: 'waterfall-fairy-right',
1578 export const getConfigValue = (config, name, fallback) =>
1579 Object.prototype.hasOwnProperty.call(config, name) ? config[name] : fallback;
1581 export const configureDungeons = config => DUNGEONS.map(dungeon => {
1582 const newDungeon = JSON.parse(JSON.stringify(dungeon));
1583 if (config.wildMap && dungeon.map) {
1586 if (config.wildCompass && dungeon.compass) {
1589 if (config.wildSmall) {
1590 newDungeon.items += dungeon.sk;
1592 if (config.wildBig && dungeon.bk && !dungeon.dropBk) {
1596 newDungeon.bosses = config.bossShuffle ? BOSSES : [dungeon.boss];
1601 export const applyLogic = (config, dungeons, state) => {
1602 const logic = Logic[config.worldState];
1604 for (const name in logic) {
1606 map[name] = logic[name](config, dungeons, state);
1608 console.error('error evaluating', name, e);
1614 export const shouldShowDungeonItem = (config, which) => {
1615 const show = config[`show${which}`] || 'always';
1616 const wild = config[`wild${which}`] || false;
1622 return wild || (which === 'Compass' && config.bossShuffle);
1628 export const toggleBoolean = name => state => ({
1630 [name]: !state[name],
1633 export const increment = (name, max, skipZero) => state => {
1634 let newValue = ((state[name] || 0) + 1) % (max + 1);
1635 if (skipZero && !newValue) {
1644 export const decrement = (name, max, skipZero) => state => {
1645 let newValue = ((state[name] || 0) + max) % (max + 1);
1646 if (skipZero && !newValue) {
1655 export const highestActive = (state, names) => {
1656 for (let i = names.length; i >= 0; --i) {
1657 if (state[names[i]]) {
1664 export const clearAll = names => state => {
1665 const changes = names.reduce((acc, cur) => ({ ...acc, [cur]: true }), {});
1666 return { ...state, ...changes };
1669 export const unclearAll = names => state => {
1670 const changes = names.reduce((acc, cur) => ({ ...acc, [cur]: false }), {});
1671 return { ...state, ...changes };
1674 export const countClearedLocations = (state, locations) =>
1675 locations.reduce((acc, cur) => state[cur] ? acc + 1 : acc, 0);
1677 export const hasClearedLocations = (state, locations) =>
1678 countClearedLocations(state, locations) === locations.length;
1680 export const getLocationStatus = (name, logic, state) => {
1681 if (state[name]) return 'cleared';
1682 if (logic[name]) return logic[name];
1683 return logic.fallback;
1686 export const getCombinedStatus = statuses => {
1687 if (statuses.filter(s => s === 'cleared').length === statuses.length) {
1690 if (statuses.filter(s => ['available', 'cleared'].includes(s)).length === statuses.length) {
1693 if (statuses.filter(s => ['unavailable', 'cleared'].includes(s)).length === statuses.length) {
1694 return 'unavailable';
1699 export const aggregateLocationStatus = (names, logic, state) => {
1700 const statuses = names.map(name => getLocationStatus(name, logic, state));
1701 return getCombinedStatus(statuses);
1704 export const countRemainingLocations = (state, locations) =>
1705 locations.reduce((acc, cur) => state[cur] ? acc : acc + 1, 0);
1707 export const getGanonCrystals = (config) => getConfigValue(config, 'ganon-crystals', 7);
1709 export const getGTCrystals = (config) => getConfigValue(config, 'gt-crystals', 7);
1711 export const getGTBoss = (state, which) => state[`gt-${which}-boss`];
1713 export const hasDungeonBoss = (state, dungeon) =>
1714 !dungeon.boss || !!state[`${dungeon.id}-boss-defeated`];
1716 export const getDungeonBoss = (state, dungeon) =>
1717 dungeon.bosses.length > 1
1718 ? state[`${dungeon.id}-boss`] || dungeon.boss || null
1719 : dungeon.bosses[0];
1721 export const hasDungeonPrize = (state, dungeon) =>
1722 !dungeon.prize || !!state[`${dungeon.id}-prize-acquired`];
1724 export const getDungeonPrize = (state, dungeon) => state[`${dungeon.id}-prize`] || null;
1726 export const getDungeonClearedItems = (state, dungeon) => state[`${dungeon.id}-checks`] || 0;
1728 export const getDungeonRemainingItems = (state, dungeon) =>
1729 Math.max(0, dungeon.items - getDungeonClearedItems(state, dungeon));
1731 export const getDungeonAcquiredSKs = (state, dungeon) => state[`${dungeon.id}-small-key`] || 0;
1733 export const isDungeonCleared = (state, dungeon) => {
1734 const hasItems = !getDungeonRemainingItems(state, dungeon);
1735 const hasBoss = hasDungeonBoss(state, dungeon);
1736 const hasPrize = hasDungeonPrize(state, dungeon);
1737 return hasItems && hasBoss && hasPrize;
1740 export const aggregateDungeonStatus = (dungeon, logic, state) => {
1741 if (isDungeonCleared(state, dungeon)) {
1744 if (logic[dungeon.id] === 'unavailable') {
1745 return 'unavailable';
1747 const checks = [...dungeon.checks];
1748 if (['ct', 'gt'].includes(dungeon.id)) {
1749 checks.push(`${dungeon.id}-boss-killable`);
1751 const statuses = checks.map(name => getLocationStatus(name, logic, state));
1752 return getCombinedStatus(statuses);
1755 export const toggleBossDefeated = dungeon => toggleBoolean(`${dungeon.id}-boss-defeated`);
1757 export const setBossDefeated = (dungeon, defeated) =>
1758 state => ({ ...state, [`${dungeon.id}-boss-defeated`]: !!defeated });
1760 export const togglePrizeAcquired = dungeon => toggleBoolean(`${dungeon.id}-prize-acquired`);
1762 export const setPrizeAcquired = (dungeon, acquired) =>
1763 state => ({ ...state, [`${dungeon.id}-prize-acquired`]: !!acquired });
1765 export const addDungeonCheck = dungeon => increment(`${dungeon.id}-checks`, dungeon.items);
1767 export const removeDungeonCheck = dungeon => decrement(`${dungeon.id}-checks`, dungeon.items);
1769 export const resetDungeonChecks = dungeon => state => ({ ...state, [`${dungeon.id}-checks`]: 0 });
1771 export const completeDungeonChecks = dungeon =>
1772 state => ({ ...state, [`${dungeon.id}-checks`]: dungeon.items });
1774 export const makeEmptyState = () => {
1776 BOOLEAN_STATES.forEach(p => {
1777 state[p] = INITIAL[p] || false;
1779 INTEGER_STATES.forEach(p => {
1780 state[p] = INITIAL[p] || 0;
1782 DUNGEONS.forEach(dungeon => {
1783 state[`${dungeon.id}-map`] = false;
1784 state[`${dungeon.id}-compass`] = false;
1785 state[`${dungeon.id}-small-key`] = 0;
1786 state[`${dungeon.id}-big-key`] = false;
1787 state[`${dungeon.id}-checks`] = 0;
1789 state[`${dungeon.id}-boss`] = dungeon.boss;
1790 state[`${dungeon.id}-boss-defeated`] = false;
1792 if (dungeon.prize) {
1793 state[`${dungeon.id}-prize`] = 'crystal';
1794 state[`${dungeon.id}-prize-acquired`] = false;
1796 if (dungeon.id === 'gt') {
1797 state['gt-bot-boss'] = 'armos';
1798 state['gt-mid-boss'] = 'lanmolas';
1799 state['gt-top-boss'] = 'moldorm';
1802 OVERWORLD_LOCATIONS.forEach(location => {
1803 state[location.id] = false;
1805 UNDERWORLD_LOCATIONS.forEach(location => {
1806 state[location.id] = false;
1808 state['mm-medallion'] = null;
1809 state['tr-medallion'] = null;
1813 const collectInventory = (state, data, prizeMap) => {
1814 state.bow = !!(data[INV_ADDR.RANDO_BOW] & 0x80);
1815 state.silvers = (data[INV_ADDR.RANDO_BOW] & 0xC0) == 0xC0;
1816 state['bowless-silvers'] = (data[INV_ADDR.RANDO_BOW] & 0xC0) == 0x40;
1817 state['blue-boomerang'] = !!(data[INV_ADDR.RANDO_BOOM] & 0x40);
1818 state['red-boomerang'] = !!(data[INV_ADDR.RANDO_BOOM] & 0x80);
1819 state.hookshot = !!data[INV_ADDR.HOOK];
1820 state.bomb = data[INV_ADDR.BOMB];
1821 state.mushroom = !!(data[INV_ADDR.RANDO_POWDER] & 0x20);
1822 state.powder = !!(data[INV_ADDR.RANDO_POWDER] & 0x10);
1823 state['fire-rod'] = !!data[INV_ADDR.FROD];
1824 state['ice-rod'] = !!data[INV_ADDR.IROD];
1825 state.bombos = !!data[INV_ADDR.BOMBOS];
1826 state.ether = !!data[INV_ADDR.ETHER];
1827 state.quake = !!data[INV_ADDR.QUAKE];
1828 state.lamp = !!data[INV_ADDR.LAMP];
1829 state.hammer = !!data[INV_ADDR.HAMMER];
1830 state.shovel = !!(data[INV_ADDR.RANDO_FLUTE] & 0x04);
1831 state.flute = !!(data[INV_ADDR.RANDO_FLUTE] & 0x03);
1832 state.duck = !!(data[INV_ADDR.RANDO_FLUTE] & 0x01);
1833 state.bugnet = !!data[INV_ADDR.BUGNET];
1834 state.book = !!data[INV_ADDR.BOOK];
1835 state['bottle-1'] = data[INV_ADDR.BOTTLE_1];
1836 state['bottle-2'] = data[INV_ADDR.BOTTLE_2];
1837 state['bottle-3'] = data[INV_ADDR.BOTTLE_3];
1838 state['bottle-4'] = data[INV_ADDR.BOTTLE_4];
1839 state.somaria = !!data[INV_ADDR.SOMARIA];
1840 state.byrna = !!data[INV_ADDR.BYRNA];
1841 state.cape = !!data[INV_ADDR.CAPE];
1842 state.mirror = !!data[INV_ADDR.MIRROR];
1843 state.lift = data[INV_ADDR.GLOVE];
1844 state.boots = !!data[INV_ADDR.BOOTS];
1845 state.flippers = !!data[INV_ADDR.FLIPPERS];
1846 state.moonpearl = !!data[INV_ADDR.MOONPEARL];
1847 state.sword = data[INV_ADDR.SWORD];
1848 state.shield = data[INV_ADDR.SHIELD];
1849 state.mail = data[INV_ADDR.ARMOR] + 1;
1850 state['heart-piece'] = data[INV_ADDR.HEART_PIECE];
1851 state['half-magic'] = data[INV_ADDR.MAGIC_USE] > 0;
1852 state['quarter-magic'] = data[INV_ADDR.MAGIC_USE] > 1;
1853 const map = getShort(data, INV_ADDR.MAP);
1854 const compass = getShort(data, INV_ADDR.COMPASS);
1855 const bigKey = getShort(data, INV_ADDR.BIG_KEY);
1856 DUNGEONS.forEach(dungeon => {
1857 state[`${dungeon.id}-map`] = !!(map & dungeon.mask);
1858 state[`${dungeon.id}-compass`] = !!(compass & dungeon.mask);
1859 state[`${dungeon.id}-small-key`] = data[INV_ADDR.RANDO_KEY_START + dungeon.offset];
1860 state[`${dungeon.id}-big-key`] = !!(bigKey & dungeon.mask);
1861 state[`${dungeon.id}-checks-collected`] =
1862 data[INV_ADDR.RANDO_CHECKS_START + dungeon.offset];
1863 if (dungeon.prize) {
1864 const isCrystal = prizeMap[dungeon.offset].isCrystal;
1865 const prizeFlags = data[isCrystal ? INV_ADDR.CRYSTALS : INV_ADDR.PENDANTS];
1866 const prizeAcquired = !!(prizeFlags & prizeMap[dungeon.offset].mask);
1867 state[`${dungeon.id}-prize-acquired`] = prizeAcquired;
1868 if (prizeAcquired) {
1870 if (prizeMap[dungeon.offset].mask === 1) {
1871 state[`${dungeon.id}-prize`] = 'red-pendant';
1872 } else if (prizeMap[dungeon.offset].mask === 2) {
1873 state[`${dungeon.id}-prize`] = 'blue-pendant';
1874 } else if (prizeMap[dungeon.offset].mask === 4) {
1875 state[`${dungeon.id}-prize`] = 'green-pendant';
1878 state[`${dungeon.id}-prize`] = 'crystal';
1885 const collectOverworld = (state, data) => {
1886 OVERWORLD_LOCATIONS.forEach(location => {
1887 state[location.id] = !!(data[location.address] & location.mask);
1891 const collectUnderworld = (state, data) => {
1892 UNDERWORLD_LOCATIONS.forEach(location => {
1893 state[location.id] = isChestOpen(data, location.room, location.chest);
1895 DUNGEONS.forEach(dungeon => {
1896 state[`${dungeon.id}-boss-defeated`] = isBossDefeated(data, dungeon.bossRoom);
1900 const getDungeonAmounts = (config, state) => {
1902 DUNGEONS.forEach(dungeon => {
1904 let total = dungeon.checks.length;
1905 if (config.checkCalculation === 'inventory') {
1906 amount = state[`${dungeon.id}-checks-collected`];
1908 dungeon.checks.forEach(check => {
1914 if (!config.wildMap && state[`${dungeon.id}-map`]) {
1918 if (!config.wildCompass && state[`${dungeon.id}-compass`]) {
1922 if (!config.wildSmall) {
1923 amount -= Math.min(state[`${dungeon.id}-small-key`], dungeon.sk);
1924 total -= dungeon.sk;
1926 if (!config.wildBig && !dungeon.dropBk && state[`${dungeon.id}-big-key`]) {
1930 amounts[dungeon.id] = Math.min(total, amount);
1935 export const computeState = (config, data, prizeMap) => {
1937 collectInventory(state, data.slice(SRAM_ADDR.INV_START), prizeMap);
1938 collectOverworld(state, data);
1939 collectUnderworld(state, data.slice(SRAM_ADDR.ROOM_DATA_START));
1940 const amounts = getDungeonAmounts(config, state);
1941 DUNGEONS.forEach(dungeon => {
1942 state[`${dungeon.id}-checks`] = amounts[dungeon.id];
1947 export const mergeStates = (autoState, manualState) => {
1948 const next = { ...autoState };
1949 BOOLEAN_STATES.forEach(name => {
1950 if (manualState[name]) {
1954 INTEGER_STATES.forEach(name => {
1955 next[name] = Math.max(autoState[name] || 0, manualState[name] || 0);
1957 DUNGEONS.forEach(dungeon => {
1958 next[`${dungeon.id}-small-key`] += manualState[`${dungeon.id}-small-key`] || 0;
1959 next[`${dungeon.id}-checks`] += manualState[`${dungeon.id}-checks`] || 0;
1960 if (manualState[`${dungeon.id}-big-key`]) {
1961 next[`${dungeon.id}-big-key`] = true;
1963 if (manualState[`${dungeon.id}-compass`]) {
1964 next[`${dungeon.id}-compass`] = true;
1966 if (manualState[`${dungeon.id}-map`]) {
1967 next[`${dungeon.id}-map`] = true;
1969 if (manualState[`${dungeon.id}-boss`]) {
1970 next[`${dungeon.id}-boss`] = manualState[`${dungeon.id}-boss`];
1972 if (manualState[`${dungeon.id}-boss-defeated`]) {
1973 next[`${dungeon.id}-boss-defeated`] = true;
1975 if (manualState[`${dungeon.id}-prize`] &&
1976 manualState[`${dungeon.id}-prize`] !== 'crystal'
1978 next[`${dungeon.id}-prize`] = manualState[`${dungeon.id}-prize`];
1979 } else if (!next[`${dungeon.id}-prize`]) {
1980 next[`${dungeon.id}-prize`] = 'crystal';
1982 if (manualState[`${dungeon.id}-prize-acquired`]) {
1983 next[`${dungeon.id}-prize-acquired`] = true;
1986 OVERWORLD_LOCATIONS.forEach(loc => {
1987 if (manualState[loc.id]) {
1988 next[loc.id] = true;
1991 UNDERWORLD_LOCATIONS.forEach(loc => {
1992 if (manualState[loc.id]) {
1993 next[loc.id] = true;
1997 next['bottle-1'] = autoState['bottle-1'] || manualState['bottle-1'] || 0;
1998 next['bottle-2'] = autoState['bottle-2'] || manualState['bottle-2'] || 0;
1999 next['bottle-3'] = autoState['bottle-3'] || manualState['bottle-3'] || 0;
2000 next['bottle-4'] = autoState['bottle-4'] || manualState['bottle-4'] || 0;
2002 next['mm-medallion'] = manualState['mm-medallion'];
2003 next['tr-medallion'] = manualState['tr-medallion'];
2004 next['gt-crystals'] = manualState['gt-crystals'];
2005 next['ganon-crystals'] = manualState['ganon-crystals'];
2006 next['gt-bot-boss'] = manualState['gt-bot-boss'];
2007 next['gt-mid-boss'] = manualState['gt-mid-boss'];
2008 next['gt-top-boss'] = manualState['gt-top-boss'];
2009 //console.log(next);