Add badge drag & drop system for InfoStand and inventory

- Drag & drop badges between slots in InfoStand (own user only)
- Mini badge picker on empty slot click with search
- Swap badges between occupied slots
- Hover animation (scale, glow) on badge slots
- Configurable group slot (user.badges.group.slot.enabled)
- Support for 6 badge slots when group slot disabled
- Race condition fix with localChangeRef
- Fixed-size array logic to prevent badge disappearing

Co-Authored-By: medievalshell <medievalshell@users.noreply.github.com>
This commit is contained in:
simoleo89
2026-03-15 20:48:05 +01:00
parent 2a29d3d08c
commit 38f38d7209
11 changed files with 1152 additions and 55 deletions
+54 -2
View File
@@ -1,5 +1,5 @@
import { GetRoomEngine } from '@nitrots/nitro-renderer';
import { CreateLinkEvent, GetRoomSession, SendMessageComposer } from '../../api';
import { FurnitureStackHeightComposer, GetRoomEngine, TextureUtils } from '@nitrots/nitro-renderer';
import { CreateLinkEvent, GetRoomSession, SendMessageComposer, VisitDesktop } from '../../api';
/**
* Plugin descriptor registered by external plugin scripts.
@@ -39,6 +39,14 @@ export interface INitroPluginApi
getRoomSession: () => ReturnType<typeof GetRoomSession>;
/** Send a message composer to the server */
sendMessage: typeof SendMessageComposer;
/** Send a chat message to the server (processed as command if starts with ':') */
sendChat: (text: string, styleId?: number) => void;
/** Send stack height update for a furniture item (objectId, heightInCentimeters) */
sendStackHeight: (objectId: number, height: number) => void;
/** Take a screenshot of the room and download it as PNG */
takeScreenshot: () => Promise<void>;
/** Leave the room and go to hotel view */
visitDesktop: () => void;
/** Create a draggable floating window and return its container element */
createWindow: (id: string, title: string, width: number) => HTMLDivElement | null;
/** Destroy a floating window by id */
@@ -96,6 +104,50 @@ const pluginApi: INitroPluginApi = {
sendMessage: SendMessageComposer,
sendChat(text: string, styleId: number = 0)
{
const session = GetRoomSession();
if (!session) return;
session.sendChatMessage(text, styleId, '');
},
sendStackHeight(objectId: number, height: number)
{
SendMessageComposer(new FurnitureStackHeightComposer(objectId, height));
},
async takeScreenshot()
{
try
{
const session = GetRoomSession();
if (!session) return;
const texture = GetRoomEngine().createTextureFromRoom(session.roomId, 1);
if (!texture) return;
const imageUrl = await TextureUtils.generateImageUrl(texture);
if (!imageUrl) return;
// Download the image
const link = document.createElement('a');
link.href = imageUrl;
link.download = `room_${session.roomId}_${Date.now()}.png`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
catch (e)
{
console.warn('[NitroPlugins] Screenshot failed:', e);
}
},
visitDesktop()
{
VisitDesktop();
},
createWindow(id: string, title: string, width: number): HTMLDivElement | null
{
// Remove existing window with same id