From d2e89b06bd80faa5085c454709c7e48c829cc6f2 Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Wed, 27 Mar 2024 23:59:28 +0100 Subject: [PATCH] simple logic tracking --- resources/js/components/tracker/Map.js | 19 +- resources/js/helpers/logic.js | 679 +++++++++++++++++++++++++ resources/js/helpers/tracker.js | 68 ++- resources/js/hooks/tracker.js | 20 +- resources/sass/tracker.scss | 10 + 5 files changed, 779 insertions(+), 17 deletions(-) create mode 100644 resources/js/helpers/logic.js diff --git a/resources/js/components/tracker/Map.js b/resources/js/components/tracker/Map.js index b77612e..3ee4b2f 100644 --- a/resources/js/components/tracker/Map.js +++ b/resources/js/components/tracker/Map.js @@ -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 index 0000000..2484dc0 --- /dev/null +++ b/resources/js/helpers/logic.js @@ -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; diff --git a/resources/js/helpers/tracker.js b/resources/js/helpers/tracker.js index e4b0ebe..47a34f1 100644 --- a/resources/js/helpers/tracker.js +++ b/resources/js/helpers/tracker.js @@ -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; }; diff --git a/resources/js/hooks/tracker.js b/resources/js/hooks/tracker.js index b77ee58..30bf242 100644 --- a/resources/js/hooks/tracker.js +++ b/resources/js/hooks/tracker.js @@ -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 {children} ; diff --git a/resources/sass/tracker.scss b/resources/sass/tracker.scss index 4857557..c4541e2 100644 --- a/resources/sass/tracker.scss +++ b/resources/sass/tracker.scss @@ -161,6 +161,16 @@ opacity: 0.4; } } + &.status-partial { + .box { + fill: yellow; + } + } + &.status-unavailable { + .box { + fill: red; + } + } &.size-lg { .box { width: 0.08px; -- 2.39.2