]> git.localhorst.tv Git - alttp.git/commitdiff
simple logic tracking
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Wed, 27 Mar 2024 22:59:28 +0000 (23:59 +0100)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Wed, 27 Mar 2024 22:59:28 +0000 (23:59 +0100)
resources/js/components/tracker/Map.js
resources/js/helpers/logic.js [new file with mode: 0644]
resources/js/helpers/tracker.js
resources/js/hooks/tracker.js
resources/sass/tracker.scss

index b77612ef65bd02ae9eb481df3c8e90b28daf4bb1..3ee4b2f81458ab4eeb6190273623467406788bc1 100644 (file)
@@ -4,12 +4,13 @@ import { useTranslation } from 'react-i18next';
 
 import {
        addDungeonCheck,
+       aggregateDungeonStatus,
+       aggregateLocationStatus,
        clearAll,
        completeDungeonChecks,
        countRemainingLocations,
        getDungeonClearedItems,
        getDungeonRemainingItems,
-       hasClearedLocations,
        hasDungeonBoss,
        hasDungeonPrize,
        isDungeonCleared,
@@ -686,15 +687,12 @@ const makeBackground = (src, level) => {
 };
 
 const Map = () => {
-       const { dungeons, setManualState, state } = useTracker();
+       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);
-               let status = 'available';
-               if (isDungeonCleared(state, definition)) {
-                       status = 'cleared';
-               }
+               const status = aggregateDungeonStatus(definition, logic, state);
                return {
                        ...dungeon,
                        status,
@@ -745,14 +743,11 @@ const Map = () => {
                                }
                        },
                };
-       }, [dungeons, setManualState, state]);
+       }, [dungeons, logic, setManualState, state]);
 
        const mapLocation = React.useCallback(loc => {
                const remaining = countRemainingLocations(state, loc.checks);
-               let status = 'available';
-               if (hasClearedLocations(state, loc.checks)) {
-                       status = 'cleared';
-               }
+               const status = aggregateLocationStatus(loc.checks, logic, state);
                return {
                        ...loc,
                        remaining,
@@ -772,7 +767,7 @@ const Map = () => {
                                }
                        },
                };
-       }, [setManualState, state]);
+       }, [logic, setManualState, state]);
 
        const lwDungeons = React.useMemo(() => LW_DUNGEONS.map(mapDungeon), [mapDungeon]);
        const lwLocations = React.useMemo(() => LW_LOCATIONS.map(mapLocation), [mapLocation]);
