import { GetRoomEngine } from '@nitrots/nitro-renderer'; import { CSSProperties, FC, MouseEvent as ReactMouseEvent, useCallback, useEffect, useState } from 'react'; import { FaMinus, FaPlus, FaTimes } from 'react-icons/fa'; import { MdGridOn } from 'react-icons/md'; import { LocalizeText, WiredFurniType } from '../../../../api'; import { Button, Text } from '../../../../common'; import { useWired } from '../../../../hooks'; import { sortWiredSourceOptions } from '../WiredSourcesSelector'; import { WiredSelectorBaseView } from './WiredSelectorBaseView'; const SOURCE_USER_TRIGGER = 0; const SOURCE_USER_SIGNAL = 1; const SOURCE_USER_CLICKED = 2; const SOURCE_FURNI_TRIGGER = 3; const SOURCE_FURNI_PICKED = 4; const SOURCE_FURNI_SIGNAL = 5; const USER_SOURCES = sortWiredSourceOptions([ { value: SOURCE_USER_TRIGGER, label: 'wiredfurni.params.sources.users.0' }, { value: SOURCE_USER_SIGNAL, label: 'wiredfurni.params.sources.users.201' }, { value: SOURCE_USER_CLICKED, label: 'wiredfurni.params.sources.users.11' }, ], 'users'); const FURNI_SOURCES = sortWiredSourceOptions([ { value: SOURCE_FURNI_TRIGGER, label: 'wiredfurni.params.sources.furni.0' }, { value: SOURCE_FURNI_PICKED, label: 'wiredfurni.params.sources.furni.100' }, { value: SOURCE_FURNI_SIGNAL, label: 'wiredfurni.params.sources.furni.201' }, ], 'furni'); const TILE_W = 22; const TILE_H = 11; const GRID_RANGE = 4; const CX = GRID_RANGE * TILE_W + TILE_W / 2; const CY = GRID_RANGE * TILE_H + TILE_H / 2; const GRID_PX_W = (GRID_RANGE * 2 + 1) * TILE_W; const GRID_PX_H = (GRID_RANGE * 2 + 1) * TILE_H; type Tile = { x: number; y: number }; const tileIncluded = (tiles: Tile[], x: number, y: number) => tiles.some(t => t.x === x && t.y === y); const tileLeft = (rx: number, ry: number) => CX + (rx - ry) * (TILE_W / 2) - TILE_W / 2; const tileTop = (rx: number, ry: number) => CY + (rx + ry) * (TILE_H / 2) - TILE_H / 2; interface GridProps { selectedTiles: Tile[]; targetTile: Tile; invert: boolean; onSetTile: (x: number, y: number, selected: boolean) => void; onMoveTarget: (x: number, y: number) => void; targetPlacementMode: boolean; } const NeighborhoodGrid: FC = ({ selectedTiles, targetTile, invert, onSetTile, onMoveTarget, targetPlacementMode }) => { const [ dragMode, setDragMode ] = useState<'add' | 'remove' | 'target' | null>(null); const tiles: JSX.Element[] = []; useEffect(() => { const stopDragging = () => setDragMode(null); window.addEventListener('mouseup', stopDragging); return () => window.removeEventListener('mouseup', stopDragging); }, []); const beginTileDrag = (event: ReactMouseEvent, rx: number, ry: number, isTarget: boolean, isSelected: boolean) => { event.preventDefault(); if(targetPlacementMode) { setDragMode('target'); onMoveTarget(rx, ry); return; } const nextMode = isSelected ? 'remove' : 'add'; setDragMode(nextMode); onSetTile(rx, ry, nextMode === 'add'); }; const continueTileDrag = (event: ReactMouseEvent, rx: number, ry: number, isTarget: boolean) => { if(!(event.buttons & 1) || !dragMode) return; if(dragMode === 'target') { onMoveTarget(rx, ry); return; } onSetTile(rx, ry, dragMode === 'add'); }; for (let ry = -GRID_RANGE; ry <= GRID_RANGE; ry++) { for (let rx = -GRID_RANGE; rx <= GRID_RANGE; rx++) { const isTarget = rx === targetTile.x && ry === targetTile.y; const isSelected = tileIncluded(selectedTiles, rx, ry); const isActive = invert ? !isSelected : isSelected; const left = tileLeft(rx, ry); const top_ = tileTop(rx, ry); const zIdx = rx + ry + GRID_RANGE * 2 + 10; const bgColor = isActive ? '#3399ff' : '#2a3042'; const borderColor = isTarget ? '#ffffff' : isActive ? '#1166cc' : '#1a2032'; const diamond: CSSProperties = { position: 'absolute', width: TILE_W, height: TILE_H, left, top: top_, clipPath: 'polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)', backgroundColor: bgColor, cursor: 'pointer', zIndex: zIdx, display: 'flex', alignItems: 'center', justifyContent: 'center', color: isTarget ? '#ffffff' : 'transparent', fontSize: 10 }; const border: CSSProperties = { position: 'absolute', width: TILE_W + (isTarget ? 6 : 2), height: TILE_H + (isTarget ? 6 : 2), left: left - (isTarget ? 3 : 1), top: top_ - (isTarget ? 3 : 1), clipPath: 'polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)', backgroundColor: borderColor, zIndex: zIdx - 1, pointerEvents: 'none', }; const targetOutline: CSSProperties = { position: 'absolute', width: TILE_W + 4, height: TILE_H + 4, left: left - 2, top: top_ - 2, zIndex: zIdx + 1, pointerEvents: 'none', overflow: 'visible' }; tiles.push(
, isTarget && ( ),
beginTileDrag(event, rx, ry, isTarget, isSelected) } onMouseEnter={ event => continueTileDrag(event, rx, ry, isTarget) } />, ); } } return (
event.preventDefault() }> { tiles }
); }; export const WiredSelectorUsersNeighborhoodView: FC<{}> = () => { const [ selectedTiles, setSelectedTiles ] = useState([]); const [ filterExisting, setFilterExisting ] = useState(false); const [ invert, setInvert ] = useState(false); const [ sourceType, setSourceType ] = useState(SOURCE_USER_TRIGGER); const [ targetTile, setTargetTile ] = useState({ x: 0, y: 0 }); const [ targetPlacementMode, setTargetPlacementMode ] = useState(false); const [ curX, setCurX ] = useState(0); const [ curY, setCurY ] = useState(0); const { trigger = null, furniIds = [], setIntParams } = useWired(); useEffect(() => { GetRoomEngine().areaSelectionManager.clearHighlight(); GetRoomEngine().areaSelectionManager.deactivate(); }, []); useEffect(() => { if(!trigger) return; const p = trigger.intData; if(p.length >= 1) setSourceType(p[0]); if(p.length >= 2) setFilterExisting(p[1] === 1); if(p.length >= 3) setInvert(p[2] === 1); if(p.length >= 5) setTargetTile({ x: p[3], y: p[4] }); else setTargetTile({ x: 0, y: 0 }); if(p.length >= 6) { const n = p[5]; const tiles: Tile[] = []; for(let i = 0; i < n; i++) { const xi = 6 + i * 2; if(xi + 1 < p.length) tiles.push({ x: p[xi], y: p[xi + 1] }); } setSelectedTiles(tiles); } else { setSelectedTiles([]); } }, [ trigger ]); const save = useCallback(() => { const params: number[] = [ sourceType, filterExisting ? 1 : 0, invert ? 1 : 0, targetTile.x, targetTile.y, selectedTiles.length, ...selectedTiles.flatMap(t => [ t.x, t.y ]), ]; setIntParams(params); }, [ sourceType, filterExisting, invert, selectedTiles, targetTile.x, targetTile.y, setIntParams ]); const setTileSelection = useCallback((x: number, y: number, selected: boolean) => { setSelectedTiles(prev => { const alreadySelected = tileIncluded(prev, x, y); if(selected) { if(alreadySelected) return prev; return [ ...prev, { x, y } ]; } if(!alreadySelected) return prev; return prev.filter(t => !(t.x === x && t.y === y)); }); }, []); const moveTargetTile = useCallback((x: number, y: number) => { setTargetTile({ x, y }); }, []); const addTile = useCallback(() => { if(!tileIncluded(selectedTiles, curX, curY)) setSelectedTiles(prev => [ ...prev, { x: curX, y: curY } ]); }, [ curX, curY, selectedTiles ]); const removeTile = useCallback(() => { setSelectedTiles(prev => prev.filter(t => !(t.x === curX && t.y === curY))); }, [ curX, curY ]); const clearTiles = useCallback(() => setSelectedTiles([]), []); const loadDefaultPattern = useCallback(() => { const tiles: Tile[] = []; for(let y = -2; y <= 2; y++) { for(let x = -2; x <= 2; x++) { if(x === 0 && y === 0) continue; tiles.push({ x, y }); } } setSelectedTiles(tiles); }, []); const isUserGroup = sourceType <= SOURCE_USER_CLICKED; const activeSources = isUserGroup ? USER_SOURCES : FURNI_SOURCES; const groupOffset = isUserGroup ? 0 : SOURCE_FURNI_TRIGGER; const groupIndex = sourceType - groupOffset; const prevSource = () => setSourceType(groupOffset + ((groupIndex - 1 + activeSources.length) % activeSources.length)); const nextSource = () => setSourceType(groupOffset + ((groupIndex + 1) % activeSources.length)); const switchGroup = (toUser: boolean) => { if(toUser === isUserGroup) return; const newOffset = toUser ? 0 : SOURCE_FURNI_TRIGGER; setSourceType(newOffset + groupIndex); }; const requiresFurni = sourceType === SOURCE_FURNI_PICKED ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID : WiredFurniType.STUFF_SELECTION_OPTION_NONE; const pickedCount = furniIds.length; const pickedLimit = trigger?.maximumItemSelectionCount ?? 20; return (
{ LocalizeText('wiredfurni.params.neighborhood_selection') }
X: setCurX(parseInt(e.target.value) || 0) } /> Y: setCurY(parseInt(e.target.value) || 0) } />

{ LocalizeText('wiredfurni.params.selector_options_selector') }
{ LocalizeText('wiredfurni.params.sources.merged.title.neighborhood') }
{ LocalizeText(activeSources[groupIndex].label) }
{ sourceType === SOURCE_FURNI_PICKED && { LocalizeText('wiredfurni.pickfurnis.caption', [ 'count', 'limit' ], [ pickedCount.toString(), pickedLimit.toString() ]) } }
); };