]> git.localhorst.tv Git - alttp.git/commitdiff
tentative inverted logic
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Sat, 30 Mar 2024 18:54:06 +0000 (19:54 +0100)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Sat, 30 Mar 2024 18:54:06 +0000 (19:54 +0100)
24 files changed:
icons.sh
package.json
public/item/bottle-good-bee.png [new file with mode: 0644]
public/item/bunny-head.png [new file with mode: 0644]
public/item/crystal-switch-blue.png [new file with mode: 0644]
public/item/crystal-switch-red.png [new file with mode: 0644]
public/item/crystal-switch.png [new file with mode: 0644]
public/item/ganon.png [new file with mode: 0644]
public/item/gt.png [new file with mode: 0644]
public/item/link-head.png [new file with mode: 0644]
public/item/triforce-piece.png [new file with mode: 0644]
public/item/triforce.png [new file with mode: 0644]
public/items-v2.png [new file with mode: 0644]
resources/js/components/common/ZeldaIcon.js
resources/js/components/tracker/ConfigDialog.js
resources/js/components/tracker/Map/Overworld.js
resources/js/components/tracker/Toolbar.js
resources/js/helpers/logic.js
resources/js/helpers/tracker.js
resources/js/hooks/tracker.js
resources/js/i18n/de.js
resources/js/i18n/en.js
resources/sass/common.scss
resources/sass/tracker.scss

index c9fe293e390290ae79f71792c2954f468b649241..b4022922ae13c05ee868c7543a6a8bb080289896 100755 (executable)
--- a/icons.sh
+++ b/icons.sh
@@ -1,6 +1,6 @@
 #!/bin/bash
 
