--- /dev/null
+#!/bin/bash
+
+gm montage -geometry '32x32>' -background transparent -gravity center -tile 8x100 public/item/*.png public/items-v1.png
+
+echo 'const ITEM_MAP = ['
+for i in public/item/*.png
+do
+ basename=$(basename "$i")
+ echo -e "\t'${basename/\.png/}',"
+done
+echo '];'
)}
/>
</Route>
+ <Route
+ path="tracker"
+ lazy={() => import(
+ /* webpackChunkName: "tracker" */
+ '../pages/Tracker'
+ )}
+ />
</Route>
)
);
import Icon from './Icon';
+const ITEM_MAP = [
+ 'aga',
+ 'armos',
+ 'arrghus',
+ 'big-key',
+ 'blind',
+ 'blue-boomerang',
+ 'blue-mail',
+ 'blue-pendant',
+ 'blue-potion',
+ 'bombos',
+ 'bomb',
+ 'book',
+ 'boots',
+ 'bottle-bee',
+ 'bottle',
+ 'bowless-silvers',
+ 'bow',
+ 'bugnet',
+ 'byrna',
+ 'cape',
+ 'chest',
+ 'compass',
+ 'crystal',
+ 'duck',
+ 'ether',
+ 'fairy',
+ 'fighter-shield',
+ 'fighter-sword',
+ 'fire-rod',
+ 'fire-shield',
+ 'flippers',
+ 'flute',
+ 'glove',
+ 'gold-sword',
+ 'green-mail',
+ 'green-pendant',
+ 'green-potion',
+ 'half-magic',
+ 'hammer',
+ 'heart-0',
+ 'heart-1',
+ 'heart-2',
+ 'heart-3',
+ 'heart-container',
+ 'heart-piece',
+ 'helma',
+ 'hookshot',
+ 'ice-rod',
+ 'kholdstare',
+ 'lamp',
+ 'lanmolas',
+ 'map',
+ 'master-sword',
+ 'mirror',
+ 'mirror-shield',
+ 'mitts',
+ 'moldorm',
+ 'moonpearl',
+ 'mothula',
+ 'mushroom',
+ 'open-chest',
+ 'powder',
+ 'quake',
+ 'quarter-magic',
+ 'red-bomb',
+ 'red-boomerang',
+ 'red-crystal',
+ 'red-mail',
+ 'red-pendant',
+ 'red-potion',
+ 'shovel',
+ 'silvers',
+ 'small-key',
+ 'somaria',
+ 'sword-1',
+ 'sword-2',
+ 'sword-3',
+ 'sword-4',
+ 'tempered-sword',
+ 'trinexx',
+ 'vitreous',
+];
+
+const isOnItemMap = name => ITEM_MAP.includes(name);
+
+const getItemMapStyle = name => {
+ const index = ITEM_MAP.indexOf(name);
+ const x = index % 8;
+ const y = Math.floor(index / 8);
+ return { backgroundPosition: `-${x * 100}% -${y * 100}%` };
+};
+
const getIconURL = name => {
switch (name) {
- case 'big-key':
- case 'blue-boomerang':
- case 'blue-mail':
- case 'blue-pendant':
- case 'blue-potion':
- case 'bombos':
- case 'bomb':
- case 'book':
- case 'boots':
- case 'bottle-bee':
- case 'bottle':
- case 'bow':
- case 'bugnet':
- case 'byrna':
- case 'cape':
- case 'compass':
- case 'crystal':
- case 'duck':
- case 'ether':
- case 'fairy':
- case 'fighter-shield':
- case 'fighter-sword':
- case 'fire-rod':
- case 'fire-shield':
- case 'flippers':
- case 'flute':
- case 'glove':
- case 'green-mail':
- case 'green-pendant':
- case 'green-potion':
- case 'hammer':
- case 'heart-container':
- case 'heart-piece':
- case 'hookshot':
- case 'ice-rod':
- case 'lamp':
- case 'map':
- case 'mirror':
- case 'mirror-shield':
- case 'mitts':
- case 'moonpearl':
- case 'mushroom':
- case 'powder':
- case 'quake':
- case 'red-bomb':
- case 'red-boomerang':
- case 'red-mail':
- case 'red-pendant':
- case 'red-potion':
- case 'shovel':
- case 'silvers':
- case 'small-key':
- case 'somaria':
- return `/item/${name}.png`;
case 'dungeon-ct':
case 'dungeon-dp':
case 'dungeon-ep':
const realTitle = title !== '' ? title || alt : null;
return <span className="zelda-icon">
+ {isOnItemMap(strippedName) ?
+ <span
+ className="item-map-icon"
+ style={getItemMapStyle(strippedName)}
+ title={realTitle}
+ />
+ : null}
{src ?
<img
alt={alt}
};
ZeldaIcon.propTypes = {
- name: PropTypes.string,
+ name: PropTypes.string.isRequired,
title: PropTypes.string,
};
--- /dev/null
+import React from 'react';
+import { Button } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+
+import Icon from '../common/Icon';
+import ToggleSwitch from '../common/ToggleSwitch';
+import {
+ IN_GAME_MODES,
+ RAM_ADDR,
+ SRAM_ADDR,
+ WRAM_ADDR,
+ buildPrizeMap,
+} from '../../helpers/alttp-ram';
+import { computeState, mergeStates } from '../../helpers/tracker';
+import { useSNES } from '../../hooks/snes';
+import { useTracker } from '../../hooks/tracker';
+
+const AutoTracking = () => {
+ const [enabled, setEnabled] = React.useState(false);
+ const [prizeMap, setPrizeMap] = React.useState(buildPrizeMap());
+
+ const {
+ disable: disableSNES,
+ enable: enableSNES,
+ openSettings,
+ sock,
+ status,
+ } = useSNES();
+ const { config, setState } = useTracker();
+ const { t } = useTranslation();
+
+ const enable = React.useCallback(() => {
+ enableSNES();
+ setEnabled(true);
+ }, []);
+
+ const disable = React.useCallback(() => {
+ disableSNES();
+ setEnabled(false);
+ }, []);
+
+ React.useEffect(() => {
+ const savedSettings = localStorage.getItem('tracker.settings');
+ if (savedSettings) {
+ const settings = JSON.parse(savedSettings);
+ if (settings.autoTrack) {
+ enable();
+ }
+ }
+ }, []);
+
+ const saveSettings = React.useCallback((newSettings) => {
+ const savedSettings = localStorage.getItem('tracker.settings');
+ const settings = savedSettings
+ ? { ...JSON.parse(savedSettings), ...newSettings }
+ : newSettings;
+ localStorage.setItem('tracker.settings', JSON.stringify(settings));
+ }, []);
+
+ const toggle = React.useCallback(() => {
+ if (enabled) {
+ disable();
+ saveSettings({ autoTrack: false });
+ } else {
+ enable();
+ saveSettings({ autoTrack: true });
+ }
+ }, [enabled]);
+
+ // poll game and push state
+ React.useEffect(() => {
+ if (!enabled || status.error || !status.connected || !status.device) return;
+ const updateState = () => {
+ const saveStart = WRAM_ADDR.SAVE_DATA;
+ const saveSize = SRAM_ADDR.INV_END;
+ sock.current.readWRAM(saveStart, saveSize, (data) => {
+ const computed = computeState(data, prizeMap);
+ setState(s => mergeStates(config, s, computed));
+ });
+ };
+ const fetchPrizes = () => {
+ sock.current.readBytes(RAM_ADDR.PRIZE_MAP, 13, (prizes) => {
+ sock.current.readBytes(RAM_ADDR.CRYSTAL_MAP, 13, (crystals) => {
+ setPrizeMap(m => {
+ const newMap = buildPrizeMap(prizes, crystals);
+ return JSON.stringify(m) === JSON.stringify(newMap) ? m : newMap;
+ });
+ });
+ });
+ };
+ const checkInGame = () => {
+ sock.current.readWRAM(WRAM_ADDR.GAME_MODE, 1, (data) => {
+ if (IN_GAME_MODES.includes(data[0])) {
+ fetchPrizes();
+ updateState();
+ }
+ });
+ };
+ const timer = setInterval(checkInGame, 1000);
+ return () => {
+ clearInterval(timer);
+ };
+ }, [enabled && !status.error && status.connected && status.device, config, prizeMap, sock]);
+
+ const statusMsg = React.useMemo(() => {
+ if (!enabled) {
+ return 'disabled';
+ }
+ if (status.error) {
+ return 'error';
+ }
+ if (!status.connected) {
+ return 'disconnected';
+ }
+ if (!status.device) {
+ return 'no-device';
+ }
+ return 'tracking';
+ }, [enabled, status]);
+
+ return <div>
+ {['disconnected', 'error', 'no-device'].includes(statusMsg) ?
+ <Icon.WARNING
+ className="me-2 text-warning"
+ size="lg"
+ title={t(`autoTracking.statusMsg.${statusMsg}`, { device: status.device })}
+ />
+ : null}
+ {['not-applicable', 'not-in-game'].includes(statusMsg) ?
+ <Icon.INFO
+ className="me-2 text-info"
+ size="lg"
+ title={t(`autoTracking.statusMsg.${statusMsg}`, { device: status.device })}
+ />
+ : null}
+ <Button
+ className="me-2"
+ onClick={openSettings}
+ size="sm"
+ title={t('snes.settings')}
+ variant="outline-secondary"
+ >
+ <Icon.SETTINGS title="" />
+ </Button>
+ <ToggleSwitch
+ onChange={toggle}
+ title={t('autoTracking.heading')}
+ value={enabled}
+ />
+ </div>;
+};
+
+export default AutoTracking;
--- /dev/null
+import PropTypes from 'prop-types';
+import React from 'react';
+
+const CountDisplay = ({ className, count }) => {
+ const classNames = ['count-display'];
+ if (className) {
+ classNames.push(className);
+ }
+ if (!count) {
+ classNames.push('is-zero');
+ }
+ return <span className={classNames.join(' ')}>
+ {count}
+ </span>;
+};
+
+CountDisplay.propTypes = {
+ className: PropTypes.string,
+ count: PropTypes.number,
+};
+
+export default CountDisplay;
--- /dev/null
+import React from 'react';
+
+import CountDisplay from './CountDisplay';
+import ToggleIcon from './ToggleIcon';
+import { useTracker } from '../../hooks/tracker';
+
+const Dungeons = () => {
+ const { dungeons, state } = useTracker();
+
+ return <div className="dungeons">
+ {dungeons.map(dungeon =>
+ <div className={`dungeon dungeon-${dungeon.id}`} key={dungeon.id}>
+ <span className="dungeon-tag">{dungeon.id.toUpperCase()}</span>
+ <ToggleIcon
+ controller={ToggleIcon.dungeonController(dungeon)}
+ icons={['map']}
+ />
+ <ToggleIcon
+ controller={ToggleIcon.dungeonController(dungeon)}
+ icons={['compass']}
+ />
+ <span className="dungeon-smalls">
+ <ToggleIcon
+ controller={ToggleIcon.dungeonCountController(dungeon, dungeon.sk)}
+ icons={['small-key']}
+ />
+ <CountDisplay count={state[`${dungeon.id}-small-key`] || 0} />
+ </span>
+ <ToggleIcon
+ controller={ToggleIcon.dungeonController(dungeon)}
+ icons={['big-key']}
+ />
+ <span className="dungeon-checks">
+ <ToggleIcon
+ controller={ToggleIcon.dungeonCheckController(dungeon, dungeon.items)}
+ icons={['open-chest', 'chest']}
+ />
+ <CountDisplay count={dungeon.items - (state[`${dungeon.id}-checks`] || 0)} />
+ </span>
+ {dungeon.boss ?
+ <ToggleIcon
+ controller={ToggleIcon.dungeonBossController(dungeon)}
+ icons={dungeon.bosses}
+ />
+ : null}
+ {dungeon.prize ?
+ <ToggleIcon
+ controller={ToggleIcon.dungeonPrizeController(dungeon)}
+ icons={[
+ 'crystal',
+ 'red-crystal',
+ 'green-pendant',
+ 'red-pendant',
+ ]}
+ />
+ : null}
+ </div>
+ )}
+ </div>;
+};
+
+export default Dungeons;
--- /dev/null
+import React from 'react';
+
+import CountDisplay from './CountDisplay';
+import ToggleIcon from './ToggleIcon';
+import { useTracker } from '../../hooks/tracker';
+
+const Equipment = () => {
+ const { state } = useTracker();
+
+ return <div className="equipment">
+ <div className="item">
+ <ToggleIcon controller={ToggleIcon.simpleController} icons={['boots']} />
+ </div>
+ <div className="item">
+ <ToggleIcon
+ controller={ToggleIcon.progressiveController('lift', 0, 2)}
+ icons={['glove', 'mitts']}
+ />
+ </div>
+ <div className="item">
+ <ToggleIcon controller={ToggleIcon.simpleController} icons={['flippers']} />
+ </div>
+ <div className="item">
+ <ToggleIcon controller={ToggleIcon.simpleController} icons={['moonpearl']} />
+ </div>
+ <div className="item">
+ <ToggleIcon
+ controller={ToggleIcon.simpleController}
+ icons={['half-magic', 'quarter-magic']}
+ />
+ </div>
+ <div className="item">
+ <ToggleIcon
+ controller={ToggleIcon.progressiveController('sword', 0, 4)}
+ icons={['sword-1', 'sword-2', 'sword-3', 'sword-4']}
+ />
+ </div>
+ <div className="item">
+ <ToggleIcon
+ controller={ToggleIcon.progressiveController('shield', 0, 3)}
+ icons={['fighter-shield', 'fire-shield', 'mirror-shield']}
+ />
+ </div>
+ <div className="item">
+ <ToggleIcon
+ controller={ToggleIcon.progressiveController('mail', 1, 3)}
+ icons={['green-mail', 'blue-mail', 'red-mail']}
+ />
+ </div>
+ <div className="item">
+ <ToggleIcon
+ controller={ToggleIcon.modulusController('heart-piece')}
+ icons={['heart-0', 'heart-1', 'heart-2', 'heart-3']}
+ />
+ </div>
+ </div>;
+};
+
+export default Equipment;
--- /dev/null
+import React from 'react';
+
+import CountDisplay from './CountDisplay';
+import ToggleIcon from './ToggleIcon';
+import { useTracker } from '../../hooks/tracker';
+
+const Items = () => {
+ const { state } = useTracker();
+
+ return <div className="items">
+ <div className="item">
+ <ToggleIcon controller={ToggleIcon.simpleController} icons={['bow', 'silvers']} />
+ </div>
+ <div className="item">
+ <ToggleIcon
+ className="left"
+ controller={ToggleIcon.simpleController}
+ icons={['blue-boomerang']}
+ />
+ <ToggleIcon
+ className="right"
+ controller={ToggleIcon.simpleController}
+ icons={['red-boomerang']}
+ />
+ </div>
+ <div className="item">
+ <ToggleIcon controller={ToggleIcon.simpleController} icons={['hookshot']} />
+ </div>
+ <div className="item">
+ <ToggleIcon controller={ToggleIcon.simpleController} icons={['bomb']} />
+ </div>
+ <div className="item">
+ <ToggleIcon
+ className="bottom-left"
+ controller={ToggleIcon.simpleController}
+ icons={['mushroom']}
+ />
+ <ToggleIcon
+ className="top-right"
+ controller={ToggleIcon.simpleController}
+ icons={['powder']}
+ />
+ </div>
+ <div className="item">
+ <ToggleIcon controller={ToggleIcon.simpleController} icons={['fire-rod']} />
+ </div>
+ <div className="item">
+ <ToggleIcon controller={ToggleIcon.simpleController} icons={['ice-rod']} />
+ </div>
+ <div className="item">
+ <ToggleIcon controller={ToggleIcon.medallionController} icons={['bombos']} />
+ </div>
+ <div className="item">
+ <ToggleIcon controller={ToggleIcon.medallionController} icons={['ether']} />
+ </div>
+ <div className="item">
+ <ToggleIcon controller={ToggleIcon.medallionController} icons={['quake']} />
+ </div>
+ <div className="item">
+ <ToggleIcon controller={ToggleIcon.simpleController} icons={['lamp']} />
+ </div>
+ <div className="item">
+ <ToggleIcon controller={ToggleIcon.simpleController} icons={['hammer']} />
+ </div>
+ <div className="item">
+ <ToggleIcon
+ className="bottom-left"
+ controller={ToggleIcon.simpleController}
+ icons={['shovel']}
+ />
+ <ToggleIcon
+ className="top-right"
+ controller={ToggleIcon.simpleController}
+ icons={['flute', 'duck']}
+ />
+ </div>
+ <div className="item">
+ <ToggleIcon controller={ToggleIcon.simpleController} icons={['bugnet']} />
+ </div>
+ <div className="item">
+ <ToggleIcon controller={ToggleIcon.simpleController} icons={['book']} />
+ </div>
+ <div className="item">
+ <ToggleIcon controller={ToggleIcon.countController(4)} icons={['bottle']} />
+ <CountDisplay className="bottom-right" count={state.bottle || 0} />
+ </div>
+ <div className="item">
+ <ToggleIcon controller={ToggleIcon.simpleController} icons={['somaria']} />
+ </div>
+ <div className="item">
+ <ToggleIcon controller={ToggleIcon.simpleController} icons={['byrna']} />
+ </div>
+ <div className="item">
+ <ToggleIcon controller={ToggleIcon.simpleController} icons={['cape']} />
+ </div>
+ <div className="item">
+ <ToggleIcon controller={ToggleIcon.simpleController} icons={['mirror']} />
+ </div>
+ </div>;
+};
+
+export default Items;
--- /dev/null
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import ZeldaIcon from '../common/ZeldaIcon';
+import {
+ decrement,
+ getDungeonBoss,
+ getDungeonPrize,
+ hasDungeonBoss,
+ hasDungeonPrize,
+ highestActive,
+ increment,
+ toggleBoolean,
+} from '../../helpers/tracker';
+import { useTracker } from '../../hooks/tracker';
+
+const ToggleIcon = ({ controller, className, icons }) => {
+ const { state, setState } = useTracker();
+ const activeController = controller || ToggleIcon.nullController;
+ const active = activeController.getActive(state, icons);
+ const defaultIcon = activeController.getDefault(state, icons);
+ const classNames = ['toggle-icon'];
+ if (active) {
+ classNames.push('active');
+ } else {
+ classNames.push('inactive');
+ }
+ if (className) {
+ classNames.push(className);
+ }
+ return <span
+ className={classNames.join(' ')}
+ onClick={(e) => {
+ activeController.handlePrimary(state, setState, icons);
+ e.preventDefault();
+ e.stopPropagation();
+ }}
+ onContextMenu={(e) => {
+ activeController.handleSecondary(state, setState, icons);
+ e.preventDefault();
+ e.stopPropagation();
+ }}
+ >
+ <ZeldaIcon name={active || defaultIcon || icons[0]} />
+ </span>;
+};
+
+const doNothing = () => { };
+
+const firstIcon = (state, icons) => icons[0];
+
+const nextIcon = (state, setState, icons) => {
+ const highest = highestActive(state, icons);
+ const highestIndex = highest ? icons.indexOf(highest) : -1;
+ if (highestIndex + 1 < icons.length) {
+ setState(toggleBoolean(icons[highestIndex + 1]));
+ } else {
+ const changes = {};
+ icons.forEach(icon => {
+ changes[icon] = false;
+ });
+ setState(s => ({ ...s, ...changes }));
+ }
+};
+
+const previousIcon = (state, setState, icons) => {
+ const highest = highestActive(state, icons);
+ const highestIndex = highest ? icons.indexOf(highest) : -1;
+ if (highestIndex >= 0) {
+ setState(toggleBoolean(icons[highestIndex]));
+ } else {
+ const changes = {};
+ icons.forEach(icon => {
+ changes[icon] = true;
+ });
+ setState(s => ({ ...s, ...changes }));
+ }
+};
+
+const nextString = property => (state, setState, icons) => {
+ const current = state[property] || icons[0];
+ const currentIndex = icons.indexOf(current);
+ const nextIndex = (currentIndex + 1) % icons.length;
+ const next = icons[nextIndex];
+ setState(s => ({ ...s, [property]: next }));
+};
+
+const previousString = property => (state, setState, icons) => {
+ const current = state[property] || icons[0];
+ const currentIndex = icons.indexOf(current);
+ const previousIndex = (currentIndex + icons.length - 1) % icons.length;
+ const previous = icons[previousIndex];
+ setState(s => ({ ...s, [property]: previous }));
+};
+
+ToggleIcon.countController = max => ({
+ getActive: highestActive,
+ getDefault: firstIcon,
+ handlePrimary: (state, setState, icons) => {
+ setState(increment(icons[0], max));
+ },
+ handleSecondary: (state, setState, icons) => {
+ setState(decrement(icons[0], max));
+ },
+});
+
+ToggleIcon.dungeonBossController = (dungeon) => ({
+ getActive: (state) => hasDungeonBoss(state, dungeon) ? getDungeonBoss(state, dungeon) : null,
+ getDefault: (state) => getDungeonBoss(state, dungeon),
+ handlePrimary: dungeon.bosses.length > 1
+ ? nextString(`${dungeon.id}-boss`)
+ : (state, setState) => {
+ setState(toggleBoolean(`${dungeon.id}-boss-defeated`));
+ },
+ handleSecondary: dungeon.bosses.length > 1 ?
+ previousString(`${dungeon.id}-boss`)
+ : (state, setState) => {
+ setState(toggleBoolean(`${dungeon.id}-boss-defeated`));
+ },
+});
+
+ToggleIcon.dungeonCheckController = (dungeon, max) => ({
+ getActive: (state, icons) => state[`${dungeon.id}-checks`] < max ? icons[1] : null,
+ getDefault: firstIcon,
+ handlePrimary: (state, setState) => {
+ setState(increment(`${dungeon.id}-checks`, max));
+ },
+ handleSecondary: (state, setState) => {
+ setState(decrement(`${dungeon.id}-checks`, max));
+ },
+});
+
+ToggleIcon.dungeonController = dungeon => ({
+ getActive: (state, icons) => state[`${dungeon.id}-${icons[0]}`] ? icons[0] : null,
+ getDefault: firstIcon,
+ handlePrimary: (state, setState, icons) => {
+ setState(toggleBoolean(`${dungeon.id}-${icons[0]}`));
+ },
+ handleSecondary: (state, setState, icons) => {
+ setState(toggleBoolean(`${dungeon.id}-${icons[0]}`));
+ },
+});
+
+ToggleIcon.dungeonCountController = (dungeon, max) => ({
+ getActive: (state, icons) => state[`${dungeon.id}-${icons[0]}`] ? icons[0] : null,
+ getDefault: firstIcon,
+ handlePrimary: (state, setState, icons) => {
+ setState(increment(`${dungeon.id}-${icons[0]}`, max));
+ },
+ handleSecondary: (state, setState, icons) => {
+ setState(decrement(`${dungeon.id}-${icons[0]}`, max));
+ },
+});
+
+ToggleIcon.dungeonPrizeController = (dungeon) => ({
+ getActive: (state) => hasDungeonPrize(state, dungeon) ? getDungeonPrize(state, dungeon) : null,
+ getDefault: (state) => getDungeonPrize(state, dungeon),
+ handlePrimary: nextString(`${dungeon.id}-prize`),
+ handleSecondary: previousString(`${dungeon.id}-prize`),
+});
+
+ToggleIcon.medallionController = {
+ getActive: highestActive,
+ getDefault: firstIcon,
+ handlePrimary: nextIcon,
+ handleSecondary: doNothing,
+};
+
+ToggleIcon.modulusController = ctrl => ({
+ getActive: (state, icons) => icons[(state[ctrl] || 0) % icons.length],
+ getDefault: firstIcon,
+ handlePrimary: (state, setState, icons) => {
+ setState(increment(icons[0], icons.length));
+ },
+ handleSecondary: (state, setState, icons) => {
+ setState(decrement(icons[0], icons.length));
+ },
+});
+
+ToggleIcon.nullController = {
+ getActive: () => null,
+ getDefault: firstIcon,
+ handlePrimary: doNothing,
+ handleSecondary: doNothing,
+};
+
+ToggleIcon.simpleController = {
+ getActive: highestActive,
+ getDefault: firstIcon,
+ handlePrimary: nextIcon,
+ handleSecondary: previousIcon,
+};
+
+ToggleIcon.progressiveController = (master, min, max) => ({
+ getActive: (state, icons) => {
+ const count = Math.max(min, Math.min(max, state[master] || 0));
+ return count ? icons[count - 1] : null;
+ },
+ getDefault: firstIcon,
+ handlePrimary: (state, setState) => {
+ setState(increment(master, max, min));
+ },
+ handleSecondary: (state, setState) => {
+ setState(decrement(master, max, min));
+ },
+});
+
+ToggleIcon.propTypes = {
+ active: PropTypes.string,
+ className: PropTypes.string,
+ controller: PropTypes.shape({
+ handlePrimary: PropTypes.func,
+ handleSecondary: PropTypes.func,
+ }),
+ icons: PropTypes.arrayOf(PropTypes.string),
+};
+
+export default ToggleIcon;
--- /dev/null
+import React from 'react';
+import { Container, Navbar } from 'react-bootstrap';
+
+import AutoTracking from './AutoTracking';
+import ToggleIcon from './ToggleIcon';
+import { useTracker } from '../../hooks/tracker';
+
+const mapWild = {
+ map: 'wildMap',
+ compass: 'wildCompass',
+ 'small-key': 'wildSmall',
+ 'big-key': 'wildBig',
+};
+
+const Toolbar = () => {
+ const { config, setConfig } = useTracker();
+
+ const controller = React.useMemo(() => ({
+ getActive: (state, icons) => config[mapWild[icons[0]]] ? icons[0] : null,
+ getDefault: (state, icons) => icons[0],
+ handlePrimary: (state, setState, icons) => {
+ const prop = mapWild[icons[0]];
+ setConfig(c => ({ ...c, [prop]: !c[prop] }));
+ },
+ handleSecondary: () => null,
+ }), [config, setConfig]);
+
+ return <Navbar bg="dark" className="tracker-toolbar" variant="dark">
+ <Container fluid>
+ <div className="button-bar">
+ <ToggleIcon controller={controller} icons={['map']} />
+ <ToggleIcon controller={controller} icons={['compass']} />
+ <ToggleIcon controller={controller} icons={['small-key']} />
+ <ToggleIcon controller={controller} icons={['big-key']} />
+ </div>
+ <AutoTracking />
+ </Container>
+ </Navbar>;
+};
+
+export default Toolbar;
--- /dev/null
+import React from 'react';
+
+import Dungeons from './Dungeons';
+import Equipment from './Equipment';
+import Items from './Items';
+import Toolbar from './Toolbar';
+
+const Tracker = () => {
+ return <div className="tracker">
+ <Toolbar />
+ <Items />
+ <Equipment />
+ <Dungeons />
+ </div>;
+};
+
+export default Tracker;
compareGTBasementState,
countGTBasementState,
getGTBasementState,
+ IN_GAME_MODES,
+ INV_ADDR,
+ RAM_ADDR,
+ SRAM_ADDR,
+ WRAM_ADDR,
} from '../../helpers/alttp-ram';
import { useSNES } from '../../hooks/snes';
-const IN_GAME_MODES = [
- 0x05, // loading game
- 0x06, // entering dungeon
- 0x07, // dungeon
- 0x08, // entering overworld
- 0x09, // overworld
- 0x0A, // entering special overworld
- 0x0B, // special overworld
- 0x0E, // text/menu/map
- 0x0F, // closing spot
- 0x10, // opening spot
- 0x11, // falling
- 0x12, // dying
- 0x13, // fanfare
- 0x15, // mirror
- 0x16, // refill
- 0x17, // S&Q
- 0x18, // aga 2 cutscene
- 0x19, // triforce room
- 0x1A, // credits
- 0x1B, // spawn select
-];
-
const GT_TYPES = [
0x02, // all dungeons
0x03, // defeat ganon
0x0B, // completionist
];
-const FREE_ITEM_MENU = 0x180045;
-const GT_CRYSTALS = 0x18019A;
-const GANON_TYPE = 0x1801A8;
-const SEED_TYPE = 0x180210;
-const INIT_SRAM = 0x183000;
-
-const GAME_MODE = 0x10;
-const CURRENT_DUNGEON = 0x10E;
-const SAVE_WRAM = 0xF000;
-const ROOM_DATA_START = 0x000;
-const ROOM_DATA_END = 0x140;
-const PYRAMID_SCREEN = 0x2DB;
-const BIG_KEYS_1 = 0x366;
-const OWNED_CRYSTALS = 0x37A;
-
const GT_ENTRANCE_ID = 55;
const GuessingGameAutoTracking = ({ onSolve, onStart, onStop }) => {
React.useEffect(() => {
if (enabled && !status.error && status.connected && status.device) {
const checkInGame = () => {
- sock.current.readWRAM(GAME_MODE, 1, (data) => {
+ sock.current.readWRAM(WRAM_ADDR.GAME_MODE, 1, (data) => {
setInGame(IN_GAME_MODES.includes(data[0]));
});
};
// refresh static game information
React.useEffect(() => {
if (!inGame) return;
- sock.current.readBytes(SEED_TYPE, 1, (data) => {
+ sock.current.readBytes(RAM_ADDR.SEED_TYPE, 1, (data) => {
setSeedType(data[0]);
});
- sock.current.readBytes(GT_CRYSTALS, 1, (data) => {
+ sock.current.readBytes(RAM_ADDR.GT_CRYSTALS, 1, (data) => {
setGTCrystals(data[0]);
});
- sock.current.readBytes(GANON_TYPE, 1, (data) => {
+ sock.current.readBytes(RAM_ADDR.GANON_TYPE, 1, (data) => {
setGanonType(data[0]);
});
- sock.current.readBytes(FREE_ITEM_MENU, 1, (data) => {
+ sock.current.readBytes(RAM_ADDR.FREE_ITEM_MENU, 1, (data) => {
setFreeItemMenu(data[0]);
});
- sock.current.readBytes(INIT_SRAM + PYRAMID_SCREEN, 1, (data) => {
+ sock.current.readBytes(RAM_ADDR.INIT_SRAM + SRAM_ADDR.PYRAMID_SCREEN, 1, (data) => {
setPyramidOpen(!!(data[0] & 0x20));
});
}, [inGame, sock]);
React.useEffect(() => {
if (!applicable || !inGame || hasBigKey) return;
const updateCrystals = () => {
- sock.current.readWRAM(SAVE_WRAM + OWNED_CRYSTALS, 1, (data) => {
+ const crAddress = WRAM_ADDR.SAVE_DATA + SRAM_ADDR.INV_START + INV_ADDR.CRYSTALS;
+ sock.current.readWRAM(crAddress, 1, (data) => {
let owned = 0;
for (let i = 0; i < 7; ++i) {
if (data[0] & Math.pow(2, i)) {
if (!applicable || hasBigKey || ownedCrystals !== gtCrystals || hasEntered) return;
controls.current.onStart();
const updateDungeon = () => {
- sock.current.readWRAM(CURRENT_DUNGEON, 2, (data) => {
+ sock.current.readWRAM(WRAM_ADDR.CURRENT_DUNGEON, 2, (data) => {
setLastEntrance(data[0] + (data[1] * 256));
});
};
React.useEffect(() => {
if (!applicable || !hasEntered || hasBigKey) return;
const updateGTState = () => {
- const roomDataSize = ROOM_DATA_END - ROOM_DATA_START;
- sock.current.readWRAM(SAVE_WRAM + ROOM_DATA_START, roomDataSize, (data) => {
+ const roomDataStart = WRAM_ADDR.SAVE_DATA + SRAM_ADDR.ROOM_DATA_START;
+ const roomDataSize = SRAM_ADDR.ROOM_DATA_END - SRAM_ADDR.ROOM_DATA_START;
+ sock.current.readWRAM(roomDataStart, roomDataSize, (data) => {
const gtState = getGTBasementState(data);
const gtCount = countGTBasementState(gtState);
setBasement(old => {
const solution = basement.last === 'torch' ? basement.torch : basement.count;
controls.current.onSolve(solution);
} else {
- sock.current.readWRAM(SAVE_WRAM + BIG_KEYS_1, 1, (data) => {
+ const bkAddr = WRAM_ADDR.SAVE_DATA + SRAM_ADDR.INV_START + INV_ADDR.BIG_KEY;
+ sock.current.readWRAM(bkAddr, 1, (data) => {
setHasBigKey(!!(data[0] & 0x04));
});
}
+export const RAM_ADDR = {
+ PRIZE_MAP: 0x1209B,
+ FREE_ITEM_MENU: 0x180045,
+ CRYSTAL_MAP: 0x180050,
+ GT_CRYSTALS: 0x18019A,
+ GANON_TYPE: 0x1801A8,
+ SEED_TYPE: 0x180210,
+ INIT_SRAM: 0x183000,
+};
+
+export const SRAM_ADDR = {
+ ROOM_DATA_START: 0x000,
+ ROOM_DATA_END: 0x250,
+ OW_DATA_START: 0x280,
+ PYRAMID_SCREEN: 0x2DB,
+ OW_DATA_END: 0x300,
+ INV_START: 0x340,
+ INV_END: 0x4EF,
+};
+
+export const WRAM_ADDR = {
+ GAME_MODE: 0x10,
+ CURRENT_DUNGEON: 0x10E,
+ SAVE_DATA: 0xF000,
+};
+
+export const INV_ADDR = {
+ BOW: 0x00,
+ BOOM: 0x01,
+ HOOK: 0x02,
+ BOMB: 0x03,
+ POWDER: 0x04,
+ FROD: 0x05,
+ IROD: 0x06,
+ BOMBOS: 0x07,
+ ETHER: 0x08,
+ QUAKE: 0x09,
+ LAMP: 0x0A,
+ HAMMER: 0x0B,
+ FLUTE: 0x0C,
+ BUGNET: 0x0D,
+ BOOK: 0x0E,
+ BOTTLE: 0x0F,
+ SOMARIA: 0x10,
+ BYRNA: 0x11,
+ CAPE: 0x12,
+ MIRROR: 0x13,
+ GLOVE: 0x14,
+ BOOTS: 0x15,
+ FLIPPERS: 0x16,
+ MOONPEARL: 0x17,
+ SWORD: 0x19,
+ SHIELD: 0x1A,
+ ARMOR: 0x1B,
+ BOTTLE_1: 0x1C,
+ BOTTLE_2: 0x1D,
+ BOTTLE_3: 0x1E,
+ BOTTLE_4: 0x1F,
+ WALLET: 0x20,
+ RUPEES: 0x22,
+ COMPASS: 0x24,
+ BIG_KEY: 0x26,
+ MAP: 0x28,
+ HEART_PIECE: 0x2B,
+ HEALTH: 0x2C,
+ MAGIC: 0x2E,
+ KEYS: 0x2F,
+ PENDANTS: 0x34,
+ ARROWS: 0x37,
+ ABILITIES: 0x39,
+ CRYSTALS: 0x3A,
+ MAGIC_USE: 0x3B,
+ SMALL_KEY_START: 0x3C,
+ SMALL_KEY_END: 0x4C,
+ RANDO_BOOM: 0x4C,
+ RANDO_POWDER: 0x4C,
+ RANDO_FLUTE: 0x4C,
+ RANDO_BOW: 0x4E,
+ RANDO_KEY_START: 0x1A0,
+ RANDO_KEY_END: 0x1AF,
+};
+
+export const DUNGEON_IDS = {
+ SEWERS: 0,
+ HC: 1,
+ EP: 2,
+ DP: 3,
+ CT: 4,
+ SP: 5,
+ PD: 6,
+ MM: 7,
+ SW: 8,
+ IP: 9,
+ TH: 10,
+ TT: 11,
+ TR: 12,
+ GT: 13,
+};
+
+export const DUNGEON_MASKS = {
+ SEWERS: 0x0080,
+ HC: 0x0040,
+ EP: 0x0020,
+ DP: 0x0010,
+ CT: 0x0008,
+ SP: 0x0004,
+ PD: 0x0002,
+ MM: 0x0001,
+ SW: 0x8000,
+ IP: 0x4000,
+ TH: 0x2000,
+ TT: 0x1000,
+ TR: 0x0800,
+ GT: 0x0400,
+};
+
+export const ABILITY_MASKS = {
+ SWIM: 0x02,
+ DASH: 0x04,
+ PULL: 0x08,
+ TALK: 0x20,
+ READ: 0x40,
+};
+
+export const IN_GAME_MODES = [
+ 0x05, // loading game
+ 0x06, // entering dungeon
+ 0x07, // dungeon
+ 0x08, // entering overworld
+ 0x09, // overworld
+ 0x0A, // entering special overworld
+ 0x0B, // special overworld
+ 0x0E, // text/menu/map
+ 0x0F, // closing spot
+ 0x10, // opening spot
+ 0x11, // falling
+ 0x12, // dying
+ 0x13, // fanfare
+ 0x15, // mirror
+ 0x16, // refill
+ 0x17, // S&Q
+ 0x18, // aga 2 cutscene
+ 0x19, // triforce room
+ 0x1B, // spawn select
+];
+
+export const getShort = (data, offset) => (data[offset] * 256) + data[offset + 1];
+
+export const buildPrizeMap = (prizes, crystals) => {
+ const map = {};
+ Object.entries(DUNGEON_IDS).forEach(([, id]) => {
+ const isCrystal = !!(crystals && crystals[id]);
+ const mask = (prizes && prizes[id]) || 0;
+ map[id] = { isCrystal, mask };
+ });
+ return map;
+};
+
+export const isBossDefeated = (data, room) => {
+ return !!(data && (data[(2 * room) + 1] & 0x08));
+};
+
export const isChestOpen = (data, room, chest) => {
if (chest < 4) {
return !!(data && (data[2 * room] & Math.pow(2, chest + 4)));
--- /dev/null
+import {
+ DUNGEON_IDS,
+ DUNGEON_MASKS,
+ INV_ADDR,
+ SRAM_ADDR,
+ getShort,
+ isBossDefeated,
+ isChestOpen,
+} from './alttp-ram';
+
+export const BOOLEAN_STATES = [
+ 'blue-boomerang',
+ 'bomb',
+ 'bombos',
+ 'bow',
+ 'bowless-silvers',
+ 'book',
+ 'boots',
+ 'bugnet',
+ 'byrna',
+ 'cape',
+ 'duck',
+ 'ether',
+ 'fire-rod',
+ 'flippers',
+ 'flute',
+ 'half-magic',
+ 'hammer',
+ 'hookshot',
+ 'ice-rod',
+ 'lamp',
+ 'mirror',
+ 'moonpearl',
+ 'mushroom',
+ 'powder',
+ 'quake',
+ 'quarter-magic',
+ 'red-boomerang',
+ 'shovel',
+ 'silvers',
+ 'somaria',
+];
+
+export const INTEGER_STATES = [
+ 'bottle',
+ 'heart-piece',
+ 'lift',
+ 'mail',
+ 'shield',
+ 'sword',
+];
+
+export const INITIAL = {
+ mail: 1,
+};
+
+export const BOSSES = [
+ 'armos',
+ 'lanmolas',
+ 'moldorm',
+ 'helma',
+ 'arrghus',
+ 'mothula',
+ 'blind',
+ 'kholdstare',
+ 'vitreous',
+ 'trinexx',
+];
+
+export const CONFIG = {
+ wildMap: false,
+ wildCompass: false,
+ wildSmall: false,
+ wildBig: false,
+ bossShuffle: false,
+};
+
+export const DUNGEONS = [
+ {
+ id: 'hc',
+ map: true,
+ compass: false,
+ sk: 1,
+ bk: true,
+ dropBk: true,
+ items: 6,
+ boss: null,
+ bosses: [],
+ bossRoom: 0x80,
+ prize: false,
+ offset: DUNGEON_IDS.HC,
+ mask: DUNGEON_MASKS.HC,
+ checks: [
+ 'dark-cross',
+ 'hc-map',
+ 'hc-boom',
+ 'hc-cell',
+ 'sanc',
+ 'sewers-left',
+ 'sewers-mid',
+ 'sewers-right',
+ ],
+ },
+ {
+ id: 'ct',
+ map: false,
+ compass: false,
+ sk: 2,
+ bk: false,
+ items: 0,
+ boss: 'aga',
+ bosses: ['aga'],
+ bossRoom: 0x20,
+ prize: false,
+ offset: DUNGEON_IDS.CT,
+ mask: DUNGEON_MASKS.CT,
+ checks: [
+ 'ct-1',
+ 'ct-2',
+ ],
+ },
+ {
+ id: 'gt',
+ map: true,
+ compass: true,
+ sk: 4,
+ bk: true,
+ items: 20,
+ boss: 'aga',
+ bosses: ['aga'],
+ bossRoom: 0x0D,
+ prize: false,
+ offset: DUNGEON_IDS.GT,
+ mask: DUNGEON_MASKS.GT,
+ checks: [
+ 'gt-hope-left',
+ 'gt-hope-right',
+ 'gt-tile-room',
+ 'gt-compass-tl',
+ 'gt-compass-tr',
+ 'gt-compass-bl',
+ 'gt-compass-br',
+ 'gt-torch',
+ 'gt-dm-tl',
+ 'gt-dm-tr',
+ 'gt-dm-bl',
+ 'gt-dm-br',
+ 'gt-map-chest',
+ 'gt-firesnake',
+ 'gt-rando-tl',
+ 'gt-rando-tr',
+ 'gt-rando-bl',
+ 'gt-rando-br',
+ 'gt-bobs-chest',
+ 'gt-ice-left',
+ 'gt-ice-mid',
+ 'gt-ice-right',
+ 'gt-big-chest',
+ 'gt-helma-left',
+ 'gt-helma-right',
+ 'gt-pre-moldorm',
+ 'gt-post-moldorm',
+ ],
+ },
+ {
+ id: 'ep',
+ map: true,
+ compass: true,
+ sk: 0,
+ bk: true,
+ items: 3,
+ boss: 'armos',
+ bosses: ['armos'],
+ bossRoom: 0xC8,
+ prize: true,
+ isPendant: true,
+ prizeMask: 0x04,
+ offset: DUNGEON_IDS.EP,
+ mask: DUNGEON_MASKS.EP,
+ checks: [
+ 'ep-cannonball',
+ 'ep-map-chest',
+ 'ep-compass-chest',
+ 'ep-big-chest',
+ 'ep-big-key-chest',
+ 'ep-boss-defeated',
+ ],
+ },
+ {
+ id: 'dp',
+ map: true,
+ compass: true,
+ sk: 1,
+ bk: true,
+ items: 2,
+ boss: 'lanmolas',
+ bosses: ['lanmolas'],
+ bossRoom: 0x33,
+ prize: true,
+ isPendant: true,
+ prizeMask: 0x02,
+ offset: DUNGEON_IDS.DP,
+ mask: DUNGEON_MASKS.DP,
+ checks: [
+ 'dp-torch',
+ 'dp-map-chest',
+ 'dp-big-chest',
+ 'dp-compass-chest',
+ 'dp-big-key-chest',
+ 'dp-boss-defeated',
+ ],
+ },
+ {
+ id: 'th',
+ map: true,
+ compass: true,
+ sk: 1,
+ bk: true,
+ items: 2,
+ boss: 'moldorm',
+ bosses: ['moldorm'],
+ bossRoom: 0x07,
+ prize: true,
+ isPendant: true,
+ prizeMask: 0x01,
+ offset: DUNGEON_IDS.TH,
+ mask: DUNGEON_MASKS.TH,
+ checks: [
+ 'th-basement-cage',
+ 'th-map-chest',
+ 'th-big-key-chest',
+ 'th-compass-chest',
+ 'th-big-chest',
+ 'th-boss-defeated',
+ ],
+ },
+ {
+ id: 'pd',
+ map: true,
+ compass: true,
+ sk: 6,
+ bk: true,
+ items: 5,
+ boss: 'helma',
+ bosses: ['helma'],
+ bossRoom: 0x5A,
+ prize: true,
+ prizeMask: 0x02,
+ offset: DUNGEON_IDS.PD,
+ mask: DUNGEON_MASKS.PD,
+ checks: [
+ 'pd-shooter-room',
+ 'pd-stalfos-basement',
+ 'pd-big-key-chest',
+ 'pd-arena-bridge',
+ 'pd-arena-ledge',
+ 'pd-map-chest',
+ 'pd-compass-chest',
+ 'pd-basement-left',
+ 'pd-basement-right',
+ 'pd-harmless-hellway',
+ 'pd-maze-top',
+ 'pd-maze-bottom',
+ 'pd-big-chest',
+ 'pd-boss-defeated',
+ ],
+ },
+ {
+ id: 'sp',
+ map: true,
+ compass: true,
+ sk: 1,
+ bk: true,
+ items: 6,
+ boss: 'arrghus',
+ bosses: ['arrghus'],
+ bossRoom: 0x06,
+ prize: true,
+ prizeMask: 0x10,
+ offset: DUNGEON_IDS.SP,
+ mask: DUNGEON_MASKS.SP,
+ checks: [
+ 'sp-lobby',
+ 'sp-map-chest',
+ 'sp-big-chest',
+ 'sp-compass-chest',
+ 'sp-west-chest',
+ 'sp-big-key-chest',
+ 'sp-flooded-left',
+ 'sp-flooded-right',
+ 'sp-waterfall',
+ 'sp-boss-defeated',
+ ],
+ },
+ {
+ id: 'sw',
+ map: true,
+ compass: true,
+ sk: 3,
+ bk: true,
+ items: 2,
+ boss: 'mothula',
+ bosses: ['mothula'],
+ bossRoom: 0x29,
+ prize: true,
+ prizeMask: 0x40,
+ offset: DUNGEON_IDS.SW,
+ mask: DUNGEON_MASKS.SW,
+ checks: [
+ 'sw-big-chest',
+ 'sw-map-chest',
+ 'sw-pot-prison',
+ 'sw-compass-chest',
+ 'sw-pinball-room',
+ 'sw-big-key-chest',
+ 'sw-bridge-chest',
+ 'sw-boss-defeated',
+ ],
+ },
+ {
+ id: 'tt',
+ map: true,
+ compass: true,
+ sk: 1,
+ bk: true,
+ items: 4,
+ boss: 'blind',
+ bosses: ['blind'],
+ bossRoom: 0xAC,
+ prize: true,
+ prizeMask: 0x20,
+ offset: DUNGEON_IDS.TT,
+ mask: DUNGEON_MASKS.TT,
+ checks: [
+ 'tt-map-chest',
+ 'tt-ambush-chest',
+ 'tt-compass-chest',
+ 'tt-big-key-chest',
+ 'tt-attic',
+ 'tt-cell',
+ 'tt-big-chest',
+ 'tt-boss-defeated',
+ ],
+ },
+ {
+ id: 'ip',
+ map: true,
+ compass: true,
+ sk: 2,
+ bk: true,
+ items: 3,
+ boss: 'kholdstare',
+ bosses: ['kholdstare'],
+ bossRoom: 0xDE,
+ prize: true,
+ prizeMask: 0x04,
+ offset: DUNGEON_IDS.IP,
+ mask: DUNGEON_MASKS.IP,
+ checks: [
+ 'ip-compass-chest',
+ 'ip-big-key-chest',
+ 'ip-map-chest',
+ 'ip-spike-chest',
+ 'ip-freezor-chest',
+ 'ip-big-chest',
+ 'ip-ice-t',
+ 'ip-boss-defeated',
+ ],
+ },
+ {
+ id: 'mm',
+ map: true,
+ compass: true,
+ sk: 3,
+ bk: true,
+ items: 2,
+ boss: 'vitreous',
+ bosses: ['vitreous'],
+ bossRoom: 0x90,
+ prize: true,
+ prizeMask: 0x01,
+ offset: DUNGEON_IDS.MM,
+ mask: DUNGEON_MASKS.MM,
+ checks: [
+ 'mm-bridge-chest',
+ 'mm-spike-chest',
+ 'mm-lobby-chest',
+ 'mm-compass-chest',
+ 'mm-big-key-chest',
+ 'mm-big-chest',
+ 'mm-map-chest',
+ 'mm-boss-defeated',
+ ],
+ },
+ {
+ id: 'tr',
+ map: true,
+ compass: true,
+ sk: 4,
+ bk: true,
+ items: 5,
+ boss: 'trinexx',
+ bosses: ['trinexx'],
+ bossRoom: 0xA4,
+ prize: true,
+ prizeMask: 0x08,
+ offset: DUNGEON_IDS.TR,
+ mask: DUNGEON_MASKS.TR,
+ checks: [
+ 'tr-roller-left',
+ 'tr-roller-right',
+ 'tr-compass-chest',
+ 'tr-chomps',
+ 'tr-big-key-chest',
+ 'tr-big-chest',
+ 'tr-crysta-roller',
+ 'tr-laser-bridge-top',
+ 'tr-laser-bridge-left',
+ 'tr-laser-bridge-right',
+ 'tr-laser-bridge-bottom',
+ 'tr-boss-defeated',
+ ],
+ },
+];
+
+export const OVERWORLD_LOCATIONS = [
+ {
+ id: 'blacksmith',
+ address: 0x411,
+ mask: 0x04,
+ },
+ {
+ id: 'bombos-tablet',
+ address: 0x411,
+ mask: 0x02,
+ },
+ {
+ id: 'bottle-vendor',
+ address: 0x3C9,
+ mask: 0x02,
+ },
+ {
+ id: 'bumper-cave',
+ address: 0x2CA,
+ mask: 0x40,
+ },
+ {
+ id: 'catfish',
+ address: 0x410,
+ mask: 0x20,
+ },
+ {
+ id: 'desert-ledge',
+ address: 0x2B0,
+ mask: 0x40,
+ },
+ {
+ id: 'digging-game',
+ address: 0x2E8,
+ mask: 0x40,
+ },
+ {
+ id: 'ether-tablet',
+ address: 0x411,
+ mask: 0x01,
+ },
+ {
+ id: 'floating-island',
+ address: 0x285,
+ mask: 0x40,
+ },
+ {
+ id: 'flute-spot',
+ address: 0x2AA,
+ mask: 0x40,
+ },
+ {
+ id: 'hobo',
+ address: 0x3C9,
+ mask: 0x01,
+ },
+ {
+ id: 'lake-hylia-island',
+ address: 0x2B5,
+ mask: 0x40,
+ },
+ {
+ id: 'library',
+ address: 0x410,
+ mask: 0x80,
+ },
+ {
+ id: 'magic-bat',
+ address: 0x411,
+ mask: 0x80,
+ },
+ {
+ id: 'mushroom-spot',
+ address: 0x411,
+ mask: 0x10,
+ },
+ {
+ id: 'old-man',
+ address: 0x410,
+ mask: 0x01,
+ },
+ {
+ id: 'pedestal',
+ address: 0x300,
+ mask: 0x40,
+ },
+ {
+ id: 'potion-shop',
+ address: 0x411,
+ mask: 0x20,
+ },
+ {
+ id: 'purple-chest',
+ address: 0x3C9,
+ mask: 0x10,
+ },
+ {
+ id: 'pyramid',
+ address: 0x2DB,
+ mask: 0x40,
+ },
+ {
+ id: 'race-game',
+ address: 0x2A8,
+ mask: 0x40,
+ },
+ {
+ id: 'saha',
+ address: 0x410,
+ mask: 0x10,
+ },
+ {
+ id: 'sick-kid',
+ address: 0x410,
+ mask: 0x04,
+ },
+ {
+ id: 'spec-rock',
+ address: 0x283,
+ mask: 0x40,
+ },
+ {
+ id: 'stumpy',
+ address: 0x410,
+ mask: 0x08,
+ },
+ {
+ id: 'sunken-treasure',
+ address: 0x2BB,
+ mask: 0x40,
+ },
+ {
+ id: 'uncle',
+ address: 0x3C6,
+ mask: 0x01,
+ },
+ {
+ id: 'zora',
+ address: 0x410,
+ mask: 0x02,
+ },
+ {
+ id: 'zora-ledge',
+ address: 0x301,
+ mask: 0x40,
+ },
+];
+
+export const UNDERWORLD_LOCATIONS = [
+ {
+ id: 'aginah',
+ room: 0x10A,
+ chest: 0,
+ },
+ {
+ id: 'blinds-hut-top',
+ room: 0x11D,
+ chest: 0,
+ },
+ {
+ id: 'blinds-hut-left',
+ room: 0x11D,
+ chest: 1,
+ },
+ {
+ id: 'blinds-hut-right',
+ room: 0x11D,
+ chest: 2,
+ },
+ {
+ id: 'blinds-hut-far-left',
+ room: 0x11D,
+ chest: 3,
+ },
+ {
+ id: 'blinds-hut-far-right',
+ room: 0x11D,
+ chest: 4,
+ },
+ {
+ id: 'bonk-rocks',
+ room: 0x124,
+ chest: 0,
+ },
+ {
+ id: 'brewery',
+ room: 0x106,
+ chest: 0,
+ },
+ {
+ id: 'c-house',
+ room: 0x11C,
+ chest: 0,
+ },
+ {
+ id: 'cave-45',
+ room: 0x11B,
+ chest: 6,
+ },
+ {
+ id: 'checkerboard',
+ room: 0x126,
+ chest: 5,
+ },
+ {
+ id: 'chest-game',
+ room: 0x106,
+ chest: 6,
+ },
+ {
+ id: 'chicken-house',
+ room: 0x108,
+ chest: 0,
+ },
+ {
+ id: 'ct-1',
+ area: 'ct',
+ room: 0xE0,
+ chest: 0,
+ },
+ {
+ id: 'ct-2',
+ area: 'ct',
+ room: 0xD0,
+ chest: 0,
+ },
+ {
+ id: 'dark-cross',
+ area: 'hc',
+ room: 0x32,
+ chest: 0,
+ },
+ {
+ id: 'dp-big-chest',
+ area: 'dp',
+ room: 0x73,
+ chest: 0,
+ },
+ {
+ id: 'dp-big-key-chest',
+ area: 'dp',
+ room: 0x75,
+ chest: 0,
+ },
+ {
+ id: 'dp-compass-chest',
+ area: 'dp',
+ room: 0x85,
+ chest: 0,
+ },
+ {
+ id: 'dp-map-chest',
+ area: 'dp',
+ room: 0x74,
+ chest: 0,
+ },
+ {
+ id: 'dp-torch',
+ area: 'dp',
+ room: 0x73,
+ chest: 6,
+ },
+ {
+ id: 'ep-big-chest',
+ area: 'ep',
+ room: 0xA9,
+ chest: 0,
+ },
+ {
+ id: 'ep-big-key-chest',
+ area: 'ep',
+ room: 0xB8,
+ chest: 0,
+ },
+ {
+ id: 'ep-cannonball',
+ area: 'ep',
+ room: 0xB9,
+ chest: 0,
+ },
+ {
+ id: 'ep-compass-chest',
+ area: 'ep',
+ room: 0xA8,
+ chest: 0,
+ },
+ {
+ id: 'ep-map-chest',
+ area: 'ep',
+ room: 0xAA,
+ chest: 0,
+ },
+ {
+ id: 'flooded-chest',
+ room: 0x10B,
+ chest: 0,
+ },
+ {
+ id: 'graveyard-ledge',
+ room: 0x11B,
+ chest: 5,
+ },
+ {
+ id: 'gt-hope-left',
+ area: 'gt',
+ room: 0x8C,
+ chest: 1,
+ },
+ {
+ id: 'gt-hope-right',
+ area: 'gt',
+ room: 0x8C,
+ chest: 2,
+ },
+ {
+ id: 'gt-tile-room',
+ area: 'gt',
+ room: 0x8D,
+ chest: 0,
+ },
+ {
+ id: 'gt-compass-tl',
+ area: 'gt',
+ room: 0x9D,
+ chest: 0,
+ },
+ {
+ id: 'gt-compass-tr',
+ area: 'gt',
+ room: 0x9D,
+ chest: 1,
+ },
+ {
+ id: 'gt-compass-bl',
+ area: 'gt',
+ room: 0x9D,
+ chest: 2,
+ },
+ {
+ id: 'gt-compass-br',
+ area: 'gt',
+ room: 0x9D,
+ chest: 3,
+ },
+ {
+ id: 'gt-torch',
+ area: 'gt',
+ room: 0x8C,
+ chest: 6,
+ },
+ {
+ id: 'gt-dm-tl',
+ area: 'gt',
+ room: 0x7B,
+ chest: 0,
+ },
+ {
+ id: 'gt-dm-tr',
+ area: 'gt',
+ room: 0x7B,
+ chest: 1,
+ },
+ {
+ id: 'gt-dm-bl',
+ area: 'gt',
+ room: 0x7B,
+ chest: 2,
+ },
+ {
+ id: 'gt-dm-br',
+ area: 'gt',
+ room: 0x7B,
+ chest: 3,
+ },
+ {
+ id: 'gt-map-chest',
+ area: 'gt',
+ room: 0x8B,
+ chest: 0,
+ },
+ {
+ id: 'gt-firesnake',
+ area: 'gt',
+ room: 0x7D,
+ chest: 0,
+ },
+ {
+ id: 'gt-rando-tl',
+ area: 'gt',
+ room: 0x7C,
+ chest: 0,
+ },
+ {
+ id: 'gt-rando-tr',
+ area: 'gt',
+ room: 0x7C,
+ chest: 1,
+ },
+ {
+ id: 'gt-rando-bl',
+ area: 'gt',
+ room: 0x7C,
+ chest: 2,
+ },
+ {
+ id: 'gt-rando-br',
+ area: 'gt',
+ room: 0x7C,
+ chest: 3,
+ },
+ {
+ id: 'gt-bobs-chest',
+ area: 'gt',
+ room: 0x8C,
+ chest: 3,
+ },
+ {
+ id: 'gt-ice-left',
+ area: 'gt',
+ room: 0x1C,
+ chest: 1,
+ },
+ {
+ id: 'gt-ice-mid',
+ area: 'gt',
+ room: 0x1C,
+ chest: 0,
+ },
+ {
+ id: 'gt-ice-right',
+ area: 'gt',
+ room: 0x1C,
+ chest: 2,
+ },
+ {
+ id: 'gt-big-chest',
+ area: 'gt',
+ room: 0x8C,
+ chest: 0,
+ },
+ {
+ id: 'gt-helma-left',
+ area: 'gt',
+ room: 0x3D,
+ chest: 0,
+ },
+ {
+ id: 'gt-helma-right',
+ area: 'gt',
+ room: 0x3D,
+ chest: 1,
+ },
+ {
+ id: 'gt-pre-moldorm',
+ area: 'gt',
+ room: 0x3D,
+ chest: 2,
+ },
+ {
+ id: 'gt-post-moldorm',
+ area: 'gt',
+ room: 0x4D,
+ chest: 0,
+ },
+ {
+ id: 'hammer-pegs',
+ room: 0x127,
+ chest: 6,
+ },
+ {
+ id: 'hc-boom',
+ area: 'hc',
+ room: 0x71,
+ chest: 0,
+ },
+ {
+ id: 'hc-cell',
+ area: 'hc',
+ room: 0x80,
+ chest: 0,
+ },
+ {
+ id: 'hc-map',
+ area: 'hc',
+ room: 0x72,
+ chest: 0,
+ },
+ {
+ id: 'hookshot-cave-br',
+ room: 0x3C,
+ chest: 3,
+ },
+ {
+ id: 'hookshot-cave-tr',
+ room: 0x3C,
+ chest: 0,
+ },
+ {
+ id: 'hookshot-cave-tl',
+ room: 0x3C,
+ chest: 1,
+ },
+ {
+ id: 'hookshot-cave-bl',
+ room: 0x3C,
+ chest: 2,
+ },
+ {
+ id: 'hype-cave-top',
+ room: 0x11E,
+ chest: 0,
+ },
+ {
+ id: 'hype-cave-left',
+ room: 0x11E,
+ chest: 1,
+ },
+ {
+ id: 'hype-cave-right',
+ room: 0x11E,
+ chest: 2,
+ },
+ {
+ id: 'hype-cave-bottom',
+ room: 0x11E,
+ chest: 4,
+ },
+ {
+ id: 'hype-cave-npc',
+ room: 0x11E,
+ chest: 6,
+ },
+ {
+ id: 'ice-rod-cave',
+ room: 0x120,
+ chest: 0,
+ },
+ {
+ id: 'ip-compass-chest',
+ area: 'ip',
+ room: 0x2E,
+ chest: 0,
+ },
+ {
+ id: 'ip-big-key-chest',
+ area: 'ip',
+ room: 0x1F,
+ chest: 0,
+ },
+ {
+ id: 'ip-map-chest',
+ area: 'ip',
+ room: 0x3F,
+ chest: 0,
+ },
+ {
+ id: 'ip-spike-chest',
+ area: 'ip',
+ room: 0x5F,
+ chest: 0,
+ },
+ {
+ id: 'ip-freezor-chest',
+ area: 'ip',
+ room: 0x7E,
+ chest: 0,
+ },
+ {
+ id: 'ip-big-chest',
+ area: 'ip',
+ room: 0x9E,
+ chest: 0,
+ },
+ {
+ id: 'ip-ice-t',
+ area: 'ip',
+ room: 0xAE,
+ chest: 0,
+ },
+ {
+ id: 'kak-well-top',
+ room: 0x2F,
+ chest: 0,
+ },
+ {
+ id: 'kak-well-left',
+ room: 0x2F,
+ chest: 1,
+ },
+ {
+ id: 'kak-well-mid',
+ room: 0x2F,
+ chest: 2,
+ },
+ {
+ id: 'kak-well-right',
+ room: 0x2F,
+ chest: 3,
+ },
+ {
+ id: 'kak-well-bottom',
+ room: 0x2F,
+ chest: 4,
+ },
+ {
+ id: 'kings-tomb',
+ room: 0x113,
+ chest: 0,
+ },
+ {
+ id: 'links-house',
+ room: 0x104,
+ chest: 0,
+ },
+ {
+ id: 'lost-woods-hideout',
+ room: 0xE1,
+ chest: 5,
+ },
+ {
+ id: 'lumberjack',
+ room: 0xE2,
+ chest: 5,
+ },
+ {
+ id: 'mimic-cave',
+ room: 0x10C,
+ chest: 0,
+ },
+ {
+ id: 'mini-moldorm-far-left',
+ room: 0x123,
+ chest: 0,
+ },
+ {
+ id: 'mini-moldorm-left',
+ room: 0x123,
+ chest: 1,
+ },
+ {
+ id: 'mini-moldorm-right',
+ room: 0x123,
+ chest: 2,
+ },
+ {
+ id: 'mini-moldorm-far-right',
+ room: 0x123,
+ chest: 3,
+ },
+ {
+ id: 'mini-moldorm-npc',
+ room: 0x123,
+ chest: 6,
+ },
+ {
+ id: 'mm-bridge-chest',
+ room: 0xA2,
+ chest: 0,
+ },
+ {
+ id: 'mm-spike-chest',
+ room: 0xB3,
+ chest: 0,
+ },
+ {
+ id: 'mm-lobby-chest',
+ room: 0xC2,
+ chest: 0,
+ },
+ {
+ id: 'mm-compass-chest',
+ room: 0xC1,
+ chest: 0,
+ },
+ {
+ id: 'mm-big-key-chest',
+ room: 0xD1,
+ chest: 0,
+ },
+ {
+ id: 'mm-big-chest',
+ room: 0xC3,
+ chest: 0,
+ },
+ {
+ id: 'mm-map-chest',
+ room: 0xC3,
+ chest: 1,
+ },
+ {
+ id: 'mire-shed-left',
+ room: 0x10D,
+ chest: 0,
+ },
+ {
+ id: 'mire-shed-right',
+ room: 0x10D,
+ chest: 1,
+ },
+ {
+ id: 'paradox-lower-far-left',
+ room: 0xEF,
+ chest: 0,
+ },
+ {
+ id: 'paradox-lower-left',
+ room: 0xEF,
+ chest: 1,
+ },
+ {
+ id: 'paradox-lower-right',
+ room: 0xEF,
+ chest: 2,
+ },
+ {
+ id: 'paradox-lower-far-right',
+ room: 0xEF,
+ chest: 4,
+ },
+ {
+ id: 'paradox-lower-mid',
+ room: 0xEF,
+ chest: 5,
+ },
+ {
+ id: 'paradox-upper-left',
+ room: 0xFF,
+ chest: 0,
+ },
+ {
+ id: 'paradox-upper-right',
+ room: 0xFF,
+ chest: 1,
+ },
+ {
+ id: 'pd-shooter-room',
+ room: 0x09,
+ chest: 0,
+ },
+ {
+ id: 'pd-stalfos-basement',
+ room: 0x0A,
+ chest: 0,
+ },
+ {
+ id: 'pd-big-key-chest',
+ room: 0x3A,
+ chest: 0,
+ },
+ {
+ id: 'pd-arena-bridge',
+ room: 0x2A,
+ chest: 1,
+ },
+ {
+ id: 'pd-arena-ledge',
+ room: 0x2A,
+ chest: 0,
+ },
+ {
+ id: 'pd-map-chest',
+ room: 0x2B,
+ chest: 0,
+ },
+ {
+ id: 'pd-big-chest',
+ room: 0x1A,
+ chest: 0,
+ },
+ {
+ id: 'pd-compass-chest',
+ room: 0x1A,
+ chest: 1,
+ },
+ {
+ id: 'pd-harmless-hellway',
+ room: 0x1A,
+ chest: 2,
+ },
+ {
+ id: 'pd-maze-top',
+ room: 0x19,
+ chest: 0,
+ },
+ {
+ id: 'pd-maze-bottom',
+ room: 0x19,
+ chest: 1,
+ },
+ {
+ id: 'pd-basement-left',
+ room: 0x6A,
+ chest: 0,
+ },
+ {
+ id: 'pd-basement-right',
+ room: 0x6A,
+ chest: 1,
+ },
+ {
+ id: 'pyramid-fairy-left',
+ room: 0x116,
+ chest: 0,
+ },
+ {
+ id: 'pyramid-fairy-right',
+ room: 0x116,
+ chest: 1,
+ },
+ {
+ id: 'saha-left',
+ room: 0x105,
+ chest: 0,
+ },
+ {
+ id: 'saha-mid',
+ room: 0x105,
+ chest: 1,
+ },
+ {
+ id: 'saha-right',
+ room: 0x105,
+ chest: 2,
+ },
+ {
+ id: 'sanc',
+ area: 'hc',
+ room: 0x12,
+ chest: 0,
+ },
+ {
+ id: 'secret-passage',
+ room: 0x55,
+ chest: 0,
+ },
+ {
+ id: 'sewers-left',
+ area: 'hc',
+ room: 0x11,
+ chest: 0,
+ },
+ {
+ id: 'sewers-mid',
+ area: 'hc',
+ room: 0x11,
+ chest: 1,
+ },
+ {
+ id: 'sewers-right',
+ area: 'hc',
+ room: 0x11,
+ chest: 2,
+ },
+ {
+ id: 'sp-lobby',
+ area: 'sp',
+ room: 0x28,
+ chest: 0,
+ },
+ {
+ id: 'sp-map-chest',
+ area: 'sp',
+ room: 0x37,
+ chest: 0,
+ },
+ {
+ id: 'sp-big-chest',
+ area: 'sp',
+ room: 0x36,
+ chest: 0,
+ },
+ {
+ id: 'sp-compass-chest',
+ area: 'sp',
+ room: 0x46,
+ chest: 0,
+ },
+ {
+ id: 'sp-west-chest',
+ area: 'sp',
+ room: 0x34,
+ chest: 0,
+ },
+ {
+ id: 'sp-big-key-chest',
+ area: 'sp',
+ room: 0x35,
+ chest: 0,
+ },
+ {
+ id: 'sp-flooded-left',
+ area: 'sp',
+ room: 0x76,
+ chest: 0,
+ },
+ {
+ id: 'sp-flooded-right',
+ area: 'sp',
+ room: 0x76,
+ chest: 1,
+ },
+ {
+ id: 'sp-waterfall',
+ area: 'sp',
+ room: 0x66,
+ chest: 0,
+ },
+ {
+ id: 'spec-rock-cave',
+ room: 0xEA,
+ chest: 6,
+ },
+ {
+ id: 'spike-cave',
+ room: 0x117,
+ chest: 0,
+ },
+ {
+ id: 'spiral-cave',
+ room: 0xFE,
+ chest: 0,
+ },
+ {
+ id: 'super-bunny-top',
+ room: 0xF8,
+ chest: 0,
+ },
+ {
+ id: 'super-bunny-bottom',
+ room: 0xF8,
+ chest: 0,
+ },
+ {
+ id: 'sw-big-chest',
+ area: 'sw',
+ room: 0x58,
+ chest: 0,
+ },
+ {
+ id: 'sw-map-chest',
+ area: 'sw',
+ room: 0x58,
+ chest: 1,
+ },
+ {
+ id: 'sw-compass-chest',
+ area: 'sw',
+ room: 0x67,
+ chest: 0,
+ },
+ {
+ id: 'sw-big-key-chest',
+ area: 'sw',
+ room: 0x57,
+ chest: 0,
+ },
+ {
+ id: 'sw-pot-prison',
+ area: 'sw',
+ room: 0x57,
+ chest: 1,
+ },
+ {
+ id: 'sw-pinball-room',
+ area: 'sw',
+ room: 0x68,
+ chest: 0,
+ },
+ {
+ id: 'sw-bridge-chest',
+ area: 'sw',
+ room: 0x59,
+ chest: 0,
+ },
+ {
+ id: 'tavern',
+ room: 0x103,
+ chest: 0,
+ },
+ {
+ id: 'th-basement-cage',
+ area: 'th',
+ room: 0x87,
+ chest: 6,
+ },
+ {
+ id: 'th-big-key-chest',
+ area: 'th',
+ room: 0x87,
+ chest: 0,
+ },
+ {
+ id: 'th-map-chest',
+ area: 'th',
+ room: 0x77,
+ chest: 0,
+ },
+ {
+ id: 'th-big-chest',
+ area: 'th',
+ room: 0x27,
+ chest: 0,
+ },
+ {
+ id: 'th-compass-chest',
+ area: 'th',
+ room: 0x27,
+ chest: 1,
+ },
+ {
+ id: 'tr-roller-left',
+ area: 'tr',
+ room: 0xB7,
+ chest: 0,
+ },
+ {
+ id: 'tr-roller-right',
+ area: 'tr',
+ room: 0xB7,
+ chest: 1,
+ },
+ {
+ id: 'tr-compass-chest',
+ area: 'tr',
+ room: 0xD6,
+ chest: 0,
+ },
+ {
+ id: 'tr-chomps',
+ area: 'tr',
+ room: 0xB6,
+ chest: 0,
+ },
+ {
+ id: 'tr-big-key-chest',
+ area: 'tr',
+ room: 0x14,
+ chest: 0,
+ },
+ {
+ id: 'tr-big-chest',
+ area: 'tr',
+ room: 0x24,
+ chest: 0,
+ },
+ {
+ id: 'tr-crysta-roller',
+ area: 'tr',
+ room: 0x04,
+ chest: 0,
+ },
+ {
+ id: 'tr-laser-bridge-top',
+ area: 'tr',
+ room: 0xD5,
+ chest: 0,
+ },
+ {
+ id: 'tr-laser-bridge-left',
+ area: 'tr',
+ room: 0xD5,
+ chest: 1,
+ },
+ {
+ id: 'tr-laser-bridge-right',
+ area: 'tr',
+ room: 0xD5,
+ chest: 2,
+ },
+ {
+ id: 'tr-laser-bridge-bottom',
+ area: 'tr',
+ room: 0xD5,
+ chest: 3,
+ },
+ {
+ id: 'tt-map-chest',
+ area: 'tt',
+ room: 0xDB,
+ chest: 0,
+ },
+ {
+ id: 'tt-big-key-chest',
+ area: 'tt',
+ room: 0xDB,
+ chest: 1,
+ },
+ {
+ id: 'tt-ambush-chest',
+ area: 'tt',
+ room: 0xCB,
+ chest: 0,
+ },
+ {
+ id: 'tt-compass-chest',
+ area: 'tt',
+ room: 0xDC,
+ chest: 0,
+ },
+ {
+ id: 'tt-attic',
+ area: 'tt',
+ room: 0x65,
+ chest: 0,
+ },
+ {
+ id: 'tt-cell',
+ area: 'tt',
+ room: 0x45,
+ chest: 0,
+ },
+ {
+ id: 'tt-big-chest',
+ area: 'tt',
+ room: 0x44,
+ chest: 0,
+ },
+ {
+ id: 'waterfall-fairy-left',
+ room: 0x114,
+ chest: 4,
+ },
+ {
+ id: 'waterfall-fairy-right',
+ room: 0x114,
+ chest: 5,
+ },
+];
+
+export const toggleBoolean = name => state => ({
+ ...state,
+ [name]: !state[name],
+});
+
+export const increment = (name, max, skipZero) => state => {
+ let newValue = ((state[name] || 0) + 1) % (max + 1);
+ if (skipZero && !newValue) {
+ newValue = 1;
+ }
+ return {
+ ...state,
+ [name]: newValue,
+ };
+};
+
+export const decrement = (name, max, skipZero) => state => {
+ let newValue = ((state[name] || 0) + max) % (max + 1);
+ if (skipZero && !newValue) {
+ newValue = max;
+ }
+ return {
+ ...state,
+ [name]: newValue,
+ };
+};
+
+export const highestActive = (state, names) => {
+ for (let i = names.length; i >= 0; --i) {
+ if (state[names[i]]) {
+ return names[i];
+ }
+ }
+ return null;
+};
+
+export const hasDungeonBoss = (state, dungeon) => !!state[`${dungeon.id}-boss-defeated`];
+
+export const getDungeonBoss = (state, dungeon) =>
+ state[`${dungeon.id}-boss`] || dungeon.boss || null;
+
+export const hasDungeonPrize = (state, dungeon) => !!state[`${dungeon.id}-prize-acquired`];
+
+export const getDungeonPrize = (state, dungeon) => state[`${dungeon.id}-prize`] || null;
+
+export const makeEmptyState = () => {
+ const state = {};
+ BOOLEAN_STATES.forEach(p => {
+ state[p] = INITIAL[p] || false;
+ });
+ INTEGER_STATES.forEach(p => {
+ state[p] = INITIAL[p] || 0;
+ });
+ DUNGEONS.forEach(dungeon => {
+ state[`${dungeon.id}-map`] = false;
+ state[`${dungeon.id}-compass`] = false;
+ state[`${dungeon.id}-small-key`] = 0;
+ state[`${dungeon.id}-big-key`] = false;
+ state[`${dungeon.id}-checks`] = 0;
+ if (dungeon.boss) {
+ state[`${dungeon.id}-boss`] = dungeon.boss;
+ state[`${dungeon.id}-boss-defeated`] = false;
+ }
+ if (dungeon.prize) {
+ state[`${dungeon.id}-prize`] = 'crystal';
+ state[`${dungeon.id}-prize-acquired`] = false;
+ }
+ });
+ return state;
+};
+
+const collectInventory = (state, data, prizeMap) => {
+ state.bow = !!(data[INV_ADDR.RANDO_BOW] & 0x80);
+ state.silvers = (data[INV_ADDR.RANDO_BOW] & 0xC0) == 0xC0;
+ state['bowless-silvers'] = (data[INV_ADDR.RANDO_BOW] & 0xC0) == 0x40;
+ state['blue-boomerang'] = !!(data[INV_ADDR.RANDO_BOOM] & 0x40);
+ state['red-boomerang'] = !!(data[INV_ADDR.RANDO_BOOM] & 0x80);
+ state.hookshot = !!data[INV_ADDR.HOOK];
+ state.bomb = data[INV_ADDR.BOMB];
+ state.mushroom = !!(data[INV_ADDR.RANDO_POWDER] & 0x20);
+ state.powder = !!(data[INV_ADDR.RANDO_POWDER] & 0x10);
+ state['fire-rod'] = !!data[INV_ADDR.FROD];
+ state['ice-rod'] = !!data[INV_ADDR.IROD];
+ state.bombos = !!data[INV_ADDR.BOMBOS];
+ state.ether = !!data[INV_ADDR.ETHER];
+ state.quake = !!data[INV_ADDR.QUAKE];
+ state.lamp = !!data[INV_ADDR.LAMP];
+ state.hammer = !!data[INV_ADDR.HAMMER];
+ state.shovel = !!(data[INV_ADDR.RANDO_FLUTE] & 0x04);
+ state.flute = !!(data[INV_ADDR.RANDO_FLUTE] & 0x03);
+ 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.somaria = !!data[INV_ADDR.SOMARIA];
+ state.byrna = !!data[INV_ADDR.BYRNA];
+ state.cape = !!data[INV_ADDR.CAPE];
+ state.mirror = !!data[INV_ADDR.MIRROR];
+ state.lift = data[INV_ADDR.GLOVE];
+ state.boots = !!data[INV_ADDR.BOOTS];
+ state.flippers = !!data[INV_ADDR.FLIPPERS];
+ state.moonpearl = !!data[INV_ADDR.MOONPEARL];
+ state.sword = data[INV_ADDR.SWORD];
+ state.shield = data[INV_ADDR.SHIELD];
+ state.mail = data[INV_ADDR.ARMOR] + 1;
+ state['heart-piece'] = data[INV_ADDR.HEART_PIECE];
+ state['half-magic'] = data[INV_ADDR.MAGIC_USE] > 0;
+ state['quarter-magic'] = data[INV_ADDR.MAGIC_USE] > 1;
+ const map = getShort(data, INV_ADDR.MAP);
+ const compass = getShort(data, INV_ADDR.COMPASS);
+ const bigKey = getShort(data, INV_ADDR.BIG_KEY);
+ DUNGEONS.forEach(dungeon => {
+ state[`${dungeon.id}-map`] = !!(map & dungeon.mask);
+ 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);
+ 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 collectOverworld = (state, data) => {
+ OVERWORLD_LOCATIONS.forEach(location => {
+ state[location.id] = !!(data[location.address] & location.mask);
+ });
+};
+
+const collectUnderworld = (state, data) => {
+ UNDERWORLD_LOCATIONS.forEach(location => {
+ state[location.id] = isChestOpen(data, location.room, location.chest);
+ });
+ DUNGEONS.forEach(dungeon => {
+ state[`${dungeon.id}-boss-defeated`] = isBossDefeated(data, dungeon.bossRoom);
+ });
+};
+
+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.wildMap && state[`${dungeon.id}-map`]) {
+ --amount;
+ --total;
+ }
+ if (!config.wildCompass && state[`${dungeon.id}-compass`]) {
+ --amount;
+ --total;
+ }
+ if (!config.wildSmall) {
+ amount -= Math.min(state[`${dungeon.id}-small-key`], dungeon.sk);
+ total -= dungeon.sk;
+ }
+ if (!config.wildBig && !dungeon.dropBk && state[`${dungeon.id}-big-key`]) {
+ --amount;
+ --total;
+ }
+ amounts[dungeon.id] = Math.min(total, amount);
+ });
+ return amounts;
+};
+
+export const mergeStates = (config, cur, inc) => {
+ const next = { ...cur, ...inc };
+ const amounts = getDungeonAmounts(config, inc);
+ DUNGEONS.forEach(dungeon => {
+ next[`${dungeon.id}-checks`] = amounts[dungeon.id];
+ });
+ //console.log(next);
+ return next;
+};
--- /dev/null
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import { CONFIG, DUNGEONS, makeEmptyState } from '../helpers/tracker';
+
+const context = React.createContext({});
+
+export const useTracker = () => React.useContext(context);
+
+export const TrackerProvider = ({ children }) => {
+ const [config, setConfig] = React.useState(CONFIG);
+ const [state, setState] = React.useState(makeEmptyState());
+ const [dungeons, setDungeons] = React.useState(DUNGEONS);
+
+ React.useEffect(() => {
+ const newDungeons = 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 (!config.bossShuffle && dungeon.boss) {
+ newDungeon.bosses = [dungeon.boss];
+ }
+ return newDungeon;
+ });
+ setDungeons(newDungeons);
+ }, [config]);
+
+ const value = React.useMemo(() => {
+ return { config, setConfig, dungeons, setState, state };
+ }, [config, dungeons, state]);
+
+ return <context.Provider value={value}>
+ {children}
+ </context.Provider>;
+};
+
+TrackerProvider.propTypes = {
+ children: PropTypes.node,
+};
'bottle-bee': 'Bee in a Bottle',
bottle: 'Bottle',
bow: 'Bow',
+ 'bowless-silvers': 'Silvers ohne Bow',
bugnet: 'Bugnet',
byrna: 'Cane of Byrna',
cape: 'Cape',
flippers: 'Flippers',
flute: 'Flute',
glove: 'Power Glove',
+ 'gold-sword': 'Gold Sword',
'green-mail': 'Green Mail',
'green-pendant': 'Pendant of Courage',
'green-potion': 'Green Potion',
+ 'half-magic': 'Half Magic',
hammer: 'Hammer',
'heart-container': 'Heart Container',
'heart-piece': 'Heart Piece',
'ice-rod': 'Ice Rod',
lamp: 'Lamp',
map: 'Map',
+ 'master-sword': 'Master Sword',
mirror: 'Mirror',
'mirror-shield': 'Mirror Shield',
mitts: 'Titan \'s Mitts',
'not-moonpearl': 'Keine Moonpearl',
powder: 'Powder',
quake: 'Quake',
+ 'quarter-magic': 'Quarter Magic',
'red-bomb': 'Red Bomb',
'red-boomerang': 'Red Boomerang',
'red-mail': 'Red Mail',
silvers: 'Silvers',
'small-key': 'Small Key',
somaria: 'Cane of Somaria',
+ 'tempered-sword': 'Tempered Sword',
},
},
map: {
'bottle-bee': 'Bee in a Bottle',
bottle: 'Bottle',
bow: 'Bow',
+ 'bowless-silvers': 'Silvers w/o Bow',
bugnet: 'Bugnet',
byrna: 'Cane of Byrna',
cape: 'Cape',
flippers: 'Flippers',
flute: 'Flute',
glove: 'Power Glove',
+ 'gold-sword': 'Gold Sword',
'green-mail': 'Green Mail',
'green-pendant': 'Pendant of Courage',
'green-potion': 'Green Potion',
+ 'half-magic': 'Half Magic',
hammer: 'Hammer',
'heart-container': 'Heart Container',
'heart-piece': 'Heart Piece',
'ice-rod': 'Ice Rod',
lamp: 'Lamp',
map: 'Map',
+ 'master-sword': 'Master Sword',
mirror: 'Mirror',
'mirror-shield': 'Mirror Shield',
mitts: 'Titan \'s Mitts',
'not-moonpearl': 'No Moonpearl',
powder: 'Powder',
quake: 'Quake',
+ 'quarter-magic': 'Quarter Magic',
'red-bomb': 'Red Bomb',
'red-boomerang': 'Red Boomerang',
'red-mail': 'Red Mail',
silvers: 'Silvers',
'small-key': 'Small Key',
somaria: 'Cane of Somaria',
+ 'tempered-sword': 'Tempered Sword',
},
},
map: {
--- /dev/null
+import React from 'react';
+import { Helmet } from 'react-helmet';
+
+import ErrorBoundary from '../components/common/ErrorBoundary';
+import Tracker from '../components/tracker';
+import { TrackerProvider } from '../hooks/tracker';
+
+export const Component = () => {
+ return <ErrorBoundary>
+ <Helmet>
+ <title>Tracker</title>
+ </Helmet>
+ <TrackerProvider>
+ <Tracker />
+ </TrackerProvider>
+ </ErrorBoundary>;
+};
@import 'rounds';
@import 'techniques';
@import 'tournaments';
+@import 'tracker';
@import 'users';
position: relative;
display: inline-flex;
align-items: center;
+ vertical-align: middle;
width: 2em;
height: 2em;
max-width: 100%;
max-height: 100%;
}
+ .item-map-icon {
+ display: inline-block;
+ width: 100%;
+ height: 100%;
+ background: url(/items-v1.png);
+ background-size: 800% 1100%;
+ }
.strike {
position: absolute;
top: 0;
--- /dev/null
+.tracker {
+ .count-display {
+ background: black;
+ font-weight: bold;
+ text-align: center;
+ }
+ .dungeon {
+ display: flex;
+ flex-direction: row;
+ align-items: stretch;
+ justify-content: flex-start;
+ gap: 1ex;
+ > * {
+ width: 2em;
+ height: 2em;
+ }
+ .dungeon-smalls .count-display,
+ .dungeon-tag {
+ background: black;
+ font-family: monospace;
+ font-size: 115%;
+ font-weight: bold;
+ text-align: center;
+ }
+ .dungeon-checks,
+ .dungeon-smalls {
+ position: relative;
+ .count-display {
+ pointer-events: none;
+ position: absolute;
+ top: .3ex;
+ left: .3ex;
+ bottom: .3ex;
+ right: .3ex;
+ &.is-zero {
+ display: none;
+ }
+ }
+ }
+ }
+ .dungeon-ep,
+ .dungeon-pd {
+ margin-top: 1ex;
+ }
+ .equipment {
+ display: grid;
+ grid-template-columns: 3em 3em 3em 3em 3em;
+ gap: 1ex;
+ padding: 1ex;
+ }
+ .items {
+ display: grid;
+ grid-template-columns: 3em 3em 3em 3em 3em;
+ gap: 1ex;
+ padding: 1ex;
+ }
+ .item {
+ position: relative;
+ width: 3em;
+ height: 3em;
+
+ .bottom-left,
+ .bottom-right,
+ .top-left,
+ .top-right {
+ position: absolute;
+ width: 50%;
+ height: 50%;
+ .zelda-icon {
+ transform: scale(1.4);
+ }
+ }
+ .bottom-left {
+ bottom: 0;
+ left: 0;
+ }
+ .bottom-right {
+ bottom: 0;
+ right: 0;
+ }
+ .top-left {
+ top: 0;
+ left: 0;
+ }
+ .top-right {
+ top: 0;
+ right: 0;
+ }
+
+ .left,
+ .right {
+ position: absolute;
+ width: 50%;
+ height: 100%;
+ overflow: hidden;
+ .zelda-icon {
+ width: 200%;
+ margin-left: -50%;
+ }
+ }
+ .left {
+ left: 0;
+ }
+ .right {
+ right: 0;
+ }
+ .count-display {
+ pointer-events: none;
+ &.is-zero {
+ display: none;
+ }
+ }
+ }
+ .toggle-icon {
+ &.inactive {
+ opacity: .5;
+ }
+ }
+ .tracker-toolbar {
+ .toggle-icon {
+ display: inline-block;
+ width: 2em;
+ height: 2em;
+ }
+ }
+ .zelda-icon {
+ width: 100%;
+ height: 100%;
+ }
+}