const vecMul = (v, f) => ({ x: v.x * f, y: v.y * f });
+const MAPS = AREAS
+ .filter((area) => !!area.map)
+ .map((area) => ({
+ id: area.id,
+ name: area.name,
+ pos: area.map.pos,
+ size: area.map.size,
+ bg: {
+ src: area.map.bg.src,
+ pos: vecAdd(area.map.pos, area.map.bg.off),
+ size: vecMul(area.map.size, area.map.bg.scale),
+ },
+ entrances: area.entrances
+ .filter((entrance) => entrance.pos)
+ .map((entrance) => ({
+ id: `${area.id}.${entrance.id}`,
+ name: entranceFull({ ...entrance, area }),
+ pos: vecAdd(area.map.pos, entrance.pos),
+ color: entrance.bgColor,
+ })),
+ }));
+
+const getMapEntrance = (id) => {
+ if (!id) return null;
+ const dotPos = id.indexOf('.');
+ if (dotPos === -1) {
+ return null;
+ }
+ const areaId = id.substring(0, dotPos);
+ const area = MAPS.find((a) => a.id === areaId);
+ if (!area) return null;
+ const entrance = area.entrances.find((e) => e.id === id);
+ return entrance;
+};
+
const SelectBox = ({ id, name, onChange, options, value }) => {
const [open, setOpen] = React.useState(false);
const [search, setSearch] = React.useState('');
};
const MapEntrance = ({ entrance }) => {
- const { connections, setConnection } = useTracker();
+ const {
+ connections,
+ isDragging,
+ onMapEntranceClick,
+ setConnection,
+ } = useTracker();
+
+ const classNames = ['entrance'];
+ if (connections[entrance.id] === 'trash') classNames.push('is-trash');
+ if (isDragging(entrance)) classNames.push('is-dragging');
return <circle
cx={entrance.pos.x}
cy={entrance.pos.y}
r="3"
- className={`entrance${connections[entrance.id] === 'trash' ? ' is-trash' : ''}`}
+ className={classNames.join(' ')}
fill={entrance.color}
stroke="#000000"
- onClick={() => {
- if (connections[entrance.id] === 'trash') {
+ onClick={(e) => {
+ onMapEntranceClick(entrance);
+ e.preventDefault();
+ e.stopPropagation();
+ }}
+ onContextMenu={(e) => {
+ if (connections[entrance.id]) {
setConnection(entrance.id, null);
} else {
setConnection(entrance.id, 'trash');
}
+ e.preventDefault();
+ e.stopPropagation();
}}
>
<title>{entrance.name}</title>
x: PropTypes.number,
y: PropTypes.number,
})
+ }),
+};
+
+const MapConnector = ({ from, to }) => {
+ return <line
+ className="connector"
+ x1={from.pos.x}
+ y1={from.pos.y}
+ x2={to.pos.x}
+ y2={to.pos.y}
+ />;
+};
+
+MapConnector.propTypes = {
+ from: PropTypes.shape({
+ pos: PropTypes.shape({
+ x: PropTypes.number,
+ y: PropTypes.number,
+ })
+ }),
+ to: PropTypes.shape({
+ pos: PropTypes.shape({
+ x: PropTypes.number,
+ y: PropTypes.number,
+ })
})
};
const MixedPoolsTracker = () => {
const [connections, setConnections] = React.useState({});
+ const [dragging, setDragging] = React.useState(null);
const setConnection = React.useCallback((src, dst) => {
setConnections((c) => {
return options;
}, []);
+ const isDragging = React.useCallback((entrance) => {
+ return dragging === entrance.id;
+ }, [dragging]);
+
+ const onMapEntranceClick = React.useCallback((entrance) => {
+ if (dragging) {
+ if (dragging !== entrance.id) {
+ setConnection(dragging, entrance.id);
+ }
+ setDragging(null);
+ } else {
+ setDragging(entrance.id);
+ }
+ }, [dragging, setConnection, setDragging]);
+
const context = React.useMemo(() => ({
- connections, entrances, setConnection,
- }), [connections, entrances, setConnection]);
+ connections,
+ entrances,
+ isDragging,
+ onMapEntranceClick,
+ setConnection,
+ }), [
+ connections,
+ entrances,
+ isDragging,
+ onMapEntranceClick,
+ setConnection,
+ ]);
const superGroups = React.useMemo(() => {
const sg = [
return sg;
}, [connections]);
- const maps = React.useMemo(() => {
- return AREAS
- .filter((area) => !!area.map)
- .map((area) => ({
- id: area.id,
- pos: area.map.pos,
- size: area.map.size,
- bg: {
- src: area.map.bg.src,
- pos: vecAdd(area.map.pos, area.map.bg.off),
- size: vecMul(area.map.size, area.map.bg.scale),
- },
- entrances: area.entrances
- .filter((entrance) => entrance.pos)
- .map((entrance) => ({
- id: `${area.id}.${entrance.id}`,
- name: entranceFull({ ...entrance, area }),
- pos: vecAdd(area.map.pos, entrance.pos),
- color: entrance.bgColor,
- })),
- }));
- }, []);
-
const connectors = React.useMemo(() => {
const cs = [];
Object.entries(connections).forEach(([from, to]) => {
+ const fromEntrance = getEntrance(from);
+ if (!fromEntrance) return;
+ if (from > to && !fromEntrance.oneway) return;
+ const fromMap = getMapEntrance(from);
+ if (!fromMap) return;
+ const toMap = getMapEntrance(to);
+ if (!toMap) return;
+ cs.push({
+ from: fromMap,
+ to: toMap,
+ });
});
return cs;
- }, [connections, maps]);
+ }, [connections]);
return <CONTEXT.Provider value={context}>
<div className="mixed-pools-tracker">
- <div className="map">
- <svg viewBox="0 0 500 370">
- {maps.map((map) =>
- <g key={map.id} title={map.name}>
- <image
- href={map.bg.src}
- pointerEvents="none"
- x={map.bg.pos.x} y={map.bg.pos.y}
- width={map.bg.size.x}
- />
- {map.entrances.map((entrance) =>
- <MapEntrance key={entrance.id} entrance={entrance} />
- )}
- </g>
- )}
- </svg>
- </div>
<div className="columns">
{superGroups.map((sg) =>
<div className="column" key={sg.key}>
)}
</div>
</div>
+ <div className="map mt-5">
+ <svg
+ viewBox="0 0 500 370"
+ onClick={() => { setDragging(null); }}
+ onContextMenu={(e) => { e.preventDefault(); e.stopPropagation(); }}
+ >
+ {MAPS.map((map) =>
+ <g key={map.id} title={map.name}>
+ <image
+ href={map.bg.src}
+ pointerEvents="none"
+ x={map.bg.pos.x} y={map.bg.pos.y}
+ width={map.bg.size.x}
+ />
+ {map.entrances.map((entrance) =>
+ <MapEntrance key={entrance.id} entrance={entrance} />
+ )}
+ </g>
+ )}
+ <g title="connectors">
+ {connectors.map((c) =>
+ <MapConnector
+ key={`${c.from.id}-${c.to.id}`}
+ from={c.from}
+ to={c.to}
+ />
+ )}
+ </g>
+ </svg>
+ </div>
</div>
</CONTEXT.Provider>;
};