1 import PropTypes from 'prop-types';
2 import React from 'react';
3 import { useTranslation } from 'react-i18next';
9 countRemainingLocations,
10 getDungeonClearedItems,
11 getDungeonRemainingItems,
21 } from '../../helpers/tracker';
22 import { useTracker } from '../../hooks/tracker';
52 const LW_LOCATIONS = [
67 'blinds-hut-far-left',
68 'blinds-hut-far-right',
147 id: 'floating-island',
163 id: 'graveyard-ledge',
207 id: 'lake-hylia-island',
231 id: 'lost-woods-hideout',
233 'lost-woods-hideout',
263 id: 'mini-moldorm-cave',
266 'mini-moldorm-right',
267 'mini-moldorm-far-left',
268 'mini-moldorm-far-right',
293 'paradox-lower-far-left',
294 'paradox-lower-left',
295 'paradox-lower-right',
296 'paradox-lower-far-right',
298 'paradox-upper-left',
299 'paradox-upper-right',
372 id: 'spec-rock-cave',
396 id: 'waterfall-fairy',
398 'waterfall-fairy-left',
399 'waterfall-fairy-right',
422 const DW_DUNGEONS = [
465 const DW_LOCATIONS = [
541 id: 'hookshot-cave-bonk',
588 'pyramid-fairy-left',
589 'pyramid-fairy-right',
614 'super-bunny-bottom',
621 const Location = ({ number, l, size }) => {
622 const { t } = useTranslation();
624 const classNames = ['location', `status-${l.status}`];
626 classNames.push(`size-${size}`);
628 if (l.handlePrimary) {
629 classNames.push('clickable');
633 className={classNames.join(' ')}
639 onContextMenu={(e) => {
644 transform={`translate(${l.x} ${l.y})`}
646 <title>{t(`tracker.location.${l.id}`)}</title>
647 <rect className="box" x="0" y="0" />
648 {number && l.remaining ?
649 <text className="text" x="0" y="0">{l.remaining}</text>
654 Location.propTypes = {
655 number: PropTypes.bool,
657 id: PropTypes.string,
660 number: PropTypes.number,
661 remaining: PropTypes.number,
662 status: PropTypes.string,
663 handlePrimary: PropTypes.func,
664 handleSecondary: PropTypes.func,
666 size: PropTypes.string,
669 const makeBackground = (src, level) => {
670 const amount = Math.pow(2, Math.max(0, level - 8));
671 const size = 1 / amount;
673 for (let y = 0; y < amount; ++y) {
674 for (let x = 0; x < amount; ++x) {
680 height={size * 1.002}
681 href={`/media/alttp/map/${src}/${level}/${x}_${y}.png`}
689 const { dungeons, setManualState, state } = useTracker();
691 const mapDungeon = React.useCallback(dungeon => {
692 const definition = dungeons.find(d => d.id === dungeon.id);
693 const remaining = getDungeonRemainingItems(state, definition);
694 let status = 'available';
695 if (isDungeonCleared(state, definition)) {
702 handlePrimary: () => {
703 if (getDungeonRemainingItems(state, definition)) {
704 setManualState(addDungeonCheck(definition));
706 !hasDungeonBoss(state, definition) || !hasDungeonPrize(state, definition)
708 if (definition.boss) {
709 setManualState(setBossDefeated(definition, true));
711 if (definition.prize) {
712 setManualState(setPrizeAcquired(definition, true));
715 setManualState(resetDungeonChecks(definition));
716 if (definition.boss) {
717 setManualState(setBossDefeated(definition, false));
719 if (definition.prize) {
720 setManualState(setPrizeAcquired(definition, false));
724 handleSecondary: () => {
725 if (isDungeonCleared(state, definition)) {
726 if (definition.items) {
727 setManualState(removeDungeonCheck(definition));
729 if (definition.boss) {
730 setManualState(setBossDefeated(definition, false));
732 if (definition.prize) {
733 setManualState(setPrizeAcquired(definition, false));
735 } else if (getDungeonClearedItems(state, definition)) {
736 setManualState(removeDungeonCheck(definition));
738 setManualState(completeDungeonChecks(definition));
739 if (definition.boss) {
740 setManualState(setBossDefeated(definition, true));
742 if (definition.prize) {
743 setManualState(setPrizeAcquired(definition, true));
748 }, [dungeons, setManualState, state]);
750 const mapLocation = React.useCallback(loc => {
751 const remaining = countRemainingLocations(state, loc.checks);
752 let status = 'available';
753 if (hasClearedLocations(state, loc.checks)) {
760 handlePrimary: () => {
762 setManualState(clearAll(loc.checks));
764 setManualState(unclearAll(loc.checks));
767 handleSecondary: () => {
769 setManualState(clearAll(loc.checks));
771 setManualState(unclearAll(loc.checks));
775 }, [setManualState, state]);
777 const lwDungeons = React.useMemo(() => LW_DUNGEONS.map(mapDungeon), [mapDungeon]);
778 const lwLocations = React.useMemo(() => LW_LOCATIONS.map(mapLocation), [mapLocation]);
780 const dwDungeons = React.useMemo(() => DW_DUNGEONS.map(mapDungeon), [mapDungeon]);
781 const dwLocations = React.useMemo(() => DW_LOCATIONS.map(mapLocation), [mapLocation]);
783 return <div className="tracker-map">
785 xmlns="http://www.w3.org/2000/svg"
790 onContextMenu={(e) => {
795 <g className="light-world">
796 <g className="background">
797 {makeBackground('lw_files', 10)}
799 <g className="locations">
800 {lwLocations.map(l =>
801 <Location key={l.id} l={l} />
804 <Location key={l.id} number l={l} size="lg" />
808 <g className="dark-world" transform="translate(1 0)">
809 <g className="background">
810 {makeBackground('dw_files', 10)}
812 <g className="locations">
813 {dwLocations.map(l =>
814 <Location key={l.id} l={l} />
817 <Location key={l.id} number l={l} size="lg" />