Merge pull request #39 from TeamLumi/trainer-URL-query

Add trainer url query and trainer search bar
This commit is contained in:
AarCon
2026-06-20 19:08:25 -06:00
committed by GitHub
10 changed files with 428 additions and 183 deletions
+73 -7
View File
@@ -5,6 +5,7 @@ import useIsBrowser from '@docusaurus/useIsBrowser';
import {
sortedCoordinates,
getSelectedLocation,
getLocationCoordsFromZoneId,
} from './coordinates';
import { SearchBar } from './SearchBar';
import { MapperTabPanel } from './TabPanel/MapperTabPanel';
@@ -14,6 +15,9 @@ import './style.css';
import {
getAreaEncounters,
getTrainersFromZoneId,
getZoneIdFromTrainerId,
getFullTrainerById,
getAllTrainers,
getFieldItemsFromZoneID,
getHiddenItemsFromZoneID,
getPokemonIdFromName
@@ -50,13 +54,7 @@ const canvasDimensions = {
height: 720
}
const versionNumber = "Beta 1.2.0";
export const CLEAR_MODE = {
HIGHLIGHT: "highlight",
SELECT: "select",
ENCOUNTER: "enc",
};
const versionNumber = "Beta 1.2.1";
export const Mapper = ({ pokemonList3, pokemonList, pokemonListV }) => {
const isBrowser = useIsBrowser();
@@ -83,6 +81,9 @@ export const Mapper = ({ pokemonList3, pokemonList, pokemonListV }) => {
const [trainerList, setTrainerList] = useState([]);
const [showTrainerModal, setShowTrainerModal] = useState(false);
const [searchBarTrainer, setSearchBarTrainer] = useState(null);
const skipSearchClear = useRef(false);
const [showSettings, setShowSettings] = useState(false);
const [colors, setColors] = useState({
hov: { r: 247, g: 100, b: 200, a: 0.7 },
@@ -105,6 +106,37 @@ export const Mapper = ({ pokemonList3, pokemonList, pokemonListV }) => {
const [fixedShopList, setFixedShops] = useState([]);
const [heartScaleShopList, setHeartScaleShop] = useState([]);
const [selectedTab, setSelectedTab] = useState(
new URLSearchParams(window.location.search).has('trainerId') ? 1 : 0
);
const handleTabChange = (newTab) => {
setSelectedTab(newTab);
};
// Handle URL query parameters for trainer ID
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const trainerId = params.get('trainerId');
if (trainerId) {
const zoneId = getZoneIdFromTrainerId(trainerId, GAMEDATA3);
if (zoneId) {
const location = getLocationCoordsFromZoneId(zoneId);
if (location) {
setSelectedZone(location.name);
}
handleSetLocationZoneId(zoneId);
const trainers = getTrainersFromZoneId(zoneId, GAMEDATA3);
const trainer = trainers.find(t => t.trainerId === parseInt(trainerId));
if (trainer) {
setSelectedTrainer(trainer);
setShowTrainerModal(true);
}
}
}
}, []);
const locationId = useRef(null);
useEffect(() => {
if(locationId !== null) {
@@ -116,6 +148,15 @@ export const Mapper = ({ pokemonList3, pokemonList, pokemonListV }) => {
setEncounterLocations(getMapperRoutesFromPokemonId(selectedPokemon?.id, GAMEDATA3));
}, [selectedPokemon]);
// Clear SearchBar trainer selection when location or selected trainer changes
useEffect(() => {
if (skipSearchClear.current) {
skipSearchClear.current = false;
return;
}
setSearchBarTrainer(null);
}, [selectedZone, selectedTrainer]);
const openTrainerModal = () => {
setShowTrainerModal(true);
};
@@ -123,6 +164,25 @@ export const Mapper = ({ pokemonList3, pokemonList, pokemonListV }) => {
setShowTrainerModal(false);
};
const allTrainers = React.useMemo(() => getAllTrainers(GAMEDATA3), []);
const handleTrainerSelect = (trainer) => {
skipSearchClear.current = true;
setSearchBarTrainer(trainer);
const zoneId = trainer.zoneId;
if (zoneId || zoneId === 0) {
const location = getLocationCoordsFromZoneId(zoneId);
if (location) {
setSelectedZone(location.name);
}
handleSetLocationZoneId(zoneId);
const fullTrainer = getFullTrainerById(trainer.trainerId, GAMEDATA3);
setSelectedTrainer(fullTrainer || trainer);
setShowTrainerModal(true);
setSelectedTab(1);
}
};
const handleOptionChange = (option, value) => {
setEncOptions({
...encOptions,
@@ -320,6 +380,8 @@ export const Mapper = ({ pokemonList3, pokemonList, pokemonListV }) => {
setSelectedTrainer={setSelectedTrainer}
openTrainerModal={openTrainerModal}
routeId={selectedLocation}
selectedTab={selectedTab}
onTabChange={handleTabChange}
/>
</div>
<SearchBar
@@ -333,6 +395,10 @@ export const Mapper = ({ pokemonList3, pokemonList, pokemonListV }) => {
selectedPokemon={selectedPokemon}
setSelectedPokemon={setSelectedPokemon}
handleShowSettings={handleShowSettings}
allTrainers={allTrainers}
onTrainerSelect={handleTrainerSelect}
searchBarTrainer={searchBarTrainer}
setSearchBarTrainer={setSearchBarTrainer}
/>
<TrainersModal
showModal={showTrainerModal}
+270 -152
View File
@@ -1,152 +1,270 @@
import React, { useEffect, useState } from 'react';
import IconButton from '@mui/material/IconButton';
import SettingsIcon from '@mui/icons-material/Settings';
import { Autocomplete, TextField } from '@mui/material';
import Fuse from 'fuse.js';
import { getLocationCoordsFromName, getLocationNames } from './coordinates';
import './style.css';
const PokemonSearchInput = ({
allPokemons,
setDebouncedText,
selectedPokemon,
setSelectedPokemon,
}) => {
const [searchText, setSearchText] = useState("");
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedText(searchText);
}, 300);
return () => clearTimeout(timer);
}, [searchText, setDebouncedText]);
const handlePokemonNameChange = (_, value, reason) => {
if (reason !== "clear" && value) {
setSelectedPokemon(value);
setSearchText(value?.name ?? "");
} else {
setSelectedPokemon(null);
setSearchText("");
}
};
const handleInputChange = (_, value) => {
setSearchText(value);
};
return (
<div className="monSearchBar">
<Autocomplete
id="pokemon-search-input"
clearOnBlur={false}
blurOnSelect
options={allPokemons}
getOptionLabel={(option) => option.name}
value={selectedPokemon}
onChange={handlePokemonNameChange}
inputValue={searchText}
onInputChange={handleInputChange}
renderInput={(params) => (
<TextField
{...params}
label="Search Pokémon Location"
fullWidth
/>
)}
/>
</div>
);
};
const LocationNameDropdown = ({
locationName,
setLocationName,
setLocationZoneId,
canvasRef
}) => {
const locations = getLocationNames();
const handleLocationChange = (e, value, reason) => {
if (reason !== "clear" && value) {
setLocationName(value);
const location = getLocationCoordsFromName(value);
setLocationZoneId(location?.zoneId);
} else {
setLocationName(null);
setLocationZoneId(null);
}
};
const defaultOption = locations.length > 0 ? locations[0] : '';
return (
<div className="location">
<Autocomplete
id="location-input"
clearOnBlur={false}
blurOnSelect
options={locations}
getOptionLabel={(option) => option}
value={locationName}
onChange={handleLocationChange}
inputValue={locationName ?? ""}
onInputChange={(_, value) => {
setLocationName(value);
}}
renderInput={(params) => (
<TextField
{...params}
label="Current Location"
fullWidth
/>
)}
/>
</div>
);
};
const SettingsButton = ({handleShowSettings}) => {
return (
<div className="settings">
<IconButton aria-label="settings" onClick={handleShowSettings}>
<SettingsIcon />
</IconButton>
</div>
);
}
export const SearchBar = ({
canvasDimensions,
pokemonList,
handleDebouncedTextChange,
locationName,
setLocationName,
setLocationZoneId,
canvasRef,
selectedPokemon,
setSelectedPokemon,
handleShowSettings,
}) => {
return (
<div
className="infoCol"
style={{
width: `${canvasDimensions.width}px`,
height: `${canvasDimensions.height}px`
}}
>
<PokemonSearchInput
allPokemons={pokemonList}
setDebouncedText={handleDebouncedTextChange}
selectedPokemon={selectedPokemon}
setSelectedPokemon={setSelectedPokemon}
/>
<LocationNameDropdown
locationName={locationName}
setLocationName={setLocationName}
setLocationZoneId={setLocationZoneId}
/>
<SettingsButton handleShowSettings={handleShowSettings} />
</div>
)
};
import React, { useEffect, useState } from 'react';
import IconButton from '@mui/material/IconButton';
import SettingsIcon from '@mui/icons-material/Settings';
import { Autocomplete, TextField } from '@mui/material';
import { FixedSizeList } from 'react-window';
import { getLocationCoordsFromName, getLocationNames } from './coordinates';
import './style.css';
const PokemonSearchInput = ({
allPokemons,
setDebouncedText,
selectedPokemon,
setSelectedPokemon,
}) => {
const [searchText, setSearchText] = useState("");
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedText(searchText);
}, 300);
return () => clearTimeout(timer);
}, [searchText, setDebouncedText]);
const handlePokemonNameChange = (_, value, reason) => {
if (reason !== "clear" && value) {
setSelectedPokemon(value);
setSearchText(value?.name ?? "");
} else {
setSelectedPokemon(null);
setSearchText("");
}
};
const handleInputChange = (_, value) => {
setSearchText(value);
};
return (
<div className="monSearchBar">
<Autocomplete
id="pokemon-search-input"
clearOnBlur={false}
blurOnSelect
options={allPokemons}
getOptionLabel={(option) => option.name}
value={selectedPokemon}
onChange={handlePokemonNameChange}
inputValue={searchText}
onInputChange={handleInputChange}
renderInput={(params) => (
<TextField
{...params}
label="Search Pokémon Location"
fullWidth
/>
)}
/>
</div>
);
};
const LocationNameDropdown = ({
locationName,
setLocationName,
setLocationZoneId,
}) => {
const locations = getLocationNames();
const handleLocationChange = (e, value, reason) => {
if (reason !== "clear" && value) {
setLocationName(value);
const location = getLocationCoordsFromName(value);
setLocationZoneId(location?.zoneId);
} else {
setLocationName(null);
setLocationZoneId(null);
}
};
const defaultOption = locations.length > 0 ? locations[0] : '';
return (
<div className="location">
<Autocomplete
id="location-input"
clearOnBlur={false}
blurOnSelect
options={locations}
getOptionLabel={(option) => option}
value={locationName}
onChange={handleLocationChange}
inputValue={locationName ?? ""}
onInputChange={(_, value) => {
setLocationName(value);
}}
renderInput={(params) => (
<TextField
{...params}
label="Current Location"
fullWidth
/>
)}
/>
</div>
);
};
const LISTBOX_PADDING = 8;
const ITEM_SIZE = 48;
const MAX_VISIBLE_ITEMS = 14;
function renderRow(props) {
const { data, index, style } = props;
const inlineStyle = {
...style,
top: (style.top ?? 0) + LISTBOX_PADDING,
};
return React.cloneElement(data[index], {
style: {
...data[index].props.style,
...inlineStyle,
},
});
}
const OuterElementContext = React.createContext({});
const OuterElementType = React.forwardRef(function OuterElementType(props, ref) {
const outerProps = React.useContext(OuterElementContext);
return <div ref={ref} {...props} {...outerProps} />;
});
const ListboxComponent = React.forwardRef(function ListboxComponent(props, ref) {
const { children, ...other } = props;
const itemData = React.Children.toArray(children);
const itemCount = itemData.length;
const height = itemCount > MAX_VISIBLE_ITEMS
? MAX_VISIBLE_ITEMS * ITEM_SIZE + 2 * LISTBOX_PADDING
: itemCount * ITEM_SIZE + 2 * LISTBOX_PADDING;
return (
<div ref={ref}>
<OuterElementContext.Provider value={other}>
<FixedSizeList
itemData={itemData}
height={height}
width="100%"
itemSize={ITEM_SIZE}
itemCount={itemCount}
outerElementType={OuterElementType}
innerElementType="ul"
>
{renderRow}
</FixedSizeList>
</OuterElementContext.Provider>
</div>
);
});
const TrainerSearchInput = ({
allTrainers,
onTrainerSelect,
value,
onChange,
}) => {
const [searchText, setSearchText] = useState("");
// Clear internal text when value is cleared externally
useEffect(() => {
if (!value) {
setSearchText("");
}
}, [value]);
const handleTrainerChange = (_, newValue, reason) => {
if (reason !== "clear" && newValue) {
onChange(newValue);
setSearchText(newValue.label);
onTrainerSelect(newValue);
} else {
onChange(null);
setSearchText("");
}
};
const handleInputChange = (_, newInputValue) => {
setSearchText(newInputValue);
};
return (
<div className="monSearchBar">
<Autocomplete
id="trainer-search-input"
clearOnBlur={false}
blurOnSelect
options={allTrainers}
ListboxComponent={ListboxComponent}
getOptionLabel={(option) => option.label}
value={value}
onChange={handleTrainerChange}
inputValue={searchText}
onInputChange={handleInputChange}
renderInput={(params) => (
<TextField
{...params}
label="Search Trainer"
fullWidth
/>
)}
/>
</div>
);
};
const SettingsButton = ({handleShowSettings}) => {
return (
<div className="settings">
<IconButton aria-label="settings" onClick={handleShowSettings}>
<SettingsIcon />
</IconButton>
</div>
);
}
export const SearchBar = ({
canvasDimensions,
pokemonList,
handleDebouncedTextChange,
locationName,
setLocationName,
setLocationZoneId,
canvasRef,
selectedPokemon,
setSelectedPokemon,
handleShowSettings,
allTrainers,
onTrainerSelect,
searchBarTrainer,
setSearchBarTrainer,
}) => {
return (
<div
className="infoCol"
style={{
width: `${canvasDimensions.width}px`,
height: `${canvasDimensions.height}px`
}}
>
<PokemonSearchInput
allPokemons={pokemonList}
setDebouncedText={handleDebouncedTextChange}
selectedPokemon={selectedPokemon}
setSelectedPokemon={setSelectedPokemon}
/>
<TrainerSearchInput
allTrainers={allTrainers}
onTrainerSelect={onTrainerSelect}
value={searchBarTrainer}
onChange={setSearchBarTrainer}
/>
<LocationNameDropdown
locationName={locationName}
setLocationName={setLocationName}
setLocationZoneId={setLocationZoneId}
/>
<SettingsButton handleShowSettings={handleShowSettings} />
</div>
)
};
@@ -32,7 +32,7 @@ const EncountersPanel = ({ encOptions, handleOptionChange, encounterList, pokemo
};
const getGroundTextColor = () => {
if (encounterList.GroundEnc.lenth === 0) {
if (encounterList.GroundEnc.length === 0) {
return modeChangeTextColor;
} else if (groundEncountersIds.includes(routeId)) {
return "#000000"
@@ -16,10 +16,12 @@ export const MapperTabPanel = ({
selectedTrainer,
setSelectedTrainer,
openTrainerModal,
routeId
routeId,
selectedTab,
onTabChange,
}) => {
return (
<PokemonTabPanel tabNames={["Encounters", "Trainers", "Items", "Shops"]}>
<PokemonTabPanel tabNames={["Encounters", "Trainers", "Items", "Shops"]} selectedTab={selectedTab} onTabChange={onTabChange}>
<EncountersPanel
encOptions={encOptions}
handleOptionChange={handleOptionChange}
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React from 'react';
import { Box, Tab, Tabs } from '@mui/material';
const PokemonPanel = ({ id, selectedTab, children }) => {
@@ -30,11 +30,14 @@ const PokemonTabs = ({ tabNames, selectedTab, handleTabChange }) => {
const PokemonTabPanel = ({
tabNames,
children
children,
selectedTab = 0,
onTabChange,
}) => {
const [selectedTab, setSelectedTab] = useState(0);
const handleTabChange = (event, newTab) => {
setSelectedTab(newTab);
if (onTabChange) {
onTabChange(newTab);
}
};
return (
+1 -1
View File
@@ -53,7 +53,7 @@ export const TrainerDropdown = ({ trainer, setTrainer, trainerList, smallest })
<Autocomplete
id="trainer-input"
options={[defaultTrainer, ...trainerList]}
getOptionLabel={(option) => `${option.team_name} (${option.trainer_id})`}
getOptionLabel={(option) => `${option.team_name} (${option.trainerId ?? option.trainer_id})`}
defaultValue={defaultTrainer}
value={trainer}
onChange={(e, value) => setTrainer(value)}
@@ -31,9 +31,9 @@ const TrainersModal = ({
};
const handleOpenShowdown = () => {
if (!selectedTrainer?.trainer_id) return;
if (!selectedTrainer?.trainerId) return;
const url = `https://calc.relumishowdown.dpdns.org/?mode=ingame&setSource=ingame&trainerId=${selectedTrainer.trainer_id}`;
const url = `https://calc.relumishowdown.dpdns.org/?mode=ingame&setSource=ingame&trainerId=${selectedTrainer.trainerId}`;
window.open(url, '_blank', 'noopener,noreferrer');
};
@@ -69,7 +69,7 @@ ${moves}`;
maxWidth="1108px"
>
<DialogTitle>
Trainer: {selectedTrainer?.team_name} ({selectedTrainer?.trainer_id})
Trainer: {selectedTrainer?.team_name} ({selectedTrainer?.trainerId})
</DialogTitle>
<IconButton
aria-label="close"
@@ -98,7 +98,7 @@ ${moves}`;
<IconButton
aria-label="Open In Showdown"
onClick={handleOpenShowdown}
to={`https://calc.relumishowdown.dpdns.org/?mode=ingame&setSource=ingame&trainerId=` + selectedTrainer?.trainer_id}
to={`https://calc.relumishowdown.dpdns.org/?mode=ingame&setSource=ingame&trainerId=` + selectedTrainer?.trainerId}
sx={{
position: 'absolute',
right: 108,
+8 -8
View File
@@ -53,11 +53,13 @@
.infoCol {
position: absolute;
top: 122px;
justify-content: end;
display: grid;
grid-template-rows: 60px 25px;
grid-template-rows: auto auto auto;
align-content: start;
justify-content: end;
pointer-events: none;
touch-action: manipulation;
overflow: hidden;
}
.monSearchBar {
@@ -69,18 +71,16 @@
.location {
pointer-events: auto;
margin-top: 16px;
margin-top: 10px;
margin-right: 10px;
}
.settings {
display: grid;
align-content: end;
justify-content: end;
position: absolute;
bottom: 10px;
right: 10px;
width: fit-content;
height: fit-content;
margin-left: 16rem;
margin-top: 36rem;
pointer-events: auto;
}
+4 -1
View File
@@ -51,7 +51,7 @@ import {
getTimeOfDayEncounters,
getAllHoneyTreeEncounters
} from './encounters'
import { getTrainersFromZoneId } from './trainers';
import { getTrainersFromZoneId, getZoneIdFromTrainerId, getFullTrainerById, getAllTrainers } from './trainers';
import { getFieldItemsFromZoneID, getHiddenItemsFromZoneID } from './location';
import { getEvolutionTree } from './evolution';
import { getEggGroupNameById, getEggGroupViaPokemonId } from './egggroup';
@@ -196,6 +196,9 @@ export {
getTimeOfDayEncounters,
getAllHoneyTreeEncounters,
getTrainersFromZoneId,
getZoneIdFromTrainerId,
getFullTrainerById,
getAllTrainers,
getFieldItemsFromZoneID,
getHiddenItemsFromZoneID
};
+56 -3
View File
@@ -9,12 +9,65 @@ function getTrainersFromZoneId(zoneId, mode = GAMEDATA2) {
zoneKey = "326" // To be at the Lake Valor After for ease of use.
}
if (zoneKey in TrainerLocations[mode]) {
return TrainerLocations[mode][zoneKey];
return TrainerLocations[mode][zoneKey].map(t => ({ ...t, trainerId: t.trainer_id }));
}
console.warn(`${zoneKey} is not in the Trainer List.`);
return [];
};
function getZoneIdFromTrainerId(trainerId, mode = GAMEDATA2) {
if (!trainerId) return null;
for (const zoneKey in TrainerLocations[mode]) {
const trainers = TrainerLocations[mode][zoneKey];
const trainer = trainers.find(t => t.trainer_id === parseInt(trainerId));
if (trainer) {
return parseInt(zoneKey);
}
}
console.warn(`Trainer ID ${trainerId} not found in any zone.`);
return null;
};
function getFullTrainerById(trainerId, mode = GAMEDATA2) {
if (!trainerId) return null;
for (const zoneKey in TrainerLocations[mode]) {
const trainers = TrainerLocations[mode][zoneKey];
const trainer = trainers.find(t => t.trainer_id === parseInt(trainerId));
if (trainer) {
return { ...trainer, trainerId: trainer.trainer_id, zoneId: parseInt(zoneKey) };
}
}
return null;
};
function getAllTrainers(mode = GAMEDATA2) {
const seen = new Set();
const trainers = [];
for (const zoneKey in TrainerLocations[mode]) {
for (const trainer of TrainerLocations[mode][zoneKey]) {
if (seen.has(trainer.trainer_id)) continue;
seen.add(trainer.trainer_id);
trainers.push({
trainerId: trainer.trainer_id,
zoneId: parseInt(zoneKey),
name: trainer.name,
route: trainer.route,
team_name: trainer.team_name,
trainerType: trainer.trainerType,
label: `${trainer.team_name} (${trainer.trainer_id})`,
});
}
}
return trainers;
};
export {
getTrainersFromZoneId
}
getTrainersFromZoneId,
getZoneIdFromTrainerId,
getFullTrainerById,
getAllTrainers,
}