From f7737c0d6bf961059a79b31f42e0d1092013432a Mon Sep 17 00:00:00 2001 From: duckietm Date: Wed, 4 Mar 2026 17:11:30 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=86=95=20=20Wired=20wf=5Fslc=5Ffurni=5Fne?= =?UTF-8?q?ighborhood?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/wired/WiredActionLayoutCode.ts | 1 + .../views/actions/WiredActionLayoutView.tsx | 3 + .../WiredSelectorFurniNeighborhoodView.tsx | 340 ++++++++++++++++++ 3 files changed, 344 insertions(+) create mode 100644 src/components/wired/views/selectors/WiredSelectorFurniNeighborhoodView.tsx diff --git a/src/api/wired/WiredActionLayoutCode.ts b/src/api/wired/WiredActionLayoutCode.ts index cf01487..2095e49 100644 --- a/src/api/wired/WiredActionLayoutCode.ts +++ b/src/api/wired/WiredActionLayoutCode.ts @@ -27,4 +27,5 @@ export class WiredActionLayoutCode public static BOT_CHANGE_FIGURE: number = 26; public static BOT_TALK_DIRECT_TO_AVTR: number = 27; public static FURNI_AREA_SELECTOR: number = 28; + public static FURNI_NEIGHBORHOOD_SELECTOR: number = 29; } diff --git a/src/components/wired/views/actions/WiredActionLayoutView.tsx b/src/components/wired/views/actions/WiredActionLayoutView.tsx index 0b83483..a9bd67f 100644 --- a/src/components/wired/views/actions/WiredActionLayoutView.tsx +++ b/src/components/wired/views/actions/WiredActionLayoutView.tsx @@ -1,6 +1,7 @@ import { WiredActionLayoutCode } from '../../../../api'; import { WiredActionBotChangeFigureView } from './WiredActionBotChangeFigureView'; import { WiredActionFurniAreaView } from '../selectors/WiredActionFurniAreaView'; +import { WiredSelectorFurniNeighborhoodView } from '../selectors/WiredSelectorFurniNeighborhoodView'; import { WiredActionBotFollowAvatarView } from './WiredActionBotFollowAvatarView'; import { WiredActionBotGiveHandItemView } from './WiredActionBotGiveHandItemView'; import { WiredActionBotMoveView } from './WiredActionBotMoveView'; @@ -82,6 +83,8 @@ export const WiredActionLayoutView = (code: number) => return ; case WiredActionLayoutCode.FURNI_AREA_SELECTOR: return ; + case WiredActionLayoutCode.FURNI_NEIGHBORHOOD_SELECTOR: + return ; } return null; diff --git a/src/components/wired/views/selectors/WiredSelectorFurniNeighborhoodView.tsx b/src/components/wired/views/selectors/WiredSelectorFurniNeighborhoodView.tsx new file mode 100644 index 0000000..0185f09 --- /dev/null +++ b/src/components/wired/views/selectors/WiredSelectorFurniNeighborhoodView.tsx @@ -0,0 +1,340 @@ +import { CSSProperties, FC, 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 { WiredActionBaseView } from '../actions/WiredActionBaseView'; + +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 = [ + { 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' }, +]; + +const FURNI_SOURCES = [ + { 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' }, +]; + +const TILE_W = 22; +const TILE_H = 11; +const GRID_RANGE = 4; // -4 … +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[]; + onToggle: (x: number, y: number) => void; +} + +const NeighborhoodGrid: FC = ({ selectedTiles, onToggle }) => +{ + const tiles: JSX.Element[] = []; + + for (let ry = -GRID_RANGE; ry <= GRID_RANGE; ry++) + { + for (let rx = -GRID_RANGE; rx <= GRID_RANGE; rx++) + { + const isCenter = rx === 0 && ry === 0; + const isSelected = tileIncluded(selectedTiles, rx, ry); + const left = tileLeft(rx, ry); + const top_ = tileTop(rx, ry); + const zIdx = rx + ry + GRID_RANGE * 2 + 10; + + const bgColor = isCenter + ? '#ff9500' + : isSelected + ? '#3399ff' + : '#2a3042'; + + const borderColor = isCenter + ? '#cc6600' + : isSelected + ? '#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: isCenter ? 'default' : 'pointer', + zIndex: zIdx, + }; + + const border: CSSProperties = { + position: 'absolute', + width: TILE_W + 2, + height: TILE_H + 2, + left: left - 1, + top: top_ - 1, + clipPath: 'polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)', + backgroundColor: borderColor, + zIndex: zIdx - 1, + pointerEvents: 'none', + }; + + tiles.push( +
, +
!isCenter && onToggle(rx, ry) } + />, + ); + } + } + + return ( +
+ { tiles } +
+ ); +}; + +export const WiredSelectorFurniNeighborhoodView: FC<{}> = () => +{ + const [ selectedTiles, setSelectedTiles ] = useState([]); + const [ filterExisting, setFilterExisting ] = useState(false); + const [ invert, setInvert ] = useState(false); + const [ sourceType, setSourceType ] = useState(SOURCE_USER_TRIGGER); + const [ curX, setCurX ] = useState(0); + const [ curY, setCurY ] = useState(0); + + const { trigger = null, furniIds = [], setIntParams } = useWired(); + + 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 >= 4) + { + const n = p[3]; + const tiles: Tile[] = []; + + for(let i = 0; i < n; i++) + { + const xi = 4 + 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, + selectedTiles.length, + ...selectedTiles.flatMap(t => [ t.x, t.y ]), + ]; + + setIntParams(params); + }, [ sourceType, filterExisting, invert, selectedTiles, setIntParams ]); + + const toggleTile = useCallback((x: number, y: number) => + { + setSelectedTiles(prev => + tileIncluded(prev, x, y) + ? prev.filter(t => !(t.x === x && t.y === y)) + : [ ...prev, { x, y } ] + ); + }, []); + + const addTile = useCallback(() => + { + if(curX === 0 && curY === 0) return; + 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 prevInGroup = () => + setSourceType(groupOffset + (groupIndex - 1 + activeSources.length) % activeSources.length); + + const nextInGroup = () => + 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() ]) } + } + +
+
+ ); +};