From a11987e1e00e8ba53ad9df31fb63a84d87c832f0 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 15 Mar 2026 00:25:57 +0100 Subject: [PATCH] Add FurniEditor tool with Next.js API integration - FurniEditor component with Search/Edit tabs (NitroCard UI) - useFurniEditor hook connecting to Next.js API routes via Vite proxy - Edit Furni button in room infostand (godMode) with sprite ID lookup - Toolbar: 3-column flex layout (icons | chat | friends) - Heroicons SVG for ID/Sprite display in infostand and edit view - Vite config: proxy /api to Next.js, aliases for renderer3 packages --- index.html | 2 +- src/components/MainView.tsx | 2 + .../furni-editor/FurniEditorView.tsx | 140 ++++++++++ .../views/FurniEditorCreateView.tsx | 159 +++++++++++ .../views/FurniEditorEditView.tsx | 249 ++++++++++++++++++ .../views/FurniEditorSearchView.tsx | 134 ++++++++++ .../infostand/InfoStandWidgetFurniView.tsx | 29 +- src/components/toolbar/ToolbarView.tsx | 60 ++--- src/hooks/furni-editor/index.ts | 1 + src/hooks/furni-editor/useFurniEditor.ts | 239 +++++++++++++++++ vite.config.mjs | 38 ++- 11 files changed, 1019 insertions(+), 34 deletions(-) create mode 100644 src/components/furni-editor/FurniEditorView.tsx create mode 100644 src/components/furni-editor/views/FurniEditorCreateView.tsx create mode 100644 src/components/furni-editor/views/FurniEditorEditView.tsx create mode 100644 src/components/furni-editor/views/FurniEditorSearchView.tsx create mode 100644 src/hooks/furni-editor/index.ts create mode 100644 src/hooks/furni-editor/useFurniEditor.ts diff --git a/index.html b/index.html index 09b372f..4e6a87e 100644 --- a/index.html +++ b/index.html @@ -16,7 +16,7 @@ - + diff --git a/src/components/MainView.tsx b/src/components/MainView.tsx index 3fef0cc..d9594bc 100644 --- a/src/components/MainView.tsx +++ b/src/components/MainView.tsx @@ -9,6 +9,7 @@ import { CampaignView } from './campaign/CampaignView'; import { CatalogView } from './catalog/CatalogView'; import { ChatHistoryView } from './chat-history/ChatHistoryView'; import { FloorplanEditorView } from './floorplan-editor/FloorplanEditorView'; +import { FurniEditorView } from './furni-editor/FurniEditorView'; import { FriendsView } from './friends/FriendsView'; import { GameCenterView } from './game-center/GameCenterView'; import { GroupsView } from './groups/GroupsView'; @@ -115,6 +116,7 @@ export const MainView: FC<{}> = props => + ); diff --git a/src/components/furni-editor/FurniEditorView.tsx b/src/components/furni-editor/FurniEditorView.tsx new file mode 100644 index 0000000..a013554 --- /dev/null +++ b/src/components/furni-editor/FurniEditorView.tsx @@ -0,0 +1,140 @@ +import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer'; +import { FC, useCallback, useEffect, useState } from 'react'; +import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common'; +import { useFurniEditor } from '../../hooks/furni-editor'; +import { FurniEditorEditView } from './views/FurniEditorEditView'; +import { FurniEditorSearchView } from './views/FurniEditorSearchView'; + +const TAB_SEARCH = 0; +const TAB_EDIT = 1; + +export const FurniEditorView: FC<{}> = () => +{ + const [ isVisible, setIsVisible ] = useState(false); + const [ activeTab, setActiveTab ] = useState(TAB_SEARCH); + + const { + items, total, page, loading, error, clearError, + selectedItem, catalogItems, furniDataEntry, + interactions, + searchItems, loadDetail, loadBySpriteId, updateItem, deleteItem, loadInteractions + } = useFurniEditor(); + + useEffect(() => + { + const linkTracker: ILinkEventTracker = { + linkReceived: (url: string) => + { + const parts = url.split('/'); + + if(parts.length < 2) return; + + switch(parts[1]) + { + case 'show': + setIsVisible(true); + return; + case 'hide': + setIsVisible(false); + return; + case 'toggle': + setIsVisible(prev => !prev); + return; + } + }, + eventUrlPrefix: 'furni-editor/' + }; + + AddLinkEventTracker(linkTracker); + + return () => RemoveLinkEventTracker(linkTracker); + }, []); + + useEffect(() => + { + if(isVisible) loadInteractions(); + }, [ isVisible ]); + + useEffect(() => + { + const handler = async (e: CustomEvent<{ spriteId: number }>) => + { + const { spriteId } = e.detail; + + const ok = await loadBySpriteId(spriteId); + + if(ok) setActiveTab(TAB_EDIT); + }; + + window.addEventListener('furni-editor:open', handler as EventListener); + + return () => window.removeEventListener('furni-editor:open', handler as EventListener); + }, [ loadBySpriteId ]); + + const handleSelect = useCallback(async (id: number) => + { + const ok = await loadDetail(id); + + if(ok) setActiveTab(TAB_EDIT); + }, [ loadDetail ]); + + const handleBack = useCallback(() => + { + setActiveTab(TAB_SEARCH); + }, []); + + const handleClose = useCallback(() => + { + setIsVisible(false); + }, []); + + if(!isVisible) return null; + + return ( + + + + setActiveTab(TAB_SEARCH) }> + Search + + selectedItem && setActiveTab(TAB_EDIT) }> + Edit + + + + { error && +
+ { error } + x +
+ } + + { activeTab === TAB_SEARCH && + + } + + { activeTab === TAB_EDIT && selectedItem && + + } + +
+
+ ); +}; diff --git a/src/components/furni-editor/views/FurniEditorCreateView.tsx b/src/components/furni-editor/views/FurniEditorCreateView.tsx new file mode 100644 index 0000000..f47530c --- /dev/null +++ b/src/components/furni-editor/views/FurniEditorCreateView.tsx @@ -0,0 +1,159 @@ +import { FC, useCallback, useState } from 'react'; +import { Button, Column, Flex, Text } from '../../../common'; + +interface FurniEditorCreateViewProps +{ + interactions: string[]; + loading: boolean; + onCreate: (fields: Record) => Promise; + onCreated: (id: number) => void; +} + +export const FurniEditorCreateView: FC = props => +{ + const { interactions, loading, onCreate, onCreated } = props; + const [ success, setSuccess ] = useState(null); + + const [ form, setForm ] = useState({ + itemName: '', + publicName: '', + spriteId: 0, + type: 's' as 's' | 'i', + width: 1, + length: 1, + stackHeight: 0, + allowStack: true, + allowSit: false, + allowLay: false, + allowWalk: false, + allowGift: true, + allowTrade: true, + allowRecycle: true, + allowMarketplaceSell: true, + allowInventoryStack: true, + interactionType: '', + interactionModesCount: 1, + customparams: '', + }); + + const setField = useCallback((key: string, value: unknown) => + { + setForm(prev => ({ ...prev, [key]: value })); + setSuccess(null); + }, []); + + const handleCreate = useCallback(async () => + { + if(!form.itemName || !form.publicName) return; + + const id = await onCreate(form); + + if(id) + { + setSuccess(id); + setTimeout(() => onCreated(id), 1000); + } + }, [ form, onCreate, onCreated ]); + + const inputClass = 'form-control form-control-sm'; + const labelClass = 'text-[11px] font-bold text-[#333] mb-0'; + + return ( + + { success && +
+ Item created with ID #{ success }! +
+ } + +
+ Basic Info +
+
+ + setField('itemName', e.target.value) } placeholder="my_custom_furni" /> +
+
+ + setField('publicName', e.target.value) } placeholder="My Custom Furni" /> +
+
+ + setField('spriteId', Number(e.target.value)) } /> +
+
+ + +
+
+
+ +
+ Dimensions +
+
+ + setField('width', Number(e.target.value)) } /> +
+
+ + setField('length', Number(e.target.value)) } /> +
+
+ + setField('stackHeight', Number(e.target.value)) } /> +
+
+
+ +
+ Permissions +
+ { [ 'allowStack', 'allowWalk', 'allowSit', 'allowLay', 'allowGift', 'allowTrade', 'allowRecycle', 'allowMarketplaceSell', 'allowInventoryStack' ].map(key => ( + + )) } +
+
+ +
+ Interaction +
+
+ + +
+
+ + setField('interactionModesCount', Number(e.target.value)) } /> +
+
+
+ + setField('customparams', e.target.value) } /> +
+
+ + + + +
+ ); +}; diff --git a/src/components/furni-editor/views/FurniEditorEditView.tsx b/src/components/furni-editor/views/FurniEditorEditView.tsx new file mode 100644 index 0000000..dc648ef --- /dev/null +++ b/src/components/furni-editor/views/FurniEditorEditView.tsx @@ -0,0 +1,249 @@ +import { FC, useCallback, useEffect, useState } from 'react'; +import { Button, Column, Flex, Text } from '../../../common'; +import { CatalogRef, FurniDetail } from '../../../hooks/furni-editor'; + +interface FurniEditorEditViewProps +{ + item: FurniDetail; + catalogItems: CatalogRef[]; + furniDataEntry: Record | null; + interactions: string[]; + loading: boolean; + onUpdate: (id: number, fields: Record) => Promise; + onDelete: (id: number) => Promise; + onBack: () => void; + onRefresh: (id: number) => void; +} + +export const FurniEditorEditView: FC = props => +{ + const { item, catalogItems, furniDataEntry, interactions, loading, onUpdate, onDelete, onBack, onRefresh } = props; + + const [ form, setForm ] = useState({ + itemName: '', + publicName: '', + spriteId: 0, + type: 's', + width: 1, + length: 1, + stackHeight: 0, + allowStack: true, + allowWalk: false, + allowSit: false, + allowLay: false, + allowGift: true, + allowTrade: true, + allowRecycle: true, + allowMarketplaceSell: true, + allowInventoryStack: true, + interactionType: '', + interactionModesCount: 0, + customparams: '', + }); + + const [ confirmDelete, setConfirmDelete ] = useState(false); + + useEffect(() => + { + if(!item) return; + + setForm({ + itemName: item.itemName || '', + publicName: item.publicName || '', + spriteId: item.spriteId || 0, + type: item.type || 's', + width: item.width || 1, + length: item.length || 1, + stackHeight: item.stackHeight || 0, + allowStack: !!item.allowStack, + allowWalk: !!item.allowWalk, + allowSit: !!item.allowSit, + allowLay: !!item.allowLay, + allowGift: !!item.allowGift, + allowTrade: !!item.allowTrade, + allowRecycle: !!item.allowRecycle, + allowMarketplaceSell: !!item.allowMarketplaceSell, + allowInventoryStack: !!item.allowInventoryStack, + interactionType: item.interactionType || '', + interactionModesCount: item.interactionModesCount || 0, + customparams: item.customparams || '', + }); + + setConfirmDelete(false); + }, [ item ]); + + const setField = useCallback((key: string, value: unknown) => + { + setForm(prev => ({ ...prev, [key]: value })); + }, []); + + const handleSave = useCallback(async () => + { + const ok = await onUpdate(item.id, form); + + if(ok) onRefresh(item.id); + }, [ item, form, onUpdate, onRefresh ]); + + const handleDelete = useCallback(async () => + { + if(!confirmDelete) return setConfirmDelete(true); + + const ok = await onDelete(item.id); + + if(ok) onBack(); + }, [ confirmDelete, item, onDelete, onBack ]); + + const inputClass = 'form-control form-control-sm'; + const labelClass = 'text-[11px] font-bold text-[#333] mb-0'; + + return ( + + + + + + + + { item.id } + | + + + + { item.spriteId } + + ({ item.usageCount } in use) + + + { /* Basic Info */ } +
+ Basic Info +
+
+ + setField('itemName', e.target.value) } /> +
+
+ + setField('publicName', e.target.value) } /> +
+
+ + setField('spriteId', Number(e.target.value)) } /> +
+
+ + +
+
+
+ + { /* Dimensions */ } +
+ Dimensions +
+
+ + setField('width', Number(e.target.value)) } /> +
+
+ + setField('length', Number(e.target.value)) } /> +
+
+ + setField('stackHeight', Number(e.target.value)) } /> +
+
+
+ + { /* Permissions */ } +
+ Permissions +
+ { [ 'allowStack', 'allowWalk', 'allowSit', 'allowLay', 'allowGift', 'allowTrade', 'allowRecycle', 'allowMarketplaceSell', 'allowInventoryStack' ].map(key => ( + + )) } +
+
+ + { /* Interaction */ } +
+ Interaction +
+
+ + +
+
+ + setField('interactionModesCount', Number(e.target.value)) } /> +
+
+
+ + setField('customparams', e.target.value) } /> +
+
+ + { /* Catalog References */ } + { catalogItems.length > 0 && +
+ Catalog ({ catalogItems.length }) +
+ { catalogItems.map(ci => ( +
+ { ci.catalogName } (page: { ci.pageName }) + { ci.costCredits }c + { ci.costPoints }p +
+ )) } +
+
+ } + + { /* FurniData.json Entry */ } + { furniDataEntry && +
+ FurniData.json +
+ { Object.entries(furniDataEntry).map(([ key, value ]) => ( +
+ { key } + { String(value ?? '') } +
+ )) } +
+
+ } + + { /* Actions */ } + + + + +
+ ); +}; diff --git a/src/components/furni-editor/views/FurniEditorSearchView.tsx b/src/components/furni-editor/views/FurniEditorSearchView.tsx new file mode 100644 index 0000000..7b3f8c6 --- /dev/null +++ b/src/components/furni-editor/views/FurniEditorSearchView.tsx @@ -0,0 +1,134 @@ +import { FC, useCallback, useEffect, useState } from 'react'; +import { Button, Column, Flex, Text } from '../../../common'; +import { FurniItem } from '../../../hooks/furni-editor'; + +interface FurniEditorSearchViewProps +{ + items: FurniItem[]; + total: number; + page: number; + loading: boolean; + onSearch: (query: string, type: string, page: number) => void; + onSelect: (id: number) => void; +} + +export const FurniEditorSearchView: FC = props => +{ + const { items, total, page, loading, onSearch, onSelect } = props; + const [ query, setQuery ] = useState(''); + const [ typeFilter, setTypeFilter ] = useState(''); + + useEffect(() => + { + onSearch('', '', 1); + }, []); + + const handleSearch = useCallback(() => + { + onSearch(query, typeFilter, 1); + }, [ query, typeFilter, onSearch ]); + + const handleKeyDown = useCallback((e: React.KeyboardEvent) => + { + if(e.key === 'Enter') handleSearch(); + }, [ handleSearch ]); + + const totalPages = Math.ceil(total / 20); + + return ( + + + + Search + setQuery(e.target.value) } + onKeyDown={ handleKeyDown } + /> + + + Type + + + + + + + + + + + + + + + + + + + { items.map(item => ( + onSelect(item.id) } + > + + + + + + + + )) } + { items.length === 0 && !loading && + + + + } + +
IDSpriteNamePublic NameTypeInteraction
{ item.id }{ item.spriteId }{ item.itemName }{ item.publicName } + + { item.type === 's' ? 'Floor' : 'Wall' } + + { item.interactionType || '-' }
No items found
+
+ + { totalPages > 1 && + + + { total } items - Page { page }/{ totalPages } + + + + + + + } +
+ ); +}; diff --git a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx index d58a720..468247c 100644 --- a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx +++ b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx @@ -552,7 +552,21 @@ export const InfoStandWidgetFurniView: FC = props { godMode && <>
- { canSeeFurniId && ID: { avatarInfo.id } } + { canSeeFurniId && +
+
+ + + + ID: { avatarInfo.id } +
+
+ + + + Sprite: { (() => { const ro = GetRoomEngine().getRoomObject(roomSession.roomId, avatarInfo.id, avatarInfo.isWallItem ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR); return ro?.model?.getValue(RoomObjectVariable.FURNITURE_TYPE_ID) ?? '?'; })() } +
+
} { (!avatarInfo.isWallItem && canMove) && <> + { dropdownOpen &&
{ /* Left panel: position + rotation */ } diff --git a/src/components/toolbar/ToolbarView.tsx b/src/components/toolbar/ToolbarView.tsx index 7d6743a..fbb5ae8 100644 --- a/src/components/toolbar/ToolbarView.tsx +++ b/src/components/toolbar/ToolbarView.tsx @@ -69,38 +69,38 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => )} - - - - - { - setMeExpanded(!isMeExpanded); - event.stopPropagation(); - } }> - - { (getTotalUnseen > 0) && - } - - { isInRoom && - VisitDesktop() } /> } - { !isInRoom && - CreateLinkEvent('navigator/goto/home') } /> } - CreateLinkEvent('navigator/toggle') } /> - { GetConfigurationValue('game.center.enabled') && - CreateLinkEvent('games/toggle') } /> } - CreateLinkEvent('catalog/toggle') } /> - CreateLinkEvent('inventory/toggle') }> - { (getFullCount > 0) && - } - - { isInRoom && - CreateLinkEvent('camera/toggle') } /> } - { isMod && - CreateLinkEvent('mod-tools/toggle') } /> } + + + + { + setMeExpanded(!isMeExpanded); + event.stopPropagation(); + } }> + + { (getTotalUnseen > 0) && + } - + { isInRoom && + VisitDesktop() } /> } + { !isInRoom && + CreateLinkEvent('navigator/goto/home') } /> } + CreateLinkEvent('navigator/toggle') } /> + { GetConfigurationValue('game.center.enabled') && + CreateLinkEvent('games/toggle') } /> } + CreateLinkEvent('catalog/toggle') } /> + CreateLinkEvent('inventory/toggle') }> + { (getFullCount > 0) && + } + + { isInRoom && + CreateLinkEvent('camera/toggle') } /> } + { isMod && + CreateLinkEvent('mod-tools/toggle') } /> } + { isMod && + CreateLinkEvent('furni-editor/toggle') } /> } - + + CreateLinkEvent('friends/toggle') }> { (requests.length > 0) && diff --git a/src/hooks/furni-editor/index.ts b/src/hooks/furni-editor/index.ts new file mode 100644 index 0000000..47ce6ef --- /dev/null +++ b/src/hooks/furni-editor/index.ts @@ -0,0 +1 @@ +export * from './useFurniEditor'; diff --git a/src/hooks/furni-editor/useFurniEditor.ts b/src/hooks/furni-editor/useFurniEditor.ts new file mode 100644 index 0000000..e4258e5 --- /dev/null +++ b/src/hooks/furni-editor/useFurniEditor.ts @@ -0,0 +1,239 @@ +import { useCallback, useState } from 'react'; + +export interface FurniItem +{ + id: number; + spriteId: number; + itemName: string; + publicName: string; + type: string; + width: number; + length: number; + stackHeight: number; + allowStack: boolean; + allowWalk: boolean; + allowSit: boolean; + allowLay: boolean; + interactionType: string; + interactionModesCount: number; +} + +export interface FurniDetail extends FurniItem +{ + allowGift: boolean; + allowTrade: boolean; + allowRecycle: boolean; + allowMarketplaceSell: boolean; + allowInventoryStack: boolean; + vendingIds: string; + customparams: string; + effectIdMale: number; + effectIdFemale: number; + clothingOnWalk: string; + multiheight: string; + description: string; + usageCount: number; +} + +export interface CatalogRef +{ + id: number; + catalogName: string; + costCredits: number; + costPoints: number; + pointsType: number; + pageId: number; + pageName: string; +} + +const API_BASE = '/api/admin/furni-editor'; + +async function apiFetch(url: string, options?: RequestInit): Promise +{ + const res = await fetch(url, { credentials: 'include', ...options }); + const data = await res.json(); + + if(!res.ok || data.error) throw new Error(data.error || 'API error'); + + return data; +} + +export const useFurniEditor = () => +{ + const [ items, setItems ] = useState([]); + const [ total, setTotal ] = useState(0); + const [ page, setPage ] = useState(1); + const [ loading, setLoading ] = useState(false); + const [ error, setError ] = useState(null); + const [ selectedItem, setSelectedItem ] = useState(null); + const [ catalogItems, setCatalogItems ] = useState([]); + const [ interactions, setInteractions ] = useState([]); + const [ furniDataEntry, setFurniDataEntry ] = useState | null>(null); + + const clearError = useCallback(() => setError(null), []); + + const searchItems = useCallback(async (query: string, type: string, pg: number) => + { + setLoading(true); + setError(null); + + try + { + const params = new URLSearchParams({ q: query, limit: '20', page: String(pg) }); + + if(type) params.set('type', type); + + const data = await apiFetch<{ items: FurniItem[]; total: number; page: number }>(`${ API_BASE }?${ params }`); + + setItems(data.items); + setTotal(data.total); + setPage(data.page); + } + catch(e: any) + { + setError(e.message); + } + finally + { + setLoading(false); + } + }, []); + + const loadDetail = useCallback(async (id: number): Promise => + { + setLoading(true); + setError(null); + + try + { + const data = await apiFetch<{ item: FurniDetail; catalogItems: CatalogRef[]; furniDataEntry: Record | null }>(`${ API_BASE }/detail?id=${ id }`); + + setSelectedItem(data.item); + setCatalogItems(data.catalogItems); + setFurniDataEntry(data.furniDataEntry); + + return true; + } + catch(e: any) + { + setError(e.message); + + return false; + } + finally + { + setLoading(false); + } + }, []); + + const updateItem = useCallback(async (id: number, fields: Record) => + { + setLoading(true); + setError(null); + + try + { + await apiFetch(`${ API_BASE }/update?id=${ id }`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(fields) + }); + + return true; + } + catch(e: any) + { + setError(e.message); + + return false; + } + finally + { + setLoading(false); + } + }, []); + + const createItem = useCallback(async (fields: Record) => + { + setLoading(true); + setError(null); + + try + { + const data = await apiFetch<{ id: number }>(`${ API_BASE }`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(fields) + }); + + return data.id; + } + catch(e: any) + { + setError(e.message); + + return null; + } + finally + { + setLoading(false); + } + }, []); + + const deleteItem = useCallback(async (id: number) => + { + setLoading(true); + setError(null); + + try + { + await apiFetch(`${ API_BASE }/delete?id=${ id }`, { method: 'POST' }); + + return true; + } + catch(e: any) + { + setError(e.message); + + return false; + } + finally + { + setLoading(false); + } + }, []); + + const loadInteractions = useCallback(async () => + { + try + { + const data = await apiFetch<{ interactions: Array }>(`${ API_BASE }/interactions`); + + setInteractions(data.interactions.map(i => typeof i === 'string' ? i : i.name)); + } + catch {} + }, []); + + const loadBySpriteId = useCallback(async (spriteId: number): Promise => + { + try + { + const data = await apiFetch<{ id: number }>(`${ API_BASE }/by-sprite?spriteId=${ spriteId }`); + + return await loadDetail(data.id); + } + catch(e: any) + { + setError(e.message); + + return false; + } + }, [ loadDetail ]); + + return { + items, total, page, loading, error, clearError, + selectedItem, setSelectedItem, catalogItems, furniDataEntry, + interactions, + searchItems, loadDetail, loadBySpriteId, updateItem, createItem, deleteItem, loadInteractions + }; +}; diff --git a/vite.config.mjs b/vite.config.mjs index 3267645..7e92144 100644 --- a/vite.config.mjs +++ b/vite.config.mjs @@ -3,12 +3,46 @@ import { resolve } from 'path'; import { defineConfig } from 'vite'; import tsconfigPaths from 'vite-tsconfig-paths'; +const renderer3 = resolve(__dirname, '..', 'renderer3'); + export default defineConfig({ plugins: [ react(), tsconfigPaths() ], + server: { + fs: { + allow: [ + resolve(__dirname), // nitro3 itself + renderer3, // renderer3 source + packages + ] + }, + proxy: { + '/api': { + target: 'http://localhost:3000', + changeOrigin: true, + } + } + }, resolve: { alias: { '@': resolve(__dirname, 'src'), - '~': resolve(__dirname, 'node_modules') + '~': resolve(__dirname, 'node_modules'), + // Renderer3 workspace packages → point to their src/index.ts + '@nitrots/api': resolve(renderer3, 'packages/api/src/index.ts'), + '@nitrots/assets': resolve(renderer3, 'packages/assets/src/index.ts'), + '@nitrots/avatar': resolve(renderer3, 'packages/avatar/src/index.ts'), + '@nitrots/camera': resolve(renderer3, 'packages/camera/src/index.ts'), + '@nitrots/communication': resolve(renderer3, 'packages/communication/src/index.ts'), + '@nitrots/configuration': resolve(renderer3, 'packages/configuration/src/index.ts'), + '@nitrots/events': resolve(renderer3, 'packages/events/src/index.ts'), + '@nitrots/localization': resolve(renderer3, 'packages/localization/src/index.ts'), + '@nitrots/room': resolve(renderer3, 'packages/room/src/index.ts'), + '@nitrots/session': resolve(renderer3, 'packages/session/src/index.ts'), + '@nitrots/sound': resolve(renderer3, 'packages/sound/src/index.ts'), + '@nitrots/utils/src': resolve(renderer3, 'packages/utils/src'), + '@nitrots/utils': resolve(renderer3, 'packages/utils/src/index.ts'), + // Resolve pixi.js and pixi-filters from renderer3's node_modules + 'pixi.js': resolve(renderer3, 'node_modules/pixi.js'), + 'pixi-filters': resolve(renderer3, 'node_modules/pixi-filters'), + 'howler': resolve(renderer3, 'node_modules/howler'), } }, build: { @@ -21,7 +55,7 @@ export default defineConfig({ { if(id.includes('node_modules')) { - if(id.includes('@nitrots/nitro-renderer')) return 'nitro-renderer'; + if(id.includes('@nitrots/nitro-renderer') || id.includes('renderer3')) return 'nitro-renderer'; return 'vendor'; }