From ea35f1994078d0b08f27550b06bde5aa6047bd62 Mon Sep 17 00:00:00 2001 From: medievalshell Date: Wed, 18 Mar 2026 20:11:40 +0100 Subject: [PATCH 1/2] Add UI Customization Panel with full color theming - New "Interfaccia" panel with color picker (HSV + hex/RGB/alpha + 30 presets) - Profile background customization tab - Accent color propagates via CSS variables to: card headers/tabs, context menus, Button dark/primary/gray variants, InfoStand panels, toolbar, room tools, purse, .btn-primary/.btn-dark CSS classes - All elements use var(--name, fallback) for zero visual change when default - Settings persisted in localStorage - Added react-colorful dependency - Added ui-config.json with header images config keys --- package.json | 1 + public/ui-config.json | 2434 +++++++++++++++++ src/App.tsx | 16 +- src/api/ui-settings/IUiSettings.ts | 21 + src/api/ui-settings/UiSettingsContext.tsx | 164 ++ src/api/ui-settings/index.ts | 2 + src/common/Button.tsx | 40 +- src/common/card/NitroCardHeaderView.tsx | 10 +- src/common/card/tabs/NitroCardTabsView.tsx | 12 +- src/components/MainView.tsx | 7 + .../InterfaceColorTabView.tsx | 179 ++ .../InterfaceImageTabView.tsx | 52 + .../InterfaceProfileTabView.tsx | 107 + .../InterfaceSettingsView.tsx | 74 + .../infostand/InfoStandBadgeSlotView.tsx | 3 +- .../infostand/InfoStandWidgetFurniView.tsx | 37 +- .../infostand/InfoStandWidgetUserView.tsx | 20 +- .../context-menu/ContextMenuHeaderView.tsx | 11 +- .../context-menu/ContextMenuListItemView.tsx | 11 +- .../widgets/context-menu/ContextMenuView.tsx | 2 +- src/components/toolbar/ToolbarView.tsx | 60 +- src/css/common/Buttons.css | 14 +- src/css/purse/PurseView.css | 4 +- src/css/room/InfoStand.css | 2 +- src/css/room/RoomWidgets.css | 6 +- 25 files changed, 3192 insertions(+), 97 deletions(-) create mode 100644 public/ui-config.json create mode 100644 src/api/ui-settings/IUiSettings.ts create mode 100644 src/api/ui-settings/UiSettingsContext.tsx create mode 100644 src/api/ui-settings/index.ts create mode 100644 src/components/interface-settings/InterfaceColorTabView.tsx create mode 100644 src/components/interface-settings/InterfaceImageTabView.tsx create mode 100644 src/components/interface-settings/InterfaceProfileTabView.tsx create mode 100644 src/components/interface-settings/InterfaceSettingsView.tsx diff --git a/package.json b/package.json index 4dab2f5..716c4bf 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "framer-motion": "^11.2.12", "react": "^19.2.4", "react-bootstrap": "^2.10.10", + "react-colorful": "^5.6.1", "react-dom": "^19.2.4", "react-icons": "^5.5.0", "react-slider": "^2.0.6", diff --git a/public/ui-config.json b/public/ui-config.json new file mode 100644 index 0000000..cec08d1 --- /dev/null +++ b/public/ui-config.json @@ -0,0 +1,2434 @@ +{ + "external.plugins": [ + "plugins/room-builder.js" + ], + "ui.header.images.count": 30, + "ui.header.images.url": "https://image.webbo.city/image/headerImage/image{id}.gif", + "image.library.notifications.url": "${image.library.url}notifications/%image%.png", + "achievements.images.url": "${image.library.url}Quests/%image%.png", + "camera.url": "/swf/usercontent/camera/", + "thumbnails.url": "/swf/usercontent/thumbnails/%thumbnail%.png", + "url.prefix": "", + "habbopages.url": "/swf/habbopages/", + "group.homepage.url": "${url.prefix}/groups/%groupid%/id", + "guide.help.alpha.groupid": 0, + "chat.viewer.height.percentage": 0.4, + "pathfinder.underpass.height": 1.5, + "widget.dimmer.colorwheel": false, + "avatar.wardrobe.max.slots": 10, + "user.badges.max.slots": 6, + "user.badges.group.slot.enabled": false, + "user.tags.enabled": false, + "camera.publish.disabled": false, + "hc.disabled": false, + "badge.descriptions.enabled": true, + "motto.max.length": 38, + "bot.name.max.length": 15, + "pet.package.name.max.length": 15, + "wired.action.bot.talk.to.avatar.max.length": 64, + "wired.action.bot.talk.max.length": 64, + "wired.action.chat.max.length": 100, + "wired.action.kick.from.room.max.length": 100, + "wired.action.mute.user.max.length": 100, + "game.center.enabled": false, + "guides.enabled": true, + "toolbar.hide.quests": true, + "navigator.room.models": [{ + "clubLevel": 0, + "tileSize": 104, + "name": "a" + }, { + "clubLevel": 0, + "tileSize": 94, + "name": "b" + }, { + "clubLevel": 0, + "tileSize": 36, + "name": "c" + }, { + "clubLevel": 0, + "tileSize": 84, + "name": "d" + }, { + "clubLevel": 0, + "tileSize": 80, + "name": "e" + }, { + "clubLevel": 0, + "tileSize": 80, + "name": "f" + }, { + "clubLevel": 0, + "tileSize": 416, + "name": "i" + }, { + "clubLevel": 0, + "tileSize": 320, + "name": "j" + }, { + "clubLevel": 0, + "tileSize": 448, + "name": "k" + }, { + "clubLevel": 0, + "tileSize": 352, + "name": "l" + }, { + "clubLevel": 0, + "tileSize": 384, + "name": "m" + }, { + "clubLevel": 0, + "tileSize": 372, + "name": "n" + }, { + "clubLevel": 1, + "tileSize": 80, + "name": "g" + }, { + "clubLevel": 1, + "tileSize": 74, + "name": "h" + }, { + "clubLevel": 1, + "tileSize": 416, + "name": "o" + }, { + "clubLevel": 1, + "tileSize": 352, + "name": "p" + }, { + "clubLevel": 1, + "tileSize": 304, + "name": "q" + }, { + "clubLevel": 1, + "tileSize": 336, + "name": "r" + }, { + "clubLevel": 1, + "tileSize": 748, + "name": "u" + }, { + "clubLevel": 1, + "tileSize": 438, + "name": "v" + }, { + "clubLevel": 2, + "tileSize": 540, + "name": "t" + }, { + "clubLevel": 2, + "tileSize": 512, + "name": "w" + }, { + "clubLevel": 2, + "tileSize": 396, + "name": "x" + }, { + "clubLevel": 2, + "tileSize": 440, + "name": "y" + }, { + "clubLevel": 2, + "tileSize": 456, + "name": "z" + }, { + "clubLevel": 2, + "tileSize": 208, + "name": "0" + }, { + "clubLevel": 2, + "tileSize": 1009, + "name": "1" + }, { + "clubLevel": 2, + "tileSize": 1044, + "name": "2" + }, { + "clubLevel": 2, + "tileSize": 183, + "name": "3" + }, { + "clubLevel": 2, + "tileSize": 254, + "name": "4" + }, { + "clubLevel": 2, + "tileSize": 1024, + "name": "5" + }, { + "clubLevel": 2, + "tileSize": 801, + "name": "6" + }, { + "clubLevel": 2, + "tileSize": 354, + "name": "7" + }, { + "clubLevel": 2, + "tileSize": 888, + "name": "8" + }, { + "clubLevel": 2, + "tileSize": 926, + "name": "9" + } + ], + "backgrounds.data": [{ + "backgroundId": 0, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 1, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 2, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 3, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 4, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 5, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 6, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 7, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 8, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 9, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 10, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 11, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 12, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 13, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 14, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 15, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 16, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 17, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 18, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 19, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 20, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 21, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 22, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 23, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 24, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 25, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 26, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 27, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 28, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 29, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 30, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 31, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 32, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 33, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 34, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 35, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 36, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 37, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 38, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 39, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 40, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 41, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 42, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 43, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 44, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 45, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 46, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 47, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 48, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 49, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 50, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 51, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 52, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 53, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 54, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 55, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 56, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 57, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 58, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 59, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 60, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 61, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 62, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 63, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 64, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 65, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 66, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 67, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 68, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 69, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 70, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 71, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 72, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 73, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 74, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 75, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 76, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 77, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 78, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 79, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 80, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 81, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 82, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 83, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 84, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 85, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 86, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 87, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 88, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 89, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 90, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 91, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 92, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 93, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 94, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 95, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 96, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 97, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 98, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 99, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 100, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 101, + "minRank": 2, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "backgroundId": 102, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 103, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 104, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 105, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 106, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 107, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 108, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 109, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 110, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 111, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 112, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 113, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 114, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 115, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 116, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 117, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 118, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 119, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 120, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 121, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 122, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 123, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 124, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 125, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 126, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 127, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 128, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 129, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 130, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 131, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 132, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 133, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 134, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 135, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 136, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 137, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 138, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 139, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 140, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 141, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 142, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 143, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 144, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 145, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 146, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 147, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 148, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 149, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 150, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 151, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 152, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 153, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 154, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 155, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 156, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 157, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 158, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 159, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 160, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 161, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 162, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 163, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 164, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 165, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 166, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 167, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 168, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 169, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 170, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 171, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 172, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 173, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 174, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 175, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 176, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 177, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 178, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 179, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 180, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 181, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 182, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 183, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 184, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 185, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 186, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "backgroundId": 187, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + } + ], + "stands.data": [{ + "standId": 0, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 1, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 2, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 3, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 4, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 5, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 6, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 7, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 8, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 9, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 10, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 11, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 12, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 13, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 14, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 15, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "standId": 16, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "standId": 17, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "standId": 18, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "standId": 19, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "standId": 20, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "standId": 21, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + } + ], + "overlays.data": [{ + "overlayId": 0, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "overlayId": 1, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "overlayId": 2, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "overlayId": 3, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "overlayId": 4, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "overlayId": 5, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "overlayId": 6, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "overlayId": 7, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "overlayId": 8, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + } + ], + "hotelview": { + "room.pool": "791", + "room.picnic": "2193", + "room.rooftop": "", + "room.rooftop.pool": "", + "room.peaceful": "", + "room.infobus": "5956", + "room.lobby": "1450", + "show.avatar": true, + "widgets": { + "slot.1.widget": "", + "slot.1.conf": {}, + "slot.2.widget": "", + "slot.2.conf": { + "image": "", + "texts": "", + "btnLink": "" + }, + "slot.3.widget": "", + "slot.3.conf": {}, + "slot.4.widget": "", + "slot.4.conf": {}, + "slot.5.widget": "", + "slot.5.conf": {}, + "slot.6.widget": "", + "slot.6.conf": { + "campaign": "" + }, + "slot.7.widget": "", + "slot.7.conf": {} + }, + "images": { + "background": "${asset.url}/images/reception/stretch_blue.png", + "background.colour": "#8ee0f0", + "sun": "${asset.url}/images/reception/sun.png", + "drape": "${asset.url}/images/reception/drape.png", + "left": "", + "right": "", + "right.repeat": "" + } + }, + "achievements.unseen.ignored": [ + "ACH_AllTimeHotelPresence" + ], + "avatareditor.show.clubitems.dimmed": true, + "avatareditor.show.clubitems.first": true, + "chat.history.max.items": 100, + "system.currency.types": [ + -1, + 0, + 5 + ], + "catalog.links": { + "hc.buy_hc": "habbo_club", + "hc.hc_gifts": "club_gifts", + "pets.buy_food": "pet_food", + "pets.buy_saddle": "saddles" + }, + "hc.center": { + "benefits.info": true, + "payday.info": true, + "gift.info": true, + "benefits.habbopage": "habboclub", + "payday.habbopage": "hcpayday" + }, + "respect.options": { + "enabled": false, + "sound": "sound_respect_received" + }, + "currency.display.number.short": false, + "currency.asset.icon.url": "${images.url}/wallet/%type%.png", + "catalog.asset.url": "${image.library.url}catalogue", + "catalog.asset.image.url": "${catalog.asset.url}/%name%.gif", + "catalog.asset.icon.url": "${catalog.asset.url}/icon_%name%.png", + "catalog.tab.icons": false, + "catalog.headers": false, + "chat.input.maxlength": 100, + "chat.styles.disabled": [], + "chat.styles": [{ + "styleId": 0, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 1, + "minRank": 5, + "isSystemStyle": true, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 2, + "minRank": 5, + "isSystemStyle": true, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 3, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 4, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 5, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 6, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 7, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 8, + "minRank": 5, + "isSystemStyle": true, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 9, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 10, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 11, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 12, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 13, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 14, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 15, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 16, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 17, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 18, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 19, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 20, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 21, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 22, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 23, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 24, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 25, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 26, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 27, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 28, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 29, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 30, + "minRank": 5, + "isSystemStyle": true, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 31, + "minRank": 5, + "isSystemStyle": true, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 32, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 33, + "minRank": 5, + "isSystemStyle": true, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 34, + "minRank": 5, + "isSystemStyle": true, + "isHcOnly": false, + "isAmbassadorOnly": false + }, { + "styleId": 35, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 36, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 37, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 38, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, { + "styleId": 39, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 40, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 41, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 42, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 43, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 44, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 45, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 46, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 47, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 48, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 49, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 50, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 51, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 52, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, { + "styleId": 53, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + } + ], + "camera.available.effects": [{ + "name": "dark_sepia", + "colorMatrix": [ + 0.4, + 0.4, + 0.1, + 0, + 110, + 0.3, + 0.4, + 0.1, + 0, + 30, + 0.3, + 0.2, + 0.1, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 0, + "enabled": true + }, { + "name": "increase_saturation", + "colorMatrix": [ + 2, + -0.5, + -0.5, + 0, + 0, + -0.5, + 2, + -0.5, + 0, + 0, + -0.5, + -0.5, + 2, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 0, + "enabled": true + }, { + "name": "increase_contrast", + "colorMatrix": [ + 1.5, + 0, + 0, + 0, + -50, + 0, + 1.5, + 0, + 0, + -50, + 0, + 0, + 1.5, + 0, + -50, + 0, + 0, + 0, + 1.5, + 0 + ], + "minLevel": 0, + "enabled": true + }, { + "name": "shadow_multiply_02", + "colorMatrix": [], + "minLevel": 0, + "blendMode": 2, + "enabled": true + }, { + "name": "color_1", + "colorMatrix": [ + 0.393, + 0.769, + 0.189, + 0, + 0, + 0.349, + 0.686, + 0.168, + 0, + 0, + 0.272, + 0.534, + 0.131, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 1, + "enabled": true + }, { + "name": "hue_bright_sat", + "colorMatrix": [ + 1, + 0.6, + 0.2, + 0, + -50, + 0.2, + 1, + 0.6, + 0, + -50, + 0.6, + 0.2, + 1, + 0, + -50, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 1, + "enabled": true + }, { + "name": "hearts_hardlight_02", + "colorMatrix": [], + "minLevel": 1, + "blendMode": 9, + "enabled": true + }, { + "name": "texture_overlay", + "colorMatrix": [], + "minLevel": 1, + "blendMode": 4, + "enabled": true + }, { + "name": "pinky_nrm", + "colorMatrix": [], + "minLevel": 1, + "blendMode": 0, + "enabled": true + }, { + "name": "color_2", + "colorMatrix": [ + 0.333, + 0.333, + 0.333, + 0, + 0, + 0.333, + 0.333, + 0.333, + 0, + 0, + 0.333, + 0.333, + 0.333, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 2, + "enabled": true + }, { + "name": "night_vision", + "colorMatrix": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1.1, + 0, + 0, + -50, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 2, + "enabled": true + }, { + "name": "stars_hardlight_02", + "colorMatrix": [], + "minLevel": 2, + "blendMode": 9, + "enabled": true + }, { + "name": "coffee_mpl", + "colorMatrix": [], + "minLevel": 2, + "blendMode": 2, + "enabled": true + }, { + "name": "security_hardlight", + "colorMatrix": [], + "minLevel": 3, + "blendMode": 9, + "enabled": true + }, { + "name": "bluemood_mpl", + "colorMatrix": [], + "minLevel": 3, + "blendMode": 2, + "enabled": true + }, { + "name": "rusty_mpl", + "colorMatrix": [], + "minLevel": 3, + "blendMode": 2, + "enabled": true + }, { + "name": "decr_conrast", + "colorMatrix": [ + 0.5, + 0, + 0, + 0, + 50, + 0, + 0.5, + 0, + 0, + 50, + 0, + 0, + 0.5, + 0, + 50, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 4, + "enabled": true + }, { + "name": "green_2", + "colorMatrix": [ + 0.5, + 0.5, + 0.5, + 0, + 0, + 0.5, + 0.5, + 0.5, + 0, + 90, + 0.5, + 0.5, + 0.5, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 4, + "enabled": true + }, { + "name": "alien_hrd", + "colorMatrix": [], + "minLevel": 4, + "blendMode": 9, + "enabled": true + }, { + "name": "color_3", + "colorMatrix": [ + 0.609, + 0.609, + 0.082, + 0, + 0, + 0.309, + 0.609, + 0.082, + 0, + 0, + 0.309, + 0.609, + 0.082, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 5, + "enabled": true + }, { + "name": "color_4", + "colorMatrix": [ + 0.8, + -0.8, + 1, + 0, + 70, + 0.8, + -0.8, + 1, + 0, + 70, + 0.8, + -0.8, + 1, + 0, + 70, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 5, + "enabled": true + }, { + "name": "toxic_hrd", + "colorMatrix": [], + "minLevel": 5, + "blendMode": 9, + "enabled": true + }, { + "name": "hypersaturated", + "colorMatrix": [ + 2, + -1, + 0, + 0, + 0, + -1, + 2, + 0, + 0, + 0, + 0, + -1, + 2, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 6, + "enabled": true + }, { + "name": "Yellow", + "colorMatrix": [ + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 6, + "enabled": true + }, { + "name": "misty_hrd", + "colorMatrix": [], + "minLevel": 6, + "blendMode": 9, + "enabled": true + }, { + "name": "x_ray", + "colorMatrix": [ + 0, + 1.2, + 0, + 0, + -100, + 0, + 2, + 0, + 0, + -120, + 0, + 2, + 0, + 0, + -120, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 7, + "enabled": true + }, { + "name": "decrease_saturation", + "colorMatrix": [ + 0.7, + 0.2, + 0.2, + 0, + 0, + 0.2, + 0.7, + 0.2, + 0, + 0, + 0.2, + 0.2, + 0.7, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 7, + "enabled": true + }, { + "name": "drops_mpl", + "colorMatrix": [], + "minLevel": 8, + "blendMode": 2, + "enabled": true + }, { + "name": "shiny_hrd", + "colorMatrix": [], + "minLevel": 9, + "blendMode": 9, + "enabled": true + }, { + "name": "glitter_hrd", + "colorMatrix": [], + "minLevel": 10, + "blendMode": 9, + "enabled": true + }, { + "name": "frame_gold", + "colorMatrix": [], + "minLevel": 10, + "blendMode": 0, + "enabled": true + }, { + "name": "frame_gray_4", + "colorMatrix": [], + "minLevel": 10, + "blendMode": 0, + "enabled": true + }, { + "name": "frame_black_2", + "colorMatrix": [], + "minLevel": 10, + "blendMode": 0, + "enabled": true + }, { + "name": "frame_wood_2", + "colorMatrix": [], + "minLevel": 10, + "blendMode": 0, + "enabled": true + }, { + "name": "finger_nrm", + "colorMatrix": [], + "minLevel": 10, + "blendMode": 0, + "enabled": true + }, { + "name": "color_5", + "colorMatrix": [ + 3.309, + 0.609, + 1.082, + 0.2, + 0, + 0.309, + 0.609, + 0.082, + 0, + 0, + 1.309, + 0.609, + 0.082, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 10, + "enabled": true + }, { + "name": "black_white_negative", + "colorMatrix": [ + -0.5, + -0.5, + -0.5, + 0, + 0, + -0.5, + -0.5, + -0.5, + 0, + 0, + -0.5, + -0.5, + -0.5, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 10, + "enabled": true + }, { + "name": "blue", + "colorMatrix": [ + 0.5, + 0.5, + 0.5, + 0, + -255, + 0.5, + 0.5, + 0.5, + 0, + -170, + 0.5, + 0.5, + 0.5, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 10, + "enabled": true + }, { + "name": "red", + "colorMatrix": [ + 0.5, + 0.5, + 0.5, + 0, + 0, + 0.5, + 0.5, + 0.5, + 0, + -170, + 0.5, + 0.5, + 0.5, + 0, + -170, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 10, + "enabled": true + }, { + "name": "green", + "colorMatrix": [ + 0.5, + 0.5, + 0.5, + 0, + -170, + 0.5, + 0.5, + 0.5, + 0, + 0, + 0.5, + 0.5, + 0.5, + 0, + -170, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 10, + "enabled": true + } + ], + "notification": { + "notification.admin.transient": { + "display": "POP_UP", + "image": "${image.library.url}/album1358/frank_wave_001.gif" + }, + "notification.builders_club.membership_expired": { + "display": "POP_UP" + }, + "notification.builders_club.membership_expires": { + "display": "POP_UP", + "image": "${image.library.url}/notifications/builders_club_room_locked_small.png" + }, + "notification.builders_club.membership_extended": { + "delivery": "PERSISTENT", + "display": "POP_UP" + }, + "notification.builders_club.membership_made": { + "delivery": "PERSISTENT", + "display": "POP_UP", + "image": "${image.library.url}/notifications/builders_club_membership_extended.png" + }, + "notification.builders_club.membership_renewed": { + "delivery": "PERSISTENT", + "display": "POP_UP", + "image": "${image.library.url}/notifications/builders_club_membership_extended.png" + }, + "notification.builders_club.room_locked": { + "display": "BUBBLE", + "image": "${image.library.url}/notifications/builders_club_room_locked_small.png" + }, + "notification.builders_club.room_unlocked": { + "display": "BUBBLE" + }, + "notification.builders_club.visit_denied_for_owner": { + "display": "BUBBLE", + "image": "${image.library.url}/notifications/builders_club_room_locked_small.png" + }, + "notification.builders_club.visit_denied_for_visitor": { + "display": "POP_UP", + "image": "${image.library.url}/notifications/builders_club_room_locked.png" + }, + "notification.campaign.credit.donation": { + "display": "BUBBLE" + }, + "notification.campaign.product.donation": { + "display": "BUBBLE" + }, + "notification.casino.too_many_dice.placement": { + "display": "POP_UP" + }, + "notification.casino.too_many_dice": { + "display": "POP_UP" + }, + "notification.cfh.created": { + "display": "POP_UP", + "title": "" + }, + "notification.feed.enabled": false, + "notification.floorplan_editor.error": { + "display": "POP_UP" + }, + "notification.forums.delivered": { + "delivery": "PERSISTENT", + "display": "POP_UP" + }, + "notification.forums.forum_settings_updated": { + "display": "BUBBLE" + }, + "notification.forums.message.hidden": { + "display": "BUBBLE" + }, + "notification.forums.message.restored": { + "display": "BUBBLE" + }, + "notification.forums.thread.hidden": { + "display": "BUBBLE" + }, + "notification.forums.thread.locked": { + "display": "BUBBLE" + }, + "notification.forums.thread.pinned": { + "display": "BUBBLE" + }, + "notification.forums.thread.restored": { + "display": "BUBBLE" + }, + "notification.forums.thread.unlocked": { + "display": "BUBBLE" + }, + "notification.forums.thread.unpinned": { + "display": "BUBBLE" + }, + "notification.furni_placement_error": { + "display": "BUBBLE" + }, + "notification.gifting.valentine": { + "delivery": "PERSISTENT", + "display": "BUBBLE", + "image": "${image.library.url}/notifications/polaroid_photo.png" + }, + "notification.items.enabled": true, + "notification.mute.forbidden.time": { + "display": "BUBBLE" + }, + "notification.npc.gift.received": { + "display": "BUBBLE", + "image": "${image.library.url}/album1584/X1517.gif" + } + } +} diff --git a/src/App.tsx b/src/App.tsx index d3566cc..f67bd6b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ import { GetAssetManager, GetAvatarRenderManager, GetCommunication, GetConfiguration, GetLocalizationManager, GetRoomEngine, GetRoomSessionManager, GetSessionDataManager, GetSoundManager, GetStage, GetTexturePool, GetTicker, HabboWebTools, LegacyExternalInterface, LoadGameUrlEvent, NitroLogger, NitroVersion, PrepareRenderer } from '@nitrots/nitro-renderer'; import { FC, useEffect, useState } from 'react'; -import { GetUIVersion } from './api'; +import { GetUIVersion, UiSettingsProvider } from './api'; import { Base } from './common'; import { LoadingView } from './components/loading/LoadingView'; import { MainView } from './components/MainView'; @@ -89,11 +89,13 @@ export const App: FC<{}> = props => }, []); return ( - - { !isReady && - } - { isReady && } - - + + + { !isReady && + } + { isReady && } + + + ); }; \ No newline at end of file diff --git a/src/api/ui-settings/IUiSettings.ts b/src/api/ui-settings/IUiSettings.ts new file mode 100644 index 0000000..24604b6 --- /dev/null +++ b/src/api/ui-settings/IUiSettings.ts @@ -0,0 +1,21 @@ +export interface IUiSettings +{ + colorMode: 'color' | 'image' | 'default'; + headerColor: string; + headerImageUrl: string; + headerAlpha: number; +} + +export const DEFAULT_UI_SETTINGS: IUiSettings = { + colorMode: 'default', + headerColor: '#1E7295', + headerImageUrl: '', + headerAlpha: 100 +}; + +export const PRESET_COLORS: string[] = [ + '#000000', '#444444', '#888888', '#CCCCCC', '#660000', '#CC3333', '#FF6666', '#CC6600', + '#FF3333', '#FF6633', '#FF9933', '#FFCC00', '#FFFF00', '#66FF00', '#00CC00', '#009900', + '#00FFCC', '#33CCFF', '#3366FF', '#0000CC', '#6633CC', '#9933FF', '#CC33FF', '#FF66CC', + '#FF99CC', '#1E7295', '#185D79', '#2DABC2', '#2B91A7', '#283F5D' +]; diff --git a/src/api/ui-settings/UiSettingsContext.tsx b/src/api/ui-settings/UiSettingsContext.tsx new file mode 100644 index 0000000..5d516cd --- /dev/null +++ b/src/api/ui-settings/UiSettingsContext.tsx @@ -0,0 +1,164 @@ +import { createContext, FC, PropsWithChildren, useCallback, useContext, useEffect, useState } from 'react'; +import { DEFAULT_UI_SETTINGS, IUiSettings } from './IUiSettings'; + +const STORAGE_KEY = 'nitro.ui.settings'; + +interface IUiSettingsContext +{ + settings: IUiSettings; + isCustomActive: boolean; + updateSettings: (partial: Partial) => void; + resetSettings: () => void; + getHeaderStyle: () => React.CSSProperties; + getTabsStyle: () => React.CSSProperties; + getAccentColor: () => string; +} + +const UiSettingsContext = createContext({ + settings: DEFAULT_UI_SETTINGS, + isCustomActive: false, + updateSettings: () => {}, + resetSettings: () => {}, + getHeaderStyle: () => ({}), + getTabsStyle: () => ({}), + getAccentColor: () => DEFAULT_UI_SETTINGS.headerColor +}); + +const darkenColor = (hex: string, amount: number): string => +{ + const num = parseInt(hex.replace('#', ''), 16); + const r = Math.max(0, ((num >> 16) & 0xFF) - amount); + const g = Math.max(0, ((num >> 8) & 0xFF) - amount); + const b = Math.max(0, (num & 0xFF) - amount); + + return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); +}; + +const loadSettings = (): IUiSettings => +{ + try + { + const stored = localStorage.getItem(STORAGE_KEY); + if(stored) return { ...DEFAULT_UI_SETTINGS, ...JSON.parse(stored) }; + } + catch(e) {} + + return { ...DEFAULT_UI_SETTINGS }; +}; + +const saveSettings = (settings: IUiSettings): void => +{ + try + { + localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); + } + catch(e) {} +}; + +export const UiSettingsProvider: FC = ({ children }) => +{ + const [ settings, setSettings ] = useState(loadSettings); + + const updateSettings = useCallback((partial: Partial) => + { + setSettings(prev => + { + const updated = { ...prev, ...partial }; + saveSettings(updated); + return updated; + }); + }, []); + + const resetSettings = useCallback(() => + { + setSettings({ ...DEFAULT_UI_SETTINGS }); + saveSettings(DEFAULT_UI_SETTINGS); + }, []); + + const getHeaderStyle = useCallback((): React.CSSProperties => + { + if(settings.colorMode === 'color') + { + return { backgroundColor: settings.headerColor }; + } + + if(settings.colorMode === 'image' && settings.headerImageUrl) + { + return { + backgroundImage: `url(${ settings.headerImageUrl })`, + backgroundSize: 'cover', + backgroundPosition: 'center', + backgroundRepeat: 'repeat' + }; + } + + return {}; + }, [ settings ]); + + const getTabsStyle = useCallback((): React.CSSProperties => + { + if(settings.colorMode === 'color') + { + return { backgroundColor: darkenColor(settings.headerColor, 30) }; + } + + if(settings.colorMode === 'image' && settings.headerImageUrl) + { + return { + backgroundImage: `url(${ settings.headerImageUrl })`, + backgroundSize: 'cover', + backgroundPosition: 'center bottom', + backgroundRepeat: 'repeat' + }; + } + + return {}; + }, [ settings ]); + + const getAccentColor = useCallback((): string => + { + if(settings.colorMode === 'color') return settings.headerColor; + return DEFAULT_UI_SETTINGS.headerColor; + }, [ settings ]); + + const isCustomActive = settings.colorMode !== 'default'; + + const ALL_CSS_VARS = [ + '--ui-accent-color', '--ui-accent-dark', + '--ui-ctx-bg', '--ui-ctx-header-bg', '--ui-ctx-item-bg1', '--ui-ctx-item-bg2', + '--ui-btn-primary-bg', '--ui-btn-primary-border', + '--ui-dark-bg', '--ui-dark-border' + ]; + + useEffect(() => + { + const root = document.documentElement; + + if(settings.colorMode === 'color') + { + const c = settings.headerColor; + root.style.setProperty('--ui-accent-color', c); + root.style.setProperty('--ui-accent-dark', darkenColor(c, 30)); + root.style.setProperty('--ui-ctx-bg', darkenColor(c, 50)); + root.style.setProperty('--ui-ctx-header-bg', darkenColor(c, 20)); + root.style.setProperty('--ui-ctx-item-bg1', darkenColor(c, 60)); + root.style.setProperty('--ui-ctx-item-bg2', darkenColor(c, 70)); + root.style.setProperty('--ui-btn-primary-bg', c); + root.style.setProperty('--ui-btn-primary-border', darkenColor(c, 20)); + root.style.setProperty('--ui-dark-bg', darkenColor(c, 55)); + root.style.setProperty('--ui-dark-border', darkenColor(c, 60)); + } + else + { + ALL_CSS_VARS.forEach(v => root.style.removeProperty(v)); + } + }, [ settings ]); + + return ( + + { children } + + ); +}; + +export const useUiSettings = () => useContext(UiSettingsContext); diff --git a/src/api/ui-settings/index.ts b/src/api/ui-settings/index.ts new file mode 100644 index 0000000..255a5da --- /dev/null +++ b/src/api/ui-settings/index.ts @@ -0,0 +1,2 @@ +export * from './IUiSettings'; +export * from './UiSettingsContext'; diff --git a/src/common/Button.tsx b/src/common/Button.tsx index 6b7d454..4da0794 100644 --- a/src/common/Button.tsx +++ b/src/common/Button.tsx @@ -12,20 +12,16 @@ export interface ButtonProps extends FlexProps export const Button: FC = props => { - const { variant = 'primary', size = 'sm', active = false, disabled = false, classNames = [], ...rest } = props; + const { variant = 'primary', size = 'sm', active = false, disabled = false, classNames = [], style = {}, ...rest } = props; const getClassNames = useMemo(() => { - - // fucked up method i know (i dont have a clue what im doing because im a ninja) - const newClassNames: string[] = [ 'pointer-events-auto inline-block font-normal leading-normal text-[#fff] text-center no-underline align-middle cursor-pointer select-none border border-[solid] border-transparent px-[.75rem] py-[.375rem] text-[.9rem] rounded-[.25rem] [transition:color_.15s_ease-in-out,background-color_.15s_ease-in-out,border-color_.15s_ease-in-out,box-shadow_.15s_ease-in-out]' ]; if(variant) { - if(variant == 'primary') - newClassNames.push('text-white bg-[#1e7295] border-[#1e7295] [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white hover:bg-[#1a617f] hover:border-[#185b77]'); + newClassNames.push('text-white [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white'); if(variant == 'success') newClassNames.push('text-white bg-[#00800b] border-[#00800b] [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white hover:bg-[#006d09] hover:border-[#006609]'); @@ -43,11 +39,10 @@ export const Button: FC = props => newClassNames.push('text-white bg-[#185d79] border-[#185d79] [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white hover:bg-[#144f67] hover:border-[#134a61]'); if(variant == 'dark') - newClassNames.push('text-white bg-dark [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white hover:bg-[#18181bfb] hover:border-[#161619fb]'); - - if(variant == 'gray') - newClassNames.push('text-white bg-[#1e7295] border-[#1e7295] [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white hover:bg-[#1a617f] hover:border-[#185b77]'); + newClassNames.push('text-white [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white'); + if(variant == 'gray') + newClassNames.push('text-white [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white'); } if(size) @@ -67,5 +62,28 @@ export const Button: FC = props => return newClassNames; }, [ variant, size, active, disabled, classNames ]); - return ; + const getStyle = useMemo(() => + { + if(variant === 'primary' || variant === 'gray') + { + return { + backgroundColor: 'var(--ui-btn-primary-bg, #1e7295)', + borderColor: 'var(--ui-btn-primary-border, #1e7295)', + ...style + }; + } + + if(variant === 'dark') + { + return { + backgroundColor: 'var(--ui-dark-bg, rgba(28, 28, 32, .98))', + borderColor: 'var(--ui-dark-border, rgba(28, 28, 32, .98))', + ...style + }; + } + + return style; + }, [ variant, style ]); + + return ; }; diff --git a/src/common/card/NitroCardHeaderView.tsx b/src/common/card/NitroCardHeaderView.tsx index 8bb354c..5bfbe2b 100644 --- a/src/common/card/NitroCardHeaderView.tsx +++ b/src/common/card/NitroCardHeaderView.tsx @@ -1,5 +1,6 @@ import { FC, MouseEvent } from 'react'; import { FaFlag } from 'react-icons/fa'; +import { useUiSettings } from '../../api'; import { Base, Column, ColumnProps, Flex } from '..'; interface NitroCardHeaderViewProps extends ColumnProps @@ -16,8 +17,7 @@ interface NitroCardHeaderViewProps extends ColumnProps export const NitroCardHeaderView: FC = props => { const { headerText = null, isGalleryPhoto = false, noCloseButton = false, isInfoToHabboPages = false, onReportPhoto = null, onClickInfoHabboPages = null, onCloseClick = null, justifyContent = 'center', alignItems = 'center', classNames = [], children = null, ...rest } = props; - - + const { isCustomActive, getHeaderStyle } = useUiSettings(); const onMouseDown = (event: MouseEvent) => { @@ -25,8 +25,12 @@ export const NitroCardHeaderView: FC = props => event.nativeEvent.stopImmediatePropagation(); }; + const headerClassName = isCustomActive + ? 'relative flex items-center justify-center flex-col drag-handler min-h-card-header max-h-card-header' + : 'relative flex items-center justify-center flex-col drag-handler min-h-card-header max-h-card-header bg-card-header'; + return ( - + { headerText } { isGalleryPhoto && diff --git a/src/common/card/tabs/NitroCardTabsView.tsx b/src/common/card/tabs/NitroCardTabsView.tsx index 5e14506..5c49ac4 100644 --- a/src/common/card/tabs/NitroCardTabsView.tsx +++ b/src/common/card/tabs/NitroCardTabsView.tsx @@ -1,21 +1,27 @@ import { FC, useMemo } from 'react'; +import { useUiSettings } from '../../../api'; import { Flex, FlexProps } from '../..'; export const NitroCardTabsView: FC = props => { const { justifyContent = 'center', gap = 1, classNames = [], children = null, ...rest } = props; + const { isCustomActive, getTabsStyle } = useUiSettings(); const getClassNames = useMemo(() => { - const newClassNames: string[] = [ 'justify-center gap-0.5 flex bg-card-tabs min-h-card-tabs max-h-card-tabs pt-1 border-b border-card-border px-2 -mt-px' ]; + const base = isCustomActive + ? 'justify-center gap-0.5 flex min-h-card-tabs max-h-card-tabs pt-1 border-b border-card-border px-2 -mt-px' + : 'justify-center gap-0.5 flex bg-card-tabs min-h-card-tabs max-h-card-tabs pt-1 border-b border-card-border px-2 -mt-px'; + + const newClassNames: string[] = [ base ]; if(classNames.length) newClassNames.push(...classNames); return newClassNames; - }, [ classNames ]); + }, [ classNames, isCustomActive ]); return ( - + { children } ); diff --git a/src/components/MainView.tsx b/src/components/MainView.tsx index 3fef0cc..41c320e 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'; @@ -21,10 +22,12 @@ import { ModToolsView } from './mod-tools/ModToolsView'; import { NavigatorView } from './navigator/NavigatorView'; import { NitrobubbleHiddenView } from './nitrobubblehidden/NitrobubbleHiddenView'; import { NitropediaView } from './nitropedia/NitropediaView'; +import { ExternalPluginLoader } from './plugins/ExternalPluginLoader'; import { RightSideView } from './right-side/RightSideView'; import { RoomView } from './room/RoomView'; import { ToolbarView } from './toolbar/ToolbarView'; import { UserProfileView } from './user-profile/UserProfileView'; +import { InterfaceSettingsView } from './interface-settings/InterfaceSettingsView'; import { UserSettingsView } from './user-settings/UserSettingsView'; import { WiredView } from './wired/WiredView'; import { YoutubeTvView } from './youtube-tv/YoutubeTvView'; @@ -85,6 +88,7 @@ export const MainView: FC<{}> = props => { landingViewVisible && @@ -105,6 +109,7 @@ export const MainView: FC<{}> = props => + @@ -115,7 +120,9 @@ export const MainView: FC<{}> = props => + + ); }; diff --git a/src/components/interface-settings/InterfaceColorTabView.tsx b/src/components/interface-settings/InterfaceColorTabView.tsx new file mode 100644 index 0000000..65bf172 --- /dev/null +++ b/src/components/interface-settings/InterfaceColorTabView.tsx @@ -0,0 +1,179 @@ +import { RgbaColorPicker, RgbaColor } from 'react-colorful'; +import { FC, useCallback, useMemo, useState } from 'react'; +import { FaUndo, FaTrash, FaPen, FaFillDrip, FaSave } from 'react-icons/fa'; +import { PRESET_COLORS, useUiSettings } from '../../api'; +import { Flex, Text } from '../../common'; + +const hexToRgba = (hex: string, a = 1): RgbaColor => +{ + const num = parseInt(hex.replace('#', ''), 16); + return { r: (num >> 16) & 0xFF, g: (num >> 8) & 0xFF, b: num & 0xFF, a }; +}; + +const rgbaToHex = (rgba: RgbaColor): string => +{ + return '#' + ((1 << 24) + (rgba.r << 16) + (rgba.g << 8) + rgba.b).toString(16).slice(1); +}; + +export const InterfaceColorTabView: FC<{}> = () => +{ + const { settings, updateSettings, resetSettings } = useUiSettings(); + const [ color, setColor ] = useState(() => hexToRgba(settings.headerColor, settings.headerAlpha / 100)); + + const hexColor = useMemo(() => rgbaToHex(color), [ color ]); + const alphaPercent = useMemo(() => Math.round((color.a ?? 1) * 100), [ color ]); + + const onHexInput = useCallback((value: string) => + { + const clean = value.replace(/[^0-9a-fA-F]/g, '').slice(0, 6); + if(clean.length === 6) + { + const rgba = hexToRgba('#' + clean, color.a); + setColor(rgba); + } + }, [ color.a ]); + + const onRgbInput = useCallback((channel: 'r' | 'g' | 'b', value: number) => + { + const clamped = Math.max(0, Math.min(255, value || 0)); + setColor(prev => ({ ...prev, [channel]: clamped })); + }, []); + + const onAlphaInput = useCallback((value: number) => + { + const clamped = Math.max(0, Math.min(100, value || 0)); + setColor(prev => ({ ...prev, a: clamped / 100 })); + }, []); + + const onPresetClick = useCallback((presetHex: string) => + { + setColor(hexToRgba(presetHex, color.a)); + }, [ color.a ]); + + const onSave = useCallback(() => + { + updateSettings({ + colorMode: 'color', + headerColor: hexColor, + headerAlpha: alphaPercent + }); + }, [ updateSettings, hexColor, alphaPercent ]); + + const onReset = useCallback(() => + { + resetSettings(); + setColor(hexToRgba('#1E7295', 1)); + }, [ resetSettings ]); + + const onDelete = useCallback(() => + { + updateSettings({ colorMode: 'default' }); + setColor(hexToRgba('#1E7295', 1)); + }, [ updateSettings ]); + + return ( + +
+ +
+ + + onHexInput(e.target.value) } + maxLength={ 6 } + /> + Hex + + + onRgbInput('r', parseInt(e.target.value)) } + min={ 0 } max={ 255 } + /> + R + + + onRgbInput('g', parseInt(e.target.value)) } + min={ 0 } max={ 255 } + /> + G + + + onRgbInput('b', parseInt(e.target.value)) } + min={ 0 } max={ 255 } + /> + B + + + onAlphaInput(parseInt(e.target.value)) } + min={ 0 } max={ 100 } + /> + A + + +
+ { PRESET_COLORS.map((presetHex, i) => ( +
onPresetClick(presetHex) } + /> + )) } +
+ + + + + + + + + ); +}; diff --git a/src/components/interface-settings/InterfaceImageTabView.tsx b/src/components/interface-settings/InterfaceImageTabView.tsx new file mode 100644 index 0000000..390a390 --- /dev/null +++ b/src/components/interface-settings/InterfaceImageTabView.tsx @@ -0,0 +1,52 @@ +import { FC, useCallback, useMemo } from 'react'; +import { GetConfigurationValue, useUiSettings } from '../../api'; + +export const InterfaceImageTabView: FC<{}> = () => +{ + const { settings, updateSettings } = useUiSettings(); + + const imageCount = useMemo(() => + { + return GetConfigurationValue('ui.header.images.count', 30); + }, []); + + const baseUrl = useMemo(() => + { + return GetConfigurationValue('ui.header.images.url', 'https://image.webbo.city/image/headerImage/image{id}.gif'); + }, []); + + const images = useMemo(() => + { + const result: string[] = []; + for(let i = 1; i <= imageCount; i++) + { + result.push(baseUrl.replace('{id}', String(i))); + } + return result; + }, [ imageCount, baseUrl ]); + + const onImageSelect = useCallback((url: string) => + { + updateSettings({ + colorMode: 'image', + headerImageUrl: url + }); + }, [ updateSettings ]); + + return ( +
+ { images.map((url, i) => ( +
onImageSelect(url) } + /> + )) } +
+ ); +}; diff --git a/src/components/interface-settings/InterfaceProfileTabView.tsx b/src/components/interface-settings/InterfaceProfileTabView.tsx new file mode 100644 index 0000000..6534c3c --- /dev/null +++ b/src/components/interface-settings/InterfaceProfileTabView.tsx @@ -0,0 +1,107 @@ +import { GetSessionDataManager, HabboClubLevelEnum } from '@nitrots/nitro-renderer'; +import { FC, useCallback, useMemo, useState } from 'react'; +import { GetClubMemberLevel, GetConfigurationValue } from '../../api'; +import { Base, Flex, Grid, LayoutCurrencyIcon, NitroCardTabsItemView, NitroCardTabsView, Text } from '../../common'; +import { useRoom } from '../../hooks'; + +interface ItemData +{ + id: number; + isHcOnly: boolean; + minRank: number; + isAmbassadorOnly: boolean; + selectable: boolean; +} + +const SUB_TABS = [ 'backgrounds', 'stands', 'overlays' ] as const; +type SubTabType = typeof SUB_TABS[number]; + +const SUB_TAB_LABELS: Record = { + backgrounds: 'Sfondi', + stands: 'Basi', + overlays: 'Overlay' +}; + +export const InterfaceProfileTabView: FC<{}> = () => +{ + const [ activeSubTab, setActiveSubTab ] = useState('backgrounds'); + const [ selectedBackground, setSelectedBackground ] = useState(0); + const [ selectedStand, setSelectedStand ] = useState(0); + const [ selectedOverlay, setSelectedOverlay ] = useState(0); + const { roomSession } = useRoom(); + + const userData = useMemo(() => ({ + isHcMember: GetClubMemberLevel() >= HabboClubLevelEnum.CLUB, + securityLevel: GetSessionDataManager().canChangeName, + isAmbassador: GetSessionDataManager().isAmbassador + }), []); + + const processData = useCallback((configData: any[], dataType: string): ItemData[] => + { + if(!configData?.length) return []; + + return configData + .filter(item => + { + const meetsRank = userData.securityLevel >= item.minRank; + const ambassadorEligible = !item.isAmbassadorOnly || userData.isAmbassador; + return item.isHcOnly || (meetsRank && ambassadorEligible); + }) + .map(item => ({ id: item[`${ dataType }Id`], ...item, selectable: !item.isHcOnly || userData.isHcMember })); + }, [ userData ]); + + const allData = useMemo(() => ({ + backgrounds: processData(GetConfigurationValue('backgrounds.data'), 'background'), + stands: processData(GetConfigurationValue('stands.data'), 'stand'), + overlays: processData(GetConfigurationValue('overlays.data'), 'overlay') + }), [ processData ]); + + const handleSelection = useCallback((id: number) => + { + if(!roomSession) return; + + const setters = { backgrounds: setSelectedBackground, stands: setSelectedStand, overlays: setSelectedOverlay }; + const currentValues = { backgrounds: selectedBackground, stands: selectedStand, overlays: selectedOverlay }; + + setters[activeSubTab](id); + const newValues = { ...currentValues, [activeSubTab]: id }; + roomSession.sendBackgroundMessage(newValues.backgrounds, newValues.stands, newValues.overlays); + }, [ activeSubTab, roomSession, selectedBackground, selectedStand, selectedOverlay ]); + + const renderItem = useCallback((item: ItemData, type: string) => ( + item.selectable && handleSelection(item.id) } + className={ item.selectable ? '' : 'non-selectable' } + > + + { item.isHcOnly && } + + ), [ handleSelection ]); + + return ( + + + { SUB_TABS.map(tab => ( + + )) } + + { !roomSession && ( + Entra in una stanza per modificare il profilo + ) } + { roomSession && ( + + { allData[activeSubTab].map(item => renderItem(item, activeSubTab.slice(0, -1))) } + + ) } + + ); +}; diff --git a/src/components/interface-settings/InterfaceSettingsView.tsx b/src/components/interface-settings/InterfaceSettingsView.tsx new file mode 100644 index 0000000..cd465c7 --- /dev/null +++ b/src/components/interface-settings/InterfaceSettingsView.tsx @@ -0,0 +1,74 @@ +import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useState } from 'react'; +import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsView, NitroCardTabsItemView, NitroCardView } from '../../common'; +import { InterfaceColorTabView } from './InterfaceColorTabView'; +import { InterfaceProfileTabView } from './InterfaceProfileTabView'; + +const TABS = [ 'color', 'profile' ] as const; +type TabType = typeof TABS[number]; + +const TAB_LABELS: Record = { + color: 'Colore', + profile: 'Sfondo profilo' +}; + +export const InterfaceSettingsView: FC<{}> = () => +{ + const [ isVisible, setIsVisible ] = useState(false); + const [ currentTab, setCurrentTab ] = useState('color'); + + 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; + case 'profile': + setCurrentTab('profile'); + setIsVisible(true); + return; + } + }, + eventUrlPrefix: 'interface-settings/' + }; + + AddLinkEventTracker(linkTracker); + return () => RemoveLinkEventTracker(linkTracker); + }, []); + + if(!isVisible) return null; + + return ( + + setIsVisible(false) } /> + + { TABS.map(tab => ( + setCurrentTab(tab) } + > + { TAB_LABELS[tab] } + + )) } + + + { currentTab === 'color' && } + { currentTab === 'profile' && } + + + ); +}; diff --git a/src/components/room/widgets/avatar-info/infostand/InfoStandBadgeSlotView.tsx b/src/components/room/widgets/avatar-info/infostand/InfoStandBadgeSlotView.tsx index 6a504d2..dde184d 100644 --- a/src/components/room/widgets/avatar-info/infostand/InfoStandBadgeSlotView.tsx +++ b/src/components/room/widgets/avatar-info/infostand/InfoStandBadgeSlotView.tsx @@ -45,7 +45,8 @@ const BadgeMiniPicker: FC<{ return (
e.stopPropagation() }> = props canUse = true; isCrackable = true; - crackableHits = stuffData.hits; - crackableTarget = stuffData.target; + crackableHits = stuffData?.hits ?? 0; + crackableTarget = stuffData?.target ?? 0; } else if(avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.JUKEBOX) @@ -458,7 +458,7 @@ export const InfoStandWidgetFurniView: FC = props return ( - +
@@ -527,7 +527,7 @@ export const InfoStandWidgetFurniView: FC = props { isCrackable && <>
- { LocalizeText('infostand.crackable_furni.hits_remaining', [ 'hits', 'target' ], [ crackableHits.toString(), crackableTarget.toString() ]) } + { LocalizeText('infostand.crackable_furni.hits_remaining', [ 'hits', 'target' ], [ (crackableHits ?? 0).toString(), (crackableTarget ?? 0).toString() ]) } } { avatarInfo.groupId > 0 && <> @@ -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/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx index 1791979..2dac49e 100644 --- a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx +++ b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx @@ -1,4 +1,4 @@ -import { GetSessionDataManager, RelationshipStatusInfoEvent, RelationshipStatusInfoMessageParser, RoomSessionFavoriteGroupUpdateEvent, RoomSessionUserBadgesEvent, RoomSessionUserFigureUpdateEvent, UserRelationshipsComposer } from '@nitrots/nitro-renderer'; +import { CreateLinkEvent, GetSessionDataManager, RelationshipStatusInfoEvent, RelationshipStatusInfoMessageParser, RoomSessionFavoriteGroupUpdateEvent, RoomSessionUserBadgesEvent, RoomSessionUserFigureUpdateEvent, UserRelationshipsComposer } from '@nitrots/nitro-renderer'; import { Dispatch, FC, FocusEvent, KeyboardEvent, SetStateAction, useCallback, useEffect, useState } from 'react'; import { FaPencilAlt, FaTimes } from 'react-icons/fa'; import { AvatarInfoUser, CloneObject, GetConfigurationValue, GetGroupInformation, GetUserProfile, LocalizeText, SendMessageComposer } from '../../../../../api'; @@ -7,7 +7,6 @@ import { useMessageEvent, useNitroEvent, useRoom } from '../../../../../hooks'; import { InfoStandBadgeSlotView } from './InfoStandBadgeSlotView'; import { InfoStandWidgetUserRelationshipsView } from './InfoStandWidgetUserRelationshipsView'; import { InfoStandWidgetUserTagsView } from './InfoStandWidgetUserTagsView'; -import { BackgroundsView } from '../../../../backgrounds/BackgroundsView'; interface InfoStandWidgetUserViewProps { avatarInfo: AvatarInfoUser; @@ -32,7 +31,7 @@ export const InfoStandWidgetUserView: FC = props = const handleProfileClick = useCallback(() => { GetUserProfile(avatarInfo.webID); }, [avatarInfo.webID]); - const handleEditClick = useCallback((event: React.MouseEvent) => { event.stopPropagation(); setIsVisible(prev => !prev); }, []); + const handleEditClick = useCallback((event: React.MouseEvent) => { event.stopPropagation(); CreateLinkEvent('interface-settings/profile'); }, []); const saveMotto = (motto: string) => { if (!isEditingMotto || motto.length > GetConfigurationValue('motto.max.length', 38) || !roomSession) return; @@ -127,7 +126,7 @@ export const InfoStandWidgetUserView: FC = props = return ( <> - +
@@ -257,19 +256,6 @@ export const InfoStandWidgetUserView: FC = props = )} - {isVisible && avatarInfo.type === AvatarInfoUser.OWN_USER && ( -
- -
- )} ); }; \ No newline at end of file diff --git a/src/components/room/widgets/context-menu/ContextMenuHeaderView.tsx b/src/components/room/widgets/context-menu/ContextMenuHeaderView.tsx index a0513cf..ff26177 100644 --- a/src/components/room/widgets/context-menu/ContextMenuHeaderView.tsx +++ b/src/components/room/widgets/context-menu/ContextMenuHeaderView.tsx @@ -3,16 +3,21 @@ import { Flex, FlexProps } from '../../../../common'; export const ContextMenuHeaderView: FC = props => { - const { justifyContent = 'center', alignItems = 'center', classNames = [], ...rest } = props; + const { justifyContent = 'center', alignItems = 'center', classNames = [], style = {}, ...rest } = props; const getClassNames = useMemo(() => { - const newClassNames: string[] = [ 'bg-[#3d5f6e] text-[#fff] min-w-[117px] h-[25px] max-h-[25px] text-[16px] mb-[2px]', 'p-1' ]; + const newClassNames: string[] = [ 'text-[#fff] min-w-[117px] h-[25px] max-h-[25px] text-[16px] mb-[2px]', 'p-1' ]; if(classNames.length) newClassNames.push(...classNames); return newClassNames; }, [ classNames ]); - return ; + const mergedStyle = useMemo(() => ({ + backgroundColor: 'var(--ui-ctx-header-bg, #3d5f6e)', + ...style + }), [ style ]); + + return ; }; diff --git a/src/components/room/widgets/context-menu/ContextMenuListItemView.tsx b/src/components/room/widgets/context-menu/ContextMenuListItemView.tsx index 0a1eacc..b012a93 100644 --- a/src/components/room/widgets/context-menu/ContextMenuListItemView.tsx +++ b/src/components/room/widgets/context-menu/ContextMenuListItemView.tsx @@ -8,7 +8,7 @@ interface ContextMenuListItemViewProps extends FlexProps export const ContextMenuListItemView: FC = props => { - const { disabled = false, fullWidth = true, justifyContent = 'center', alignItems = 'center', classNames = [], onClick = null, ...rest } = props; + const { disabled = false, fullWidth = true, justifyContent = 'center', alignItems = 'center', classNames = [], style = {}, onClick = null, ...rest } = props; const handleClick = (event: MouseEvent) => { @@ -19,7 +19,7 @@ export const ContextMenuListItemView: FC = props = const getClassNames = useMemo(() => { - const newClassNames: string[] = [ 'relative mb-[2px] p-[3px] overflow-hidden', 'h-[24px] max-h-[24px] p-[3px] bg-[repeating-linear-gradient(#131e25,#131e25_50%,#0d171d_50%,#0d171d_100%)] cursor-pointer' ]; + const newClassNames: string[] = [ 'relative mb-[2px] p-[3px] overflow-hidden', 'h-[24px] max-h-[24px] p-[3px] cursor-pointer' ]; if(disabled) newClassNames.push('disabled'); @@ -28,5 +28,10 @@ export const ContextMenuListItemView: FC = props = return newClassNames; }, [ disabled, classNames ]); - return ; + const mergedStyle = useMemo(() => ({ + background: 'repeating-linear-gradient(var(--ui-ctx-item-bg1, #131e25), var(--ui-ctx-item-bg1, #131e25) 50%, var(--ui-ctx-item-bg2, #0d171d) 50%, var(--ui-ctx-item-bg2, #0d171d) 100%)', + ...style + }), [ style ]); + + return ; }; diff --git a/src/components/room/widgets/context-menu/ContextMenuView.tsx b/src/components/room/widgets/context-menu/ContextMenuView.tsx index 1ca3e83..b92dc89 100644 --- a/src/components/room/widgets/context-menu/ContextMenuView.tsx +++ b/src/components/room/widgets/context-menu/ContextMenuView.tsx @@ -76,7 +76,6 @@ export const ContextMenuView: FC = ({ const getClassNames = useMemo(() => { const classes = [ 'p-[2px]!', - 'bg-[#1c323f]', 'border-2', 'border-[solid]', 'border-[rgba(255,255,255,.5)]', @@ -98,6 +97,7 @@ export const ContextMenuView: FC = ({ top: pos.y ?? 0, transition: isFading ? 'opacity 75ms linear' : undefined, opacity, + backgroundColor: 'var(--ui-ctx-bg, #1c323f)', ...style, }), [pos, opacity, isFading, style] diff --git a/src/components/toolbar/ToolbarView.tsx b/src/components/toolbar/ToolbarView.tsx index 7d6743a..62503bb 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/css/common/Buttons.css b/src/css/common/Buttons.css index 106a3cc..21d7f1f 100644 --- a/src/css/common/Buttons.css +++ b/src/css/common/Buttons.css @@ -24,8 +24,8 @@ input[type=number] { .btn-primary { color: #fff; - background-color: #3c6d82; - border: 2px solid #1a617f; + background-color: var(--ui-btn-primary-bg, #3c6d82); + border: 2px solid var(--ui-btn-primary-border, #1a617f); padding: 0.25rem 0.5rem; font-size: .7875rem; border-radius: 0.5rem; @@ -33,7 +33,7 @@ input[type=number] { } .btn-primary:hover { - border: 2px solid #1a617f; + border: 2px solid var(--ui-btn-primary-border, #1a617f); box-shadow: none!important; } @@ -81,16 +81,16 @@ input[type=number] { .btn-dark { color: #fff; - background-color: #212131; - border: 2px solid #1c1c2a; + background-color: var(--ui-dark-bg, #212131); + border: 2px solid var(--ui-dark-border, #1c1c2a); box-shadow: none!important; border-radius: 8px; padding: 4px 11px 4px 11px; } .btn-dark:hover{ - background-color: #212131; - border: 2px solid #1c1c2a; + background-color: var(--ui-dark-bg, #212131); + border: 2px solid var(--ui-dark-border, #1c1c2a); box-shadow: none!important; border-radius: 8px; padding: 4px 11px 4px 11px; diff --git a/src/css/purse/PurseView.css b/src/css/purse/PurseView.css index 7134551..69c1732 100644 --- a/src/css/purse/PurseView.css +++ b/src/css/purse/PurseView.css @@ -22,7 +22,7 @@ pointer-events: all; } .borderhccontent{ - background-color: #212131; + background-color: var(--ui-dark-bg, #212131); border-radius: 0.5rem!important; border: 2px solid #383853; height: calc(100% - 3px); @@ -46,7 +46,7 @@ } .nitro-purse-seasonal-currency { - background-color: #212131; + background-color: var(--ui-dark-bg, #212131); background: linear-gradient(to right, #5f5f8d, transparent); height: 30px; margin-bottom: 4px; diff --git a/src/css/room/InfoStand.css b/src/css/room/InfoStand.css index e44b062..7e1a050 100644 --- a/src/css/room/InfoStand.css +++ b/src/css/room/InfoStand.css @@ -27,7 +27,7 @@ width: clamp(160px, 20vw, 190px); /* Responsive width */ z-index: 30; pointer-events: auto; - background: #212131; + background: var(--ui-dark-bg, #212131); box-shadow: inset 0 5px rgba(38, 38, 57, 0.6), inset 0 -4px rgba(25, 25, 37, 0.6); border-radius: 0.5rem; padding: 10px; diff --git a/src/css/room/RoomWidgets.css b/src/css/room/RoomWidgets.css index 093fa67..b4d6ee2 100644 --- a/src/css/room/RoomWidgets.css +++ b/src/css/room/RoomWidgets.css @@ -4,7 +4,7 @@ left: 15px; .nitro-room-tools { - background: #212131; + background: var(--ui-dark-bg, #212131); box-shadow: inset 0px 5px lighten(rgba(#000, .6), 2.5), inset 0 -4px darken(rgba(#000, .6), 4); border-top-right-radius: .25rem; border-bottom-right-radius: .25rem; @@ -54,7 +54,7 @@ } .nitro-room-history { - background: #212131; + background: var(--ui-dark-bg, #212131); box-shadow: inset 0px 5px lighten(rgba(#000, .6), 2.5), inset 0 -4px darken(rgba(#000, .6), 4); transition: all .2s ease; width: 150px; @@ -63,7 +63,7 @@ } .nitro-room-tools-info { - background: #212131; + background: var(--ui-dark-bg, #212131); box-shadow: inset 0px 5px lighten(rgba(#000, .6), 2.5), inset 0 -4px darken(rgba(#000, .6), 4); transition: all .2s ease; max-width: 250px; From d6fbd19ee0799b2fc24142312edddd859873f7c9 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Mon, 16 Mar 2026 22:09:52 +0100 Subject: [PATCH 2/2] Add real-time 3D preview to floor plan editor Redesign the floor plan editor with side-by-side layout featuring: - Real-time isometric 3D preview that updates as tiles are drawn - Vertical height gradient selector with COLORMAP colors - Area counter showing total and walkable tile counts - Zoom controls (+/-) on the 2D canvas - Simplified single-row toolbar - Wall height control in the preview panel Co-Authored-By: medievalshell --- .../FloorplanEditorContext.tsx | 14 +- .../floorplan-editor/FloorplanEditorView.tsx | 156 ++++++++- .../views/FloorplanCanvasView.tsx | 105 +++--- .../views/FloorplanHeightSelector.tsx | 54 +++ .../views/FloorplanOptionsView.tsx | 247 ++++--------- .../views/FloorplanPreviewView.tsx | 328 ++++++++++++++++++ 6 files changed, 647 insertions(+), 257 deletions(-) create mode 100644 src/components/floorplan-editor/views/FloorplanHeightSelector.tsx create mode 100644 src/components/floorplan-editor/views/FloorplanPreviewView.tsx diff --git a/src/components/floorplan-editor/FloorplanEditorContext.tsx b/src/components/floorplan-editor/FloorplanEditorContext.tsx index eb528cf..1b2a3c4 100644 --- a/src/components/floorplan-editor/FloorplanEditorContext.tsx +++ b/src/components/floorplan-editor/FloorplanEditorContext.tsx @@ -8,13 +8,25 @@ interface IFloorplanEditorContext setOriginalFloorplanSettings: Dispatch>; visualizationSettings: IVisualizationSettings; setVisualizationSettings: Dispatch>; + floorHeight: number; + setFloorHeight: Dispatch>; + floorAction: number; + setFloorAction: Dispatch>; + tilemapVersion: number; + areaInfo: { total: number; walkable: number }; } const FloorplanEditorContext = createContext({ originalFloorplanSettings: null, setOriginalFloorplanSettings: null, visualizationSettings: null, - setVisualizationSettings: null + setVisualizationSettings: null, + floorHeight: 0, + setFloorHeight: null, + floorAction: 3, + setFloorAction: null, + tilemapVersion: 0, + areaInfo: { total: 0, walkable: 0 } }); export const FloorplanEditorContextProvider: FC> = props => ; diff --git a/src/components/floorplan-editor/FloorplanEditorView.tsx b/src/components/floorplan-editor/FloorplanEditorView.tsx index 59f7709..4003d13 100644 --- a/src/components/floorplan-editor/FloorplanEditorView.tsx +++ b/src/components/floorplan-editor/FloorplanEditorView.tsx @@ -1,19 +1,22 @@ import { AddLinkEventTracker, FloorHeightMapEvent, ILinkEventTracker, RemoveLinkEventTracker, RoomEngineEvent, RoomVisualizationSettingsEvent, UpdateFloorPropertiesMessageComposer } from '@nitrots/nitro-renderer'; -import { FC, useEffect, useState } from 'react'; +import { FC, useCallback, useEffect, useState } from 'react'; +import { FaCaretLeft, FaCaretRight } from 'react-icons/fa'; import { LocalizeText, SendMessageComposer } from '../../api'; -import { Button, ButtonGroup, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../common'; +import { Button, ButtonGroup, Column, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common'; import { useMessageEvent, useNitroEvent } from '../../hooks'; import { FloorplanEditorContextProvider } from './FloorplanEditorContext'; import { FloorplanEditor } from '@nitrots/nitro-renderer'; import { IFloorplanSettings } from '@nitrots/nitro-renderer'; import { IVisualizationSettings } from '@nitrots/nitro-renderer'; -import { convertNumbersForSaving, convertSettingToNumber } from '@nitrots/nitro-renderer'; +import { convertNumbersForSaving, convertSettingToNumber, FloorAction, HEIGHT_SCHEME } from '@nitrots/nitro-renderer'; import { FloorplanCanvasView } from './views/FloorplanCanvasView'; import { FloorplanImportExportView } from './views/FloorplanImportExportView'; import { FloorplanOptionsView } from './views/FloorplanOptionsView'; +import { FloorplanHeightSelector } from './views/FloorplanHeightSelector'; +import { FloorplanPreviewView } from './views/FloorplanPreviewView'; - -type ScrollDirection = 'up' | 'down' | 'left' | 'right'; +const MIN_WALL_HEIGHT = 0; +const MAX_WALL_HEIGHT = 16; export const FloorplanEditorView: FC<{}> = props => { @@ -34,7 +37,65 @@ export const FloorplanEditorView: FC<{}> = props => thicknessWall: 1, thicknessFloor: 1 }); - const [ canvasScrollHandler, setCanvasScrollHandler ] = useState<((direction: ScrollDirection) => void) | null>(null); + const [ floorHeight, setFloorHeight ] = useState(0); + const [ floorAction, setFloorAction ] = useState(FloorAction.SET); + const [ tilemapVersion, setTilemapVersion ] = useState(0); + const [ areaInfo, setAreaInfo ] = useState({ total: 0, walkable: 0 }); + + const calculateArea = useCallback(() => + { + const tilemap = FloorplanEditor.instance.tilemap; + + if(!tilemap || tilemap.length === 0) + { + setAreaInfo({ total: 0, walkable: 0 }); + + return; + } + + let total = 0; + let walkable = 0; + + for(let y = 0; y < tilemap.length; y++) + { + if(!tilemap[y]) continue; + + for(let x = 0; x < tilemap[y].length; x++) + { + if(!tilemap[y][x] || tilemap[y][x].height === 'x') continue; + + total++; + + if(!tilemap[y][x].isBlocked) walkable++; + } + } + + setAreaInfo({ total, walkable }); + }, []); + + // sync floorHeight/floorAction changes to the FloorplanEditor instance + useEffect(() => + { + FloorplanEditor.instance.actionSettings.currentAction = floorAction; + FloorplanEditor.instance.actionSettings.currentHeight = floorHeight.toString(36); + }, [ floorHeight, floorAction ]); + + // register onTilemapChange callback + useEffect(() => + { + if(!isVisible) return; + + FloorplanEditor.instance.onTilemapChange = () => + { + setTilemapVersion(prev => prev + 1); + calculateArea(); + }; + + return () => + { + FloorplanEditor.instance.onTilemapChange = null; + }; + }, [ isVisible, calculateArea ]); const saveFloorChanges = () => { @@ -47,16 +108,50 @@ export const FloorplanEditorView: FC<{}> = props => convertNumbersForSaving(visualizationSettings.thicknessFloor), (visualizationSettings.wallHeight - 1) )); - } + }; const revertChanges = () => { setVisualizationSettings({ wallHeight: originalFloorplanSettings.wallHeight, thicknessWall: originalFloorplanSettings.thicknessWall, thicknessFloor: originalFloorplanSettings.thicknessFloor, entryPointDir: originalFloorplanSettings.entryPointDir }); - + FloorplanEditor.instance.doorLocation = { x: originalFloorplanSettings.entryPoint[0], y: originalFloorplanSettings.entryPoint[1] }; FloorplanEditor.instance.setTilemap(originalFloorplanSettings.tilemap, originalFloorplanSettings.reservedTiles); FloorplanEditor.instance.renderTiles(); - } + }; + + const onWallHeightChange = (value: number) => + { + if(isNaN(value) || (value <= 0)) value = MIN_WALL_HEIGHT; + + if(value > MAX_WALL_HEIGHT) value = MAX_WALL_HEIGHT; + + setVisualizationSettings(prevValue => + { + const newValue = { ...prevValue }; + + newValue.wallHeight = value; + + return newValue; + }); + }; + + const increaseWallHeight = () => + { + let height = (visualizationSettings.wallHeight + 1); + + if(height > MAX_WALL_HEIGHT) height = MAX_WALL_HEIGHT; + + onWallHeightChange(height); + }; + + const decreaseWallHeight = () => + { + let height = (visualizationSettings.wallHeight - 1); + + if(height <= 0) height = MIN_WALL_HEIGHT; + + onWallHeightChange(height); + }; useNitroEvent(RoomEngineEvent.DISPOSED, event => setIsVisible(false)); @@ -117,7 +212,7 @@ export const FloorplanEditorView: FC<{}> = props => const parts = url.split('/'); if(parts.length < 2) return; - + switch(parts[1]) { case 'show': @@ -140,17 +235,42 @@ export const FloorplanEditorView: FC<{}> = props => }, []); return ( - + { isVisible && - + setIsVisible(false) } /> - - canvasScrollHandler && canvasScrollHandler(direction) } /> - + + + + + + + + + { LocalizeText('floor.editor.wall.height') } + + onWallHeightChange(event.target.valueAsNumber) } /> + + + + Area: { areaInfo.total } ({ areaInfo.walkable } caselle) + + + - + - @@ -161,4 +281,4 @@ export const FloorplanEditorView: FC<{}> = props => setImportExportVisible(false) } /> } ); -} +}; diff --git a/src/components/floorplan-editor/views/FloorplanCanvasView.tsx b/src/components/floorplan-editor/views/FloorplanCanvasView.tsx index e8f39a8..9db0903 100644 --- a/src/components/floorplan-editor/views/FloorplanCanvasView.tsx +++ b/src/components/floorplan-editor/views/FloorplanCanvasView.tsx @@ -1,25 +1,25 @@ import { GetOccupiedTilesMessageComposer, GetRoomEntryTileMessageComposer, RoomEntryTileMessageEvent, RoomOccupiedTilesMessageEvent } from '@nitrots/nitro-renderer'; import { FC, useEffect, useRef, useState } from 'react'; +import { FaPlus, FaMinus } from 'react-icons/fa'; import { SendMessageComposer } from '../../../api'; import { Base, Column, ColumnProps } from '../../../common'; import { useMessageEvent } from '../../../hooks'; import { useFloorplanEditorContext } from '../FloorplanEditorContext'; import { FloorplanEditor } from '@nitrots/nitro-renderer'; -type ScrollDirection = 'up' | 'down' | 'left' | 'right'; - interface FloorplanCanvasViewProps extends ColumnProps { - setScrollHandler(handler: ((direction: ScrollDirection) => void) | null): void; } export const FloorplanCanvasView: FC = props => { - const { gap = 1, children = null, setScrollHandler = null, ...rest } = props; - const [ occupiedTilesReceived , setOccupiedTilesReceived ] = useState(false); + const { gap = 1, children = null, ...rest } = props; + const [ occupiedTilesReceived, setOccupiedTilesReceived ] = useState(false); const [ entryTileReceived, setEntryTileReceived ] = useState(false); + const [ zoomLevel, setZoomLevel ] = useState(1.0); const { originalFloorplanSettings = null, setOriginalFloorplanSettings = null, setVisualizationSettings = null } = useFloorplanEditorContext(); const elementRef = useRef(null); + const canvasWrapperRef = useRef(null); useMessageEvent(RoomOccupiedTilesMessageEvent, event => { @@ -37,7 +37,7 @@ export const FloorplanCanvasView: FC = props => }); setOccupiedTilesReceived(true); - + elementRef.current.scrollTo((FloorplanEditor.instance.renderer.canvas.width / 3), 0); }); @@ -63,39 +63,16 @@ export const FloorplanCanvasView: FC = props => return newValue; }); - + FloorplanEditor.instance.doorLocation = { x: parser.x, y: parser.y }; setEntryTileReceived(true); }); - const onClickArrowButton = (scrollDirection: ScrollDirection) => - { - const element = elementRef.current; - - if(!element) return; - - switch(scrollDirection) - { - case 'up': - element.scrollBy({ top: -10 }); - break; - case 'down': - element.scrollBy({ top: 10 }); - break; - case 'left': - element.scrollBy({ left: -10 }); - break; - case 'right': - element.scrollBy({ left: 10 }); - break; - } - } - const onPointerEvent = (event: PointerEvent) => { event.preventDefault(); - + switch(event.type) { case 'pointerout': @@ -109,7 +86,10 @@ export const FloorplanCanvasView: FC = props => FloorplanEditor.instance.onPointerMove(event); break; } - } + }; + + const zoomIn = () => setZoomLevel(prev => Math.min(prev + 0.25, 2.0)); + const zoomOut = () => setZoomLevel(prev => Math.max(prev - 0.25, 0.5)); useEffect(() => { @@ -124,15 +104,15 @@ export const FloorplanCanvasView: FC = props => thicknessWall: originalFloorplanSettings.thicknessWall, thicknessFloor: originalFloorplanSettings.thicknessFloor, entryPointDir: prevValue.entryPointDir - } + }; }); - } + }; }, [ originalFloorplanSettings.thicknessFloor, originalFloorplanSettings.thicknessWall, originalFloorplanSettings.wallHeight, setVisualizationSettings ]); useEffect(() => { if(!entryTileReceived || !occupiedTilesReceived) return; - + FloorplanEditor.instance.renderTiles(); }, [ entryTileReceived, occupiedTilesReceived ]); @@ -144,45 +124,56 @@ export const FloorplanCanvasView: FC = props => const currentElement = elementRef.current; if(!currentElement) return; - - currentElement.appendChild(FloorplanEditor.instance.renderer.canvas); + + const wrapper = canvasWrapperRef.current; + + if(wrapper) wrapper.appendChild(FloorplanEditor.instance.renderer.canvas); currentElement.addEventListener('pointerup', onPointerEvent); - currentElement.addEventListener('pointerout', onPointerEvent); - currentElement.addEventListener('pointerdown', onPointerEvent); - currentElement.addEventListener('pointermove', onPointerEvent); - return () => + return () => { if(currentElement) { currentElement.removeEventListener('pointerup', onPointerEvent); - currentElement.removeEventListener('pointerout', onPointerEvent); - currentElement.removeEventListener('pointerdown', onPointerEvent); - currentElement.removeEventListener('pointermove', onPointerEvent); } - } + }; }, []); - useEffect(() => - { - if(!setScrollHandler) return; - - setScrollHandler(() => onClickArrowButton); - - return () => setScrollHandler(null); - }, [ setScrollHandler ]); - return ( - - + + +
+ +
+ + +
{ children } ); -} +}; diff --git a/src/components/floorplan-editor/views/FloorplanHeightSelector.tsx b/src/components/floorplan-editor/views/FloorplanHeightSelector.tsx new file mode 100644 index 0000000..8163c98 --- /dev/null +++ b/src/components/floorplan-editor/views/FloorplanHeightSelector.tsx @@ -0,0 +1,54 @@ +import { FC } from 'react'; +import { COLORMAP, FloorAction, HEIGHT_SCHEME } from '@nitrots/nitro-renderer'; +import { FloorplanEditor } from '@nitrots/nitro-renderer'; +import { Column, Text } from '../../../common'; +import { useFloorplanEditorContext } from '../FloorplanEditorContext'; + +const colormap = COLORMAP as Record; + +export const FloorplanHeightSelector: FC<{}> = () => +{ + const { floorHeight, setFloorHeight, setFloorAction } = useFloorplanEditorContext(); + + const onSelectHeight = (height: number) => + { + setFloorHeight(height); + setFloorAction(FloorAction.SET); + + FloorplanEditor.instance.actionSettings.currentAction = FloorAction.SET; + FloorplanEditor.instance.actionSettings.currentHeight = height.toString(36); + }; + + const heights: number[] = []; + + for(let i = 26; i >= 0; i--) heights.push(i); + + return ( + + { floorHeight } +
+ { heights.map(h => + { + const char = HEIGHT_SCHEME[h + 1]; + const color = colormap[char] || '101010'; + const isActive = (floorHeight === h); + + return ( +
onSelectHeight(h) } + title={ `${ h }` } + /> + ); + }) } +
+ + ); +}; diff --git a/src/components/floorplan-editor/views/FloorplanOptionsView.tsx b/src/components/floorplan-editor/views/FloorplanOptionsView.tsx index 5207b15..d4e7705 100644 --- a/src/components/floorplan-editor/views/FloorplanOptionsView.tsx +++ b/src/components/floorplan-editor/views/FloorplanOptionsView.tsx @@ -1,45 +1,32 @@ -import { FC, useState } from 'react'; -import { FaArrowDown, FaArrowLeft, FaArrowRight, FaArrowUp, FaCaretLeft, FaCaretRight } from 'react-icons/fa'; +import { FC } from 'react'; import { LocalizeText } from '../../../api'; -import { Button, Column, Flex, LayoutGridItem, Slider, Text } from '../../../common'; -import { COLORMAP, FloorAction } from '@nitrots/nitro-renderer'; +import { Flex, LayoutGridItem, Text } from '../../../common'; +import { FloorAction } from '@nitrots/nitro-renderer'; import { FloorplanEditor } from '@nitrots/nitro-renderer'; import { useFloorplanEditorContext } from '../FloorplanEditorContext'; -const MIN_WALL_HEIGHT: number = 0; -const MAX_WALL_HEIGHT: number = 16; - -const MIN_FLOOR_HEIGHT: number = 0; -const MAX_FLOOR_HEIGHT: number = 26; - -type ScrollDirection = 'up' | 'down' | 'left' | 'right'; - interface FloorplanOptionsViewProps { - onCanvasScroll?(direction: ScrollDirection): void; } export const FloorplanOptionsView: FC = props => { - const { onCanvasScroll = () => {} } = props; - const { visualizationSettings = null, setVisualizationSettings = null } = useFloorplanEditorContext(); - const [ floorAction, setFloorAction ] = useState(FloorAction.SET); - const [ floorHeight, setFloorHeight ] = useState(0); - const [ isSquareSelectMode, setSquareSelectMode ] = useState(false); - + const { visualizationSettings = null, setVisualizationSettings = null, floorAction, setFloorAction } = useFloorplanEditorContext(); + const isSquareSelectMode = FloorplanEditor.instance.isSquareSelectMode; + const selectAction = (action: number) => { setFloorAction(action); FloorplanEditor.instance.actionSettings.currentAction = action; - } + }; const toggleSquareSelectMode = () => { - const nextValue = FloorplanEditor.instance.toggleSquareSelectMode(); - - setSquareSelectMode(nextValue); - } + FloorplanEditor.instance.toggleSquareSelectMode(); + // force re-render by toggling action to same value + setFloorAction(prev => prev); + }; const changeDoorDirection = () => { @@ -58,18 +45,19 @@ export const FloorplanOptionsView: FC = props => return newValue; }); - } + }; - const onFloorHeightChange = (value: number) => + const onWallThicknessChange = (value: number) => { - if(isNaN(value) || (value <= 0)) value = 0; + setVisualizationSettings(prevValue => + { + const newValue = { ...prevValue }; - if(value > 26) value = 26; + newValue.thicknessWall = value; - setFloorHeight(value); - - FloorplanEditor.instance.actionSettings.currentHeight = value.toString(36); - } + return newValue; + }); + }; const onFloorThicknessChange = (value: number) => { @@ -81,157 +69,54 @@ export const FloorplanOptionsView: FC = props => return newValue; }); - } - - const onWallThicknessChange = (value: number) => - { - setVisualizationSettings(prevValue => - { - const newValue = { ...prevValue }; - - newValue.thicknessWall = value; - - return newValue; - }); - } - - const onWallHeightChange = (value: number) => - { - if(isNaN(value) || (value <= 0)) value = MIN_WALL_HEIGHT; - - if(value > MAX_WALL_HEIGHT) value = MAX_WALL_HEIGHT; - - setVisualizationSettings(prevValue => - { - const newValue = { ...prevValue }; - - newValue.wallHeight = value; - - return newValue; - }); - } - - const increaseWallHeight = () => - { - let height = (visualizationSettings.wallHeight + 1); - - if(height > MAX_WALL_HEIGHT) height = MAX_WALL_HEIGHT; - - onWallHeightChange(height); - } - - const decreaseWallHeight = () => - { - let height = (visualizationSettings.wallHeight - 1); - - if(height <= 0) height = MIN_WALL_HEIGHT; - - onWallHeightChange(height); - } + }; return ( - - - - { LocalizeText('floor.plan.editor.draw.mode') } - - - selectAction(FloorAction.SET) }> - - - selectAction(FloorAction.UNSET) }> - - - - - selectAction(FloorAction.UP) }> - - - selectAction(FloorAction.DOWN) }> - - - - selectAction(FloorAction.DOOR) }> - - - FloorplanEditor.instance.toggleSelectAll() }> - - - - - - - - - { LocalizeText('floor.plan.editor.enter.direction') } - - - - { LocalizeText('floor.editor.wall.height') } - - - onWallHeightChange(event.target.valueAsNumber) } /> - - - - - { LocalizeText('floor.plan.editor.room.options') } - - - - - + + + { LocalizeText('floor.plan.editor.draw.mode') } + + selectAction(FloorAction.SET) }> + + + selectAction(FloorAction.UNSET) }> + + + selectAction(FloorAction.UP) }> + + + selectAction(FloorAction.DOWN) }> + + + selectAction(FloorAction.DOOR) }> + + + FloorplanEditor.instance.toggleSelectAll() }> + + + + + + - - - { LocalizeText('floor.plan.editor.tile.height') }: { floorHeight } -
- onFloorHeightChange(event) } - renderThumb={ (props, state) => - { - const { key, style, ...rest } = (props as Record); - - return
{ state.valueNow }
; - } } /> -
-
- - - - - - -
- - - - - - + + { LocalizeText('floor.plan.editor.enter.direction') } + - + + + + + ); -} \ No newline at end of file +}; diff --git a/src/components/floorplan-editor/views/FloorplanPreviewView.tsx b/src/components/floorplan-editor/views/FloorplanPreviewView.tsx new file mode 100644 index 0000000..cd82a9c --- /dev/null +++ b/src/components/floorplan-editor/views/FloorplanPreviewView.tsx @@ -0,0 +1,328 @@ +import { FC, useEffect, useRef } from 'react'; +import { COLORMAP, HEIGHT_SCHEME, FloorplanEditor } from '@nitrots/nitro-renderer'; +import { useFloorplanEditorContext } from '../FloorplanEditorContext'; + +const colormap = COLORMAP as Record; + +const PREVIEW_TILE_W = 16; +const PREVIEW_TILE_H = 8; +const PREVIEW_BLOCK_H = 5; +const WALL_HEIGHT_PX = 40; +const WALL_COLOR = '#6B7B5E'; +const WALL_SIDE_COLOR = '#5A6A4F'; +const WALL_TOP_COLOR = '#7D8E6F'; + +function hexToRgb(hex: string): [number, number, number] +{ + const r = parseInt(hex.substring(0, 2), 16); + const g = parseInt(hex.substring(2, 4), 16); + const b = parseInt(hex.substring(4, 6), 16); + + return [ r, g, b ]; +} + +function rgbToHex(r: number, g: number, b: number): string +{ + return `#${ ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1) }`; +} + +function darken(hex: string, factor: number): string +{ + const [ r, g, b ] = hexToRgb(hex); + + return rgbToHex( + Math.floor(r * factor), + Math.floor(g * factor), + Math.floor(b * factor) + ); +} + +function getTilemapBounds(tilemap: any[][]): { minX: number; minY: number; maxX: number; maxY: number } +{ + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + + for(let y = 0; y < tilemap.length; y++) + { + if(!tilemap[y]) continue; + + for(let x = 0; x < tilemap[y].length; x++) + { + if(!tilemap[y][x] || tilemap[y][x].height === 'x') continue; + + if(x < minX) minX = x; + if(x > maxX) maxX = x; + if(y < minY) minY = y; + if(y > maxY) maxY = y; + } + } + + if(minX === Infinity) return { minX: 0, minY: 0, maxX: 0, maxY: 0 }; + + return { minX, minY, maxX, maxY }; +} + +function renderPreview(canvas: HTMLCanvasElement, wallHeight: number): void +{ + const ctx = canvas.getContext('2d'); + const tilemap = FloorplanEditor.instance.tilemap; + + if(!ctx || !tilemap || tilemap.length === 0) + { + if(ctx) + { + ctx.fillStyle = '#1a1a1a'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + } + + return; + } + + const bounds = getTilemapBounds(tilemap); + const tilesW = bounds.maxX - bounds.minX + 1; + const tilesH = bounds.maxY - bounds.minY + 1; + + // find max height for offset calculation + let maxTileHeight = 0; + + for(let y = bounds.minY; y <= bounds.maxY; y++) + { + for(let x = bounds.minX; x <= bounds.maxX; x++) + { + if(!tilemap[y] || !tilemap[y][x] || tilemap[y][x].height === 'x') continue; + + const hi = HEIGHT_SCHEME.indexOf(tilemap[y][x].height) - 1; + + if(hi > maxTileHeight) maxTileHeight = hi; + } + } + + // calculate isometric bounds + const isoW = (tilesW + tilesH) * PREVIEW_TILE_W; + const isoH = (tilesW + tilesH) * PREVIEW_TILE_H + maxTileHeight * PREVIEW_BLOCK_H + WALL_HEIGHT_PX; + + // scale to fit canvas + const scaleX = (canvas.width - 20) / isoW; + const scaleY = (canvas.height - 20) / isoH; + const scale = Math.min(scaleX, scaleY, 3); + + const offsetX = (canvas.width - isoW * scale) / 2; + const offsetY = (canvas.height - isoH * scale) / 2 + WALL_HEIGHT_PX * scale * 0.5; + + ctx.fillStyle = '#1a1a1a'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.save(); + ctx.translate(offsetX, offsetY); + ctx.scale(scale, scale); + + const tw = PREVIEW_TILE_W; + const th = PREVIEW_TILE_H; + + function isoX(gx: number, gy: number): number + { + return (gx - bounds.minX - gy + bounds.minY) * tw + (tilesH - 1) * tw; + } + + function isoY(gx: number, gy: number): number + { + return (gx - bounds.minX + gy - bounds.minY) * th; + } + + function hasActiveTile(gx: number, gy: number): boolean + { + return tilemap[gy] && tilemap[gy][gx] && tilemap[gy][gx].height !== 'x'; + } + + function getTileHeight(gx: number, gy: number): number + { + if(!hasActiveTile(gx, gy)) return 0; + + return Math.max(0, HEIGHT_SCHEME.indexOf(tilemap[gy][gx].height) - 1); + } + + // draw walls on north and west edges + const wallH = wallHeight > 0 ? wallHeight * PREVIEW_BLOCK_H + WALL_HEIGHT_PX * 0.3 : WALL_HEIGHT_PX * 0.6; + + for(let y = bounds.minY; y <= bounds.maxY; y++) + { + for(let x = bounds.minX; x <= bounds.maxX; x++) + { + if(!hasActiveTile(x, y)) continue; + + const tileH = getTileHeight(x, y) * PREVIEW_BLOCK_H; + const cx = isoX(x, y); + const cy = isoY(x, y) - tileH; + + // west wall (no tile to the left) + if(!hasActiveTile(x - 1, y)) + { + ctx.beginPath(); + ctx.moveTo(cx, cy + th); + ctx.lineTo(cx, cy + th - wallH); + ctx.lineTo(cx + tw, cy - wallH); + ctx.lineTo(cx + tw, cy); + ctx.closePath(); + ctx.fillStyle = WALL_SIDE_COLOR; + ctx.fill(); + ctx.strokeStyle = '#4A5A3F'; + ctx.lineWidth = 0.5; + ctx.stroke(); + } + + // north wall (no tile above) + if(!hasActiveTile(x, y - 1)) + { + ctx.beginPath(); + ctx.moveTo(cx + tw, cy); + ctx.lineTo(cx + tw, cy - wallH); + ctx.lineTo(cx + tw * 2, cy + th - wallH); + ctx.lineTo(cx + tw * 2, cy + th); + ctx.closePath(); + ctx.fillStyle = WALL_COLOR; + ctx.fill(); + ctx.strokeStyle = '#4A5A3F'; + ctx.lineWidth = 0.5; + ctx.stroke(); + } + + // wall top cap - corner + if(!hasActiveTile(x - 1, y) && !hasActiveTile(x, y - 1)) + { + ctx.beginPath(); + ctx.moveTo(cx + tw, cy - wallH); + ctx.lineTo(cx + tw + tw * 0.3, cy - wallH - th * 0.3); + ctx.lineTo(cx + tw, cy - wallH - th * 0.6); + ctx.lineTo(cx + tw - tw * 0.3, cy - wallH - th * 0.3); + ctx.closePath(); + ctx.fillStyle = WALL_TOP_COLOR; + ctx.fill(); + } + } + } + + // draw tiles back-to-front + for(let y = bounds.minY; y <= bounds.maxY; y++) + { + for(let x = bounds.minX; x <= bounds.maxX; x++) + { + if(!hasActiveTile(x, y)) continue; + + const tile = tilemap[y][x]; + const heightIndex = HEIGHT_SCHEME.indexOf(tile.height) - 1; + const tileH = Math.max(0, heightIndex) * PREVIEW_BLOCK_H; + + const cx = isoX(x, y); + const cy = isoY(x, y) - tileH; + + const heightChar = tile.height; + const baseColor = colormap[heightChar] || 'aaaaaa'; + const topColor = `#${ baseColor }`; + const leftColor = darken(baseColor, 0.65); + const rightColor = darken(baseColor, 0.80); + + // draw side faces if tile has height + const blockH = Math.max(0, heightIndex) * PREVIEW_BLOCK_H; + + // left face (visible when no neighbor to south or neighbor is shorter) + const southH = getTileHeight(x, y + 1); + const leftExpose = hasActiveTile(x, y + 1) ? Math.max(0, heightIndex - southH) * PREVIEW_BLOCK_H : blockH + PREVIEW_BLOCK_H; + + if(leftExpose > 0) + { + ctx.beginPath(); + ctx.moveTo(cx, cy + th); + ctx.lineTo(cx + tw, cy + th * 2); + ctx.lineTo(cx + tw, cy + th * 2 + leftExpose); + ctx.lineTo(cx, cy + th + leftExpose); + ctx.closePath(); + ctx.fillStyle = leftColor; + ctx.fill(); + } + + // right face + const eastH = getTileHeight(x + 1, y); + const rightExpose = hasActiveTile(x + 1, y) ? Math.max(0, heightIndex - eastH) * PREVIEW_BLOCK_H : blockH + PREVIEW_BLOCK_H; + + if(rightExpose > 0) + { + ctx.beginPath(); + ctx.moveTo(cx + tw * 2, cy + th); + ctx.lineTo(cx + tw, cy + th * 2); + ctx.lineTo(cx + tw, cy + th * 2 + rightExpose); + ctx.lineTo(cx + tw * 2, cy + th + rightExpose); + ctx.closePath(); + ctx.fillStyle = rightColor; + ctx.fill(); + } + + // top face + ctx.beginPath(); + ctx.moveTo(cx + tw, cy); + ctx.lineTo(cx + tw * 2, cy + th); + ctx.lineTo(cx + tw, cy + th * 2); + ctx.lineTo(cx, cy + th); + ctx.closePath(); + ctx.fillStyle = topColor; + ctx.fill(); + + // door indicator + const door = FloorplanEditor.instance.doorLocation; + + if(door.x === x && door.y === y) + { + ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'; + ctx.fill(); + ctx.strokeStyle = '#ffffff'; + ctx.lineWidth = 1; + ctx.stroke(); + } + } + } + + ctx.restore(); +} + +export const FloorplanPreviewView: FC<{}> = () => +{ + const { tilemapVersion, visualizationSettings } = useFloorplanEditorContext(); + const canvasRef = useRef(null); + const rafRef = useRef(0); + + useEffect(() => + { + if(!canvasRef.current) return; + + if(rafRef.current) cancelAnimationFrame(rafRef.current); + + rafRef.current = requestAnimationFrame(() => + { + const canvas = canvasRef.current; + + if(!canvas) return; + + const parent = canvas.parentElement; + + if(parent) + { + canvas.width = parent.clientWidth; + canvas.height = parent.clientHeight; + } + + renderPreview(canvas, visualizationSettings?.wallHeight ?? 0); + }); + + return () => + { + if(rafRef.current) cancelAnimationFrame(rafRef.current); + }; + }, [ tilemapVersion, visualizationSettings?.wallHeight ]); + + return ( +
+ +
+ ); +};