-gm montage -geometry '32x32>' -background transparent -gravity center -tile 8x100 public/item/*.png public/items-v1.png
+gm montage -geometry '32x32>' -background transparent -gravity center -tile 8x100 public/item/*.png public/items-v2.png
 
 echo 'const ITEM_MAP = ['
 for i in public/item/*.png
index 78f545384302777447982bdba5985546d33c95af..1fdb3627e79f0f4e784d63b6d226fc6587c60d3d 100644 (file)
@@ -45,6 +45,7 @@
                     "tabWidth": 4
                 }
             ],
+                       "no-use-before-define": "error",
             "no-extra-parens": [
                 "warn",
                 "all",
diff --git a/public/item/bottle-good-bee.png b/public/item/bottle-good-bee.png
new file mode 100644 (file)
index 0000000..25fd630
Binary files /dev/null and b/public/item/bottle-good-bee.png differ
diff --git a/public/item/bunny-head.png b/public/item/bunny-head.png
new file mode 100644 (file)
index 0000000..c97d8f5
Binary files /dev/null and b/public/item/bunny-head.png differ
diff --git a/public/item/crystal-switch-blue.png b/public/item/crystal-switch-blue.png
new file mode 100644 (file)
index 0000000..17c5c42
Binary files /dev/null and b/public/item/crystal-switch-blue.png differ
diff --git a/public/item/crystal-switch-red.png b/public/item/crystal-switch-red.png
new file mode 100644 (file)
index 0000000..318ef3b
Binary files /dev/null and b/public/item/crystal-switch-red.png differ
diff --git a/public/item/crystal-switch.png b/public/item/crystal-switch.png
new file mode 100644 (file)
index 0000000..f2d647d
Binary files /dev/null and b/public/item/crystal-switch.png differ
diff --git a/public/item/ganon.png b/public/item/ganon.png
new file mode 100644 (file)
index 0000000..89fe005
Binary files /dev/null and b/public/item/ganon.png differ
diff --git a/public/item/gt.png b/public/item/gt.png
new file mode 100644 (file)
index 0000000..cc95d58
Binary files /dev/null and b/public/item/gt.png differ
diff --git a/public/item/link-head.png b/public/item/link-head.png
new file mode 100644 (file)
index 0000000..5c1d31b
Binary files /dev/null and b/public/item/link-head.png differ
diff --git a/public/item/triforce-piece.png b/public/item/triforce-piece.png
new file mode 100644 (file)
index 0000000..a7356fe
Binary files /dev/null and b/public/item/triforce-piece.png differ
diff --git a/public/item/triforce.png b/public/item/triforce.png
new file mode 100644 (file)
index 0000000..1b26293
Binary files /dev/null and b/public/item/triforce.png differ
diff --git a/public/items-v2.png b/public/items-v2.png
new file mode 100644 (file)
index 0000000..d479d84
Binary files /dev/null and b/public/items-v2.png differ
index 230b14e9232f8d688b43da1bcf666347249446b2..3504dcf17b719bb1b483f63fca3170f26b1dca44 100644 (file)
@@ -19,15 +19,20 @@ const ITEM_MAP = [
     'book',
     'boots',
     'bottle-bee',
+    'bottle-good-bee',
     'bottle',
     'bowless-silvers',
     'bow',
     'bugnet',
+    'bunny-head',
     'byrna',
     'cape',
     'chest',
     'compass',
     'crystal',
+    'crystal-switch-blue',
+    'crystal-switch',
+    'crystal-switch-red',
     'duck',
     'ether',
     'fairy',
@@ -37,11 +42,13 @@ const ITEM_MAP = [
     'fire-shield',
     'flippers',
     'flute',
+    'ganon',
     'glove',
     'gold-sword',
     'green-mail',
     'green-pendant',
     'green-potion',
+    'gt',
     'half-magic',
     'hammer',
     'heart-0',
@@ -56,6 +63,7 @@ const ITEM_MAP = [
     'kholdstare',
     'lamp',
     'lanmolas',
+    'link-head',
     'map',
     'master-sword',
     'mirror',
@@ -84,20 +92,32 @@ const ITEM_MAP = [
     'sword-3',
     'sword-4',
     'tempered-sword',
+    'triforce-piece',
+    'triforce',
     'trinexx',
     'vitreous',
 ];
 
+const ITEM_MAP_WIDTH = 8;
+
+const ITEM_MAP_HEIGHT = Math.ceil(ITEM_MAP.length / ITEM_MAP_WIDTH);
+
+const ITEM_MAP_URL = '/items-v2.png';
+
 const isOnItemMap = name => ITEM_MAP.includes(name);
 
-const getItemMapX = name => ITEM_MAP.indexOf(name) % 8;
+const getItemMapX = name => ITEM_MAP.indexOf(name) % ITEM_MAP_WIDTH;
 
-const getItemMapY = name => Math.floor(ITEM_MAP.indexOf(name) / 8);
+const getItemMapY = name => Math.floor(ITEM_MAP.indexOf(name) / ITEM_MAP_WIDTH);
 
 const getItemMapStyle = name => {
        const x = getItemMapX(name);
        const y = getItemMapY(name);
-       return { backgroundPosition: `-${x * 100}% -${y * 100}%` };
+       return {
+               backgroundImage: `url(${ITEM_MAP_URL})`,
+               backgroundPosition: `-${x * 100}% -${y * 100}%`,
+               backgroundSize: `${ITEM_MAP_WIDTH * 100}% ${ITEM_MAP_HEIGHT * 100}%`,
+       };
 };
 
 const getIconURL = name => {
@@ -116,10 +136,6 @@ const getIconURL = name => {
                case 'dungeon-tr':
                case 'dungeon-tt':
                        return `/dungeon/${name.substr(8)}.png`;
-               case 'crystal-switch':
-               case 'crystal-switch-blue':
-               case 'crystal-switch-red':
-                       return `/icon/${name}.png`;
                default:
                        return '';
        }
@@ -138,9 +154,9 @@ const ZeldaIcon = ({ name, svg, title }) => {
                const clipX = getItemMapX(strippedName);
                const clipY = getItemMapY(strippedName);
                return <image
-                       href={isOnItemMap(strippedName) ? '/items-v1.png' : src}
-                       width="8"
-                       height="11"
+                       href={isOnItemMap(strippedName) ? ITEM_MAP_URL : src}
+                       width={ITEM_MAP_WIDTH}
+                       height={ITEM_MAP_HEIGHT}
                        x="0"
                        y="0"
                        transform={`translate(-${clipX} -${clipY})`}
index 88c6d73e90a3992669d837fd2c70555be9a2aa7a..ca95ba0d2bfe86a1d1ef05e6bab555a969c58963 100644 (file)
@@ -4,6 +4,7 @@ import { Col, Form, Modal, Row } from 'react-bootstrap';
 import { useTranslation } from 'react-i18next';
 
 import LargeCheck from '../common/LargeCheck';
+import { getConfigValue } from '../../helpers/tracker';
 import { useTracker } from '../../hooks/tracker';
 
 const ConfigDialog = ({
@@ -25,6 +26,116 @@ const ConfigDialog = ({
                </Modal.Header>
                <Modal.Body>
                        <Row>
+                               <Col sm={6}>
+                                       <h3>{t('tracker.config.logic')}</h3>
+                                       <Form.Group
+                                               className="d-flex justify-content-between my-2"
+                                               controlId="tracker.worldState"
+                                       >
+                                               <Form.Label>{t('tracker.config.worldState')}</Form.Label>
+                                               <Form.Select
+                                                       className="w-auto"
+                                                       name="world-state"
+                                                       onChange={handleChange}
+                                                       value={getConfigValue(config, 'world-state', 'open')}
+                                               >
+                                                       {['open', 'inverted'].map(n =>
+                                                               <option key={n} value={n}>
+                                                                       {t(`tracker.config.worldStates.${n}`)}
+                                                               </option>
+                                                       )}
+                                               </Form.Select>
+                                       </Form.Group>
+                                       <Form.Group
+                                               className="d-flex justify-content-between my-2"
+                                               controlId="tracker.glitches"
+                                       >
+                                               <Form.Label>{t('tracker.config.glitches')}</Form.Label>
+                                               <Form.Select
+                                                       className="w-auto"
+                                                       name="glitches"
+                                                       onChange={handleChange}
+                                                       value={getConfigValue(config, 'glitches', 'none')}
+                                               >
+                                                       {['none', 'owg', 'hmg', 'mg', 'nl'].map(n =>
+                                                               <option key={n} value={n}>
+                                                                       {t(`tracker.config.glitchRules.${n}`)}
+                                                               </option>
+                                                       )}
+                                               </Form.Select>
+                                       </Form.Group>
+                                       <Form.Group
+                                               className="d-flex justify-content-between my-2"
+                                               controlId="tracker.bossShuffle"
+                                       >
+                                               <Form.Label>{t('tracker.config.bossShuffle')}</Form.Label>
+                                               <Form.Control
+                                                       as={LargeCheck}
+                                                       name="bossShuffle"
+                                                       onChange={handleChange}
+                                                       value={!!config.bossShuffle}
+                                               />
+                                       </Form.Group>
+                               </Col>
+                               <Col sm={6}>
+                                       <h3>{t('tracker.config.goal')}</h3>
+                                       <Form.Group
+                                               className="d-flex justify-content-between my-2"
+                                               controlId="tracker.gtCrystals"
+                                       >
+                                               <Form.Label>{t('tracker.config.gtCrystals')}</Form.Label>
+                                               <Form.Select
+                                                       className="w-auto"
+                                                       name="gt-crystals"
+                                                       onChange={handleChange}
+                                                       value={getConfigValue(config, 'gt-crystals', 7)}
+                                               >
+                                                       {['?', 0, 1, 2, 3, 4, 5, 6, 7].map(n =>
+                                                               <option key={n} value={n}>
+                                                                       {n}
+                                                               </option>
+                                                       )}
+                                               </Form.Select>
+                                       </Form.Group>
+                                       <Form.Group
+                                               className="d-flex justify-content-between my-2"
+                                               controlId="tracker.ganonCrystals"
+                                       >
+                                               <Form.Label>{t('tracker.config.ganonCrystals')}</Form.Label>
+                                               <Form.Select
+                                                       className="w-auto"
+                                                       name="ganon-crystals"
+                                                       onChange={handleChange}
+                                                       value={getConfigValue(config, 'ganon-crystals', 7)}
+                                               >
+                                                       {['?', 0, 1, 2, 3, 4, 5, 6, 7].map(n =>
+                                                               <option key={n} value={n}>
+                                                                       {n}
+                                                               </option>
+                                                       )}
+                                               </Form.Select>
+                                       </Form.Group>
+                                       <Form.Group
+                                               className="d-flex justify-content-between my-2"
+                                               controlId="tracker.goal"
+                                       >
+                                               <Form.Label>{t('tracker.config.goal')}</Form.Label>
+                                               <Form.Select
+                                                       className="w-auto"
+                                                       name="goal"
+                                                       onChange={handleChange}
+                                                       value={getConfigValue(config, 'goal', 'ganon')}
+                                               >
+                                                       {['ganon', 'fast', 'ad', 'ped', 'trinity', 'thunt', 'ghunt'].map(n =>
+                                                               <option key={n} value={n}>
+                                                                       {t(`tracker.config.goals.${n}`)}
+                                                               </option>
+                                                       )}
+                                               </Form.Select>
+                                       </Form.Group>
+                               </Col>
+                       </Row>
+                       <Row className="mt-3">
                                <Col sm={6}>
                                        <h3>{t('tracker.config.wildItems')}</h3>
                                        <Form.Group
index 609dc34cd7c92fd282f566580a9f3a2cc99dc4c3..a865c19849d7901e74af8b942bef96fc52039b99 100644 (file)
@@ -22,17 +22,12 @@ import {
 } from '../../../helpers/tracker';
 import { useTracker } from '../../../hooks/tracker';
 
-const LW_DUNGEONS = [
+const GENERIC_LW_DUNGEONS = [
        {
                id: 'hc',
                x: 0.5,
                y: 0.5,
        },
-       {
-               id: 'ct',
-               x: 0.5,
-               y: 0.4,
-       },
        {
                id: 'ep',
                x: 0.95,
@@ -50,7 +45,25 @@ const LW_DUNGEONS = [
        },
 ];
 
-const LW_LOCATIONS = [
+const LW_DUNGEONS = [
+       ...GENERIC_LW_DUNGEONS,
+       {
+               id: 'ct',
+               x: 0.5,
+               y: 0.4,
+       },
+];
+
+const INVERTED_LW_DUNGEONS = [
+       ...GENERIC_LW_DUNGEONS,
+       {
+               id: 'gt',
+               x: 0.5,
+               y: 0.4,
+       },
+];
+
+const GENERIC_LW_LOCATIONS = [
        {
                id: 'aginah',
                checks: [
@@ -220,14 +233,6 @@ const LW_LOCATIONS = [
                x: 0.15,
                y: 0.65,
        },
-       {
-               id: 'links-house',
-               checks: [
-                       'links-house',
-               ],
-               x: 0.55,
-               y: 0.6875,
-       },
        {
                id: 'lost-woods-hideout',
                checks: [
@@ -420,7 +425,21 @@ const LW_LOCATIONS = [
        },
 ];
 
-const DW_DUNGEONS = [
+const LW_LOCATIONS = [
+       ...GENERIC_LW_LOCATIONS,
+       {
+               id: 'links-house',
+               checks: [
+                       'links-house',
+               ],
+               x: 0.55,
+               y: 0.6875,
+       },
+];
+
+const INVERTED_LW_LOCATIONS = GENERIC_LW_LOCATIONS;
+
+const GENERIC_DW_DUNGEONS = [
        {
                id: 'pd',
                x: 0.95,
@@ -456,6 +475,10 @@ const DW_DUNGEONS = [
                x: 0.94,
                y: 0.06,
        },
+];
+
+const DW_DUNGEONS = [
+       ...GENERIC_DW_DUNGEONS,
        {
                id: 'gt',
                x: 0.56,
@@ -463,7 +486,16 @@ const DW_DUNGEONS = [
        },
 ];
 
-const DW_LOCATIONS = [
+const INVERTED_DW_DUNGEONS = [
+       ...GENERIC_DW_DUNGEONS,
+       {
+               id: 'ct',
+               x: 0.56,
+               y: 0.05,
+       },
+];
+
+const GENERIC_DW_LOCATIONS = [
        {
                id: 'blacksmith',
                checks: [
@@ -619,6 +651,20 @@ const DW_LOCATIONS = [
        },
 ];
 
+const DW_LOCATIONS = GENERIC_DW_LOCATIONS;
+
+const INVERTED_DW_LOCATIONS = [
+       ...GENERIC_DW_LOCATIONS,
+       {
+               id: 'links-house',
+               checks: [
+                       'links-house',
+               ],
+               x: 0.55,
+               y: 0.6875,
+       },
+];
+
 const Location = ({ number, l, size }) => {
        const { t } = useTranslation();
 
@@ -687,7 +733,7 @@ const makeBackground = (src, level) => {
 };
 
 const Overworld = () => {
-       const { dungeons, logic, setManualState, state } = useTracker();
+       const { config, dungeons, logic, setManualState, state } = useTracker();
 
        const mapDungeon = React.useCallback(dungeon => {
                const definition = dungeons.find(d => d.id === dungeon.id);
@@ -769,11 +815,23 @@ const Overworld = () => {
                };
        }, [logic, setManualState, state]);
 
-       const lwDungeons = React.useMemo(() => LW_DUNGEONS.map(mapDungeon), [mapDungeon]);
-       const lwLocations = React.useMemo(() => LW_LOCATIONS.map(mapLocation), [mapLocation]);
+       const lwDungeons = React.useMemo(() =>
+               (config.worldState === 'inverted' ? INVERTED_LW_DUNGEONS : LW_DUNGEONS)
+               .map(mapDungeon)
+       , [mapDungeon]);
+       const lwLocations = React.useMemo(() =>
+               (config.worldState === 'inverted' ? INVERTED_LW_LOCATIONS : LW_LOCATIONS)
+               .map(mapLocation)
+       , [mapLocation]);
 
-       const dwDungeons = React.useMemo(() => DW_DUNGEONS.map(mapDungeon), [mapDungeon]);
-       const dwLocations = React.useMemo(() => DW_LOCATIONS.map(mapLocation), [mapLocation]);
+       const dwDungeons = React.useMemo(() =>
+               (config.worldState === 'inverted' ? INVERTED_DW_DUNGEONS : DW_DUNGEONS)
+               .map(mapDungeon)
+       , [mapDungeon]);
+       const dwLocations = React.useMemo(() =>
+               (config.worldState === 'inverted' ? INVERTED_DW_LOCATIONS : DW_LOCATIONS)
+               .map(mapLocation)
+       , [mapLocation]);
 
        return <svg
                xmlns="http://www.w3.org/2000/svg"
index 020826635a65a215fa89bf452c81d5af58734a61..85e0f0428bf6bb4d47c7dbfa6833d602a9c1df80 100644 (file)
@@ -1,10 +1,13 @@
 import React from 'react';
-import { Button, Container, Navbar } from 'react-bootstrap';
+import { Button, Container, Form, Navbar } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
 
 import AutoTracking from './AutoTracking';
 import ConfigDialog from './ConfigDialog';
 import ToggleIcon from './ToggleIcon';
 import Icon from '../common/Icon';
+import ZeldaIcon from '../common/ZeldaIcon';
+import { getConfigValue } from '../../helpers/tracker';
 import { useTracker } from '../../hooks/tracker';
 
 const mapWild = {
@@ -17,8 +20,22 @@ const mapWild = {
 const Toolbar = () => {
        const [showConfigDialog, setShowConfigDialog] = React.useState(false);
        const { config, saveConfig } = useTracker();
+       const { t } = useTranslation();
 
-       const controller = React.useMemo(() => ({
+       const handleConfigChange = React.useCallback(({ target: { name, value } }) => {
+               saveConfig({ [name]: value });
+       }, [saveConfig]);
+
+       const bossController = React.useMemo(() => ({
+               getActive: (state, icons) => config.bossShuffle ? icons[0] : null,
+               getDefault: (state, icons) => icons[0],
+               handlePrimary: () => {
+                       saveConfig({ bossShuffle: !config.bossShuffle});
+               },
+               handleSecondary: () => null,
+       }), [config, saveConfig]);
+
+       const wildController = React.useMemo(() => ({
                getActive: (state, icons) => config[mapWild[icons[0]]] ? icons[0] : null,
                getDefault: (state, icons) => icons[0],
                handlePrimary: (state, setState, icons) => {
@@ -28,6 +45,15 @@ const Toolbar = () => {
                handleSecondary: () => null,
        }), [config, saveConfig]);
 
+       const worldController = React.useMemo(() => ({
+               getActive: (state, icons) => config.worldState === 'inverted' ? icons[1] : icons[0],
+               getDefault: (state, icons) => icons[0],
+               handlePrimary: () => {
+                       saveConfig({ worldState: config.worldState == 'inverted' ? 'open' : 'inverted' });
+               },
+               handleSecondary: () => null,
+       }), [config, saveConfig]);
+
        return <Navbar bg="dark" className="tracker-toolbar" variant="dark">
                <Container fluid>
                        <div className="button-bar">
@@ -38,12 +64,78 @@ const Toolbar = () => {
                                >
                                        <Icon.SETTINGS />
                                </Button>
-                               <ToggleIcon controller={controller} icons={['map']} />
-                               <ToggleIcon controller={controller} icons={['compass']} />
-                               <ToggleIcon controller={controller} icons={['small-key']} />
-                               <ToggleIcon controller={controller} icons={['big-key']} />
+                               <ToggleIcon controller={wildController} icons={['map']} />
+                               <ToggleIcon controller={wildController} icons={['compass']} />
+                               <ToggleIcon controller={wildController} icons={['small-key']} />
+                               <ToggleIcon controller={wildController} icons={['big-key']} />
+                               <ToggleIcon className="ms-3" controller={bossController} icons={['armos']} />
+                               <ToggleIcon controller={worldController} icons={['link-head', 'bunny-head']} />
+                       </div>
+                       <div>
+                               <Form.Group
+                                       className="d-inline-flex align-items-center justify-content-between"
+                                       controlId="tracker.gtCrystals"
+                               >
+                                       <Form.Label className="me-1">
+                                               <ZeldaIcon name="gt" title={t('tracker.config.gtCrystals')} />
+                                       </Form.Label>
+                                       <Form.Select
+                                               className="w-auto bg-dark"
+                                               name="gt-crystals"
+                                               onChange={handleConfigChange}
+                                               value={getConfigValue(config, 'gt-crystals', 7)}
+                                       >
+                                               {['?', 0, 1, 2, 3, 4, 5, 6, 7].map(n =>
+                                                       <option key={n} value={n}>
+                                                               {n}
+                                                       </option>
+                                               )}
+                                       </Form.Select>
+                               </Form.Group>
+                               <Form.Group
+                                       className="d-inline-flex align-items-center justify-content-between"
+                                       controlId="tracker.ganonCrystals"
+                               >
+                                       <Form.Label className="me-1">
+                                               <ZeldaIcon name="ganon" title={t('tracker.config.ganonCrystals')} />
+                                       </Form.Label>
+                                       <Form.Select
+                                               className="w-auto bg-dark"
+                                               name="ganon-crystals"
+                                               onChange={handleConfigChange}
+                                               value={getConfigValue(config, 'ganon-crystals', 7)}
+                                       >
+                                               {['?', 0, 1, 2, 3, 4, 5, 6, 7].map(n =>
+                                                       <option key={n} value={n}>
+                                                               {n}
+                                                       </option>
+                                               )}
+                                       </Form.Select>
+                               </Form.Group>
+                               <Form.Group
+                                       className="d-inline-flex align-items-center justify-content-between"
+                                       controlId="tracker.goal"
+                               >
+                                       <Form.Label className="me-1">
+                                               <ZeldaIcon name="triforce" title={t('tracker.config.goal')} />
+                                       </Form.Label>
+                                       <Form.Select
+                                               className="w-auto bg-dark"
+                                               name="goal"
+                                               onChange={handleConfigChange}
+                                               value={getConfigValue(config, 'goal', 'ganon')}
+                                       >
+                                               {['ganon', 'fast', 'ad', 'ped', 'trinity', 'thunt', 'ghunt'].map(n =>
+                                                       <option key={n} value={n}>
+                                                               {t(`tracker.config.goals.${n}`)}
+                                                       </option>
+                                               )}
+                                       </Form.Select>
+                               </Form.Group>
+                       </div>
+                       <div>
+                               <AutoTracking />
                        </div>
-                       <AutoTracking />
                </Container>
                <ConfigDialog onHide={() => setShowConfigDialog(false)} show={showConfigDialog} />
        </Navbar>;
index 175494178041d689479fcbd43e796d969ab6b407..522561336342cc2b4ce2e5c36332958e2699db88 100644 (file)
@@ -13,8 +13,17 @@ const and = (...predicates) => (...args) =>
 const or = (...predicates) => (...args) =>
        predicates.reduce((acc, cur) => acc || cur(...args), false);
 
+const when = (condition, then, otherwise) => (...args) =>
+       condition(...args) ? then(...args) : otherwise(...args);
+
+const alwaysAvailable = () => true;
+
+const neverAvailable = () => false;
+
 const fromBool = b => (...args) => b(...args) ? 'available' : 'unavailable';
 
+const isInverted = (config) => config.worldState === 'inverted';
+
 const agaDead = (config, dungeons, state) =>
        hasDungeonBoss(state, dungeons.find(d => d.id === 'ct'));
 
@@ -34,7 +43,7 @@ const countRedCrystals = (config, dungeons, state) => dungeons
 const hasRedCrystals = n => (...args) => countRedCrystals(...args) >= n;
 
 const hasGTCrystals = (config, dungeons, state) =>
-       countCrystals(config, dungeons, state) >= getGTCrystals(state);
+       countCrystals(config, dungeons, state) >= getGTCrystals(config);
 
 const countPendants = (config, dungeons, state) => dungeons
        .filter(dungeon =>
@@ -128,14 +137,14 @@ const hasTRMedallion = (config, dungeons, state) =>
 
 // Abilities
 
-const canActivateFlute = () => true;
-
 const canBomb = () => true;
 
 const canBonk = hasBoots;
 
 const canDarkRoom = hasLamp;
 
+const canShootArrows = hasBow;
+
 const canFlipSwitches = or(
        canBomb,
        hasBoom,
@@ -149,14 +158,26 @@ const canFlipSwitches = or(
        hasIceRod,
 );
 
-const canFly = or(hasBird, and(hasFlute, canActivateFlute));
-
 const canGetGoodBee = and(hasBugnet, hasBottle(), or(canBonk, and(hasSword(), hasQuake)));
 
 const canLift = (config, dungeons, state) => state['lift'] >= 1;
 
 const canHeavyLift = (config, dungeons, state) => state['lift'] >= 2;
 
+const canActivateFlute = when(isInverted,
+       and(
+               hasMoonpearl,
+               or(
+                       // partial copy of east light world
+                       agaDead,
+                       and(hasMoonpearl, or(and(canLift, hasHammer), canHeavyLift)),
+               ),
+       ),
+       alwaysAvailable,
+);
+
+const canFly = or(hasBird, and(hasFlute, canActivateFlute));
+
 const canKill = damage => damage && damage < 6
        ? or(hasBow, hasFireRod, hasHammer, hasSomaria, hasSword(1), canBomb, hasByrna)
        : or(hasBow, hasFireRod, hasHammer, hasSomaria, hasSword(1));
@@ -167,8 +188,6 @@ const canMeltThings = or(hasFireRod, and(hasBombos, canMedallion));
 
 const canPassCurtains = hasSword();
 
-const canShootArrows = hasBow;
-
 const canSwim = (config, dungeons, state) => !!state['flippers'];
 
 const canTablet = and(hasBook, hasSword(2));
@@ -184,53 +203,100 @@ const westDeathMountain = or(
        and(canLift, canDarkRoom),
 );
 
-const eastDeathMountain = and(
+const westDarkDeathMountain = when(isInverted,
+       or(canFly, and(canLift, canDarkRoom)),
        westDeathMountain,
-       or(
-               hasHookshot,
-               and(hasHammer, hasMirror),
-       ),
 );
 
-const northDeathMountain = and(
-       westDeathMountain,
+const eastDeathMountain = when(isInverted,
        or(
-               hasMirror,
-               and(hasHammer, hasHookshot),
+               and(canHeavyLift, or(
+                       // copy of eastDarkDeathMountain, to avoid circular reference
+                       westDarkDeathMountain,
+                       and(westDeathMountain, hasMoonpearl, hasHookshot, hasMirror),
+               )),
+               and(westDeathMountain, hasMoonpearl, hasHookshot),
+       ),
+       and(
+               westDeathMountain,
+               or(
+                       hasHookshot,
+                       and(hasHammer, hasMirror),
+               ),
        ),
 );
 
-const eastDarkDeathMountain = and(
-       eastDeathMountain,
-       canHeavyLift,
+const northDeathMountain = when(isInverted,
+       and(eastDeathMountain, hasMoonpearl, hasHammer),
+       and(
+               westDeathMountain,
+               or(
+                       hasMirror,
+                       and(hasHammer, hasHookshot),
+               ),
+       ),
 );
 
-const westDarkDeathMountain = westDeathMountain;
-
-const eastDarkWorld = and(
-       hasMoonpearl,
+const eastDarkDeathMountain = when(isInverted,
        or(
-               agaDead,
+               westDarkDeathMountain,
+               and(westDeathMountain, hasMoonpearl, hasHookshot, hasMirror),
+       ),
+       and(
+               eastDeathMountain,
                canHeavyLift,
-               and(canLift, hasHammer),
        ),
 );
 
-const westDarkWorld = and(
-       hasMoonpearl,
+const eastLightWorld = when(isInverted,
        or(
-               and(canLift, hasHammer),
-               canHeavyLift,
-               and(eastDarkWorld, hasHookshot, or(canSwim, canLift, hasHammer)),
+               agaDead,
+               and(hasMoonpearl, or(and(canLift, hasHammer), canHeavyLift)),
+               and(canFly, canHeavyLift),
        ),
+       alwaysAvailable,
 );
 
-const southDarkWorld = or(
-       westDarkWorld,
-       and(eastDarkWorld, hasMoonpearl, hasHammer),
+const westLightWorld = eastLightWorld;
+
+const southLightWorld = eastLightWorld;
+
+const eastDarkWorld = when(isInverted,
+       or(canFly, canSwim, hasHammer, and(hasMoonpearl, eastLightWorld)),
+       and(
+               hasMoonpearl,
+               or(
+                       agaDead,
+                       canHeavyLift,
+                       and(canLift, hasHammer),
+               ),
+       ),
 );
 
-const mireArea = and(canFly, canHeavyLift);
+const westDarkWorld = when(isInverted,
+       alwaysAvailable,
+       and(
+               hasMoonpearl,
+               or(
+                       and(canLift, hasHammer),
+                       canHeavyLift,
+                       and(eastDarkWorld, hasHookshot, or(canSwim, canLift, hasHammer)),
+               ),
+       ),
+);
+
+const southDarkWorld = when(isInverted,
+       alwaysAvailable,
+       or(
+               westDarkWorld,
+               and(eastDarkWorld, hasMoonpearl, hasHammer),
+       ),
+);
+
+const mireArea = when(isInverted,
+       or(canFly, and(hasMirror, southLightWorld)),
+       and(canFly, canHeavyLift),
+);
 
 // Bosses
 
@@ -285,58 +351,112 @@ const canKillGTBoss = which => (config, dungeons, state) => {
 
 // Dungeons
 
-const canEnterCT = or(hasCape, hasSword(2));
+const canEnterHC = when(isInverted,
+       and(hasMoonpearl, eastLightWorld),
+       alwaysAvailable,
+);
+
+const canEnterCT = when(isInverted,
+       westDarkDeathMountain,
+       or(hasCape, hasSword(2)),
+);
 
-const canEnterGT = and(eastDarkDeathMountain, hasGTCrystals, hasMoonpearl);
+const canEnterGT = when(isInverted,
+       and(eastLightWorld, hasGTCrystals, hasMoonpearl),
+       and(eastDarkDeathMountain, hasGTCrystals, hasMoonpearl),
+);
+
+const canEnterEP = when(isInverted,
+       and(hasMoonpearl, eastLightWorld),
+       alwaysAvailable,
+);
 
-const canEnterDPFront = or(hasBook, and(mireArea, hasMirror));
-const canEnterDPBack = or(and(canEnterDPFront, canLift), and(mireArea, hasMirror));
+const canEnterDPFront = when(isInverted,
+       and(southLightWorld, hasBook),
+       or(hasBook, and(mireArea, hasMirror)),
+);
+const canEnterDPBack = when(isInverted,
+       and(canEnterDPFront, canLift),
+       or(and(canEnterDPFront, canLift), and(mireArea, hasMirror)),
+);
 
 const canEnterTH = northDeathMountain;
 
-const canEnterPD = and(eastDarkWorld, hasMoonpearl);
+const canEnterPD = when(isInverted,
+       eastDarkWorld,
+       and(eastDarkWorld, hasMoonpearl),
+);
 
-const canEnterSP = and(southDarkWorld, hasMirror, hasMoonpearl, canSwim);
+const canEnterSP = when(isInverted,
+       and(southLightWorld, hasMirror, hasMoonpearl, canSwim),
+       and(southDarkWorld, hasMirror, hasMoonpearl, canSwim),
+);
 
-const canEnterSWFront = and(westDarkWorld, hasMoonpearl);
-const canEnterSWMid = and(westDarkWorld, hasMoonpearl);
-const canEnterSWBack = and(westDarkWorld, hasMoonpearl, hasFireRod);
+const canEnterSWFront = when(isInverted,
+       westDarkWorld,
+       and(westDarkWorld, hasMoonpearl),
+);
+const canEnterSWMid = canEnterSWFront;
+const canEnterSWBack = and(canEnterSWMid, hasFireRod);
 
-const canEnterTT = and(westDarkWorld, hasMoonpearl);
+const canEnterTT = when(isInverted,
+       westDarkWorld,
+       and(westDarkWorld, hasMoonpearl),
+);
 
-const canEnterIP = and(canSwim, canHeavyLift, hasMoonpearl, canMeltThings);
+const canEnterIP = when(isInverted,
+       and(canSwim, canMeltThings),
+       and(canSwim, canHeavyLift, hasMoonpearl, canMeltThings),
+);
 const rightSideIP = or(hasHookshot, hasSmall('ip'));
 
-const canEnterMM = and(
-       mireArea,
-       hasMoonpearl,
-       hasMMMedallion,
-       canMedallion,
-       or(canBonk, hasHookshot),
-       canKill(8),
+const canEnterMM = when(isInverted,
+       and(
+               mireArea,
+               hasMMMedallion,
+               canMedallion,
+               or(canBonk, hasHookshot),
+               canKill(8),
+       ),
+       and(
+               mireArea,
+               hasMoonpearl,
+               hasMMMedallion,
+               canMedallion,
+               or(canBonk, hasHookshot),
+               canKill(8),
+       ),
 );
 
-const canEnterTRFront = and(
-       eastDeathMountain,
-       canHeavyLift,
-       hasHammer,
-       hasMoonpearl,
-       canMedallion,
-       hasTRMedallion,
+const canEnterTRFront = when(isInverted,
+       and(
+               eastDarkDeathMountain,
+               canMedallion,
+               hasTRMedallion,
+               hasSomaria,
+       ),
+       and(
+               eastDeathMountain,
+               canHeavyLift,
+               hasHammer,
+               hasMoonpearl,
+               canMedallion,
+               hasTRMedallion,
+               hasSomaria,
+       ),
+);
+const canEnterTRWest = when(isInverted,
+       and(eastDeathMountain, hasMirror),
+       neverAvailable,
 );
-const canEnterTRWest = and(canEnterTRFront, canBomb, hasSmall('tr', 2));
 const canEnterTREast = and(canEnterTRWest, or(hasHookshot, hasSomaria));
-const canEnterTRBack = and(
-       or(canEnterTRWest, canEnterTREast),
-       or(canBomb, canBonk),
-       hasBig('tr'),
-       hasSmall('tr', 3),
-       hasSomaria,
-       canDarkRoom,
+const canEnterTRBack = when(isInverted,
+       and(eastDeathMountain, hasMirror),
+       neverAvailable,
 );
 const laserBridge = or(
        and(
-               or(canEnterDPFront, canEnterTRWest, canEnterTREast),
+               or(canEnterTRFront, canEnterTRWest, canEnterTREast),
                canDarkRoom,
                hasSomaria,
                hasBig('tr'),
@@ -358,111 +478,18 @@ const canRescueSmith = and(westDarkWorld, hasMoonpearl, canHeavyLift);
 
 const Logic = {};
 
-Logic.open = {
-       fallback: () => 'available',
-       aginah: fromBool(canBomb),
-       blacksmith: fromBool(canRescueSmith),
-       'blinds-hut-top': fromBool(canBomb),
-       'bombos-tablet': fromBool(and(southDarkWorld, hasMirror, canTablet)),
-       'bonk-rocks': fromBool(canBonk),
-       brewery: fromBool(and(westDarkWorld, canBomb, hasMoonpearl)),
-       'bumper-cave': fromBool(and(westDarkWorld, hasMoonpearl, canLift, hasCape)),
-       'c-house': fromBool(and(westDarkWorld, hasMoonpearl)),
-       catfish: fromBool(and(eastDarkWorld, hasMoonpearl)),
-       'cave-45': fromBool(and(southDarkWorld, hasMirror)),
-       checkerboard: fromBool(and(mireArea, hasMirror)),
-       'chest-game': fromBool(and(westDarkWorld, hasMoonpearl)),
-       'chicken-house': fromBool(canBomb),
-       'desert-ledge': fromBool(canEnterDPFront),
-       'digging-game': fromBool(and(southDarkWorld, hasMoonpearl)),
-       'ether-tablet': fromBool(and(northDeathMountain, canTablet)),
-       'floating-island': fromBool(
-               and(eastDarkDeathMountain, hasMoonpearl, canLift, canBomb, hasMirror),
-       ),
-       'flute-spot': fromBool(hasShovel),
-       'graveyard-ledge': fromBool(and(westDarkWorld, hasMoonpearl, hasMirror)),
-       'hammer-pegs': fromBool(and(westDarkWorld, hasHammer, hasMoonpearl, canHeavyLift)),
-       hobo: fromBool(canSwim),
-       'hookshot-cave-tl': fromBool(and(eastDarkDeathMountain, hasMoonpearl, canLift, hasHookshot)),
-       'hookshot-cave-tr': fromBool(and(eastDarkDeathMountain, hasMoonpearl, canLift, hasHookshot)),
-       'hookshot-cave-bl': fromBool(and(eastDarkDeathMountain, hasMoonpearl, canLift, hasHookshot)),
-       'hookshot-cave-br': fromBool(
-               and(eastDarkDeathMountain, hasMoonpearl, canLift, or(hasHookshot, canBonk)),
-       ),
-       'hype-cave-npc': fromBool(and(southDarkWorld, hasMoonpearl, canBomb)),
-       'hype-cave-top': fromBool(and(southDarkWorld, hasMoonpearl, canBomb)),
-       'hype-cave-right': fromBool(and(southDarkWorld, hasMoonpearl, canBomb)),
-       'hype-cave-left': fromBool(and(southDarkWorld, hasMoonpearl, canBomb)),
-       'hype-cave-bottom': fromBool(and(southDarkWorld, hasMoonpearl, canBomb)),
-       'ice-rod-cave': fromBool(canBomb),
-       'kak-well-top': fromBool(canBomb),
-       'kings-tomb': fromBool(and(canBonk, or(canHeavyLift, and(westDarkWorld, hasMirror)))),
-       'lake-hylia-island': fromBool(
-               and(canSwim, hasMirror, hasMoonpearl, or(eastDarkWorld, southDarkWorld)),
-       ),
-       library: fromBool(canBonk),
-       lumberjack: fromBool(and(canBonk, agaDead)),
-       'magic-bat': fromBool(and(hasPowder,
-               or(hasHammer, and(westDarkWorld, hasMoonpearl, canHeavyLift, hasMirror)),
-       )),
-       'mimic-cave': fromBool(and(canEnterTREast, hasMirror, hasHammer)),
-       'mini-moldorm-left': fromBool(canBomb),
-       'mini-moldorm-right': fromBool(canBomb),
-       'mini-moldorm-far-left': fromBool(canBomb),
-       'mini-moldorm-far-right': fromBool(canBomb),
-       'mini-moldorm-npc': fromBool(canBomb),
-       'mire-shed-left': fromBool(and(mireArea, hasMoonpearl)),
-       'mire-shed-right': fromBool(and(mireArea, hasMoonpearl)),
-       'old-man': fromBool(and(westDeathMountain, canDarkRoom)),
-       'paradox-lower-far-left': fromBool(paradoxLower),
-       'paradox-lower-left': fromBool(paradoxLower),
-       'paradox-lower-right': fromBool(paradoxLower),
-       'paradox-lower-far-right': fromBool(paradoxLower),
-       'paradox-lower-mid': fromBool(paradoxLower),
-       'paradox-upper-left': fromBool(and(eastDeathMountain, canBomb)),
-       'paradox-upper-right': fromBool(and(eastDeathMountain, canBomb)),
-       pedestal: fromBool(hasPendants(3)),
-       'potion-shop': fromBool(hasMushroom),
-       'purple-chest': fromBool(and(canRescueSmith, hasMoonpearl, canHeavyLift)),
-       pyramid: fromBool(eastDarkWorld),
-       'pyramid-fairy-left': fromBool(and(hasRedCrystals(2), southDarkWorld, canBridgeRedBomb)),
-       'pyramid-fairy-right': fromBool(and(hasRedCrystals(2), southDarkWorld, canBridgeRedBomb)),
-       'race-game': fromBool(or(canBomb, canBonk)),
-       saha: fromBool(hasGreenPendant),
-       'saha-left': fromBool(or(canBomb, canBonk)),
-       'saha-mid': fromBool(or(canBomb, canBonk)),
-       'saha-right': fromBool(or(canBomb, canBonk)),
-       'sick-kid': fromBool(hasBottle(1)),
-       'spec-rock': fromBool(and(westDeathMountain, hasMirror)),
-       'spec-rock-cave': fromBool(westDeathMountain),
-       'spike-cave': fromBool(and(
-               westDarkDeathMountain,
-               hasMoonpearl,
-               hasHammer,
-               canLift,
-               or(hasByrna, and(hasCape, hasMagicBars(2))),
-       )),
-       'spiral-cave': fromBool(eastDeathMountain),
-       stumpy: fromBool(and(southDarkWorld, hasMoonpearl)),
-       'super-bunny-top': fromBool(and(eastDarkDeathMountain, hasMoonpearl)),
-       'super-bunny-bottom': fromBool(and(eastDarkDeathMountain, hasMoonpearl)),
-       'waterfall-fairy-left': fromBool(canSwim),
-       'waterfall-fairy-right': fromBool(canSwim),
-       zora: fromBool(or(canLift, canSwim)),
-       'zora-ledge': fromBool(canSwim),
+Logic.dungeonInterior = {
        'hc-boom': fromBool(and(hasSmall('hc'), canKill())),
        'hc-cell': fromBool(and(hasSmall('hc'), canKill())),
        'dark-cross': fromBool(canTorchDarkRoom),
        'sewers-left': fromBool(or(canLift, and(canTorchDarkRoom, hasSmall('hc'), canKill()))),
        'sewers-mid': fromBool(or(canLift, and(canTorchDarkRoom, hasSmall('hc'), canKill()))),
        'sewers-right': fromBool(or(canLift, and(canTorchDarkRoom, hasSmall('hc'), canKill()))),
-       ct: fromBool(canEnterCT),
        'ct-1': fromBool(canKill()),
        'ct-2': fromBool(and(canKill(), hasSmall('ct'), canDarkRoom)),
        'ct-boss-killable': fromBool(and(
                canKill(), hasSmall('ct', 2), canDarkRoom, canPassCurtains, canKillBoss('ct'),
        )),
-       gt: fromBool(canEnterGT),
        'gt-tile-room': fromBool(hasSomaria),
        'gt-compass-tl': fromBool(and(hasSomaria, hasFireRod, hasSmall('gt', 4))),
        'gt-compass-tr': fromBool(and(hasSomaria, hasFireRod, hasSmall('gt', 4))),
@@ -548,7 +575,6 @@ Logic.open = {
        'ep-boss-defeated': fromBool(and(
                canShootArrows, canTorchDarkRoom, hasBig('ep'), canKillBoss('ep'),
        )),
-       dp: fromBool(or(canEnterDPFront, canEnterDPBack)),
        'dp-big-chest': fromBool(and(canEnterDPFront, hasBig('dp'))),
        'dp-big-key-chest': fromBool(and(canEnterDPFront, hasSmall('dp'), canKill())),
        'dp-compass-chest': fromBool(and(canEnterDPFront, hasSmall('dp'))),
@@ -561,7 +587,6 @@ Logic.open = {
                hasSmall('dp'),
                canKillBoss('dp'),
        )),
-       th: fromBool(canEnterTH),
        'th-basement-cage': fromBool(canFlipSwitches),
        'th-map-chest': fromBool(canFlipSwitches),
        'th-big-key-chest': fromBool(and(canFlipSwitches, hasSmall('th'), canTorch)),
@@ -572,7 +597,6 @@ Logic.open = {
                hasBig('th'),
                canKillBoss('th'),
        )),
-       pd: fromBool(canEnterPD),
        'pd-stalfos-basement': fromBool(or(hasSmall('pd', 1), and(canShootArrows, hasHammer))),
        'pd-big-key-chest': fromBool(hasSmall('pd', 6)),
        'pd-arena-bridge': fromBool(or(hasSmall('pd', 1), and(canShootArrows, hasHammer))),
@@ -588,7 +612,6 @@ Logic.open = {
        'pd-boss-defeated': fromBool(and(
                canDarkRoom, hasBig('pd'), hasSmall('pd', 6), canShootArrows, hasHammer, canKillBoss('pd'),
        )),
-       sp: fromBool(canEnterSP),
        'sp-map-chest': fromBool(and(hasSmall('sp'), canBomb)),
        'sp-big-chest': fromBool(and(hasSmall('sp'), hasHammer, hasBig('sp'))),
        'sp-compass-chest': fromBool(and(hasSmall('sp'), hasHammer)),
@@ -598,18 +621,15 @@ Logic.open = {
        'sp-flooded-right': fromBool(and(hasSmall('sp'), hasHammer, hasHookshot)),
        'sp-waterfall': fromBool(and(hasSmall('sp'), hasHammer, hasHookshot)),
        'sp-boss-defeated': fromBool(and(hasSmall('sp'), hasHammer, hasHookshot, canKillBoss('sp'))),
-       sw: fromBool(or(canEnterSWFront, canEnterSWMid, canEnterSWBack)),
        'sw-big-chest': fromBool(and(canEnterSWFront, hasBig('sw'))),
        'sw-bridge-chest': fromBool(canEnterSWBack),
        'sw-boss-defeated': fromBool(and(
                canEnterSWBack, canPassCurtains, hasFireRod, hasSmall('sw', 3), canKillBoss('sw'),
        )),
-       tt: fromBool(canEnterTT),
        'tt-attic': fromBool(and(hasBig('tt'), hasSmall('tt'), canBomb)),
        'tt-cell': fromBool(hasBig('tt')),
        'tt-big-chest': fromBool(and(hasBig('tt'), hasSmall('tt'), hasHammer)),
        'tt-boss-defeated': fromBool(and(hasBig('tt'), hasSmall('tt'), canKillBoss('tt'))),
-       ip: fromBool(canEnterIP),
        'ip-big-key-chest': fromBool(and(rightSideIP, hasHammer, canLift)),
        'ip-map-chest': fromBool(and(rightSideIP, hasHammer, canLift)),
        'ip-spike-chest': fromBool(rightSideIP),
@@ -622,14 +642,12 @@ Logic.open = {
                or(hasSmall('ip', 2), and(hasSomaria, hasSmall('ip'))),
                canKillBoss('ip'),
        )),
-       mm: fromBool(canEnterMM),
        'mm-lobby-chest': fromBool(or(hasBig('mm'), hasSmall('mm'))),
        'mm-compass-chest': fromBool(and(canTorch, hasSmall('mm', 3))),
        'mm-big-key-chest': fromBool(and(canTorch, hasSmall('mm', 3))),
        'mm-big-chest': fromBool(hasBig('mm')),
        'mm-map-chest': fromBool(or(hasBig('mm'), hasSmall('mm'))),
        'mm-boss-defeated': fromBool(and(hasBig('mm'), canDarkRoom, hasSomaria, canKillBoss('mm'))),
-       tr: fromBool(or(canEnterTRFront, canEnterTRWest, canEnterTREast, canEnterTRBack)),
        'tr-roller-left': fromBool(and(hasFireRod, hasSomaria, or(
                canEnterTRFront,
                and(or(canEnterTRWest, canEnterTREast), hasSmall('tr', 4)),
@@ -679,4 +697,250 @@ Logic.open = {
        )),
 };
 
+Logic.open = {
+       fallback: fromBool(alwaysAvailable),
+       aginah: fromBool(canBomb),
+       blacksmith: fromBool(canRescueSmith),
+       'blinds-hut-top': fromBool(canBomb),
+       'bombos-tablet': fromBool(and(southDarkWorld, hasMirror, canTablet)),
+       'bonk-rocks': fromBool(canBonk),
+       brewery: fromBool(and(westDarkWorld, canBomb, hasMoonpearl)),
+       'bumper-cave': fromBool(and(westDarkWorld, hasMoonpearl, canLift, hasCape)),
+       'c-house': fromBool(and(westDarkWorld, hasMoonpearl)),
+       catfish: fromBool(and(eastDarkWorld, hasMoonpearl)),
+       'cave-45': fromBool(and(southDarkWorld, hasMirror)),
+       checkerboard: fromBool(and(mireArea, hasMirror)),
+       'chest-game': fromBool(and(westDarkWorld, hasMoonpearl)),
+       'chicken-house': fromBool(canBomb),
+       'desert-ledge': fromBool(canEnterDPFront),
+       'digging-game': fromBool(and(southDarkWorld, hasMoonpearl)),
+       'ether-tablet': fromBool(and(northDeathMountain, canTablet)),
+       'floating-island': fromBool(
+               and(eastDarkDeathMountain, hasMoonpearl, canLift, canBomb, hasMirror),
+       ),
+       'flute-spot': fromBool(hasShovel),
+       'graveyard-ledge': fromBool(and(westDarkWorld, hasMoonpearl, hasMirror)),
+       'hammer-pegs': fromBool(and(westDarkWorld, hasHammer, hasMoonpearl, canHeavyLift)),
+       hobo: fromBool(canSwim),
+       'hookshot-cave-tl': fromBool(and(eastDarkDeathMountain, hasMoonpearl, canLift, hasHookshot)),
+       'hookshot-cave-tr': fromBool(and(eastDarkDeathMountain, hasMoonpearl, canLift, hasHookshot)),
+       'hookshot-cave-bl': fromBool(and(eastDarkDeathMountain, hasMoonpearl, canLift, hasHookshot)),
+       'hookshot-cave-br': fromBool(
+               and(eastDarkDeathMountain, hasMoonpearl, canLift, or(hasHookshot, canBonk)),
+       ),
+       'hype-cave-npc': fromBool(and(southDarkWorld, hasMoonpearl, canBomb)),
+       'hype-cave-top': fromBool(and(southDarkWorld, hasMoonpearl, canBomb)),
+       'hype-cave-right': fromBool(and(southDarkWorld, hasMoonpearl, canBomb)),
+       'hype-cave-left': fromBool(and(southDarkWorld, hasMoonpearl, canBomb)),
+       'hype-cave-bottom': fromBool(and(southDarkWorld, hasMoonpearl, canBomb)),
+       'ice-rod-cave': fromBool(canBomb),
+       'kak-well-top': fromBool(canBomb),
+       'kings-tomb': fromBool(and(canBonk, or(canHeavyLift, and(westDarkWorld, hasMirror)))),
+       'lake-hylia-island': fromBool(
+               and(canSwim, hasMirror, hasMoonpearl, or(eastDarkWorld, southDarkWorld)),
+       ),
+       library: fromBool(canBonk),
+       lumberjack: fromBool(and(canBonk, agaDead)),
+       'magic-bat': fromBool(and(hasPowder,
+               or(hasHammer, and(westDarkWorld, hasMoonpearl, canHeavyLift, hasMirror)),
+       )),
+       'mimic-cave': fromBool(and(canEnterTRFront, canBomb, hasSmall('tr', 2), hasMirror, hasHammer)),
+       'mini-moldorm-left': fromBool(canBomb),
+       'mini-moldorm-right': fromBool(canBomb),
+       'mini-moldorm-far-left': fromBool(canBomb),
+       'mini-moldorm-far-right': fromBool(canBomb),
+       'mini-moldorm-npc': fromBool(canBomb),
+       'mire-shed-left': fromBool(and(mireArea, hasMoonpearl)),
+       'mire-shed-right': fromBool(and(mireArea, hasMoonpearl)),
+       'old-man': fromBool(and(westDeathMountain, canDarkRoom)),
+       'paradox-lower-far-left': fromBool(paradoxLower),
+       'paradox-lower-left': fromBool(paradoxLower),
+       'paradox-lower-right': fromBool(paradoxLower),
+       'paradox-lower-far-right': fromBool(paradoxLower),
+       'paradox-lower-mid': fromBool(paradoxLower),
+       'paradox-upper-left': fromBool(and(eastDeathMountain, canBomb)),
+       'paradox-upper-right': fromBool(and(eastDeathMountain, canBomb)),
+       pedestal: fromBool(hasPendants(3)),
+       'potion-shop': fromBool(hasMushroom),
+       'purple-chest': fromBool(and(canRescueSmith, hasMoonpearl, canHeavyLift)),
+       pyramid: fromBool(eastDarkWorld),
+       'pyramid-fairy-left': fromBool(and(hasRedCrystals(2), southDarkWorld, canBridgeRedBomb)),
+       'pyramid-fairy-right': fromBool(and(hasRedCrystals(2), southDarkWorld, canBridgeRedBomb)),
+       'race-game': fromBool(or(canBomb, canBonk)),
+       saha: fromBool(hasGreenPendant),
+       'saha-left': fromBool(or(canBomb, canBonk)),
+       'saha-mid': fromBool(or(canBomb, canBonk)),
+       'saha-right': fromBool(or(canBomb, canBonk)),
+       'sick-kid': fromBool(hasBottle(1)),
+       'spec-rock': fromBool(and(westDeathMountain, hasMirror)),
+       'spec-rock-cave': fromBool(westDeathMountain),
+       'spike-cave': fromBool(and(
+               westDarkDeathMountain,
+               hasMoonpearl,
+               hasHammer,
+               canLift,
+               or(hasByrna, and(hasCape, hasMagicBars(2))),
+       )),
+       'spiral-cave': fromBool(eastDeathMountain),
+       stumpy: fromBool(and(southDarkWorld, hasMoonpearl)),
+       'super-bunny-top': fromBool(and(eastDarkDeathMountain, hasMoonpearl)),
+       'super-bunny-bottom': fromBool(and(eastDarkDeathMountain, hasMoonpearl)),
+       'waterfall-fairy-left': fromBool(canSwim),
+       'waterfall-fairy-right': fromBool(canSwim),
+       zora: fromBool(or(canLift, canSwim)),
+       'zora-ledge': fromBool(canSwim),
+       ct: fromBool(canEnterCT),
+       gt: fromBool(canEnterGT),
+       dp: fromBool(or(canEnterDPFront, canEnterDPBack)),
+       th: fromBool(canEnterTH),
+       pd: fromBool(canEnterPD),
+       sp: fromBool(canEnterSP),
+       sw: fromBool(or(canEnterSWFront, canEnterSWMid, canEnterSWBack)),
+       tt: fromBool(canEnterTT),
+       ip: fromBool(canEnterIP),
+       mm: fromBool(canEnterMM),
+       tr: fromBool(or(canEnterTRFront, canEnterTRWest, canEnterTREast, canEnterTRBack)),
+       ...Logic.dungeonInterior,
+};
+
+Logic.inverted = {
+       fallback: fromBool(alwaysAvailable),
+       aginah: fromBool(and(southLightWorld, hasMoonpearl, canBomb)),
+       blacksmith: fromBool(and(or(canHeavyLift, hasMirror), westLightWorld)),
+       'blinds-hut-top': fromBool(and(westLightWorld, hasMoonpearl, canBomb)),
+       'blinds-hut-far-left': fromBool(and(westLightWorld, hasMoonpearl)),
+       'blinds-hut-left': fromBool(and(westLightWorld, hasMoonpearl)),
+       'blinds-hut-right': fromBool(and(westLightWorld, hasMoonpearl)),
+       'blinds-hut-far-right': fromBool(and(westLightWorld, hasMoonpearl)),
+       'bombos-tablet': fromBool(and(southLightWorld, canTablet)),
+       'bonk-rocks': fromBool(and(westLightWorld, hasMoonpearl, canBonk)),
+       'bottle-vendor': fromBool(westLightWorld),
+       brewery: fromBool(canBomb),
+       'bumper-cave': fromBool(and(canLift, hasCape, hasMoonpearl, hasMirror, westLightWorld)),
+       catfish: fromBool(or(
+               and(eastDarkWorld, canLift),
+               and(hasMirror, southLightWorld, hasMoonpearl, canSwim),
+       )),
+       'cave-45': fromBool(and(southLightWorld, hasMoonpearl)),
+       checkerboard: fromBool(and(southLightWorld, hasMoonpearl, canLift)),
+       'chicken-house': fromBool(and(westLightWorld, hasMoonpearl, canBomb)),
+       'desert-ledge': fromBool(and(hasMoonpearl, canEnterDPFront)),
+       'ether-tablet': fromBool(and(northDeathMountain, canTablet)),
+       'floating-island': fromBool(and(eastDeathMountain)),
+       'flooded-chest': fromBool(and(southLightWorld, hasMoonpearl)),
+       'flute-spot': fromBool(and(southLightWorld, hasMoonpearl, hasShovel)),
+       'graveyard-ledge': fromBool(and(westLightWorld, hasMoonpearl)),
+       'hammer-pegs': fromBool(and(hasHammer, or(canHeavyLift, and(westLightWorld, hasMirror)))),
+       hobo: fromBool(and(southLightWorld, hasMoonpearl, canSwim)),
+       'hookshot-cave-tl': fromBool(and(
+               eastDarkDeathMountain,
+               hasHookshot,
+               or(canLift, and(canBomb, hasMirror, eastDeathMountain)),
+       )),
+       'hookshot-cave-tr': fromBool(and(
+               eastDarkDeathMountain,
+               hasHookshot,
+               or(canLift, and(canBomb, hasMirror, eastDeathMountain)),
+       )),
+       'hookshot-cave-bl': fromBool(and(
+               eastDarkDeathMountain,
+               hasHookshot,
+               or(canLift, and(canBomb, hasMirror, eastDeathMountain)),
+       )),
+       'hookshot-cave-br': fromBool(and(
+               eastDarkDeathMountain,
+               or(canBonk, hasHookshot),
+               or(canLift, and(canBomb, hasMirror, eastDeathMountain)),
+       )),
+       'hype-cave-npc': fromBool(canBomb),
+       'hype-cave-top': fromBool(canBomb),
+       'hype-cave-right': fromBool(canBomb),
+       'hype-cave-left': fromBool(canBomb),
+       'hype-cave-bottom': fromBool(canBomb),
+       'ice-rod-cave': fromBool(and(southLightWorld, hasMoonpearl, canBomb)),
+       'kak-well-top': fromBool(and(westLightWorld, hasMoonpearl, canBomb)),
+       'kak-well-left': fromBool(and(westLightWorld, hasMoonpearl)),
+       'kak-well-mid': fromBool(and(westLightWorld, hasMoonpearl)),
+       'kak-well-right': fromBool(and(westLightWorld, hasMoonpearl)),
+       'kak-well-bottom': fromBool(and(westLightWorld, hasMoonpearl)),
+       'kings-tomb': fromBool(and(westLightWorld, hasMoonpearl, canBonk, canHeavyLift)),
+       'lake-hylia-island': fromBool(
+               and(southLightWorld, hasMoonpearl, canSwim),
+       ),
+       library: fromBool(and(southLightWorld, hasMoonpearl, canBonk)),
+       lumberjack: fromBool(and(westLightWorld, hasMoonpearl, canBonk, agaDead)),
+       'lost-woods-hideout': fromBool(and(westLightWorld, hasMoonpearl)),
+       'magic-bat': fromBool(and(westLightWorld, hasMoonpearl, hasPowder, hasHammer)),
+       'maze-race': fromBool(and(southLightWorld, hasMoonpearl, or(canBomb, canBonk))),
+       'mimic-cave': fromBool(and(
+               eastDeathMountain,
+               hasMoonpearl,
+               hasHammer,
+       )),
+       'mini-moldorm-far-left': fromBool(and(southLightWorld, hasMoonpearl, canBomb, canKill)),
+       'mini-moldorm-left': fromBool(and(southLightWorld, hasMoonpearl, canBomb, canKill)),
+       'mini-moldorm-right': fromBool(and(southLightWorld, hasMoonpearl, canBomb, canKill)),
+       'mini-moldorm-far-right': fromBool(and(southLightWorld, hasMoonpearl, canBomb, canKill)),
+       'mini-moldorm-npc': fromBool(and(southLightWorld, hasMoonpearl, canBomb, canKill)),
+       'mire-shed-left': fromBool(mireArea),
+       'mire-shed-right': fromBool(mireArea),
+       'mushroom-spot': fromBool(and(westLightWorld, hasMoonpearl)),
+       'old-man': fromBool(and(westDeathMountain, canDarkRoom)),
+       'paradox-lower-far-left': fromBool(and(hasMoonpearl, paradoxLower)),
+       'paradox-lower-left': fromBool(and(hasMoonpearl, paradoxLower)),
+       'paradox-lower-mid': fromBool(and(hasMoonpearl, paradoxLower)),
+       'paradox-lower-right': fromBool(and(hasMoonpearl, paradoxLower)),
+       'paradox-lower-far-right': fromBool(and(hasMoonpearl, paradoxLower)),
+       'paradox-upper-left': fromBool(and(eastDeathMountain, hasMoonpearl, canBomb)),
+       'paradox-upper-right': fromBool(and(eastDeathMountain, hasMoonpearl, canBomb)),
+       pedestal: fromBool(and(westLightWorld, hasPendants(3))),
+       'potion-shop': fromBool(and(eastLightWorld, hasMushroom, hasMoonpearl)),
+       'purple-chest': fromBool(and(or(canHeavyLift, hasMirror), westLightWorld, southLightWorld)),
+       pyramid: fromBool(eastDarkWorld),
+       'pyramid-fairy-left': fromBool(and(hasRedCrystals(2), hasMirror)),
+       'pyramid-fairy-right': fromBool(and(hasRedCrystals(2), hasMirror)),
+       'race-game': fromBool(and(westLightWorld, hasMoonpearl, or(canBomb, canBonk))),
+       saha: fromBool(and(eastLightWorld, hasGreenPendant)),
+       'saha-left': fromBool(and(eastLightWorld, hasMoonpearl, or(canBomb, canBonk))),
+       'saha-mid': fromBool(and(eastLightWorld, hasMoonpearl, or(canBomb, canBonk))),
+       'saha-right': fromBool(and(eastLightWorld, or(canBomb, canBonk))),
+       'secret-passage': fromBool(and(eastLightWorld, hasMoonpearl)),
+       'sick-kid': fromBool(and(westLightWorld, hasBottle(1))),
+       'spec-rock': fromBool(northDeathMountain),
+       'spec-rock-cave': fromBool(westDeathMountain),
+       'spike-cave': fromBool(and(
+               westDarkDeathMountain,
+               hasHammer,
+               canLift,
+               or(hasByrna, and(hasCape, hasMagicBars(2))),
+       )),
+       'spiral-cave': fromBool(and(
+               eastDeathMountain,
+               hasMoonpearl,
+       )),
+       'sunken-treasure': fromBool(and(southLightWorld, hasMoonpearl)),
+       'super-bunny-top': fromBool(eastDarkDeathMountain),
+       'super-bunny-bottom': fromBool(eastDarkDeathMountain),
+       tavern: fromBool(and(westLightWorld, hasMoonpearl)),
+       uncle: fromBool(and(eastLightWorld, hasMoonpearl)),
+       'waterfall-fairy-left': fromBool(and(eastLightWorld, hasMoonpearl, canSwim)),
+       'waterfall-fairy-right': fromBool(and(eastLightWorld, hasMoonpearl, canSwim)),
+       zora: fromBool(and(eastLightWorld, hasMoonpearl, or(canLift, canSwim))),
+       'zora-ledge': fromBool(and(eastLightWorld, hasMoonpearl, canSwim)),
+       hc: fromBool(canEnterHC),
+       ct: fromBool(canEnterCT),
+       gt: fromBool(canEnterGT),
+       ep: fromBool(canEnterEP),
+       dp: fromBool(canEnterDPFront),
+       th: fromBool(canEnterTH),
+       pd: fromBool(canEnterPD),
+       sp: fromBool(canEnterSP),
+       sw: fromBool(or(canEnterSWFront, canEnterSWMid, canEnterSWBack)),
+       tt: fromBool(canEnterTT),
+       ip: fromBool(canEnterIP),
+       mm: fromBool(canEnterMM),
+       tr: fromBool(or(canEnterTRFront, canEnterTRWest, canEnterTREast, canEnterTRBack)),
+       ...Logic.dungeonInterior,
+};
+
 export default Logic;
index 8b210d2632f266c94e629d204ec2715436badfe9..20894332d669271b2b7508abe74145544b20664f 100644 (file)
@@ -1573,11 +1573,18 @@ export const UNDERWORLD_LOCATIONS = [
        },
 ];
 
+export const getConfigValue = (config, name, fallback) =>
+       Object.prototype.hasOwnProperty.call(config, name) ? config[name] : fallback;
+
 export const applyLogic = (config, dungeons, state) => {
        const logic = Logic[config.worldState];
        const map = {};
        for (const name in logic) {
-               map[name] = logic[name](config, dungeons, state);
+               try {
+                       map[name] = logic[name](config, dungeons, state);
+               } catch (e) {
+                       console.error('error evaluating', name, e);
+               }
        }
        return map;
 };
@@ -1675,9 +1682,9 @@ export const aggregateLocationStatus = (names, logic, state) => {
 export const countRemainingLocations = (state, locations) =>
        locations.reduce((acc, cur) => state[cur] ? acc : acc + 1, 0);
 
-export const getGanonCrystals = (state) => state['ganon-crystals'];
+export const getGanonCrystals = (config) => getConfigValue(config, 'ganon-crystals', 7);
 
-export const getGTCrystals = (state) => state['gt-crystals'];
+export const getGTCrystals = (config) => getConfigValue(config, 'gt-crystals', 7);
 
 export const getGTBoss = (state, which) => state[`gt-${which}-boss`];
 
@@ -1685,7 +1692,9 @@ export const hasDungeonBoss = (state, dungeon) =>
        !dungeon.boss || !!state[`${dungeon.id}-boss-defeated`];
 
 export const getDungeonBoss = (state, dungeon) =>
-       state[`${dungeon.id}-boss`] || dungeon.boss || null;
+       dungeon.bosses.length > 1
+               ? state[`${dungeon.id}-boss`] || dungeon.boss || null
+               : dungeon.bosses[0];
 
 export const hasDungeonPrize = (state, dungeon) =>
        !dungeon.prize || !!state[`${dungeon.id}-prize-acquired`];
@@ -1776,8 +1785,6 @@ export const makeEmptyState = () => {
        });
        state['mm-medallion'] = null;
        state['tr-medallion'] = null;
-       state['gt-crystals'] = 7;
-       state['ganon-crystals'] = 7;
        return state;
 };
 
@@ -1917,6 +1924,9 @@ export const mergeStates = (autoState, manualState) => {
                if (manualState[`${dungeon.id}-map`]) {
                        next[`${dungeon.id}-map`] = true;
                }
+               if (manualState[`${dungeon.id}-boss`]) {
+                       next[`${dungeon.id}-boss`] = manualState[`${dungeon.id}-boss`];
+               }
                if (manualState[`${dungeon.id}-boss-defeated`]) {
                        next[`${dungeon.id}-boss-defeated`] = true;
                }
index 4004152eba0db9b53d975a3ea552cf37a3d508c9..ec1e8ce25c2a6725c5ebbd726377eb6a890755ad 100644 (file)
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
 import React from 'react';
 
 import {
+       BOSSES,
        CONFIG,
        DUNGEONS,
        applyLogic,
@@ -51,8 +52,8 @@ export const TrackerProvider = ({ children }) => {
                        if (config.wildBig && dungeon.bk && !dungeon.dropBk) {
                                ++newDungeon.items;
                        }
-                       if (!config.bossShuffle && dungeon.boss) {
-                               newDungeon.bosses = [dungeon.boss];
+                       if (dungeon.boss) {
+                               newDungeon.bosses = config.bossShuffle ? BOSSES : [dungeon.boss];
                        }
                        return newDungeon;
                });
index 87b6198469a421d2b6da332ea858f1a7274adb2e..9647da576d49e581c23f015285774cb831946af0 100644 (file)
@@ -502,6 +502,28 @@ export default {
                },
                tracker: {
                        config: {
+                               bossShuffle: 'Boss Shuffle',
+                               ganonCrystals: 'Ganon Crystals',
+                               glitches: 'Glitches',
+                               glitchRules: {
+                                       hmg: 'HMG',
+                                       mg: 'Major',
+                                       nl: 'No Logic',
+                                       none: 'Keine',
+                                       owg: 'OWG',
+                               },
+                               goal: 'Ziel',
+                               goals: {
+                                       ad: 'All Dungeons',
+                                       fast: 'Fast Ganon',
+                                       ganon: 'Ganon',
+                                       ghunt: 'Ganon Hunt',
+                                       ped: 'Pedestal',
+                                       thunt: 'Triforce Hunt',
+                                       trinity: 'Trinity',
+                               },
+                               gtCrystals: 'GT Crystals',
+                               logic: 'Logik',
                                showBig: 'Big Keys',
                                showCompass: 'Kompanden',
                                showItemOptions: {
@@ -518,6 +540,11 @@ export default {
                                wildItems: 'Wild Dungeon Items',
                                wildMap: 'Maps',
                                wildSmall: 'Small Keys',
+                               worldState: 'World State',
+                               worldStates: {
+                                       inverted: 'Inverted',
+                                       open: 'Open',
+                               },
                        },
                        location: {
                                aginah: 'Aginah',
index 9ca7a6b9148a9fea5f0eeeec09c8504427133798..7554f6ed4c2013992d5a2235e23116121b46acde 100644 (file)
@@ -502,6 +502,28 @@ export default {
                },
                tracker: {
                        config: {
+                               bossShuffle: 'Boss Shuffle',
+                               ganonCrystals: 'Ganon Crystals',
+                               glitches: 'Glitches',
+                               glitchRules: {
+                                       hmg: 'HMG',
+                                       mg: 'Major',
+                                       nl: 'No Logic',
+                                       none: 'None',
+                                       owg: 'OWG',
+                               },
+                               goal: 'Goal',
+                               goals: {
+                                       ad: 'All Dungeons',
+                                       fast: 'Fast Ganon',
+                                       ganon: 'Ganon',
+                                       ghunt: 'Ganon Hunt',
+                                       ped: 'Pedestal',
+                                       thunt: 'Triforce Hunt',
+                                       trinity: 'Trinity',
+                               },
+                               gtCrystals: 'GT Crystals',
+                               logic: 'Logic',
                                showBig: 'Big Keys',
                                showCompass: 'Compasses',
                                showItemOptions: {
@@ -518,6 +540,11 @@ export default {
                                wildItems: 'Wild Dungeon Items',
                                wildMap: 'Maps',
                                wildSmall: 'Small Keys',
+                               worldState: 'World State',
+                               worldStates: {
+                                       inverted: 'Inverted',
+                                       open: 'Open',
+                               },
                        },
                        location: {
                                aginah: 'Aginah',
index 8fe76bbf588ac5c871dff2d3690e0b2ed34441f6..0d61af3f5605901eda06ee3f67b8b8cf17d09a63 100644 (file)
@@ -271,8 +271,6 @@ h1 {
                display: inline-block;
                width: 100%;
                height: 100%;
-               background: url(/items-v1.png);
-               background-size: 800% 1100%;
        }
        .strike {
                position: absolute;
index c4541e25de993928fd577b3c150c0eb8f214ea1f..27bd55e9da9f16e6302a623a266a594c3438fd6a 100644 (file)
                }
        }
        .tracker-toolbar {
+               label .zelda-icon,
                .toggle-icon {
                        display: inline-block;
                        width: 2em;
                        height: 2em;
                }
+               .form-select {
+                       background-image: none;
+                       padding-right: 0.75rem;
+                       border: none;
+               }
        }
        .zelda-icon {
                width: 100%;