diff --git a/resources/js/helpers/logic.js b/resources/js/helpers/logic.js
new file mode 100644 (file)
index 0000000..2484dc0
--- /dev/null
@@ -0,0 +1,679 @@
+import {
+       getDungeonBoss,
+       getDungeonPrize,
+       getGTBoss,
+       getGTCrystals,
+       hasDungeonBoss,
+       hasDungeonPrize,
+} from './tracker';
+
+const and = (...predicates) => (...args) =>
+       predicates.reduce((acc, cur) => acc && cur(...args), true);
+
+const or = (...predicates) => (...args) =>
+       predicates.reduce((acc, cur) => acc || cur(...args), false);
+
+const fromBool = b => (...args) => b(...args) ? 'available' : 'unavailable';
+
+const agaDead = (config, dungeons, state) =>
+       hasDungeonBoss(state, dungeons.find(d => d.id === 'ct'));
+
+const countCrystals = (config, dungeons, state) => dungeons
+       .filter(dungeon =>
+               (getDungeonPrize(state, dungeon) || 'crystal').endsWith('crystal') &&
+               dungeon.prize &&
+               hasDungeonPrize(state, dungeon)
+       ).length;
+
+const countRedCrystals = (config, dungeons, state) => dungeons
+       .filter(dungeon =>
+               getDungeonPrize(state, dungeon) === 'red-crystal' &&
+               hasDungeonPrize(state, dungeon)
+       ).length;
+
+const hasRedCrystals = n => (...args) => countRedCrystals(...args) >= n;
+
+const hasGTCrystals = (config, dungeons, state) =>
+       countCrystals(config, dungeons, state) >= getGTCrystals(state);
+
+const countPendants = (config, dungeons, state) => dungeons
+       .filter(dungeon =>
+               (getDungeonPrize(state, dungeon) || '').endsWith('pendant') &&
+               hasDungeonPrize(state, dungeon)
+       ).length;
+
+const hasGreenPendant = (config, dungeons, state) => dungeons
+       .filter(dungeon =>
+               getDungeonPrize(state, dungeon) === 'green-pendant' &&
+               hasDungeonPrize(state, dungeon)
+       ).length >= 1;
+
+const hasPendants = n => (...args) => countPendants(...args) >= n;
+
+// Equipment
+
+const hasBig = dungeon => (config, dungeons, state) =>
+       !config.wildBig || !!state[`${dungeon}-big-key`];
+
+const hasBird = (config, dungeons, state) => !!state['bird'];
+
+const hasBombos = (config, dungeons, state) => !!state['bombos'];
+
+const hasBook = (config, dungeons, state) => !!state['book'];
+
+const hasBoom = (config, dungeons, state) => !!(state['blue-boomerang'] || state['red-boomerang']);
+
+const hasBoots = (config, dungeons, state) => !!state['boots'];
+
+const hasBottle = n => (config, dungeons, state) => state['bottle'] >= (n || 1);
+
+const hasBow = (config, dungeons, state) => !!state['bow'];
+
+const hasBugnet = (config, dungeons, state) => !!state['bugnet'];
+
+const hasByrna = (config, dungeons, state) => !!state['byrna'];
+
+const hasCape = (config, dungeons, state) => !!state['cape'];
+
+const hasFireRod = (config, dungeons, state) => !!state['fire-rod'];
+
+const hasFlute = (config, dungeons, state) => !!state['flute'];
+
+const hasHammer = (config, dungeons, state) => !!state['hammer'];
+
+const hasHookshot = (config, dungeons, state) => !!state['hookshot'];
+
+const hasIceRod = (config, dungeons, state) => !!state['ice-rod'];
+
+const hasLamp = (config, dungeons, state) => !!state['lamp'];
+
+const hasMagicBars = n => (config, dungeons, state) => {
+       let bars = 1 + (state['bottle'] || 0);
+       if (state['half-magic']) {
+               bars *= 2;
+       }
+       if (state['quarter-magic']) {
+               bars *= 2;
+       }
+       return bars >= (n || 1);
+};
+
+const hasMirror = (config, dungeons, state) => !!state['mirror'];
+
+const hasMMMedallion = (config, dungeons, state) =>
+       !!state['mm-medallion'] && !!state[state['mm-medallion']];
+
+const hasMoonpearl = (config, dungeons, state) => !!state['moonpearl'];
+
+const hasMushroom = (config, dungeons, state) => !!state['mushroom'];
+
+const hasPowder = (config, dungeons, state) => !!state['powder'];
+
+const hasQuake = (config, dungeons, state) => !!state['quake'];
+
+const hasShovel = (config, dungeons, state) => !!state['shovel'];
+
+const hasSmall = (dungeon, amount) => (config, dungeons, state) =>
+       !config.wildSmall || state[`${dungeon}-small-key`] >= (amount || 1);
+
+const hasSomaria = (config, dungeons, state) => !!state['somaria'];
+
+const hasSword = n => (config, dungeons, state) => state['sword'] >= (n || 1);
+
+const hasTRMedallion = (config, dungeons, state) =>
+       !!state['tr-medallion'] && !!state[state['tr-medallion']];
+
+// Abilities
+
+const canActivateFlute = () => true;
+
+const canBomb = () => true;
+
+const canBonk = hasBoots;
+
+const canDarkRoom = hasLamp;
+
+const canFlipSwitches = or(
+       canBomb,
+       hasBoom,
+       canShootArrows,
+       hasSword(),
+       hasHammer,
+       hasHookshot,
+       hasSomaria,
+       hasByrna,
+       hasFireRod,
+       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 canKill = damage => damage && damage < 6
+       ? or(hasBow, hasFireRod, hasHammer, hasSomaria, hasSword(1), canBomb, hasByrna)
+       : or(hasBow, hasFireRod, hasHammer, hasSomaria, hasSword(1));
+
+const canMedallion = hasSword();
+
+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));
+
+const canTorch = or(hasFireRod, hasLamp);
+
+const canTorchDarkRoom = or(canDarkRoom, canTorch);
+
+// Regions
+
+const westDeathMountain = or(
+       canFly,
+       and(canLift, canDarkRoom),
+);
+
+const eastDeathMountain = and(
+       westDeathMountain,
+       or(
+               hasHookshot,
+               and(hasHammer, hasMirror),
+       ),
+);
+
+const northDeathMountain = and(
+       westDeathMountain,
+       or(
+               hasMirror,
+               and(hasHammer, hasHookshot),
+       ),
+);
+
+const eastDarkDeathMountain = and(
+       eastDeathMountain,
+       canHeavyLift,
+);
+
+const westDarkDeathMountain = westDeathMountain;
+
+const eastDarkWorld = and(
+       hasMoonpearl,
+       or(
+               agaDead,
+               canHeavyLift,
+               and(canLift, hasHammer),
+       ),
+);
+
+const westDarkWorld = and(
+       hasMoonpearl,
+       or(
+               and(canLift, hasHammer),
+               canHeavyLift,
+               and(eastDarkWorld, hasHookshot, or(canSwim, canLift, hasHammer)),
+       ),
+);
+
+const southDarkWorld = or(
+       westDarkWorld,
+       and(eastDarkWorld, hasMoonpearl, hasHammer),
+);
+
+const mireArea = and(canFly, canHeavyLift);
+
+// Bosses
+
+const BOSS_RULES = {
+       aga: or(hasBugnet, hasHammer, hasSword()),
+       armos: or(
+               hasSword(), hasHammer, canShootArrows, hasBoom,
+               and(hasMagicBars(4), or(hasFireRod, hasIceRod)),
+               and(hasMagicBars(2), or(hasByrna, hasSomaria)),
+       ),
+       arrghus: and(hasHookshot, or(
+               hasSword(),
+               hasHammer,
+               and(or(hasMagicBars(2), canShootArrows), or(hasFireRod, hasIceRod)),
+       )),
+       blind: or(hasSword(), hasHammer, hasSomaria, hasByrna),
+       helma: and(or(canBomb, hasHammer), or(hasSword(), canShootArrows)),
+       kholdstare: and(canMeltThings, or(
+               hasHammer,
+               hasSword(),
+               and(hasFireRod, hasMagicBars(3)),
+               and(hasFireRod, hasBombos, hasMagicBars(2), canMedallion),
+       )),
+       lanmolas: or(
+               hasSword(), hasHammer, canShootArrows, hasFireRod, hasIceRod, hasByrna, hasSomaria,
+       ),
+       moldorm: or(hasSword(), hasHammer),
+       mothula: or(
+               hasSword(),
+               hasHammer,
+               and(hasMagicBars(2), or(hasFireRod, hasSomaria, hasByrna)),
+               canGetGoodBee,
+       ),
+       trinexx: and(hasFireRod, hasIceRod, or(
+               hasSword(3),
+               hasHammer,
+               and(hasMagicBars(2), hasSword(2)),
+               and(hasMagicBars(4), hasSword(1)),
+       )),
+       vitreous: or(hasSword(), hasHammer, canShootArrows),
+};
+
+const canKillBoss = dungeon => (config, dungeons, state) => {
+       const boss = getDungeonBoss(state, dungeons.find(d => d.id === dungeon));
+       return BOSS_RULES[boss](config, dungeons, state);
+};
+
+const canKillGTBoss = which => (config, dungeons, state) => {
+       const boss = getGTBoss(state, which);
+       return BOSS_RULES[boss](config, dungeons, state);
+};
+
+// Dungeons
+
+const canEnterCT = or(hasCape, hasSword(2));
+
+const canEnterGT = and(eastDarkDeathMountain, hasGTCrystals, hasMoonpearl);
+
+const canEnterDPFront = or(hasBook, and(mireArea, hasMirror));
+const canEnterDPBack = or(and(canEnterDPFront, canLift), and(mireArea, hasMirror));
+
+const canEnterTH = northDeathMountain;
+
+const canEnterPD = and(eastDarkWorld, hasMoonpearl);
+
+const canEnterSP = and(southDarkWorld, hasMirror, hasMoonpearl, canSwim);
+
+const canEnterSWFront = and(westDarkWorld, hasMoonpearl);
+const canEnterSWMid = and(westDarkWorld, hasMoonpearl);
+const canEnterSWBack = and(westDarkWorld, hasMoonpearl, hasFireRod);
+
+const canEnterTT = and(westDarkWorld, hasMoonpearl);
+
+const canEnterIP = and(canSwim, canHeavyLift, hasMoonpearl, canMeltThings);
+const rightSideIP = or(hasHookshot, hasSmall('ip'));
+
+const canEnterMM = and(
+       mireArea,
+       hasMoonpearl,
+       hasMMMedallion,
+       canMedallion,
+       or(canBonk, hasHookshot),
+       canKill(8),
+);
+
+const canEnterTRFront = and(
+       eastDeathMountain,
+       canHeavyLift,
+       hasHammer,
+       hasMoonpearl,
+       canMedallion,
+       hasTRMedallion,
+);
+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 laserBridge = or(
+       and(
+               or(canEnterDPFront, canEnterTRWest, canEnterTREast),
+               canDarkRoom,
+               hasSomaria,
+               hasBig('tr'),
+               hasSmall('tr', 3),
+       ),
+       canEnterTRBack,
+);
+
+// Misc Macros
+
+const paradoxLower = and(eastDeathMountain, or(canBomb, hasBoom, canShootArrows, hasSomaria));
+
+const canBridgeRedBomb = or(
+       and(hasMoonpearl, hasHammer),
+       and(agaDead, hasMirror),
+);
+
+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),
+       '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))),
+       'gt-compass-bl': fromBool(and(hasSomaria, hasFireRod, hasSmall('gt', 4))),
+       'gt-compass-br': fromBool(and(hasSomaria, hasFireRod, hasSmall('gt', 4))),
+       'gt-torch': fromBool(canBonk),
+       'gt-dm-tl': fromBool(and(hasHammer, hasHookshot)),
+       'gt-dm-tr': fromBool(and(hasHammer, hasHookshot)),
+       'gt-dm-bl': fromBool(and(hasHammer, hasHookshot)),
+       'gt-dm-br': fromBool(and(hasHammer, hasHookshot)),
+       'gt-map-chest': fromBool(and(hasHammer, or(hasHookshot, canBonk), hasSmall('gt', 4))),
+       'gt-firesnake': fromBool(and(hasHammer, hasHookshot, hasSmall('gt', 3))),
+       'gt-rando-tl': fromBool(and(hasHammer, hasHookshot, hasSmall('gt', 4))),
+       'gt-rando-tr': fromBool(and(hasHammer, hasHookshot, hasSmall('gt', 4))),
+       'gt-rando-bl': fromBool(and(hasHammer, hasHookshot, hasSmall('gt', 4))),
+       'gt-rando-br': fromBool(and(hasHammer, hasHookshot, hasSmall('gt', 4))),
+       'gt-bobs-chest': fromBool(and(
+               or(and(hasHammer, hasHookshot), and(hasFireRod, hasSomaria)),
+               hasSmall('gt', 3),
+       )),
+       'gt-ice-left': fromBool(and(
+               or(and(hasHammer, hasHookshot), and(hasFireRod, hasSomaria)),
+               hasSmall('gt', 3),
+               canKillGTBoss('bot'),
+       )),
+       'gt-ice-mid': fromBool(and(
+               or(and(hasHammer, hasHookshot), and(hasFireRod, hasSomaria)),
+               hasSmall('gt', 3),
+               canKillGTBoss('bot'),
+       )),
+       'gt-ice-right': fromBool(and(
+               or(and(hasHammer, hasHookshot), and(hasFireRod, hasSomaria)),
+               hasSmall('gt', 3),
+               canKillGTBoss('bot'),
+       )),
+       'gt-big-chest': fromBool(and(
+               or(and(hasHammer, hasHookshot), and(hasFireRod, hasSomaria)),
+               hasSmall('gt', 3),
+               hasBig('gt'),
+       )),
+       'gt-helma-left': fromBool(and(
+               hasBig('gt'),
+               hasSmall('gt', 3),
+               canShootArrows,
+               canTorch,
+               canKillGTBoss('mid'),
+       )),
+       'gt-helma-right': fromBool(and(
+               hasBig('gt'),
+               hasSmall('gt', 3),
+               canShootArrows,
+               canTorch,
+               canKillGTBoss('mid'),
+       )),
+       'gt-pre-moldorm': fromBool(and(
+               hasBig('gt'),
+               hasSmall('gt', 3),
+               canShootArrows,
+               canTorch,
+               canKillGTBoss('mid'),
+       )),
+       'gt-post-moldorm': fromBool(and(
+               hasBig('gt'),
+               hasSmall('gt', 4),
+               canShootArrows,
+               canTorch,
+               canKillGTBoss('mid'),
+               canKillGTBoss('top'),
+               hasHookshot,
+       )),
+       'gt-boss-killable': fromBool(and(
+               hasBig('gt'),
+               hasSmall('gt', 4),
+               canShootArrows,
+               canTorch,
+               canKillGTBoss('mid'),
+               canKillGTBoss('top'),
+               hasHookshot,
+               canKillBoss('ct'),
+       )),
+       'ep-big-chest': fromBool(hasBig('ep')),
+       'ep-big-key-chest': fromBool(canDarkRoom),
+       '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'))),
+       'dp-map-chest': fromBool(canEnterDPFront),
+       'dp-torch': fromBool(and(canEnterDPFront, canBonk)),
+       'dp-boss-defeated': fromBool(and(
+               canEnterDPBack,
+               canTorch,
+               hasBig('dp'),
+               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)),
+       'th-compass-chest': fromBool(and(canFlipSwitches, hasBig('th'))),
+       'th-big-chest': fromBool(and(canFlipSwitches, hasBig('th'))),
+       'th-boss-defeated': fromBool(and(
+               canFlipSwitches,
+               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))),
+       'pd-arena-ledge': fromBool(canShootArrows),
+       'pd-map-chest': fromBool(canShootArrows),
+       'pd-compass-chest': fromBool(hasSmall('pd', 4)),
+       'pd-basement-left': fromBool(and(canTorchDarkRoom, hasSmall('pd', 4))),
+       'pd-basement-right': fromBool(and(canTorchDarkRoom, hasSmall('pd', 4))),
+       'pd-harmless-hellway': fromBool(hasSmall('pd', 6)),
+       'pd-maze-top': fromBool(and(canDarkRoom, hasSmall('pd', 6))),
+       'pd-maze-bottom': fromBool(and(canDarkRoom, hasSmall('pd', 6))),
+       'pd-big-chest': fromBool(and(canDarkRoom, hasBig('pd'), hasSmall('pd', 6))),
+       '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)),
+       'sp-west-chest': fromBool(and(hasSmall('sp'), hasHammer)),
+       'sp-big-key-chest': fromBool(and(hasSmall('sp'), hasHammer)),
+       'sp-flooded-left': fromBool(and(hasSmall('sp'), hasHammer, hasHookshot)),
+       '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),
+       'ip-freezor-chest': fromBool(canMeltThings),
+       'ip-big-chest': fromBool(hasBig('ip')),
+       'ip-boss-defeated': fromBool(and(
+               hasBig('ip'),
+               hasHammer,
+               canLift,
+               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)),
+               and(canEnterTRBack, canDarkRoom, hasSmall('tr', 4)),
+       ))),
+       'tr-roller-right': fromBool(and(hasFireRod, hasSomaria, or(
+               canEnterTRFront,
+               and(or(canEnterTRWest, canEnterTREast), hasSmall('tr', 4)),
+               and(canEnterTRBack, canDarkRoom, hasSmall('tr', 4)),
+       ))),
+       'tr-compass-chest': fromBool(and(hasSomaria, or(
+               canEnterTRFront,
+               and(or(canEnterTRWest, canEnterTREast), hasSmall('tr', 4)),
+               and(canEnterTRBack, canDarkRoom, hasSmall('tr', 4)),
+       ))),
+       'tr-chomps': fromBool(or(
+               and(canEnterTRFront, hasSmall('tr')),
+               canEnterTRWest,
+               canEnterTREast,
+               and(canEnterTRBack, canDarkRoom, hasSomaria),
+       )),
+       'tr-big-key-chest': fromBool(and(hasSmall('tr', 4), or(
+               and(canEnterTRFront, hasSomaria),
+               canEnterTRWest,
+               canEnterTREast,
+               and(canEnterTRBack, canDarkRoom, hasSomaria),
+       ))),
+       'tr-big-chest': fromBool(canEnterTREast),
+       'tr-crysta-roller': fromBool(or(
+               and(hasBig('tr'), or(
+                       and(canEnterTRFront, hasSomaria, hasSmall('tr', 2)),
+                       canEnterTRWest,
+                       canEnterTREast,
+               )),
+               and(canEnterTRBack, canDarkRoom, hasSomaria),
+       )),
+       'tr-laser-bridge-top': fromBool(laserBridge),
+       'tr-laser-bridge-left': fromBool(laserBridge),
+       'tr-laser-bridge-right': fromBool(laserBridge),
+       'tr-laser-bridge-bottom': fromBool(laserBridge),
+       'tr-boss-defeated': fromBool(and(
+               laserBridge,
+               hasSmall('tr', 4),
+               hasBig('tr'),
+               hasSomaria,
+               canKillBoss('tr'),
+       )),
+};
+
+export default Logic;
index e4b0ebe9f765a33d497ff2d994537a64168860ae..47a34f135bbd55e7ec3eb31f2efccc7a824eccfe 100644 (file)
@@ -7,6 +7,7 @@ import {
        isBossDefeated,
        isChestOpen,
 } from './alttp-ram';
