]> git.localhorst.tv Git - alttp.git/commitdiff
restructure map for future
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Thu, 28 Mar 2024 12:35:36 +0000 (13:35 +0100)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Thu, 28 Mar 2024 12:35:36 +0000 (13:35 +0100)
resources/js/components/tracker/Map.js [deleted file]
resources/js/components/tracker/Map/Overworld.js [new file with mode: 0644]
resources/js/components/tracker/Map/index.js [new file with mode: 0644]

diff --git a/resources/js/components/tracker/Map.js b/resources/js/components/tracker/Map.js
deleted file mode 100644 (file)
index 3ee4b2f..0000000
+++ /dev/null
@@ -1,820 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import { useTranslation } from 'react-i18next';
-
-import {
-       addDungeonCheck,
-       aggregateDungeonStatus,
-       aggregateLocationStatus,
-       clearAll,
-       completeDungeonChecks,
-       countRemainingLocations,
-       getDungeonClearedItems,
-       getDungeonRemainingItems,
-       hasDungeonBoss,
-       hasDungeonPrize,
-       isDungeonCleared,
-       removeDungeonCheck,
-       resetDungeonChecks,
-       setBossDefeated,
-       setPrizeAcquired,
-       unclearAll,
-} from '../../helpers/tracker';
-import { useTracker } from '../../hooks/tracker';
-
-const LW_DUNGEONS = [
-       {
-               id: 'hc',
-               x: 0.5,
-               y: 0.5,
-       },
-       {
-               id: 'ct',
-               x: 0.5,
-               y: 0.4,
-       },
-       {
-               id: 'ep',
-               x: 0.95,
-               y: 0.42,
-       },
-       {
-               id: 'dp',
-               x: 0.075,
-               y: 0.8,
-       },
-       {
-               id: 'th',
-               x: 0.56,
-               y: 0.05,
-       },
-];
-
-const LW_LOCATIONS = [
-       {
-               id: 'aginah',
-               checks: [
-                       'aginah',
-               ],
-               x: 0.2,
-               y: 0.83,
-       },
-       {
-               id: 'blinds-hut',
-               checks: [
-                       'blinds-hut-top',
-                       'blinds-hut-left',
-                       'blinds-hut-right',
-                       'blinds-hut-far-left',
-                       'blinds-hut-far-right',
-               ],
-               x: 0.14,
-               y: 0.42,
-       },
-       {
-               id: 'bombos-tablet',
-               checks: [
-                       'bombos-tablet',
-               ],
-               x: 0.225,
-               y: 0.925,
-       },
-       {
-               id: 'bonk-rocks',
-               checks: [
-                       'bonk-rocks',
-               ],
-               x: 0.4,
-               y: 0.3,
-       },
-       {
-               id: 'bottle-vendor',
-               checks: [
-                       'bottle-vendor',
-               ],
-               x: 0.1,
-               y: 0.475,
-       },
-       {
-               id: 'cave-45',
-               checks: [
-                       'cave-45',
-               ],
-               x: 0.27,
-               y: 0.83,
-       },
-       {
-               id: 'checkerboard',
-               checks: [
-                       'checkerboard',
-               ],
-               x: 0.18,
-               y: 0.78,
-       },
-       {
-               id: 'chicken-house',
-               checks: [
-                       'chicken-house',
-               ],
-               x: 0.1,
-               y: 0.53,
-       },
-       {
-               id: 'dam',
-               checks: [
-                       'flooded-chest',
-                       'sunken-treasure',
-               ],
-               x: 0.4675,
-               y: 0.9375,
-       },
-       {
-               id: 'desert-ledge',
-               checks: [
-                       'desert-ledge',
-               ],
-               x: 0.025,
-               y: 0.9,
-       },
-       {
-               id: 'ether-tablet',
-               checks: [
-                       'ether-tablet',
-               ],
-               x: 0.425,
-               y: 0.025,
-       },
-       {
-               id: 'floating-island',
-               checks: [
-                       'floating-island',
-               ],
-               x: 0.8,
-               y: 0.025,
-       },
-       {
-               id: 'flute-spot',
-               checks: [
-                       'flute-spot',
-               ],
-               x: 0.3,
-               y: 0.675,
-       },
-       {
-               id: 'graveyard-ledge',
-               checks: [
-                       'graveyard-ledge',
-               ],
-               x: 0.57,
-               y: 0.28,
-       },
-       {
-               id: 'hobo',
-               checks: [
-                       'hobo',
-               ],
-               x: 0.7,
-               y: 0.7,
-       },
-       {
-               id: 'ice-rod-cave',
-               checks: [
-                       'ice-rod-cave',
-               ],
-               x: 0.9,
-               y: 0.76,
-       },
-       {
-               id: 'kak-well',
-               checks: [
-                       'kak-well-top',
-                       'kak-well-left',
-                       'kak-well-mid',
-                       'kak-well-right',
-                       'kak-well-bottom',
-               ],
-               x: 0.04,
-               y: 0.425,
-       },
-       {
-               id: 'kings-tomb',
-               checks: [
-                       'kings-tomb',
-               ],
-               x: 0.62,
-               y: 0.3,
-       },
-       {
-               id: 'lake-hylia-island',
-               checks: [
-                       'lake-hylia-island',
-               ],
-               x: 0.725,
-               y: 0.8375,
-       },
-       {
-               id: 'library',
-               checks: [
-                       'library',
-               ],
-               x: 0.15,
-               y: 0.65,
-       },
-       {
-               id: 'links-house',
-               checks: [
-                       'links-house',
-               ],
-               x: 0.55,
-               y: 0.6875,
-       },
-       {
-               id: 'lost-woods-hideout',
-               checks: [
-                       'lost-woods-hideout',
-               ],
-               x: 0.19,
-               y: 0.14,
-       },
-       {
-               id: 'lumberjack',
-               checks: [
-                       'lumberjack',
-               ],
-               x: 0.3,
-               y: 0.07,
-       },
-       {
-               id: 'magic-bat',
-               checks: [
-                       'magic-bat',
-               ],
-               x: 0.325,
-               y: 0.55,
-       },
-       {
-               id: 'mimic-cave',
-               checks: [
-                       'mimic-cave',
-               ],
-               x: 0.85,
-               y: 0.1,
-       },
-       {
-               id: 'mini-moldorm-cave',
-               checks: [
-                       'mini-moldorm-left',
-                       'mini-moldorm-right',
-                       'mini-moldorm-far-left',
-                       'mini-moldorm-far-right',
-                       'mini-moldorm-npc',
-               ],
-               x: 0.65,
-               y: 0.95,
-       },
-       {
-               id: 'mushroom-spot',
-               checks: [
-                       'mushroom-spot',
-               ],
-               x: 0.125,
-               y: 0.08,
-       },
-       {
-               id: 'old-man',
-               checks: [
-                       'old-man',
-               ],
-               x: 0.405,
-               y: 0.195,
-       },
-       {
-               id: 'paradox-cave',
-               checks: [
-                       'paradox-lower-far-left',
-                       'paradox-lower-left',
-                       'paradox-lower-right',
-                       'paradox-lower-far-right',
-                       'paradox-lower-mid',
-                       'paradox-upper-left',
-                       'paradox-upper-right',
-               ],
-               x: 0.85,
-               y: 0.2,
-       },
-       {
-               id: 'pedestal',
-               checks: [
-                       'pedestal',
-               ],
-               x: 0.03,
-               y: 0.05,
-       },
-       {
-               id: 'potion-shop',
-               checks: [
-                       'potion-shop',
-               ],
-               x: 0.8,
-               y: 0.325,
-       },
-       {
-               id: 'race-game',
-               checks: [
-                       'race-game',
-               ],
-               x: 0.025,
-               y: 0.7,
-       },
-       {
-               id: 'saha',
-               checks: [
-                       'saha',
-               ],
-               x: 0.815,
-               y: 0.465,
-       },
-       {
-               id: 'saha-hut',
-               checks: [
-                       'saha-left',
-                       'saha-mid',
-                       'saha-right',
-               ],
-               x: 0.815,
-               y: 0.42,
-       },
-       {
-               id: 'sick-kid',
-               checks: [
-                       'sick-kid',
-               ],
-               x: 0.155,
-               y: 0.525,
-       },
-       {
-               id: 'uncle',
-               checks: [
-                       'uncle',
-                       'secret-passage',
-               ],
-               x: 0.6,
-               y: 0.4,
-       },
-       {
-               id: 'spec-rock',
-               checks: [
-                       'spec-rock',
-               ],
-               x: 0.48,
-               y: 0.09,
-       },
-       {
-               id: 'spec-rock-cave',
-               checks: [
-                       'spec-rock-cave',
-               ],
-               x: 0.48,
-               y: 0.14,
-       },
-       {
-               id: 'spiral-cave',
-               checks: [
-                       'spiral-cave',
-               ],
-               x: 0.8,
-               y: 0.1,
-       },
-       {
-               id: 'tavern',
-               checks: [
-                       'tavern',
-               ],
-               x: 0.16,
-               y: 0.58,
-       },
-       {
-               id: 'waterfall-fairy',
-               checks: [
-                       'waterfall-fairy-left',
-                       'waterfall-fairy-right',
-               ],
-               x: 0.9,
-               y: 0.15,
-       },
-       {
-               id: 'zora',
-               checks: [
-                       'zora',
-               ],
-               x: 0.975,
-               y: 0.12,
-       },
-       {
-               id: 'zora-ledge',
-               checks: [
-                       'zora-ledge',
-               ],
-               x: 0.975,
-               y: 0.165,
-       },
-];
-
-const DW_DUNGEONS = [
-       {
-               id: 'pd',
-               x: 0.95,
-               y: 0.42,
-       },
-       {
-               id: 'sp',
-               x: 0.4675,
-               y: 0.9375,
-       },
-       {
-               id: 'sw',
-               x: 0.05,
-               y: 0.05,
-       },
-       {
-               id: 'tt',
-               x: 0.125,
-               y: 0.475,
-       },
-       {
-               id: 'ip',
-               x: 0.7975,
-               y: 0.86,
-       },
-       {
-               id: 'mm',
-               x: 0.12,
-               y: 0.82,
-       },
-       {
-               id: 'tr',
-               x: 0.94,
-               y: 0.06,
-       },
-       {
-               id: 'gt',
-               x: 0.56,
-               y: 0.05,
-       },
-];
-
-const DW_LOCATIONS = [
-       {
-               id: 'blacksmith',
-               checks: [
-                       'blacksmith',
-               ],
-               x: 0.15,
-               y: 0.65,
-       },
-       {
-               id: 'brewery',
-               checks: [
-                       'brewery',
-               ],
-               x: 0.1,
-               y: 0.6,
-       },
-       {
-               id: 'bumper-cave',
-               checks: [
-                       'bumper-cave',
-               ],
-               x: 0.325,
-               y: 0.15,
-       },
-       {
-               id: 'c-house',
-               checks: [
-                       'c-house',
-               ],
-               x: 0.2,
-               y: 0.5,
-       },
-       {
-               id: 'catfish',
-               checks: [
-                       'catfish',
-               ],
-               x: 0.9,
-               y: 0.175,
-       },
-       {
-               id: 'chest-game',
-               checks: [
-                       'chest-game',
-               ],
-               x: 0.05,
-               y: 0.45,
-       },
-       {
-               id: 'digging-game',
-               checks: [
-                       'digging-game',
-               ],
-               x: 0.05,
-               y: 0.7,
-       },
-       {
-               id: 'hammer-pegs',
-               checks: [
-                       'hammer-pegs',
-               ],
-               x: 0.3125,
-               y: 0.6,
-       },
-       {
-               id: 'hookshot-cave',
-               checks: [
-                       'hookshot-cave-tl',
-                       'hookshot-cave-tr',
-                       'hookshot-cave-bl',
-               ],
-               x: 0.85,
-               y: 0.02,
-       },
-       {
-               id: 'hookshot-cave-bonk',
-               checks: [
-                       'hookshot-cave-br',
-               ],
-               x: 0.85,
-               y: 0.065,
-       },
-       {
-               id: 'hype-cave',
-               checks: [
-                       'hype-cave-top',
-                       'hype-cave-left',
-                       'hype-cave-right',
-                       'hype-cave-bottom',
-                       'hype-cave-npc',
-               ],
-               x: 0.6,
-               y: 0.75,
-       },
-       {
-               id: 'mire-shed',
-               checks: [
-                       'mire-shed-left',
-                       'mire-shed-right',
-               ],
-               x: 0.04,
-               y: 0.8,
-       },
-       {
-               id: 'purple-chest',
-               checks: [
-                       'purple-chest',
-               ],
-               x: 0.3125,
-               y: 0.525,
-       },
-       {
-               id: 'pyramid',
-               checks: [
-                       'pyramid',
-               ],
-               x: 0.575,
-               y: 0.45,
-       },
-       {
-               id: 'pyramid-fairy',
-               checks: [
-                       'pyramid-fairy-left',
-                       'pyramid-fairy-right',
-               ],
-               x: 0.45,
-               y: 0.5,
-       },
-       {
-               id: 'spike-cave',
-               checks: [
-                       'spike-cave',
-               ],
-               x: 0.575,
-               y: 0.15,
-       },
-       {
-               id: 'stumpy',
-               checks: [
-                       'stumpy',
-               ],
-               x: 0.3125,
-               y: 0.6875,
-       },
-       {
-               id: 'super-bunny',
-               checks: [
-                       'super-bunny-top',
-                       'super-bunny-bottom',
-               ],
-               x: 0.85,
-               y: 0.15,
-       },
-];
-
-const Location = ({ number, l, size }) => {
-       const { t } = useTranslation();
-
-       const classNames = ['location', `status-${l.status}`];
-       if (size) {
-               classNames.push(`size-${size}`);
-       }
-       if (l.handlePrimary) {
-               classNames.push('clickable');
-       }
-
-       return <g
-               className={classNames.join(' ')}
-               onClick={(e) => {
-                       l.handlePrimary();
-                       e.preventDefault();
-                       e.stopPropagation();
-               }}
-               onContextMenu={(e) => {
-                       l.handleSecondary();
-                       e.preventDefault();
-                       e.stopPropagation();
-               }}
-               transform={`translate(${l.x} ${l.y})`}
-       >
-               <title>{t(`tracker.location.${l.id}`)}</title>
-               <rect className="box" x="0" y="0" />
-               {number && l.remaining ?
-                       <text className="text" x="0" y="0">{l.remaining}</text>
-               : null}
-       </g>;
-};
-
-Location.propTypes = {
-       number: PropTypes.bool,
-       l: PropTypes.shape({
-               id: PropTypes.string,
-               x: PropTypes.number,
-               y: PropTypes.number,
-               number: PropTypes.number,
-               remaining: PropTypes.number,
-               status: PropTypes.string,
-               handlePrimary: PropTypes.func,
-               handleSecondary: PropTypes.func,
-       }),
-       size: PropTypes.string,
-};
-
-const makeBackground = (src, level) => {
-       const amount = Math.pow(2, Math.max(0, level - 8));
-       const size = 1 / amount;
-       const tiles = [];
-       for (let y = 0; y < amount; ++y) {
-               for (let x = 0; x < amount; ++x) {
-                       tiles.push(<image
-                               key={`${x}-${y}`}
-                               x={x * size}
-                               y={y * size}
-                               width={size * 1.002}
-                               height={size * 1.002}
-                               href={`/media/alttp/map/${src}/${level}/${x}_${y}.png`}
-                       />);
-               }
-       }
-       return tiles;
-};
-
-const Map = () => {
-       const { dungeons, logic, setManualState, state } = useTracker();
-
-       const mapDungeon = React.useCallback(dungeon => {
-               const definition = dungeons.find(d => d.id === dungeon.id);
-               const remaining = getDungeonRemainingItems(state, definition);
-               const status = aggregateDungeonStatus(definition, logic, state);
-               return {
-                       ...dungeon,
-                       status,
-                       remaining,
-                       handlePrimary: () => {
-                               if (getDungeonRemainingItems(state, definition)) {
-                                       setManualState(addDungeonCheck(definition));
-                               } else if (
-                                       !hasDungeonBoss(state, definition) || !hasDungeonPrize(state, definition)
-                               ) {
-                                       if (definition.boss) {
-                                               setManualState(setBossDefeated(definition, true));
-                                       }
-                                       if (definition.prize) {
-                                               setManualState(setPrizeAcquired(definition, true));
-                                       }
-                               } else {
-                                       setManualState(resetDungeonChecks(definition));
-                                       if (definition.boss) {
-                                               setManualState(setBossDefeated(definition, false));
-                                       }
-                                       if (definition.prize) {
-                                               setManualState(setPrizeAcquired(definition, false));
-                                       }
-                               }
-                       },
-                       handleSecondary: () => {
-                               if (isDungeonCleared(state, definition)) {
-                                       if (definition.items) {
-                                               setManualState(removeDungeonCheck(definition));
-                                       }
-                                       if (definition.boss) {
-                                               setManualState(setBossDefeated(definition, false));
-                                       }
-                                       if (definition.prize) {
-                                               setManualState(setPrizeAcquired(definition, false));
-                                       }
-                               } else if (getDungeonClearedItems(state, definition)) {
-                                       setManualState(removeDungeonCheck(definition));
-                               } else {
-                                       setManualState(completeDungeonChecks(definition));
-                                       if (definition.boss) {
-                                               setManualState(setBossDefeated(definition, true));
-                                       }
-                                       if (definition.prize) {
-                                               setManualState(setPrizeAcquired(definition, true));
-                                       }
-                               }
-                       },
-               };
-       }, [dungeons, logic, setManualState, state]);
-
-       const mapLocation = React.useCallback(loc => {
-               const remaining = countRemainingLocations(state, loc.checks);
-               const status = aggregateLocationStatus(loc.checks, logic, state);
-               return {
-                       ...loc,
-                       remaining,
-                       status,
-                       handlePrimary: () => {
-                               if (remaining) {
-                                       setManualState(clearAll(loc.checks));
-                               } else {
-                                       setManualState(unclearAll(loc.checks));
-                               }
-                       },
-                       handleSecondary: () => {
-                               if (remaining) {
-                                       setManualState(clearAll(loc.checks));
-                               } else {
-                                       setManualState(unclearAll(loc.checks));
-                               }
-                       },
-               };
-       }, [logic, setManualState, state]);
-
-       const lwDungeons = React.useMemo(() => LW_DUNGEONS.map(mapDungeon), [mapDungeon]);
-       const lwLocations = React.useMemo(() => LW_LOCATIONS.map(mapLocation), [mapLocation]);
-
-       const dwDungeons = React.useMemo(() => DW_DUNGEONS.map(mapDungeon), [mapDungeon]);
-       const dwLocations = React.useMemo(() => DW_LOCATIONS.map(mapLocation), [mapLocation]);
-
-       return <div className="tracker-map">
-               <svg
-                       xmlns="http://www.w3.org/2000/svg"
-                       className="canvas"
-                       width="2"
-                       height="1"
-                       viewBox="0 0 2 1"
-                       onContextMenu={(e) => {
-                               e.preventDefault();
-                               e.stopPropagation();
-                       }}
-               >
-                       <g className="light-world">
-                               <g className="background">
-                                       {makeBackground('lw_files', 10)}
-                               </g>
-                               <g className="locations">
-                                       {lwLocations.map(l =>
-                                               <Location key={l.id} l={l} />
-                                       )}
-                                       {lwDungeons.map(l =>
-                                               <Location key={l.id} number l={l} size="lg" />
-                                       )}
-                               </g>
-                       </g>
-                       <g className="dark-world" transform="translate(1 0)">
-                               <g className="background">
-                                       {makeBackground('dw_files', 10)}
-                               </g>
-                               <g className="locations">
-                                       {dwLocations.map(l =>
-                                               <Location key={l.id} l={l} />
-                                       )}
-                                       {dwDungeons.map(l =>
-                                               <Location key={l.id} number l={l} size="lg" />
-                                       )}
-                               </g>
-                       </g>
-               </svg>
-       </div>;
-};
-
-export default Map;
diff --git a/resources/js/components/tracker/Map/Overworld.js b/resources/js/components/tracker/Map/Overworld.js
new file mode 100644 (file)
index 0000000..609dc34
--- /dev/null
@@ -0,0 +1,818 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+
+import {
+       addDungeonCheck,
+       aggregateDungeonStatus,
+       aggregateLocationStatus,
+       clearAll,
+       completeDungeonChecks,
+       countRemainingLocations,
+       getDungeonClearedItems,
+       getDungeonRemainingItems,
+       hasDungeonBoss,
+       hasDungeonPrize,
+       isDungeonCleared,
+       removeDungeonCheck,
+       resetDungeonChecks,
+       setBossDefeated,
+       setPrizeAcquired,
+       unclearAll,
+} from '../../../helpers/tracker';
+import { useTracker } from '../../../hooks/tracker';
+
+const LW_DUNGEONS = [
+       {
+               id: 'hc',
+               x: 0.5,
+               y: 0.5,
+       },
+       {
+               id: 'ct',
+               x: 0.5,
+               y: 0.4,
+       },
+       {
+               id: 'ep',
+               x: 0.95,
+               y: 0.42,
+       },
+       {
+               id: 'dp',
+               x: 0.075,
+               y: 0.8,
+       },
+       {
+               id: 'th',
+               x: 0.56,
+               y: 0.05,
+       },
+];
+
+const LW_LOCATIONS = [
+       {
+               id: 'aginah',
+               checks: [
+                       'aginah',
+               ],
+               x: 0.2,
+               y: 0.83,
+       },
+       {
+               id: 'blinds-hut',
+               checks: [
+                       'blinds-hut-top',
+                       'blinds-hut-left',
+                       'blinds-hut-right',
+                       'blinds-hut-far-left',
+                       'blinds-hut-far-right',
+               ],
+               x: 0.14,
+               y: 0.42,
+       },
+       {
+               id: 'bombos-tablet',
+               checks: [
+                       'bombos-tablet',
+               ],
+               x: 0.225,
+               y: 0.925,
+       },
+       {
+               id: 'bonk-rocks',
+               checks: [
+                       'bonk-rocks',
+               ],
+               x: 0.4,
+               y: 0.3,
+       },
+       {
+               id: 'bottle-vendor',
+               checks: [
+                       'bottle-vendor',
+               ],
+               x: 0.1,
+               y: 0.475,
+       },
+       {
+               id: 'cave-45',
+               checks: [
+                       'cave-45',
+               ],
+               x: 0.27,
+               y: 0.83,
+       },
+       {
+               id: 'checkerboard',
+               checks: [
+                       'checkerboard',
+               ],
+               x: 0.18,
+               y: 0.78,
+       },
+       {
+               id: 'chicken-house',
+               checks: [
+                       'chicken-house',
+               ],
+               x: 0.1,
+               y: 0.53,
+       },
+       {
+               id: 'dam',
+               checks: [
+                       'flooded-chest',
+                       'sunken-treasure',
+               ],
+               x: 0.4675,
+               y: 0.9375,
+       },
+       {
+               id: 'desert-ledge',
+               checks: [
+                       'desert-ledge',
+               ],
+               x: 0.025,
+               y: 0.9,
+       },
+       {
+               id: 'ether-tablet',
+               checks: [
+                       'ether-tablet',
+               ],
+               x: 0.425,
+               y: 0.025,
+       },
+       {
+               id: 'floating-island',
+               checks: [
+                       'floating-island',
+               ],
+               x: 0.8,
+               y: 0.025,
+       },
+       {
+               id: 'flute-spot',
+               checks: [
+                       'flute-spot',
+               ],
+               x: 0.3,
+               y: 0.675,
+       },
+       {
+               id: 'graveyard-ledge',
+               checks: [
+                       'graveyard-ledge',
+               ],
+               x: 0.57,
+               y: 0.28,
+       },
+       {
+               id: 'hobo',
+               checks: [
+                       'hobo',
+               ],
+               x: 0.7,
+               y: 0.7,
+       },
+       {
+               id: 'ice-rod-cave',
+               checks: [
+                       'ice-rod-cave',
+               ],
+               x: 0.9,
+               y: 0.76,
+       },
+       {
+               id: 'kak-well',
+               checks: [
+                       'kak-well-top',
+                       'kak-well-left',
+                       'kak-well-mid',
+                       'kak-well-right',
+                       'kak-well-bottom',
+               ],
+               x: 0.04,
+               y: 0.425,
+       },
+       {
+               id: 'kings-tomb',
+               checks: [
+                       'kings-tomb',
+               ],
+               x: 0.62,
+               y: 0.3,
+       },
+       {
+               id: 'lake-hylia-island',
+               checks: [
+                       'lake-hylia-island',
+               ],
+               x: 0.725,
+               y: 0.8375,
+       },
+       {
+               id: 'library',
+               checks: [
+                       'library',
+               ],
+               x: 0.15,
+               y: 0.65,
+       },
+       {
+               id: 'links-house',
+               checks: [
+                       'links-house',
+               ],
+               x: 0.55,
+               y: 0.6875,
+       },
+       {
+               id: 'lost-woods-hideout',
+               checks: [
+                       'lost-woods-hideout',
+               ],
+               x: 0.19,
+               y: 0.14,
+       },
+       {
+               id: 'lumberjack',
+               checks: [
+                       'lumberjack',
+               ],
+               x: 0.3,
+               y: 0.07,
+       },
+       {
+               id: 'magic-bat',
+               checks: [
+                       'magic-bat',
+               ],
+               x: 0.325,
+               y: 0.55,
+       },
+       {
+               id: 'mimic-cave',
+               checks: [
+                       'mimic-cave',
+               ],
+               x: 0.85,
+               y: 0.1,
+       },
+       {
+               id: 'mini-moldorm-cave',
+               checks: [
+                       'mini-moldorm-left',
+                       'mini-moldorm-right',
+                       'mini-moldorm-far-left',
+                       'mini-moldorm-far-right',
+                       'mini-moldorm-npc',
+               ],
+               x: 0.65,
+               y: 0.95,
+       },
+       {
+               id: 'mushroom-spot',
+               checks: [
+                       'mushroom-spot',
+               ],
+               x: 0.125,
+               y: 0.08,
+       },
+       {
+               id: 'old-man',
+               checks: [
+                       'old-man',
+               ],
+               x: 0.405,
+               y: 0.195,
+       },
+       {
+               id: 'paradox-cave',
+               checks: [
+                       'paradox-lower-far-left',
+                       'paradox-lower-left',
+                       'paradox-lower-right',
+                       'paradox-lower-far-right',
+                       'paradox-lower-mid',
+                       'paradox-upper-left',
+                       'paradox-upper-right',
+               ],
+               x: 0.85,
+               y: 0.2,
+       },
+       {
+               id: 'pedestal',
+               checks: [
+                       'pedestal',
+               ],
+               x: 0.03,
+               y: 0.05,
+       },
+       {
+               id: 'potion-shop',
+               checks: [
+                       'potion-shop',
+               ],
+               x: 0.8,
+               y: 0.325,
+       },
+       {
+               id: 'race-game',
+               checks: [
+                       'race-game',
+               ],
+               x: 0.025,
+               y: 0.7,
+       },
+       {
+               id: 'saha',
+               checks: [
+                       'saha',
+               ],
+               x: 0.815,
+               y: 0.465,
+       },
+       {
+               id: 'saha-hut',
+               checks: [
+                       'saha-left',
+                       'saha-mid',
+                       'saha-right',
+               ],
+               x: 0.815,
+               y: 0.42,
+       },
+       {
+               id: 'sick-kid',
+               checks: [
+                       'sick-kid',
+               ],
+               x: 0.155,
+               y: 0.525,
+       },
+       {
+               id: 'uncle',
+               checks: [
+                       'uncle',
+                       'secret-passage',
+               ],
+               x: 0.6,
+               y: 0.4,
+       },
+       {
+               id: 'spec-rock',
+               checks: [
+                       'spec-rock',
+               ],
+               x: 0.48,
+               y: 0.09,
+       },
+       {
+               id: 'spec-rock-cave',
+               checks: [
+                       'spec-rock-cave',
+               ],
+               x: 0.48,
+               y: 0.14,
+       },
+       {
+               id: 'spiral-cave',
+               checks: [
+                       'spiral-cave',
+               ],
+               x: 0.8,
+               y: 0.1,
+       },
+       {
+               id: 'tavern',
+               checks: [
+                       'tavern',
+               ],
+               x: 0.16,
+               y: 0.58,
+       },
+       {
+               id: 'waterfall-fairy',
+               checks: [
+                       'waterfall-fairy-left',
+                       'waterfall-fairy-right',
+               ],
+               x: 0.9,
+               y: 0.15,
+       },
+       {
+               id: 'zora',
+               checks: [
+                       'zora',
+               ],
+               x: 0.975,
+               y: 0.12,
+       },
+       {
+               id: 'zora-ledge',
+               checks: [
+                       'zora-ledge',
+               ],
+               x: 0.975,
+               y: 0.165,
+       },
+];
+
+const DW_DUNGEONS = [
+       {
+               id: 'pd',
+               x: 0.95,
+               y: 0.42,
+       },
+       {
+               id: 'sp',
+               x: 0.4675,
+               y: 0.9375,
+       },
+       {
+               id: 'sw',
+               x: 0.05,
+               y: 0.05,
+       },
+       {
+               id: 'tt',
+               x: 0.125,
+               y: 0.475,
+       },
+       {
+               id: 'ip',
+               x: 0.7975,
+               y: 0.86,
+       },
+       {
+               id: 'mm',
+               x: 0.12,
+               y: 0.82,
+       },
+       {
+               id: 'tr',
+               x: 0.94,
+               y: 0.06,
+       },
+       {
+               id: 'gt',
+               x: 0.56,
+               y: 0.05,
+       },
+];
+
+const DW_LOCATIONS = [
+       {
+               id: 'blacksmith',
+               checks: [
+                       'blacksmith',
+               ],
+               x: 0.15,
+               y: 0.65,
+       },
+       {
+               id: 'brewery',
+               checks: [
+                       'brewery',
+               ],
+               x: 0.1,
+               y: 0.6,
+       },
+       {
+               id: 'bumper-cave',
+               checks: [
+                       'bumper-cave',
+               ],
+               x: 0.325,
+               y: 0.15,
+       },
+       {
+               id: 'c-house',
+               checks: [
+                       'c-house',
+               ],
+               x: 0.2,
+               y: 0.5,
+       },
+       {
+               id: 'catfish',
+               checks: [
+                       'catfish',
+               ],
+               x: 0.9,
+               y: 0.175,
+       },
+       {
+               id: 'chest-game',
+               checks: [
+                       'chest-game',
+               ],
+               x: 0.05,
+               y: 0.45,
+       },
+       {
+               id: 'digging-game',
+               checks: [
+                       'digging-game',
+               ],
+               x: 0.05,
+               y: 0.7,
+       },
+       {
+               id: 'hammer-pegs',
+               checks: [
+                       'hammer-pegs',
+               ],
+               x: 0.3125,
+               y: 0.6,
+       },
+       {
+               id: 'hookshot-cave',
+               checks: [
+                       'hookshot-cave-tl',
+                       'hookshot-cave-tr',
+                       'hookshot-cave-bl',
+               ],
+               x: 0.85,
+               y: 0.02,
+       },
+       {
+               id: 'hookshot-cave-bonk',
+               checks: [
+                       'hookshot-cave-br',
+               ],
+               x: 0.85,
+               y: 0.065,
+       },
+       {
+               id: 'hype-cave',
+               checks: [
+                       'hype-cave-top',
+                       'hype-cave-left',
+                       'hype-cave-right',
+                       'hype-cave-bottom',
+                       'hype-cave-npc',
+               ],
+               x: 0.6,
+               y: 0.75,
+       },
+       {
+               id: 'mire-shed',
+               checks: [
+                       'mire-shed-left',
+                       'mire-shed-right',
+               ],
+               x: 0.04,
+               y: 0.8,
+       },
+       {
+               id: 'purple-chest',
+               checks: [
+                       'purple-chest',
+               ],
+               x: 0.3125,
+               y: 0.525,
+       },
+       {
+               id: 'pyramid',
+               checks: [
+                       'pyramid',
+               ],
+               x: 0.575,
+               y: 0.45,
+       },
+       {
+               id: 'pyramid-fairy',
+               checks: [
+                       'pyramid-fairy-left',
+                       'pyramid-fairy-right',
+               ],
+               x: 0.45,
+               y: 0.5,
+       },
+       {
+               id: 'spike-cave',
+               checks: [
+                       'spike-cave',
+               ],
+               x: 0.575,
+               y: 0.15,
+       },
+       {
+               id: 'stumpy',
+               checks: [
+                       'stumpy',
+               ],
+               x: 0.3125,
+               y: 0.6875,
+       },
+       {
+               id: 'super-bunny',
+               checks: [
+                       'super-bunny-top',
+                       'super-bunny-bottom',
+               ],
+               x: 0.85,
+               y: 0.15,
+       },
+];
+
+const Location = ({ number, l, size }) => {
+       const { t } = useTranslation();
+
+       const classNames = ['location', `status-${l.status}`];
+       if (size) {
+               classNames.push(`size-${size}`);
+       }
+       if (l.handlePrimary) {
+               classNames.push('clickable');
+       }
+
+       return <g
+               className={classNames.join(' ')}
+               onClick={(e) => {
+                       l.handlePrimary();
+                       e.preventDefault();
+                       e.stopPropagation();
+               }}
+               onContextMenu={(e) => {
+                       l.handleSecondary();
+                       e.preventDefault();
+                       e.stopPropagation();
+               }}
+               transform={`translate(${l.x} ${l.y})`}
+       >
+               <title>{t(`tracker.location.${l.id}`)}</title>
+               <rect className="box" x="0" y="0" />
+               {number && l.remaining ?
+                       <text className="text" x="0" y="0">{l.remaining}</text>
+               : null}
+       </g>;
+};
+
+Location.propTypes = {
+       number: PropTypes.bool,
+       l: PropTypes.shape({
+               id: PropTypes.string,
+               x: PropTypes.number,
+               y: PropTypes.number,
+               number: PropTypes.number,
+               remaining: PropTypes.number,
+               status: PropTypes.string,
+               handlePrimary: PropTypes.func,
+               handleSecondary: PropTypes.func,
+       }),
+       size: PropTypes.string,
+};
+
+const makeBackground = (src, level) => {
+       const amount = Math.pow(2, Math.max(0, level - 8));
+       const size = 1 / amount;
+       const tiles = [];
+       for (let y = 0; y < amount; ++y) {
+               for (let x = 0; x < amount; ++x) {
+                       tiles.push(<image
+                               key={`${x}-${y}`}
+                               x={x * size}
+                               y={y * size}
+                               width={size * 1.002}
+                               height={size * 1.002}
+                               href={`/media/alttp/map/${src}/${level}/${x}_${y}.png`}
+                       />);
+               }
+       }
+       return tiles;
+};
+
+const Overworld = () => {
+       const { dungeons, logic, setManualState, state } = useTracker();
+
+       const mapDungeon = React.useCallback(dungeon => {
+               const definition = dungeons.find(d => d.id === dungeon.id);
+               const remaining = getDungeonRemainingItems(state, definition);
+               const status = aggregateDungeonStatus(definition, logic, state);
+               return {
+                       ...dungeon,
+                       status,
+                       remaining,
+                       handlePrimary: () => {
+                               if (getDungeonRemainingItems(state, definition)) {
+                                       setManualState(addDungeonCheck(definition));
+                               } else if (
+                                       !hasDungeonBoss(state, definition) || !hasDungeonPrize(state, definition)
+                               ) {
+                                       if (definition.boss) {
+                                               setManualState(setBossDefeated(definition, true));
+                                       }
+                                       if (definition.prize) {
+                                               setManualState(setPrizeAcquired(definition, true));
+                                       }
+                               } else {
+                                       setManualState(resetDungeonChecks(definition));
+                                       if (definition.boss) {
+                                               setManualState(setBossDefeated(definition, false));
+                                       }
+                                       if (definition.prize) {
+                                               setManualState(setPrizeAcquired(definition, false));
+                                       }
+                               }
+                       },
+                       handleSecondary: () => {
+                               if (isDungeonCleared(state, definition)) {
+                                       if (definition.items) {
+                                               setManualState(removeDungeonCheck(definition));
+                                       }
+                                       if (definition.boss) {
+                                               setManualState(setBossDefeated(definition, false));
+                                       }
+                                       if (definition.prize) {
+                                               setManualState(setPrizeAcquired(definition, false));
+                                       }
+                               } else if (getDungeonClearedItems(state, definition)) {
+                                       setManualState(removeDungeonCheck(definition));
+                               } else {
+                                       setManualState(completeDungeonChecks(definition));
+                                       if (definition.boss) {
+                                               setManualState(setBossDefeated(definition, true));
+                                       }
+                                       if (definition.prize) {
+                                               setManualState(setPrizeAcquired(definition, true));
+                                       }
+                               }
+                       },
+               };
+       }, [dungeons, logic, setManualState, state]);
+
+       const mapLocation = React.useCallback(loc => {
+               const remaining = countRemainingLocations(state, loc.checks);
+               const status = aggregateLocationStatus(loc.checks, logic, state);
+               return {
+                       ...loc,
+                       remaining,
+                       status,
+                       handlePrimary: () => {
+                               if (remaining) {
+                                       setManualState(clearAll(loc.checks));
+                               } else {
+                                       setManualState(unclearAll(loc.checks));
+                               }
+                       },
+                       handleSecondary: () => {
+                               if (remaining) {
+                                       setManualState(clearAll(loc.checks));
+                               } else {
+                                       setManualState(unclearAll(loc.checks));
+                               }
+                       },
+               };
+       }, [logic, setManualState, state]);
+
+       const lwDungeons = React.useMemo(() => LW_DUNGEONS.map(mapDungeon), [mapDungeon]);
+       const lwLocations = React.useMemo(() => LW_LOCATIONS.map(mapLocation), [mapLocation]);
+
+       const dwDungeons = React.useMemo(() => DW_DUNGEONS.map(mapDungeon), [mapDungeon]);
+       const dwLocations = React.useMemo(() => DW_LOCATIONS.map(mapLocation), [mapLocation]);
+
+       return <svg
+               xmlns="http://www.w3.org/2000/svg"
+               className="canvas"
+               width="2"
+               height="1"
+               viewBox="0 0 2 1"
+               onContextMenu={(e) => {
+                       e.preventDefault();
+                       e.stopPropagation();
+               }}
+       >
+               <g className="light-world">
+                       <g className="background">
+                               {makeBackground('lw_files', 10)}
+                       </g>
+                       <g className="locations">
+                               {lwLocations.map(l =>
+                                       <Location key={l.id} l={l} />
+                               )}
+                               {lwDungeons.map(l =>
+                                       <Location key={l.id} number l={l} size="lg" />
+                               )}
+                       </g>
+               </g>
+               <g className="dark-world" transform="translate(1 0)">
+                       <g className="background">
+                               {makeBackground('dw_files', 10)}
+                       </g>
+                       <g className="locations">
+                               {dwLocations.map(l =>
+                                       <Location key={l.id} l={l} />
+                               )}
+                               {dwDungeons.map(l =>
+                                       <Location key={l.id} number l={l} size="lg" />
+                               )}
+                       </g>
+               </g>
+       </svg>;
+};
+
+export default Overworld;
diff --git a/resources/js/components/tracker/Map/index.js b/resources/js/components/tracker/Map/index.js
new file mode 100644 (file)
index 0000000..65dcbd3
--- /dev/null
@@ -0,0 +1,11 @@
+import React from 'react';
+
+import Overworld from './Overworld';
+
+const Map = () => {
+       return <div className="tracker-map">
+               <Overworld />
+       </div>;
+};
+
+export default Map;