From: Daniel Karbach Date: Sat, 23 Mar 2024 13:11:45 +0000 (+0100) Subject: basic auto tracking X-Git-Url: https://git.localhorst.tv/?a=commitdiff_plain;h=b5a50d74cf042fa7fc874d8184dc37ae20bb74dd;p=alttp.git basic auto tracking --- diff --git a/icons.sh b/icons.sh new file mode 100755 index 0000000..c9fe293 --- /dev/null +++ b/icons.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +gm montage -geometry '32x32>' -background transparent -gravity center -tile 8x100 public/item/*.png public/items-v1.png + +echo 'const ITEM_MAP = [' +for i in public/item/*.png +do + basename=$(basename "$i") + echo -e "\t'${basename/\.png/}'," +done +echo '];' diff --git a/public/item/aga.png b/public/item/aga.png new file mode 100644 index 0000000..3eda6c8 Binary files /dev/null and b/public/item/aga.png differ diff --git a/public/item/armos.png b/public/item/armos.png new file mode 100644 index 0000000..2b92c87 Binary files /dev/null and b/public/item/armos.png differ diff --git a/public/item/arrghus.png b/public/item/arrghus.png new file mode 100644 index 0000000..1510975 Binary files /dev/null and b/public/item/arrghus.png differ diff --git a/public/item/blind.png b/public/item/blind.png new file mode 100644 index 0000000..875142d Binary files /dev/null and b/public/item/blind.png differ diff --git a/public/item/bottle-bee.png b/public/item/bottle-bee.png index 20ad511..04a929b 100644 Binary files a/public/item/bottle-bee.png and b/public/item/bottle-bee.png differ diff --git a/public/item/chest.png b/public/item/chest.png new file mode 100644 index 0000000..9bba19d Binary files /dev/null and b/public/item/chest.png differ diff --git a/public/item/heart-0.png b/public/item/heart-0.png new file mode 100644 index 0000000..a9984d7 Binary files /dev/null and b/public/item/heart-0.png differ diff --git a/public/item/heart-1.png b/public/item/heart-1.png new file mode 100644 index 0000000..f0044b7 Binary files /dev/null and b/public/item/heart-1.png differ diff --git a/public/item/heart-2.png b/public/item/heart-2.png new file mode 100644 index 0000000..328cece Binary files /dev/null and b/public/item/heart-2.png differ diff --git a/public/item/heart-3.png b/public/item/heart-3.png new file mode 100644 index 0000000..1fa2113 Binary files /dev/null and b/public/item/heart-3.png differ diff --git a/public/item/helma.png b/public/item/helma.png new file mode 100644 index 0000000..fd2d37a Binary files /dev/null and b/public/item/helma.png differ diff --git a/public/item/kholdstare.png b/public/item/kholdstare.png new file mode 100644 index 0000000..54f0f2e Binary files /dev/null and b/public/item/kholdstare.png differ diff --git a/public/item/lanmolas.png b/public/item/lanmolas.png new file mode 100644 index 0000000..86b80a3 Binary files /dev/null and b/public/item/lanmolas.png differ diff --git a/public/item/moldorm.png b/public/item/moldorm.png new file mode 100644 index 0000000..070d451 Binary files /dev/null and b/public/item/moldorm.png differ diff --git a/public/item/mothula.png b/public/item/mothula.png new file mode 100644 index 0000000..0a7ab90 Binary files /dev/null and b/public/item/mothula.png differ diff --git a/public/item/open-chest.png b/public/item/open-chest.png new file mode 100644 index 0000000..16d2b1f Binary files /dev/null and b/public/item/open-chest.png differ diff --git a/public/item/red-crystal.png b/public/item/red-crystal.png new file mode 100644 index 0000000..25a3a2a Binary files /dev/null and b/public/item/red-crystal.png differ diff --git a/public/item/small-key.png b/public/item/small-key.png index d38cc7a..66ac0a0 100644 Binary files a/public/item/small-key.png and b/public/item/small-key.png differ diff --git a/public/item/sword-1.png b/public/item/sword-1.png new file mode 100644 index 0000000..8d1220e Binary files /dev/null and b/public/item/sword-1.png differ diff --git a/public/item/sword-2.png b/public/item/sword-2.png new file mode 100644 index 0000000..b7b485b Binary files /dev/null and b/public/item/sword-2.png differ diff --git a/public/item/sword-3.png b/public/item/sword-3.png new file mode 100644 index 0000000..b721d6f Binary files /dev/null and b/public/item/sword-3.png differ diff --git a/public/item/sword-4.png b/public/item/sword-4.png new file mode 100644 index 0000000..9832624 Binary files /dev/null and b/public/item/sword-4.png differ diff --git a/public/item/trinexx.png b/public/item/trinexx.png new file mode 100644 index 0000000..03217db Binary files /dev/null and b/public/item/trinexx.png differ diff --git a/public/item/vitreous.png b/public/item/vitreous.png new file mode 100644 index 0000000..e550ea4 Binary files /dev/null and b/public/item/vitreous.png differ diff --git a/public/items-v1.png b/public/items-v1.png new file mode 100644 index 0000000..ef5401a Binary files /dev/null and b/public/items-v1.png differ diff --git a/public/items.png b/public/items.png new file mode 100644 index 0000000..51e21af Binary files /dev/null and b/public/items.png differ diff --git a/resources/js/app/Routes.js b/resources/js/app/Routes.js index b7269e2..0e35ddb 100644 --- a/resources/js/app/Routes.js +++ b/resources/js/app/Routes.js @@ -150,6 +150,13 @@ const router = createBrowserRouter( )} /> + import( + /* webpackChunkName: "tracker" */ + '../pages/Tracker' + )} + /> ) ); diff --git a/resources/js/components/common/ZeldaIcon.js b/resources/js/components/common/ZeldaIcon.js index ef07b92..13efe31 100644 --- a/resources/js/components/common/ZeldaIcon.js +++ b/resources/js/components/common/ZeldaIcon.js @@ -4,62 +4,101 @@ import { useTranslation } from 'react-i18next'; import Icon from './Icon'; +const ITEM_MAP = [ + 'aga', + 'armos', + 'arrghus', + 'big-key', + 'blind', + 'blue-boomerang', + 'blue-mail', + 'blue-pendant', + 'blue-potion', + 'bombos', + 'bomb', + 'book', + 'boots', + 'bottle-bee', + 'bottle', + 'bowless-silvers', + 'bow', + 'bugnet', + 'byrna', + 'cape', + 'chest', + 'compass', + 'crystal', + 'duck', + 'ether', + 'fairy', + 'fighter-shield', + 'fighter-sword', + 'fire-rod', + 'fire-shield', + 'flippers', + 'flute', + 'glove', + 'gold-sword', + 'green-mail', + 'green-pendant', + 'green-potion', + 'half-magic', + 'hammer', + 'heart-0', + 'heart-1', + 'heart-2', + 'heart-3', + 'heart-container', + 'heart-piece', + 'helma', + 'hookshot', + 'ice-rod', + 'kholdstare', + 'lamp', + 'lanmolas', + 'map', + 'master-sword', + 'mirror', + 'mirror-shield', + 'mitts', + 'moldorm', + 'moonpearl', + 'mothula', + 'mushroom', + 'open-chest', + 'powder', + 'quake', + 'quarter-magic', + 'red-bomb', + 'red-boomerang', + 'red-crystal', + 'red-mail', + 'red-pendant', + 'red-potion', + 'shovel', + 'silvers', + 'small-key', + 'somaria', + 'sword-1', + 'sword-2', + 'sword-3', + 'sword-4', + 'tempered-sword', + 'trinexx', + 'vitreous', +]; + +const isOnItemMap = name => ITEM_MAP.includes(name); + +const getItemMapStyle = name => { + const index = ITEM_MAP.indexOf(name); + const x = index % 8; + const y = Math.floor(index / 8); + return { backgroundPosition: `-${x * 100}% -${y * 100}%` }; +}; + const getIconURL = name => { switch (name) { - case 'big-key': - case 'blue-boomerang': - case 'blue-mail': - case 'blue-pendant': - case 'blue-potion': - case 'bombos': - case 'bomb': - case 'book': - case 'boots': - case 'bottle-bee': - case 'bottle': - case 'bow': - case 'bugnet': - case 'byrna': - case 'cape': - case 'compass': - case 'crystal': - case 'duck': - case 'ether': - case 'fairy': - case 'fighter-shield': - case 'fighter-sword': - case 'fire-rod': - case 'fire-shield': - case 'flippers': - case 'flute': - case 'glove': - case 'green-mail': - case 'green-pendant': - case 'green-potion': - case 'hammer': - case 'heart-container': - case 'heart-piece': - case 'hookshot': - case 'ice-rod': - case 'lamp': - case 'map': - case 'mirror': - case 'mirror-shield': - case 'mitts': - case 'moonpearl': - case 'mushroom': - case 'powder': - case 'quake': - case 'red-bomb': - case 'red-boomerang': - case 'red-mail': - case 'red-pendant': - case 'red-potion': - case 'shovel': - case 'silvers': - case 'small-key': - case 'somaria': - return `/item/${name}.png`; case 'dungeon-ct': case 'dungeon-dp': case 'dungeon-ep': @@ -93,6 +132,13 @@ const ZeldaIcon = ({ name, title }) => { const realTitle = title !== '' ? title || alt : null; return + {isOnItemMap(strippedName) ? + + : null} {src ? {alt} { }; ZeldaIcon.propTypes = { - name: PropTypes.string, + name: PropTypes.string.isRequired, title: PropTypes.string, }; diff --git a/resources/js/components/tracker/AutoTracking.js b/resources/js/components/tracker/AutoTracking.js new file mode 100644 index 0000000..8528856 --- /dev/null +++ b/resources/js/components/tracker/AutoTracking.js @@ -0,0 +1,153 @@ +import React from 'react'; +import { Button } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import Icon from '../common/Icon'; +import ToggleSwitch from '../common/ToggleSwitch'; +import { + IN_GAME_MODES, + RAM_ADDR, + SRAM_ADDR, + WRAM_ADDR, + buildPrizeMap, +} from '../../helpers/alttp-ram'; +import { computeState, mergeStates } from '../../helpers/tracker'; +import { useSNES } from '../../hooks/snes'; +import { useTracker } from '../../hooks/tracker'; + +const AutoTracking = () => { + const [enabled, setEnabled] = React.useState(false); + const [prizeMap, setPrizeMap] = React.useState(buildPrizeMap()); + + const { + disable: disableSNES, + enable: enableSNES, + openSettings, + sock, + status, + } = useSNES(); + const { config, setState } = useTracker(); + const { t } = useTranslation(); + + const enable = React.useCallback(() => { + enableSNES(); + setEnabled(true); + }, []); + + const disable = React.useCallback(() => { + disableSNES(); + setEnabled(false); + }, []); + + React.useEffect(() => { + const savedSettings = localStorage.getItem('tracker.settings'); + if (savedSettings) { + const settings = JSON.parse(savedSettings); + if (settings.autoTrack) { + enable(); + } + } + }, []); + + const saveSettings = React.useCallback((newSettings) => { + const savedSettings = localStorage.getItem('tracker.settings'); + const settings = savedSettings + ? { ...JSON.parse(savedSettings), ...newSettings } + : newSettings; + localStorage.setItem('tracker.settings', JSON.stringify(settings)); + }, []); + + const toggle = React.useCallback(() => { + if (enabled) { + disable(); + saveSettings({ autoTrack: false }); + } else { + enable(); + saveSettings({ autoTrack: true }); + } + }, [enabled]); + + // poll game and push state + React.useEffect(() => { + if (!enabled || status.error || !status.connected || !status.device) return; + const updateState = () => { + const saveStart = WRAM_ADDR.SAVE_DATA; + const saveSize = SRAM_ADDR.INV_END; + sock.current.readWRAM(saveStart, saveSize, (data) => { + const computed = computeState(data, prizeMap); + setState(s => mergeStates(config, s, computed)); + }); + }; + const fetchPrizes = () => { + sock.current.readBytes(RAM_ADDR.PRIZE_MAP, 13, (prizes) => { + sock.current.readBytes(RAM_ADDR.CRYSTAL_MAP, 13, (crystals) => { + setPrizeMap(m => { + const newMap = buildPrizeMap(prizes, crystals); + return JSON.stringify(m) === JSON.stringify(newMap) ? m : newMap; + }); + }); + }); + }; + const checkInGame = () => { + sock.current.readWRAM(WRAM_ADDR.GAME_MODE, 1, (data) => { + if (IN_GAME_MODES.includes(data[0])) { + fetchPrizes(); + updateState(); + } + }); + }; + const timer = setInterval(checkInGame, 1000); + return () => { + clearInterval(timer); + }; + }, [enabled && !status.error && status.connected && status.device, config, prizeMap, sock]); + + const statusMsg = React.useMemo(() => { + if (!enabled) { + return 'disabled'; + } + if (status.error) { + return 'error'; + } + if (!status.connected) { + return 'disconnected'; + } + if (!status.device) { + return 'no-device'; + } + return 'tracking'; + }, [enabled, status]); + + return
+ {['disconnected', 'error', 'no-device'].includes(statusMsg) ? + + : null} + {['not-applicable', 'not-in-game'].includes(statusMsg) ? + + : null} + + +
; +}; + +export default AutoTracking; diff --git a/resources/js/components/tracker/CountDisplay.js b/resources/js/components/tracker/CountDisplay.js new file mode 100644 index 0000000..ed9b91f --- /dev/null +++ b/resources/js/components/tracker/CountDisplay.js @@ -0,0 +1,22 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +const CountDisplay = ({ className, count }) => { + const classNames = ['count-display']; + if (className) { + classNames.push(className); + } + if (!count) { + classNames.push('is-zero'); + } + return + {count} + ; +}; + +CountDisplay.propTypes = { + className: PropTypes.string, + count: PropTypes.number, +}; + +export default CountDisplay; diff --git a/resources/js/components/tracker/Dungeons.js b/resources/js/components/tracker/Dungeons.js new file mode 100644 index 0000000..07b6638 --- /dev/null +++ b/resources/js/components/tracker/Dungeons.js @@ -0,0 +1,62 @@ +import React from 'react'; + +import CountDisplay from './CountDisplay'; +import ToggleIcon from './ToggleIcon'; +import { useTracker } from '../../hooks/tracker'; + +const Dungeons = () => { + const { dungeons, state } = useTracker(); + + return
+ {dungeons.map(dungeon => +
+ {dungeon.id.toUpperCase()} + + + + + + + + + + + + {dungeon.boss ? + + : null} + {dungeon.prize ? + + : null} +
+ )} +
; +}; + +export default Dungeons; diff --git a/resources/js/components/tracker/Equipment.js b/resources/js/components/tracker/Equipment.js new file mode 100644 index 0000000..bfb9b20 --- /dev/null +++ b/resources/js/components/tracker/Equipment.js @@ -0,0 +1,59 @@ +import React from 'react'; + +import CountDisplay from './CountDisplay'; +import ToggleIcon from './ToggleIcon'; +import { useTracker } from '../../hooks/tracker'; + +const Equipment = () => { + const { state } = useTracker(); + + return
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
; +}; + +export default Equipment; diff --git a/resources/js/components/tracker/Items.js b/resources/js/components/tracker/Items.js new file mode 100644 index 0000000..712ee6c --- /dev/null +++ b/resources/js/components/tracker/Items.js @@ -0,0 +1,102 @@ +import React from 'react'; + +import CountDisplay from './CountDisplay'; +import ToggleIcon from './ToggleIcon'; +import { useTracker } from '../../hooks/tracker'; + +const Items = () => { + const { state } = useTracker(); + + return
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
; +}; + +export default Items; diff --git a/resources/js/components/tracker/ToggleIcon.js b/resources/js/components/tracker/ToggleIcon.js new file mode 100644 index 0000000..49375ef --- /dev/null +++ b/resources/js/components/tracker/ToggleIcon.js @@ -0,0 +1,218 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +import ZeldaIcon from '../common/ZeldaIcon'; +import { + decrement, + getDungeonBoss, + getDungeonPrize, + hasDungeonBoss, + hasDungeonPrize, + highestActive, + increment, + toggleBoolean, +} from '../../helpers/tracker'; +import { useTracker } from '../../hooks/tracker'; + +const ToggleIcon = ({ controller, className, icons }) => { + const { state, setState } = useTracker(); + const activeController = controller || ToggleIcon.nullController; + const active = activeController.getActive(state, icons); + const defaultIcon = activeController.getDefault(state, icons); + const classNames = ['toggle-icon']; + if (active) { + classNames.push('active'); + } else { + classNames.push('inactive'); + } + if (className) { + classNames.push(className); + } + return { + activeController.handlePrimary(state, setState, icons); + e.preventDefault(); + e.stopPropagation(); + }} + onContextMenu={(e) => { + activeController.handleSecondary(state, setState, icons); + e.preventDefault(); + e.stopPropagation(); + }} + > + + ; +}; + +const doNothing = () => { }; + +const firstIcon = (state, icons) => icons[0]; + +const nextIcon = (state, setState, icons) => { + const highest = highestActive(state, icons); + const highestIndex = highest ? icons.indexOf(highest) : -1; + if (highestIndex + 1 < icons.length) { + setState(toggleBoolean(icons[highestIndex + 1])); + } else { + const changes = {}; + icons.forEach(icon => { + changes[icon] = false; + }); + setState(s => ({ ...s, ...changes })); + } +}; + +const previousIcon = (state, setState, icons) => { + const highest = highestActive(state, icons); + const highestIndex = highest ? icons.indexOf(highest) : -1; + if (highestIndex >= 0) { + setState(toggleBoolean(icons[highestIndex])); + } else { + const changes = {}; + icons.forEach(icon => { + changes[icon] = true; + }); + setState(s => ({ ...s, ...changes })); + } +}; + +const nextString = property => (state, setState, icons) => { + const current = state[property] || icons[0]; + const currentIndex = icons.indexOf(current); + const nextIndex = (currentIndex + 1) % icons.length; + const next = icons[nextIndex]; + setState(s => ({ ...s, [property]: next })); +}; + +const previousString = property => (state, setState, icons) => { + const current = state[property] || icons[0]; + const currentIndex = icons.indexOf(current); + const previousIndex = (currentIndex + icons.length - 1) % icons.length; + const previous = icons[previousIndex]; + setState(s => ({ ...s, [property]: previous })); +}; + +ToggleIcon.countController = max => ({ + getActive: highestActive, + getDefault: firstIcon, + handlePrimary: (state, setState, icons) => { + setState(increment(icons[0], max)); + }, + handleSecondary: (state, setState, icons) => { + setState(decrement(icons[0], max)); + }, +}); + +ToggleIcon.dungeonBossController = (dungeon) => ({ + getActive: (state) => hasDungeonBoss(state, dungeon) ? getDungeonBoss(state, dungeon) : null, + getDefault: (state) => getDungeonBoss(state, dungeon), + handlePrimary: dungeon.bosses.length > 1 + ? nextString(`${dungeon.id}-boss`) + : (state, setState) => { + setState(toggleBoolean(`${dungeon.id}-boss-defeated`)); + }, + handleSecondary: dungeon.bosses.length > 1 ? + previousString(`${dungeon.id}-boss`) + : (state, setState) => { + setState(toggleBoolean(`${dungeon.id}-boss-defeated`)); + }, +}); + +ToggleIcon.dungeonCheckController = (dungeon, max) => ({ + getActive: (state, icons) => state[`${dungeon.id}-checks`] < max ? icons[1] : null, + getDefault: firstIcon, + handlePrimary: (state, setState) => { + setState(increment(`${dungeon.id}-checks`, max)); + }, + handleSecondary: (state, setState) => { + setState(decrement(`${dungeon.id}-checks`, max)); + }, +}); + +ToggleIcon.dungeonController = dungeon => ({ + getActive: (state, icons) => state[`${dungeon.id}-${icons[0]}`] ? icons[0] : null, + getDefault: firstIcon, + handlePrimary: (state, setState, icons) => { + setState(toggleBoolean(`${dungeon.id}-${icons[0]}`)); + }, + handleSecondary: (state, setState, icons) => { + setState(toggleBoolean(`${dungeon.id}-${icons[0]}`)); + }, +}); + +ToggleIcon.dungeonCountController = (dungeon, max) => ({ + getActive: (state, icons) => state[`${dungeon.id}-${icons[0]}`] ? icons[0] : null, + getDefault: firstIcon, + handlePrimary: (state, setState, icons) => { + setState(increment(`${dungeon.id}-${icons[0]}`, max)); + }, + handleSecondary: (state, setState, icons) => { + setState(decrement(`${dungeon.id}-${icons[0]}`, max)); + }, +}); + +ToggleIcon.dungeonPrizeController = (dungeon) => ({ + getActive: (state) => hasDungeonPrize(state, dungeon) ? getDungeonPrize(state, dungeon) : null, + getDefault: (state) => getDungeonPrize(state, dungeon), + handlePrimary: nextString(`${dungeon.id}-prize`), + handleSecondary: previousString(`${dungeon.id}-prize`), +}); + +ToggleIcon.medallionController = { + getActive: highestActive, + getDefault: firstIcon, + handlePrimary: nextIcon, + handleSecondary: doNothing, +}; + +ToggleIcon.modulusController = ctrl => ({ + getActive: (state, icons) => icons[(state[ctrl] || 0) % icons.length], + getDefault: firstIcon, + handlePrimary: (state, setState, icons) => { + setState(increment(icons[0], icons.length)); + }, + handleSecondary: (state, setState, icons) => { + setState(decrement(icons[0], icons.length)); + }, +}); + +ToggleIcon.nullController = { + getActive: () => null, + getDefault: firstIcon, + handlePrimary: doNothing, + handleSecondary: doNothing, +}; + +ToggleIcon.simpleController = { + getActive: highestActive, + getDefault: firstIcon, + handlePrimary: nextIcon, + handleSecondary: previousIcon, +}; + +ToggleIcon.progressiveController = (master, min, max) => ({ + getActive: (state, icons) => { + const count = Math.max(min, Math.min(max, state[master] || 0)); + return count ? icons[count - 1] : null; + }, + getDefault: firstIcon, + handlePrimary: (state, setState) => { + setState(increment(master, max, min)); + }, + handleSecondary: (state, setState) => { + setState(decrement(master, max, min)); + }, +}); + +ToggleIcon.propTypes = { + active: PropTypes.string, + className: PropTypes.string, + controller: PropTypes.shape({ + handlePrimary: PropTypes.func, + handleSecondary: PropTypes.func, + }), + icons: PropTypes.arrayOf(PropTypes.string), +}; + +export default ToggleIcon; diff --git a/resources/js/components/tracker/Toolbar.js b/resources/js/components/tracker/Toolbar.js new file mode 100644 index 0000000..169d7bc --- /dev/null +++ b/resources/js/components/tracker/Toolbar.js @@ -0,0 +1,41 @@ +import React from 'react'; +import { Container, Navbar } from 'react-bootstrap'; + +import AutoTracking from './AutoTracking'; +import ToggleIcon from './ToggleIcon'; +import { useTracker } from '../../hooks/tracker'; + +const mapWild = { + map: 'wildMap', + compass: 'wildCompass', + 'small-key': 'wildSmall', + 'big-key': 'wildBig', +}; + +const Toolbar = () => { + const { config, setConfig } = useTracker(); + + const controller = React.useMemo(() => ({ + getActive: (state, icons) => config[mapWild[icons[0]]] ? icons[0] : null, + getDefault: (state, icons) => icons[0], + handlePrimary: (state, setState, icons) => { + const prop = mapWild[icons[0]]; + setConfig(c => ({ ...c, [prop]: !c[prop] })); + }, + handleSecondary: () => null, + }), [config, setConfig]); + + return + +
+ + + + +
+ +
+
; +}; + +export default Toolbar; diff --git a/resources/js/components/tracker/index.js b/resources/js/components/tracker/index.js new file mode 100644 index 0000000..5a505b5 --- /dev/null +++ b/resources/js/components/tracker/index.js @@ -0,0 +1,17 @@ +import React from 'react'; + +import Dungeons from './Dungeons'; +import Equipment from './Equipment'; +import Items from './Items'; +import Toolbar from './Toolbar'; + +const Tracker = () => { + return
+ + + + +
; +}; + +export default Tracker; diff --git a/resources/js/components/twitch-bot/GuessingGameAutoTracking.js b/resources/js/components/twitch-bot/GuessingGameAutoTracking.js index 093e121..451c597 100644 --- a/resources/js/components/twitch-bot/GuessingGameAutoTracking.js +++ b/resources/js/components/twitch-bot/GuessingGameAutoTracking.js @@ -9,32 +9,14 @@ import { compareGTBasementState, countGTBasementState, getGTBasementState, + IN_GAME_MODES, + INV_ADDR, + RAM_ADDR, + SRAM_ADDR, + WRAM_ADDR, } from '../../helpers/alttp-ram'; import { useSNES } from '../../hooks/snes'; -const IN_GAME_MODES = [ - 0x05, // loading game - 0x06, // entering dungeon - 0x07, // dungeon - 0x08, // entering overworld - 0x09, // overworld - 0x0A, // entering special overworld - 0x0B, // special overworld - 0x0E, // text/menu/map - 0x0F, // closing spot - 0x10, // opening spot - 0x11, // falling - 0x12, // dying - 0x13, // fanfare - 0x15, // mirror - 0x16, // refill - 0x17, // S&Q - 0x18, // aga 2 cutscene - 0x19, // triforce room - 0x1A, // credits - 0x1B, // spawn select -]; - const GT_TYPES = [ 0x02, // all dungeons 0x03, // defeat ganon @@ -45,21 +27,6 @@ const GT_TYPES = [ 0x0B, // completionist ]; -const FREE_ITEM_MENU = 0x180045; -const GT_CRYSTALS = 0x18019A; -const GANON_TYPE = 0x1801A8; -const SEED_TYPE = 0x180210; -const INIT_SRAM = 0x183000; - -const GAME_MODE = 0x10; -const CURRENT_DUNGEON = 0x10E; -const SAVE_WRAM = 0xF000; -const ROOM_DATA_START = 0x000; -const ROOM_DATA_END = 0x140; -const PYRAMID_SCREEN = 0x2DB; -const BIG_KEYS_1 = 0x366; -const OWNED_CRYSTALS = 0x37A; - const GT_ENTRANCE_ID = 55; const GuessingGameAutoTracking = ({ onSolve, onStart, onStop }) => { @@ -167,7 +134,7 @@ const GuessingGameAutoTracking = ({ onSolve, onStart, onStop }) => { React.useEffect(() => { if (enabled && !status.error && status.connected && status.device) { const checkInGame = () => { - sock.current.readWRAM(GAME_MODE, 1, (data) => { + sock.current.readWRAM(WRAM_ADDR.GAME_MODE, 1, (data) => { setInGame(IN_GAME_MODES.includes(data[0])); }); }; @@ -184,19 +151,19 @@ const GuessingGameAutoTracking = ({ onSolve, onStart, onStop }) => { // refresh static game information React.useEffect(() => { if (!inGame) return; - sock.current.readBytes(SEED_TYPE, 1, (data) => { + sock.current.readBytes(RAM_ADDR.SEED_TYPE, 1, (data) => { setSeedType(data[0]); }); - sock.current.readBytes(GT_CRYSTALS, 1, (data) => { + sock.current.readBytes(RAM_ADDR.GT_CRYSTALS, 1, (data) => { setGTCrystals(data[0]); }); - sock.current.readBytes(GANON_TYPE, 1, (data) => { + sock.current.readBytes(RAM_ADDR.GANON_TYPE, 1, (data) => { setGanonType(data[0]); }); - sock.current.readBytes(FREE_ITEM_MENU, 1, (data) => { + sock.current.readBytes(RAM_ADDR.FREE_ITEM_MENU, 1, (data) => { setFreeItemMenu(data[0]); }); - sock.current.readBytes(INIT_SRAM + PYRAMID_SCREEN, 1, (data) => { + sock.current.readBytes(RAM_ADDR.INIT_SRAM + SRAM_ADDR.PYRAMID_SCREEN, 1, (data) => { setPyramidOpen(!!(data[0] & 0x20)); }); }, [inGame, sock]); @@ -213,7 +180,8 @@ const GuessingGameAutoTracking = ({ onSolve, onStart, onStop }) => { React.useEffect(() => { if (!applicable || !inGame || hasBigKey) return; const updateCrystals = () => { - sock.current.readWRAM(SAVE_WRAM + OWNED_CRYSTALS, 1, (data) => { + const crAddress = WRAM_ADDR.SAVE_DATA + SRAM_ADDR.INV_START + INV_ADDR.CRYSTALS; + sock.current.readWRAM(crAddress, 1, (data) => { let owned = 0; for (let i = 0; i < 7; ++i) { if (data[0] & Math.pow(2, i)) { @@ -235,7 +203,7 @@ const GuessingGameAutoTracking = ({ onSolve, onStart, onStop }) => { if (!applicable || hasBigKey || ownedCrystals !== gtCrystals || hasEntered) return; controls.current.onStart(); const updateDungeon = () => { - sock.current.readWRAM(CURRENT_DUNGEON, 2, (data) => { + sock.current.readWRAM(WRAM_ADDR.CURRENT_DUNGEON, 2, (data) => { setLastEntrance(data[0] + (data[1] * 256)); }); }; @@ -258,8 +226,9 @@ const GuessingGameAutoTracking = ({ onSolve, onStart, onStop }) => { React.useEffect(() => { if (!applicable || !hasEntered || hasBigKey) return; const updateGTState = () => { - const roomDataSize = ROOM_DATA_END - ROOM_DATA_START; - sock.current.readWRAM(SAVE_WRAM + ROOM_DATA_START, roomDataSize, (data) => { + const roomDataStart = WRAM_ADDR.SAVE_DATA + SRAM_ADDR.ROOM_DATA_START; + const roomDataSize = SRAM_ADDR.ROOM_DATA_END - SRAM_ADDR.ROOM_DATA_START; + sock.current.readWRAM(roomDataStart, roomDataSize, (data) => { const gtState = getGTBasementState(data); const gtCount = countGTBasementState(gtState); setBasement(old => { @@ -288,7 +257,8 @@ const GuessingGameAutoTracking = ({ onSolve, onStart, onStop }) => { const solution = basement.last === 'torch' ? basement.torch : basement.count; controls.current.onSolve(solution); } else { - sock.current.readWRAM(SAVE_WRAM + BIG_KEYS_1, 1, (data) => { + const bkAddr = WRAM_ADDR.SAVE_DATA + SRAM_ADDR.INV_START + INV_ADDR.BIG_KEY; + sock.current.readWRAM(bkAddr, 1, (data) => { setHasBigKey(!!(data[0] & 0x04)); }); } diff --git a/resources/js/helpers/alttp-ram.js b/resources/js/helpers/alttp-ram.js index 2ae71cf..d5bfa1b 100644 --- a/resources/js/helpers/alttp-ram.js +++ b/resources/js/helpers/alttp-ram.js @@ -1,3 +1,165 @@ +export const RAM_ADDR = { + PRIZE_MAP: 0x1209B, + FREE_ITEM_MENU: 0x180045, + CRYSTAL_MAP: 0x180050, + GT_CRYSTALS: 0x18019A, + GANON_TYPE: 0x1801A8, + SEED_TYPE: 0x180210, + INIT_SRAM: 0x183000, +}; + +export const SRAM_ADDR = { + ROOM_DATA_START: 0x000, + ROOM_DATA_END: 0x250, + OW_DATA_START: 0x280, + PYRAMID_SCREEN: 0x2DB, + OW_DATA_END: 0x300, + INV_START: 0x340, + INV_END: 0x4EF, +}; + +export const WRAM_ADDR = { + GAME_MODE: 0x10, + CURRENT_DUNGEON: 0x10E, + SAVE_DATA: 0xF000, +}; + +export const INV_ADDR = { + BOW: 0x00, + BOOM: 0x01, + HOOK: 0x02, + BOMB: 0x03, + POWDER: 0x04, + FROD: 0x05, + IROD: 0x06, + BOMBOS: 0x07, + ETHER: 0x08, + QUAKE: 0x09, + LAMP: 0x0A, + HAMMER: 0x0B, + FLUTE: 0x0C, + BUGNET: 0x0D, + BOOK: 0x0E, + BOTTLE: 0x0F, + SOMARIA: 0x10, + BYRNA: 0x11, + CAPE: 0x12, + MIRROR: 0x13, + GLOVE: 0x14, + BOOTS: 0x15, + FLIPPERS: 0x16, + MOONPEARL: 0x17, + SWORD: 0x19, + SHIELD: 0x1A, + ARMOR: 0x1B, + BOTTLE_1: 0x1C, + BOTTLE_2: 0x1D, + BOTTLE_3: 0x1E, + BOTTLE_4: 0x1F, + WALLET: 0x20, + RUPEES: 0x22, + COMPASS: 0x24, + BIG_KEY: 0x26, + MAP: 0x28, + HEART_PIECE: 0x2B, + HEALTH: 0x2C, + MAGIC: 0x2E, + KEYS: 0x2F, + PENDANTS: 0x34, + ARROWS: 0x37, + ABILITIES: 0x39, + CRYSTALS: 0x3A, + MAGIC_USE: 0x3B, + SMALL_KEY_START: 0x3C, + SMALL_KEY_END: 0x4C, + RANDO_BOOM: 0x4C, + RANDO_POWDER: 0x4C, + RANDO_FLUTE: 0x4C, + RANDO_BOW: 0x4E, + RANDO_KEY_START: 0x1A0, + RANDO_KEY_END: 0x1AF, +}; + +export const DUNGEON_IDS = { + SEWERS: 0, + HC: 1, + EP: 2, + DP: 3, + CT: 4, + SP: 5, + PD: 6, + MM: 7, + SW: 8, + IP: 9, + TH: 10, + TT: 11, + TR: 12, + GT: 13, +}; + +export const DUNGEON_MASKS = { + SEWERS: 0x0080, + HC: 0x0040, + EP: 0x0020, + DP: 0x0010, + CT: 0x0008, + SP: 0x0004, + PD: 0x0002, + MM: 0x0001, + SW: 0x8000, + IP: 0x4000, + TH: 0x2000, + TT: 0x1000, + TR: 0x0800, + GT: 0x0400, +}; + +export const ABILITY_MASKS = { + SWIM: 0x02, + DASH: 0x04, + PULL: 0x08, + TALK: 0x20, + READ: 0x40, +}; + +export const IN_GAME_MODES = [ + 0x05, // loading game + 0x06, // entering dungeon + 0x07, // dungeon + 0x08, // entering overworld + 0x09, // overworld + 0x0A, // entering special overworld + 0x0B, // special overworld + 0x0E, // text/menu/map + 0x0F, // closing spot + 0x10, // opening spot + 0x11, // falling + 0x12, // dying + 0x13, // fanfare + 0x15, // mirror + 0x16, // refill + 0x17, // S&Q + 0x18, // aga 2 cutscene + 0x19, // triforce room + 0x1B, // spawn select +]; + +export const getShort = (data, offset) => (data[offset] * 256) + data[offset + 1]; + +export const buildPrizeMap = (prizes, crystals) => { + const map = {}; + Object.entries(DUNGEON_IDS).forEach(([, id]) => { + const isCrystal = !!(crystals && crystals[id]); + const mask = (prizes && prizes[id]) || 0; + map[id] = { isCrystal, mask }; + }); + return map; +}; + +export const isBossDefeated = (data, room) => { + return !!(data && (data[(2 * room) + 1] & 0x08)); +}; + export const isChestOpen = (data, room, chest) => { if (chest < 4) { return !!(data && (data[2 * room] & Math.pow(2, chest + 4))); diff --git a/resources/js/helpers/tracker.js b/resources/js/helpers/tracker.js new file mode 100644 index 0000000..707e3f7 --- /dev/null +++ b/resources/js/helpers/tracker.js @@ -0,0 +1,1753 @@ +import { + DUNGEON_IDS, + DUNGEON_MASKS, + INV_ADDR, + SRAM_ADDR, + getShort, + isBossDefeated, + isChestOpen, +} from './alttp-ram'; + +export const BOOLEAN_STATES = [ + 'blue-boomerang', + 'bomb', + 'bombos', + 'bow', + 'bowless-silvers', + 'book', + 'boots', + 'bugnet', + 'byrna', + 'cape', + 'duck', + 'ether', + 'fire-rod', + 'flippers', + 'flute', + 'half-magic', + 'hammer', + 'hookshot', + 'ice-rod', + 'lamp', + 'mirror', + 'moonpearl', + 'mushroom', + 'powder', + 'quake', + 'quarter-magic', + 'red-boomerang', + 'shovel', + 'silvers', + 'somaria', +]; + +export const INTEGER_STATES = [ + 'bottle', + 'heart-piece', + 'lift', + 'mail', + 'shield', + 'sword', +]; + +export const INITIAL = { + mail: 1, +}; + +export const BOSSES = [ + 'armos', + 'lanmolas', + 'moldorm', + 'helma', + 'arrghus', + 'mothula', + 'blind', + 'kholdstare', + 'vitreous', + 'trinexx', +]; + +export const CONFIG = { + wildMap: false, + wildCompass: false, + wildSmall: false, + wildBig: false, + bossShuffle: false, +}; + +export const DUNGEONS = [ + { + id: 'hc', + map: true, + compass: false, + sk: 1, + bk: true, + dropBk: true, + items: 6, + boss: null, + bosses: [], + bossRoom: 0x80, + prize: false, + offset: DUNGEON_IDS.HC, + mask: DUNGEON_MASKS.HC, + checks: [ + 'dark-cross', + 'hc-map', + 'hc-boom', + 'hc-cell', + 'sanc', + 'sewers-left', + 'sewers-mid', + 'sewers-right', + ], + }, + { + id: 'ct', + map: false, + compass: false, + sk: 2, + bk: false, + items: 0, + boss: 'aga', + bosses: ['aga'], + bossRoom: 0x20, + prize: false, + offset: DUNGEON_IDS.CT, + mask: DUNGEON_MASKS.CT, + checks: [ + 'ct-1', + 'ct-2', + ], + }, + { + id: 'gt', + map: true, + compass: true, + sk: 4, + bk: true, + items: 20, + boss: 'aga', + bosses: ['aga'], + bossRoom: 0x0D, + prize: false, + offset: DUNGEON_IDS.GT, + mask: DUNGEON_MASKS.GT, + checks: [ + 'gt-hope-left', + 'gt-hope-right', + 'gt-tile-room', + 'gt-compass-tl', + 'gt-compass-tr', + 'gt-compass-bl', + 'gt-compass-br', + 'gt-torch', + 'gt-dm-tl', + 'gt-dm-tr', + 'gt-dm-bl', + 'gt-dm-br', + 'gt-map-chest', + 'gt-firesnake', + 'gt-rando-tl', + 'gt-rando-tr', + 'gt-rando-bl', + 'gt-rando-br', + 'gt-bobs-chest', + 'gt-ice-left', + 'gt-ice-mid', + 'gt-ice-right', + 'gt-big-chest', + 'gt-helma-left', + 'gt-helma-right', + 'gt-pre-moldorm', + 'gt-post-moldorm', + ], + }, + { + id: 'ep', + map: true, + compass: true, + sk: 0, + bk: true, + items: 3, + boss: 'armos', + bosses: ['armos'], + bossRoom: 0xC8, + prize: true, + isPendant: true, + prizeMask: 0x04, + offset: DUNGEON_IDS.EP, + mask: DUNGEON_MASKS.EP, + checks: [ + 'ep-cannonball', + 'ep-map-chest', + 'ep-compass-chest', + 'ep-big-chest', + 'ep-big-key-chest', + 'ep-boss-defeated', + ], + }, + { + id: 'dp', + map: true, + compass: true, + sk: 1, + bk: true, + items: 2, + boss: 'lanmolas', + bosses: ['lanmolas'], + bossRoom: 0x33, + prize: true, + isPendant: true, + prizeMask: 0x02, + offset: DUNGEON_IDS.DP, + mask: DUNGEON_MASKS.DP, + checks: [ + 'dp-torch', + 'dp-map-chest', + 'dp-big-chest', + 'dp-compass-chest', + 'dp-big-key-chest', + 'dp-boss-defeated', + ], + }, + { + id: 'th', + map: true, + compass: true, + sk: 1, + bk: true, + items: 2, + boss: 'moldorm', + bosses: ['moldorm'], + bossRoom: 0x07, + prize: true, + isPendant: true, + prizeMask: 0x01, + offset: DUNGEON_IDS.TH, + mask: DUNGEON_MASKS.TH, + checks: [ + 'th-basement-cage', + 'th-map-chest', + 'th-big-key-chest', + 'th-compass-chest', + 'th-big-chest', + 'th-boss-defeated', + ], + }, + { + id: 'pd', + map: true, + compass: true, + sk: 6, + bk: true, + items: 5, + boss: 'helma', + bosses: ['helma'], + bossRoom: 0x5A, + prize: true, + prizeMask: 0x02, + offset: DUNGEON_IDS.PD, + mask: DUNGEON_MASKS.PD, + checks: [ + 'pd-shooter-room', + 'pd-stalfos-basement', + 'pd-big-key-chest', + 'pd-arena-bridge', + 'pd-arena-ledge', + 'pd-map-chest', + 'pd-compass-chest', + 'pd-basement-left', + 'pd-basement-right', + 'pd-harmless-hellway', + 'pd-maze-top', + 'pd-maze-bottom', + 'pd-big-chest', + 'pd-boss-defeated', + ], + }, + { + id: 'sp', + map: true, + compass: true, + sk: 1, + bk: true, + items: 6, + boss: 'arrghus', + bosses: ['arrghus'], + bossRoom: 0x06, + prize: true, + prizeMask: 0x10, + offset: DUNGEON_IDS.SP, + mask: DUNGEON_MASKS.SP, + checks: [ + 'sp-lobby', + 'sp-map-chest', + 'sp-big-chest', + 'sp-compass-chest', + 'sp-west-chest', + 'sp-big-key-chest', + 'sp-flooded-left', + 'sp-flooded-right', + 'sp-waterfall', + 'sp-boss-defeated', + ], + }, + { + id: 'sw', + map: true, + compass: true, + sk: 3, + bk: true, + items: 2, + boss: 'mothula', + bosses: ['mothula'], + bossRoom: 0x29, + prize: true, + prizeMask: 0x40, + offset: DUNGEON_IDS.SW, + mask: DUNGEON_MASKS.SW, + checks: [ + 'sw-big-chest', + 'sw-map-chest', + 'sw-pot-prison', + 'sw-compass-chest', + 'sw-pinball-room', + 'sw-big-key-chest', + 'sw-bridge-chest', + 'sw-boss-defeated', + ], + }, + { + id: 'tt', + map: true, + compass: true, + sk: 1, + bk: true, + items: 4, + boss: 'blind', + bosses: ['blind'], + bossRoom: 0xAC, + prize: true, + prizeMask: 0x20, + offset: DUNGEON_IDS.TT, + mask: DUNGEON_MASKS.TT, + checks: [ + 'tt-map-chest', + 'tt-ambush-chest', + 'tt-compass-chest', + 'tt-big-key-chest', + 'tt-attic', + 'tt-cell', + 'tt-big-chest', + 'tt-boss-defeated', + ], + }, + { + id: 'ip', + map: true, + compass: true, + sk: 2, + bk: true, + items: 3, + boss: 'kholdstare', + bosses: ['kholdstare'], + bossRoom: 0xDE, + prize: true, + prizeMask: 0x04, + offset: DUNGEON_IDS.IP, + mask: DUNGEON_MASKS.IP, + checks: [ + 'ip-compass-chest', + 'ip-big-key-chest', + 'ip-map-chest', + 'ip-spike-chest', + 'ip-freezor-chest', + 'ip-big-chest', + 'ip-ice-t', + 'ip-boss-defeated', + ], + }, + { + id: 'mm', + map: true, + compass: true, + sk: 3, + bk: true, + items: 2, + boss: 'vitreous', + bosses: ['vitreous'], + bossRoom: 0x90, + prize: true, + prizeMask: 0x01, + offset: DUNGEON_IDS.MM, + mask: DUNGEON_MASKS.MM, + checks: [ + 'mm-bridge-chest', + 'mm-spike-chest', + 'mm-lobby-chest', + 'mm-compass-chest', + 'mm-big-key-chest', + 'mm-big-chest', + 'mm-map-chest', + 'mm-boss-defeated', + ], + }, + { + id: 'tr', + map: true, + compass: true, + sk: 4, + bk: true, + items: 5, + boss: 'trinexx', + bosses: ['trinexx'], + bossRoom: 0xA4, + prize: true, + prizeMask: 0x08, + offset: DUNGEON_IDS.TR, + mask: DUNGEON_MASKS.TR, + checks: [ + 'tr-roller-left', + 'tr-roller-right', + 'tr-compass-chest', + 'tr-chomps', + 'tr-big-key-chest', + 'tr-big-chest', + 'tr-crysta-roller', + 'tr-laser-bridge-top', + 'tr-laser-bridge-left', + 'tr-laser-bridge-right', + 'tr-laser-bridge-bottom', + 'tr-boss-defeated', + ], + }, +]; + +export const OVERWORLD_LOCATIONS = [ + { + id: 'blacksmith', + address: 0x411, + mask: 0x04, + }, + { + id: 'bombos-tablet', + address: 0x411, + mask: 0x02, + }, + { + id: 'bottle-vendor', + address: 0x3C9, + mask: 0x02, + }, + { + id: 'bumper-cave', + address: 0x2CA, + mask: 0x40, + }, + { + id: 'catfish', + address: 0x410, + mask: 0x20, + }, + { + id: 'desert-ledge', + address: 0x2B0, + mask: 0x40, + }, + { + id: 'digging-game', + address: 0x2E8, + mask: 0x40, + }, + { + id: 'ether-tablet', + address: 0x411, + mask: 0x01, + }, + { + id: 'floating-island', + address: 0x285, + mask: 0x40, + }, + { + id: 'flute-spot', + address: 0x2AA, + mask: 0x40, + }, + { + id: 'hobo', + address: 0x3C9, + mask: 0x01, + }, + { + id: 'lake-hylia-island', + address: 0x2B5, + mask: 0x40, + }, + { + id: 'library', + address: 0x410, + mask: 0x80, + }, + { + id: 'magic-bat', + address: 0x411, + mask: 0x80, + }, + { + id: 'mushroom-spot', + address: 0x411, + mask: 0x10, + }, + { + id: 'old-man', + address: 0x410, + mask: 0x01, + }, + { + id: 'pedestal', + address: 0x300, + mask: 0x40, + }, + { + id: 'potion-shop', + address: 0x411, + mask: 0x20, + }, + { + id: 'purple-chest', + address: 0x3C9, + mask: 0x10, + }, + { + id: 'pyramid', + address: 0x2DB, + mask: 0x40, + }, + { + id: 'race-game', + address: 0x2A8, + mask: 0x40, + }, + { + id: 'saha', + address: 0x410, + mask: 0x10, + }, + { + id: 'sick-kid', + address: 0x410, + mask: 0x04, + }, + { + id: 'spec-rock', + address: 0x283, + mask: 0x40, + }, + { + id: 'stumpy', + address: 0x410, + mask: 0x08, + }, + { + id: 'sunken-treasure', + address: 0x2BB, + mask: 0x40, + }, + { + id: 'uncle', + address: 0x3C6, + mask: 0x01, + }, + { + id: 'zora', + address: 0x410, + mask: 0x02, + }, + { + id: 'zora-ledge', + address: 0x301, + mask: 0x40, + }, +]; + +export const UNDERWORLD_LOCATIONS = [ + { + id: 'aginah', + room: 0x10A, + chest: 0, + }, + { + id: 'blinds-hut-top', + room: 0x11D, + chest: 0, + }, + { + id: 'blinds-hut-left', + room: 0x11D, + chest: 1, + }, + { + id: 'blinds-hut-right', + room: 0x11D, + chest: 2, + }, + { + id: 'blinds-hut-far-left', + room: 0x11D, + chest: 3, + }, + { + id: 'blinds-hut-far-right', + room: 0x11D, + chest: 4, + }, + { + id: 'bonk-rocks', + room: 0x124, + chest: 0, + }, + { + id: 'brewery', + room: 0x106, + chest: 0, + }, + { + id: 'c-house', + room: 0x11C, + chest: 0, + }, + { + id: 'cave-45', + room: 0x11B, + chest: 6, + }, + { + id: 'checkerboard', + room: 0x126, + chest: 5, + }, + { + id: 'chest-game', + room: 0x106, + chest: 6, + }, + { + id: 'chicken-house', + room: 0x108, + chest: 0, + }, + { + id: 'ct-1', + area: 'ct', + room: 0xE0, + chest: 0, + }, + { + id: 'ct-2', + area: 'ct', + room: 0xD0, + chest: 0, + }, + { + id: 'dark-cross', + area: 'hc', + room: 0x32, + chest: 0, + }, + { + id: 'dp-big-chest', + area: 'dp', + room: 0x73, + chest: 0, + }, + { + id: 'dp-big-key-chest', + area: 'dp', + room: 0x75, + chest: 0, + }, + { + id: 'dp-compass-chest', + area: 'dp', + room: 0x85, + chest: 0, + }, + { + id: 'dp-map-chest', + area: 'dp', + room: 0x74, + chest: 0, + }, + { + id: 'dp-torch', + area: 'dp', + room: 0x73, + chest: 6, + }, + { + id: 'ep-big-chest', + area: 'ep', + room: 0xA9, + chest: 0, + }, + { + id: 'ep-big-key-chest', + area: 'ep', + room: 0xB8, + chest: 0, + }, + { + id: 'ep-cannonball', + area: 'ep', + room: 0xB9, + chest: 0, + }, + { + id: 'ep-compass-chest', + area: 'ep', + room: 0xA8, + chest: 0, + }, + { + id: 'ep-map-chest', + area: 'ep', + room: 0xAA, + chest: 0, + }, + { + id: 'flooded-chest', + room: 0x10B, + chest: 0, + }, + { + id: 'graveyard-ledge', + room: 0x11B, + chest: 5, + }, + { + id: 'gt-hope-left', + area: 'gt', + room: 0x8C, + chest: 1, + }, + { + id: 'gt-hope-right', + area: 'gt', + room: 0x8C, + chest: 2, + }, + { + id: 'gt-tile-room', + area: 'gt', + room: 0x8D, + chest: 0, + }, + { + id: 'gt-compass-tl', + area: 'gt', + room: 0x9D, + chest: 0, + }, + { + id: 'gt-compass-tr', + area: 'gt', + room: 0x9D, + chest: 1, + }, + { + id: 'gt-compass-bl', + area: 'gt', + room: 0x9D, + chest: 2, + }, + { + id: 'gt-compass-br', + area: 'gt', + room: 0x9D, + chest: 3, + }, + { + id: 'gt-torch', + area: 'gt', + room: 0x8C, + chest: 6, + }, + { + id: 'gt-dm-tl', + area: 'gt', + room: 0x7B, + chest: 0, + }, + { + id: 'gt-dm-tr', + area: 'gt', + room: 0x7B, + chest: 1, + }, + { + id: 'gt-dm-bl', + area: 'gt', + room: 0x7B, + chest: 2, + }, + { + id: 'gt-dm-br', + area: 'gt', + room: 0x7B, + chest: 3, + }, + { + id: 'gt-map-chest', + area: 'gt', + room: 0x8B, + chest: 0, + }, + { + id: 'gt-firesnake', + area: 'gt', + room: 0x7D, + chest: 0, + }, + { + id: 'gt-rando-tl', + area: 'gt', + room: 0x7C, + chest: 0, + }, + { + id: 'gt-rando-tr', + area: 'gt', + room: 0x7C, + chest: 1, + }, + { + id: 'gt-rando-bl', + area: 'gt', + room: 0x7C, + chest: 2, + }, + { + id: 'gt-rando-br', + area: 'gt', + room: 0x7C, + chest: 3, + }, + { + id: 'gt-bobs-chest', + area: 'gt', + room: 0x8C, + chest: 3, + }, + { + id: 'gt-ice-left', + area: 'gt', + room: 0x1C, + chest: 1, + }, + { + id: 'gt-ice-mid', + area: 'gt', + room: 0x1C, + chest: 0, + }, + { + id: 'gt-ice-right', + area: 'gt', + room: 0x1C, + chest: 2, + }, + { + id: 'gt-big-chest', + area: 'gt', + room: 0x8C, + chest: 0, + }, + { + id: 'gt-helma-left', + area: 'gt', + room: 0x3D, + chest: 0, + }, + { + id: 'gt-helma-right', + area: 'gt', + room: 0x3D, + chest: 1, + }, + { + id: 'gt-pre-moldorm', + area: 'gt', + room: 0x3D, + chest: 2, + }, + { + id: 'gt-post-moldorm', + area: 'gt', + room: 0x4D, + chest: 0, + }, + { + id: 'hammer-pegs', + room: 0x127, + chest: 6, + }, + { + id: 'hc-boom', + area: 'hc', + room: 0x71, + chest: 0, + }, + { + id: 'hc-cell', + area: 'hc', + room: 0x80, + chest: 0, + }, + { + id: 'hc-map', + area: 'hc', + room: 0x72, + chest: 0, + }, + { + id: 'hookshot-cave-br', + room: 0x3C, + chest: 3, + }, + { + id: 'hookshot-cave-tr', + room: 0x3C, + chest: 0, + }, + { + id: 'hookshot-cave-tl', + room: 0x3C, + chest: 1, + }, + { + id: 'hookshot-cave-bl', + room: 0x3C, + chest: 2, + }, + { + id: 'hype-cave-top', + room: 0x11E, + chest: 0, + }, + { + id: 'hype-cave-left', + room: 0x11E, + chest: 1, + }, + { + id: 'hype-cave-right', + room: 0x11E, + chest: 2, + }, + { + id: 'hype-cave-bottom', + room: 0x11E, + chest: 4, + }, + { + id: 'hype-cave-npc', + room: 0x11E, + chest: 6, + }, + { + id: 'ice-rod-cave', + room: 0x120, + chest: 0, + }, + { + id: 'ip-compass-chest', + area: 'ip', + room: 0x2E, + chest: 0, + }, + { + id: 'ip-big-key-chest', + area: 'ip', + room: 0x1F, + chest: 0, + }, + { + id: 'ip-map-chest', + area: 'ip', + room: 0x3F, + chest: 0, + }, + { + id: 'ip-spike-chest', + area: 'ip', + room: 0x5F, + chest: 0, + }, + { + id: 'ip-freezor-chest', + area: 'ip', + room: 0x7E, + chest: 0, + }, + { + id: 'ip-big-chest', + area: 'ip', + room: 0x9E, + chest: 0, + }, + { + id: 'ip-ice-t', + area: 'ip', + room: 0xAE, + chest: 0, + }, + { + id: 'kak-well-top', + room: 0x2F, + chest: 0, + }, + { + id: 'kak-well-left', + room: 0x2F, + chest: 1, + }, + { + id: 'kak-well-mid', + room: 0x2F, + chest: 2, + }, + { + id: 'kak-well-right', + room: 0x2F, + chest: 3, + }, + { + id: 'kak-well-bottom', + room: 0x2F, + chest: 4, + }, + { + id: 'kings-tomb', + room: 0x113, + chest: 0, + }, + { + id: 'links-house', + room: 0x104, + chest: 0, + }, + { + id: 'lost-woods-hideout', + room: 0xE1, + chest: 5, + }, + { + id: 'lumberjack', + room: 0xE2, + chest: 5, + }, + { + id: 'mimic-cave', + room: 0x10C, + chest: 0, + }, + { + id: 'mini-moldorm-far-left', + room: 0x123, + chest: 0, + }, + { + id: 'mini-moldorm-left', + room: 0x123, + chest: 1, + }, + { + id: 'mini-moldorm-right', + room: 0x123, + chest: 2, + }, + { + id: 'mini-moldorm-far-right', + room: 0x123, + chest: 3, + }, + { + id: 'mini-moldorm-npc', + room: 0x123, + chest: 6, + }, + { + id: 'mm-bridge-chest', + room: 0xA2, + chest: 0, + }, + { + id: 'mm-spike-chest', + room: 0xB3, + chest: 0, + }, + { + id: 'mm-lobby-chest', + room: 0xC2, + chest: 0, + }, + { + id: 'mm-compass-chest', + room: 0xC1, + chest: 0, + }, + { + id: 'mm-big-key-chest', + room: 0xD1, + chest: 0, + }, + { + id: 'mm-big-chest', + room: 0xC3, + chest: 0, + }, + { + id: 'mm-map-chest', + room: 0xC3, + chest: 1, + }, + { + id: 'mire-shed-left', + room: 0x10D, + chest: 0, + }, + { + id: 'mire-shed-right', + room: 0x10D, + chest: 1, + }, + { + id: 'paradox-lower-far-left', + room: 0xEF, + chest: 0, + }, + { + id: 'paradox-lower-left', + room: 0xEF, + chest: 1, + }, + { + id: 'paradox-lower-right', + room: 0xEF, + chest: 2, + }, + { + id: 'paradox-lower-far-right', + room: 0xEF, + chest: 4, + }, + { + id: 'paradox-lower-mid', + room: 0xEF, + chest: 5, + }, + { + id: 'paradox-upper-left', + room: 0xFF, + chest: 0, + }, + { + id: 'paradox-upper-right', + room: 0xFF, + chest: 1, + }, + { + id: 'pd-shooter-room', + room: 0x09, + chest: 0, + }, + { + id: 'pd-stalfos-basement', + room: 0x0A, + chest: 0, + }, + { + id: 'pd-big-key-chest', + room: 0x3A, + chest: 0, + }, + { + id: 'pd-arena-bridge', + room: 0x2A, + chest: 1, + }, + { + id: 'pd-arena-ledge', + room: 0x2A, + chest: 0, + }, + { + id: 'pd-map-chest', + room: 0x2B, + chest: 0, + }, + { + id: 'pd-big-chest', + room: 0x1A, + chest: 0, + }, + { + id: 'pd-compass-chest', + room: 0x1A, + chest: 1, + }, + { + id: 'pd-harmless-hellway', + room: 0x1A, + chest: 2, + }, + { + id: 'pd-maze-top', + room: 0x19, + chest: 0, + }, + { + id: 'pd-maze-bottom', + room: 0x19, + chest: 1, + }, + { + id: 'pd-basement-left', + room: 0x6A, + chest: 0, + }, + { + id: 'pd-basement-right', + room: 0x6A, + chest: 1, + }, + { + id: 'pyramid-fairy-left', + room: 0x116, + chest: 0, + }, + { + id: 'pyramid-fairy-right', + room: 0x116, + chest: 1, + }, + { + id: 'saha-left', + room: 0x105, + chest: 0, + }, + { + id: 'saha-mid', + room: 0x105, + chest: 1, + }, + { + id: 'saha-right', + room: 0x105, + chest: 2, + }, + { + id: 'sanc', + area: 'hc', + room: 0x12, + chest: 0, + }, + { + id: 'secret-passage', + room: 0x55, + chest: 0, + }, + { + id: 'sewers-left', + area: 'hc', + room: 0x11, + chest: 0, + }, + { + id: 'sewers-mid', + area: 'hc', + room: 0x11, + chest: 1, + }, + { + id: 'sewers-right', + area: 'hc', + room: 0x11, + chest: 2, + }, + { + id: 'sp-lobby', + area: 'sp', + room: 0x28, + chest: 0, + }, + { + id: 'sp-map-chest', + area: 'sp', + room: 0x37, + chest: 0, + }, + { + id: 'sp-big-chest', + area: 'sp', + room: 0x36, + chest: 0, + }, + { + id: 'sp-compass-chest', + area: 'sp', + room: 0x46, + chest: 0, + }, + { + id: 'sp-west-chest', + area: 'sp', + room: 0x34, + chest: 0, + }, + { + id: 'sp-big-key-chest', + area: 'sp', + room: 0x35, + chest: 0, + }, + { + id: 'sp-flooded-left', + area: 'sp', + room: 0x76, + chest: 0, + }, + { + id: 'sp-flooded-right', + area: 'sp', + room: 0x76, + chest: 1, + }, + { + id: 'sp-waterfall', + area: 'sp', + room: 0x66, + chest: 0, + }, + { + id: 'spec-rock-cave', + room: 0xEA, + chest: 6, + }, + { + id: 'spike-cave', + room: 0x117, + chest: 0, + }, + { + id: 'spiral-cave', + room: 0xFE, + chest: 0, + }, + { + id: 'super-bunny-top', + room: 0xF8, + chest: 0, + }, + { + id: 'super-bunny-bottom', + room: 0xF8, + chest: 0, + }, + { + id: 'sw-big-chest', + area: 'sw', + room: 0x58, + chest: 0, + }, + { + id: 'sw-map-chest', + area: 'sw', + room: 0x58, + chest: 1, + }, + { + id: 'sw-compass-chest', + area: 'sw', + room: 0x67, + chest: 0, + }, + { + id: 'sw-big-key-chest', + area: 'sw', + room: 0x57, + chest: 0, + }, + { + id: 'sw-pot-prison', + area: 'sw', + room: 0x57, + chest: 1, + }, + { + id: 'sw-pinball-room', + area: 'sw', + room: 0x68, + chest: 0, + }, + { + id: 'sw-bridge-chest', + area: 'sw', + room: 0x59, + chest: 0, + }, + { + id: 'tavern', + room: 0x103, + chest: 0, + }, + { + id: 'th-basement-cage', + area: 'th', + room: 0x87, + chest: 6, + }, + { + id: 'th-big-key-chest', + area: 'th', + room: 0x87, + chest: 0, + }, + { + id: 'th-map-chest', + area: 'th', + room: 0x77, + chest: 0, + }, + { + id: 'th-big-chest', + area: 'th', + room: 0x27, + chest: 0, + }, + { + id: 'th-compass-chest', + area: 'th', + room: 0x27, + chest: 1, + }, + { + id: 'tr-roller-left', + area: 'tr', + room: 0xB7, + chest: 0, + }, + { + id: 'tr-roller-right', + area: 'tr', + room: 0xB7, + chest: 1, + }, + { + id: 'tr-compass-chest', + area: 'tr', + room: 0xD6, + chest: 0, + }, + { + id: 'tr-chomps', + area: 'tr', + room: 0xB6, + chest: 0, + }, + { + id: 'tr-big-key-chest', + area: 'tr', + room: 0x14, + chest: 0, + }, + { + id: 'tr-big-chest', + area: 'tr', + room: 0x24, + chest: 0, + }, + { + id: 'tr-crysta-roller', + area: 'tr', + room: 0x04, + chest: 0, + }, + { + id: 'tr-laser-bridge-top', + area: 'tr', + room: 0xD5, + chest: 0, + }, + { + id: 'tr-laser-bridge-left', + area: 'tr', + room: 0xD5, + chest: 1, + }, + { + id: 'tr-laser-bridge-right', + area: 'tr', + room: 0xD5, + chest: 2, + }, + { + id: 'tr-laser-bridge-bottom', + area: 'tr', + room: 0xD5, + chest: 3, + }, + { + id: 'tt-map-chest', + area: 'tt', + room: 0xDB, + chest: 0, + }, + { + id: 'tt-big-key-chest', + area: 'tt', + room: 0xDB, + chest: 1, + }, + { + id: 'tt-ambush-chest', + area: 'tt', + room: 0xCB, + chest: 0, + }, + { + id: 'tt-compass-chest', + area: 'tt', + room: 0xDC, + chest: 0, + }, + { + id: 'tt-attic', + area: 'tt', + room: 0x65, + chest: 0, + }, + { + id: 'tt-cell', + area: 'tt', + room: 0x45, + chest: 0, + }, + { + id: 'tt-big-chest', + area: 'tt', + room: 0x44, + chest: 0, + }, + { + id: 'waterfall-fairy-left', + room: 0x114, + chest: 4, + }, + { + id: 'waterfall-fairy-right', + room: 0x114, + chest: 5, + }, +]; + +export const toggleBoolean = name => state => ({ + ...state, + [name]: !state[name], +}); + +export const increment = (name, max, skipZero) => state => { + let newValue = ((state[name] || 0) + 1) % (max + 1); + if (skipZero && !newValue) { + newValue = 1; + } + return { + ...state, + [name]: newValue, + }; +}; + +export const decrement = (name, max, skipZero) => state => { + let newValue = ((state[name] || 0) + max) % (max + 1); + if (skipZero && !newValue) { + newValue = max; + } + return { + ...state, + [name]: newValue, + }; +}; + +export const highestActive = (state, names) => { + for (let i = names.length; i >= 0; --i) { + if (state[names[i]]) { + return names[i]; + } + } + return null; +}; + +export const hasDungeonBoss = (state, dungeon) => !!state[`${dungeon.id}-boss-defeated`]; + +export const getDungeonBoss = (state, dungeon) => + state[`${dungeon.id}-boss`] || dungeon.boss || null; + +export const hasDungeonPrize = (state, dungeon) => !!state[`${dungeon.id}-prize-acquired`]; + +export const getDungeonPrize = (state, dungeon) => state[`${dungeon.id}-prize`] || null; + +export const makeEmptyState = () => { + const state = {}; + BOOLEAN_STATES.forEach(p => { + state[p] = INITIAL[p] || false; + }); + INTEGER_STATES.forEach(p => { + state[p] = INITIAL[p] || 0; + }); + DUNGEONS.forEach(dungeon => { + state[`${dungeon.id}-map`] = false; + state[`${dungeon.id}-compass`] = false; + state[`${dungeon.id}-small-key`] = 0; + state[`${dungeon.id}-big-key`] = false; + state[`${dungeon.id}-checks`] = 0; + if (dungeon.boss) { + state[`${dungeon.id}-boss`] = dungeon.boss; + state[`${dungeon.id}-boss-defeated`] = false; + } + if (dungeon.prize) { + state[`${dungeon.id}-prize`] = 'crystal'; + state[`${dungeon.id}-prize-acquired`] = false; + } + }); + return state; +}; + +const collectInventory = (state, data, prizeMap) => { + state.bow = !!(data[INV_ADDR.RANDO_BOW] & 0x80); + state.silvers = (data[INV_ADDR.RANDO_BOW] & 0xC0) == 0xC0; + state['bowless-silvers'] = (data[INV_ADDR.RANDO_BOW] & 0xC0) == 0x40; + state['blue-boomerang'] = !!(data[INV_ADDR.RANDO_BOOM] & 0x40); + state['red-boomerang'] = !!(data[INV_ADDR.RANDO_BOOM] & 0x80); + state.hookshot = !!data[INV_ADDR.HOOK]; + state.bomb = data[INV_ADDR.BOMB]; + state.mushroom = !!(data[INV_ADDR.RANDO_POWDER] & 0x20); + state.powder = !!(data[INV_ADDR.RANDO_POWDER] & 0x10); + state['fire-rod'] = !!data[INV_ADDR.FROD]; + state['ice-rod'] = !!data[INV_ADDR.IROD]; + state.bombos = !!data[INV_ADDR.BOMBOS]; + state.ether = !!data[INV_ADDR.ETHER]; + state.quake = !!data[INV_ADDR.QUAKE]; + state.lamp = !!data[INV_ADDR.LAMP]; + state.hammer = !!data[INV_ADDR.HAMMER]; + state.shovel = !!(data[INV_ADDR.RANDO_FLUTE] & 0x04); + state.flute = !!(data[INV_ADDR.RANDO_FLUTE] & 0x03); + state.duck = !!(data[INV_ADDR.RANDO_FLUTE] & 0x01); + state.bugnet = !!data[INV_ADDR.BUGNET]; + state.book = !!data[INV_ADDR.BOOK]; + state.bottle = 0; + if (data[INV_ADDR.BOTTLE_1]) { + ++state.bottle; + } + if (data[INV_ADDR.BOTTLE_2]) { + ++state.bottle; + } + if (data[INV_ADDR.BOTTLE_3]) { + ++state.bottle; + } + if (data[INV_ADDR.BOTTLE_4]) { + ++state.bottle; + } + state.somaria = !!data[INV_ADDR.SOMARIA]; + state.byrna = !!data[INV_ADDR.BYRNA]; + state.cape = !!data[INV_ADDR.CAPE]; + state.mirror = !!data[INV_ADDR.MIRROR]; + state.lift = data[INV_ADDR.GLOVE]; + state.boots = !!data[INV_ADDR.BOOTS]; + state.flippers = !!data[INV_ADDR.FLIPPERS]; + state.moonpearl = !!data[INV_ADDR.MOONPEARL]; + state.sword = data[INV_ADDR.SWORD]; + state.shield = data[INV_ADDR.SHIELD]; + state.mail = data[INV_ADDR.ARMOR] + 1; + state['heart-piece'] = data[INV_ADDR.HEART_PIECE]; + state['half-magic'] = data[INV_ADDR.MAGIC_USE] > 0; + state['quarter-magic'] = data[INV_ADDR.MAGIC_USE] > 1; + const map = getShort(data, INV_ADDR.MAP); + const compass = getShort(data, INV_ADDR.COMPASS); + const bigKey = getShort(data, INV_ADDR.BIG_KEY); + DUNGEONS.forEach(dungeon => { + state[`${dungeon.id}-map`] = !!(map & dungeon.mask); + state[`${dungeon.id}-compass`] = !!(compass & dungeon.mask); + state[`${dungeon.id}-small-key`] = data[INV_ADDR.RANDO_KEY_START + dungeon.offset]; + state[`${dungeon.id}-big-key`] = !!(bigKey & dungeon.mask); + if (dungeon.prize) { + const isCrystal = prizeMap[dungeon.offset].isCrystal; + const prizeFlags = data[isCrystal ? INV_ADDR.CRYSTALS : INV_ADDR.PENDANTS]; + state[`${dungeon.id}-prize-acquired`] = !!(prizeFlags & prizeMap[dungeon.offset].mask); + } + }); +}; + +const collectOverworld = (state, data) => { + OVERWORLD_LOCATIONS.forEach(location => { + state[location.id] = !!(data[location.address] & location.mask); + }); +}; + +const collectUnderworld = (state, data) => { + UNDERWORLD_LOCATIONS.forEach(location => { + state[location.id] = isChestOpen(data, location.room, location.chest); + }); + DUNGEONS.forEach(dungeon => { + state[`${dungeon.id}-boss-defeated`] = isBossDefeated(data, dungeon.bossRoom); + }); +}; + +export const computeState = (data, prizeMap) => { + const state = {}; + collectInventory(state, data.slice(SRAM_ADDR.INV_START), prizeMap); + collectOverworld(state, data); + collectUnderworld(state, data.slice(SRAM_ADDR.ROOM_DATA_START)); + return state; +}; + +const getDungeonAmounts = (config, state) => { + const amounts = {}; + DUNGEONS.forEach(dungeon => { + let amount = 0; + let total = dungeon.checks.length; + dungeon.checks.forEach(check => { + if (state[check]) { + ++amount; + } + }); + if (!config.wildMap && state[`${dungeon.id}-map`]) { + --amount; + --total; + } + if (!config.wildCompass && state[`${dungeon.id}-compass`]) { + --amount; + --total; + } + if (!config.wildSmall) { + amount -= Math.min(state[`${dungeon.id}-small-key`], dungeon.sk); + total -= dungeon.sk; + } + if (!config.wildBig && !dungeon.dropBk && state[`${dungeon.id}-big-key`]) { + --amount; + --total; + } + amounts[dungeon.id] = Math.min(total, amount); + }); + return amounts; +}; + +export const mergeStates = (config, cur, inc) => { + const next = { ...cur, ...inc }; + const amounts = getDungeonAmounts(config, inc); + DUNGEONS.forEach(dungeon => { + next[`${dungeon.id}-checks`] = amounts[dungeon.id]; + }); + //console.log(next); + return next; +}; diff --git a/resources/js/hooks/tracker.js b/resources/js/hooks/tracker.js new file mode 100644 index 0000000..be5a52f --- /dev/null +++ b/resources/js/hooks/tracker.js @@ -0,0 +1,49 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +import { CONFIG, DUNGEONS, makeEmptyState } from '../helpers/tracker'; + +const context = React.createContext({}); + +export const useTracker = () => React.useContext(context); + +export const TrackerProvider = ({ children }) => { + const [config, setConfig] = React.useState(CONFIG); + const [state, setState] = React.useState(makeEmptyState()); + const [dungeons, setDungeons] = React.useState(DUNGEONS); + + React.useEffect(() => { + const newDungeons = DUNGEONS.map(dungeon => { + const newDungeon = JSON.parse(JSON.stringify(dungeon)); + if (config.wildMap && dungeon.map) { + ++newDungeon.items; + } + if (config.wildCompass && dungeon.compass) { + ++newDungeon.items; + } + if (config.wildSmall) { + newDungeon.items += dungeon.sk; + } + if (config.wildBig && dungeon.bk && !dungeon.dropBk) { + ++newDungeon.items; + } + if (!config.bossShuffle && dungeon.boss) { + newDungeon.bosses = [dungeon.boss]; + } + return newDungeon; + }); + setDungeons(newDungeons); + }, [config]); + + const value = React.useMemo(() => { + return { config, setConfig, dungeons, setState, state }; + }, [config, dungeons, state]); + + return + {children} + ; +}; + +TrackerProvider.propTypes = { + children: PropTypes.node, +}; diff --git a/resources/js/i18n/de.js b/resources/js/i18n/de.js index b8442dc..3b6240c 100644 --- a/resources/js/i18n/de.js +++ b/resources/js/i18n/de.js @@ -242,6 +242,7 @@ export default { 'bottle-bee': 'Bee in a Bottle', bottle: 'Bottle', bow: 'Bow', + 'bowless-silvers': 'Silvers ohne Bow', bugnet: 'Bugnet', byrna: 'Cane of Byrna', cape: 'Cape', @@ -270,9 +271,11 @@ export default { flippers: 'Flippers', flute: 'Flute', glove: 'Power Glove', + 'gold-sword': 'Gold Sword', 'green-mail': 'Green Mail', 'green-pendant': 'Pendant of Courage', 'green-potion': 'Green Potion', + 'half-magic': 'Half Magic', hammer: 'Hammer', 'heart-container': 'Heart Container', 'heart-piece': 'Heart Piece', @@ -280,6 +283,7 @@ export default { 'ice-rod': 'Ice Rod', lamp: 'Lamp', map: 'Map', + 'master-sword': 'Master Sword', mirror: 'Mirror', 'mirror-shield': 'Mirror Shield', mitts: 'Titan \'s Mitts', @@ -290,6 +294,7 @@ export default { 'not-moonpearl': 'Keine Moonpearl', powder: 'Powder', quake: 'Quake', + 'quarter-magic': 'Quarter Magic', 'red-bomb': 'Red Bomb', 'red-boomerang': 'Red Boomerang', 'red-mail': 'Red Mail', @@ -299,6 +304,7 @@ export default { silvers: 'Silvers', 'small-key': 'Small Key', somaria: 'Cane of Somaria', + 'tempered-sword': 'Tempered Sword', }, }, map: { diff --git a/resources/js/i18n/en.js b/resources/js/i18n/en.js index c5efdf4..b4acc0c 100644 --- a/resources/js/i18n/en.js +++ b/resources/js/i18n/en.js @@ -242,6 +242,7 @@ export default { 'bottle-bee': 'Bee in a Bottle', bottle: 'Bottle', bow: 'Bow', + 'bowless-silvers': 'Silvers w/o Bow', bugnet: 'Bugnet', byrna: 'Cane of Byrna', cape: 'Cape', @@ -270,9 +271,11 @@ export default { flippers: 'Flippers', flute: 'Flute', glove: 'Power Glove', + 'gold-sword': 'Gold Sword', 'green-mail': 'Green Mail', 'green-pendant': 'Pendant of Courage', 'green-potion': 'Green Potion', + 'half-magic': 'Half Magic', hammer: 'Hammer', 'heart-container': 'Heart Container', 'heart-piece': 'Heart Piece', @@ -280,6 +283,7 @@ export default { 'ice-rod': 'Ice Rod', lamp: 'Lamp', map: 'Map', + 'master-sword': 'Master Sword', mirror: 'Mirror', 'mirror-shield': 'Mirror Shield', mitts: 'Titan \'s Mitts', @@ -290,6 +294,7 @@ export default { 'not-moonpearl': 'No Moonpearl', powder: 'Powder', quake: 'Quake', + 'quarter-magic': 'Quarter Magic', 'red-bomb': 'Red Bomb', 'red-boomerang': 'Red Boomerang', 'red-mail': 'Red Mail', @@ -299,6 +304,7 @@ export default { silvers: 'Silvers', 'small-key': 'Small Key', somaria: 'Cane of Somaria', + 'tempered-sword': 'Tempered Sword', }, }, map: { diff --git a/resources/js/pages/Tracker.js b/resources/js/pages/Tracker.js new file mode 100644 index 0000000..eccd766 --- /dev/null +++ b/resources/js/pages/Tracker.js @@ -0,0 +1,17 @@ +import React from 'react'; +import { Helmet } from 'react-helmet'; + +import ErrorBoundary from '../components/common/ErrorBoundary'; +import Tracker from '../components/tracker'; +import { TrackerProvider } from '../hooks/tracker'; + +export const Component = () => { + return + + Tracker + + + + + ; +}; diff --git a/resources/sass/app.scss b/resources/sass/app.scss index 8413908..7554a9e 100644 --- a/resources/sass/app.scss +++ b/resources/sass/app.scss @@ -19,4 +19,5 @@ @import 'rounds'; @import 'techniques'; @import 'tournaments'; +@import 'tracker'; @import 'users'; diff --git a/resources/sass/common.scss b/resources/sass/common.scss index 0cb6655..8fe76bb 100644 --- a/resources/sass/common.scss +++ b/resources/sass/common.scss @@ -258,6 +258,7 @@ h1 { position: relative; display: inline-flex; align-items: center; + vertical-align: middle; width: 2em; height: 2em; @@ -266,6 +267,13 @@ h1 { max-width: 100%; max-height: 100%; } + .item-map-icon { + display: inline-block; + width: 100%; + height: 100%; + background: url(/items-v1.png); + background-size: 800% 1100%; + } .strike { position: absolute; top: 0; diff --git a/resources/sass/tracker.scss b/resources/sass/tracker.scss new file mode 100644 index 0000000..06f1577 --- /dev/null +++ b/resources/sass/tracker.scss @@ -0,0 +1,130 @@ +.tracker { + .count-display { + background: black; + font-weight: bold; + text-align: center; + } + .dungeon { + display: flex; + flex-direction: row; + align-items: stretch; + justify-content: flex-start; + gap: 1ex; + > * { + width: 2em; + height: 2em; + } + .dungeon-smalls .count-display, + .dungeon-tag { + background: black; + font-family: monospace; + font-size: 115%; + font-weight: bold; + text-align: center; + } + .dungeon-checks, + .dungeon-smalls { + position: relative; + .count-display { + pointer-events: none; + position: absolute; + top: .3ex; + left: .3ex; + bottom: .3ex; + right: .3ex; + &.is-zero { + display: none; + } + } + } + } + .dungeon-ep, + .dungeon-pd { + margin-top: 1ex; + } + .equipment { + display: grid; + grid-template-columns: 3em 3em 3em 3em 3em; + gap: 1ex; + padding: 1ex; + } + .items { + display: grid; + grid-template-columns: 3em 3em 3em 3em 3em; + gap: 1ex; + padding: 1ex; + } + .item { + position: relative; + width: 3em; + height: 3em; + + .bottom-left, + .bottom-right, + .top-left, + .top-right { + position: absolute; + width: 50%; + height: 50%; + .zelda-icon { + transform: scale(1.4); + } + } + .bottom-left { + bottom: 0; + left: 0; + } + .bottom-right { + bottom: 0; + right: 0; + } + .top-left { + top: 0; + left: 0; + } + .top-right { + top: 0; + right: 0; + } + + .left, + .right { + position: absolute; + width: 50%; + height: 100%; + overflow: hidden; + .zelda-icon { + width: 200%; + margin-left: -50%; + } + } + .left { + left: 0; + } + .right { + right: 0; + } + .count-display { + pointer-events: none; + &.is-zero { + display: none; + } + } + } + .toggle-icon { + &.inactive { + opacity: .5; + } + } + .tracker-toolbar { + .toggle-icon { + display: inline-block; + width: 2em; + height: 2em; + } + } + .zelda-icon { + width: 100%; + height: 100%; + } +}