+import Logic from './logic';
 
 export const BOOLEAN_STATES = [
        'blue-boomerang',
@@ -68,6 +69,8 @@ export const BOSSES = [
 ];
 
 export const CONFIG = {
+       bossShuffle: false,
+       glitches: 'none',
        showMap: 'situational',
        showCompass: 'situational',
        showSmall: 'always',
@@ -76,7 +79,7 @@ export const CONFIG = {
        wildCompass: false,
        wildSmall: false,
        wildBig: false,
-       bossShuffle: false,
+       worldState: 'open',
 };
 
 export const DUNGEONS = [
@@ -1556,6 +1559,15 @@ export const UNDERWORLD_LOCATIONS = [
        },
 ];
 
+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);
+       }
+       return map;
+};
+
 export const shouldShowDungeonItem = (config, which) => {
        const show = config[`show${which}`] || 'always';
        const wild = config[`wild${which}`] || false;
@@ -1622,9 +1634,39 @@ export const countClearedLocations = (state, locations) =>
 export const hasClearedLocations = (state, locations) =>
        countClearedLocations(state, locations) === locations.length;
 
+export const getLocationStatus = (name, logic, state) => {
+       if (state[name]) return 'cleared';
+       if (logic[name]) return logic[name];
+       return logic.fallback;
+};
+
+export const getCombinedStatus = statuses => {
+       if (statuses.filter(s => s === 'cleared').length === statuses.length) {
+               return 'cleared';
+       }
+       if (statuses.filter(s => ['available', 'cleared'].includes(s)).length === statuses.length) {
+               return 'available';
+       }
+       if (statuses.filter(s => s === 'unavailable').length === statuses.length) {
+               return 'unavailable';
+       }
+       return 'partial';
+};
+
+export const aggregateLocationStatus = (names, logic, state) => {
+       const statuses = names.map(name => getLocationStatus(name, logic, state));
+       return getCombinedStatus(statuses);
+};
+
 export const countRemainingLocations = (state, locations) =>
        locations.reduce((acc, cur) => state[cur] ? acc : acc + 1, 0);
 
+export const getGanonCrystals = (state) => state['ganon-crystals'];
+
+export const getGTCrystals = (state) => state['gt-crystals'];
+
+export const getGTBoss = (state, which) => state[`gt-${which}-boss`];
+
 export const hasDungeonBoss = (state, dungeon) =>
        !dungeon.boss || !!state[`${dungeon.id}-boss-defeated`];
 
@@ -1650,6 +1692,21 @@ export const isDungeonCleared = (state, dungeon) => {
        return hasItems && hasBoss && hasPrize;
 };
 
+export const aggregateDungeonStatus = (dungeon, logic, state) => {
+       if (isDungeonCleared(state, dungeon)) {
+               return 'cleared';
+       }
+       if (logic[dungeon.id] === 'unavailable') {
+               return 'unavailable';
+       }
+       const checks = [...dungeon.checks];
+       if (['ct', 'gt'].includes(dungeon.id)) {
+               checks.push(`${dungeon.id}-boss-killable`);
+       }
+       const statuses = checks.map(name => getLocationStatus(name, logic, state));
+       return getCombinedStatus(statuses);
+};
+
 export const toggleBossDefeated = dungeon => toggleBoolean(`${dungeon.id}-boss-defeated`);
 
 export const setBossDefeated = (dungeon, defeated) =>
@@ -1691,6 +1748,11 @@ export const makeEmptyState = () => {
                        state[`${dungeon.id}-prize`] = 'crystal';
                        state[`${dungeon.id}-prize-acquired`] = false;
                }
+               if (dungeon.id === 'gt') {
+                       state['gt-bot-boss'] = 'armos';
+                       state['gt-mid-boss'] = 'lanmolas';
+                       state['gt-top-boss'] = 'moldorm';
+               }
        });
        OVERWORLD_LOCATIONS.forEach(location => {
                state[location.id] = false;
@@ -1700,6 +1762,8 @@ export const makeEmptyState = () => {
        });
        state['mm-medallion'] = null;
        state['tr-medallion'] = null;
+       state['gt-crystals'] = 7;
+       state['ganon-crystals'] = 7;
        return state;
 };
 
