diff --git a/src/api/wired/WiredActionLayoutCode.ts b/src/api/wired/WiredActionLayoutCode.ts index 9e3cee2..c9d4324 100644 --- a/src/api/wired/WiredActionLayoutCode.ts +++ b/src/api/wired/WiredActionLayoutCode.ts @@ -32,6 +32,9 @@ export class WiredActionLayoutCode public static USERS_AREA_SELECTOR: number = 31; public static USERS_NEIGHBORHOOD_SELECTOR: number = 32; public static SEND_SIGNAL: number = 33; - public static SET_ALTITUDE: number = 39; - public static RELATIVE_MOVE: number = 40; + public static FREEZE: number = 34; + public static UNFREEZE: number = 35; + public static FURNI_TO_USER: number = 36; + public static USER_TO_FURNI: number = 37; + public static FURNI_TO_FURNI: number = 38; } diff --git a/src/api/wired/WiredSelectionVisualizer.ts b/src/api/wired/WiredSelectionVisualizer.ts index 18edbf7..06aeb57 100644 --- a/src/api/wired/WiredSelectionVisualizer.ts +++ b/src/api/wired/WiredSelectionVisualizer.ts @@ -3,25 +3,42 @@ import { GetRoomEngine, IRoomObject, IRoomObjectSpriteVisualization, RoomObjectC export class WiredSelectionVisualizer { private static _selectionShader: WiredFilter = new WiredFilter({ - lineColor: [ 1, 1, 1 ], - color: [ 0.6, 0.6, 0.6 ] + lineColor: [ 0.45, 0.95, 0.55 ], + color: [ 0.18, 0.78, 0.30 ] + }); + private static _secondarySelectionShader: WiredFilter = new WiredFilter({ + lineColor: [ 0.45, 0.78, 1 ], + color: [ 0.20, 0.52, 0.95 ] }); public static show(furniId: number): void { - WiredSelectionVisualizer.applySelectionShader(WiredSelectionVisualizer.getRoomObject(furniId)); + WiredSelectionVisualizer.applySelectionShader(WiredSelectionVisualizer.getRoomObject(furniId), WiredSelectionVisualizer._selectionShader); } public static hide(furniId: number): void { - WiredSelectionVisualizer.clearSelectionShader(WiredSelectionVisualizer.getRoomObject(furniId)); + const roomObject = WiredSelectionVisualizer.getRoomObject(furniId); + + WiredSelectionVisualizer.clearSelectionShader(roomObject, WiredSelectionVisualizer._selectionShader); + WiredSelectionVisualizer.clearSelectionShader(roomObject, WiredSelectionVisualizer._secondarySelectionShader); + } + + public static showSecondary(furniId: number): void + { + WiredSelectionVisualizer.applySelectionShader(WiredSelectionVisualizer.getRoomObject(furniId), WiredSelectionVisualizer._secondarySelectionShader); + } + + public static hideSecondary(furniId: number): void + { + WiredSelectionVisualizer.clearSelectionShader(WiredSelectionVisualizer.getRoomObject(furniId), WiredSelectionVisualizer._secondarySelectionShader); } public static clearSelectionShaderFromFurni(furniIds: number[]): void { for(const furniId of furniIds) { - WiredSelectionVisualizer.clearSelectionShader(WiredSelectionVisualizer.getRoomObject(furniId)); + WiredSelectionVisualizer.clearSelectionShader(WiredSelectionVisualizer.getRoomObject(furniId), WiredSelectionVisualizer._selectionShader); } } @@ -29,7 +46,39 @@ export class WiredSelectionVisualizer { for(const furniId of furniIds) { - WiredSelectionVisualizer.applySelectionShader(WiredSelectionVisualizer.getRoomObject(furniId)); + WiredSelectionVisualizer.applySelectionShader(WiredSelectionVisualizer.getRoomObject(furniId), WiredSelectionVisualizer._selectionShader); + } + } + + public static clearSecondarySelectionShaderFromFurni(furniIds: number[]): void + { + for(const furniId of furniIds) + { + WiredSelectionVisualizer.clearSelectionShader(WiredSelectionVisualizer.getRoomObject(furniId), WiredSelectionVisualizer._secondarySelectionShader); + } + } + + public static applySecondarySelectionShaderToFurni(furniIds: number[]): void + { + for(const furniId of furniIds) + { + WiredSelectionVisualizer.applySelectionShader(WiredSelectionVisualizer.getRoomObject(furniId), WiredSelectionVisualizer._secondarySelectionShader); + } + } + + public static clearAllSelectionShaders(): void + { + const roomEngine = GetRoomEngine(); + const roomId = roomEngine.activeRoomId; + + if(roomId < 0) return; + + const roomObjects = roomEngine.getRoomObjects(roomId, RoomObjectCategory.FLOOR); + + for(const roomObject of roomObjects) + { + WiredSelectionVisualizer.clearSelectionShader(roomObject, WiredSelectionVisualizer._selectionShader); + WiredSelectionVisualizer.clearSelectionShader(roomObject, WiredSelectionVisualizer._secondarySelectionShader); } } @@ -40,7 +89,7 @@ export class WiredSelectionVisualizer return roomEngine.getRoomObject(roomEngine.activeRoomId, objectId, RoomObjectCategory.FLOOR); } - private static applySelectionShader(roomObject: IRoomObject): void + private static applySelectionShader(roomObject: IRoomObject, filter: WiredFilter): void { if(!roomObject) return; @@ -54,13 +103,15 @@ export class WiredSelectionVisualizer if(!sprite.filters) sprite.filters = []; - sprite.filters.push(WiredSelectionVisualizer._selectionShader); + if(sprite.filters.includes(filter)) continue; + + sprite.filters.push(filter); sprite.increaseUpdateCounter(); } } - private static clearSelectionShader(roomObject: IRoomObject): void + private static clearSelectionShader(roomObject: IRoomObject, filter: WiredFilter): void { if(!roomObject) return; @@ -72,7 +123,7 @@ export class WiredSelectionVisualizer { if(!sprite.filters) continue; - const index = sprite.filters.indexOf(WiredSelectionVisualizer._selectionShader); + const index = sprite.filters.indexOf(filter); if(index >= 0) { diff --git a/src/components/wired/views/WiredBaseView.tsx b/src/components/wired/views/WiredBaseView.tsx index 4c4f0db..f9477af 100644 --- a/src/components/wired/views/WiredBaseView.tsx +++ b/src/components/wired/views/WiredBaseView.tsx @@ -24,7 +24,11 @@ export const WiredBaseView: FC> = props => const [ needsSave, setNeedsSave ] = useState(false); const { trigger = null, setTrigger = null, setIntParams = null, setStringParam = null, setFurniIds = null, setAllowsFurni = null, saveWired = null } = useWired(); - const onClose = () => setTrigger(null); + const onClose = () => + { + WiredSelectionVisualizer.clearAllSelectionShaders(); + setTrigger(null); + }; const onSave = () => { @@ -48,6 +52,8 @@ export const WiredBaseView: FC> = props => { if(!trigger) return; + WiredSelectionVisualizer.clearAllSelectionShaders(); + const spriteId = (trigger.spriteId || -1); const furniData = GetSessionDataManager().getFloorItemData(spriteId); diff --git a/src/components/wired/views/WiredSourcesSelector.tsx b/src/components/wired/views/WiredSourcesSelector.tsx index db072a6..2d5ebc1 100644 --- a/src/components/wired/views/WiredSourcesSelector.tsx +++ b/src/components/wired/views/WiredSourcesSelector.tsx @@ -16,45 +16,66 @@ export const USER_SOURCES = [ { value: 201, label: 'wiredfurni.params.sources.users.201' } ]; +export interface WiredSourceOption +{ + value: number; + label: string; +} + interface WiredSourcesSelectorProps { showFurni?: boolean; showUsers?: boolean; furniSource?: number; userSource?: number; + furniTitle?: string; + usersTitle?: string; + furniSources?: WiredSourceOption[]; + userSources?: WiredSourceOption[]; onChangeFurni?: (source: number) => void; onChangeUsers?: (source: number) => void; } export const WiredSourcesSelector: FC = props => { - const { showFurni = false, showUsers = false, furniSource = 0, userSource = 0, onChangeFurni = null, onChangeUsers = null } = props; + const { + showFurni = false, + showUsers = false, + furniSource = 0, + userSource = 0, + furniTitle = 'wiredfurni.params.sources.furni.title', + usersTitle = 'wiredfurni.params.sources.users.title', + furniSources = FURNI_SOURCES, + userSources = USER_SOURCES, + onChangeFurni = null, + onChangeUsers = null + } = props; - const furniIndex = Math.max(0, FURNI_SOURCES.findIndex(s => s.value === furniSource)); - const userIndex = Math.max(0, USER_SOURCES.findIndex(s => s.value === userSource)); + const furniIndex = Math.max(0, furniSources.findIndex(s => s.value === furniSource)); + const userIndex = Math.max(0, userSources.findIndex(s => s.value === userSource)); const prevFurni = () => { - const next = (furniIndex - 1 + FURNI_SOURCES.length) % FURNI_SOURCES.length; - onChangeFurni && onChangeFurni(FURNI_SOURCES[next].value); + const next = (furniIndex - 1 + furniSources.length) % furniSources.length; + onChangeFurni && onChangeFurni(furniSources[next].value); }; const nextFurni = () => { - const next = (furniIndex + 1) % FURNI_SOURCES.length; - onChangeFurni && onChangeFurni(FURNI_SOURCES[next].value); + const next = (furniIndex + 1) % furniSources.length; + onChangeFurni && onChangeFurni(furniSources[next].value); }; const prevUsers = () => { - const next = (userIndex - 1 + USER_SOURCES.length) % USER_SOURCES.length; - onChangeUsers && onChangeUsers(USER_SOURCES[next].value); + const next = (userIndex - 1 + userSources.length) % userSources.length; + onChangeUsers && onChangeUsers(userSources[next].value); }; const nextUsers = () => { - const next = (userIndex + 1) % USER_SOURCES.length; - onChangeUsers && onChangeUsers(USER_SOURCES[next].value); + const next = (userIndex + 1) % userSources.length; + onChangeUsers && onChangeUsers(userSources[next].value); }; if(!showFurni && !showUsers) return null; @@ -63,11 +84,11 @@ export const WiredSourcesSelector: FC = props =>
{ showFurni && <> - { LocalizeText('wiredfurni.params.sources.furni.title') } + { LocalizeText(furniTitle) }
- { LocalizeText(FURNI_SOURCES[furniIndex].label) } + { LocalizeText(furniSources[furniIndex].label) }
@@ -77,11 +98,11 @@ export const WiredSourcesSelector: FC = props => { showUsers && <> - { LocalizeText('wiredfurni.params.sources.users.title') } + { LocalizeText(usersTitle) }
- { LocalizeText(USER_SOURCES[userIndex].label) } + { LocalizeText(userSources[userIndex].label) }
@@ -89,4 +110,3 @@ export const WiredSourcesSelector: FC = props =>
); }; - diff --git a/src/components/wired/views/actions/WiredActionFreezeView.tsx b/src/components/wired/views/actions/WiredActionFreezeView.tsx new file mode 100644 index 0000000..716acaa --- /dev/null +++ b/src/components/wired/views/actions/WiredActionFreezeView.tsx @@ -0,0 +1,54 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredActionBaseView } from './WiredActionBaseView'; +import { WiredSourcesSelector } from '../WiredSourcesSelector'; + +const EFFECT_OPTIONS = [ + { value: 218, label: 'fx_218' }, + { value: 12, label: 'fx_12' }, + { value: 11, label: 'fx_11' }, + { value: 53, label: 'fx_53' }, + { value: 163, label: 'fx_163' } +]; + +export const WiredActionFreezeView: FC<{}> = () => +{ + const [ effectId, setEffectId ] = useState(218); + const [ cancelOnTeleport, setCancelOnTeleport ] = useState(false); + const [ userSource, setUserSource ] = useState(0); + const { trigger = null, setIntParams = null } = useWired(); + + const save = () => setIntParams([ + effectId, + cancelOnTeleport ? 1 : 0, + userSource + ]); + + useEffect(() => + { + setEffectId((trigger?.intData?.length > 0) ? trigger.intData[0] : 218); + setCancelOnTeleport((trigger?.intData?.length > 1) ? (trigger.intData[1] === 1) : false); + setUserSource((trigger?.intData?.length > 2) ? trigger.intData[2] : 0); + }, [ trigger ]); + + return ( + }> +
+ Effect + +
+
+ setCancelOnTeleport(event.target.checked) } /> + { LocalizeText('wiredfurni.params.freeze.cancel_on_teleport') } +
+
+ ); +}; diff --git a/src/components/wired/views/actions/WiredActionFurniToFurniView.tsx b/src/components/wired/views/actions/WiredActionFurniToFurniView.tsx new file mode 100644 index 0000000..5f7cefc --- /dev/null +++ b/src/components/wired/views/actions/WiredActionFurniToFurniView.tsx @@ -0,0 +1,222 @@ +import { FC, useCallback, useEffect, useRef, useState } from 'react'; +import { LocalizeText, WiredFurniType, WiredSelectionVisualizer } from '../../../../api'; +import { Button, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredSourcesSelector, FURNI_SOURCES, WiredSourceOption } from '../WiredSourcesSelector'; +import { WiredActionBaseView } from './WiredActionBaseView'; + +const SOURCE_TRIGGER = 0; +const SOURCE_SELECTED = 100; +const SOURCE_SECONDARY_SELECTED = 101; +const FURNI_DELIMITER = ';'; + +const TARGET_FURNI_SOURCES: WiredSourceOption[] = [ + { value: 0, label: 'wiredfurni.params.sources.furni.0' }, + { value: SOURCE_SECONDARY_SELECTED, label: 'wiredfurni.params.sources.furni.101' }, + { value: 200, label: 'wiredfurni.params.sources.furni.200' }, + { value: 201, label: 'wiredfurni.params.sources.furni.201' } +]; + +type SelectionMode = 'move' | 'target'; + +const parseIds = (data: string): number[] => +{ + if(!data || !data.length) return []; + + const ids = new Set(); + + for(const part of data.split(/[;,\t]/)) + { + const trimmed = part.trim(); + if(!trimmed.length) continue; + + const value = parseInt(trimmed, 10); + if(!isNaN(value) && value > 0) ids.add(value); + } + + return Array.from(ids); +}; + +const serializeIds = (ids: number[]): string => +{ + if(!ids || !ids.length) return ''; + + return ids.filter(id => (id > 0)).join(FURNI_DELIMITER); +}; + +export const WiredActionFurniToFurniView: FC<{}> = () => +{ + const [ moveSource, setMoveSource ] = useState(SOURCE_TRIGGER); + const [ targetSource, setTargetSource ] = useState(SOURCE_TRIGGER); + const [ moveFurniIds, setMoveFurniIds ] = useState([]); + const [ targetFurniIds, setTargetFurniIds ] = useState([]); + const [ selectionMode, setSelectionMode ] = useState('move'); + + const highlightedIds = useRef([]); + + const { trigger = null, furniIds = [], setFurniIds, setIntParams, setStringParam, setAllowsFurni } = useWired(); + + const syncHighlights = useCallback((nextMoveIds: number[], nextTargetIds: number[]) => + { + if(highlightedIds.current.length) + { + WiredSelectionVisualizer.clearSelectionShaderFromFurni(highlightedIds.current); + WiredSelectionVisualizer.clearSecondarySelectionShaderFromFurni(highlightedIds.current); + } + + const targetSet = new Set(nextTargetIds); + const moveOnlyIds = nextMoveIds.filter(id => !targetSet.has(id)); + + if(moveOnlyIds.length) WiredSelectionVisualizer.applySelectionShaderToFurni(moveOnlyIds); + if(nextTargetIds.length) WiredSelectionVisualizer.applySecondarySelectionShaderToFurni(nextTargetIds); + + highlightedIds.current = Array.from(new Set([ ...nextMoveIds, ...nextTargetIds ])); + }, []); + + const switchSelection = useCallback((mode: SelectionMode) => + { + const canEditMove = (moveSource === SOURCE_SELECTED); + const canEditTarget = (targetSource === SOURCE_SECONDARY_SELECTED); + + if(mode === 'move' && !canEditMove) return; + if(mode === 'target' && !canEditTarget) return; + + setSelectionMode(mode); + setFurniIds([ ...(mode === 'move' ? moveFurniIds : targetFurniIds) ]); + }, [ moveSource, targetSource, moveFurniIds, targetFurniIds, setFurniIds ]); + + useEffect(() => + { + if(!trigger) return; + + const nextMoveIds = trigger.selectedItems ?? []; + const nextTargetIds = parseIds(trigger.stringData); + const nextMoveSource = (trigger.intData.length >= 1) + ? trigger.intData[0] + : (nextMoveIds.length ? SOURCE_SELECTED : SOURCE_TRIGGER); + const nextTargetSourceRaw = (trigger.intData.length >= 2) + ? trigger.intData[1] + : (nextTargetIds.length ? SOURCE_SECONDARY_SELECTED : SOURCE_TRIGGER); + const nextTargetSource = (nextTargetSourceRaw === SOURCE_SELECTED) ? SOURCE_SECONDARY_SELECTED : nextTargetSourceRaw; + + setMoveSource(nextMoveSource); + setTargetSource(nextTargetSource); + setMoveFurniIds(nextMoveIds); + setTargetFurniIds(nextTargetIds); + setSelectionMode('move'); + setFurniIds([ ...nextMoveIds ]); + }, [ trigger, setFurniIds ]); + + useEffect(() => + { + if(selectionMode === 'move') setMoveFurniIds(furniIds); + else setTargetFurniIds(furniIds); + }, [ furniIds, selectionMode ]); + + useEffect(() => + { + syncHighlights(moveFurniIds, targetFurniIds); + }, [ moveFurniIds, targetFurniIds, syncHighlights ]); + + useEffect(() => + { + const canEditMove = (moveSource === SOURCE_SELECTED); + const canEditTarget = (targetSource === SOURCE_SECONDARY_SELECTED); + + if(selectionMode === 'move' && !canEditMove && canEditTarget) + { + switchSelection('target'); + return; + } + + if(selectionMode === 'target' && !canEditTarget && canEditMove) + { + switchSelection('move'); + return; + } + + const canEditCurrent = ((selectionMode === 'move') ? canEditMove : canEditTarget); + setAllowsFurni(canEditCurrent ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID : WiredFurniType.STUFF_SELECTION_OPTION_NONE); + }, [ selectionMode, moveSource, targetSource, switchSelection, setAllowsFurni ]); + + useEffect(() => + { + return () => + { + if(!highlightedIds.current.length) return; + + WiredSelectionVisualizer.clearSelectionShaderFromFurni(highlightedIds.current); + WiredSelectionVisualizer.clearSecondarySelectionShaderFromFurni(highlightedIds.current); + highlightedIds.current = []; + }; + }, []); + + const save = useCallback(() => + { + if(selectionMode === 'target') + { + setSelectionMode('move'); + setFurniIds([ ...moveFurniIds ]); + } + + setIntParams([ + moveSource, + targetSource + ]); + + setStringParam(serializeIds(targetFurniIds)); + }, [ selectionMode, moveFurniIds, moveSource, targetSource, targetFurniIds, setFurniIds, setIntParams, setStringParam ]); + + const selectionLimit = trigger?.maximumItemSelectionCount ?? 0; + + return ( + + +
+ setTargetSource((value === SOURCE_SELECTED) ? SOURCE_SECONDARY_SELECTED : value) } /> + + }> +
+
+ { LocalizeText('wiredfurni.params.sources.furni.title.mv.0') } +
+ + { selectionLimit ? `${ moveFurniIds.length }/${ selectionLimit }` : moveFurniIds.length } +
+
+
+ { LocalizeText('wiredfurni.params.sources.furni.title.mv.1') } +
+ + { selectionLimit ? `${ targetFurniIds.length }/${ selectionLimit }` : targetFurniIds.length } +
+
+
+
+ ); +}; diff --git a/src/components/wired/views/actions/WiredActionLayoutView.tsx b/src/components/wired/views/actions/WiredActionLayoutView.tsx index e4d14db..551da1a 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 { WiredActionSetAltitudeView } from './WiredActionSetAltitudeView'; +import { WiredActionFreezeView } from './WiredActionFreezeView'; +import { WiredActionFurniToFurniView } from './WiredActionFurniToFurniView'; import { WiredActionSendSignalView } from './WiredActionSendSignalView'; import { WiredActionFurniAreaView } from '../selectors/WiredActionFurniAreaView'; import { WiredSelectorFurniNeighborhoodView } from '../selectors/WiredSelectorFurniNeighborhoodView'; @@ -32,6 +33,7 @@ import { WiredActionResetView } from './WiredActionResetView'; import { WiredActionSetFurniStateToView } from './WiredActionSetFurniStateToView'; import { WiredActionTeleportView } from './WiredActionTeleportView'; import { WiredActionToggleFurniStateView } from './WiredActionToggleFurniStateView'; +import { WiredActionUnfreezeView } from './WiredActionUnfreezeView'; export const WiredActionLayoutView = (code: number) => { @@ -59,8 +61,12 @@ export const WiredActionLayoutView = (code: number) => return ; case WiredActionLayoutCode.FLEE: return ; - case WiredActionLayoutCode.SET_ALTITUDE: - return ; + case WiredActionLayoutCode.FREEZE: + return ; + case WiredActionLayoutCode.FURNI_TO_USER: + return ; + case WiredActionLayoutCode.FURNI_TO_FURNI: + return ; case WiredActionLayoutCode.GIVE_REWARD: return ; case WiredActionLayoutCode.GIVE_SCORE: @@ -91,6 +97,10 @@ export const WiredActionLayoutView = (code: number) => return ; case WiredActionLayoutCode.TOGGLE_FURNI_STATE: return ; + case WiredActionLayoutCode.UNFREEZE: + return ; + case WiredActionLayoutCode.USER_TO_FURNI: + return ; case WiredActionLayoutCode.FURNI_AREA_SELECTOR: return ; case WiredActionLayoutCode.FURNI_NEIGHBORHOOD_SELECTOR: diff --git a/src/components/wired/views/actions/WiredActionUnfreezeView.tsx b/src/components/wired/views/actions/WiredActionUnfreezeView.tsx new file mode 100644 index 0000000..98b31a6 --- /dev/null +++ b/src/components/wired/views/actions/WiredActionUnfreezeView.tsx @@ -0,0 +1,26 @@ +import { FC, useEffect, useState } from 'react'; +import { WiredFurniType } from '../../../../api'; +import { useWired } from '../../../../hooks'; +import { WiredActionBaseView } from './WiredActionBaseView'; +import { WiredSourcesSelector } from '../WiredSourcesSelector'; + +export const WiredActionUnfreezeView: FC<{}> = () => +{ + const [ userSource, setUserSource ] = useState(0); + const { trigger = null, setIntParams = null } = useWired(); + + const save = () => setIntParams([ userSource ]); + + useEffect(() => + { + setUserSource((trigger?.intData?.length > 0) ? trigger.intData[0] : 0); + }, [ trigger ]); + + return ( + } /> + ); +}; diff --git a/src/hooks/wired/useWired.ts b/src/hooks/wired/useWired.ts index b0c3f32..19e6ed8 100644 --- a/src/hooks/wired/useWired.ts +++ b/src/hooks/wired/useWired.ts @@ -235,6 +235,7 @@ const useWiredState = () => { const parser = event.getParser(); + WiredSelectionVisualizer.clearAllSelectionShaders(); setTrigger(null); }); @@ -275,6 +276,7 @@ const useWiredState = () => return () => { + WiredSelectionVisualizer.clearAllSelectionShaders(); setIntParams([]); setStringParam(''); setActionDelay(0);