@@ -1874,6 +1938,8 @@ export const mergeStates = (autoState, manualState) => {
        });
        next['mm-medallion'] = manualState['mm-medallion'];
        next['tr-medallion'] = manualState['tr-medallion'];
+       next['gt-crystals'] = manualState['gt-crystals'];
+       next['ganon-crystals'] = manualState['ganon-crystals'];
        //console.log(next);
        return next;
 };
index b77ee58dbf48233524feee501facad34a7242ab9..30bf2426a7a125781f2aa5579e9e9a389f74511e 100644 (file)
@@ -1,7 +1,13 @@
 import PropTypes from 'prop-types';
 import React from 'react';
 
-import { CONFIG, DUNGEONS, makeEmptyState, mergeStates } from '../helpers/tracker';
+import {
+       CONFIG,
+       DUNGEONS,
+       applyLogic,
+       makeEmptyState,
+       mergeStates,
+} from '../helpers/tracker';
 
 const context = React.createContext({});
 
@@ -13,6 +19,7 @@ export const TrackerProvider = ({ children }) => {
        const [autoState, setAutoState] = React.useState(makeEmptyState());
        const [manualState, setManualState] = React.useState(makeEmptyState());
        const [dungeons, setDungeons] = React.useState(DUNGEONS);
+       const [logic, setLogic] = React.useState({});
 
        const saveConfig = React.useCallback((values) => {
                setConfig(s => {
@@ -25,7 +32,7 @@ export const TrackerProvider = ({ children }) => {
        React.useEffect(() => {
                const savedConfig = localStorage.getItem('tracker.config');
                if (savedConfig) {
-                       setConfig(JSON.parse(savedConfig));
+                       setConfig(c => ({ ...c, ...JSON.parse(savedConfig) }));
                }
        }, []);
 
@@ -56,10 +63,15 @@ export const TrackerProvider = ({ children }) => {
                setState(mergeStates(autoState, manualState));
        }, [autoState, manualState]);
 
-       const value = React.useMemo(() => {
-               return { config, saveConfig, dungeons, setAutoState, setManualState, state };
+       React.useEffect(() => {
+               setLogic(applyLogic(config, dungeons, state));
        }, [config, dungeons, state]);
 
+       const value = React.useMemo(() => {
+               console.log(logic);
+               return { config, dungeons, logic, saveConfig, setAutoState, setManualState, state };
+       }, [config, dungeons, logic, state]);
+
        return <context.Provider value={value}>
                {children}
        </context.Provider>;
index 48575576a0fe879b8cc66519dd40fed12b537b91..c4541e25de993928fd577b3c150c0eb8f214ea1f 100644 (file)
                                                opacity: 0.4;
                                        }
                                }
+                               &.status-partial {
+                                       .box {
+                                               fill: yellow;
+                                       }
+                               }
+                               &.status-unavailable {
+                                       .box {
+                                               fill: red;
+                                       }
+                               }
                                &.size-lg {
                                        .box {
                                                width: 0.08px;