From 5548909beeecdb335f3d0df3097d812489f76d6e Mon Sep 17 00:00:00 2001 From: Gurkengewuerz Date: Thu, 16 Mar 2023 23:54:48 +0100 Subject: [PATCH] init monorepo with nitro-react --- .gitignore | 2 + .prettierrc | 13 +- apps/frontend/index.html | 35 +- .../public/android-chrome-192x192.png | Bin 0 -> 9604 bytes .../public/android-chrome-512x512.png | Bin 0 -> 29298 bytes apps/frontend/public/apple-touch-icon.png | Bin 0 -> 8241 bytes apps/frontend/public/browserconfig.xml | 9 + apps/frontend/public/favicon-16x16.png | Bin 0 -> 1017 bytes apps/frontend/public/favicon-32x32.png | Bin 0 -> 1778 bytes apps/frontend/public/favicon.ico | Bin 15086 -> 15086 bytes apps/frontend/public/mstile-150x150.png | Bin 0 -> 6600 bytes .../public/renderer-config.json.example | 112 ++ apps/frontend/public/robots.txt | 3 + apps/frontend/public/safari-pinned-tab.svg | 154 ++ apps/frontend/public/site.webmanifest | 20 + apps/frontend/public/ui-config.json.example | 1203 ++++++++++++ apps/frontend/src/App.scss | 102 + apps/frontend/src/App.tsx | 141 ++ apps/frontend/src/api/GetRendererVersion.ts | 3 + apps/frontend/src/api/GetUIVersion.ts | 1 + .../api/achievements/AchievementCategory.ts | 40 + .../api/achievements/AchievementUtilities.ts | 97 + .../api/achievements/IAchievementCategory.ts | 7 + apps/frontend/src/api/achievements/index.ts | 3 + .../src/api/avatar/AvatarEditorAction.ts | 7 + .../api/avatar/AvatarEditorGridColorItem.ts | 65 + .../api/avatar/AvatarEditorGridPartItem.ts | 337 ++++ .../src/api/avatar/AvatarEditorUtilities.ts | 277 +++ apps/frontend/src/api/avatar/BodyModel.ts | 76 + .../src/api/avatar/CategoryBaseModel.ts | 246 +++ apps/frontend/src/api/avatar/CategoryData.ts | 487 +++++ apps/frontend/src/api/avatar/FigureData.ts | 287 +++ .../src/api/avatar/FigureGenerator.ts | 90 + apps/frontend/src/api/avatar/HeadModel.ts | 24 + .../api/avatar/IAvatarEditorCategoryModel.ts | 19 + apps/frontend/src/api/avatar/LegModel.ts | 22 + apps/frontend/src/api/avatar/TorsoModel.ts | 23 + apps/frontend/src/api/avatar/index.ts | 13 + .../src/api/camera/CameraEditorTabs.ts | 5 + apps/frontend/src/api/camera/CameraPicture.ts | 9 + .../src/api/camera/CameraPictureThumbnail.ts | 7 + apps/frontend/src/api/camera/index.ts | 3 + .../frontend/src/api/campaign/CalendarItem.ts | 30 + .../src/api/campaign/CalendarItemState.ts | 7 + .../src/api/campaign/ICalendarItem.ts | 6 + apps/frontend/src/api/campaign/index.ts | 3 + .../catalog/BuilderFurniPlaceableStatus.ts | 10 + apps/frontend/src/api/catalog/CatalogNode.ts | 124 ++ apps/frontend/src/api/catalog/CatalogPage.ts | 59 + .../src/api/catalog/CatalogPageName.ts | 26 + .../src/api/catalog/CatalogPetPalette.ts | 10 + .../src/api/catalog/CatalogPurchaseState.ts | 10 + apps/frontend/src/api/catalog/CatalogType.ts | 5 + .../src/api/catalog/CatalogUtilities.ts | 125 ++ .../src/api/catalog/FurnitureOffer.ts | 120 ++ .../api/catalog/GetImageIconUrlForProduct.ts | 19 + .../api/catalog/GiftWrappingConfiguration.ts | 51 + apps/frontend/src/api/catalog/ICatalogNode.ts | 21 + .../src/api/catalog/ICatalogOptions.ts | 13 + apps/frontend/src/api/catalog/ICatalogPage.ts | 12 + .../api/catalog/IMarketplaceSearchOptions.ts | 7 + .../src/api/catalog/IPageLocalization.ts | 5 + apps/frontend/src/api/catalog/IProduct.ts | 16 + .../src/api/catalog/IPurchasableOffer.ts | 25 + .../src/api/catalog/IPurchaseOptions.ts | 9 + .../src/api/catalog/MarketplaceOfferData.ts | 128 ++ .../src/api/catalog/MarketplaceOfferState.ts | 7 + .../src/api/catalog/MarketplaceSearchType.ts | 6 + apps/frontend/src/api/catalog/Offer.ts | 245 +++ .../src/api/catalog/PageLocalization.ts | 36 + .../api/catalog/PlacedObjectPurchaseData.ts | 41 + apps/frontend/src/api/catalog/Product.ts | 143 ++ .../src/api/catalog/ProductTypeEnum.ts | 11 + .../frontend/src/api/catalog/RequestedPage.ts | 63 + apps/frontend/src/api/catalog/SearchResult.ts | 11 + apps/frontend/src/api/catalog/index.ts | 29 + .../src/api/chat-history/ChatEntryType.ts | 6 + .../chat-history/ChatHistoryCurrentDate.ts | 6 + .../src/api/chat-history/IChatEntry.ts | 17 + .../src/api/chat-history/IRoomHistoryEntry.ts | 5 + .../MessengerHistoryCurrentDate.ts | 6 + apps/frontend/src/api/chat-history/index.ts | 5 + apps/frontend/src/api/events/DispatchEvent.ts | 3 + .../src/api/events/DispatchMainEvent.ts | 5 + .../src/api/events/DispatchUiEvent.ts | 5 + .../src/api/events/UI_EVENT_DISPATCHER.ts | 3 + apps/frontend/src/api/events/index.ts | 4 + .../src/api/friends/GetGroupChatData.ts | 13 + .../src/api/friends/IGroupChatData.ts | 6 + .../src/api/friends/MessengerFriend.ts | 43 + .../src/api/friends/MessengerGroupType.ts | 5 + .../src/api/friends/MessengerIconState.ts | 6 + .../src/api/friends/MessengerRequest.ts | 41 + .../src/api/friends/MessengerSettings.ts | 11 + .../src/api/friends/MessengerThread.ts | 96 + .../src/api/friends/MessengerThreadChat.ts | 54 + .../api/friends/MessengerThreadChatGroup.ts | 41 + .../src/api/friends/OpenMessengerChat.ts | 7 + apps/frontend/src/api/friends/index.ts | 11 + .../src/api/groups/GetGroupInformation.ts | 7 + .../src/api/groups/GetGroupManager.ts | 6 + .../src/api/groups/GetGroupMembers.ts | 7 + .../frontend/src/api/groups/GroupBadgePart.ts | 31 + .../src/api/groups/GroupMembershipType.ts | 6 + apps/frontend/src/api/groups/GroupType.ts | 6 + .../src/api/groups/IGroupCustomize.ts | 8 + apps/frontend/src/api/groups/IGroupData.ts | 13 + .../src/api/groups/ToggleFavoriteGroup.ts | 7 + apps/frontend/src/api/groups/TryJoinGroup.ts | 4 + apps/frontend/src/api/groups/index.ts | 10 + .../src/api/guide-tool/GuideSessionState.ts | 23 + .../src/api/guide-tool/GuideToolMessage.ts | 21 + .../api/guide-tool/GuideToolMessageGroup.ts | 28 + apps/frontend/src/api/guide-tool/index.ts | 3 + apps/frontend/src/api/hc-center/ClubStatus.ts | 6 + .../src/api/hc-center/GetClubBadge.ts | 11 + apps/frontend/src/api/hc-center/index.ts | 2 + .../src/api/help/CallForHelpResult.ts | 5 + .../src/api/help/GetCloseReasonKey.ts | 8 + apps/frontend/src/api/help/IHelpReport.ts | 19 + apps/frontend/src/api/help/IReportedUser.ts | 5 + apps/frontend/src/api/help/ReportState.ts | 8 + apps/frontend/src/api/help/ReportType.ts | 11 + apps/frontend/src/api/help/index.ts | 6 + apps/frontend/src/api/index.ts | 29 + .../src/api/inventory/FurniCategory.ts | 26 + .../src/api/inventory/FurnitureItem.ts | 245 +++ .../src/api/inventory/FurnitureUtilities.ts | 172 ++ apps/frontend/src/api/inventory/GroupItem.ts | 461 +++++ apps/frontend/src/api/inventory/IBotItem.ts | 6 + .../src/api/inventory/IFurnitureItem.ts | 17 + apps/frontend/src/api/inventory/IPetItem.ts | 6 + .../src/api/inventory/IUnseenItemTracker.ts | 12 + .../src/api/inventory/InventoryUtilities.ts | 117 ++ .../src/api/inventory/PetUtilities.ts | 104 + apps/frontend/src/api/inventory/TradeState.ts | 10 + .../src/api/inventory/TradeUserData.ts | 15 + .../api/inventory/TradingNotificationType.ts | 12 + .../src/api/inventory/TradingUtilities.ts | 71 + .../src/api/inventory/UnseenItemCategory.ts | 9 + apps/frontend/src/api/inventory/index.ts | 15 + .../src/api/mod-tools/GetIssueCategoryName.ts | 35 + .../src/api/mod-tools/ISelectedUser.ts | 5 + apps/frontend/src/api/mod-tools/IUserInfo.ts | 6 + .../src/api/mod-tools/ModActionDefinition.ts | 49 + apps/frontend/src/api/mod-tools/index.ts | 4 + .../src/api/navigator/DoorStateType.ts | 12 + .../src/api/navigator/INavigatorData.ts | 17 + .../api/navigator/INavigatorSearchFilter.ts | 5 + .../src/api/navigator/IRoomChatSettings.ts | 8 + apps/frontend/src/api/navigator/IRoomData.ts | 23 + apps/frontend/src/api/navigator/IRoomModel.ts | 6 + .../api/navigator/IRoomModerationSettings.ts | 6 + .../NavigatorSearchResultViewDisplayMode.ts | 6 + .../src/api/navigator/RoomInfoData.ts | 60 + .../src/api/navigator/RoomSettingsUtils.ts | 10 + .../src/api/navigator/SearchFilterOptions.ts | 24 + .../src/api/navigator/TryVisitRoom.ts | 7 + apps/frontend/src/api/navigator/index.ts | 12 + .../src/api/nitro/AddLinkEventTracker.ts | 7 + .../frontend/src/api/nitro/CreateLinkEvent.ts | 8 + .../src/api/nitro/GetCommunication.ts | 7 + .../src/api/nitro/GetConfiguration.ts | 6 + apps/frontend/src/api/nitro/GetConnection.ts | 7 + .../frontend/src/api/nitro/GetLocalization.ts | 7 + .../src/api/nitro/GetNitroInstance.ts | 6 + apps/frontend/src/api/nitro/OpenUrl.ts | 16 + .../src/api/nitro/RemoveLinkEventTracker.ts | 7 + .../src/api/nitro/SendMessageComposer.ts | 4 + .../src/api/nitro/avatar/GetAvatarPalette.ts | 7 + .../nitro/avatar/GetAvatarRenderManager.ts | 7 + .../src/api/nitro/avatar/GetAvatarSetType.ts | 7 + apps/frontend/src/api/nitro/avatar/index.ts | 3 + .../camera/GetRoomCameraWidgetManager.ts | 7 + apps/frontend/src/api/nitro/camera/index.ts | 1 + .../api/nitro/core/GetConfigurationManager.ts | 7 + .../src/api/nitro/core/GetNitroCore.ts | 7 + apps/frontend/src/api/nitro/core/index.ts | 2 + apps/frontend/src/api/nitro/index.ts | 15 + .../src/api/nitro/room/DispatchMouseEvent.ts | 55 + .../src/api/nitro/room/DispatchTouchEvent.ts | 82 + .../src/api/nitro/room/GetOwnRoomObject.ts | 32 + .../src/api/nitro/room/GetRoomEngine.ts | 7 + .../src/api/nitro/room/GetRoomObjectBounds.ts | 13 + .../nitro/room/GetRoomObjectScreenLocation.ts | 13 + .../InitializeRoomInstanceRenderingCanvas.ts | 9 + .../room/IsFurnitureSelectionDisabled.ts | 24 + .../nitro/room/ProcessRoomObjectOperation.ts | 6 + .../src/api/nitro/room/SetActiveRoomId.ts | 6 + apps/frontend/src/api/nitro/room/index.ts | 10 + .../nitro/session/CanManipulateFurniture.ts | 11 + .../api/nitro/session/CreateRoomSession.ts | 6 + .../src/api/nitro/session/GetCanStandUp.ts | 13 + .../api/nitro/session/GetCanUseExpression.ts | 14 + .../api/nitro/session/GetClubMemberLevel.ts | 10 + .../src/api/nitro/session/GetFurnitureData.ts | 20 + .../GetFurnitureDataForProductOffer.ts | 21 + .../session/GetFurnitureDataForRoomObject.ts | 22 + .../src/api/nitro/session/GetOwnPosture.ts | 13 + .../session/GetProductDataForLocalization.ts | 9 + .../src/api/nitro/session/GetRoomSession.ts | 7 + .../nitro/session/GetRoomSessionManager.ts | 7 + .../nitro/session/GetSessionDataManager.ts | 7 + .../src/api/nitro/session/GoToDesktop.ts | 7 + .../src/api/nitro/session/HasHabboClub.ts | 7 + .../src/api/nitro/session/HasHabboVip.ts | 7 + .../nitro/session/IsOwnerOfFloorFurniture.ts | 16 + .../api/nitro/session/IsOwnerOfFurniture.ts | 12 + .../src/api/nitro/session/IsRidingHorse.ts | 14 + .../src/api/nitro/session/StartRoomSession.ts | 7 + .../src/api/nitro/session/VisitDesktop.ts | 9 + apps/frontend/src/api/nitro/session/index.ts | 21 + .../api/notification/NotificationAlertItem.ts | 67 + .../api/notification/NotificationAlertType.ts | 10 + .../notification/NotificationBubbleItem.ts | 48 + .../notification/NotificationBubbleType.ts | 19 + .../notification/NotificationConfirmItem.ts | 67 + .../notification/NotificationConfirmType.ts | 4 + apps/frontend/src/api/notification/index.ts | 6 + apps/frontend/src/api/purse/IPurse.ts | 15 + apps/frontend/src/api/purse/Purse.ts | 165 ++ apps/frontend/src/api/purse/index.ts | 2 + .../room/events/RoomWidgetPollUpdateEvent.ts | 110 ++ ...WidgetUpdateBackgroundColorPreviewEvent.ts | 35 + .../RoomWidgetUpdateChatInputContentEvent.ts | 29 + .../api/room/events/RoomWidgetUpdateEvent.ts | 4 + .../RoomWidgetUpdateRentableBotChatEvent.ts | 62 + .../events/RoomWidgetUpdateRoomObjectEvent.ts | 43 + apps/frontend/src/api/room/events/index.ts | 6 + apps/frontend/src/api/room/index.ts | 2 + .../src/api/room/widgets/AvatarInfoFurni.ts | 38 + .../src/api/room/widgets/AvatarInfoName.ts | 11 + .../src/api/room/widgets/AvatarInfoPet.ts | 46 + .../api/room/widgets/AvatarInfoRentableBot.ts | 23 + .../src/api/room/widgets/AvatarInfoUser.ts | 49 + .../api/room/widgets/AvatarInfoUtilities.ts | 454 +++++ .../src/api/room/widgets/BotSkillsEnum.ts | 18 + .../src/api/room/widgets/ChatBubbleMessage.ts | 56 + .../api/room/widgets/ChatMessageTypeEnum.ts | 6 + .../DimmerFurnitureWidgetPresetItem.ts | 9 + .../src/api/room/widgets/DoChatsOverlap.ts | 7 + .../room/widgets/FurnitureDimmerUtilities.ts | 29 + .../src/api/room/widgets/GetDiskColor.ts | 37 + .../src/api/room/widgets/IAvatarInfo.ts | 4 + .../api/room/widgets/ICraftingIngredient.ts | 6 + .../src/api/room/widgets/ICraftingRecipe.ts | 6 + .../src/api/room/widgets/IPhotoData.ts | 42 + .../api/room/widgets/MannequinUtilities.ts | 39 + .../src/api/room/widgets/PetSupplementEnum.ts | 5 + .../src/api/room/widgets/PostureTypeEnum.ts | 5 + .../src/api/room/widgets/RoomDimmerPreset.ts | 35 + .../src/api/room/widgets/RoomObjectItem.ts | 28 + .../src/api/room/widgets/UseProductItem.ts | 12 + .../src/api/room/widgets/VoteValue.ts | 8 + .../widgets/YoutubeVideoPlaybackStateEnum.ts | 9 + apps/frontend/src/api/room/widgets/index.ts | 25 + apps/frontend/src/api/user/GetUserProfile.ts | 7 + apps/frontend/src/api/user/index.ts | 1 + apps/frontend/src/api/utils/CloneObject.ts | 14 + apps/frontend/src/api/utils/ColorUtils.ts | 65 + apps/frontend/src/api/utils/ConvertSeconds.ts | 9 + .../frontend/src/api/utils/GetLocalStorage.ts | 11 + .../src/api/utils/LocalStorageKeys.ts | 5 + .../src/api/utils/LocalizeBadgeDescription.ts | 10 + .../src/api/utils/LocalizeBageName.ts | 10 + .../src/api/utils/LocalizeFormattedNumber.ts | 6 + .../src/api/utils/LocalizeShortNumber.ts | 36 + apps/frontend/src/api/utils/LocalizeText.ts | 6 + apps/frontend/src/api/utils/PlaySound.ts | 24 + .../src/api/utils/ProductImageUtility.ts | 59 + apps/frontend/src/api/utils/Randomizer.ts | 28 + .../src/api/utils/RoomChatFormatter.ts | 75 + .../frontend/src/api/utils/SetLocalStorage.ts | 1 + apps/frontend/src/api/utils/SoundNames.ts | 9 + .../src/api/utils/WindowSaveOptions.ts | 5 + apps/frontend/src/api/utils/index.ts | 17 + .../src/api/wired/GetWiredTimeLocale.ts | 8 + .../src/api/wired/WiredActionLayoutCode.ts | 29 + .../src/api/wired/WiredConditionLayoutCode.ts | 29 + .../src/api/wired/WiredDateToString.ts | 1 + apps/frontend/src/api/wired/WiredFurniType.ts | 7 + .../src/api/wired/WiredSelectionFilter.ts | 95 + .../src/api/wired/WiredSelectionVisualizer.ts | 68 + .../src/api/wired/WiredStringDelimeter.ts | 1 + .../src/api/wired/WiredTriggerLayoutCode.ts | 17 + apps/frontend/src/api/wired/index.ts | 9 + apps/frontend/src/app/app.module.scss | 1 - apps/frontend/src/app/app.spec.tsx | 15 - apps/frontend/src/app/app.tsx | 16 - apps/frontend/src/app/nx-welcome.tsx | 820 -------- apps/frontend/src/assets/.gitkeep | 0 .../assets/images/achievements/back-arrow.png | Bin 0 -> 331 bytes .../images/avatareditor/arrow-left-icon.png | Bin 0 -> 198 bytes .../images/avatareditor/arrow-right-icon.png | Bin 0 -> 192 bytes .../avatar-editor-spritesheet.png | Bin 0 -> 23260 bytes .../assets/images/avatareditor/ca-icon.png | Bin 0 -> 259 bytes .../images/avatareditor/ca-selected-icon.png | Bin 0 -> 335 bytes .../assets/images/avatareditor/cc-icon.png | Bin 0 -> 282 bytes .../images/avatareditor/cc-selected-icon.png | Bin 0 -> 338 bytes .../assets/images/avatareditor/ch-icon.png | Bin 0 -> 228 bytes .../images/avatareditor/ch-selected-icon.png | Bin 0 -> 260 bytes .../assets/images/avatareditor/clear-icon.png | Bin 0 -> 272 bytes .../assets/images/avatareditor/cp-icon.png | Bin 0 -> 252 bytes .../images/avatareditor/cp-selected-icon.png | Bin 0 -> 280 bytes .../assets/images/avatareditor/ea-icon.png | Bin 0 -> 251 bytes .../images/avatareditor/ea-selected-icon.png | Bin 0 -> 298 bytes .../assets/images/avatareditor/fa-icon.png | Bin 0 -> 234 bytes .../images/avatareditor/fa-selected-icon.png | Bin 0 -> 286 bytes .../images/avatareditor/female-icon.png | Bin 0 -> 236 bytes .../avatareditor/female-selected-icon.png | Bin 0 -> 270 bytes .../assets/images/avatareditor/ha-icon.png | Bin 0 -> 241 bytes .../images/avatareditor/ha-selected-icon.png | Bin 0 -> 285 bytes .../assets/images/avatareditor/he-icon.png | Bin 0 -> 267 bytes .../images/avatareditor/he-selected-icon.png | Bin 0 -> 338 bytes .../assets/images/avatareditor/hr-icon.png | Bin 0 -> 257 bytes .../images/avatareditor/hr-selected-icon.png | Bin 0 -> 348 bytes .../assets/images/avatareditor/lg-icon.png | Bin 0 -> 196 bytes .../images/avatareditor/lg-selected-icon.png | Bin 0 -> 236 bytes .../images/avatareditor/loading-icon.png | Bin 0 -> 181 bytes .../assets/images/avatareditor/male-icon.png | Bin 0 -> 228 bytes .../avatareditor/male-selected-icon.png | Bin 0 -> 256 bytes .../images/avatareditor/sellable-icon.png | Bin 0 -> 229 bytes .../assets/images/avatareditor/sh-icon.png | Bin 0 -> 208 bytes .../images/avatareditor/sh-selected-icon.png | Bin 0 -> 266 bytes .../images/avatareditor/spotlight-icon.png | Bin 0 -> 11373 bytes .../assets/images/avatareditor/wa-icon.png | Bin 0 -> 257 bytes .../images/avatareditor/wa-selected-icon.png | Bin 0 -> 308 bytes .../src/assets/images/campaign/available.png | Bin 0 -> 1118 bytes .../campaign/campaign_day_generic_bg.png | Bin 0 -> 36045 bytes .../images/campaign/campaign_opened.png | Bin 0 -> 744 bytes .../images/campaign/campaign_spritesheet.png | Bin 0 -> 83689 bytes .../src/assets/images/campaign/locked.png | Bin 0 -> 220 bytes .../src/assets/images/campaign/locked_bg.png | Bin 0 -> 5414 bytes .../src/assets/images/campaign/next.png | Bin 0 -> 244 bytes .../src/assets/images/campaign/prev.png | Bin 0 -> 235 bytes .../assets/images/campaign/unavailable.png | Bin 0 -> 448 bytes .../assets/images/campaign/unlocked_bg.png | Bin 0 -> 8798 bytes .../catalog/diamond_info_illustration.gif | Bin 0 -> 3883 bytes .../assets/images/catalog/hc_banner_big.png | Bin 0 -> 3539 bytes .../src/assets/images/catalog/hc_big.png | Bin 0 -> 2596 bytes .../src/assets/images/catalog/hc_small.png | Bin 0 -> 2208 bytes .../src/assets/images/catalog/paint-icon.png | Bin 0 -> 262 bytes .../assets/images/catalog/target-price.png | Bin 0 -> 3562 bytes .../src/assets/images/catalog/vip.png | Bin 0 -> 521 bytes .../images/chat/chatbubbles/bubble_0.png | Bin 0 -> 5025 bytes .../chatbubbles/bubble_0_1_33_34_pointer.png | Bin 0 -> 127 bytes .../chat/chatbubbles/bubble_0_transparent.png | Bin 0 -> 256 bytes .../images/chat/chatbubbles/bubble_1.png | Bin 0 -> 449 bytes .../images/chat/chatbubbles/bubble_10.png | Bin 0 -> 778 bytes .../chat/chatbubbles/bubble_10_pointer.png | Bin 0 -> 138 bytes .../images/chat/chatbubbles/bubble_11.png | Bin 0 -> 242 bytes .../chat/chatbubbles/bubble_11_pointer.png | Bin 0 -> 104 bytes .../images/chat/chatbubbles/bubble_12.png | Bin 0 -> 242 bytes .../chat/chatbubbles/bubble_12_pointer.png | Bin 0 -> 104 bytes .../images/chat/chatbubbles/bubble_13.png | Bin 0 -> 397 bytes .../chat/chatbubbles/bubble_13_pointer.png | Bin 0 -> 105 bytes .../images/chat/chatbubbles/bubble_14.png | Bin 0 -> 234 bytes .../chat/chatbubbles/bubble_14_pointer.png | Bin 0 -> 105 bytes .../images/chat/chatbubbles/bubble_15.png | Bin 0 -> 247 bytes .../chat/chatbubbles/bubble_15_pointer.png | Bin 0 -> 105 bytes .../images/chat/chatbubbles/bubble_16.png | Bin 0 -> 716 bytes .../chat/chatbubbles/bubble_16_pointer.png | Bin 0 -> 149 bytes .../images/chat/chatbubbles/bubble_17.png | Bin 0 -> 806 bytes .../chat/chatbubbles/bubble_17_pointer.png | Bin 0 -> 105 bytes .../images/chat/chatbubbles/bubble_18.png | Bin 0 -> 247 bytes .../chat/chatbubbles/bubble_18_pointer.png | Bin 0 -> 119 bytes .../images/chat/chatbubbles/bubble_19.png | Bin 0 -> 688 bytes .../chat/chatbubbles/bubble_19_20_pointer.png | Bin 0 -> 110 bytes .../images/chat/chatbubbles/bubble_2.png | Bin 0 -> 436 bytes .../images/chat/chatbubbles/bubble_20.png | Bin 0 -> 417 bytes .../images/chat/chatbubbles/bubble_21.png | Bin 0 -> 584 bytes .../chat/chatbubbles/bubble_21_pointer.png | Bin 0 -> 109 bytes .../images/chat/chatbubbles/bubble_22.png | Bin 0 -> 650 bytes .../chat/chatbubbles/bubble_22_pointer.png | Bin 0 -> 109 bytes .../images/chat/chatbubbles/bubble_23.png | Bin 0 -> 332 bytes .../chat/chatbubbles/bubble_23_37_pointer.png | Bin 0 -> 104 bytes .../images/chat/chatbubbles/bubble_24.png | Bin 0 -> 380 bytes .../chat/chatbubbles/bubble_24_pointer.png | Bin 0 -> 105 bytes .../images/chat/chatbubbles/bubble_25.png | Bin 0 -> 280 bytes .../chat/chatbubbles/bubble_25_pointer.png | Bin 0 -> 131 bytes .../images/chat/chatbubbles/bubble_26.png | Bin 0 -> 557 bytes .../chat/chatbubbles/bubble_26_pointer.png | Bin 0 -> 149 bytes .../images/chat/chatbubbles/bubble_27.png | Bin 0 -> 632 bytes .../chat/chatbubbles/bubble_27_pointer.png | Bin 0 -> 105 bytes .../images/chat/chatbubbles/bubble_28.png | Bin 0 -> 758 bytes .../chat/chatbubbles/bubble_28_pointer.png | Bin 0 -> 105 bytes .../images/chat/chatbubbles/bubble_29.png | Bin 0 -> 476 bytes .../chat/chatbubbles/bubble_29_pointer.png | Bin 0 -> 102 bytes .../chat/chatbubbles/bubble_2_31_pointer.png | Bin 0 -> 178 bytes .../images/chat/chatbubbles/bubble_3.png | Bin 0 -> 236 bytes .../images/chat/chatbubbles/bubble_30.png | Bin 0 -> 427 bytes .../chat/chatbubbles/bubble_30_pointer.png | Bin 0 -> 143 bytes .../images/chat/chatbubbles/bubble_32.png | Bin 0 -> 399 bytes .../chat/chatbubbles/bubble_32_pointer.png | Bin 0 -> 144 bytes .../images/chat/chatbubbles/bubble_33_34.png | Bin 0 -> 344 bytes .../chat/chatbubbles/bubble_33_extra.png | Bin 0 -> 542 bytes .../chat/chatbubbles/bubble_34_extra.png | Bin 0 -> 310 bytes .../images/chat/chatbubbles/bubble_35.png | Bin 0 -> 869 bytes .../chat/chatbubbles/bubble_35_pointer.png | Bin 0 -> 126 bytes .../images/chat/chatbubbles/bubble_36.png | Bin 0 -> 266 bytes .../chat/chatbubbles/bubble_36_extra.png | Bin 0 -> 239 bytes .../chat/chatbubbles/bubble_36_pointer.png | Bin 0 -> 105 bytes .../images/chat/chatbubbles/bubble_37.png | Bin 0 -> 376 bytes .../images/chat/chatbubbles/bubble_38.png | Bin 0 -> 276 bytes .../chat/chatbubbles/bubble_38_extra.png | Bin 0 -> 228 bytes .../chat/chatbubbles/bubble_38_pointer.png | Bin 0 -> 105 bytes .../chat/chatbubbles/bubble_3_pointer.png | Bin 0 -> 105 bytes .../images/chat/chatbubbles/bubble_4.png | Bin 0 -> 242 bytes .../chat/chatbubbles/bubble_4_pointer.png | Bin 0 -> 104 bytes .../images/chat/chatbubbles/bubble_5.png | Bin 0 -> 372 bytes .../chat/chatbubbles/bubble_5_pointer.png | Bin 0 -> 104 bytes .../images/chat/chatbubbles/bubble_6.png | Bin 0 -> 402 bytes .../chat/chatbubbles/bubble_6_pointer.png | Bin 0 -> 105 bytes .../images/chat/chatbubbles/bubble_7.png | Bin 0 -> 231 bytes .../chat/chatbubbles/bubble_7_pointer.png | Bin 0 -> 105 bytes .../images/chat/chatbubbles/bubble_8.png | Bin 0 -> 1206 bytes .../chat/chatbubbles/bubble_8_pointer.png | Bin 0 -> 105 bytes .../images/chat/chatbubbles/bubble_9.png | Bin 0 -> 499 bytes .../chat/chatbubbles/bubble_9_pointer.png | Bin 0 -> 146 bytes .../src/assets/images/chat/styles-icon.png | Bin 0 -> 314 bytes .../floorplaneditor/door-direction-0.png | Bin 0 -> 742 bytes .../floorplaneditor/door-direction-1.png | Bin 0 -> 738 bytes .../floorplaneditor/door-direction-2.png | Bin 0 -> 750 bytes .../floorplaneditor/door-direction-3.png | Bin 0 -> 697 bytes .../floorplaneditor/door-direction-4.png | Bin 0 -> 756 bytes .../floorplaneditor/door-direction-5.png | Bin 0 -> 754 bytes .../floorplaneditor/door-direction-6.png | Bin 0 -> 747 bytes .../floorplaneditor/door-direction-7.png | Bin 0 -> 698 bytes .../images/floorplaneditor/icon-door.png | Bin 0 -> 806 bytes .../images/floorplaneditor/icon-tile-down.png | Bin 0 -> 609 bytes .../images/floorplaneditor/icon-tile-set.png | Bin 0 -> 525 bytes .../floorplaneditor/icon-tile-unset.png | Bin 0 -> 544 bytes .../images/floorplaneditor/icon-tile-up.png | Bin 0 -> 555 bytes .../images/floorplaneditor/preview_tile.png | Bin 0 -> 146 bytes .../floorplaneditor/selected_height_icon.png | Bin 0 -> 175 bytes .../images/friends/friends-spritesheet.png | Bin 0 -> 3494 bytes .../src/assets/images/friends/icon-accept.png | Bin 0 -> 174 bytes .../src/assets/images/friends/icon-add.png | Bin 0 -> 205 bytes .../src/assets/images/friends/icon-bobba.png | Bin 0 -> 169 bytes .../src/assets/images/friends/icon-chat.png | Bin 0 -> 199 bytes .../src/assets/images/friends/icon-deny.png | Bin 0 -> 173 bytes .../src/assets/images/friends/icon-follow.png | Bin 0 -> 162 bytes .../images/friends/icon-friendbar-chat.png | Bin 0 -> 1740 bytes .../images/friends/icon-friendbar-visit.png | Bin 0 -> 2150 bytes .../src/assets/images/friends/icon-heart.png | Bin 0 -> 201 bytes .../images/friends/icon-new-message.png | Bin 0 -> 219 bytes .../src/assets/images/friends/icon-none.png | Bin 0 -> 177 bytes .../images/friends/icon-profile-sm-hover.png | Bin 0 -> 264 bytes .../assets/images/friends/icon-profile-sm.png | Bin 0 -> 257 bytes .../assets/images/friends/icon-profile.png | Bin 0 -> 1819 bytes .../src/assets/images/friends/icon-smile.png | Bin 0 -> 205 bytes .../assets/images/friends/icon-warning.png | Bin 0 -> 225 bytes .../friends/messenger_notification_icon.png | Bin 0 -> 164 bytes .../assets/images/gamecenter/selectedIcon.png | Bin 0 -> 248 bytes .../src/assets/images/gift/gift_tag.png | Bin 0 -> 795 bytes .../src/assets/images/gift/incognito.png | Bin 0 -> 1057 bytes .../assets/images/groups/creator_images.png | Bin 0 -> 7966 bytes .../src/assets/images/groups/creator_tabs.png | Bin 0 -> 1215 bytes .../groups/icons/group_decorate_icon.png | Bin 0 -> 311 bytes .../images/groups/icons/group_favorite.png | Bin 0 -> 198 bytes .../images/groups/icons/group_icon_admin.png | Bin 0 -> 203 bytes .../groups/icons/group_icon_big_admin.png | Bin 0 -> 429 bytes .../groups/icons/group_icon_big_member.png | Bin 0 -> 447 bytes .../groups/icons/group_icon_big_owner.png | Bin 0 -> 460 bytes .../groups/icons/group_icon_not_admin.png | Bin 0 -> 198 bytes .../groups/icons/group_icon_small_owner.png | Bin 0 -> 215 bytes .../images/groups/icons/group_notfavorite.png | Bin 0 -> 175 bytes .../images/groups/icons/grouptype_icon_0.png | Bin 0 -> 542 bytes .../images/groups/icons/grouptype_icon_1.png | Bin 0 -> 349 bytes .../images/groups/icons/grouptype_icon_2.png | Bin 0 -> 373 bytes .../src/assets/images/groups/no-group-1.png | Bin 0 -> 5007 bytes .../src/assets/images/groups/no-group-2.png | Bin 0 -> 4401 bytes .../src/assets/images/groups/no-group-3.png | Bin 0 -> 4566 bytes .../images/groups/no-group-spritesheet.png | Bin 0 -> 29386 bytes .../guide-tool/guide_tool_duty_switch.png | Bin 0 -> 537 bytes .../guide-tool/guide_tool_info_icon.png | Bin 0 -> 184 bytes .../src/assets/images/hc-center/benefits.png | Bin 0 -> 14475 bytes .../src/assets/images/hc-center/clock.png | Bin 0 -> 267 bytes .../src/assets/images/hc-center/hc_logo.gif | Bin 0 -> 1251 bytes .../src/assets/images/hc-center/payday.png | Bin 0 -> 721 bytes .../src/assets/images/help/help_index.png | Bin 0 -> 2569 bytes .../src/assets/images/icons/arrows.png | Bin 0 -> 222 bytes .../images/icons/camera-colormatrix.png | Bin 0 -> 249 bytes .../assets/images/icons/camera-composite.png | Bin 0 -> 204 bytes .../src/assets/images/icons/camera-small.png | Bin 0 -> 296 bytes .../src/assets/images/icons/chat-history.png | Bin 0 -> 574 bytes .../src/assets/images/icons/close.png | Bin 0 -> 1163 bytes apps/frontend/src/assets/images/icons/cog.png | Bin 0 -> 448 bytes .../frontend/src/assets/images/icons/help.png | Bin 0 -> 233 bytes .../src/assets/images/icons/house-small.png | Bin 0 -> 361 bytes .../src/assets/images/icons/icon_cog.png | Bin 0 -> 218 bytes .../src/assets/images/icons/like-room.png | Bin 0 -> 481 bytes .../src/assets/images/icons/loading-icon.png | Bin 0 -> 222 bytes .../src/assets/images/icons/room-link.png | Bin 0 -> 322 bytes .../assets/images/icons/sign-exclamation.png | Bin 0 -> 236 bytes .../src/assets/images/icons/sign-heart.png | Bin 0 -> 256 bytes .../src/assets/images/icons/sign-red.png | Bin 0 -> 324 bytes .../src/assets/images/icons/sign-skull.png | Bin 0 -> 199 bytes .../src/assets/images/icons/sign-smile.png | Bin 0 -> 288 bytes .../src/assets/images/icons/sign-soccer.png | Bin 0 -> 641 bytes .../src/assets/images/icons/sign-yellow.png | Bin 0 -> 304 bytes .../src/assets/images/icons/small-room.png | Bin 0 -> 413 bytes .../src/assets/images/icons/tickets.png | Bin 0 -> 143 bytes .../frontend/src/assets/images/icons/user.png | Bin 0 -> 881 bytes .../src/assets/images/icons/zoom-less.png | Bin 0 -> 252 bytes .../src/assets/images/icons/zoom-more.png | Bin 0 -> 475 bytes .../images/infostand/bot_background.png | Bin 0 -> 811 bytes .../images/infostand/countown-timer.png | Bin 0 -> 219 bytes .../assets/images/infostand/disk-creator.png | Bin 0 -> 189 bytes .../src/assets/images/infostand/disk-icon.png | Bin 0 -> 240 bytes .../assets/images/infostand/pencil-icon.png | Bin 0 -> 288 bytes .../assets/images/infostand/rarity-level.png | Bin 0 -> 273 bytes .../src/assets/images/inventory/empty.png | Bin 0 -> 6377 bytes .../assets/images/inventory/rarity-level.png | Bin 0 -> 131 bytes .../images/inventory/trading/locked-icon.png | Bin 0 -> 309 bytes .../inventory/trading/unlocked-icon.png | Bin 0 -> 332 bytes .../loading/connecting-duck-spritesheet.png | Bin 0 -> 34720 bytes .../images/loading/connecting_duck_01.png | Bin 0 -> 3985 bytes .../images/loading/connecting_duck_02.png | Bin 0 -> 4923 bytes .../images/loading/connecting_duck_03.png | Bin 0 -> 5842 bytes .../images/loading/connecting_duck_04.png | Bin 0 -> 3270 bytes .../images/loading/connecting_duck_05.png | Bin 0 -> 4558 bytes .../images/loading/connecting_duck_06.png | Bin 0 -> 5290 bytes .../images/loading/connecting_duck_07.png | Bin 0 -> 2731 bytes .../assets/images/loading/progress_habbos.gif | Bin 0 -> 11805 bytes .../src/assets/images/modtool/chatlog.gif | Bin 0 -> 109 bytes .../src/assets/images/modtool/key.gif | Bin 0 -> 214 bytes .../src/assets/images/modtool/m_icon.png | Bin 0 -> 299 bytes .../src/assets/images/modtool/reports.png | Bin 0 -> 3551 bytes .../src/assets/images/modtool/room.gif | Bin 0 -> 169 bytes .../src/assets/images/modtool/room.png | Bin 0 -> 3102 bytes .../src/assets/images/modtool/user.gif | Bin 0 -> 1130 bytes .../src/assets/images/modtool/wrench.gif | Bin 0 -> 117 bytes .../chain_mysterybox_box_overlay.png | Bin 0 -> 369 bytes .../assets/images/mysterybox/key_overlay.png | Bin 0 -> 260 bytes .../assets/images/mysterybox/mystery_box.png | Bin 0 -> 514 bytes .../images/mysterybox/mystery_box_key.png | Bin 0 -> 487 bytes .../mysterytrophy/frank_mystery_trophy.png | Bin 0 -> 1653 bytes .../assets/images/navigator/icons/info.png | Bin 0 -> 256 bytes .../images/navigator/icons/room_group.png | Bin 0 -> 191 bytes .../images/navigator/icons/room_invisible.png | Bin 0 -> 173 bytes .../images/navigator/icons/room_locked.png | Bin 0 -> 221 bytes .../images/navigator/icons/room_password.png | Bin 0 -> 188 bytes .../images/navigator/models/model_0.png | Bin 0 -> 4027 bytes .../images/navigator/models/model_1.png | Bin 0 -> 956 bytes .../images/navigator/models/model_2.png | Bin 0 -> 920 bytes .../images/navigator/models/model_3.png | Bin 0 -> 581 bytes .../images/navigator/models/model_4.png | Bin 0 -> 709 bytes .../images/navigator/models/model_5.png | Bin 0 -> 1730 bytes .../images/navigator/models/model_6.png | Bin 0 -> 2147 bytes .../images/navigator/models/model_7.png | Bin 0 -> 2368 bytes .../images/navigator/models/model_8.png | Bin 0 -> 2123 bytes .../images/navigator/models/model_9.png | Bin 0 -> 1739 bytes .../images/navigator/models/model_a.png | Bin 0 -> 3279 bytes .../images/navigator/models/model_b.png | Bin 0 -> 3254 bytes .../images/navigator/models/model_c.png | Bin 0 -> 3102 bytes .../images/navigator/models/model_d.png | Bin 0 -> 3293 bytes .../images/navigator/models/model_e.png | Bin 0 -> 3267 bytes .../images/navigator/models/model_f.png | Bin 0 -> 3294 bytes .../images/navigator/models/model_g.png | Bin 0 -> 3395 bytes .../images/navigator/models/model_h.png | Bin 0 -> 3354 bytes .../images/navigator/models/model_i.png | Bin 0 -> 3690 bytes .../images/navigator/models/model_j.png | Bin 0 -> 3603 bytes .../images/navigator/models/model_k.png | Bin 0 -> 3785 bytes .../images/navigator/models/model_l.png | Bin 0 -> 3717 bytes .../images/navigator/models/model_m.png | Bin 0 -> 3559 bytes .../images/navigator/models/model_n.png | Bin 0 -> 3778 bytes .../images/navigator/models/model_o.png | Bin 0 -> 3598 bytes .../images/navigator/models/model_p.png | Bin 0 -> 3751 bytes .../images/navigator/models/model_q.png | Bin 0 -> 3874 bytes .../images/navigator/models/model_r.png | Bin 0 -> 4231 bytes .../navigator/models/model_snowwar1.png | Bin 0 -> 19090 bytes .../navigator/models/model_snowwar2.png | Bin 0 -> 19090 bytes .../images/navigator/models/model_t.png | Bin 0 -> 4329 bytes .../images/navigator/models/model_u.png | Bin 0 -> 4126 bytes .../images/navigator/models/model_v.png | Bin 0 -> 4416 bytes .../images/navigator/models/model_w.png | Bin 0 -> 4331 bytes .../images/navigator/models/model_x.png | Bin 0 -> 4103 bytes .../images/navigator/models/model_y.png | Bin 0 -> 4245 bytes .../images/navigator/models/model_z.png | Bin 0 -> 4087 bytes .../navigator/thumbnail_placeholder.png | Bin 0 -> 1801 bytes .../src/assets/images/nitro/nitro-dark.svg | 43 + .../src/assets/images/nitro/nitro-light.svg | 43 + .../src/assets/images/nitro/nitro-n-dark.svg | 28 + .../src/assets/images/nitro/nitro-n-light.svg | 29 + .../src/assets/images/notifications/frank.gif | Bin 0 -> 1204 bytes .../assets/images/pets/pet-package/gnome.png | Bin 0 -> 1600 bytes .../pets/pet-package/leprechaun_box.png | Bin 0 -> 1520 bytes .../images/pets/pet-package/petbox_epic.png | Bin 0 -> 2180 bytes .../images/pets/pet-package/pterosaur_egg.png | Bin 0 -> 1631 bytes .../images/pets/pet-package/val11_present.png | Bin 0 -> 2720 bytes .../pets/pet-package/velociraptor_egg.png | Bin 0 -> 1511 bytes .../assets/images/prize/prize_background.png | Bin 0 -> 5861 bytes .../assets/images/profile/icons/offline.png | Bin 0 -> 306 bytes .../assets/images/profile/icons/online.gif | Bin 0 -> 666 bytes .../src/assets/images/profile/icons/tick.png | Bin 0 -> 129 bytes .../room_spectator_bottom_left.png | Bin 0 -> 461 bytes .../room_spectator_bottom_right.png | Bin 0 -> 456 bytes .../room_spectator_middle_bottom.png | Bin 0 -> 98 bytes .../room_spectator_middle_left.png | Bin 0 -> 94 bytes .../room_spectator_middle_right.png | Bin 0 -> 94 bytes .../room_spectator_middle_top.png | Bin 0 -> 95 bytes .../room_spectator_top_left.png | Bin 0 -> 408 bytes .../room_spectator_top_right.png | Bin 0 -> 412 bytes .../avatar-info/preview-background.png | Bin 0 -> 7756 bytes .../images/room-widgets/camera-widget/btn.png | Bin 0 -> 830 bytes .../room-widgets/camera-widget/btn_down.png | Bin 0 -> 1000 bytes .../room-widgets/camera-widget/btn_hi.png | Bin 0 -> 935 bytes .../room-widgets/camera-widget/cam_bg.png | Bin 0 -> 2166 bytes .../camera-widget/camera-spritesheet.png | Bin 0 -> 15640 bytes .../room-widgets/camera-widget/viewfinder.png | Bin 0 -> 959 bytes .../dimmer-widget/dimmer_banner.png | Bin 0 -> 1041 bytes .../engraving-lock-spritesheet.png | Bin 0 -> 41062 bytes .../exchange-credit/exchange-credit-image.png | Bin 0 -> 9507 bytes .../monsterplant-preview.png | Bin 0 -> 2048 bytes .../mannequin-spritesheet.png | Bin 0 -> 5719 bytes .../room-widgets/playlist-editor/disk_2.png | Bin 0 -> 680 bytes .../playlist-editor/disk_image.png | Bin 0 -> 1441 bytes .../room-widgets/playlist-editor/move.png | Bin 0 -> 164 bytes .../playlist-editor/pause-btn.png | Bin 0 -> 111 bytes .../room-widgets/playlist-editor/pause.png | Bin 0 -> 114 bytes .../room-widgets/playlist-editor/playing.png | Bin 0 -> 309 bytes .../room-widgets/playlist-editor/preview.png | Bin 0 -> 147 bytes .../stickie-widget/stickie-blue.png | Bin 0 -> 401 bytes .../stickie-widget/stickie-christmas.png | Bin 0 -> 1272 bytes .../stickie-widget/stickie-close.png | Bin 0 -> 189 bytes .../stickie-widget/stickie-dreams.png | Bin 0 -> 26556 bytes .../stickie-widget/stickie-green.png | Bin 0 -> 401 bytes .../stickie-widget/stickie-heart.png | Bin 0 -> 945 bytes .../stickie-widget/stickie-juninas.png | Bin 0 -> 28162 bytes .../stickie-widget/stickie-pink.png | Bin 0 -> 758 bytes .../stickie-widget/stickie-shakesp.png | Bin 0 -> 5036 bytes .../stickie-widget/stickie-spritesheet.png | Bin 0 -> 5828 bytes .../stickie-widget/stickie-trash.png | Bin 0 -> 170 bytes .../stickie-widget/stickie-yellow.png | Bin 0 -> 401 bytes .../thumbnail-camera-spritesheet.png | Bin 0 -> 883 bytes .../trophy-widget/trophy-spritesheet.png | Bin 0 -> 6595 bytes .../wordquiz-widget/thumbs-down-small.png | Bin 0 -> 145 bytes .../wordquiz-widget/thumbs-down.png | Bin 0 -> 182 bytes .../wordquiz-widget/thumbs-up-small.png | Bin 0 -> 135 bytes .../wordquiz-widget/thumbs-up.png | Bin 0 -> 174 bytes .../room-widgets/youtube-widget/next.png | Bin 0 -> 164 bytes .../room-widgets/youtube-widget/prev.png | Bin 0 -> 249 bytes .../images/stackhelper/slider-background.png | Bin 0 -> 695 bytes .../images/stackhelper/slider-pointer.png | Bin 0 -> 373 bytes .../src/assets/images/toolbar/arrow.png | Bin 0 -> 14533 bytes .../assets/images/toolbar/friend-search.png | Bin 0 -> 2213 bytes .../images/toolbar/icons/buildersclub.png | Bin 0 -> 576 bytes .../assets/images/toolbar/icons/camera.png | Bin 0 -> 1732 bytes .../assets/images/toolbar/icons/catalog.png | Bin 0 -> 1202 bytes .../images/toolbar/icons/friend_all.png | Bin 0 -> 648 bytes .../images/toolbar/icons/friend_head.png | Bin 0 -> 680 bytes .../images/toolbar/icons/friend_search.png | Bin 0 -> 15241 bytes .../src/assets/images/toolbar/icons/game.png | Bin 0 -> 740 bytes .../src/assets/images/toolbar/icons/habbo.png | Bin 0 -> 1304 bytes .../src/assets/images/toolbar/icons/house.png | Bin 0 -> 399 bytes .../assets/images/toolbar/icons/inventory.png | Bin 0 -> 1973 bytes .../assets/images/toolbar/icons/joinroom.png | Bin 0 -> 1084 bytes .../toolbar/icons/me-menu/achievements.png | Bin 0 -> 2306 bytes .../images/toolbar/icons/me-menu/clothing.png | Bin 0 -> 2255 bytes .../images/toolbar/icons/me-menu/cog.png | Bin 0 -> 657 bytes .../images/toolbar/icons/me-menu/forums.png | Bin 0 -> 416 bytes .../toolbar/icons/me-menu/helper-tool.png | Bin 0 -> 309 bytes .../images/toolbar/icons/me-menu/my-rooms.png | Bin 0 -> 2299 bytes .../images/toolbar/icons/me-menu/profile.png | Bin 0 -> 468 bytes .../images/toolbar/icons/me-menu/rooms.png | Bin 0 -> 1465 bytes .../images/toolbar/icons/me-menu/talents.png | Bin 0 -> 397 bytes .../assets/images/toolbar/icons/message.png | Bin 0 -> 1099 bytes .../images/toolbar/icons/message_unsee.gif | Bin 0 -> 1511 bytes .../assets/images/toolbar/icons/modtools.png | Bin 0 -> 404 bytes .../src/assets/images/toolbar/icons/rooms.png | Bin 0 -> 1465 bytes .../images/toolbar/icons/sendmessage.png | Bin 0 -> 15647 bytes .../images/unique/catalog-info-amount-bg.png | Bin 0 -> 757 bytes .../images/unique/catalog-info-sold-out.png | Bin 0 -> 1100 bytes .../assets/images/unique/grid-bg-glass.png | Bin 0 -> 213 bytes .../assets/images/unique/grid-bg-sold-out.png | Bin 0 -> 332 bytes .../src/assets/images/unique/grid-bg.png | Bin 0 -> 201 bytes .../assets/images/unique/grid-count-bg.png | Bin 0 -> 155 bytes .../unique/inventory-info-amount-bg.png | Bin 0 -> 521 bytes .../src/assets/images/unique/numbers.png | Bin 0 -> 146 bytes .../images/wired/card-action-corners.png | Bin 0 -> 1827 bytes .../src/assets/images/wired/icon_action.png | Bin 0 -> 171 bytes .../assets/images/wired/icon_condition.png | Bin 0 -> 173 bytes .../src/assets/images/wired/icon_trigger.png | Bin 0 -> 169 bytes .../assets/images/wired/icon_wired_around.png | Bin 0 -> 197 bytes .../images/wired/icon_wired_left_right.png | Bin 0 -> 172 bytes .../images/wired/icon_wired_north_east.png | Bin 0 -> 157 bytes .../images/wired/icon_wired_north_west.png | Bin 0 -> 166 bytes .../wired/icon_wired_rotate_clockwise.png | Bin 0 -> 146 bytes .../icon_wired_rotate_counter_clockwise.png | Bin 0 -> 148 bytes .../images/wired/icon_wired_south_east.png | Bin 0 -> 171 bytes .../images/wired/icon_wired_south_west.png | Bin 0 -> 169 bytes .../images/wired/icon_wired_up_down.png | Bin 0 -> 153 bytes .../assets/styles/bootstrap/_accordion.scss | 118 ++ .../src/assets/styles/bootstrap/_alert.scss | 57 + .../src/assets/styles/bootstrap/_badge.scss | 29 + .../assets/styles/bootstrap/_breadcrumb.scss | 28 + .../styles/bootstrap/_button-group.scss | 139 ++ .../src/assets/styles/bootstrap/_buttons.scss | 116 ++ .../src/assets/styles/bootstrap/_card.scss | 216 +++ .../assets/styles/bootstrap/_carousel.scss | 229 +++ .../src/assets/styles/bootstrap/_close.scss | 40 + .../assets/styles/bootstrap/_containers.scss | 41 + .../assets/styles/bootstrap/_dropdown.scss | 240 +++ .../src/assets/styles/bootstrap/_forms.scss | 9 + .../assets/styles/bootstrap/_functions.scss | 296 +++ .../src/assets/styles/bootstrap/_grid.scss | 33 + .../src/assets/styles/bootstrap/_helpers.scss | 9 + .../src/assets/styles/bootstrap/_images.scss | 42 + .../assets/styles/bootstrap/_list-group.scss | 174 ++ .../src/assets/styles/bootstrap/_mixins.scss | 43 + .../src/assets/styles/bootstrap/_modal.scss | 209 ++ .../src/assets/styles/bootstrap/_nav.scss | 160 ++ .../src/assets/styles/bootstrap/_navbar.scss | 335 ++++ .../assets/styles/bootstrap/_offcanvas.scss | 83 + .../assets/styles/bootstrap/_pagination.scss | 64 + .../styles/bootstrap/_placeholders.scss | 51 + .../src/assets/styles/bootstrap/_popover.scss | 158 ++ .../assets/styles/bootstrap/_progress.scss | 48 + .../src/assets/styles/bootstrap/_reboot.scss | 609 ++++++ .../src/assets/styles/bootstrap/_root.scss | 53 + .../assets/styles/bootstrap/_spinners.scss | 69 + .../src/assets/styles/bootstrap/_tables.scss | 151 ++ .../src/assets/styles/bootstrap/_toasts.scss | 51 + .../src/assets/styles/bootstrap/_tooltip.scss | 115 ++ .../assets/styles/bootstrap/_transitions.scss | 27 + .../src/assets/styles/bootstrap/_type.scss | 104 + .../assets/styles/bootstrap/_utilities.scss | 638 +++++++ .../assets/styles/bootstrap/_variables.scss | 1683 +++++++++++++++++ .../styles/bootstrap/bootstrap-grid.scss | 65 + .../styles/bootstrap/bootstrap-reboot.scss | 15 + .../styles/bootstrap/bootstrap-utilities.scss | 18 + .../assets/styles/bootstrap/bootstrap.scss | 53 + .../bootstrap/forms/_floating-labels.scss | 63 + .../styles/bootstrap/forms/_form-check.scss | 152 ++ .../styles/bootstrap/forms/_form-control.scss | 219 +++ .../styles/bootstrap/forms/_form-range.scss | 91 + .../styles/bootstrap/forms/_form-select.scss | 70 + .../styles/bootstrap/forms/_form-text.scss | 11 + .../styles/bootstrap/forms/_input-group.scss | 121 ++ .../styles/bootstrap/forms/_labels.scss | 36 + .../styles/bootstrap/forms/_validation.scss | 12 + .../styles/bootstrap/helpers/_clearfix.scss | 3 + .../bootstrap/helpers/_colored-links.scss | 12 + .../styles/bootstrap/helpers/_position.scss | 30 + .../styles/bootstrap/helpers/_ratio.scss | 26 + .../styles/bootstrap/helpers/_stacks.scss | 15 + .../bootstrap/helpers/_stretched-link.scss | 15 + .../bootstrap/helpers/_text-truncation.scss | 7 + .../bootstrap/helpers/_visually-hidden.scss | 8 + .../assets/styles/bootstrap/helpers/_vr.scss | 8 + .../styles/bootstrap/mixins/_alert.scss | 11 + .../styles/bootstrap/mixins/_backdrop.scss | 14 + .../bootstrap/mixins/_border-radius.scss | 78 + .../styles/bootstrap/mixins/_box-shadow.scss | 18 + .../styles/bootstrap/mixins/_breakpoints.scss | 127 ++ .../styles/bootstrap/mixins/_buttons.scss | 133 ++ .../styles/bootstrap/mixins/_caret.scss | 64 + .../styles/bootstrap/mixins/_clearfix.scss | 9 + .../bootstrap/mixins/_color-scheme.scss | 7 + .../styles/bootstrap/mixins/_container.scss | 9 + .../styles/bootstrap/mixins/_deprecate.scss | 10 + .../styles/bootstrap/mixins/_forms.scss | 144 ++ .../styles/bootstrap/mixins/_gradients.scss | 47 + .../assets/styles/bootstrap/mixins/_grid.scss | 150 ++ .../styles/bootstrap/mixins/_image.scss | 16 + .../styles/bootstrap/mixins/_list-group.scss | 24 + .../styles/bootstrap/mixins/_lists.scss | 7 + .../styles/bootstrap/mixins/_pagination.scss | 31 + .../styles/bootstrap/mixins/_reset-text.scss | 17 + .../styles/bootstrap/mixins/_resize.scss | 6 + .../bootstrap/mixins/_table-variants.scss | 21 + .../bootstrap/mixins/_text-truncate.scss | 8 + .../styles/bootstrap/mixins/_transition.scss | 26 + .../styles/bootstrap/mixins/_utilities.scss | 89 + .../bootstrap/mixins/_visually-hidden.scss | 29 + .../styles/bootstrap/utilities/_api.scss | 47 + .../assets/styles/bootstrap/vendor/_rfs.scss | 354 ++++ apps/frontend/src/assets/styles/fonts.scss | 4 + apps/frontend/src/assets/styles/icons.scss | 629 ++++++ apps/frontend/src/assets/styles/index.scss | 26 + .../src/assets/styles/scrollbars.scss | 53 + apps/frontend/src/assets/styles/slider.scss | 54 + apps/frontend/src/assets/styles/utils.scss | 134 ++ .../frontend/src/assets/webfonts/Ubuntu-C.ttf | Bin 0 -> 369840 bytes .../frontend/src/assets/webfonts/Ubuntu-b.ttf | Bin 0 -> 152496 bytes .../frontend/src/assets/webfonts/Ubuntu-i.ttf | Bin 0 -> 160792 bytes .../src/assets/webfonts/Ubuntu-ib.ttf | Bin 0 -> 148524 bytes .../frontend/src/assets/webfonts/Ubuntu-m.ttf | Bin 0 -> 140976 bytes apps/frontend/src/assets/webfonts/Ubuntu.ttf | Bin 0 -> 155908 bytes apps/frontend/src/common/AutoGrid.tsx | 28 + apps/frontend/src/common/Base.tsx | 84 + apps/frontend/src/common/Button.tsx | 35 + apps/frontend/src/common/ButtonGroup.tsx | 22 + apps/frontend/src/common/Column.tsx | 46 + apps/frontend/src/common/Flex.tsx | 50 + apps/frontend/src/common/FormGroup.tsx | 22 + apps/frontend/src/common/Grid.tsx | 63 + apps/frontend/src/common/GridContext.tsx | 17 + apps/frontend/src/common/HorizontalRule.tsx | 38 + apps/frontend/src/common/InfiniteScroll.tsx | 63 + apps/frontend/src/common/Text.tsx | 63 + .../src/common/card/NitroCardContentView.tsx | 18 + .../src/common/card/NitroCardContext.tsx | 17 + .../src/common/card/NitroCardHeaderView.tsx | 48 + .../common/card/NitroCardSubHeaderView.tsx | 23 + .../src/common/card/NitroCardView.scss | 248 +++ .../src/common/card/NitroCardView.tsx | 64 + .../accordion/NitroCardAccordionContext.tsx | 21 + .../accordion/NitroCardAccordionItemView.tsx | 18 + .../accordion/NitroCardAccordionSetView.tsx | 84 + .../card/accordion/NitroCardAccordionView.tsx | 25 + .../src/common/card/accordion/index.ts | 4 + apps/frontend/src/common/card/index.ts | 7 + .../card/tabs/NitroCardTabsItemView.tsx | 35 + .../common/card/tabs/NitroCardTabsView.tsx | 22 + apps/frontend/src/common/card/tabs/index.ts | 2 + apps/frontend/src/common/classNames.ts | 1 + .../draggable-window/DraggableWindow.tsx | 270 +++ .../DraggableWindowPosition.ts | 7 + .../src/common/draggable-window/index.ts | 2 + apps/frontend/src/common/index.scss | 561 ++++++ apps/frontend/src/common/index.ts | 22 + .../common/layout/LayoutAvatarImageView.tsx | 89 + .../common/layout/LayoutBackgroundImage.tsx | 23 + .../common/layout/LayoutBadgeImageView.tsx | 103 + .../common/layout/LayoutCounterTimeView.tsx | 43 + .../src/common/layout/LayoutCurrencyIcon.tsx | 44 + .../layout/LayoutFurniIconImageView.tsx | 17 + .../common/layout/LayoutFurniImageView.tsx | 82 + .../src/common/layout/LayoutGiftTagView.tsx | 39 + .../src/common/layout/LayoutGridItem.tsx | 75 + .../src/common/layout/LayoutImage.tsx | 13 + .../src/common/layout/LayoutItemCountView.tsx | 28 + .../layout/LayoutLoadingSpinnerView.tsx | 15 + .../common/layout/LayoutMiniCameraView.tsx | 44 + .../layout/LayoutNotificationAlertView.tsx | 35 + .../layout/LayoutNotificationBubbleView.tsx | 52 + .../src/common/layout/LayoutPetImageView.tsx | 121 ++ .../layout/LayoutPrizeProductImageView.tsx | 30 + .../src/common/layout/LayoutProgressBar.tsx | 32 + .../common/layout/LayoutRarityLevelView.tsx | 28 + .../common/layout/LayoutRoomPreviewerView.tsx | 97 + .../common/layout/LayoutRoomThumbnailView.tsx | 37 + .../src/common/layout/LayoutTrophyView.tsx | 39 + .../src/common/layout/UserProfileIconView.tsx | 29 + apps/frontend/src/common/layout/index.ts | 23 + .../LayoutLimitedEditionCompactPlateView.tsx | 35 + .../LayoutLimitedEditionCompletePlateView.tsx | 41 + .../LayoutLimitedEditionStyledNumberView.tsx | 18 + .../common/layout/limited-edition/index.ts | 3 + .../transitions/TransitionAnimation.tsx | 52 + .../transitions/TransitionAnimationStyles.ts | 136 ++ .../transitions/TransitionAnimationTypes.ts | 11 + apps/frontend/src/common/transitions/index.ts | 3 + .../src/common/types/AlignItemType.ts | 1 + .../src/common/types/AlignSelfType.ts | 1 + .../src/common/types/ButtonSizeType.ts | 1 + .../src/common/types/ColorVariantType.ts | 1 + .../src/common/types/ColumnSizesType.ts | 1 + apps/frontend/src/common/types/DisplayType.ts | 1 + apps/frontend/src/common/types/FloatType.ts | 1 + .../frontend/src/common/types/FontSizeType.ts | 1 + .../src/common/types/FontWeightType.ts | 1 + .../src/common/types/JustifyContentType.ts | 1 + .../frontend/src/common/types/OverflowType.ts | 1 + .../frontend/src/common/types/PositionType.ts | 1 + apps/frontend/src/common/types/SpacingType.ts | 1 + .../src/common/types/TextAlignType.ts | 1 + apps/frontend/src/common/types/index.ts | 14 + .../common/utils/CreateTransitionToIcon.ts | 14 + .../src/common/utils/FriendlyTimeView.tsx | 28 + apps/frontend/src/common/utils/index.ts | 2 + .../achievements/AchievementsView.scss | 15 + .../achievements/AchievementsView.tsx | 72 + .../views/AchievementBadgeView.tsx | 19 + .../views/AchievementCategoryView.tsx | 37 + .../views/AchievementDetailsView.tsx | 53 + .../AchievementListItemView.tsx | 24 + .../achievement-list/AchievementListView.tsx | 20 + .../views/achievement-list/index.ts | 2 + .../AchievementsCategoryListItemView.tsx | 31 + .../AchievementsCategoryListView.tsx | 22 + .../achievements/views/category-list/index.ts | 2 + .../components/achievements/views/index.ts | 5 + .../avatar-editor/AvatarEditorView.scss | 336 ++++ .../avatar-editor/AvatarEditorView.tsx | 316 ++++ .../views/AvatarEditorFigurePreviewView.tsx | 55 + .../avatar-editor/views/AvatarEditorIcon.tsx | 30 + .../views/AvatarEditorModelView.tsx | 88 + .../views/AvatarEditorWardrobeView.tsx | 79 + .../AvatarEditorFigureSetItemView.tsx | 35 + .../figure-set/AvatarEditorFigureSetView.tsx | 44 + .../AvatarEditorPaletteSetItemView.tsx | 32 + .../AvatarEditorPaletteSetView.tsx | 41 + .../components/camera/CameraWidgetView.scss | 133 ++ .../components/camera/CameraWidgetView.tsx | 97 + .../camera/views/CameraWidgetCaptureView.tsx | 90 + .../camera/views/CameraWidgetCheckoutView.tsx | 159 ++ .../views/CameraWidgetShowPhotoView.tsx | 71 + .../views/editor/CameraWidgetEditorView.tsx | 229 +++ .../CameraWidgetEffectListItemView.tsx | 40 + .../CameraWidgetEffectListView.tsx | 31 + .../components/campaign/CalendarItemView.tsx | 52 + .../src/components/campaign/CalendarView.tsx | 143 ++ .../src/components/campaign/CampaignView.scss | 71 + .../src/components/campaign/CampaignView.tsx | 101 + .../src/components/catalog/CatalogView.scss | 158 ++ .../src/components/catalog/CatalogView.tsx | 111 ++ .../views/CatalogPurchaseConfirmView.tsx | 10 + .../catalog-header/CatalogHeaderView.tsx | 26 + .../views/catalog-icon/CatalogIconView.tsx | 20 + .../CatalogRoomPreviewerView.tsx | 42 + .../catalog/views/gift/CatalogGiftView.tsx | 289 +++ .../navigation/CatalogNavigationItemView.tsx | 35 + .../navigation/CatalogNavigationSetView.tsx | 25 + .../navigation/CatalogNavigationView.tsx | 34 + .../page/common/CatalogGridOfferView.tsx | 59 + .../page/common/CatalogRedeemVoucherView.tsx | 60 + .../views/page/common/CatalogSearchView.tsx | 96 + .../views/page/layout/CatalogLayout.types.ts | 7 + .../layout/CatalogLayoutBadgeDisplayView.tsx | 54 + .../layout/CatalogLayoutColorGroupingView.tsx | 176 ++ .../page/layout/CatalogLayoutDefaultView.tsx | 61 + .../CatalogLayoutGuildCustomFurniView.tsx | 48 + .../layout/CatalogLayoutGuildForumView.tsx | 49 + .../CatalogLayoutGuildFrontpageView.tsx | 29 + .../layout/CatalogLayoutInfoLoyaltyView.tsx | 15 + .../page/layout/CatalogLayoutPets2View.tsx | 8 + .../page/layout/CatalogLayoutPets3View.tsx | 27 + .../page/layout/CatalogLayoutRoomAdsView.tsx | 113 ++ .../layout/CatalogLayoutRoomBundleView.tsx | 43 + .../layout/CatalogLayoutSingleBundleView.tsx | 43 + .../layout/CatalogLayoutSoundMachineView.tsx | 113 ++ .../page/layout/CatalogLayoutSpacesView.tsx | 48 + .../page/layout/CatalogLayoutTrophiesView.tsx | 56 + .../page/layout/CatalogLayoutVipBuyView.tsx | 191 ++ .../views/page/layout/GetCatalogLayout.tsx | 80 + .../CatalogLayoutFrontPageItemView.tsx | 37 + .../CatalogLayoutFrontpage4View.tsx | 50 + .../CatalogLayoutMarketplaceItemView.tsx | 83 + .../CatalogLayoutMarketplaceOwnItemsView.tsx | 102 + ...atalogLayoutMarketplacePublicItemsView.tsx | 161 ++ ...CatalogLayoutMarketplaceSearchFormView.tsx | 71 + .../marketplace/MarketplacePostOfferView.tsx | 121 ++ .../page/layout/pets/CatalogLayoutPetView.tsx | 243 +++ .../vip-gifts/CatalogLayoutVipGiftsView.tsx | 62 + .../page/layout/vip-gifts/VipGiftItemView.tsx | 63 + .../widgets/CatalogAddOnBadgeWidgetView.tsx | 18 + .../CatalogBadgeSelectorWidgetView.tsx | 76 + .../widgets/CatalogBundleGridWidgetView.tsx | 29 + .../CatalogFirstProductSelectorWidgetView.tsx | 16 + .../widgets/CatalogGuildBadgeWidgetView.tsx | 31 + .../CatalogGuildSelectorWidgetView.tsx | 75 + .../widgets/CatalogItemGridWidgetView.tsx | 52 + .../widgets/CatalogLimitedItemWidgetView.tsx | 19 + .../widgets/CatalogPriceDisplayWidgetView.tsx | 37 + .../widgets/CatalogPurchaseWidgetView.tsx | 164 ++ .../widgets/CatalogSimplePriceWidgetView.tsx | 21 + .../widgets/CatalogSingleViewWidgetView.tsx | 7 + .../page/widgets/CatalogSpacesWidgetView.tsx | 115 ++ .../page/widgets/CatalogSpinnerWidgetView.tsx | 46 + .../page/widgets/CatalogTotalPriceWidget.tsx | 20 + .../widgets/CatalogViewProductWidgetView.tsx | 99 + .../catalog/views/targeted-offer/Offer.scss | 27 + .../views/targeted-offer/OfferBubbleView.tsx | 16 + .../views/targeted-offer/OfferView.tsx | 32 + .../views/targeted-offer/OfferWindowView.tsx | 82 + .../chat-history/ChatHistoryView.scss | 4 + .../chat-history/ChatHistoryView.tsx | 96 + .../FloorplanEditorContext.tsx | 22 + .../floorplan-editor/FloorplanEditorView.scss | 9 + .../floorplan-editor/FloorplanEditorView.tsx | 165 ++ .../floorplan-editor/common/ActionSettings.ts | 39 + .../floorplan-editor/common/Constants.ts | 44 + .../common/ConvertMapToString.ts | 1 + .../common/FloorplanEditor.ts | 420 ++++ .../common/IFloorplanSettings.ts | 8 + .../common/IVisualizationSettings.ts | 7 + .../floorplan-editor/common/Tile.ts | 31 + .../floorplan-editor/common/Utils.ts | 53 + .../views/FloorplanCanvasView.tsx | 156 ++ .../views/FloorplanImportExportView.tsx | 55 + .../views/FloorplanOptionsView.tsx | 189 ++ .../src/components/friends/FriendsView.scss | 244 +++ .../src/components/friends/FriendsView.tsx | 21 + .../views/friends-bar/FriendBarItemView.tsx | 59 + .../views/friends-bar/FriendsBarView.tsx | 26 + .../FriendsListRemoveConfirmationView.tsx | 29 + .../FriendsListRoomInviteView.tsx | 31 + .../friends-list/FriendsListSearchView.tsx | 103 + .../views/friends-list/FriendsListView.tsx | 150 ++ .../FriendsListGroupItemView.tsx | 85 + .../FriendsListGroupView.tsx | 23 + .../FriendsListRequestItemView.tsx | 25 + .../FriendsListRequestView.tsx | 29 + .../views/messenger/FriendsMessengerView.tsx | 177 ++ .../FriendsMessengerThreadGroup.tsx | 72 + .../FriendsMessengerThreadView.tsx | 16 + .../game-center/GameCenterView.scss | 44 + .../components/game-center/GameCenterView.tsx | 50 + .../game-center/views/GameListView.tsx | 32 + .../game-center/views/GameStageView.tsx | 47 + .../components/game-center/views/GameView.tsx | 58 + .../src/components/groups/GroupView.scss | 190 ++ .../src/components/groups/GroupsView.tsx | 63 + .../groups/views/GroupBadgeCreatorView.tsx | 83 + .../groups/views/GroupCreatorView.tsx | 164 ++ .../views/GroupInformationStandaloneView.tsx | 29 + .../groups/views/GroupInformationView.tsx | 146 ++ .../groups/views/GroupManagerView.tsx | 119 ++ .../groups/views/GroupMembersView.tsx | 210 ++ .../groups/views/GroupRoomInformationView.tsx | 132 ++ .../groups/views/tabs/GroupTabBadgeView.tsx | 120 ++ .../groups/views/tabs/GroupTabColorsView.tsx | 127 ++ .../tabs/GroupTabCreatorConfirmationView.tsx | 67 + .../views/tabs/GroupTabIdentityView.tsx | 116 ++ .../views/tabs/GroupTabSettingsView.tsx | 89 + .../components/guide-tool/GuideToolView.scss | 87 + .../components/guide-tool/GuideToolView.tsx | 356 ++++ .../guide-tool/views/GuideToolAcceptView.tsx | 35 + .../guide-tool/views/GuideToolMenuView.tsx | 76 + .../guide-tool/views/GuideToolOngoingView.tsx | 130 ++ .../views/GuideToolUserCreateRequestView.tsx | 32 + .../views/GuideToolUserFeedbackView.tsx | 43 + .../views/GuideToolUserNoHelpersView.tsx | 13 + .../views/GuideToolUserPendingView.tsx | 33 + .../views/GuideToolUserSomethingWrogView.tsx | 12 + .../views/GuideToolUserThanksView.tsx | 13 + .../components/hc-center/HcCenterView.scss | 44 + .../src/components/hc-center/HcCenterView.tsx | 204 ++ .../src/components/help/HelpView.scss | 18 + .../frontend/src/components/help/HelpView.tsx | 116 ++ .../help/views/DescribeReportView.tsx | 48 + .../components/help/views/HelpIndexView.tsx | 37 + .../help/views/ReportSummaryView.tsx | 57 + .../help/views/SanctionStatusView.tsx | 75 + .../help/views/SelectReportedChatsView.tsx | 90 + .../help/views/SelectReportedUserView.tsx | 85 + .../components/help/views/SelectTopicView.tsx | 53 + .../NameChangeConfirmationView.tsx | 44 + .../views/name-change/NameChangeInitView.tsx | 19 + .../views/name-change/NameChangeInputView.tsx | 97 + .../help/views/name-change/NameChangeView.tsx | 66 + .../views/name-change/NameChangeView.types.ts | 5 + .../src/components/hotel-view/HotelView.scss | 97 + .../src/components/hotel-view/HotelView.tsx | 106 ++ .../views/widgets/GetWidgetLayout.tsx | 29 + .../views/widgets/HotelViewWidgets.scss | 4 + .../views/widgets/WidgetSlotView.tsx | 20 + .../bonus-rare/BonusRareWidgetView.scss | 10 + .../bonus-rare/BonusRareWidgetView.tsx | 42 + .../hall-of-fame-item/HallOfFameItemView.tsx | 27 + .../hall-of-fame/HallOfFameWidgetView.scss | 70 + .../hall-of-fame/HallOfFameWidgetView.tsx | 37 + .../HallOfFameWidgetView.types.ts | 5 + .../promo-article/PromoArticleWidgetView.scss | 27 + .../promo-article/PromoArticleWidgetView.tsx | 46 + .../widget-container/WidgetContainerView.scss | 9 + .../widget-container/WidgetContainerView.tsx | 39 + apps/frontend/src/components/index.scss | 26 + .../components/inventory/InventoryView.scss | 17 + .../components/inventory/InventoryView.tsx | 152 ++ .../views/InventoryCategoryEmptyView.tsx | 26 + .../views/badge/InventoryBadgeItemView.tsx | 19 + .../views/badge/InventoryBadgeView.tsx | 66 + .../views/bot/InventoryBotItemView.tsx | 43 + .../inventory/views/bot/InventoryBotView.tsx | 91 + .../furniture/InventoryFurnitureItemView.tsx | 38 + .../InventoryFurnitureSearchView.tsx | 47 + .../furniture/InventoryFurnitureView.tsx | 146 ++ .../views/furniture/InventoryTradeView.tsx | 279 +++ .../views/pet/InventoryPetItemView.tsx | 43 + .../inventory/views/pet/InventoryPetView.tsx | 90 + .../src/components/loading/LoadingView.scss | 70 + .../src/components/loading/LoadingView.tsx | 35 + .../frontend/src/components/main/MainView.tsx | 112 ++ .../components/mod-tools/ModToolsView.scss | 70 + .../src/components/mod-tools/ModToolsView.tsx | 147 ++ .../mod-tools/views/chatlog/ChatlogRecord.ts | 11 + .../mod-tools/views/chatlog/ChatlogView.tsx | 91 + .../views/room/ModToolsChatlogView.tsx | 44 + .../mod-tools/views/room/ModToolsRoomView.tsx | 117 ++ .../views/tickets/CfhChatlogView.tsx | 41 + .../views/tickets/ModToolsIssueInfoView.tsx | 86 + .../views/tickets/ModToolsMyIssuesTabView.tsx | 47 + .../tickets/ModToolsOpenIssuesTabView.tsx | 42 + .../tickets/ModToolsPickedIssuesTabView.tsx | 39 + .../views/tickets/ModToolsTicketsView.tsx | 91 + .../views/user/ModToolsUserChatlogView.tsx | 44 + .../views/user/ModToolsUserModActionView.tsx | 176 ++ .../views/user/ModToolsUserRoomVisitsView.tsx | 60 + .../user/ModToolsUserSendMessageView.tsx | 45 + .../mod-tools/views/user/ModToolsUserView.tsx | 156 ++ .../components/navigator/NavigatorView.scss | 65 + .../components/navigator/NavigatorView.tsx | 233 +++ .../views/NavigatorDoorStateView.tsx | 110 ++ .../views/NavigatorRoomCreatorView.tsx | 122 ++ .../navigator/views/NavigatorRoomInfoView.tsx | 180 ++ .../navigator/views/NavigatorRoomLinkView.tsx | 33 + .../NavigatorRoomSettingsAccessTabView.tsx | 88 + .../NavigatorRoomSettingsBasicTabView.tsx | 171 ++ .../NavigatorRoomSettingsModTabView.tsx | 118 ++ .../NavigatorRoomSettingsRightsTabView.tsx | 91 + .../NavigatorRoomSettingsView.tsx | 206 ++ .../NavigatorRoomSettingsVipChatTabView.tsx | 76 + .../NavigatorSearchResultItemInfoView.tsx | 81 + .../search/NavigatorSearchResultItemView.tsx | 121 ++ .../search/NavigatorSearchResultView.tsx | 118 ++ .../views/search/NavigatorSearchView.tsx | 84 + .../components/nitropedia/NitropediaView.scss | 47 + .../components/nitropedia/NitropediaView.tsx | 105 + .../NotificationCenterView.scss | 59 + .../NotificationCenterView.tsx | 77 + .../views/alert-layouts/GetAlertLayout.tsx | 21 + .../alert-layouts/NitroSystemAlertView.tsx | 39 + .../NotificationDefaultAlertView.tsx | 56 + .../NotificationSearchAlertView.tsx | 61 + .../views/bubble-layouts/GetBubbleLayout.tsx | 18 + .../NotificationClubGiftBubbleView.tsx | 26 + .../NotificationDefaultBubbleView.tsx | 25 + .../confirm-layouts/GetConfirmLayout.tsx | 15 + .../NotificationDefaultConfirmView.tsx | 40 + .../src/components/purse/PurseView.scss | 26 + .../src/components/purse/PurseView.tsx | 90 + .../components/purse/views/CurrencyView.tsx | 39 + .../components/purse/views/SeasonalView.tsx | 24 + .../components/right-side/RightSideView.scss | 10 + .../components/right-side/RightSideView.tsx | 24 + .../src/components/room/RoomView.scss | 2 + .../frontend/src/components/room/RoomView.tsx | 45 + .../room/spectator/RoomSpectatorView.scss | 26 + .../room/spectator/RoomSpectatorView.tsx | 8 + .../components/room/widgets/RoomWidgets.scss | 110 ++ .../room/widgets/RoomWidgetsView.tsx | 172 ++ .../AvatarInfoPetTrainingPanelView.tsx | 59 + .../AvatarInfoRentableBotChatView.tsx | 68 + .../AvatarInfoUseProductConfirmView.tsx | 281 +++ .../avatar-info/AvatarInfoUseProductView.tsx | 135 ++ .../avatar-info/AvatarInfoWidgetView.scss | 134 ++ .../avatar-info/AvatarInfoWidgetView.tsx | 139 ++ .../infostand/InfoStandWidgetBotView.tsx | 55 + .../infostand/InfoStandWidgetFurniView.tsx | 473 +++++ .../infostand/InfoStandWidgetPetView.tsx | 209 ++ .../InfoStandWidgetRentableBotView.tsx | 84 + ...nfoStandWidgetUserRelationshipItemView.tsx | 31 + .../InfoStandWidgetUserRelationshipsView.tsx | 23 + .../infostand/InfoStandWidgetUserTagsView.tsx | 31 + .../infostand/InfoStandWidgetUserView.tsx | 215 +++ .../menu/AvatarInfoWidgetAvatarView.tsx | 372 ++++ .../menu/AvatarInfoWidgetDecorateView.tsx | 29 + .../menu/AvatarInfoWidgetFurniView.tsx | 67 + .../menu/AvatarInfoWidgetNameView.tsx | 31 + .../menu/AvatarInfoWidgetOwnAvatarView.tsx | 292 +++ .../menu/AvatarInfoWidgetOwnPetView.tsx | 220 +++ .../menu/AvatarInfoWidgetPetView.tsx | 138 ++ .../menu/AvatarInfoWidgetRentableBotView.tsx | 197 ++ .../chat-input/ChatInputStyleSelectorView.tsx | 68 + .../widgets/chat-input/ChatInputView.scss | 84 + .../room/widgets/chat-input/ChatInputView.tsx | 248 +++ .../widgets/chat/ChatWidgetMessageView.tsx | 95 + .../room/widgets/chat/ChatWidgetView.scss | 919 +++++++++ .../room/widgets/chat/ChatWidgetView.tsx | 160 ++ .../widgets/choosers/ChooserWidgetView.scss | 4 + .../widgets/choosers/ChooserWidgetView.tsx | 50 + .../choosers/FurniChooserWidgetView.tsx | 31 + .../choosers/UserChooserWidgetView.tsx | 31 + .../widgets/context-menu/ContextMenu.scss | 129 ++ .../context-menu/ContextMenuCaretView.tsx | 26 + .../context-menu/ContextMenuHeaderView.tsx | 18 + .../context-menu/ContextMenuListItemView.tsx | 32 + .../context-menu/ContextMenuListView.tsx | 18 + .../widgets/context-menu/ContextMenuView.tsx | 170 ++ .../widgets/doorbell/DoorbellWidgetView.tsx | 51 + .../FriendRequestDialogView.scss | 4 + .../FriendRequestDialogView.tsx | 28 + .../FriendRequestWidgetView.tsx | 17 + .../FurnitureBackgroundColorView.tsx | 30 + .../furniture/FurnitureBadgeDisplayView.tsx | 12 + .../furniture/FurnitureCraftingView.tsx | 115 ++ .../widgets/furniture/FurnitureDimmerView.tsx | 86 + .../furniture/FurnitureExchangeCreditView.tsx | 33 + .../furniture/FurnitureExternalImageView.tsx | 22 + .../furniture/FurnitureFriendFurniView.tsx | 66 + .../furniture/FurnitureGiftOpeningView.tsx | 72 + .../furniture/FurnitureHighScoreView.tsx | 60 + .../furniture/FurnitureInternalLinkView.tsx | 9 + .../furniture/FurnitureMannequinView.tsx | 144 ++ .../FurnitureMysteryBoxOpenDialogView.tsx | 81 + .../FurnitureMysteryTrophyOpenDialogView.tsx | 50 + .../furniture/FurnitureRoomLinkView.tsx | 9 + .../furniture/FurnitureSpamWallPostItView.tsx | 46 + .../furniture/FurnitureStackHeightView.tsx | 58 + .../furniture/FurnitureStickieView.tsx | 66 + .../widgets/furniture/FurnitureTrophyView.tsx | 12 + .../widgets/furniture/FurnitureWidgets.scss | 526 ++++++ .../furniture/FurnitureWidgetsView.tsx | 48 + .../furniture/FurnitureYoutubeDisplayView.tsx | 109 ++ .../context-menu/EffectBoxConfirmView.tsx | 40 + .../context-menu/FurnitureContextMenuView.tsx | 130 ++ .../MonsterPlantSeedConfirmView.tsx | 85 + .../PurchasableClothingConfirmView.tsx | 104 + .../playlist-editor/DiskInventoryView.tsx | 94 + .../FurniturePlaylistEditorWidgetView.tsx | 29 + .../playlist-editor/SongPlaylistView.tsx | 79 + .../mysterybox/MysteryBoxExtensionView.scss | 52 + .../mysterybox/MysteryBoxExtensionView.tsx | 67 + .../object-location/ObjectLocationView.tsx | 61 + .../pet-package/PetPackageWidgetView.scss | 106 ++ .../pet-package/PetPackageWidgetView.tsx | 42 + .../RoomFilterWordsWidgetView.tsx | 74 + .../room-promotes/RoomPromotesWidgetView.tsx | 56 + .../views/RoomPromoteEditWidgetView.tsx | 44 + .../views/RoomPromoteMyOwnEventWidgetView.tsx | 35 + .../views/RoomPromoteOtherEventWidgetView.tsx | 30 + .../room/widgets/room-promotes/views/index.ts | 3 + .../RoomThumbnailWidgetView.tsx | 42 + .../room-tools/RoomToolsWidgetView.tsx | 100 + .../user-location/UserLocationView.tsx | 24 + .../word-quiz/WordQuizQuestionView.tsx | 44 + .../widgets/word-quiz/WordQuizVoteView.tsx | 24 + .../widgets/word-quiz/WordQuizWidgetView.tsx | 19 + .../src/components/toolbar/ToolbarMeView.tsx | 52 + .../src/components/toolbar/ToolbarView.scss | 81 + .../src/components/toolbar/ToolbarView.tsx | 111 ++ .../user-profile/UserProfileVew.scss | 67 + .../user-profile/UserProfileView.tsx | 122 ++ .../views/BadgesContainerView.tsx | 25 + .../views/FriendsContainerView.tsx | 28 + .../views/GroupsContainerView.tsx | 90 + .../views/RelationshipsContainerView.tsx | 62 + .../user-profile/views/UserContainerView.tsx | 74 + .../user-settings/UserSettingsView.tsx | 187 ++ .../src/components/wired/WiredView.scss | 175 ++ .../src/components/wired/WiredView.tsx | 21 + .../components/wired/views/WiredBaseView.tsx | 113 ++ .../wired/views/WiredFurniSelectorView.tsx | 16 + .../views/actions/WiredActionBaseView.tsx | 41 + .../WiredActionBotChangeFigureView.tsx | 37 + .../WiredActionBotFollowAvatarView.tsx | 43 + .../WiredActionBotGiveHandItemView.tsx | 42 + .../views/actions/WiredActionBotMoveView.tsx | 27 + .../WiredActionBotTalkToAvatarView.tsx | 52 + .../views/actions/WiredActionBotTalkView.tsx | 52 + .../actions/WiredActionBotTeleportView.tsx | 27 + .../WiredActionCallAnotherStackView.tsx | 8 + .../views/actions/WiredActionChaseView.tsx | 8 + .../views/actions/WiredActionChatView.tsx | 27 + .../views/actions/WiredActionFleeView.tsx | 8 + .../actions/WiredActionGiveRewardView.tsx | 160 ++ ...redActionGiveScoreToPredefinedTeamView.tsx | 67 + .../actions/WiredActionGiveScoreView.tsx | 52 + .../views/actions/WiredActionJoinTeamView.tsx | 35 + .../actions/WiredActionKickFromRoomView.tsx | 27 + .../views/actions/WiredActionLayoutView.tsx | 85 + .../actions/WiredActionLeaveTeamView.tsx | 8 + .../WiredActionMoveAndRotateFurniView.tsx | 82 + .../actions/WiredActionMoveFurniToView.tsx | 76 + .../actions/WiredActionMoveFurniView.tsx | 100 + .../views/actions/WiredActionMuteUserView.tsx | 43 + .../views/actions/WiredActionResetView.tsx | 8 + .../WiredActionSetFurniStateToView.tsx | 42 + .../views/actions/WiredActionTeleportView.tsx | 8 + .../WiredActionToggleFurniStateView.tsx | 8 + .../WiredConditionActorHasHandItem.tsx | 34 + .../WiredConditionActorIsGroupMemberView.tsx | 8 + .../WiredConditionActorIsOnFurniView.tsx | 8 + .../WiredConditionActorIsTeamMemberView.tsx | 37 + .../WiredConditionActorIsWearingBadgeView.tsx | 27 + ...WiredConditionActorIsWearingEffectView.tsx | 27 + .../conditions/WiredConditionBaseView.tsx | 23 + .../WiredConditionDateRangeView.tsx | 58 + .../WiredConditionFurniHasAvatarOnView.tsx | 8 + .../WiredConditionFurniHasFurniOnView.tsx | 35 + .../WiredConditionFurniHasNotFurniOnView.tsx | 35 + .../WiredConditionFurniIsOfTypeView.tsx | 8 + ...WiredConditionFurniMatchesSnapshotView.tsx | 42 + .../conditions/WiredConditionLayoutView.tsx | 64 + .../WiredConditionTimeElapsedLessView.tsx | 33 + .../WiredConditionTimeElapsedMoreView.tsx | 33 + .../WiredConditionUserCountInRoomView.tsx | 52 + .../WiredTriggerAvatarEnterRoomView.tsx | 38 + .../WiredTriggerAvatarSaysSomethingView.tsx | 44 + .../WiredTriggerAvatarWalksOffFurniView.tsx | 8 + .../WiredTriggerAvatarWalksOnFurni.tsx | 8 + .../views/triggers/WiredTriggerBaseView.tsx | 23 + .../WiredTriggerBotReachedAvatarView.tsx | 27 + .../WiredTriggerBotReachedStuffView.tsx | 27 + .../triggers/WiredTriggerCollisionView.tsx | 8 + .../triggers/WiredTriggerExecuteOnceView.tsx | 33 + ...iredTriggerExecutePeriodicallyLongView.tsx | 34 + .../WiredTriggerExecutePeriodicallyView.tsx | 33 + .../triggers/WiredTriggerGameEndsView.tsx | 8 + .../triggers/WiredTriggerGameStartsView.tsx | 8 + .../views/triggers/WiredTriggerLayoutView.tsx | 52 + .../WiredTriggerScoreAchievedView.tsx | 33 + .../triggers/WiredTriggerToggleFurniView.tsx | 8 + .../src/events/catalog/CatalogEvent.ts | 14 + .../events/catalog/CatalogInitGiftEvent.ts | 32 + .../CatalogPostMarketplaceOfferEvent.ts | 20 + .../catalog/CatalogPurchaseFailureEvent.ts | 20 + .../catalog/CatalogPurchaseNotAllowedEvent.ts | 20 + .../catalog/CatalogPurchaseOverrideEvent.ts | 19 + .../catalog/CatalogPurchaseSoldOutEvent.ts | 11 + .../events/catalog/CatalogPurchasedEvent.ts | 20 + .../CatalogSetRoomPreviewerStuffDataEvent.ts | 19 + .../src/events/catalog/CatalogWidgetEvent.ts | 26 + .../catalog/SetRoomPreviewerStuffDataEvent.ts | 28 + apps/frontend/src/events/catalog/index.ts | 11 + .../src/events/guide-tool/GuideToolEvent.ts | 10 + apps/frontend/src/events/guide-tool/index.ts | 1 + .../src/events/help/HelpNameChangeEvent.ts | 6 + apps/frontend/src/events/help/index.ts | 1 + apps/frontend/src/events/index.ts | 6 + .../inventory/InventoryFurniAddedEvent.ts | 14 + apps/frontend/src/events/inventory/index.ts | 1 + .../frontend/src/events/room-widgets/index.ts | 1 + .../thumbnail/RoomWidgetThumbnailEvent.ts | 8 + .../events/room-widgets/thumbnail/index.ts | 1 + apps/frontend/src/hooks/UseMountEffect.tsx | 7 + apps/frontend/src/hooks/achievements/index.ts | 1 + .../src/hooks/achievements/useAchievements.ts | 185 ++ apps/frontend/src/hooks/camera/index.ts | 1 + apps/frontend/src/hooks/camera/useCamera.ts | 42 + apps/frontend/src/hooks/catalog/index.ts | 3 + apps/frontend/src/hooks/catalog/useCatalog.ts | 913 +++++++++ .../catalog/useCatalogPlaceMultipleItems.ts | 7 + .../useCatalogSkipPurchaseConfirmation.ts | 7 + apps/frontend/src/hooks/chat-history/index.ts | 1 + .../src/hooks/chat-history/useChatHistory.ts | 104 + apps/frontend/src/hooks/events/core/index.ts | 2 + .../events/core/useCommunicationEvent.tsx | 5 + .../events/core/useConfigurationEvent.tsx | 5 + apps/frontend/src/hooks/events/index.ts | 5 + apps/frontend/src/hooks/events/nitro/index.ts | 8 + .../src/hooks/events/nitro/useAvatarEvent.tsx | 5 + .../src/hooks/events/nitro/useCameraEvent.tsx | 5 + .../events/nitro/useLocalizationEvent.tsx | 5 + .../src/hooks/events/nitro/useMainEvent.tsx | 5 + .../hooks/events/nitro/useRoomEngineEvent.tsx | 5 + .../nitro/useRoomSessionManagerEvent.tsx | 5 + .../nitro/useSessionDataManagerEvent.tsx | 5 + .../src/hooks/events/nitro/useSoundEvent.tsx | 5 + .../src/hooks/events/useEventDispatcher.tsx | 31 + .../src/hooks/events/useMessageEvent.tsx | 16 + apps/frontend/src/hooks/events/useUiEvent.tsx | 5 + apps/frontend/src/hooks/friends/index.ts | 2 + apps/frontend/src/hooks/friends/useFriends.ts | 266 +++ .../src/hooks/friends/useMessenger.ts | 187 ++ apps/frontend/src/hooks/game-center/index.ts | 1 + .../src/hooks/game-center/useGameCenter.ts | 83 + apps/frontend/src/hooks/groups/index.ts | 1 + apps/frontend/src/hooks/groups/useGroup.ts | 55 + apps/frontend/src/hooks/help/index.ts | 1 + apps/frontend/src/hooks/help/useHelp.ts | 149 ++ apps/frontend/src/hooks/index.ts | 22 + apps/frontend/src/hooks/inventory/index.ts | 6 + .../src/hooks/inventory/useInventoryBadges.ts | 152 ++ .../src/hooks/inventory/useInventoryBots.ts | 158 ++ .../src/hooks/inventory/useInventoryFurni.ts | 298 +++ .../src/hooks/inventory/useInventoryPets.ts | 107 ++ .../src/hooks/inventory/useInventoryTrade.ts | 288 +++ .../inventory/useInventoryUnseenTracker.ts | 132 ++ apps/frontend/src/hooks/mod-tools/index.ts | 1 + .../src/hooks/mod-tools/useModTools.ts | 207 ++ apps/frontend/src/hooks/navigator/index.ts | 1 + .../src/hooks/navigator/useNavigator.ts | 442 +++++ apps/frontend/src/hooks/notification/index.ts | 1 + .../src/hooks/notification/useNotification.ts | 432 +++++ apps/frontend/src/hooks/purse/index.ts | 1 + apps/frontend/src/hooks/purse/usePurse.ts | 126 ++ apps/frontend/src/hooks/rooms/engine/index.ts | 9 + .../hooks/rooms/engine/useFurniAddedEvent.ts | 19 + .../rooms/engine/useFurniRemovedEvent.ts | 19 + .../rooms/engine/useObjectDeselectedEvent.ts | 7 + .../engine/useObjectDoubleClickedEvent.ts | 7 + .../rooms/engine/useObjectRollOutEvent.ts | 7 + .../rooms/engine/useObjectRollOverEvent.ts | 7 + .../rooms/engine/useObjectSelectedEvent.ts | 7 + .../hooks/rooms/engine/useUserAddedEvent.ts | 19 + .../hooks/rooms/engine/useUserRemovedEvent.ts | 19 + apps/frontend/src/hooks/rooms/index.ts | 4 + .../src/hooks/rooms/promotes/index.ts | 1 + .../hooks/rooms/promotes/useRoomPromote.ts | 23 + apps/frontend/src/hooks/rooms/useRoom.ts | 298 +++ .../hooks/rooms/widgets/furniture/index.ts | 19 + .../useFurnitureBackgroundColorWidget.ts | 71 + .../useFurnitureBadgeDisplayWidget.ts | 75 + .../useFurnitureContextMenuWidget.ts | 179 ++ .../furniture/useFurnitureCraftingWidget.ts | 166 ++ .../furniture/useFurnitureDimmerWidget.ts | 110 ++ .../furniture/useFurnitureExchangeWidget.ts | 48 + .../useFurnitureExternalImageWidget.ts | 74 + .../useFurnitureFriendFurniWidget.ts | 75 + .../furniture/useFurnitureHighScoreWidget.ts | 56 + .../useFurnitureInternalLinkWidget.ts | 27 + .../furniture/useFurnitureMannequinWidget.ts | 80 + .../useFurniturePlaylistEditorWidget.ts | 108 ++ .../furniture/useFurniturePresentWidget.ts | 225 +++ .../furniture/useFurnitureRoomLinkWidget.ts | 49 + .../useFurnitureSpamWallPostItWidget.ts | 59 + .../useFurnitureStackHeightWidget.ts | 79 + .../furniture/useFurnitureStickieWidget.ts | 85 + .../furniture/useFurnitureTrophyWidget.ts | 63 + .../furniture/useFurnitureYoutubeWidget.ts | 127 ++ .../frontend/src/hooks/rooms/widgets/index.ts | 12 + .../rooms/widgets/useAvatarInfoWidget.ts | 355 ++++ .../hooks/rooms/widgets/useChatInputWidget.ts | 279 +++ .../src/hooks/rooms/widgets/useChatWidget.ts | 252 +++ .../hooks/rooms/widgets/useDoorbellWidget.ts | 44 + .../rooms/widgets/useFilterWordsWidget.ts | 23 + .../rooms/widgets/useFriendRequestWidget.ts | 81 + .../rooms/widgets/useFurniChooserWidget.ts | 132 ++ .../rooms/widgets/usePetPackageWidget.ts | 75 + .../src/hooks/rooms/widgets/usePollWidget.ts | 52 + .../rooms/widgets/useUserChooserWidget.ts | 80 + .../hooks/rooms/widgets/useWordQuizWidget.ts | 149 ++ apps/frontend/src/hooks/session/index.ts | 1 + .../src/hooks/session/useSessionInfo.ts | 93 + apps/frontend/src/hooks/useLocalStorage.ts | 44 + .../frontend/src/hooks/useSharedVisibility.ts | 44 + apps/frontend/src/hooks/wired/index.ts | 1 + apps/frontend/src/hooks/wired/useWired.ts | 133 ++ apps/frontend/src/index.scss | 26 + apps/frontend/src/index.tsx | 5 + apps/frontend/src/main.tsx | 13 - apps/frontend/src/styles.scss | 1 - .../frontend/src/workers/IntervalWebWorker.ts | 26 + apps/frontend/src/workers/WorkerBuilder.ts | 10 + apps/frontend/vite.config.ts | 8 + libs/renderer/.eslintrc.json | 18 + libs/renderer/README.md | 7 + libs/renderer/package.json | 5 + libs/renderer/project.json | 26 + libs/renderer/src/index.ts | 1 + libs/renderer/src/lib/renderer.ts | 3 + libs/renderer/tsconfig.json | 19 + libs/renderer/tsconfig.lib.json | 10 + package-lock.json | 1359 ++++++++++++- package.json | 9 +- tsconfig.base.json | 4 +- 1441 files changed, 65517 insertions(+), 923 deletions(-) create mode 100644 apps/frontend/public/android-chrome-192x192.png create mode 100644 apps/frontend/public/android-chrome-512x512.png create mode 100644 apps/frontend/public/apple-touch-icon.png create mode 100644 apps/frontend/public/browserconfig.xml create mode 100644 apps/frontend/public/favicon-16x16.png create mode 100644 apps/frontend/public/favicon-32x32.png create mode 100644 apps/frontend/public/mstile-150x150.png create mode 100644 apps/frontend/public/renderer-config.json.example create mode 100644 apps/frontend/public/robots.txt create mode 100644 apps/frontend/public/safari-pinned-tab.svg create mode 100644 apps/frontend/public/site.webmanifest create mode 100644 apps/frontend/public/ui-config.json.example create mode 100644 apps/frontend/src/App.scss create mode 100644 apps/frontend/src/App.tsx create mode 100644 apps/frontend/src/api/GetRendererVersion.ts create mode 100644 apps/frontend/src/api/GetUIVersion.ts create mode 100644 apps/frontend/src/api/achievements/AchievementCategory.ts create mode 100644 apps/frontend/src/api/achievements/AchievementUtilities.ts create mode 100644 apps/frontend/src/api/achievements/IAchievementCategory.ts create mode 100644 apps/frontend/src/api/achievements/index.ts create mode 100644 apps/frontend/src/api/avatar/AvatarEditorAction.ts create mode 100644 apps/frontend/src/api/avatar/AvatarEditorGridColorItem.ts create mode 100644 apps/frontend/src/api/avatar/AvatarEditorGridPartItem.ts create mode 100644 apps/frontend/src/api/avatar/AvatarEditorUtilities.ts create mode 100644 apps/frontend/src/api/avatar/BodyModel.ts create mode 100644 apps/frontend/src/api/avatar/CategoryBaseModel.ts create mode 100644 apps/frontend/src/api/avatar/CategoryData.ts create mode 100644 apps/frontend/src/api/avatar/FigureData.ts create mode 100644 apps/frontend/src/api/avatar/FigureGenerator.ts create mode 100644 apps/frontend/src/api/avatar/HeadModel.ts create mode 100644 apps/frontend/src/api/avatar/IAvatarEditorCategoryModel.ts create mode 100644 apps/frontend/src/api/avatar/LegModel.ts create mode 100644 apps/frontend/src/api/avatar/TorsoModel.ts create mode 100644 apps/frontend/src/api/avatar/index.ts create mode 100644 apps/frontend/src/api/camera/CameraEditorTabs.ts create mode 100644 apps/frontend/src/api/camera/CameraPicture.ts create mode 100644 apps/frontend/src/api/camera/CameraPictureThumbnail.ts create mode 100644 apps/frontend/src/api/camera/index.ts create mode 100644 apps/frontend/src/api/campaign/CalendarItem.ts create mode 100644 apps/frontend/src/api/campaign/CalendarItemState.ts create mode 100644 apps/frontend/src/api/campaign/ICalendarItem.ts create mode 100644 apps/frontend/src/api/campaign/index.ts create mode 100644 apps/frontend/src/api/catalog/BuilderFurniPlaceableStatus.ts create mode 100644 apps/frontend/src/api/catalog/CatalogNode.ts create mode 100644 apps/frontend/src/api/catalog/CatalogPage.ts create mode 100644 apps/frontend/src/api/catalog/CatalogPageName.ts create mode 100644 apps/frontend/src/api/catalog/CatalogPetPalette.ts create mode 100644 apps/frontend/src/api/catalog/CatalogPurchaseState.ts create mode 100644 apps/frontend/src/api/catalog/CatalogType.ts create mode 100644 apps/frontend/src/api/catalog/CatalogUtilities.ts create mode 100644 apps/frontend/src/api/catalog/FurnitureOffer.ts create mode 100644 apps/frontend/src/api/catalog/GetImageIconUrlForProduct.ts create mode 100644 apps/frontend/src/api/catalog/GiftWrappingConfiguration.ts create mode 100644 apps/frontend/src/api/catalog/ICatalogNode.ts create mode 100644 apps/frontend/src/api/catalog/ICatalogOptions.ts create mode 100644 apps/frontend/src/api/catalog/ICatalogPage.ts create mode 100644 apps/frontend/src/api/catalog/IMarketplaceSearchOptions.ts create mode 100644 apps/frontend/src/api/catalog/IPageLocalization.ts create mode 100644 apps/frontend/src/api/catalog/IProduct.ts create mode 100644 apps/frontend/src/api/catalog/IPurchasableOffer.ts create mode 100644 apps/frontend/src/api/catalog/IPurchaseOptions.ts create mode 100644 apps/frontend/src/api/catalog/MarketplaceOfferData.ts create mode 100644 apps/frontend/src/api/catalog/MarketplaceOfferState.ts create mode 100644 apps/frontend/src/api/catalog/MarketplaceSearchType.ts create mode 100644 apps/frontend/src/api/catalog/Offer.ts create mode 100644 apps/frontend/src/api/catalog/PageLocalization.ts create mode 100644 apps/frontend/src/api/catalog/PlacedObjectPurchaseData.ts create mode 100644 apps/frontend/src/api/catalog/Product.ts create mode 100644 apps/frontend/src/api/catalog/ProductTypeEnum.ts create mode 100644 apps/frontend/src/api/catalog/RequestedPage.ts create mode 100644 apps/frontend/src/api/catalog/SearchResult.ts create mode 100644 apps/frontend/src/api/catalog/index.ts create mode 100644 apps/frontend/src/api/chat-history/ChatEntryType.ts create mode 100644 apps/frontend/src/api/chat-history/ChatHistoryCurrentDate.ts create mode 100644 apps/frontend/src/api/chat-history/IChatEntry.ts create mode 100644 apps/frontend/src/api/chat-history/IRoomHistoryEntry.ts create mode 100644 apps/frontend/src/api/chat-history/MessengerHistoryCurrentDate.ts create mode 100644 apps/frontend/src/api/chat-history/index.ts create mode 100644 apps/frontend/src/api/events/DispatchEvent.ts create mode 100644 apps/frontend/src/api/events/DispatchMainEvent.ts create mode 100644 apps/frontend/src/api/events/DispatchUiEvent.ts create mode 100644 apps/frontend/src/api/events/UI_EVENT_DISPATCHER.ts create mode 100644 apps/frontend/src/api/events/index.ts create mode 100644 apps/frontend/src/api/friends/GetGroupChatData.ts create mode 100644 apps/frontend/src/api/friends/IGroupChatData.ts create mode 100644 apps/frontend/src/api/friends/MessengerFriend.ts create mode 100644 apps/frontend/src/api/friends/MessengerGroupType.ts create mode 100644 apps/frontend/src/api/friends/MessengerIconState.ts create mode 100644 apps/frontend/src/api/friends/MessengerRequest.ts create mode 100644 apps/frontend/src/api/friends/MessengerSettings.ts create mode 100644 apps/frontend/src/api/friends/MessengerThread.ts create mode 100644 apps/frontend/src/api/friends/MessengerThreadChat.ts create mode 100644 apps/frontend/src/api/friends/MessengerThreadChatGroup.ts create mode 100644 apps/frontend/src/api/friends/OpenMessengerChat.ts create mode 100644 apps/frontend/src/api/friends/index.ts create mode 100644 apps/frontend/src/api/groups/GetGroupInformation.ts create mode 100644 apps/frontend/src/api/groups/GetGroupManager.ts create mode 100644 apps/frontend/src/api/groups/GetGroupMembers.ts create mode 100644 apps/frontend/src/api/groups/GroupBadgePart.ts create mode 100644 apps/frontend/src/api/groups/GroupMembershipType.ts create mode 100644 apps/frontend/src/api/groups/GroupType.ts create mode 100644 apps/frontend/src/api/groups/IGroupCustomize.ts create mode 100644 apps/frontend/src/api/groups/IGroupData.ts create mode 100644 apps/frontend/src/api/groups/ToggleFavoriteGroup.ts create mode 100644 apps/frontend/src/api/groups/TryJoinGroup.ts create mode 100644 apps/frontend/src/api/groups/index.ts create mode 100644 apps/frontend/src/api/guide-tool/GuideSessionState.ts create mode 100644 apps/frontend/src/api/guide-tool/GuideToolMessage.ts create mode 100644 apps/frontend/src/api/guide-tool/GuideToolMessageGroup.ts create mode 100644 apps/frontend/src/api/guide-tool/index.ts create mode 100644 apps/frontend/src/api/hc-center/ClubStatus.ts create mode 100644 apps/frontend/src/api/hc-center/GetClubBadge.ts create mode 100644 apps/frontend/src/api/hc-center/index.ts create mode 100644 apps/frontend/src/api/help/CallForHelpResult.ts create mode 100644 apps/frontend/src/api/help/GetCloseReasonKey.ts create mode 100644 apps/frontend/src/api/help/IHelpReport.ts create mode 100644 apps/frontend/src/api/help/IReportedUser.ts create mode 100644 apps/frontend/src/api/help/ReportState.ts create mode 100644 apps/frontend/src/api/help/ReportType.ts create mode 100644 apps/frontend/src/api/help/index.ts create mode 100644 apps/frontend/src/api/index.ts create mode 100644 apps/frontend/src/api/inventory/FurniCategory.ts create mode 100644 apps/frontend/src/api/inventory/FurnitureItem.ts create mode 100644 apps/frontend/src/api/inventory/FurnitureUtilities.ts create mode 100644 apps/frontend/src/api/inventory/GroupItem.ts create mode 100644 apps/frontend/src/api/inventory/IBotItem.ts create mode 100644 apps/frontend/src/api/inventory/IFurnitureItem.ts create mode 100644 apps/frontend/src/api/inventory/IPetItem.ts create mode 100644 apps/frontend/src/api/inventory/IUnseenItemTracker.ts create mode 100644 apps/frontend/src/api/inventory/InventoryUtilities.ts create mode 100644 apps/frontend/src/api/inventory/PetUtilities.ts create mode 100644 apps/frontend/src/api/inventory/TradeState.ts create mode 100644 apps/frontend/src/api/inventory/TradeUserData.ts create mode 100644 apps/frontend/src/api/inventory/TradingNotificationType.ts create mode 100644 apps/frontend/src/api/inventory/TradingUtilities.ts create mode 100644 apps/frontend/src/api/inventory/UnseenItemCategory.ts create mode 100644 apps/frontend/src/api/inventory/index.ts create mode 100644 apps/frontend/src/api/mod-tools/GetIssueCategoryName.ts create mode 100644 apps/frontend/src/api/mod-tools/ISelectedUser.ts create mode 100644 apps/frontend/src/api/mod-tools/IUserInfo.ts create mode 100644 apps/frontend/src/api/mod-tools/ModActionDefinition.ts create mode 100644 apps/frontend/src/api/mod-tools/index.ts create mode 100644 apps/frontend/src/api/navigator/DoorStateType.ts create mode 100644 apps/frontend/src/api/navigator/INavigatorData.ts create mode 100644 apps/frontend/src/api/navigator/INavigatorSearchFilter.ts create mode 100644 apps/frontend/src/api/navigator/IRoomChatSettings.ts create mode 100644 apps/frontend/src/api/navigator/IRoomData.ts create mode 100644 apps/frontend/src/api/navigator/IRoomModel.ts create mode 100644 apps/frontend/src/api/navigator/IRoomModerationSettings.ts create mode 100644 apps/frontend/src/api/navigator/NavigatorSearchResultViewDisplayMode.ts create mode 100644 apps/frontend/src/api/navigator/RoomInfoData.ts create mode 100644 apps/frontend/src/api/navigator/RoomSettingsUtils.ts create mode 100644 apps/frontend/src/api/navigator/SearchFilterOptions.ts create mode 100644 apps/frontend/src/api/navigator/TryVisitRoom.ts create mode 100644 apps/frontend/src/api/navigator/index.ts create mode 100644 apps/frontend/src/api/nitro/AddLinkEventTracker.ts create mode 100644 apps/frontend/src/api/nitro/CreateLinkEvent.ts create mode 100644 apps/frontend/src/api/nitro/GetCommunication.ts create mode 100644 apps/frontend/src/api/nitro/GetConfiguration.ts create mode 100644 apps/frontend/src/api/nitro/GetConnection.ts create mode 100644 apps/frontend/src/api/nitro/GetLocalization.ts create mode 100644 apps/frontend/src/api/nitro/GetNitroInstance.ts create mode 100644 apps/frontend/src/api/nitro/OpenUrl.ts create mode 100644 apps/frontend/src/api/nitro/RemoveLinkEventTracker.ts create mode 100644 apps/frontend/src/api/nitro/SendMessageComposer.ts create mode 100644 apps/frontend/src/api/nitro/avatar/GetAvatarPalette.ts create mode 100644 apps/frontend/src/api/nitro/avatar/GetAvatarRenderManager.ts create mode 100644 apps/frontend/src/api/nitro/avatar/GetAvatarSetType.ts create mode 100644 apps/frontend/src/api/nitro/avatar/index.ts create mode 100644 apps/frontend/src/api/nitro/camera/GetRoomCameraWidgetManager.ts create mode 100644 apps/frontend/src/api/nitro/camera/index.ts create mode 100644 apps/frontend/src/api/nitro/core/GetConfigurationManager.ts create mode 100644 apps/frontend/src/api/nitro/core/GetNitroCore.ts create mode 100644 apps/frontend/src/api/nitro/core/index.ts create mode 100644 apps/frontend/src/api/nitro/index.ts create mode 100644 apps/frontend/src/api/nitro/room/DispatchMouseEvent.ts create mode 100644 apps/frontend/src/api/nitro/room/DispatchTouchEvent.ts create mode 100644 apps/frontend/src/api/nitro/room/GetOwnRoomObject.ts create mode 100644 apps/frontend/src/api/nitro/room/GetRoomEngine.ts create mode 100644 apps/frontend/src/api/nitro/room/GetRoomObjectBounds.ts create mode 100644 apps/frontend/src/api/nitro/room/GetRoomObjectScreenLocation.ts create mode 100644 apps/frontend/src/api/nitro/room/InitializeRoomInstanceRenderingCanvas.ts create mode 100644 apps/frontend/src/api/nitro/room/IsFurnitureSelectionDisabled.ts create mode 100644 apps/frontend/src/api/nitro/room/ProcessRoomObjectOperation.ts create mode 100644 apps/frontend/src/api/nitro/room/SetActiveRoomId.ts create mode 100644 apps/frontend/src/api/nitro/room/index.ts create mode 100644 apps/frontend/src/api/nitro/session/CanManipulateFurniture.ts create mode 100644 apps/frontend/src/api/nitro/session/CreateRoomSession.ts create mode 100644 apps/frontend/src/api/nitro/session/GetCanStandUp.ts create mode 100644 apps/frontend/src/api/nitro/session/GetCanUseExpression.ts create mode 100644 apps/frontend/src/api/nitro/session/GetClubMemberLevel.ts create mode 100644 apps/frontend/src/api/nitro/session/GetFurnitureData.ts create mode 100644 apps/frontend/src/api/nitro/session/GetFurnitureDataForProductOffer.ts create mode 100644 apps/frontend/src/api/nitro/session/GetFurnitureDataForRoomObject.ts create mode 100644 apps/frontend/src/api/nitro/session/GetOwnPosture.ts create mode 100644 apps/frontend/src/api/nitro/session/GetProductDataForLocalization.ts create mode 100644 apps/frontend/src/api/nitro/session/GetRoomSession.ts create mode 100644 apps/frontend/src/api/nitro/session/GetRoomSessionManager.ts create mode 100644 apps/frontend/src/api/nitro/session/GetSessionDataManager.ts create mode 100644 apps/frontend/src/api/nitro/session/GoToDesktop.ts create mode 100644 apps/frontend/src/api/nitro/session/HasHabboClub.ts create mode 100644 apps/frontend/src/api/nitro/session/HasHabboVip.ts create mode 100644 apps/frontend/src/api/nitro/session/IsOwnerOfFloorFurniture.ts create mode 100644 apps/frontend/src/api/nitro/session/IsOwnerOfFurniture.ts create mode 100644 apps/frontend/src/api/nitro/session/IsRidingHorse.ts create mode 100644 apps/frontend/src/api/nitro/session/StartRoomSession.ts create mode 100644 apps/frontend/src/api/nitro/session/VisitDesktop.ts create mode 100644 apps/frontend/src/api/nitro/session/index.ts create mode 100644 apps/frontend/src/api/notification/NotificationAlertItem.ts create mode 100644 apps/frontend/src/api/notification/NotificationAlertType.ts create mode 100644 apps/frontend/src/api/notification/NotificationBubbleItem.ts create mode 100644 apps/frontend/src/api/notification/NotificationBubbleType.ts create mode 100644 apps/frontend/src/api/notification/NotificationConfirmItem.ts create mode 100644 apps/frontend/src/api/notification/NotificationConfirmType.ts create mode 100644 apps/frontend/src/api/notification/index.ts create mode 100644 apps/frontend/src/api/purse/IPurse.ts create mode 100644 apps/frontend/src/api/purse/Purse.ts create mode 100644 apps/frontend/src/api/purse/index.ts create mode 100644 apps/frontend/src/api/room/events/RoomWidgetPollUpdateEvent.ts create mode 100644 apps/frontend/src/api/room/events/RoomWidgetUpdateBackgroundColorPreviewEvent.ts create mode 100644 apps/frontend/src/api/room/events/RoomWidgetUpdateChatInputContentEvent.ts create mode 100644 apps/frontend/src/api/room/events/RoomWidgetUpdateEvent.ts create mode 100644 apps/frontend/src/api/room/events/RoomWidgetUpdateRentableBotChatEvent.ts create mode 100644 apps/frontend/src/api/room/events/RoomWidgetUpdateRoomObjectEvent.ts create mode 100644 apps/frontend/src/api/room/events/index.ts create mode 100644 apps/frontend/src/api/room/index.ts create mode 100644 apps/frontend/src/api/room/widgets/AvatarInfoFurni.ts create mode 100644 apps/frontend/src/api/room/widgets/AvatarInfoName.ts create mode 100644 apps/frontend/src/api/room/widgets/AvatarInfoPet.ts create mode 100644 apps/frontend/src/api/room/widgets/AvatarInfoRentableBot.ts create mode 100644 apps/frontend/src/api/room/widgets/AvatarInfoUser.ts create mode 100644 apps/frontend/src/api/room/widgets/AvatarInfoUtilities.ts create mode 100644 apps/frontend/src/api/room/widgets/BotSkillsEnum.ts create mode 100644 apps/frontend/src/api/room/widgets/ChatBubbleMessage.ts create mode 100644 apps/frontend/src/api/room/widgets/ChatMessageTypeEnum.ts create mode 100644 apps/frontend/src/api/room/widgets/DimmerFurnitureWidgetPresetItem.ts create mode 100644 apps/frontend/src/api/room/widgets/DoChatsOverlap.ts create mode 100644 apps/frontend/src/api/room/widgets/FurnitureDimmerUtilities.ts create mode 100644 apps/frontend/src/api/room/widgets/GetDiskColor.ts create mode 100644 apps/frontend/src/api/room/widgets/IAvatarInfo.ts create mode 100644 apps/frontend/src/api/room/widgets/ICraftingIngredient.ts create mode 100644 apps/frontend/src/api/room/widgets/ICraftingRecipe.ts create mode 100644 apps/frontend/src/api/room/widgets/IPhotoData.ts create mode 100644 apps/frontend/src/api/room/widgets/MannequinUtilities.ts create mode 100644 apps/frontend/src/api/room/widgets/PetSupplementEnum.ts create mode 100644 apps/frontend/src/api/room/widgets/PostureTypeEnum.ts create mode 100644 apps/frontend/src/api/room/widgets/RoomDimmerPreset.ts create mode 100644 apps/frontend/src/api/room/widgets/RoomObjectItem.ts create mode 100644 apps/frontend/src/api/room/widgets/UseProductItem.ts create mode 100644 apps/frontend/src/api/room/widgets/VoteValue.ts create mode 100644 apps/frontend/src/api/room/widgets/YoutubeVideoPlaybackStateEnum.ts create mode 100644 apps/frontend/src/api/room/widgets/index.ts create mode 100644 apps/frontend/src/api/user/GetUserProfile.ts create mode 100644 apps/frontend/src/api/user/index.ts create mode 100644 apps/frontend/src/api/utils/CloneObject.ts create mode 100644 apps/frontend/src/api/utils/ColorUtils.ts create mode 100644 apps/frontend/src/api/utils/ConvertSeconds.ts create mode 100644 apps/frontend/src/api/utils/GetLocalStorage.ts create mode 100644 apps/frontend/src/api/utils/LocalStorageKeys.ts create mode 100644 apps/frontend/src/api/utils/LocalizeBadgeDescription.ts create mode 100644 apps/frontend/src/api/utils/LocalizeBageName.ts create mode 100644 apps/frontend/src/api/utils/LocalizeFormattedNumber.ts create mode 100644 apps/frontend/src/api/utils/LocalizeShortNumber.ts create mode 100644 apps/frontend/src/api/utils/LocalizeText.ts create mode 100644 apps/frontend/src/api/utils/PlaySound.ts create mode 100644 apps/frontend/src/api/utils/ProductImageUtility.ts create mode 100644 apps/frontend/src/api/utils/Randomizer.ts create mode 100644 apps/frontend/src/api/utils/RoomChatFormatter.ts create mode 100644 apps/frontend/src/api/utils/SetLocalStorage.ts create mode 100644 apps/frontend/src/api/utils/SoundNames.ts create mode 100644 apps/frontend/src/api/utils/WindowSaveOptions.ts create mode 100644 apps/frontend/src/api/utils/index.ts create mode 100644 apps/frontend/src/api/wired/GetWiredTimeLocale.ts create mode 100644 apps/frontend/src/api/wired/WiredActionLayoutCode.ts create mode 100644 apps/frontend/src/api/wired/WiredConditionLayoutCode.ts create mode 100644 apps/frontend/src/api/wired/WiredDateToString.ts create mode 100644 apps/frontend/src/api/wired/WiredFurniType.ts create mode 100644 apps/frontend/src/api/wired/WiredSelectionFilter.ts create mode 100644 apps/frontend/src/api/wired/WiredSelectionVisualizer.ts create mode 100644 apps/frontend/src/api/wired/WiredStringDelimeter.ts create mode 100644 apps/frontend/src/api/wired/WiredTriggerLayoutCode.ts create mode 100644 apps/frontend/src/api/wired/index.ts delete mode 100644 apps/frontend/src/app/app.module.scss delete mode 100644 apps/frontend/src/app/app.spec.tsx delete mode 100644 apps/frontend/src/app/app.tsx delete mode 100644 apps/frontend/src/app/nx-welcome.tsx delete mode 100644 apps/frontend/src/assets/.gitkeep create mode 100644 apps/frontend/src/assets/images/achievements/back-arrow.png create mode 100644 apps/frontend/src/assets/images/avatareditor/arrow-left-icon.png create mode 100644 apps/frontend/src/assets/images/avatareditor/arrow-right-icon.png create mode 100644 apps/frontend/src/assets/images/avatareditor/avatar-editor-spritesheet.png create mode 100644 apps/frontend/src/assets/images/avatareditor/ca-icon.png create mode 100644 apps/frontend/src/assets/images/avatareditor/ca-selected-icon.png create mode 100644 apps/frontend/src/assets/images/avatareditor/cc-icon.png create mode 100644 apps/frontend/src/assets/images/avatareditor/cc-selected-icon.png create mode 100644 apps/frontend/src/assets/images/avatareditor/ch-icon.png create mode 100644 apps/frontend/src/assets/images/avatareditor/ch-selected-icon.png create mode 100644 apps/frontend/src/assets/images/avatareditor/clear-icon.png create mode 100644 apps/frontend/src/assets/images/avatareditor/cp-icon.png create mode 100644 apps/frontend/src/assets/images/avatareditor/cp-selected-icon.png create mode 100644 apps/frontend/src/assets/images/avatareditor/ea-icon.png create mode 100644 apps/frontend/src/assets/images/avatareditor/ea-selected-icon.png create mode 100644 apps/frontend/src/assets/images/avatareditor/fa-icon.png create mode 100644 apps/frontend/src/assets/images/avatareditor/fa-selected-icon.png create mode 100644 apps/frontend/src/assets/images/avatareditor/female-icon.png create mode 100644 apps/frontend/src/assets/images/avatareditor/female-selected-icon.png create mode 100644 apps/frontend/src/assets/images/avatareditor/ha-icon.png create mode 100644 apps/frontend/src/assets/images/avatareditor/ha-selected-icon.png create mode 100644 apps/frontend/src/assets/images/avatareditor/he-icon.png create mode 100644 apps/frontend/src/assets/images/avatareditor/he-selected-icon.png create mode 100644 apps/frontend/src/assets/images/avatareditor/hr-icon.png create mode 100644 apps/frontend/src/assets/images/avatareditor/hr-selected-icon.png create mode 100644 apps/frontend/src/assets/images/avatareditor/lg-icon.png create mode 100644 apps/frontend/src/assets/images/avatareditor/lg-selected-icon.png create mode 100644 apps/frontend/src/assets/images/avatareditor/loading-icon.png create mode 100644 apps/frontend/src/assets/images/avatareditor/male-icon.png create mode 100644 apps/frontend/src/assets/images/avatareditor/male-selected-icon.png create mode 100644 apps/frontend/src/assets/images/avatareditor/sellable-icon.png create mode 100644 apps/frontend/src/assets/images/avatareditor/sh-icon.png create mode 100644 apps/frontend/src/assets/images/avatareditor/sh-selected-icon.png create mode 100644 apps/frontend/src/assets/images/avatareditor/spotlight-icon.png create mode 100644 apps/frontend/src/assets/images/avatareditor/wa-icon.png create mode 100644 apps/frontend/src/assets/images/avatareditor/wa-selected-icon.png create mode 100644 apps/frontend/src/assets/images/campaign/available.png create mode 100644 apps/frontend/src/assets/images/campaign/campaign_day_generic_bg.png create mode 100644 apps/frontend/src/assets/images/campaign/campaign_opened.png create mode 100644 apps/frontend/src/assets/images/campaign/campaign_spritesheet.png create mode 100644 apps/frontend/src/assets/images/campaign/locked.png create mode 100644 apps/frontend/src/assets/images/campaign/locked_bg.png create mode 100644 apps/frontend/src/assets/images/campaign/next.png create mode 100644 apps/frontend/src/assets/images/campaign/prev.png create mode 100644 apps/frontend/src/assets/images/campaign/unavailable.png create mode 100644 apps/frontend/src/assets/images/campaign/unlocked_bg.png create mode 100644 apps/frontend/src/assets/images/catalog/diamond_info_illustration.gif create mode 100644 apps/frontend/src/assets/images/catalog/hc_banner_big.png create mode 100644 apps/frontend/src/assets/images/catalog/hc_big.png create mode 100644 apps/frontend/src/assets/images/catalog/hc_small.png create mode 100644 apps/frontend/src/assets/images/catalog/paint-icon.png create mode 100644 apps/frontend/src/assets/images/catalog/target-price.png create mode 100644 apps/frontend/src/assets/images/catalog/vip.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_0.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_0_1_33_34_pointer.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_0_transparent.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_1.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_10.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_10_pointer.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_11.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_11_pointer.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_12.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_12_pointer.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_13.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_13_pointer.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_14.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_14_pointer.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_15.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_15_pointer.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_16.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_16_pointer.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_17.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_17_pointer.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_18.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_18_pointer.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_19.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_19_20_pointer.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_2.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_20.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_21.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_21_pointer.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_22.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_22_pointer.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_23.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_23_37_pointer.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_24.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_24_pointer.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_25.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_25_pointer.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_26.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_26_pointer.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_27.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_27_pointer.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_28.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_28_pointer.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_29.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_29_pointer.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_2_31_pointer.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_3.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_30.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_30_pointer.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_32.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_32_pointer.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_33_34.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_33_extra.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_34_extra.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_35.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_35_pointer.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_36.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_36_extra.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_36_pointer.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_37.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_38.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_38_extra.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_38_pointer.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_3_pointer.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_4.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_4_pointer.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_5.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_5_pointer.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_6.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_6_pointer.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_7.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_7_pointer.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_8.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_8_pointer.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_9.png create mode 100644 apps/frontend/src/assets/images/chat/chatbubbles/bubble_9_pointer.png create mode 100644 apps/frontend/src/assets/images/chat/styles-icon.png create mode 100644 apps/frontend/src/assets/images/floorplaneditor/door-direction-0.png create mode 100644 apps/frontend/src/assets/images/floorplaneditor/door-direction-1.png create mode 100644 apps/frontend/src/assets/images/floorplaneditor/door-direction-2.png create mode 100644 apps/frontend/src/assets/images/floorplaneditor/door-direction-3.png create mode 100644 apps/frontend/src/assets/images/floorplaneditor/door-direction-4.png create mode 100644 apps/frontend/src/assets/images/floorplaneditor/door-direction-5.png create mode 100644 apps/frontend/src/assets/images/floorplaneditor/door-direction-6.png create mode 100644 apps/frontend/src/assets/images/floorplaneditor/door-direction-7.png create mode 100644 apps/frontend/src/assets/images/floorplaneditor/icon-door.png create mode 100644 apps/frontend/src/assets/images/floorplaneditor/icon-tile-down.png create mode 100644 apps/frontend/src/assets/images/floorplaneditor/icon-tile-set.png create mode 100644 apps/frontend/src/assets/images/floorplaneditor/icon-tile-unset.png create mode 100644 apps/frontend/src/assets/images/floorplaneditor/icon-tile-up.png create mode 100644 apps/frontend/src/assets/images/floorplaneditor/preview_tile.png create mode 100644 apps/frontend/src/assets/images/floorplaneditor/selected_height_icon.png create mode 100644 apps/frontend/src/assets/images/friends/friends-spritesheet.png create mode 100644 apps/frontend/src/assets/images/friends/icon-accept.png create mode 100644 apps/frontend/src/assets/images/friends/icon-add.png create mode 100644 apps/frontend/src/assets/images/friends/icon-bobba.png create mode 100644 apps/frontend/src/assets/images/friends/icon-chat.png create mode 100644 apps/frontend/src/assets/images/friends/icon-deny.png create mode 100644 apps/frontend/src/assets/images/friends/icon-follow.png create mode 100644 apps/frontend/src/assets/images/friends/icon-friendbar-chat.png create mode 100644 apps/frontend/src/assets/images/friends/icon-friendbar-visit.png create mode 100644 apps/frontend/src/assets/images/friends/icon-heart.png create mode 100644 apps/frontend/src/assets/images/friends/icon-new-message.png create mode 100644 apps/frontend/src/assets/images/friends/icon-none.png create mode 100644 apps/frontend/src/assets/images/friends/icon-profile-sm-hover.png create mode 100644 apps/frontend/src/assets/images/friends/icon-profile-sm.png create mode 100644 apps/frontend/src/assets/images/friends/icon-profile.png create mode 100644 apps/frontend/src/assets/images/friends/icon-smile.png create mode 100644 apps/frontend/src/assets/images/friends/icon-warning.png create mode 100644 apps/frontend/src/assets/images/friends/messenger_notification_icon.png create mode 100644 apps/frontend/src/assets/images/gamecenter/selectedIcon.png create mode 100644 apps/frontend/src/assets/images/gift/gift_tag.png create mode 100644 apps/frontend/src/assets/images/gift/incognito.png create mode 100644 apps/frontend/src/assets/images/groups/creator_images.png create mode 100644 apps/frontend/src/assets/images/groups/creator_tabs.png create mode 100644 apps/frontend/src/assets/images/groups/icons/group_decorate_icon.png create mode 100644 apps/frontend/src/assets/images/groups/icons/group_favorite.png create mode 100644 apps/frontend/src/assets/images/groups/icons/group_icon_admin.png create mode 100644 apps/frontend/src/assets/images/groups/icons/group_icon_big_admin.png create mode 100644 apps/frontend/src/assets/images/groups/icons/group_icon_big_member.png create mode 100644 apps/frontend/src/assets/images/groups/icons/group_icon_big_owner.png create mode 100644 apps/frontend/src/assets/images/groups/icons/group_icon_not_admin.png create mode 100644 apps/frontend/src/assets/images/groups/icons/group_icon_small_owner.png create mode 100644 apps/frontend/src/assets/images/groups/icons/group_notfavorite.png create mode 100644 apps/frontend/src/assets/images/groups/icons/grouptype_icon_0.png create mode 100644 apps/frontend/src/assets/images/groups/icons/grouptype_icon_1.png create mode 100644 apps/frontend/src/assets/images/groups/icons/grouptype_icon_2.png create mode 100644 apps/frontend/src/assets/images/groups/no-group-1.png create mode 100644 apps/frontend/src/assets/images/groups/no-group-2.png create mode 100644 apps/frontend/src/assets/images/groups/no-group-3.png create mode 100644 apps/frontend/src/assets/images/groups/no-group-spritesheet.png create mode 100644 apps/frontend/src/assets/images/guide-tool/guide_tool_duty_switch.png create mode 100644 apps/frontend/src/assets/images/guide-tool/guide_tool_info_icon.png create mode 100644 apps/frontend/src/assets/images/hc-center/benefits.png create mode 100644 apps/frontend/src/assets/images/hc-center/clock.png create mode 100644 apps/frontend/src/assets/images/hc-center/hc_logo.gif create mode 100644 apps/frontend/src/assets/images/hc-center/payday.png create mode 100644 apps/frontend/src/assets/images/help/help_index.png create mode 100644 apps/frontend/src/assets/images/icons/arrows.png create mode 100644 apps/frontend/src/assets/images/icons/camera-colormatrix.png create mode 100644 apps/frontend/src/assets/images/icons/camera-composite.png create mode 100644 apps/frontend/src/assets/images/icons/camera-small.png create mode 100644 apps/frontend/src/assets/images/icons/chat-history.png create mode 100644 apps/frontend/src/assets/images/icons/close.png create mode 100644 apps/frontend/src/assets/images/icons/cog.png create mode 100644 apps/frontend/src/assets/images/icons/help.png create mode 100644 apps/frontend/src/assets/images/icons/house-small.png create mode 100644 apps/frontend/src/assets/images/icons/icon_cog.png create mode 100644 apps/frontend/src/assets/images/icons/like-room.png create mode 100644 apps/frontend/src/assets/images/icons/loading-icon.png create mode 100644 apps/frontend/src/assets/images/icons/room-link.png create mode 100644 apps/frontend/src/assets/images/icons/sign-exclamation.png create mode 100644 apps/frontend/src/assets/images/icons/sign-heart.png create mode 100644 apps/frontend/src/assets/images/icons/sign-red.png create mode 100644 apps/frontend/src/assets/images/icons/sign-skull.png create mode 100644 apps/frontend/src/assets/images/icons/sign-smile.png create mode 100644 apps/frontend/src/assets/images/icons/sign-soccer.png create mode 100644 apps/frontend/src/assets/images/icons/sign-yellow.png create mode 100644 apps/frontend/src/assets/images/icons/small-room.png create mode 100644 apps/frontend/src/assets/images/icons/tickets.png create mode 100644 apps/frontend/src/assets/images/icons/user.png create mode 100644 apps/frontend/src/assets/images/icons/zoom-less.png create mode 100644 apps/frontend/src/assets/images/icons/zoom-more.png create mode 100644 apps/frontend/src/assets/images/infostand/bot_background.png create mode 100644 apps/frontend/src/assets/images/infostand/countown-timer.png create mode 100644 apps/frontend/src/assets/images/infostand/disk-creator.png create mode 100644 apps/frontend/src/assets/images/infostand/disk-icon.png create mode 100644 apps/frontend/src/assets/images/infostand/pencil-icon.png create mode 100644 apps/frontend/src/assets/images/infostand/rarity-level.png create mode 100644 apps/frontend/src/assets/images/inventory/empty.png create mode 100644 apps/frontend/src/assets/images/inventory/rarity-level.png create mode 100644 apps/frontend/src/assets/images/inventory/trading/locked-icon.png create mode 100644 apps/frontend/src/assets/images/inventory/trading/unlocked-icon.png create mode 100644 apps/frontend/src/assets/images/loading/connecting-duck-spritesheet.png create mode 100644 apps/frontend/src/assets/images/loading/connecting_duck_01.png create mode 100644 apps/frontend/src/assets/images/loading/connecting_duck_02.png create mode 100644 apps/frontend/src/assets/images/loading/connecting_duck_03.png create mode 100644 apps/frontend/src/assets/images/loading/connecting_duck_04.png create mode 100644 apps/frontend/src/assets/images/loading/connecting_duck_05.png create mode 100644 apps/frontend/src/assets/images/loading/connecting_duck_06.png create mode 100644 apps/frontend/src/assets/images/loading/connecting_duck_07.png create mode 100644 apps/frontend/src/assets/images/loading/progress_habbos.gif create mode 100644 apps/frontend/src/assets/images/modtool/chatlog.gif create mode 100644 apps/frontend/src/assets/images/modtool/key.gif create mode 100644 apps/frontend/src/assets/images/modtool/m_icon.png create mode 100644 apps/frontend/src/assets/images/modtool/reports.png create mode 100644 apps/frontend/src/assets/images/modtool/room.gif create mode 100644 apps/frontend/src/assets/images/modtool/room.png create mode 100644 apps/frontend/src/assets/images/modtool/user.gif create mode 100644 apps/frontend/src/assets/images/modtool/wrench.gif create mode 100644 apps/frontend/src/assets/images/mysterybox/chain_mysterybox_box_overlay.png create mode 100644 apps/frontend/src/assets/images/mysterybox/key_overlay.png create mode 100644 apps/frontend/src/assets/images/mysterybox/mystery_box.png create mode 100644 apps/frontend/src/assets/images/mysterybox/mystery_box_key.png create mode 100644 apps/frontend/src/assets/images/mysterytrophy/frank_mystery_trophy.png create mode 100644 apps/frontend/src/assets/images/navigator/icons/info.png create mode 100644 apps/frontend/src/assets/images/navigator/icons/room_group.png create mode 100644 apps/frontend/src/assets/images/navigator/icons/room_invisible.png create mode 100644 apps/frontend/src/assets/images/navigator/icons/room_locked.png create mode 100644 apps/frontend/src/assets/images/navigator/icons/room_password.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_0.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_1.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_2.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_3.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_4.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_5.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_6.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_7.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_8.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_9.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_a.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_b.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_c.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_d.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_e.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_f.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_g.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_h.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_i.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_j.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_k.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_l.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_m.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_n.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_o.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_p.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_q.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_r.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_snowwar1.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_snowwar2.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_t.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_u.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_v.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_w.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_x.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_y.png create mode 100644 apps/frontend/src/assets/images/navigator/models/model_z.png create mode 100644 apps/frontend/src/assets/images/navigator/thumbnail_placeholder.png create mode 100644 apps/frontend/src/assets/images/nitro/nitro-dark.svg create mode 100644 apps/frontend/src/assets/images/nitro/nitro-light.svg create mode 100644 apps/frontend/src/assets/images/nitro/nitro-n-dark.svg create mode 100644 apps/frontend/src/assets/images/nitro/nitro-n-light.svg create mode 100644 apps/frontend/src/assets/images/notifications/frank.gif create mode 100644 apps/frontend/src/assets/images/pets/pet-package/gnome.png create mode 100644 apps/frontend/src/assets/images/pets/pet-package/leprechaun_box.png create mode 100644 apps/frontend/src/assets/images/pets/pet-package/petbox_epic.png create mode 100644 apps/frontend/src/assets/images/pets/pet-package/pterosaur_egg.png create mode 100644 apps/frontend/src/assets/images/pets/pet-package/val11_present.png create mode 100644 apps/frontend/src/assets/images/pets/pet-package/velociraptor_egg.png create mode 100644 apps/frontend/src/assets/images/prize/prize_background.png create mode 100644 apps/frontend/src/assets/images/profile/icons/offline.png create mode 100644 apps/frontend/src/assets/images/profile/icons/online.gif create mode 100644 apps/frontend/src/assets/images/profile/icons/tick.png create mode 100644 apps/frontend/src/assets/images/room-spectator/room_spectator_bottom_left.png create mode 100644 apps/frontend/src/assets/images/room-spectator/room_spectator_bottom_right.png create mode 100644 apps/frontend/src/assets/images/room-spectator/room_spectator_middle_bottom.png create mode 100644 apps/frontend/src/assets/images/room-spectator/room_spectator_middle_left.png create mode 100644 apps/frontend/src/assets/images/room-spectator/room_spectator_middle_right.png create mode 100644 apps/frontend/src/assets/images/room-spectator/room_spectator_middle_top.png create mode 100644 apps/frontend/src/assets/images/room-spectator/room_spectator_top_left.png create mode 100644 apps/frontend/src/assets/images/room-spectator/room_spectator_top_right.png create mode 100644 apps/frontend/src/assets/images/room-widgets/avatar-info/preview-background.png create mode 100644 apps/frontend/src/assets/images/room-widgets/camera-widget/btn.png create mode 100644 apps/frontend/src/assets/images/room-widgets/camera-widget/btn_down.png create mode 100644 apps/frontend/src/assets/images/room-widgets/camera-widget/btn_hi.png create mode 100644 apps/frontend/src/assets/images/room-widgets/camera-widget/cam_bg.png create mode 100644 apps/frontend/src/assets/images/room-widgets/camera-widget/camera-spritesheet.png create mode 100644 apps/frontend/src/assets/images/room-widgets/camera-widget/viewfinder.png create mode 100644 apps/frontend/src/assets/images/room-widgets/dimmer-widget/dimmer_banner.png create mode 100644 apps/frontend/src/assets/images/room-widgets/engraving-lock-widget/engraving-lock-spritesheet.png create mode 100644 apps/frontend/src/assets/images/room-widgets/exchange-credit/exchange-credit-image.png create mode 100644 apps/frontend/src/assets/images/room-widgets/furni-context-menu/monsterplant-preview.png create mode 100644 apps/frontend/src/assets/images/room-widgets/mannequin-widget/mannequin-spritesheet.png create mode 100644 apps/frontend/src/assets/images/room-widgets/playlist-editor/disk_2.png create mode 100644 apps/frontend/src/assets/images/room-widgets/playlist-editor/disk_image.png create mode 100644 apps/frontend/src/assets/images/room-widgets/playlist-editor/move.png create mode 100644 apps/frontend/src/assets/images/room-widgets/playlist-editor/pause-btn.png create mode 100644 apps/frontend/src/assets/images/room-widgets/playlist-editor/pause.png create mode 100644 apps/frontend/src/assets/images/room-widgets/playlist-editor/playing.png create mode 100644 apps/frontend/src/assets/images/room-widgets/playlist-editor/preview.png create mode 100644 apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-blue.png create mode 100644 apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-christmas.png create mode 100644 apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-close.png create mode 100644 apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-dreams.png create mode 100644 apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-green.png create mode 100644 apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-heart.png create mode 100644 apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-juninas.png create mode 100644 apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-pink.png create mode 100644 apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-shakesp.png create mode 100644 apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-spritesheet.png create mode 100644 apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-trash.png create mode 100644 apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-yellow.png create mode 100644 apps/frontend/src/assets/images/room-widgets/thumbnail-widget/thumbnail-camera-spritesheet.png create mode 100644 apps/frontend/src/assets/images/room-widgets/trophy-widget/trophy-spritesheet.png create mode 100644 apps/frontend/src/assets/images/room-widgets/wordquiz-widget/thumbs-down-small.png create mode 100644 apps/frontend/src/assets/images/room-widgets/wordquiz-widget/thumbs-down.png create mode 100644 apps/frontend/src/assets/images/room-widgets/wordquiz-widget/thumbs-up-small.png create mode 100644 apps/frontend/src/assets/images/room-widgets/wordquiz-widget/thumbs-up.png create mode 100644 apps/frontend/src/assets/images/room-widgets/youtube-widget/next.png create mode 100644 apps/frontend/src/assets/images/room-widgets/youtube-widget/prev.png create mode 100644 apps/frontend/src/assets/images/stackhelper/slider-background.png create mode 100644 apps/frontend/src/assets/images/stackhelper/slider-pointer.png create mode 100644 apps/frontend/src/assets/images/toolbar/arrow.png create mode 100644 apps/frontend/src/assets/images/toolbar/friend-search.png create mode 100644 apps/frontend/src/assets/images/toolbar/icons/buildersclub.png create mode 100644 apps/frontend/src/assets/images/toolbar/icons/camera.png create mode 100644 apps/frontend/src/assets/images/toolbar/icons/catalog.png create mode 100644 apps/frontend/src/assets/images/toolbar/icons/friend_all.png create mode 100644 apps/frontend/src/assets/images/toolbar/icons/friend_head.png create mode 100644 apps/frontend/src/assets/images/toolbar/icons/friend_search.png create mode 100644 apps/frontend/src/assets/images/toolbar/icons/game.png create mode 100644 apps/frontend/src/assets/images/toolbar/icons/habbo.png create mode 100644 apps/frontend/src/assets/images/toolbar/icons/house.png create mode 100644 apps/frontend/src/assets/images/toolbar/icons/inventory.png create mode 100644 apps/frontend/src/assets/images/toolbar/icons/joinroom.png create mode 100644 apps/frontend/src/assets/images/toolbar/icons/me-menu/achievements.png create mode 100644 apps/frontend/src/assets/images/toolbar/icons/me-menu/clothing.png create mode 100644 apps/frontend/src/assets/images/toolbar/icons/me-menu/cog.png create mode 100644 apps/frontend/src/assets/images/toolbar/icons/me-menu/forums.png create mode 100644 apps/frontend/src/assets/images/toolbar/icons/me-menu/helper-tool.png create mode 100644 apps/frontend/src/assets/images/toolbar/icons/me-menu/my-rooms.png create mode 100644 apps/frontend/src/assets/images/toolbar/icons/me-menu/profile.png create mode 100644 apps/frontend/src/assets/images/toolbar/icons/me-menu/rooms.png create mode 100644 apps/frontend/src/assets/images/toolbar/icons/me-menu/talents.png create mode 100644 apps/frontend/src/assets/images/toolbar/icons/message.png create mode 100644 apps/frontend/src/assets/images/toolbar/icons/message_unsee.gif create mode 100644 apps/frontend/src/assets/images/toolbar/icons/modtools.png create mode 100644 apps/frontend/src/assets/images/toolbar/icons/rooms.png create mode 100644 apps/frontend/src/assets/images/toolbar/icons/sendmessage.png create mode 100644 apps/frontend/src/assets/images/unique/catalog-info-amount-bg.png create mode 100644 apps/frontend/src/assets/images/unique/catalog-info-sold-out.png create mode 100644 apps/frontend/src/assets/images/unique/grid-bg-glass.png create mode 100644 apps/frontend/src/assets/images/unique/grid-bg-sold-out.png create mode 100644 apps/frontend/src/assets/images/unique/grid-bg.png create mode 100644 apps/frontend/src/assets/images/unique/grid-count-bg.png create mode 100644 apps/frontend/src/assets/images/unique/inventory-info-amount-bg.png create mode 100644 apps/frontend/src/assets/images/unique/numbers.png create mode 100644 apps/frontend/src/assets/images/wired/card-action-corners.png create mode 100644 apps/frontend/src/assets/images/wired/icon_action.png create mode 100644 apps/frontend/src/assets/images/wired/icon_condition.png create mode 100644 apps/frontend/src/assets/images/wired/icon_trigger.png create mode 100644 apps/frontend/src/assets/images/wired/icon_wired_around.png create mode 100644 apps/frontend/src/assets/images/wired/icon_wired_left_right.png create mode 100644 apps/frontend/src/assets/images/wired/icon_wired_north_east.png create mode 100644 apps/frontend/src/assets/images/wired/icon_wired_north_west.png create mode 100644 apps/frontend/src/assets/images/wired/icon_wired_rotate_clockwise.png create mode 100644 apps/frontend/src/assets/images/wired/icon_wired_rotate_counter_clockwise.png create mode 100644 apps/frontend/src/assets/images/wired/icon_wired_south_east.png create mode 100644 apps/frontend/src/assets/images/wired/icon_wired_south_west.png create mode 100644 apps/frontend/src/assets/images/wired/icon_wired_up_down.png create mode 100644 apps/frontend/src/assets/styles/bootstrap/_accordion.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/_alert.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/_badge.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/_breadcrumb.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/_button-group.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/_buttons.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/_card.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/_carousel.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/_close.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/_containers.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/_dropdown.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/_forms.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/_functions.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/_grid.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/_helpers.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/_images.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/_list-group.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/_mixins.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/_modal.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/_nav.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/_navbar.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/_offcanvas.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/_pagination.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/_placeholders.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/_popover.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/_progress.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/_reboot.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/_root.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/_spinners.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/_tables.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/_toasts.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/_tooltip.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/_transitions.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/_type.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/_utilities.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/_variables.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/bootstrap-grid.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/bootstrap-reboot.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/bootstrap-utilities.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/bootstrap.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/forms/_floating-labels.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/forms/_form-check.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/forms/_form-control.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/forms/_form-range.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/forms/_form-select.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/forms/_form-text.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/forms/_input-group.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/forms/_labels.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/forms/_validation.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/helpers/_clearfix.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/helpers/_colored-links.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/helpers/_position.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/helpers/_ratio.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/helpers/_stacks.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/helpers/_stretched-link.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/helpers/_text-truncation.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/helpers/_visually-hidden.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/helpers/_vr.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/mixins/_alert.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/mixins/_backdrop.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/mixins/_border-radius.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/mixins/_box-shadow.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/mixins/_breakpoints.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/mixins/_buttons.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/mixins/_caret.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/mixins/_clearfix.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/mixins/_color-scheme.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/mixins/_container.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/mixins/_deprecate.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/mixins/_forms.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/mixins/_gradients.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/mixins/_grid.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/mixins/_image.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/mixins/_list-group.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/mixins/_lists.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/mixins/_pagination.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/mixins/_reset-text.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/mixins/_resize.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/mixins/_table-variants.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/mixins/_text-truncate.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/mixins/_transition.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/mixins/_utilities.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/mixins/_visually-hidden.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/utilities/_api.scss create mode 100644 apps/frontend/src/assets/styles/bootstrap/vendor/_rfs.scss create mode 100644 apps/frontend/src/assets/styles/fonts.scss create mode 100644 apps/frontend/src/assets/styles/icons.scss create mode 100644 apps/frontend/src/assets/styles/index.scss create mode 100644 apps/frontend/src/assets/styles/scrollbars.scss create mode 100644 apps/frontend/src/assets/styles/slider.scss create mode 100644 apps/frontend/src/assets/styles/utils.scss create mode 100644 apps/frontend/src/assets/webfonts/Ubuntu-C.ttf create mode 100644 apps/frontend/src/assets/webfonts/Ubuntu-b.ttf create mode 100644 apps/frontend/src/assets/webfonts/Ubuntu-i.ttf create mode 100644 apps/frontend/src/assets/webfonts/Ubuntu-ib.ttf create mode 100644 apps/frontend/src/assets/webfonts/Ubuntu-m.ttf create mode 100644 apps/frontend/src/assets/webfonts/Ubuntu.ttf create mode 100644 apps/frontend/src/common/AutoGrid.tsx create mode 100644 apps/frontend/src/common/Base.tsx create mode 100644 apps/frontend/src/common/Button.tsx create mode 100644 apps/frontend/src/common/ButtonGroup.tsx create mode 100644 apps/frontend/src/common/Column.tsx create mode 100644 apps/frontend/src/common/Flex.tsx create mode 100644 apps/frontend/src/common/FormGroup.tsx create mode 100644 apps/frontend/src/common/Grid.tsx create mode 100644 apps/frontend/src/common/GridContext.tsx create mode 100644 apps/frontend/src/common/HorizontalRule.tsx create mode 100644 apps/frontend/src/common/InfiniteScroll.tsx create mode 100644 apps/frontend/src/common/Text.tsx create mode 100644 apps/frontend/src/common/card/NitroCardContentView.tsx create mode 100644 apps/frontend/src/common/card/NitroCardContext.tsx create mode 100644 apps/frontend/src/common/card/NitroCardHeaderView.tsx create mode 100644 apps/frontend/src/common/card/NitroCardSubHeaderView.tsx create mode 100644 apps/frontend/src/common/card/NitroCardView.scss create mode 100644 apps/frontend/src/common/card/NitroCardView.tsx create mode 100644 apps/frontend/src/common/card/accordion/NitroCardAccordionContext.tsx create mode 100644 apps/frontend/src/common/card/accordion/NitroCardAccordionItemView.tsx create mode 100644 apps/frontend/src/common/card/accordion/NitroCardAccordionSetView.tsx create mode 100644 apps/frontend/src/common/card/accordion/NitroCardAccordionView.tsx create mode 100644 apps/frontend/src/common/card/accordion/index.ts create mode 100644 apps/frontend/src/common/card/index.ts create mode 100644 apps/frontend/src/common/card/tabs/NitroCardTabsItemView.tsx create mode 100644 apps/frontend/src/common/card/tabs/NitroCardTabsView.tsx create mode 100644 apps/frontend/src/common/card/tabs/index.ts create mode 100644 apps/frontend/src/common/classNames.ts create mode 100644 apps/frontend/src/common/draggable-window/DraggableWindow.tsx create mode 100644 apps/frontend/src/common/draggable-window/DraggableWindowPosition.ts create mode 100644 apps/frontend/src/common/draggable-window/index.ts create mode 100644 apps/frontend/src/common/index.scss create mode 100644 apps/frontend/src/common/index.ts create mode 100644 apps/frontend/src/common/layout/LayoutAvatarImageView.tsx create mode 100644 apps/frontend/src/common/layout/LayoutBackgroundImage.tsx create mode 100644 apps/frontend/src/common/layout/LayoutBadgeImageView.tsx create mode 100644 apps/frontend/src/common/layout/LayoutCounterTimeView.tsx create mode 100644 apps/frontend/src/common/layout/LayoutCurrencyIcon.tsx create mode 100644 apps/frontend/src/common/layout/LayoutFurniIconImageView.tsx create mode 100644 apps/frontend/src/common/layout/LayoutFurniImageView.tsx create mode 100644 apps/frontend/src/common/layout/LayoutGiftTagView.tsx create mode 100644 apps/frontend/src/common/layout/LayoutGridItem.tsx create mode 100644 apps/frontend/src/common/layout/LayoutImage.tsx create mode 100644 apps/frontend/src/common/layout/LayoutItemCountView.tsx create mode 100644 apps/frontend/src/common/layout/LayoutLoadingSpinnerView.tsx create mode 100644 apps/frontend/src/common/layout/LayoutMiniCameraView.tsx create mode 100644 apps/frontend/src/common/layout/LayoutNotificationAlertView.tsx create mode 100644 apps/frontend/src/common/layout/LayoutNotificationBubbleView.tsx create mode 100644 apps/frontend/src/common/layout/LayoutPetImageView.tsx create mode 100644 apps/frontend/src/common/layout/LayoutPrizeProductImageView.tsx create mode 100644 apps/frontend/src/common/layout/LayoutProgressBar.tsx create mode 100644 apps/frontend/src/common/layout/LayoutRarityLevelView.tsx create mode 100644 apps/frontend/src/common/layout/LayoutRoomPreviewerView.tsx create mode 100644 apps/frontend/src/common/layout/LayoutRoomThumbnailView.tsx create mode 100644 apps/frontend/src/common/layout/LayoutTrophyView.tsx create mode 100644 apps/frontend/src/common/layout/UserProfileIconView.tsx create mode 100644 apps/frontend/src/common/layout/index.ts create mode 100644 apps/frontend/src/common/layout/limited-edition/LayoutLimitedEditionCompactPlateView.tsx create mode 100644 apps/frontend/src/common/layout/limited-edition/LayoutLimitedEditionCompletePlateView.tsx create mode 100644 apps/frontend/src/common/layout/limited-edition/LayoutLimitedEditionStyledNumberView.tsx create mode 100644 apps/frontend/src/common/layout/limited-edition/index.ts create mode 100644 apps/frontend/src/common/transitions/TransitionAnimation.tsx create mode 100644 apps/frontend/src/common/transitions/TransitionAnimationStyles.ts create mode 100644 apps/frontend/src/common/transitions/TransitionAnimationTypes.ts create mode 100644 apps/frontend/src/common/transitions/index.ts create mode 100644 apps/frontend/src/common/types/AlignItemType.ts create mode 100644 apps/frontend/src/common/types/AlignSelfType.ts create mode 100644 apps/frontend/src/common/types/ButtonSizeType.ts create mode 100644 apps/frontend/src/common/types/ColorVariantType.ts create mode 100644 apps/frontend/src/common/types/ColumnSizesType.ts create mode 100644 apps/frontend/src/common/types/DisplayType.ts create mode 100644 apps/frontend/src/common/types/FloatType.ts create mode 100644 apps/frontend/src/common/types/FontSizeType.ts create mode 100644 apps/frontend/src/common/types/FontWeightType.ts create mode 100644 apps/frontend/src/common/types/JustifyContentType.ts create mode 100644 apps/frontend/src/common/types/OverflowType.ts create mode 100644 apps/frontend/src/common/types/PositionType.ts create mode 100644 apps/frontend/src/common/types/SpacingType.ts create mode 100644 apps/frontend/src/common/types/TextAlignType.ts create mode 100644 apps/frontend/src/common/types/index.ts create mode 100644 apps/frontend/src/common/utils/CreateTransitionToIcon.ts create mode 100644 apps/frontend/src/common/utils/FriendlyTimeView.tsx create mode 100644 apps/frontend/src/common/utils/index.ts create mode 100644 apps/frontend/src/components/achievements/AchievementsView.scss create mode 100644 apps/frontend/src/components/achievements/AchievementsView.tsx create mode 100644 apps/frontend/src/components/achievements/views/AchievementBadgeView.tsx create mode 100644 apps/frontend/src/components/achievements/views/AchievementCategoryView.tsx create mode 100644 apps/frontend/src/components/achievements/views/AchievementDetailsView.tsx create mode 100644 apps/frontend/src/components/achievements/views/achievement-list/AchievementListItemView.tsx create mode 100644 apps/frontend/src/components/achievements/views/achievement-list/AchievementListView.tsx create mode 100644 apps/frontend/src/components/achievements/views/achievement-list/index.ts create mode 100644 apps/frontend/src/components/achievements/views/category-list/AchievementsCategoryListItemView.tsx create mode 100644 apps/frontend/src/components/achievements/views/category-list/AchievementsCategoryListView.tsx create mode 100644 apps/frontend/src/components/achievements/views/category-list/index.ts create mode 100644 apps/frontend/src/components/achievements/views/index.ts create mode 100644 apps/frontend/src/components/avatar-editor/AvatarEditorView.scss create mode 100644 apps/frontend/src/components/avatar-editor/AvatarEditorView.tsx create mode 100644 apps/frontend/src/components/avatar-editor/views/AvatarEditorFigurePreviewView.tsx create mode 100644 apps/frontend/src/components/avatar-editor/views/AvatarEditorIcon.tsx create mode 100644 apps/frontend/src/components/avatar-editor/views/AvatarEditorModelView.tsx create mode 100644 apps/frontend/src/components/avatar-editor/views/AvatarEditorWardrobeView.tsx create mode 100644 apps/frontend/src/components/avatar-editor/views/figure-set/AvatarEditorFigureSetItemView.tsx create mode 100644 apps/frontend/src/components/avatar-editor/views/figure-set/AvatarEditorFigureSetView.tsx create mode 100644 apps/frontend/src/components/avatar-editor/views/palette-set/AvatarEditorPaletteSetItemView.tsx create mode 100644 apps/frontend/src/components/avatar-editor/views/palette-set/AvatarEditorPaletteSetView.tsx create mode 100644 apps/frontend/src/components/camera/CameraWidgetView.scss create mode 100644 apps/frontend/src/components/camera/CameraWidgetView.tsx create mode 100644 apps/frontend/src/components/camera/views/CameraWidgetCaptureView.tsx create mode 100644 apps/frontend/src/components/camera/views/CameraWidgetCheckoutView.tsx create mode 100644 apps/frontend/src/components/camera/views/CameraWidgetShowPhotoView.tsx create mode 100644 apps/frontend/src/components/camera/views/editor/CameraWidgetEditorView.tsx create mode 100644 apps/frontend/src/components/camera/views/editor/effect-list/CameraWidgetEffectListItemView.tsx create mode 100644 apps/frontend/src/components/camera/views/editor/effect-list/CameraWidgetEffectListView.tsx create mode 100644 apps/frontend/src/components/campaign/CalendarItemView.tsx create mode 100644 apps/frontend/src/components/campaign/CalendarView.tsx create mode 100644 apps/frontend/src/components/campaign/CampaignView.scss create mode 100644 apps/frontend/src/components/campaign/CampaignView.tsx create mode 100644 apps/frontend/src/components/catalog/CatalogView.scss create mode 100644 apps/frontend/src/components/catalog/CatalogView.tsx create mode 100644 apps/frontend/src/components/catalog/views/CatalogPurchaseConfirmView.tsx create mode 100644 apps/frontend/src/components/catalog/views/catalog-header/CatalogHeaderView.tsx create mode 100644 apps/frontend/src/components/catalog/views/catalog-icon/CatalogIconView.tsx create mode 100644 apps/frontend/src/components/catalog/views/catalog-room-previewer/CatalogRoomPreviewerView.tsx create mode 100644 apps/frontend/src/components/catalog/views/gift/CatalogGiftView.tsx create mode 100644 apps/frontend/src/components/catalog/views/navigation/CatalogNavigationItemView.tsx create mode 100644 apps/frontend/src/components/catalog/views/navigation/CatalogNavigationSetView.tsx create mode 100644 apps/frontend/src/components/catalog/views/navigation/CatalogNavigationView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/common/CatalogGridOfferView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/common/CatalogRedeemVoucherView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/common/CatalogSearchView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/layout/CatalogLayout.types.ts create mode 100644 apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutBadgeDisplayView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutColorGroupingView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutGuildCustomFurniView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutGuildForumView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutGuildFrontpageView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutInfoLoyaltyView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutPets2View.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutPets3View.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutRoomAdsView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutRoomBundleView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutSingleBundleView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutSoundMachineView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutSpacesView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutTrophiesView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutVipBuyView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/layout/GetCatalogLayout.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/layout/frontpage4/CatalogLayoutFrontPageItemView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/layout/frontpage4/CatalogLayoutFrontpage4View.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/layout/marketplace/CatalogLayoutMarketplaceItemView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/layout/marketplace/CatalogLayoutMarketplaceOwnItemsView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/layout/marketplace/CatalogLayoutMarketplacePublicItemsView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/layout/marketplace/CatalogLayoutMarketplaceSearchFormView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/layout/marketplace/MarketplacePostOfferView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/layout/pets/CatalogLayoutPetView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/layout/vip-gifts/CatalogLayoutVipGiftsView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/layout/vip-gifts/VipGiftItemView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/widgets/CatalogAddOnBadgeWidgetView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/widgets/CatalogBadgeSelectorWidgetView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/widgets/CatalogBundleGridWidgetView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/widgets/CatalogFirstProductSelectorWidgetView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/widgets/CatalogGuildBadgeWidgetView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/widgets/CatalogGuildSelectorWidgetView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/widgets/CatalogItemGridWidgetView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/widgets/CatalogLimitedItemWidgetView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/widgets/CatalogPriceDisplayWidgetView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/widgets/CatalogPurchaseWidgetView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/widgets/CatalogSimplePriceWidgetView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/widgets/CatalogSingleViewWidgetView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/widgets/CatalogSpacesWidgetView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/widgets/CatalogSpinnerWidgetView.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/widgets/CatalogTotalPriceWidget.tsx create mode 100644 apps/frontend/src/components/catalog/views/page/widgets/CatalogViewProductWidgetView.tsx create mode 100644 apps/frontend/src/components/catalog/views/targeted-offer/Offer.scss create mode 100644 apps/frontend/src/components/catalog/views/targeted-offer/OfferBubbleView.tsx create mode 100644 apps/frontend/src/components/catalog/views/targeted-offer/OfferView.tsx create mode 100644 apps/frontend/src/components/catalog/views/targeted-offer/OfferWindowView.tsx create mode 100644 apps/frontend/src/components/chat-history/ChatHistoryView.scss create mode 100644 apps/frontend/src/components/chat-history/ChatHistoryView.tsx create mode 100644 apps/frontend/src/components/floorplan-editor/FloorplanEditorContext.tsx create mode 100644 apps/frontend/src/components/floorplan-editor/FloorplanEditorView.scss create mode 100644 apps/frontend/src/components/floorplan-editor/FloorplanEditorView.tsx create mode 100644 apps/frontend/src/components/floorplan-editor/common/ActionSettings.ts create mode 100644 apps/frontend/src/components/floorplan-editor/common/Constants.ts create mode 100644 apps/frontend/src/components/floorplan-editor/common/ConvertMapToString.ts create mode 100644 apps/frontend/src/components/floorplan-editor/common/FloorplanEditor.ts create mode 100644 apps/frontend/src/components/floorplan-editor/common/IFloorplanSettings.ts create mode 100644 apps/frontend/src/components/floorplan-editor/common/IVisualizationSettings.ts create mode 100644 apps/frontend/src/components/floorplan-editor/common/Tile.ts create mode 100644 apps/frontend/src/components/floorplan-editor/common/Utils.ts create mode 100644 apps/frontend/src/components/floorplan-editor/views/FloorplanCanvasView.tsx create mode 100644 apps/frontend/src/components/floorplan-editor/views/FloorplanImportExportView.tsx create mode 100644 apps/frontend/src/components/floorplan-editor/views/FloorplanOptionsView.tsx create mode 100644 apps/frontend/src/components/friends/FriendsView.scss create mode 100644 apps/frontend/src/components/friends/FriendsView.tsx create mode 100644 apps/frontend/src/components/friends/views/friends-bar/FriendBarItemView.tsx create mode 100644 apps/frontend/src/components/friends/views/friends-bar/FriendsBarView.tsx create mode 100644 apps/frontend/src/components/friends/views/friends-list/FriendsListRemoveConfirmationView.tsx create mode 100644 apps/frontend/src/components/friends/views/friends-list/FriendsListRoomInviteView.tsx create mode 100644 apps/frontend/src/components/friends/views/friends-list/FriendsListSearchView.tsx create mode 100644 apps/frontend/src/components/friends/views/friends-list/FriendsListView.tsx create mode 100644 apps/frontend/src/components/friends/views/friends-list/friends-list-group/FriendsListGroupItemView.tsx create mode 100644 apps/frontend/src/components/friends/views/friends-list/friends-list-group/FriendsListGroupView.tsx create mode 100644 apps/frontend/src/components/friends/views/friends-list/friends-list-request/FriendsListRequestItemView.tsx create mode 100644 apps/frontend/src/components/friends/views/friends-list/friends-list-request/FriendsListRequestView.tsx create mode 100644 apps/frontend/src/components/friends/views/messenger/FriendsMessengerView.tsx create mode 100644 apps/frontend/src/components/friends/views/messenger/messenger-thread/FriendsMessengerThreadGroup.tsx create mode 100644 apps/frontend/src/components/friends/views/messenger/messenger-thread/FriendsMessengerThreadView.tsx create mode 100644 apps/frontend/src/components/game-center/GameCenterView.scss create mode 100644 apps/frontend/src/components/game-center/GameCenterView.tsx create mode 100644 apps/frontend/src/components/game-center/views/GameListView.tsx create mode 100644 apps/frontend/src/components/game-center/views/GameStageView.tsx create mode 100644 apps/frontend/src/components/game-center/views/GameView.tsx create mode 100644 apps/frontend/src/components/groups/GroupView.scss create mode 100644 apps/frontend/src/components/groups/GroupsView.tsx create mode 100644 apps/frontend/src/components/groups/views/GroupBadgeCreatorView.tsx create mode 100644 apps/frontend/src/components/groups/views/GroupCreatorView.tsx create mode 100644 apps/frontend/src/components/groups/views/GroupInformationStandaloneView.tsx create mode 100644 apps/frontend/src/components/groups/views/GroupInformationView.tsx create mode 100644 apps/frontend/src/components/groups/views/GroupManagerView.tsx create mode 100644 apps/frontend/src/components/groups/views/GroupMembersView.tsx create mode 100644 apps/frontend/src/components/groups/views/GroupRoomInformationView.tsx create mode 100644 apps/frontend/src/components/groups/views/tabs/GroupTabBadgeView.tsx create mode 100644 apps/frontend/src/components/groups/views/tabs/GroupTabColorsView.tsx create mode 100644 apps/frontend/src/components/groups/views/tabs/GroupTabCreatorConfirmationView.tsx create mode 100644 apps/frontend/src/components/groups/views/tabs/GroupTabIdentityView.tsx create mode 100644 apps/frontend/src/components/groups/views/tabs/GroupTabSettingsView.tsx create mode 100644 apps/frontend/src/components/guide-tool/GuideToolView.scss create mode 100644 apps/frontend/src/components/guide-tool/GuideToolView.tsx create mode 100644 apps/frontend/src/components/guide-tool/views/GuideToolAcceptView.tsx create mode 100644 apps/frontend/src/components/guide-tool/views/GuideToolMenuView.tsx create mode 100644 apps/frontend/src/components/guide-tool/views/GuideToolOngoingView.tsx create mode 100644 apps/frontend/src/components/guide-tool/views/GuideToolUserCreateRequestView.tsx create mode 100644 apps/frontend/src/components/guide-tool/views/GuideToolUserFeedbackView.tsx create mode 100644 apps/frontend/src/components/guide-tool/views/GuideToolUserNoHelpersView.tsx create mode 100644 apps/frontend/src/components/guide-tool/views/GuideToolUserPendingView.tsx create mode 100644 apps/frontend/src/components/guide-tool/views/GuideToolUserSomethingWrogView.tsx create mode 100644 apps/frontend/src/components/guide-tool/views/GuideToolUserThanksView.tsx create mode 100644 apps/frontend/src/components/hc-center/HcCenterView.scss create mode 100644 apps/frontend/src/components/hc-center/HcCenterView.tsx create mode 100644 apps/frontend/src/components/help/HelpView.scss create mode 100644 apps/frontend/src/components/help/HelpView.tsx create mode 100644 apps/frontend/src/components/help/views/DescribeReportView.tsx create mode 100644 apps/frontend/src/components/help/views/HelpIndexView.tsx create mode 100644 apps/frontend/src/components/help/views/ReportSummaryView.tsx create mode 100644 apps/frontend/src/components/help/views/SanctionStatusView.tsx create mode 100644 apps/frontend/src/components/help/views/SelectReportedChatsView.tsx create mode 100644 apps/frontend/src/components/help/views/SelectReportedUserView.tsx create mode 100644 apps/frontend/src/components/help/views/SelectTopicView.tsx create mode 100644 apps/frontend/src/components/help/views/name-change/NameChangeConfirmationView.tsx create mode 100644 apps/frontend/src/components/help/views/name-change/NameChangeInitView.tsx create mode 100644 apps/frontend/src/components/help/views/name-change/NameChangeInputView.tsx create mode 100644 apps/frontend/src/components/help/views/name-change/NameChangeView.tsx create mode 100644 apps/frontend/src/components/help/views/name-change/NameChangeView.types.ts create mode 100644 apps/frontend/src/components/hotel-view/HotelView.scss create mode 100644 apps/frontend/src/components/hotel-view/HotelView.tsx create mode 100644 apps/frontend/src/components/hotel-view/views/widgets/GetWidgetLayout.tsx create mode 100644 apps/frontend/src/components/hotel-view/views/widgets/HotelViewWidgets.scss create mode 100644 apps/frontend/src/components/hotel-view/views/widgets/WidgetSlotView.tsx create mode 100644 apps/frontend/src/components/hotel-view/views/widgets/bonus-rare/BonusRareWidgetView.scss create mode 100644 apps/frontend/src/components/hotel-view/views/widgets/bonus-rare/BonusRareWidgetView.tsx create mode 100644 apps/frontend/src/components/hotel-view/views/widgets/hall-of-fame-item/HallOfFameItemView.tsx create mode 100644 apps/frontend/src/components/hotel-view/views/widgets/hall-of-fame/HallOfFameWidgetView.scss create mode 100644 apps/frontend/src/components/hotel-view/views/widgets/hall-of-fame/HallOfFameWidgetView.tsx create mode 100644 apps/frontend/src/components/hotel-view/views/widgets/hall-of-fame/HallOfFameWidgetView.types.ts create mode 100644 apps/frontend/src/components/hotel-view/views/widgets/promo-article/PromoArticleWidgetView.scss create mode 100644 apps/frontend/src/components/hotel-view/views/widgets/promo-article/PromoArticleWidgetView.tsx create mode 100644 apps/frontend/src/components/hotel-view/views/widgets/widget-container/WidgetContainerView.scss create mode 100644 apps/frontend/src/components/hotel-view/views/widgets/widget-container/WidgetContainerView.tsx create mode 100644 apps/frontend/src/components/index.scss create mode 100644 apps/frontend/src/components/inventory/InventoryView.scss create mode 100644 apps/frontend/src/components/inventory/InventoryView.tsx create mode 100644 apps/frontend/src/components/inventory/views/InventoryCategoryEmptyView.tsx create mode 100644 apps/frontend/src/components/inventory/views/badge/InventoryBadgeItemView.tsx create mode 100644 apps/frontend/src/components/inventory/views/badge/InventoryBadgeView.tsx create mode 100644 apps/frontend/src/components/inventory/views/bot/InventoryBotItemView.tsx create mode 100644 apps/frontend/src/components/inventory/views/bot/InventoryBotView.tsx create mode 100644 apps/frontend/src/components/inventory/views/furniture/InventoryFurnitureItemView.tsx create mode 100644 apps/frontend/src/components/inventory/views/furniture/InventoryFurnitureSearchView.tsx create mode 100644 apps/frontend/src/components/inventory/views/furniture/InventoryFurnitureView.tsx create mode 100644 apps/frontend/src/components/inventory/views/furniture/InventoryTradeView.tsx create mode 100644 apps/frontend/src/components/inventory/views/pet/InventoryPetItemView.tsx create mode 100644 apps/frontend/src/components/inventory/views/pet/InventoryPetView.tsx create mode 100644 apps/frontend/src/components/loading/LoadingView.scss create mode 100644 apps/frontend/src/components/loading/LoadingView.tsx create mode 100644 apps/frontend/src/components/main/MainView.tsx create mode 100644 apps/frontend/src/components/mod-tools/ModToolsView.scss create mode 100644 apps/frontend/src/components/mod-tools/ModToolsView.tsx create mode 100644 apps/frontend/src/components/mod-tools/views/chatlog/ChatlogRecord.ts create mode 100644 apps/frontend/src/components/mod-tools/views/chatlog/ChatlogView.tsx create mode 100644 apps/frontend/src/components/mod-tools/views/room/ModToolsChatlogView.tsx create mode 100644 apps/frontend/src/components/mod-tools/views/room/ModToolsRoomView.tsx create mode 100644 apps/frontend/src/components/mod-tools/views/tickets/CfhChatlogView.tsx create mode 100644 apps/frontend/src/components/mod-tools/views/tickets/ModToolsIssueInfoView.tsx create mode 100644 apps/frontend/src/components/mod-tools/views/tickets/ModToolsMyIssuesTabView.tsx create mode 100644 apps/frontend/src/components/mod-tools/views/tickets/ModToolsOpenIssuesTabView.tsx create mode 100644 apps/frontend/src/components/mod-tools/views/tickets/ModToolsPickedIssuesTabView.tsx create mode 100644 apps/frontend/src/components/mod-tools/views/tickets/ModToolsTicketsView.tsx create mode 100644 apps/frontend/src/components/mod-tools/views/user/ModToolsUserChatlogView.tsx create mode 100644 apps/frontend/src/components/mod-tools/views/user/ModToolsUserModActionView.tsx create mode 100644 apps/frontend/src/components/mod-tools/views/user/ModToolsUserRoomVisitsView.tsx create mode 100644 apps/frontend/src/components/mod-tools/views/user/ModToolsUserSendMessageView.tsx create mode 100644 apps/frontend/src/components/mod-tools/views/user/ModToolsUserView.tsx create mode 100644 apps/frontend/src/components/navigator/NavigatorView.scss create mode 100644 apps/frontend/src/components/navigator/NavigatorView.tsx create mode 100644 apps/frontend/src/components/navigator/views/NavigatorDoorStateView.tsx create mode 100644 apps/frontend/src/components/navigator/views/NavigatorRoomCreatorView.tsx create mode 100644 apps/frontend/src/components/navigator/views/NavigatorRoomInfoView.tsx create mode 100644 apps/frontend/src/components/navigator/views/NavigatorRoomLinkView.tsx create mode 100644 apps/frontend/src/components/navigator/views/room-settings/NavigatorRoomSettingsAccessTabView.tsx create mode 100644 apps/frontend/src/components/navigator/views/room-settings/NavigatorRoomSettingsBasicTabView.tsx create mode 100644 apps/frontend/src/components/navigator/views/room-settings/NavigatorRoomSettingsModTabView.tsx create mode 100644 apps/frontend/src/components/navigator/views/room-settings/NavigatorRoomSettingsRightsTabView.tsx create mode 100644 apps/frontend/src/components/navigator/views/room-settings/NavigatorRoomSettingsView.tsx create mode 100644 apps/frontend/src/components/navigator/views/room-settings/NavigatorRoomSettingsVipChatTabView.tsx create mode 100644 apps/frontend/src/components/navigator/views/search/NavigatorSearchResultItemInfoView.tsx create mode 100644 apps/frontend/src/components/navigator/views/search/NavigatorSearchResultItemView.tsx create mode 100644 apps/frontend/src/components/navigator/views/search/NavigatorSearchResultView.tsx create mode 100644 apps/frontend/src/components/navigator/views/search/NavigatorSearchView.tsx create mode 100644 apps/frontend/src/components/nitropedia/NitropediaView.scss create mode 100644 apps/frontend/src/components/nitropedia/NitropediaView.tsx create mode 100644 apps/frontend/src/components/notification-center/NotificationCenterView.scss create mode 100644 apps/frontend/src/components/notification-center/NotificationCenterView.tsx create mode 100644 apps/frontend/src/components/notification-center/views/alert-layouts/GetAlertLayout.tsx create mode 100644 apps/frontend/src/components/notification-center/views/alert-layouts/NitroSystemAlertView.tsx create mode 100644 apps/frontend/src/components/notification-center/views/alert-layouts/NotificationDefaultAlertView.tsx create mode 100644 apps/frontend/src/components/notification-center/views/alert-layouts/NotificationSearchAlertView.tsx create mode 100644 apps/frontend/src/components/notification-center/views/bubble-layouts/GetBubbleLayout.tsx create mode 100644 apps/frontend/src/components/notification-center/views/bubble-layouts/NotificationClubGiftBubbleView.tsx create mode 100644 apps/frontend/src/components/notification-center/views/bubble-layouts/NotificationDefaultBubbleView.tsx create mode 100644 apps/frontend/src/components/notification-center/views/confirm-layouts/GetConfirmLayout.tsx create mode 100644 apps/frontend/src/components/notification-center/views/confirm-layouts/NotificationDefaultConfirmView.tsx create mode 100644 apps/frontend/src/components/purse/PurseView.scss create mode 100644 apps/frontend/src/components/purse/PurseView.tsx create mode 100644 apps/frontend/src/components/purse/views/CurrencyView.tsx create mode 100644 apps/frontend/src/components/purse/views/SeasonalView.tsx create mode 100644 apps/frontend/src/components/right-side/RightSideView.scss create mode 100644 apps/frontend/src/components/right-side/RightSideView.tsx create mode 100644 apps/frontend/src/components/room/RoomView.scss create mode 100644 apps/frontend/src/components/room/RoomView.tsx create mode 100644 apps/frontend/src/components/room/spectator/RoomSpectatorView.scss create mode 100644 apps/frontend/src/components/room/spectator/RoomSpectatorView.tsx create mode 100644 apps/frontend/src/components/room/widgets/RoomWidgets.scss create mode 100644 apps/frontend/src/components/room/widgets/RoomWidgetsView.tsx create mode 100644 apps/frontend/src/components/room/widgets/avatar-info/AvatarInfoPetTrainingPanelView.tsx create mode 100644 apps/frontend/src/components/room/widgets/avatar-info/AvatarInfoRentableBotChatView.tsx create mode 100644 apps/frontend/src/components/room/widgets/avatar-info/AvatarInfoUseProductConfirmView.tsx create mode 100644 apps/frontend/src/components/room/widgets/avatar-info/AvatarInfoUseProductView.tsx create mode 100644 apps/frontend/src/components/room/widgets/avatar-info/AvatarInfoWidgetView.scss create mode 100644 apps/frontend/src/components/room/widgets/avatar-info/AvatarInfoWidgetView.tsx create mode 100644 apps/frontend/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetBotView.tsx create mode 100644 apps/frontend/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx create mode 100644 apps/frontend/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetPetView.tsx create mode 100644 apps/frontend/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetRentableBotView.tsx create mode 100644 apps/frontend/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserRelationshipItemView.tsx create mode 100644 apps/frontend/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserRelationshipsView.tsx create mode 100644 apps/frontend/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserTagsView.tsx create mode 100644 apps/frontend/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx create mode 100644 apps/frontend/src/components/room/widgets/avatar-info/menu/AvatarInfoWidgetAvatarView.tsx create mode 100644 apps/frontend/src/components/room/widgets/avatar-info/menu/AvatarInfoWidgetDecorateView.tsx create mode 100644 apps/frontend/src/components/room/widgets/avatar-info/menu/AvatarInfoWidgetFurniView.tsx create mode 100644 apps/frontend/src/components/room/widgets/avatar-info/menu/AvatarInfoWidgetNameView.tsx create mode 100644 apps/frontend/src/components/room/widgets/avatar-info/menu/AvatarInfoWidgetOwnAvatarView.tsx create mode 100644 apps/frontend/src/components/room/widgets/avatar-info/menu/AvatarInfoWidgetOwnPetView.tsx create mode 100644 apps/frontend/src/components/room/widgets/avatar-info/menu/AvatarInfoWidgetPetView.tsx create mode 100644 apps/frontend/src/components/room/widgets/avatar-info/menu/AvatarInfoWidgetRentableBotView.tsx create mode 100644 apps/frontend/src/components/room/widgets/chat-input/ChatInputStyleSelectorView.tsx create mode 100644 apps/frontend/src/components/room/widgets/chat-input/ChatInputView.scss create mode 100644 apps/frontend/src/components/room/widgets/chat-input/ChatInputView.tsx create mode 100644 apps/frontend/src/components/room/widgets/chat/ChatWidgetMessageView.tsx create mode 100644 apps/frontend/src/components/room/widgets/chat/ChatWidgetView.scss create mode 100644 apps/frontend/src/components/room/widgets/chat/ChatWidgetView.tsx create mode 100644 apps/frontend/src/components/room/widgets/choosers/ChooserWidgetView.scss create mode 100644 apps/frontend/src/components/room/widgets/choosers/ChooserWidgetView.tsx create mode 100644 apps/frontend/src/components/room/widgets/choosers/FurniChooserWidgetView.tsx create mode 100644 apps/frontend/src/components/room/widgets/choosers/UserChooserWidgetView.tsx create mode 100644 apps/frontend/src/components/room/widgets/context-menu/ContextMenu.scss create mode 100644 apps/frontend/src/components/room/widgets/context-menu/ContextMenuCaretView.tsx create mode 100644 apps/frontend/src/components/room/widgets/context-menu/ContextMenuHeaderView.tsx create mode 100644 apps/frontend/src/components/room/widgets/context-menu/ContextMenuListItemView.tsx create mode 100644 apps/frontend/src/components/room/widgets/context-menu/ContextMenuListView.tsx create mode 100644 apps/frontend/src/components/room/widgets/context-menu/ContextMenuView.tsx create mode 100644 apps/frontend/src/components/room/widgets/doorbell/DoorbellWidgetView.tsx create mode 100644 apps/frontend/src/components/room/widgets/friend-request/FriendRequestDialogView.scss create mode 100644 apps/frontend/src/components/room/widgets/friend-request/FriendRequestDialogView.tsx create mode 100644 apps/frontend/src/components/room/widgets/friend-request/FriendRequestWidgetView.tsx create mode 100644 apps/frontend/src/components/room/widgets/furniture/FurnitureBackgroundColorView.tsx create mode 100644 apps/frontend/src/components/room/widgets/furniture/FurnitureBadgeDisplayView.tsx create mode 100644 apps/frontend/src/components/room/widgets/furniture/FurnitureCraftingView.tsx create mode 100644 apps/frontend/src/components/room/widgets/furniture/FurnitureDimmerView.tsx create mode 100644 apps/frontend/src/components/room/widgets/furniture/FurnitureExchangeCreditView.tsx create mode 100644 apps/frontend/src/components/room/widgets/furniture/FurnitureExternalImageView.tsx create mode 100644 apps/frontend/src/components/room/widgets/furniture/FurnitureFriendFurniView.tsx create mode 100644 apps/frontend/src/components/room/widgets/furniture/FurnitureGiftOpeningView.tsx create mode 100644 apps/frontend/src/components/room/widgets/furniture/FurnitureHighScoreView.tsx create mode 100644 apps/frontend/src/components/room/widgets/furniture/FurnitureInternalLinkView.tsx create mode 100644 apps/frontend/src/components/room/widgets/furniture/FurnitureMannequinView.tsx create mode 100644 apps/frontend/src/components/room/widgets/furniture/FurnitureMysteryBoxOpenDialogView.tsx create mode 100644 apps/frontend/src/components/room/widgets/furniture/FurnitureMysteryTrophyOpenDialogView.tsx create mode 100644 apps/frontend/src/components/room/widgets/furniture/FurnitureRoomLinkView.tsx create mode 100644 apps/frontend/src/components/room/widgets/furniture/FurnitureSpamWallPostItView.tsx create mode 100644 apps/frontend/src/components/room/widgets/furniture/FurnitureStackHeightView.tsx create mode 100644 apps/frontend/src/components/room/widgets/furniture/FurnitureStickieView.tsx create mode 100644 apps/frontend/src/components/room/widgets/furniture/FurnitureTrophyView.tsx create mode 100644 apps/frontend/src/components/room/widgets/furniture/FurnitureWidgets.scss create mode 100644 apps/frontend/src/components/room/widgets/furniture/FurnitureWidgetsView.tsx create mode 100644 apps/frontend/src/components/room/widgets/furniture/FurnitureYoutubeDisplayView.tsx create mode 100644 apps/frontend/src/components/room/widgets/furniture/context-menu/EffectBoxConfirmView.tsx create mode 100644 apps/frontend/src/components/room/widgets/furniture/context-menu/FurnitureContextMenuView.tsx create mode 100644 apps/frontend/src/components/room/widgets/furniture/context-menu/MonsterPlantSeedConfirmView.tsx create mode 100644 apps/frontend/src/components/room/widgets/furniture/context-menu/PurchasableClothingConfirmView.tsx create mode 100644 apps/frontend/src/components/room/widgets/furniture/playlist-editor/DiskInventoryView.tsx create mode 100644 apps/frontend/src/components/room/widgets/furniture/playlist-editor/FurniturePlaylistEditorWidgetView.tsx create mode 100644 apps/frontend/src/components/room/widgets/furniture/playlist-editor/SongPlaylistView.tsx create mode 100644 apps/frontend/src/components/room/widgets/mysterybox/MysteryBoxExtensionView.scss create mode 100644 apps/frontend/src/components/room/widgets/mysterybox/MysteryBoxExtensionView.tsx create mode 100644 apps/frontend/src/components/room/widgets/object-location/ObjectLocationView.tsx create mode 100644 apps/frontend/src/components/room/widgets/pet-package/PetPackageWidgetView.scss create mode 100644 apps/frontend/src/components/room/widgets/pet-package/PetPackageWidgetView.tsx create mode 100644 apps/frontend/src/components/room/widgets/room-filter-words/RoomFilterWordsWidgetView.tsx create mode 100644 apps/frontend/src/components/room/widgets/room-promotes/RoomPromotesWidgetView.tsx create mode 100644 apps/frontend/src/components/room/widgets/room-promotes/views/RoomPromoteEditWidgetView.tsx create mode 100644 apps/frontend/src/components/room/widgets/room-promotes/views/RoomPromoteMyOwnEventWidgetView.tsx create mode 100644 apps/frontend/src/components/room/widgets/room-promotes/views/RoomPromoteOtherEventWidgetView.tsx create mode 100644 apps/frontend/src/components/room/widgets/room-promotes/views/index.ts create mode 100644 apps/frontend/src/components/room/widgets/room-thumbnail/RoomThumbnailWidgetView.tsx create mode 100644 apps/frontend/src/components/room/widgets/room-tools/RoomToolsWidgetView.tsx create mode 100644 apps/frontend/src/components/room/widgets/user-location/UserLocationView.tsx create mode 100644 apps/frontend/src/components/room/widgets/word-quiz/WordQuizQuestionView.tsx create mode 100644 apps/frontend/src/components/room/widgets/word-quiz/WordQuizVoteView.tsx create mode 100644 apps/frontend/src/components/room/widgets/word-quiz/WordQuizWidgetView.tsx create mode 100644 apps/frontend/src/components/toolbar/ToolbarMeView.tsx create mode 100644 apps/frontend/src/components/toolbar/ToolbarView.scss create mode 100644 apps/frontend/src/components/toolbar/ToolbarView.tsx create mode 100644 apps/frontend/src/components/user-profile/UserProfileVew.scss create mode 100644 apps/frontend/src/components/user-profile/UserProfileView.tsx create mode 100644 apps/frontend/src/components/user-profile/views/BadgesContainerView.tsx create mode 100644 apps/frontend/src/components/user-profile/views/FriendsContainerView.tsx create mode 100644 apps/frontend/src/components/user-profile/views/GroupsContainerView.tsx create mode 100644 apps/frontend/src/components/user-profile/views/RelationshipsContainerView.tsx create mode 100644 apps/frontend/src/components/user-profile/views/UserContainerView.tsx create mode 100644 apps/frontend/src/components/user-settings/UserSettingsView.tsx create mode 100644 apps/frontend/src/components/wired/WiredView.scss create mode 100644 apps/frontend/src/components/wired/WiredView.tsx create mode 100644 apps/frontend/src/components/wired/views/WiredBaseView.tsx create mode 100644 apps/frontend/src/components/wired/views/WiredFurniSelectorView.tsx create mode 100644 apps/frontend/src/components/wired/views/actions/WiredActionBaseView.tsx create mode 100644 apps/frontend/src/components/wired/views/actions/WiredActionBotChangeFigureView.tsx create mode 100644 apps/frontend/src/components/wired/views/actions/WiredActionBotFollowAvatarView.tsx create mode 100644 apps/frontend/src/components/wired/views/actions/WiredActionBotGiveHandItemView.tsx create mode 100644 apps/frontend/src/components/wired/views/actions/WiredActionBotMoveView.tsx create mode 100644 apps/frontend/src/components/wired/views/actions/WiredActionBotTalkToAvatarView.tsx create mode 100644 apps/frontend/src/components/wired/views/actions/WiredActionBotTalkView.tsx create mode 100644 apps/frontend/src/components/wired/views/actions/WiredActionBotTeleportView.tsx create mode 100644 apps/frontend/src/components/wired/views/actions/WiredActionCallAnotherStackView.tsx create mode 100644 apps/frontend/src/components/wired/views/actions/WiredActionChaseView.tsx create mode 100644 apps/frontend/src/components/wired/views/actions/WiredActionChatView.tsx create mode 100644 apps/frontend/src/components/wired/views/actions/WiredActionFleeView.tsx create mode 100644 apps/frontend/src/components/wired/views/actions/WiredActionGiveRewardView.tsx create mode 100644 apps/frontend/src/components/wired/views/actions/WiredActionGiveScoreToPredefinedTeamView.tsx create mode 100644 apps/frontend/src/components/wired/views/actions/WiredActionGiveScoreView.tsx create mode 100644 apps/frontend/src/components/wired/views/actions/WiredActionJoinTeamView.tsx create mode 100644 apps/frontend/src/components/wired/views/actions/WiredActionKickFromRoomView.tsx create mode 100644 apps/frontend/src/components/wired/views/actions/WiredActionLayoutView.tsx create mode 100644 apps/frontend/src/components/wired/views/actions/WiredActionLeaveTeamView.tsx create mode 100644 apps/frontend/src/components/wired/views/actions/WiredActionMoveAndRotateFurniView.tsx create mode 100644 apps/frontend/src/components/wired/views/actions/WiredActionMoveFurniToView.tsx create mode 100644 apps/frontend/src/components/wired/views/actions/WiredActionMoveFurniView.tsx create mode 100644 apps/frontend/src/components/wired/views/actions/WiredActionMuteUserView.tsx create mode 100644 apps/frontend/src/components/wired/views/actions/WiredActionResetView.tsx create mode 100644 apps/frontend/src/components/wired/views/actions/WiredActionSetFurniStateToView.tsx create mode 100644 apps/frontend/src/components/wired/views/actions/WiredActionTeleportView.tsx create mode 100644 apps/frontend/src/components/wired/views/actions/WiredActionToggleFurniStateView.tsx create mode 100644 apps/frontend/src/components/wired/views/conditions/WiredConditionActorHasHandItem.tsx create mode 100644 apps/frontend/src/components/wired/views/conditions/WiredConditionActorIsGroupMemberView.tsx create mode 100644 apps/frontend/src/components/wired/views/conditions/WiredConditionActorIsOnFurniView.tsx create mode 100644 apps/frontend/src/components/wired/views/conditions/WiredConditionActorIsTeamMemberView.tsx create mode 100644 apps/frontend/src/components/wired/views/conditions/WiredConditionActorIsWearingBadgeView.tsx create mode 100644 apps/frontend/src/components/wired/views/conditions/WiredConditionActorIsWearingEffectView.tsx create mode 100644 apps/frontend/src/components/wired/views/conditions/WiredConditionBaseView.tsx create mode 100644 apps/frontend/src/components/wired/views/conditions/WiredConditionDateRangeView.tsx create mode 100644 apps/frontend/src/components/wired/views/conditions/WiredConditionFurniHasAvatarOnView.tsx create mode 100644 apps/frontend/src/components/wired/views/conditions/WiredConditionFurniHasFurniOnView.tsx create mode 100644 apps/frontend/src/components/wired/views/conditions/WiredConditionFurniHasNotFurniOnView.tsx create mode 100644 apps/frontend/src/components/wired/views/conditions/WiredConditionFurniIsOfTypeView.tsx create mode 100644 apps/frontend/src/components/wired/views/conditions/WiredConditionFurniMatchesSnapshotView.tsx create mode 100644 apps/frontend/src/components/wired/views/conditions/WiredConditionLayoutView.tsx create mode 100644 apps/frontend/src/components/wired/views/conditions/WiredConditionTimeElapsedLessView.tsx create mode 100644 apps/frontend/src/components/wired/views/conditions/WiredConditionTimeElapsedMoreView.tsx create mode 100644 apps/frontend/src/components/wired/views/conditions/WiredConditionUserCountInRoomView.tsx create mode 100644 apps/frontend/src/components/wired/views/triggers/WiredTriggerAvatarEnterRoomView.tsx create mode 100644 apps/frontend/src/components/wired/views/triggers/WiredTriggerAvatarSaysSomethingView.tsx create mode 100644 apps/frontend/src/components/wired/views/triggers/WiredTriggerAvatarWalksOffFurniView.tsx create mode 100644 apps/frontend/src/components/wired/views/triggers/WiredTriggerAvatarWalksOnFurni.tsx create mode 100644 apps/frontend/src/components/wired/views/triggers/WiredTriggerBaseView.tsx create mode 100644 apps/frontend/src/components/wired/views/triggers/WiredTriggerBotReachedAvatarView.tsx create mode 100644 apps/frontend/src/components/wired/views/triggers/WiredTriggerBotReachedStuffView.tsx create mode 100644 apps/frontend/src/components/wired/views/triggers/WiredTriggerCollisionView.tsx create mode 100644 apps/frontend/src/components/wired/views/triggers/WiredTriggerExecuteOnceView.tsx create mode 100644 apps/frontend/src/components/wired/views/triggers/WiredTriggerExecutePeriodicallyLongView.tsx create mode 100644 apps/frontend/src/components/wired/views/triggers/WiredTriggerExecutePeriodicallyView.tsx create mode 100644 apps/frontend/src/components/wired/views/triggers/WiredTriggerGameEndsView.tsx create mode 100644 apps/frontend/src/components/wired/views/triggers/WiredTriggerGameStartsView.tsx create mode 100644 apps/frontend/src/components/wired/views/triggers/WiredTriggerLayoutView.tsx create mode 100644 apps/frontend/src/components/wired/views/triggers/WiredTriggerScoreAchievedView.tsx create mode 100644 apps/frontend/src/components/wired/views/triggers/WiredTriggerToggleFurniView.tsx create mode 100644 apps/frontend/src/events/catalog/CatalogEvent.ts create mode 100644 apps/frontend/src/events/catalog/CatalogInitGiftEvent.ts create mode 100644 apps/frontend/src/events/catalog/CatalogPostMarketplaceOfferEvent.ts create mode 100644 apps/frontend/src/events/catalog/CatalogPurchaseFailureEvent.ts create mode 100644 apps/frontend/src/events/catalog/CatalogPurchaseNotAllowedEvent.ts create mode 100644 apps/frontend/src/events/catalog/CatalogPurchaseOverrideEvent.ts create mode 100644 apps/frontend/src/events/catalog/CatalogPurchaseSoldOutEvent.ts create mode 100644 apps/frontend/src/events/catalog/CatalogPurchasedEvent.ts create mode 100644 apps/frontend/src/events/catalog/CatalogSetRoomPreviewerStuffDataEvent.ts create mode 100644 apps/frontend/src/events/catalog/CatalogWidgetEvent.ts create mode 100644 apps/frontend/src/events/catalog/SetRoomPreviewerStuffDataEvent.ts create mode 100644 apps/frontend/src/events/catalog/index.ts create mode 100644 apps/frontend/src/events/guide-tool/GuideToolEvent.ts create mode 100644 apps/frontend/src/events/guide-tool/index.ts create mode 100644 apps/frontend/src/events/help/HelpNameChangeEvent.ts create mode 100644 apps/frontend/src/events/help/index.ts create mode 100644 apps/frontend/src/events/index.ts create mode 100644 apps/frontend/src/events/inventory/InventoryFurniAddedEvent.ts create mode 100644 apps/frontend/src/events/inventory/index.ts create mode 100644 apps/frontend/src/events/room-widgets/index.ts create mode 100644 apps/frontend/src/events/room-widgets/thumbnail/RoomWidgetThumbnailEvent.ts create mode 100644 apps/frontend/src/events/room-widgets/thumbnail/index.ts create mode 100644 apps/frontend/src/hooks/UseMountEffect.tsx create mode 100644 apps/frontend/src/hooks/achievements/index.ts create mode 100644 apps/frontend/src/hooks/achievements/useAchievements.ts create mode 100644 apps/frontend/src/hooks/camera/index.ts create mode 100644 apps/frontend/src/hooks/camera/useCamera.ts create mode 100644 apps/frontend/src/hooks/catalog/index.ts create mode 100644 apps/frontend/src/hooks/catalog/useCatalog.ts create mode 100644 apps/frontend/src/hooks/catalog/useCatalogPlaceMultipleItems.ts create mode 100644 apps/frontend/src/hooks/catalog/useCatalogSkipPurchaseConfirmation.ts create mode 100644 apps/frontend/src/hooks/chat-history/index.ts create mode 100644 apps/frontend/src/hooks/chat-history/useChatHistory.ts create mode 100644 apps/frontend/src/hooks/events/core/index.ts create mode 100644 apps/frontend/src/hooks/events/core/useCommunicationEvent.tsx create mode 100644 apps/frontend/src/hooks/events/core/useConfigurationEvent.tsx create mode 100644 apps/frontend/src/hooks/events/index.ts create mode 100644 apps/frontend/src/hooks/events/nitro/index.ts create mode 100644 apps/frontend/src/hooks/events/nitro/useAvatarEvent.tsx create mode 100644 apps/frontend/src/hooks/events/nitro/useCameraEvent.tsx create mode 100644 apps/frontend/src/hooks/events/nitro/useLocalizationEvent.tsx create mode 100644 apps/frontend/src/hooks/events/nitro/useMainEvent.tsx create mode 100644 apps/frontend/src/hooks/events/nitro/useRoomEngineEvent.tsx create mode 100644 apps/frontend/src/hooks/events/nitro/useRoomSessionManagerEvent.tsx create mode 100644 apps/frontend/src/hooks/events/nitro/useSessionDataManagerEvent.tsx create mode 100644 apps/frontend/src/hooks/events/nitro/useSoundEvent.tsx create mode 100644 apps/frontend/src/hooks/events/useEventDispatcher.tsx create mode 100644 apps/frontend/src/hooks/events/useMessageEvent.tsx create mode 100644 apps/frontend/src/hooks/events/useUiEvent.tsx create mode 100644 apps/frontend/src/hooks/friends/index.ts create mode 100644 apps/frontend/src/hooks/friends/useFriends.ts create mode 100644 apps/frontend/src/hooks/friends/useMessenger.ts create mode 100644 apps/frontend/src/hooks/game-center/index.ts create mode 100644 apps/frontend/src/hooks/game-center/useGameCenter.ts create mode 100644 apps/frontend/src/hooks/groups/index.ts create mode 100644 apps/frontend/src/hooks/groups/useGroup.ts create mode 100644 apps/frontend/src/hooks/help/index.ts create mode 100644 apps/frontend/src/hooks/help/useHelp.ts create mode 100644 apps/frontend/src/hooks/index.ts create mode 100644 apps/frontend/src/hooks/inventory/index.ts create mode 100644 apps/frontend/src/hooks/inventory/useInventoryBadges.ts create mode 100644 apps/frontend/src/hooks/inventory/useInventoryBots.ts create mode 100644 apps/frontend/src/hooks/inventory/useInventoryFurni.ts create mode 100644 apps/frontend/src/hooks/inventory/useInventoryPets.ts create mode 100644 apps/frontend/src/hooks/inventory/useInventoryTrade.ts create mode 100644 apps/frontend/src/hooks/inventory/useInventoryUnseenTracker.ts create mode 100644 apps/frontend/src/hooks/mod-tools/index.ts create mode 100644 apps/frontend/src/hooks/mod-tools/useModTools.ts create mode 100644 apps/frontend/src/hooks/navigator/index.ts create mode 100644 apps/frontend/src/hooks/navigator/useNavigator.ts create mode 100644 apps/frontend/src/hooks/notification/index.ts create mode 100644 apps/frontend/src/hooks/notification/useNotification.ts create mode 100644 apps/frontend/src/hooks/purse/index.ts create mode 100644 apps/frontend/src/hooks/purse/usePurse.ts create mode 100644 apps/frontend/src/hooks/rooms/engine/index.ts create mode 100644 apps/frontend/src/hooks/rooms/engine/useFurniAddedEvent.ts create mode 100644 apps/frontend/src/hooks/rooms/engine/useFurniRemovedEvent.ts create mode 100644 apps/frontend/src/hooks/rooms/engine/useObjectDeselectedEvent.ts create mode 100644 apps/frontend/src/hooks/rooms/engine/useObjectDoubleClickedEvent.ts create mode 100644 apps/frontend/src/hooks/rooms/engine/useObjectRollOutEvent.ts create mode 100644 apps/frontend/src/hooks/rooms/engine/useObjectRollOverEvent.ts create mode 100644 apps/frontend/src/hooks/rooms/engine/useObjectSelectedEvent.ts create mode 100644 apps/frontend/src/hooks/rooms/engine/useUserAddedEvent.ts create mode 100644 apps/frontend/src/hooks/rooms/engine/useUserRemovedEvent.ts create mode 100644 apps/frontend/src/hooks/rooms/index.ts create mode 100644 apps/frontend/src/hooks/rooms/promotes/index.ts create mode 100644 apps/frontend/src/hooks/rooms/promotes/useRoomPromote.ts create mode 100644 apps/frontend/src/hooks/rooms/useRoom.ts create mode 100644 apps/frontend/src/hooks/rooms/widgets/furniture/index.ts create mode 100644 apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureBackgroundColorWidget.ts create mode 100644 apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureBadgeDisplayWidget.ts create mode 100644 apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureContextMenuWidget.ts create mode 100644 apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureCraftingWidget.ts create mode 100644 apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureDimmerWidget.ts create mode 100644 apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureExchangeWidget.ts create mode 100644 apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureExternalImageWidget.ts create mode 100644 apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureFriendFurniWidget.ts create mode 100644 apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureHighScoreWidget.ts create mode 100644 apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureInternalLinkWidget.ts create mode 100644 apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureMannequinWidget.ts create mode 100644 apps/frontend/src/hooks/rooms/widgets/furniture/useFurniturePlaylistEditorWidget.ts create mode 100644 apps/frontend/src/hooks/rooms/widgets/furniture/useFurniturePresentWidget.ts create mode 100644 apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureRoomLinkWidget.ts create mode 100644 apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureSpamWallPostItWidget.ts create mode 100644 apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureStackHeightWidget.ts create mode 100644 apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureStickieWidget.ts create mode 100644 apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureTrophyWidget.ts create mode 100644 apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureYoutubeWidget.ts create mode 100644 apps/frontend/src/hooks/rooms/widgets/index.ts create mode 100644 apps/frontend/src/hooks/rooms/widgets/useAvatarInfoWidget.ts create mode 100644 apps/frontend/src/hooks/rooms/widgets/useChatInputWidget.ts create mode 100644 apps/frontend/src/hooks/rooms/widgets/useChatWidget.ts create mode 100644 apps/frontend/src/hooks/rooms/widgets/useDoorbellWidget.ts create mode 100644 apps/frontend/src/hooks/rooms/widgets/useFilterWordsWidget.ts create mode 100644 apps/frontend/src/hooks/rooms/widgets/useFriendRequestWidget.ts create mode 100644 apps/frontend/src/hooks/rooms/widgets/useFurniChooserWidget.ts create mode 100644 apps/frontend/src/hooks/rooms/widgets/usePetPackageWidget.ts create mode 100644 apps/frontend/src/hooks/rooms/widgets/usePollWidget.ts create mode 100644 apps/frontend/src/hooks/rooms/widgets/useUserChooserWidget.ts create mode 100644 apps/frontend/src/hooks/rooms/widgets/useWordQuizWidget.ts create mode 100644 apps/frontend/src/hooks/session/index.ts create mode 100644 apps/frontend/src/hooks/session/useSessionInfo.ts create mode 100644 apps/frontend/src/hooks/useLocalStorage.ts create mode 100644 apps/frontend/src/hooks/useSharedVisibility.ts create mode 100644 apps/frontend/src/hooks/wired/index.ts create mode 100644 apps/frontend/src/hooks/wired/useWired.ts create mode 100644 apps/frontend/src/index.scss create mode 100644 apps/frontend/src/index.tsx delete mode 100644 apps/frontend/src/main.tsx delete mode 100644 apps/frontend/src/styles.scss create mode 100644 apps/frontend/src/workers/IntervalWebWorker.ts create mode 100644 apps/frontend/src/workers/WorkerBuilder.ts create mode 100644 libs/renderer/.eslintrc.json create mode 100644 libs/renderer/README.md create mode 100644 libs/renderer/package.json create mode 100644 libs/renderer/project.json create mode 100644 libs/renderer/src/index.ts create mode 100644 libs/renderer/src/lib/renderer.ts create mode 100644 libs/renderer/tsconfig.json create mode 100644 libs/renderer/tsconfig.lib.json diff --git a/.gitignore b/.gitignore index 51b9af5..17f6a15 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,5 @@ testem.log # System Files .DS_Store Thumbs.db + +apps/**/public/*.json \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index 8db60ca..e8812cd 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,3 +1,12 @@ { - "singleQuote": true -} + "trailingComma": "es5", + "tabWidth": 2, + "bracketSpacing": false, + "singleQuote": false, + "arrowParens": "avoid", + "printWidth": 160, + "jsxBracketSameLine": true, + "importOrder": ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"], + "importOrderSeparation": true, + "importOrderSortSpecifiers": true + } \ No newline at end of file diff --git a/apps/frontend/index.html b/apps/frontend/index.html index 3ef900c..dd4538b 100644 --- a/apps/frontend/index.html +++ b/apps/frontend/index.html @@ -2,15 +2,34 @@ - Frontend - - - - - + + + + + + + + + + + + + + + Nitro -
- + +
+ + diff --git a/apps/frontend/public/android-chrome-192x192.png b/apps/frontend/public/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..634eb063ec992a9de0818597bd6ea3c25820c563 GIT binary patch literal 9604 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 aBw&+;F6aQQkV{elxN-fKZavyL=Klcpaw0GQ literal 0 HcmV?d00001 diff --git a/apps/frontend/public/android-chrome-512x512.png b/apps/frontend/public/android-chrome-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..33cf6c6ab19d882ccc9e2f05ea256021a12f72ce GIT binary patch literal 29298 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 literal 0 HcmV?d00001 diff --git a/apps/frontend/public/browserconfig.xml b/apps/frontend/public/browserconfig.xml new file mode 100644 index 0000000..b3930d0 --- /dev/null +++ b/apps/frontend/public/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #da532c + + + diff --git a/apps/frontend/public/favicon-16x16.png b/apps/frontend/public/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..788a7f12b42a2b1d9b7766676ff5e8e69cc19b89 GIT binary patch literal 1017 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 QGN}Y`BYF~A@YE~+0>ge95&!@I literal 0 HcmV?d00001 diff --git a/apps/frontend/public/favicon.ico b/apps/frontend/public/favicon.ico index 317ebcb2336e0833a22dddf0ab287849f26fda57..34ba1b3d4ace82c0c28e6093993d5d3fe9a47d23 100644 GIT binary patch literal 15086 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 ka>#U^%p|z7g|#(P)qFEA@4f!_@qOK2 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 literal 0 HcmV?d00001 diff --git a/apps/frontend/public/renderer-config.json.example b/apps/frontend/public/renderer-config.json.example new file mode 100644 index 0000000..dbcbcc8 --- /dev/null +++ b/apps/frontend/public/renderer-config.json.example @@ -0,0 +1,112 @@ +{ + "socket.url": "wss://ws.website.com:2096", + "asset.url": "https://website.com", + "image.library.url": "https://website.com/c_images/", + "hof.furni.url": "https://website.com/dcr/hof_furni", + "images.url": "${asset.url}/images", + "gamedata.url": "${asset.url}/gamedata", + "sounds.url": "${asset.url}/sounds/%sample%.mp3", + "external.texts.url": [ "${gamedata.url}/ExternalTexts.json", "${gamedata.url}/UITexts.json" ], + "external.samples.url": "${hof.furni.url}/mp3/sound_machine_sample_%sample%.mp3", + "furnidata.url": "${gamedata.url}/FurnitureData.json", + "productdata.url": "${gamedata.url}/ProductData.json", + "avatar.actions.url": "${gamedata.url}/HabboAvatarActions.json", + "avatar.figuredata.url": "${gamedata.url}/FigureData.json", + "avatar.figuremap.url": "${gamedata.url}/FigureMap.json", + "avatar.effectmap.url": "${gamedata.url}/EffectMap.json", + "avatar.asset.url": "${asset.url}/bundled/figure/%libname%.nitro", + "avatar.asset.effect.url": "${asset.url}/bundled/effect/%libname%.nitro", + "furni.asset.url": "${asset.url}/bundled/furniture/%libname%.nitro", + "furni.asset.icon.url": "${hof.furni.url}/icons/%libname%%param%_icon.png", + "pet.asset.url": "${asset.url}/bundled/pet/%libname%.nitro", + "generic.asset.url": "${asset.url}/bundled/generic/%libname%.nitro", + "badge.asset.url": "${image.library.url}album1584/%badgename%.gif", + "furni.rotation.bounce.steps": 20, + "furni.rotation.bounce.height": 0.0625, + "enable.avatar.arrow": false, + "system.log.debug": false, + "system.log.warn": false, + "system.log.error": false, + "system.log.events": false, + "system.log.packets": false, + "system.fps.animation": 24, + "system.fps.max": 60, + "system.pong.manually": true, + "system.pong.interval.ms": 20000, + "room.color.skip.transition": true, + "room.landscapes.enabled": true, + "avatar.mandatory.libraries": [ + "bd:1", + "li:0" + ], + "avatar.mandatory.effect.libraries": [ + "dance.1", + "dance.2", + "dance.3", + "dance.4" + ], + "avatar.default.figuredata": {"palettes":[{"id":1,"colors":[{"id":99999,"index":1001,"club":0,"selectable":false,"hexCode":"DDDDDD"},{"id":99998,"index":1001,"club":0,"selectable":false,"hexCode":"FAFAFA"}]},{"id":3,"colors":[{"id":10001,"index":1001,"club":0,"selectable":false,"hexCode":"EEEEEE"},{"id":10002,"index":1002,"club":0,"selectable":false,"hexCode":"FA3831"},{"id":10003,"index":1003,"club":0,"selectable":false,"hexCode":"FD92A0"},{"id":10004,"index":1004,"club":0,"selectable":false,"hexCode":"2AC7D2"},{"id":10005,"index":1005,"club":0,"selectable":false,"hexCode":"35332C"},{"id":10006,"index":1006,"club":0,"selectable":false,"hexCode":"EFFF92"},{"id":10007,"index":1007,"club":0,"selectable":false,"hexCode":"C6FF98"},{"id":10008,"index":1008,"club":0,"selectable":false,"hexCode":"FF925A"},{"id":10009,"index":1009,"club":0,"selectable":false,"hexCode":"9D597E"},{"id":10010,"index":1010,"club":0,"selectable":false,"hexCode":"B6F3FF"},{"id":10011,"index":1011,"club":0,"selectable":false,"hexCode":"6DFF33"},{"id":10012,"index":1012,"club":0,"selectable":false,"hexCode":"3378C9"},{"id":10013,"index":1013,"club":0,"selectable":false,"hexCode":"FFB631"},{"id":10014,"index":1014,"club":0,"selectable":false,"hexCode":"DFA1E9"},{"id":10015,"index":1015,"club":0,"selectable":false,"hexCode":"F9FB32"},{"id":10016,"index":1016,"club":0,"selectable":false,"hexCode":"CAAF8F"},{"id":10017,"index":1017,"club":0,"selectable":false,"hexCode":"C5C6C5"},{"id":10018,"index":1018,"club":0,"selectable":false,"hexCode":"47623D"},{"id":10019,"index":1019,"club":0,"selectable":false,"hexCode":"8A8361"},{"id":10020,"index":1020,"club":0,"selectable":false,"hexCode":"FF8C33"},{"id":10021,"index":1021,"club":0,"selectable":false,"hexCode":"54C627"},{"id":10022,"index":1022,"club":0,"selectable":false,"hexCode":"1E6C99"},{"id":10023,"index":1023,"club":0,"selectable":false,"hexCode":"984F88"},{"id":10024,"index":1024,"club":0,"selectable":false,"hexCode":"77C8FF"},{"id":10025,"index":1025,"club":0,"selectable":false,"hexCode":"FFC08E"},{"id":10026,"index":1026,"club":0,"selectable":false,"hexCode":"3C4B87"},{"id":10027,"index":1027,"club":0,"selectable":false,"hexCode":"7C2C47"},{"id":10028,"index":1028,"club":0,"selectable":false,"hexCode":"D7FFE3"},{"id":10029,"index":1029,"club":0,"selectable":false,"hexCode":"8F3F1C"},{"id":10030,"index":1030,"club":0,"selectable":false,"hexCode":"FF6393"},{"id":10031,"index":1031,"club":0,"selectable":false,"hexCode":"1F9B79"},{"id":10032,"index":1032,"club":0,"selectable":false,"hexCode":"FDFF33"}]}],"setTypes":[{"type":"hd","paletteId":1,"mandatory_f_0":true,"mandatory_f_1":true,"mandatory_m_0":true,"mandatory_m_1":true,"sets":[{"id":99999,"gender":"U","club":0,"colorable":true,"selectable":false,"preselectable":false,"sellable":false,"parts":[{"id":1,"type":"bd","colorable":true,"index":0,"colorindex":1},{"id":1,"type":"hd","colorable":true,"index":0,"colorindex":1},{"id":1,"type":"lh","colorable":true,"index":0,"colorindex":1},{"id":1,"type":"rh","colorable":true,"index":0,"colorindex":1}]}]},{"type":"bds","paletteId":1,"mandatory_f_0":false,"mandatory_f_1":false,"mandatory_m_0":false,"mandatory_m_1":false,"sets":[{"id":10001,"gender":"U","club":0,"colorable":true,"selectable":false,"preselectable":false,"sellable":false,"parts":[{"id":10001,"type":"bds","colorable":true,"index":0,"colorindex":1},{"id":10001,"type":"lhs","colorable":true,"index":0,"colorindex":1},{"id":10001,"type":"rhs","colorable":true,"index":0,"colorindex":1}],"hiddenLayers":[{"partType":"bd"},{"partType":"rh"},{"partType":"lh"}]}]},{"type":"ss","paletteId":3,"mandatory_f_0":false,"mandatory_f_1":false,"mandatory_m_0":false,"mandatory_m_1":false,"sets":[{"id":10010,"gender":"F","club":0,"colorable":true,"selectable":false,"preselectable":false,"sellable":false,"parts":[{"id":10001,"type":"ss","colorable":true,"index":0,"colorindex":1}],"hiddenLayers":[{"partType":"ch"},{"partType":"lg"},{"partType":"ca"},{"partType":"wa"},{"partType":"sh"},{"partType":"ls"},{"partType":"rs"},{"partType":"lc"},{"partType":"rc"},{"partType":"cc"},{"partType":"cp"}]},{"id":10011,"gender":"M","club":0,"colorable":true,"selectable":false,"preselectable":false,"sellable":false,"parts":[{"id":10002,"type":"ss","colorable":true,"index":0,"colorindex":1}],"hiddenLayers":[{"partType":"ch"},{"partType":"lg"},{"partType":"ca"},{"partType":"wa"},{"partType":"sh"},{"partType":"ls"},{"partType":"rs"},{"partType":"lc"},{"partType":"rc"},{"partType":"cc"},{"partType":"cp"}]}]}]}, + "avatar.default.actions": { + "actions": [ + { + "id": "Default", + "state": "std", + "precedence": 1000, + "main": true, + "isDefault": true, + "geometryType": "vertical", + "activePartSet": "figure", + "assetPartDefinition": "std" + } + ] + }, + "pet.types": [ + "dog", + "cat", + "croco", + "terrier", + "bear", + "pig", + "lion", + "rhino", + "spider", + "turtle", + "chicken", + "frog", + "dragon", + "monster", + "monkey", + "horse", + "monsterplant", + "bunnyeaster", + "bunnyevil", + "bunnydepressed", + "bunnylove", + "pigeongood", + "pigeonevil", + "demonmonkey", + "bearbaby", + "terrierbaby", + "gnome", + "gnome", + "kittenbaby", + "puppybaby", + "pigletbaby", + "haloompa", + "fools", + "pterosaur", + "velociraptor", + "cow", + "LeetPen", + "bbwibb", + "elephants" + ], + "preload.assets.urls": [ + "${asset.url}/bundled/generic/avatar_additions.nitro", + "${asset.url}/bundled/generic/group_badge.nitro", + "${asset.url}/bundled/generic/floor_editor.nitro", + "${images.url}/loading_icon.png", + "${images.url}/clear_icon.png", + "${images.url}/big_arrow.png" + ] +} diff --git a/apps/frontend/public/robots.txt b/apps/frontend/public/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/apps/frontend/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/apps/frontend/public/safari-pinned-tab.svg b/apps/frontend/public/safari-pinned-tab.svg new file mode 100644 index 0000000..dc7ced3 --- /dev/null +++ b/apps/frontend/public/safari-pinned-tab.svg @@ -0,0 +1,154 @@ + + + + +Created by potrace 1.14, written by Peter Selinger 2001-2017 + + + + + diff --git a/apps/frontend/public/site.webmanifest b/apps/frontend/public/site.webmanifest new file mode 100644 index 0000000..6264b89 --- /dev/null +++ b/apps/frontend/public/site.webmanifest @@ -0,0 +1,20 @@ +{ + "start_url": "/", + "name": "Nitro", + "short_name": "Nitro", + "icons": [ + { + "src": "android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/apps/frontend/public/ui-config.json.example b/apps/frontend/public/ui-config.json.example new file mode 100644 index 0000000..2187bc0 --- /dev/null +++ b/apps/frontend/public/ui-config.json.example @@ -0,0 +1,1203 @@ +{ + "image.library.notifications.url": "${image.library.url}notifications/%image%.png", + "achievements.images.url": "${image.library.url}Quests/%image%.png", + "camera.url": "https://camera.url", + "thumbnails.url": "https://camera.url/thumbnail/%thumbnail%.png", + "url.prefix": "https://website.com", + "habbopages.url": "${url.prefix}/", + "group.homepage.url": "${url.prefix}/groups/%groupid%/id", + "guide.help.alpha.groupid": 0, + "chat.viewer.height.percentage": 0.40, + "widget.dimmer.colorwheel": false, + "avatar.wardrobe.max.slots": 10, + "user.badges.max.slots": 5, + "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" } + ], + "hotelview": { + "show.avatar": true, + "widgets": { + "slot.1.widget": "promoarticle", + "slot.1.conf": {}, + "slot.2.widget": "widgetcontainer", + "slot.2.conf": { + "image": "${image.library.url}web_promo_small/spromo_Canal_Bundle.png", + "texts": "2021NitroPromo", + "btnLink": "https://google.com" + }, + "slot.3.widget": "promoarticle", + "slot.3.conf": {}, + "slot.4.widget": "", + "slot.4.conf": {}, + "slot.5.widget": "", + "slot.5.conf": {}, + "slot.6.widget": "achievementcompetition_hall_of_fame", + "slot.6.conf": { + "campaign": "habboFameComp" + }, + "slot.7.widget": "", + "slot.7.conf": {} + }, + "images": { + "background": "${asset.url}/images/reception/stretch_blue.png", + "background.colour": "#6eadc8", + "sun": "${asset.url}/images/reception/sun.png", + "drape": "${asset.url}/images/reception/drape.png", + "left": "${asset.url}/images/reception/ts.png", + "right": "${asset.url}/images/reception/US_right.png", + "right.repeat": "${asset.url}/images/reception/US_top_right.png" + } + }, + "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 + } + ], + "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/apps/frontend/src/App.scss b/apps/frontend/src/App.scss new file mode 100644 index 0000000..2000ade --- /dev/null +++ b/apps/frontend/src/App.scss @@ -0,0 +1,102 @@ +$toolbar-me-zindex: 90; +$chatinput-zindex: 80; +$toolbar-zindex: 70; +$rightside-zindex: 69; +$notification-center-zindex: 68; +$toolbar-memenu-zindex: 60; +$roomtools-zindex: 50; +$context-menu-zindex: 40; +$infostand-zindex: 30; +$quiz-zindex: 21; +$chat-zindex: 20; +$highscore-zindex: 19; + +$grid-bg-color: #cdd3d9; +$grid-border-color: $muted; +$grid-active-bg-color: #ececec; +$grid-active-border-color: $white; + +$toolbar-height: 55px; + +$achievement-width: 375px; +$achievement-height: 405px; + +$avatar-editor-width: 620px; +$avatar-editor-height: 374px; + +$catalog-width: 630px; +$catalog-height: 400px; + +$inventory-width: 528px; +$inventory-height: 320px; + +$navigator-width: 420px; +$navigator-height: 440px; + +$chat-input-style-selector-widget-width: 210px; +$chat-input-style-selector-widget-height: 200px; + +$user-profile-width: 470px; +$user-profile-height: 460px; + +$nitro-widget-custom-stack-height-width: 275px; +$nitro-widget-custom-stack-height-height: 220px; + +$nitro-widget-exchange-credit-width: 375px; +$nitro-widget-exchange-credit-height: 150px; + +$nitro-widget-crafting-width: 500px; +$nitro-widget-crafting-height: 300px; + +$chat-history-width: 300px; +$chat-history-height: 300px; + +$friends-list-width: 250px; +$friends-list-height: 300px; + +$help-width: 450px; +$help-height: 290px; + +$nitropedia-width: 400px; +$nitropedia-height: 400px; + +$messenger-width: 500px; +$messenger-height: 370px; + +$marketplace-post-offer-width: 430px; +$marketplace-post-offer-height: 250px; + +$camera-editor-width: 600px; +$camera-editor-height: 500px; + +$camera-checkout-width: 350px; + +$room-info-width: 325px; + +$nitro-group-creator-width: 383px; +$nitro-mod-tools-width: 175px; + +$nitro-group-manager-width: 390px; +$nitro-group-manager-height: 355px; + +$nitro-chooser-width: 200px; +$nitro-chooser-height: 200px; + +$nitro-doorbell-width: 300px; +$nitro-doorbell-height: 200px; + +$nitro-guide-tool-width: 250px; + +$nitro-floor-editor-width: 760px; +$nitro-floor-editor-height: 500px; + +$nitro-calendar-width: 850px; +$nitro-calendar-height: 400px; + +.nitro-app { + width: 100%; + height: 100%; +} + +@import './common'; +@import './components'; diff --git a/apps/frontend/src/App.tsx b/apps/frontend/src/App.tsx new file mode 100644 index 0000000..072b05d --- /dev/null +++ b/apps/frontend/src/App.tsx @@ -0,0 +1,141 @@ +import { ConfigurationEvent, GetAssetManager, HabboWebTools, LegacyExternalInterface, Nitro, NitroCommunicationDemoEvent, NitroConfiguration, NitroEvent, NitroLocalizationEvent, NitroVersion, RoomEngineEvent } from '@nitrots/nitro-renderer'; +import { FC, useCallback, useEffect, useState } from 'react'; +import { GetCommunication, GetConfiguration, GetNitroInstance, GetUIVersion } from './api'; +import { Base, TransitionAnimation, TransitionAnimationTypes } from './common'; +import { LoadingView } from './components/loading/LoadingView'; +import { MainView } from './components/main/MainView'; +import { useConfigurationEvent, useLocalizationEvent, useMainEvent, useRoomEngineEvent } from './hooks'; + +NitroVersion.UI_VERSION = GetUIVersion(); + +export const App: FC<{}> = props => +{ + const [ isReady, setIsReady ] = useState(false); + const [ isError, setIsError ] = useState(false); + const [ message, setMessage ] = useState('Getting Ready'); + const [ percent, setPercent ] = useState(0); + const [ imageRendering, setImageRendering ] = useState(true); + + if(!GetNitroInstance()) + { + //@ts-ignore + if(!NitroConfig) throw new Error('NitroConfig is not defined!'); + + Nitro.bootstrap(); + } + + const handler = useCallback(async (event: NitroEvent) => + { + switch(event.type) + { + case ConfigurationEvent.LOADED: + GetNitroInstance().localization.init(); + setPercent(prevValue => (prevValue + 20)); + return; + case ConfigurationEvent.FAILED: + setIsError(true); + setMessage('Configuration Failed'); + return; + case Nitro.WEBGL_UNAVAILABLE: + setIsError(true); + setMessage('WebGL Required'); + return; + case Nitro.WEBGL_CONTEXT_LOST: + setIsError(true); + setMessage('WebGL Context Lost - Reloading'); + + setTimeout(() => window.location.reload(), 1500); + return; + case NitroCommunicationDemoEvent.CONNECTION_HANDSHAKING: + setPercent(prevValue => (prevValue + 20)); + return; + case NitroCommunicationDemoEvent.CONNECTION_HANDSHAKE_FAILED: + setIsError(true); + setMessage('Handshake Failed'); + return; + case NitroCommunicationDemoEvent.CONNECTION_AUTHENTICATED: + setPercent(prevValue => (prevValue + 20)); + + GetNitroInstance().init(); + + if(LegacyExternalInterface.available) LegacyExternalInterface.call('legacyTrack', 'authentication', 'authok', []); + return; + case NitroCommunicationDemoEvent.CONNECTION_ERROR: + setIsError(true); + setMessage('Connection Error'); + return; + case NitroCommunicationDemoEvent.CONNECTION_CLOSED: + //if(GetNitroInstance().roomEngine) GetNitroInstance().roomEngine.dispose(); + //setIsError(true); + setMessage('Connection Error'); + + HabboWebTools.send(-1, 'client.init.handshake.fail'); + return; + case RoomEngineEvent.ENGINE_INITIALIZED: + setPercent(prevValue => (prevValue + 20)); + + setTimeout(() => setIsReady(true), 300); + return; + case NitroLocalizationEvent.LOADED: { + const assetUrls = GetConfiguration('preload.assets.urls'); + const urls: string[] = []; + + if(assetUrls && assetUrls.length) for(const url of assetUrls) urls.push(NitroConfiguration.interpolate(url)); + + const status = await GetAssetManager().downloadAssets(urls); + + if(status) + { + GetCommunication().init(); + + setPercent(prevValue => (prevValue + 20)) + } + else + { + setIsError(true); + setMessage('Assets Failed'); + } + return; + } + } + }, []); + + useMainEvent(Nitro.WEBGL_UNAVAILABLE, handler); + useMainEvent(Nitro.WEBGL_CONTEXT_LOST, handler); + useMainEvent(NitroCommunicationDemoEvent.CONNECTION_HANDSHAKING, handler); + useMainEvent(NitroCommunicationDemoEvent.CONNECTION_HANDSHAKE_FAILED, handler); + useMainEvent(NitroCommunicationDemoEvent.CONNECTION_AUTHENTICATED, handler); + useMainEvent(NitroCommunicationDemoEvent.CONNECTION_ERROR, handler); + useMainEvent(NitroCommunicationDemoEvent.CONNECTION_CLOSED, handler); + useRoomEngineEvent(RoomEngineEvent.ENGINE_INITIALIZED, handler); + useLocalizationEvent(NitroLocalizationEvent.LOADED, handler); + useConfigurationEvent(ConfigurationEvent.LOADED, handler); + useConfigurationEvent(ConfigurationEvent.FAILED, handler); + + useEffect(() => + { + GetNitroInstance().core.configuration.init(); + + const resize = (event: UIEvent) => setImageRendering(!(window.devicePixelRatio % 1)); + + window.addEventListener('resize', resize); + + resize(null); + + return () => + { + window.removeEventListener('resize', resize); + } + }, []); + + return ( + + { (!isReady || isError) && + } + + + + + + ); +} diff --git a/apps/frontend/src/api/GetRendererVersion.ts b/apps/frontend/src/api/GetRendererVersion.ts new file mode 100644 index 0000000..bb9e461 --- /dev/null +++ b/apps/frontend/src/api/GetRendererVersion.ts @@ -0,0 +1,3 @@ +import { NitroVersion } from '@nitrots/nitro-renderer'; + +export const GetRendererVersion = () => NitroVersion.RENDERER_VERSION; diff --git a/apps/frontend/src/api/GetUIVersion.ts b/apps/frontend/src/api/GetUIVersion.ts new file mode 100644 index 0000000..cf8d5a5 --- /dev/null +++ b/apps/frontend/src/api/GetUIVersion.ts @@ -0,0 +1 @@ +export const GetUIVersion = () => '2.1.1'; diff --git a/apps/frontend/src/api/achievements/AchievementCategory.ts b/apps/frontend/src/api/achievements/AchievementCategory.ts new file mode 100644 index 0000000..906d8da --- /dev/null +++ b/apps/frontend/src/api/achievements/AchievementCategory.ts @@ -0,0 +1,40 @@ +import { AchievementData } from '@nitrots/nitro-renderer'; +import { AchievementUtilities } from './AchievementUtilities'; +import { IAchievementCategory } from './IAchievementCategory'; + +export class AchievementCategory implements IAchievementCategory +{ + private _code: string; + private _achievements: AchievementData[]; + + constructor(code: string) + { + this._code = code; + this._achievements = []; + } + + public getProgress(): number + { + return AchievementUtilities.getAchievementCategoryProgress(this); + } + + public getMaxProgress(): number + { + return AchievementUtilities.getAchievementCategoryMaxProgress(this); + } + + public get code(): string + { + return this._code; + } + + public get achievements(): AchievementData[] + { + return this._achievements; + } + + public set achievements(achievements: AchievementData[]) + { + this._achievements = achievements; + } +} diff --git a/apps/frontend/src/api/achievements/AchievementUtilities.ts b/apps/frontend/src/api/achievements/AchievementUtilities.ts new file mode 100644 index 0000000..b581f97 --- /dev/null +++ b/apps/frontend/src/api/achievements/AchievementUtilities.ts @@ -0,0 +1,97 @@ +import { AchievementData } from '@nitrots/nitro-renderer'; +import { GetConfiguration, GetLocalization } from '../nitro'; +import { IAchievementCategory } from './IAchievementCategory'; + +export class AchievementUtilities +{ + public static getAchievementBadgeCode(achievement: AchievementData): string + { + if(!achievement) return null; + + let badgeId = achievement.badgeId; + + if(!achievement.finalLevel) badgeId = GetLocalization().getPreviousLevelBadgeId(badgeId); + + return badgeId; + } + + public static getAchievementCategoryImageUrl(category: IAchievementCategory, progress: number = null, icon: boolean = false): string + { + const imageUrl = GetConfiguration('achievements.images.url'); + + let imageName = icon ? 'achicon_' : 'achcategory_'; + + imageName += category.code; + + if(progress !== null) imageName += `_${ ((progress > 0) ? 'active' : 'inactive') }`; + + return imageUrl.replace('%image%', imageName); + } + + public static getAchievementCategoryMaxProgress(category: IAchievementCategory): number + { + if(!category) return 0; + + let progress = 0; + + for(const achievement of category.achievements) + { + progress += achievement.levelCount; + } + + return progress; + } + + public static getAchievementCategoryProgress(category: IAchievementCategory): number + { + if(!category) return 0; + + let progress = 0; + + for(const achievement of category.achievements) progress += (achievement.finalLevel ? achievement.level : (achievement.level - 1)); + + return progress; + } + + public static getAchievementCategoryTotalUnseen(category: IAchievementCategory): number + { + if(!category) return 0; + + let unseen = 0; + + for(const achievement of category.achievements) ((achievement.unseen > 0) && unseen++); + + return unseen; + } + + public static getAchievementHasStarted(achievement: AchievementData): boolean + { + if(!achievement) return false; + + if(achievement.finalLevel || ((achievement.level - 1) > 0)) return true; + + return false; + } + + public static getAchievementIsIgnored(achievement: AchievementData): boolean + { + if(!achievement) return false; + + const ignored = GetConfiguration('achievements.unseen.ignored'); + const value = achievement.badgeId.replace(/[0-9]/g, ''); + const index = ignored.indexOf(value); + + if(index >= 0) return true; + + return false; + } + + public static getAchievementLevel(achievement: AchievementData): number + { + if(!achievement) return 0; + + if(achievement.finalLevel) return achievement.level; + + return (achievement.level - 1); + } +} diff --git a/apps/frontend/src/api/achievements/IAchievementCategory.ts b/apps/frontend/src/api/achievements/IAchievementCategory.ts new file mode 100644 index 0000000..a049d46 --- /dev/null +++ b/apps/frontend/src/api/achievements/IAchievementCategory.ts @@ -0,0 +1,7 @@ +import { AchievementData } from '@nitrots/nitro-renderer'; + +export interface IAchievementCategory +{ + code: string; + achievements: AchievementData[]; +} diff --git a/apps/frontend/src/api/achievements/index.ts b/apps/frontend/src/api/achievements/index.ts new file mode 100644 index 0000000..a3d44b7 --- /dev/null +++ b/apps/frontend/src/api/achievements/index.ts @@ -0,0 +1,3 @@ +export * from './AchievementCategory'; +export * from './AchievementUtilities'; +export * from './IAchievementCategory'; diff --git a/apps/frontend/src/api/avatar/AvatarEditorAction.ts b/apps/frontend/src/api/avatar/AvatarEditorAction.ts new file mode 100644 index 0000000..064d6df --- /dev/null +++ b/apps/frontend/src/api/avatar/AvatarEditorAction.ts @@ -0,0 +1,7 @@ +export class AvatarEditorAction +{ + public static ACTION_SAVE: string = 'AEA_ACTION_SAVE'; + public static ACTION_CLEAR: string = 'AEA_ACTION_CLEAR'; + public static ACTION_RESET: string = 'AEA_ACTION_RESET'; + public static ACTION_RANDOMIZE: string = 'AEA_ACTION_RANDOMIZE'; +} diff --git a/apps/frontend/src/api/avatar/AvatarEditorGridColorItem.ts b/apps/frontend/src/api/avatar/AvatarEditorGridColorItem.ts new file mode 100644 index 0000000..dee3dae --- /dev/null +++ b/apps/frontend/src/api/avatar/AvatarEditorGridColorItem.ts @@ -0,0 +1,65 @@ +import { ColorConverter, IPartColor } from '@nitrots/nitro-renderer'; + +export class AvatarEditorGridColorItem +{ + private _partColor: IPartColor; + private _isDisabled: boolean; + private _isHC: boolean; + private _isSelected: boolean; + private _notifier: () => void; + + constructor(partColor: IPartColor, isDisabled: boolean = false) + { + this._partColor = partColor; + this._isDisabled = isDisabled; + this._isHC = (this._partColor.clubLevel > 0); + this._isSelected = false; + } + + public dispose(): void + { + this._partColor = null; + } + + public get partColor(): IPartColor + { + return this._partColor; + } + + public get color(): string + { + return ColorConverter.int2rgb(this._partColor.rgb); + } + + public get isDisabled(): boolean + { + return this._isDisabled; + } + + public get isHC(): boolean + { + return this._isHC; + } + + public get isSelected(): boolean + { + return this._isSelected; + } + + public set isSelected(flag: boolean) + { + this._isSelected = flag; + + if(this.notify) this.notify(); + } + + public get notify(): () => void + { + return this._notifier; + } + + public set notify(notifier: () => void) + { + this._notifier = notifier; + } +} diff --git a/apps/frontend/src/api/avatar/AvatarEditorGridPartItem.ts b/apps/frontend/src/api/avatar/AvatarEditorGridPartItem.ts new file mode 100644 index 0000000..a3b1661 --- /dev/null +++ b/apps/frontend/src/api/avatar/AvatarEditorGridPartItem.ts @@ -0,0 +1,337 @@ +import { AvatarFigurePartType, IAvatarImageListener, IAvatarRenderManager, IFigurePart, IFigurePartSet, IGraphicAsset, IPartColor, NitroAlphaFilter, NitroContainer, NitroSprite, TextureUtils } from '@nitrots/nitro-renderer'; +import { GetAvatarRenderManager } from '../nitro'; +import { FigureData } from './FigureData'; + +export class AvatarEditorGridPartItem implements IAvatarImageListener +{ + private static ALPHA_FILTER: NitroAlphaFilter = new NitroAlphaFilter(0.2); + private static THUMB_DIRECTIONS: number[] = [ 2, 6, 0, 4, 3, 1 ]; + private static DRAW_ORDER: string[] = [ + AvatarFigurePartType.LEFT_HAND_ITEM, + AvatarFigurePartType.LEFT_HAND, + AvatarFigurePartType.LEFT_SLEEVE, + AvatarFigurePartType.LEFT_COAT_SLEEVE, + AvatarFigurePartType.BODY, + AvatarFigurePartType.SHOES, + AvatarFigurePartType.LEGS, + AvatarFigurePartType.CHEST, + AvatarFigurePartType.CHEST_ACCESSORY, + AvatarFigurePartType.COAT_CHEST, + AvatarFigurePartType.CHEST_PRINT, + AvatarFigurePartType.WAIST_ACCESSORY, + AvatarFigurePartType.RIGHT_HAND, + AvatarFigurePartType.RIGHT_SLEEVE, + AvatarFigurePartType.RIGHT_COAT_SLEEVE, + AvatarFigurePartType.HEAD, + AvatarFigurePartType.FACE, + AvatarFigurePartType.EYES, + AvatarFigurePartType.HAIR, + AvatarFigurePartType.HAIR_BIG, + AvatarFigurePartType.FACE_ACCESSORY, + AvatarFigurePartType.EYE_ACCESSORY, + AvatarFigurePartType.HEAD_ACCESSORY, + AvatarFigurePartType.HEAD_ACCESSORY_EXTRA, + AvatarFigurePartType.RIGHT_HAND_ITEM, + ]; + + private _renderManager: IAvatarRenderManager; + private _partSet: IFigurePartSet; + private _partColors: IPartColor[]; + private _useColors: boolean; + private _isDisabled: boolean; + private _thumbContainer: NitroContainer; + private _imageUrl: string; + private _maxColorIndex: number; + private _isValidFigure: boolean; + private _isHC: boolean; + private _isSellable: boolean; + private _isClear: boolean; + private _isSelected: boolean; + private _disposed: boolean; + private _isInitalized: boolean; + private _notifier: () => void; + + constructor(partSet: IFigurePartSet, partColors: IPartColor[], useColors: boolean = true, isDisabled: boolean = false) + { + this._renderManager = GetAvatarRenderManager(); + this._partSet = partSet; + this._partColors = partColors; + this._useColors = useColors; + this._isDisabled = isDisabled; + this._thumbContainer = null; + this._imageUrl = null; + this._maxColorIndex = 0; + this._isValidFigure = false; + this._isHC = false; + this._isSellable = false; + this._isClear = false; + this._isSelected = false; + this._disposed = false; + this._isInitalized = false; + + if(partSet) + { + const colors = partSet.parts; + + for(const color of colors) this._maxColorIndex = Math.max(this._maxColorIndex, color.colorLayerIndex); + } + } + + public init(): void + { + if(this._isInitalized) return; + + this._isInitalized = true; + + this.update(); + } + + public dispose(): void + { + if(this._disposed) return; + + this._renderManager = null; + this._partSet = null; + this._partColors = null; + this._imageUrl = null; + this._disposed = true; + this._isInitalized = false; + + if(this._thumbContainer) + { + this._thumbContainer.destroy(); + + this._thumbContainer = null; + } + } + + public update(): void + { + this.updateThumbVisualization(); + } + + private analyzeFigure(): boolean + { + if(!this._renderManager || !this._partSet || !this._partSet.parts || !this._partSet.parts.length) return false; + + const figureContainer = this._renderManager.createFigureContainer(((this.partSet.type + '-') + this.partSet.id)); + + if(!this._renderManager.isFigureContainerReady(figureContainer)) + { + this._renderManager.downloadAvatarFigure(figureContainer, this); + + return false; + } + + this._isValidFigure = true; + + return true; + } + + private renderThumb(): NitroContainer + { + if(!this._renderManager || !this._partSet) return null; + + if(!this._isValidFigure) + { + if(!this.analyzeFigure()) return null; + } + + const parts = this._partSet.parts.concat().sort(this.sortByDrawOrder); + const container = new NitroContainer(); + + for(const part of parts) + { + if(!part) continue; + + let asset: IGraphicAsset = null; + let direction = 0; + let hasAsset = false; + + while(!hasAsset && (direction < AvatarEditorGridPartItem.THUMB_DIRECTIONS.length)) + { + const assetName = ((((((((((FigureData.SCALE + '_') + FigureData.STD) + '_') + part.type) + '_') + part.id) + '_') + AvatarEditorGridPartItem.THUMB_DIRECTIONS[direction]) + '_') + FigureData.DEFAULT_FRAME); + + asset = this._renderManager.getAssetByName(assetName); + + if(asset && asset.texture) + { + hasAsset = true; + } + else + { + direction++; + } + } + + if(!hasAsset) continue; + + const x = asset.offsetX; + const y = asset.offsetY; + let partColor: IPartColor = null; + + if(this._useColors && (part.colorLayerIndex > 0)) + { + const color = this._partColors[(part.colorLayerIndex - 1)]; + + if(color) partColor = color; + } + + const sprite = new NitroSprite(asset.texture); + + sprite.position.set(x, y); + + if(partColor) sprite.tint = partColor.rgb; + + container.addChild(sprite); + } + + return container; + } + + private updateThumbVisualization(): void + { + if(!this._isInitalized) return; + + let container = this._thumbContainer; + + if(!container) container = this.renderThumb(); + + if(!container) return; + + if(this._partSet) + { + this._isHC = (this._partSet.clubLevel > 0); + this._isSellable = this._partSet.isSellable; + } + else + { + this._isHC = false; + this._isSellable = false; + } + + if(this._isDisabled) this.setAlpha(container, 0.2); + + this._imageUrl = TextureUtils.generateImageUrl(container); + + if(this.notify) this.notify(); + } + + private setAlpha(container: NitroContainer, alpha: number): NitroContainer + { + container.filters = [ AvatarEditorGridPartItem.ALPHA_FILTER ]; + + return container; + } + + private sortByDrawOrder(a: IFigurePart, b: IFigurePart): number + { + const indexA = AvatarEditorGridPartItem.DRAW_ORDER.indexOf(a.type); + const indexB = AvatarEditorGridPartItem.DRAW_ORDER.indexOf(b.type); + + if(indexA < indexB) return -1; + + if(indexA > indexB) return 1; + + if(a.index < b.index) return -1; + + if(a.index > b.index) return 1; + + return 0; + } + + public resetFigure(figure: string): void + { + if(!this.analyzeFigure()) return; + + this.update(); + } + + public get disposed(): boolean + { + return this._disposed; + } + + public get id(): number + { + if(!this._partSet) return -1; + + return this._partSet.id; + } + + public get partSet(): IFigurePartSet + { + return this._partSet; + } + + public set partColors(partColors: IPartColor[]) + { + this._partColors = partColors; + + this.update(); + } + + public get isDisabled(): boolean + { + return this._isDisabled; + } + + public set thumbContainer(container: NitroContainer) + { + this._thumbContainer = container; + + this.update(); + } + + public get imageUrl(): string + { + return this._imageUrl; + } + + public get maxColorIndex(): number + { + return this._maxColorIndex; + } + + public get isHC(): boolean + { + return this._isHC; + } + + public get isSellable(): boolean + { + return this._isSellable; + } + + public get isClear(): boolean + { + return this._isClear; + } + + public set isClear(flag: boolean) + { + this._isClear = flag; + } + + public get isSelected(): boolean + { + return this._isSelected; + } + + public set isSelected(flag: boolean) + { + this._isSelected = flag; + + if(this.notify) this.notify(); + } + + public get notify(): () => void + { + return this._notifier; + } + + public set notify(notifier: () => void) + { + this._notifier = notifier; + } +} diff --git a/apps/frontend/src/api/avatar/AvatarEditorUtilities.ts b/apps/frontend/src/api/avatar/AvatarEditorUtilities.ts new file mode 100644 index 0000000..3626ab8 --- /dev/null +++ b/apps/frontend/src/api/avatar/AvatarEditorUtilities.ts @@ -0,0 +1,277 @@ +import { IPartColor } from '@nitrots/nitro-renderer'; +import { GetAvatarPalette, GetAvatarRenderManager, GetAvatarSetType, GetClubMemberLevel, GetConfiguration } from '../nitro'; +import { AvatarEditorGridColorItem } from './AvatarEditorGridColorItem'; +import { AvatarEditorGridPartItem } from './AvatarEditorGridPartItem'; +import { CategoryBaseModel } from './CategoryBaseModel'; +import { CategoryData } from './CategoryData'; +import { FigureData } from './FigureData'; + +export class AvatarEditorUtilities +{ + private static MAX_PALETTES: number = 2; + + public static CURRENT_FIGURE: FigureData = null; + public static FIGURE_SET_IDS: number[] = []; + public static BOUND_FURNITURE_NAMES: string[] = []; + + public static getGender(gender: string): string + { + switch(gender) + { + case FigureData.MALE: + case 'm': + case 'M': + gender = FigureData.MALE; + break; + case FigureData.FEMALE: + case 'f': + case 'F': + gender = FigureData.FEMALE; + break; + default: + gender = FigureData.MALE; + } + + return gender; + } + + public static hasFigureSetId(setId: number): boolean + { + return (this.FIGURE_SET_IDS.indexOf(setId) >= 0); + } + + public static createCategory(model: CategoryBaseModel, name: string): CategoryData + { + if(!model || !name || !this.CURRENT_FIGURE) return null; + + const partItems: AvatarEditorGridPartItem[] = []; + const colorItems: AvatarEditorGridColorItem[][] = []; + + let i = 0; + + while(i < this.MAX_PALETTES) + { + colorItems.push([]); + + i++; + } + + const setType = GetAvatarSetType(name); + + if(!setType) return null; + + const palette = GetAvatarPalette(setType.paletteID); + + if(!palette) return null; + + let colorIds = this.CURRENT_FIGURE.getColorIds(name); + + if(!colorIds) colorIds = []; + + const partColors: IPartColor[] = new Array(colorIds.length); + const clubItemsDimmed = this.clubItemsDimmed; + const clubMemberLevel = GetClubMemberLevel(); + + for(const partColor of palette.colors.getValues()) + { + if(partColor.isSelectable && (clubItemsDimmed || (clubMemberLevel >= partColor.clubLevel))) + { + let i = 0; + + while(i < this.MAX_PALETTES) + { + const isDisabled = (clubMemberLevel < partColor.clubLevel); + const colorItem = new AvatarEditorGridColorItem(partColor, isDisabled); + + colorItems[i].push(colorItem); + + i++; + } + + if(name !== FigureData.FACE) + { + let i = 0; + + while(i < colorIds.length) + { + if(partColor.id === colorIds[i]) partColors[i] = partColor; + + i++; + } + } + } + } + + let mandatorySetIds: string[] = []; + + if(clubItemsDimmed) + { + mandatorySetIds = GetAvatarRenderManager().getMandatoryAvatarPartSetIds(this.CURRENT_FIGURE.gender, 2); + } + else + { + mandatorySetIds = GetAvatarRenderManager().getMandatoryAvatarPartSetIds(this.CURRENT_FIGURE.gender, clubMemberLevel); + } + + const isntMandatorySet = (mandatorySetIds.indexOf(name) === -1); + + if(isntMandatorySet) + { + const partItem = new AvatarEditorGridPartItem(null, null, false); + + partItem.isClear = true; + + partItems.push(partItem); + } + + const usesColors = (name !== FigureData.FACE); + const partSets = setType.partSets; + const totalPartSets = partSets.length; + + i = (totalPartSets - 1); + + while(i >= 0) + { + const partSet = partSets.getWithIndex(i); + + let isValidGender = false; + + if(partSet.gender === FigureData.UNISEX) + { + isValidGender = true; + } + + else if(partSet.gender === this.CURRENT_FIGURE.gender) + { + isValidGender = true; + } + + if(partSet.isSelectable && isValidGender && (clubItemsDimmed || (clubMemberLevel >= partSet.clubLevel))) + { + const isDisabled = (clubMemberLevel < partSet.clubLevel); + + let isValid = true; + + if(partSet.isSellable) isValid = this.hasFigureSetId(partSet.id); + + if(isValid) partItems.push(new AvatarEditorGridPartItem(partSet, partColors, usesColors, isDisabled)); + } + + i--; + } + + partItems.sort(this.clubItemsFirst ? this.clubSorter : this.noobSorter); + + // if(this._forceSellableClothingVisibility || GetNitroInstance().getConfiguration("avatareditor.support.sellablefurni", false)) + // { + // _local_31 = (this._manager.windowManager.assets.getAssetByName("camera_zoom_in") as BitmapDataAsset); + // _local_32 = (_local_31.content as BitmapData).clone(); + // _local_33 = (AvatarEditorView._Str_6802.clone() as IWindowContainer); + // _local_33.name = AvatarEditorGridView.GET_MORE; + // _local_7 = new AvatarEditorGridPartItem(_local_33, k, null, null, false); + // _local_7._Str_3093 = _local_32; + // _local_3.push(_local_7); + // } + + i = 0; + + while(i < this.MAX_PALETTES) + { + colorItems[i].sort(this.colorSorter); + + i++; + } + + return new CategoryData(name, partItems, colorItems); + } + + public static clubSorter(a: AvatarEditorGridPartItem, b: AvatarEditorGridPartItem): number + { + const clubLevelA = (!a.partSet ? 9999999999 : a.partSet.clubLevel); + const clubLevelB = (!b.partSet ? 9999999999 : b.partSet.clubLevel); + const isSellableA = (!a.partSet ? false : a.partSet.isSellable); + const isSellableB = (!b.partSet ? false : b.partSet.isSellable); + + if(isSellableA && !isSellableB) return 1; + + if(isSellableB && !isSellableA) return -1; + + if(clubLevelA > clubLevelB) return -1; + + if(clubLevelA < clubLevelB) return 1; + + if(a.partSet.id > b.partSet.id) return -1; + + if(a.partSet.id < b.partSet.id) return 1; + + return 0; + } + + public static colorSorter(a: AvatarEditorGridColorItem, b: AvatarEditorGridColorItem): number + { + const clubLevelA = (!a.partColor ? -1 : a.partColor.clubLevel); + const clubLevelB = (!b.partColor ? -1 : b.partColor.clubLevel); + + if(clubLevelA < clubLevelB) return -1; + + if(clubLevelA > clubLevelB) return 1; + + if(a.partColor.index < b.partColor.index) return -1; + + if(a.partColor.index > b.partColor.index) return 1; + + return 0; + } + + public static noobSorter(a: AvatarEditorGridPartItem, b: AvatarEditorGridPartItem): number + { + const clubLevelA = (!a.partSet ? -1 : a.partSet.clubLevel); + const clubLevelB = (!b.partSet ? -1 : b.partSet.clubLevel); + const isSellableA = (!a.partSet ? false : a.partSet.isSellable); + const isSellableB = (!b.partSet ? false : b.partSet.isSellable); + + if(isSellableA && !isSellableB) return 1; + + if(isSellableB && !isSellableA) return -1; + + if(clubLevelA < clubLevelB) return -1; + + if(clubLevelA > clubLevelB) return 1; + + if(a.partSet.id < b.partSet.id) return -1; + + if(a.partSet.id > b.partSet.id) return 1; + + return 0; + } + + public static avatarSetFirstSelectableColor(name: string): number + { + const setType = GetAvatarSetType(name); + + if(!setType) return -1; + + const palette = GetAvatarPalette(setType.paletteID); + + if(!palette) return -1; + + for(const color of palette.colors.getValues()) + { + if(!color.isSelectable || (GetClubMemberLevel() < color.clubLevel)) continue; + + return color.id; + } + + return -1; + } + + public static get clubItemsFirst(): boolean + { + return GetConfiguration('avatareditor.show.clubitems.first', true); + } + + public static get clubItemsDimmed(): boolean + { + return GetConfiguration('avatareditor.show.clubitems.dimmed', true); + } +} diff --git a/apps/frontend/src/api/avatar/BodyModel.ts b/apps/frontend/src/api/avatar/BodyModel.ts new file mode 100644 index 0000000..7cdb34c --- /dev/null +++ b/apps/frontend/src/api/avatar/BodyModel.ts @@ -0,0 +1,76 @@ +import { AvatarEditorFigureCategory, AvatarScaleType, AvatarSetType } from '@nitrots/nitro-renderer'; +import { GetAvatarRenderManager } from '../nitro'; +import { AvatarEditorUtilities } from './AvatarEditorUtilities'; +import { CategoryBaseModel } from './CategoryBaseModel'; +import { FigureData } from './FigureData'; + +export class BodyModel extends CategoryBaseModel +{ + private _imageCallBackHandled: boolean = false; + + public init(): void + { + super.init(); + + this.addCategory(FigureData.FACE); + + this._isInitalized = true; + } + + public selectColor(category: string, colorIndex: number, paletteId: number): void + { + super.selectColor(category, colorIndex, paletteId); + + this.updateSelectionsFromFigure(FigureData.FACE); + } + + protected updateSelectionsFromFigure(name: string): void + { + if(!this._categories || !AvatarEditorUtilities.CURRENT_FIGURE) return; + + const category = this._categories.get(name); + + if(!category) return; + + const setId = AvatarEditorUtilities.CURRENT_FIGURE.getPartSetId(name); + + let colorIds = AvatarEditorUtilities.CURRENT_FIGURE.getColorIds(name); + + if(!colorIds) colorIds = []; + + category.selectPartId(setId); + category.selectColorIds(colorIds); + + for(const part of category.parts) + { + const resetFigure = (figure: string) => + { + const figureString = AvatarEditorUtilities.CURRENT_FIGURE.getFigureStringWithFace(part.id); + const avatarImage = GetAvatarRenderManager().createAvatarImage(figureString, AvatarScaleType.LARGE, null, { resetFigure, dispose: null, disposed: false }); + + const sprite = avatarImage.getImageAsSprite(AvatarSetType.HEAD); + + if(sprite) + { + sprite.y = 10; + + part.thumbContainer = sprite; + + setTimeout(() => avatarImage.dispose(), 0); + } + } + + resetFigure(null); + } + } + + public get canSetGender(): boolean + { + return true; + } + + public get name(): string + { + return AvatarEditorFigureCategory.GENERIC; + } +} diff --git a/apps/frontend/src/api/avatar/CategoryBaseModel.ts b/apps/frontend/src/api/avatar/CategoryBaseModel.ts new file mode 100644 index 0000000..34dd933 --- /dev/null +++ b/apps/frontend/src/api/avatar/CategoryBaseModel.ts @@ -0,0 +1,246 @@ +import { AvatarEditorUtilities } from './AvatarEditorUtilities'; +import { CategoryData } from './CategoryData'; +import { IAvatarEditorCategoryModel } from './IAvatarEditorCategoryModel'; + +export class CategoryBaseModel implements IAvatarEditorCategoryModel +{ + protected _categories: Map; + protected _isInitalized: boolean; + protected _maxPaletteCount: number; + private _disposed: boolean; + + constructor() + { + this._isInitalized = false; + this._maxPaletteCount = 0; + } + + public dispose(): void + { + this._categories = null; + this._disposed = true; + } + + public get disposed(): boolean + { + return this._disposed; + } + + public init(): void + { + if(!this._categories) this._categories = new Map(); + } + + public reset(): void + { + this._isInitalized = false; + + if(this._categories) + { + for(const category of this._categories.values()) (category && category.dispose()); + } + + this._categories = new Map(); + } + + protected addCategory(name: string): void + { + let existing = this._categories.get(name); + + if(existing) return; + + existing = AvatarEditorUtilities.createCategory(this, name); + + if(!existing) return; + + this._categories.set(name, existing); + + this.updateSelectionsFromFigure(name); + } + + protected updateSelectionsFromFigure(figure: string): void + { + const category = this._categories.get(figure); + + if(!category) return; + + const setId = AvatarEditorUtilities.CURRENT_FIGURE.getPartSetId(figure); + + let colorIds = AvatarEditorUtilities.CURRENT_FIGURE.getColorIds(figure); + + if(!colorIds) colorIds = []; + + category.selectPartId(setId); + category.selectColorIds(colorIds); + } + + public hasClubSelectionsOverLevel(level: number): boolean + { + if(!this._categories) return false; + + for(const category of this._categories.values()) + { + if(!category) continue; + + if(category.hasClubSelectionsOverLevel(level)) return true; + } + + return false; + } + + public hasInvalidSelectedItems(ownedItems: number[]): boolean + { + if(!this._categories) return false; + + for(const category of this._categories.values()) + { + if(category.hasInvalidSelectedItems(ownedItems)) return true; + } + + return false; + } + + public stripClubItemsOverLevel(level: number): boolean + { + if(!this._categories) return false; + + let didStrip = false; + + for(const [ name, category ] of this._categories.entries()) + { + let isValid = false; + + if(category.stripClubItemsOverLevel(level)) isValid = true; + + if(category.stripClubColorsOverLevel(level)) isValid = true; + + if(isValid) + { + const partItem = category.getCurrentPart(); + + if(partItem && AvatarEditorUtilities.CURRENT_FIGURE) + { + AvatarEditorUtilities.CURRENT_FIGURE.savePartData(name, partItem.id, category.getSelectedColorIds(), true); + } + + didStrip = true; + } + } + + return didStrip; + } + + public stripInvalidSellableItems(): boolean + { + if(!this._categories) return false; + + let didStrip = false; + + for(const [ name, category ] of this._categories.entries()) + { + const isValid = false; + + // if(category._Str_8360(this._Str_2278.manager.inventory)) _local_6 = true; + + if(isValid) + { + const partItem = category.getCurrentPart(); + + if(partItem && AvatarEditorUtilities.CURRENT_FIGURE) + { + AvatarEditorUtilities.CURRENT_FIGURE.savePartData(name, partItem.id, category.getSelectedColorIds(), true); + } + + didStrip = true; + } + } + + return didStrip; + } + + public selectPart(category: string, partIndex: number): void + { + const categoryData = this._categories.get(category); + + if(!categoryData) return; + + const selectedPartIndex = categoryData.selectedPartIndex; + + categoryData.selectPartIndex(partIndex); + + const partItem = categoryData.getCurrentPart(); + + if(!partItem) return; + + if(partItem.isDisabled) + { + categoryData.selectPartIndex(selectedPartIndex); + + // open hc window + + return; + } + + this._maxPaletteCount = partItem.maxColorIndex; + + AvatarEditorUtilities.CURRENT_FIGURE.savePartData(category, partItem.id, categoryData.getSelectedColorIds(), true); + } + + public selectColor(category: string, colorIndex: number, paletteId: number): void + { + const categoryData = this._categories.get(category); + + if(!categoryData) return; + + const paletteIndex = categoryData.getCurrentColorIndex(paletteId); + + categoryData.selectColorIndex(colorIndex, paletteId); + + const colorItem = categoryData.getSelectedColor(paletteId); + + if(colorItem.isDisabled) + { + categoryData.selectColorIndex(paletteIndex, paletteId); + + // open hc window + + return; + } + + AvatarEditorUtilities.CURRENT_FIGURE.savePartSetColourId(category, categoryData.getSelectedColorIds(), true); + } + + public getCategoryData(category: string): CategoryData + { + if(!this._isInitalized) this.init(); + + if(!this._categories) return null; + + return this._categories.get(category); + } + + public get categories(): Map + { + return this._categories; + } + + public get canSetGender(): boolean + { + return false; + } + + public get maxPaletteCount(): number + { + return (this._maxPaletteCount || 1); + } + + public set maxPaletteCount(count: number) + { + this._maxPaletteCount = count; + } + + public get name(): string + { + return null; + } +} diff --git a/apps/frontend/src/api/avatar/CategoryData.ts b/apps/frontend/src/api/avatar/CategoryData.ts new file mode 100644 index 0000000..db82f01 --- /dev/null +++ b/apps/frontend/src/api/avatar/CategoryData.ts @@ -0,0 +1,487 @@ +import { IPartColor } from '@nitrots/nitro-renderer'; +import { AvatarEditorGridColorItem } from './AvatarEditorGridColorItem'; +import { AvatarEditorGridPartItem } from './AvatarEditorGridPartItem'; + +export class CategoryData +{ + private _name: string; + private _parts: AvatarEditorGridPartItem[]; + private _palettes: AvatarEditorGridColorItem[][]; + private _selectedPartIndex: number = -1; + private _paletteIndexes: number[]; + + constructor(name: string, partItems: AvatarEditorGridPartItem[], colorItems: AvatarEditorGridColorItem[][]) + { + this._name = name; + this._parts = partItems; + this._palettes = colorItems; + this._selectedPartIndex = -1; + } + + private static defaultColorId(palettes: AvatarEditorGridColorItem[], clubLevel: number): number + { + if(!palettes || !palettes.length) return -1; + + let i = 0; + + while(i < palettes.length) + { + const colorItem = palettes[i]; + + if(colorItem.partColor && (colorItem.partColor.clubLevel <= clubLevel)) + { + return colorItem.partColor.id; + } + + i++; + } + + return -1; + } + + public init(): void + { + for(const part of this._parts) + { + if(!part) continue; + + part.init(); + } + } + + public dispose(): void + { + if(this._parts) + { + for(const part of this._parts) part.dispose(); + + this._parts = null; + } + + if(this._palettes) + { + for(const palette of this._palettes) for(const colorItem of palette) colorItem.dispose(); + + this._palettes = null; + } + + this._selectedPartIndex = -1; + this._paletteIndexes = null; + } + + public selectPartId(partId: number): void + { + if(!this._parts) return; + + let i = 0; + + while(i < this._parts.length) + { + const partItem = this._parts[i]; + + if(partItem.id === partId) + { + this.selectPartIndex(i); + + return; + } + + i++; + } + } + + public selectColorIds(colorIds: number[]): void + { + if(!colorIds || !this._palettes) return; + + this._paletteIndexes = new Array(colorIds.length); + + let i = 0; + + while(i < this._palettes.length) + { + const palette = this.getPalette(i); + + if(palette) + { + let colorId = 0; + + if(colorIds.length > i) + { + colorId = colorIds[i]; + } + else + { + const colorItem = palette[0]; + + if(colorItem && colorItem.partColor) colorId = colorItem.partColor.id; + } + + let j = 0; + + while(j < palette.length) + { + const colorItem = palette[j]; + + if(colorItem.partColor.id === colorId) + { + this._paletteIndexes[i] = j; + + colorItem.isSelected = true; + } + else + { + colorItem.isSelected = false; + } + + j++; + } + } + + i++; + } + + this.updatePartColors(); + } + + public selectPartIndex(partIndex: number): AvatarEditorGridPartItem + { + if(!this._parts) return null; + + if((this._selectedPartIndex >= 0) && (this._parts.length > this._selectedPartIndex)) + { + const partItem = this._parts[this._selectedPartIndex]; + + if(partItem) partItem.isSelected = false; + } + + if(this._parts.length > partIndex) + { + const partItem = this._parts[partIndex]; + + if(partItem) + { + partItem.isSelected = true; + + this._selectedPartIndex = partIndex; + + return partItem; + } + } + + return null; + } + + public selectColorIndex(colorIndex: number, paletteId: number): AvatarEditorGridColorItem + { + const palette = this.getPalette(paletteId); + + if(!palette) return null; + + if(palette.length <= colorIndex) return null; + + this.deselectColorIndex(this._paletteIndexes[paletteId], paletteId); + + this._paletteIndexes[paletteId] = colorIndex; + + const colorItem = palette[colorIndex]; + + if(!colorItem) return null; + + colorItem.isSelected = true; + + this.updatePartColors(); + + return colorItem; + } + + public getCurrentColorIndex(k: number): number + { + return this._paletteIndexes[k]; + } + + private deselectColorIndex(colorIndex: number, paletteIndex: number): void + { + const palette = this.getPalette(paletteIndex); + + if(!palette) return; + + if(palette.length <= colorIndex) return; + + const colorItem = palette[colorIndex]; + + if(!colorItem) return; + + colorItem.isSelected = false; + } + + public getSelectedColorIds(): number[] + { + if(!this._paletteIndexes || !this._paletteIndexes.length) return null; + + if(!this._palettes || !this._palettes.length) return null; + + const palette = this._palettes[0]; + + if(!palette || (!palette.length)) return null; + + const colorItem = palette[0]; + + if(!colorItem || !colorItem.partColor) return null; + + const colorId = colorItem.partColor.id; + const colorIds: number[] = []; + + let i = 0; + + while(i < this._paletteIndexes.length) + { + const paletteSet = this._palettes[i]; + + if(!((!(paletteSet)) || (paletteSet.length <= i))) + { + if(paletteSet.length > this._paletteIndexes[i]) + { + const color = paletteSet[this._paletteIndexes[i]]; + + if(color && color.partColor) + { + colorIds.push(color.partColor.id); + } + else + { + colorIds.push(colorId); + } + } + else + { + colorIds.push(colorId); + } + } + + i++; + } + + const partItem = this.getCurrentPart(); + + if(!partItem) return null; + + return colorIds.slice(0, Math.max(partItem.maxColorIndex, 1)); + } + + private getSelectedColors(): IPartColor[] + { + const partColors: IPartColor[] = []; + + let i = 0; + + while(i < this._paletteIndexes.length) + { + const colorItem = this.getSelectedColor(i); + + if(colorItem) + { + partColors.push(colorItem.partColor); + } + else + { + partColors.push(null); + } + + i++; + } + + return partColors; + } + + public getSelectedColor(paletteId: number): AvatarEditorGridColorItem + { + const palette = this.getPalette(paletteId); + + if(!palette || (palette.length <= this._paletteIndexes[paletteId])) return null; + + return palette[this._paletteIndexes[paletteId]]; + } + + public getSelectedColorId(paletteId: number): number + { + const colorItem = this.getSelectedColor(paletteId); + + if(colorItem && (colorItem.partColor)) return colorItem.partColor.id; + + return 0; + } + + public getPalette(paletteId: number): AvatarEditorGridColorItem[] + { + if(!this._paletteIndexes || !this._palettes || (this._palettes.length <= paletteId)) + { + return null; + } + + return this._palettes[paletteId]; + } + + public getCurrentPart(): AvatarEditorGridPartItem + { + return this._parts[this._selectedPartIndex] as AvatarEditorGridPartItem; + } + + private updatePartColors(): void + { + const partColors = this.getSelectedColors(); + + for(const partItem of this._parts) + { + if(partItem) partItem.partColors = partColors; + } + } + + public hasClubSelectionsOverLevel(level: number): boolean + { + let hasInvalidSelections = false; + + const partColors = this.getSelectedColors(); + + if(partColors) + { + let i = 0; + + while(i < partColors.length) + { + const partColor = partColors[i]; + + if(partColor && (partColor.clubLevel > level)) hasInvalidSelections = true; + + i++; + } + } + + const partItem = this.getCurrentPart(); + + if(partItem && partItem.partSet) + { + const partSet = partItem.partSet; + + if(partSet && (partSet.clubLevel > level)) hasInvalidSelections = true; + } + + return hasInvalidSelections; + } + + public hasInvalidSelectedItems(ownedItems: number[]): boolean + { + const part = this.getCurrentPart(); + + if(!part) return false; + + const partSet = part.partSet; + + if(!partSet || !partSet.isSellable) return; + + return (ownedItems.indexOf(partSet.id) > -1); + } + + public stripClubItemsOverLevel(level: number): boolean + { + const partItem = this.getCurrentPart(); + + if(partItem && partItem.partSet) + { + const partSet = partItem.partSet; + + if(partSet.clubLevel > level) + { + const newPartItem = this.selectPartIndex(0); + + if(newPartItem && !newPartItem.partSet) this.selectPartIndex(1); + + return true; + } + } + + return false; + } + + public stripClubColorsOverLevel(level: number): boolean + { + const colorIds: number[] = []; + const partColors = this.getSelectedColors(); + const colorItems = this.getPalette(0); + + let didStrip = false; + + const colorId = CategoryData.defaultColorId(colorItems, level); + + if(colorId === -1) return false; + + let i = 0; + + while(i < partColors.length) + { + const partColor = partColors[i]; + + if(!partColor) + { + colorIds.push(colorId); + + didStrip = true; + } + else + { + if(partColor.clubLevel > level) + { + colorIds.push(colorId); + + didStrip = true; + } + else + { + colorIds.push(partColor.id); + } + } + + i++; + } + + if(didStrip) this.selectColorIds(colorIds); + + return didStrip; + } + + // public stripInvalidSellableItems(k:IHabboInventory): boolean + // { + // var _local_3:IFigurePartSet; + // var _local_4:AvatarEditorGridPartItem; + // var _local_2:AvatarEditorGridPartItem = this._Str_6315(); + // if (((_local_2) && (_local_2.partSet))) + // { + // _local_3 = _local_2.partSet; + // if (((_local_3.isSellable) && (!(k._Str_14439(_local_3.id))))) + // { + // _local_4 = this._Str_8066(0); + // if (((!(_local_4 == null)) && (_local_4.partSet == null))) + // { + // this._Str_8066(1); + // } + // return true; + // } + // } + // return false; + // } + + public get name(): string + { + return this._name; + } + + public get parts(): AvatarEditorGridPartItem[] + { + return this._parts; + } + + public get selectedPartIndex(): number + { + return this._selectedPartIndex; + } +} diff --git a/apps/frontend/src/api/avatar/FigureData.ts b/apps/frontend/src/api/avatar/FigureData.ts new file mode 100644 index 0000000..78014d1 --- /dev/null +++ b/apps/frontend/src/api/avatar/FigureData.ts @@ -0,0 +1,287 @@ +import { AvatarEditorUtilities } from './AvatarEditorUtilities'; + +export class FigureData +{ + private static DEFAULT_DIRECTION: number = 4; + + public static MALE: string = 'M'; + public static FEMALE: string = 'F'; + public static UNISEX: string = 'U'; + public static SCALE: string = 'h'; + public static STD: string = 'std'; + public static DEFAULT_FRAME: string = '0'; + public static FACE: string = 'hd'; + public static HAIR: string = 'hr'; + public static HAT: string = 'ha'; + public static HEAD_ACCESSORIES: string = 'he'; + public static EYE_ACCESSORIES: string = 'ea'; + public static FACE_ACCESSORIES: string = 'fa'; + public static JACKET: string = 'cc'; + public static SHIRT: string = 'ch'; + public static CHEST_ACCESSORIES: string = 'ca'; + public static CHEST_PRINTS: string = 'cp'; + public static TROUSERS: string = 'lg'; + public static SHOES: string = 'sh'; + public static TROUSER_ACCESSORIES: string = 'wa'; + public static SET_TYPES = [ FigureData.FACE, FigureData.HAIR, FigureData.HAT, FigureData.HEAD_ACCESSORIES, FigureData.EYE_ACCESSORIES, FigureData.FACE_ACCESSORIES, FigureData.JACKET, FigureData.SHIRT, FigureData.CHEST_ACCESSORIES, FigureData.CHEST_PRINTS, FigureData.TROUSERS, FigureData.SHOES, FigureData.TROUSERS ]; + + private _data: Map; + private _colors: Map; + private _gender: string = 'M'; + private _direction: number = FigureData.DEFAULT_DIRECTION; + private _avatarEffectType: number = -1; + private _notifier: () => void = null; + + public loadAvatarData(figureString: string, gender: string): void + { + this._data = new Map(); + this._colors = new Map(); + this._gender = gender; + + this.parseFigureString(figureString); + this.updateView(); + } + + private parseFigureString(figure: string): void + { + if(!figure) return; + + const sets = figure.split('.'); + + if(!sets || !sets.length) return; + + for(const set of sets) + { + const parts = set.split('-'); + + if(!parts.length) continue; + + const setType = parts[0]; + const setId = parseInt(parts[1]); + const colorIds: number[] = []; + + let offset = 2; + + while(offset < parts.length) + { + colorIds.push(parseInt(parts[offset])); + + offset++; + } + + if(!colorIds.length) colorIds.push(0); + + this.savePartSetId(setType, setId, false); + this.savePartSetColourId(setType, colorIds, false); + } + } + + public getPartSetId(setType: string): number + { + const existing = this._data.get(setType); + + if(existing !== undefined) return existing; + + return -1; + } + + public getColorIds(setType: string): number[] + { + const existing = this._colors.get(setType); + + if(existing !== undefined) return existing; + + return [ AvatarEditorUtilities.avatarSetFirstSelectableColor(setType) ]; + } + + public getFigureString(): string + { + let figureString = ''; + const setParts: string[] = []; + + for(const [ setType, setId ] of this._data.entries()) + { + const colorIds = this._colors.get(setType); + + let setPart = ((setType + '-') + setId); + + if(colorIds && colorIds.length) + { + let i = 0; + + while(i < colorIds.length) + { + setPart = (setPart + ('-' + colorIds[i])); + + i++; + } + } + + setParts.push(setPart); + } + + let i = 0; + + while(i < setParts.length) + { + figureString = (figureString + setParts[i]); + + if(i < (setParts.length - 1)) figureString = (figureString + '.'); + + i++; + } + + return figureString; + } + + public savePartData(setType: string, partId: number, colorIds: number[], update: boolean = false): void + { + this.savePartSetId(setType, partId, update); + this.savePartSetColourId(setType, colorIds, update); + } + + private savePartSetId(setType: string, partId: number, update: boolean = true): void + { + switch(setType) + { + case FigureData.FACE: + case FigureData.HAIR: + case FigureData.HAT: + case FigureData.HEAD_ACCESSORIES: + case FigureData.EYE_ACCESSORIES: + case FigureData.FACE_ACCESSORIES: + case FigureData.SHIRT: + case FigureData.JACKET: + case FigureData.CHEST_ACCESSORIES: + case FigureData.CHEST_PRINTS: + case FigureData.TROUSERS: + case FigureData.SHOES: + case FigureData.TROUSER_ACCESSORIES: + if(partId >= 0) + { + this._data.set(setType, partId); + } + else + { + this._data.delete(setType); + } + break; + } + + if(update) this.updateView(); + } + + public savePartSetColourId(setType: string, colorIds: number[], update: boolean = true): void + { + switch(setType) + { + case FigureData.FACE: + case FigureData.HAIR: + case FigureData.HAT: + case FigureData.HEAD_ACCESSORIES: + case FigureData.EYE_ACCESSORIES: + case FigureData.FACE_ACCESSORIES: + case FigureData.SHIRT: + case FigureData.JACKET: + case FigureData.CHEST_ACCESSORIES: + case FigureData.CHEST_PRINTS: + case FigureData.TROUSERS: + case FigureData.SHOES: + case FigureData.TROUSER_ACCESSORIES: + this._colors.set(setType, colorIds); + break; + } + + if(update) this.updateView(); + } + + public getFigureStringWithFace(k: number, override = true): string + { + let figureString = ''; + + const setTypes: string[] = [ FigureData.FACE ]; + const figureSets: string[] = []; + + for(const setType of setTypes) + { + const colors = this._colors.get(setType); + + if(!colors) continue; + + let setId = this._data.get(setType); + + if((setType === FigureData.FACE) && override) setId = k; + + let figureSet = ((setType + '-') + setId); + + if(setId >= 0) + { + let i = 0; + + while(i < colors.length) + { + figureSet = (figureSet + ('-' + colors[i])); + + i++; + } + } + + figureSets.push(figureSet); + } + + let i = 0; + + while(i < figureSets.length) + { + figureString = (figureString + figureSets[i]); + + if(i < (figureSets.length - 1)) figureString = (figureString + '.'); + + i++; + } + + return figureString; + } + + public updateView(): void + { + if(this.notify) this.notify(); + } + + public get gender(): string + { + return this._gender; + } + + public get direction(): number + { + return this._direction; + } + + public set direction(direction: number) + { + this._direction = direction; + + this.updateView(); + } + + public set avatarEffectType(k: number) + { + this._avatarEffectType = k; + } + + public get avatarEffectType(): number + { + return this._avatarEffectType; + } + + public get notify(): () => void + { + return this._notifier; + } + + public set notify(notifier: () => void) + { + this._notifier = notifier; + } +} diff --git a/apps/frontend/src/api/avatar/FigureGenerator.ts b/apps/frontend/src/api/avatar/FigureGenerator.ts new file mode 100644 index 0000000..b83a661 --- /dev/null +++ b/apps/frontend/src/api/avatar/FigureGenerator.ts @@ -0,0 +1,90 @@ +import { AvatarFigureContainer, IFigurePartSet, IPalette, IPartColor, SetType } from '@nitrots/nitro-renderer'; +import { GetAvatarRenderManager } from '../nitro'; +import { Randomizer } from '../utils'; +import { FigureData } from './FigureData'; + +function getTotalColors(partSet: IFigurePartSet): number +{ + const parts = partSet.parts; + + let totalColors = 0; + + for(const part of parts) totalColors = Math.max(totalColors, part.colorLayerIndex); + + return totalColors; +} + +function getRandomSetTypes(requiredSets: string[], options: string[]): string[] +{ + options = options.filter(option => (requiredSets.indexOf(option) === -1)); + + return [ ...requiredSets, ...Randomizer.getRandomElements(options, (Randomizer.getRandomNumber(options.length) + 1)) ]; +} + +function getRandomPartSet(setType: SetType, gender: string, clubLevel: number = 0, figureSetIds: number[] = []): IFigurePartSet +{ + if(!setType) return null; + + const options = setType.partSets.getValues().filter(option => + { + if(!option.isSelectable || ((option.gender !== 'U') && (option.gender !== gender)) || (option.clubLevel > clubLevel) || (option.isSellable && (figureSetIds.indexOf(option.id) === -1))) return null; + + return option; + }); + + if(!options || !options.length) return null; + + return Randomizer.getRandomElement(options); +} + +function getRandomColors(palette: IPalette, partSet: IFigurePartSet, clubLevel: number = 0): IPartColor[] +{ + if(!palette) return []; + + const options = palette.colors.getValues().filter(option => + { + if(!option.isSelectable || (option.clubLevel > clubLevel)) return null; + + return option; + }); + + if(!options || !options.length) return null; + + return Randomizer.getRandomElements(options, getTotalColors(partSet)); +} + +export function generateRandomFigure(figureData: FigureData, gender: string, clubLevel: number = 0, figureSetIds: number[] = [], ignoredSets: string[] = []): string +{ + const structure = GetAvatarRenderManager().structure; + const figureContainer = new AvatarFigureContainer(''); + const requiredSets = getRandomSetTypes(structure.getMandatorySetTypeIds(gender, clubLevel), FigureData.SET_TYPES); + + for(const setType of ignoredSets) + { + const partSetId = figureData.getPartSetId(setType); + const colors = figureData.getColorIds(setType); + + figureContainer.updatePart(setType, partSetId, colors); + } + + for(const type of requiredSets) + { + if(figureContainer.hasPartType(type)) continue; + + const setType = (structure.figureData.getSetType(type) as SetType); + const selectedSet = getRandomPartSet(setType, gender, clubLevel, figureSetIds); + + if(!selectedSet) continue; + + let selectedColors: number[] = []; + + if(selectedSet.isColorable) + { + selectedColors = getRandomColors(structure.figureData.getPalette(setType.paletteID), selectedSet, clubLevel).map(color => color.id); + } + + figureContainer.updatePart(setType.type, selectedSet.id, selectedColors); + } + + return figureContainer.getFigureString(); +} diff --git a/apps/frontend/src/api/avatar/HeadModel.ts b/apps/frontend/src/api/avatar/HeadModel.ts new file mode 100644 index 0000000..5dc30cd --- /dev/null +++ b/apps/frontend/src/api/avatar/HeadModel.ts @@ -0,0 +1,24 @@ +import { AvatarEditorFigureCategory } from '@nitrots/nitro-renderer'; +import { CategoryBaseModel } from './CategoryBaseModel'; +import { FigureData } from './FigureData'; + +export class HeadModel extends CategoryBaseModel +{ + public init(): void + { + super.init(); + + this.addCategory(FigureData.HAIR); + this.addCategory(FigureData.HAT); + this.addCategory(FigureData.HEAD_ACCESSORIES); + this.addCategory(FigureData.EYE_ACCESSORIES); + this.addCategory(FigureData.FACE_ACCESSORIES); + + this._isInitalized = true; + } + + public get name(): string + { + return AvatarEditorFigureCategory.HEAD; + } +} diff --git a/apps/frontend/src/api/avatar/IAvatarEditorCategoryModel.ts b/apps/frontend/src/api/avatar/IAvatarEditorCategoryModel.ts new file mode 100644 index 0000000..dc9affa --- /dev/null +++ b/apps/frontend/src/api/avatar/IAvatarEditorCategoryModel.ts @@ -0,0 +1,19 @@ +import { CategoryData } from './CategoryData'; + +export interface IAvatarEditorCategoryModel +{ + init(): void; + dispose(): void; + reset(): void; + getCategoryData(category: string): CategoryData; + selectPart(category: string, partIndex: number): void; + selectColor(category: string, colorIndex: number, paletteId: number): void; + hasClubSelectionsOverLevel(level: number): boolean; + hasInvalidSelectedItems(ownedItems: number[]): boolean; + stripClubItemsOverLevel(level: number): boolean; + stripInvalidSellableItems(): boolean; + categories: Map; + canSetGender: boolean; + maxPaletteCount: number; + name: string; +} diff --git a/apps/frontend/src/api/avatar/LegModel.ts b/apps/frontend/src/api/avatar/LegModel.ts new file mode 100644 index 0000000..5633930 --- /dev/null +++ b/apps/frontend/src/api/avatar/LegModel.ts @@ -0,0 +1,22 @@ +import { AvatarEditorFigureCategory } from '@nitrots/nitro-renderer'; +import { CategoryBaseModel } from './CategoryBaseModel'; +import { FigureData } from './FigureData'; + +export class LegModel extends CategoryBaseModel +{ + public init(): void + { + super.init(); + + this.addCategory(FigureData.TROUSERS); + this.addCategory(FigureData.SHOES); + this.addCategory(FigureData.TROUSER_ACCESSORIES); + + this._isInitalized = true; + } + + public get name(): string + { + return AvatarEditorFigureCategory.LEGS; + } +} diff --git a/apps/frontend/src/api/avatar/TorsoModel.ts b/apps/frontend/src/api/avatar/TorsoModel.ts new file mode 100644 index 0000000..43e48cf --- /dev/null +++ b/apps/frontend/src/api/avatar/TorsoModel.ts @@ -0,0 +1,23 @@ +import { AvatarEditorFigureCategory } from '@nitrots/nitro-renderer'; +import { CategoryBaseModel } from './CategoryBaseModel'; +import { FigureData } from './FigureData'; + +export class TorsoModel extends CategoryBaseModel +{ + public init(): void + { + super.init(); + + this.addCategory(FigureData.SHIRT); + this.addCategory(FigureData.CHEST_PRINTS); + this.addCategory(FigureData.JACKET); + this.addCategory(FigureData.CHEST_ACCESSORIES); + + this._isInitalized = true; + } + + public get name(): string + { + return AvatarEditorFigureCategory.TORSO; + } +} diff --git a/apps/frontend/src/api/avatar/index.ts b/apps/frontend/src/api/avatar/index.ts new file mode 100644 index 0000000..37b3072 --- /dev/null +++ b/apps/frontend/src/api/avatar/index.ts @@ -0,0 +1,13 @@ +export * from './AvatarEditorAction'; +export * from './AvatarEditorGridColorItem'; +export * from './AvatarEditorGridPartItem'; +export * from './AvatarEditorUtilities'; +export * from './BodyModel'; +export * from './CategoryBaseModel'; +export * from './CategoryData'; +export * from './FigureData'; +export * from './FigureGenerator'; +export * from './HeadModel'; +export * from './IAvatarEditorCategoryModel'; +export * from './LegModel'; +export * from './TorsoModel'; diff --git a/apps/frontend/src/api/camera/CameraEditorTabs.ts b/apps/frontend/src/api/camera/CameraEditorTabs.ts new file mode 100644 index 0000000..6e894e7 --- /dev/null +++ b/apps/frontend/src/api/camera/CameraEditorTabs.ts @@ -0,0 +1,5 @@ +export class CameraEditorTabs +{ + public static readonly COLORMATRIX: string = 'colormatrix'; + public static readonly COMPOSITE: string = 'composite'; +} diff --git a/apps/frontend/src/api/camera/CameraPicture.ts b/apps/frontend/src/api/camera/CameraPicture.ts new file mode 100644 index 0000000..fe8c221 --- /dev/null +++ b/apps/frontend/src/api/camera/CameraPicture.ts @@ -0,0 +1,9 @@ +import { NitroTexture } from '@nitrots/nitro-renderer'; + +export class CameraPicture +{ + constructor( + public texture: NitroTexture, + public imageUrl: string) + {} +} diff --git a/apps/frontend/src/api/camera/CameraPictureThumbnail.ts b/apps/frontend/src/api/camera/CameraPictureThumbnail.ts new file mode 100644 index 0000000..cd12660 --- /dev/null +++ b/apps/frontend/src/api/camera/CameraPictureThumbnail.ts @@ -0,0 +1,7 @@ +export class CameraPictureThumbnail +{ + constructor( + public effectName: string, + public thumbnailUrl: string) + {} +} diff --git a/apps/frontend/src/api/camera/index.ts b/apps/frontend/src/api/camera/index.ts new file mode 100644 index 0000000..93c6ccb --- /dev/null +++ b/apps/frontend/src/api/camera/index.ts @@ -0,0 +1,3 @@ +export * from './CameraEditorTabs'; +export * from './CameraPicture'; +export * from './CameraPictureThumbnail'; diff --git a/apps/frontend/src/api/campaign/CalendarItem.ts b/apps/frontend/src/api/campaign/CalendarItem.ts new file mode 100644 index 0000000..d3634b3 --- /dev/null +++ b/apps/frontend/src/api/campaign/CalendarItem.ts @@ -0,0 +1,30 @@ +import { ICalendarItem } from './ICalendarItem'; + +export class CalendarItem implements ICalendarItem +{ + private _productName: string; + private _customImage: string; + private _furnitureClassName: string; + + constructor(productName: string, customImage: string, furnitureClassName: string) + { + this._productName = productName; + this._customImage = customImage; + this._furnitureClassName = furnitureClassName; + } + + public get productName(): string + { + return this._productName; + } + + public get customImage(): string + { + return this._customImage; + } + + public get furnitureClassName(): string + { + return this._furnitureClassName; + } +} diff --git a/apps/frontend/src/api/campaign/CalendarItemState.ts b/apps/frontend/src/api/campaign/CalendarItemState.ts new file mode 100644 index 0000000..1b91ca3 --- /dev/null +++ b/apps/frontend/src/api/campaign/CalendarItemState.ts @@ -0,0 +1,7 @@ +export class CalendarItemState +{ + public static readonly STATE_UNLOCKED = 1; + public static readonly STATE_LOCKED_AVAILABLE = 2; + public static readonly STATE_LOCKED_EXPIRED = 3; + public static readonly STATE_LOCKED_FUTURE = 4; +} diff --git a/apps/frontend/src/api/campaign/ICalendarItem.ts b/apps/frontend/src/api/campaign/ICalendarItem.ts new file mode 100644 index 0000000..87dfbd6 --- /dev/null +++ b/apps/frontend/src/api/campaign/ICalendarItem.ts @@ -0,0 +1,6 @@ +export interface ICalendarItem +{ + readonly productName: string; + readonly customImage: string; + readonly furnitureClassName: string; +} diff --git a/apps/frontend/src/api/campaign/index.ts b/apps/frontend/src/api/campaign/index.ts new file mode 100644 index 0000000..a86e40c --- /dev/null +++ b/apps/frontend/src/api/campaign/index.ts @@ -0,0 +1,3 @@ +export * from './CalendarItem'; +export * from './CalendarItemState'; +export * from './ICalendarItem'; diff --git a/apps/frontend/src/api/catalog/BuilderFurniPlaceableStatus.ts b/apps/frontend/src/api/catalog/BuilderFurniPlaceableStatus.ts new file mode 100644 index 0000000..40eb6f6 --- /dev/null +++ b/apps/frontend/src/api/catalog/BuilderFurniPlaceableStatus.ts @@ -0,0 +1,10 @@ +export class BuilderFurniPlaceableStatus +{ + public static OKAY: number = 0; + public static MISSING_OFFER: number = 1; + public static FURNI_LIMIT_REACHED: number = 2; + public static NOT_IN_ROOM: number = 3; + public static NOT_ROOM_OWNER: number = 4; + public static GUILD_ROOM: number = 5; + public static VISITORS_IN_ROOM: number = 6; +} diff --git a/apps/frontend/src/api/catalog/CatalogNode.ts b/apps/frontend/src/api/catalog/CatalogNode.ts new file mode 100644 index 0000000..893aa32 --- /dev/null +++ b/apps/frontend/src/api/catalog/CatalogNode.ts @@ -0,0 +1,124 @@ +import { NodeData } from '@nitrots/nitro-renderer'; +import { ICatalogNode } from './ICatalogNode'; + +export class CatalogNode implements ICatalogNode +{ + private _depth: number = 0; + private _localization: string = ''; + private _pageId: number = -1; + private _pageName: string = ''; + private _iconId: number = 0; + private _children: ICatalogNode[]; + private _offerIds: number[]; + private _parent: ICatalogNode; + private _isVisible: boolean; + private _isActive: boolean; + private _isOpen: boolean; + + constructor(node: NodeData, depth: number, parent: ICatalogNode) + { + this._depth = depth; + this._parent = parent; + this._localization = node.localization; + this._pageId = node.pageId; + this._pageName = node.pageName; + this._iconId = node.icon; + this._children = []; + this._offerIds = node.offerIds; + this._isVisible = node.visible; + this._isActive = false; + this._isOpen = false; + } + + public activate(): void + { + this._isActive = true; + } + + public deactivate(): void + { + this._isActive = false; + } + + public open(): void + { + this._isOpen = true; + } + + public close(): void + { + this._isOpen = false; + } + + public addChild(child: ICatalogNode):void + { + if(!child) return; + + this._children.push(child); + } + + public get depth(): number + { + return this._depth; + } + + public get isBranch(): boolean + { + return (this._children.length > 0); + } + + public get isLeaf(): boolean + { + return (this._children.length === 0); + } + + public get localization(): string + { + return this._localization; + } + + public get pageId(): number + { + return this._pageId; + } + + public get pageName(): string + { + return this._pageName; + } + + public get iconId(): number + { + return this._iconId; + } + + public get children(): ICatalogNode[] + { + return this._children; + } + + public get offerIds(): number[] + { + return this._offerIds; + } + + public get parent(): ICatalogNode + { + return this._parent; + } + + public get isVisible(): boolean + { + return this._isVisible; + } + + public get isActive(): boolean + { + return this._isActive; + } + + public get isOpen(): boolean + { + return this._isOpen; + } +} diff --git a/apps/frontend/src/api/catalog/CatalogPage.ts b/apps/frontend/src/api/catalog/CatalogPage.ts new file mode 100644 index 0000000..1e80609 --- /dev/null +++ b/apps/frontend/src/api/catalog/CatalogPage.ts @@ -0,0 +1,59 @@ +import { ICatalogPage } from './ICatalogPage'; +import { IPageLocalization } from './IPageLocalization'; +import { IPurchasableOffer } from './IPurchasableOffer'; + +export class CatalogPage implements ICatalogPage +{ + public static MODE_NORMAL: number = 0; + + private _pageId: number; + private _layoutCode: string; + private _localization: IPageLocalization; + private _offers: IPurchasableOffer[]; + private _acceptSeasonCurrencyAsCredits: boolean; + private _mode: number; + + constructor(pageId: number, layoutCode: string, localization: IPageLocalization, offers: IPurchasableOffer[], acceptSeasonCurrencyAsCredits: boolean, mode: number = -1) + { + this._pageId = pageId; + this._layoutCode = layoutCode; + this._localization = localization; + this._offers = offers; + this._acceptSeasonCurrencyAsCredits = acceptSeasonCurrencyAsCredits; + + for(const offer of offers) (offer.page = this); + + if(mode === -1) this._mode = CatalogPage.MODE_NORMAL; + else this._mode = mode; + } + + public get pageId(): number + { + return this._pageId; + } + + public get layoutCode(): string + { + return this._layoutCode; + } + + public get localization(): IPageLocalization + { + return this._localization; + } + + public get offers(): IPurchasableOffer[] + { + return this._offers; + } + + public get acceptSeasonCurrencyAsCredits(): boolean + { + return this._acceptSeasonCurrencyAsCredits; + } + + public get mode(): number + { + return this._mode; + } +} diff --git a/apps/frontend/src/api/catalog/CatalogPageName.ts b/apps/frontend/src/api/catalog/CatalogPageName.ts new file mode 100644 index 0000000..8e4c7b6 --- /dev/null +++ b/apps/frontend/src/api/catalog/CatalogPageName.ts @@ -0,0 +1,26 @@ +export class CatalogPageName +{ + public static DUCKET_INFO: string = 'ducket_info'; + public static CREDITS: string = 'credits'; + public static AVATAR_EFFECTS: string = 'avatar_effects'; + public static HC_MEMBERSHIP: string = 'hc_membership'; + public static CLUB_GIFTS: string = 'club_gifts'; + public static LIMITED_SOLD: string = 'limited_sold'; + public static PET_ACCESSORIES: string = 'pet_accessories'; + public static TRAX_SONGS: string = 'trax_songs'; + public static NEW_ADDITIONS: string = 'new_additions'; + public static QUEST_SHELL: string = 'quest_shell'; + public static QUEST_SNOWFLAKES: string = 'quest_snowflakes'; + public static VAL_QUESTS: string = 'val_quests'; + public static GUILD_CUSTOM_FURNI: string = 'guild_custom_furni'; + public static GIFT_SHOP: string = 'gift_shop'; + public static HORSE_STYLES: string = 'horse_styles'; + public static HORSE_SHOE: string = 'horse_shoe'; + public static SET_EASTER: string = 'set_easter'; + public static ECOTRON_TRANSFORM: string = 'ecotron_transform'; + public static LOYALTY_INFO: string = 'loyalty_info'; + public static ROOM_BUNDLES: string = 'room_bundles'; + public static ROOM_BUNDLES_MOBILE: string = 'room_bundles_mobile'; + public static HABBO_CLUB_DESKTOP: string = 'habbo_club_desktop'; + public static MOBILE_SUBSCRIPTIONS: string = 'mobile_subscriptions'; +} diff --git a/apps/frontend/src/api/catalog/CatalogPetPalette.ts b/apps/frontend/src/api/catalog/CatalogPetPalette.ts new file mode 100644 index 0000000..d92c40d --- /dev/null +++ b/apps/frontend/src/api/catalog/CatalogPetPalette.ts @@ -0,0 +1,10 @@ +import { SellablePetPaletteData } from '@nitrots/nitro-renderer'; + +export class CatalogPetPalette +{ + constructor( + public readonly breed: string, + public readonly palettes: SellablePetPaletteData[] + ) + {} +} diff --git a/apps/frontend/src/api/catalog/CatalogPurchaseState.ts b/apps/frontend/src/api/catalog/CatalogPurchaseState.ts new file mode 100644 index 0000000..b442f62 --- /dev/null +++ b/apps/frontend/src/api/catalog/CatalogPurchaseState.ts @@ -0,0 +1,10 @@ +export class CatalogPurchaseState +{ + public static NONE = 0; + public static CONFIRM = 1; + public static PURCHASE = 2; + public static NO_CREDITS = 3; + public static NO_POINTS = 4; + public static SOLD_OUT = 5; + public static FAILED = 6; +} diff --git a/apps/frontend/src/api/catalog/CatalogType.ts b/apps/frontend/src/api/catalog/CatalogType.ts new file mode 100644 index 0000000..670ad6f --- /dev/null +++ b/apps/frontend/src/api/catalog/CatalogType.ts @@ -0,0 +1,5 @@ +export class CatalogType +{ + public static NORMAL: string = 'NORMAL'; + public static BUILDER: string = 'BUILDERS_CLUB'; +} diff --git a/apps/frontend/src/api/catalog/CatalogUtilities.ts b/apps/frontend/src/api/catalog/CatalogUtilities.ts new file mode 100644 index 0000000..5ca8fed --- /dev/null +++ b/apps/frontend/src/api/catalog/CatalogUtilities.ts @@ -0,0 +1,125 @@ +import { SellablePetPaletteData } from '@nitrots/nitro-renderer'; +import { GetRoomEngine } from '../nitro'; +import { ICatalogNode } from './ICatalogNode'; + +export const GetPixelEffectIcon = (id: number) => +{ + return ''; +} + +export const GetSubscriptionProductIcon = (id: number) => +{ + return ''; +} + +export const GetOfferNodes = (offerNodes: Map, offerId: number) => +{ + const nodes = offerNodes.get(offerId); + const allowedNodes: ICatalogNode[] = []; + + if(nodes && nodes.length) + { + for(const node of nodes) + { + if(!node.isVisible) continue; + + allowedNodes.push(node); + } + } + + return allowedNodes; +} + +export const FilterCatalogNode = (search: string, furniLines: string[], node: ICatalogNode, nodes: ICatalogNode[]) => +{ + if(node.isVisible && (node.pageId > 0)) + { + let nodeAdded = false; + + const hayStack = [ node.pageName, node.localization ].join(' ').toLowerCase().replace(/ /gi, ''); + + if(hayStack.indexOf(search) > -1) + { + nodes.push(node); + + nodeAdded = true; + } + + if(!nodeAdded) + { + for(const furniLine of furniLines) + { + if(hayStack.indexOf(furniLine) >= 0) + { + nodes.push(node); + + break; + } + } + } + } + + for(const child of node.children) FilterCatalogNode(search, furniLines, child, nodes); +} + +export function GetPetIndexFromLocalization(localization: string) +{ + if(!localization.length) return 0; + + let index = (localization.length - 1); + + while(index >= 0) + { + if(isNaN(parseInt(localization.charAt(index)))) break; + + index--; + } + + if(index > 0) return parseInt(localization.substring(index + 1)); + + return -1; +} + +export function GetPetAvailableColors(petIndex: number, palettes: SellablePetPaletteData[]): number[][] +{ + switch(petIndex) + { + case 0: + return [ [ 16743226 ], [ 16750435 ], [ 16764339 ], [ 0xF59500 ], [ 16498012 ], [ 16704690 ], [ 0xEDD400 ], [ 16115545 ], [ 16513201 ], [ 8694111 ], [ 11585939 ], [ 14413767 ], [ 6664599 ], [ 9553845 ], [ 12971486 ], [ 8358322 ], [ 10002885 ], [ 13292268 ], [ 10780600 ], [ 12623573 ], [ 14403561 ], [ 12418717 ], [ 14327229 ], [ 15517403 ], [ 14515069 ], [ 15764368 ], [ 16366271 ], [ 0xABABAB ], [ 0xD4D4D4 ], [ 0xFFFFFF ], [ 14256481 ], [ 14656129 ], [ 15848130 ], [ 14005087 ], [ 14337152 ], [ 15918540 ], [ 15118118 ], [ 15531929 ], [ 9764857 ], [ 11258085 ] ]; + case 1: + return [ [ 16743226 ], [ 16750435 ], [ 16764339 ], [ 0xF59500 ], [ 16498012 ], [ 16704690 ], [ 0xEDD400 ], [ 16115545 ], [ 16513201 ], [ 8694111 ], [ 11585939 ], [ 14413767 ], [ 6664599 ], [ 9553845 ], [ 12971486 ], [ 8358322 ], [ 10002885 ], [ 13292268 ], [ 10780600 ], [ 12623573 ], [ 14403561 ], [ 12418717 ], [ 14327229 ], [ 15517403 ], [ 14515069 ], [ 15764368 ], [ 16366271 ], [ 0xABABAB ], [ 0xD4D4D4 ], [ 0xFFFFFF ], [ 14256481 ], [ 14656129 ], [ 15848130 ], [ 14005087 ], [ 14337152 ], [ 15918540 ], [ 15118118 ], [ 15531929 ], [ 9764857 ], [ 11258085 ] ]; + case 2: + return [ [ 16579283 ], [ 15378351 ], [ 8830016 ], [ 15257125 ], [ 9340985 ], [ 8949607 ], [ 6198292 ], [ 8703620 ], [ 9889626 ], [ 8972045 ], [ 12161285 ], [ 13162269 ], [ 8620113 ], [ 12616503 ], [ 8628101 ], [ 0xD2FF00 ], [ 9764857 ] ]; + case 3: + return [ [ 0xFFFFFF ], [ 0xEEEEEE ], [ 0xDDDDDD ] ]; + case 4: + return [ [ 0xFFFFFF ], [ 16053490 ], [ 15464440 ], [ 16248792 ], [ 15396319 ], [ 15007487 ] ]; + case 5: + return [ [ 0xFFFFFF ], [ 0xEEEEEE ], [ 0xDDDDDD ] ]; + case 6: + return [ [ 0xFFFFFF ], [ 0xEEEEEE ], [ 0xDDDDDD ], [ 16767177 ], [ 16770205 ], [ 16751331 ] ]; + case 7: + return [ [ 0xCCCCCC ], [ 0xAEAEAE ], [ 16751331 ], [ 10149119 ], [ 16763290 ], [ 16743786 ] ]; + default: { + const colors: number[][] = []; + + for(const palette of palettes) + { + const petColorResult = GetRoomEngine().getPetColorResult(petIndex, palette.paletteId); + + if(!petColorResult) continue; + + if(petColorResult.primaryColor === petColorResult.secondaryColor) + { + colors.push([ petColorResult.primaryColor ]); + } + else + { + colors.push([ petColorResult.primaryColor, petColorResult.secondaryColor ]); + } + } + + return colors; + } + } +} diff --git a/apps/frontend/src/api/catalog/FurnitureOffer.ts b/apps/frontend/src/api/catalog/FurnitureOffer.ts new file mode 100644 index 0000000..4c9c9f9 --- /dev/null +++ b/apps/frontend/src/api/catalog/FurnitureOffer.ts @@ -0,0 +1,120 @@ +import { GetProductOfferComposer, IFurnitureData } from '@nitrots/nitro-renderer'; +import { GetProductDataForLocalization, SendMessageComposer } from '..'; +import { ICatalogPage } from './ICatalogPage'; +import { IProduct } from './IProduct'; +import { IPurchasableOffer } from './IPurchasableOffer'; +import { Offer } from './Offer'; +import { Product } from './Product'; + +export class FurnitureOffer implements IPurchasableOffer +{ + private _furniData:IFurnitureData; + private _page: ICatalogPage; + private _product: IProduct; + + constructor(furniData: IFurnitureData) + { + this._furniData = furniData; + this._product = (new Product(this._furniData.type, this._furniData.id, this._furniData.customParams, 1, GetProductDataForLocalization(this._furniData.className), this._furniData) as IProduct); + } + + public activate(): void + { + SendMessageComposer(new GetProductOfferComposer((this._furniData.rentOfferId > -1) ? this._furniData.rentOfferId : this._furniData.purchaseOfferId)); + } + + public get offerId(): number + { + return (this.isRentOffer) ? this._furniData.rentOfferId : this._furniData.purchaseOfferId; + } + + public get priceInActivityPoints(): number + { + return 0; + } + + public get activityPointType(): number + { + return 0; + } + + public get priceInCredits(): number + { + return 0; + } + + public get page(): ICatalogPage + { + return this._page; + } + + public set page(page: ICatalogPage) + { + this._page = page; + } + + public get priceType(): string + { + return ''; + } + + public get product(): IProduct + { + return this._product; + } + + public get products(): IProduct[] + { + return [ this._product ]; + } + + public get localizationId(): string + { + return 'roomItem.name.' + this._furniData.id; + } + + public get bundlePurchaseAllowed(): boolean + { + return false; + } + + public get isRentOffer(): boolean + { + return (this._furniData.rentOfferId > -1); + } + + public get giftable(): boolean + { + return false; + } + + public get pricingModel(): string + { + return Offer.PRICING_MODEL_FURNITURE; + } + + public get clubLevel(): number + { + return 0; + } + + public get badgeCode(): string + { + return ''; + } + + public get localizationName(): string + { + return this._furniData.name; + } + + public get localizationDescription(): string + { + return this._furniData.description; + } + + public get isLazy(): boolean + { + return true; + } +} diff --git a/apps/frontend/src/api/catalog/GetImageIconUrlForProduct.ts b/apps/frontend/src/api/catalog/GetImageIconUrlForProduct.ts new file mode 100644 index 0000000..1e8d8c0 --- /dev/null +++ b/apps/frontend/src/api/catalog/GetImageIconUrlForProduct.ts @@ -0,0 +1,19 @@ +import { GetRoomEngine } from '../nitro'; +import { ProductTypeEnum } from './ProductTypeEnum'; + +export const GetImageIconUrlForProduct = (productType: string, productClassId: number, extraData: string = null) => +{ + let imageUrl: string = null; + + switch(productType.toLocaleLowerCase()) + { + case ProductTypeEnum.FLOOR: + imageUrl = GetRoomEngine().getFurnitureFloorIconUrl(productClassId); + break; + case ProductTypeEnum.WALL: + imageUrl = GetRoomEngine().getFurnitureWallIconUrl(productClassId, extraData); + break; + } + + return imageUrl; +} diff --git a/apps/frontend/src/api/catalog/GiftWrappingConfiguration.ts b/apps/frontend/src/api/catalog/GiftWrappingConfiguration.ts new file mode 100644 index 0000000..9d29b8c --- /dev/null +++ b/apps/frontend/src/api/catalog/GiftWrappingConfiguration.ts @@ -0,0 +1,51 @@ +import { GiftWrappingConfigurationParser } from '@nitrots/nitro-renderer'; + +export class GiftWrappingConfiguration +{ + private _isEnabled: boolean = false; + private _price: number = null; + private _stuffTypes: number[] = null; + private _boxTypes: number[] = null; + private _ribbonTypes: number[] = null; + private _defaultStuffTypes: number[] = null; + + constructor(parser: GiftWrappingConfigurationParser) + { + this._isEnabled = parser.isEnabled; + this._price = parser.price; + this._boxTypes = parser.boxTypes; + this._ribbonTypes = parser.ribbonTypes; + this._stuffTypes = parser.giftWrappers; + this._defaultStuffTypes = parser.giftFurnis; + } + + public get isEnabled(): boolean + { + return this._isEnabled; + } + + public get price(): number + { + return this._price; + } + + public get stuffTypes(): number[] + { + return this._stuffTypes; + } + + public get boxTypes(): number[] + { + return this._boxTypes; + } + + public get ribbonTypes(): number[] + { + return this._ribbonTypes; + } + + public get defaultStuffTypes(): number[] + { + return this._defaultStuffTypes; + } +} diff --git a/apps/frontend/src/api/catalog/ICatalogNode.ts b/apps/frontend/src/api/catalog/ICatalogNode.ts new file mode 100644 index 0000000..c69f5a6 --- /dev/null +++ b/apps/frontend/src/api/catalog/ICatalogNode.ts @@ -0,0 +1,21 @@ +export interface ICatalogNode +{ + activate(): void; + deactivate(): void; + open(): void; + close(): void; + addChild(node: ICatalogNode): void; + readonly depth: number; + readonly isBranch: boolean; + readonly isLeaf: boolean; + readonly localization: string; + readonly pageId: number; + readonly pageName: string; + readonly iconId: number; + readonly children: ICatalogNode[]; + readonly offerIds: number[]; + readonly parent: ICatalogNode; + readonly isVisible: boolean; + readonly isActive: boolean; + readonly isOpen: boolean; +} diff --git a/apps/frontend/src/api/catalog/ICatalogOptions.ts b/apps/frontend/src/api/catalog/ICatalogOptions.ts new file mode 100644 index 0000000..2035694 --- /dev/null +++ b/apps/frontend/src/api/catalog/ICatalogOptions.ts @@ -0,0 +1,13 @@ +import { ClubGiftInfoParser, ClubOfferData, HabboGroupEntryData, MarketplaceConfigurationMessageParser } from '@nitrots/nitro-renderer'; +import { CatalogPetPalette } from './CatalogPetPalette'; +import { GiftWrappingConfiguration } from './GiftWrappingConfiguration'; + +export interface ICatalogOptions +{ + groups?: HabboGroupEntryData[]; + petPalettes?: CatalogPetPalette[]; + clubOffers?: ClubOfferData[]; + clubGifts?: ClubGiftInfoParser; + giftConfiguration?: GiftWrappingConfiguration; + marketplaceConfiguration?: MarketplaceConfigurationMessageParser; +} diff --git a/apps/frontend/src/api/catalog/ICatalogPage.ts b/apps/frontend/src/api/catalog/ICatalogPage.ts new file mode 100644 index 0000000..ed11ba0 --- /dev/null +++ b/apps/frontend/src/api/catalog/ICatalogPage.ts @@ -0,0 +1,12 @@ +import { IPageLocalization } from './IPageLocalization'; +import { IPurchasableOffer } from './IPurchasableOffer'; + +export interface ICatalogPage +{ + readonly pageId: number; + readonly layoutCode: string; + readonly localization: IPageLocalization; + readonly offers: IPurchasableOffer[]; + readonly acceptSeasonCurrencyAsCredits: boolean; + readonly mode: number; +} diff --git a/apps/frontend/src/api/catalog/IMarketplaceSearchOptions.ts b/apps/frontend/src/api/catalog/IMarketplaceSearchOptions.ts new file mode 100644 index 0000000..9489ef0 --- /dev/null +++ b/apps/frontend/src/api/catalog/IMarketplaceSearchOptions.ts @@ -0,0 +1,7 @@ +export interface IMarketplaceSearchOptions +{ + query: string; + type: number; + minPrice: number; + maxPrice: number; +} diff --git a/apps/frontend/src/api/catalog/IPageLocalization.ts b/apps/frontend/src/api/catalog/IPageLocalization.ts new file mode 100644 index 0000000..ad652e1 --- /dev/null +++ b/apps/frontend/src/api/catalog/IPageLocalization.ts @@ -0,0 +1,5 @@ +export interface IPageLocalization +{ + getText(index: number): string + getImage(index: number): string +} diff --git a/apps/frontend/src/api/catalog/IProduct.ts b/apps/frontend/src/api/catalog/IProduct.ts new file mode 100644 index 0000000..4a1a392 --- /dev/null +++ b/apps/frontend/src/api/catalog/IProduct.ts @@ -0,0 +1,16 @@ +import { IFurnitureData, IProductData } from '@nitrots/nitro-renderer'; +import { IPurchasableOffer } from './IPurchasableOffer'; + +export interface IProduct +{ + getIconUrl(offer?: IPurchasableOffer): string; + productType: string; + productClassId: number; + extraParam: string; + productCount: number; + productData: IProductData; + furnitureData: IFurnitureData; + isUniqueLimitedItem: boolean; + uniqueLimitedItemSeriesSize: number; + uniqueLimitedItemsLeft: number; +} diff --git a/apps/frontend/src/api/catalog/IPurchasableOffer.ts b/apps/frontend/src/api/catalog/IPurchasableOffer.ts new file mode 100644 index 0000000..b182865 --- /dev/null +++ b/apps/frontend/src/api/catalog/IPurchasableOffer.ts @@ -0,0 +1,25 @@ +import { ICatalogPage } from './ICatalogPage'; +import { IProduct } from './IProduct'; + +export interface IPurchasableOffer +{ + activate(): void; + clubLevel: number; + page: ICatalogPage; + offerId: number; + localizationId: string; + priceInCredits: number; + priceInActivityPoints: number; + activityPointType: number; + giftable: boolean; + product: IProduct; + pricingModel: string; + priceType: string; + bundlePurchaseAllowed: boolean; + isRentOffer: boolean; + badgeCode: string; + localizationName: string; + localizationDescription: string; + isLazy: boolean; + products: IProduct[]; +} diff --git a/apps/frontend/src/api/catalog/IPurchaseOptions.ts b/apps/frontend/src/api/catalog/IPurchaseOptions.ts new file mode 100644 index 0000000..c9fab89 --- /dev/null +++ b/apps/frontend/src/api/catalog/IPurchaseOptions.ts @@ -0,0 +1,9 @@ +import { IObjectData } from '@nitrots/nitro-renderer'; + +export interface IPurchaseOptions +{ + quantity?: number; + extraData?: string; + extraParamRequired?: boolean; + previewStuffData?: IObjectData; +} diff --git a/apps/frontend/src/api/catalog/MarketplaceOfferData.ts b/apps/frontend/src/api/catalog/MarketplaceOfferData.ts new file mode 100644 index 0000000..ba1fa88 --- /dev/null +++ b/apps/frontend/src/api/catalog/MarketplaceOfferData.ts @@ -0,0 +1,128 @@ +import { IObjectData } from '@nitrots/nitro-renderer'; + +export class MarketplaceOfferData +{ + public static readonly TYPE_FLOOR: number = 1; + public static readonly TYPE_WALL: number = 2; + + private _offerId: number; + private _furniId: number; + private _furniType: number; + private _extraData: string; + private _stuffData: IObjectData; + private _price: number; + private _averagePrice: number; + private _imageCallback: number; + private _status: number; + private _timeLeftMinutes: number = -1; + private _offerCount: number; + private _image: string; + + constructor(offerId: number, furniId: number, furniType: number, extraData: string, stuffData: IObjectData, price: number, status: number, averagePrice: number, offerCount: number = -1) + { + this._offerId = offerId; + this._furniId = furniId; + this._furniType = furniType; + this._extraData = extraData; + this._stuffData = stuffData; + this._price = price; + this._status = status; + this._averagePrice = averagePrice; + this._offerCount = offerCount; + } + + public get offerId(): number + { + return this._offerId; + } + + public set offerId(offerId: number) + { + this._offerId = offerId; + } + + public get furniId(): number + { + return this._furniId; + } + + public get furniType(): number + { + return this._furniType; + } + + public get extraData(): string + { + return this._extraData; + } + + public get stuffData(): IObjectData + { + return this._stuffData; + } + + public get price(): number + { + return this._price; + } + + public set price(price: number) + { + this._price = price; + } + + public get averagePrice(): number + { + return this._averagePrice; + } + + public get image(): string + { + return this._image; + } + + public set image(image: string) + { + this._image = image; + } + + public get imageCallback(): number + { + return this._imageCallback; + } + + public set imageCallback(callback: number) + { + this._imageCallback = callback; + } + + public get status(): number + { + return this._status; + } + + public get timeLeftMinutes(): number + { + return this._timeLeftMinutes; + } + + public set timeLeftMinutes(minutes: number) + { + this._timeLeftMinutes = minutes; + } + + public get offerCount(): number + { + return this._offerCount; + } + + public set offerCount(count: number) + { + this._offerCount = count; + } + + public get isUniqueLimitedItem(): boolean + { + return (this.stuffData && (this.stuffData.uniqueSeries > 0)); + } +} diff --git a/apps/frontend/src/api/catalog/MarketplaceOfferState.ts b/apps/frontend/src/api/catalog/MarketplaceOfferState.ts new file mode 100644 index 0000000..20c0e45 --- /dev/null +++ b/apps/frontend/src/api/catalog/MarketplaceOfferState.ts @@ -0,0 +1,7 @@ +export class MarketPlaceOfferState +{ + public static readonly ONGOING = 1; + public static readonly ONGOING_OWN = 1; + public static readonly SOLD = 2; + public static readonly EXPIRED = 3; +} diff --git a/apps/frontend/src/api/catalog/MarketplaceSearchType.ts b/apps/frontend/src/api/catalog/MarketplaceSearchType.ts new file mode 100644 index 0000000..ac7a701 --- /dev/null +++ b/apps/frontend/src/api/catalog/MarketplaceSearchType.ts @@ -0,0 +1,6 @@ +export class MarketplaceSearchType +{ + public static readonly BY_ACTIVITY = 1; + public static readonly BY_VALUE = 2; + public static readonly ADVANCED = 3; +} diff --git a/apps/frontend/src/api/catalog/Offer.ts b/apps/frontend/src/api/catalog/Offer.ts new file mode 100644 index 0000000..c14d6ac --- /dev/null +++ b/apps/frontend/src/api/catalog/Offer.ts @@ -0,0 +1,245 @@ +import { GetFurnitureData, GetProductDataForLocalization, LocalizeText, ProductTypeEnum } from '..'; +import { ICatalogPage } from './ICatalogPage'; +import { IProduct } from './IProduct'; +import { IPurchasableOffer } from './IPurchasableOffer'; +import { Product } from './Product'; + +export class Offer implements IPurchasableOffer +{ + public static PRICING_MODEL_UNKNOWN: string = 'pricing_model_unknown'; + public static PRICING_MODEL_SINGLE: string = 'pricing_model_single'; + public static PRICING_MODEL_MULTI: string = 'pricing_model_multi'; + public static PRICING_MODEL_BUNDLE: string = 'pricing_model_bundle'; + public static PRICING_MODEL_FURNITURE: string = 'pricing_model_furniture'; + public static PRICE_TYPE_NONE: string = 'price_type_none'; + public static PRICE_TYPE_CREDITS: string = 'price_type_credits'; + public static PRICE_TYPE_ACTIVITYPOINTS: string = 'price_type_activitypoints'; + public static PRICE_TYPE_CREDITS_ACTIVITYPOINTS: string = 'price_type_credits_and_activitypoints'; + + private _pricingModel: string; + private _priceType: string; + private _offerId: number; + private _localizationId: string; + private _priceInCredits: number; + private _priceInActivityPoints: number; + private _activityPointType: number; + private _giftable: boolean; + private _isRentOffer: boolean; + private _page: ICatalogPage; + private _clubLevel: number = 0; + private _products: IProduct[]; + private _badgeCode: string; + private _bundlePurchaseAllowed: boolean = false; + + constructor(offerId: number, localizationId: string, isRentOffer: boolean, priceInCredits: number, priceInActivityPoints: number, activityPointType: number, giftable: boolean, clubLevel: number, products: IProduct[], bundlePurchaseAllowed: boolean) + { + this._offerId = offerId; + this._localizationId = localizationId; + this._isRentOffer = isRentOffer; + this._priceInCredits = priceInCredits; + this._priceInActivityPoints = priceInActivityPoints; + this._activityPointType = activityPointType; + this._giftable = giftable; + this._clubLevel = clubLevel; + this._products = products; + this._bundlePurchaseAllowed = bundlePurchaseAllowed; + + this.setPricingModelForProducts(); + this.setPricingType(); + + for(const product of products) + { + if(product.productType === ProductTypeEnum.BADGE) + { + this._badgeCode = product.extraParam; + + break; + } + } + } + + public activate(): void + { + + } + + public get clubLevel(): number + { + return this._clubLevel; + } + + public get page(): ICatalogPage + { + return this._page; + } + + public set page(k: ICatalogPage) + { + this._page = k; + } + + public get offerId(): number + { + return this._offerId; + } + + public get localizationId(): string + { + return this._localizationId; + } + + public get priceInCredits(): number + { + return this._priceInCredits; + } + + public get priceInActivityPoints(): number + { + return this._priceInActivityPoints; + } + + public get activityPointType(): number + { + return this._activityPointType; + } + + public get giftable(): boolean + { + return this._giftable; + } + + public get product(): IProduct + { + if(!this._products || !this._products.length) return null; + + if(this._products.length === 1) return this._products[0]; + + const products = Product.stripAddonProducts(this._products); + + if(products.length) return products[0]; + + return null; + } + + public get pricingModel(): string + { + return this._pricingModel; + } + + public get priceType(): string + { + return this._priceType; + } + + public get bundlePurchaseAllowed(): boolean + { + return this._bundlePurchaseAllowed; + } + + public get isRentOffer(): boolean + { + return this._isRentOffer; + } + + public get badgeCode(): string + { + return this._badgeCode; + } + + public get localizationName(): string + { + const productData = GetProductDataForLocalization(this._localizationId); + + if(productData) return productData.name; + + return LocalizeText(this._localizationId); + } + + public get localizationDescription(): string + { + const productData = GetProductDataForLocalization(this._localizationId); + + if(productData) return productData.description; + + return LocalizeText(this._localizationId); + } + + public get isLazy(): boolean + { + return false; + } + + public get products(): IProduct[] + { + return this._products; + } + + private setPricingModelForProducts(): void + { + const products = Product.stripAddonProducts(this._products); + + if(products.length === 1) + { + if(products[0].productCount === 1) + { + this._pricingModel = Offer.PRICING_MODEL_SINGLE; + } + else + { + this._pricingModel = Offer.PRICING_MODEL_MULTI; + } + } + + else if(products.length > 1) + { + this._pricingModel = Offer.PRICING_MODEL_BUNDLE; + } + + else + { + this._pricingModel = Offer.PRICING_MODEL_UNKNOWN; + } + } + + private setPricingType(): void + { + if((this._priceInCredits > 0) && (this._priceInActivityPoints > 0)) + { + this._priceType = Offer.PRICE_TYPE_CREDITS_ACTIVITYPOINTS; + } + + else if(this._priceInCredits > 0) + { + this._priceType = Offer.PRICE_TYPE_CREDITS; + } + + else if(this._priceInActivityPoints > 0) + { + this._priceType = Offer.PRICE_TYPE_ACTIVITYPOINTS; + } + + else + { + this._priceType = Offer.PRICE_TYPE_NONE; + } + } + + public clone(): IPurchasableOffer + { + const products: IProduct[] = []; + const productData = GetProductDataForLocalization(this.localizationId); + + for(const product of this._products) + { + const furnitureData = GetFurnitureData(product.productClassId, product.productType); + + products.push(new Product(product.productType, product.productClassId, product.extraParam, product.productCount, productData, furnitureData)); + } + + const offer = new Offer(this.offerId, this.localizationId, this.isRentOffer, this.priceInCredits, this.priceInActivityPoints, this.activityPointType, this.giftable, this.clubLevel, products, this.bundlePurchaseAllowed); + + offer.page = this.page; + + return offer; + } +} diff --git a/apps/frontend/src/api/catalog/PageLocalization.ts b/apps/frontend/src/api/catalog/PageLocalization.ts new file mode 100644 index 0000000..91e3ce6 --- /dev/null +++ b/apps/frontend/src/api/catalog/PageLocalization.ts @@ -0,0 +1,36 @@ +import { GetConfiguration } from '../nitro'; +import { IPageLocalization } from './IPageLocalization'; + +export class PageLocalization implements IPageLocalization +{ + private _images: string[]; + private _texts: string[] + + constructor(images: string[], texts: string[]) + { + this._images = images; + this._texts = texts; + } + + public getText(index: number): string + { + let message = (this._texts[index] || ''); + + if(message && message.length) message = message.replace(/\r\n|\r|\n/g, '
'); + + return message; + } + + public getImage(index: number): string + { + const imageName = (this._images[index] || ''); + + if(!imageName || !imageName.length) return null; + + let assetUrl = GetConfiguration('catalog.asset.image.url'); + + assetUrl = assetUrl.replace('%name%', imageName); + + return assetUrl; + } +} diff --git a/apps/frontend/src/api/catalog/PlacedObjectPurchaseData.ts b/apps/frontend/src/api/catalog/PlacedObjectPurchaseData.ts new file mode 100644 index 0000000..84bad8c --- /dev/null +++ b/apps/frontend/src/api/catalog/PlacedObjectPurchaseData.ts @@ -0,0 +1,41 @@ +import { IFurnitureData, IProductData } from '@nitrots/nitro-renderer'; +import { IPurchasableOffer } from './IPurchasableOffer'; + +export class PlacedObjectPurchaseData +{ + constructor( + public readonly roomId: number, + public readonly objectId: number, + public readonly category: number, + public readonly wallLocation: string, + public readonly x: number, + public readonly y: number, + public readonly direction: number, + public readonly offer: IPurchasableOffer) + {} + + public get offerId(): number + { + return this.offer.offerId; + } + + public get productClassId(): number + { + return this.offer.product.productClassId; + } + + public get productData(): IProductData + { + return this.offer.product.productData; + } + + public get furniData(): IFurnitureData + { + return this.offer.product.furnitureData; + } + + public get extraParam(): string + { + return this.offer.product.extraParam; + } +} diff --git a/apps/frontend/src/api/catalog/Product.ts b/apps/frontend/src/api/catalog/Product.ts new file mode 100644 index 0000000..bfb760f --- /dev/null +++ b/apps/frontend/src/api/catalog/Product.ts @@ -0,0 +1,143 @@ +import { IFurnitureData, IObjectData, IProductData } from '@nitrots/nitro-renderer'; +import { GetConfiguration, GetRoomEngine, GetSessionDataManager } from '../nitro'; +import { GetPixelEffectIcon, GetSubscriptionProductIcon } from './CatalogUtilities'; +import { IProduct } from './IProduct'; +import { IPurchasableOffer } from './IPurchasableOffer'; +import { ProductTypeEnum } from './ProductTypeEnum'; + +export class Product implements IProduct +{ + public static EFFECT_CLASSID_NINJA_DISAPPEAR: number = 108; + + private _productType: string; + private _productClassId: number; + private _extraParam: string; + private _productCount: number; + private _productData: IProductData; + private _furnitureData: IFurnitureData; + private _isUniqueLimitedItem: boolean; + private _uniqueLimitedItemSeriesSize: number; + private _uniqueLimitedItemsLeft: number; + + constructor(productType: string, productClassId: number, extraParam: string, productCount: number, productData: IProductData, furnitureData: IFurnitureData, isUniqueLimitedItem: boolean = false, uniqueLimitedItemSeriesSize: number = 0, uniqueLimitedItemsLeft: number = 0) + { + this._productType = productType.toLowerCase(); + this._productClassId = productClassId; + this._extraParam = extraParam; + this._productCount = productCount; + this._productData = productData; + this._furnitureData = furnitureData; + this._isUniqueLimitedItem = isUniqueLimitedItem; + this._uniqueLimitedItemSeriesSize = uniqueLimitedItemSeriesSize; + this._uniqueLimitedItemsLeft = uniqueLimitedItemsLeft; + } + + public static stripAddonProducts(products: IProduct[]): IProduct[] + { + if(products.length === 1) return products; + + return products.filter(product => ((product.productType !== ProductTypeEnum.BADGE) && (product.productType !== ProductTypeEnum.EFFECT) && (product.productClassId !== Product.EFFECT_CLASSID_NINJA_DISAPPEAR))); + } + + public getIconUrl(offer: IPurchasableOffer = null, stuffData: IObjectData = null): string + { + switch(this._productType) + { + case ProductTypeEnum.FLOOR: + return GetRoomEngine().getFurnitureFloorIconUrl(this.productClassId); + case ProductTypeEnum.WALL: { + if(offer && this._furnitureData) + { + let iconName = ''; + + switch(this._furnitureData.className) + { + case 'floor': + iconName = [ 'th', this._furnitureData.className, offer.product.extraParam ].join('_'); + break; + case 'wallpaper': + iconName = [ 'th', 'wall', offer.product.extraParam ].join('_'); + break; + case 'landscape': + iconName = [ 'th', this._furnitureData.className, (offer.product.extraParam || '').replace('.', '_'), '001' ].join('_'); + break; + } + + if(iconName !== '') + { + const assetUrl = GetConfiguration('catalog.asset.url'); + + return `${ assetUrl }/${ iconName }.png`; + } + } + + return GetRoomEngine().getFurnitureWallIconUrl(this.productClassId, this._extraParam); + } + case ProductTypeEnum.EFFECT: + return GetPixelEffectIcon(this.productClassId); + case ProductTypeEnum.HABBO_CLUB: + return GetSubscriptionProductIcon(this.productClassId); + case ProductTypeEnum.BADGE: + return GetSessionDataManager().getBadgeUrl(this._extraParam); + case ProductTypeEnum.ROBOT: + return null; + } + + return null; + } + + public get productType(): string + { + return this._productType; + } + + public get productClassId(): number + { + return this._productClassId; + } + + public get extraParam(): string + { + return this._extraParam; + } + + public set extraParam(extraParam: string) + { + this._extraParam = extraParam; + } + + public get productCount(): number + { + return this._productCount; + } + + public get productData(): IProductData + { + return this._productData; + } + + public get furnitureData(): IFurnitureData + { + return this._furnitureData; + } + + public get isUniqueLimitedItem(): boolean + { + return this._isUniqueLimitedItem; + } + + public get uniqueLimitedItemSeriesSize(): number + { + return this._uniqueLimitedItemSeriesSize; + } + + public get uniqueLimitedItemsLeft(): number + { + return this._uniqueLimitedItemsLeft; + } + + public set uniqueLimitedItemsLeft(uniqueLimitedItemsLeft: number) + { + this._uniqueLimitedItemsLeft = uniqueLimitedItemsLeft; + } +} diff --git a/apps/frontend/src/api/catalog/ProductTypeEnum.ts b/apps/frontend/src/api/catalog/ProductTypeEnum.ts new file mode 100644 index 0000000..f249081 --- /dev/null +++ b/apps/frontend/src/api/catalog/ProductTypeEnum.ts @@ -0,0 +1,11 @@ +export class ProductTypeEnum +{ + public static WALL: string = 'i'; + public static FLOOR: string = 's'; + public static EFFECT: string = 'e'; + public static HABBO_CLUB: string = 'h'; + public static BADGE: string = 'b'; + public static GAME_TOKEN: string = 'GAME_TOKEN'; + public static PET: string = 'p'; + public static ROBOT: string = 'r'; +} diff --git a/apps/frontend/src/api/catalog/RequestedPage.ts b/apps/frontend/src/api/catalog/RequestedPage.ts new file mode 100644 index 0000000..8c22730 --- /dev/null +++ b/apps/frontend/src/api/catalog/RequestedPage.ts @@ -0,0 +1,63 @@ +export class RequestedPage +{ + public static REQUEST_TYPE_NONE: number = 0; + public static REQUEST_TYPE_ID: number = 1; + public static REQUEST_TYPE_OFFER: number = 2; + public static REQUEST_TYPE_NAME: number = 3; + + private _requestType: number; + private _requestById: number; + private _requestedByOfferId: number; + private _requestByName: string; + + constructor() + { + this._requestType = RequestedPage.REQUEST_TYPE_NONE; + } + + public resetRequest():void + { + this._requestType = RequestedPage.REQUEST_TYPE_NONE; + this._requestById = -1; + this._requestedByOfferId = -1; + this._requestByName = null; + } + + public get requestType(): number + { + return this._requestType; + } + + public get requestById(): number + { + return this._requestById; + } + + public set requestById(id: number) + { + this._requestType = RequestedPage.REQUEST_TYPE_ID; + this._requestById = id; + } + + public get requestedByOfferId(): number + { + return this._requestedByOfferId; + } + + public set requestedByOfferId(offerId: number) + { + this._requestType = RequestedPage.REQUEST_TYPE_OFFER; + this._requestedByOfferId = offerId; + } + + public get requestByName(): string + { + return this._requestByName; + } + + public set requestByName(name: string) + { + this._requestType = RequestedPage.REQUEST_TYPE_NAME; + this._requestByName = name; + } +} diff --git a/apps/frontend/src/api/catalog/SearchResult.ts b/apps/frontend/src/api/catalog/SearchResult.ts new file mode 100644 index 0000000..419f3cf --- /dev/null +++ b/apps/frontend/src/api/catalog/SearchResult.ts @@ -0,0 +1,11 @@ +import { ICatalogNode } from './ICatalogNode'; +import { IPurchasableOffer } from './IPurchasableOffer'; + +export class SearchResult +{ + constructor( + public readonly searchValue: string, + public readonly offers: IPurchasableOffer[], + public readonly filteredNodes: ICatalogNode[]) + {} +} diff --git a/apps/frontend/src/api/catalog/index.ts b/apps/frontend/src/api/catalog/index.ts new file mode 100644 index 0000000..6c5b9e2 --- /dev/null +++ b/apps/frontend/src/api/catalog/index.ts @@ -0,0 +1,29 @@ +export * from './BuilderFurniPlaceableStatus'; +export * from './CatalogNode'; +export * from './CatalogPage'; +export * from './CatalogPageName'; +export * from './CatalogPetPalette'; +export * from './CatalogPurchaseState'; +export * from './CatalogType'; +export * from './CatalogUtilities'; +export * from './FurnitureOffer'; +export * from './GetImageIconUrlForProduct'; +export * from './GiftWrappingConfiguration'; +export * from './ICatalogNode'; +export * from './ICatalogOptions'; +export * from './ICatalogPage'; +export * from './IMarketplaceSearchOptions'; +export * from './IPageLocalization'; +export * from './IProduct'; +export * from './IPurchasableOffer'; +export * from './IPurchaseOptions'; +export * from './MarketplaceOfferData'; +export * from './MarketplaceOfferState'; +export * from './MarketplaceSearchType'; +export * from './Offer'; +export * from './PageLocalization'; +export * from './PlacedObjectPurchaseData'; +export * from './Product'; +export * from './ProductTypeEnum'; +export * from './RequestedPage'; +export * from './SearchResult'; diff --git a/apps/frontend/src/api/chat-history/ChatEntryType.ts b/apps/frontend/src/api/chat-history/ChatEntryType.ts new file mode 100644 index 0000000..045f00c --- /dev/null +++ b/apps/frontend/src/api/chat-history/ChatEntryType.ts @@ -0,0 +1,6 @@ +export class ChatEntryType +{ + public static TYPE_CHAT = 1; + public static TYPE_ROOM_INFO = 2; + public static TYPE_IM = 3; +} diff --git a/apps/frontend/src/api/chat-history/ChatHistoryCurrentDate.ts b/apps/frontend/src/api/chat-history/ChatHistoryCurrentDate.ts new file mode 100644 index 0000000..6947bca --- /dev/null +++ b/apps/frontend/src/api/chat-history/ChatHistoryCurrentDate.ts @@ -0,0 +1,6 @@ +export const ChatHistoryCurrentDate = () => +{ + const currentTime = new Date(); + + return `${ currentTime.getHours().toString().padStart(2, '0') }:${ currentTime.getMinutes().toString().padStart(2, '0') }`; +} diff --git a/apps/frontend/src/api/chat-history/IChatEntry.ts b/apps/frontend/src/api/chat-history/IChatEntry.ts new file mode 100644 index 0000000..1bf7a52 --- /dev/null +++ b/apps/frontend/src/api/chat-history/IChatEntry.ts @@ -0,0 +1,17 @@ +export interface IChatEntry +{ + id: number; + webId: number; + entityId: number; + name: string; + look?: string; + message?: string; + entityType?: number; + style?: number; + chatType?: number; + imageUrl?: string; + color?: string; + roomId: number; + timestamp: string; + type: number; +} diff --git a/apps/frontend/src/api/chat-history/IRoomHistoryEntry.ts b/apps/frontend/src/api/chat-history/IRoomHistoryEntry.ts new file mode 100644 index 0000000..4986154 --- /dev/null +++ b/apps/frontend/src/api/chat-history/IRoomHistoryEntry.ts @@ -0,0 +1,5 @@ +export interface IRoomHistoryEntry +{ + id: number; + name: string; +} diff --git a/apps/frontend/src/api/chat-history/MessengerHistoryCurrentDate.ts b/apps/frontend/src/api/chat-history/MessengerHistoryCurrentDate.ts new file mode 100644 index 0000000..3aeebc5 --- /dev/null +++ b/apps/frontend/src/api/chat-history/MessengerHistoryCurrentDate.ts @@ -0,0 +1,6 @@ +export const MessengerHistoryCurrentDate = (secondsSinceNow: number = 0) => +{ + const currentTime = secondsSinceNow ? new Date(Date.now() - secondsSinceNow * 1000) : new Date(); + + return `${ currentTime.getHours().toString().padStart(2, '0') }:${ currentTime.getMinutes().toString().padStart(2, '0') }`; +} diff --git a/apps/frontend/src/api/chat-history/index.ts b/apps/frontend/src/api/chat-history/index.ts new file mode 100644 index 0000000..a989374 --- /dev/null +++ b/apps/frontend/src/api/chat-history/index.ts @@ -0,0 +1,5 @@ +export * from './ChatEntryType'; +export * from './ChatHistoryCurrentDate'; +export * from './IChatEntry'; +export * from './IRoomHistoryEntry'; +export * from './MessengerHistoryCurrentDate'; diff --git a/apps/frontend/src/api/events/DispatchEvent.ts b/apps/frontend/src/api/events/DispatchEvent.ts new file mode 100644 index 0000000..79e2f5c --- /dev/null +++ b/apps/frontend/src/api/events/DispatchEvent.ts @@ -0,0 +1,3 @@ +import { IEventDispatcher, NitroEvent } from '@nitrots/nitro-renderer'; + +export const DispatchEvent = (eventDispatcher: IEventDispatcher, event: NitroEvent) => eventDispatcher.dispatchEvent(event); diff --git a/apps/frontend/src/api/events/DispatchMainEvent.ts b/apps/frontend/src/api/events/DispatchMainEvent.ts new file mode 100644 index 0000000..385a888 --- /dev/null +++ b/apps/frontend/src/api/events/DispatchMainEvent.ts @@ -0,0 +1,5 @@ +import { NitroEvent } from '@nitrots/nitro-renderer'; +import { GetNitroInstance } from '../nitro'; +import { DispatchEvent } from './DispatchEvent'; + +export const DispatchMainEvent = (event: NitroEvent) => DispatchEvent(GetNitroInstance().events, event); diff --git a/apps/frontend/src/api/events/DispatchUiEvent.ts b/apps/frontend/src/api/events/DispatchUiEvent.ts new file mode 100644 index 0000000..5200bb4 --- /dev/null +++ b/apps/frontend/src/api/events/DispatchUiEvent.ts @@ -0,0 +1,5 @@ +import { NitroEvent } from '@nitrots/nitro-renderer'; +import { DispatchEvent } from './DispatchEvent'; +import { UI_EVENT_DISPATCHER } from './UI_EVENT_DISPATCHER'; + +export const DispatchUiEvent = (event: NitroEvent) => DispatchEvent(UI_EVENT_DISPATCHER, event); diff --git a/apps/frontend/src/api/events/UI_EVENT_DISPATCHER.ts b/apps/frontend/src/api/events/UI_EVENT_DISPATCHER.ts new file mode 100644 index 0000000..cb57311 --- /dev/null +++ b/apps/frontend/src/api/events/UI_EVENT_DISPATCHER.ts @@ -0,0 +1,3 @@ +import { EventDispatcher, IEventDispatcher } from '@nitrots/nitro-renderer'; + +export const UI_EVENT_DISPATCHER: IEventDispatcher = new EventDispatcher(); diff --git a/apps/frontend/src/api/events/index.ts b/apps/frontend/src/api/events/index.ts new file mode 100644 index 0000000..b7c22ee --- /dev/null +++ b/apps/frontend/src/api/events/index.ts @@ -0,0 +1,4 @@ +export * from './DispatchEvent'; +export * from './DispatchMainEvent'; +export * from './DispatchUiEvent'; +export * from './UI_EVENT_DISPATCHER'; diff --git a/apps/frontend/src/api/friends/GetGroupChatData.ts b/apps/frontend/src/api/friends/GetGroupChatData.ts new file mode 100644 index 0000000..75df962 --- /dev/null +++ b/apps/frontend/src/api/friends/GetGroupChatData.ts @@ -0,0 +1,13 @@ +import { IGroupChatData } from './IGroupChatData'; + +export const GetGroupChatData = (extraData: string) => +{ + if(!extraData || !extraData.length) return null; + + const splitData = extraData.split('/'); + const username = splitData[0]; + const figure = splitData[1]; + const userId = parseInt(splitData[2]); + + return ({ username: username, figure: figure, userId: userId } as IGroupChatData); +} diff --git a/apps/frontend/src/api/friends/IGroupChatData.ts b/apps/frontend/src/api/friends/IGroupChatData.ts new file mode 100644 index 0000000..24a3f9c --- /dev/null +++ b/apps/frontend/src/api/friends/IGroupChatData.ts @@ -0,0 +1,6 @@ +export interface IGroupChatData +{ + username: string; + figure: string; + userId: number; +} diff --git a/apps/frontend/src/api/friends/MessengerFriend.ts b/apps/frontend/src/api/friends/MessengerFriend.ts new file mode 100644 index 0000000..b5cfc88 --- /dev/null +++ b/apps/frontend/src/api/friends/MessengerFriend.ts @@ -0,0 +1,43 @@ +import { FriendParser } from '@nitrots/nitro-renderer'; + +export class MessengerFriend +{ + public static RELATIONSHIP_NONE: number = 0; + public static RELATIONSHIP_HEART: number = 1; + public static RELATIONSHIP_SMILE: number = 2; + public static RELATIONSHIP_BOBBA: number = 3; + + public id: number = -1; + public name: string = null; + public gender: number = 0; + public online: boolean = false; + public followingAllowed: boolean = false; + public figure: string = null; + public categoryId: number = 0; + public motto: string = null; + public realName: string = null; + public lastAccess: string = null; + public persistedMessageUser: boolean = false; + public vipMember: boolean = false; + public pocketHabboUser: boolean = false; + public relationshipStatus: number = -1; + public unread: number = 0; + + public populate(parser: FriendParser): void + { + this.id = parser.id; + this.name = parser.name; + this.gender = parser.gender; + this.online = parser.online; + this.followingAllowed = parser.followingAllowed; + this.figure = parser.figure; + this.categoryId = parser.categoryId; + this.motto = parser.motto; + this.realName = parser.realName; + this.lastAccess = parser.lastAccess; + this.persistedMessageUser = parser.persistedMessageUser; + this.vipMember = parser.vipMember; + this.pocketHabboUser = parser.pocketHabboUser; + this.relationshipStatus = parser.relationshipStatus; + } +} diff --git a/apps/frontend/src/api/friends/MessengerGroupType.ts b/apps/frontend/src/api/friends/MessengerGroupType.ts new file mode 100644 index 0000000..d46a1b6 --- /dev/null +++ b/apps/frontend/src/api/friends/MessengerGroupType.ts @@ -0,0 +1,5 @@ +export class MessengerGroupType +{ + public static readonly GROUP_CHAT = 0; + public static readonly PRIVATE_CHAT = 1; +} diff --git a/apps/frontend/src/api/friends/MessengerIconState.ts b/apps/frontend/src/api/friends/MessengerIconState.ts new file mode 100644 index 0000000..63f8c13 --- /dev/null +++ b/apps/frontend/src/api/friends/MessengerIconState.ts @@ -0,0 +1,6 @@ +export class MessengerIconState +{ + public static HIDDEN: number = 0; + public static SHOW: number = 1; + public static UNREAD: number = 2; +} diff --git a/apps/frontend/src/api/friends/MessengerRequest.ts b/apps/frontend/src/api/friends/MessengerRequest.ts new file mode 100644 index 0000000..89ceec5 --- /dev/null +++ b/apps/frontend/src/api/friends/MessengerRequest.ts @@ -0,0 +1,41 @@ +import { FriendRequestData } from '@nitrots/nitro-renderer'; + +export class MessengerRequest +{ + private _id: number; + private _name: string; + private _requesterUserId: number; + private _figureString: string; + + public populate(data: FriendRequestData): boolean + { + if(!data) return false; + + this._id = data.requestId; + this._name = data.requesterName; + this._figureString = data.figureString; + this._requesterUserId = data.requesterUserId; + + return true; + } + + public get id(): number + { + return this._id; + } + + public get name(): string + { + return this._name; + } + + public get requesterUserId(): number + { + return this._requesterUserId; + } + + public get figureString(): string + { + return this._figureString; + } +} diff --git a/apps/frontend/src/api/friends/MessengerSettings.ts b/apps/frontend/src/api/friends/MessengerSettings.ts new file mode 100644 index 0000000..e0fc8c2 --- /dev/null +++ b/apps/frontend/src/api/friends/MessengerSettings.ts @@ -0,0 +1,11 @@ +import { FriendCategoryData } from '@nitrots/nitro-renderer'; + +export class MessengerSettings +{ + constructor( + public userFriendLimit: number = 0, + public normalFriendLimit: number = 0, + public extendedFriendLimit: number = 0, + public categories: FriendCategoryData[] = []) + {} +} diff --git a/apps/frontend/src/api/friends/MessengerThread.ts b/apps/frontend/src/api/friends/MessengerThread.ts new file mode 100644 index 0000000..405ea33 --- /dev/null +++ b/apps/frontend/src/api/friends/MessengerThread.ts @@ -0,0 +1,96 @@ +import { GetGroupChatData } from './GetGroupChatData'; +import { MessengerFriend } from './MessengerFriend'; +import { MessengerGroupType } from './MessengerGroupType'; +import { MessengerThreadChat } from './MessengerThreadChat'; +import { MessengerThreadChatGroup } from './MessengerThreadChatGroup'; + +export class MessengerThread +{ + public static MESSAGE_RECEIVED: string = 'MT_MESSAGE_RECEIVED'; + public static THREAD_ID: number = 0; + + private _threadId: number; + private _participant: MessengerFriend; + private _groups: MessengerThreadChatGroup[]; + private _lastUpdated: Date; + private _unreadCount: number; + + constructor(participant: MessengerFriend) + { + this._threadId = ++MessengerThread.THREAD_ID; + this._participant = participant; + this._groups = []; + this._lastUpdated = new Date(); + this._unreadCount = 0; + } + + public addMessage(senderId: number, message: string, secondsSinceSent: number = 0, extraData: string = null, type: number = 0): MessengerThreadChat + { + const isGroupChat = (senderId < 0 && extraData); + const userId = isGroupChat ? GetGroupChatData(extraData).userId : senderId; + + const group = this.getLastGroup(userId); + + if(!group) return; + + if(isGroupChat) group.type = MessengerGroupType.GROUP_CHAT; + + const chat = new MessengerThreadChat(senderId, message, secondsSinceSent, extraData, type); + + group.addChat(chat); + + this._lastUpdated = new Date(); + + this._unreadCount++; + + return chat; + } + + private getLastGroup(userId: number): MessengerThreadChatGroup + { + let group = this._groups[(this._groups.length - 1)]; + + if(group && (group.userId === userId)) return group; + + group = new MessengerThreadChatGroup(userId); + + this._groups.push(group); + + return group; + } + + public setRead(): void + { + this._unreadCount = 0; + } + + public get threadId(): number + { + return this._threadId; + } + + public get participant(): MessengerFriend + { + return this._participant; + } + + public get groups(): MessengerThreadChatGroup[] + { + return this._groups; + } + + public get lastUpdated(): Date + { + return this._lastUpdated; + } + + public get unreadCount(): number + { + return this._unreadCount; + } + + public get unread(): boolean + { + return (this._unreadCount > 0); + } +} diff --git a/apps/frontend/src/api/friends/MessengerThreadChat.ts b/apps/frontend/src/api/friends/MessengerThreadChat.ts new file mode 100644 index 0000000..2927fec --- /dev/null +++ b/apps/frontend/src/api/friends/MessengerThreadChat.ts @@ -0,0 +1,54 @@ +export class MessengerThreadChat +{ + public static CHAT: number = 0; + public static ROOM_INVITE: number = 1; + public static STATUS_NOTIFICATION: number = 2; + public static SECURITY_NOTIFICATION: number = 3; + + private _type: number; + private _senderId: number; + private _message: string; + private _secondsSinceSent: number; + private _extraData: string; + private _date: Date; + + constructor(senderId: number, message: string, secondsSinceSent: number = 0, extraData: string = null, type: number = 0) + { + this._type = type; + this._senderId = senderId; + this._message = message; + this._secondsSinceSent = secondsSinceSent; + this._extraData = extraData; + this._date = new Date(); + } + + public get type(): number + { + return this._type; + } + + public get senderId(): number + { + return this._senderId; + } + + public get message(): string + { + return this._message; + } + + public get secondsSinceSent(): number + { + return this._secondsSinceSent; + } + + public get extraData(): string + { + return this._extraData; + } + + public get date(): Date + { + return this._date; + } +} diff --git a/apps/frontend/src/api/friends/MessengerThreadChatGroup.ts b/apps/frontend/src/api/friends/MessengerThreadChatGroup.ts new file mode 100644 index 0000000..1668aed --- /dev/null +++ b/apps/frontend/src/api/friends/MessengerThreadChatGroup.ts @@ -0,0 +1,41 @@ +import { MessengerGroupType } from './MessengerGroupType'; +import { MessengerThreadChat } from './MessengerThreadChat'; + +export class MessengerThreadChatGroup +{ + private _userId: number; + private _chats: MessengerThreadChat[]; + private _type: number; + + constructor(userId: number, type = MessengerGroupType.PRIVATE_CHAT) + { + this._userId = userId; + this._chats = []; + this._type = type; + } + + public addChat(message: MessengerThreadChat): void + { + this._chats.push(message); + } + + public get userId(): number + { + return this._userId; + } + + public get chats(): MessengerThreadChat[] + { + return this._chats; + } + + public get type(): number + { + return this._type; + } + + public set type(type: number) + { + this._type = type; + } +} diff --git a/apps/frontend/src/api/friends/OpenMessengerChat.ts b/apps/frontend/src/api/friends/OpenMessengerChat.ts new file mode 100644 index 0000000..9270981 --- /dev/null +++ b/apps/frontend/src/api/friends/OpenMessengerChat.ts @@ -0,0 +1,7 @@ +import { CreateLinkEvent } from '..'; + +export function OpenMessengerChat(friendId: number = 0): void +{ + if(friendId === 0) CreateLinkEvent('friends-messenger/toggle'); + else CreateLinkEvent(`friends-messenger/${ friendId }`); +} diff --git a/apps/frontend/src/api/friends/index.ts b/apps/frontend/src/api/friends/index.ts new file mode 100644 index 0000000..ce1ed60 --- /dev/null +++ b/apps/frontend/src/api/friends/index.ts @@ -0,0 +1,11 @@ +export * from './GetGroupChatData'; +export * from './IGroupChatData'; +export * from './MessengerFriend'; +export * from './MessengerGroupType'; +export * from './MessengerIconState'; +export * from './MessengerRequest'; +export * from './MessengerSettings'; +export * from './MessengerThread'; +export * from './MessengerThreadChat'; +export * from './MessengerThreadChatGroup'; +export * from './OpenMessengerChat'; diff --git a/apps/frontend/src/api/groups/GetGroupInformation.ts b/apps/frontend/src/api/groups/GetGroupInformation.ts new file mode 100644 index 0000000..6b4a48c --- /dev/null +++ b/apps/frontend/src/api/groups/GetGroupInformation.ts @@ -0,0 +1,7 @@ +import { GroupInformationComposer } from '@nitrots/nitro-renderer'; +import { SendMessageComposer } from '..'; + +export function GetGroupInformation(groupId: number): void +{ + SendMessageComposer(new GroupInformationComposer(groupId, true)); +} diff --git a/apps/frontend/src/api/groups/GetGroupManager.ts b/apps/frontend/src/api/groups/GetGroupManager.ts new file mode 100644 index 0000000..d372ace --- /dev/null +++ b/apps/frontend/src/api/groups/GetGroupManager.ts @@ -0,0 +1,6 @@ +import { CreateLinkEvent } from '..'; + +export function GetGroupManager(groupId: number): void +{ + CreateLinkEvent(`groups/manage/${ groupId }`); +} diff --git a/apps/frontend/src/api/groups/GetGroupMembers.ts b/apps/frontend/src/api/groups/GetGroupMembers.ts new file mode 100644 index 0000000..27fb6d5 --- /dev/null +++ b/apps/frontend/src/api/groups/GetGroupMembers.ts @@ -0,0 +1,7 @@ +import { CreateLinkEvent } from '..'; + +export function GetGroupMembers(groupId: number, levelId?: number): void +{ + if(!levelId) CreateLinkEvent(`group-members/${ groupId }`); + else CreateLinkEvent(`group-members/${ groupId }/${ levelId }`); +} diff --git a/apps/frontend/src/api/groups/GroupBadgePart.ts b/apps/frontend/src/api/groups/GroupBadgePart.ts new file mode 100644 index 0000000..3b74875 --- /dev/null +++ b/apps/frontend/src/api/groups/GroupBadgePart.ts @@ -0,0 +1,31 @@ + +export class GroupBadgePart +{ + public static BASE: string = 'b'; + public static SYMBOL: string = 's'; + + public type: string; + public key: number; + public color: number; + public position: number; + + constructor(type: string, key?: number, color?: number, position?: number) + { + this.type = type; + this.key = key ? key : 0; + this.color = color ? color : 0; + this.position = position ? position : 4; + } + + public get code(): string + { + if((this.key === 0) && (this.type !== GroupBadgePart.BASE)) return null; + + return GroupBadgePart.getCode(this.type, this.key, this.color, this.position); + } + + public static getCode(type: string, key: number, color: number, position: number): string + { + return type + (key < 10 ? '0' : '') + key + (color < 10 ? '0' : '') + color + position; + } +} diff --git a/apps/frontend/src/api/groups/GroupMembershipType.ts b/apps/frontend/src/api/groups/GroupMembershipType.ts new file mode 100644 index 0000000..532c836 --- /dev/null +++ b/apps/frontend/src/api/groups/GroupMembershipType.ts @@ -0,0 +1,6 @@ +export class GroupMembershipType +{ + public static NOT_MEMBER: number = 0; + public static MEMBER: number = 1; + public static REQUEST_PENDING: number = 2; +} diff --git a/apps/frontend/src/api/groups/GroupType.ts b/apps/frontend/src/api/groups/GroupType.ts new file mode 100644 index 0000000..58ae72c --- /dev/null +++ b/apps/frontend/src/api/groups/GroupType.ts @@ -0,0 +1,6 @@ +export class GroupType +{ + public static REGULAR: number = 0; + public static EXCLUSIVE: number = 1; + public static PRIVATE: number = 2; +} diff --git a/apps/frontend/src/api/groups/IGroupCustomize.ts b/apps/frontend/src/api/groups/IGroupCustomize.ts new file mode 100644 index 0000000..44fc4ff --- /dev/null +++ b/apps/frontend/src/api/groups/IGroupCustomize.ts @@ -0,0 +1,8 @@ +export interface IGroupCustomize +{ + badgeBases: { id: number, images: string[] }[]; + badgeSymbols: { id: number, images: string[] }[]; + badgePartColors: { id: number, color: string }[]; + groupColorsA: { id: number, color: string }[]; + groupColorsB: { id: number, color: string }[]; +} diff --git a/apps/frontend/src/api/groups/IGroupData.ts b/apps/frontend/src/api/groups/IGroupData.ts new file mode 100644 index 0000000..bb65b49 --- /dev/null +++ b/apps/frontend/src/api/groups/IGroupData.ts @@ -0,0 +1,13 @@ +import { GroupBadgePart } from './GroupBadgePart'; + +export interface IGroupData +{ + groupId: number; + groupName: string; + groupDescription: string; + groupHomeroomId: number; + groupState: number; + groupCanMembersDecorate: boolean; + groupColors: number[]; + groupBadgeParts: GroupBadgePart[]; +} diff --git a/apps/frontend/src/api/groups/ToggleFavoriteGroup.ts b/apps/frontend/src/api/groups/ToggleFavoriteGroup.ts new file mode 100644 index 0000000..a4b929c --- /dev/null +++ b/apps/frontend/src/api/groups/ToggleFavoriteGroup.ts @@ -0,0 +1,7 @@ +import { GroupFavoriteComposer, GroupUnfavoriteComposer, HabboGroupEntryData } from '@nitrots/nitro-renderer'; +import { SendMessageComposer } from '..'; + +export const ToggleFavoriteGroup = (group: HabboGroupEntryData) => +{ + SendMessageComposer(group.favourite ? new GroupUnfavoriteComposer(group.groupId) : new GroupFavoriteComposer(group.groupId)); +} diff --git a/apps/frontend/src/api/groups/TryJoinGroup.ts b/apps/frontend/src/api/groups/TryJoinGroup.ts new file mode 100644 index 0000000..4fbbcde --- /dev/null +++ b/apps/frontend/src/api/groups/TryJoinGroup.ts @@ -0,0 +1,4 @@ +import { GroupJoinComposer } from '@nitrots/nitro-renderer'; +import { SendMessageComposer } from '..'; + +export const TryJoinGroup = (groupId: number) => SendMessageComposer(new GroupJoinComposer(groupId)); diff --git a/apps/frontend/src/api/groups/index.ts b/apps/frontend/src/api/groups/index.ts new file mode 100644 index 0000000..4842948 --- /dev/null +++ b/apps/frontend/src/api/groups/index.ts @@ -0,0 +1,10 @@ +export * from './GetGroupInformation'; +export * from './GetGroupManager'; +export * from './GetGroupMembers'; +export * from './GroupBadgePart'; +export * from './GroupMembershipType'; +export * from './GroupType'; +export * from './IGroupCustomize'; +export * from './IGroupData'; +export * from './ToggleFavoriteGroup'; +export * from './TryJoinGroup'; diff --git a/apps/frontend/src/api/guide-tool/GuideSessionState.ts b/apps/frontend/src/api/guide-tool/GuideSessionState.ts new file mode 100644 index 0000000..c5e24f3 --- /dev/null +++ b/apps/frontend/src/api/guide-tool/GuideSessionState.ts @@ -0,0 +1,23 @@ +export class GuideSessionState +{ + public static readonly NONE: string = 'NONE'; + public static readonly ERROR: string = 'ERROR'; + public static readonly REJECTED: string = 'REJECTED'; + public static readonly USER_CREATE: string = 'USER_CREATE'; + public static readonly USER_PENDING: string = 'USER_PENDING'; + public static readonly USER_ONGOING: string = 'USER_ONGOING'; + public static readonly USER_FEEDBACK: string = 'USER_FEEDBACK'; + public static readonly USER_NO_HELPERS: string = 'USER_NO_HELPERS'; + public static readonly USER_SOMETHING_WRONG: string = 'USER_SOMETHING_WRONG'; + public static readonly USER_THANKS: string = 'USER_THANKS'; + public static readonly USER_GUIDE_DISCONNECTED: string = 'USER_GUIDE_DISCONNECTED'; + public static readonly GUIDE_TOOL_MENU: string = 'GUIDE_TOOL_MENU'; + public static readonly GUIDE_ACCEPT: string = 'GUIDE_ACCEPT'; + public static readonly GUIDE_ONGOING: string = 'GUIDE_ONGOING'; + public static readonly GUIDE_CLOSED: string = 'GUIDE_CLOSED'; + public static readonly GUARDIAN_CHAT_REVIEW_ACCEPT: string = 'GUARDIAN_CHAT_REVIEW_ACCEPT'; + public static readonly GUARDIAN_CHAT_REVIEW_WAIT_FOR_VOTERS: string = 'GUARDIAN_CHAT_REVIEW_WAIT_FOR_VOTERS'; + public static readonly GUARDIAN_CHAT_REVIEW_VOTE: string = 'GUARDIAN_CHAT_REVIEW_VOTE'; + public static readonly GUARDIAN_CHAT_REVIEW_WAIT_FOR_RESULTS: string = 'GUARDIAN_CHAT_REVIEW_WAIT_FOR_RESULTS'; + public static readonly GUARDIAN_CHAT_REVIEW_RESULTS: string = 'GUARDIAN_CHAT_REVIEW_RESULTS'; +} diff --git a/apps/frontend/src/api/guide-tool/GuideToolMessage.ts b/apps/frontend/src/api/guide-tool/GuideToolMessage.ts new file mode 100644 index 0000000..3810726 --- /dev/null +++ b/apps/frontend/src/api/guide-tool/GuideToolMessage.ts @@ -0,0 +1,21 @@ +export class GuideToolMessage +{ + private _message: string; + private _roomId: number; + + constructor(message: string, roomId?: number) + { + this._message = message; + this._roomId = roomId; + } + + public get message(): string + { + return this._message; + } + + public get roomId(): number + { + return this._roomId; + } +} diff --git a/apps/frontend/src/api/guide-tool/GuideToolMessageGroup.ts b/apps/frontend/src/api/guide-tool/GuideToolMessageGroup.ts new file mode 100644 index 0000000..bf03c9b --- /dev/null +++ b/apps/frontend/src/api/guide-tool/GuideToolMessageGroup.ts @@ -0,0 +1,28 @@ +import { GuideToolMessage } from './GuideToolMessage'; + +export class GuideToolMessageGroup +{ + private _userId: number; + private _messages: GuideToolMessage[]; + + constructor(userId: number) + { + this._userId = userId; + this._messages = []; + } + + public addChat(message: GuideToolMessage): void + { + this._messages.push(message); + } + + public get userId(): number + { + return this._userId; + } + + public get messages(): GuideToolMessage[] + { + return this._messages; + } +} diff --git a/apps/frontend/src/api/guide-tool/index.ts b/apps/frontend/src/api/guide-tool/index.ts new file mode 100644 index 0000000..1400adc --- /dev/null +++ b/apps/frontend/src/api/guide-tool/index.ts @@ -0,0 +1,3 @@ +export * from './GuideSessionState'; +export * from './GuideToolMessage'; +export * from './GuideToolMessageGroup'; diff --git a/apps/frontend/src/api/hc-center/ClubStatus.ts b/apps/frontend/src/api/hc-center/ClubStatus.ts new file mode 100644 index 0000000..8200b14 --- /dev/null +++ b/apps/frontend/src/api/hc-center/ClubStatus.ts @@ -0,0 +1,6 @@ +export class ClubStatus +{ + public static ACTIVE: string = 'active'; + public static NONE: string = 'none'; + public static EXPIRED: string = 'expired'; +} diff --git a/apps/frontend/src/api/hc-center/GetClubBadge.ts b/apps/frontend/src/api/hc-center/GetClubBadge.ts new file mode 100644 index 0000000..6b779e0 --- /dev/null +++ b/apps/frontend/src/api/hc-center/GetClubBadge.ts @@ -0,0 +1,11 @@ +const DEFAULT_BADGE: string = 'HC1'; +const BADGES: string[] = [ 'ACH_VipHC1', 'ACH_VipHC2', 'ACH_VipHC3', 'ACH_VipHC4', 'ACH_VipHC5', 'HC1', 'HC2', 'HC3', 'HC4', 'HC5' ]; + +export const GetClubBadge = (badgeCodes: string[]) => +{ + let badgeCode: string = null; + + BADGES.forEach(badge => ((badgeCodes.indexOf(badge) > -1) && (badgeCode = badge))); + + return (badgeCode || DEFAULT_BADGE); +} diff --git a/apps/frontend/src/api/hc-center/index.ts b/apps/frontend/src/api/hc-center/index.ts new file mode 100644 index 0000000..cee8f69 --- /dev/null +++ b/apps/frontend/src/api/hc-center/index.ts @@ -0,0 +1,2 @@ +export * from './ClubStatus'; +export * from './GetClubBadge'; diff --git a/apps/frontend/src/api/help/CallForHelpResult.ts b/apps/frontend/src/api/help/CallForHelpResult.ts new file mode 100644 index 0000000..37e7ea1 --- /dev/null +++ b/apps/frontend/src/api/help/CallForHelpResult.ts @@ -0,0 +1,5 @@ +export class CallForHelpResult +{ + public static readonly TOO_MANY_PENDING_CALLS_CODE = 1; + public static readonly HAS_ABUSIVE_CALL_CODE = 2; +} diff --git a/apps/frontend/src/api/help/GetCloseReasonKey.ts b/apps/frontend/src/api/help/GetCloseReasonKey.ts new file mode 100644 index 0000000..520d14f --- /dev/null +++ b/apps/frontend/src/api/help/GetCloseReasonKey.ts @@ -0,0 +1,8 @@ +export const GetCloseReasonKey = (code: number) => +{ + if(code === 1) return 'useless'; + + if(code === 2) return 'abusive'; + + return 'resolved'; +} diff --git a/apps/frontend/src/api/help/IHelpReport.ts b/apps/frontend/src/api/help/IHelpReport.ts new file mode 100644 index 0000000..8611707 --- /dev/null +++ b/apps/frontend/src/api/help/IHelpReport.ts @@ -0,0 +1,19 @@ +import { IChatEntry } from '../chat-history'; + +export interface IHelpReport +{ + reportType: number; + reportedUserId: number; + reportedChats: IChatEntry[]; + cfhCategory: number; + cfhTopic: number; + roomId: number; + roomName: string; + groupId: number; + threadId: number; + messageId: number; + extraData: string; + roomObjectId: number; + message: string; + currentStep: number; +} diff --git a/apps/frontend/src/api/help/IReportedUser.ts b/apps/frontend/src/api/help/IReportedUser.ts new file mode 100644 index 0000000..90a3887 --- /dev/null +++ b/apps/frontend/src/api/help/IReportedUser.ts @@ -0,0 +1,5 @@ +export interface IReportedUser +{ + id: number; + username: string; +} diff --git a/apps/frontend/src/api/help/ReportState.ts b/apps/frontend/src/api/help/ReportState.ts new file mode 100644 index 0000000..ae3a3bd --- /dev/null +++ b/apps/frontend/src/api/help/ReportState.ts @@ -0,0 +1,8 @@ +export class ReportState +{ + public static readonly SELECT_USER = 0; + public static readonly SELECT_CHATS = 1; + public static readonly SELECT_TOPICS = 2; + public static readonly INPUT_REPORT_MESSAGE = 3; + public static readonly REPORT_SUMMARY = 4; +} diff --git a/apps/frontend/src/api/help/ReportType.ts b/apps/frontend/src/api/help/ReportType.ts new file mode 100644 index 0000000..24eb7ae --- /dev/null +++ b/apps/frontend/src/api/help/ReportType.ts @@ -0,0 +1,11 @@ +export class ReportType +{ + public static readonly EMERGENCY = 1; + public static readonly GUIDE = 2; + public static readonly IM = 3; + public static readonly ROOM = 4; + public static readonly BULLY = 6; + public static readonly THREAD = 7; + public static readonly MESSAGE = 8; + public static readonly PHOTO = 9; +} diff --git a/apps/frontend/src/api/help/index.ts b/apps/frontend/src/api/help/index.ts new file mode 100644 index 0000000..6fa2045 --- /dev/null +++ b/apps/frontend/src/api/help/index.ts @@ -0,0 +1,6 @@ +export * from './CallForHelpResult'; +export * from './GetCloseReasonKey'; +export * from './IHelpReport'; +export * from './IReportedUser'; +export * from './ReportState'; +export * from './ReportType'; diff --git a/apps/frontend/src/api/index.ts b/apps/frontend/src/api/index.ts new file mode 100644 index 0000000..af96444 --- /dev/null +++ b/apps/frontend/src/api/index.ts @@ -0,0 +1,29 @@ +export * from './achievements'; +export * from './avatar'; +export * from './camera'; +export * from './campaign'; +export * from './catalog'; +export * from './chat-history'; +export * from './events'; +export * from './friends'; +export * from './GetRendererVersion'; +export * from './GetUIVersion'; +export * from './groups'; +export * from './guide-tool'; +export * from './hc-center'; +export * from './help'; +export * from './inventory'; +export * from './mod-tools'; +export * from './navigator'; +export * from './nitro'; +export * from './nitro/avatar'; +export * from './nitro/camera'; +export * from './nitro/core'; +export * from './nitro/room'; +export * from './nitro/session'; +export * from './notification'; +export * from './purse'; +export * from './room'; +export * from './user'; +export * from './utils'; +export * from './wired'; diff --git a/apps/frontend/src/api/inventory/FurniCategory.ts b/apps/frontend/src/api/inventory/FurniCategory.ts new file mode 100644 index 0000000..6528947 --- /dev/null +++ b/apps/frontend/src/api/inventory/FurniCategory.ts @@ -0,0 +1,26 @@ +export class FurniCategory +{ + public static DEFAULT: number = 1; + public static WALL_PAPER: number = 2; + public static FLOOR: number = 3; + public static LANDSCAPE: number = 4; + public static POST_IT: number = 5; + public static POSTER: number = 6; + public static SOUND_SET: number = 7; + public static TRAX_SONG: number = 8; + public static PRESENT: number = 9; + public static ECOTRON_BOX: number = 10; + public static TROPHY: number = 11; + public static CREDIT_FURNI: number = 12; + public static PET_SHAMPOO: number = 13; + public static PET_CUSTOM_PART: number = 14; + public static PET_CUSTOM_PART_SHAMPOO: number = 15; + public static PET_SADDLE: number = 16; + public static GUILD_FURNI: number = 17; + public static GAME_FURNI: number = 18; + public static MONSTERPLANT_SEED: number = 19; + public static MONSTERPLANT_REVIVAL: number = 20; + public static MONSTERPLANT_REBREED: number = 21; + public static MONSTERPLANT_FERTILIZE: number = 22; + public static FIGURE_PURCHASABLE_SET: number = 23; +} diff --git a/apps/frontend/src/api/inventory/FurnitureItem.ts b/apps/frontend/src/api/inventory/FurnitureItem.ts new file mode 100644 index 0000000..f5f2f0f --- /dev/null +++ b/apps/frontend/src/api/inventory/FurnitureItem.ts @@ -0,0 +1,245 @@ +import { GetTickerTime, IFurnitureItemData, IObjectData } from '@nitrots/nitro-renderer'; +import { IFurnitureItem } from './IFurnitureItem'; + +export class FurnitureItem implements IFurnitureItem +{ + private _expirationTimeStamp: number; + private _isWallItem: boolean; + private _songId: number; + private _locked: boolean; + private _id: number; + private _ref: number; + private _category: number; + private _type: number; + private _stuffData: IObjectData; + private _extra: number; + private _recyclable: boolean; + private _tradeable: boolean; + private _groupable: boolean; + private _sellable: boolean; + private _secondsToExpiration: number; + private _hasRentPeriodStarted: boolean; + private _creationDay: number; + private _creationMonth: number; + private _creationYear: number; + private _slotId: string; + private _isRented: boolean; + private _flatId: number; + + constructor(parser: IFurnitureItemData) + { + if(!parser) return; + + this._locked = false; + this._id = parser.itemId; + this._type = parser.spriteId; + this._ref = parser.ref; + this._category = parser.category; + this._groupable = ((parser.isGroupable) && (!(parser.rentable))); + this._tradeable = parser.tradable; + this._recyclable = parser.isRecycleable; + this._sellable = parser.sellable; + this._stuffData = parser.stuffData; + this._extra = parser.extra; + this._secondsToExpiration = parser.secondsToExpiration; + this._expirationTimeStamp = parser.expirationTimeStamp; + this._hasRentPeriodStarted = parser.hasRentPeriodStarted; + this._creationDay = parser.creationDay; + this._creationMonth = parser.creationMonth; + this._creationYear = parser.creationYear; + this._slotId = parser.slotId; + this._songId = parser.songId; + this._flatId = parser.flatId; + this._isRented = parser.rentable; + this._isWallItem = parser.isWallItem; + } + + public get rentable(): boolean + { + return this._isRented; + } + + public get id(): number + { + return this._id; + } + + public get ref(): number + { + return this._ref; + } + + public get category(): number + { + return this._category; + } + + public get type(): number + { + return this._type; + } + + public get stuffData(): IObjectData + { + return this._stuffData; + } + + public set stuffData(k: IObjectData) + { + this._stuffData = k; + } + + public get extra(): number + { + return this._extra; + } + + public get recyclable(): boolean + { + return this._recyclable; + } + + public get isTradable(): boolean + { + return this._tradeable; + } + + public get isGroupable(): boolean + { + return this._groupable; + } + + public get sellable(): boolean + { + return this._sellable; + } + + public get secondsToExpiration(): number + { + if(this._secondsToExpiration === -1) return -1; + + let time = -1; + + if(this._hasRentPeriodStarted) + { + time = (this._secondsToExpiration - ((GetTickerTime() - this._expirationTimeStamp) / 1000)); + + if(time < 0) time = 0; + } + else + { + time = this._secondsToExpiration; + } + + return time; + } + + public get creationDay(): number + { + return this._creationDay; + } + + public get creationMonth(): number + { + return this._creationMonth; + } + + public get creationYear(): number + { + return this._creationYear; + } + + public get slotId(): string + { + return this._slotId; + } + + public get songId(): number + { + return this._songId; + } + + public get locked(): boolean + { + return this._locked; + } + + public set locked(k: boolean) + { + this._locked = k; + } + + public get flatId(): number + { + return this._flatId; + } + + public get isWallItem(): boolean + { + return this._isWallItem; + } + + public get hasRentPeriodStarted(): boolean + { + return this._hasRentPeriodStarted; + } + + public get expirationTimeStamp(): number + { + return this._expirationTimeStamp; + } + + public update(parser: IFurnitureItemData): void + { + this._type = parser.spriteId; + this._ref = parser.ref; + this._category = parser.category; + this._groupable = (parser.isGroupable && !parser.rentable); + this._tradeable = parser.tradable; + this._recyclable = parser.isRecycleable; + this._sellable = parser.sellable; + this._stuffData = parser.stuffData; + this._extra = parser.extra; + this._secondsToExpiration = parser.secondsToExpiration; + this._expirationTimeStamp = parser.expirationTimeStamp; + this._hasRentPeriodStarted = parser.hasRentPeriodStarted; + this._creationDay = parser.creationDay; + this._creationMonth = parser.creationMonth; + this._creationYear = parser.creationYear; + this._slotId = parser.slotId; + this._songId = parser.songId; + this._flatId = parser.flatId; + this._isRented = parser.rentable; + this._isWallItem = parser.isWallItem; + } + + public clone(): FurnitureItem + { + const item = new FurnitureItem(null); + + item._expirationTimeStamp = this._expirationTimeStamp; + item._isWallItem = this._isWallItem; + item._songId = this._songId; + item._locked = this._locked; + item._id = this._id; + item._ref = this._ref; + item._category = this._category; + item._type = this._type; + item._stuffData = this._stuffData; + item._extra = this._extra; + item._recyclable = this._recyclable; + item._tradeable = this._tradeable; + item._groupable = this._groupable; + item._sellable = this._sellable; + item._secondsToExpiration = this._secondsToExpiration; + item._hasRentPeriodStarted = this._hasRentPeriodStarted; + item._creationDay = this._creationDay; + item._creationMonth = this._creationMonth; + item._creationYear = this._creationYear; + item._slotId = this._slotId; + item._isRented = this._isRented; + item._flatId = this._flatId; + + return item; + } +} diff --git a/apps/frontend/src/api/inventory/FurnitureUtilities.ts b/apps/frontend/src/api/inventory/FurnitureUtilities.ts new file mode 100644 index 0000000..741f1ea --- /dev/null +++ b/apps/frontend/src/api/inventory/FurnitureUtilities.ts @@ -0,0 +1,172 @@ +import { FurnitureListItemParser, IObjectData } from '@nitrots/nitro-renderer'; +import { GetRoomEngine } from '../nitro'; +import { FurniCategory } from './FurniCategory'; +import { FurnitureItem } from './FurnitureItem'; +import { GroupItem } from './GroupItem'; + +export const createGroupItem = (type: number, category: number, stuffData: IObjectData, extra: number = NaN) => new GroupItem(type, category, GetRoomEngine(), stuffData, extra); + +const addSingleFurnitureItem = (set: GroupItem[], item: FurnitureItem, unseen: boolean) => +{ + const groupItems: GroupItem[] = []; + + for(const groupItem of set) + { + if(groupItem.type === item.type) groupItems.push(groupItem); + } + + for(const groupItem of groupItems) + { + if(groupItem.getItemById(item.id)) return groupItem; + } + + const groupItem = createGroupItem(item.type, item.category, item.stuffData, item.extra); + + groupItem.push(item); + + if(unseen) + { + groupItem.hasUnseenItems = true; + + set.unshift(groupItem); + } + else + { + set.push(groupItem); + } + + return groupItem; +} + +const addGroupableFurnitureItem = (set: GroupItem[], item: FurnitureItem, unseen: boolean) => +{ + let existingGroup: GroupItem = null; + + for(const groupItem of set) + { + if((groupItem.type === item.type) && (groupItem.isWallItem === item.isWallItem) && groupItem.isGroupable) + { + if(item.category === FurniCategory.POSTER) + { + if(groupItem.stuffData.getLegacyString() === item.stuffData.getLegacyString()) + { + existingGroup = groupItem; + + break; + } + } + + else if(item.category === FurniCategory.GUILD_FURNI) + { + if(item.stuffData.compare(groupItem.stuffData)) + { + existingGroup = groupItem; + + break; + } + } + + else + { + existingGroup = groupItem; + + break; + } + } + } + + if(existingGroup) + { + existingGroup.push(item); + + if(unseen) + { + existingGroup.hasUnseenItems = true; + + const index = set.indexOf(existingGroup); + + if(index >= 0) set.splice(index, 1); + + set.unshift(existingGroup); + } + + return existingGroup; + } + + existingGroup = createGroupItem(item.type, item.category, item.stuffData, item.extra); + + existingGroup.push(item); + + if(unseen) + { + existingGroup.hasUnseenItems = true; + + set.unshift(existingGroup); + } + else + { + set.push(existingGroup); + } + + return existingGroup; +} + +export const addFurnitureItem = (set: GroupItem[], item: FurnitureItem, unseen: boolean) => +{ + if(!item.isGroupable) + { + addSingleFurnitureItem(set, item, unseen); + } + else + { + addGroupableFurnitureItem(set, item, unseen); + } +} + +export const mergeFurniFragments = (fragment: Map, totalFragments: number, fragmentNumber: number, fragments: Map[]) => +{ + if(totalFragments === 1) return fragment; + + fragments[fragmentNumber] = fragment; + + for(const frag of fragments) + { + if(!frag) return null; + } + + const merged: Map = new Map(); + + for(const frag of fragments) + { + for(const [ key, value ] of frag) merged.set(key, value); + + frag.clear(); + } + + fragments = null; + + return merged; +} + +export const getAllItemIds = (groupItems: GroupItem[]) => +{ + const itemIds: number[] = []; + + for(const groupItem of groupItems) + { + let totalCount = groupItem.getTotalCount(); + + if(groupItem.category === FurniCategory.POST_IT) totalCount = 1; + + let i = 0; + + while(i < totalCount) + { + itemIds.push(groupItem.getItemByIndex(i).id); + + i++; + } + } + + return itemIds; +} diff --git a/apps/frontend/src/api/inventory/GroupItem.ts b/apps/frontend/src/api/inventory/GroupItem.ts new file mode 100644 index 0000000..8569321 --- /dev/null +++ b/apps/frontend/src/api/inventory/GroupItem.ts @@ -0,0 +1,461 @@ +import { IObjectData, IRoomEngine } from '@nitrots/nitro-renderer'; +import { LocalizeText } from '../utils'; +import { FurniCategory } from './FurniCategory'; +import { FurnitureItem } from './FurnitureItem'; +import { IFurnitureItem } from './IFurnitureItem'; + +export class GroupItem +{ + private _type: number; + private _category: number; + private _roomEngine: IRoomEngine; + private _stuffData: IObjectData; + private _extra: number; + private _isWallItem: boolean; + private _iconUrl: string; + private _name: string; + private _description: string; + private _locked: boolean; + private _selected: boolean; + private _hasUnseenItems: boolean; + private _items: FurnitureItem[]; + + constructor(type: number = -1, category: number = -1, roomEngine: IRoomEngine = null, stuffData: IObjectData = null, extra: number = -1) + { + this._type = type; + this._category = category; + this._roomEngine = roomEngine; + this._stuffData = stuffData; + this._extra = extra; + this._isWallItem = false; + this._iconUrl = null; + this._name = null; + this._description = null; + this._locked = false; + this._selected = false; + this._hasUnseenItems = false; + this._items = []; + } + + public clone(): GroupItem + { + const groupItem = new GroupItem(); + + groupItem._type = this._type; + groupItem._category = this._category; + groupItem._roomEngine = this._roomEngine; + groupItem._stuffData = this._stuffData; + groupItem._extra = this._extra; + groupItem._isWallItem = this._isWallItem; + groupItem._iconUrl = this._iconUrl; + groupItem._name = this._name; + groupItem._description = this._description; + groupItem._locked = this._locked; + groupItem._selected = this._selected; + groupItem._hasUnseenItems = this._hasUnseenItems; + groupItem._items = this._items; + + return groupItem; + } + + public prepareGroup(): void + { + this.setIcon(); + this.setName(); + this.setDescription(); + } + + public dispose(): void + { + + } + + public getItemByIndex(index: number): FurnitureItem + { + return this._items[index]; + } + + public getItemById(id: number): FurnitureItem + { + for(const item of this._items) + { + if(item.id !== id) continue; + + return item; + } + + return null; + } + + public getTradeItems(count: number): IFurnitureItem[] + { + const items: IFurnitureItem[] = []; + + const furnitureItem = this.getLastItem(); + + if(!furnitureItem) return items; + + let found = 0; + let i = 0; + + while(i < this._items.length) + { + if(found >= count) break; + + const item = this.getItemByIndex(i); + + if(!item.locked && item.isTradable && (item.type === furnitureItem.type)) + { + items.push(item); + + found++; + } + + i++; + } + + return items; + } + + public push(item: FurnitureItem): void + { + const items = [ ...this._items ]; + + let index = 0; + + while(index < items.length) + { + let existingItem = items[index]; + + if(existingItem.id === item.id) + { + existingItem = existingItem.clone(); + + existingItem.locked = false; + + items.splice(index, 1); + + items.push(existingItem); + + this._items = items; + + return; + } + + index++; + } + + items.push(item); + + this._items = items; + + if(this._items.length === 1) this.prepareGroup(); + } + + public pop(): FurnitureItem + { + const items = [ ...this._items ]; + + let item: FurnitureItem = null; + + if(items.length > 0) + { + const index = (items.length - 1); + + item = items[index]; + + items.splice(index, 1); + } + + this._items = items; + + return item; + } + + public remove(k: number): FurnitureItem + { + const items = [ ...this._items ]; + + let index = 0; + + while(index < items.length) + { + let existingItem = items[index]; + + if(existingItem.id === k) + { + items.splice(index, 1); + + this._items = items; + + return existingItem; + } + + index++; + } + + return null; + } + + public getTotalCount(): number + { + if(this._category === FurniCategory.POST_IT) + { + let count = 0; + let index = 0; + + while(index < this._items.length) + { + const item = this.getItemByIndex(index); + + count = (count + parseInt(item.stuffData.getLegacyString())); + + index++; + } + + return count; + } + + return this._items.length; + } + + public getUnlockedCount(): number + { + if(this.category === FurniCategory.POST_IT) return this.getTotalCount(); + + let count = 0; + let index = 0; + + while(index < this._items.length) + { + const item = this.getItemByIndex(index); + + if(!item.locked) count++; + + index++; + } + + return count; + } + + public getLastItem(): FurnitureItem + { + if(!this._items.length) return null; + + const item = this.getItemByIndex((this._items.length - 1)); + + return item; + } + + public unlockAllItems(): void + { + const items = [ ...this._items ]; + + let index = 0; + + while(index < items.length) + { + const item = items[index]; + + if(item.locked) + { + const newItem = item.clone(); + + newItem.locked = false; + + items[index] = newItem; + } + + index++; + } + + this._items = items; + } + + public lockItemIds(itemIds: number[]): boolean + { + const items = [ ...this._items ]; + + let index = 0; + let updated = false; + + while(index < items.length) + { + const item = items[index]; + const locked = (itemIds.indexOf(item.ref) >= 0); + + if(item.locked !== locked) + { + updated = true; + + const newItem = item.clone(); + + newItem.locked = locked; + + items[index] = newItem; + } + + index++; + } + + this._items = items; + + return updated; + } + + private setName(): void + { + const k = this.getLastItem(); + + if(!k) + { + this._name = ''; + + return; + } + + let key = ''; + + switch(this._category) + { + case FurniCategory.POSTER: + key = (('poster_' + k.stuffData.getLegacyString()) + '_name'); + break; + case FurniCategory.TRAX_SONG: + this._name = 'SONG_NAME'; + return; + default: + if(this.isWallItem) + { + key = ('wallItem.name.' + k.type); + } + else + { + key = ('roomItem.name.' + k.type); + } + } + + this._name = LocalizeText(key); + } + + private setDescription(): void + { + this._description = ''; + } + + private setIcon(): void + { + if(this._iconUrl) return; + + let url = null; + + if(this.isWallItem) + { + url = this._roomEngine.getFurnitureWallIconUrl(this._type, this._stuffData.getLegacyString()); + } + else + { + url = this._roomEngine.getFurnitureFloorIconUrl(this._type); + } + + if(!url) return; + + this._iconUrl = url; + } + + public get type(): number + { + return this._type; + } + + public get category(): number + { + return this._category; + } + + public get stuffData(): IObjectData + { + return this._stuffData; + } + + public get extra(): number + { + return this._extra; + } + + public get iconUrl(): string + { + return this._iconUrl; + } + + public get name(): string + { + return this._name; + } + + public get description(): string + { + return this._description; + } + + public get hasUnseenItems(): boolean + { + return this._hasUnseenItems; + } + + public set hasUnseenItems(flag: boolean) + { + this._hasUnseenItems = flag; + } + + public get locked(): boolean + { + return this._locked; + } + + public set locked(flag: boolean) + { + this._locked = flag; + } + + public get selected(): boolean + { + return this._selected; + } + + public set selected(flag: boolean) + { + this._selected = flag; + } + + public get isWallItem(): boolean + { + const item = this.getItemByIndex(0); + + return (item ? item.isWallItem : false); + } + + public get isGroupable(): boolean + { + const item = this.getItemByIndex(0); + + return (item ? item.isGroupable : false); + } + + public get isSellable(): boolean + { + const item = this.getItemByIndex(0); + + return (item ? item.sellable : false); + } + + public get items(): FurnitureItem[] + { + return this._items; + } + + public set items(items: FurnitureItem[]) + { + this._items = items; + } +} diff --git a/apps/frontend/src/api/inventory/IBotItem.ts b/apps/frontend/src/api/inventory/IBotItem.ts new file mode 100644 index 0000000..0a370ba --- /dev/null +++ b/apps/frontend/src/api/inventory/IBotItem.ts @@ -0,0 +1,6 @@ +import { BotData } from '@nitrots/nitro-renderer'; + +export interface IBotItem +{ + botData: BotData; +} diff --git a/apps/frontend/src/api/inventory/IFurnitureItem.ts b/apps/frontend/src/api/inventory/IFurnitureItem.ts new file mode 100644 index 0000000..435597d --- /dev/null +++ b/apps/frontend/src/api/inventory/IFurnitureItem.ts @@ -0,0 +1,17 @@ +import { IObjectData } from '@nitrots/nitro-renderer'; + +export interface IFurnitureItem +{ + id: number; + ref: number; + type: number; + stuffData: IObjectData; + extra: number; + category: number; + recyclable: boolean; + isTradable: boolean; + isGroupable: boolean; + sellable: boolean; + locked: boolean; + isWallItem: boolean; +} diff --git a/apps/frontend/src/api/inventory/IPetItem.ts b/apps/frontend/src/api/inventory/IPetItem.ts new file mode 100644 index 0000000..910d5df --- /dev/null +++ b/apps/frontend/src/api/inventory/IPetItem.ts @@ -0,0 +1,6 @@ +import { PetData } from '@nitrots/nitro-renderer'; + +export interface IPetItem +{ + petData: PetData; +} diff --git a/apps/frontend/src/api/inventory/IUnseenItemTracker.ts b/apps/frontend/src/api/inventory/IUnseenItemTracker.ts new file mode 100644 index 0000000..8a70a16 --- /dev/null +++ b/apps/frontend/src/api/inventory/IUnseenItemTracker.ts @@ -0,0 +1,12 @@ +export interface IUnseenItemTracker +{ + dispose(): void; + resetCategory(category: number): boolean; + resetItems(category: number, itemIds: number[]): boolean; + isUnseen(category: number, itemId: number): boolean; + removeUnseen(category: number, itemId: number): boolean; + getIds(category: number): number[]; + getCount(category: number): number; + getFullCount(): number; + addItems(category: number, itemIds: number[]): void; +} diff --git a/apps/frontend/src/api/inventory/InventoryUtilities.ts b/apps/frontend/src/api/inventory/InventoryUtilities.ts new file mode 100644 index 0000000..e263b8c --- /dev/null +++ b/apps/frontend/src/api/inventory/InventoryUtilities.ts @@ -0,0 +1,117 @@ +import { FurniturePlacePaintComposer, RoomObjectCategory, RoomObjectPlacementSource, RoomObjectType } from '@nitrots/nitro-renderer'; +import { CreateLinkEvent, GetRoomEngine, GetRoomSessionManager, SendMessageComposer } from '../nitro'; +import { FurniCategory } from './FurniCategory'; +import { GroupItem } from './GroupItem'; +import { IBotItem } from './IBotItem'; +import { IPetItem } from './IPetItem'; + +let objectMoverRequested = false; +let itemIdInPlacing = -1; + +export const isObjectMoverRequested = () => objectMoverRequested; + +export const setObjectMoverRequested = (flag: boolean) => objectMoverRequested = flag; + +export const getPlacingItemId = () => itemIdInPlacing; + +export const setPlacingItemId = (id: number) => (itemIdInPlacing = id); + +export const cancelRoomObjectPlacement = () => +{ + if(getPlacingItemId() === -1) return; + + GetRoomEngine().cancelRoomObjectPlacement(); + + setPlacingItemId(-1); + setObjectMoverRequested(false); +} + +export const attemptPetPlacement = (petItem: IPetItem, flag: boolean = false) => +{ + const petData = petItem.petData; + + if(!petData) return false; + + const session = GetRoomSessionManager().getSession(1); + + if(!session) return false; + + if(!session.isRoomOwner && !session.allowPets) return false; + + CreateLinkEvent('inventory/hide'); + + if(GetRoomEngine().processRoomObjectPlacement(RoomObjectPlacementSource.INVENTORY, -(petData.id), RoomObjectCategory.UNIT, RoomObjectType.PET, petData.figureData.figuredata)) + { + setPlacingItemId(petData.id); + setObjectMoverRequested(true); + } + + return true; +} + +export const attemptItemPlacement = (groupItem: GroupItem, flag: boolean = false) => +{ + if(!groupItem || !groupItem.getUnlockedCount()) return false; + + const item = groupItem.getLastItem(); + + if(!item) return false; + + if((item.category === FurniCategory.FLOOR) || (item.category === FurniCategory.WALL_PAPER) || (item.category === FurniCategory.LANDSCAPE)) + { + if(flag) return false; + + SendMessageComposer(new FurniturePlacePaintComposer(item.id)); + + return false; + } + else + { + CreateLinkEvent('inventory/hide'); + + let category = 0; + let isMoving = false; + + if(item.isWallItem) category = RoomObjectCategory.WALL; + else category = RoomObjectCategory.FLOOR; + + if((item.category === FurniCategory.POSTER)) // or external image from furnidata + { + isMoving = GetRoomEngine().processRoomObjectPlacement(RoomObjectPlacementSource.INVENTORY, item.id, category, item.type, item.stuffData.getLegacyString()); + } + else + { + isMoving = GetRoomEngine().processRoomObjectPlacement(RoomObjectPlacementSource.INVENTORY, item.id, category, item.type, item.extra.toString(), item.stuffData); + } + + if(isMoving) + { + setPlacingItemId(item.ref); + setObjectMoverRequested(true); + } + } + + return true; +} + + +export const attemptBotPlacement = (botItem: IBotItem, flag: boolean = false) => +{ + const botData = botItem.botData; + + if(!botData) return false; + + const session = GetRoomSessionManager().getSession(1); + + if(!session || !session.isRoomOwner) return false; + + CreateLinkEvent('inventory/hide'); + + if(GetRoomEngine().processRoomObjectPlacement(RoomObjectPlacementSource.INVENTORY, -(botData.id), RoomObjectCategory.UNIT, RoomObjectType.RENTABLE_BOT, botData.figure)) + { + setPlacingItemId(botData.id); + setObjectMoverRequested(true); + } + + return true; +} diff --git a/apps/frontend/src/api/inventory/PetUtilities.ts b/apps/frontend/src/api/inventory/PetUtilities.ts new file mode 100644 index 0000000..881a09e --- /dev/null +++ b/apps/frontend/src/api/inventory/PetUtilities.ts @@ -0,0 +1,104 @@ +import { PetData } from '@nitrots/nitro-renderer'; +import { CreateLinkEvent } from '../nitro'; +import { cancelRoomObjectPlacement, getPlacingItemId } from './InventoryUtilities'; +import { IPetItem } from './IPetItem'; +import { UnseenItemCategory } from './UnseenItemCategory'; + +export const getAllPetIds = (petItems: IPetItem[]) => petItems.map(item => item.petData.id); + +export const addSinglePetItem = (petData: PetData, set: IPetItem[], unseen: boolean = true) => +{ + const petItem = { petData }; + + if(unseen) + { + //petItem.isUnseen = true; + + set.unshift(petItem); + } + else + { + set.push(petItem); + } + + return petItem; +} + +export const removePetItemById = (id: number, set: IPetItem[]) => +{ + let index = 0; + + while(index < set.length) + { + const petItem = set[index]; + + if(petItem && (petItem.petData.id === id)) + { + if(getPlacingItemId() === petItem.petData.id) + { + cancelRoomObjectPlacement(); + + CreateLinkEvent('inventory/open'); + } + + set.splice(index, 1); + + return petItem; + } + + index++; + } + + return null; +} + +export const processPetFragment = (set: IPetItem[], fragment: Map, isUnseen: (category: number, itemId: number) => boolean) => +{ + const existingIds = getAllPetIds(set); + const addedIds: number[] = []; + const removedIds: number[] = []; + + for(const key of fragment.keys()) (existingIds.indexOf(key) === -1) && addedIds.push(key); + + for(const itemId of existingIds) (!fragment.get(itemId)) && removedIds.push(itemId); + + const emptyExistingSet = (existingIds.length === 0); + + for(const id of removedIds) removePetItemById(id, set); + + for(const id of addedIds) + { + const parser = fragment.get(id); + + if(!parser) continue; + + addSinglePetItem(parser, set, isUnseen(UnseenItemCategory.PET, parser.id)); + } + + return set; +} + +export const mergePetFragments = (fragment: Map, totalFragments: number, fragmentNumber: number, fragments: Map[]) => +{ + if(totalFragments === 1) return fragment; + + fragments[fragmentNumber] = fragment; + + for(const frag of fragments) + { + if(!frag) return null; + } + + const merged: Map = new Map(); + + for(const frag of fragments) + { + for(const [ key, value ] of frag) merged.set(key, value); + + frag.clear(); + } + + fragments = null; + + return merged; +} diff --git a/apps/frontend/src/api/inventory/TradeState.ts b/apps/frontend/src/api/inventory/TradeState.ts new file mode 100644 index 0000000..3df418b --- /dev/null +++ b/apps/frontend/src/api/inventory/TradeState.ts @@ -0,0 +1,10 @@ +export class TradeState +{ + public static TRADING_STATE_READY: number = 0; + public static TRADING_STATE_RUNNING: number = 1; + public static TRADING_STATE_COUNTDOWN: number = 2; + public static TRADING_STATE_CONFIRMING: number = 3; + public static TRADING_STATE_CONFIRMED: number = 4; + public static TRADING_STATE_COMPLETED: number = 5; + public static TRADING_STATE_CANCELLED: number = 6; +} diff --git a/apps/frontend/src/api/inventory/TradeUserData.ts b/apps/frontend/src/api/inventory/TradeUserData.ts new file mode 100644 index 0000000..452c7ee --- /dev/null +++ b/apps/frontend/src/api/inventory/TradeUserData.ts @@ -0,0 +1,15 @@ +import { AdvancedMap } from '@nitrots/nitro-renderer'; +import { GroupItem } from './GroupItem'; + +export class TradeUserData +{ + constructor( + public userId: number = -1, + public userName: string = '', + public userItems: AdvancedMap = new AdvancedMap(), + public itemCount: number = 0, + public creditsCount: number = 0, + public accepts: boolean = false, + public canTrade: boolean = false) + {} +} diff --git a/apps/frontend/src/api/inventory/TradingNotificationType.ts b/apps/frontend/src/api/inventory/TradingNotificationType.ts new file mode 100644 index 0000000..4aed490 --- /dev/null +++ b/apps/frontend/src/api/inventory/TradingNotificationType.ts @@ -0,0 +1,12 @@ +export class TradingNotificationType +{ + public static ALERT_SCAM: number = 0; + public static HOTEL_TRADING_DISABLED = 1; + public static YOU_NOT_ALLOWED: number = 2; + public static THEY_NOT_ALLOWED: number = 4; + public static ROOM_DISABLED: number = 6; + public static YOU_OPEN: number = 7; + public static THEY_OPEN: number = 8; + public static ERROR_WHILE_COMMIT: number = 9; + public static THEY_CANCELLED: number = 10; +} diff --git a/apps/frontend/src/api/inventory/TradingUtilities.ts b/apps/frontend/src/api/inventory/TradingUtilities.ts new file mode 100644 index 0000000..28ca8c8 --- /dev/null +++ b/apps/frontend/src/api/inventory/TradingUtilities.ts @@ -0,0 +1,71 @@ +import { AdvancedMap, IObjectData, ItemDataStructure, StringDataType } from '@nitrots/nitro-renderer'; +import { GetSessionDataManager } from '../nitro'; +import { FurniCategory } from './FurniCategory'; +import { FurnitureItem } from './FurnitureItem'; +import { createGroupItem } from './FurnitureUtilities'; +import { GroupItem } from './GroupItem'; + +const isExternalImage = (spriteId: number) => GetSessionDataManager().getWallItemData(spriteId)?.isExternalImage || false; + +export const parseTradeItems = (items: ItemDataStructure[]) => +{ + const existingItems = new AdvancedMap(); + const totalItems = items.length; + + if(totalItems) + { + for(const item of items) + { + const spriteId = item.spriteId; + const category = item.category; + + let name = (item.furniType + spriteId); + + if(!item.isGroupable || isExternalImage(spriteId)) + { + name = ('itemid' + item.itemId); + } + + if(item.category === FurniCategory.POSTER) + { + name = (item.itemId + 'poster' + item.stuffData.getLegacyString()); + } + + else if(item.category === FurniCategory.GUILD_FURNI) + { + name = ''; + } + + let groupItem = ((item.isGroupable && !isExternalImage(item.spriteId)) ? existingItems.getValue(name) : null); + + if(!groupItem) + { + groupItem = createGroupItem(spriteId, category, item.stuffData); + + existingItems.add(name, groupItem); + } + + groupItem.push(new FurnitureItem(item)); + } + } + + return existingItems; +} + +export const getGuildFurniType = (spriteId: number, stuffData: IObjectData) => +{ + let type = spriteId.toString(); + + if(!(stuffData instanceof StringDataType)) return type; + + let i = 1; + + while(i < 5) + { + type = (type + (',' + stuffData.getValue(i))); + + i++; + } + + return type; +} diff --git a/apps/frontend/src/api/inventory/UnseenItemCategory.ts b/apps/frontend/src/api/inventory/UnseenItemCategory.ts new file mode 100644 index 0000000..cbd7e9b --- /dev/null +++ b/apps/frontend/src/api/inventory/UnseenItemCategory.ts @@ -0,0 +1,9 @@ +export class UnseenItemCategory +{ + public static FURNI: number = 1; + public static RENTABLE: number = 2; + public static PET: number = 3; + public static BADGE: number = 4; + public static BOT: number = 5; + public static GAMES: number = 6; +} diff --git a/apps/frontend/src/api/inventory/index.ts b/apps/frontend/src/api/inventory/index.ts new file mode 100644 index 0000000..76962cf --- /dev/null +++ b/apps/frontend/src/api/inventory/index.ts @@ -0,0 +1,15 @@ +export * from './FurniCategory'; +export * from './FurnitureItem'; +export * from './FurnitureUtilities'; +export * from './GroupItem'; +export * from './IBotItem'; +export * from './IFurnitureItem'; +export * from './InventoryUtilities'; +export * from './IPetItem'; +export * from './IUnseenItemTracker'; +export * from './PetUtilities'; +export * from './TradeState'; +export * from './TradeUserData'; +export * from './TradingNotificationType'; +export * from './TradingUtilities'; +export * from './UnseenItemCategory'; diff --git a/apps/frontend/src/api/mod-tools/GetIssueCategoryName.ts b/apps/frontend/src/api/mod-tools/GetIssueCategoryName.ts new file mode 100644 index 0000000..81a3f86 --- /dev/null +++ b/apps/frontend/src/api/mod-tools/GetIssueCategoryName.ts @@ -0,0 +1,35 @@ +export const GetIssueCategoryName = (categoryId: number) => +{ + switch(categoryId) + { + case 1: + case 2: + return 'Normal'; + case 3: + return 'Automatic'; + case 4: + return 'Automatic IM'; + case 5: + return 'Guide System'; + case 6: + return 'IM'; + case 7: + return 'Room'; + case 8: + return 'Panic'; + case 9: + return 'Guardian'; + case 10: + return 'Automatic Helper'; + case 11: + return 'Discussion'; + case 12: + return 'Selfie'; + case 14: + return 'Photo'; + case 15: + return 'Ambassador'; + } + + return 'Unknown'; +} diff --git a/apps/frontend/src/api/mod-tools/ISelectedUser.ts b/apps/frontend/src/api/mod-tools/ISelectedUser.ts new file mode 100644 index 0000000..4f6e76b --- /dev/null +++ b/apps/frontend/src/api/mod-tools/ISelectedUser.ts @@ -0,0 +1,5 @@ +export interface ISelectedUser +{ + userId: number; + username: string; +} diff --git a/apps/frontend/src/api/mod-tools/IUserInfo.ts b/apps/frontend/src/api/mod-tools/IUserInfo.ts new file mode 100644 index 0000000..8d49aa7 --- /dev/null +++ b/apps/frontend/src/api/mod-tools/IUserInfo.ts @@ -0,0 +1,6 @@ +export interface IUserInfo +{ + nameKey: string; + nameKeyFallback: string; + value: string; +} diff --git a/apps/frontend/src/api/mod-tools/ModActionDefinition.ts b/apps/frontend/src/api/mod-tools/ModActionDefinition.ts new file mode 100644 index 0000000..b28aa9c --- /dev/null +++ b/apps/frontend/src/api/mod-tools/ModActionDefinition.ts @@ -0,0 +1,49 @@ +export class ModActionDefinition +{ + public static ALERT: number = 1; + public static MUTE: number = 2; + public static BAN: number = 3; + public static KICK: number = 4; + public static TRADE_LOCK: number = 5; + public static MESSAGE: number = 6; + + private readonly _actionId: number; + private readonly _name: string; + private readonly _actionType: number; + private readonly _sanctionTypeId: number; + private readonly _actionLengthHours: number; + + constructor(actionId: number, actionName: string, actionType: number, sanctionTypeId: number, actionLengthHours:number) + { + this._actionId = actionId; + this._name = actionName; + this._actionType = actionType; + this._sanctionTypeId = sanctionTypeId; + this._actionLengthHours = actionLengthHours; + } + + public get actionId(): number + { + return this._actionId; + } + + public get name(): string + { + return this._name; + } + + public get actionType(): number + { + return this._actionType; + } + + public get sanctionTypeId(): number + { + return this._sanctionTypeId; + } + + public get actionLengthHours(): number + { + return this._actionLengthHours; + } +} diff --git a/apps/frontend/src/api/mod-tools/index.ts b/apps/frontend/src/api/mod-tools/index.ts new file mode 100644 index 0000000..004bbaa --- /dev/null +++ b/apps/frontend/src/api/mod-tools/index.ts @@ -0,0 +1,4 @@ +export * from './GetIssueCategoryName'; +export * from './ISelectedUser'; +export * from './IUserInfo'; +export * from './ModActionDefinition'; diff --git a/apps/frontend/src/api/navigator/DoorStateType.ts b/apps/frontend/src/api/navigator/DoorStateType.ts new file mode 100644 index 0000000..1f8a8ef --- /dev/null +++ b/apps/frontend/src/api/navigator/DoorStateType.ts @@ -0,0 +1,12 @@ +export class DoorStateType +{ + public static NONE: number = 0; + public static START_DOORBELL: number = 1; + public static START_PASSWORD: number = 2; + public static STATE_PENDING_SERVER: number = 3; + public static UPDATE_STATE: number = 4; + public static STATE_WAITING: number = 5; + public static STATE_NO_ANSWER: number = 6; + public static STATE_WRONG_PASSWORD: number = 7; + public static STATE_ACCEPTED: number = 8; +} diff --git a/apps/frontend/src/api/navigator/INavigatorData.ts b/apps/frontend/src/api/navigator/INavigatorData.ts new file mode 100644 index 0000000..e50b6fe --- /dev/null +++ b/apps/frontend/src/api/navigator/INavigatorData.ts @@ -0,0 +1,17 @@ +import { RoomDataParser } from '@nitrots/nitro-renderer'; + +export interface INavigatorData +{ + homeRoomId: number; + settingsReceived: boolean; + enteredGuestRoom: RoomDataParser; + currentRoomOwner: boolean; + currentRoomId: number; + currentRoomIsStaffPick: boolean; + createdFlatId: number; + avatarId: number; + roomPicker: boolean; + eventMod: boolean; + currentRoomRating: number; + canRate: boolean; +} diff --git a/apps/frontend/src/api/navigator/INavigatorSearchFilter.ts b/apps/frontend/src/api/navigator/INavigatorSearchFilter.ts new file mode 100644 index 0000000..179d5d5 --- /dev/null +++ b/apps/frontend/src/api/navigator/INavigatorSearchFilter.ts @@ -0,0 +1,5 @@ +export interface INavigatorSearchFilter +{ + name: string; + query: string; +} diff --git a/apps/frontend/src/api/navigator/IRoomChatSettings.ts b/apps/frontend/src/api/navigator/IRoomChatSettings.ts new file mode 100644 index 0000000..aee426c --- /dev/null +++ b/apps/frontend/src/api/navigator/IRoomChatSettings.ts @@ -0,0 +1,8 @@ +export interface IRoomChatSettings +{ + mode: number; + weight: number; + speed: number; + distance: number; + protection: number; +} diff --git a/apps/frontend/src/api/navigator/IRoomData.ts b/apps/frontend/src/api/navigator/IRoomData.ts new file mode 100644 index 0000000..9146314 --- /dev/null +++ b/apps/frontend/src/api/navigator/IRoomData.ts @@ -0,0 +1,23 @@ +import { IRoomChatSettings } from './IRoomChatSettings'; +import { IRoomModerationSettings } from './IRoomModerationSettings'; + +export interface IRoomData +{ + roomId: number; + roomName: string; + roomDescription: string; + categoryId: number; + userCount: number; + tags: string[]; + tradeState: number; + allowWalkthrough: boolean; + lockState: number; + password: string; + allowPets: boolean; + allowPetsEat: boolean; + hideWalls: boolean; + wallThickness: number; + floorThickness: number; + chatSettings: IRoomChatSettings; + moderationSettings: IRoomModerationSettings; +} diff --git a/apps/frontend/src/api/navigator/IRoomModel.ts b/apps/frontend/src/api/navigator/IRoomModel.ts new file mode 100644 index 0000000..73dfe27 --- /dev/null +++ b/apps/frontend/src/api/navigator/IRoomModel.ts @@ -0,0 +1,6 @@ +export interface IRoomModel +{ + clubLevel: number; + tileSize: number; + name: string; +} diff --git a/apps/frontend/src/api/navigator/IRoomModerationSettings.ts b/apps/frontend/src/api/navigator/IRoomModerationSettings.ts new file mode 100644 index 0000000..266fe47 --- /dev/null +++ b/apps/frontend/src/api/navigator/IRoomModerationSettings.ts @@ -0,0 +1,6 @@ +export interface IRoomModerationSettings +{ + allowMute: number; + allowKick: number; + allowBan: number; +} diff --git a/apps/frontend/src/api/navigator/NavigatorSearchResultViewDisplayMode.ts b/apps/frontend/src/api/navigator/NavigatorSearchResultViewDisplayMode.ts new file mode 100644 index 0000000..b532d1a --- /dev/null +++ b/apps/frontend/src/api/navigator/NavigatorSearchResultViewDisplayMode.ts @@ -0,0 +1,6 @@ +export class NavigatorSearchResultViewDisplayMode +{ + public static readonly LIST: number = 0; + public static readonly THUMBNAILS: number = 1; + public static readonly FORCED_THUMBNAILS: number = 2; +} diff --git a/apps/frontend/src/api/navigator/RoomInfoData.ts b/apps/frontend/src/api/navigator/RoomInfoData.ts new file mode 100644 index 0000000..fc0a93b --- /dev/null +++ b/apps/frontend/src/api/navigator/RoomInfoData.ts @@ -0,0 +1,60 @@ +import { RoomDataParser } from '@nitrots/nitro-renderer'; + +export class RoomInfoData +{ + private _enteredGuestRoom: RoomDataParser = null; + private _createdRoomId: number = 0; + private _currentRoomId: number = 0; + private _currentRoomOwner: boolean = false; + private _canRate: boolean = false; + + public get enteredGuestRoom(): RoomDataParser + { + return this._enteredGuestRoom; + } + + public set enteredGuestRoom(data: RoomDataParser) + { + this._enteredGuestRoom = data; + } + + public get createdRoomId(): number + { + return this._createdRoomId; + } + + public set createdRoomId(id: number) + { + this._createdRoomId = id; + } + + public get currentRoomId(): number + { + return this._currentRoomId; + } + + public set currentRoomId(id: number) + { + this._currentRoomId = id; + } + + public get currentRoomOwner(): boolean + { + return this._currentRoomOwner; + } + + public set currentRoomOwner(flag: boolean) + { + this._currentRoomOwner = flag; + } + + public get canRate(): boolean + { + return this._canRate; + } + + public set canRate(flag: boolean) + { + this._canRate = flag; + } +} diff --git a/apps/frontend/src/api/navigator/RoomSettingsUtils.ts b/apps/frontend/src/api/navigator/RoomSettingsUtils.ts new file mode 100644 index 0000000..bc611da --- /dev/null +++ b/apps/frontend/src/api/navigator/RoomSettingsUtils.ts @@ -0,0 +1,10 @@ +const BuildMaxVisitorsList = () => +{ + const list: number[] = []; + + for(let i = 10; i <= 100; i = i + 10) list.push(i); + + return list; +} + +export const GetMaxVisitorsList = BuildMaxVisitorsList(); diff --git a/apps/frontend/src/api/navigator/SearchFilterOptions.ts b/apps/frontend/src/api/navigator/SearchFilterOptions.ts new file mode 100644 index 0000000..aaf1290 --- /dev/null +++ b/apps/frontend/src/api/navigator/SearchFilterOptions.ts @@ -0,0 +1,24 @@ +import { INavigatorSearchFilter } from './INavigatorSearchFilter'; + +export const SearchFilterOptions: INavigatorSearchFilter[] = [ + { + name: 'anything', + query: null + }, + { + name: 'room.name', + query: 'roomname' + }, + { + name: 'owner', + query: 'owner' + }, + { + name: 'tag', + query: 'tag' + }, + { + name: 'group', + query: 'group' + } +]; diff --git a/apps/frontend/src/api/navigator/TryVisitRoom.ts b/apps/frontend/src/api/navigator/TryVisitRoom.ts new file mode 100644 index 0000000..81138d6 --- /dev/null +++ b/apps/frontend/src/api/navigator/TryVisitRoom.ts @@ -0,0 +1,7 @@ +import { GetGuestRoomMessageComposer } from '@nitrots/nitro-renderer'; +import { SendMessageComposer } from '../nitro'; + +export function TryVisitRoom(roomId: number): void +{ + SendMessageComposer(new GetGuestRoomMessageComposer(roomId, false, true)); +} diff --git a/apps/frontend/src/api/navigator/index.ts b/apps/frontend/src/api/navigator/index.ts new file mode 100644 index 0000000..bceb33e --- /dev/null +++ b/apps/frontend/src/api/navigator/index.ts @@ -0,0 +1,12 @@ +export * from './DoorStateType'; +export * from './INavigatorData'; +export * from './INavigatorSearchFilter'; +export * from './IRoomChatSettings'; +export * from './IRoomData'; +export * from './IRoomModel'; +export * from './IRoomModerationSettings'; +export * from './NavigatorSearchResultViewDisplayMode'; +export * from './RoomInfoData'; +export * from './RoomSettingsUtils'; +export * from './SearchFilterOptions'; +export * from './TryVisitRoom'; diff --git a/apps/frontend/src/api/nitro/AddLinkEventTracker.ts b/apps/frontend/src/api/nitro/AddLinkEventTracker.ts new file mode 100644 index 0000000..8b9f00f --- /dev/null +++ b/apps/frontend/src/api/nitro/AddLinkEventTracker.ts @@ -0,0 +1,7 @@ +import { ILinkEventTracker } from '@nitrots/nitro-renderer'; +import { GetNitroInstance } from './GetNitroInstance'; + +export function AddEventLinkTracker(tracker: ILinkEventTracker): void +{ + GetNitroInstance().addLinkEventTracker(tracker); +} diff --git a/apps/frontend/src/api/nitro/CreateLinkEvent.ts b/apps/frontend/src/api/nitro/CreateLinkEvent.ts new file mode 100644 index 0000000..2acfa86 --- /dev/null +++ b/apps/frontend/src/api/nitro/CreateLinkEvent.ts @@ -0,0 +1,8 @@ +import { GetNitroInstance } from './GetNitroInstance'; + +export function CreateLinkEvent(link: string): void +{ + link = (link.startsWith('event:') ? link.substring(6) : link); + + GetNitroInstance().createLinkEvent(link); +} diff --git a/apps/frontend/src/api/nitro/GetCommunication.ts b/apps/frontend/src/api/nitro/GetCommunication.ts new file mode 100644 index 0000000..5dc5761 --- /dev/null +++ b/apps/frontend/src/api/nitro/GetCommunication.ts @@ -0,0 +1,7 @@ +import { INitroCommunicationManager } from '@nitrots/nitro-renderer'; +import { GetNitroInstance } from './GetNitroInstance'; + +export function GetCommunication(): INitroCommunicationManager +{ + return GetNitroInstance()?.communication; +} diff --git a/apps/frontend/src/api/nitro/GetConfiguration.ts b/apps/frontend/src/api/nitro/GetConfiguration.ts new file mode 100644 index 0000000..800d1f1 --- /dev/null +++ b/apps/frontend/src/api/nitro/GetConfiguration.ts @@ -0,0 +1,6 @@ +import { NitroConfiguration } from '@nitrots/nitro-renderer'; + +export function GetConfiguration(key: string, value: T = null): T +{ + return NitroConfiguration.getValue(key, value); +} diff --git a/apps/frontend/src/api/nitro/GetConnection.ts b/apps/frontend/src/api/nitro/GetConnection.ts new file mode 100644 index 0000000..ec39d8d --- /dev/null +++ b/apps/frontend/src/api/nitro/GetConnection.ts @@ -0,0 +1,7 @@ +import { IConnection } from '@nitrots/nitro-renderer'; +import { GetCommunication } from './GetCommunication'; + +export function GetConnection(): IConnection +{ + return GetCommunication()?.connection; +} diff --git a/apps/frontend/src/api/nitro/GetLocalization.ts b/apps/frontend/src/api/nitro/GetLocalization.ts new file mode 100644 index 0000000..7f105e6 --- /dev/null +++ b/apps/frontend/src/api/nitro/GetLocalization.ts @@ -0,0 +1,7 @@ +import { INitroLocalizationManager } from '@nitrots/nitro-renderer'; +import { GetNitroInstance } from './GetNitroInstance'; + +export function GetLocalization(): INitroLocalizationManager +{ + return GetNitroInstance().localization; +} diff --git a/apps/frontend/src/api/nitro/GetNitroInstance.ts b/apps/frontend/src/api/nitro/GetNitroInstance.ts new file mode 100644 index 0000000..5e64a65 --- /dev/null +++ b/apps/frontend/src/api/nitro/GetNitroInstance.ts @@ -0,0 +1,6 @@ +import { INitro, Nitro } from '@nitrots/nitro-renderer'; + +export function GetNitroInstance(): INitro +{ + return Nitro.instance; +} diff --git a/apps/frontend/src/api/nitro/OpenUrl.ts b/apps/frontend/src/api/nitro/OpenUrl.ts new file mode 100644 index 0000000..096776d --- /dev/null +++ b/apps/frontend/src/api/nitro/OpenUrl.ts @@ -0,0 +1,16 @@ +import { HabboWebTools } from '@nitrots/nitro-renderer'; +import { CreateLinkEvent } from './CreateLinkEvent'; + +export const OpenUrl = (url: string) => +{ + if(!url || !url.length) return; + + if(url.startsWith('http')) + { + HabboWebTools.openWebPage(url); + } + else + { + CreateLinkEvent(url); + } +} diff --git a/apps/frontend/src/api/nitro/RemoveLinkEventTracker.ts b/apps/frontend/src/api/nitro/RemoveLinkEventTracker.ts new file mode 100644 index 0000000..d551c69 --- /dev/null +++ b/apps/frontend/src/api/nitro/RemoveLinkEventTracker.ts @@ -0,0 +1,7 @@ +import { ILinkEventTracker } from '@nitrots/nitro-renderer'; +import { GetNitroInstance } from './GetNitroInstance'; + +export function RemoveLinkEventTracker(tracker: ILinkEventTracker): void +{ + GetNitroInstance().removeLinkEventTracker(tracker); +} diff --git a/apps/frontend/src/api/nitro/SendMessageComposer.ts b/apps/frontend/src/api/nitro/SendMessageComposer.ts new file mode 100644 index 0000000..dd54c02 --- /dev/null +++ b/apps/frontend/src/api/nitro/SendMessageComposer.ts @@ -0,0 +1,4 @@ +import { IMessageComposer } from '@nitrots/nitro-renderer'; +import { GetConnection } from './GetConnection'; + +export const SendMessageComposer = (event: IMessageComposer) => GetConnection().send(event); diff --git a/apps/frontend/src/api/nitro/avatar/GetAvatarPalette.ts b/apps/frontend/src/api/nitro/avatar/GetAvatarPalette.ts new file mode 100644 index 0000000..a46acb2 --- /dev/null +++ b/apps/frontend/src/api/nitro/avatar/GetAvatarPalette.ts @@ -0,0 +1,7 @@ +import { IPalette } from '@nitrots/nitro-renderer'; +import { GetAvatarRenderManager } from './GetAvatarRenderManager'; + +export function GetAvatarPalette(paletteId: number): IPalette +{ + return GetAvatarRenderManager().structureData.getPalette(paletteId); +} diff --git a/apps/frontend/src/api/nitro/avatar/GetAvatarRenderManager.ts b/apps/frontend/src/api/nitro/avatar/GetAvatarRenderManager.ts new file mode 100644 index 0000000..460f010 --- /dev/null +++ b/apps/frontend/src/api/nitro/avatar/GetAvatarRenderManager.ts @@ -0,0 +1,7 @@ +import { IAvatarRenderManager } from '@nitrots/nitro-renderer'; +import { GetNitroInstance } from '../GetNitroInstance'; + +export function GetAvatarRenderManager(): IAvatarRenderManager +{ + return GetNitroInstance().avatar; +} diff --git a/apps/frontend/src/api/nitro/avatar/GetAvatarSetType.ts b/apps/frontend/src/api/nitro/avatar/GetAvatarSetType.ts new file mode 100644 index 0000000..ad95f44 --- /dev/null +++ b/apps/frontend/src/api/nitro/avatar/GetAvatarSetType.ts @@ -0,0 +1,7 @@ +import { ISetType } from '@nitrots/nitro-renderer'; +import { GetAvatarRenderManager } from './GetAvatarRenderManager'; + +export function GetAvatarSetType(setType: string): ISetType +{ + return GetAvatarRenderManager().structureData.getSetType(setType); +} diff --git a/apps/frontend/src/api/nitro/avatar/index.ts b/apps/frontend/src/api/nitro/avatar/index.ts new file mode 100644 index 0000000..258a1ce --- /dev/null +++ b/apps/frontend/src/api/nitro/avatar/index.ts @@ -0,0 +1,3 @@ +export * from './GetAvatarPalette'; +export * from './GetAvatarRenderManager'; +export * from './GetAvatarSetType'; diff --git a/apps/frontend/src/api/nitro/camera/GetRoomCameraWidgetManager.ts b/apps/frontend/src/api/nitro/camera/GetRoomCameraWidgetManager.ts new file mode 100644 index 0000000..392495d --- /dev/null +++ b/apps/frontend/src/api/nitro/camera/GetRoomCameraWidgetManager.ts @@ -0,0 +1,7 @@ +import { IRoomCameraWidgetManager } from '@nitrots/nitro-renderer'; +import { GetNitroInstance } from '../GetNitroInstance'; + +export function GetRoomCameraWidgetManager(): IRoomCameraWidgetManager +{ + return GetNitroInstance().cameraManager; +} diff --git a/apps/frontend/src/api/nitro/camera/index.ts b/apps/frontend/src/api/nitro/camera/index.ts new file mode 100644 index 0000000..3c2707c --- /dev/null +++ b/apps/frontend/src/api/nitro/camera/index.ts @@ -0,0 +1 @@ +export * from './GetRoomCameraWidgetManager'; diff --git a/apps/frontend/src/api/nitro/core/GetConfigurationManager.ts b/apps/frontend/src/api/nitro/core/GetConfigurationManager.ts new file mode 100644 index 0000000..66ce153 --- /dev/null +++ b/apps/frontend/src/api/nitro/core/GetConfigurationManager.ts @@ -0,0 +1,7 @@ +import { IConfigurationManager } from '@nitrots/nitro-renderer'; +import { GetNitroCore } from './GetNitroCore'; + +export function GetConfigurationManager(): IConfigurationManager +{ + return GetNitroCore().configuration; +} diff --git a/apps/frontend/src/api/nitro/core/GetNitroCore.ts b/apps/frontend/src/api/nitro/core/GetNitroCore.ts new file mode 100644 index 0000000..ef34b66 --- /dev/null +++ b/apps/frontend/src/api/nitro/core/GetNitroCore.ts @@ -0,0 +1,7 @@ +import { INitroCore } from '@nitrots/nitro-renderer'; +import { GetNitroInstance } from '..'; + +export function GetNitroCore(): INitroCore +{ + return GetNitroInstance().core; +} diff --git a/apps/frontend/src/api/nitro/core/index.ts b/apps/frontend/src/api/nitro/core/index.ts new file mode 100644 index 0000000..3322c9c --- /dev/null +++ b/apps/frontend/src/api/nitro/core/index.ts @@ -0,0 +1,2 @@ +export * from './GetConfigurationManager'; +export * from './GetNitroCore'; diff --git a/apps/frontend/src/api/nitro/index.ts b/apps/frontend/src/api/nitro/index.ts new file mode 100644 index 0000000..c43e958 --- /dev/null +++ b/apps/frontend/src/api/nitro/index.ts @@ -0,0 +1,15 @@ +export * from './AddLinkEventTracker'; +export * from './avatar'; +export * from './camera'; +export * from './core'; +export * from './CreateLinkEvent'; +export * from './GetCommunication'; +export * from './GetConfiguration'; +export * from './GetConnection'; +export * from './GetLocalization'; +export * from './GetNitroInstance'; +export * from './OpenUrl'; +export * from './RemoveLinkEventTracker'; +export * from './room'; +export * from './SendMessageComposer'; +export * from './session'; diff --git a/apps/frontend/src/api/nitro/room/DispatchMouseEvent.ts b/apps/frontend/src/api/nitro/room/DispatchMouseEvent.ts new file mode 100644 index 0000000..51111ac --- /dev/null +++ b/apps/frontend/src/api/nitro/room/DispatchMouseEvent.ts @@ -0,0 +1,55 @@ +import { MouseEventType } from '@nitrots/nitro-renderer'; +import { GetRoomEngine } from './GetRoomEngine'; + +let didMouseMove = false; +let lastClick = 0; +let clickCount = 0; + +export const DispatchMouseEvent = (event: MouseEvent, canvasId: number = 1) => +{ + const x = event.clientX; + const y = event.clientY; + + let eventType = event.type; + + if(eventType === MouseEventType.MOUSE_CLICK) + { + if(lastClick) + { + clickCount = 1; + + if(lastClick >= Date.now() - 300) clickCount++; + } + + lastClick = Date.now(); + + if(clickCount === 2) + { + if(!didMouseMove) eventType = MouseEventType.DOUBLE_CLICK; + + clickCount = 0; + lastClick = null; + } + } + + switch(eventType) + { + case MouseEventType.MOUSE_CLICK: + break; + case MouseEventType.DOUBLE_CLICK: + break; + case MouseEventType.MOUSE_MOVE: + didMouseMove = true; + break; + case MouseEventType.MOUSE_DOWN: + didMouseMove = false; + break; + case MouseEventType.MOUSE_UP: + break; + case MouseEventType.RIGHT_CLICK: + break; + default: return; + } + + GetRoomEngine().dispatchMouseEvent(canvasId, x, y, eventType, event.altKey, (event.ctrlKey || event.metaKey), event.shiftKey, false); +} diff --git a/apps/frontend/src/api/nitro/room/DispatchTouchEvent.ts b/apps/frontend/src/api/nitro/room/DispatchTouchEvent.ts new file mode 100644 index 0000000..5017544 --- /dev/null +++ b/apps/frontend/src/api/nitro/room/DispatchTouchEvent.ts @@ -0,0 +1,82 @@ +import { MouseEventType, TouchEventType } from '@nitrots/nitro-renderer'; +import { GetRoomEngine } from './GetRoomEngine'; + +let didMouseMove = false; +let lastClick = 0; +let clickCount = 0; + +export const DispatchTouchEvent = (event: TouchEvent, canvasId: number = 1, longTouch: boolean = false, altKey: boolean = false, ctrlKey: boolean = false, shiftKey: boolean = false) => +{ + let x = 0; + let y = 0; + + if(event.touches[0]) + { + x = event.touches[0].clientX; + y = event.touches[0].clientY; + } + + else if(event.changedTouches[0]) + { + x = event.changedTouches[0].clientX; + y = event.changedTouches[0].clientY; + } + + let eventType = event.type; + + if(longTouch) eventType = TouchEventType.TOUCH_LONG; + + if(eventType === MouseEventType.MOUSE_CLICK || eventType === TouchEventType.TOUCH_END) + { + eventType = MouseEventType.MOUSE_CLICK; + + if(lastClick) + { + clickCount = 1; + + if(lastClick >= (Date.now() - 300)) clickCount++; + } + + lastClick = Date.now(); + + if(clickCount === 2) + { + if(!didMouseMove) eventType = MouseEventType.DOUBLE_CLICK; + + clickCount = 0; + lastClick = null; + } + } + + switch(eventType) + { + case MouseEventType.MOUSE_CLICK: + break; + case MouseEventType.DOUBLE_CLICK: + break; + case TouchEventType.TOUCH_START: + eventType = MouseEventType.MOUSE_DOWN; + + didMouseMove = false; + break; + case TouchEventType.TOUCH_MOVE: + eventType = MouseEventType.MOUSE_MOVE; + + didMouseMove = true; + break; + case TouchEventType.TOUCH_END: + eventType = MouseEventType.MOUSE_UP; + break; + case TouchEventType.TOUCH_LONG: + eventType = MouseEventType.MOUSE_DOWN_LONG; + break; + default: return; + } + + if (eventType === TouchEventType.TOUCH_START) + { + GetRoomEngine().dispatchMouseEvent(canvasId, x, y, eventType, altKey, ctrlKey, shiftKey, false); + } + + GetRoomEngine().dispatchMouseEvent(canvasId, x, y, eventType, altKey, ctrlKey, shiftKey, false); +} diff --git a/apps/frontend/src/api/nitro/room/GetOwnRoomObject.ts b/apps/frontend/src/api/nitro/room/GetOwnRoomObject.ts new file mode 100644 index 0000000..b8d0364 --- /dev/null +++ b/apps/frontend/src/api/nitro/room/GetOwnRoomObject.ts @@ -0,0 +1,32 @@ +import { IRoomObjectController, RoomObjectCategory } from '@nitrots/nitro-renderer'; +import { GetRoomSession, GetSessionDataManager } from '../session'; +import { GetRoomEngine } from './GetRoomEngine'; + +export function GetOwnRoomObject(): IRoomObjectController +{ + const userId = GetSessionDataManager().userId; + const roomId = GetRoomEngine().activeRoomId; + const category = RoomObjectCategory.UNIT; + const totalObjects = GetRoomEngine().getTotalObjectsForManager(roomId, category); + + let i = 0; + + while(i < totalObjects) + { + const roomObject = GetRoomEngine().getRoomObjectByIndex(roomId, i, category); + + if(roomObject) + { + const userData = GetRoomSession().userDataManager.getUserDataByIndex(roomObject.id); + + if(userData) + { + if(userData.webID === userId) return roomObject; + } + } + + i++; + } + + return null; +} diff --git a/apps/frontend/src/api/nitro/room/GetRoomEngine.ts b/apps/frontend/src/api/nitro/room/GetRoomEngine.ts new file mode 100644 index 0000000..4d18ea7 --- /dev/null +++ b/apps/frontend/src/api/nitro/room/GetRoomEngine.ts @@ -0,0 +1,7 @@ +import { IRoomEngine } from '@nitrots/nitro-renderer'; +import { GetNitroInstance } from '../GetNitroInstance'; + +export function GetRoomEngine(): IRoomEngine +{ + return GetNitroInstance().roomEngine; +} diff --git a/apps/frontend/src/api/nitro/room/GetRoomObjectBounds.ts b/apps/frontend/src/api/nitro/room/GetRoomObjectBounds.ts new file mode 100644 index 0000000..0a42ad6 --- /dev/null +++ b/apps/frontend/src/api/nitro/room/GetRoomObjectBounds.ts @@ -0,0 +1,13 @@ +import { GetRoomEngine } from './GetRoomEngine'; + +export const GetRoomObjectBounds = (roomId: number, objectId: number, category: number, canvasId = 1) => +{ + const rectangle = GetRoomEngine().getRoomObjectBoundingRectangle(roomId, objectId, category, canvasId); + + if(!rectangle) return null; + + rectangle.x = Math.round(rectangle.x); + rectangle.y = Math.round(rectangle.y); + + return rectangle; +} diff --git a/apps/frontend/src/api/nitro/room/GetRoomObjectScreenLocation.ts b/apps/frontend/src/api/nitro/room/GetRoomObjectScreenLocation.ts new file mode 100644 index 0000000..1a8d973 --- /dev/null +++ b/apps/frontend/src/api/nitro/room/GetRoomObjectScreenLocation.ts @@ -0,0 +1,13 @@ +import { GetRoomEngine } from './GetRoomEngine'; + +export const GetRoomObjectScreenLocation = (roomId: number, objectId: number, category: number, canvasId = 1) => +{ + const point = GetRoomEngine().getRoomObjectScreenLocation(roomId, objectId, category, canvasId); + + if(!point) return null; + + point.x = Math.round(point.x); + point.y = Math.round(point.y); + + return point; +} diff --git a/apps/frontend/src/api/nitro/room/InitializeRoomInstanceRenderingCanvas.ts b/apps/frontend/src/api/nitro/room/InitializeRoomInstanceRenderingCanvas.ts new file mode 100644 index 0000000..d85d739 --- /dev/null +++ b/apps/frontend/src/api/nitro/room/InitializeRoomInstanceRenderingCanvas.ts @@ -0,0 +1,9 @@ +import { GetRoomEngine } from './GetRoomEngine'; + +export const InitializeRoomInstanceRenderingCanvas = (width: number, height: number, canvasId: number = 1) => +{ + const roomEngine = GetRoomEngine(); + const roomId = roomEngine.activeRoomId; + + roomEngine.initializeRoomInstanceRenderingCanvas(roomId, canvasId, width, height); +} diff --git a/apps/frontend/src/api/nitro/room/IsFurnitureSelectionDisabled.ts b/apps/frontend/src/api/nitro/room/IsFurnitureSelectionDisabled.ts new file mode 100644 index 0000000..367f389 --- /dev/null +++ b/apps/frontend/src/api/nitro/room/IsFurnitureSelectionDisabled.ts @@ -0,0 +1,24 @@ +import { RoomEngineObjectEvent, RoomObjectVariable } from '@nitrots/nitro-renderer'; +import { GetSessionDataManager } from '../..'; +import { GetRoomEngine } from './GetRoomEngine'; + +export function IsFurnitureSelectionDisabled(event: RoomEngineObjectEvent): boolean +{ + let result = false; + + const roomObject = GetRoomEngine().getRoomObject(event.roomId, event.objectId, event.category); + + if(roomObject) + { + const selectionDisabled = (roomObject.model.getValue(RoomObjectVariable.FURNITURE_SELECTION_DISABLED) === 1); + + if(selectionDisabled) + { + result = true; + + if(GetSessionDataManager().isModerator) result = false; + } + } + + return result; +} diff --git a/apps/frontend/src/api/nitro/room/ProcessRoomObjectOperation.ts b/apps/frontend/src/api/nitro/room/ProcessRoomObjectOperation.ts new file mode 100644 index 0000000..b9187fa --- /dev/null +++ b/apps/frontend/src/api/nitro/room/ProcessRoomObjectOperation.ts @@ -0,0 +1,6 @@ +import { GetRoomEngine } from './GetRoomEngine'; + +export function ProcessRoomObjectOperation(objectId: number, category: number, operation: string): void +{ + GetRoomEngine().processRoomObjectOperation(objectId, category, operation); +} diff --git a/apps/frontend/src/api/nitro/room/SetActiveRoomId.ts b/apps/frontend/src/api/nitro/room/SetActiveRoomId.ts new file mode 100644 index 0000000..2cccd84 --- /dev/null +++ b/apps/frontend/src/api/nitro/room/SetActiveRoomId.ts @@ -0,0 +1,6 @@ +import { GetRoomEngine } from './GetRoomEngine'; + +export function SetActiveRoomId(roomId: number): void +{ + GetRoomEngine().setActiveRoomId(roomId); +} diff --git a/apps/frontend/src/api/nitro/room/index.ts b/apps/frontend/src/api/nitro/room/index.ts new file mode 100644 index 0000000..764b72e --- /dev/null +++ b/apps/frontend/src/api/nitro/room/index.ts @@ -0,0 +1,10 @@ +export * from './DispatchMouseEvent'; +export * from './DispatchTouchEvent'; +export * from './GetOwnRoomObject'; +export * from './GetRoomEngine'; +export * from './GetRoomObjectBounds'; +export * from './GetRoomObjectScreenLocation'; +export * from './InitializeRoomInstanceRenderingCanvas'; +export * from './IsFurnitureSelectionDisabled'; +export * from './ProcessRoomObjectOperation'; +export * from './SetActiveRoomId'; diff --git a/apps/frontend/src/api/nitro/session/CanManipulateFurniture.ts b/apps/frontend/src/api/nitro/session/CanManipulateFurniture.ts new file mode 100644 index 0000000..8655292 --- /dev/null +++ b/apps/frontend/src/api/nitro/session/CanManipulateFurniture.ts @@ -0,0 +1,11 @@ +import { IRoomSession, RoomControllerLevel } from '@nitrots/nitro-renderer'; +import { GetSessionDataManager } from '../..'; +import { GetRoomEngine } from '../room/GetRoomEngine'; +import { IsOwnerOfFurniture } from './IsOwnerOfFurniture'; + +export function CanManipulateFurniture(roomSession: IRoomSession, objectId: number, category: number): boolean +{ + if(!roomSession) return false; + + return (roomSession.isRoomOwner || (roomSession.controllerLevel >= RoomControllerLevel.GUEST) || GetSessionDataManager().isModerator || IsOwnerOfFurniture(GetRoomEngine().getRoomObject(roomSession.roomId, objectId, category))); +} diff --git a/apps/frontend/src/api/nitro/session/CreateRoomSession.ts b/apps/frontend/src/api/nitro/session/CreateRoomSession.ts new file mode 100644 index 0000000..3be6a8a --- /dev/null +++ b/apps/frontend/src/api/nitro/session/CreateRoomSession.ts @@ -0,0 +1,6 @@ +import { GetRoomSessionManager } from './GetRoomSessionManager'; + +export function CreateRoomSession(roomId: number, password: string = null): void +{ + GetRoomSessionManager().createSession(roomId, password); +} diff --git a/apps/frontend/src/api/nitro/session/GetCanStandUp.ts b/apps/frontend/src/api/nitro/session/GetCanStandUp.ts new file mode 100644 index 0000000..4915d18 --- /dev/null +++ b/apps/frontend/src/api/nitro/session/GetCanStandUp.ts @@ -0,0 +1,13 @@ +import { AvatarAction, RoomObjectVariable } from '@nitrots/nitro-renderer'; +import { GetOwnRoomObject } from '../room'; + +export function GetCanStandUp(): string +{ + const roomObject = GetOwnRoomObject(); + + if(!roomObject) return AvatarAction.POSTURE_STAND; + + const model = roomObject.model; + + return model.getValue(RoomObjectVariable.FIGURE_CAN_STAND_UP); +} diff --git a/apps/frontend/src/api/nitro/session/GetCanUseExpression.ts b/apps/frontend/src/api/nitro/session/GetCanUseExpression.ts new file mode 100644 index 0000000..c7c7367 --- /dev/null +++ b/apps/frontend/src/api/nitro/session/GetCanUseExpression.ts @@ -0,0 +1,14 @@ +import { RoomObjectVariable } from '@nitrots/nitro-renderer'; +import { GetOwnRoomObject } from '../room'; + +export function GetCanUseExpression(): boolean +{ + const roomObject = GetOwnRoomObject(); + + if(!roomObject) return false; + + const model = roomObject.model; + const effectId = model.getValue(RoomObjectVariable.FIGURE_EFFECT); + + return !((effectId === 29) || (effectId === 30) || (effectId === 185)); +} diff --git a/apps/frontend/src/api/nitro/session/GetClubMemberLevel.ts b/apps/frontend/src/api/nitro/session/GetClubMemberLevel.ts new file mode 100644 index 0000000..fea75b7 --- /dev/null +++ b/apps/frontend/src/api/nitro/session/GetClubMemberLevel.ts @@ -0,0 +1,10 @@ +import { HabboClubLevelEnum } from '@nitrots/nitro-renderer'; +import { GetConfiguration } from '..'; +import { GetSessionDataManager } from './GetSessionDataManager'; + +export function GetClubMemberLevel(): number +{ + if(GetConfiguration('hc.disabled', false)) return HabboClubLevelEnum.VIP; + + return GetSessionDataManager().clubLevel; +} diff --git a/apps/frontend/src/api/nitro/session/GetFurnitureData.ts b/apps/frontend/src/api/nitro/session/GetFurnitureData.ts new file mode 100644 index 0000000..71afef8 --- /dev/null +++ b/apps/frontend/src/api/nitro/session/GetFurnitureData.ts @@ -0,0 +1,20 @@ +import { IFurnitureData } from '@nitrots/nitro-renderer'; +import { GetSessionDataManager } from '.'; +import { ProductTypeEnum } from '../../catalog'; + +export function GetFurnitureData(furniClassId: number, productType: string): IFurnitureData +{ + let furniData: IFurnitureData = null; + + switch(productType.toLowerCase()) + { + case ProductTypeEnum.FLOOR: + furniData = GetSessionDataManager().getFloorItemData(furniClassId); + break; + case ProductTypeEnum.WALL: + furniData = GetSessionDataManager().getWallItemData(furniClassId); + break; + } + + return furniData; +} diff --git a/apps/frontend/src/api/nitro/session/GetFurnitureDataForProductOffer.ts b/apps/frontend/src/api/nitro/session/GetFurnitureDataForProductOffer.ts new file mode 100644 index 0000000..0588835 --- /dev/null +++ b/apps/frontend/src/api/nitro/session/GetFurnitureDataForProductOffer.ts @@ -0,0 +1,21 @@ +import { CatalogPageMessageProductData, FurnitureType, IFurnitureData } from '@nitrots/nitro-renderer'; +import { GetSessionDataManager } from './GetSessionDataManager'; + +export function GetFurnitureDataForProductOffer(offer: CatalogPageMessageProductData): IFurnitureData +{ + if(!offer) return null; + + let furniData: IFurnitureData = null; + + switch((offer.productType.toUpperCase())) + { + case FurnitureType.FLOOR: + furniData = GetSessionDataManager().getFloorItemData(offer.furniClassId); + break; + case FurnitureType.WALL: + furniData = GetSessionDataManager().getWallItemData(offer.furniClassId); + break; + } + + return furniData; +} diff --git a/apps/frontend/src/api/nitro/session/GetFurnitureDataForRoomObject.ts b/apps/frontend/src/api/nitro/session/GetFurnitureDataForRoomObject.ts new file mode 100644 index 0000000..360f18d --- /dev/null +++ b/apps/frontend/src/api/nitro/session/GetFurnitureDataForRoomObject.ts @@ -0,0 +1,22 @@ +import { IFurnitureData, RoomObjectCategory, RoomObjectVariable } from '@nitrots/nitro-renderer'; +import { GetRoomEngine } from '../room'; +import { GetSessionDataManager } from './GetSessionDataManager'; + +export function GetFurnitureDataForRoomObject(roomId: number, objectId: number, category: number): IFurnitureData +{ + const roomObject = GetRoomEngine().getRoomObject(roomId, objectId, category); + + if(!roomObject) return; + + const typeId = roomObject.model.getValue(RoomObjectVariable.FURNITURE_TYPE_ID); + + switch(category) + { + case RoomObjectCategory.FLOOR: + return GetSessionDataManager().getFloorItemData(typeId); + case RoomObjectCategory.WALL: + return GetSessionDataManager().getWallItemData(typeId); + } + + return null; +} diff --git a/apps/frontend/src/api/nitro/session/GetOwnPosture.ts b/apps/frontend/src/api/nitro/session/GetOwnPosture.ts new file mode 100644 index 0000000..fe0c5f3 --- /dev/null +++ b/apps/frontend/src/api/nitro/session/GetOwnPosture.ts @@ -0,0 +1,13 @@ +import { AvatarAction, RoomObjectVariable } from '@nitrots/nitro-renderer'; +import { GetOwnRoomObject } from '../room'; + +export function GetOwnPosture(): string +{ + const roomObject = GetOwnRoomObject(); + + if(!roomObject) return AvatarAction.POSTURE_STAND; + + const model = roomObject.model; + + return model.getValue(RoomObjectVariable.FIGURE_POSTURE); +} diff --git a/apps/frontend/src/api/nitro/session/GetProductDataForLocalization.ts b/apps/frontend/src/api/nitro/session/GetProductDataForLocalization.ts new file mode 100644 index 0000000..692e0ec --- /dev/null +++ b/apps/frontend/src/api/nitro/session/GetProductDataForLocalization.ts @@ -0,0 +1,9 @@ +import { IProductData } from '@nitrots/nitro-renderer'; +import { GetSessionDataManager } from './GetSessionDataManager'; + +export function GetProductDataForLocalization(localizationId: string): IProductData +{ + if(!localizationId) return null; + + return GetSessionDataManager().getProductData(localizationId); +} diff --git a/apps/frontend/src/api/nitro/session/GetRoomSession.ts b/apps/frontend/src/api/nitro/session/GetRoomSession.ts new file mode 100644 index 0000000..37a8b9f --- /dev/null +++ b/apps/frontend/src/api/nitro/session/GetRoomSession.ts @@ -0,0 +1,7 @@ +import { IRoomSession } from '@nitrots/nitro-renderer'; +import { GetRoomSessionManager } from './GetRoomSessionManager'; + +export function GetRoomSession(): IRoomSession +{ + return GetRoomSessionManager().getSession(-1); +} diff --git a/apps/frontend/src/api/nitro/session/GetRoomSessionManager.ts b/apps/frontend/src/api/nitro/session/GetRoomSessionManager.ts new file mode 100644 index 0000000..579342d --- /dev/null +++ b/apps/frontend/src/api/nitro/session/GetRoomSessionManager.ts @@ -0,0 +1,7 @@ +import { IRoomSessionManager } from '@nitrots/nitro-renderer'; +import { GetNitroInstance } from '../GetNitroInstance'; + +export function GetRoomSessionManager(): IRoomSessionManager +{ + return GetNitroInstance().roomSessionManager; +} diff --git a/apps/frontend/src/api/nitro/session/GetSessionDataManager.ts b/apps/frontend/src/api/nitro/session/GetSessionDataManager.ts new file mode 100644 index 0000000..1f3674e --- /dev/null +++ b/apps/frontend/src/api/nitro/session/GetSessionDataManager.ts @@ -0,0 +1,7 @@ +import { ISessionDataManager } from '@nitrots/nitro-renderer'; +import { GetNitroInstance } from '../GetNitroInstance'; + +export function GetSessionDataManager(): ISessionDataManager +{ + return GetNitroInstance().sessionDataManager; +} diff --git a/apps/frontend/src/api/nitro/session/GoToDesktop.ts b/apps/frontend/src/api/nitro/session/GoToDesktop.ts new file mode 100644 index 0000000..1bbe016 --- /dev/null +++ b/apps/frontend/src/api/nitro/session/GoToDesktop.ts @@ -0,0 +1,7 @@ +import { DesktopViewComposer } from '@nitrots/nitro-renderer'; +import { SendMessageComposer } from '..'; + +export function GoToDesktop(): void +{ + SendMessageComposer(new DesktopViewComposer()); +} diff --git a/apps/frontend/src/api/nitro/session/HasHabboClub.ts b/apps/frontend/src/api/nitro/session/HasHabboClub.ts new file mode 100644 index 0000000..4077a0f --- /dev/null +++ b/apps/frontend/src/api/nitro/session/HasHabboClub.ts @@ -0,0 +1,7 @@ +import { HabboClubLevelEnum } from '@nitrots/nitro-renderer'; +import { GetSessionDataManager } from './GetSessionDataManager'; + +export function HasHabboClub(): boolean +{ + return (GetSessionDataManager().clubLevel >= HabboClubLevelEnum.CLUB); +} diff --git a/apps/frontend/src/api/nitro/session/HasHabboVip.ts b/apps/frontend/src/api/nitro/session/HasHabboVip.ts new file mode 100644 index 0000000..87f1156 --- /dev/null +++ b/apps/frontend/src/api/nitro/session/HasHabboVip.ts @@ -0,0 +1,7 @@ +import { HabboClubLevelEnum } from '@nitrots/nitro-renderer'; +import { GetSessionDataManager } from './GetSessionDataManager'; + +export function HasHabboVip(): boolean +{ + return (GetSessionDataManager().clubLevel >= HabboClubLevelEnum.VIP); +} diff --git a/apps/frontend/src/api/nitro/session/IsOwnerOfFloorFurniture.ts b/apps/frontend/src/api/nitro/session/IsOwnerOfFloorFurniture.ts new file mode 100644 index 0000000..65ef7fb --- /dev/null +++ b/apps/frontend/src/api/nitro/session/IsOwnerOfFloorFurniture.ts @@ -0,0 +1,16 @@ +import { RoomObjectCategory, RoomObjectVariable } from '@nitrots/nitro-renderer'; +import { GetRoomSession } from '.'; +import { GetRoomEngine } from '..'; +import { GetSessionDataManager } from '../../../api'; + +export function IsOwnerOfFloorFurniture(id: number): boolean +{ + const roomObject = GetRoomEngine().getRoomObject(GetRoomSession().roomId, id, RoomObjectCategory.FLOOR); + + if(!roomObject || !roomObject.model) return false; + + const userId = GetSessionDataManager().userId; + const objectOwnerId = roomObject.model.getValue(RoomObjectVariable.FURNITURE_OWNER_ID); + + return (userId === objectOwnerId); +} diff --git a/apps/frontend/src/api/nitro/session/IsOwnerOfFurniture.ts b/apps/frontend/src/api/nitro/session/IsOwnerOfFurniture.ts new file mode 100644 index 0000000..56b7fc3 --- /dev/null +++ b/apps/frontend/src/api/nitro/session/IsOwnerOfFurniture.ts @@ -0,0 +1,12 @@ +import { IRoomObject, RoomObjectVariable } from '@nitrots/nitro-renderer'; +import { GetSessionDataManager } from '../../../api'; + +export function IsOwnerOfFurniture(roomObject: IRoomObject): boolean +{ + if(!roomObject || !roomObject.model) return false; + + const userId = GetSessionDataManager().userId; + const objectOwnerId = roomObject.model.getValue(RoomObjectVariable.FURNITURE_OWNER_ID); + + return (userId === objectOwnerId); +} diff --git a/apps/frontend/src/api/nitro/session/IsRidingHorse.ts b/apps/frontend/src/api/nitro/session/IsRidingHorse.ts new file mode 100644 index 0000000..f946b69 --- /dev/null +++ b/apps/frontend/src/api/nitro/session/IsRidingHorse.ts @@ -0,0 +1,14 @@ +import { RoomObjectVariable } from '@nitrots/nitro-renderer'; +import { GetOwnRoomObject } from '../room'; + +export function IsRidingHorse(): boolean +{ + const roomObject = GetOwnRoomObject(); + + if(!roomObject) return false; + + const model = roomObject.model; + const effectId = model.getValue(RoomObjectVariable.FIGURE_EFFECT); + + return (effectId === 77); +} diff --git a/apps/frontend/src/api/nitro/session/StartRoomSession.ts b/apps/frontend/src/api/nitro/session/StartRoomSession.ts new file mode 100644 index 0000000..99d9d0b --- /dev/null +++ b/apps/frontend/src/api/nitro/session/StartRoomSession.ts @@ -0,0 +1,7 @@ +import { IRoomSession } from '@nitrots/nitro-renderer'; +import { GetRoomSessionManager } from './GetRoomSessionManager'; + +export function StartRoomSession(session: IRoomSession): void +{ + GetRoomSessionManager().startSession(session); +} diff --git a/apps/frontend/src/api/nitro/session/VisitDesktop.ts b/apps/frontend/src/api/nitro/session/VisitDesktop.ts new file mode 100644 index 0000000..e7416a3 --- /dev/null +++ b/apps/frontend/src/api/nitro/session/VisitDesktop.ts @@ -0,0 +1,9 @@ +import { GetRoomSession, GetRoomSessionManager, GoToDesktop } from '.'; + +export const VisitDesktop = () => +{ + if(!GetRoomSession()) return; + + GoToDesktop(); + GetRoomSessionManager().removeSession(-1); +} diff --git a/apps/frontend/src/api/nitro/session/index.ts b/apps/frontend/src/api/nitro/session/index.ts new file mode 100644 index 0000000..a860d7a --- /dev/null +++ b/apps/frontend/src/api/nitro/session/index.ts @@ -0,0 +1,21 @@ +export * from './CanManipulateFurniture'; +export * from './CreateRoomSession'; +export * from './GetCanStandUp'; +export * from './GetCanUseExpression'; +export * from './GetClubMemberLevel'; +export * from './GetFurnitureData'; +export * from './GetFurnitureDataForProductOffer'; +export * from './GetFurnitureDataForRoomObject'; +export * from './GetOwnPosture'; +export * from './GetProductDataForLocalization'; +export * from './GetRoomSession'; +export * from './GetRoomSessionManager'; +export * from './GetSessionDataManager'; +export * from './GoToDesktop'; +export * from './HasHabboClub'; +export * from './HasHabboVip'; +export * from './IsOwnerOfFloorFurniture'; +export * from './IsOwnerOfFurniture'; +export * from './IsRidingHorse'; +export * from './StartRoomSession'; +export * from './VisitDesktop'; diff --git a/apps/frontend/src/api/notification/NotificationAlertItem.ts b/apps/frontend/src/api/notification/NotificationAlertItem.ts new file mode 100644 index 0000000..2d7702c --- /dev/null +++ b/apps/frontend/src/api/notification/NotificationAlertItem.ts @@ -0,0 +1,67 @@ +import { NotificationAlertType } from './NotificationAlertType'; + +export class NotificationAlertItem +{ + private static ITEM_ID: number = -1; + + private _id: number; + private _messages: string[]; + private _alertType: string; + private _clickUrl: string; + private _clickUrlText: string; + private _title: string; + private _imageUrl: string; + + constructor(messages: string[], alertType: string = NotificationAlertType.DEFAULT, clickUrl: string = null, clickUrlText: string = null, title: string = null, imageUrl: string = null) + { + NotificationAlertItem.ITEM_ID += 1; + + this._id = NotificationAlertItem.ITEM_ID; + this._messages = messages; + this._alertType = alertType; + this._clickUrl = clickUrl; + this._clickUrlText = clickUrlText; + this._title = title; + this._imageUrl = imageUrl; + } + + public get id(): number + { + return this._id; + } + + public get messages(): string[] + { + return this._messages; + } + + public set alertType(alertType: string) + { + this._alertType = alertType; + } + + public get alertType(): string + { + return this._alertType; + } + + public get clickUrl(): string + { + return this._clickUrl; + } + + public get clickUrlText(): string + { + return this._clickUrlText; + } + + public get title(): string + { + return this._title; + } + + public get imageUrl(): string + { + return this._imageUrl; + } +} diff --git a/apps/frontend/src/api/notification/NotificationAlertType.ts b/apps/frontend/src/api/notification/NotificationAlertType.ts new file mode 100644 index 0000000..ad804e8 --- /dev/null +++ b/apps/frontend/src/api/notification/NotificationAlertType.ts @@ -0,0 +1,10 @@ +export class NotificationAlertType +{ + public static DEFAULT: string = 'default'; + public static MOTD: string = 'motd'; + public static MODERATION: string = 'moderation'; + public static EVENT: string = 'event'; + public static NITRO: string = 'nitro'; + public static SEARCH: string = 'search'; + public static ALERT: string = 'alert'; +} diff --git a/apps/frontend/src/api/notification/NotificationBubbleItem.ts b/apps/frontend/src/api/notification/NotificationBubbleItem.ts new file mode 100644 index 0000000..fe90dab --- /dev/null +++ b/apps/frontend/src/api/notification/NotificationBubbleItem.ts @@ -0,0 +1,48 @@ +import { NotificationBubbleType } from './NotificationBubbleType'; + +export class NotificationBubbleItem +{ + private static ITEM_ID: number = -1; + + private _id: number; + private _message: string; + private _notificationType: string; + private _iconUrl: string; + private _linkUrl: string; + + constructor(message: string, notificationType: string = NotificationBubbleType.INFO, iconUrl: string = null, linkUrl: string = null) + { + NotificationBubbleItem.ITEM_ID += 1; + + this._id = NotificationBubbleItem.ITEM_ID; + this._message = message; + this._notificationType = notificationType; + this._iconUrl = iconUrl; + this._linkUrl = linkUrl; + } + + public get id(): number + { + return this._id; + } + + public get message(): string + { + return this._message; + } + + public get notificationType(): string + { + return this._notificationType; + } + + public get iconUrl(): string + { + return this._iconUrl; + } + + public get linkUrl(): string + { + return this._linkUrl; + } +} diff --git a/apps/frontend/src/api/notification/NotificationBubbleType.ts b/apps/frontend/src/api/notification/NotificationBubbleType.ts new file mode 100644 index 0000000..cce38f5 --- /dev/null +++ b/apps/frontend/src/api/notification/NotificationBubbleType.ts @@ -0,0 +1,19 @@ +export class NotificationBubbleType +{ + public static FRIENDOFFLINE: string = 'friendoffline'; + public static FRIENDONLINE: string = 'friendonline'; + public static THIRDPARTYFRIENDOFFLINE: string = 'thirdpartyfriendoffline'; + public static THIRDPARTYFRIENDONLINE: string = 'thirdpartyfriendonline'; + public static ACHIEVEMENT: string = 'achievement'; + public static BADGE_RECEIVED: string = 'badge_received'; + public static INFO: string = 'info'; + public static RECYCLEROK: string = 'recyclerok'; + public static RESPECT: string = 'respect'; + public static CLUB: string = 'club'; + public static SOUNDMACHINE: string = 'soundmachine'; + public static PETLEVEL: string = 'petlevel'; + public static CLUBGIFT: string = 'clubgift'; + public static BUYFURNI: string = 'buyfurni'; + public static VIP: string = 'vip'; + public static ROOMMESSAGESPOSTED: string = 'roommessagesposted'; +} diff --git a/apps/frontend/src/api/notification/NotificationConfirmItem.ts b/apps/frontend/src/api/notification/NotificationConfirmItem.ts new file mode 100644 index 0000000..0455662 --- /dev/null +++ b/apps/frontend/src/api/notification/NotificationConfirmItem.ts @@ -0,0 +1,67 @@ +export class NotificationConfirmItem +{ + private static ITEM_ID: number = -1; + + private _id: number; + private _confirmType: string; + private _message: string; + private _onConfirm: Function; + private _onCancel: Function; + private _confirmText: string; + private _cancelText: string; + private _title: string; + + constructor(confirmType: string, message: string, onConfirm: Function, onCancel: Function, confirmText: string, cancelText: string, title: string) + { + NotificationConfirmItem.ITEM_ID += 1; + + this._id = NotificationConfirmItem.ITEM_ID; + this._confirmType = confirmType; + this._message = message; + this._onConfirm = onConfirm; + this._onCancel = onCancel; + this._confirmText = confirmText; + this._cancelText = cancelText; + this._title = title; + } + + public get id(): number + { + return this._id; + } + + public get confirmType(): string + { + return this._confirmType; + } + + public get message(): string + { + return this._message; + } + + public get onConfirm(): Function + { + return this._onConfirm; + } + + public get onCancel(): Function + { + return this._onCancel; + } + + public get confirmText(): string + { + return this._confirmText; + } + + public get cancelText(): string + { + return this._cancelText; + } + + public get title(): string + { + return this._title; + } +} diff --git a/apps/frontend/src/api/notification/NotificationConfirmType.ts b/apps/frontend/src/api/notification/NotificationConfirmType.ts new file mode 100644 index 0000000..533ca05 --- /dev/null +++ b/apps/frontend/src/api/notification/NotificationConfirmType.ts @@ -0,0 +1,4 @@ +export class NotificationConfirmType +{ + public static DEFAULT: string = 'default'; +} diff --git a/apps/frontend/src/api/notification/index.ts b/apps/frontend/src/api/notification/index.ts new file mode 100644 index 0000000..23476d3 --- /dev/null +++ b/apps/frontend/src/api/notification/index.ts @@ -0,0 +1,6 @@ +export * from './NotificationAlertItem'; +export * from './NotificationAlertType'; +export * from './NotificationBubbleItem'; +export * from './NotificationBubbleType'; +export * from './NotificationConfirmItem'; +export * from './NotificationConfirmType'; diff --git a/apps/frontend/src/api/purse/IPurse.ts b/apps/frontend/src/api/purse/IPurse.ts new file mode 100644 index 0000000..9fffb18 --- /dev/null +++ b/apps/frontend/src/api/purse/IPurse.ts @@ -0,0 +1,15 @@ +export interface IPurse +{ + credits: number; + activityPoints: Map; + clubDays: number; + clubPeriods: number; + hasClubLeft: boolean; + isVip: boolean; + pastClubDays: number; + pastVipDays: number; + isExpiring: boolean; + minutesUntilExpiration: number; + minutesSinceLastModified: number; + clubLevel: number; +} diff --git a/apps/frontend/src/api/purse/Purse.ts b/apps/frontend/src/api/purse/Purse.ts new file mode 100644 index 0000000..6970e59 --- /dev/null +++ b/apps/frontend/src/api/purse/Purse.ts @@ -0,0 +1,165 @@ +import { GetTickerTime, HabboClubLevelEnum } from '@nitrots/nitro-renderer'; +import { IPurse } from './IPurse'; + +export class Purse implements IPurse +{ + private _credits: number = 0; + private _activityPoints: Map = new Map(); + private _clubDays: number = 0; + private _clubPeriods: number = 0; + private _isVIP: boolean = false; + private _pastClubDays: number = 0; + private _pastVipDays: number = 0; + private _isExpiring: boolean = false; + private _minutesUntilExpiration: number = 0; + private _minutesSinceLastModified: number = 0; + private _lastUpdated: number = 0; + + public static from(purse: Purse): Purse + { + const newPurse = new Purse(); + + newPurse._credits = purse._credits; + newPurse._activityPoints = purse._activityPoints; + newPurse._clubDays = purse._clubDays; + newPurse._clubPeriods = purse._clubPeriods; + newPurse._isVIP = purse._isVIP; + newPurse._pastClubDays = purse._pastClubDays; + newPurse._pastVipDays = purse._pastVipDays; + newPurse._isExpiring = purse._isExpiring; + newPurse._minutesUntilExpiration = purse._minutesUntilExpiration; + newPurse._minutesSinceLastModified = purse._minutesSinceLastModified; + newPurse._lastUpdated = purse._lastUpdated; + + return newPurse; + } + + public get credits(): number + { + return this._credits; + } + + public set credits(credits: number) + { + this._lastUpdated = GetTickerTime(); + this._credits = credits; + } + + public get activityPoints(): Map + { + return this._activityPoints; + } + + public set activityPoints(k: Map) + { + this._lastUpdated = GetTickerTime(); + this._activityPoints = k; + } + + public get clubDays(): number + { + return this._clubDays; + } + + public set clubDays(k: number) + { + this._lastUpdated = GetTickerTime(); + this._clubDays = k; + } + + public get clubPeriods(): number + { + return this._clubPeriods; + } + + public set clubPeriods(k: number) + { + this._lastUpdated = GetTickerTime(); + this._clubPeriods = k; + } + + public get hasClubLeft(): boolean + { + return (this._clubDays > 0) || (this._clubPeriods > 0); + } + + public get isVip(): boolean + { + return this._isVIP; + } + + public set isVip(k: boolean) + { + this._isVIP = k; + } + + public get pastClubDays(): number + { + return this._pastClubDays; + } + + public set pastClubDays(k: number) + { + this._lastUpdated = GetTickerTime(); + this._pastClubDays = k; + } + + public get pastVipDays(): number + { + return this._pastVipDays; + } + + public set pastVipDays(k: number) + { + this._lastUpdated = GetTickerTime(); + this._pastVipDays = k; + } + + public get isExpiring(): boolean + { + return this._isExpiring; + } + + public set isExpiring(k: boolean) + { + this._isExpiring = k; + } + + public get minutesUntilExpiration(): number + { + var k: number = ((GetTickerTime() - this._lastUpdated) / (1000 * 60)); + var _local_2: number = (this._minutesUntilExpiration - k); + return (_local_2 > 0) ? _local_2 : 0; + } + + public set minutesUntilExpiration(k: number) + { + this._lastUpdated = GetTickerTime(); + this._minutesUntilExpiration = k; + } + + public get minutesSinceLastModified(): number + { + return this._minutesSinceLastModified; + } + + public set minutesSinceLastModified(k: number) + { + this._lastUpdated = GetTickerTime(); + this._minutesSinceLastModified = k; + } + + public get lastUpdated(): number + { + return this._lastUpdated; + } + + public get clubLevel(): number + { + if(((this.clubDays === 0) && (this.clubPeriods === 0))) return HabboClubLevelEnum.NO_CLUB; + + if(this.isVip) return HabboClubLevelEnum.VIP; + + return HabboClubLevelEnum.CLUB; + } +} diff --git a/apps/frontend/src/api/purse/index.ts b/apps/frontend/src/api/purse/index.ts new file mode 100644 index 0000000..ed34480 --- /dev/null +++ b/apps/frontend/src/api/purse/index.ts @@ -0,0 +1,2 @@ +export * from './IPurse'; +export * from './Purse'; diff --git a/apps/frontend/src/api/room/events/RoomWidgetPollUpdateEvent.ts b/apps/frontend/src/api/room/events/RoomWidgetPollUpdateEvent.ts new file mode 100644 index 0000000..edfb8fd --- /dev/null +++ b/apps/frontend/src/api/room/events/RoomWidgetPollUpdateEvent.ts @@ -0,0 +1,110 @@ +import { IPollQuestion } from '@nitrots/nitro-renderer'; +import { RoomWidgetUpdateEvent } from './RoomWidgetUpdateEvent'; + +export class RoomWidgetPollUpdateEvent extends RoomWidgetUpdateEvent +{ + public static readonly OFFER = 'RWPUW_OFFER'; + public static readonly ERROR = 'RWPUW_ERROR'; + public static readonly CONTENT = 'RWPUW_CONTENT'; + + private _id = -1; + private _summary: string; + private _headline: string; + private _numQuestions = 0; + private _startMessage = ''; + private _endMessage = ''; + private _questionArray: IPollQuestion[] = null; + private _pollType = ''; + private _npsPoll = false; + + constructor(type: string, id: number) + { + super(type); + this._id = id; + } + + public get id(): number + { + return this._id; + } + + public get summary(): string + { + return this._summary; + } + + public set summary(k: string) + { + this._summary = k; + } + + public get headline(): string + { + return this._headline; + } + + public set headline(k: string) + { + this._headline = k; + } + + public get numQuestions(): number + { + return this._numQuestions; + } + + public set numQuestions(k: number) + { + this._numQuestions = k; + } + + public get startMessage(): string + { + return this._startMessage; + } + + public set startMessage(k: string) + { + this._startMessage = k; + } + + public get endMessage(): string + { + return this._endMessage; + } + + public set endMessage(k: string) + { + this._endMessage = k; + } + + public get questionArray(): IPollQuestion[] + { + return this._questionArray; + } + + public set questionArray(k: IPollQuestion[]) + { + this._questionArray = k; + } + + public get pollType(): string + { + return this._pollType; + } + + public set pollType(k: string) + { + this._pollType = k; + } + + public get npsPoll(): boolean + { + return this._npsPoll; + } + + public set npsPoll(k: boolean) + { + this._npsPoll = k; + } +} diff --git a/apps/frontend/src/api/room/events/RoomWidgetUpdateBackgroundColorPreviewEvent.ts b/apps/frontend/src/api/room/events/RoomWidgetUpdateBackgroundColorPreviewEvent.ts new file mode 100644 index 0000000..30135a3 --- /dev/null +++ b/apps/frontend/src/api/room/events/RoomWidgetUpdateBackgroundColorPreviewEvent.ts @@ -0,0 +1,35 @@ +import { RoomWidgetUpdateEvent } from './RoomWidgetUpdateEvent'; + +export class RoomWidgetUpdateBackgroundColorPreviewEvent extends RoomWidgetUpdateEvent +{ + public static PREVIEW = 'RWUBCPE_PREVIEW'; + public static CLEAR_PREVIEW = 'RWUBCPE_CLEAR_PREVIEW'; + + private _hue: number; + private _saturation: number; + private _lightness: number; + + constructor(type: string, hue: number = 0, saturation: number = 0, lightness: number = 0) + { + super(type); + + this._hue = hue; + this._saturation = saturation; + this._lightness = lightness; + } + + public get hue(): number + { + return this._hue; + } + + public get saturation(): number + { + return this._saturation; + } + + public get lightness(): number + { + return this._lightness; + } +} diff --git a/apps/frontend/src/api/room/events/RoomWidgetUpdateChatInputContentEvent.ts b/apps/frontend/src/api/room/events/RoomWidgetUpdateChatInputContentEvent.ts new file mode 100644 index 0000000..9352372 --- /dev/null +++ b/apps/frontend/src/api/room/events/RoomWidgetUpdateChatInputContentEvent.ts @@ -0,0 +1,29 @@ +import { RoomWidgetUpdateEvent } from './RoomWidgetUpdateEvent'; + +export class RoomWidgetUpdateChatInputContentEvent extends RoomWidgetUpdateEvent +{ + public static CHAT_INPUT_CONTENT: string = 'RWUCICE_CHAT_INPUT_CONTENT'; + public static WHISPER: string = 'whisper'; + public static SHOUT: string = 'shout'; + + private _chatMode: string = ''; + private _userName: string = ''; + + constructor(chatMode: string, userName: string) + { + super(RoomWidgetUpdateChatInputContentEvent.CHAT_INPUT_CONTENT); + + this._chatMode = chatMode; + this._userName = userName; + } + + public get chatMode(): string + { + return this._chatMode; + } + + public get userName(): string + { + return this._userName; + } +} diff --git a/apps/frontend/src/api/room/events/RoomWidgetUpdateEvent.ts b/apps/frontend/src/api/room/events/RoomWidgetUpdateEvent.ts new file mode 100644 index 0000000..0ac8ff8 --- /dev/null +++ b/apps/frontend/src/api/room/events/RoomWidgetUpdateEvent.ts @@ -0,0 +1,4 @@ +import { NitroEvent } from '@nitrots/nitro-renderer'; + +export class RoomWidgetUpdateEvent extends NitroEvent +{} diff --git a/apps/frontend/src/api/room/events/RoomWidgetUpdateRentableBotChatEvent.ts b/apps/frontend/src/api/room/events/RoomWidgetUpdateRentableBotChatEvent.ts new file mode 100644 index 0000000..6191e1b --- /dev/null +++ b/apps/frontend/src/api/room/events/RoomWidgetUpdateRentableBotChatEvent.ts @@ -0,0 +1,62 @@ +import { RoomWidgetUpdateEvent } from './RoomWidgetUpdateEvent'; + +export class RoomWidgetUpdateRentableBotChatEvent extends RoomWidgetUpdateEvent +{ + public static UPDATE_CHAT: string = 'RWURBCE_UPDATE_CHAT'; + + private _objectId: number; + private _category: number; + private _botId: number; + private _chat: string; + private _automaticChat: boolean; + private _chatDelay: number; + private _mixSentences: boolean; + + constructor(objectId: number, category: number, botId: number, chat: string, automaticChat: boolean, chatDelay: number, mixSentences: boolean) + { + super(RoomWidgetUpdateRentableBotChatEvent.UPDATE_CHAT); + + this._objectId = objectId; + this._category = category; + this._botId = botId; + this._chat = chat; + this._automaticChat = automaticChat; + this._chatDelay = chatDelay; + this._mixSentences = mixSentences; + } + + public get objectId(): number + { + return this._objectId; + } + + public get category(): number + { + return this._category; + } + + public get botId(): number + { + return this._botId; + } + + public get chat(): string + { + return this._chat; + } + + public get automaticChat(): boolean + { + return this._automaticChat; + } + + public get chatDelay(): number + { + return this._chatDelay; + } + + public get mixSentences(): boolean + { + return this._mixSentences; + } +} diff --git a/apps/frontend/src/api/room/events/RoomWidgetUpdateRoomObjectEvent.ts b/apps/frontend/src/api/room/events/RoomWidgetUpdateRoomObjectEvent.ts new file mode 100644 index 0000000..0660276 --- /dev/null +++ b/apps/frontend/src/api/room/events/RoomWidgetUpdateRoomObjectEvent.ts @@ -0,0 +1,43 @@ +import { RoomWidgetUpdateEvent } from './RoomWidgetUpdateEvent'; + +export class RoomWidgetUpdateRoomObjectEvent extends RoomWidgetUpdateEvent +{ + public static OBJECT_SELECTED: string = 'RWUROE_OBJECT_SELECTED'; + public static OBJECT_DESELECTED: string = 'RWUROE_OBJECT_DESELECTED'; + public static USER_REMOVED: string = 'RWUROE_USER_REMOVED'; + public static FURNI_REMOVED: string = 'RWUROE_FURNI_REMOVED'; + public static FURNI_ADDED: string = 'RWUROE_FURNI_ADDED'; + public static USER_ADDED: string = 'RWUROE_USER_ADDED'; + public static OBJECT_ROLL_OVER: string = 'RWUROE_OBJECT_ROLL_OVER'; + public static OBJECT_ROLL_OUT: string = 'RWUROE_OBJECT_ROLL_OUT'; + public static OBJECT_REQUEST_MANIPULATION: string = 'RWUROE_OBJECT_REQUEST_MANIPULATION'; + public static OBJECT_DOUBLE_CLICKED: string = 'RWUROE_OBJECT_DOUBLE_CLICKED'; + + private _id: number; + private _category: number; + private _roomId: number; + + constructor(type: string, id: number, category: number, roomId: number) + { + super(type); + + this._id = id; + this._category = category; + this._roomId = roomId; + } + + public get id(): number + { + return this._id; + } + + public get category(): number + { + return this._category; + } + + public get roomId(): number + { + return this._roomId; + } +} diff --git a/apps/frontend/src/api/room/events/index.ts b/apps/frontend/src/api/room/events/index.ts new file mode 100644 index 0000000..e5ed0d8 --- /dev/null +++ b/apps/frontend/src/api/room/events/index.ts @@ -0,0 +1,6 @@ +export * from './RoomWidgetPollUpdateEvent'; +export * from './RoomWidgetUpdateBackgroundColorPreviewEvent'; +export * from './RoomWidgetUpdateChatInputContentEvent'; +export * from './RoomWidgetUpdateEvent'; +export * from './RoomWidgetUpdateRentableBotChatEvent'; +export * from './RoomWidgetUpdateRoomObjectEvent'; diff --git a/apps/frontend/src/api/room/index.ts b/apps/frontend/src/api/room/index.ts new file mode 100644 index 0000000..56aea79 --- /dev/null +++ b/apps/frontend/src/api/room/index.ts @@ -0,0 +1,2 @@ +export * from './events'; +export * from './widgets'; diff --git a/apps/frontend/src/api/room/widgets/AvatarInfoFurni.ts b/apps/frontend/src/api/room/widgets/AvatarInfoFurni.ts new file mode 100644 index 0000000..3380282 --- /dev/null +++ b/apps/frontend/src/api/room/widgets/AvatarInfoFurni.ts @@ -0,0 +1,38 @@ +import { IObjectData } from '@nitrots/nitro-renderer'; +import { IAvatarInfo } from './IAvatarInfo'; + +export class AvatarInfoFurni implements IAvatarInfo +{ + public static FURNI: string = 'IFI_FURNI'; + + public id: number = 0; + public category: number = 0; + public name: string = ''; + public description: string = ''; + public image: HTMLImageElement = null; + public isWallItem: boolean = false; + public isStickie: boolean = false; + public isRoomOwner: boolean = false; + public roomControllerLevel: number = 0; + public isAnyRoomController: boolean = false; + public expiration: number = -1; + public purchaseCatalogPageId: number = -1; + public purchaseOfferId: number = -1; + public extraParam: string = ''; + public isOwner: boolean = false; + public stuffData: IObjectData = null; + public groupId: number = 0; + public ownerId: number = 0; + public ownerName: string = ''; + public usagePolicy: number = 0; + public rentCatalogPageId: number = -1; + public rentOfferId: number = -1; + public purchaseCouldBeUsedForBuyout: boolean = false; + public rentCouldBeUsedForBuyout: boolean = false; + public availableForBuildersClub: boolean = false; + public tileSizeX: number = 1; + public tileSizeY: number = 1; + + constructor(public readonly type: string) + {} +} diff --git a/apps/frontend/src/api/room/widgets/AvatarInfoName.ts b/apps/frontend/src/api/room/widgets/AvatarInfoName.ts new file mode 100644 index 0000000..66a6a7e --- /dev/null +++ b/apps/frontend/src/api/room/widgets/AvatarInfoName.ts @@ -0,0 +1,11 @@ +export class AvatarInfoName +{ + constructor( + public readonly roomIndex: number, + public readonly category: number, + public readonly id: number, + public readonly name: string, + public readonly userType: number, + public readonly isFriend: boolean = false) + {} +} diff --git a/apps/frontend/src/api/room/widgets/AvatarInfoPet.ts b/apps/frontend/src/api/room/widgets/AvatarInfoPet.ts new file mode 100644 index 0000000..0c0435a --- /dev/null +++ b/apps/frontend/src/api/room/widgets/AvatarInfoPet.ts @@ -0,0 +1,46 @@ +import { IAvatarInfo } from './IAvatarInfo'; + +export class AvatarInfoPet implements IAvatarInfo +{ + public static PET_INFO: string = 'IPI_PET_INFO'; + + public level: number = 0; + public maximumLevel: number = 0; + public experience: number = 0; + public levelExperienceGoal: number = 0; + public energy: number = 0; + public maximumEnergy: number = 0; + public happyness: number = 0; + public maximumHappyness: number = 0; + public respectsPetLeft: number = 0; + public respect: number = 0; + public age: number = 0; + public name: string = ''; + public id: number = -1; + public image: HTMLImageElement = null; + public petType: number = 0; + public petBreed: number = 0; + public petFigure: string = ''; + public posture: string = 'std'; + public isOwner: boolean = false; + public ownerId: number = -1; + public ownerName: string = ''; + public canRemovePet: boolean = false; + public roomIndex: number = 0; + public unknownRarityLevel: number = 0; + public saddle: boolean = false; + public rider: boolean = false; + public breedable: boolean = false; + public skillTresholds: number[] = []; + public publiclyRideable: number = 0; + public fullyGrown: boolean = false; + public dead: boolean = false; + public rarityLevel: number = 0; + public maximumTimeToLive: number = 0; + public remainingTimeToLive: number = 0; + public remainingGrowTime: number = 0; + public publiclyBreedable: boolean = false; + + constructor(public readonly type: string) + {} +} diff --git a/apps/frontend/src/api/room/widgets/AvatarInfoRentableBot.ts b/apps/frontend/src/api/room/widgets/AvatarInfoRentableBot.ts new file mode 100644 index 0000000..77fb10c --- /dev/null +++ b/apps/frontend/src/api/room/widgets/AvatarInfoRentableBot.ts @@ -0,0 +1,23 @@ +import { IAvatarInfo } from './IAvatarInfo'; + +export class AvatarInfoRentableBot implements IAvatarInfo +{ + public static RENTABLE_BOT: string = 'IRBI_RENTABLE_BOT'; + + public name: string = ''; + public motto: string = ''; + public webID: number = 0; + public figure: string = ''; + public badges: string[] = []; + public carryItem: number = 0; + public roomIndex: number = 0; + public amIOwner: boolean = false; + public amIAnyRoomController: boolean = false; + public roomControllerLevel: number = 0; + public ownerId: number = -1; + public ownerName: string = ''; + public botSkills: number[] = []; + + constructor(public readonly type: string) + {} +} diff --git a/apps/frontend/src/api/room/widgets/AvatarInfoUser.ts b/apps/frontend/src/api/room/widgets/AvatarInfoUser.ts new file mode 100644 index 0000000..270bfbd --- /dev/null +++ b/apps/frontend/src/api/room/widgets/AvatarInfoUser.ts @@ -0,0 +1,49 @@ +import { IAvatarInfo } from './IAvatarInfo'; + +export class AvatarInfoUser implements IAvatarInfo +{ + public static OWN_USER: string = 'IUI_OWN_USER'; + public static PEER: string = 'IUI_PEER'; + public static BOT: string = 'IUI_BOT'; + public static TRADE_REASON_OK: number = 0; + public static TRADE_REASON_SHUTDOWN: number = 2; + public static TRADE_REASON_NO_TRADING: number = 3; + public static DEFAULT_BOT_BADGE_ID: string = 'BOT'; + + public name: string = ''; + public motto: string = ''; + public achievementScore: number = 0; + public webID: number = 0; + public xp: number = 0; + public userType: number = -1; + public figure: string = ''; + public badges: string[] = []; + public groupId: number = 0; + public groupName: string = ''; + public groupBadgeId: string = ''; + public carryItem: number = 0; + public roomIndex: number = 0; + public isSpectatorMode: boolean = false; + public allowNameChange: boolean = false; + public amIOwner: boolean = false; + public amIAnyRoomController: boolean = false; + public roomControllerLevel: number = 0; + public canBeKicked: boolean = false; + public canBeBanned: boolean = false; + public canBeMuted: boolean = false; + public respectLeft: number = 0; + public isIgnored: boolean = false; + public isGuildRoom: boolean = false; + public canTrade: boolean = false; + public canTradeReason: number = 0; + public targetRoomControllerLevel: number = 0; + public isAmbassador: boolean = false; + + constructor(public readonly type: string) + {} + + public get isOwnUser(): boolean + { + return (this.type === AvatarInfoUser.OWN_USER); + } +} diff --git a/apps/frontend/src/api/room/widgets/AvatarInfoUtilities.ts b/apps/frontend/src/api/room/widgets/AvatarInfoUtilities.ts new file mode 100644 index 0000000..7c48944 --- /dev/null +++ b/apps/frontend/src/api/room/widgets/AvatarInfoUtilities.ts @@ -0,0 +1,454 @@ +import { GetTickerTime, IFurnitureData, IRoomModerationSettings, IRoomPetData, IRoomUserData, ObjectDataFactory, PetFigureData, PetType, RoomControllerLevel, RoomModerationSettings, RoomObjectCategory, RoomObjectType, RoomObjectVariable, RoomTradingLevelEnum, RoomWidgetEnumItemExtradataParameter, Vector3d } from '@nitrots/nitro-renderer'; +import { GetRoomEngine, GetRoomSession, GetSessionDataManager, IsOwnerOfFurniture } from '../../nitro'; +import { LocalizeText } from '../../utils'; +import { AvatarInfoFurni } from './AvatarInfoFurni'; +import { AvatarInfoName } from './AvatarInfoName'; +import { AvatarInfoPet } from './AvatarInfoPet'; +import { AvatarInfoRentableBot } from './AvatarInfoRentableBot'; +import { AvatarInfoUser } from './AvatarInfoUser'; + +export class AvatarInfoUtilities +{ + public static getObjectName(objectId: number, category: number): AvatarInfoName + { + const roomSession = GetRoomSession(); + + let id = -1; + let name: string = null; + let userType = 0; + + switch(category) + { + case RoomObjectCategory.FLOOR: + case RoomObjectCategory.WALL: { + const roomObject = GetRoomEngine().getRoomObject(roomSession.roomId, objectId, category); + + if(!roomObject) break; + + if(roomObject.type.indexOf('poster') === 0) + { + name = LocalizeText('${poster_' + parseInt(roomObject.type.replace('poster', '')) + '_name}'); + } + else + { + let furniData: IFurnitureData = null; + + const typeId = roomObject.model.getValue(RoomObjectVariable.FURNITURE_TYPE_ID); + + if(category === RoomObjectCategory.FLOOR) + { + furniData = GetSessionDataManager().getFloorItemData(typeId); + } + + else if(category === RoomObjectCategory.WALL) + { + furniData = GetSessionDataManager().getWallItemData(typeId); + } + + if(!furniData) break; + + id = furniData.id; + name = furniData.name; + } + break; + } + case RoomObjectCategory.UNIT: { + const userData = roomSession.userDataManager.getUserDataByIndex(objectId); + + if(!userData) break; + + id = userData.webID; + name = userData.name; + userType = userData.type; + break; + } + } + + if(!name || !name.length) return null; + + return new AvatarInfoName(objectId, category, id, name, userType); + } + + public static getFurniInfo(objectId: number, category: number): AvatarInfoFurni + { + const roomSession = GetRoomSession(); + const furniInfo = new AvatarInfoFurni(AvatarInfoFurni.FURNI); + + furniInfo.id = objectId; + furniInfo.category = category; + + const roomObject = GetRoomEngine().getRoomObject(roomSession.roomId, objectId, category); + + if(!roomObject) return; + + const model = roomObject.model; + + if(model.getValue(RoomWidgetEnumItemExtradataParameter.INFOSTAND_EXTRA_PARAM)) + { + furniInfo.extraParam = model.getValue(RoomWidgetEnumItemExtradataParameter.INFOSTAND_EXTRA_PARAM); + } + + const dataFormat = model.getValue(RoomObjectVariable.FURNITURE_DATA_FORMAT); + const objectData = ObjectDataFactory.getData(dataFormat); + + objectData.initializeFromRoomObjectModel(model); + + furniInfo.stuffData = objectData; + + const objectType = roomObject.type; + + if(objectType.indexOf('poster') === 0) + { + const posterId = parseInt(objectType.replace('poster', '')); + + furniInfo.name = LocalizeText(('${poster_' + posterId) + '_name}'); + furniInfo.description = LocalizeText(('${poster_' + posterId) + '_desc}'); + } + else + { + const typeId = model.getValue(RoomObjectVariable.FURNITURE_TYPE_ID); + + let furnitureData: IFurnitureData = null; + + if(category === RoomObjectCategory.FLOOR) + { + furnitureData = GetSessionDataManager().getFloorItemData(typeId); + } + + else if(category === RoomObjectCategory.WALL) + { + furnitureData = GetSessionDataManager().getWallItemData(typeId); + } + + if(furnitureData) + { + furniInfo.name = furnitureData.name; + furniInfo.description = furnitureData.description; + furniInfo.purchaseOfferId = furnitureData.purchaseOfferId; + furniInfo.purchaseCouldBeUsedForBuyout = furnitureData.purchaseCouldBeUsedForBuyout; + furniInfo.rentOfferId = furnitureData.rentOfferId; + furniInfo.rentCouldBeUsedForBuyout = furnitureData.rentCouldBeUsedForBuyout; + furniInfo.availableForBuildersClub = furnitureData.availableForBuildersClub; + furniInfo.tileSizeX = furnitureData.tileSizeX; + furniInfo.tileSizeY = furnitureData.tileSizeY; + } + } + + if(objectType.indexOf('post_it') > -1) furniInfo.isStickie = true; + + const expiryTime = model.getValue(RoomObjectVariable.FURNITURE_EXPIRY_TIME); + const expiryTimestamp = model.getValue(RoomObjectVariable.FURNITURE_EXPIRTY_TIMESTAMP); + + furniInfo.expiration = ((expiryTime < 0) ? expiryTime : Math.max(0, (expiryTime - ((GetTickerTime() - expiryTimestamp) / 1000)))); + + let roomObjectImage = GetRoomEngine().getRoomObjectImage(roomSession.roomId, objectId, category, new Vector3d(180), 64, null); + + if(!roomObjectImage.data || (roomObjectImage.data.width > 140) || (roomObjectImage.data.height > 200)) + { + roomObjectImage = GetRoomEngine().getRoomObjectImage(roomSession.roomId, objectId, category, new Vector3d(180), 1, null); + } + + furniInfo.image = roomObjectImage.getImage(); + furniInfo.isWallItem = (category === RoomObjectCategory.WALL); + furniInfo.isRoomOwner = roomSession.isRoomOwner; + furniInfo.roomControllerLevel = roomSession.controllerLevel; + furniInfo.isAnyRoomController = GetSessionDataManager().isModerator; + furniInfo.ownerId = model.getValue(RoomObjectVariable.FURNITURE_OWNER_ID); + furniInfo.ownerName = model.getValue(RoomObjectVariable.FURNITURE_OWNER_NAME); + furniInfo.usagePolicy = model.getValue(RoomObjectVariable.FURNITURE_USAGE_POLICY); + + const guildId = model.getValue(RoomObjectVariable.FURNITURE_GUILD_CUSTOMIZED_GUILD_ID); + + if(guildId !== 0) + { + furniInfo.groupId = guildId; + //this.container.connection.send(new GroupInformationComposer(guildId, false)); + } + + if(IsOwnerOfFurniture(roomObject)) furniInfo.isOwner = true; + + return furniInfo; + } + + public static getUserInfo(category: number, userData: IRoomUserData): AvatarInfoUser + { + const roomSession = GetRoomSession(); + + let userInfoType = AvatarInfoUser.OWN_USER; + + if(userData.webID !== GetSessionDataManager().userId) userInfoType = AvatarInfoUser.PEER; + + const userInfo = new AvatarInfoUser(userInfoType); + + userInfo.isSpectatorMode = roomSession.isSpectator; + userInfo.name = userData.name; + userInfo.motto = userData.custom; + userInfo.achievementScore = userData.activityPoints; + userInfo.webID = userData.webID; + userInfo.roomIndex = userData.roomIndex; + userInfo.userType = RoomObjectType.USER; + + const roomObject = GetRoomEngine().getRoomObject(roomSession.roomId, userData.roomIndex, category); + + if(roomObject) userInfo.carryItem = (roomObject.model.getValue(RoomObjectVariable.FIGURE_CARRY_OBJECT) || 0); + + if(userInfoType === AvatarInfoUser.OWN_USER) userInfo.allowNameChange = GetSessionDataManager().canChangeName; + + userInfo.amIOwner = roomSession.isRoomOwner; + userInfo.isGuildRoom = roomSession.isGuildRoom; + userInfo.roomControllerLevel = roomSession.controllerLevel; + userInfo.amIAnyRoomController = GetSessionDataManager().isModerator; + userInfo.isAmbassador = GetSessionDataManager().isAmbassador; + + if(userInfoType === AvatarInfoUser.PEER) + { + if(roomObject) + { + const flatControl = roomObject.model.getValue(RoomObjectVariable.FIGURE_FLAT_CONTROL); + + if(flatControl !== null) userInfo.targetRoomControllerLevel = flatControl; + + userInfo.canBeMuted = this.canBeMuted(userInfo); + userInfo.canBeKicked = this.canBeKicked(userInfo); + userInfo.canBeBanned = this.canBeBanned(userInfo); + } + + userInfo.isIgnored = GetSessionDataManager().isUserIgnored(userData.name); + userInfo.respectLeft = GetSessionDataManager().respectsLeft; + + const isShuttingDown = GetSessionDataManager().isSystemShutdown; + const tradeMode = roomSession.tradeMode; + + if(isShuttingDown) + { + userInfo.canTrade = false; + } + else + { + switch(tradeMode) + { + case RoomTradingLevelEnum.ROOM_CONTROLLER_REQUIRED: { + const roomController = ((userInfo.roomControllerLevel !== RoomControllerLevel.NONE) && (userInfo.roomControllerLevel !== RoomControllerLevel.GUILD_MEMBER)); + const targetController = ((userInfo.targetRoomControllerLevel !== RoomControllerLevel.NONE) && (userInfo.targetRoomControllerLevel !== RoomControllerLevel.GUILD_MEMBER)); + + userInfo.canTrade = (roomController || targetController); + break; + } + case RoomTradingLevelEnum.NO_TRADING: + userInfo.canTrade = true; + break; + default: + userInfo.canTrade = false; + break; + } + } + + userInfo.canTradeReason = AvatarInfoUser.TRADE_REASON_OK; + + if(isShuttingDown) userInfo.canTradeReason = AvatarInfoUser.TRADE_REASON_SHUTDOWN; + + if(tradeMode !== RoomTradingLevelEnum.FREE_TRADING) userInfo.canTradeReason = AvatarInfoUser.TRADE_REASON_NO_TRADING; + + // const _local_12 = GetSessionDataManager().userId; + // _local_13 = GetSessionDataManager().getUserTags(_local_12); + // this._Str_16287(_local_12, _local_13); + } + + userInfo.groupId = userData.groupId; + userInfo.groupBadgeId = GetSessionDataManager().getGroupBadge(userInfo.groupId); + userInfo.groupName = userData.groupName; + userInfo.badges = roomSession.userDataManager.getUserBadges(userData.webID); + userInfo.figure = userData.figure; + //var _local_8:Array = GetSessionDataManager().getUserTags(userData.webID); + //this._Str_16287(userData.webId, _local_8); + //this._container.habboGroupsManager.updateVisibleExtendedProfile(userData.webID); + //this._container.connection.send(new GetRelationshipStatusInfoMessageComposer(userData.webId)); + + return userInfo; + } + + public static getBotInfo(category: number, userData: IRoomUserData): AvatarInfoUser + { + const roomSession = GetRoomSession(); + const userInfo = new AvatarInfoUser(AvatarInfoUser.BOT); + + userInfo.name = userData.name; + userInfo.motto = userData.custom; + userInfo.webID = userData.webID; + userInfo.roomIndex = userData.roomIndex; + userInfo.userType = userData.type; + + const roomObject = GetRoomEngine().getRoomObject(roomSession.roomId, userData.roomIndex, category); + + if(roomObject) userInfo.carryItem = (roomObject.model.getValue(RoomObjectVariable.FIGURE_CARRY_OBJECT) || 0); + + userInfo.amIOwner = roomSession.isRoomOwner; + userInfo.isGuildRoom = roomSession.isGuildRoom; + userInfo.roomControllerLevel = roomSession.controllerLevel; + userInfo.amIAnyRoomController = GetSessionDataManager().isModerator; + userInfo.isAmbassador = GetSessionDataManager().isAmbassador; + userInfo.badges = [ AvatarInfoUser.DEFAULT_BOT_BADGE_ID ]; + userInfo.figure = userData.figure; + + return userInfo; + } + + public static getRentableBotInfo(category: number, userData: IRoomUserData): AvatarInfoRentableBot + { + const roomSession = GetRoomSession(); + const botInfo = new AvatarInfoRentableBot(AvatarInfoRentableBot.RENTABLE_BOT); + + botInfo.name = userData.name; + botInfo.motto = userData.custom; + botInfo.webID = userData.webID; + botInfo.roomIndex = userData.roomIndex; + botInfo.ownerId = userData.ownerId; + botInfo.ownerName = userData.ownerName; + botInfo.botSkills = userData.botSkills; + + const roomObject = GetRoomEngine().getRoomObject(roomSession.roomId, userData.roomIndex, category); + + if(roomObject) botInfo.carryItem = (roomObject.model.getValue(RoomObjectVariable.FIGURE_CARRY_OBJECT) || 0); + + botInfo.amIOwner = roomSession.isRoomOwner; + botInfo.roomControllerLevel = roomSession.controllerLevel; + botInfo.amIAnyRoomController = GetSessionDataManager().isModerator; + botInfo.badges = [ AvatarInfoUser.DEFAULT_BOT_BADGE_ID ]; + botInfo.figure = userData.figure; + + return botInfo; + } + + public static getPetInfo(petData: IRoomPetData): AvatarInfoPet + { + const roomSession = GetRoomSession(); + const userData = roomSession.userDataManager.getPetData(petData.id); + + if(!userData) return; + + const figure = new PetFigureData(userData.figure); + + let posture: string = null; + + if(figure.typeId === PetType.MONSTERPLANT) + { + if(petData.level >= petData.adultLevel) posture = 'std'; + else posture = ('grw' + petData.level); + } + + const isOwner = (petData.ownerId === GetSessionDataManager().userId); + const petInfo = new AvatarInfoPet(AvatarInfoPet.PET_INFO); + + petInfo.name = userData.name; + petInfo.id = petData.id; + petInfo.ownerId = petData.ownerId; + petInfo.ownerName = petData.ownerName; + petInfo.rarityLevel = petData.rarityLevel; + petInfo.petType = figure.typeId; + petInfo.petBreed = figure.paletteId; + petInfo.petFigure = userData.figure; + petInfo.posture = posture; + petInfo.isOwner = isOwner; + petInfo.roomIndex = userData.roomIndex; + petInfo.level = petData.level; + petInfo.maximumLevel = petData.maximumLevel; + petInfo.experience = petData.experience; + petInfo.levelExperienceGoal = petData.levelExperienceGoal; + petInfo.energy = petData.energy; + petInfo.maximumEnergy = petData.maximumEnergy; + petInfo.happyness = petData.happyness; + petInfo.maximumHappyness = petData.maximumHappyness; + petInfo.respect = petData.respect; + petInfo.respectsPetLeft = GetSessionDataManager().respectsPetLeft; + petInfo.age = petData.age; + petInfo.saddle = petData.saddle; + petInfo.rider = petData.rider; + petInfo.breedable = petData.breedable; + petInfo.fullyGrown = petData.fullyGrown; + petInfo.dead = petData.dead; + petInfo.rarityLevel = petData.rarityLevel; + petInfo.skillTresholds = petData.skillTresholds; + petInfo.canRemovePet = false; + petInfo.publiclyRideable = petData.publiclyRideable; + petInfo.maximumTimeToLive = petData.maximumTimeToLive; + petInfo.remainingTimeToLive = petData.remainingTimeToLive; + petInfo.remainingGrowTime = petData.remainingGrowTime; + petInfo.publiclyBreedable = petData.publiclyBreedable; + + if(isOwner || roomSession.isRoomOwner || GetSessionDataManager().isModerator || (roomSession.controllerLevel >= RoomControllerLevel.GUEST)) petInfo.canRemovePet = true; + + return petInfo; + } + + private static checkGuildSetting(userInfo: AvatarInfoUser): boolean + { + if(userInfo.isGuildRoom) return (userInfo.roomControllerLevel >= RoomControllerLevel.GUILD_ADMIN); + + return (userInfo.roomControllerLevel >= RoomControllerLevel.GUEST); + } + + private static isValidSetting(userInfo: AvatarInfoUser, checkSetting: (userInfo: AvatarInfoUser, moderation: IRoomModerationSettings) => boolean): boolean + { + const roomSession = GetRoomSession(); + + if(!roomSession.isPrivateRoom) return false; + + const moderation = roomSession.moderationSettings; + + let flag = false; + + if(moderation) flag = checkSetting(userInfo, moderation); + + return (flag && (userInfo.targetRoomControllerLevel < RoomControllerLevel.ROOM_OWNER)); + } + + private static canBeMuted(userInfo: AvatarInfoUser): boolean + { + const checkSetting = (userInfo: AvatarInfoUser, moderation: IRoomModerationSettings) => + { + switch(moderation.allowMute) + { + case RoomModerationSettings.MODERATION_LEVEL_USER_WITH_RIGHTS: + return this.checkGuildSetting(userInfo); + default: + return (userInfo.roomControllerLevel >= RoomControllerLevel.ROOM_OWNER); + } + } + + return this.isValidSetting(userInfo, checkSetting); + } + + private static canBeKicked(userInfo: AvatarInfoUser): boolean + { + const checkSetting = (userInfo: AvatarInfoUser, moderation: IRoomModerationSettings) => + { + switch(moderation.allowKick) + { + case RoomModerationSettings.MODERATION_LEVEL_ALL: + return true; + case RoomModerationSettings.MODERATION_LEVEL_USER_WITH_RIGHTS: + return this.checkGuildSetting(userInfo); + default: + return (userInfo.roomControllerLevel >= RoomControllerLevel.ROOM_OWNER); + } + } + + return this.isValidSetting(userInfo, checkSetting); + } + + private static canBeBanned(userInfo: AvatarInfoUser): boolean + { + const checkSetting = (userInfo: AvatarInfoUser, moderation: IRoomModerationSettings) => + { + switch(moderation.allowBan) + { + case RoomModerationSettings.MODERATION_LEVEL_USER_WITH_RIGHTS: + return this.checkGuildSetting(userInfo); + default: + return (userInfo.roomControllerLevel >= RoomControllerLevel.ROOM_OWNER); + } + } + + return this.isValidSetting(userInfo, checkSetting); + } +} diff --git a/apps/frontend/src/api/room/widgets/BotSkillsEnum.ts b/apps/frontend/src/api/room/widgets/BotSkillsEnum.ts new file mode 100644 index 0000000..b879cdc --- /dev/null +++ b/apps/frontend/src/api/room/widgets/BotSkillsEnum.ts @@ -0,0 +1,18 @@ +export class BotSkillsEnum +{ + public static GENERIC_SKILL: number = 0; + public static DRESS_UP: number = 1; + public static SETUP_CHAT: number = 2; + public static RANDOM_WALK: number = 3; + public static DANCE: number = 4; + public static CHANGE_BOT_NAME: number = 5; + public static SERVE_BEVERAGE: number = 6; + public static INCLIENT_LINK: number = 7; + public static NUX_PROCEED: number = 8; + public static CHANGE_BOT_MOTTO: number = 9; + public static NUX_TAKE_TOUR: number = 10; + public static NO_PICK_UP: number = 12; + public static NAVIGATOR_SEARCH: number = 14; + public static DONATE_TO_USER: number = 24; + public static DONATE_TO_ALL: number = 25; +} diff --git a/apps/frontend/src/api/room/widgets/ChatBubbleMessage.ts b/apps/frontend/src/api/room/widgets/ChatBubbleMessage.ts new file mode 100644 index 0000000..c9cede3 --- /dev/null +++ b/apps/frontend/src/api/room/widgets/ChatBubbleMessage.ts @@ -0,0 +1,56 @@ +import { INitroPoint } from '@nitrots/nitro-renderer'; + +export class ChatBubbleMessage +{ + public static BUBBLE_COUNTER: number = 0; + + public id: number = -1; + public width: number = 0; + public height: number = 0; + public elementRef: HTMLDivElement = null; + public skipMovement: boolean = false; + + private _top: number = 0; + private _left: number = 0; + + constructor( + public senderId: number = -1, + public senderCategory: number = -1, + public roomId: number = -1, + public text: string = '', + public formattedText: string = '', + public username: string = '', + public location: INitroPoint = null, + public type: number = 0, + public styleId: number = 0, + public imageUrl: string = null, + public color: string = null + ) + { + this.id = ++ChatBubbleMessage.BUBBLE_COUNTER; + } + + public get top(): number + { + return this._top; + } + + public set top(value: number) + { + this._top = value; + + if(this.elementRef) this.elementRef.style.top = (this._top + 'px'); + } + + public get left(): number + { + return this._left; + } + + public set left(value: number) + { + this._left = value; + + if(this.elementRef) this.elementRef.style.left = (this._left + 'px'); + } +} diff --git a/apps/frontend/src/api/room/widgets/ChatMessageTypeEnum.ts b/apps/frontend/src/api/room/widgets/ChatMessageTypeEnum.ts new file mode 100644 index 0000000..1a5296b --- /dev/null +++ b/apps/frontend/src/api/room/widgets/ChatMessageTypeEnum.ts @@ -0,0 +1,6 @@ +export class ChatMessageTypeEnum +{ + public static CHAT_DEFAULT: number = 0; + public static CHAT_WHISPER: number = 1; + public static CHAT_SHOUT: number = 2; +} diff --git a/apps/frontend/src/api/room/widgets/DimmerFurnitureWidgetPresetItem.ts b/apps/frontend/src/api/room/widgets/DimmerFurnitureWidgetPresetItem.ts new file mode 100644 index 0000000..1a2759f --- /dev/null +++ b/apps/frontend/src/api/room/widgets/DimmerFurnitureWidgetPresetItem.ts @@ -0,0 +1,9 @@ +export class DimmerFurnitureWidgetPresetItem +{ + constructor( + public id: number = 0, + public type: number = 0, + public color: number = 0, + public light: number = 0) + {} +} diff --git a/apps/frontend/src/api/room/widgets/DoChatsOverlap.ts b/apps/frontend/src/api/room/widgets/DoChatsOverlap.ts new file mode 100644 index 0000000..092ce5d --- /dev/null +++ b/apps/frontend/src/api/room/widgets/DoChatsOverlap.ts @@ -0,0 +1,7 @@ +import { ChatBubbleMessage } from './ChatBubbleMessage'; + +export const DoChatsOverlap = (a: ChatBubbleMessage, b: ChatBubbleMessage, additionalBTop: number, padding: number = 0) => +{ + return !((((a.left + padding) + a.width) < (b.left + padding)) || ((a.left + padding) > ((b.left + padding) + b.width)) || ((a.top + a.height) < (b.top + additionalBTop)) || (a.top > ((b.top + additionalBTop) + b.height))); +} + \ No newline at end of file diff --git a/apps/frontend/src/api/room/widgets/FurnitureDimmerUtilities.ts b/apps/frontend/src/api/room/widgets/FurnitureDimmerUtilities.ts new file mode 100644 index 0000000..9d252d1 --- /dev/null +++ b/apps/frontend/src/api/room/widgets/FurnitureDimmerUtilities.ts @@ -0,0 +1,29 @@ +import { GetRoomEngine, GetRoomSession } from '../../nitro'; + +export class FurnitureDimmerUtilities +{ + public static AVAILABLE_COLORS: number[] = [ 7665141, 21495, 15161822, 15353138, 15923281, 8581961, 0 ]; + public static HTML_COLORS: string[] = [ '#74F5F5', '#0053F7', '#E759DE', '#EA4532', '#F2F851', '#82F349', '#000000' ]; + public static MIN_BRIGHTNESS: number = 76; + public static MAX_BRIGHTNESS: number = 255; + + public static savePreset(presetNumber: number, effectTypeId: number, color: number, brightness: number, apply: boolean): void + { + GetRoomSession().updateMoodlightData(presetNumber, effectTypeId, color, brightness, apply); + } + + public static changeState(): void + { + GetRoomSession().toggleMoodlightState(); + } + + public static previewDimmer(color: number, brightness: number, bgOnly: boolean): void + { + GetRoomEngine().updateObjectRoomColor(GetRoomSession().roomId, color, brightness, bgOnly); + } + + public static scaleBrightness(value: number): number + { + return ~~((((value - this.MIN_BRIGHTNESS) * (100 - 0)) / (this.MAX_BRIGHTNESS - this.MIN_BRIGHTNESS)) + 0); + } +} diff --git a/apps/frontend/src/api/room/widgets/GetDiskColor.ts b/apps/frontend/src/api/room/widgets/GetDiskColor.ts new file mode 100644 index 0000000..989f294 --- /dev/null +++ b/apps/frontend/src/api/room/widgets/GetDiskColor.ts @@ -0,0 +1,37 @@ +const DISK_COLOR_RED_MIN: number = 130; +const DISK_COLOR_RED_RANGE: number = 100; +const DISK_COLOR_GREEN_MIN: number = 130; +const DISK_COLOR_GREEN_RANGE: number = 100; +const DISK_COLOR_BLUE_MIN: number = 130; +const DISK_COLOR_BLUE_RANGE: number = 100; + +export const GetDiskColor = (name: string) => +{ + let r: number = 0; + let g: number = 0; + let b: number = 0; + let index: number = 0; + + while (index < name.length) + { + switch ((index % 3)) + { + case 0: + r = (r + ( name.charCodeAt(index) * 37) ); + break; + case 1: + g = (g + ( name.charCodeAt(index) * 37) ); + break; + case 2: + b = (b + ( name.charCodeAt(index) * 37) ); + break; + } + index++; + } + + r = ((r % DISK_COLOR_RED_RANGE) + DISK_COLOR_RED_MIN); + g = ((g % DISK_COLOR_GREEN_RANGE) + DISK_COLOR_GREEN_MIN); + b = ((b % DISK_COLOR_BLUE_RANGE) + DISK_COLOR_BLUE_MIN); + + return `rgb(${ r },${ g },${ b })`; +} diff --git a/apps/frontend/src/api/room/widgets/IAvatarInfo.ts b/apps/frontend/src/api/room/widgets/IAvatarInfo.ts new file mode 100644 index 0000000..23fb47b --- /dev/null +++ b/apps/frontend/src/api/room/widgets/IAvatarInfo.ts @@ -0,0 +1,4 @@ +export interface IAvatarInfo +{ + type: string; +} diff --git a/apps/frontend/src/api/room/widgets/ICraftingIngredient.ts b/apps/frontend/src/api/room/widgets/ICraftingIngredient.ts new file mode 100644 index 0000000..cb2b031 --- /dev/null +++ b/apps/frontend/src/api/room/widgets/ICraftingIngredient.ts @@ -0,0 +1,6 @@ +export interface ICraftingIngredient +{ + name: string; + iconUrl: string; + count: number; +} diff --git a/apps/frontend/src/api/room/widgets/ICraftingRecipe.ts b/apps/frontend/src/api/room/widgets/ICraftingRecipe.ts new file mode 100644 index 0000000..dd99291 --- /dev/null +++ b/apps/frontend/src/api/room/widgets/ICraftingRecipe.ts @@ -0,0 +1,6 @@ +export interface ICraftingRecipe +{ + name: string; + localizedName: string; + iconUrl: string; +} diff --git a/apps/frontend/src/api/room/widgets/IPhotoData.ts b/apps/frontend/src/api/room/widgets/IPhotoData.ts new file mode 100644 index 0000000..9a7b846 --- /dev/null +++ b/apps/frontend/src/api/room/widgets/IPhotoData.ts @@ -0,0 +1,42 @@ +export interface IPhotoData +{ + /** + * creator username + */ + n?: string; + + /** + * creator user id + */ + s?: number; + + /** + * photo unique id + */ + u?: number; + + /** + * creation timestamp + */ + t?: number; + + /** + * photo caption + */ + m?: string; + + /** + * photo image url + */ + w?: string; + + /** + * owner id + */ + oi?: number; + + /** + * owner name + */ + o?: string; +} \ No newline at end of file diff --git a/apps/frontend/src/api/room/widgets/MannequinUtilities.ts b/apps/frontend/src/api/room/widgets/MannequinUtilities.ts new file mode 100644 index 0000000..5e82b9a --- /dev/null +++ b/apps/frontend/src/api/room/widgets/MannequinUtilities.ts @@ -0,0 +1,39 @@ +import { AvatarFigurePartType, IAvatarFigureContainer } from '@nitrots/nitro-renderer'; +import { GetAvatarRenderManager } from '../../nitro'; + +export class MannequinUtilities +{ + public static MANNEQUIN_FIGURE = [ 'hd', 99999, [ 99998 ] ]; + public static MANNEQUIN_CLOTHING_PART_TYPES = [ + AvatarFigurePartType.CHEST_ACCESSORY, + AvatarFigurePartType.COAT_CHEST, + AvatarFigurePartType.CHEST, + AvatarFigurePartType.LEGS, + AvatarFigurePartType.SHOES, + AvatarFigurePartType.WAIST_ACCESSORY + ]; + + public static getMergedMannequinFigureContainer(figure: string, targetFigure: string): IAvatarFigureContainer + { + const figureContainer = GetAvatarRenderManager().createFigureContainer(figure); + const targetFigureContainer = GetAvatarRenderManager().createFigureContainer(targetFigure); + + for(const part of this.MANNEQUIN_CLOTHING_PART_TYPES) figureContainer.removePart(part); + + for(const part of targetFigureContainer.getPartTypeIds()) figureContainer.updatePart(part, targetFigureContainer.getPartSetId(part), targetFigureContainer.getPartColorIds(part)); + + return figureContainer; + } + + public static transformAsMannequinFigure(figureContainer: IAvatarFigureContainer): void + { + for(const part of figureContainer.getPartTypeIds()) + { + if(this.MANNEQUIN_CLOTHING_PART_TYPES.indexOf(part) >= 0) continue; + + figureContainer.removePart(part); + } + + figureContainer.updatePart((this.MANNEQUIN_FIGURE[0] as string), (this.MANNEQUIN_FIGURE[1] as number), (this.MANNEQUIN_FIGURE[2] as number[])); + }; +} diff --git a/apps/frontend/src/api/room/widgets/PetSupplementEnum.ts b/apps/frontend/src/api/room/widgets/PetSupplementEnum.ts new file mode 100644 index 0000000..eb23687 --- /dev/null +++ b/apps/frontend/src/api/room/widgets/PetSupplementEnum.ts @@ -0,0 +1,5 @@ +export class PetSupplementEnum +{ + public static WATER: number = 0; + public static LIGHT: number = 1; +} diff --git a/apps/frontend/src/api/room/widgets/PostureTypeEnum.ts b/apps/frontend/src/api/room/widgets/PostureTypeEnum.ts new file mode 100644 index 0000000..21352d7 --- /dev/null +++ b/apps/frontend/src/api/room/widgets/PostureTypeEnum.ts @@ -0,0 +1,5 @@ +export class PostureTypeEnum +{ + public static POSTURE_STAND: number = 0; + public static POSTURE_SIT: number = 1; +} diff --git a/apps/frontend/src/api/room/widgets/RoomDimmerPreset.ts b/apps/frontend/src/api/room/widgets/RoomDimmerPreset.ts new file mode 100644 index 0000000..86600d5 --- /dev/null +++ b/apps/frontend/src/api/room/widgets/RoomDimmerPreset.ts @@ -0,0 +1,35 @@ +export class RoomDimmerPreset +{ + private _id: number; + private _type: number; + private _color: number; + private _brightness: number; + + constructor(id: number, type: number, color: number, brightness: number) + { + this._id = id; + this._type = type; + this._color = color; + this._brightness = brightness; + } + + public get id(): number + { + return this._id; + } + + public get type(): number + { + return this._type; + } + + public get color(): number + { + return this._color; + } + + public get brightness(): number + { + return this._brightness; + } +} diff --git a/apps/frontend/src/api/room/widgets/RoomObjectItem.ts b/apps/frontend/src/api/room/widgets/RoomObjectItem.ts new file mode 100644 index 0000000..f4fb2d6 --- /dev/null +++ b/apps/frontend/src/api/room/widgets/RoomObjectItem.ts @@ -0,0 +1,28 @@ +export class RoomObjectItem +{ + private _id: number; + private _category: number; + private _name: string; + + constructor(id: number, category: number, name: string) + { + this._id = id; + this._category = category; + this._name = name; + } + + public get id(): number + { + return this._id; + } + + public get category(): number + { + return this._category; + } + + public get name(): string + { + return this._name; + } +} diff --git a/apps/frontend/src/api/room/widgets/UseProductItem.ts b/apps/frontend/src/api/room/widgets/UseProductItem.ts new file mode 100644 index 0000000..d3e2088 --- /dev/null +++ b/apps/frontend/src/api/room/widgets/UseProductItem.ts @@ -0,0 +1,12 @@ +export class UseProductItem +{ + constructor( + public readonly id: number, + public readonly category: number, + public readonly name: string, + public readonly requestRoomObjectId: number, + public readonly targetRoomObjectId: number, + public readonly requestInventoryStripId: number, + public readonly replace: boolean) + {} +} diff --git a/apps/frontend/src/api/room/widgets/VoteValue.ts b/apps/frontend/src/api/room/widgets/VoteValue.ts new file mode 100644 index 0000000..ecf4336 --- /dev/null +++ b/apps/frontend/src/api/room/widgets/VoteValue.ts @@ -0,0 +1,8 @@ +export const VALUE_KEY_DISLIKE = '0'; +export const VALUE_KEY_LIKE = '1'; + +export interface VoteValue +{ + value: string; + secondsLeft: number; +} diff --git a/apps/frontend/src/api/room/widgets/YoutubeVideoPlaybackStateEnum.ts b/apps/frontend/src/api/room/widgets/YoutubeVideoPlaybackStateEnum.ts new file mode 100644 index 0000000..3a885d1 --- /dev/null +++ b/apps/frontend/src/api/room/widgets/YoutubeVideoPlaybackStateEnum.ts @@ -0,0 +1,9 @@ +export class YoutubeVideoPlaybackStateEnum +{ + public static readonly UNSTARTED = -1; + public static readonly ENDED = 0; + public static readonly PLAYING = 1; + public static readonly PAUSED = 2; + public static readonly BUFFERING = 3; + public static readonly CUED = 5; +} diff --git a/apps/frontend/src/api/room/widgets/index.ts b/apps/frontend/src/api/room/widgets/index.ts new file mode 100644 index 0000000..c43d6aa --- /dev/null +++ b/apps/frontend/src/api/room/widgets/index.ts @@ -0,0 +1,25 @@ +export * from './AvatarInfoFurni'; +export * from './AvatarInfoName'; +export * from './AvatarInfoPet'; +export * from './AvatarInfoRentableBot'; +export * from './AvatarInfoUser'; +export * from './AvatarInfoUtilities'; +export * from './BotSkillsEnum'; +export * from './ChatBubbleMessage'; +export * from './ChatMessageTypeEnum'; +export * from './DimmerFurnitureWidgetPresetItem'; +export * from './DoChatsOverlap'; +export * from './FurnitureDimmerUtilities'; +export * from './GetDiskColor'; +export * from './IAvatarInfo'; +export * from './ICraftingIngredient'; +export * from './ICraftingRecipe'; +export * from './IPhotoData'; +export * from './MannequinUtilities'; +export * from './PetSupplementEnum'; +export * from './PostureTypeEnum'; +export * from './RoomDimmerPreset'; +export * from './RoomObjectItem'; +export * from './UseProductItem'; +export * from './VoteValue'; +export * from './YoutubeVideoPlaybackStateEnum'; diff --git a/apps/frontend/src/api/user/GetUserProfile.ts b/apps/frontend/src/api/user/GetUserProfile.ts new file mode 100644 index 0000000..0f2be77 --- /dev/null +++ b/apps/frontend/src/api/user/GetUserProfile.ts @@ -0,0 +1,7 @@ +import { UserProfileComposer } from '@nitrots/nitro-renderer'; +import { SendMessageComposer } from '..'; + +export function GetUserProfile(userId: number): void +{ + SendMessageComposer(new UserProfileComposer(userId)); +} diff --git a/apps/frontend/src/api/user/index.ts b/apps/frontend/src/api/user/index.ts new file mode 100644 index 0000000..1c609ea --- /dev/null +++ b/apps/frontend/src/api/user/index.ts @@ -0,0 +1 @@ +export * from './GetUserProfile'; diff --git a/apps/frontend/src/api/utils/CloneObject.ts b/apps/frontend/src/api/utils/CloneObject.ts new file mode 100644 index 0000000..6cd8d3e --- /dev/null +++ b/apps/frontend/src/api/utils/CloneObject.ts @@ -0,0 +1,14 @@ +export const CloneObject = (object: T): T => +{ + if((object == null) || ('object' != typeof object)) return object; + + // @ts-ignore + const copy = new object.constructor(); + + for(const attr in object) + { + if(object.hasOwnProperty(attr)) copy[attr] = object[attr]; + } + + return copy; +} diff --git a/apps/frontend/src/api/utils/ColorUtils.ts b/apps/frontend/src/api/utils/ColorUtils.ts new file mode 100644 index 0000000..c32f8fb --- /dev/null +++ b/apps/frontend/src/api/utils/ColorUtils.ts @@ -0,0 +1,65 @@ +export class ColorUtils +{ + public static makeColorHex(color: string): string + { + return ('#' + color); + } + + public static makeColorNumberHex(color: number): string + { + let val = color.toString(16); + return ( '#' + val.padStart(6, '0')); + } + + public static convertFromHex(color: string): number + { + return parseInt(color.replace('#', ''), 16); + } + + public static uintHexColor(color: number): string + { + const realColor = color >>>0; + + return ColorUtils.makeColorHex(realColor.toString(16).substring(2)); + } + + /** + * Converts an integer format into an array of 8-bit values + * @param {number} value value in integer format + * @returns {Array} 8-bit values + */ + public static int_to_8BitVals(value: number): [number, number, number, number] + { + const val1 = ((value >> 24) & 0xFF) + const val2 = ((value >> 16) & 0xFF); + const val3 = ((value >> 8) & 0xFF); + const val4 = (value & 0xFF); + + return [ val1, val2, val3, val4 ]; + } + + /** + * Combines 4 8-bit values into a 32-bit integer. Values are combined in + * in the order of the parameters + * @param val1 + * @param val2 + * @param val3 + * @param val4 + * @returns 32-bit integer of combined values + */ + public static eight_bitVals_to_int(val1: number, val2: number, val3: number, val4: number): number + { + return (((val1) << 24) + ((val2) << 16) + ((val3) << 8) + (val4| 0)); + } + + public static int2rgb(color: number): string + { + color >>>= 0; + const b = color & 0xFF; + const g = (color & 0xFF00) >>> 8; + const r = (color & 0xFF0000) >>> 16; + const a = ((color & 0xFF000000) >>> 24) / 255; + + return 'rgba(' + [ r, g, b, 1 ].join(',') + ')'; + } +} diff --git a/apps/frontend/src/api/utils/ConvertSeconds.ts b/apps/frontend/src/api/utils/ConvertSeconds.ts new file mode 100644 index 0000000..f559dea --- /dev/null +++ b/apps/frontend/src/api/utils/ConvertSeconds.ts @@ -0,0 +1,9 @@ +export const ConvertSeconds = (seconds: number) => +{ + let numDays = Math.floor(seconds / 86400); + let numHours = Math.floor((seconds % 86400) / 3600); + let numMinutes = Math.floor(((seconds % 86400) % 3600) / 60); + let numSeconds = ((seconds % 86400) % 3600) % 60; + + return numDays.toString().padStart(2, '0') + ':' + numHours.toString().padStart(2, '0') + ':' + numMinutes.toString().padStart(2, '0') + ':' + numSeconds.toString().padStart(2, '0'); +} diff --git a/apps/frontend/src/api/utils/GetLocalStorage.ts b/apps/frontend/src/api/utils/GetLocalStorage.ts new file mode 100644 index 0000000..769df6d --- /dev/null +++ b/apps/frontend/src/api/utils/GetLocalStorage.ts @@ -0,0 +1,11 @@ +export const GetLocalStorage = (key: string) => +{ + try + { + JSON.parse(window.localStorage.getItem(key)) as T ?? null + } + catch(e) + { + return null; + } +} diff --git a/apps/frontend/src/api/utils/LocalStorageKeys.ts b/apps/frontend/src/api/utils/LocalStorageKeys.ts new file mode 100644 index 0000000..6c92279 --- /dev/null +++ b/apps/frontend/src/api/utils/LocalStorageKeys.ts @@ -0,0 +1,5 @@ +export class LocalStorageKeys +{ + public static CATALOG_PLACE_MULTIPLE_OBJECTS: string = 'catalogPlaceMultipleObjects'; + public static CATALOG_SKIP_PURCHASE_CONFIRMATION: string = 'catalogSkipPurchaseConfirmation'; +} diff --git a/apps/frontend/src/api/utils/LocalizeBadgeDescription.ts b/apps/frontend/src/api/utils/LocalizeBadgeDescription.ts new file mode 100644 index 0000000..04fd7df --- /dev/null +++ b/apps/frontend/src/api/utils/LocalizeBadgeDescription.ts @@ -0,0 +1,10 @@ +import { GetNitroInstance } from '..'; + +export const LocalizeBadgeDescription = (key: string) => +{ + let badgeDesc = GetNitroInstance().localization.getBadgeDesc(key); + + if(!badgeDesc || !badgeDesc.length) badgeDesc = `badge_desc_${ key }`; + + return badgeDesc; +} diff --git a/apps/frontend/src/api/utils/LocalizeBageName.ts b/apps/frontend/src/api/utils/LocalizeBageName.ts new file mode 100644 index 0000000..d722ab0 --- /dev/null +++ b/apps/frontend/src/api/utils/LocalizeBageName.ts @@ -0,0 +1,10 @@ +import { GetNitroInstance } from '..'; + +export const LocalizeBadgeName = (key: string) => +{ + let badgeName = GetNitroInstance().localization.getBadgeName(key); + + if(!badgeName || !badgeName.length) badgeName = `badge_name_${ key }`; + + return badgeName; +} diff --git a/apps/frontend/src/api/utils/LocalizeFormattedNumber.ts b/apps/frontend/src/api/utils/LocalizeFormattedNumber.ts new file mode 100644 index 0000000..fab30d4 --- /dev/null +++ b/apps/frontend/src/api/utils/LocalizeFormattedNumber.ts @@ -0,0 +1,6 @@ +export function LocalizeFormattedNumber(number: number): string +{ + if(!number || isNaN(number)) return '0'; + + return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' '); +}; diff --git a/apps/frontend/src/api/utils/LocalizeShortNumber.ts b/apps/frontend/src/api/utils/LocalizeShortNumber.ts new file mode 100644 index 0000000..30975ec --- /dev/null +++ b/apps/frontend/src/api/utils/LocalizeShortNumber.ts @@ -0,0 +1,36 @@ +export function LocalizeShortNumber(number: number): string +{ + if(!number || isNaN(number)) return '0'; + + let abs = Math.abs(number); + + const rounder = Math.pow(10, 1); + const isNegative = (number < 0); + + let key = ''; + + const powers = [ + { key: 'Q', value: Math.pow(10, 15) }, + { key: 'T', value: Math.pow(10, 12) }, + { key: 'B', value: Math.pow(10, 9) }, + { key: 'M', value: Math.pow(10, 6) }, + { key: 'K', value: 1000 } + ]; + + for(const power of powers) + { + let reduced = abs / power.value; + + reduced = Math.round(reduced * rounder) / rounder; + + if(reduced >= 1) + { + abs = reduced; + key = power.key; + + break; + } + } + + return ((isNegative ? '-' : '') + abs + key); +} diff --git a/apps/frontend/src/api/utils/LocalizeText.ts b/apps/frontend/src/api/utils/LocalizeText.ts new file mode 100644 index 0000000..cf34a1f --- /dev/null +++ b/apps/frontend/src/api/utils/LocalizeText.ts @@ -0,0 +1,6 @@ +import { GetNitroInstance } from '..'; + +export function LocalizeText(key: string, parameters: string[] = null, replacements: string[] = null): string +{ + return GetNitroInstance().getLocalizationWithParameters(key, parameters, replacements); +} diff --git a/apps/frontend/src/api/utils/PlaySound.ts b/apps/frontend/src/api/utils/PlaySound.ts new file mode 100644 index 0000000..0f9a39d --- /dev/null +++ b/apps/frontend/src/api/utils/PlaySound.ts @@ -0,0 +1,24 @@ +import { MouseEventType, NitroSoundEvent } from '@nitrots/nitro-renderer'; +import { DispatchMainEvent } from '../events'; + +let canPlaySound = false; + +export const PlaySound = (sampleCode: string) => +{ + if(!canPlaySound) return; + + DispatchMainEvent(new NitroSoundEvent(NitroSoundEvent.PLAY_SOUND, sampleCode)); +} + +const eventTypes = [ MouseEventType.MOUSE_CLICK ]; + +const startListening = () => +{ + const stopListening = () => eventTypes.forEach(type => window.removeEventListener(type, onEvent)); + + const onEvent = (event: Event) => ((canPlaySound = true) && stopListening()); + + eventTypes.forEach(type => window.addEventListener(type, onEvent)); +} + +startListening(); diff --git a/apps/frontend/src/api/utils/ProductImageUtility.ts b/apps/frontend/src/api/utils/ProductImageUtility.ts new file mode 100644 index 0000000..59c8c4f --- /dev/null +++ b/apps/frontend/src/api/utils/ProductImageUtility.ts @@ -0,0 +1,59 @@ +import { CatalogPageMessageProductData } from '@nitrots/nitro-renderer'; +import { FurniCategory } from '../inventory'; +import { GetRoomEngine } from '../nitro'; + +export class ProductImageUtility +{ + public static getProductImageUrl(productType: string, furniClassId: number, extraParam: string): string + { + let imageUrl: string = null; + + switch(productType) + { + case CatalogPageMessageProductData.S: + imageUrl = GetRoomEngine().getFurnitureFloorIconUrl(furniClassId); + break; + case CatalogPageMessageProductData.I: + const productCategory = this.getProductCategory(CatalogPageMessageProductData.I, furniClassId); + + if(productCategory === 1) + { + imageUrl = GetRoomEngine().getFurnitureWallIconUrl(furniClassId, extraParam); + } + else + { + switch(productCategory) + { + case FurniCategory.WALL_PAPER: + break; + case FurniCategory.LANDSCAPE: + break; + case FurniCategory.FLOOR: + break; + } + } + break; + case CatalogPageMessageProductData.E: + // fx_icon_furniClassId_png + break; + } + + return imageUrl; + } + + public static getProductCategory(productType: string, furniClassId: number): number + { + if(productType === CatalogPageMessageProductData.S) return 1; + + if(productType === CatalogPageMessageProductData.I) + { + if(furniClassId === 3001) return FurniCategory.WALL_PAPER; + + if(furniClassId === 3002) return FurniCategory.FLOOR; + + if(furniClassId === 4057) return FurniCategory.LANDSCAPE; + } + + return 1; + } +} diff --git a/apps/frontend/src/api/utils/Randomizer.ts b/apps/frontend/src/api/utils/Randomizer.ts new file mode 100644 index 0000000..1f67a12 --- /dev/null +++ b/apps/frontend/src/api/utils/Randomizer.ts @@ -0,0 +1,28 @@ +export class Randomizer +{ + public static getRandomNumber(count: number): number + { + return Math.floor(Math.random() * count); + } + + public static getRandomElement(elements: T[]): T + { + return elements[this.getRandomNumber(elements.length)]; + } + + public static getRandomElements(elements: T[], count: number): T[] + { + const result: T[] = new Array(count); + let len = elements.length; + const taken = new Array(len); + + while(count--) + { + var x = this.getRandomNumber(len); + result[count] = elements[x in taken ? taken[x] : x]; + taken[x] = --len in taken ? taken[len] : len; + } + + return result; + } +} diff --git a/apps/frontend/src/api/utils/RoomChatFormatter.ts b/apps/frontend/src/api/utils/RoomChatFormatter.ts new file mode 100644 index 0000000..a24cdf5 --- /dev/null +++ b/apps/frontend/src/api/utils/RoomChatFormatter.ts @@ -0,0 +1,75 @@ +const allowedColours: Map = new Map(); + +allowedColours.set('r', 'red'); +allowedColours.set('b', 'blue'); +allowedColours.set('g', 'green'); +allowedColours.set('y', 'yellow'); +allowedColours.set('w', 'white'); +allowedColours.set('o', 'orange'); +allowedColours.set('c', 'cyan'); +allowedColours.set('br', 'brown'); +allowedColours.set('pr', 'purple'); +allowedColours.set('pk', 'pink'); + +allowedColours.set('red', 'red'); +allowedColours.set('blue', 'blue'); +allowedColours.set('green', 'green'); +allowedColours.set('yellow', 'yellow'); +allowedColours.set('white', 'white'); +allowedColours.set('orange', 'orange'); +allowedColours.set('cyan', 'cyan'); +allowedColours.set('brown', 'brown'); +allowedColours.set('purple', 'purple'); +allowedColours.set('pink', 'pink'); + +const encodeHTML = (str: string) => +{ + return str.replace(/([\u00A0-\u9999<>&])(.|$)/g, (full, char, next) => + { + if(char !== '&' || next !== '#') + { + if(/[\u00A0-\u9999<>&]/.test(next)) next = '&#' + next.charCodeAt(0) + ';'; + + return '&#' + char.charCodeAt(0) + ';' + next; + } + + return full; + }); +} + +export const RoomChatFormatter = (content: string) => +{ + let result = ''; + + content = encodeHTML(content); + //content = (joypixels.shortnameToUnicode(content) as string) + + if(content.startsWith('@') && content.indexOf('@', 1) > -1) + { + let match = null; + + while((match = /@[a-zA-Z]+@/g.exec(content)) !== null) + { + const colorTag = match[0].toString(); + const colorName = colorTag.substr(1, colorTag.length - 2); + const text = content.replace(colorTag, ''); + + if(!allowedColours.has(colorName)) + { + result = text; + } + else + { + const color = allowedColours.get(colorName); + result = '' + text + ''; + } + break; + } + } + else + { + result = content; + } + + return result; +} diff --git a/apps/frontend/src/api/utils/SetLocalStorage.ts b/apps/frontend/src/api/utils/SetLocalStorage.ts new file mode 100644 index 0000000..02aa8f3 --- /dev/null +++ b/apps/frontend/src/api/utils/SetLocalStorage.ts @@ -0,0 +1 @@ +export const SetLocalStorage = (key: string, value: T) => window.localStorage.setItem(key, JSON.stringify(value)); diff --git a/apps/frontend/src/api/utils/SoundNames.ts b/apps/frontend/src/api/utils/SoundNames.ts new file mode 100644 index 0000000..4459651 --- /dev/null +++ b/apps/frontend/src/api/utils/SoundNames.ts @@ -0,0 +1,9 @@ +export class SoundNames +{ + public static CAMERA_SHUTTER = 'camera_shutter'; + public static CREDITS = 'credits'; + public static DUCKETS = 'duckets'; + public static MESSENGER_NEW_THREAD = 'messenger_new_thread'; + public static MESSENGER_MESSAGE_RECEIVED = 'messenger_message_received'; + public static MODTOOLS_NEW_TICKET = 'modtools_new_ticket'; +} diff --git a/apps/frontend/src/api/utils/WindowSaveOptions.ts b/apps/frontend/src/api/utils/WindowSaveOptions.ts new file mode 100644 index 0000000..9aa8456 --- /dev/null +++ b/apps/frontend/src/api/utils/WindowSaveOptions.ts @@ -0,0 +1,5 @@ +export interface WindowSaveOptions +{ + offset: { x: number, y: number }; + size: { width: number, height: number }; +} diff --git a/apps/frontend/src/api/utils/index.ts b/apps/frontend/src/api/utils/index.ts new file mode 100644 index 0000000..0c51fc4 --- /dev/null +++ b/apps/frontend/src/api/utils/index.ts @@ -0,0 +1,17 @@ +export * from './CloneObject'; +export * from './ColorUtils'; +export * from './ConvertSeconds'; +export * from './GetLocalStorage'; +export * from './LocalizeBadgeDescription'; +export * from './LocalizeBageName'; +export * from './LocalizeFormattedNumber'; +export * from './LocalizeShortNumber'; +export * from './LocalizeText'; +export * from './LocalStorageKeys'; +export * from './PlaySound'; +export * from './ProductImageUtility'; +export * from './Randomizer'; +export * from './RoomChatFormatter'; +export * from './SetLocalStorage'; +export * from './SoundNames'; +export * from './WindowSaveOptions'; diff --git a/apps/frontend/src/api/wired/GetWiredTimeLocale.ts b/apps/frontend/src/api/wired/GetWiredTimeLocale.ts new file mode 100644 index 0000000..49025fe --- /dev/null +++ b/apps/frontend/src/api/wired/GetWiredTimeLocale.ts @@ -0,0 +1,8 @@ +export const GetWiredTimeLocale = (value: number) => +{ + const time = Math.floor((value / 2)); + + if(!(value % 2)) return time.toString(); + + return (time + 0.5).toString(); +} diff --git a/apps/frontend/src/api/wired/WiredActionLayoutCode.ts b/apps/frontend/src/api/wired/WiredActionLayoutCode.ts new file mode 100644 index 0000000..5282dc5 --- /dev/null +++ b/apps/frontend/src/api/wired/WiredActionLayoutCode.ts @@ -0,0 +1,29 @@ +export class WiredActionLayoutCode +{ + public static TOGGLE_FURNI_STATE: number = 0; + public static RESET: number = 1; + public static SET_FURNI_STATE: number = 3; + public static MOVE_FURNI: number = 4; + public static GIVE_SCORE: number = 6; + public static CHAT: number = 7; + public static TELEPORT: number = 8; + public static JOIN_TEAM: number = 9; + public static LEAVE_TEAM: number = 10; + public static CHASE: number = 11; + public static FLEE: number = 12; + public static MOVE_AND_ROTATE_FURNI: number = 13; + public static GIVE_SCORE_TO_PREDEFINED_TEAM: number = 14; + public static TOGGLE_TO_RANDOM_STATE: number = 15; + public static MOVE_FURNI_TO: number = 16; + public static GIVE_REWARD: number = 17; + public static CALL_ANOTHER_STACK: number = 18; + public static KICK_FROM_ROOM: number = 19; + public static MUTE_USER: number = 20; + public static BOT_TELEPORT: number = 21; + public static BOT_MOVE: number = 22; + public static BOT_TALK: number = 23; + public static BOT_GIVE_HAND_ITEM: number = 24; + public static BOT_FOLLOW_AVATAR: number = 25; + public static BOT_CHANGE_FIGURE: number = 26; + public static BOT_TALK_DIRECT_TO_AVTR: number = 27; +} diff --git a/apps/frontend/src/api/wired/WiredConditionLayoutCode.ts b/apps/frontend/src/api/wired/WiredConditionLayoutCode.ts new file mode 100644 index 0000000..58cae5d --- /dev/null +++ b/apps/frontend/src/api/wired/WiredConditionLayoutCode.ts @@ -0,0 +1,29 @@ +export class WiredConditionlayout +{ + public static STATES_MATCH: number = 0; + public static FURNIS_HAVE_AVATARS: number = 1; + public static ACTOR_IS_ON_FURNI: number = 2; + public static TIME_ELAPSED_MORE: number = 3; + public static TIME_ELAPSED_LESS: number = 4; + public static USER_COUNT_IN: number = 5; + public static ACTOR_IS_IN_TEAM: number = 6; + public static HAS_STACKED_FURNIS: number = 7; + public static STUFF_TYPE_MATCHES: number = 8; + public static STUFFS_IN_FORMATION: number = 9; + public static ACTOR_IS_GROUP_MEMBER: number = 10; + public static ACTOR_IS_WEARING_BADGE: number = 11; + public static ACTOR_IS_WEARING_EFFECT: number = 12; + public static NOT_STATES_MATCH: number = 13; + public static FURNI_NOT_HAVE_HABBO: number = 14; + public static NOT_ACTOR_ON_FURNI: number = 15; + public static NOT_USER_COUNT_IN: number = 16; + public static NOT_ACTOR_IN_TEAM: number = 17; + public static NOT_HAS_STACKED_FURNIS: number = 18; + public static NOT_FURNI_IS_OF_TYPE: number = 19; + public static NOT_STUFFS_IN_FORMATION: number = 20; + public static NOT_ACTOR_IN_GROUP: number = 21; + public static NOT_ACTOR_WEARS_BADGE: number = 22; + public static NOT_ACTOR_WEARING_EFFECT: number = 23; + public static DATE_RANGE_ACTIVE: number = 24; + public static ACTOR_HAS_HANDITEM: number = 25; +} diff --git a/apps/frontend/src/api/wired/WiredDateToString.ts b/apps/frontend/src/api/wired/WiredDateToString.ts new file mode 100644 index 0000000..825adc8 --- /dev/null +++ b/apps/frontend/src/api/wired/WiredDateToString.ts @@ -0,0 +1 @@ +export const WiredDateToString = (date: Date) => `${ date.getFullYear() }/${ ('0' + (date.getMonth() + 1)).slice(-2) }/${ ('0' + date.getDate()).slice(-2) } ${ ('0' + date.getHours()).slice(-2) }:${ ('0' + date.getMinutes()).slice(-2) }`; diff --git a/apps/frontend/src/api/wired/WiredFurniType.ts b/apps/frontend/src/api/wired/WiredFurniType.ts new file mode 100644 index 0000000..447e970 --- /dev/null +++ b/apps/frontend/src/api/wired/WiredFurniType.ts @@ -0,0 +1,7 @@ +export class WiredFurniType +{ + public static STUFF_SELECTION_OPTION_NONE: number = 0; + public static STUFF_SELECTION_OPTION_BY_ID: number = 1; + public static STUFF_SELECTION_OPTION_BY_ID_OR_BY_TYPE: number = 2; + public static STUFF_SELECTION_OPTION_BY_ID_BY_TYPE_OR_FROM_CONTEXT: number = 3; +} diff --git a/apps/frontend/src/api/wired/WiredSelectionFilter.ts b/apps/frontend/src/api/wired/WiredSelectionFilter.ts new file mode 100644 index 0000000..d661bcf --- /dev/null +++ b/apps/frontend/src/api/wired/WiredSelectionFilter.ts @@ -0,0 +1,95 @@ +import { ColorConverter, NitroFilter } from '@nitrots/nitro-renderer'; + +const vertex = ` +attribute vec2 aVertexPosition; +attribute vec2 aTextureCoord; +uniform mat3 projectionMatrix; +varying vec2 vTextureCoord; +void main(void) +{ + gl_Position = vec4((projectionMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); + vTextureCoord = aTextureCoord; +}`; + +const fragment = ` +varying vec2 vTextureCoord; +uniform sampler2D uSampler; +uniform vec3 lineColor; +uniform vec3 color; +void main(void) { + vec4 currentColor = texture2D(uSampler, vTextureCoord); + vec3 colorLine = lineColor * currentColor.a; + vec3 colorOverlay = color * currentColor.a; + + if(currentColor.r == 0.0 && currentColor.g == 0.0 && currentColor.b == 0.0 && currentColor.a > 0.0) { + gl_FragColor = vec4(colorLine.r, colorLine.g, colorLine.b, currentColor.a); + } else if(currentColor.a > 0.0) { + gl_FragColor = vec4(colorOverlay.r, colorOverlay.g, colorOverlay.b, currentColor.a); + } +}`; + +export class WiredSelectionFilter extends NitroFilter +{ + private _lineColor: number; + private _color: number; + + constructor(lineColor: number | number[], color: number | number[]) + { + super(vertex, fragment); + + this.uniforms.lineColor = new Float32Array(3); + this.uniforms.color = new Float32Array(3); + this.lineColor = lineColor; + this.color = color; + } + + public get lineColor(): number | number[] + { + return this._lineColor; + } + + public set lineColor(value: number | number[]) + { + const arr = this.uniforms.lineColor; + + if(typeof value === 'number') + { + ColorConverter.hex2rgb(value, arr); + + this._lineColor = value; + } + else + { + arr[0] = value[0]; + arr[1] = value[1]; + arr[2] = value[2]; + + this._lineColor = ColorConverter.rgb2hex(arr); + } + } + + public get color(): number | number[] + { + return this._color; + } + + public set color(value: number | number[]) + { + const arr = this.uniforms.color; + + if(typeof value === 'number') + { + ColorConverter.hex2rgb(value, arr); + + this._color = value; + } + else + { + arr[0] = value[0]; + arr[1] = value[1]; + arr[2] = value[2]; + + this._color = ColorConverter.rgb2hex(arr); + } + } +} diff --git a/apps/frontend/src/api/wired/WiredSelectionVisualizer.ts b/apps/frontend/src/api/wired/WiredSelectionVisualizer.ts new file mode 100644 index 0000000..7f5c332 --- /dev/null +++ b/apps/frontend/src/api/wired/WiredSelectionVisualizer.ts @@ -0,0 +1,68 @@ +import { IRoomObject, IRoomObjectSpriteVisualization, NitroFilter, RoomObjectCategory } from '@nitrots/nitro-renderer'; +import { WiredSelectionFilter } from '.'; +import { GetRoomEngine } from '..'; + +export class WiredSelectionVisualizer +{ + private static _selectionShader: NitroFilter = new WiredSelectionFilter([ 1, 1, 1 ], [ 0.6, 0.6, 0.6 ]); + + public static show(furniId: number): void + { + WiredSelectionVisualizer.applySelectionShader(WiredSelectionVisualizer.getRoomObject(furniId)); + } + + public static hide(furniId: number): void + { + WiredSelectionVisualizer.clearSelectionShader(WiredSelectionVisualizer.getRoomObject(furniId)); + } + + public static clearSelectionShaderFromFurni(furniIds: number[]): void + { + for(const furniId of furniIds) + { + WiredSelectionVisualizer.clearSelectionShader(WiredSelectionVisualizer.getRoomObject(furniId)); + } + } + + public static applySelectionShaderToFurni(furniIds: number[]): void + { + for(const furniId of furniIds) + { + WiredSelectionVisualizer.applySelectionShader(WiredSelectionVisualizer.getRoomObject(furniId)); + } + } + + private static getRoomObject(objectId: number): IRoomObject + { + const roomEngine = GetRoomEngine(); + + return roomEngine.getRoomObject(roomEngine.activeRoomId, objectId, RoomObjectCategory.FLOOR); + } + + private static applySelectionShader(roomObject: IRoomObject): void + { + if(!roomObject) return; + + const visualization = (roomObject.visualization as IRoomObjectSpriteVisualization); + + if(!visualization) return; + + for(const sprite of visualization.sprites) + { + if(sprite.blendMode === 1) continue; // BLEND_MODE: ADD + + sprite.filters = [ WiredSelectionVisualizer._selectionShader ]; + } + } + + private static clearSelectionShader(roomObject: IRoomObject): void + { + if(!roomObject) return; + + const visualization = (roomObject.visualization as IRoomObjectSpriteVisualization); + + if(!visualization) return; + + for(const sprite of visualization.sprites) sprite.filters = []; + } +} diff --git a/apps/frontend/src/api/wired/WiredStringDelimeter.ts b/apps/frontend/src/api/wired/WiredStringDelimeter.ts new file mode 100644 index 0000000..bc4cf2e --- /dev/null +++ b/apps/frontend/src/api/wired/WiredStringDelimeter.ts @@ -0,0 +1 @@ +export const WIRED_STRING_DELIMETER: string = '\t'; diff --git a/apps/frontend/src/api/wired/WiredTriggerLayoutCode.ts b/apps/frontend/src/api/wired/WiredTriggerLayoutCode.ts new file mode 100644 index 0000000..fd758df --- /dev/null +++ b/apps/frontend/src/api/wired/WiredTriggerLayoutCode.ts @@ -0,0 +1,17 @@ +export class WiredTriggerLayout +{ + public static AVATAR_SAYS_SOMETHING: number = 0; + public static AVATAR_WALKS_ON_FURNI: number = 1; + public static AVATAR_WALKS_OFF_FURNI: number = 2; + public static EXECUTE_ONCE: number = 3; + public static TOGGLE_FURNI: number = 4; + public static EXECUTE_PERIODICALLY: number = 6; + public static AVATAR_ENTERS_ROOM: number = 7; + public static GAME_STARTS: number = 8; + public static GAME_ENDS: number = 9; + public static SCORE_ACHIEVED: number = 10; + public static COLLISION: number = 11; + public static EXECUTE_PERIODICALLY_LONG: number = 12; + public static BOT_REACHED_STUFF: number = 13; + public static BOT_REACHED_AVATAR: number = 14; +} diff --git a/apps/frontend/src/api/wired/index.ts b/apps/frontend/src/api/wired/index.ts new file mode 100644 index 0000000..e855881 --- /dev/null +++ b/apps/frontend/src/api/wired/index.ts @@ -0,0 +1,9 @@ +export * from './GetWiredTimeLocale'; +export * from './WiredActionLayoutCode'; +export * from './WiredConditionLayoutCode'; +export * from './WiredDateToString'; +export * from './WiredFurniType'; +export * from './WiredSelectionFilter'; +export * from './WiredSelectionVisualizer'; +export * from './WiredStringDelimeter'; +export * from './WiredTriggerLayoutCode'; diff --git a/apps/frontend/src/app/app.module.scss b/apps/frontend/src/app/app.module.scss deleted file mode 100644 index 7b88fba..0000000 --- a/apps/frontend/src/app/app.module.scss +++ /dev/null @@ -1 +0,0 @@ -/* Your styles goes here. */ diff --git a/apps/frontend/src/app/app.spec.tsx b/apps/frontend/src/app/app.spec.tsx deleted file mode 100644 index 95caf44..0000000 --- a/apps/frontend/src/app/app.spec.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { render } from '@testing-library/react'; - -import App from './app'; - -describe('App', () => { - it('should render successfully', () => { - const { baseElement } = render(); - expect(baseElement).toBeTruthy(); - }); - - it('should have a greeting as the title', () => { - const { getByText } = render(); - expect(getByText(/Welcome frontend/gi)).toBeTruthy(); - }); -}); diff --git a/apps/frontend/src/app/app.tsx b/apps/frontend/src/app/app.tsx deleted file mode 100644 index 3247e9e..0000000 --- a/apps/frontend/src/app/app.tsx +++ /dev/null @@ -1,16 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import styles from './app.module.scss'; - -import NxWelcome from './nx-welcome'; - -export function App() { - return ( - <> - - -
- - ); -} - -export default App; diff --git a/apps/frontend/src/app/nx-welcome.tsx b/apps/frontend/src/app/nx-welcome.tsx deleted file mode 100644 index 761a47a..0000000 --- a/apps/frontend/src/app/nx-welcome.tsx +++ /dev/null @@ -1,820 +0,0 @@ -/* - * * * * * * * * * * * * * * * * * * * * * * * * * * * * - This is a starter component and can be deleted. - * * * * * * * * * * * * * * * * * * * * * * * * * * * * - Delete this file and get started with your project! - * * * * * * * * * * * * * * * * * * * * * * * * * * * * - */ -export function NxWelcome({ title }: { title: string }) { - return ( - <> - + + + + + + + + + + + diff --git a/apps/frontend/src/assets/images/nitro/nitro-light.svg b/apps/frontend/src/assets/images/nitro/nitro-light.svg new file mode 100644 index 0000000..5706684 --- /dev/null +++ b/apps/frontend/src/assets/images/nitro/nitro-light.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + diff --git a/apps/frontend/src/assets/images/nitro/nitro-n-dark.svg b/apps/frontend/src/assets/images/nitro/nitro-n-dark.svg new file mode 100644 index 0000000..f8d0ebd --- /dev/null +++ b/apps/frontend/src/assets/images/nitro/nitro-n-dark.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + diff --git a/apps/frontend/src/assets/images/nitro/nitro-n-light.svg b/apps/frontend/src/assets/images/nitro/nitro-n-light.svg new file mode 100644 index 0000000..4dd94fc --- /dev/null +++ b/apps/frontend/src/assets/images/nitro/nitro-n-light.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + diff --git a/apps/frontend/src/assets/images/notifications/frank.gif b/apps/frontend/src/assets/images/notifications/frank.gif new file mode 100644 index 0000000000000000000000000000000000000000..211634f74533f4dd21c474dbdbcde385ee3d675e GIT binary patch literal 1204 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 V(Y~5D$Q0iQK%-ocOnY4Xe*yh$$o>ET literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/pets/pet-package/pterosaur_egg.png b/apps/frontend/src/assets/images/pets/pet-package/pterosaur_egg.png new file mode 100644 index 0000000000000000000000000000000000000000..43ee1418ab722ebea561ceb72fe7381d61408c7e GIT binary patch literal 1631 zcmb`H`#;kS6vsFBrZul}sYPy~+7MPtjc2oFr*+F(=c zyPnDkY5n_A>6=GaMju~$(adc~uruDh>%kih0Mdu57&dE`yTT)%xs}e-{th7*sRD3NrD`>E*9Ed*>d(H-6yGU!e zOdpG@DJNoB4lZOHGSxQyqC=UG5yrtEjz6^@9tdUgN#(Dy?9*Ju0PDc>%LI337epQ6ko zDpqF-4w8C5UjOy@k4A%|7W(X-Z%Yw28ZnnkVyA6wS4;ma8*@LqdvP(=OL@2?a2}W; z*JN=RembbYh_W#{+p^m*GE*2+4~ojsMKH~kim5~S|NNy=+1WT|pfO)D6QVkCDGht~ zPF}-GuuV3;=0WrDdVp#jc&=@P(VT!j#EtlQ${V2(UNgEuj5vZy_|si9J?DAP)raQQ zAbc>KHzzrC5cBrk0s*gI2hJkQEyGq-e|n&{u5tsAsy;d6V1m6JPUa`z?pP(xAi9Cvuqe%f!e~L`{W*TfE*t&29pW>NeX}V0CB+j2wn_%H zH}Ds4OJvDwVkKj?wO_U-PA+;S953?K6-C8&J)e3pd$E@zd1{?^3p%w@jH~=x6W#e_ zbja!ez*_Kxr(bVdI*I=R?A*L?l{5M3<=6+V8T1}vGO54UG`$;I%;j6oL?xi~Zde=xA2x+SD6zo%g&=xYfv|9rG}~x}qBosx?2dh@Pr{ ziPq+kk6C*^Ov39JWv@g0pXY2sZjCeTd1o6=tCo*-H90dp!;0DD<_%_I zJGzzpTuf&4ZG$B}#%-u)XyI{DI#8 z)kvUyd!p)|-T}?96wQY6<=hs_GeG@!<%uH0=bE3DzA}9@EGv=f!E^j2G8Qeh@y19O zk(m`ii8?2PWU?s$)y-HmqCBDl(G~?frM#Rs{BrS~?wY4DxF*&)i1jnTlM+wZlKk{` zhl2K}sQob!vj;BjE)-VACu`AO-DYij2m%mwO_62S*=3Mo<0I{gf~rLKn(*9Xk2f11 zotRhB*3?2`f#ls)#_rkc8BnbCfU-c6rBETr5c@YYL+DyP(~xiM;V^E8oOC%U(Iw-aY#8Ar4%!z@1x1^6>J<26862AFg(w@%{zE C?iq*x literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/pets/pet-package/val11_present.png b/apps/frontend/src/assets/images/pets/pet-package/val11_present.png new file mode 100644 index 0000000000000000000000000000000000000000..3d371b5b3747985100af5278106f3c87b5462982 GIT binary patch literal 2720 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 N002ovPDHLkV1jkP!14e9 literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/prize/prize_background.png b/apps/frontend/src/assets/images/prize/prize_background.png new file mode 100644 index 0000000000000000000000000000000000000000..ec9c0306536734ee07dc697eec63304e2c0721c6 GIT binary patch literal 5861 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 literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/profile/icons/online.gif b/apps/frontend/src/assets/images/profile/icons/online.gif new file mode 100644 index 0000000000000000000000000000000000000000..3a79838bd329181393f7e91ac17fd74b09d95b8d GIT binary patch literal 666 zcmZ?wbhEHb)L;-`*v!eWbLY;Ej*h6Ps8!)C|7S4#Kg)IZ?p+23hW}syWS{}X|J;7A zA;Hd$0j@@R2F#2=X~mx`tS1;a8FWBOLB=w$v>BY7$+Fktb?)~6(-rz;Guj#&*RTc5 zTQ)(e(z$}YlEhSXen9e`ppZNI&HUZ^7fmb zPDv{4xSvRWTlB~>(o7N|;QpAOSJI%{*tor+J_N}B## zg`fU@`eoVSMS&q6t$t^3cnUEhEaqTfXOIS4tY&c1bF~TQ^PDYz#kW@~bj;0&tW5V< z6|;I_)JMe)t#8WW-r2-!G9k1g+$>|@nUJ!|EbwsQyBIS@6@|D5l}jEe3Lcqxbdw0! zE>5r~5w5lbx|(rsg;et9`?IGrsx)+nXZW;VQF}Xkg*Nx`4K8nL<{h_~ccJ|=XS?}y zqrx`}XSA;PvU0N9%1Imu>k+Y;268ztHqY+ers!aP4N9 caD(Ig2>~yD|7BrzlBlV#Z*1=D5@fIj0HK@smH+?% literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/profile/icons/tick.png b/apps/frontend/src/assets/images/profile/icons/tick.png new file mode 100644 index 0000000000000000000000000000000000000000..ec8c52fdf71f359dbd22ddeae746f2df2459e03f GIT binary patch literal 129 zcmeAS@N?(olHy`uVBq!ia0vp^+(695!3HFgJ}hYlQXZZzjv*DddV3hT7!){;ynA1# z5TBDFkvNOD>BY7NwluLn36C0L9BT?L^2s{>Q52qPD)23$t1B*PLf1LbkR&miqY_&l cNrv2JDa>e9Jhg1gZJ?P9p00i_>zopr09Ai08UO$Q literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/room-spectator/room_spectator_bottom_left.png b/apps/frontend/src/assets/images/room-spectator/room_spectator_bottom_left.png new file mode 100644 index 0000000000000000000000000000000000000000..01688cb295d0e7551042935ed2f4f3109aba7c77 GIT binary patch literal 461 zcmeAS@N?(olHy`uVBq!ia0vp^(LkKX!3HEf8uQy37#OE`x;TbZ+3@i2r9VQgauiG$dG6K)(p$WA zfY2Bh`DR&W!}Md@@<3wy)VA4A65X&CWQ=vv?%P1-Z-t!K>l$+JvL{BDv*tXQy0J0* zz`vT^e}D1qIkZXm7e__QUdHMjFTTv2bl|^g)l1_`?eCiX7uf6li@Oor9oJ>oET6bO z;!A|kFNuFXjMp`jwad;ey8ftN)Zk2$QJ-%1d*eyXv!fmS6kAWv^3yBpbiJ;zBqefl z>bp|6z5}+=k;Rd{wZ~lJRcD{)IKp&;bI)t5?sc8K+nLJ#&sj2=X`@0J)BDAzH>D)< zUrc>opOsRbaJWr(Ln1dY6j;r28hNF+9l*jrKR2IYH`k`;ayMgv(Zt~C>gTe~DWM4f DB_q~? literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/room-spectator/room_spectator_bottom_right.png b/apps/frontend/src/assets/images/room-spectator/room_spectator_bottom_right.png new file mode 100644 index 0000000000000000000000000000000000000000..59c8ef2c2daef66a18a2a2dd18dea786943ace29 GIT binary patch literal 456 zcmeAS@N?(olHy`uVBq!ia0vp^(LkKX!3HEf8uQy37#RCKT^vIyZoR#}F}K-4qAhWj zM$oG-aSFNL5@mM1nb>(kq^Dpaw}2Ac=i(>SHX5VgvaN**K^72w3d4zy9obBEK)eM#_ zONxXXT>zn0*t_`Tt7^25Y8F+H{$gTe~ HDWM4fUf$6q literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/room-spectator/room_spectator_middle_bottom.png b/apps/frontend/src/assets/images/room-spectator/room_spectator_middle_bottom.png new file mode 100644 index 0000000000000000000000000000000000000000..ba6fdecccebd3460ca5d32675b070dbddbbab852 GIT binary patch literal 98 zcmeAS@N?(olHy`uVBq!ia0vp^j6kf$!3HE*_k3grQktGFjv*Ddk`odVew@E>@#4q% x2d9Xhh+3-L@3^P_|37=?h`YX(>l|Gf7`EN_?kMv1F9T{~@O1TaS?83{1OP^IAYK3f literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/room-spectator/room_spectator_middle_left.png b/apps/frontend/src/assets/images/room-spectator/room_spectator_middle_left.png new file mode 100644 index 0000000000000000000000000000000000000000..6d9aaa7958b41a907aacae0e1025f191f43c5418 GIT binary patch literal 94 zcmeAS@N?(olHy`uVBq!ia0vp^!a&T(!3HF4{HDeLDOFDw$B>F!$vZU9f1NLJ=D>ju t|Ns8}{{Oe@jld0>+K2!5_3>@6VOaQ6#dBs-r7BPZgQu&X%Q~loCIAFlAkzQ< literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/room-spectator/room_spectator_middle_right.png b/apps/frontend/src/assets/images/room-spectator/room_spectator_middle_right.png new file mode 100644 index 0000000000000000000000000000000000000000..9d963b3d111202dc6ba066b300ee3e378186d025 GIT binary patch literal 94 zcmeAS@N?(olHy`uVBq!ia0vp^!a&T(!3HF4{HDeLDOFDw$B>F!$q5MwKki?=`0@Uf tsSoS-b69iL{r~s3e!?H$O1(t}3}&7xo*jP_76LUec)I$ztaD0e0sy?l9^n80 literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/room-spectator/room_spectator_middle_top.png b/apps/frontend/src/assets/images/room-spectator/room_spectator_middle_top.png new file mode 100644 index 0000000000000000000000000000000000000000..f6559cee0eb6b9c10edbe88989d77f6781520a82 GIT binary patch literal 95 zcmeAS@N?(olHy`uVBq!ia0vp^j6f{R!3HD)xzixJtr z2d=!_-1X+TmG;Kd-_B^o9Ae>CY`M7NefgJ|9Z$3Qru*~Uf4_bAw9Ezu>1~JF)^12V z9Cl;+Q9i{N^NuO>KVD%i)#q@&fB(I>_w%f4AIvTO9J%rN=Zu8( zm=iNMray6i@WEL3cMtdfij%ir7+<<{DB1AWzVo+Uq)v~j;9Qedm-WwDSyQ}D#E8-3 z-jb-YGl#YxD2ol3UoHAZnLGYaY-8=aC2`-Twf=XFJ761MG!z$;~z zb7RBpW8VtbK-oYkUTL#8CSXNC7En5o8$^J`fsE+mY*1mKNOS^B7%tBW)?y!9$H3lL W`0exJA! z9_ubY7um^Q>gP25^~B(UmeLpn)oH0^jk1+<%`b^x$=}B|ao>Ia$fQXM2ikNuBy#86 z*uYwL{f7PIr8c`aJg+<#G|%?M%g<9bO?mqI?N@%QHws@eXRSJzDE=sJ|6x9-{WjAl z2Ql;Q-gJ@s%hSD5zaOhUomP3wEkEy@Mc>ZUs+en0i_Md_|Kse42rF6qJz@L2XQ!>Q z7?<|erMy{oTB>bf2BW*#ubCTHPcO`7{C*)@`tSUW;o={p_p{Wly|{tgh4bbR9bJ-0V7-{zGz z19E=<^f&~R1qmN+g9!Drw?h~(5jK#*#KUGAsu^&}z?5#_ebcw`{K>AuZ`}tiSb?F= N;OXk;vd$@?2>@0kz9#?x literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/room-widgets/avatar-info/preview-background.png b/apps/frontend/src/assets/images/room-widgets/avatar-info/preview-background.png new file mode 100644 index 0000000000000000000000000000000000000000..dea4f08dd49c0ddbc562e6eb17c854411d830cab GIT binary patch literal 7756 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 IM6N<$f^1`jw*UYD literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/room-widgets/camera-widget/btn_down.png b/apps/frontend/src/assets/images/room-widgets/camera-widget/btn_down.png new file mode 100644 index 0000000000000000000000000000000000000000..76f25da1d9243426aa9bb4852f3121c80e910385 GIT binary patch literal 1000 zcmV>P)S_5?q5U31f=@VAwx@ z&;}@kWx*lHfH0N`Kn#w?W`+d6ei zB|ZuL`M|!uSo7_9{C<5U4OSS$0Nmks(LX=`4+3}(`Pa(}Y4pO72H*ytgnodZ(Hr1B z}I3P0g@ zgWnCk#83F0;CDqo|b)<;FJ;8P2zf>*abI(mVBS-#^ywq4r# z2Q+`W5ke=vHlKw|*sj3i@#Y4Xd`ZTfYK61-*bbL1(u8N$VSOZTGAUeQ(fgv*$V zmzEn#x#X*8Kf%4Clko*RC0t#6pYukA=)(QaC4kDb=n9>MA9$+@m*_40+X9=jxJIwx zs{)w39ntwQgxU-0e8;;1INE@#yx(DWKw}3mY0J6r;M@RilQ&sh4xAc*C8bW~b3BOrE#5CD7I5U#CrXFk}y)@n_++cqwPi^ERRmY zrXmBv-~^Bp9F5HkVYvWQ;K1>L`n}Wu!lH}8jZUr>0faK_E4j>M{VXwn5cn+gkB{G-06v8J`l9WU-%SX>6`zfM1Aj#C&C(Gvb(Rl)mK}gM zJ{tWAK!Pj@V-!F%d=z>U-0Etk6+$ln1%6m`H*i7#q4448-r$`8LgB;Fqkz8(0El1c ztKpMIOeS;%K6K=;+MWfSi;r$SsqN=m09nw5_>k6<+TKfkb#x9ss`c!)XGIs_t6DE@ zdrI^kKC<=Xwr3wPvs+(ncWQmL-M016F=P25q^LW|ggS0mV`jIuQ(G??H`Vqzt;dd; z9oo)3$&$v6dd%$Jc?BK0#8O+8u2myY};BvXxwLS_sAOMQd&60eF(gZ## zxFP^4{G9votAGmvi2Fv!s^~;~dw9tSc6IatzS*1IsyUwpy@Rhlv9|EV@Y&EC`05iY zblhY`KjE8ygpb|&9pN=&Ml){yypRpxsUOoOjTzV%6E>Lf)4rHct;e*T{XM8B_#Io% zY&&brKwF>iKTt2rciRwc*S0<#KIK_9h_;JcpAmnAj&8fS^_lP~=(h)GAlt5LeFl6A z`UxJ^c2(FL7>n-cV*G`dstAKl#m}%fOE`22zA6Cc<%k}SVX3vC9$)w{fItgyonQB|E1~ETFS1AhPqE77`9MV>D3=v|^%}NW)AiNCpF=o+3+cB0y{%U=ZlUzT z=zsO_k2UI-;TA<9^&es`PrZ1N5+?uv002ov JPDHLkV1g3Zz}Wx* literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/room-widgets/camera-widget/cam_bg.png b/apps/frontend/src/assets/images/room-widgets/camera-widget/cam_bg.png new file mode 100644 index 0000000000000000000000000000000000000000..d6cf994d063352f748ce25d259c4dd3e54645754 GIT binary patch literal 2166 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 literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/room-widgets/camera-widget/viewfinder.png b/apps/frontend/src/assets/images/room-widgets/camera-widget/viewfinder.png new file mode 100644 index 0000000000000000000000000000000000000000..ab6a9b24f6bfbe61befeddf8e75e1690811a46e6 GIT binary patch literal 959 zcmeAS@N?(olHy`uVBq!ia0y~yV6+2a4mO~Oh((P70|RrLr;B4q#jUq@eNVk|5NNx& z#!UJQ)9Hza8z#Q{a=x0c<1l|vC_k6@`$C`=28P=!SFQefI(V!0x6YN)DH|(_58ARW zyJ38~A8NPV+=HY? zq2`7$*dAIE5Bx%7*T&CVmCkcne(CarvpQlvg;&y(WQq3Bv{hy!3{t>X-)^+da%@`v1?sM~#X{vlv{5lGnoFt1q~FISuQ zCG3B7&&`$BrBfOO>}#&F0woiCdj2S*N2pc`=9l%#X|um6hrgfX)-x)yALJser}% zJO7x?#Rw*xA$^9Yz$J~%LJ1zys1X2(A8whz{^{zke==dQj;cZj)~$N=Nn}H|%iEc$ zF2Il$uxsIO;eW{d(D{c0kPAxOH%uXkyOjkhh@@si`ZU3Zhv!AQA?X1|Do6b5>F)dA lJM~sne)@Lm9LU8E>-f1hoe(_ia##<<^>p=fS?83{1OQ0_I2!-} literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/room-widgets/dimmer-widget/dimmer_banner.png b/apps/frontend/src/assets/images/room-widgets/dimmer-widget/dimmer_banner.png new file mode 100644 index 0000000000000000000000000000000000000000..fdc6e9fabae522025b080149b464c349738c3014 GIT binary patch literal 1041 zcmV+s1n&EZP)pI!%0LzRCwC$+&yR%K^O+$U}NJ6K@?FiO%4@<0YStcL<=RT2;vns zX*{qqHiA})pjarVaD|0P8Y76Hs7-7IQMumI!bUr_6lJ}4j_>?-ZfECbXZB{7O?b`i zLDqftot@j8giIw<>CeFj{z>HLtNJ+xJ9_t}NL`>QVCU(_q;AkLAoB3jm)by;K%@?h zYJpX6)EHRRMy-KWYt$K7l|~B#V{gL2HCUfTSUc5LO2S4yfnP9*-Vtf%4A|_{`)2I2A|QGSuRZw|l+ZGlbP=*qaz%IBbf$~lI&-u29D>OQw412M$B|_V+ zwQH=P(EwGj&s(QOT%i*O>R^OZLGitFn_KY#ZD8{Asz#BhfH+Xo=CXh$LpKX(yTVYY zK|0Vps0li2=*llz%A;_(oDO3k5^2S?k)GN>ys)PB0dI#n~U0|+I zdY>8$s5&55m<^Nz$A05CG&J-HYKR7tK%xWr%uYBkCTS;nU-@M)$^mi?{ z=l7AfhsCIYcf0c71E?h4kVI9^2hWK~aB2G$2$k-??@wf=rNy)26l3jHMLy-`h`vYr z__@(N)u?C)fRrTvv*f)GonwHlrK3js6-?Xp;R?8&brfAuJKN~3_35j0``7uL`ERaA z3+4Ky3om);Nh9u8oz_=P_`%u0q!CJakp4xFK|!JdelN)m>_H{Nu+2=T3PZmjM(J=0 z>~Wp1igclI5n2>(l4F05qxQq%{v6?cVU%QpVE^QRBuy|#vO)iTghmH*BK!_LbKn$g zlpyT2)F3A};#XeZtBHVy+|a+ebt!;<9?@36J^PRFD$9JuWPlbW^rGwmC8#h)=J%5z zo9U;4t3-hoJ?`KUNu5opsA?)yf1SMNvMVhpZB&(NDVpQgZPQj~f`k8S008xKU=s~9POxbZ}A9epG6$9LU`f$B)BIKFAL%lG^JCyxVajmILVl>+#Rvw;NF}}eZA>)V?Yq(bHDAYARi4=EvTP;Tk+PQg zO^@pB35R`|iwsEMx5>;WRxkD5&k61sBEfv8krb=6q4C6WmAJ9Qw~#p&aNPKD!(K(VAyj}kJ492zK;~^F@K;#4g z=%oFf7FkC>=1A`6TsQ?6c_v%W!{A1TXTohqDAPjHjmyqEjT_HK&0xPRx60&B5S|9? z5o|$+yVmN6SoE>f%(pA{Mc!rf5tNAZ4f(&~(XJg6wrr2%oqn?gZ2G^|vsPTyY3fp~ zKn5(IX7>csAo|&HCdr^z+uN!vL}FagukkiF<8&i_{`q~Z4Tk;vu~mM18_rW9=ht;v z8syuJ{q?q+pfpG~)V3vI6nS^15B=+g>SkN26wdB{5#O~8n_yQTPXoMJ0q?W%#5489 zt+@(QGHb_{5^6&Qh}qQnppLklK{J`SU@_==>j+{FZ_qm1M}hd$Wf*q}SR(im74Pw9 z;jabZv^P^x@^x2dYT8Ci{u9XtIerEG3H);~svK+CGK@B`=lac)g^;eENxAGZORj?4 z$=BohcyDAGn;D0mfr2laRk5D!Zm@bwAj8^$PgxjaIo1=3tAH~75II3#b>=PImqff;#ma2U)2dm zKdvzzKa*!jnY6Eis`H`py9&jHpUa6355cRy-LfRj*d*fHdn7Gnge#cMV?}$7@iIE{ zQk05%L!%l81;G0Op9JB&5Vmb<$y5B{0JN2eeFYv$g3KnxDnh9{`EtvLe%+|VC1nfa zH>GD_{#I-GAX~(NgyLJ^|K(5hJx|htk!ldwAVeKMv@F?INk$>iT8Ng!Ro>$ zW9Za#W%#lTSa@{=EF>kk+UQfr-ZTfQwzZ@wM>8#4cB#%;vDdS1*XY&=2FI*5d5bb) z@AA!{dk5CP^6e}!Fq~y6cF9Nhlw3uXn02B@NAxk{M~3`G^?o4XF_an`B`y)5 z1r1bPAd}-dKxg*>K5N1G{RL9RU-0SVKlF;p`4H=l5`y9V-XGzTZoHF;9JzE=P{(rS zkeCXURa|Z$h>MYeLo|N-H+a{x)JtXj`yZPB2+)omgmjVCF-mIS#Ys>~x=IG+osI<9 zXO6LatJ(oDn;Mj}g2XFC&APvZZT4a=BV>^shRU0T2~QrV9z)P9DFT}b_sW^(^oc_` zLBiey@HKS+SV`m=lNHF#XM}x(Gends;TtuAkK&>jXOaa}IDiAlte_156#rhcwWMR! zqmrEe>bKGhdw0qCeD7)9w6o|-L}SGRijQ0k)Ou^dr;(@q0giGLOm6C%b167Qcc3By znVSgr><&Knv6hP=G*L+Pld#=;P)e{km8_F2=WQ~OE2hS5pw4JmWuVtYjic1=P^dfc zdEIimbM9b3G$Jabi1G0!i?tB$)a6A|&^0}$Yy1Of+gZDT6RyoVCf7SmSoTKo+9JQ4 zwIN=|pFs4de6xEF%;B9nXjA<}p&FmbO2C+^9d|1kO=oE>2;}Q8sqR z2*9bK0ucP|nXi}Iy;YhLCce)Ob!<>QLo&jxBH@Z6@kOc$7_0W#sBgEUv8M6wSv!%711yH}xwZSCmG z6iaIolO$q`_BFoV65fl=94OtdU zd~_(dTWA|^?j>-eTv3#vlp8-YFJ4=LQ%EEvu3@-)pMgfB&)7WJGZ~L#PtGX%GX>v* zHq`&eJr1189PnePpc@JNRx!Vm*1vr!4x~@BMvFG;&vciy;yQ)mgItP7OjZ>_VqVOB zWx*8#3FS{_^>`C@4&3fP%4rKsMT@x!K*b>IviLpeUm;XR653|<280$C<3AV%eS^+> zwFM~aYFysYycjy`unGo@FcaOeEDX!cYd~#CwY2(rUpEmE#k!Ny#=h{!w zhUBw+JFm_s;XX4O5#@x3TMkRHgjw_uCB#QkH{C<=QPVU6_^Sa{3b;_m2l+kqQi_o` zBtM%&e&Z$qtlS02*eDWohI)++nrFHv8Xf2`hU#A{J9?5I<($+^x62cjj5T*OG7Qi9 z1WK66*Sk2}Or39mU2i`1bZ+WAl(ZC;ar021(|~2uy2+r89O_Km140-(+)bUNGw$eg zaTLN%%*!txFwqjJH-#7QZ12ZdOQ{hPm32G^&1-UpCb}&C}D~b90nRg%pHu#T=O>6jzF;f*Sdt}Wg@eBIqi+=3b zAfLC@;Etys- zRlx9pV46# zfaCe$^ws&`lR>AsOSURA^EUo5!+UPsshj$l7Src;NY<$F@Z}?2ptB=mJV6}Mr!6^j zqn~r0-sXf|aCGqG#yey#4FZfpMa8=mh7wA+4QtMKMJK-61imI$tt;=m&*B7f(p>$F zNk53KV1D%04=gm_uia{8AQafcF-B~IFJy1p#BVPjU@+3UKZZGB!Pb?m(x~O0O350k zIs5$vyPxXH`k4XpHr-G899F@(e~o$X`LKg#%*1n8+=-^4AXu+exg<`XJZtR5TsQ;; zZUEC5WNI;^@H97Wm8%0ZzMnU6@jdMAyr@Q!;EFIki?LqbcjNbIocv`s-(gVIg`ecFZILXPDs$!uN1bzci=OvG!_iMFu~e9dlGV ztrzH0>;IKjU=0>R^|#oDL3s$X$hIi5@DMC{o9tY2qeX<0*%5lxg2se{Y2CErZpX{j z9XkXd@s4A=;dc*g=D^3gxz1bTLZ1am;N!N6*7gVWa)hR#s?B{ZWnyo)a*9RTH@nhj zI|*?bx^ah3eS(iyf}Cov8fPo@AHDr11kS$bn%>>PV13dXqhD%&`{$59?O0B(&;R!~ zboLg`RuYAqqlKvA>h^;W*jx_o>SI5P-;8)q=^g+(dc$I`AAgmbw7^xtf55SNMhItJ zO|koIh(e`oP3gTQLvEP2T=Lfes&R@_ArcuCl(|3bdAxtNDsws75oX5V<%W2l{GqTh zJL{d^WeT*fZ8~}LrVUN@WaYBQv~is)^0x2#H`|^i>@C$wpO4I{NelTZ>OqK3q{*W_ zPva0#dN*&55n7$8E^eEF)mHPH9Wu)IWu?RHblz3TKYEX)~Oq-V;qYM2z8>uV-On(00VX3sXxZzqTsaeS z04td9(e09{E*|%zX1m&}+Le`%KFqw~)+{B-zj4GFiILEc$01sP#ak~8v4J=UoI~g` z@5wS~Y$-$)XaCg7@96EV;l$Yi0Qo-eDDuq4fuvAgC1Msti}1u9+%K-E?XxvFfXk}I zPar`587Zj9LrEZcSX05x!a7fk6e{%A?+wbN&OPU5h+8#Z5zlGEw!Plyrn0_Vzy0=N zdN=in{2%cn)a6?u=0}0U|Aj6v`^scTrwks)Na<;WP3cRUWdeYzyn9RamDT757M3 z!vlu7Un;{ga4^q^$+j=!n2km9n(nDZwL}Mn{cXVg>!{1f^W?l*s}_HoIvu-1Vc*)- zi#UUj(8gnwsL7j!RYW5q>~oqgg^AJo!auPdIcl+l9ln>nnSKXP-sGJuV4u$R)4tN_ zqh-YDE!@W(8H=7B^V>SS`hB#ia@pr@TdshM)I ziP$)U?8*jCHn{v?PSYe6bCE|zlYEkL&e6!tUvf$giMt~}mSwS2L;-0kBHJpU>l2cG zlEYO@__5o)>8|p@5iV5j*I4z%(w_+ds1KrX2rTXvfJ@zepD`|Ylggk}!sXs++9#(Y z^m4UP>tT6wCAjzRb4ud&>&AQzXNxJ;JM2aX2P7dyVB($dppyI*q|56I%Gn_xQ7(zV zC|F3}w6${O17KKguvmE`m<=`6Fw0E@q>{|z@dq~S1I6;XKFxd{-|5Z!cqM()v?uIa zvR;U|L>+HcEGPu2dKYCeK!xTS#uE9sk*p1^W>;xTC^8c(oW4Db+pNFeY0iGv1{(9W zw&~)LektZAy?FrdUjtjsf;|roeaxENi`DiCubbb})qfO*dZiUG*`MuF`qLFDdlN;v zlq^>5rC&To-a};~y`^o#vh`xNT!G^tQ98bzaljmV+8M=%d)V;`}3Dp zW?mPVI!a{jP>D0FMiOcopup(NmY{R~iuboG{Lm)G9D%PB(Cdv1#>w)Bf&sMAv)k}_ zIq`E{@+q|yGjgcVa>r)0W(y(p?;eSxUa&>ncKeE6?q&WR=ifZ+Cgr#Lo720p_4{pB zEJtK+2stas=d9FwgbO_vNcMfI>sGJiTNykQN;2P|K%CJ&w)W(?=0%njrdVgz^iKNt zD#DEHPIXn3{M9wrn$E6tHHlKK^G9oh-^LI7BQ?Ho)ksaKl9edxOR?Y4PVu1d`rYF6 zh$2^)`jjB;++&eB9C*TE$0qwM!tX@JGA#Bn{@w?!i_dXnnq{nDuHVYPFb79V1ZpGf z*kbB;*b_}-36$JL*o|d73PWyby%n(0%ToTDlY*pKe}_3Alf1<@Zt5=Ui*m2JOV;c% zAj}xTJ?7Ksbd5t6D{e6*Y~`nqKz1G!3Du~4Mbx=CCUBsO>OR_=EmgrqG-kZi7AUzS z|A(FfcSf}$C(lbO-+f4giiSSB#V}=*QZMvLl2Ll0&f24 zOPZGfp-c;Xfe8710}Ib_8H=^oc%yNmz1(Vj+%(#?7(J+N13`RiLTg4`J=U<4;G)W2 zdsb#fF;+<Z*fer3R{3+t&d#%orvjwPUy zIhTV4HH?B~#cN^<)vYUHV~&8xsV1k&w^PWy|?*k*Mon~lJ&BN2!_5G7UGF8?E{R6FOa zV7mR&pA<_n_qy=7ZYP}ua-l~MA#pNQZjOu>)b*q~t zz^QDjTfy3!!5D2)b(ap-KL4{h&kt04{Rse3x1a-zty@|%1n#=7JpJ)!!=^NR#_71& zGCzm4T-+PSh1%K7nH z4r1i__5K)(L%BuR&}00Amwn`1b~Go4+yglB}Z$b^VxQY@M4J$^gC4q~#~(r7Ro^ zI}{#$TC~j0Dt0Y00a62Cq8o-2)c}GPVU0o=;|T+45Q^Xi(yG1`GR<$QWtrzIJeZBd zaEmaKoS*8=j&${%f8ok>ZH@zlPDGCssQ0k6E^CZ8I!C>bTG|`92ung+9kwLhebuP; z%&dpN^jXDMDt$so?>kLOH#@p&2g{}I7t48qM0IK}8zxXm@XNcUOi<>K3)b-;C zzguoPiq3#3QRNaA_dLrgJ7N&VchB*B6w`Fg8cNajs6y<&9+`RqD7V#g140;?f~PK* zNI;4QN;q9sWKbZkc#M4eT<E&N4;eYf-7%eB3R;dN670GuD>2~Fg9w|Y~Gv5;sx znUP=c4#|^NAO=afh`nWB=)6EI8Mr3l|LYfOXxVjrfp*gCDi`~u_W*Q!r@rJEG9O6S z!M~$8?I%+Ir#Dp2WZ*jvc2ZR5rq1mv1OO405u4hKf+?*VzIf15hcdW7!x`y|59(T8 z*K#j}`RyVaZS-VuJk4|-&~*k3MJm1}sCd-S0dT2FWDAb-DYR^EN2<|w+y=Vf0onSn zIc~{@NMTa=N5^tL*I@||ZulmK1XWibdqEBYWX);rLd!dxCW4CZ zUt5SO&?<@hJ?%l#Aj#;Oq7h=mRWZ;vw zI!naO|9H9-#Nv>2`zI?N!9pWu_PFUat9$ygh7Tz4s~ucIt=1M(JWr49Qvz~J95xo2 zyo~5KOa!2tLVCe*ujo)YyuXr9LnuUg0zH%2g5|`6{>%m-J~-BJRS^OIE6v$kB6gZF zdJaciPX@$Z&2{v?u|kv_U|z|Z^Nd9fUTb|B++;=|$2g#j+@K6;#$UH~$-IyK_Easx zu55I!GOn#Gtp$LUN#!@@mGz@lNJQr?!hFA-5yZ(u@WK#!#8YW!Jk-Q;BSAm3aNIyR7MF- zpktn!-f(=})HuelI0>>V9Owfe1vaS$-5gw7dO8q5RfRMs3p{Pslrns}zG#voXtO@9 zSSyY#x_|lDzINwZiiO^q8JVHzj1?!ePBJijs3u6tT|!iIbLj=CBqQj3URm|{a%0KG zANyof_&z2w}qyN?fQFBurc6PZt^I=~Z#g=7&#twe{x7u7VgmkCsop1D(dOyu3 z5mB#I?m_DoKSk1lAhM6YJP~A)lvc#y>&I@Xlo@h_C)tH6HSuc3C_d~eG+@KEH4G;O z=w4p-z(IJ0BmnCosW&wl*M}IJ(!vEVyC)@{Uv9OtA@UJ*qt z`J9n?@lzTeo9w9S_vz=(HM7p7?p>@=9jdofu)4pJ-@MX>dpp-i-1&MhkqpifR#bVb z{n^BOXhY$kc37^qFn>)Hb`eCH+N>U@{PjrxP!lg#w#nT8f`Og;=}RA_%10em8I~rc*?JrUJyzGfD0vb`b2==@5^)344)M? z+@zU^HQT zN)-!% zpx?Jk1DRiTV-2vamACQdh}-$kezS)@-LQ8D*7EYzI}?6Uc)-viwSgiX0`#lGU?h+6 ztk|0eP-5VB`Vh{skiFfXcb;PQ4UUXhMi&|;sayJFm~G-Xaud{t-#6atPbd@VSjX-% zm;{1;rT^Cs=s%r`f86p<9PJ7h2NuEO3O&obcK!#wU;FU!~{*t&Iv4D=~Q`Yb-;upx9;X_c=|Vp1(wj@lu3%-A_?lz15Sg+K=w zW-I>)HjZmN&n5L)6300JuwgNh_hS4zE{+zgSBTtAkksq(@re}BiRhQYBG-f8*B>b? ze#w%tK!a4qhL$ov>QWV(9pN3A?OFWz8yDMMeahV6jPKQ;<17scfLpi<#?thkCY0*C zA>Fc%S-Ouk{D19UxhbHiK`Bz*9{4g$tup2=#s}uv&jB41^i|~rEmK656;wpn7$|>? zNu4Y_KMUU+7BJ*E4dnQXv8u8xlkFGyA{wyW;(t^odX3-ckA&xn3infs(f_sID}1E= z;59q*cr8en!-n^-q@96UpM?Q=;Wno@AyHCGWt2{a!sGWX8+tW%{~Wgj;Fh!e{T^#p zAs6*y++Zc_M|y7Rq48GLejQ?9*Sju!K-18FPVVd{!T-On)ZsO@-5ATnQbVxp5`D^l z#p&>#)_6dM{}G_XA2~xFfxxmjQfSJu2v6<*`}<;v|2|aX;UD+kQYPT3%1-b8u|&P{ ze?+z#O8rN99=)+t;{h3p@LzM)>9)JW6aTef0$&;_A5Zs>J}f;2tCkq}50&MAFaMtU zm(KDJBj!KLzt-mc?*);k!~2hK|7ZEfi&*B%UuXW$^1t-`WySpeYX2b%{l~i+!NWPX zR}@&kRJ~S0#p1vh5$rSb+T% z@~@fL$KtQi|5^TlRK&lx_n+k-NBo7`-+S(QhZTkO&R>cDS^l@M|8Fw8-i0arD>RAk zP2-xW0lj+EuQsfVl!vI6V|3*lPi6B}-1YiL`RlG}gP@JsGIQmVeWq5M9255q4rI>2 z7V=<29ddnN8+J%GtJ}1M0$J-ev9lQ;yfG$;$DQqb?b_&{gWAKfzIknaoP#wKn~*5s z32*->mr^aBwAB(rwEVj0&=%GViSL)+1(Md?i&50Xza~4_qi}8Z&+%QDzNR|hC|Z3^_| z0*;zQ%5d!Qz?!qH2_ReQh2-SZQ8D z=MOvv@Y?KdFyiv=^|cV&a;`1D<%gP=@$qy0Z5)!>#dppH)og)-S;-j@`hW7sB`$CL z7d?Kx$n8ik;=4{ixS$hG9-b(r!;0qA-ZT{K~|U>bw+DmfMK4$e&U_WR5 zGkIl@uL#7b8?5euwasdM`z)UF+g-*(HSn}l;`zC*ev6f<*u=K1)mi6Ncb=gN0@E8x z&yLq)qhzUkFj6b2kt9c=CU^-l=i_^87Q3RhRfzEliaQMv{gVi$sRwTF!s6bA6hrPw zxzzAvAOEUtS+z6|>Tn$RxOZRstdIn6<{|FtJ+4!~v*S767Xz;S?#Zca+QS)B+)UjT z4_wri(^J&C2${IcSSVNz(h%A{_#+Jr_-AxT9`w!VoA~cx?Q8O7cW=$xd<1`+FU#+@ zt<*C%x3yGmT*pgx!B&#lWp;1(&dam@k+rcL>a>z-Vn z9ed5$#}|yqCj0RwN5lma{tO|!Z`Xdo=d`*CGh4j*MyRjQFI%wmc;y#^`N#0M=i8^u z&|k~wz^ywm^lzTyuu)*Ak2rm{qd~{Je?B?U&;m%7)4dUGW{(J~(pGtwwD}-zqYegN z+6aZ@FGd1p?jJq-USN@LEmkRgYUVVadz@V$=tjNJ`IPbp!E(@$DvoP4zPr}1?1rhs z->)&k#0kt`^#T?5nC{&Cko?p-1(o3B(75{SWtSXE>+*l(>sOk$_8Vtio-Wp7 z0jrBZ3bP_zbd$UAC6fsGXQ&_jvb3a&?(1)%AQs39c)K8RC(#vC(flTD-g)WL<&80H zVd*HFYsIrNpsXkCefM^9*xAw(lb>L8=dyLB%|2>1$gu7x zrZDvAOz_zvroWaAHY9BLQ?Yx&1uE;>+{cDatev{0JZk=J%C(X#oNj=lns$EqzEhRQ z6oUF?wvWCm)HqsC3u+(^I-d%tG`s2Myy$@lKx*FZ7R{E)c`a$oRMsz_%$)B{8)UaN z-KK6;-u~R(DmOpaQWglh!gB?~HuHlJX15#mGLrr+K9|~odpluwAN3A4gM)VFif!U@ zeEfPJia4PZ+(}e)g@nt0iDlFz4Z$k$rD5MxgJ~B6`P|Mx8g0eNT)%Eim{>>h<=U^_ ze*0#z#exg^rOW%l9=(>O#aVAhr(cgs zS+>R+@cOj?q`_3`CNt?9Z7Gsk7de>2jXeXYyHbYc@-@yki<473RX_x5~o zRf^@gmxA|HZGCKOXy)0e1zk1tUbZ-A&a_g2xSNLs-o`;W(|i%w;67JhS-UylA(`xG z)%cdO@sqpIg05;U+-HEOcX6*jH>w+(x99Dha?akgT*FMZ8rU~j`>u0*k6cyQPdqs8 zqXR~ci|dYH*%K1KZ{d|P-c%x0u}6cx{Z|Y4Y)oPe3S4q)A%vl1D*Vp8?qn>^bC7Lf zn;PBOvGKtxPpJ}CT1HZTmYl(-Wsd+-F3m8*8TG~M#k&cMUVM=G1E>CNIC)xZhe>vL z%fa69L|R-#T&;%~^qv%MAkwaEj~hQPrBIJFs{%p8m5m2yZSd{mPo!Dx#uDx-loAIb zC|2*!hE;q(cUTAdQh_{z`je&7^62wwj=!cN%x2Km#L~X<1?xRYvs9mc%bX?h{O6W~t~^~aRW~VZiE9DV3_N9)csE3?VYV%1==m6QZgnbRJQrCZ`iKQ5 z?1A>rVB*!V7}d1nhdcy>z6@_zg+yM?SCuU}OFH-)Rvz))psL!4-Dw8Y`l`T2RY5nf zx%S&dBRlavI^&t^Z@vKO3ty|X520~5&%SLU*c*;^U&?!+jPCkij+U>@*T;+HLA)iS z%6sX7R32UV?6uMdS)Lba#GrQn@2wY|s{HQQ(Y7+8(?=s=_`X94qdBb;w;3`=%psb(vEdHrQzG4{0)(|UN zF#N0v*3O?*w%(ztKo{N9Zx7bC$TzjUOYNwLH$M1Vo=Kz;@nIC3gC=POpl9cK0)OkEa8qtHs? zed;`yqM(!=E~9bZYkWRtr1{HvmC395*2p94C(BPidbXUbGhE9uK18l;8A{&`FSC7s zB|jar=0q9O&p86%M9W)3L;@^MOTJc$)*EMQN3-~kIZ!yHthB5k0Nbc6ckVc1+@*y6 zxr#>!go{bU=VmHU8GAj#5S8-KmRGG+E=@VDxy|fc`IE^?FooRgb&Q`}%i45W^lf1O zgqk8Zk#u%3H-X^xGpX$IT4+H(htPyXj(4Q&tRd7a77U`@o=wv%*~sHq`?!+r=g9Gc z&(e>!ORbhEqP0ZE(^yD1V7%Tfy=ReP`BuQI<$%lrx+c8voBYTS)e(xEQ74wmX5bwB zOs~hNRL-x7KbR*VN(G_~J6?oHA;yD=Z|Z2@PpL=Q(m8d!JIbTBFS#b4HFUg5xz><> z5H-O7SrtFiDLk@o*=)%7TngeELmqZbDd%_B5PZ$Ht)_r=Vj&pq005OFF@o&YdL)&E zKaQ!9$+8SwF#_^p7G1PmXq|Ra7Hz5mx4C&uM?%u<=elR=5#w=vN(dT>OrrRyfD-W2 zm7krPvw?f%L{`GmsH5`YFtik-M*sW{Pz>oF*V&6T4dX8B6*pIqR?Bmr-g+F(WTre@ zA7Qo~IED@)&qN+Y-o29fGIwMQ%R6Vkw`K@hZfPh~v|ou0TG|k{n*#$Pt;D24E?%vg z=6JzA_%pqXPnu18IiP7he7<)!c@))uhEOrq^l4ERM#6#xYgWZ z+^sfZ?-%D+;`x8&UWSq-+R z+*Df<)wbSr*ZOW#a$UCfbb(g7F)gk_xfqy(MOu^MkZQ^fU*8?1HV)fMe;&E*V82;2NT>w5G~ z!p-qr9hL5`<7LcLx6^X-W@;D$o=Ptq*{O^qaHGV!UPE7=EuB z@DB0|Q99t9jv{5$osZmKdUZ9x#gHLDB32iyRbtEeRFCVh2$<-AVhfs*)$dzGFPNgh zh3T0A)+^e!+QY+D<1R|h!{-E<6UZ^d@w~!?X}4r}pi0Z20PB!64R$ z0LE)LFDCOT$5RkDT?OL!=R>7Uq&*@?L-%2wKuz1IbRiZM_z!Dj7g9k6C-i7Bmcx!nLC{fsv_W4 zQ?J(YJmyQBT?l71Sy=EZH3c3*Hix85MQ*fOyMELb)SQR4TOirQ69(yD2ZI_OZz!cU ziob8)IpJajWk0oEqm|C)Jc=yK8Z-IO*oCcqzSlWXWX)j9z;LQexF&|&e{|L!!72A zTD8Vv)y-Flo6@;j&Dn5e6Md4^2Xc-?9s5^^x|cY-@gJ%pdA&00?{nBO;F87d;D-}M zDdyingF%2Lk$(3D@eW|dTEggTQy1aO>Zzn?aqY<)&0p9F{Fi{0jo_;4C_vuFgFI9m zuT80fv9Y9bmui^Ug(2DYneE)!OW^QwyI>LhRIiB)Z^U8F;>Of*xI%siXR=1EZP=)d zl)j>Z5|fz>%%x*s_s@GCYZ;1|=_EkBP3eF|&D)71K!pBb%{Wx$JDqtdDgD6{iE~WlbeP)|}Ipwt&5o(H_ z-1$?~=CnF+5<4P}o9VmoTpBkcNM5{|gNk)SX8ilQ-Q@mcORyuLUZo+}>4XNtt;%4k z@rs&!%)I@S>pB>(A4i21#45?E&xi4Np`L5aF=^YrES$24e{yHX_o0jWLX&}9pQJho z2ZcufIjhy@7iZy*P0h@j$CVJ{iAd7N27LaHClIUvkQpX}!(y^0Rp01!lj~p+Hylbl zSY&rMuJ-MNfY;JLHf?g=RgEWl7qutYwqV{oSw+^aJg^J9U(kp!1i03pb zY@8B!jwSqM^J)wFx3Z0Moq6&+`K|{$kdysDm*x2~T7#C_Z%%EcF@1!$`kgHxomg&A zitrDkv(zrK9`*vu^6l)6ne)}5d763Zb<_j#XkWparm@LkVNFMB)WQ8KNVE41WUz?e zMt#W}*5l=sdIcVuI!)=nLfEuanPAR-ym?j%+3JR-t?TaL17E=iKbc=;i6*JxF-n@I zw7f8)U+oqEzCAP~61PCMKTF?>BH$(v`|^ciypN+}fFMr7XZ|}0A22{`tc2!d3Eea+ z_a?oZezj0ytHIxjOaGTyJinm65&O{&ATy6LYo7tgBzq4Q6i>}MS> z1>k&5lj>UsJ+v`O!Yinj93A;P6b^j-hNSR}2+k%5GL6C_ED&V%W1CrvRI*k`K&CwA zXPTN;-o5~`u4#WXAT)T}gBF#}l#!wVf_FOD!Gt8ocr z^sJ7chZ2La0I#P+7~172Wr@#0%B?hE42R(nUl#&Lx$GvlJihDh=o0cwMTt$Z4;q0$ zc6y{Fw zdHu~?MaJ^e_h?~xwV#Gp%t{L*vW&8IC$A<6K;_q?>kV^`Tbrw=ul9l$r|g|?3PNv{ zd8pUBND$+6IDIuxqo!FS9?~hh>Y|7AsDTpULDfvj=xeR7a_I_6d)h~xZ$6~OP2IVpkVp{TCh}7aUy^? z_IL2@_yB!~jD0{@VEh`*<#ST9EN@n8{P2NvMU(ALUV_mqi|Z@l7nDfCD|W;VW67ISQ^FG zDeUh1SI2JcX&sKDnsBh{Zt+gnKukD=R%RV`2(4_4*T^^`U6+?qc6is3!PAL`OykHr z*FoF=`M5D6rQnfr0WvkB!rstt&W{?_q8G0D)kKq65@eYx4t?+eXJXb>OI&r5x?Gd` zmk3p>r+l65WRtw6<8@=2=Iq66liru~pJhw-W<$MlkE3&c3#*_RCR6ilce_7VkIM`a zRvIa)I{?YpO2v@zZNz1JyoCzQ4O88u)l7L6!FQwy4^*pJ>>B%Wn|bHqN(rC5*VMdm z+-GZpHTP6-w14=JWCxr=-303r(@votWgipa72MXE_W6Z2F9wV1aGcWvB|NbSRl@Y< z9)ACGA4#A}W2xtlT>vaJ$os{Rx%E1bR%dUZfrN6vk@-qXU@FE5e}T12Hp4h1 zj~;ZvN^TknGX}xD=Ub_RT&22||Y-`T77s$nU=bKtJU4GgI;Kr4e zTGz#t-&?H>qU_|xXL}haJjk7P7YZBa`dkmJf-3FI`aj|Mw5=%r z9DscnR3Fw_ed?0=m?b5uCrfCP?|bx!1T@27n@olex-0qM_oLde0<%ml>I`Y9(oCW~ zA2u#Gu~vuyhvLOTJ$t6X&y~y?E6Ik=Jvaw)cs9RS0TkV#s&}={*N3G-iX;YA9SbAA zx1_uZx+o+4^oi@EO^Kg02VdO5Hm275ewrF2JHFONeT#JNXkap7_{(4v!!QX*zjJpr zfE-j}6D-wHq@uH?_5Ds^1uhwUChd7tKzZ*SiAXV7ty6_`&ZXAdfDih8IK+htc#2=EHFc6AqsCN5%WI%s zbF5U7{vx)Hx3RB(eMUGF07}=r%R$=XvqOC(uT2;SzaN@2@iMIX_9co&^7;%losHIY zaIdRgcaX6+JVG6j$Qg@3)t4Z`{l2Lp>MBGC1wXln;_llc@AI$`TC8^wpHDk$XFXqb{6TPf(Q<~n;IBgq}75B*8ks?%} zXrFC%5I<9@s7{)4eln|?fM>a8pS}@nXKZ23PuzVaKc~)Ol-oZJAE@hlAJq`(p^h08 z?!R^9fi4`8d3gu`Whwnn>JH=?q~#X!9}$^wV_LS>GR#D^yvr>B>zgnTjEuC1LAaDSPUUWFI zsZ4b?_;ApZ(o!dY@6Rf2`m~N`A%Q?&{LCmm465DtMM>7Rjse@XpkSrQa1M(TG_byw zk4WbWLj@vl9Erw%m* zU&+HQtevt1NkD>IDSmSXI*VL$MqIM_h=Z}W)fJu#N`1~N+Ro-bwHBb`V%5;xbsN>P z_&MJ+Esg#Z-ph(HTzr{+lpmn+6rhy&VMDjSAp1ogxT&MpX>LCilki&d@Y{XjPC^Be zLW#)bSwvKA@!?*5ECGB1%bW3Y2B-1(5ohn~xquEnpHwwLDHLS;R1TRASUQ{zkD_bs zn4<1HiSP91wwbg0*{h(85<@^+Y{?24ZG{}$%#E7)?6Vcp2iyeXXT#!(eLXCaLnF(N zQuW$P3j>4*fojAJJDcsY_oPmtN{IahT_7Gv{B+RjCZ}fbd0V>r-7(t1&CutBS-b() zV@1|-RhWrOFXWBW2X_QjoLOjC$6)BYG|ez*P0~iiYpd(f7cRJ+Y;Vli3CKMK;A|3^ z`m1*>JXqlr1iU)WgEqKa;Gl^`@&blLh?@fVD0winNM(HQ zl@kTUp`MLEV2TsOMRD zzN32DU7A#33YiyV^ib}p%MrDl8sv_-DO$X%muHPA#MrWV35t=q!66|kq-^N_hqCty zYwC->Md=_#5d@`!3K*Iq5IPDdKPdqXJrrr722gsD-UN*xU?`zV4K)M;(m^RgD4{2G z1?fc;h=SnR{?EPN{qDngIIo1gc3XR{xyBr0OoHLbhywOzVkCCcj%@MSf{Dq`J-wnv zUA{C`h}k{N68t|UFPb;7sJ?l)g1}8*M}d%~<^?s+DVI7J-Oa*GHVsM@kd;{k^?kL} zk3kL{{1A&tdi7{LTw69H{p-y3mb3kdy0?$BS6CM>X@hd4^io5+8v;C;A|rFoxBhmQ zIluzIKHIP5mZ^=Yzl2_R4Qlv#vXZ?J0&4VKp`dPw4dlR5#t%T?eAJ1j#_Lhupjc(2&JV>rd!wivgN8St6%(z z(d>pqL0IL-<)8F-GNA>f(KWKEGN>_V#%Uz-D*t}P>-M3>oV|C=*vsXyQhY3Yx`{Gz zkk4nCr>gO>$N&#FQczen^xe4jSw8!x%gaGDqHJ$}ONb^djx4?HYQqDmoAe~3WRn)t z+5stSC2Xj!ZlwVIXz*Mb`z^C)>pCK&5uuk}zPAY}X9ZwEUcafAibkRho#Ukr+_&)9 zy8N;#g-c3~(PE6?)IOGFS5iY>eyUu8&RS)|V))E&t-Idh^8Gq*DoZbA>0%&&NTq$wgBNI8WHY*S zMmZDxNo+@UKOF8(zkhoz;g-dPJwn@+xxWJeMXZ*37^M`KcW$q=GIGbcly8nvlO0lJ z87{ld9Df>L|1XJp<(gNM*{4_tC!uuozUqP-qcqw?VV51DXPnjAvea^1ul)7=zdh>z z9()qJ$qR<#Y^Od_N5u!Li-ivG^+vV2$dx zUbNdtVpgXAZfjV8G`SnU&g=e5=%cp%zO%X;NY2cdakbB%^=If#+N0Yx}{0~ zp)0jc^muok=gH*z;-f#@wSUuUh$8*MKP}so!OhsZ$5=CD4X=0Mjjrzfu}e1Nr{aCBYClmI9my!*-Pb1Lk*1m)PgrmSQ$bk4wvx_gwtL25OW4a}W^6sQ$d^?& zrXfsV(M}hG%2F1@;Tc0oE$#eC>*Lgx6lcWVYpVW{=C^xkRC49l>#(P$0K!FI7J+xViwkzCgiyLw0+bH_1qo|;{}%t-o45% zQYqZfnHns?4d%UNdqi~^0HC|X4nx=OV2smC-Ez_A75wA7|NAi|`J_cvejZ)Xo zpBw!UxP7Bq{@v{M-{Q)?1ox|hfjhE6mY%8I+_Rkq8x8OPCiKNKM9Z{F5qzQ+PAj+g zv9z?pck}g0&QLv3jnDfu4}OCaZ2Vq4AO!1n6j}<#7;pJrK%hE`|BX4>J-L zGadFl1-{IZhYZ;ey6KNnMS|Ih_;iP>96c_qdLRf+3|^fp;@8uz;b51?eyHg@)kQ7*s6ndVkqCP zYw;w^-<6Lw_=^HGL7yjHfSlaZpCdlH4B^Ir?drqUQ3n(q8ig@ zsiAF{iv1rc$Rp~@Z6DKa5F(jTB@zu%pWai)Hqy(t!;9je*%#st9KnK!c*8k1yco7i zS0IB*ODv!fi;$=D=qvFEX+jPM)$#7DUji2<5kvq4H-6SVn3-afQ5p|EH7jrsxj}&5 zzeb2;lM-kd1KxQpr}%=)1d=Swcj2cn9e63MHk6G9MM~>LWe0<>NkBmg;W5G) z=0{nw=wKLF!K-05iKuWi?$?-UaYKak$aKS+bYJg{c2nzdxKX~BoafZCf=eFqJ|i}Y z#ncMy=#>9X>|#SC)DWDraXXfgitk`Pw!n#n=9MZEHB7}tr>5^RvWHQ&0_S~o^CUO8 zH<-=dXM0A(j7BnAJGR^f)F2`4ya33`D~&njrhQG13@GYusav;RBY(c@?)PQBD7q_? z>{1f-8%h8jpG}@!Y!{UU;BHwsRd=({I1b}KGbQ-+YGAI({q9vr z8x1pu47M7gcE-glZm~h-5DU3MC@nsdE0jfMLNI*8zCz@R&!X+m^o|)7ErGoy9W@)@ z<||IsX_I#`;|xBcU3GxMVGM;dSUej16N=^eh`X?`ul-?P51M_XEhqTCaA|puS^YuT zVaA#DXlwWJ1vE23Ouo6^V=;85OG6qRZoi^%jm7ktA>K-c9zCp6zgK55)YmfBrkgAJ zY=e->(k&`KD#pN9=hfGMXiIqPy*O;a#oBP5n&4`2z3@Tdq&D9CNpGnMCzwqd&0n(A zQuWY-GSBY_ZfR&{L?MUC4*ktNJS27dLZm3(W!Y+~+U-U~Ci&Y_rHbCQ$RVw1YHzzh~K=Ut7WG!tek z-fC6y%5$r|TrcR2%yp4VPxY21<3`~E#+L~Ughg#%B)q6Tu<+gmw866Q5s&!8)`^?n zrz(VPKXbTenRnhK^c}x#>blT8*78wo`eMN}4O8>B4`9Pntf6maMQ}gJqxLm33-3NRw3lu^G4kH zc)`X~u5e2%mV!GxM0#FVccl(30wd=um4jxIL;#>2BLWw6Toor~dYe7f9`uHTCX=Mq}HLZ%JU3ISVrr+RAL)J=7L2-wA+vgFjIv2ArrDXh&Q7{e>8wK5BlI&Vpl|%vQW&XH~fqggM~6bKu4q4 z{HAOd+OC(|LnHNZdb?oyx*>flOBq_06OPaO8+qE_CpShGU>I3e}7LafCZ?9AA`b!-8qG|o>x6~l;{p{f1Nnr;y*if*+O* zaP&hu6}gc}Ew0ki=nkRr7LA^%59^*~!^CwPX};%Wm%;P^M9uuHF(>Mt(@-e`jblj) z>dM#jo+#DusR(P@^~}@o)7kw?n&VW9pF9k{%jd%SYo6X<3J;z8-PpPmX4Gae)D$tZ zaOq`}V8>r|N=yHVZ@bWrxloL_a(^DI%!w7htV#33HzTj}WcKeidd3V1Flo&k6u;`Q zbUW)MDmxyH4kVHvO~{=EW5Vm<9u|DnMBKoD4muTXoT`=kb5&jkbXa4WPcM)4%A**& z)N)Oa-uvn?Q)_{OD#}LPoq#_Ve$0oC5f55*Ac`qUNXSdge6Nn=t#|L}}sR z3!t!fxy}|bV+fl39i0$%22=jRC3B4$a3-Knif0rS7EbvsaH4*;VEs)X`VGR^n~~}= ziRnJwb*jrfF-R&#!jCL|@hbY?e}`Qb3b!mAmk-0Q(k-+9TIOGFV=|$;bvtCWX(#4Q zjvSMN+(t*Dr%m`yE~SS06gxJK+x#hlQ=s!jS6QBM)Fbo?2^FIPYvdqMSeQ#-u#uoI z=+#(RtCrDMg9}@wZo#{^HA1D!i$wP|H)oF;>})Q#vi@F9SQI)lMB1U*+ok^M2i$yc zEI!Efm(a2pQnxgJlqrIhav+qfaI1o|y0Ux)8hp&(h458?CGmXbA)a zNd%OQ)Pql35v+)UlpmN^vk=kVCO=P~TVs~ydTqqH?W+zfL5Q8;tx7OVh_I-Z(Xqb| zC6qcXBBwH9Aqf?uVNF(Ex$F1VAZg4dZ`=TM1zT8+E$h)r#d%fX()D_6S8luIKCb9l zIvM&4r7I=vQ;whd8Wh&)DQKhxSzJIIDrcs_jmJZZ--Wi_CiHo{@5_YrXF?#YlXVq$ zjAXIO#qb;Zz1EK!snKQu99<)j>))^VvVt2A%niu>SHctV)OUKrBlJnJeb=XdRWX2n zy~(LmwglT)C9}Gbiv>A6w`rV<{9lquS2{JM(1jW6nMiJ9L>`#4&+P-iEX#cKQGfUNVgYrdQR-N23!-yX>$wC-d( z4wWWn8VQ-cS0@a?z(YFdNVuHS`|ccQu|6Os(39cYIwesF8#`U!yX<*E(ht~nsiDLZUZYsL@g@TzZm$eAhQZZ_5;BlFn#Vf_f?XlncGQDrTW-KQzuW>LeHP7n;tO zk|?3O7C=;FQ)v~{xSA5gUD#!Sg6Ds@2y5uej8+XXFnS=-=jbTk@|D@%*p>VUvm>%b zHt5Ci%$`lRMBg_b$SvFpyDeIs%o%-G=11Q7(wz6M?8ffa?(*6E5+96lj+iRGXDrq8ZSOm`xylp;&j2S1}wr|n9 z)IPm^Z~yavbWzF|$%x~_BgqgE@;42Nc)lY!yr$DT!BjtP# zu6}uW$u9bju4Lkj zg0@h@(7w=H1cwcny{PORG49FHPrznu<6S3(Vzu?xQ#rhax`0kH%(c}tw6Pa%TKY6O?`bUasc!NbIaP0I^b2KJ z;Ym@$UxTH+yHSIcHA?oE?oQ{5EhPxj`!S)TGm(Y|KUF4VPpk69hm}7}oZO035!aun zNAv}`x?P^gBCwvz&NlVGF%oBBie2>u01jzB)n+-a`YfxY!^wF^-@F~=z(81jnsNW@ zF+@Flg7Qm$u>L+{ZRAD!iSlSbcEMfmSIR2lj2_E1a|GBZ?m^~(+rV4B6;_?xvoNLT zWJhMYc>#Tqiw;V5sT*BSd2|Oa?xIzu{s5*yH~AuBC?QjJjanLizwEwF;9fC&;troQ zSJ9?B`LzHouS$xw?~B)88ZN!Q^o~a39W#TCbZ%kz(ls)?c~9Nq4zEg%%Y-8kS5d>H zp8=&_%~A@_3z88Pppr-J3{??=V7qYw7^||g7;4P zdLc{D#cz2RgZ11!#_$FpBTI0|4{`em5x(k4p9`AsEqdiLn3*%^?3oYC`@8Vgr}~&x z%F(9G?uv*A1|s@s2ShD(|E%%M3M@Zj4>GI0ANVT@nMJ28oUOr%y8A zM)%USn!o)b;<6&)#%M@m@j$LIw#~iZA)zFH`^MEDHja_{GECc>3B+Hng!kWm^$7lO zNYi#B%Wt@(sa_Ie)HBYTSN?{^nhrTL_je8_vf30*nFq|7a6HjF(ng9-incZ|i7$)<+Q74NcX$j^r`d89-0Ezz`tXN5LIw zPn+fbtS_2)y2D*gK40YJ`zrNmV)w*dIqUgAT7uOZMn|wZ49tC(`#SbTBShEWOKFy) za5!3cVk+18C-F}2>~r5Z!-3!bGfRf`ucV|2pLvx&dxG~Hh1A*jdPckhffhhkw#FVd zZ&yZUQ32no)kJNi^*slGqR6z42XV9y?ebH9R0F=WuAgnKgs&L(^%A$C?e=`dGx5ew*~h|)NtfagkOOM-8;-^{boIQa#C-}H@Gb! z6H58|*tDQ*&2XZcA$}*{WKNJ5;N1hUftjYzFjpZ>@f3?#{iU=L)(q(yS z3U;tP?B`Oo_kMS-)##-t`;s@ zaEU~ISYkpCR*Zt39gTC;HyRB1Gqnf(2>1P1O4M%l=;a-jt!eCxV`R(RV4g!_e&?q< zL(2GXq*XluIZX3YEeO#^f;H9w9Eq$lA~|*GK=a^vNX6y}tt zzO|^z)BsLF_9c`8sT_DKoqw3%{&D)F4OjiVyu98>1i)Q)0mT=53I@kvshwn$SMCJn zA#H2w5FhxP5Nk?~p@X{(t;a0xPhQo03u=&(lq(r1#G{ZGS2OY=JjkDMaqNdSL%2FF zI#==)7;rf*WZ8l2@6D9r{52_e*nM$IA6Rf zwyX(UG%aN$nYzt+aNg*+_^!yTezIj@frsSlj^hj;3i&}ayOST7UmDF!derKIMmkqY z$;HdX6!^+zT3T7DOozEUqB{q zwh|Al`VjGh#~$wlW?U?@WL6a7zrn?m`-C45O7HNoVWUnBni8fHsc^5`eq6PF)&u+A z*;Mrqz!-LLHZz>wXoc~kR-2Bchgyz{Uo+c$Nj~%Oy!o=DMwO12kYRiK?*91=EoP^J z_1un^C!U`CO!;;rSEbH?bR}TxH9ny)jhf+TLMZ4HFsiMWh!RG+si|SbU42n`ZpSgd zIyqviKa+Qkcrg0AY50raOJr*}{-;kdRhbF9FoM~m$db?}X{=2l{mNzR&#_;+K8HzO zNzl4Bm`iqX(f1C0zf<~HR=8;g`gCumj`_uG5-l{+*Z~ZDX<&facddT2DS(vYz~LzQ zW%BNoADN)tY89)-in!?}=-zme{l)>Nx|h4+zMV9m+a(Ce2?mC0cD*8}TYwD`ZKvA` zto4gW$G^{|=1x}}&s!Lsm(1wr!$V<{4w68PLYOG)Z0tyu!YEKJMEJus`}W-K9R(|f zP3Q4BGDPT8TnQ@#%n$y0gx1^yimiT3LW(bO>`|E^3~WH&3&jZoAocuP?H<@x%~aE@Oh*;QY>j2l*QM3ph8X;-l)4+DG7$5i*#_BxG@BbqS&3{xmbVynT` zc~+pRo5!~Hl^x839lKiSbx8m{Z|Oir=pyowcFM_Ne#O) zUj=gfh*Q`-S)VldwpTelYl3Qpfo+F$w3!mA0WYLsMj*`t^6MeZws@&5enLDu@3C%6 zGR#xq=PjQ%-XI`2>~)L4E(EPNdA#- zsz@#u9q*J5=LUe(i}&(~m*^K{qsEj_5Z&qqGdZG_D45kIzT9K;kJ)ai{;_v$zNnR$ zsI0*4z9NtO3ns-5RFX`7J1&lu8rwb8w^bqih(lZ^T2v8_k9Fn2L+T*Ei~BbYKis5P zzM@I1Ui=SBNK5TEaHyqXU_i0 znR9ZV>me@Zise$Mk2LbYRo`u{P%ajJ1D^+#8%l70YF$4+%fU@~ezio!$@GA9ep+4) z{!f3Pg#bmpkYDj zWK?Ot?~`%dsoluCL4fkoR$wwAx`V!iBpT4?Z|R9V9*ao-j5uhqqb%>PM%w764k30p zp(b>oBN%+GCuMwwU@L6s;+;ON_tQa3MfDBY)&6IvbLg?|u$MOzMR)gC5Xy&;z=3$` z;&|~a)OX&#T0Eg~@C^YJsXVEMe*8W%{1JCpsmNFVuZJ*}mmS4$!0|yhn46&UsD{O7 z_Aa2kDI74e{d#}M=8GnzKgE^2lhD!u(vzY;brQ$tuIhR|D!DTSNa=UJo$%k4eNR{= z%|@fOxx1tiF_V2&RpCDblb-*589sqZYP43<*Xg)ztn+U?p~q5%IdC?C-H}(-Pb(gv?8wW19vut!^)H%W(*0PZ)s>|K(qv@$Ad(89?N)@A zsgX7-2$K)alY76vdD6o(Rk+vs7KfAes4#^nbjgj2r!U?lt z>`CEjVQFP6al#zdX>X~fX!h4FFzZ}n@2a)XkCZn{`u_s!`&HSX6Wy7ZH!g--uRgcx zeiK89Q->OFGc(k|t8UARqEmBcJJbdDPo+`Sr?L$l_<2N`iRDtv3#N*e`@$p188{hK z%Di98PJBB?>k`jwSlNR2iP)4;JgL_D_M#enb+J$XDD30nN#oBde9qB+{mdn)|3bre zet&-QzObU(w$bJD-IuwS&>tLYFV!YJ-})0^*>|+stJJpLFX7+XPSk!n*#1o*|9pN+ zuKMq@j-!JC3cVJG7cq|>2Bt?LS#e^mZ@#$ZcU*lmA^t=X!p2sYBlX}=w?3bc7UQ)g zrZ*{3!ta_mPVAi7dn$>sW}4ky*D}%W(=tW1qv>jGdl@TZ)tDSkXVoKrO4HM~qYbog z9X1EP@Co}I6ap$a)-{$4J5Axhy>3rn@>cU-e%e@8yOgtSDy4nO&JE2j96!wamL8on z^Ty(y$;L`O;W$XEU+6@Awta7R$=0aweoNfzqZ|t+(zf|X!fv|T(YuBGz_t)_o_FQa zDutk(WdX)%*c;y&Wrz{g@bG^8aI;KiJRQm{S}XE(Ve&_FQ@CCt;h64v;qHnrn*jOg zXAa4@H}RQg>Vm8Pbfl&8#f3{A3;D;sXQ4X~i8G}JXd|h&}mw@@M_lFcB9%-uL&e@|>!z(dOX(yHgt}n^5)7xN|^=eP-QH z*S@V^E2JuPLfJUlJs8l=RJ1(~`WDo7Iw6>U-eKE!&?R^C(rzq7XUgTkl1Am^`m~~e z;d|@!MO{wOLT~8ra2dZt-3)kp#A#SNfAs67s?f)=Qq{zRpi_NK`J>RCgW#3w&e~)n z)Dfg$G$A}S2y6iUqq`q&pqH&@q$LJe{ zX18wyM$B?F<`X{nw}+qDa^IRgh*mWY*DlXT-n{-f#IFecQRfv!@LRyCOmPX)bSGr_ z_gZFbpYtAos_S&!TvxPfAANCn$wD*opWV#=o%ziFcCyj>KN{NnZzr4o-Y-r%tMC+EK;u>{dqbFZXt(43# zZgBJWkA1IrVBnXd9n49Sdx0;XAB?=xaM}JFI3hkG8THS4CXEU*ggZ#55V08i9avYX z{%aQ|rcm}IOEBHVe@}!td#U5c`}Wnyl!u&9oVI%Rx&@EuLAJ?!>0|{DGq~>O`%$6) zZ!l_Pm)NnB5Y1z$qKXHFrSq=F046l=YSS<3pQFMC8@29b#(;fI= zm_30RydwY0H2%+hP;NbDFlImDSXDmO_n(RO?5XSKp!uFiXg*1gk8b3({f&YHyOrPX zGoIlN__R9f4k;z~jcL&7adPRb>6RTyIAYl8;9gtKCG%;D49>TwVsG! zi;^=Yxe{aTR^YQkznwgY8%x%-ll;W=k7C?6u4!S`1kInyqEnx5vVco0eHE|+?ITs! zk82j1my&H8oms(jBSUtMnm4X`GpXr+SGOXPmMm#=u<@n7IcMT54;MLa}k&11ra`x#x&X25PDoM z^7Z_eI%@H7VWO5UB6a<#EIA$NTTy2c^iN>}&1{It=gL?fPiq?BxpAiQFE9VK z*ts4cF#0Y0^{^&k>_7b^OHWaRgPu~rCxdp3=>`MRrsoH`n&Up3l=O*n@Y*{?lWymw zw6;o8%~OK^(k~TJ&vf@hEhN&>oD_Tf*7wDNh_9BbHZNuzlO!YcT*q0D!W<{D_QgPL|XlAQd%WiejS`;0UzvM^4!Pn-Qu?_lPQri6a2leGi4!N$q=8u8Oe6P0t{7I7;k`+7Lm?fLVB#xeLDrmV{v z=)^^P`QiI_iS%#OWvU~0&yVMI-q=ltN+$013#J9Og>^Z5t)$YTNk^AkN`36paL-O6 zd(r{oIs$|!l@i_KtDOR(sjKMqqz8X*>xt84Lwi+Xp~xs5A#V@Tkf-&IY$$e&8yOf@ zRl=c!pE(?l#qwMOD}+yIO|1dRN+wT^Bf1pPe1g%TTm}mW`;y}_xL(m4dIfyGB6FC1cF2%DY`otx z{1fl8XC?DwaS;hFhO*`EkKd2*jPBy->y2|G<6!wtnw3sBJ1`uK0Gy4JKn&*m}v^zUcP>6*qeDWsDqyPu-bA4o%xSh z$=0pJGYWzT(Ud&2Qaa|j zm*wLi!xuhN8|n&sZ)HiIP`-7Et(nbJV%_z=&n5NMU2r$1MKb27k*qt#HC$`VQyI~q zs6JZ5owj%htNr$f-7Lnt3tF6|zIJkDr?qro>HC~97wzR}5Ohj5^*U?M3fDg{w!1eR zQ`{7FE8agZ|I>GctAhT`eoeCmV2jdzq&?_-5zwvo*9}g8b%xOQ>f6a>oYt->Sw+TU z-Kfyq;)Ps@(xC28U*SWSLYfGS53lmsrf=S2S@~nUgTP77NU(L)H&lxTRJ5?|eRD{s zGM7YYz+ZeU+sMO#Q*Nc$;m=sm!6(CJsd&YKk$Qx?oiX$RIR=UF9%MFkdL#;_Jm2)O zSq1utXA;;vVH5qe7lESV&8^R; zF_cCWSX>0<0dIzm->9vkpDg!F@f604IK&gVz;&Uct&QufIVM94Kb_RD7TqBY2%Si_ z$keoR{)#)QlrjlbT`7hfUQ!UVGoO7?6c9U0KJ_sg3L>BdSvL?`T^Aq^u_qGrKlu&Co#%-ezE+@o^0^4@Eq+zWM)MP*fu{?YkWzsCmrkSa>X-j&M89?L!FGJy+e zUcs_9?n>(q>k!-?#2*Y=dSl40=l;)3v34FzV9fTU?NZsjtp+j50KH4@l9OuxIZ=C_ zv4dZL(W>+2p+;RXlBefZr>}oHHgpxNPymVo zzLDlnVlM=&{jo}A*1$}8coCtM^nvRpENR`ooF19MFFZh1NV$McgaO?cfxF+r>QeWA zbguP&(Su7gRkLMA=)t5iy838bMzD!b8J`n$nmCiY#mdYqBw55=gp^Mus0~N^C|ekzm^nSjl@g%Gy=evcs=D0A{EA$nbW#Z>Ts;AMK)^6INH?(#ajgiw#=sxUNuA*c6puTjD=qIr&gaIUZa-=>> z<(XPZHf=sXp>J+4-M(nlbUPSan2GHA+K@KEt%nd8rE$x8aOZ(mGy|Z4_}8V9?O#*g zf8I`vhgSN`U};Hn3N?Bj(eSSw8*>hk^Tm=^1LjSfMvV*>G@t{F!K@@JDaPHO`6`aH zGJ4n4;d>+?_KHT*LoNalu`Y>djLI6`sOUI3EX=NH+sc}XM>(I)Wh&tq1u0! z4r{_@GUc}H1SQ}d0c==ByV^a*m2StN;u1jJ%g^7hL(+N*H>s5 ziQc&v{|e%gu(nmluYL^{f$0J3l|dl=|OAAH#pnw1&e&JA9&y zi|jnED4r+xocC^u;o*3h5Dt9IQm4$<79Y|+j5WJ`gqg?oqY3NVosWzDCtmP zOg`}A<`w%1Q)LuFwZz02XtyO6FdVXSw`aead<^^$h>t83WB}Yua26QdEz#5#?hK|J zpcRD%2&G2P4*vA1&llZ0?-oYeuZl|j4FkgN@Y|PX=OUV%Dpw1ilNl6eN!U&1oJss6 zk~|L4#sa$}1@t^bw9u3p5qs>?PKKMe(_iZ^nOV!YHVS~l=93F)x^Dg1#>2|vE=i-A z5WuW0>8@vY^|709X(5a{QSrB zW{qd(pJw{OpTtZL&Fz4W*U|`w0y2!W>QwaYqZoZQKj|>bOPP`2?5$;czmPxYS8y2j zVHs77^UkTm2#6!QLb%{v9Q{ZK^k!)O(|r2#Ox6oosm(BvpwATd zD!zTCc)^(TUp2kkwCneL!u-mGiT?$VZTA0Ek4fQb{|wghui8I8pTS{3{ zz&ybK3YdrWyRztZ9dkt?sXk1;Mf<4l^1a5Oq$YVYT1_dsmADxwY2!JDzMK7P?kMT^ zC0ikM{0ql!hR0d6XKwqZY(P_{YQL}3B^n;fv0e=|w5p~=zvfK+uPUP0I+?T-c+pcp zteNgFr&D#^H0nAdcyS(;{U8d2)UMeUb0*W(UUDQ`#LZC781#XaB?{15EH}^~b12o> zlUcOLB^@WVo0>6jT%A+HU6Ob-nVqY9CHWT$Ejv5piweWg)mVcv7eAsY=Q1`cM)U=BbT0E)w zn!OLU7C!mn5Qt%UHZ*kEf`sk8f74C)s=Tz`IICsf3khuST8ZLLnjP;U{850g-EZvrd@6IYB$P5*_YD{hS zh`6kh!vZ+3%=@od8(uZ~=uEJis5lJT`g)0;$I5W42ckjutZk>Wdt`^hf7Dtn$iOnGplnkzU~66WPqx30)pgf4>2fm`bPd zrbbUb7~(GA@w0yYZAL*_l~LaDlh_f}%vFR>=z?9IuszTpDCh4laYM1`>(q}VRfGZN zR#0D&Ve&^YhMY-O%DmQ_S%8B0M^ta3B|)uZmPRS;li2z%y@*g|aQB&QsP^=Oq4{kQ zMtNz#S2fIF3t+sUdRHw(`<5q0X^tfJ|uZ;9DYB6_mxjw+?IErL-futv% z((qNTwv(W)*6vZ9^tt4StHju~$z>JQKiE&9b?#a-!5$=51@UAr`H#*4*u+G}oNQia zO>@2b1EEPBT&}`nQnfTJ;!}n#++fhd+LCRHSUcZkrJ-i`=jV7_n#WH0sd~XR`Lf(U zAKY2WxDZh4Fu<+bZMxMt5xdE$v@JeM^Xfg9>vzjL)gOo@+%$>S-m-zPx^1bHyxfy& zV0pn|c`X{bbw+!!`0s=E`XVHEbjR0^NS#*fDf{5zRjW8;4 zdUU1-sQ8w?=1RBNe_z(S`XnhFI4__jQzg62$(-ZLHUC|%kIn|f^jn@oe}qEwAV`)s zz?>DMMx(+V+pD3=PM_8W`R0F?EqT!MvY@R?@}nVDH5N5|(t({KBtQB$ihHLLs@7x% zum~}BmMUFuu!ZQ!NK}(H)xVR@uppsXuofC z8#+}u4Cr@GFXdsEW^B`S8fF9iXuSR&up;9|DrQ~oj(9sC8=3xt``QD(sJw0dnXJHF+h@nWLpk)B%6Ff~gsqL}PxONAFAs!j7rV|;w1+2{_+VfN zvs<5Ck#_;+Cfgx3pHW?iX_})#7wlHZBPMwf`s&(Vvwrr|+~$?LbO;{@`8to8`e(4> z2Z5diOxxwj%?cRLaU9T=CPSD=uT*WHR{7XC34fckddsa@&`RLb z4tm42We_JrD%${`fp1hRCwACuq#i?WC`l0GW*RlEXs%1-C5Q#YB&7R$0P9OnpD0QR z;H%daD1XI%LR))-C>Dv8o;%?0Rl;{1NkPMTYNVW#m&Vgtq(jCWZtOso@m+?0kJ9Xk z5EevlI5Jv=OV}?^0?siZ_`xTXc#n2yt+_|4?QD<_E$5Bz7Ki^%;PQi zelJKXsnLkYN{>nP_wDO`^|ZIIIN|)Oj1l@nHG=(oee>3U#)eP?*l#1qW3{-bgu=kqQGk-3ob^ZMkOz9S;j{dSa#3o1>)>yV_Fz&2dr1C3TauTE(6C3U;wxrbjwIocQ5 z*y6hmq^|9^Palmsh@L0N1n859I5)4~4fAHV?qqWc3$TFcAfZaE7jggtfE8JcQ&fMR zpZosNsE_`iWX~_`V3v9W9Zg|m_{9f?>d$ZBs)upwS{{~50~){aZ)ow+Y?^Wr%K*6T zIP7{gr=2m+ZzP!uo^d3bXdK~Uzyj>Hl@U@VJ@}GtPD0Wc`(@$WBa+S7+_=pD3LzQZ z<};gxmg9r2xzSQ?y1p%+R5ltRF<+Nj3^uh7O}PH$9`nGXD+>%!)wwo#fUqHKk_!0% zTy}t?@QP$EkQu66%B=)V1BG3fuuR-J_*|GOfdmqNZ7On6t~wOY-);T=6(lm zHQWo2KjqBHUGI;1_@jIME-jk{&a+v&&`!*on3{qlp_2PjIC(uKViwF%)c4 z1tD9{C*k(my3u0ST1j7;*t$BFGn8oendeG-%6UIt9VZHXm$2D_Nu74GrjD}=A-K^V z|Clq^L4LsKu6Ma>r_wl(yWZtni`JitWkWcO3!B)a&XNbHoW3;rdnFbUEQ6SJV6)n4 z;x4*!z!)7p37ajdAkgPL9Pg&X(+>7|r#swf&?h*lINcREgw6I^HZ983l*n19K7P+S zkO{~-u465s8UU0wS<98&^lrjt!LZp&VLtJcL6yCAAKdv4cb+V4VwD958^XqOjd{Ip zYTa&P)c`j3y3CDp=2|&@B?jcW1PPmk!A8SH(wA#28arRd#(QEnk-bUoc9*;M-hR4| zR@8*eytkKAh_f{K4>y`DdoDA3U*u%A!J_eE9@e$CXYrU4G^G%c?LPY9kLqN(lulSB zY?f3(UN|vE$6zg?SJDbZdf~F@o4aLEkPr2 z%L(eoESJ(G2f}7ysn5Aq%vl?V#4^-mZcb@(fql9oYzT)Xz@~LJqyPkhhr0C~9OV+z zKmc@}c;FD?jB7P@xU_FBu~O-4xKuG-~nstTG`nvO%rm(g(hc~ zJl%=qglsfgHiX06V55@(`pQDHE;3j*;!*=(x=gf8B?%1OCYrFB0c_Bz6VlbP@k-OA zTyYjiC8a#X{f2C`p+Q5~aC=>0(`Tx)XW;-Wux|7{mK#^81B@FgT& z!Rgzb7K$(F-%)DWgepG}HUq&%ve7yh{CjL(tft1jU@I8dxi&RbobT45TJ%(^KHHYW zEdu7;-f|NJN{uZDGtN^ujo`c2GwE;d*OP-|+kHeb`!P z*-Q*JGRQ-~CgeiXmWpktR&wLU6v^CUI;AD*$AC@WK)1gU){B=tCFff{6RQUA)mj{- zzpW2&)Qqom<`e}l)dRYuC`%?n zLDf0LWyZFIQoTSrxvvHQx^`Ek`lQHbH9>i;*sYa3mvd_8sN_t_tg)6#=D_H#p+mr? zLpUrN%e)TUl< zRd-c)^-NFC_fXp1w{Pano0+c9)NjD112!e#@V|55Y+>2ZqLsQr=N1qkVpoh8zh6DQ zi1n=CTH|i45$U`&a7x|Bk05+9=hhi;9+z7bY&dAN(dVTWLx7C~Hl;)X4L$@MZ!o^a z9YKrvmQ};zI;+kAiFTK8#mg1zt3urC2;a*C)*g#Xiv>OT%j0E$lTYT{Dk;EgO+~;) z0*#aqAXzkkO$9W321jn$lz^j|lpHvbnP-;RNvgr2&(sv<>JaJyMBXKPMysSJVS|4s zuiH+8N)8WF%B6chKetD?Cq3>i{@%xbe&n)bT553UeZ8>JK%>D%(v?Yva-F&|aJoRF z!N!sbU9xaCt6(-1d=}H9|N3M}0JXTlq;%;SteX6xL&}UIi`ojN=Z~JbES$7YejjlV zmhP_^;)U9=jc6H@KI>m^e{Qb1Db@DS@$!U$9@vyXQ%ZG~U~?AWbb+QM7TD|!9E&CO z;_>rpTJ-8QHdb)cwxiF7RC8i=Wl2GPx{lUNLM>6CmZ6yMn=<<%EB&L-y>@IPTENn%BMZ_t+|||!n;K{&*jRMt>cBC_Xf)7hzDWr-Gg>&c#c2W3N=XHl@1hlb(zc`9 zq>$3~q?xxU;o3H-#wH0tPdgT^C^J{+{C_-#<8cM@;@b%DX!pY&2>>mz8D3Ws3~8*6g9PCJ$)x`ukV6fW!Zu5xy;{(%QX|07Lss z?@4Yoip6M_Rtb>wIULw|hNS2CBcZR|`<|Fg?-+x~wmIA?y7TKh&6#JOnL{nWSuttA zoOMDpC*(+!$hP|R}ywt|ht zPrj-()03i1ps86i5l_?+MVqbr;E<8_!Cyp=+wv$a8LZM3P?;Ivvw zwZhF(OvmF!(>evTfJM`ps{_Xk8%sKK3+cCnjir8J)#A>mROea`wLPq}mb#eU;!L`a zyzOa9@Q||U$YnRnebkyDpiRQu1EgNqeD=G~ZuLNGk&@m7Yz&}jflZ$Nw!|~ipjAph*!sSRzJHRXGrDuN z;G7CJo-}3;Y$VV`!^U${mfE?m;#%u@LLO01({o-&H9oy*am3CQuhA)e?uh$@*W-)| zuv-;=q~)4GQ*xnQ1oS_+b1}H=liz$|9&(oAGfDVs@4WNQzu*7W`?sV78wofOu&Dv( zl(4B~U`RzvwJeMrG*;L=z53qdmhdPeu0(hiQ2sPc8A-F&$CRLOogT}2ndN=WS_@%Y zLA004GS$n}?wNFKQn%+4Txu@b^uLtaTzino-D&xlfjoxhh29JgQVBGr&rj~UzdwH) zGJB>b1QrQ695y*{O8QMIcDH1)ogOxpbU&@hoMzRuf+m0Jlrp5YaO2Dvi%Ckqb}qCT zDP6(rzr8%1E3o7)yQ!b4w8-aa&r}~H-5-3|SxRl@i3QSdy4pQfyJj^D)_eWE=iHiM zR{Q*P9`=S!2{^UvG7oG>j{-Ih*py;EQbItB`;6b9$;#oDPV~Z=-G#=jlxU@&TRs=R zv!~LLu=gtBi1?ipl)tuWg_bwzy*v$CgNy_OZaqg?95t(F3k$TT7AJS5d23=?TX<;Re_%ktD} zDV^Cnh%LHUHe&x#7NiF@nwu;dHkRYI0;g1ac)G67wc%(M&{%WgHL-NyMH5Zgs@bdB z-&&9v>B7=p=Zeo;0NVRNldKv3ed$=}Cr?JOT7}Y3wbt&|QqQ{x>)d;P@dpw9nKN28 z*6&*bjy5j(R9&Ara7x3`tf1*lNi6L{b2VtL42#TGOA`@GnY9x@lwy~%$Ef-~+EPeE z*SIwzC8CtNKhtsXW3&Q8`Yx@OqNQJ6Pd~jDODKowQD9?rx#hsg_3J6;*>K=!^7-#S zue#2nVUsUxOWL#d33iosHj<+7q=_G`l}+B_#d;mR*E?A&6#FS89HX~STT3jZ)~!IY%JOZraR8}HKna91!KOG8 zqOx8&Y)Dpx%TU;i$|fn@1nJ`LWr1t~xFdF--a4?ID**4P=vfj|O54>Mf_m?h5w%zr zcU(%$kj4}6>nADm8DL{6UbdW9z@`BWa?t+Cdk{gY*kaTTqo$pUTL!cl)GJ4 z?B=a~CvCKWwJ%y^7Mta|YU$4xWeb9}Hv?=U?^~`r0h>u+!&^nv%v&u$im}dhNz*6k z-0|O!G&Vu1)?aOGgY|Q2ZnDVPI6VXTtPdu8!zQ}RHehog%f^!_w(20X5h>DInsm+5 z(R!zC8K2p6$k-@rDzAI7e$J@uvc1;V-mu}_7$U#}*c=Epd=C7GMSWyW=qnnPo0DKp}jN|1}bUu!TKVQq{W+Gde7D6Lg@wCI^UV~($f^g!18 z{Q;YuU{kvxv?q>H%06%Te)KvV9{7(KF)&4;s>6EO>N>TIqK>e5wrX~o&p_JJ!m2&2 zwzjB|^h_FOQ0n0p5u*ZZCb4Yt8%Kl9idjdag8y140c(RzK1N^$@ErBp=f|CodmgE@ zDLr{h%S5WPY59DM{;&e4Hew)+Gw^(;mU9W%41~?y5ATM#(Y7e2thF9FVg~_f?u_*b zjn4U~=9wdv-)!*-*<>q?$eSz=lJxh-AANiycRsjt{aVsDB)pj~{heH8NxThz zMfdb~PhIN|usKkIfOkRgr^i|r8(**^>j(Bs8t_;r8f=R$A7OFjkoRVd)naSZ#BxL( z$q4sZ?Qd(m=K^9ce{@?oBP@BTz_WK9m0~DVD4Ila6?4mq&RKeIYLTPe$ChcL^w3tV zX%dbJusK)AkV-!_sFYySVm0Rv zd4SD^uxZJEPySXO;blHw`RwJjElrZ??(R!b*YCi(Q!$Tad6cW2FN)jB_K zkqnalPar9csV;%6B{MUBFa~T6XNc=u}UnEkrFTR@6+?yEo04-e$3wo*c=EpuxjLbnik+hLaLTg6=Bs#f$~!9 zBG2@SI*!p#89F+>abyj7Bn`=~;3?B1IkV$&; z>fse&GaGDbN|0`jvsyS(JDD6V+5)0&)6eMiWw$HHkj86Gt388NvsGKABerjvF$sj$ zXMzmeXp_N)D^bgWNKdm8aB^4MN>I`uk-x^z0fC5b>lvE1g(BCi8CWdaSRfoQvs*gR zHD!^Yx^``68IXwCOb8p#4U-mx)x2IwVOh@+KL=E6&@<67EuIl$9!mS1F_R1+ThH3@ zf~uqgrA`BY%|T-}EsNx=snfirOm8xQRq=bXfYg}NPJ_EnUAwi|x!z*kTf8QK%_6Wl zLuG!?Vx!7uYaq1ucsy2QUVLdyab8(L?imM~@u4iDC^zCB(e`02^aN}sf=#aEtV+-N zv_5Ol)4RYa7Gpg>UdZ$+9IM+axzF;xTBKM!$^mY+wtJo4GrNm9D&s67^RpFNfX!sE zX>C((ecviR>D(urgNMV{<2K8yu}42el3R`|J9-TOiE%BLphNFjF;+9;p1sKYbUqLP zHgKaY*-3z>Lf7W`R%s0Gk8Bru2lq zbwkRdL5O#&)jHSa>Ci78KOgQ!gUkz}AAk7Ase=8Z<9>k6fnZa*(N^jRo!18O0?Za` zBTo#lK9RkB_T~`TOh^uZM0MgAxt0N&McimBr8Vd7upAOz;CRkWsrjYVqTJf5ffWUr zRflkZ0XB%)EU_qhQ$RU={}ZqQY}P8+>~R4k&`?Te z%}S6e{iWAhjt3UXJb*!V8e%qp4dg;7)2zn@K<+uM$D9O&n!63)K&u)EU<24J>QXC# zLUU2^;?M+$np=#lnCO-@5XkCoSHK3aIlK!^i=XhKGtI4)Ue5x@_7YekxAAFrRjsuuILxW(W~gRGEI7PU{nW)aw+sMEh_ zw1H$lzxm1hWM@ejfIAIYU2w|*Hp|x89Mx)x=>A*+FW(>Y+&KU14mQfEk5Pa z{z{gJc|MQ=Ex6RqtyFO?WXJ$EjljA0#l36MPAH^JzWMr_=4~JR@xjHv`Z077smi3E)V7*g_*vVZvX!)wv@-wGiKHV_B| zu<75ye|I6d)Bv7sfyANl)gQj9S`+y;Es&GMW5sZb3j#SCWX=Sd5ZL6Gd0)gx({{fkxtN~nkmHq$#002ovPDHLkV1hk&t#kkY literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/room-widgets/exchange-credit/exchange-credit-image.png b/apps/frontend/src/assets/images/room-widgets/exchange-credit/exchange-credit-image.png new file mode 100644 index 0000000000000000000000000000000000000000..eef5da6cb5e1bcc91db1e615b5600064da805d73 GIT binary patch literal 9507 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 B4|V_m literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/room-widgets/furni-context-menu/monsterplant-preview.png b/apps/frontend/src/assets/images/room-widgets/furni-context-menu/monsterplant-preview.png new file mode 100644 index 0000000000000000000000000000000000000000..8d3d771ef46781f618464db09215eff640b7aa74 GIT binary patch literal 2048 zcmbW2`9Bj39LJX{rqSVyJn@v9BoT6@FgbD#Ifh}vMrh8+7$L*s%n?bvupvid_Atwa z4G{^`9GiF?QPP1td;W#z^Ll+g@7L#-&+Gf!_wgM&OAzoZ5C8yxz*aXMxpd>&S&)~T zmmdQXxi}W-Xn6yGAC+C>7QP@eTQdNl6)Sw;%g^oYcWj(*ap?z#XXWMQWyDq$roMa< z(%wyY-d2jvfOvT8IRt~T3AXtKLH8h+y4&l5y=@!uk3$hwmvwaBknn?qrz7viQ(`lbUS3_Byx3Qo&>#0u6g`CC#*h)Sp>celS&k$m&_c*>Ckl zfK`AhQUcD}pk!x>qV$;B0yGVwF_~+M3({@P3O-t=9c@oemxa-KaDP?SG8hb4388cO zhrsw&PDlS=0Aj{^9d}14+{!%?01%=4=VLTvJq!R4cmlp@=ImzKF%62J za68Gr5X=*N-ZUE5h|daBnZ;mL-)lzyJUR*)D>Vcoj1i{$8)m@Nl@TqJRfFuXl}6{z zYbPFsAP2DlG&x*KbopAGN*K|wNV3lv)`JwpuUim7IrUckJCO5E=f#A1?u9 zL5iE^*gOX6^q;YDsZ?UrUjp>h0R2CP;N^S6JbEufnh*H``j&UxUKy8$_krm7r{%Ew zu(b%Z_Cj9vcn;VNK}IMJLq(QFq)8k&vOqJMRU%tJy z^@^3*ro6}}mDyK80_W4&jtNp*YJD4Db*?d*K1kRrv`M=q5cTBKjB zs1m2CQPW+6iWp+;jE^V6!7AU2vUc=6TdtjJQK`^e^kaIO&!Z?dY;E$CY9o~FRIWJq z4}H&l{8n$foa~icmLjtBg4~JL?g-3B;)1!ENQBElX2tbnbe2j!RO9%mfX=1RjI{EL z4yP@XI^AU_D%}qwMWdB>r+un@9S|o7SY?$;vGC zO-=dc6fr@Z|Nfbk2=ANqK(|Fb?~JTr=}DDiu$*NvL@g-tiM46kC|j%;5#T#p`p*Mi>Ri;{gH`enzT z?&8LIROQ#5v5?REs=nlV3F~w6{-XI@v=+=p+Se8{VI(CDeQ%CHkp;}D(^UoI0sU9d z$D2G-5J6;q#?$nqtaUz4gWox$(Z`Z=;CEi(LQa314!wx|?qL?}jZHre#6t%q^COho z**vZdI-#?cCl`g(sCpOt)%=b%VyLBA1N}qyx zz}tt{ReGgiiK$#FF523-2h5zhz~)co_PAu18rl7FK$07yXs@iV(Cv9 zR%TDA%aEu-n!FpHc~iiRK#F7QvVu=@~e z8z`n|8HWZ&_~7{G`MYP>7hYdaX6`E^zE`x=_?{^mu|^Fz;`HJKowM<)@=qaoX@X=ik96b8*ISBu$L@Xy&^-ITY zgUqCzRGz)^Oq?)q>Xp3AL&30N-n-<4{a+(W^ElB;r}wJ*bXU^e+1pC55}_k^V!zsw zzV#apmO!!rab$(7G5z-PbDO9hZNkSVG=HrC8v*5jh-BG;+3ht4;A__}%h*409~@TJ zn2GxhhIRZ{W6#2aAIoAfL3NQ8n0$-gE+J$A!a;j-;CZk3ae13=g2>Osm-gOdJS+f9 z@zD04mN#MUvcY3i?MXy41~Y--F)+>Fru$XqMmFJk<$t9mDZSz90zWiyN90(L?(U+ zoN>PCz!%y`3;3fOx+_s%pvCrE&`~cSPZ$|ZS-)=+ogQHdZ2Tg!AW5HM+ZdBfi6(Vu z>_hJ2-o8YVNnBajf_g2TQ}20{ogE_2R_tLRtLH+pWKtCDrhIm+_n^$#?cXbbpD_eE ztA|W#P&3<_vp;5AVhfm;N7bp$psRJ`8T!=2s4n%cHC^B0cbrep+4&kwyC1Nh(wn>U zDQR<{#sr>C1M<}U%S8x)@!(iel3XF~RP84}q`=}C7^A>P$^Y~D>I(ZjkeYv)SIYkH zhS0L%RFqMxjOO0N?XgOwA@c6#4}~sNZ}H$E(d>5;v{G|(rkNA|LzI~lwlva4VRF9} N0DQ~tCjJIAWP@ literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/room-widgets/mannequin-widget/mannequin-spritesheet.png b/apps/frontend/src/assets/images/room-widgets/mannequin-widget/mannequin-spritesheet.png new file mode 100644 index 0000000000000000000000000000000000000000..45e11f346dcf87cc6d35244e1940d23995ba07c0 GIT binary patch literal 5719 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 JPDHLkV1o8Ili&aV literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/room-widgets/playlist-editor/disk_2.png b/apps/frontend/src/assets/images/room-widgets/playlist-editor/disk_2.png new file mode 100644 index 0000000000000000000000000000000000000000..3033020977be1a2dfe98bdb42d51f4538dc3bd87 GIT binary patch literal 680 zcmV;Z0$2TsP){W9?Yu8`}x6c~}}<$jc|=>qq?c z^;vNZ>a~Q5Z?{{d)9K{3-|xNpf~>*VVgWasO;8tr5s;S0|(QCKcdC|Ep z0Bb;iph}JbXuV#$#bV*5a02)~lm*gqzu&z8f=H*f+pSwJmyuh511MdT9Q+I_1=Qpi z;AXR#2O|vOc+D$tS*R4y<#KV)=hMf-;oz#O@=~_|M+YIe$_@gxfs3G!&*es(1|7D3pzKEe96z zfrB5RAe$eE77G8Fhl(eYiMw8}VRFWA93vdARhKMW zBN*p1L2?uDt}t`3>U`dl&%~tT5O4bDe`}91WOl(m|RsmHeqr z&};IidS$9d_BmXHoY1ur$1(-d!LpA5Haqew5X3iCxVBRgXAX6YQ&Q`>QYZ1$8}s4+ zHHnuT^Kf?A!_|dN-aeN-H87SjEEGK)mpJ+nl7r*_u9wyknA&V}Vu+8C8r)bu-x}d^ zfOM?O58f=?7`6x^xUp@j1h^C+A&1(A$^wpI<1PtkA&1=REJ9}VcKHE36C2i>EEie; O00005Jg!SYhW>Kf>jU^K}G~b=tU5b5g?I}AQ2!Dk@8-Rba~g8s_Jgf$HwlGO5@Mi z?m2y{`a3>2=+>=UxBqr{c=*-gn=bF-rU2gF-dulrdRp=O`}@K70$>IxvIro+U0+`h zzEki#NNOJ+AA>(XKYt(1A0HoA{O;~<@CY2k+w^ren}YHH!CMsua93AXgDbRO1+E5^ zXYcRtgBuVW0S10^bK~YTgLZy?{sY=pgNr~#N&+Bwt-=hN`NhS>;2f3)4ZhXjGDSs@ z)IL8yzg}KmzK`Y)4-YGTdwcssT?Xy+^mK3rWYBPqHV|6@D+@gjl7V3_DGZ^p6^H5;@DSO%*# zETg0!C43G?e%+ukB!*_!0CmjNkpMo! zkUVI7O_M+aFs=y>MawlAtO%TEQ7Z`u9xQGR8B_!bk71~IKpO>2VR4NstPEbMJ5ig% zG=FK%(hyWgoQF|Xu6(4MwV`>y5c^z99uzz;bt&|KZCzL|b%WDAg=ikL=&fpl7 zmPbKFkGVVwZ4zMUC40F+)usjWU@0ifXPum!d>`TVT)|~Iipx-Hj79_VU@@?$cB3$e zU;zR^F%)$LXGexss-#Hy3T!QCc9JDlPhH$}YyRM5+X^dF8NcsA%R+7ul>r#fR%XDs zIoEu`g|=e^i&b`XbhP5Ct_)h1H)~Zk2{6oFa#QZX^M)c^+sIiSEC70aeO++|=Rqr} zY*t_%ECuF`-58iREU_cQvI|yN1}8Fr%b-QwnOXB#NnH#}AA2(d9I618!Lp-nHsSXa zS_`mQR0d#J+p}XWFr8vy)kW&^V0mRyXmhJ<6fmsy=^RuBjGr?whGmcK;xkwtv@t5% z7%=Z~1k8>K46p#9rZuv2JeB=gVEh`McVLacaO}Xk!o~w@$zgk!^w(`A_o7=-+Yi%D zY}6Oq$&PF7&`5_#(E{qu4vivc>x?D6$;P<`-`Py>*3VYx96M$#_G)^kG#{4DiRskK zk`st6d2KYX(iC+xU7gLZqkua#MGa`tB1p{&^Ik22)BuZ?;j{4Y3Om;_8~@JY%WS2! z$!HngOTbZ$Gq^bya%PF|T z2pZQ!3p9PLMr|H2hBkJ~rYC|)U;!A%f8P{5BeWWD{Q6F8JNJO~8ygmYR_b2t)U7k? zKwAeKo2RfWEVu6FtXgRO_PKWi@%als{z;ltjIA1r|=lyUjH4I8t;`*<+LiyOXNEP$}`<; vqF_pWFa7+cuV-;n043jadAoJ%wpI2QHV9BE9t?&*00000NkvXXu0mjf-JODz literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/room-widgets/playlist-editor/move.png b/apps/frontend/src/assets/images/room-widgets/playlist-editor/move.png new file mode 100644 index 0000000000000000000000000000000000000000..9d1635d83ea95063669963bb6724495bb57eaea5 GIT binary patch literal 164 zcmeAS@N?(olHy`uVBq!ia0vp^Vn8g!!3HGbRrPd%RJNy!V@SoEtyd26HYflT;xDC#cp>*f#7Xk*QIai{9S36%_P;fqX@FY<5)Y(CfjkKwAxtyhLmpLqhU OW$<+Mb6Mw<&;$U|mON+x literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/room-widgets/playlist-editor/pause-btn.png b/apps/frontend/src/assets/images/room-widgets/playlist-editor/pause-btn.png new file mode 100644 index 0000000000000000000000000000000000000000..900f99b4d7014aa4554a7e03a2ab7a580c550352 GIT binary patch literal 111 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`W}YsNAr*6y6A}`B^fNDJp7ZM8 zeO?}ygFAQ*EX}*p;Fx$IO;}i@=QKmiD>q&V5w-^hB-j!TGh}WIWH?#Wdm3m0gQu&X J%Q~loCIF@%BK`mX literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/room-widgets/playlist-editor/pause.png b/apps/frontend/src/assets/images/room-widgets/playlist-editor/pause.png new file mode 100644 index 0000000000000000000000000000000000000000..ec5fef47dce0ad5cb6ebb5e32da844a8bb0bd313 GIT binary patch literal 114 zcmeAS@N?(olHy`uVBq!ia0vp^LO?9S!3HE7rssMADN9cm$B>FS$rVxY|K-Kg9GEMv zHgV)IOn0=n+9zd_C1A?4tIhe7lvwkJRkEfTR}``&Tey>rImFo*LM>kH_*_t<1T=)f M)78&qol`;+04t#%-v9sr literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/room-widgets/playlist-editor/playing.png b/apps/frontend/src/assets/images/room-widgets/playlist-editor/playing.png new file mode 100644 index 0000000000000000000000000000000000000000..0e3449d161276ae0e82e7a956d76098c099fbde7 GIT binary patch literal 309 zcmV-50m}Y~P)*{D6Ir^!I3%(k26bHQU=n1@vIP%QLU`~n zCFGafei%%GNc-V1>wXxl_5KMP2L?a+l`r0%aj`RuuAg*FN)P59uw*u%*G6$(EtfM}eu9k^c!(T>!@h1zp|N vcE-<~CMAsP@|P^+bUDPX@Q^jbBA-F-zh0bQ>+IP;TNpfD{an^LB{Ts5<%TfM literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-blue.png b/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-blue.png new file mode 100644 index 0000000000000000000000000000000000000000..9a14182e515e449d5e6fe27417fc085859e50395 GIT binary patch literal 401 zcmeAS@N?(olHy`uVBq!ia0vp^JArr;2Q!e=+%j)1kYX$ja(7}_cTVOdki(Mh=_Wunup>$LgbmML}{cq!Q_Wsr1*Wb>XWB+*a=lMS#{V}(* zs6YDsS^dYIKkI99|Mm5s?f-G@Pfz~Y^B~IpnRDFp<*$FSZ54k1?%*y93t$K^c)I$z JtaD0e0suDw%me@c literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-christmas.png b/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-christmas.png new file mode 100644 index 0000000000000000000000000000000000000000..82b4732fa8c57e023ac61336c798ba32bf622b7b GIT binary patch literal 1272 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�v~n=?I0_(WlwAn@duV^Jm-?w~x+k=33l#}t#C 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 literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-green.png b/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-green.png new file mode 100644 index 0000000000000000000000000000000000000000..5e73c74733bd4c64de6da0e677e7c54f4947a3b7 GIT binary patch literal 401 zcmeAS@N?(olHy`uVBq!ia0vp^JArr;2Q!e=+%j)1kYX$ja(7}_cTVOdki(Mh=lBSEK+1zj?YihE&A8J=f^#6v*Ie81Vm}Hh0%2HH(wJw>Yb$%kHP# zGk!nw`u+bxv41@I^ZXx={+QcY z)F1u+tp4N9pY=7l|N8pR_W!u{rzijHc@Sm)%sKA)^4GuEwhF(0cW{@51uz5{JYD@< J);T3K0RYHh%dY?c literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-heart.png b/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-heart.png new file mode 100644 index 0000000000000000000000000000000000000000..455238513ae84ee27c35e49718b3549aefe70781 GIT binary patch literal 945 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 V;qoj=JZAW>i2VYERjVUY{|16+xRn3^ literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/toolbar/icons/catalog.png b/apps/frontend/src/assets/images/toolbar/icons/catalog.png new file mode 100644 index 0000000000000000000000000000000000000000..f68092171113671bf64cb13c059b92f59809f644 GIT binary patch literal 1202 zcmV;j1Wo&iP)2}$f`ONoy-^^?s=V*T*<5>JL%*XrZIsP{o1ALM& z%QsGA48y1Vp?Zgc8Z!C>vpmxU#%fcn`cSO+mWoh!1du-jyRHL40`CPw(0t^8pXd$B zfEmHG4ELIUerDP&La7!qT&WrgC~ z1rI!#N(L$jh;wT@LN%%0**7~K$AHiGM|Cyz149EBG1Bh5ZpTpQuTv*aT2Q14f%p>+ z2c?m|cZ4F(P(=|GsnrEaD--pO8Pl!@meK2uh9NIC3Ia5k6wyhDR(ZEKH3%cCH1VL^ zF?offAO?#95)d>d*wh51o>g#U{$7>{S>B=U*sBEKp@GdbX%(o$L?G%lm1z+`JNY_=9f3sU+jdTpx2%SbKk5v&@rmsI1iv5O36ZXaCc zjl@B?Wcu(mwmCUQ`pyDXyltsDVB%OhVzFy`bHIv zVQ6??&cf8)9AsXuKxB;GpwhIj84#j!Jf5`|RnCtX7~7?d3f|aSfX(fv0_t8KvM&cy zN_eX#2B(=N?_{m5rC7D$=$e41jWj8kt*_7cx`I$iRBC2^p>Iz`)DpAFA{foGvYIoL zD>Aqzw;}pY_X%VjgFZLpCk3^o^v%9r9VmUqnJLibyp1!YPAhA9s2XtO8>N~Fu@wPI z-p#_y<0mk?QXV*c43#pxHT$}Zu?AC^N*Y9knF@kh|B&N!eW}Uka1t~bQd?D?uz)}K z5t;TuQsd#PGTcJ7$P?Fa5Qf76%~U8*Qu4j@5T#vJoIZ8fMM;5|;0dq5d;g3ggrSF$%|3ohk~ z10tsONwE*@w;VKjZP#leYnk zT}`#?5PFS`Z=&rUg2krR!n=TDzz(r(2p6s-V^b=wpB5R6;yC?8*>Ph30PFMYiDeL! Q7ytkO07*qoM6N<$g2VwgQ2+n{ literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/toolbar/icons/friend_all.png b/apps/frontend/src/assets/images/toolbar/icons/friend_all.png new file mode 100644 index 0000000000000000000000000000000000000000..b2ca0d7d52b32aa4b6923f119618a8876862790a GIT binary patch literal 648 zcmV;30(bq1P)>-IhnZOAUGHo7Y7z0Vq%<~92^J(Mkn3W#rOhZVt55t zU!!^oUpe>mw3izYZ}O!rJ?;1FX?riZ+}}TjY37eP+FCTvC+oKS4+wKrDOz(XX9?ia zzPUW-lm);uoJ@wt3(^7*nh;v&Vn-vnF|OzA(_H} zuavJGfbO@l58(!q0fMCfp`{&TkO|aPh&}&6dK1EoL!qy??F)Sp0&U8ci_tQ0bFh|> zoRI3brGYd6^kEDK!4)*0Z7c0So5oJTgWw=A&>&#iNu`P(51!0!ddEMWFO8GE201z;-d&*3C+m3m;Q#0Vq-yRZ~u$7D2wSx#{KtfR4 zGY7yB!dp3+hX6Q)ROfU)dKw3BlX2}q(iinqT@(<}fa z`W+Ke3Binu_ai7Qz7N1jtkXhHBRG=A*r08r8i1r__D{tAij4=5M`80BBB9 iZ)i?s^lLan+2;pJ`lO2}U};GJ0000>NW%u_|*qM`#)a%v>4T-9 z9*7qC^8zEWXh7D>Gyz$hv=}bFX2_Cx1u|C6LQ9<& zCC0K)@G4`B3GBsim91k#TC1VvWQJ>R{cycKhBi!}`uPk#8qPBtWQxfxNgB^1m;k4E zCZk*KI59_toY}cB+_Pdu2ykLOpG%+ui8(V5Z@(;E1pB$yQ*`xsjUb7=xu^Qwg4_Cf z^-kbj1}{M$s_w!(?RMDbuH?kA3j97Z51KVoKy9;PICQxk3MZ$=5%khc!JZ3yl)V_Q ztT(O8vBsB~t-9{A3aCUjpD;H&8?r}f@rTM_78s1D$^fpHbvQT~n+#lJW{F^k0b7aS zFh}+%d+O29)0$!;wC15~y+v?4Y*>%Rb6W<@)YLkl;haAzQ_sUR!&X7#DCVZmg|SV- zWo{FY7HeThv>@NeyepYs)kt0Gb1wm`e|Br$ELe@LLUy{jhQq^dHf&hnZpGNzBvxzA z+Ap)$Ox#T*^lq|$dbPsWo@vp?>;t{19VD%hJ4?Rt^$8i}QuJjhKz>`6O)r(v1GbLX zhK0*R>GVyk6?;=lJOB9>dKmWq0k#Ps2u?jK@!Q|sqkMY^@1F**tn~|YTxGIbpA&-s O0000W6W0H-@A^~*- z3FB3*o?@xAv|`mcY|BX?ou>H+k`zYf>uJ6A6(vrYg_wa_O$Z%kD^MedD#c;N@HGU@TTPUbmSq0G$(pVl~p?X`<2KStlaz(WcFh1SSUg# zGZ}EN$69VPxrS!IQNl!+2@7opKJlbJYZ*mT_A+XUlHTqq3xhe)YQ4259k1CuX=yv1 zu?`^cAf41{&#$x+XdYpw%56B2u@2ly)MR&0lg6>h8JCIanR4uegPg*~^tf%VrQn_+ z6}VSrx{bhS%9c-2CbfH&=|&Oq($jg13rLHBs<6kXP+#@fRF5ShG#E{&!BSKRL^6R` zo-YxDb*Pj{<3wOhil8KJrB4nmWgv}}6LLtD(nO_H2Iqj3zB$0wF<>NeaCdvkeMW_>0=w!{1%eBaqSr$8uS#TmtqXrE^ zk~Dz(B&5b990R-ChzSTxDiUBukz9}@Ps9|2RF)_wa2G_a7Pe1Ft)cL8W}|pg8-8YL z$UY(U3L6OyIm`rUn9={-5|gBvP%4wOgQZysEm^Q|UB}o;V(s|W(GC;oI*zQE%}y|z zEtx;M+q~zO_jqDFu1%}NaONOW)5coPLGZJe<#qLAF@wfqoRzkJZnR_ zQ@K=AC$UpTx&pHisij~|uxxX8!toB@%qo;Q^G)RVIdHd*pPJFdI&msh%u%KU%K!`s zbKw`O&blD&K27Y@em=od~5r{jviG+1K}7b_xIz~O>vu*MuN zRz$FX!v)h|jX7Mbh+qMS3#P#ubGTR$!2%8!OoKJ%aIqqS1spDz25ZdWVnqZCI9xCd z)|kV^iU<~PxL_KrF^7v45iH=XafWrmTV2wFktcYL% zhYO~`8gsZ<5y1iu7fgdS=5Vnhf(0Bdmc<$YJu zuX`@y(a6>M%bU_h>N~b9LQ`J)jj6jc3U_W8{PF8M(auE+=Ks4aI51L&Zm4N%s#|u; zzsnX7R#$)EbWF?QrGD{eF9uwEqxIulQh)A3+tI@{vZVoO+fsW%oprCx?c4JAN6lDc z%(0`fhJPFs=SKy%`5){_oMSprQ~KK_5iNf%UGTz}o$uy`7*7SKwjQ4qv?W~rujOgK zYuiLgboz7R z%#EVD;fKzl+L$8$AI}HAcCwk{N><6Nco2I?KcHgyq zx+enzXhXcf`T4rLmlGE?Pmhgz(Ar^K{JgE&_F8n&cUiOf-S_;Ox?gs_v&S#eKc4by zke_}kWWPURIX~4?rd##0st8+RB`+zCD{&wv4OHmKw zcWiHJw;U5a_-wZ(-1hXztlD#pckHu=&iAf56h?mYYRsD--G6$!>wf+B1ubv>s^_i2 z*o&e)s@$-UORu2y-U)E1c(cQdbU|A%@Z8QyZ`cKWK_ z**Bt{orV2#vuB~%XNTr2eK+Gy`n)AMA0%gOmv7zBzVYkIpg+uO_tzI!H|*cHsp-8{ zmuGLc_V;&n4^X!L)Q49h{#LN$_a{G2?Z1`Lc``9=z>I<<1T0&Y1#9Np9`j3mxBgKDItdL52hW Wv$rRrd>iw!Y*t2&=8tJ5oBs#*X1^@s6N;AwK00083NklIskTjx#|H^tZVV3XBo87lBd3o@$?{?>Aem8G+_w6~CzI;Nby&Cn+U%cIW=2u^D zl|=m~4B%_}&T~p%eDm!y>(jqqLDDCZAQjmP5_|2(@(+IVU^n*P`F6X_YK^zTJ&n_t z4q8_m`+MdO#vfD#?QbnT4Tc2Q?RLGpFy$}reCL)v3$%a>JU$6a0!j8*0u*=Urt?2G zhq={Hf|z2^JczKJPKQB7^f?AC>J5%^Kec*H!wT@h%OU^!<9&ukU{Va2i(MuH0cl8t zPXX^PIfmVRj`#ansv=nX{A6eWqjuoKT|aX4ilJk` zA?*m9Q^UAgSa2w`3mKj(XLw*?(hv8cb%qf-lmph-#mOt?Dnb2XOA`WRfy+!s=^Cb* zKRkbpTWj6cSuOx$tr46S!s<0sLkp~Bhm((B|12)|+fE5oSSo?7B}l_sJDETuY|Ku^ zdV?*7l?+?S%L2B9fo{yt`-kgmEV`Fq_%u>>)(?s$t~Rh_j9Lw$VI;#jc32)UhBF5~ z-h%hCfvZ-T6PJX3%$9J{m?3hP`G2^eay~U zE^7npos0UXXXk=r4Wnnc+ViXIf?tZEUs;7C3XBUp!ip>y{i+x{6eR2lF?8arozBJ1 zSP9tJK{EHiSsK^Sv2D5Z0wl_SZTc$2)I7c-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxR5#hc&_u!9QqR!T z(8R(}N5ROz&{*HVSl`fC*U-qyz|zXlQ~?TIxIyg#@@$ndN=gc>^!3Zj z%k|2Q_413-^$jg8E%gnI^o@*kfhu&1EAvVcD|GXUm0>2hq!uR^WfqiV=I1GZOiWD5 zFD$Tv3bSNU;+l1ennz|zM-B0$V)JVzP|XC=H|jx7ncO3BHWAB;NpiyW)Z+ZoqGVvir744~DzI`cN=+=uFAB-e&w+(vKt_H^esM;Afr7KMf<|~|UP^v> zu_jo#udkJ7UU5lcUUI6Zi>(sS0KLr26e|-8Qx|79CnIx9b2CFjS4#_X7e_}|6H6CU zXGUI2$*<4On9mVa^UGcH4m8Bi-4(mjoHi=1_s7nPZ!6K zid%2OqKghI@T6$6pH!?;d+zi0<*om53vc@L7)}zHamJ!P@tBPrcXs zO2cyIAHOHz2fw9Qs06WnvskdYa&tTXB*)f+&)0rs%H*rL?5AhU1IeRaO3w1BAK=+8V|?13-*UQ5r5dA%fBXQ%eH&-zl`lYd3FC~#GncL*%& z+h&_@tY><$ykv`ANYrS5WmA^0$+hoW zjs>aLiQ29+*(0T7@p6+W^Ld8=vt7~J;SGD$!ljmPS29`p=HorlExxa{kHt1HGFZy) VDRlbp)(tAFJYD@<);T3K0RULZ(Cq*K literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/toolbar/icons/house.png b/apps/frontend/src/assets/images/toolbar/icons/house.png new file mode 100644 index 0000000000000000000000000000000000000000..f2c8746ab2d045bcae2b99e07df7e534ca299ae7 GIT binary patch literal 399 zcmV;A0dW3_P)PFkQj_bQ7!pmCio@=cniW4^UL4yCcAsB0B)d5#(ah900N$6bi^8u&6r`>A}5bOYu#UKZO_#qC$a11gOgo)+DFe#3JrvY+(j?9OJ0x2N@4|Z~V zj4K342??tB9GQBjLjD?IcylO`WGAiS4ApfID)VP_FRH!1kyw~0GE28 z13;lbj3ZDC1ZjX_VjO@|J=6i%9D&yX=sqG8$Am%vnE!i)U49N6gZNZ+0CH(YO-!r< t=;jE5O*2|Mf~W>6EgeC0Go8j53;@05@P!IqvTgtX002ovPDHLkV1g{Rnh5{^ literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/toolbar/icons/inventory.png b/apps/frontend/src/assets/images/toolbar/icons/inventory.png new file mode 100644 index 0000000000000000000000000000000000000000..d848586ae31f18b3a33f6badb4de59a0b0e73c09 GIT binary patch literal 1973 zcmV;m2TJ&fP)Mdq%#-IFu1lap5QG57Sn*ZzwJHlZi4hK*$CiODtOAb`upUFG)MuHt=o&9DWghSGeEbQBRF0^6`fWcx+& ziwYigl@=ZV>@Dlj5cGuyI%c+MyWIc`Ai(U-D|ni_2b9(0H>(p2jf9Yd53G^%EW*cD zarL;Tv{(V&xtYs)ul@zW-4@6U!Dh$IYubP90mBLa08ITcz4o;UQyNUk>PLxSAFw+# z9t?$v_`nR-7ll_(D>U9Z91sOYaoNIJj<=bmc!FTZICe5c28GJLC4%cIQFm=pVtqms zrUI^|sk1 zwH4H?KMVJLaE6Y#t^in7nytcZE&M3jyuW6)9;`F?`=h|gD%fY>h{6t7vR{V6u~sfR z23>z10Su!VJQ$SrRU?H<T+!{WPcpbFuE5p%R$YSKdbnY#w8hxH;X5Xv z&~Faz5|KVkhK9nrx&d}OTVz-$%|%GTpc?=kC%afsDnh>A0jIdF?b-zepn%6Q!dDou zRZ@4$4+1X%c;t@rbLvwb_7#R=LFa%eb@q!z2p-@710-N^eu{#DGX>AX;Yz11O_yZu z4)nSYD4;BW6`Hp+Cct+?U~X#-3kUaMw{VDJ0l}iJ1)YPu4`_e_GD!EEH7`)?T>HUV%jeAg{7O+`2VsLa6H*5p+WTBtGu z!y0S}Dl(i0?9N1)&zGS1rGXH5EEWMqF+sTl3wdDsu7)PU`+vMOrJ*}cVYhG`Rdo6- zvE+G{)nlms&?@Nx9)qe{ct|8vIL6<}K5(@x1%-IWaH_UIA_;T`fZnt5S>T9=ttFP@ z(vJZORhQ4UaVukm2^5_K(Cp#tctqhyza>b(0J>@m4aFtO?n{x81r!$7`3g5#!WCUM z6BeDPn6$Md+a$O&C~eIGvR1)126u6yztiT$j!VPB$1;#xJt&4;v5(0;o=)REPzc}z$QDkZE zI^P<3h?~TQdV9x$Ve*`J&cTl7=^?G0{1ol`if8Qg3S6~|1kcy-ESc{H^K@Nku*A~i zIkUJh!`bvu1O)--39g2j!>gh4SDgAKn`n={|96^eu`Kpo`M6C3vibw0WtU{KJ_ z2T(fh;`tIT(Xajm2shzjT~=}SBs|KA$gB4i{nC6sIObIFuJZ}@{o*kcJr?-V0>G_8 z7eA}$@#d?DC$(lYkrV$F6B7gc1SYl~pFMHVZ~gyoUp?PH3m)g34)!Ld00000NkvXX Hu0mjfpuwsH literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/toolbar/icons/joinroom.png b/apps/frontend/src/assets/images/toolbar/icons/joinroom.png new file mode 100644 index 0000000000000000000000000000000000000000..894ee78ff75fafb47bff9aa359b4d2e170897c06 GIT binary patch literal 1084 zcmaJ=K}Zx)7#{Ibq8-8nt#BFU2@iCcg2McInF%YNoU?P^VS_b z6i6W@b_qlx1VM-{cI;4LL;~1+8)fj_rLkR@BjYy{<#;6 zc9fM=l@J6`7U>k?cs}DiCyMcVcDZLBPn9T~LJ2s4vXTjiHU;(rGNMU?APyvD^yV^X zCWzt-YBGgVVizw%%`G_?x2+kNO%Tm3wjs$w0FnJ*P}Tj^j~9y+sVaV|mlZvt5dvwo zbIb&Zv1n2r87gKuYcWhehW8`vpnir931sM&~dYyxT_}~pLc$*v$3vZ z`18x6@sHxCokuETvQJun@NBWp cwYOE#N8CME5?FcuVH+DDBH^g85bPWO10OhQfdBvi literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/toolbar/icons/me-menu/achievements.png b/apps/frontend/src/assets/images/toolbar/icons/me-menu/achievements.png new file mode 100644 index 0000000000000000000000000000000000000000..575464d6179161e17ea2322fbc271bb17b9e07bd GIT binary patch literal 2306 zcmcIleQXnD9KPusV=}CQW2n*NoP-6gcm23)d!cM&oveh8x>ek8#$NBqL-@Swd;4yUOMEl;Nca zGG~zUdQVDfgADHSdXJr$TbLt*bIq#X|BqSPfeMdcewGGB>vQ%E9Ajs|0v^xWNwKE?Iyqnq8UW1U2e2glmSZVX`C~d zt*FJOH_&F%M9)Kwq`^jzR>EMyNdrUD3}r!67Y41#yq~FbJe;xxzwB7Os!9w&gu`Kd z*rXTbI>KPHK@KBfG~y6}E76e3M(~jG;5dT=D4Z-vsvw3?jgj?dEcWlhjZO7FiCE1swXh)SS{jWP$ynm|Z>aNHQr zrT0pWa$wX`o+Cg21R+d;!VGCsQoX2(O1=0Wex#Sv-bhTT3|Li^J)#&GPn3I{1vNsi z(K#-b6G9qqp{6Od1vpq0*fAq%G{B7EM%qJK7^9gn(KtylBsm6k!Ajs+m7N$^{ zD&v0@p9(Ek&xYy%?5+f+xkG4uXe=?rKXW#^c0pw4bvSMex!Ri2tjHYcIZKm z%?*E@&pRC@p7Nt7Uhb|KAaxtI&znM`r;oCWTH5Opvve3csVw4cUxQa zrq8E7G4+k}`@fp`|wM!$;dy)FZU*FTViQCqaJl|6_oUyh4&a2X@ zHxs`cELi)=sV%WDKioHiIZ7@qjmdX%-8Iz3k?FOw z*Ys_5PLEGsQgZ!=Z`ik{Nf(d)HnQvN#mptYd1KYfh8nJQ&wKp)#ihv36TfdOvcJ+4 zUChpZZsXl&udI&O?EG|6-l5AYoV|{8PZSrGJ z#TLHj?7bSBet+H{?!xu^8?Jn8INcv=*fD*eYVXX-DN7AgUo2W(qJ#%6QtX@I+GK=D zHrLJ=QCg4h|Dthk@WY?DxG|%JYi{{@!&a$g75>OjjdWnc@aBqS`|<+ISI-P|_y>0F z%6$6TA^oRl?T^ml@jA;Y9J?2KTmAw!-a94$ literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/toolbar/icons/me-menu/clothing.png b/apps/frontend/src/assets/images/toolbar/icons/me-menu/clothing.png new file mode 100644 index 0000000000000000000000000000000000000000..bfacabd844f94288df500e1b6ab84ae5e7c5e7fa GIT binary patch literal 2255 zcmcIleQXnD9KP`}w(*e$6WCDUb|4wD>wUDn?M@0C>#)NWMk%r(hFf2mnbro_K%m`-Fxr* zywC6XeZ0S0Q844xp-H2X5Cj>TXSX@w`+0bePKbxk%PHVGg2bgtIXMNh9R;pjJO@G2 zP6}@K>3~+Mg5hrWkrNXqB&i|i^5s=dcXLZiYs(4rxch7Onwt6ZJCY3@Um^n{e?{tt z)K#ut+qsY_}X-?nBk3%I54#JA5_vinZuh1{%}$VDaV=QFLo@$tJ^LsP=?H`mOIYh2&T zHg9T7Gg-ITT934}WVKQ+O_?w!bzkGYahA^tTTKMpT!%KNHH@Fe~k z4Kr=a>rZ;!21amS=p2MZ!j7Ak5AIdNm?Z}6ML`4^{G5KrAscFw5oF*mlB-ZFbmVh_ z;y3c5;sM66KLF7PGHFUUzzYk3hI&A;BwMjN2bwWd60O)=i-U9oazKe>uTa74iWx4U zVxho_*p$iWq%a2w{6OQ;u-_*Kxv&-M=H=j6pC&N08=@_=V%fSuw9rw2<|ryaEk*_x zD03#7!5S&XOq!TWsWcDbuVNRNCW$KMz6iRyhKanKbB0Ky4^N|`Ev+;SjmV|$H> zLTqoKO!akpDhdSf0Y8KVp)e|DDo~HY)~286`>N7+|6^8P%(EUZ0}J0oGZ-ogoL5o(JggMS&ldwC zAQxlkziM(6pQ1Vx5ysr~_&Om~OS`%q6}i2vrSd+o8F)eDD>P#?xRF#L%1a3Di!6?SN2 zt`qk6xHs}_*)IFx%jaK_+p-Mt9o9r3S1)^cI+%6lRKdRdqlxLx@3vj-_XDzGRn9e( zZCgf-zVQ?94V{bF2izPP|Mvagzc@bJQ?+^Bv5B5Z;B3Xg3x^};Z~XayPPw-r0l&2C z`r4KU2mYRz-;};y361@5Zofk>dNL2a3mU5UJ%^@cZzngP7e`R`%~#{fgu;}Q?b{^J zrKY^$RfdXBKj~jMZQ08Dww<>g7{;5*x3?pEQ;h>o71obfn!F_xZ`qM>x8@ouCyrTI zGk8?xjl)Gr{Q8e~-yJvd;G%P%ud41k^WLzdRKIX7dNi@6u>oywpLgJ^Yx!-tuXWzR z?fD@pzq34$vUkaz9ixQGJM-HzPi@>s4Zb&ZEp_|HF^9#)k5(4l_;t{kbKmZ28ZvT6 z7ukjW)-mTk`qoxU&B1MZJ7EeWyUN_p8=@l^6r`zZv{t&-}D-K_6NqfxMu z%F<3kp^#o~MQqU&pt4n(BpBQYuOO!0o|$GSHE(nwR+<3GE2u+cx3{CXg$!5mF2Vh? z*cjRjuL6@T$Z6J`_SKe02~;~3xSgij+8l+Pv2MbI1ZfAA5~#L_*!Ij$0u%WvmxcYI z3o7(mI~J3!7Q^QAW~cuFxU0F_)@Iw=;&Hd}6#CuAK$G5URC0S$7*zHLIz6+rRRP8d z6(%xU6G#eqLfa@%e>w1Z{o_BoE`gfzKrHng)UBg1CKI1|+)Sntfikt#a=7`wtI|oE rK^!;0HU;+jlbR@748r~LY0&%vC+1Ek*ngK*00000NkvXXu0mjf&T2Fj literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/toolbar/icons/me-menu/forums.png b/apps/frontend/src/assets/images/toolbar/icons/me-menu/forums.png new file mode 100644 index 0000000000000000000000000000000000000000..e22426e6d67447fb698f844fb454b2ac6fb29024 GIT binary patch literal 416 zcmV;R0bl-!P)-t%kH38_1VPzyPb!m7_Q!m2xC|=D$sxqR5V|kNOpx<`06Z(mXX~98RRF>;WE#y^ z>~|cWD?N<6R3pF;ssVVN0%*_YgDK0>T93zE2pS1sE4;d`0n#*SEq0^rCJ(NsQgB_2 zQL&8x5CAqO?~k!Wa!SR2axC1NLnr|tFp46w*8xmlHzpZ?@BMrSGy<@4ps-#5NYjAh z`*VQLKUxR9J0Ku10P*zB9Ka<9w&R_SO$CZ`;()5;C~1;zjk3$xQ{$dllbqa0`|O>}~q0q~ouo=i?ynRiKI z{Fk#efUujIf}$6WXyU8@sFw~b0icP47u^Z~Ulc<&c@22|IQ;_c=nI{BT3d<$0000< KMNUMnLSTY(XQ?Rw literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/toolbar/icons/me-menu/helper-tool.png b/apps/frontend/src/assets/images/toolbar/icons/me-menu/helper-tool.png new file mode 100644 index 0000000000000000000000000000000000000000..e324611fca78d810deffe9c00082e8a0ebdcdf37 GIT binary patch literal 309 zcmV-50m}Y~P)9`S-s*y2< zV1BF#LHBhf=UfEvWAD>|2)p&-0PssfWQ*3!&yj-2g6=xcnY4nEp0|%)bjbw2bR3)p z5Pz-x3JXH;OC3W9E+`GS(z`?;hCGLma$M=0?f}SrUITb7z;ir+gOce3TQMMB2U1A6*hv<>piw}ZLeH!TRRat z7y*Qs4JR6%p)#XGBEq6PBy$}Q9sx2z88he{U`!AU4|xd^{N2`7Vu0))FS)zl{eGX{ z=ll6Se&2Rue(t>Fq%lbl1SRKW+Y7+^CH2;7z%vcswH<;ICWx7t`3oKS&MY(&f>PU9 zm+P!wSt0W;xLn6iPoI_~R}^g6u(`n15{L2Mt6_jZ757(+@-%`)5B+a5vQbaEXi5YBI_TLOBWf^(&HU&@FYz!k5sV zO>@@NS6#DZn3mJkZ)%R@uGxJ(+>w=$2mL5z{QiBbXNqRGRkv5W;k7MG5}Mv?wYEf> zQVq5Sd+YING^3T6oIdT<3HzJ&Ponk}wi+;NOC#Kpx@QtKpmF4-S$LQBZnAd8xiTSQ zg4U&1-?8e(?%M1w*15GcOP4q`n-Zt6hgMdPE_x>=WyVk($InkVQm}}`Jr~_BU7mfz zjyA!iGqJ9Yivyuh&~b%Yf4c_6Y>+>@C;&l&hpTr2^g(?J1P%B@bQUUwjs-L;dG!n@ zxp{ri>j!8EnwcK-Gi)iZz;3=+^x2T>2U`$WN}xZw52X?=lM zm_Wd=AmhgfJ+8Xc3v@W15A}NE=zx+{4y?rVK9xA&4EcGifDcGzGRtR`^FC!_uQ85| z@Aa3-o^DS$7UMm<7r+8Q7!fz+FOd`}P$E6UkN9%j8$X>X^Nb?NPD%3gCMv&|1tx*l zaH@l0MW4z$P1O|J!rK{zw;?1>5+GwJX>#IbnxtrgL~)A7@gArHR078+Oy9r~Ckmmy zKphS`#}`l-AIs<1Z3qyh7e$UX3ao)MSq!M#Od3%mK?x{B@i-V_UlAL5M7X;u zX@*q~gAHNTD&RRJHZ49SxP5b@hqj`G2c*xbjUHG)5|j!?=4Tg!MD(HhuxH5+Fy+q| z-?z&6U&Y5l%a$;{Vjgr?3{l;|)IQXc80^0}>t1`JXZ6BCBUFd+4h%lxJ)HLeY8iBB z^Z11)!B6M<9Q$l%_Oa%zw%kNQw{GI3C1a8-c74Xcd67>WB5mW4Bz<&PW5IWa#zp2_ z(u@t0+QXL6$v>}kq_MU3%K7a@4?hfDZYi2Cwm1BecQ)zH^>wShczfpps5j$|FZaMKcgIsf+F{kTC$IX|MTeKb4s#c*=<>!Sm@ zMQLld?fR`KJj#{)RkWEJx|SK%SZ`-C9G{3pCcnX z5|wi)(4AGjhlTv3s(s0|n?qA~R;|A|KeA_WTH=jxY4q-uU$qhXz|pPc^~c_Y@B=e$ zjKIqiUr8`dJ-@n>LdtRJ`@By5 PdjsWU<=S`6ajp3WsjVn1 literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/toolbar/icons/me-menu/profile.png b/apps/frontend/src/assets/images/toolbar/icons/me-menu/profile.png new file mode 100644 index 0000000000000000000000000000000000000000..04964bfe64038532a534b1e5d1b31ff2d4f9135c GIT binary patch literal 468 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 literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/toolbar/icons/message.png b/apps/frontend/src/assets/images/toolbar/icons/message.png new file mode 100644 index 0000000000000000000000000000000000000000..c12d5bb4a4b0975f5507b76e4f10c96b8aab2a1b GIT binary patch literal 1099 zcmaJ=K}Zx)7@pEX1VciKy!0BQwA!7ST~~JoH_@G4U31rEU2qjDXJ_8JgU-Bh=IO4U zERpC?DHIe`bnxP#pdu-xgf0<<5POM85W)l@>LMOGY;RY0?a($b^WOiz@B9Amz4y=c zaHylIvbK_3=k?%^st%nT9urG#tQ39tV6H;voibD|4q#+oAk}@&+ z9=1?a*$Fio!?9quAS2B#hTw}iBZ&g%+fo=Y<{$V2e3hu$$23x2ox>{UY!3i(-3$Dd2-WY*OH*v_fRLzZj!EEbj-!cA8|O` z9rdlRuOo!A*(^z9G8r;y*zDLK!9g4e^^%aRJYSw)CYjlQ*cMHHxt;lRXbMyu2>;s5 zX3cVEb<%dpc!*!FChzQj^x#bO9PI+G@`V}h^z`Jc*|ioO>@D-#>ZS3u=-R2TkKZ00 zJ@Wn9-FFR%d&>3JY-Ph!;*ARULgJJ5!K;4&d;@RT literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/toolbar/icons/message_unsee.gif b/apps/frontend/src/assets/images/toolbar/icons/message_unsee.gif new file mode 100644 index 0000000000000000000000000000000000000000..eddfe1cc2b7d244145d98d3e0429f758602fa39d GIT binary patch literal 1511 zcmZ?wbhEHblwwd|Sj5htm$xBo&tCZxN)4{^3rViZPPR-@vbR&Psj#ZZEyztRNmQuF&B-gas<2f`Ovz75wF0t1!um=I zU?nBlwn~m52?day&iO^D3Z{Cdy2%EHCJN@3dWNQkCKiS|3PuKo#`*@v`i923hDKHf zmR5$Q3Q(W~w5=#5%__*n4QdyVXRDM^Qc_^0uU}qXu2*iXmtT~wZ)j<0sc&GUZ)Btk zRH0j3nOBlnp_^B%3^TzcwK%ybv!En1KTiQIIg#cFVINM%8)eo$(0erZv1Dp0vH$f^P> z=c3falKi5O{QMkPCww={D!G<3DJG%|FxFn2UJbv8D2GPf{>>2=9ZF3nBND}m`v zLFhHZsTY(IatnYqyQCInmZhe+73JqDfPHM0iQ6s4IL(9VO~LIJ6P$YWfsWA!#Vb-g z!-Rl|2gHP@S|A6W?o;!CiM%OrteG>WPn$Yr@}!9q`ulo&y1P0% z+S^)NnwuIM>g#H2s;eq1%F9Yiii-*h^7C?Yva>QX($i8?l9Lh>;^SgtqN5@s!oxyC zf`bAB{QZ1=yuCa<+}&JVoShsU?CorAtgS39%*{+qjExKp^!0Rgw6!!f)YVi~l$8_} z2E%h009bK&rn)CWx>KY`bbWJEwSTbu`A>XQvu2%D> z#aQz1h^PwMy=OyX(X4~~`x_mm9(P#hAg_4o#O13(oFK2TqIqRA&@2B0rb(`zr5t>_ z;NQWt%KHK=ZHgkIoQpC~+2xfT`*G%qTF>!a%g{$uTZMxiKdbyyko3*0sxAZyD4@^}hd9@DpmBs0;Z4FH&Ufo9R zjm6Cw|gW!U_%O?XxI14-? ziy0WWg+Z8+Vb&Z8pdfpRr>`sfLrx|^289Xnil2Z&vY8S|xv6<2KrRD=b5Uwy zNotBhd1gt5g1e`0K#E=}J5cN9Otb&+B`H$r)!7-h*mM_6OWMD9e$&z@k*&GfYwrq%UHMdv9zj?o0G?D*( z)F^g=%!IF%#RvbpS;_NW&&!w9G(6&LlY4ndj?UC7F|n007m~hgIlpd#&O*0KR&pma q9orR))>`bkVfQ|&@C)lJtGBFE`qNVjcXm$&1&*hypUXO@geCydQkbm( literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/toolbar/icons/rooms.png b/apps/frontend/src/assets/images/toolbar/icons/rooms.png new file mode 100644 index 0000000000000000000000000000000000000000..00261ceed4d77b87ce0c6d84fa6311558aff4ab3 GIT binary patch literal 1465 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 XSX*->TJxg$2y>vh)xZ7m?p1#QUUeJ! literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/unique/catalog-info-amount-bg.png b/apps/frontend/src/assets/images/unique/catalog-info-amount-bg.png new file mode 100644 index 0000000000000000000000000000000000000000..4a56c9b265580af4a41d2db6a93c0749e5f447e3 GIT binary patch literal 757 zcmVHU6$*_|olkZ5@Yv}3=2k7Pe%oFutjyA1iMDh4Eu|d$HMaFJ z=2ly{Yq2PWwR(Deuiiet7P^0aQAw(wwh8Ml(_g8!bNVf%9Q!r4^)cpFTexe{2*TQc zx~w4qYrxt!tP4=5(tIsszs}VU3y9 zH>?)6P*BGLYq$ZvVYRSDf;t9R!)NskEA{=GL6zWvjY)$B>u^3x+gHHq7glbvVnIDP zITL5Pv_lD0IA|Qq7_1Qu@C~c9va+B`C#M}!^rsgH-2hky+k|xkR===n+&~so?qui; zYd}rg!^$WJJL0UqVb$KVu%J>WW6rP!)YNsrK*b#FNDBCd6|%#@ih{bfzCoUxUNl8N z1!}s?W1zw|BZK7|R*aqW`>#1{%TZ8g^D7co2%3~p!e(1I^;14=q+5N%N=Zg2KIB+>}-JDdeB|f|g<=C&WtyC;siVAV<*8kTW zwq-ke`*aEwGlYwKayLxeGEdcy2U&d2tkRDlJ>CH|jiXYqa=BV-Qh>X`!opJlt1nnJ zs{{$Ev^;Qzlu8ORNa8pAFLMtn*_Cw=S!`Qems2b z8&=EBA{+1@8rIk;;2Ty;WJ3gXNLXWAi*HyhVK^12L%|w*uzbU6nY36Rb!b>)!=P_i zEt6IU)OHu@n0A{6tdVZ5Rlg6Q18Tq;u#OQ{^d%G;eSH5klFCzIWn=BHRNFcImQs%W n8r%99b13w+KB3T9-#zsU?45%fJ6~3100000NkvXXu0mjfX8veL literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/unique/catalog-info-sold-out.png b/apps/frontend/src/assets/images/unique/catalog-info-sold-out.png new file mode 100644 index 0000000000000000000000000000000000000000..79626e146ba750d69a301276da31ea7ef3f5cf9f GIT binary patch literal 1100 zcmeAS@N?(olHy`uVBq!ia0y~yV9Wus+RgrtArf|+Rp#}#h@3H{`@JgOMv(~k-itlr1eUQnY2zEJgu}q~=E*E`3LzkBxfcI$Ha zoR?RRw!isk>9x^iZBmtG$+}B>;(C9t`74n8`01uk?q6L@s|)uQXCGVm?C*oue}75I z-?m%*^+gEN+plxKF28zy>$&^7+e<#yPPMgj*PON@)JMPlT`1`s>Q`URu1dw%d2?_}ZGVsMjG(UZ?8Uof5(MBiQTMk^zG|X&(56}uOoK2XlD#}26OI$ zy_~Px&&P$m{QG?U;;i#?Cp$maI{W(SjZNR2ds?TTJ|%xu`0AR+TwB&!zZE)`)@W;5 zUZuzCaj@l|gLDS-Ti=MswR_&#&bOJgXVPpr&D1y7WVSIxGu^E(+9&A4w2j5=80)S7 z+FLGv-Ne1&W1W56^xbbVeHsfmlQ+j(y%K-Yu(tVAEANGOlDBjldcXhv@Jr-Ikssd; zvE;*g6TdL=zPjK-Yy-vGDH+=l}vO3nk(7@=}q~&Ww!c`w2|B3+^g! ziDr_1KY!bG-30%IwyA5@E#BenvS6-@y*5zy>;r!P-#w~b@V2*+;kH%yg1ISI1Fxta zQes)ayCwXgtl2>lXX~63)lQBr^fMl)FIoxPNd*l^4r<=G<(aWe4Ul=ZfC|c{bO+ zE{{+8J@ea1c5f^%@HO&o0fzpz$Jq(j*Gx!uO_{H)w7mAl)(d6g=NNKQJC#t6R%<#j)JtaNQL3H7$32mhPO37rxag2!TSYHz01FxapSF z+rqbso#WUtx$o)rFAZiF?94LMmu7n`>3{va`1o^;Q&+7ZDSKn;+ja?V{pnNVjkmJg zvOL!IJ5m1gQDD$~)BJZw0+_%9?+D&FT)>*^pLxsWiR|RZsvEP4Dvx`AzNT?%tM{E4 zzPYPS)|bY;yEE;5YJ76>t9{m~@zIu1KcwX2*Cp;vE3Fd9PzL2u&gK7^WQ+WSO%%1B Q0*eO*Pgg&ebxsLQ0Oxob-v9sr literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/unique/grid-bg-glass.png b/apps/frontend/src/assets/images/unique/grid-bg-glass.png new file mode 100644 index 0000000000000000000000000000000000000000..5b64c480bf04361ebab979eab0f6caa99077be46 GIT binary patch literal 213 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbB=6Jd|hE&{od)1q(!GMS5!p;TX z?fqOX^gaonAURcJmxhyY+-nhw_L;fIk1#*pcYW)(wCc(CO-;QPs|0`I@}9crkkQf= z8@x1i*916~ofh?4QD?nPXq8Ta_CjGlAJr0}$tzp(S~gF)P#YfM^)Xjm^Qoe0fYU5a zCe{BZQkwt( literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/unique/grid-bg-sold-out.png b/apps/frontend/src/assets/images/unique/grid-bg-sold-out.png new file mode 100644 index 0000000000000000000000000000000000000000..94f66620ab811801e478e811b15ed62f043fe2a5 GIT binary patch literal 332 zcmV-S0ki&zP)h5Ga+``7kYa zTrp5p1lpq+Dta6m1np5m#S(C@u91AsKcnY-&l9qr>nW)@RV)E&&fkYF#tEcBQ4^U2 zZ{$lOwq}e8CghukP!gIn0E95cn-H1@36V#(F%1bZ%Rm!GURE*%-&|-w0mL6>0r77u zQ{eNEFc%?&ftMgkC}kdU5UPasM8Jd+e;q{-E0E?Ms)>LJwL}CHYJpgRS|iZkj;07+ eT_6ysi~0dr*QT5gZfC^+0000|k1|%Oc%$NbBI14-?iy0WWg+Q3`(%rg0K*4HH z7srr_TW_x~yg!L}n3b0WpUM0vb84vR#~Bx1Y>1Iw*T`_mi2y^Z(BE zzRp{_WrA=|eV(xBl!j{$LYY}O1QZ+^7#NwrjAIU&M^=4fe0tpC?5fn}H@|nx4UhTe syjCogNkzk9KWpfeG~H#3rf7d>%2~-dJ5BUh3eXu0p00i_>zopr0Jl*@f&c&j literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/unique/grid-count-bg.png b/apps/frontend/src/assets/images/unique/grid-count-bg.png new file mode 100644 index 0000000000000000000000000000000000000000..68e13bddbb5ecc5d535563bf0073c16f78a854d3 GIT binary patch literal 155 zcmeAS@N?(olHy`uVBq!ia0vp^DnQK1!3HFwjQR>doC1%?Vg?3oArNM~bhqvgP|(-Y z#WAGfRXdq6fvLB6=2f1V4r`oA!&m2g1p2Z^)DWM{Xe^ZuB<`N zn-z`^6=p1UlQl_^WhuYnkd$)#fUM~O2_A->-#iQ+f2}?PG?>BD)z4*}Q$iB};qNn{ literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/unique/inventory-info-amount-bg.png b/apps/frontend/src/assets/images/unique/inventory-info-amount-bg.png new file mode 100644 index 0000000000000000000000000000000000000000..af4e31e2e55f34b6625efeca646af58d95562065 GIT binary patch literal 521 zcmV+k0`~ohP)YULD%yF`E#M-(sSQ%7;Yg;D}8O$1^L97Js3?hS}G5YPT1SWL`X^?mRmxw+;EC`c`nZlA;cb=jmoWm68ZdHs;7zL(OcbOf3?*;_ z61#);4HJOWg4qC}Ek(ww>zg7l6o=an^Cvtb4AaL?O9TU#W_G_QvP=|*{UJ!K==8Nr z!S<~M1M5f)7_*L)hKbgZk}zhnmWBxm+P3!zJvR3~BMaKU_z&$Du$2Q`L=+;<00000 LNkvXXu0mjfB01-z literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/unique/numbers.png b/apps/frontend/src/assets/images/unique/numbers.png new file mode 100644 index 0000000000000000000000000000000000000000..e1ece79f46a35183bb72afcc166d116bc04615ff GIT binary patch literal 146 zcmeAS@N?(olHy`uVBq!ia0vp^hCs~9!3HGXl(Q@cQjwl6jv*DdmIga=F&J=s`ai#H z`auP+$-Ha}O|>OI@V$3EC=|ghwpJ=46%P_p!L9lxe&L5m1>4+P z>(ae#ibLh%1^N8Q1A>%QL>{F{R(hj`GUoMQEMBXMJcd-;4W%gp%3J=r+~Nh3t(uLf zZo0CAYr1V}b^AHl&~&V7uBz9OrhA(0S&lqC6o^)Yk+;#lG>rvM4W(aIxu>eb;jlKW z*LcxWb&P?-Pz?hC0+qX2MMfwq&(0XytfWPpS253I!AJr=s2U0=ourWFbK0z&+60EF zBa*9nO%o~2fWCh;G)?E|vRc`JC{w+M5|`cGoT(eEINDtV>ib?ND=U&w)@e5s z5LJug&@)|PGM!>%Vw1wfVkmG3MV1@kFmfUbo8ed&%x`l#5MGG+lg14>1&+C&9;=%s zqmUbM%mBHXsUcTqAqw0&wQ!(EIKnfwmx>tv9g^J5DpZBQ=ui#U%`iZYfg@zvc7Os) zbCE+_TBn%Uj^QZsowzNY@WSUIxLa4`ka`g>QUV8j^xFd*Ji}?WNvJ zeo1zY7Qa~O|5x!-YiXZkJqF9Cs)%${vCvEkqyD#McWOTCnjmRvZz zO(`*K!!=E$8Q4KVWMUNP1S1kT77HSa={T6ENaC5E39Wi`{IT-8mdx2?Sck=Mz5&3) zd<$V2BwoME zH?A%zAAa<}N$<=fzrMUrS_fB8KN)m>|EzWT(jVb7E!Fzv^55^0Z-2UF{_yk7558HG juYB=o`Pi3lU%&sW+F!419-NEBHr82LYk$7{#ykH20l7o+ literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/wired/icon_action.png b/apps/frontend/src/assets/images/wired/icon_action.png new file mode 100644 index 0000000000000000000000000000000000000000..78e90e6302316b47e54429b3440551c8499eaf87 GIT binary patch literal 171 zcmeAS@N?(olHy`uVBq!ia0vp@K+MO%1|+}KPrC%9iacE$Ln>}1Cmi7YXFP%9-}^gz z9@ra7DNhvnTmSP1%cf=j11k3{Q_en=ovq@tasGUHuHMsXJ60OBii(OB^q%;nagIr| zA#noR1PLEy9%XCeNiMUMCjNEqnXj}`yy}FR(&xgX!kfOnSmyrdQp@SENeq6x48`Bq VJ#7q2kpbGy;OXk;vd$@?2>>7KL7o5r literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/wired/icon_condition.png b/apps/frontend/src/assets/images/wired/icon_condition.png new file mode 100644 index 0000000000000000000000000000000000000000..26925a63f58d0070286af95dcc6280d98af51d98 GIT binary patch literal 173 zcmeAS@N?(olHy`uVBq!ia0vp@K+MO%1|+}KPrC%9N<3X0Ln>}1Cp0wt&#(~ElIwKk zTaph~(&hMDg!(;G-q3rL! V%ztc#SAq63c)I$ztaD0e0s!`tI1K;* literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/wired/icon_trigger.png b/apps/frontend/src/assets/images/wired/icon_trigger.png new file mode 100644 index 0000000000000000000000000000000000000000..f48d13c8751c476841934a05d27981388a5b467c GIT binary patch literal 169 zcmeAS@N?(olHy`uVBq!ia0vp@K+MO%1|+}KPrC%93OrpLLn>}1Cmdk;zcFFYga4mnY&+^FqP9Q~q0J2GqVqmv+}x%E|>n*9!#1_LgA}Aj!gWf#JSWyN0W(_f#KYQv$3aY STb2XuX7F_Nb6Mw<&;$SpoITb6 literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/wired/icon_wired_around.png b/apps/frontend/src/assets/images/wired/icon_wired_around.png new file mode 100644 index 0000000000000000000000000000000000000000..0b4b5a12545cb73a0f588dae5ad803c8954255af GIT binary patch literal 197 zcmeAS@N?(olHy`uVBq!ia0vp^0zk~k!3HF)wbmGcI0YV&#S9GGLLkg|>2BR0pkTSD zi(^Q|t>lD+gdhE^_t_sYu(5bFbR`CNhi+(K6y;#}$P~oWuv1B{<8|W?Te;GNBqjlA zhC*h!*8dCHOdYNsiaGM2;aN;g+k$FeMwc_~Mju#3%+@!4d2?Kx?f1fU!3TsJBA7qg qisc{N^K0IXhz3jHNaL{gj0}9DSzfmuh6@6nz~JfX=d#Wzp$PzgMnP== literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/wired/icon_wired_left_right.png b/apps/frontend/src/assets/images/wired/icon_wired_left_right.png new file mode 100644 index 0000000000000000000000000000000000000000..862d6d8138b8d597bd57b732bd0ca62e9c5db082 GIT binary patch literal 172 zcmeAS@N?(olHy`uVBq!ia0vp^0zk~k!3HF)wbmGcI0YV&#S9GGLLkg|>2BR0pkSP* zi(^Q|t>gp+J{5+KwF>|GS%q0y8Y&nq*&0l)F@LOG zc-`2z6q4+wG-@xnc39*Q(~dvel4}^k7TehNS0f9h@EE^ Q2HM2n>FVdQ&MBb@0GiJ;hyVZp literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/wired/icon_wired_north_east.png b/apps/frontend/src/assets/images/wired/icon_wired_north_east.png new file mode 100644 index 0000000000000000000000000000000000000000..3710854fdafa269ca0973583aca7e810ed450f44 GIT binary patch literal 157 zcmeAS@N?(olHy`uVBq!ia0vp@K+MU(1|(lrEz1IN3Opi<85p>QK$!8;-MT+OL4Qvd z$B>F!$q5b(EDCk}NB(yn&~RYvct3d+Ckso%4;#%{G9DW_Q>yGI{$tGMI*|OUcEwxH zj4T6|gbJoBtS{F6Y}dV#uFVdQ&MBb@02RV9 An*aa+ literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/wired/icon_wired_north_west.png b/apps/frontend/src/assets/images/wired/icon_wired_north_west.png new file mode 100644 index 0000000000000000000000000000000000000000..09eeefc18a1708ce99e036dac88ed5e552db1e87 GIT binary patch literal 166 zcmeAS@N?(olHy`uVBq!ia0vp@K+MU(1|(lrEz1IN3Opi<85p>QK$!8;-MT+O!3a+m z$B>F!$q5MwKm1wh_>XY%cr;9C`|GggKm!{`<^e8_n0fz)_lKXE*DxGSV3_~JMY`J6+6HI`gQu&X J%Q~loCIBUrHKPCk literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/wired/icon_wired_rotate_clockwise.png b/apps/frontend/src/assets/images/wired/icon_wired_rotate_clockwise.png new file mode 100644 index 0000000000000000000000000000000000000000..2827e3d23da0f4d5b51ecec406ccd267291579cd GIT binary patch literal 146 zcmeAS@N?(olHy`uVBq!ia0vp^+(699!3HFsq+HVlaSA*li-F=oAk28_ZrvZCpo^!A zV@SoVmdKI;Vst08a5LT>t<8 literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/wired/icon_wired_rotate_counter_clockwise.png b/apps/frontend/src/assets/images/wired/icon_wired_rotate_counter_clockwise.png new file mode 100644 index 0000000000000000000000000000000000000000..7e281bacbc901f43d3fd7afdc492c23fd85fe029 GIT binary patch literal 148 zcmeAS@N?(olHy`uVBq!ia0vp^+(699!3HFsq+HVlaSA*li-F=oAk28_ZrvZCpqrBWJMJ@otT*XdC@@=TP1kj0&pGT#>})m+%Rbl%a2p6P7#tNY(9AgS q_!wJ~smX(4C+3Y@6HOn4a4=ZSRQ(^2`y>)*B7>)^pUXO@geCyMyDJ+2 literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/wired/icon_wired_south_east.png b/apps/frontend/src/assets/images/wired/icon_wired_south_east.png new file mode 100644 index 0000000000000000000000000000000000000000..4217c4b830ff0c3101323d4440eb9fe1be8380a1 GIT binary patch literal 171 zcmeAS@N?(olHy`uVBq!ia0vp@K+MU(1|(lrEz1IN3Opi<85p>QK$!8;-MT+O!B|fh z$B>F!$q5b&>QK$!8;-MT+O!Dvqx z$B>F!$q5QPEDaV6u>p+zZ0rjA_>cT={9&^xYJ!9P6_=FDzZVvrkVtW`VHRs-P`SJ@ zvnb(!VkfV^Lm98)(Spkp8SQwV7q(ugS2EF>wCbh2!2RY{DGmmPiA!Ci0_L~g23o`5 M>FVdQ&MBb@00|Z}u>b%7 literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/wired/icon_wired_up_down.png b/apps/frontend/src/assets/images/wired/icon_wired_up_down.png new file mode 100644 index 0000000000000000000000000000000000000000..c2d243bae57101cfa041bcba5e4673e892e9fbc9 GIT binary patch literal 153 zcmeAS@N?(olHy`uVBq!ia0vp^0zk~k!3HF)wbmGcI0YV&#S9GGLLkg|>2BR0prE&> zi(^Q|t>lD+gdhE^_t_sYuqiQ`F>^_HF#P|xLupto?r7~m}M6D9WP*Hcs<85QdRWCZJ@CXp00i_>zopr0KBCt*#H0l literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/styles/bootstrap/_accordion.scss b/apps/frontend/src/assets/styles/bootstrap/_accordion.scss new file mode 100644 index 0000000..fc62ceb --- /dev/null +++ b/apps/frontend/src/assets/styles/bootstrap/_accordion.scss @@ -0,0 +1,118 @@ +// +// Base styles +// + +.accordion-button { + position: relative; + display: flex; + align-items: center; + width: 100%; + padding: $accordion-button-padding-y $accordion-button-padding-x; + @include font-size($font-size-base); + color: $accordion-button-color; + text-align: left; // Reset button style + background-color: $accordion-button-bg; + border: 0; + @include border-radius(0); + overflow-anchor: none; + @include transition($accordion-transition); + + &:not(.collapsed) { + color: $accordion-button-active-color; + background-color: $accordion-button-active-bg; + box-shadow: inset 0 ($accordion-border-width * -1) 0 $accordion-border-color; + + &::after { + background-image: escape-svg($accordion-button-active-icon); + transform: $accordion-icon-transform; + } + } + + // Accordion icon + &::after { + flex-shrink: 0; + width: $accordion-icon-width; + height: $accordion-icon-width; + margin-left: auto; + content: ""; + background-image: escape-svg($accordion-button-icon); + background-repeat: no-repeat; + background-size: $accordion-icon-width; + @include transition($accordion-icon-transition); + } + + &:hover { + z-index: 2; + } + + &:focus { + z-index: 3; + border-color: $accordion-button-focus-border-color; + outline: 0; + box-shadow: $accordion-button-focus-box-shadow; + } +} + +.accordion-header { + margin-bottom: 0; +} + +.accordion-item { + background-color: $accordion-bg; + border: $accordion-border-width solid $accordion-border-color; + + &:first-of-type { + @include border-top-radius($accordion-border-radius); + + .accordion-button { + @include border-top-radius($accordion-inner-border-radius); + } + } + + &:not(:first-of-type) { + border-top: 0; + } + + // Only set a border-radius on the last item if the accordion is collapsed + &:last-of-type { + @include border-bottom-radius($accordion-border-radius); + + .accordion-button { + &.collapsed { + @include border-bottom-radius($accordion-inner-border-radius); + } + } + + .accordion-collapse { + @include border-bottom-radius($accordion-border-radius); + } + } +} + +.accordion-body { + padding: $accordion-body-padding-y $accordion-body-padding-x; +} + + +// Flush accordion items +// +// Remove borders and border-radius to keep accordion items edge-to-edge. + +.accordion-flush { + .accordion-collapse { + border-width: 0; + } + + .accordion-item { + border-right: 0; + border-left: 0; + @include border-radius(0); + + &:first-child { border-top: 0; } + &:last-child { border-bottom: 0; } + + .accordion-button { + @include border-radius(0); + } + } +} diff --git a/apps/frontend/src/assets/styles/bootstrap/_alert.scss b/apps/frontend/src/assets/styles/bootstrap/_alert.scss new file mode 100644 index 0000000..34f1e84 --- /dev/null +++ b/apps/frontend/src/assets/styles/bootstrap/_alert.scss @@ -0,0 +1,57 @@ +// +// Base styles +// + +.alert { + position: relative; + padding: $alert-padding-y $alert-padding-x; + margin-bottom: $alert-margin-bottom; + border: $alert-border-width solid transparent; + @include border-radius($alert-border-radius); +} + +// Headings for larger alerts +.alert-heading { + // Specified to prevent conflicts of changing $headings-color + color: inherit; +} + +// Provide class for links that match alerts +.alert-link { + font-weight: $alert-link-font-weight; +} + + +// Dismissible alerts +// +// Expand the right padding and account for the close button's positioning. + +.alert-dismissible { + padding-right: $alert-dismissible-padding-r; + + // Adjust close link position + .btn-close { + position: absolute; + top: 0; + right: 0; + z-index: $stretched-link-z-index + 1; + padding: $alert-padding-y * 1.25 $alert-padding-x; + } +} + + +// scss-docs-start alert-modifiers +// Generate contextual modifier classes for colorizing the alert. + +@each $state, $value in $theme-colors { + $alert-background: shift-color($value, $alert-bg-scale); + $alert-border: shift-color($value, $alert-border-scale); + $alert-color: shift-color($value, $alert-color-scale); + @if (contrast-ratio($alert-background, $alert-color) < $min-contrast-ratio) { + $alert-color: mix($value, color-contrast($alert-background), abs($alert-color-scale)); + } + .alert-#{$state} { + @include alert-variant($alert-background, $alert-border, $alert-color); + } +} +// scss-docs-end alert-modifiers diff --git a/apps/frontend/src/assets/styles/bootstrap/_badge.scss b/apps/frontend/src/assets/styles/bootstrap/_badge.scss new file mode 100644 index 0000000..08df1b8 --- /dev/null +++ b/apps/frontend/src/assets/styles/bootstrap/_badge.scss @@ -0,0 +1,29 @@ +// Base class +// +// Requires one of the contextual, color modifier classes for `color` and +// `background-color`. + +.badge { + display: inline-block; + padding: $badge-padding-y $badge-padding-x; + @include font-size($badge-font-size); + font-weight: $badge-font-weight; + line-height: 1; + color: $badge-color; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + @include border-radius($badge-border-radius); + @include gradient-bg(); + + // Empty badges collapse automatically + &:empty { + display: none; + } +} + +// Quick fix for badges in buttons +.btn .badge { + position: relative; + top: -1px; +} diff --git a/apps/frontend/src/assets/styles/bootstrap/_breadcrumb.scss b/apps/frontend/src/assets/styles/bootstrap/_breadcrumb.scss new file mode 100644 index 0000000..f7fafe7 --- /dev/null +++ b/apps/frontend/src/assets/styles/bootstrap/_breadcrumb.scss @@ -0,0 +1,28 @@ +.breadcrumb { + display: flex; + flex-wrap: wrap; + padding: $breadcrumb-padding-y $breadcrumb-padding-x; + margin-bottom: $breadcrumb-margin-bottom; + @include font-size($breadcrumb-font-size); + list-style: none; + background-color: $breadcrumb-bg; + @include border-radius($breadcrumb-border-radius); +} + +.breadcrumb-item { + // The separator between breadcrumbs (by default, a forward-slash: "/") + + .breadcrumb-item { + padding-left: $breadcrumb-item-padding-x; + + &::before { + float: left; // Suppress inline spacings and underlining of the separator + padding-right: $breadcrumb-item-padding-x; + color: $breadcrumb-divider-color; + content: var(--#{$variable-prefix}breadcrumb-divider, escape-svg($breadcrumb-divider)) #{"/* rtl:"} var(--#{$variable-prefix}breadcrumb-divider, escape-svg($breadcrumb-divider-flipped)) #{"*/"}; + } + } + + &.active { + color: $breadcrumb-active-color; + } +} diff --git a/apps/frontend/src/assets/styles/bootstrap/_button-group.scss b/apps/frontend/src/assets/styles/bootstrap/_button-group.scss new file mode 100644 index 0000000..13aa056 --- /dev/null +++ b/apps/frontend/src/assets/styles/bootstrap/_button-group.scss @@ -0,0 +1,139 @@ +// Make the div behave like a button +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-flex; + vertical-align: middle; // match .btn alignment given font-size hack above + + > .btn { + position: relative; + flex: 1 1 auto; + } + + // Bring the hover, focused, and "active" buttons to the front to overlay + // the borders properly + > .btn-check:checked + .btn, + > .btn-check:focus + .btn, + > .btn:hover, + > .btn:focus, + > .btn:active, + > .btn.active { + z-index: 1; + } +} + +// Optional: Group multiple button groups together for a toolbar +.btn-toolbar { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; + + .input-group { + width: auto; + } +} + +.btn-group { + // Prevent double borders when buttons are next to each other + > .btn:not(:first-child), + > .btn-group:not(:first-child) { + margin-left: -$btn-border-width; + } + + // Reset rounded corners + > .btn:not(:last-child):not(.dropdown-toggle), + > .btn-group:not(:last-child) > .btn { + @include border-end-radius(0); + } + + // The left radius should be 0 if the button is: + // - the "third or more" child + // - the second child and the previous element isn't `.btn-check` (making it the first child visually) + // - part of a btn-group which isn't the first child + > .btn:nth-child(n + 3), + > :not(.btn-check) + .btn, + > .btn-group:not(:first-child) > .btn { + @include border-start-radius(0); + } +} + +// Sizing +// +// Remix the default button sizing classes into new ones for easier manipulation. + +.btn-group-sm > .btn { @extend .btn-sm; } +.btn-group-lg > .btn { @extend .btn-lg; } + + +// +// Split button dropdowns +// + +.dropdown-toggle-split { + padding-right: $btn-padding-x * .75; + padding-left: $btn-padding-x * .75; + + &::after, + .dropup &::after, + .dropend &::after { + margin-left: 0; + } + + .dropstart &::before { + margin-right: 0; + } +} + +.btn-sm + .dropdown-toggle-split { + padding-right: $btn-padding-x-sm * .75; + padding-left: $btn-padding-x-sm * .75; +} + +.btn-lg + .dropdown-toggle-split { + padding-right: $btn-padding-x-lg * .75; + padding-left: $btn-padding-x-lg * .75; +} + + +// The clickable button for toggling the menu +// Set the same inset shadow as the :active state +.btn-group.show .dropdown-toggle { + @include box-shadow($btn-active-box-shadow); + + // Show no shadow for `.btn-link` since it has no other button styles. + &.btn-link { + @include box-shadow(none); + } +} + + +// +// Vertical button groups +// + +.btn-group-vertical { + flex-direction: column; + align-items: flex-start; + justify-content: center; + + > .btn, + > .btn-group { + width: 100%; + } + + > .btn:not(:first-child), + > .btn-group:not(:first-child) { + margin-top: -$btn-border-width; + } + + // Reset rounded corners + > .btn:not(:last-child):not(.dropdown-toggle), + > .btn-group:not(:last-child) > .btn { + @include border-bottom-radius(0); + } + + > .btn ~ .btn, + > .btn-group:not(:first-child) > .btn { + @include border-top-radius(0); + } +} diff --git a/apps/frontend/src/assets/styles/bootstrap/_buttons.scss b/apps/frontend/src/assets/styles/bootstrap/_buttons.scss new file mode 100644 index 0000000..3c2cba9 --- /dev/null +++ b/apps/frontend/src/assets/styles/bootstrap/_buttons.scss @@ -0,0 +1,116 @@ +// +// Base styles +// + +.btn { + display: inline-block; + font-family: $btn-font-family; + font-weight: $btn-font-weight; + line-height: $btn-line-height; + color: $body-color; + text-align: center; + text-decoration: if($link-decoration == none, null, none); + white-space: $btn-white-space; + vertical-align: middle; + cursor: if($enable-button-pointers, pointer, null); + user-select: none; + background-color: transparent; + border: $btn-border-width solid transparent; + @include button-size($btn-padding-y, $btn-padding-x, $btn-font-size, $btn-border-radius); + @include transition($btn-transition); + + &:hover { + color: $body-color; + text-decoration: if($link-hover-decoration == underline, none, null); + } + + .btn-check:focus + &, + &:focus { + outline: 0; + box-shadow: $btn-focus-box-shadow; + } + + .btn-check:checked + &, + .btn-check:active + &, + &:active, + &.active { + @include box-shadow($btn-active-box-shadow); + + &:focus { + @include box-shadow($btn-focus-box-shadow, $btn-active-box-shadow); + } + } + + &:disabled, + &.disabled, + fieldset:disabled & { + pointer-events: none; + opacity: $btn-disabled-opacity; + @include box-shadow(none); + } +} + + +// +// Alternate buttons +// + +// scss-docs-start btn-variant-loops +@each $color, $value in $theme-colors { + .btn-#{$color} { + @include button-variant($value, $value); + } +} + +@each $color, $value in $theme-colors { + .btn-outline-#{$color} { + @include button-outline-variant($value); + } +} +// scss-docs-end btn-variant-loops + + +// +// Link buttons +// + +// Make a button look and behave like a link +.btn-link { + font-weight: $font-weight-normal; + color: $btn-link-color; + text-decoration: $link-decoration; + box-shadow: none !important; + + &:active { + color: $btn-link-color !important; + } + + &:hover { + color: $btn-link-hover-color; + text-decoration: $link-hover-decoration; + } + + &:focus { + text-decoration: $link-hover-decoration; + } + + &:disabled, + &.disabled { + color: $btn-link-disabled-color; + } + + // No need for an active state here +} + + +// +// Button Sizes +// + +.btn-lg { + @include button-size($btn-padding-y-lg, $btn-padding-x-lg, $btn-font-size-lg, $btn-border-radius-lg); +} + +.btn-sm { + @include button-size($btn-padding-y-sm, $btn-padding-x-sm, $btn-font-size-sm, $btn-border-radius-sm); +} diff --git a/apps/frontend/src/assets/styles/bootstrap/_card.scss b/apps/frontend/src/assets/styles/bootstrap/_card.scss new file mode 100644 index 0000000..22890f5 --- /dev/null +++ b/apps/frontend/src/assets/styles/bootstrap/_card.scss @@ -0,0 +1,216 @@ +// +// Base styles +// + +.card { + position: relative; + display: flex; + flex-direction: column; + min-width: 0; // See https://github.com/twbs/bootstrap/pull/22740#issuecomment-305868106 + height: $card-height; + word-wrap: break-word; + background-color: $card-bg; + background-clip: border-box; + border: $card-border-width solid $card-border-color; + @include border-radius($card-border-radius); + @include box-shadow($card-box-shadow); + + > hr { + margin-right: 0; + margin-left: 0; + } + + > .list-group { + border-top: inherit; + border-bottom: inherit; + + &:first-child { + border-top-width: 0; + @include border-top-radius($card-inner-border-radius); + } + + &:last-child { + border-bottom-width: 0; + @include border-bottom-radius($card-inner-border-radius); + } + } + + // Due to specificity of the above selector (`.card > .list-group`), we must + // use a child selector here to prevent double borders. + > .card-header + .list-group, + > .list-group + .card-footer { + border-top: 0; + } +} + +.card-body { + // Enable `flex-grow: 1` for decks and groups so that card blocks take up + // as much space as possible, ensuring footers are aligned to the bottom. + flex: 1 1 auto; + padding: $card-spacer-y $card-spacer-x; + color: $card-color; +} + +.card-title { + margin-bottom: $card-title-spacer-y; +} + +.card-subtitle { + margin-top: -$card-title-spacer-y * .5; + margin-bottom: 0; +} + +.card-text:last-child { + margin-bottom: 0; +} + +.card-link { + &:hover { + text-decoration: if($link-hover-decoration == underline, none, null); + } + + + .card-link { + margin-left: $card-spacer-x; + } +} + +// +// Optional textual caps +// + +.card-header { + padding: $card-cap-padding-y $card-cap-padding-x; + margin-bottom: 0; // Removes the default margin-bottom of + color: $card-cap-color; + background-color: $card-cap-bg; + border-bottom: $card-border-width solid $card-border-color; + + &:first-child { + @include border-radius($card-inner-border-radius $card-inner-border-radius 0 0); + } +} + +.card-footer { + padding: $card-cap-padding-y $card-cap-padding-x; + color: $card-cap-color; + background-color: $card-cap-bg; + border-top: $card-border-width solid $card-border-color; + + &:last-child { + @include border-radius(0 0 $card-inner-border-radius $card-inner-border-radius); + } +} + + +// +// Header navs +// + +.card-header-tabs { + margin-right: -$card-cap-padding-x * .5; + margin-bottom: -$card-cap-padding-y; + margin-left: -$card-cap-padding-x * .5; + border-bottom: 0; + + @if $nav-tabs-link-active-bg != $card-bg { + .nav-link.active { + background-color: $card-bg; + border-bottom-color: $card-bg; + } + } +} + +.card-header-pills { + margin-right: -$card-cap-padding-x * .5; + margin-left: -$card-cap-padding-x * .5; +} + +// Card image +.card-img-overlay { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + padding: $card-img-overlay-padding; + @include border-radius($card-inner-border-radius); +} + +.card-img, +.card-img-top, +.card-img-bottom { + width: 100%; // Required because we use flexbox and this inherently applies align-self: stretch +} + +.card-img, +.card-img-top { + @include border-top-radius($card-inner-border-radius); +} + +.card-img, +.card-img-bottom { + @include border-bottom-radius($card-inner-border-radius); +} + + +// +// Card groups +// + +.card-group { + // The child selector allows nested `.card` within `.card-group` + // to display properly. + > .card { + margin-bottom: $card-group-margin; + } + + @include media-breakpoint-up(sm) { + display: flex; + flex-flow: row wrap; + // The child selector allows nested `.card` within `.card-group` + // to display properly. + > .card { + // Flexbugs #4: https://github.com/philipwalton/flexbugs#flexbug-4 + flex: 1 0 0%; + margin-bottom: 0; + + + .card { + margin-left: 0; + border-left: 0; + } + + // Handle rounded corners + @if $enable-rounded { + &:not(:last-child) { + @include border-end-radius(0); + + .card-img-top, + .card-header { + // stylelint-disable-next-line property-disallowed-list + border-top-right-radius: 0; + } + .card-img-bottom, + .card-footer { + // stylelint-disable-next-line property-disallowed-list + border-bottom-right-radius: 0; + } + } + + &:not(:first-child) { + @include border-start-radius(0); + + .card-img-top, + .card-header { + // stylelint-disable-next-line property-disallowed-list + border-top-left-radius: 0; + } + .card-img-bottom, + .card-footer { + // stylelint-disable-next-line property-disallowed-list + border-bottom-left-radius: 0; + } + } + } + } + } +} diff --git a/apps/frontend/src/assets/styles/bootstrap/_carousel.scss b/apps/frontend/src/assets/styles/bootstrap/_carousel.scss new file mode 100644 index 0000000..3d8fb15 --- /dev/null +++ b/apps/frontend/src/assets/styles/bootstrap/_carousel.scss @@ -0,0 +1,229 @@ +// Notes on the classes: +// +// 1. .carousel.pointer-event should ideally be pan-y (to allow for users to scroll vertically) +// even when their scroll action started on a carousel, but for compatibility (with Firefox) +// we're preventing all actions instead +// 2. The .carousel-item-start and .carousel-item-end is used to indicate where +// the active slide is heading. +// 3. .active.carousel-item is the current slide. +// 4. .active.carousel-item-start and .active.carousel-item-end is the current +// slide in its in-transition state. Only one of these occurs at a time. +// 5. .carousel-item-next.carousel-item-start and .carousel-item-prev.carousel-item-end +// is the upcoming slide in transition. + +.carousel { + position: relative; +} + +.carousel.pointer-event { + touch-action: pan-y; +} + +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; + @include clearfix(); +} + +.carousel-item { + position: relative; + display: none; + float: left; + width: 100%; + margin-right: -100%; + backface-visibility: hidden; + @include transition($carousel-transition); +} + +.carousel-item.active, +.carousel-item-next, +.carousel-item-prev { + display: block; +} + +/* rtl:begin:ignore */ +.carousel-item-next:not(.carousel-item-start), +.active.carousel-item-end { + transform: translateX(100%); +} + +.carousel-item-prev:not(.carousel-item-end), +.active.carousel-item-start { + transform: translateX(-100%); +} + +/* rtl:end:ignore */ + + +// +// Alternate transitions +// + +.carousel-fade { + .carousel-item { + opacity: 0; + transition-property: opacity; + transform: none; + } + + .carousel-item.active, + .carousel-item-next.carousel-item-start, + .carousel-item-prev.carousel-item-end { + z-index: 1; + opacity: 1; + } + + .active.carousel-item-start, + .active.carousel-item-end { + z-index: 0; + opacity: 0; + @include transition(opacity 0s $carousel-transition-duration); + } +} + + +// +// Left/right controls for nav +// + +.carousel-control-prev, +.carousel-control-next { + position: absolute; + top: 0; + bottom: 0; + z-index: 1; + // Use flex for alignment (1-3) + display: flex; // 1. allow flex styles + align-items: center; // 2. vertically center contents + justify-content: center; // 3. horizontally center contents + width: $carousel-control-width; + padding: 0; + color: $carousel-control-color; + text-align: center; + background: none; + border: 0; + opacity: $carousel-control-opacity; + @include transition($carousel-control-transition); + + // Hover/focus state + &:hover, + &:focus { + color: $carousel-control-color; + text-decoration: none; + outline: 0; + opacity: $carousel-control-hover-opacity; + } +} +.carousel-control-prev { + left: 0; + background-image: if($enable-gradients, linear-gradient(90deg, rgba($black, .25), rgba($black, .001)), null); +} +.carousel-control-next { + right: 0; + background-image: if($enable-gradients, linear-gradient(270deg, rgba($black, .25), rgba($black, .001)), null); +} + +// Icons for within +.carousel-control-prev-icon, +.carousel-control-next-icon { + display: inline-block; + width: $carousel-control-icon-width; + height: $carousel-control-icon-width; + background-repeat: no-repeat; + background-position: 50%; + background-size: 100% 100%; +} + +/* rtl:options: { + "autoRename": true, + "stringMap":[ { + "name" : "prev-next", + "search" : "prev", + "replace" : "next" + } ] +} */ +.carousel-control-prev-icon { + background-image: escape-svg($carousel-control-prev-icon-bg); +} +.carousel-control-next-icon { + background-image: escape-svg($carousel-control-next-icon-bg); +} + +// Optional indicator pips/controls +// +// Add a container (such as a list) with the following class and add an item (ideally a focusable control, +// like a button) with data-bs-target for each slide your carousel holds. + +.carousel-indicators { + position: absolute; + right: 0; + bottom: 0; + left: 0; + z-index: 2; + display: flex; + justify-content: center; + padding: 0; + // Use the .carousel-control's width as margin so we don't overlay those + margin-right: $carousel-control-width; + margin-bottom: 1rem; + margin-left: $carousel-control-width; + list-style: none; + + [data-bs-target] { + box-sizing: content-box; + flex: 0 1 auto; + width: $carousel-indicator-width; + height: $carousel-indicator-height; + padding: 0; + margin-right: $carousel-indicator-spacer; + margin-left: $carousel-indicator-spacer; + text-indent: -999px; + cursor: pointer; + background-color: $carousel-indicator-active-bg; + background-clip: padding-box; + border: 0; + // Use transparent borders to increase the hit area by 10px on top and bottom. + border-top: $carousel-indicator-hit-area-height solid transparent; + border-bottom: $carousel-indicator-hit-area-height solid transparent; + opacity: $carousel-indicator-opacity; + @include transition($carousel-indicator-transition); + } + + .active { + opacity: $carousel-indicator-active-opacity; + } +} + + +// Optional captions +// +// + +.carousel-caption { + position: absolute; + right: (100% - $carousel-caption-width) * .5; + bottom: $carousel-caption-spacer; + left: (100% - $carousel-caption-width) * .5; + padding-top: $carousel-caption-padding-y; + padding-bottom: $carousel-caption-padding-y; + color: $carousel-caption-color; + text-align: center; +} + +// Dark mode carousel + +.carousel-dark { + .carousel-control-prev-icon, + .carousel-control-next-icon { + filter: $carousel-dark-control-icon-filter; + } + + .carousel-indicators [data-bs-target] { + background-color: $carousel-dark-indicator-active-bg; + } + + .carousel-caption { + color: $carousel-dark-caption-color; + } +} diff --git a/apps/frontend/src/assets/styles/bootstrap/_close.scss b/apps/frontend/src/assets/styles/bootstrap/_close.scss new file mode 100644 index 0000000..32a0f68 --- /dev/null +++ b/apps/frontend/src/assets/styles/bootstrap/_close.scss @@ -0,0 +1,40 @@ +// transparent background and border properties included for button version. +// iOS requires the button element instead of an anchor tag. +// If you want the anchor version, it requires `href="#"`. +// See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile + +.btn-close { + box-sizing: content-box; + width: $btn-close-width; + height: $btn-close-height; + padding: $btn-close-padding-y $btn-close-padding-x; + color: $btn-close-color; + background: transparent escape-svg($btn-close-bg) center / $btn-close-width auto no-repeat; // include transparent for button elements + border: 0; // for button elements + @include border-radius(); + opacity: $btn-close-opacity; + + // Override 's hover style + &:hover { + color: $btn-close-color; + text-decoration: none; + opacity: $btn-close-hover-opacity; + } + + &:focus { + outline: 0; + box-shadow: $btn-close-focus-shadow; + opacity: $btn-close-focus-opacity; + } + + &:disabled, + &.disabled { + pointer-events: none; + user-select: none; + opacity: $btn-close-disabled-opacity; + } +} + +.btn-close-white { + filter: $btn-close-white-filter; +} diff --git a/apps/frontend/src/assets/styles/bootstrap/_containers.scss b/apps/frontend/src/assets/styles/bootstrap/_containers.scss new file mode 100644 index 0000000..f88f1e5 --- /dev/null +++ b/apps/frontend/src/assets/styles/bootstrap/_containers.scss @@ -0,0 +1,41 @@ +// Container widths +// +// Set the container width, and override it for fixed navbars in media queries. + +@if $enable-grid-classes { + // Single container class with breakpoint max-widths + .container, + // 100% wide container at all breakpoints + .container-fluid { + @include make-container(); + } + + // Responsive containers that are 100% wide until a breakpoint + @each $breakpoint, $container-max-width in $container-max-widths { + .container-#{$breakpoint} { + @extend .container-fluid; + } + + @include media-breakpoint-up($breakpoint, $grid-breakpoints) { + %responsive-container-#{$breakpoint} { + max-width: $container-max-width; + } + + // Extend each breakpoint which is smaller or equal to the current breakpoint + $extend-breakpoint: true; + + @each $name, $width in $grid-breakpoints { + @if ($extend-breakpoint) { + .container#{breakpoint-infix($name, $grid-breakpoints)} { + @extend %responsive-container-#{$breakpoint}; + } + + // Once the current breakpoint is reached, stop extending + @if ($breakpoint == $name) { + $extend-breakpoint: false; + } + } + } + } + } +} diff --git a/apps/frontend/src/assets/styles/bootstrap/_dropdown.scss b/apps/frontend/src/assets/styles/bootstrap/_dropdown.scss new file mode 100644 index 0000000..adc1143 --- /dev/null +++ b/apps/frontend/src/assets/styles/bootstrap/_dropdown.scss @@ -0,0 +1,240 @@ +// The dropdown wrapper (`

`) +.dropup, +.dropend, +.dropdown, +.dropstart { + position: relative; +} + +.dropdown-toggle { + white-space: nowrap; + + // Generate the caret automatically + @include caret(); +} + +// The dropdown menu +.dropdown-menu { + position: absolute; + z-index: $zindex-dropdown; + display: none; // none by default, but block on "open" of the menu + min-width: $dropdown-min-width; + padding: $dropdown-padding-y $dropdown-padding-x; + margin: 0; // Override default margin of ul + @include font-size($dropdown-font-size); + color: $dropdown-color; + text-align: left; // Ensures proper alignment if parent has it changed (e.g., modal footer) + list-style: none; + background-color: $dropdown-bg; + background-clip: padding-box; + border: $dropdown-border-width solid $dropdown-border-color; + @include border-radius($dropdown-border-radius); + @include box-shadow($dropdown-box-shadow); + + &[data-bs-popper] { + top: 100%; + left: 0; + margin-top: $dropdown-spacer; + } +} + +// scss-docs-start responsive-breakpoints +// We deliberately hardcode the `bs-` prefix because we check +// this custom property in JS to determine Popper's positioning + +@each $breakpoint in map-keys($grid-breakpoints) { + @include media-breakpoint-up($breakpoint) { + $infix: breakpoint-infix($breakpoint, $grid-breakpoints); + + .dropdown-menu#{$infix}-start { + --bs-position: start; + + &[data-bs-popper] { + right: auto; + left: 0; + } + } + + .dropdown-menu#{$infix}-end { + --bs-position: end; + + &[data-bs-popper] { + right: 0; + left: auto; + } + } + } +} +// scss-docs-end responsive-breakpoints + +// Allow for dropdowns to go bottom up (aka, dropup-menu) +// Just add .dropup after the standard .dropdown class and you're set. +.dropup { + .dropdown-menu[data-bs-popper] { + top: auto; + bottom: 100%; + margin-top: 0; + margin-bottom: $dropdown-spacer; + } + + .dropdown-toggle { + @include caret(up); + } +} + +.dropend { + .dropdown-menu[data-bs-popper] { + top: 0; + right: auto; + left: 100%; + margin-top: 0; + margin-left: $dropdown-spacer; + } + + .dropdown-toggle { + @include caret(end); + &::after { + vertical-align: 0; + } + } +} + +.dropstart { + .dropdown-menu[data-bs-popper] { + top: 0; + right: 100%; + left: auto; + margin-top: 0; + margin-right: $dropdown-spacer; + } + + .dropdown-toggle { + @include caret(start); + &::before { + vertical-align: 0; + } + } +} + + +// Dividers (basically an `
`) within the dropdown +.dropdown-divider { + height: 0; + margin: $dropdown-divider-margin-y 0; + overflow: hidden; + border-top: 1px solid $dropdown-divider-bg; +} + +// Links, buttons, and more within the dropdown menu +// +// `
+ +
+

N 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 Xk!GtIeuWX3RvA29{an^LB{Ts5AIV;W literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-shakesp.png b/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-shakesp.png new file mode 100644 index 0000000000000000000000000000000000000000..d5011c74b29bd0641a0c9897c207903d63a3491c GIT binary patch literal 5036 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 literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-spritesheet.png b/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-spritesheet.png new file mode 100644 index 0000000000000000000000000000000000000000..02495714fc62ad63e0da7c100dc969321abdc86e GIT binary patch literal 5828 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 J);T3K0RV@$%B27R literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/room-widgets/thumbnail-widget/thumbnail-camera-spritesheet.png b/apps/frontend/src/assets/images/room-widgets/thumbnail-widget/thumbnail-camera-spritesheet.png new file mode 100644 index 0000000000000000000000000000000000000000..63a9397f20e4cedf864e6111ce0bd850044caaba GIT binary patch literal 883 zcmeAS@N?(olHy`uVBq!ia0vp^EkJyLgAGU??LB=LNO2Z;L>4nJa0`Jj~)z5CGjwu3~&!|3L`3*X{5dqp$~ewyAF_-w~YBi2$S9>0Q1Y`rw`FhJ+3mYt`#1Emm*(1S@9pcB?R#5R{pLp@-=hDgOXk1s zF74j^tIa^KiD6+aN3wyNDJTIkwikl7b4s3}RGN0Ly|*?NH;J z5VMm>kiq5OBhd~v$0sw_IfU>iiSJ@!be^lx5W(im#?+=2=)yj6Aw!Z@*K+RbY5_9@ zE(@5nD;ag{G`!Qr+~FWG&!^*?noA~+WQT=>Pe+$SiblhZ>n$tzS~(e;IT=|6zT5mv zaWXMbJhaF_F~LAl?9P%Lr!AjndkG(EnNhp>&$3G`E55%xRM}B9`>Bn~;pd7)B0OqG zYBc7oEP1u-Z1XmU0}L(VuNfXbt#PP0s4sAji-99wim7G0D98gG5Z`Rr;lRMiB7h*6 zoVc4$i0xeW{}ww!6*MG4T3~ALuqEX$T%DQkbRxLz9seR1vkxV<--_3+4!$1s?8??x zxnTnBeDw=ny}fmhiSbB@K){yHL?Mo?l8`8Y*o|Z=G_oRO6tD$0)U9CkAlEWE{bylh z<&l^8^;?sH@rb|?PA-E-4i@#RB<__y73SJ{|KxsYKE88#M<@TX-Vm`yKp@VZw{7BY jW7X{{T$ef9oa-6y>7~B8=s#DM0SG)@{an^LB{Ts5nJ6~q literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/room-widgets/trophy-widget/trophy-spritesheet.png b/apps/frontend/src/assets/images/room-widgets/trophy-widget/trophy-spritesheet.png new file mode 100644 index 0000000000000000000000000000000000000000..f9184cb57814534a20d5d669d292764fd173ff1d GIT binary patch literal 6595 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 literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/room-widgets/wordquiz-widget/thumbs-down-small.png b/apps/frontend/src/assets/images/room-widgets/wordquiz-widget/thumbs-down-small.png new file mode 100644 index 0000000000000000000000000000000000000000..78e51cfe5666ccc5fe46dcd7976f615d19eab56b GIT binary patch literal 145 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H1|*Mc$*~4f5uPrNAr-gYPIcsBP~c$s?*H$9 zZdkj6i$Tur7G^ow5`Hd;Ee-EoTf{t%3P-w%zGj`@S-x*>Vfq>2*}a+NP7@R&Lzig! sFNyP+%$9WF=#@-=yCb|#^S2#h&~MqhAozyaL!ccDp00i_>zopr00L$)8~^|S literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/room-widgets/wordquiz-widget/thumbs-down.png b/apps/frontend/src/assets/images/room-widgets/wordquiz-widget/thumbs-down.png new file mode 100644 index 0000000000000000000000000000000000000000..fd320c51d72f0debe17f9537d132ec4de35bf815 GIT binary patch literal 182 zcmeAS@N?(olHy`uVBq!ia0vp^@<6P_!3HFkdog(dsaj7L$B>F!Z?9bBYf#{5O}y9n zz20n9x0&0EGbb0S{y*j`US~PiMJ+NW|82kxr`Fv+B;%QntL{+KYvB5RZqC;?8;co3 zcluQ5JW=caG8r!IE7jJKC2Yu=J)QJ hqVly?g7yYi8Nao2ZhvIZ@C@h>22WQ%mvv4FO#s~;NumG% literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/room-widgets/wordquiz-widget/thumbs-up-small.png b/apps/frontend/src/assets/images/room-widgets/wordquiz-widget/thumbs-up-small.png new file mode 100644 index 0000000000000000000000000000000000000000..b93111f0cc078bd21ac90ce83f59ce4663474d53 GIT binary patch literal 135 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H1|*Mc$*~4fex5FlAr-fh6BLC0v@@|~G0&3r zDLwo$$zUt5x!UOu|5KP|N%~wU@Zvp|-f74ad^KU#&Wab3Hd4o&EqXL{^;hH>T$#?2 iEs<F!Z>KwQF(~pde^&qh zf7`pzl&z;$ a8+P{toZC}5t{w$CfWgz%&t;ucLK6V707rlT literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/room-widgets/youtube-widget/next.png b/apps/frontend/src/assets/images/room-widgets/youtube-widget/next.png new file mode 100644 index 0000000000000000000000000000000000000000..a02e164ba6f2f67e15c80ea935f447722be8e217 GIT binary patch literal 164 zcmeAS@N?(olHy`uVBq!ia0vp^qChOb!3HFM-ZD`IQrVs^jv*Ddk`odVe)P9VUbvCc z_`iEzMWf-Sg&u$YhnR}^{;?0`RXtv?h*$M!!6aVQV+EUdkEUB3ZA&u#Q~x-Q*}%aqjdAIwIv2^hiBLBl4!m^o1g#8dvmwF_@^$Zh9E?xd~`3 NgQu&X%Q~loCIF11JCFbX literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/room-widgets/youtube-widget/prev.png b/apps/frontend/src/assets/images/room-widgets/youtube-widget/prev.png new file mode 100644 index 0000000000000000000000000000000000000000..d48b658ebae5457b7933bdda12413135564ea62d GIT binary patch literal 249 zcmeAS@N?(olHy`uVBq!ia0vp^qChOb!3HFM-ZD`IQjEnx?oJHr&dIz4a#+$GeH|GX zHuiJ>Nn{1`ISV`@iy0XB4ude`@%$AjK*2sw7sn6_|FxG+^EN1mxCCxcsxC+mJ{I=o zu$;~zy9thM0!}BY58VC0IZxu~tbZ{wi3M^|k5+It?7sIm{^*8h^VobNIH!3A7#@pS zA(d>pMLOBis>g7_WrJz&Zd9(92$8+rtSC_VM%xNb!1@J*w6hZk(Ggg$;Z>hF{Fa=?cG?vR{;`k54Tx~uagj% z+*oj4WrnbL&#_zmH@sf>-zjvopY&B_o{aXZQ*IwkXGMhb-}2kKZri%o&;B{t?n(K( z!`=Cfu2Evbd$C&~`3vS*A3d8EELgVt?u)2$tI6AKW%z=p-#$5c3Dff$7rs{i7*) z(^u-xwDSwd*H^miK?%cKQn)ADNQh3MrzspnW_Qr*C_kQ1f|GoQl z<)V^fwHhD4+&}PQ_ui@7zU*FlJ#+VuW&3wub~|Q&`0qv8XV=fyU;lmPr)Xhem;A>w h8ZY$9be&_~OCMAT6X`lCRS8UI44$rjF6*2UngEKP)aP};9wU8$gdB;^Uld4*mQ#>+Vq zgulpuN+};5<2aHb^a+7A^l9q4j)vo*G{M`pQEJZfyv}7=Qrotw*}Ck(B1nl4f|g~` zw`4JYyE->bL$R@~l7j!t6??(4v><4--A#@Z;QyfuGef+gRONKh9FdduRfGFUZ@Fv3YWL*BGlmvJr1?K TLp@I}00000NkvXXu0mjfrPG%b literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/toolbar/arrow.png b/apps/frontend/src/assets/images/toolbar/arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..bf04ea0ecf9d491eb96f2433e37502b673bf7856 GIT binary patch literal 14533 zcmeI3O>g5w7{>=8(2~_kNE}#kvbQb&=}X{B(K5ZxEae*B8iUc@@iaA+G}q~O)nsl zUepy^)hg0*t*Gf*rK-Ipl~k>a)iTz~idHhznxW}ZcF2tai_F038QZN}SvmHrE+0ja zZ(uwcjf$gk(F+c-R;$&pTEeB0!W2q);zrh3al;#FlJh(*650XvBkH+QoY(4l!>BIH zaiRRk?rY%Zh1@V>$0*{l=&OQDx=qFusY)txOK-?%~K5eegKk_0kJn|Nl z%r7r2%*LeM&TG%@?_e;ubQrDOVF)s$bDd%5#3y*0gx)Z)$=V%uD>vrdeMEbc!I{bw zUs>39GNub`@s-q;OoeoWOm?qEGay#v1s%`pH_}g==))ZA&wB=4oH7Zr3Qhrlqb5aFKQsqpJw&&2^$($T|+0ZL62T;!DU`xlb zBI`e8fm)Eqv5lS=46LX@2i76MzI$lmNh46^6Uu0MeJ^0$B8{?%=QRUYGRce$H;gRT zCL65=Yba9c7|TQ}b$d%?#qMeqrPQscilugYO4+eVx2jhvRozY?w%gEtA$7~Mhw(zm zQaewW8rUzS{ve=ile79j6Zif}OUy}gq0~AJ+0;A%Q!&U+B|hJMyg{ zB=KUa%jc^t-@o$hDfT3b*0Ah&BWu|4S|E;zr&=f7%AJ{w{@(#i^8@?h8lxlPUbdjp zREcVFlF;i#BP$@Qhip!8mU)_R+A*8i#POcrr_*DQwoVVt`DDE~lxDn@8EhJ`E?EhF z&BPblCY4kDUOb%rVWM9ysqDa7{GD0hF>2DTe@L9A(t}CZ2Mf_{=`>>KG2@|(t!eh;mJ6rt?U7T zJ)7YC;beM<^A+%vgUJm&<-{e z3)0}m0v8t%6bM|91~(SCxQL)Y;DR)`vB1Se1O);Yq`{2^E-oS{5V#->ZY*$d5kY~# z1!-_&fs2a>3Ir}lgBuH6TtrYHa6uZ}Sm5F!f&zgH(%{Ad7Z(u}2wac`Hx{_Kh@e2= zf;70Xz{N!b1p*hO!HoqjE+QxpxF8K~EO2oVL4m*pX>enKi;D;f1TIK}8w*@qL{K1b zK^ojx;Nl{J0)Y$C;KrBYDokGYBQAT#Z^T~a`{~z*E9^x;$zI=XBXoKbq0c@?=-)GT z{sW<*iqM}22pL}^^s;yVu(JRF literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/toolbar/friend-search.png b/apps/frontend/src/assets/images/toolbar/friend-search.png new file mode 100644 index 0000000000000000000000000000000000000000..7156c4fd2ef8a18ba27ed00b62cd7aaa474a74cc GIT binary patch literal 2213 zcmcImc~I0=9A4LBRK#i(wMaF_qgu@-+3XqJ3bG2Qs}v}%N|kK#7E*RM>n6Ahb*G+F zozV`8rJ!hS6&ZC<8MSpp>s3#%*4v7$SQVsr;a#b=FR*N#jyn9)%)I2iyzhO#?|0-k zSrk7ZCbVOCM}3Gd+$*PdOF`4&a0^5eg%Nrrha*B#7z4Yy1dq2fcCA(H_7^f+tB3 z9LS}b&&OOzNF3*yWs6pB*uJVzUV(EzlR4b$okET%DXBxVGx2D2IL zjD{h!M!?t`b;0vyfz6ZyD4%aCTuxviM=(aTR?9FHY0zVofiq$h(AY5p!4eqYILHSy zECck-Y!f{^{B{_}%dE004l?RAq+P?>IZRJd9HwD`0W$zQz!iiLPwzp$I^De z<78l^@J=QT;BHqMiu|i4T5t#+i@?E{Q*u2+cIZVcFT&(y1-_6(Fe5N?@JM6X(F`kB zy9s6GGy@I|yyjmKy_V^0qV=W&Ncx84H^D@~E_oRbh)RQzZb5nBuahq_Gv6$}WvTyP z#Ro#mrZcWI0NW>y%I@HDqiISE{@$O~};b+D{kv8-BRUkoHfPsuPNw zIZGxUpHyDXZi??$sXe^;){-atrxY%1(=B7=s^bx}t53#G7j_@jFN?JI!S>~knU*@> zd?#z^wDld1{=V*zyB>?qt`1#Zdi7Gwz(?m66&*F4D2&YPa!{KbQ+L1YOy#rIYyF+C z-F;yC?$^II+{LSMucF(c*5_ZI5_e{XvRhTe-^Xs|ZAz;?nnRThKUlPskDg7{-LNW$ z<`hs5?mZq_G%j`Imhy<_fR4$j_$?)ox-xgFzyBV}H+`S!=G0nM1^7PE-zTrokUVE^ zkLp$S6YtNibm$^>UGBUd;edE0&t_Oi$AF@fNb{#;lsD zLfGZbz0a?$y?A_0L3qh4=emWCryOCLFKn)7DZMHOjH=eP;>lAXOL7aItTql> zc%|6i$3(Avai|1rOAW8=_)Be%@XiNaI)}B&b?D-y0XnVsR&Q=g!Tgo6`uWFB$J7|+ z+`mNO&!){<({snXb=n)Mfm@!;W5aSjTxz?Q5cy}xu)bu>Th@0=Lsyj#zbidHy|W>6 zrcKzs{lVA5H%IEb>@K;}?ns+1ZHiZ8_2^6hk%awm5 N;zmy}ZyuF8_aErWD;59% literal 0 HcmV?d00001 diff --git a/apps/frontend/src/assets/images/toolbar/icons/buildersclub.png b/apps/frontend/src/assets/images/toolbar/icons/buildersclub.png new file mode 100644 index 0000000000000000000000000000000000000000..bbf6d681267e9fe2ed4a125dbd558233cbb6ea22 GIT binary patch literal 576 zcmV-G0>Ax%@?n(5=LnY#13`g3nb&svJ90Mhib-p|3+@iB2bdBI#Yh7eB zhCr49CrX8?M93A;>6aO(NdSaCUhP;*6FXuCJbP;}PX>LHqBV-u%Rn{&(yt|S@O8*G zK&iO)UIr|QB3MCdPeV4al>ymGIS=~vcoFws?{|g?RN?8fPA^AOdf1=P$#f$GOGuGs z0SS`F^1YzJ2o^xJiezvtp`Qo~YzefAm~d;>C2&cgBGS2Vx%M@%Cm;(`iX;NodIOHm z+)2Z%^BR70yhZeRUKB_qEI8k+L7tcvVEF0YdXfoOK*N#xR)NdIE`%k(F~6Rb1;Sgc z|Ez*givYm%JPX15fHRa^ge9r065-&GAw~OLk0HWdTGoh z2px^@Jt?45EunD?1()}sza>!o`QV!^u$(c%gKH5_!2Ux1xD8=tfUh$Ym5xI5z@dLX z-d{dg$rF*8@&p_(4gO8@L>Mm9zf5pdL}p@4a9c)J04|EG66`(3&D#%5=lPeguvU5i O0000cL_!rjfMv5GDI^;=3j{=qIO<@b zD2K{nhk{TM0g-y3cq^AsWDsH*uBuEGp&W`@!O{(a?Q|TcJG1-V`{(=q|ND5@-o*8j3~JnIH(#87w-BMI{haZK4{7bX2w0VZnnC z(ZZNggDX)rVDJb

+ + ); +}; diff --git a/apps/frontend/src/common/layout/LayoutNotificationAlertView.tsx b/apps/frontend/src/common/layout/LayoutNotificationAlertView.tsx new file mode 100644 index 0000000..8db771a --- /dev/null +++ b/apps/frontend/src/common/layout/LayoutNotificationAlertView.tsx @@ -0,0 +1,35 @@ +import { FC, useMemo } from 'react'; +import { NotificationAlertType } from '../../api'; +import { NitroCardContentView, NitroCardHeaderView, NitroCardView, NitroCardViewProps } from '../card'; + +export interface LayoutNotificationAlertViewProps extends NitroCardViewProps +{ + title?: string; + type?: string; + onClose: () => void; +} + +export const LayoutNotificationAlertView: FC = props => +{ + const { title = '', onClose = null, classNames = [], children = null,type = NotificationAlertType.DEFAULT, ...rest } = props; + + const getClassNames = useMemo(() => + { + const newClassNames: string[] = [ 'nitro-alert' ]; + + newClassNames.push('nitro-alert-' + type); + + if(classNames.length) newClassNames.push(...classNames); + + return newClassNames; + }, [ classNames, type ]); + + return ( + + + + { children } + + + ); +} diff --git a/apps/frontend/src/common/layout/LayoutNotificationBubbleView.tsx b/apps/frontend/src/common/layout/LayoutNotificationBubbleView.tsx new file mode 100644 index 0000000..a482166 --- /dev/null +++ b/apps/frontend/src/common/layout/LayoutNotificationBubbleView.tsx @@ -0,0 +1,52 @@ +import { FC, useEffect, useMemo, useState } from 'react'; +import { Flex, FlexProps } from '..'; +import { TransitionAnimation, TransitionAnimationTypes } from '../transitions'; + +export interface LayoutNotificationBubbleViewProps extends FlexProps +{ + fadesOut?: boolean; + timeoutMs?: number; + onClose: () => void; +} + +export const LayoutNotificationBubbleView: FC = props => +{ + const { fadesOut = true, timeoutMs = 8000, onClose = null, overflow = 'hidden', classNames = [], ...rest } = props; + const [ isVisible, setIsVisible ] = useState(false); + + const getClassNames = useMemo(() => + { + const newClassNames: string[] = [ 'nitro-notification-bubble', 'rounded' ]; + + if(classNames.length) newClassNames.push(...classNames); + + return newClassNames; + }, [ classNames ]); + + useEffect(() => + { + setIsVisible(true); + + return () => setIsVisible(false); + }, []); + + useEffect(() => + { + if(!fadesOut) return; + + const timeout = setTimeout(() => + { + setIsVisible(false); + + setTimeout(() => onClose(), 300); + }, timeoutMs); + + return () => clearTimeout(timeout); + }, [ fadesOut, timeoutMs, onClose ]); + + return ( + + + + ); +} diff --git a/apps/frontend/src/common/layout/LayoutPetImageView.tsx b/apps/frontend/src/common/layout/LayoutPetImageView.tsx new file mode 100644 index 0000000..27d43de --- /dev/null +++ b/apps/frontend/src/common/layout/LayoutPetImageView.tsx @@ -0,0 +1,121 @@ +import { IPetCustomPart, PetFigureData, TextureUtils, Vector3d } from '@nitrots/nitro-renderer'; +import { CSSProperties, FC, useEffect, useMemo, useRef, useState } from 'react'; +import { GetRoomEngine } from '../../api'; +import { Base, BaseProps } from '../Base'; + +interface LayoutPetImageViewProps extends BaseProps +{ + figure?: string; + typeId?: number; + paletteId?: number; + petColor?: number; + customParts?: IPetCustomPart[]; + posture?: string; + headOnly?: boolean; + direction?: number; + scale?: number; +} + +export const LayoutPetImageView: FC = props => +{ + const { figure = '', typeId = -1, paletteId = -1, petColor = 0xFFFFFF, customParts = [], posture = 'std', headOnly = false, direction = 0, scale = 1, style = {}, ...rest } = props; + const [ petUrl, setPetUrl ] = useState(null); + const [ width, setWidth ] = useState(0); + const [ height, setHeight ] = useState(0); + const isDisposed = useRef(false); + + const getStyle = useMemo(() => + { + let newStyle: CSSProperties = {}; + + if(petUrl && petUrl.length) newStyle.backgroundImage = `url(${ petUrl })`; + + if(scale !== 1) + { + newStyle.transform = `scale(${ scale })`; + + if(!(scale % 1)) newStyle.imageRendering = 'pixelated'; + } + + newStyle.width = width; + newStyle.height = height; + + if(Object.keys(style).length) newStyle = { ...newStyle, ...style }; + + return newStyle; + }, [ petUrl, scale, style, width, height ]); + + useEffect(() => + { + let url = null; + + let petTypeId = typeId; + let petPaletteId = paletteId; + let petColor1 = petColor; + let petCustomParts: IPetCustomPart[] = customParts; + let petHeadOnly = headOnly; + + if(figure && figure.length) + { + const petFigureData = new PetFigureData(figure); + + petTypeId = petFigureData.typeId; + petPaletteId = petFigureData.paletteId; + petColor1 = petFigureData.color; + petCustomParts = petFigureData.customParts; + } + + if(petTypeId === 16) petHeadOnly = false; + + const imageResult = GetRoomEngine().getRoomObjectPetImage(petTypeId, petPaletteId, petColor1, new Vector3d((direction * 45)), 64, { + imageReady: (id, texture, image) => + { + if(isDisposed.current) return; + + if(image) + { + setPetUrl(image.src); + setWidth(image.width); + setHeight(image.height); + } + + else if(texture) + { + setPetUrl(TextureUtils.generateImageUrl(texture)); + setWidth(texture.width); + setHeight(texture.height); + } + }, + imageFailed: (id) => + { + + } + }, petHeadOnly, 0, petCustomParts, posture); + + if(imageResult) + { + const image = imageResult.getImage(); + + if(image) + { + setPetUrl(image.src); + setWidth(image.width); + setHeight(image.height); + } + } + }, [ figure, typeId, paletteId, petColor, customParts, posture, headOnly, direction ]); + + useEffect(() => + { + isDisposed.current = false; + + return () => + { + isDisposed.current = true; + } + }, []); + + const url = `url('${ petUrl }')`; + + return ; +} diff --git a/apps/frontend/src/common/layout/LayoutPrizeProductImageView.tsx b/apps/frontend/src/common/layout/LayoutPrizeProductImageView.tsx new file mode 100644 index 0000000..206c7a8 --- /dev/null +++ b/apps/frontend/src/common/layout/LayoutPrizeProductImageView.tsx @@ -0,0 +1,30 @@ +import { FC } from 'react'; +import { ProductTypeEnum } from '../../api'; +import { LayoutBadgeImageView } from './LayoutBadgeImageView'; +import { LayoutCurrencyIcon } from './LayoutCurrencyIcon'; +import { LayoutFurniImageView } from './LayoutFurniImageView'; + +interface LayoutPrizeProductImageViewProps +{ + productType: string; + classId: number; + extraParam?: string; +} + +export const LayoutPrizeProductImageView: FC = props => +{ + const { productType = ProductTypeEnum.FLOOR, classId = -1, extraParam = undefined } = props; + + switch(productType) + { + case ProductTypeEnum.WALL: + case ProductTypeEnum.FLOOR: + return + case ProductTypeEnum.BADGE: + return + case ProductTypeEnum.HABBO_CLUB: + return + } + + return null; +} diff --git a/apps/frontend/src/common/layout/LayoutProgressBar.tsx b/apps/frontend/src/common/layout/LayoutProgressBar.tsx new file mode 100644 index 0000000..8f82171 --- /dev/null +++ b/apps/frontend/src/common/layout/LayoutProgressBar.tsx @@ -0,0 +1,32 @@ +import { FC, useMemo } from 'react'; +import { Base, Column, ColumnProps, Flex } from '..'; + +interface LayoutProgressBarProps extends ColumnProps +{ + text?: string; + progress: number; + maxProgress?: number; +} + +export const LayoutProgressBar: FC = props => +{ + const { text = '', progress = 0, maxProgress = 100, position = 'relative', justifyContent = 'center', classNames = [], children = null, ...rest } = props; + + const getClassNames = useMemo(() => + { + const newClassNames: string[] = [ 'nitro-progress-bar', 'text-white' ]; + + if(classNames.length) newClassNames.push(...classNames); + + return newClassNames; + }, [ classNames ]); + + return ( + + { text && (text.length > 0) && + { text } } + + { children } + + ); +} diff --git a/apps/frontend/src/common/layout/LayoutRarityLevelView.tsx b/apps/frontend/src/common/layout/LayoutRarityLevelView.tsx new file mode 100644 index 0000000..2831374 --- /dev/null +++ b/apps/frontend/src/common/layout/LayoutRarityLevelView.tsx @@ -0,0 +1,28 @@ +import { FC, useMemo } from 'react'; +import { Base, BaseProps } from '..'; + +interface LayoutRarityLevelViewProps extends BaseProps +{ + level: number; +} + +export const LayoutRarityLevelView: FC = props => +{ + const { level = 0, classNames = [], children = null, ...rest } = props; + + const getClassNames = useMemo(() => + { + const newClassNames: string[] = [ 'nitro-rarity-level' ]; + + if(classNames.length) newClassNames.push(...classNames); + + return newClassNames; + }, [ classNames ]); + + return ( + +
{ level }
+ { children } + + ); +} diff --git a/apps/frontend/src/common/layout/LayoutRoomPreviewerView.tsx b/apps/frontend/src/common/layout/LayoutRoomPreviewerView.tsx new file mode 100644 index 0000000..69fbea8 --- /dev/null +++ b/apps/frontend/src/common/layout/LayoutRoomPreviewerView.tsx @@ -0,0 +1,97 @@ +import { ColorConverter, GetTicker, IRoomRenderingCanvas, RoomPreviewer, TextureUtils } from '@nitrots/nitro-renderer'; +import { FC, MouseEvent, ReactNode, useEffect, useRef, useState } from 'react'; + +export interface LayoutRoomPreviewerViewProps +{ + roomPreviewer: RoomPreviewer; + height?: number; + children?: ReactNode; +} + +export const LayoutRoomPreviewerView: FC = props => +{ + const { roomPreviewer = null, height = 0, children = null } = props; + const [ renderingCanvas, setRenderingCanvas ] = useState(null); + const elementRef = useRef(); + + const onClick = (event: MouseEvent) => + { + if(!roomPreviewer) return; + + if(event.shiftKey) roomPreviewer.changeRoomObjectDirection(); + else roomPreviewer.changeRoomObjectState(); + } + + useEffect(() => + { + if(!roomPreviewer) return; + + const update = (time: number) => + { + if(!roomPreviewer || !renderingCanvas || !elementRef.current) return; + + roomPreviewer.updatePreviewRoomView(); + + if(!renderingCanvas.canvasUpdated) return; + + elementRef.current.style.backgroundImage = `url(${ TextureUtils.generateImageUrl(renderingCanvas.master) })`; + } + + if(!renderingCanvas) + { + if(elementRef.current && roomPreviewer) + { + const computed = document.defaultView.getComputedStyle(elementRef.current, null); + + let backgroundColor = computed.backgroundColor; + + backgroundColor = ColorConverter.rgbStringToHex(backgroundColor); + backgroundColor = backgroundColor.replace('#', '0x'); + + roomPreviewer.backgroundColor = parseInt(backgroundColor, 16); + + const width = elementRef.current.parentElement.clientWidth; + + roomPreviewer.getRoomCanvas(width, height); + + const canvas = roomPreviewer.getRenderingCanvas(); + + setRenderingCanvas(canvas); + + canvas.canvasUpdated = true; + + update(-1); + } + } + + GetTicker().add(update); + + const resizeObserver = new ResizeObserver(() => + { + if(!roomPreviewer || !elementRef.current) return; + + const width = elementRef.current.parentElement.offsetWidth; + + roomPreviewer.modifyRoomCanvas(width, height); + + update(-1); + }); + + resizeObserver.observe(elementRef.current); + + return () => + { + resizeObserver.disconnect(); + + GetTicker().remove(update); + } + + }, [ renderingCanvas, roomPreviewer, elementRef, height ]); + + return ( +
+
+ { children } +
+ ); +} diff --git a/apps/frontend/src/common/layout/LayoutRoomThumbnailView.tsx b/apps/frontend/src/common/layout/LayoutRoomThumbnailView.tsx new file mode 100644 index 0000000..46d884f --- /dev/null +++ b/apps/frontend/src/common/layout/LayoutRoomThumbnailView.tsx @@ -0,0 +1,37 @@ +import { FC, useMemo } from 'react'; +import { GetConfiguration } from '../../api'; +import { Base, BaseProps } from '../Base'; + +export interface LayoutRoomThumbnailViewProps extends BaseProps +{ + roomId?: number; + customUrl?: string; +} + +export const LayoutRoomThumbnailView: FC = props => +{ + const { roomId = -1, customUrl = null, shrink = true, overflow = 'hidden', classNames = [], children = null, ...rest } = props; + + const getClassNames = useMemo(() => + { + const newClassNames: string[] = [ 'room-thumbnail', 'rounded', 'border' ]; + + if(classNames.length) newClassNames.push(...classNames); + + return newClassNames; + }, [ classNames ]); + + const getImageUrl = useMemo(() => + { + if(customUrl && customUrl.length) return (GetConfiguration('image.library.url') + customUrl); + + return (GetConfiguration('thumbnails.url').replace('%thumbnail%', roomId.toString())); + }, [ customUrl, roomId ]); + + return ( + + { getImageUrl && } + { children } + + ); +} diff --git a/apps/frontend/src/common/layout/LayoutTrophyView.tsx b/apps/frontend/src/common/layout/LayoutTrophyView.tsx new file mode 100644 index 0000000..1320625 --- /dev/null +++ b/apps/frontend/src/common/layout/LayoutTrophyView.tsx @@ -0,0 +1,39 @@ +import { FC } from 'react'; +import { Base, Column, Flex, Text } from '..'; +import { LocalizeText } from '../../api'; +import { DraggableWindow } from '../draggable-window'; + +interface LayoutTrophyViewProps +{ + color: string; + message: string; + date: string; + senderName: string; + customTitle?: string; + onCloseClick: () => void; +} + +export const LayoutTrophyView: FC = props => +{ + const { color = '', message = '', date = '', senderName = '', customTitle = null, onCloseClick = null } = props; + + return ( + + + + + { LocalizeText('widget.furni.trophy.title') } + + + { customTitle && + { customTitle } } + { message } + + + { date } + { senderName } + + + + ); +} diff --git a/apps/frontend/src/common/layout/UserProfileIconView.tsx b/apps/frontend/src/common/layout/UserProfileIconView.tsx new file mode 100644 index 0000000..d31fa66 --- /dev/null +++ b/apps/frontend/src/common/layout/UserProfileIconView.tsx @@ -0,0 +1,29 @@ +import { FC, useMemo } from 'react'; +import { GetUserProfile } from '../../api'; +import { Base, BaseProps } from '../Base'; + +export interface UserProfileIconViewProps extends BaseProps +{ + userId?: number; + userName?: string; +} + +export const UserProfileIconView: FC = props => +{ + const { userId = 0, userName = null, classNames = [], pointer = true, children = null, ...rest } = props; + + const getClassNames = useMemo(() => + { + const newClassNames: string[] = [ 'nitro-friends-spritesheet', 'icon-profile-sm' ]; + + if(classNames.length) newClassNames.push(...classNames); + + return newClassNames; + }, [ classNames ]); + + return ( + GetUserProfile(userId) } { ... rest }> + { children } + + ); +} diff --git a/apps/frontend/src/common/layout/index.ts b/apps/frontend/src/common/layout/index.ts new file mode 100644 index 0000000..3c4238e --- /dev/null +++ b/apps/frontend/src/common/layout/index.ts @@ -0,0 +1,23 @@ +export * from './LayoutAvatarImageView'; +export * from './LayoutBackgroundImage'; +export * from './LayoutBadgeImageView'; +export * from './LayoutCounterTimeView'; +export * from './LayoutCurrencyIcon'; +export * from './LayoutFurniIconImageView'; +export * from './LayoutFurniImageView'; +export * from './LayoutGiftTagView'; +export * from './LayoutGridItem'; +export * from './LayoutImage'; +export * from './LayoutItemCountView'; +export * from './LayoutLoadingSpinnerView'; +export * from './LayoutMiniCameraView'; +export * from './LayoutNotificationAlertView'; +export * from './LayoutNotificationBubbleView'; +export * from './LayoutPetImageView'; +export * from './LayoutProgressBar'; +export * from './LayoutRarityLevelView'; +export * from './LayoutRoomPreviewerView'; +export * from './LayoutRoomThumbnailView'; +export * from './LayoutTrophyView'; +export * from './limited-edition'; +export * from './UserProfileIconView'; diff --git a/apps/frontend/src/common/layout/limited-edition/LayoutLimitedEditionCompactPlateView.tsx b/apps/frontend/src/common/layout/limited-edition/LayoutLimitedEditionCompactPlateView.tsx new file mode 100644 index 0000000..ee41c6c --- /dev/null +++ b/apps/frontend/src/common/layout/limited-edition/LayoutLimitedEditionCompactPlateView.tsx @@ -0,0 +1,35 @@ +import { FC, useMemo } from 'react'; +import { Base, BaseProps } from '../../Base'; +import { LayoutLimitedEditionStyledNumberView } from './LayoutLimitedEditionStyledNumberView'; + +interface LayoutLimitedEditionCompactPlateViewProps extends BaseProps +{ + uniqueNumber: number; + uniqueSeries: number; +} + +export const LayoutLimitedEditionCompactPlateView: FC = props => +{ + const { uniqueNumber = 0, uniqueSeries = 0, classNames = [], children = null, ...rest } = props; + + const getClassNames = useMemo(() => + { + const newClassNames: string[] = [ 'unique-compact-plate', 'z-index-1' ]; + + if(classNames.length) newClassNames.push(...classNames); + + return newClassNames; + }, [ classNames ]); + + return ( + +
+ +
+
+ +
+ { children } + + ); +} diff --git a/apps/frontend/src/common/layout/limited-edition/LayoutLimitedEditionCompletePlateView.tsx b/apps/frontend/src/common/layout/limited-edition/LayoutLimitedEditionCompletePlateView.tsx new file mode 100644 index 0000000..c83e230 --- /dev/null +++ b/apps/frontend/src/common/layout/limited-edition/LayoutLimitedEditionCompletePlateView.tsx @@ -0,0 +1,41 @@ +import { FC, useMemo } from 'react'; +import { LocalizeText } from '../../../api'; +import { Base, BaseProps } from '../../Base'; +import { Column } from '../../Column'; +import { Flex } from '../../Flex'; +import { LayoutLimitedEditionStyledNumberView } from './LayoutLimitedEditionStyledNumberView'; + +interface LayoutLimitedEditionCompletePlateViewProps extends BaseProps +{ + uniqueLimitedItemsLeft: number; + uniqueLimitedSeriesSize: number; +} + +export const LayoutLimitedEditionCompletePlateView: FC = props => +{ + const { uniqueLimitedItemsLeft = 0, uniqueLimitedSeriesSize = 0, classNames = [], ...rest } = props; + + const getClassNames = useMemo(() => + { + const newClassNames: string[] = [ 'unique-complete-plate' ]; + + if(classNames.length) newClassNames.push(...classNames); + + return newClassNames; + }, [ classNames ]); + + return ( + + + + { LocalizeText('unique.items.left') } +
+
+ + { LocalizeText('unique.items.number.sold') } +
+
+
+ + ); +} diff --git a/apps/frontend/src/common/layout/limited-edition/LayoutLimitedEditionStyledNumberView.tsx b/apps/frontend/src/common/layout/limited-edition/LayoutLimitedEditionStyledNumberView.tsx new file mode 100644 index 0000000..fe34ba4 --- /dev/null +++ b/apps/frontend/src/common/layout/limited-edition/LayoutLimitedEditionStyledNumberView.tsx @@ -0,0 +1,18 @@ +import { FC } from 'react'; + +interface LayoutLimitedEditionStyledNumberViewProps +{ + value: number; +} + +export const LayoutLimitedEditionStyledNumberView: FC = props => +{ + const { value = 0 } = props; + const numbers = value.toString().split(''); + + return ( + <> + { numbers.map((number, index) => ) } + + ); +} diff --git a/apps/frontend/src/common/layout/limited-edition/index.ts b/apps/frontend/src/common/layout/limited-edition/index.ts new file mode 100644 index 0000000..ee41cf9 --- /dev/null +++ b/apps/frontend/src/common/layout/limited-edition/index.ts @@ -0,0 +1,3 @@ +export * from './LayoutLimitedEditionCompactPlateView'; +export * from './LayoutLimitedEditionCompletePlateView'; +export * from './LayoutLimitedEditionStyledNumberView'; diff --git a/apps/frontend/src/common/transitions/TransitionAnimation.tsx b/apps/frontend/src/common/transitions/TransitionAnimation.tsx new file mode 100644 index 0000000..6eefd2d --- /dev/null +++ b/apps/frontend/src/common/transitions/TransitionAnimation.tsx @@ -0,0 +1,52 @@ +import { FC, ReactNode, useEffect, useState } from 'react'; +import { Transition } from 'react-transition-group'; +import { getTransitionAnimationStyle } from './TransitionAnimationStyles'; + +interface TransitionAnimationProps +{ + type: string; + inProp: boolean; + timeout?: number; + className?: string; + children?: ReactNode; +} + +export const TransitionAnimation: FC = props => +{ + const { type = null, inProp = false, timeout = 300, className = null, children = null } = props; + + const [ isChildrenVisible, setChildrenVisible ] = useState(false); + + useEffect(() => + { + let timeoutData: ReturnType = null; + + if(inProp) + { + setChildrenVisible(true); + } + else + { + timeoutData = setTimeout(() => + { + setChildrenVisible(false); + clearTimeout(timeout); + }, timeout); + } + + return () => + { + if(timeoutData) clearTimeout(timeoutData); + } + }, [ inProp, timeout ]); + + return ( + + { state => ( +
+ { isChildrenVisible && children } +
+ ) } +
+ ); +} diff --git a/apps/frontend/src/common/transitions/TransitionAnimationStyles.ts b/apps/frontend/src/common/transitions/TransitionAnimationStyles.ts new file mode 100644 index 0000000..0d512d0 --- /dev/null +++ b/apps/frontend/src/common/transitions/TransitionAnimationStyles.ts @@ -0,0 +1,136 @@ +import { CSSProperties } from 'react'; +import { TransitionStatus } from 'react-transition-group'; +import { ENTERING, EXITING } from 'react-transition-group/Transition'; +import { TransitionAnimationTypes } from './TransitionAnimationTypes'; + +export function getTransitionAnimationStyle(type: string, transition: TransitionStatus, timeout: number = 300): Partial +{ + switch(type) + { + case TransitionAnimationTypes.BOUNCE: + switch(transition) + { + default: + return {} + case ENTERING: + return { + animationName: 'bounceIn', + animationDuration: `${ timeout }ms` + } + case EXITING: + return { + animationName: 'bounceOut', + animationDuration: `${ timeout }ms` + } + } + case TransitionAnimationTypes.SLIDE_LEFT: + switch(transition) + { + default: + return {} + case ENTERING: + return { + animationName: 'slideInLeft', + animationDuration: `${ timeout }ms` + } + case EXITING: + return { + animationName: 'slideOutLeft', + animationDuration: `${ timeout }ms` + } + } + case TransitionAnimationTypes.SLIDE_RIGHT: + switch(transition) + { + default: + return {} + case ENTERING: + return { + animationName: 'slideInRight', + animationDuration: `${ timeout }ms` + } + case EXITING: + return { + animationName: 'slideOutRight', + animationDuration: `${ timeout }ms` + } + } + case TransitionAnimationTypes.FLIP_X: + switch(transition) + { + default: + return {} + case ENTERING: + return { + animationName: 'flipInX', + animationDuration: `${ timeout }ms` + } + case EXITING: + return { + animationName: 'flipOutX', + animationDuration: `${ timeout }ms` + } + } + case TransitionAnimationTypes.FADE_UP: + switch(transition) + { + default: + return {} + case ENTERING: + return { + animationName: 'fadeInUp', + animationDuration: `${ timeout }ms` + } + case EXITING: + return { + animationName: 'fadeOutDown', + animationDuration: `${ timeout }ms` + } + } + case TransitionAnimationTypes.FADE_IN: + switch(transition) + { + default: + return {} + case ENTERING: + return { + animationName: 'fadeIn', + animationDuration: `${ timeout }ms` + } + case EXITING: + return { + animationName: 'fadeOut', + animationDuration: `${ timeout }ms` + } + } + case TransitionAnimationTypes.FADE_DOWN: + switch(transition) + { + default: + return {} + case ENTERING: + return { + animationName: 'fadeInDown', + animationDuration: `${ timeout }ms` + } + case EXITING: + return { + animationName: 'fadeOutUp', + animationDuration: `${ timeout }ms` + } + } + case TransitionAnimationTypes.HEAD_SHAKE: + switch(transition) + { + default: + return {} + case ENTERING: + return { + animationName: 'headShake', + animationDuration: `${ timeout }ms` + } + } + } + + return null; +} diff --git a/apps/frontend/src/common/transitions/TransitionAnimationTypes.ts b/apps/frontend/src/common/transitions/TransitionAnimationTypes.ts new file mode 100644 index 0000000..4ecc23b --- /dev/null +++ b/apps/frontend/src/common/transitions/TransitionAnimationTypes.ts @@ -0,0 +1,11 @@ +export class TransitionAnimationTypes +{ + public static BOUNCE: string = 'bounce'; + public static SLIDE_LEFT: string = 'slideLeft'; + public static SLIDE_RIGHT: string = 'slideRight'; + public static FLIP_X: string = 'flipX'; + public static FADE_IN: string = 'fadeIn'; + public static FADE_DOWN: string = 'fadeDown'; + public static FADE_UP: string = 'fadeUp'; + public static HEAD_SHAKE: string = 'headShake'; +} diff --git a/apps/frontend/src/common/transitions/index.ts b/apps/frontend/src/common/transitions/index.ts new file mode 100644 index 0000000..283a005 --- /dev/null +++ b/apps/frontend/src/common/transitions/index.ts @@ -0,0 +1,3 @@ +export * from './TransitionAnimation'; +export * from './TransitionAnimationStyles'; +export * from './TransitionAnimationTypes'; diff --git a/apps/frontend/src/common/types/AlignItemType.ts b/apps/frontend/src/common/types/AlignItemType.ts new file mode 100644 index 0000000..5a61476 --- /dev/null +++ b/apps/frontend/src/common/types/AlignItemType.ts @@ -0,0 +1 @@ +export type AlignItemType = 'start' | 'end' | 'center' | 'baseline' | 'stretch'; diff --git a/apps/frontend/src/common/types/AlignSelfType.ts b/apps/frontend/src/common/types/AlignSelfType.ts new file mode 100644 index 0000000..8e26378 --- /dev/null +++ b/apps/frontend/src/common/types/AlignSelfType.ts @@ -0,0 +1 @@ +export type AlignSelfType = 'start' | 'end' | 'center' | 'baseline' | 'stretch'; diff --git a/apps/frontend/src/common/types/ButtonSizeType.ts b/apps/frontend/src/common/types/ButtonSizeType.ts new file mode 100644 index 0000000..0f9dd4a --- /dev/null +++ b/apps/frontend/src/common/types/ButtonSizeType.ts @@ -0,0 +1 @@ +export type ButtonSizeType = 'lg' | 'sm'; diff --git a/apps/frontend/src/common/types/ColorVariantType.ts b/apps/frontend/src/common/types/ColorVariantType.ts new file mode 100644 index 0000000..1ff6f60 --- /dev/null +++ b/apps/frontend/src/common/types/ColorVariantType.ts @@ -0,0 +1 @@ +export type ColorVariantType = 'primary' | 'success' | 'danger' | 'secondary' | 'link' | 'black' | 'white' | 'dark' | 'warning' | 'muted' | 'light'; diff --git a/apps/frontend/src/common/types/ColumnSizesType.ts b/apps/frontend/src/common/types/ColumnSizesType.ts new file mode 100644 index 0000000..2b130d8 --- /dev/null +++ b/apps/frontend/src/common/types/ColumnSizesType.ts @@ -0,0 +1 @@ +export type ColumnSizesType = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; diff --git a/apps/frontend/src/common/types/DisplayType.ts b/apps/frontend/src/common/types/DisplayType.ts new file mode 100644 index 0000000..7551d72 --- /dev/null +++ b/apps/frontend/src/common/types/DisplayType.ts @@ -0,0 +1 @@ +export type DisplayType = 'none' | 'inline' | 'inline-block' | 'block' | 'grid' | 'table' | 'table-cell' | 'table-row' | 'flex' | 'inline-flex'; diff --git a/apps/frontend/src/common/types/FloatType.ts b/apps/frontend/src/common/types/FloatType.ts new file mode 100644 index 0000000..63e495f --- /dev/null +++ b/apps/frontend/src/common/types/FloatType.ts @@ -0,0 +1 @@ +export type FloatType = 'start' | 'end' | 'none'; diff --git a/apps/frontend/src/common/types/FontSizeType.ts b/apps/frontend/src/common/types/FontSizeType.ts new file mode 100644 index 0000000..120c11c --- /dev/null +++ b/apps/frontend/src/common/types/FontSizeType.ts @@ -0,0 +1 @@ +export type FontSizeType = 1 | 2 | 3 | 4 | 5 | 6; diff --git a/apps/frontend/src/common/types/FontWeightType.ts b/apps/frontend/src/common/types/FontWeightType.ts new file mode 100644 index 0000000..c7c9286 --- /dev/null +++ b/apps/frontend/src/common/types/FontWeightType.ts @@ -0,0 +1 @@ +export type FontWeightType = 'bold' | 'bolder' | 'normal' | 'light' | 'lighter'; diff --git a/apps/frontend/src/common/types/JustifyContentType.ts b/apps/frontend/src/common/types/JustifyContentType.ts new file mode 100644 index 0000000..73a318d --- /dev/null +++ b/apps/frontend/src/common/types/JustifyContentType.ts @@ -0,0 +1 @@ +export type JustifyContentType = 'start' | 'end' | 'center' | 'between' | 'around' | 'evenly'; diff --git a/apps/frontend/src/common/types/OverflowType.ts b/apps/frontend/src/common/types/OverflowType.ts new file mode 100644 index 0000000..9231ff9 --- /dev/null +++ b/apps/frontend/src/common/types/OverflowType.ts @@ -0,0 +1 @@ +export type OverflowType = 'auto' | 'hidden' | 'visible' | 'scroll' | 'y-scroll' | 'unset'; diff --git a/apps/frontend/src/common/types/PositionType.ts b/apps/frontend/src/common/types/PositionType.ts new file mode 100644 index 0000000..4e20b2f --- /dev/null +++ b/apps/frontend/src/common/types/PositionType.ts @@ -0,0 +1 @@ +export type PositionType = 'static' | 'relative' | 'fixed' | 'absolute' | 'sticky'; diff --git a/apps/frontend/src/common/types/SpacingType.ts b/apps/frontend/src/common/types/SpacingType.ts new file mode 100644 index 0000000..91c2bb5 --- /dev/null +++ b/apps/frontend/src/common/types/SpacingType.ts @@ -0,0 +1 @@ +export type SpacingType = 0 | 1 | 2 | 3 | 4 | 5; diff --git a/apps/frontend/src/common/types/TextAlignType.ts b/apps/frontend/src/common/types/TextAlignType.ts new file mode 100644 index 0000000..cb82648 --- /dev/null +++ b/apps/frontend/src/common/types/TextAlignType.ts @@ -0,0 +1 @@ +export type TextAlignType = 'start' | 'center' | 'end'; diff --git a/apps/frontend/src/common/types/index.ts b/apps/frontend/src/common/types/index.ts new file mode 100644 index 0000000..333177e --- /dev/null +++ b/apps/frontend/src/common/types/index.ts @@ -0,0 +1,14 @@ +export * from './AlignItemType'; +export * from './AlignSelfType'; +export * from './ButtonSizeType'; +export * from './ColorVariantType'; +export * from './ColumnSizesType'; +export * from './DisplayType'; +export * from './FloatType'; +export * from './FontSizeType'; +export * from './FontWeightType'; +export * from './JustifyContentType'; +export * from './OverflowType'; +export * from './PositionType'; +export * from './SpacingType'; +export * from './TextAlignType'; diff --git a/apps/frontend/src/common/utils/CreateTransitionToIcon.ts b/apps/frontend/src/common/utils/CreateTransitionToIcon.ts new file mode 100644 index 0000000..dbc5902 --- /dev/null +++ b/apps/frontend/src/common/utils/CreateTransitionToIcon.ts @@ -0,0 +1,14 @@ +import { NitroToolbarAnimateIconEvent } from '@nitrots/nitro-renderer'; +import { GetRoomEngine } from '../../api'; + +export const CreateTransitionToIcon = (image: HTMLImageElement, fromElement: HTMLElement, icon: string) => +{ + const bounds = fromElement.getBoundingClientRect(); + const x = (bounds.x + (bounds.width / 2)); + const y = (bounds.y + (bounds.height / 2)); + const event = new NitroToolbarAnimateIconEvent(image, x, y); + + event.iconName = icon; + + GetRoomEngine().events.dispatchEvent(event); +} diff --git a/apps/frontend/src/common/utils/FriendlyTimeView.tsx b/apps/frontend/src/common/utils/FriendlyTimeView.tsx new file mode 100644 index 0000000..95af3a3 --- /dev/null +++ b/apps/frontend/src/common/utils/FriendlyTimeView.tsx @@ -0,0 +1,28 @@ +import { FriendlyTime } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useMemo, useState } from 'react'; +import { Base, BaseProps } from '..'; + +interface FriendlyTimeViewProps extends BaseProps +{ + seconds: number; + isShort?: boolean; +} + +export const FriendlyTimeView: FC = props => +{ + const { seconds = 0, isShort = false, children = null, ...rest } = props; + const [ updateId, setUpdateId ] = useState(-1); + + const getStartSeconds = useMemo(() => (Math.round(new Date().getSeconds()) - seconds), [ seconds ]); + + useEffect(() => + { + const interval = setInterval(() => setUpdateId(prevValue => (prevValue + 1)), 10000); + + return () => clearInterval(interval); + }, []); + + const value = (Math.round(new Date().getSeconds()) - getStartSeconds); + + return { isShort ? FriendlyTime.shortFormat(value) : FriendlyTime.format(value) }; +} diff --git a/apps/frontend/src/common/utils/index.ts b/apps/frontend/src/common/utils/index.ts new file mode 100644 index 0000000..11d60a3 --- /dev/null +++ b/apps/frontend/src/common/utils/index.ts @@ -0,0 +1,2 @@ +export * from './CreateTransitionToIcon'; +export * from './FriendlyTimeView'; diff --git a/apps/frontend/src/components/achievements/AchievementsView.scss b/apps/frontend/src/components/achievements/AchievementsView.scss new file mode 100644 index 0000000..3fa61ac --- /dev/null +++ b/apps/frontend/src/components/achievements/AchievementsView.scss @@ -0,0 +1,15 @@ +.nitro-achievements { + width: $achievement-width; + height: $achievement-height; +} + +.nitro-achievements-back-arrow { + background: url('@/assets/images/achievements/back-arrow.png') no-repeat center; + width: 33px; + height: 34px; +} + +.nitro-achievements-badge-image { + width: 80px !important; + height: 80px !important; +} diff --git a/apps/frontend/src/components/achievements/AchievementsView.tsx b/apps/frontend/src/components/achievements/AchievementsView.tsx new file mode 100644 index 0000000..6c7940a --- /dev/null +++ b/apps/frontend/src/components/achievements/AchievementsView.tsx @@ -0,0 +1,72 @@ +import { ILinkEventTracker } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useState } from 'react'; +import { AchievementUtilities, AddEventLinkTracker, LocalizeText, RemoveLinkEventTracker } from '../../api'; +import { Base, Column, LayoutImage, LayoutProgressBar, NitroCardContentView, NitroCardHeaderView, NitroCardSubHeaderView, NitroCardView, Text } from '../../common'; +import { useAchievements } from '../../hooks'; +import { AchievementCategoryView } from './views/AchievementCategoryView'; +import { AchievementsCategoryListView } from './views/category-list/AchievementsCategoryListView'; + +export const AchievementsView: FC<{}> = props => +{ + const [ isVisible, setIsVisible ] = useState(false); + const { achievementCategories = [], selectedCategoryCode = null, setSelectedCategoryCode = null, achievementScore = 0, getProgress = 0, getMaxProgress = 0, selectedCategory = null } = useAchievements(); + + 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(prevValue => !prevValue); + return; + } + }, + eventUrlPrefix: 'achievements/' + }; + + AddEventLinkTracker(linkTracker); + + return () => RemoveLinkEventTracker(linkTracker); + }, []); + + if(!isVisible) return null; + + return ( + + setIsVisible(false) } /> + { selectedCategory && + + setSelectedCategoryCode(null) } className="nitro-achievements-back-arrow" /> + + { LocalizeText(`quests.${ selectedCategory.code }.name`) } + { LocalizeText('achievements.details.categoryprogress', [ 'progress', 'limit' ], [ selectedCategory.getProgress().toString(), selectedCategory.getMaxProgress().toString() ]) } + + + } + + { !selectedCategory && + <> + + + { LocalizeText('achievements.categories.score', [ 'score' ], [ achievementScore.toString() ]) } + + + } + { selectedCategory && + } + + + ); +}; diff --git a/apps/frontend/src/components/achievements/views/AchievementBadgeView.tsx b/apps/frontend/src/components/achievements/views/AchievementBadgeView.tsx new file mode 100644 index 0000000..5b0d4f9 --- /dev/null +++ b/apps/frontend/src/components/achievements/views/AchievementBadgeView.tsx @@ -0,0 +1,19 @@ +import { AchievementData } from '@nitrots/nitro-renderer'; +import { FC } from 'react'; +import { AchievementUtilities } from '../../../api'; +import { BaseProps, LayoutBadgeImageView } from '../../../common'; + +interface AchievementBadgeViewProps extends BaseProps +{ + achievement: AchievementData; + scale?: number; +} + +export const AchievementBadgeView: FC = props => +{ + const { achievement = null, scale = 1, ...rest } = props; + + if(!achievement) return null; + + return ; +} diff --git a/apps/frontend/src/components/achievements/views/AchievementCategoryView.tsx b/apps/frontend/src/components/achievements/views/AchievementCategoryView.tsx new file mode 100644 index 0000000..8774d20 --- /dev/null +++ b/apps/frontend/src/components/achievements/views/AchievementCategoryView.tsx @@ -0,0 +1,37 @@ +import { FC, useEffect } from 'react'; +import { AchievementCategory } from '../../../api'; +import { Column } from '../../../common'; +import { useAchievements } from '../../../hooks'; +import { AchievementListView } from './achievement-list'; +import { AchievementDetailsView } from './AchievementDetailsView'; + +interface AchievementCategoryViewProps +{ + category: AchievementCategory; +} + +export const AchievementCategoryView: FC = props => +{ + const { category = null } = props; + const { selectedAchievement = null, setSelectedAchievementId = null } = useAchievements(); + + useEffect(() => + { + if(!category) return; + + if(!selectedAchievement) + { + setSelectedAchievementId(category?.achievements?.[0]?.achievementId); + } + }, [ category, selectedAchievement, setSelectedAchievementId ]); + + if(!category) return null; + + return ( + + + { !!selectedAchievement && + } + + ); +} diff --git a/apps/frontend/src/components/achievements/views/AchievementDetailsView.tsx b/apps/frontend/src/components/achievements/views/AchievementDetailsView.tsx new file mode 100644 index 0000000..b04fa18 --- /dev/null +++ b/apps/frontend/src/components/achievements/views/AchievementDetailsView.tsx @@ -0,0 +1,53 @@ +import { AchievementData } from '@nitrots/nitro-renderer'; +import { FC } from 'react'; +import { AchievementUtilities, LocalizeBadgeDescription, LocalizeBadgeName, LocalizeText } from '../../../api'; +import { Column, Flex, LayoutCurrencyIcon, LayoutProgressBar, Text } from '../../../common'; +import { AchievementBadgeView } from './AchievementBadgeView'; + +interface AchievementDetailsViewProps +{ + achievement: AchievementData; +} + +export const AchievementDetailsView: FC = props => +{ + const { achievement = null } = props; + + if(!achievement) return null; + + return ( + + + + + { LocalizeText('achievements.details.level', [ 'level', 'limit' ], [ AchievementUtilities.getAchievementLevel(achievement).toString(), achievement.levelCount.toString() ]) } + + + + + + { LocalizeBadgeName(AchievementUtilities.getAchievementBadgeCode(achievement)) } + + + { LocalizeBadgeDescription(AchievementUtilities.getAchievementBadgeCode(achievement)) } + + + { ((achievement.levelRewardPoints > 0) || (achievement.scoreLimit > 0)) && + + { (achievement.levelRewardPoints > 0) && + + + { LocalizeText('achievements.details.reward') } + + + { achievement.levelRewardPoints } + + + } + { (achievement.scoreLimit > 0) && + } + } + + + ) +} diff --git a/apps/frontend/src/components/achievements/views/achievement-list/AchievementListItemView.tsx b/apps/frontend/src/components/achievements/views/achievement-list/AchievementListItemView.tsx new file mode 100644 index 0000000..9da632b --- /dev/null +++ b/apps/frontend/src/components/achievements/views/achievement-list/AchievementListItemView.tsx @@ -0,0 +1,24 @@ +import { AchievementData } from '@nitrots/nitro-renderer'; +import { FC } from 'react'; +import { LayoutGridItem } from '../../../../common'; +import { useAchievements } from '../../../../hooks'; +import { AchievementBadgeView } from '../AchievementBadgeView'; + +interface AchievementListItemViewProps +{ + achievement: AchievementData; +} + +export const AchievementListItemView: FC = props => +{ + const { achievement = null } = props; + const { selectedAchievement = null, setSelectedAchievementId = null } = useAchievements(); + + if(!achievement) return null; + + return ( + 0) } onClick={ event => setSelectedAchievementId(achievement.achievementId) }> + + + ); +} diff --git a/apps/frontend/src/components/achievements/views/achievement-list/AchievementListView.tsx b/apps/frontend/src/components/achievements/views/achievement-list/AchievementListView.tsx new file mode 100644 index 0000000..f009581 --- /dev/null +++ b/apps/frontend/src/components/achievements/views/achievement-list/AchievementListView.tsx @@ -0,0 +1,20 @@ +import { AchievementData } from '@nitrots/nitro-renderer'; +import { FC } from 'react'; +import { AutoGrid } from '../../../../common'; +import { AchievementListItemView } from './AchievementListItemView'; + +interface AchievementListViewProps +{ + achievements: AchievementData[]; +} + +export const AchievementListView: FC = props => +{ + const { achievements = null } = props; + + return ( + + { achievements && (achievements.length > 0) && achievements.map((achievement, index) => ) } + + ); +} diff --git a/apps/frontend/src/components/achievements/views/achievement-list/index.ts b/apps/frontend/src/components/achievements/views/achievement-list/index.ts new file mode 100644 index 0000000..87ccb43 --- /dev/null +++ b/apps/frontend/src/components/achievements/views/achievement-list/index.ts @@ -0,0 +1,2 @@ +export * from './AchievementListItemView'; +export * from './AchievementListView'; diff --git a/apps/frontend/src/components/achievements/views/category-list/AchievementsCategoryListItemView.tsx b/apps/frontend/src/components/achievements/views/category-list/AchievementsCategoryListItemView.tsx new file mode 100644 index 0000000..91b96c6 --- /dev/null +++ b/apps/frontend/src/components/achievements/views/category-list/AchievementsCategoryListItemView.tsx @@ -0,0 +1,31 @@ +import { Dispatch, FC, SetStateAction } from 'react'; +import { AchievementUtilities, IAchievementCategory, LocalizeText } from '../../../../api'; +import { LayoutBackgroundImage, LayoutGridItem, Text } from '../../../../common'; + +interface AchievementCategoryListItemViewProps +{ + category: IAchievementCategory; + selectedCategoryCode: string; + setSelectedCategoryCode: Dispatch>; +} + +export const AchievementsCategoryListItemView: FC = props => +{ + const { category = null, selectedCategoryCode = null, setSelectedCategoryCode = null } = props; + + if(!category) return null; + + const progress = AchievementUtilities.getAchievementCategoryProgress(category); + const maxProgress = AchievementUtilities.getAchievementCategoryMaxProgress(category); + const getCategoryImage = AchievementUtilities.getAchievementCategoryImageUrl(category, progress); + const getTotalUnseen = AchievementUtilities.getAchievementCategoryTotalUnseen(category); + + return ( + setSelectedCategoryCode(category.code) }> + { LocalizeText(`quests.${ category.code }.name`) } + + { progress } / { maxProgress } + + + ); +} diff --git a/apps/frontend/src/components/achievements/views/category-list/AchievementsCategoryListView.tsx b/apps/frontend/src/components/achievements/views/category-list/AchievementsCategoryListView.tsx new file mode 100644 index 0000000..ca36296 --- /dev/null +++ b/apps/frontend/src/components/achievements/views/category-list/AchievementsCategoryListView.tsx @@ -0,0 +1,22 @@ +import { Dispatch, FC, SetStateAction } from 'react'; +import { IAchievementCategory } from '../../../../api'; +import { AutoGrid } from '../../../../common'; +import { AchievementsCategoryListItemView } from './AchievementsCategoryListItemView'; + +interface AchievementsCategoryListViewProps +{ + categories: IAchievementCategory[]; + selectedCategoryCode: string; + setSelectedCategoryCode: Dispatch>; +} + +export const AchievementsCategoryListView: FC = props => +{ + const { categories = null, selectedCategoryCode = null, setSelectedCategoryCode = null } = props; + + return ( + + { categories && (categories.length > 0) && categories.map((category, index) => ) } + + ); +}; diff --git a/apps/frontend/src/components/achievements/views/category-list/index.ts b/apps/frontend/src/components/achievements/views/category-list/index.ts new file mode 100644 index 0000000..5a367f8 --- /dev/null +++ b/apps/frontend/src/components/achievements/views/category-list/index.ts @@ -0,0 +1,2 @@ +export * from './AchievementsCategoryListItemView'; +export * from './AchievementsCategoryListView'; diff --git a/apps/frontend/src/components/achievements/views/index.ts b/apps/frontend/src/components/achievements/views/index.ts new file mode 100644 index 0000000..576c635 --- /dev/null +++ b/apps/frontend/src/components/achievements/views/index.ts @@ -0,0 +1,5 @@ +export * from './achievement-list'; +export * from './AchievementBadgeView'; +export * from './AchievementCategoryView'; +export * from './AchievementDetailsView'; +export * from './category-list'; diff --git a/apps/frontend/src/components/avatar-editor/AvatarEditorView.scss b/apps/frontend/src/components/avatar-editor/AvatarEditorView.scss new file mode 100644 index 0000000..22b873d --- /dev/null +++ b/apps/frontend/src/components/avatar-editor/AvatarEditorView.scss @@ -0,0 +1,336 @@ +.nitro-avatar-editor-spritesheet { + background: url('@/assets/images/avatareditor/avatar-editor-spritesheet.png') transparent no-repeat; + + &.arrow-left-icon { + width: 28px; + height: 21px; + background-position: -226px -131px; + } + + &.arrow-right-icon { + width: 28px; + height: 21px; + background-position: -226px -162px; + } + + &.ca-icon { + width: 25px; + height: 25px; + background-position: -226px -61px; + + &.selected { + width: 25px; + height: 25px; + background-position: -226px -96px; + } + } + + &.cc-icon { + width: 31px; + height: 29px; + background-position: -145px -5px; + + &.selected { + width: 31px; + height: 29px; + background-position: -145px -44px; + } + } + + &.ch-icon { + width: 29px; + height: 24px; + background-position: -186px -39px; + + &.selected { + width: 29px; + height: 24px; + background-position: -186px -73px; + } + } + + &.clear-icon { + width: 27px; + height: 27px; + background-position: -145px -157px; + } + + &.cp-icon { + width: 30px; + height: 24px; + background-position: -145px -264px; + + &.selected { + width: 30px; + height: 24px; + background-position: -186px -5px; + } + } + + + &.ea-icon { + width: 35px; + height: 16px; + background-position: -226px -193px; + + &.selected { + width: 35px; + height: 16px; + background-position: -226px -219px; + } + } + + &.fa-icon { + width: 27px; + height: 20px; + background-position: -186px -137px; + + &.selected { + width: 27px; + height: 20px; + background-position: -186px -107px; + } + } + + &.female-icon { + width: 18px; + height: 27px; + background-position: -186px -202px; + + &.selected { + width: 18px; + height: 27px; + background-position: -186px -239px; + } + } + + &.ha-icon { + width: 25px; + height: 22px; + background-position: -226px -245px; + + &.selected { + width: 25px; + height: 22px; + background-position: -226px -277px; + } + } + + &.he-icon { + width: 31px; + height: 27px; + background-position: -145px -83px; + + &.selected { + width: 31px; + height: 27px; + background-position: -145px -120px; + } + } + + &.hr-icon { + width: 29px; + height: 25px; + background-position: -145px -194px; + + &.selected { + width: 29px; + height: 25px; + background-position: -145px -229px; + } + } + + &.lg-icon { + width: 19px; + height: 20px; + background-position: -303px -45px; + + &.selected { + width: 19px; + height: 20px; + background-position: -303px -75px; + } + } + + &.loading-icon { + width: 21px; + height: 25px; + background-position: -186px -167px; + } + + + &.male-icon { + width: 21px; + height: 21px; + background-position: -186px -276px; + + &.selected { + width: 21px; + height: 21px; + background-position: -272px -5px; + } + } + + + &.sellable-icon { + width: 17px; + height: 15px; + background-position: -303px -105px; + } + + + &.sh-icon { + width: 37px; + height: 10px; + background-position: -303px -5px; + + &.selected { + width: 37px; + height: 10px; + background-position: -303px -25px; + } + } + + + &.spotlight-icon { + width: 130px; + height: 305px; + background-position: -5px -5px; + } + + + &.wa-icon { + width: 36px; + height: 18px; + background-position: -226px -5px; + + &.selected { + width: 36px; + height: 18px; + background-position: -226px -33px; + } + } +} + +.nitro-avatar-editor-wardrobe-figure-preview { + background-color: $pale-sky; + overflow: hidden; + z-index: 1; + + .avatar-image { + position: absolute; + bottom: -15px; + margin: 0 auto; + z-index: 4; + } + + .avatar-shadow { + position: absolute; + left: 0; + right: 0; + bottom: 25px; + width: 40px; + height: 20px; + margin: 0 auto; + border-radius: 100%; + background-color: rgba(0, 0, 0, 0.20); + z-index: 2; + } + + &:after { + position: absolute; + content: ''; + top: 75%; + bottom: 0; + left: 0; + right: 0; + border-radius: 50%; + background-color: $pale-sky; + box-shadow: 0 0 8px 2px rgba($white,.6); + transform: scale(2); + } + + .button-container { + position: absolute; + bottom: 0; + z-index: 5; + } +} + +.nitro-avatar-editor { + width: $avatar-editor-width; + height: $avatar-editor-height; + + .category-item { + height: 40px; + } + + .figure-preview-container { + position: relative; + height: 100%; + background-color: $pale-sky; + overflow: hidden; + z-index: 1; + + .arrow-container { + position: absolute; + width: 100%; + margin: 0 auto; + padding: 0 10px; + display: flex; + justify-content: space-between; + bottom: 12px; + z-index: 5; + + .icon { + cursor: pointer; + } + } + + .avatar-image { + position: absolute; + left: 0; + right: 0; + bottom: 50px; + margin: 0 auto; + z-index: 4; + } + + .avatar-spotlight { + position: absolute; + top: -10px; + left: 0; + right: 0; + margin: 0 auto; + opacity: 0.3; + pointer-events: none; + z-index: 3; + } + + .avatar-shadow { + position: absolute; + left: 0; + right: 0; + bottom: 15px; + width: 70px; + height: 30px; + margin: 0 auto; + border-radius: 100%; + background-color: rgba(0, 0, 0, 0.20); + z-index: 2; + } + + &:after { + position: absolute; + content: ''; + top: 75%; + bottom: 0; + left: 0; + right: 0; + border-radius: 50%; + background-color: $pale-sky; + box-shadow: 0 0 8px 2px rgba($white,.6); + transform: scale(2); + } + } +} diff --git a/apps/frontend/src/components/avatar-editor/AvatarEditorView.tsx b/apps/frontend/src/components/avatar-editor/AvatarEditorView.tsx new file mode 100644 index 0000000..6b3766e --- /dev/null +++ b/apps/frontend/src/components/avatar-editor/AvatarEditorView.tsx @@ -0,0 +1,316 @@ +import { AvatarEditorFigureCategory, FigureSetIdsMessageEvent, GetWardrobeMessageComposer, IAvatarFigureContainer, ILinkEventTracker, UserFigureComposer, UserWardrobePageEvent } from '@nitrots/nitro-renderer'; +import { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import { FaDice, FaTrash, FaUndo } from 'react-icons/fa'; +import { AddEventLinkTracker, AvatarEditorAction, AvatarEditorUtilities, BodyModel, FigureData, generateRandomFigure, GetAvatarRenderManager, GetClubMemberLevel, GetConfiguration, GetSessionDataManager, HeadModel, IAvatarEditorCategoryModel, LegModel, LocalizeText, RemoveLinkEventTracker, SendMessageComposer, TorsoModel } from '../../api'; +import { Button, ButtonGroup, Column, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common'; +import { useMessageEvent } from '../../hooks'; +import { AvatarEditorFigurePreviewView } from './views/AvatarEditorFigurePreviewView'; +import { AvatarEditorModelView } from './views/AvatarEditorModelView'; +import { AvatarEditorWardrobeView } from './views/AvatarEditorWardrobeView'; + +const DEFAULT_MALE_FIGURE: string = 'hr-100.hd-180-7.ch-215-66.lg-270-79.sh-305-62.ha-1002-70.wa-2007'; +const DEFAULT_FEMALE_FIGURE: string = 'hr-515-33.hd-600-1.ch-635-70.lg-716-66-62.sh-735-68'; + +export const AvatarEditorView: FC<{}> = props => +{ + const [ isVisible, setIsVisible ] = useState(false); + const [ figures, setFigures ] = useState>(null); + const [ figureData, setFigureData ] = useState(null); + const [ categories, setCategories ] = useState>(null); + const [ activeCategory, setActiveCategory ] = useState(null); + const [ figureSetIds, setFigureSetIds ] = useState([]); + const [ boundFurnitureNames, setBoundFurnitureNames ] = useState([]); + const [ savedFigures, setSavedFigures ] = useState<[ IAvatarFigureContainer, string ][]>([]); + const [ isWardrobeVisible, setIsWardrobeVisible ] = useState(false); + const [ lastFigure, setLastFigure ] = useState(null); + const [ lastGender, setLastGender ] = useState(null); + const [ needsReset, setNeedsReset ] = useState(true); + const [ isInitalized, setIsInitalized ] = useState(false); + + const maxWardrobeSlots = useMemo(() => GetConfiguration('avatar.wardrobe.max.slots', 10), []); + + useMessageEvent(FigureSetIdsMessageEvent, event => + { + const parser = event.getParser(); + + setFigureSetIds(parser.figureSetIds); + setBoundFurnitureNames(parser.boundsFurnitureNames); + }); + + useMessageEvent(UserWardrobePageEvent, event => + { + const parser = event.getParser(); + const savedFigures: [ IAvatarFigureContainer, string ][] = []; + + let i = 0; + + while(i < maxWardrobeSlots) + { + savedFigures.push([ null, null ]); + + i++; + } + + for(let [ index, [ look, gender ] ] of parser.looks.entries()) + { + const container = GetAvatarRenderManager().createFigureContainer(look); + + savedFigures[(index - 1)] = [ container, gender ]; + } + + setSavedFigures(savedFigures); + }); + + const selectCategory = useCallback((name: string) => + { + if(!categories) return; + + setActiveCategory(categories.get(name)); + }, [ categories ]); + + const resetCategories = useCallback(() => + { + const categories = new Map(); + + categories.set(AvatarEditorFigureCategory.GENERIC, new BodyModel()); + categories.set(AvatarEditorFigureCategory.HEAD, new HeadModel()); + categories.set(AvatarEditorFigureCategory.TORSO, new TorsoModel()); + categories.set(AvatarEditorFigureCategory.LEGS, new LegModel()); + + setCategories(categories); + }, []); + + const setupFigures = useCallback(() => + { + const figures: Map = new Map(); + + const maleFigure = new FigureData(); + const femaleFigure = new FigureData(); + + maleFigure.loadAvatarData(DEFAULT_MALE_FIGURE, FigureData.MALE); + femaleFigure.loadAvatarData(DEFAULT_FEMALE_FIGURE, FigureData.FEMALE); + + figures.set(FigureData.MALE, maleFigure); + figures.set(FigureData.FEMALE, femaleFigure); + + setFigures(figures); + setFigureData(figures.get(FigureData.MALE)); + }, []); + + const loadAvatarInEditor = useCallback((figure: string, gender: string, reset: boolean = true) => + { + gender = AvatarEditorUtilities.getGender(gender); + + let newFigureData = figureData; + + if(gender !== newFigureData.gender) newFigureData = figures.get(gender); + + if(figure !== newFigureData.getFigureString()) newFigureData.loadAvatarData(figure, gender); + + if(newFigureData !== figureData) setFigureData(newFigureData); + + if(reset) + { + setLastFigure(figureData.getFigureString()); + setLastGender(figureData.gender); + } + }, [ figures, figureData ]); + + const processAction = useCallback((action: string) => + { + switch(action) + { + case AvatarEditorAction.ACTION_CLEAR: + loadAvatarInEditor(figureData.getFigureStringWithFace(0, false), figureData.gender, false); + resetCategories(); + return; + case AvatarEditorAction.ACTION_RESET: + loadAvatarInEditor(lastFigure, lastGender); + resetCategories(); + return; + case AvatarEditorAction.ACTION_RANDOMIZE: + const figure = generateRandomFigure(figureData, figureData.gender, GetClubMemberLevel(), figureSetIds, [ FigureData.FACE ]); + + loadAvatarInEditor(figure, figureData.gender, false); + resetCategories(); + return; + case AvatarEditorAction.ACTION_SAVE: + SendMessageComposer(new UserFigureComposer(figureData.gender, figureData.getFigureString())); + setIsVisible(false); + return; + } + }, [ figureData, lastFigure, lastGender, figureSetIds, loadAvatarInEditor, resetCategories ]) + + const setGender = useCallback((gender: string) => + { + gender = AvatarEditorUtilities.getGender(gender); + + setFigureData(figures.get(gender)); + }, [ figures ]); + + 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(prevValue => !prevValue); + return; + } + }, + eventUrlPrefix: 'avatar-editor/' + }; + + AddEventLinkTracker(linkTracker); + + return () => RemoveLinkEventTracker(linkTracker); + }, []); + + useEffect(() => + { + setSavedFigures(new Array(maxWardrobeSlots)); + }, [ maxWardrobeSlots ]); + + useEffect(() => + { + if(!isWardrobeVisible) return; + + setActiveCategory(null); + SendMessageComposer(new GetWardrobeMessageComposer()); + }, [ isWardrobeVisible ]); + + useEffect(() => + { + if(!activeCategory) return; + + setIsWardrobeVisible(false); + }, [ activeCategory ]); + + useEffect(() => + { + if(!categories) return; + + selectCategory(AvatarEditorFigureCategory.GENERIC); + }, [ categories, selectCategory ]); + + useEffect(() => + { + if(!figureData) return; + + AvatarEditorUtilities.CURRENT_FIGURE = figureData; + + resetCategories(); + + return () => AvatarEditorUtilities.CURRENT_FIGURE = null; + }, [ figureData, resetCategories ]); + + useEffect(() => + { + AvatarEditorUtilities.FIGURE_SET_IDS = figureSetIds; + AvatarEditorUtilities.BOUND_FURNITURE_NAMES = boundFurnitureNames; + + resetCategories(); + + return () => + { + AvatarEditorUtilities.FIGURE_SET_IDS = null; + AvatarEditorUtilities.BOUND_FURNITURE_NAMES = null; + } + }, [ figureSetIds, boundFurnitureNames, resetCategories ]); + + useEffect(() => + { + if(!isVisible) return; + + if(!figures) + { + setupFigures(); + + setIsInitalized(true); + + return; + } + }, [ isVisible, figures, setupFigures ]); + + useEffect(() => + { + if(!isVisible || !isInitalized || !needsReset) return; + + loadAvatarInEditor(GetSessionDataManager().figure, GetSessionDataManager().gender); + setNeedsReset(false); + }, [ isVisible, isInitalized, needsReset, loadAvatarInEditor ]); + + useEffect(() => + { + if(isVisible) return; + + return () => + { + setNeedsReset(true); + } + }, [ isVisible ]); + + if(!isVisible || !figureData) return null; + + return ( + + setIsVisible(false) } /> + + { categories && (categories.size > 0) && Array.from(categories.keys()).map(category => + { + const isActive = (activeCategory && (activeCategory.name === category)); + + return ( + selectCategory(category) }> + { LocalizeText(`avatareditor.category.${ category }`) } + + ); + }) } + setIsWardrobeVisible(true) }> + { LocalizeText('avatareditor.category.wardrobe') } + + + + + + { (activeCategory && !isWardrobeVisible) && + } + { isWardrobeVisible && + } + + + + + + + + + + + + + + + + ); +} diff --git a/apps/frontend/src/components/avatar-editor/views/AvatarEditorFigurePreviewView.tsx b/apps/frontend/src/components/avatar-editor/views/AvatarEditorFigurePreviewView.tsx new file mode 100644 index 0000000..d5715ac --- /dev/null +++ b/apps/frontend/src/components/avatar-editor/views/AvatarEditorFigurePreviewView.tsx @@ -0,0 +1,55 @@ +import { AvatarDirectionAngle } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useState } from 'react'; +import { FigureData } from '../../../api'; +import { Base, Column, LayoutAvatarImageView } from '../../../common'; +import { AvatarEditorIcon } from './AvatarEditorIcon'; + +export interface AvatarEditorFigurePreviewViewProps +{ + figureData: FigureData; +} + +export const AvatarEditorFigurePreviewView: FC = props => +{ + const { figureData = null } = props; + const [ updateId, setUpdateId ] = useState(-1); + + const rotateFigure = (direction: number) => + { + if(direction < AvatarDirectionAngle.MIN_DIRECTION) + { + direction = (AvatarDirectionAngle.MAX_DIRECTION + (direction + 1)); + } + + if(direction > AvatarDirectionAngle.MAX_DIRECTION) + { + direction = (direction - (AvatarDirectionAngle.MAX_DIRECTION + 1)); + } + + figureData.direction = direction; + } + + useEffect(() => + { + if(!figureData) return; + + figureData.notify = () => setUpdateId(prevValue => (prevValue + 1)); + + return () => + { + figureData.notify = null; + } + }, [ figureData ] ); + + return ( + + + + + + rotateFigure(figureData.direction + 1) } /> + rotateFigure(figureData.direction - 1) } /> + + + ); +} diff --git a/apps/frontend/src/components/avatar-editor/views/AvatarEditorIcon.tsx b/apps/frontend/src/components/avatar-editor/views/AvatarEditorIcon.tsx new file mode 100644 index 0000000..a05baa1 --- /dev/null +++ b/apps/frontend/src/components/avatar-editor/views/AvatarEditorIcon.tsx @@ -0,0 +1,30 @@ +import { FC, useMemo } from 'react'; +import { Base, BaseProps } from '../../../common'; + +type AvatarIconType = 'male' | 'female' | 'clear' | 'sellable' | string; + +export interface AvatarEditorIconProps extends BaseProps +{ + icon: AvatarIconType; + selected?: boolean; +} + +export const AvatarEditorIcon: FC = props => +{ + const { icon = null, selected = false, classNames = [], children = null, ...rest } = props; + + const getClassNames = useMemo(() => + { + const newClassNames: string[] = [ 'nitro-avatar-editor-spritesheet' ]; + + if(icon && icon.length) newClassNames.push(icon + '-icon'); + + if(selected) newClassNames.push('selected'); + + if(classNames.length) newClassNames.push(...classNames); + + return newClassNames; + }, [ icon, selected, classNames ]); + + return +} diff --git a/apps/frontend/src/components/avatar-editor/views/AvatarEditorModelView.tsx b/apps/frontend/src/components/avatar-editor/views/AvatarEditorModelView.tsx new file mode 100644 index 0000000..6eb8fe3 --- /dev/null +++ b/apps/frontend/src/components/avatar-editor/views/AvatarEditorModelView.tsx @@ -0,0 +1,88 @@ +import { Dispatch, FC, SetStateAction, useCallback, useEffect, useState } from 'react'; +import { CategoryData, FigureData, IAvatarEditorCategoryModel } from '../../../api'; +import { Column, Flex, Grid } from '../../../common'; +import { AvatarEditorIcon } from './AvatarEditorIcon'; +import { AvatarEditorFigureSetView } from './figure-set/AvatarEditorFigureSetView'; +import { AvatarEditorPaletteSetView } from './palette-set/AvatarEditorPaletteSetView'; +export interface AvatarEditorModelViewProps +{ + model: IAvatarEditorCategoryModel; + gender: string; + setGender: Dispatch>; +} + +export const AvatarEditorModelView: FC = props => +{ + const { model = null, gender = null, setGender = null } = props; + const [ activeCategory, setActiveCategory ] = useState(null); + const [ maxPaletteCount, setMaxPaletteCount ] = useState(1); + + const selectCategory = useCallback((name: string) => + { + const category = model.categories.get(name); + + if(!category) return; + + category.init(); + + setActiveCategory(category); + + for(const part of category.parts) + { + if(!part || !part.isSelected) continue; + + setMaxPaletteCount(part.maxColorIndex || 1); + + break; + } + }, [ model ]); + + useEffect(() => + { + model.init(); + + for(const name of model.categories.keys()) + { + selectCategory(name); + + break; + } + }, [ model, selectCategory ]); + + if(!model || !activeCategory) return null; + + return ( + + + { model.canSetGender && + <> + setGender(FigureData.MALE) }> + + + setGender(FigureData.FEMALE) }> + + + } + { !model.canSetGender && model.categories && (model.categories.size > 0) && Array.from(model.categories.keys()).map(name => + { + const category = model.categories.get(name); + + return ( + selectCategory(name) }> + + + ); + }) } + + + + + + { (maxPaletteCount >= 1) && + } + { (maxPaletteCount === 2) && + } + + + ); +} diff --git a/apps/frontend/src/components/avatar-editor/views/AvatarEditorWardrobeView.tsx b/apps/frontend/src/components/avatar-editor/views/AvatarEditorWardrobeView.tsx new file mode 100644 index 0000000..9811ab8 --- /dev/null +++ b/apps/frontend/src/components/avatar-editor/views/AvatarEditorWardrobeView.tsx @@ -0,0 +1,79 @@ +import { IAvatarFigureContainer, SaveWardrobeOutfitMessageComposer } from '@nitrots/nitro-renderer'; +import { Dispatch, FC, SetStateAction, useCallback, useMemo } from 'react'; +import { FigureData, GetAvatarRenderManager, GetClubMemberLevel, GetConfiguration, LocalizeText, SendMessageComposer } from '../../../api'; +import { AutoGrid, Base, Button, Flex, LayoutAvatarImageView, LayoutCurrencyIcon, LayoutGridItem } from '../../../common'; + +export interface AvatarEditorWardrobeViewProps +{ + figureData: FigureData; + savedFigures: [ IAvatarFigureContainer, string ][]; + setSavedFigures: Dispatch>; + loadAvatarInEditor: (figure: string, gender: string, reset?: boolean) => void; +} + +export const AvatarEditorWardrobeView: FC = props => +{ + const { figureData = null, savedFigures = [], setSavedFigures = null, loadAvatarInEditor = null } = props; + + const hcDisabled = GetConfiguration('hc.disabled', false); + + const wearFigureAtIndex = useCallback((index: number) => + { + if((index >= savedFigures.length) || (index < 0)) return; + + const [ figure, gender ] = savedFigures[index]; + + loadAvatarInEditor(figure.getFigureString(), gender); + }, [ savedFigures, loadAvatarInEditor ]); + + const saveFigureAtWardrobeIndex = useCallback((index: number) => + { + if(!figureData || (index >= savedFigures.length) || (index < 0)) return; + + const newFigures = [ ...savedFigures ]; + + const figure = figureData.getFigureString(); + const gender = figureData.gender; + + newFigures[index] = [ GetAvatarRenderManager().createFigureContainer(figure), gender ]; + + setSavedFigures(newFigures); + SendMessageComposer(new SaveWardrobeOutfitMessageComposer((index + 1), figure, gender)); + }, [ figureData, savedFigures, setSavedFigures ]); + + const figures = useMemo(() => + { + if(!savedFigures || !savedFigures.length) return []; + + const items: JSX.Element[] = []; + + savedFigures.forEach(([ figureContainer, gender ], index) => + { + let clubLevel = 0; + + if(figureContainer) clubLevel = GetAvatarRenderManager().getFigureClubLevel(figureContainer, gender); + + items.push( + + { figureContainer && + } + + { !hcDisabled && (clubLevel > 0) && } + + + { figureContainer && + } + + + ); + }); + + return items; + }, [ savedFigures, hcDisabled, saveFigureAtWardrobeIndex, wearFigureAtIndex ]); + + return ( + + { figures } + + ); +} diff --git a/apps/frontend/src/components/avatar-editor/views/figure-set/AvatarEditorFigureSetItemView.tsx b/apps/frontend/src/components/avatar-editor/views/figure-set/AvatarEditorFigureSetItemView.tsx new file mode 100644 index 0000000..fd28dc5 --- /dev/null +++ b/apps/frontend/src/components/avatar-editor/views/figure-set/AvatarEditorFigureSetItemView.tsx @@ -0,0 +1,35 @@ +import { FC, useEffect, useState } from 'react'; +import { AvatarEditorGridPartItem, GetConfiguration } from '../../../../api'; +import { LayoutCurrencyIcon, LayoutGridItem, LayoutGridItemProps } from '../../../../common'; +import { AvatarEditorIcon } from '../AvatarEditorIcon'; + +export interface AvatarEditorFigureSetItemViewProps extends LayoutGridItemProps +{ + partItem: AvatarEditorGridPartItem; +} + +export const AvatarEditorFigureSetItemView: FC = props => +{ + const { partItem = null, children = null, ...rest } = props; + const [ updateId, setUpdateId ] = useState(-1); + + const hcDisabled = GetConfiguration('hc.disabled', false); + + useEffect(() => + { + const rerender = () => setUpdateId(prevValue => (prevValue + 1)); + + partItem.notify = rerender; + + return () => partItem.notify = null; + }, [ partItem ]); + + return ( + + { !hcDisabled && partItem.isHC && } + { partItem.isClear && } + { partItem.isSellable && } + { children } + + ); +} diff --git a/apps/frontend/src/components/avatar-editor/views/figure-set/AvatarEditorFigureSetView.tsx b/apps/frontend/src/components/avatar-editor/views/figure-set/AvatarEditorFigureSetView.tsx new file mode 100644 index 0000000..3755731 --- /dev/null +++ b/apps/frontend/src/components/avatar-editor/views/figure-set/AvatarEditorFigureSetView.tsx @@ -0,0 +1,44 @@ +import { Dispatch, FC, SetStateAction, useCallback, useEffect, useRef } from 'react'; +import { AvatarEditorGridPartItem, CategoryData, IAvatarEditorCategoryModel } from '../../../../api'; +import { AutoGrid } from '../../../../common'; +import { AvatarEditorFigureSetItemView } from './AvatarEditorFigureSetItemView'; + +export interface AvatarEditorFigureSetViewProps +{ + model: IAvatarEditorCategoryModel; + category: CategoryData; + setMaxPaletteCount: Dispatch>; +} + +export const AvatarEditorFigureSetView: FC = props => +{ + const { model = null, category = null, setMaxPaletteCount = null } = props; + const elementRef = useRef(null); + + const selectPart = useCallback((item: AvatarEditorGridPartItem) => + { + const index = category.parts.indexOf(item); + + if(index === -1) return; + + model.selectPart(category.name, index); + + const partItem = category.getCurrentPart(); + + setMaxPaletteCount(partItem.maxColorIndex || 1); + }, [ model, category, setMaxPaletteCount ]); + + useEffect(() => + { + if(!model || !category || !elementRef || !elementRef.current) return; + + elementRef.current.scrollTop = 0; + }, [ model, category ]); + + return ( + + { (category.parts.length > 0) && category.parts.map((item, index) => + selectPart(item) } />) } + + ); +} diff --git a/apps/frontend/src/components/avatar-editor/views/palette-set/AvatarEditorPaletteSetItemView.tsx b/apps/frontend/src/components/avatar-editor/views/palette-set/AvatarEditorPaletteSetItemView.tsx new file mode 100644 index 0000000..638a9d1 --- /dev/null +++ b/apps/frontend/src/components/avatar-editor/views/palette-set/AvatarEditorPaletteSetItemView.tsx @@ -0,0 +1,32 @@ +import { FC, useEffect, useState } from 'react'; +import { AvatarEditorGridColorItem, GetConfiguration } from '../../../../api'; +import { LayoutCurrencyIcon, LayoutGridItem, LayoutGridItemProps } from '../../../../common'; + +export interface AvatarEditorPaletteSetItemProps extends LayoutGridItemProps +{ + colorItem: AvatarEditorGridColorItem; +} + +export const AvatarEditorPaletteSetItem: FC = props => +{ + const { colorItem = null, children = null, ...rest } = props; + const [ updateId, setUpdateId ] = useState(-1); + + const hcDisabled = GetConfiguration('hc.disabled', false); + + useEffect(() => + { + const rerender = () => setUpdateId(prevValue => (prevValue + 1)); + + colorItem.notify = rerender; + + return () => colorItem.notify = null; + }, [ colorItem ]); + + return ( + + { !hcDisabled && colorItem.isHC && } + { children } + + ); +} diff --git a/apps/frontend/src/components/avatar-editor/views/palette-set/AvatarEditorPaletteSetView.tsx b/apps/frontend/src/components/avatar-editor/views/palette-set/AvatarEditorPaletteSetView.tsx new file mode 100644 index 0000000..c55dcb4 --- /dev/null +++ b/apps/frontend/src/components/avatar-editor/views/palette-set/AvatarEditorPaletteSetView.tsx @@ -0,0 +1,41 @@ +import { FC, useCallback, useEffect, useRef } from 'react'; +import { AvatarEditorGridColorItem, CategoryData, IAvatarEditorCategoryModel } from '../../../../api'; +import { AutoGrid } from '../../../../common'; +import { AvatarEditorPaletteSetItem } from './AvatarEditorPaletteSetItemView'; + +export interface AvatarEditorPaletteSetViewProps +{ + model: IAvatarEditorCategoryModel; + category: CategoryData; + paletteSet: AvatarEditorGridColorItem[]; + paletteIndex: number; +} + +export const AvatarEditorPaletteSetView: FC = props => +{ + const { model = null, category = null, paletteSet = [], paletteIndex = -1 } = props; + const elementRef = useRef(null); + + const selectColor = useCallback((item: AvatarEditorGridColorItem) => + { + const index = paletteSet.indexOf(item); + + if(index === -1) return; + + model.selectColor(category.name, index, paletteIndex); + }, [ model, category, paletteSet, paletteIndex ]); + + useEffect(() => + { + if(!model || !category || !elementRef || !elementRef.current) return; + + elementRef.current.scrollTop = 0; + }, [ model, category ]); + + return ( + + { (paletteSet.length > 0) && paletteSet.map((item, index) => + selectColor(item) } />) } + + ); +} diff --git a/apps/frontend/src/components/camera/CameraWidgetView.scss b/apps/frontend/src/components/camera/CameraWidgetView.scss new file mode 100644 index 0000000..aecd0b1 --- /dev/null +++ b/apps/frontend/src/components/camera/CameraWidgetView.scss @@ -0,0 +1,133 @@ +.nitro-camera-capture { + position: relative; + + .header-close { + top: 8px; + right: 8px; + border-radius: $border-radius; + box-shadow: 0 0 0 1.5px $white; + border: 2px solid #921911; + background: repeating-linear-gradient( + rgba(245, 80, 65, 1), + rgba(245, 80, 65, 1) 50%, + rgba(194, 48, 39, 1) 50%, + rgba(194, 48, 39, 1) 100% + ); + cursor: pointer; + line-height: 1; + padding: 1px 3px; + + &:hover { + filter: brightness(1.2); + } + + &:active { + filter: brightness(0.8); + } + } + + .camera-area { + position: absolute; + top: 37px; + left: 10px; + width: 320px; + height: 320px; + } + + .camera-canvas { + position: relative; + width: 340px; + height: 462px; + background-image: url('@/assets/images/room-widgets/camera-widget/camera-spritesheet.png'); + background-position: -1px -1px; + z-index: 2; + + .camera-button { + width: 94px; + height: 94px; + cursor: pointer; + margin-top: 362px; + + background-image: url('@/assets/images/room-widgets/camera-widget/camera-spritesheet.png'); + background-position: -343px -321px; + + &:hover { + background-position: -535px -321px; + } + + &:active { + background-position: -439px -321px; + } + } + + .camera-view-finder { + background-image: url('@/assets/images/room-widgets/camera-widget/camera-spritesheet.png'); + background-position: -343px -1px; + } + + .camera-frame { + .camera-frame-preview-actions { + background: rgba(0, 0, 0, 0.5); + } + } + } + + .camera-roll { + width: 330px; + background: #bab8b4; + border-bottom-left-radius: 10px; + border-bottom-right-radius: 10px; + border: 1px solid black; + box-shadow: inset 1px 0px white, inset -1px -1px white; + + img { + width: 56px; + height: 56px; + border: 1px solid black; + object-fit: contain; + image-rendering: initial; + } + } +} + +.nitro-camera-editor { + width: $camera-editor-width; + height: $camera-editor-height; + + .picture-preview { + width: 320px; + height: 320px; + } + + .layout-grid-item { + height: 60px !important; + max-height: 60px !important; + } + + .effect-thumbnail-image { + img { + width: 50px; + height: 50px; + image-rendering: auto; + object-fit: contain; + } + } + + .remove-effect { + position: absolute; + top: 1px; + right: 1px; + padding: 2px; + font-size: 10px; + min-height: unset; + } +} + +.nitro-camera-checkout { + width: $camera-checkout-width; + + .picture-preview { + width: 320px; + height: 320px; + } +} diff --git a/apps/frontend/src/components/camera/CameraWidgetView.tsx b/apps/frontend/src/components/camera/CameraWidgetView.tsx new file mode 100644 index 0000000..00ab0a8 --- /dev/null +++ b/apps/frontend/src/components/camera/CameraWidgetView.tsx @@ -0,0 +1,97 @@ +import { ILinkEventTracker, RoomSessionEvent } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useState } from 'react'; +import { AddEventLinkTracker, RemoveLinkEventTracker } from '../../api'; +import { useCamera, useRoomSessionManagerEvent } from '../../hooks'; +import { CameraWidgetCaptureView } from './views/CameraWidgetCaptureView'; +import { CameraWidgetCheckoutView } from './views/CameraWidgetCheckoutView'; +import { CameraWidgetEditorView } from './views/editor/CameraWidgetEditorView'; + +const MODE_NONE: number = 0; +const MODE_CAPTURE: number = 1; +const MODE_EDITOR: number = 2; +const MODE_CHECKOUT: number = 3; + +export const CameraWidgetView: FC<{}> = props => +{ + const [ mode, setMode ] = useState(MODE_NONE); + const [ base64Url, setSavedPictureUrl ] = useState(null); + const { availableEffects = [], selectedPictureIndex = -1, cameraRoll = [], setCameraRoll = null, myLevel = 0, price = { credits: 0, duckets: 0, publishDucketPrice: 0 }} = useCamera(); + + const processAction = (type: string) => + { + switch(type) + { + case 'close': + setMode(MODE_NONE); + return; + case 'edit': + setMode(MODE_EDITOR); + return; + case 'delete': + setCameraRoll(prevValue => + { + const clone = [ ...prevValue ]; + + clone.splice(selectedPictureIndex, 1); + + return clone; + }); + return; + case 'editor_cancel': + setMode(MODE_CAPTURE); + return; + } + } + + const checkoutPictureUrl = (pictureUrl: string) => + { + setSavedPictureUrl(pictureUrl); + setMode(MODE_CHECKOUT); + } + + useRoomSessionManagerEvent(RoomSessionEvent.ENDED, event => setMode(MODE_NONE)); + + useEffect(() => + { + const linkTracker: ILinkEventTracker = { + linkReceived: (url: string) => + { + const parts = url.split('/'); + + if(parts.length < 2) return; + + switch(parts[1]) + { + case 'show': + setMode(MODE_CAPTURE); + return; + case 'hide': + setMode(MODE_NONE); + return; + case 'toggle': + setMode(prevValue => + { + if(!prevValue) return MODE_CAPTURE; + else return MODE_NONE; + }); + return; + } + }, + eventUrlPrefix: 'camera/' + }; + + AddEventLinkTracker(linkTracker); + + return () => RemoveLinkEventTracker(linkTracker); + }, []); + + if(mode === MODE_NONE) return null; + + return ( + <> + { (mode === MODE_CAPTURE) && processAction('close') } onEdit={ () => processAction('edit') } onDelete={ () => processAction('delete') } /> } + { (mode === MODE_EDITOR) && processAction('close') } onCancel={ () => processAction('editor_cancel') } onCheckout={ checkoutPictureUrl } availableEffects={ availableEffects } /> } + { (mode === MODE_CHECKOUT) && processAction('close') } onCancelClick={ () => processAction('editor_cancel') } price={ price }> } + + ); +} diff --git a/apps/frontend/src/components/camera/views/CameraWidgetCaptureView.tsx b/apps/frontend/src/components/camera/views/CameraWidgetCaptureView.tsx new file mode 100644 index 0000000..308bb83 --- /dev/null +++ b/apps/frontend/src/components/camera/views/CameraWidgetCaptureView.tsx @@ -0,0 +1,90 @@ +import { NitroRectangle, TextureUtils } from '@nitrots/nitro-renderer'; +import { FC, useRef } from 'react'; +import { FaTimes } from 'react-icons/fa'; +import { CameraPicture, GetRoomEngine, GetRoomSession, LocalizeText, PlaySound, SoundNames } from '../../../api'; +import { Column, DraggableWindow, Flex } from '../../../common'; +import { useCamera, useNotification } from '../../../hooks'; + +export interface CameraWidgetCaptureViewProps +{ + onClose: () => void; + onEdit: () => void; + onDelete: () => void; +} + +const CAMERA_ROLL_LIMIT: number = 5; + +export const CameraWidgetCaptureView: FC = props => +{ + const { onClose = null, onEdit = null, onDelete = null } = props; + const { cameraRoll = null, setCameraRoll = null, selectedPictureIndex = -1, setSelectedPictureIndex = null } = useCamera(); + const { simpleAlert = null } = useNotification(); + const elementRef = useRef(); + + const selectedPicture = ((selectedPictureIndex > -1) ? cameraRoll[selectedPictureIndex] : null); + + const getCameraBounds = () => + { + if(!elementRef || !elementRef.current) return null; + + const frameBounds = elementRef.current.getBoundingClientRect(); + + return new NitroRectangle(Math.floor(frameBounds.x), Math.floor(frameBounds.y), Math.floor(frameBounds.width), Math.floor(frameBounds.height)); + } + + const takePicture = () => + { + if(selectedPictureIndex > -1) + { + setSelectedPictureIndex(-1); + return; + } + + const texture = GetRoomEngine().createTextureFromRoom(GetRoomSession().roomId, 1, getCameraBounds()); + + const clone = [ ...cameraRoll ]; + + if(clone.length >= CAMERA_ROLL_LIMIT) + { + simpleAlert(LocalizeText('camera.full.body')); + + clone.pop(); + } + + PlaySound(SoundNames.CAMERA_SHUTTER); + clone.push(new CameraPicture(texture, TextureUtils.generateImageUrl(texture))); + + setCameraRoll(clone); + } + + return ( + + + { selectedPicture && } +
+
+ +
+ { !selectedPicture &&
} + { selectedPicture && +
+
+ + +
+
} +
+
+
+
+ { (cameraRoll.length > 0) && + + { cameraRoll.map((picture, index) => + { + return setSelectedPictureIndex(index) } />; + }) } + } + + + ); +} diff --git a/apps/frontend/src/components/camera/views/CameraWidgetCheckoutView.tsx b/apps/frontend/src/components/camera/views/CameraWidgetCheckoutView.tsx new file mode 100644 index 0000000..6ee92f5 --- /dev/null +++ b/apps/frontend/src/components/camera/views/CameraWidgetCheckoutView.tsx @@ -0,0 +1,159 @@ +import { CameraPublishStatusMessageEvent, CameraPurchaseOKMessageEvent, CameraStorageUrlMessageEvent, PublishPhotoMessageComposer, PurchasePhotoMessageComposer } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useMemo, useState } from 'react'; +import { CreateLinkEvent, GetConfiguration, GetRoomEngine, LocalizeText, SendMessageComposer } from '../../../api'; +import { Button, Column, Flex, LayoutCurrencyIcon, LayoutImage, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../common'; +import { useMessageEvent } from '../../../hooks'; + +export interface CameraWidgetCheckoutViewProps +{ + base64Url: string; + onCloseClick: () => void; + onCancelClick: () => void; + price: { credits: number, duckets: number, publishDucketPrice: number }; +} + +export const CameraWidgetCheckoutView: FC = props => +{ + const { base64Url = null, onCloseClick = null, onCancelClick = null, price = null } = props; + const [ pictureUrl, setPictureUrl ] = useState(null); + const [ publishUrl, setPublishUrl ] = useState(null); + const [ picturesBought, setPicturesBought ] = useState(0); + const [ wasPicturePublished, setWasPicturePublished ] = useState(false); + const [ isWaiting, setIsWaiting ] = useState(false); + const [ publishCooldown, setPublishCooldown ] = useState(0); + + const publishDisabled = useMemo(() => GetConfiguration('camera.publish.disabled', false), []); + + useMessageEvent(CameraPurchaseOKMessageEvent, event => + { + setPicturesBought(value => (value + 1)); + setIsWaiting(false); + }); + + useMessageEvent(CameraPublishStatusMessageEvent, event => + { + const parser = event.getParser(); + + setPublishUrl(parser.extraDataId); + setPublishCooldown(parser.secondsToWait); + setWasPicturePublished(parser.ok); + setIsWaiting(false); + }); + + useMessageEvent(CameraStorageUrlMessageEvent, event => + { + const parser = event.getParser(); + + setPictureUrl(GetConfiguration('camera.url') + '/' + parser.url); + }); + + const processAction = (type: string, value: string | number = null) => + { + switch(type) + { + case 'close': + onCloseClick(); + return; + case 'buy': + if(isWaiting) return; + + setIsWaiting(true); + SendMessageComposer(new PurchasePhotoMessageComposer('')); + return; + case 'publish': + if(isWaiting) return; + + setIsWaiting(true); + SendMessageComposer(new PublishPhotoMessageComposer()); + return; + case 'cancel': + onCancelClick(); + return; + } + } + + useEffect(() => + { + if(!base64Url) return; + + GetRoomEngine().saveBase64AsScreenshot(base64Url); + }, [ base64Url ]); + + if(!price) return null; + + return ( + + processAction('close') } /> + + + { (pictureUrl && pictureUrl.length) && + } + { (!pictureUrl || !pictureUrl.length) && + + { LocalizeText('camera.loading') } + } + + + + + { LocalizeText('camera.purchase.header') } + + { ((price.credits > 0) || (price.duckets > 0)) && + + { LocalizeText('catalog.purchase.confirmation.dialog.cost') } + { (price.credits > 0) && + + { price.credits } + + } + { (price.duckets > 0) && + + { price.duckets } + + } + } + { (picturesBought > 0) && + + { LocalizeText('camera.purchase.count.info') } { picturesBought } + CreateLinkEvent('inventory/toggle') }>{ LocalizeText('camera.open.inventory') } + } + + + + + + { !publishDisabled && + + + + { LocalizeText(wasPicturePublished ? 'camera.publish.successful' : 'camera.publish.explanation') } + + + { LocalizeText(wasPicturePublished ? 'camera.publish.success.short.info' : 'camera.publish.detailed.explanation') } + + { wasPicturePublished &&
{ LocalizeText('camera.link.to.published') } } + { !wasPicturePublished && (price.publishDucketPrice > 0) && + + { LocalizeText('catalog.purchase.confirmation.dialog.cost') } + + { price.publishDucketPrice } + + + } + { (publishCooldown > 0) &&
{ LocalizeText('camera.publish.wait', [ 'minutes' ], [ Math.ceil( publishCooldown / 60).toString() ]) }
} + + { !wasPicturePublished && + + + } + } + { LocalizeText('camera.warning.disclaimer') } + + + + + + ); +} diff --git a/apps/frontend/src/components/camera/views/CameraWidgetShowPhotoView.tsx b/apps/frontend/src/components/camera/views/CameraWidgetShowPhotoView.tsx new file mode 100644 index 0000000..8614695 --- /dev/null +++ b/apps/frontend/src/components/camera/views/CameraWidgetShowPhotoView.tsx @@ -0,0 +1,71 @@ +import { FC, useEffect, useState } from 'react'; +import { FaArrowLeft, FaArrowRight } from 'react-icons/fa'; +import { GetUserProfile, IPhotoData, LocalizeText } from '../../../api'; +import { Flex, Grid, Text } from '../../../common'; + +export interface CameraWidgetShowPhotoViewProps +{ + currentIndex: number; + currentPhotos: IPhotoData[]; +} + +export const CameraWidgetShowPhotoView: FC = props => +{ + const { currentIndex = -1, currentPhotos = null } = props; + const [ imageIndex, setImageIndex ] = useState(0); + + const currentImage = (currentPhotos && currentPhotos.length) ? currentPhotos[imageIndex] : null; + + const next = () => + { + setImageIndex(prevValue => + { + let newIndex = (prevValue + 1); + + if(newIndex >= currentPhotos.length) newIndex = 0; + + return newIndex; + }); + } + + const previous = () => + { + setImageIndex(prevValue => + { + let newIndex = (prevValue - 1); + + if(newIndex < 0) newIndex = (currentPhotos.length - 1); + + return newIndex; + }); + } + + useEffect(() => + { + setImageIndex(currentIndex); + }, [ currentIndex ]); + + if(!currentImage) return null; + + return ( + + + { !currentImage.w && + { LocalizeText('camera.loading') } } + + { currentImage.m && currentImage.m.length && + { currentImage.m } } + + { (currentImage.n || '') } + { new Date(currentImage.t * 1000).toLocaleDateString() } + + { (currentPhotos.length > 1) && + + + GetUserProfile(currentImage.oi) }>{ currentImage.o } + + + } + + ); +} diff --git a/apps/frontend/src/components/camera/views/editor/CameraWidgetEditorView.tsx b/apps/frontend/src/components/camera/views/editor/CameraWidgetEditorView.tsx new file mode 100644 index 0000000..d7dfbf7 --- /dev/null +++ b/apps/frontend/src/components/camera/views/editor/CameraWidgetEditorView.tsx @@ -0,0 +1,229 @@ +import { IRoomCameraWidgetEffect, IRoomCameraWidgetSelectedEffect, RoomCameraWidgetSelectedEffect } from '@nitrots/nitro-renderer'; +import { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import { FaSave, FaSearchMinus, FaSearchPlus, FaTrash } from 'react-icons/fa'; +import ReactSlider from 'react-slider'; +import { CameraEditorTabs, CameraPicture, CameraPictureThumbnail, GetRoomCameraWidgetManager, LocalizeText } from '../../../../api'; +import { Button, ButtonGroup, Column, Flex, Grid, LayoutImage, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView, Text } from '../../../../common'; +import { CameraWidgetEffectListView } from './effect-list/CameraWidgetEffectListView'; + +export interface CameraWidgetEditorViewProps +{ + picture: CameraPicture; + availableEffects: IRoomCameraWidgetEffect[]; + myLevel: number; + onClose: () => void; + onCancel: () => void; + onCheckout: (pictureUrl: string) => void; +} + +const TABS: string[] = [ CameraEditorTabs.COLORMATRIX, CameraEditorTabs.COMPOSITE ]; + +export const CameraWidgetEditorView: FC = props => +{ + const { picture = null, availableEffects = null, myLevel = 1, onClose = null, onCancel = null, onCheckout = null } = props; + const [ currentTab, setCurrentTab ] = useState(TABS[0]); + const [ selectedEffectName, setSelectedEffectName ] = useState(null); + const [ selectedEffects, setSelectedEffects ] = useState([]); + const [ effectsThumbnails, setEffectsThumbnails ] = useState([]); + const [ isZoomed, setIsZoomed ] = useState(false); + + const getColorMatrixEffects = useMemo(() => + { + return availableEffects.filter(effect => effect.colorMatrix); + }, [ availableEffects ]); + + const getCompositeEffects = useMemo(() => + { + return availableEffects.filter(effect => effect.texture); + }, [ availableEffects ]); + + const getEffectList = useCallback(() => + { + if(currentTab === CameraEditorTabs.COLORMATRIX) + { + return getColorMatrixEffects; + } + + return getCompositeEffects; + }, [ currentTab, getColorMatrixEffects, getCompositeEffects ]); + + const getSelectedEffectIndex = useCallback((name: string) => + { + if(!name || !name.length || !selectedEffects || !selectedEffects.length) return -1; + + return selectedEffects.findIndex(effect => (effect.effect.name === name)); + }, [ selectedEffects ]) + + const getCurrentEffectIndex = useMemo(() => + { + return getSelectedEffectIndex(selectedEffectName) + }, [ selectedEffectName, getSelectedEffectIndex ]) + + const getCurrentEffect = useMemo(() => + { + if(!selectedEffectName) return null; + + return (selectedEffects[getCurrentEffectIndex] || null); + }, [ selectedEffectName, getCurrentEffectIndex, selectedEffects ]); + + const setSelectedEffectAlpha = useCallback((alpha: number) => + { + const index = getCurrentEffectIndex; + + if(index === -1) return; + + setSelectedEffects(prevValue => + { + const clone = [ ...prevValue ]; + const currentEffect = clone[index]; + + clone[getCurrentEffectIndex] = new RoomCameraWidgetSelectedEffect(currentEffect.effect, alpha); + + return clone; + }); + }, [ getCurrentEffectIndex, setSelectedEffects ]); + + const getCurrentPictureUrl = useMemo(() => + { + return GetRoomCameraWidgetManager().applyEffects(picture.texture, selectedEffects, isZoomed).src; + }, [ picture, selectedEffects, isZoomed ]); + + const processAction = useCallback((type: string, effectName: string = null) => + { + switch(type) + { + case 'close': + onClose(); + return; + case 'cancel': + onCancel(); + return; + case 'checkout': + onCheckout(getCurrentPictureUrl); + return; + case 'change_tab': + setCurrentTab(String(effectName)); + return; + case 'select_effect': { + let existingIndex = getSelectedEffectIndex(effectName); + + if(existingIndex >= 0) return; + + const effect = availableEffects.find(effect => (effect.name === effectName)); + + if(!effect) return; + + setSelectedEffects(prevValue => + { + return [ ...prevValue, new RoomCameraWidgetSelectedEffect(effect, 1) ]; + }); + + setSelectedEffectName(effect.name); + return; + } + case 'remove_effect': { + let existingIndex = getSelectedEffectIndex(effectName); + + if(existingIndex === -1) return; + + setSelectedEffects(prevValue => + { + const clone = [ ...prevValue ]; + + clone.splice(existingIndex, 1); + + return clone; + }); + + if(selectedEffectName === effectName) setSelectedEffectName(null); + return; + } + case 'clear_effects': + setSelectedEffectName(null); + setSelectedEffects([]); + return; + case 'download': { + const image = new Image(); + + image.src = getCurrentPictureUrl + + const newWindow = window.open(''); + newWindow.document.write(image.outerHTML); + return; + } + case 'zoom': + setIsZoomed(!isZoomed); + return; + } + }, [ isZoomed, availableEffects, selectedEffectName, getCurrentPictureUrl, getSelectedEffectIndex, onCancel, onCheckout, onClose, setIsZoomed, setSelectedEffects ]); + + useEffect(() => + { + const thumbnails: CameraPictureThumbnail[] = []; + + for(const effect of availableEffects) + { + thumbnails.push(new CameraPictureThumbnail(effect.name, GetRoomCameraWidgetManager().applyEffects(picture.texture, [ new RoomCameraWidgetSelectedEffect(effect, 1) ], false).src)); + } + + setEffectsThumbnails(thumbnails); + }, [ picture, availableEffects ]); + + return ( + + processAction('close') } /> + + { TABS.map(tab => + { + return processAction('change_tab', tab) }> + }) } + + + + + + + + + + { selectedEffectName && + + { LocalizeText('camera.effect.name.' + selectedEffectName) } + setSelectedEffectAlpha(event) } + renderThumb={ (props, state) =>
{ state.valueNow }
} /> +
} +
+ + + + + + + + + + + +
+
+
+
+ ); +} diff --git a/apps/frontend/src/components/camera/views/editor/effect-list/CameraWidgetEffectListItemView.tsx b/apps/frontend/src/components/camera/views/editor/effect-list/CameraWidgetEffectListItemView.tsx new file mode 100644 index 0000000..cac3a34 --- /dev/null +++ b/apps/frontend/src/components/camera/views/editor/effect-list/CameraWidgetEffectListItemView.tsx @@ -0,0 +1,40 @@ +import { IRoomCameraWidgetEffect } from '@nitrots/nitro-renderer'; +import { FC } from 'react'; +import { FaLock, FaTimes } from 'react-icons/fa'; +import { LocalizeText } from '../../../../../api'; +import { Button, LayoutGridItem, Text } from '../../../../../common'; + +export interface CameraWidgetEffectListItemViewProps +{ + effect: IRoomCameraWidgetEffect; + thumbnailUrl: string; + isActive: boolean; + isLocked: boolean; + selectEffect: () => void; + removeEffect: () => void; +} + +export const CameraWidgetEffectListItemView: FC = props => +{ + const { effect = null, thumbnailUrl = null, isActive = false, isLocked = false, selectEffect = null, removeEffect = null } = props; + + return ( + (!isActive && selectEffect()) }> + { isActive && + } + { !isLocked && (thumbnailUrl && thumbnailUrl.length > 0) && +
+ +
} + { isLocked && + +
+ +
+ { effect.minLevel } +
} +
+ ); +} diff --git a/apps/frontend/src/components/camera/views/editor/effect-list/CameraWidgetEffectListView.tsx b/apps/frontend/src/components/camera/views/editor/effect-list/CameraWidgetEffectListView.tsx new file mode 100644 index 0000000..3f67cea --- /dev/null +++ b/apps/frontend/src/components/camera/views/editor/effect-list/CameraWidgetEffectListView.tsx @@ -0,0 +1,31 @@ +import { IRoomCameraWidgetEffect, IRoomCameraWidgetSelectedEffect } from '@nitrots/nitro-renderer'; +import { FC } from 'react'; +import { CameraPictureThumbnail } from '../../../../../api'; +import { Grid } from '../../../../../common'; +import { CameraWidgetEffectListItemView } from './CameraWidgetEffectListItemView'; + +export interface CameraWidgetEffectListViewProps +{ + myLevel: number; + selectedEffects: IRoomCameraWidgetSelectedEffect[]; + effects: IRoomCameraWidgetEffect[]; + thumbnails: CameraPictureThumbnail[]; + processAction: (type: string, name: string) => void; +} + +export const CameraWidgetEffectListView: FC = props => +{ + const { myLevel = 0, selectedEffects = [], effects = [], thumbnails = [], processAction = null } = props; + + return ( + + { effects && (effects.length > 0) && effects.map((effect, index) => + { + const thumbnailUrl = (thumbnails.find(thumbnail => (thumbnail.effectName === effect.name))); + const isActive = (selectedEffects.findIndex(selectedEffect => (selectedEffect.effect.name === effect.name)) > -1); + + return myLevel) } selectEffect={ () => processAction('select_effect', effect.name) } removeEffect={ () => processAction('remove_effect', effect.name) } /> + }) } + + ); +} diff --git a/apps/frontend/src/components/campaign/CalendarItemView.tsx b/apps/frontend/src/components/campaign/CalendarItemView.tsx new file mode 100644 index 0000000..816fa75 --- /dev/null +++ b/apps/frontend/src/components/campaign/CalendarItemView.tsx @@ -0,0 +1,52 @@ +import { FC } from 'react'; +import { CalendarItemState, GetConfiguration, GetRoomEngine, GetSessionDataManager, ICalendarItem } from '../../api'; +import { Base, Column, Flex, LayoutImage } from '../../common'; + +interface CalendarItemViewProps +{ + itemId: number; + state: number; + active?: boolean; + product?: ICalendarItem; + onClick: (itemId: number) => void; +} + +export const CalendarItemView: FC = props => +{ + const { itemId = -1, state = null, product = null, active = false, onClick = null } = props; + + const getFurnitureIcon = (name: string) => + { + let furniData = GetSessionDataManager().getFloorItemDataByName(name); + let url = null; + + if(furniData) url = GetRoomEngine().getFurnitureFloorIconUrl(furniData.id); + else + { + furniData = GetSessionDataManager().getWallItemDataByName(name); + + if(furniData) url = GetRoomEngine().getFurnitureWallIconUrl(furniData.id); + } + + return url; + } + + return ( + onClick(itemId) }> + { (state === CalendarItemState.STATE_UNLOCKED) && + + + { product && + ('image.library.url') + product.customImage : getFurnitureIcon(product.productName) } /> } + + } + { (state !== CalendarItemState.STATE_UNLOCKED) && + + { (state === CalendarItemState.STATE_LOCKED_AVAILABLE) && + } + { ((state === CalendarItemState.STATE_LOCKED_EXPIRED) || (state === CalendarItemState.STATE_LOCKED_FUTURE)) && + } + } + + ); +} diff --git a/apps/frontend/src/components/campaign/CalendarView.tsx b/apps/frontend/src/components/campaign/CalendarView.tsx new file mode 100644 index 0000000..e296129 --- /dev/null +++ b/apps/frontend/src/components/campaign/CalendarView.tsx @@ -0,0 +1,143 @@ +import { FC, useState } from 'react'; +import { CalendarItemState, GetSessionDataManager, ICalendarItem, LocalizeText } from '../../api'; +import { Base, Button, Column, Flex, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common'; +import { CalendarItemView } from './CalendarItemView'; + +interface CalendarViewProps +{ + onClose(): void; + openPackage(id: number, asStaff: boolean): void; + receivedProducts: Map; + campaignName: string; + currentDay: number; + numDays: number; + openedDays: number[]; + missedDays: number[]; +} + +const TOTAL_SHOWN_ITEMS = 5; + +export const CalendarView: FC = props => +{ + const { onClose = null, campaignName = null, currentDay = null, numDays = null, missedDays = null, openedDays = null, openPackage = null, receivedProducts = null } = props; + const [ selectedDay, setSelectedDay ] = useState(currentDay); + const [ index, setIndex ] = useState(Math.max(0, (selectedDay - 1))); + + const getDayState = (day: number) => + { + if(openedDays.includes(day)) return CalendarItemState.STATE_UNLOCKED; + + if(day > currentDay) return CalendarItemState.STATE_LOCKED_FUTURE; + + if(missedDays.includes(day)) return CalendarItemState.STATE_LOCKED_EXPIRED; + + return CalendarItemState.STATE_LOCKED_AVAILABLE; + } + + const dayMessage = (day: number) => + { + const state = getDayState(day); + + switch(state) + { + case CalendarItemState.STATE_UNLOCKED: + return LocalizeText('campaign.calendar.info.unlocked'); + case CalendarItemState.STATE_LOCKED_FUTURE: + return LocalizeText('campaign.calendar.info.future'); + case CalendarItemState.STATE_LOCKED_EXPIRED: + return LocalizeText('campaign.calendar.info.expired'); + default: + return LocalizeText('campaign.calendar.info.available.desktop'); + } + } + + const onClickNext = () => + { + const nextDay = (selectedDay + 1); + + if(nextDay === numDays) return; + + setSelectedDay(nextDay); + + if((index + TOTAL_SHOWN_ITEMS) < (nextDay + 1)) setIndex(index + 1); + } + + const onClickPrev = () => + { + const prevDay = (selectedDay - 1); + + if(prevDay < 0) return; + + setSelectedDay(prevDay); + + if(index > prevDay) setIndex(index - 1); + } + + const onClickItem = (item: number) => + { + if(selectedDay === item) + { + const state = getDayState(item); + + if(state === CalendarItemState.STATE_LOCKED_AVAILABLE) openPackage(item, false); + + return; + } + + setSelectedDay(item); + } + + const forceOpen = () => + { + const id = selectedDay; + const state = getDayState(id); + + if(state !== CalendarItemState.STATE_UNLOCKED) openPackage(id, true); + } + + return ( + + + + + + + + + { LocalizeText('campaign.calendar.heading.day', [ 'number' ], [ (selectedDay + 1).toString() ]) } + { dayMessage(selectedDay) } + +
+ { GetSessionDataManager().isModerator && + } +
+
+
+ +
+ + + + + + + { [ ...Array(TOTAL_SHOWN_ITEMS) ].map((e, i) => + { + const day = (index + i); + + return ( + + + + ); + }) } + + + + + + +
+
+ ) +} diff --git a/apps/frontend/src/components/campaign/CampaignView.scss b/apps/frontend/src/components/campaign/CampaignView.scss new file mode 100644 index 0000000..cea973f --- /dev/null +++ b/apps/frontend/src/components/campaign/CampaignView.scss @@ -0,0 +1,71 @@ +.nitro-campaign-calendar { + width: $nitro-calendar-width; + height: $nitro-calendar-height; + + .calendar-item { + filter: brightness(80%); + + &.active { + filter: brightness(100%); + } + } +} + +.campaign-spritesheet { + display: block; + background: transparent url('@/assets/images/campaign/campaign_spritesheet.png') no-repeat; + + &.available { + width: 69px; + height: 78px; + background-position: -5px -5px; + } + + &.campaign-day-generic-bg { + max-width: 202px; + height: 447px; + background-position: -84px -5px; + } + + &.campaign-opened { + width: 96px; + height: 66px; + background-position: -296px -5px; + } + + &.locked { + width: 42px; + height: 42px; + background-position: -296px -81px; + } + + &.locked-bg { + width: 132px; + height: 132px; + background-position: -402px -5px; + } + + &.next { + width: 33px; + height: 34px; + background-position: -5px -147px; + } + + &.prev { + width: 33px; + height: 34px; + background-position: -296px -147px; + } + + &.unavailable { + width: 68px; + height: 78px; + background-position: -339px -147px; + } + + &.unlocked-bg { + width: 190px; + height: 189px; + background-position: -296px -235px; + } +} diff --git a/apps/frontend/src/components/campaign/CampaignView.tsx b/apps/frontend/src/components/campaign/CampaignView.tsx new file mode 100644 index 0000000..fe80383 --- /dev/null +++ b/apps/frontend/src/components/campaign/CampaignView.tsx @@ -0,0 +1,101 @@ +import { CampaignCalendarData, CampaignCalendarDataMessageEvent, CampaignCalendarDoorOpenedMessageEvent, ILinkEventTracker, OpenCampaignCalendarDoorAsStaffComposer, OpenCampaignCalendarDoorComposer } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useState } from 'react'; +import { AddEventLinkTracker, CalendarItem, RemoveLinkEventTracker, SendMessageComposer } from '../../api'; +import { useMessageEvent } from '../../hooks'; +import { CalendarView } from './CalendarView'; + +export const CampaignView: FC<{}> = props => +{ + const [ calendarData, setCalendarData ] = useState(null); + const [ lastOpenAttempt, setLastOpenAttempt ] = useState(-1); + const [ receivedProducts, setReceivedProducts ] = useState>(new Map()); + const [ isCalendarOpen, setCalendarOpen ] = useState(false); + + const openPackage = (id: number, asStaff = false) => + { + if(!calendarData) return; + + setLastOpenAttempt(id); + + if(asStaff) + { + SendMessageComposer(new OpenCampaignCalendarDoorAsStaffComposer(calendarData.campaignName, id)); + } + + else + { + SendMessageComposer(new OpenCampaignCalendarDoorComposer(calendarData.campaignName, id)); + } + } + + useMessageEvent(CampaignCalendarDataMessageEvent, event => + { + const parser = event.getParser(); + + if(!parser) return; + + setCalendarData(parser.calendarData); + }); + + useMessageEvent(CampaignCalendarDoorOpenedMessageEvent, event => + { + const parser = event.getParser(); + + if(!parser) return; + + const lastAttempt = lastOpenAttempt; + + if(parser.doorOpened) + { + setCalendarData(prev => + { + const copy = prev.clone(); + copy.openedDays.push(lastOpenAttempt); + + return copy; + }); + + setReceivedProducts(prev => + { + const copy = new Map(prev); + copy.set(lastAttempt, new CalendarItem(parser.productName, parser.customImage,parser.furnitureClassName)); + + return copy; + }); + } + + setLastOpenAttempt(-1); + }); + + useEffect(() => + { + const linkTracker: ILinkEventTracker = { + linkReceived: (url: string) => + { + const value = url.split('/'); + + if(value.length < 2) return; + + switch(value[1]) + { + case 'calendar': + setCalendarOpen(true); + break; + } + }, + eventUrlPrefix: 'openView/' + }; + + AddEventLinkTracker(linkTracker); + + return () => RemoveLinkEventTracker(linkTracker); + }, []); + + return ( + <> + { (calendarData && isCalendarOpen) && + setCalendarOpen(false) } campaignName={ calendarData.campaignName } currentDay={ calendarData.currentDay } numDays={ calendarData.campaignDays } openedDays={ calendarData.openedDays } missedDays={ calendarData.missedDays } openPackage={ openPackage } receivedProducts={ receivedProducts } /> + } + + ) +} diff --git a/apps/frontend/src/components/catalog/CatalogView.scss b/apps/frontend/src/components/catalog/CatalogView.scss new file mode 100644 index 0000000..824ddb7 --- /dev/null +++ b/apps/frontend/src/components/catalog/CatalogView.scss @@ -0,0 +1,158 @@ +.nitro-catalog { + width: $catalog-width; + height: $catalog-height; + + font[size='16'] { + font-size: 20px; + } + + .catalog-search-button { + min-width: 30px; + width: 30px; + } + + .quantity-input { + min-height: 17px; + height: 17px; + width: 28px; + padding: 0 4px; + text-align: right; + } +} + +.nitro-catalog-gift { + width: 325px; + + .gift-preview { + width: 80px; + height: 80px; + overflow: hidden; + display: flex; + justify-content: center; + align-items: center; + } + + .gift-color { + width: 15px; + height: 15px; + border-radius: $border-radius; + } +} + +.nitro-catalog-navigation-grid-container { + border-color: #b6bec5 !important; + background-color: #cdd3d9; + border: 2px solid; + + .nitro-catalog-navigation-section { + display: grid; + + .nitro-catalog-navigation-section { + padding-left: 5px; + border-left: 2px solid #b6bec5; + } + } + + .layout-grid-item { + font-size: $font-size-sm; + height: 23px !important; + border-color: unset !important; + background-color: #cdd3d9; + border: 0 !important; + padding: 1px 3px; + + .svg-inline--fa { + color: $black; + font-size: 10px; + padding: 1px; + } + } +} + +.nitro-catalog-layout-info-loyalty { + .info-loyalty-content { + background-repeat: no-repeat; + background-position: top right; + background-image: url('@/assets/images/catalog/diamond_info_illustration.gif'); + padding-right: 123px; + } + + .info-image { + width: 123px; + height: 350px; + background-image: url('@/assets/images/catalog/diamond_info_illustration.gif'); + } +} + +.nitro-catalog-layout-vip-buy-grid { + .layout-grid-item { + height: 50px !important; + max-height: 50px !important; + + .icon-hc-banner { + width: 68px; + height: 40px; + background: url('@/assets/images/catalog/hc_big.png') center + no-repeat; + } + } +} + +.nitro-catalog-layout-marketplace-grid { + .layout-grid-item { + height: 75px !important; + } +} + +.nitro-catalog-layout-vip-gifts-grid { + .layout-grid-item { + height: 55px !important; + max-height: 55px !important; + } +} + +.nitro-catalog-layout-marketplace-post-offer { + width: $marketplace-post-offer-width; + height: $marketplace-post-offer-height; +} + +.nitro-catalog-layout-bundle-grid { + .layout-grid-item { + background-color: transparent; + } +} + +.nitro-catalog-header { + width: 290px; + height: 60px; +} + +.autocomplete-gift-container { + background: #fff; + padding: 8px; + list-style-type: none; + min-width: 307px; + border-radius: 0.2rem; + position: absolute; + font-size: 0.7875rem; + top: 81px; + left: 8px; + border: 1px solid #b6c1ce; + margin: 0; + border-radius: 2px; + margin: 0; + box-sizing: border-box; + max-height: 280px; + overflow-y: auto; + z-index: 1; + + .autocomplete-gift-item { + width: 100%; + box-sizing: border-box; + &:hover { + background-color: #ebf4ff; + } + } +} + +@import './views/targeted-offer/Offer.scss'; diff --git a/apps/frontend/src/components/catalog/CatalogView.tsx b/apps/frontend/src/components/catalog/CatalogView.tsx new file mode 100644 index 0000000..6201c33 --- /dev/null +++ b/apps/frontend/src/components/catalog/CatalogView.tsx @@ -0,0 +1,111 @@ +import { ILinkEventTracker } from '@nitrots/nitro-renderer'; +import { FC, useEffect } from 'react'; +import { AddEventLinkTracker, GetConfiguration, LocalizeText, RemoveLinkEventTracker } from '../../api'; +import { Column, Flex, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common'; +import { useCatalog } from '../../hooks'; +import { CatalogIconView } from './views/catalog-icon/CatalogIconView'; +import { CatalogGiftView } from './views/gift/CatalogGiftView'; +import { CatalogNavigationView } from './views/navigation/CatalogNavigationView'; +import { GetCatalogLayout } from './views/page/layout/GetCatalogLayout'; +import { MarketplacePostOfferView } from './views/page/layout/marketplace/MarketplacePostOfferView'; + +export const CatalogView: FC<{}> = props => +{ + const { isVisible = false, setIsVisible = null, rootNode = null, currentPage = null, navigationHidden = false, setNavigationHidden = null, activeNodes = [], searchResult = null, setSearchResult = null, openPageByName = null, openPageByOfferId = null, activateNode = null, getNodeById } = useCatalog(); + + 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(prevValue => !prevValue); + return; + case 'open': + if(parts.length > 2) + { + if(parts.length === 4) + { + switch(parts[2]) + { + case 'offerId': + openPageByOfferId(parseInt(parts[3])); + return; + } + } + else + { + openPageByName(parts[2]); + } + } + else + { + setIsVisible(true); + } + + return; + } + }, + eventUrlPrefix: 'catalog/' + }; + + AddEventLinkTracker(linkTracker); + + return () => RemoveLinkEventTracker(linkTracker); + }, [ setIsVisible, openPageByOfferId, openPageByName ]); + + return ( + <> + { isVisible && + + setIsVisible(false) } /> + + { rootNode && (rootNode.children.length > 0) && rootNode.children.map(child => + { + if(!child.isVisible) return null; + + return ( + + { + if(searchResult) setSearchResult(null); + + activateNode(child); + } } > + + { GetConfiguration('catalog.tab.icons') && } + { child.localization } + + + ); + }) } + + + + { !navigationHidden && + + { activeNodes && (activeNodes.length > 0) && + } + } + + { GetCatalogLayout(currentPage, () => setNavigationHidden(true)) } + + + + } + + + + ); +} diff --git a/apps/frontend/src/components/catalog/views/CatalogPurchaseConfirmView.tsx b/apps/frontend/src/components/catalog/views/CatalogPurchaseConfirmView.tsx new file mode 100644 index 0000000..30dcfc3 --- /dev/null +++ b/apps/frontend/src/components/catalog/views/CatalogPurchaseConfirmView.tsx @@ -0,0 +1,10 @@ +import { FC } from 'react'; + +export const CatalogPurchaseConfirmView: FC<{}> = props => +{ + const {} = props; + + return ( +
+ ); +} diff --git a/apps/frontend/src/components/catalog/views/catalog-header/CatalogHeaderView.tsx b/apps/frontend/src/components/catalog/views/catalog-header/CatalogHeaderView.tsx new file mode 100644 index 0000000..eb8d92c --- /dev/null +++ b/apps/frontend/src/components/catalog/views/catalog-header/CatalogHeaderView.tsx @@ -0,0 +1,26 @@ +import { FC, useEffect, useState } from 'react'; +import { GetConfiguration } from '../../../../api'; +import { Flex } from '../../../../common'; + +export interface CatalogHeaderViewProps +{ + imageUrl?: string; +} + +export const CatalogHeaderView: FC = props => +{ + const { imageUrl = null } = props; + const [ displayImageUrl, setDisplayImageUrl ] = useState(''); + + useEffect(() => + { + setDisplayImageUrl(imageUrl ?? GetConfiguration('catalog.asset.image.url').replace('%name%', 'catalog_header_roombuilder')); + }, [ imageUrl ]); + + return + + { + currentTarget.src = GetConfiguration('catalog.asset.image.url').replace('%name%', 'catalog_header_roombuilder'); + } } /> + ; +} diff --git a/apps/frontend/src/components/catalog/views/catalog-icon/CatalogIconView.tsx b/apps/frontend/src/components/catalog/views/catalog-icon/CatalogIconView.tsx new file mode 100644 index 0000000..7c28ac9 --- /dev/null +++ b/apps/frontend/src/components/catalog/views/catalog-icon/CatalogIconView.tsx @@ -0,0 +1,20 @@ +import { FC, useMemo } from 'react'; +import { GetConfiguration } from '../../../../api'; +import { LayoutImage } from '../../../../common/layout/LayoutImage'; + +export interface CatalogIconViewProps +{ + icon: number; +} + +export const CatalogIconView: FC = props => +{ + const { icon = 0 } = props; + + const getIconUrl = useMemo(() => + { + return ((GetConfiguration('catalog.asset.icon.url')).replace('%name%', icon.toString())); + }, [ icon ]); + + return ; +} diff --git a/apps/frontend/src/components/catalog/views/catalog-room-previewer/CatalogRoomPreviewerView.tsx b/apps/frontend/src/components/catalog/views/catalog-room-previewer/CatalogRoomPreviewerView.tsx new file mode 100644 index 0000000..1eb673f --- /dev/null +++ b/apps/frontend/src/components/catalog/views/catalog-room-previewer/CatalogRoomPreviewerView.tsx @@ -0,0 +1,42 @@ +import { NitroToolbarAnimateIconEvent, TextureUtils, ToolbarIconEnum } from '@nitrots/nitro-renderer'; +import { FC, useRef } from 'react'; +import { GetRoomEngine } from '../../../../api'; +import { LayoutRoomPreviewerView, LayoutRoomPreviewerViewProps } from '../../../../common'; +import { CatalogPurchasedEvent } from '../../../../events'; +import { useUiEvent } from '../../../../hooks'; + +export const CatalogRoomPreviewerView: FC = props => +{ + const { roomPreviewer = null } = props; + const elementRef = useRef(null); + + useUiEvent(CatalogPurchasedEvent.PURCHASE_SUCCESS, event => + { + if(!elementRef) return; + + const renderTexture = roomPreviewer.getRoomObjectCurrentImage(); + + if(!renderTexture) return; + + const image = TextureUtils.generateImage(renderTexture); + + if(!image) return; + + const bounds = elementRef.current.getBoundingClientRect(); + + const x = (bounds.x + (bounds.width / 2)); + const y = (bounds.y + (bounds.height / 2)); + + const animateEvent = new NitroToolbarAnimateIconEvent(image, x, y); + + animateEvent.iconName = ToolbarIconEnum.INVENTORY; + + GetRoomEngine().events.dispatchEvent(animateEvent); + }); + + return ( +
+ +
+ ); +} diff --git a/apps/frontend/src/components/catalog/views/gift/CatalogGiftView.tsx b/apps/frontend/src/components/catalog/views/gift/CatalogGiftView.tsx new file mode 100644 index 0000000..64e1dd1 --- /dev/null +++ b/apps/frontend/src/components/catalog/views/gift/CatalogGiftView.tsx @@ -0,0 +1,289 @@ +import { GiftReceiverNotFoundEvent, PurchaseFromCatalogAsGiftComposer } from '@nitrots/nitro-renderer'; +import { ChangeEvent, FC, useCallback, useEffect, useMemo, useState } from 'react'; +import { FaChevronLeft, FaChevronRight } from 'react-icons/fa'; +import { ColorUtils, GetSessionDataManager, LocalizeText, MessengerFriend, ProductTypeEnum, SendMessageComposer } from '../../../../api'; +import { Base, Button, ButtonGroup, classNames, Column, Flex, FormGroup, LayoutCurrencyIcon, LayoutFurniImageView, LayoutGiftTagView, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common'; +import { CatalogEvent, CatalogInitGiftEvent, CatalogPurchasedEvent } from '../../../../events'; +import { useCatalog, useFriends, useMessageEvent, useUiEvent } from '../../../../hooks'; + +export const CatalogGiftView: FC<{}> = props => +{ + const [ isVisible, setIsVisible ] = useState(false); + const [ pageId, setPageId ] = useState(0); + const [ offerId, setOfferId ] = useState(0); + const [ extraData, setExtraData ] = useState(''); + const [ receiverName, setReceiverName ] = useState(''); + const [ showMyFace, setShowMyFace ] = useState(true); + const [ message, setMessage ] = useState(''); + const [ colors, setColors ] = useState<{ id: number, color: string }[]>([]); + const [ selectedBoxIndex, setSelectedBoxIndex ] = useState(0); + const [ selectedRibbonIndex, setSelectedRibbonIndex ] = useState(0); + const [ selectedColorId, setSelectedColorId ] = useState(0); + const [ maxBoxIndex, setMaxBoxIndex ] = useState(0); + const [ maxRibbonIndex, setMaxRibbonIndex ] = useState(0); + const [ receiverNotFound, setReceiverNotFound ] = useState(false); + const { catalogOptions = null } = useCatalog(); + const { friends } = useFriends(); + const { giftConfiguration = null } = catalogOptions; + const [ boxTypes, setBoxTypes ] = useState([]); + const [ suggestions, setSuggestions ] = useState([]); + const [ isAutocompleteVisible, setIsAutocompleteVisible ] = useState(true); + + const onClose = useCallback(() => + { + setIsVisible(false); + setPageId(0); + setOfferId(0); + setExtraData(''); + setReceiverName(''); + setShowMyFace(true); + setMessage(''); + setSelectedBoxIndex(0); + setSelectedRibbonIndex(0); + setIsAutocompleteVisible(false); + setSuggestions([]); + + if(colors.length) setSelectedColorId(colors[0].id); + }, [ colors ]); + + const isBoxDefault = useMemo(() => + { + return giftConfiguration ? (giftConfiguration.defaultStuffTypes.findIndex(s => (s === boxTypes[selectedBoxIndex])) > -1) : false; + }, [ boxTypes, giftConfiguration, selectedBoxIndex ]); + + const boxExtraData = useMemo(() => + { + if (!giftConfiguration) return ''; + + return ((boxTypes[selectedBoxIndex] * 1000) + giftConfiguration.ribbonTypes[selectedRibbonIndex]).toString(); + }, [ giftConfiguration, selectedBoxIndex, selectedRibbonIndex, boxTypes ]); + + const isColorable = useMemo(() => + { + if (!giftConfiguration) return false; + + if (isBoxDefault) return false; + + const boxType = boxTypes[selectedBoxIndex]; + + return (boxType === 8 || (boxType >= 3 && boxType <= 6)) ? false : true; + }, [ giftConfiguration, selectedBoxIndex, isBoxDefault, boxTypes ]); + + const colourId = useMemo(() => + { + return isBoxDefault ? boxTypes[selectedBoxIndex] : selectedColorId; + },[ isBoxDefault, boxTypes, selectedBoxIndex, selectedColorId ]) + + const allFriends = friends.filter( (friend: MessengerFriend) => friend.id !== -1 ); + + const onTextChanged = (e: ChangeEvent) => + { + const value = e.target.value; + + let suggestions = []; + + if (value.length > 0) + { + suggestions = allFriends.sort().filter((friend: MessengerFriend) => friend.name.includes(value)); + } + + setReceiverName(value); + setIsAutocompleteVisible(true); + setSuggestions(suggestions); + }; + + const selectedReceiverName = (friendName: string) => + { + setReceiverName(friendName); + setIsAutocompleteVisible(false); + } + + const handleAction = useCallback((action: string) => + { + switch(action) + { + case 'prev_box': + setSelectedBoxIndex(value => (value === 0 ? maxBoxIndex : value - 1)); + return; + case 'next_box': + setSelectedBoxIndex(value => (value === maxBoxIndex ? 0 : value + 1)); + return; + case 'prev_ribbon': + setSelectedRibbonIndex(value => (value === 0 ? maxRibbonIndex : value - 1)); + return; + case 'next_ribbon': + setSelectedRibbonIndex(value => (value === maxRibbonIndex ? 0 : value + 1)); + return; + case 'buy': + if(!receiverName || (receiverName.length === 0)) + { + setReceiverNotFound(true); + return; + } + + SendMessageComposer(new PurchaseFromCatalogAsGiftComposer(pageId, offerId, extraData, receiverName, message, colourId , selectedBoxIndex, selectedRibbonIndex, showMyFace)); + return; + } + }, [ colourId, extraData, maxBoxIndex, maxRibbonIndex, message, offerId, pageId, receiverName, selectedBoxIndex, selectedRibbonIndex, showMyFace ]); + + useMessageEvent(GiftReceiverNotFoundEvent, event => setReceiverNotFound(true)); + + useUiEvent([ + CatalogPurchasedEvent.PURCHASE_SUCCESS, + CatalogEvent.INIT_GIFT ], event => + { + switch(event.type) + { + case CatalogPurchasedEvent.PURCHASE_SUCCESS: + onClose(); + return; + case CatalogEvent.INIT_GIFT: + const castedEvent = (event as CatalogInitGiftEvent); + + onClose(); + + setPageId(castedEvent.pageId); + setOfferId(castedEvent.offerId); + setExtraData(castedEvent.extraData); + setIsVisible(true); + return; + } + }); + + useEffect(() => + { + setReceiverNotFound(false); + }, [ receiverName ]); + + const createBoxTypes = useCallback(() => + { + if (!giftConfiguration) return; + + setBoxTypes(prev => + { + let newPrev = [ ...giftConfiguration.boxTypes ]; + + newPrev.push(giftConfiguration.defaultStuffTypes[ Math.floor((Math.random() * (giftConfiguration.defaultStuffTypes.length - 1))) ]); + + setMaxBoxIndex(newPrev.length- 1); + setMaxRibbonIndex(newPrev.length - 1); + + return newPrev; + }) + },[ giftConfiguration ]) + + useEffect(() => + { + if(!giftConfiguration) return; + + const newColors: { id: number, color: string }[] = []; + + for(const colorId of giftConfiguration.stuffTypes) + { + const giftData = GetSessionDataManager().getFloorItemData(colorId); + + if(!giftData) continue; + + if(giftData.colors && giftData.colors.length > 0) newColors.push({ id: colorId, color: ColorUtils.makeColorNumberHex(giftData.colors[0]) }); + } + + createBoxTypes(); + + if(newColors.length) + { + setSelectedColorId(newColors[0].id); + setColors(newColors); + } + }, [ giftConfiguration, createBoxTypes ]); + + useEffect(() => + { + if (!isVisible) return; + + createBoxTypes(); + },[ createBoxTypes, isVisible ]) + + if(!giftConfiguration || !giftConfiguration.isEnabled || !isVisible) return null; + + const boxName = 'catalog.gift_wrapping_new.box.' + (isBoxDefault ? 'default' : boxTypes[selectedBoxIndex]); + const ribbonName = `catalog.gift_wrapping_new.ribbon.${ selectedRibbonIndex }`; + const priceText = 'catalog.gift_wrapping_new.' + (isBoxDefault ? 'freeprice' : 'price'); + + return ( + + + + + { LocalizeText('catalog.gift_wrapping.receiver') } + onTextChanged(e) } /> + { (suggestions.length > 0 && isAutocompleteVisible) && + + { suggestions.map((friend: MessengerFriend) => ( + selectedReceiverName(friend.name) }>{ friend.name } + )) } + + } + { receiverNotFound && + { LocalizeText('catalog.gift_wrapping.receiver_not_found.title') } } + + setMessage(value) } /> + + setShowMyFace(value => !value) } /> + + + + { selectedColorId && + + + } + + + + + + + + { LocalizeText(boxName) } + + { LocalizeText(priceText, [ 'price' ], [ giftConfiguration.price.toString() ]) } + + + + + + + + + + { LocalizeText(ribbonName) } + + + + + + { LocalizeText('catalog.gift_wrapping.pick_color') } + + + { colors.map(color => + + + + + ); +}; diff --git a/apps/frontend/src/components/catalog/views/navigation/CatalogNavigationItemView.tsx b/apps/frontend/src/components/catalog/views/navigation/CatalogNavigationItemView.tsx new file mode 100644 index 0000000..45f7f43 --- /dev/null +++ b/apps/frontend/src/components/catalog/views/navigation/CatalogNavigationItemView.tsx @@ -0,0 +1,35 @@ +import { FC } from 'react'; +import { FaCaretDown, FaCaretUp } from 'react-icons/fa'; +import { ICatalogNode } from '../../../../api'; +import { Base, LayoutGridItem, Text } from '../../../../common'; +import { useCatalog } from '../../../../hooks'; +import { CatalogIconView } from '../catalog-icon/CatalogIconView'; +import { CatalogNavigationSetView } from './CatalogNavigationSetView'; + +export interface CatalogNavigationItemViewProps +{ + node: ICatalogNode; + child?: boolean; +} + +export const CatalogNavigationItemView: FC = props => +{ + const { node = null, child = false } = props; + const { activateNode = null } = useCatalog(); + + return ( + + activateNode(node) } className={ child ? 'inset' : '' }> + + { node.localization } + { node.isBranch && + <> + { node.isOpen && } + { !node.isOpen && } + } + + { node.isOpen && node.isBranch && + } + + ); +} diff --git a/apps/frontend/src/components/catalog/views/navigation/CatalogNavigationSetView.tsx b/apps/frontend/src/components/catalog/views/navigation/CatalogNavigationSetView.tsx new file mode 100644 index 0000000..8bfdd48 --- /dev/null +++ b/apps/frontend/src/components/catalog/views/navigation/CatalogNavigationSetView.tsx @@ -0,0 +1,25 @@ +import { FC } from 'react'; +import { ICatalogNode } from '../../../../api'; +import { CatalogNavigationItemView } from './CatalogNavigationItemView'; + +export interface CatalogNavigationSetViewProps +{ + node: ICatalogNode; + child?: boolean; +} + +export const CatalogNavigationSetView: FC = props => +{ + const { node = null, child = false } = props; + + return ( + <> + { node && (node.children.length > 0) && node.children.map((n, index) => + { + if(!n.isVisible) return null; + + return + }) } + + ); +} diff --git a/apps/frontend/src/components/catalog/views/navigation/CatalogNavigationView.tsx b/apps/frontend/src/components/catalog/views/navigation/CatalogNavigationView.tsx new file mode 100644 index 0000000..2d03f06 --- /dev/null +++ b/apps/frontend/src/components/catalog/views/navigation/CatalogNavigationView.tsx @@ -0,0 +1,34 @@ +import { FC } from 'react'; +import { ICatalogNode } from '../../../../api'; +import { AutoGrid, Column } from '../../../../common'; +import { useCatalog } from '../../../../hooks'; +import { CatalogSearchView } from '../page/common/CatalogSearchView'; +import { CatalogNavigationItemView } from './CatalogNavigationItemView'; +import { CatalogNavigationSetView } from './CatalogNavigationSetView'; + +export interface CatalogNavigationViewProps +{ + node: ICatalogNode; +} + +export const CatalogNavigationView: FC = props => +{ + const { node = null } = props; + const { searchResult = null } = useCatalog(); + + return ( + <> + + + + { searchResult && (searchResult.filteredNodes.length > 0) && searchResult.filteredNodes.map((n, index) => + { + return ; + }) } + { !searchResult && + } + + + + ); +} diff --git a/apps/frontend/src/components/catalog/views/page/common/CatalogGridOfferView.tsx b/apps/frontend/src/components/catalog/views/page/common/CatalogGridOfferView.tsx new file mode 100644 index 0000000..507bb6c --- /dev/null +++ b/apps/frontend/src/components/catalog/views/page/common/CatalogGridOfferView.tsx @@ -0,0 +1,59 @@ +import { MouseEventType } from '@nitrots/nitro-renderer'; +import { FC, MouseEvent, useMemo, useState } from 'react'; +import { IPurchasableOffer, Offer, ProductTypeEnum } from '../../../../../api'; +import { LayoutAvatarImageView, LayoutGridItem, LayoutGridItemProps } from '../../../../../common'; +import { useCatalog, useInventoryFurni } from '../../../../../hooks'; + +interface CatalogGridOfferViewProps extends LayoutGridItemProps +{ + offer: IPurchasableOffer; + selectOffer: (offer: IPurchasableOffer) => void; +} + +export const CatalogGridOfferView: FC = props => +{ + const { offer = null, selectOffer = null, itemActive = false, ...rest } = props; + const [ isMouseDown, setMouseDown ] = useState(false); + const { requestOfferToMover = null } = useCatalog(); + const { isVisible = false } = useInventoryFurni(); + + const iconUrl = useMemo(() => + { + if(offer.pricingModel === Offer.PRICING_MODEL_BUNDLE) + { + return null; + } + + return offer.product.getIconUrl(offer); + }, [ offer ]); + + const onMouseEvent = (event: MouseEvent) => + { + switch(event.type) + { + case MouseEventType.MOUSE_DOWN: + selectOffer(offer); + setMouseDown(true); + return; + case MouseEventType.MOUSE_UP: + setMouseDown(false); + return; + case MouseEventType.ROLL_OUT: + if(!isMouseDown || !itemActive || !isVisible) return; + + requestOfferToMover(offer); + return; + } + } + + const product = offer.product; + + if(!product) return null; + + return ( + + { (offer.product.productType === ProductTypeEnum.ROBOT) && + } + + ); +} diff --git a/apps/frontend/src/components/catalog/views/page/common/CatalogRedeemVoucherView.tsx b/apps/frontend/src/components/catalog/views/page/common/CatalogRedeemVoucherView.tsx new file mode 100644 index 0000000..504bd4b --- /dev/null +++ b/apps/frontend/src/components/catalog/views/page/common/CatalogRedeemVoucherView.tsx @@ -0,0 +1,60 @@ +import { RedeemVoucherMessageComposer, VoucherRedeemErrorMessageEvent, VoucherRedeemOkMessageEvent } from '@nitrots/nitro-renderer'; +import { FC, useState } from 'react'; +import { FaTag } from 'react-icons/fa'; +import { LocalizeText, SendMessageComposer } from '../../../../../api'; +import { Button, Flex } from '../../../../../common'; +import { useMessageEvent, useNotification } from '../../../../../hooks'; + +export interface CatalogRedeemVoucherViewProps +{ + text: string; +} + +export const CatalogRedeemVoucherView: FC = props => +{ + const { text = null } = props; + const [ voucher, setVoucher ] = useState(''); + const [ isWaiting, setIsWaiting ] = useState(false); + const { simpleAlert = null } = useNotification(); + + const redeemVoucher = () => + { + if(!voucher || !voucher.length || isWaiting) return; + + SendMessageComposer(new RedeemVoucherMessageComposer(voucher)); + + setIsWaiting(true); + } + + useMessageEvent(VoucherRedeemOkMessageEvent, event => + { + const parser = event.getParser(); + + let message = LocalizeText('catalog.alert.voucherredeem.ok.description'); + + if(parser.productName) message = LocalizeText('catalog.alert.voucherredeem.ok.description.furni', [ 'productName', 'productDescription' ], [ parser.productName, parser.productDescription ]); + + simpleAlert(message, null, null, null, LocalizeText('catalog.alert.voucherredeem.ok.title')); + + setIsWaiting(false); + setVoucher(''); + }); + + useMessageEvent(VoucherRedeemErrorMessageEvent, event => + { + const parser = event.getParser(); + + simpleAlert(LocalizeText(`catalog.alert.voucherredeem.error.description.${ parser.errorCode }`), null, null, null, LocalizeText('catalog.alert.voucherredeem.error.title')); + + setIsWaiting(false); + }); + + return ( + + setVoucher(event.target.value) } /> + + + ); +} diff --git a/apps/frontend/src/components/catalog/views/page/common/CatalogSearchView.tsx b/apps/frontend/src/components/catalog/views/page/common/CatalogSearchView.tsx new file mode 100644 index 0000000..69fb789 --- /dev/null +++ b/apps/frontend/src/components/catalog/views/page/common/CatalogSearchView.tsx @@ -0,0 +1,96 @@ +import { IFurnitureData } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useState } from 'react'; +import { FaSearch, FaTimes } from 'react-icons/fa'; +import { CatalogPage, CatalogType, FilterCatalogNode, FurnitureOffer, GetOfferNodes, GetSessionDataManager, ICatalogNode, ICatalogPage, IPurchasableOffer, LocalizeText, PageLocalization, SearchResult } from '../../../../../api'; +import { Button, Flex } from '../../../../../common'; +import { useCatalog } from '../../../../../hooks'; + +export const CatalogSearchView: FC<{}> = props => +{ + const [ searchValue, setSearchValue ] = useState(''); + const { currentType = null, rootNode = null, offersToNodes = null, searchResult = null, setSearchResult = null, setCurrentPage = null } = useCatalog(); + + useEffect(() => + { + let search = searchValue?.toLocaleLowerCase().replace(' ', ''); + + if(!search || !search.length) + { + setSearchResult(null); + + return; + } + + const timeout = setTimeout(() => + { + const furnitureDatas = GetSessionDataManager().getAllFurnitureData({ + loadFurnitureData: null + }); + + if(!furnitureDatas || !furnitureDatas.length) return; + + const foundFurniture: IFurnitureData[] = []; + const foundFurniLines: string[] = []; + + for(const furniture of furnitureDatas) + { + if((currentType === CatalogType.BUILDER) && !furniture.availableForBuildersClub) continue; + + if((currentType === CatalogType.NORMAL) && furniture.excludeDynamic) continue; + + const searchValues = [ furniture.className, furniture.name, furniture.description ].join(' ').replace(/ /gi, '').toLowerCase(); + + if((currentType === CatalogType.BUILDER) && (furniture.purchaseOfferId === -1) && (furniture.rentOfferId === -1)) + { + if((furniture.furniLine !== '') && (foundFurniLines.indexOf(furniture.furniLine) < 0)) + { + if(searchValues.indexOf(search) >= 0) foundFurniLines.push(furniture.furniLine); + } + } + else + { + const foundNodes = [ + ...GetOfferNodes(offersToNodes, furniture.purchaseOfferId), + ...GetOfferNodes(offersToNodes, furniture.rentOfferId) + ]; + + if(foundNodes.length) + { + if(searchValues.indexOf(search) >= 0) foundFurniture.push(furniture); + + if(foundFurniture.length === 250) break; + } + } + } + + const offers: IPurchasableOffer[] = []; + + for(const furniture of foundFurniture) offers.push(new FurnitureOffer(furniture)); + + let nodes: ICatalogNode[] = []; + + FilterCatalogNode(search, foundFurniLines, rootNode, nodes); + + setSearchResult(new SearchResult(search, offers, nodes.filter(node => (node.isVisible)))); + setCurrentPage((new CatalogPage(-1, 'default_3x3', new PageLocalization([], []), offers, false, 1) as ICatalogPage)); + }, 300); + + return () => clearTimeout(timeout); + }, [ offersToNodes, currentType, rootNode, searchValue, setCurrentPage, setSearchResult ]); + + return ( + + + setSearchValue(event.target.value) } /> + + { (!searchValue || !searchValue.length) && + } + { searchValue && !!searchValue.length && + } + + ); +} diff --git a/apps/frontend/src/components/catalog/views/page/layout/CatalogLayout.types.ts b/apps/frontend/src/components/catalog/views/page/layout/CatalogLayout.types.ts new file mode 100644 index 0000000..b05bccf --- /dev/null +++ b/apps/frontend/src/components/catalog/views/page/layout/CatalogLayout.types.ts @@ -0,0 +1,7 @@ +import { ICatalogPage } from '../../../../../api'; + +export interface CatalogLayoutProps +{ + page: ICatalogPage; + hideNavigation: () => void; +} diff --git a/apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutBadgeDisplayView.tsx b/apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutBadgeDisplayView.tsx new file mode 100644 index 0000000..9b6ec6a --- /dev/null +++ b/apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutBadgeDisplayView.tsx @@ -0,0 +1,54 @@ +import { FC } from 'react'; +import { LocalizeText } from '../../../../../api'; +import { Base, Column, Flex, Grid, Text } from '../../../../../common'; +import { useCatalog } from '../../../../../hooks'; +import { CatalogBadgeSelectorWidgetView } from '../widgets/CatalogBadgeSelectorWidgetView'; +import { CatalogFirstProductSelectorWidgetView } from '../widgets/CatalogFirstProductSelectorWidgetView'; +import { CatalogItemGridWidgetView } from '../widgets/CatalogItemGridWidgetView'; +import { CatalogLimitedItemWidgetView } from '../widgets/CatalogLimitedItemWidgetView'; +import { CatalogPurchaseWidgetView } from '../widgets/CatalogPurchaseWidgetView'; +import { CatalogTotalPriceWidget } from '../widgets/CatalogTotalPriceWidget'; +import { CatalogViewProductWidgetView } from '../widgets/CatalogViewProductWidgetView'; +import { CatalogLayoutProps } from './CatalogLayout.types'; + +export const CatalogLayoutBadgeDisplayView: FC = props => +{ + const { page = null } = props; + const { currentOffer = null } = useCatalog(); + + return ( + <> + + + + + + { LocalizeText('catalog_selectbadge') } + + + + + { !currentOffer && + <> + { !!page.localization.getImage(1) && } + + } + { currentOffer && + <> + + + + + + { currentOffer.localizationName } + + + + + + } + + + + ); +} diff --git a/apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutColorGroupingView.tsx b/apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutColorGroupingView.tsx new file mode 100644 index 0000000..60ccdba --- /dev/null +++ b/apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutColorGroupingView.tsx @@ -0,0 +1,176 @@ +import { ColorConverter } from '@nitrots/nitro-renderer'; +import { FC, useMemo, useState } from 'react'; +import { FaFillDrip } from 'react-icons/fa'; +import { IPurchasableOffer } from '../../../../../api'; +import { AutoGrid, Base, Button, Column, Flex, Grid, LayoutGridItem, Text } from '../../../../../common'; +import { useCatalog } from '../../../../../hooks'; +import { CatalogGridOfferView } from '../common/CatalogGridOfferView'; +import { CatalogAddOnBadgeWidgetView } from '../widgets/CatalogAddOnBadgeWidgetView'; +import { CatalogLimitedItemWidgetView } from '../widgets/CatalogLimitedItemWidgetView'; +import { CatalogPurchaseWidgetView } from '../widgets/CatalogPurchaseWidgetView'; +import { CatalogSpinnerWidgetView } from '../widgets/CatalogSpinnerWidgetView'; +import { CatalogTotalPriceWidget } from '../widgets/CatalogTotalPriceWidget'; +import { CatalogViewProductWidgetView } from '../widgets/CatalogViewProductWidgetView'; +import { CatalogLayoutProps } from './CatalogLayout.types'; + +export interface CatalogLayoutColorGroupViewProps extends CatalogLayoutProps +{ + +} + +export const CatalogLayoutColorGroupingView : FC = props => +{ + const { page = null } = props; + const [ colorableItems, setColorableItems ] = useState>(new Map()); + const { currentOffer = null, setCurrentOffer = null } = useCatalog(); + const [ colorsShowing, setColorsShowing ] = useState(false); + + const sortByColorIndex = (a: IPurchasableOffer, b: IPurchasableOffer) => + { + if (((!(a.product.furnitureData.colorIndex)) || (!(b.product.furnitureData.colorIndex)))) + { + return 1; + } + if (a.product.furnitureData.colorIndex > b.product.furnitureData.colorIndex) + { + return 1; + } + if (a == b) + { + return 0; + } + return -1; + } + + const sortyByFurnitureClassName = (a: IPurchasableOffer, b: IPurchasableOffer) => + { + if (a.product.furnitureData.className > b.product.furnitureData.className) + { + return 1; + } + if (a == b) + { + return 0; + } + return -1; + } + + const selectOffer = (offer: IPurchasableOffer) => + { + offer.activate(); + setCurrentOffer(offer); + } + + const selectColor = (colorIndex: number, productName: string) => + { + const fullName = `${ productName }*${ colorIndex }`; + const index = page.offers.findIndex(offer => offer.product.furnitureData.fullName === fullName); + if (index > -1) + { + selectOffer(page.offers[index]); + } + } + + const offers = useMemo(() => + { + const offers: IPurchasableOffer[] = []; + const addedColorableItems = new Map(); + const updatedColorableItems = new Map(); + + page.offers.sort(sortByColorIndex); + + page.offers.forEach(offer => + { + if(!offer.product) return; + + const furniData = offer.product.furnitureData; + + if(!furniData || !furniData.hasIndexedColor) + { + offers.push(offer); + } + else + { + const name = furniData.className; + const colorIndex = furniData.colorIndex; + + if(!updatedColorableItems.has(name)) + { + updatedColorableItems.set(name, []); + } + + let selectedColor = 0xFFFFFF; + + if(furniData.colors) + { + for(let color of furniData.colors) + { + if(color !== 0xFFFFFF) // skip the white colors + { + selectedColor = color; + } + } + + if(updatedColorableItems.get(name).indexOf(selectedColor) === -1) + { + updatedColorableItems.get(name)[colorIndex] = selectedColor; + } + + } + + if(!addedColorableItems.has(name)) + { + offers.push(offer); + addedColorableItems.set(name, true); + } + } + }); + offers.sort(sortyByFurnitureClassName); + setColorableItems(updatedColorableItems); + return offers; + }, [ page.offers ]); + + return ( + + + + { (!colorsShowing || !currentOffer || !colorableItems.has(currentOffer.product.furnitureData.className)) && + offers.map((offer, index) => ) + } + { (colorsShowing && currentOffer && colorableItems.has(currentOffer.product.furnitureData.className)) && + colorableItems.get(currentOffer.product.furnitureData.className).map((color, index) => selectColor(index, currentOffer.product.furnitureData.className) } />) + } + + + + { !currentOffer && + <> + { !!page.localization.getImage(1) && } + + } + { currentOffer && + <> + + + + { currentOffer.product.furnitureData.hasIndexedColor && + } + + + + { currentOffer.localizationName } + + + + + + + + + } + + + ); +} diff --git a/apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx b/apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx new file mode 100644 index 0000000..b497e4e --- /dev/null +++ b/apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx @@ -0,0 +1,61 @@ +import { FC } from 'react'; +import { GetConfiguration, ProductTypeEnum } from '../../../../../api'; +import { Column, Flex, Grid, LayoutImage, Text } from '../../../../../common'; +import { useCatalog } from '../../../../../hooks'; +import { CatalogHeaderView } from '../../catalog-header/CatalogHeaderView'; +import { CatalogAddOnBadgeWidgetView } from '../widgets/CatalogAddOnBadgeWidgetView'; +import { CatalogItemGridWidgetView } from '../widgets/CatalogItemGridWidgetView'; +import { CatalogLimitedItemWidgetView } from '../widgets/CatalogLimitedItemWidgetView'; +import { CatalogPurchaseWidgetView } from '../widgets/CatalogPurchaseWidgetView'; +import { CatalogSpinnerWidgetView } from '../widgets/CatalogSpinnerWidgetView'; +import { CatalogTotalPriceWidget } from '../widgets/CatalogTotalPriceWidget'; +import { CatalogViewProductWidgetView } from '../widgets/CatalogViewProductWidgetView'; +import { CatalogLayoutProps } from './CatalogLayout.types'; + +export const CatalogLayoutDefaultView: FC = props => +{ + const { page = null } = props; + const { currentOffer = null, currentPage = null } = useCatalog(); + + return ( + <> + + + { GetConfiguration('catalog.headers') && + } + + + + { !currentOffer && + <> + { !!page.localization.getImage(1) && + } + + } + { currentOffer && + <> + + { (currentOffer.product.productType !== ProductTypeEnum.BADGE) && + <> + + + } + { (currentOffer.product.productType === ProductTypeEnum.BADGE) && } + + + + { currentOffer.localizationName } + + + + + + + + + } + + + + ); +} diff --git a/apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutGuildCustomFurniView.tsx b/apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutGuildCustomFurniView.tsx new file mode 100644 index 0000000..cf12951 --- /dev/null +++ b/apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutGuildCustomFurniView.tsx @@ -0,0 +1,48 @@ +import { FC } from 'react'; +import { Base, Column, Flex, Grid, Text } from '../../../../../common'; +import { useCatalog } from '../../../../../hooks'; +import { CatalogGuildBadgeWidgetView } from '../widgets/CatalogGuildBadgeWidgetView'; +import { CatalogGuildSelectorWidgetView } from '../widgets/CatalogGuildSelectorWidgetView'; +import { CatalogItemGridWidgetView } from '../widgets/CatalogItemGridWidgetView'; +import { CatalogPurchaseWidgetView } from '../widgets/CatalogPurchaseWidgetView'; +import { CatalogTotalPriceWidget } from '../widgets/CatalogTotalPriceWidget'; +import { CatalogViewProductWidgetView } from '../widgets/CatalogViewProductWidgetView'; +import { CatalogLayoutProps } from './CatalogLayout.types'; + +export const CatalogLayouGuildCustomFurniView: FC = props => +{ + const { page = null } = props; + const { currentOffer = null } = useCatalog(); + + return ( + + + + + + { !currentOffer && + <> + { !!page.localization.getImage(1) && } + + } + { currentOffer && + <> + + + + + + { currentOffer.localizationName } + + + + + + + + + } + + + ); +} diff --git a/apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutGuildForumView.tsx b/apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutGuildForumView.tsx new file mode 100644 index 0000000..b5a89ca --- /dev/null +++ b/apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutGuildForumView.tsx @@ -0,0 +1,49 @@ +import { CatalogGroupsComposer } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useState } from 'react'; +import { SendMessageComposer } from '../../../../../api'; +import { Base, Column, Flex, Grid, Text } from '../../../../../common'; +import { useCatalog } from '../../../../../hooks'; +import { CatalogFirstProductSelectorWidgetView } from '../widgets/CatalogFirstProductSelectorWidgetView'; +import { CatalogGuildSelectorWidgetView } from '../widgets/CatalogGuildSelectorWidgetView'; +import { CatalogPurchaseWidgetView } from '../widgets/CatalogPurchaseWidgetView'; +import { CatalogTotalPriceWidget } from '../widgets/CatalogTotalPriceWidget'; +import { CatalogLayoutProps } from './CatalogLayout.types'; + +export const CatalogLayouGuildForumView: FC = props => +{ + const { page = null } = props; + const [ selectedGroupIndex, setSelectedGroupIndex ] = useState(0); + const { currentOffer = null, setCurrentOffer = null, catalogOptions = null } = useCatalog(); + const { groups = null } = catalogOptions; + + useEffect(() => + { + SendMessageComposer(new CatalogGroupsComposer()); + }, [ page ]); + + return ( + <> + + + + + + + { !!currentOffer && + <> + + { currentOffer.localizationName } + + + + + + + + + } + + + + ); +} diff --git a/apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutGuildFrontpageView.tsx b/apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutGuildFrontpageView.tsx new file mode 100644 index 0000000..d467aa4 --- /dev/null +++ b/apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutGuildFrontpageView.tsx @@ -0,0 +1,29 @@ +import { FC } from 'react'; +import { CreateLinkEvent, LocalizeText } from '../../../../../api'; +import { Base } from '../../../../../common/Base'; +import { Button } from '../../../../../common/Button'; +import { Column } from '../../../../../common/Column'; +import { Grid } from '../../../../../common/Grid'; +import { LayoutImage } from '../../../../../common/layout/LayoutImage'; +import { CatalogLayoutProps } from './CatalogLayout.types'; + +export const CatalogLayouGuildFrontpageView: FC = props => +{ + const { page = null } = props; + + return ( + + + + + + + + + + + + ); +} diff --git a/apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutInfoLoyaltyView.tsx b/apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutInfoLoyaltyView.tsx new file mode 100644 index 0000000..06c4c03 --- /dev/null +++ b/apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutInfoLoyaltyView.tsx @@ -0,0 +1,15 @@ +import { FC } from 'react'; +import { CatalogLayoutProps } from './CatalogLayout.types'; + +export const CatalogLayoutInfoLoyaltyView: FC = props => +{ + const { page = null } = props; + + return ( +
+
+
+
+
+ ); +} diff --git a/apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutPets2View.tsx b/apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutPets2View.tsx new file mode 100644 index 0000000..38ad284 --- /dev/null +++ b/apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutPets2View.tsx @@ -0,0 +1,8 @@ +import { FC } from 'react'; +import { CatalogLayoutProps } from './CatalogLayout.types'; +import { CatalogLayoutPets3View } from './CatalogLayoutPets3View'; + +export const CatalogLayoutPets2View: FC = props => +{ + return +} diff --git a/apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutPets3View.tsx b/apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutPets3View.tsx new file mode 100644 index 0000000..2ce3e39 --- /dev/null +++ b/apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutPets3View.tsx @@ -0,0 +1,27 @@ +import { FC } from 'react'; +import { Base } from '../../../../../common/Base'; +import { Column } from '../../../../../common/Column'; +import { Flex } from '../../../../../common/Flex'; +import { CatalogLayoutProps } from './CatalogLayout.types'; + +export const CatalogLayoutPets3View: FC = props => +{ + const { page = null } = props; + + const imageUrl = page.localization.getImage(1); + + return ( + + + { imageUrl && } + + + + + + + + + + ); +} diff --git a/apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutRoomAdsView.tsx b/apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutRoomAdsView.tsx new file mode 100644 index 0000000..92dd4bf --- /dev/null +++ b/apps/frontend/src/components/catalog/views/page/layout/CatalogLayoutRoomAdsView.tsx @@ -0,0 +1,113 @@ +import { GetRoomAdPurchaseInfoComposer, GetUserEventCatsMessageComposer, PurchaseRoomAdMessageComposer, RoomAdPurchaseInfoEvent, RoomEntryData } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, SendMessageComposer } from '../../../../../api'; +import { Base, Button, Column, Text } from '../../../../../common'; +import { useCatalog, useMessageEvent, useNavigator, useRoomPromote } from '../../../../../hooks'; +import { CatalogLayoutProps } from './CatalogLayout.types'; + +export const CatalogLayoutRoomAdsView: FC = props => +{ + const { page = null } = props; + const [ eventName, setEventName ] = useState(''); + const [ eventDesc, setEventDesc ] = useState(''); + const [ roomId, setRoomId ] = useState(-1); + const [ availableRooms, setAvailableRooms ] = useState([]); + const [ extended, setExtended ] = useState(false); + const [ categoryId, setCategoryId ] = useState(1); + const { categories = null } = useNavigator(); + const { setIsVisible = null } = useCatalog(); + const { promoteInformation, isExtended, setIsExtended } = useRoomPromote(); + + useEffect(() => + { + if(isExtended) + { + setRoomId(promoteInformation.data.flatId); + setEventName(promoteInformation.data.eventName); + setEventDesc(promoteInformation.data.eventDescription); + setCategoryId(promoteInformation.data.categoryId); + setExtended(isExtended); // This is for sending to packet + setIsExtended(false); // This is from hook useRoomPromotte + } + + }, [ isExtended, eventName, eventDesc, categoryId ]); + + const resetData = () => + { + setRoomId(-1); + setEventName(''); + setEventDesc(''); + setCategoryId(1); + setIsExtended(false); + setIsVisible(false); + } + + const purchaseAd = () => + { + const pageId = page.pageId; + const offerId = page.offers.length >= 1 ? page.offers[0].offerId : -1; + const flatId = roomId; + const name = eventName; + const desc = eventDesc; + const catId = categoryId; + + SendMessageComposer(new PurchaseRoomAdMessageComposer(pageId, offerId, flatId, name, extended, desc, catId)); + resetData(); + } + + useMessageEvent(RoomAdPurchaseInfoEvent, event => + { + const parser = event.getParser(); + + if(!parser) return; + + setAvailableRooms(parser.rooms); + }); + + useEffect(() => + { + SendMessageComposer(new GetRoomAdPurchaseInfoComposer()); + // TODO: someone needs to fix this for morningstar + SendMessageComposer(new GetUserEventCatsMessageComposer()); + }, []); + + return (<> + { LocalizeText('roomad.catalog_header') } + + { LocalizeText('roomad.catalog_text', [ 'duration' ], [ '120' ]) } + + + { LocalizeText('navigator.category') } + + + + { LocalizeText('roomad.catalog_name') } + setEventName(event.target.value) } readOnly={ extended } /> + + + { LocalizeText('roomad.catalog_description') } + + { LocalizeText('friendlist.invite.note') } + + + + + + + ); +}; diff --git a/apps/frontend/src/components/friends/views/friends-list/FriendsListSearchView.tsx b/apps/frontend/src/components/friends/views/friends-list/FriendsListSearchView.tsx new file mode 100644 index 0000000..69073a2 --- /dev/null +++ b/apps/frontend/src/components/friends/views/friends-list/FriendsListSearchView.tsx @@ -0,0 +1,103 @@ +import { HabboSearchComposer, HabboSearchResultData, HabboSearchResultEvent } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, OpenMessengerChat, SendMessageComposer } from '../../../../api'; +import { Base, Column, Flex, NitroCardAccordionItemView, NitroCardAccordionSetView, NitroCardAccordionSetViewProps, Text, UserProfileIconView } from '../../../../common'; +import { useFriends, useMessageEvent } from '../../../../hooks'; + +interface FriendsSearchViewProps extends NitroCardAccordionSetViewProps +{ + +} + +export const FriendsSearchView: FC = props => +{ + const { ...rest } = props; + const [ searchValue, setSearchValue ] = useState(''); + const [ friendResults, setFriendResults ] = useState(null); + const [ otherResults, setOtherResults ] = useState(null); + const { canRequestFriend = null, requestFriend = null } = useFriends(); + + useMessageEvent(HabboSearchResultEvent, event => + { + const parser = event.getParser(); + + setFriendResults(parser.friends); + setOtherResults(parser.others); + }); + + useEffect(() => + { + if(!searchValue || !searchValue.length) return; + + const timeout = setTimeout(() => + { + if(!searchValue || !searchValue.length) return; + + SendMessageComposer(new HabboSearchComposer(searchValue)); + }, 500); + + return () => clearTimeout(timeout); + }, [ searchValue ]); + + return ( + + setSearchValue(event.target.value) } /> + + { friendResults && + <> + { (friendResults.length === 0) && + { LocalizeText('friendlist.search.nofriendsfound') } } + { (friendResults.length > 0) && + + { LocalizeText('friendlist.search.friendscaption', [ 'cnt' ], [ friendResults.length.toString() ]) } +
+ + { friendResults.map(result => + { + return ( + + + +
{ result.avatarName }
+
+ + { result.isAvatarOnline && + OpenMessengerChat(result.avatarId) } title={ LocalizeText('friendlist.tip.im') } /> } + +
+ ) + }) } +
+
} + } + { otherResults && + <> + { (otherResults.length === 0) && + { LocalizeText('friendlist.search.noothersfound') } } + { (otherResults.length > 0) && + + { LocalizeText('friendlist.search.otherscaption', [ 'cnt' ], [ otherResults.length.toString() ]) } +
+ + { otherResults.map(result => + { + return ( + + + +
{ result.avatarName }
+
+ + { canRequestFriend(result.avatarId) && + requestFriend(result.avatarId, result.avatarName) } title={ LocalizeText('friendlist.tip.addfriend') } /> } + +
+ ) + }) } +
+
} + } +
+
+ ); +} diff --git a/apps/frontend/src/components/friends/views/friends-list/FriendsListView.tsx b/apps/frontend/src/components/friends/views/friends-list/FriendsListView.tsx new file mode 100644 index 0000000..d98e93a --- /dev/null +++ b/apps/frontend/src/components/friends/views/friends-list/FriendsListView.tsx @@ -0,0 +1,150 @@ +import { ILinkEventTracker, RemoveFriendComposer, SendRoomInviteComposer } from '@nitrots/nitro-renderer'; +import { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import { AddEventLinkTracker, LocalizeText, MessengerFriend, RemoveLinkEventTracker, SendMessageComposer } from '../../../../api'; +import { Button, Flex, NitroCardAccordionSetView, NitroCardAccordionView, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common'; +import { useFriends } from '../../../../hooks'; +import { FriendsListGroupView } from './friends-list-group/FriendsListGroupView'; +import { FriendsListRequestView } from './friends-list-request/FriendsListRequestView'; +import { FriendsRemoveConfirmationView } from './FriendsListRemoveConfirmationView'; +import { FriendsRoomInviteView } from './FriendsListRoomInviteView'; +import { FriendsSearchView } from './FriendsListSearchView'; + +export const FriendsListView: FC<{}> = props => +{ + const [ isVisible, setIsVisible ] = useState(false); + const [ selectedFriendsIds, setSelectedFriendsIds ] = useState([]); + const [ showRoomInvite, setShowRoomInvite ] = useState(false); + const [ showRemoveFriendsConfirmation, setShowRemoveFriendsConfirmation ] = useState(false); + const { onlineFriends = [], offlineFriends = [], requests = [], requestFriend = null } = useFriends(); + + const removeFriendsText = useMemo(() => + { + if(!selectedFriendsIds || !selectedFriendsIds.length) return ''; + + const userNames: string[] = []; + + for(const userId of selectedFriendsIds) + { + let existingFriend: MessengerFriend = onlineFriends.find(f => f.id === userId); + + if(!existingFriend) existingFriend = offlineFriends.find(f => f.id === userId); + + if(!existingFriend) continue; + + userNames.push(existingFriend.name); + } + + return LocalizeText('friendlist.removefriendconfirm.userlist', [ 'user_names' ], [ userNames.join(', ') ]); + }, [ offlineFriends, onlineFriends, selectedFriendsIds ]); + + const selectFriend = useCallback((userId: number) => + { + if(userId < 0) return; + + setSelectedFriendsIds(prevValue => + { + const newValue = [ ...prevValue ]; + + const existingUserIdIndex: number = newValue.indexOf(userId); + + if(existingUserIdIndex > -1) + { + newValue.splice(existingUserIdIndex, 1) + } + else + { + newValue.push(userId); + } + + return newValue; + }); + }, [ setSelectedFriendsIds ]); + + const sendRoomInvite = (message: string) => + { + if(!selectedFriendsIds.length || !message || !message.length || (message.length > 255)) return; + + SendMessageComposer(new SendRoomInviteComposer(message, selectedFriendsIds)); + + setShowRoomInvite(false); + } + + const removeSelectedFriends = () => + { + if(selectedFriendsIds.length === 0) return; + + setSelectedFriendsIds(prevValue => + { + SendMessageComposer(new RemoveFriendComposer(...prevValue)); + + return []; + }); + + setShowRemoveFriendsConfirmation(false); + } + + 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(prevValue => !prevValue); + return; + case 'request': + if(parts.length < 4) return; + + requestFriend(parseInt(parts[2]), parts[3]); + } + }, + eventUrlPrefix: 'friends/' + }; + + AddEventLinkTracker(linkTracker); + + return () => RemoveLinkEventTracker(linkTracker); + }, [ requestFriend ]); + + if(!isVisible) return null; + + return ( + <> + + setIsVisible(false) } /> + + + + + + + + + + + + { selectedFriendsIds && selectedFriendsIds.length > 0 && + + + + } + + + { showRoomInvite && + setShowRoomInvite(false) } sendRoomInvite={ sendRoomInvite } /> } + { showRemoveFriendsConfirmation && + setShowRemoveFriendsConfirmation(false) } removeSelectedFriends={ removeSelectedFriends } /> } + + ); +}; diff --git a/apps/frontend/src/components/friends/views/friends-list/friends-list-group/FriendsListGroupItemView.tsx b/apps/frontend/src/components/friends/views/friends-list/friends-list-group/FriendsListGroupItemView.tsx new file mode 100644 index 0000000..03cd7e9 --- /dev/null +++ b/apps/frontend/src/components/friends/views/friends-list/friends-list-group/FriendsListGroupItemView.tsx @@ -0,0 +1,85 @@ +import { FC, MouseEvent, useState } from 'react'; +import { LocalizeText, MessengerFriend, OpenMessengerChat } from '../../../../../api'; +import { Base, Flex, NitroCardAccordionItemView, UserProfileIconView } from '../../../../../common'; +import { useFriends } from '../../../../../hooks'; + +export const FriendsListGroupItemView: FC<{ friend: MessengerFriend, selected: boolean, selectFriend: (userId: number) => void }> = props => +{ + const { friend = null, selected = false, selectFriend = null } = props; + const [ isRelationshipOpen, setIsRelationshipOpen ] = useState(false); + const { followFriend = null, updateRelationship = null } = useFriends(); + + const clickFollowFriend = (event: MouseEvent) => + { + event.stopPropagation(); + + followFriend(friend); + } + + const openMessengerChat = (event: MouseEvent) => + { + event.stopPropagation(); + + OpenMessengerChat(friend.id); + } + + const openRelationship = (event: MouseEvent) => + { + event.stopPropagation(); + + setIsRelationshipOpen(true); + } + + const clickUpdateRelationship = (event: MouseEvent, type: number) => + { + event.stopPropagation(); + + updateRelationship(friend, type); + + setIsRelationshipOpen(false); + } + + const getCurrentRelationshipName = () => + { + if(!friend) return 'none'; + + switch(friend.relationshipStatus) + { + case MessengerFriend.RELATIONSHIP_HEART: return 'heart'; + case MessengerFriend.RELATIONSHIP_SMILE: return 'smile'; + case MessengerFriend.RELATIONSHIP_BOBBA: return 'bobba'; + default: return 'none'; + } + } + + if(!friend) return null; + + return ( + selectFriend(friend.id) }> + + event.stopPropagation() }> + + +
{ friend.name }
+
+ + { !isRelationshipOpen && + <> + { friend.followingAllowed && + } + { friend.online && + } + { (friend.id > 0) && + } + } + { isRelationshipOpen && + <> + clickUpdateRelationship(event, MessengerFriend.RELATIONSHIP_HEART) } /> + clickUpdateRelationship(event, MessengerFriend.RELATIONSHIP_SMILE) } /> + clickUpdateRelationship(event, MessengerFriend.RELATIONSHIP_BOBBA) } /> + clickUpdateRelationship(event, MessengerFriend.RELATIONSHIP_NONE) } /> + } + +
+ ); +} diff --git a/apps/frontend/src/components/friends/views/friends-list/friends-list-group/FriendsListGroupView.tsx b/apps/frontend/src/components/friends/views/friends-list/friends-list-group/FriendsListGroupView.tsx new file mode 100644 index 0000000..c593003 --- /dev/null +++ b/apps/frontend/src/components/friends/views/friends-list/friends-list-group/FriendsListGroupView.tsx @@ -0,0 +1,23 @@ +import { FC } from 'react'; +import { MessengerFriend } from '../../../../../api'; +import { FriendsListGroupItemView } from './FriendsListGroupItemView'; + +interface FriendsListGroupViewProps +{ + list: MessengerFriend[]; + selectedFriendsIds: number[]; + selectFriend: (userId: number) => void; +} + +export const FriendsListGroupView: FC = props => +{ + const { list = null, selectedFriendsIds = null, selectFriend = null } = props; + + if(!list || !list.length) return null; + + return ( + <> + { list.map((item, index) => = 0) } selectFriend={ selectFriend } />) } + + ); +} diff --git a/apps/frontend/src/components/friends/views/friends-list/friends-list-request/FriendsListRequestItemView.tsx b/apps/frontend/src/components/friends/views/friends-list/friends-list-request/FriendsListRequestItemView.tsx new file mode 100644 index 0000000..de5d3a3 --- /dev/null +++ b/apps/frontend/src/components/friends/views/friends-list/friends-list-request/FriendsListRequestItemView.tsx @@ -0,0 +1,25 @@ +import { FC } from 'react'; +import { MessengerRequest } from '../../../../../api'; +import { Base, Flex, NitroCardAccordionItemView, UserProfileIconView } from '../../../../../common'; +import { useFriends } from '../../../../../hooks'; + +export const FriendsListRequestItemView: FC<{ request: MessengerRequest }> = props => +{ + const { request = null } = props; + const { requestResponse = null } = useFriends(); + + if(!request) return null; + + return ( + + + +
{ request.name }
+
+ + requestResponse(request.id, true) } /> + requestResponse(request.id, false) } /> + +
+ ); +} diff --git a/apps/frontend/src/components/friends/views/friends-list/friends-list-request/FriendsListRequestView.tsx b/apps/frontend/src/components/friends/views/friends-list/friends-list-request/FriendsListRequestView.tsx new file mode 100644 index 0000000..5f6e991 --- /dev/null +++ b/apps/frontend/src/components/friends/views/friends-list/friends-list-request/FriendsListRequestView.tsx @@ -0,0 +1,29 @@ +import { FC } from 'react'; +import { LocalizeText } from '../../../../../api'; +import { Button, Column, Flex, NitroCardAccordionSetView, NitroCardAccordionSetViewProps } from '../../../../../common'; +import { useFriends } from '../../../../../hooks'; +import { FriendsListRequestItemView } from './FriendsListRequestItemView'; + +export const FriendsListRequestView: FC = props => +{ + const { children = null, ...rest } = props; + const { requests = [], requestResponse = null } = useFriends(); + + if(!requests.length) return null; + + return ( + + + + { requests.map((request, index) => ) } + + + + + + { children } + + ); +} diff --git a/apps/frontend/src/components/friends/views/messenger/FriendsMessengerView.tsx b/apps/frontend/src/components/friends/views/messenger/FriendsMessengerView.tsx new file mode 100644 index 0000000..9246e7a --- /dev/null +++ b/apps/frontend/src/components/friends/views/messenger/FriendsMessengerView.tsx @@ -0,0 +1,177 @@ +import { FollowFriendMessageComposer, ILinkEventTracker } from '@nitrots/nitro-renderer'; +import { FC, KeyboardEvent, useEffect, useRef, useState } from 'react'; +import { FaTimes } from 'react-icons/fa'; +import { AddEventLinkTracker, GetSessionDataManager, GetUserProfile, LocalizeText, RemoveLinkEventTracker, ReportType, SendMessageComposer } from '../../../../api'; +import { Base, Button, ButtonGroup, Column, Flex, Grid, LayoutAvatarImageView, LayoutBadgeImageView, LayoutGridItem, LayoutItemCountView, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common'; +import { useHelp, useMessenger } from '../../../../hooks'; +import { FriendsMessengerThreadView } from './messenger-thread/FriendsMessengerThreadView'; + +export const FriendsMessengerView: FC<{}> = props => +{ + const [ isVisible, setIsVisible ] = useState(false); + const [ lastThreadId, setLastThreadId ] = useState(-1); + const [ messageText, setMessageText ] = useState(''); + const { visibleThreads = [], activeThread = null, getMessageThread = null, sendMessage = null, setActiveThreadId = null, closeThread = null } = useMessenger(); + const { report = null } = useHelp(); + const messagesBox = useRef(); + + const followFriend = () => (activeThread && activeThread.participant && SendMessageComposer(new FollowFriendMessageComposer(activeThread.participant.id))); + const openProfile = () => (activeThread && activeThread.participant && GetUserProfile(activeThread.participant.id)); + + const send = () => + { + if(!activeThread || !messageText.length) return; + + sendMessage(activeThread, GetSessionDataManager().userId, messageText); + + setMessageText(''); + } + + const onKeyDown = (event: KeyboardEvent) => + { + if(event.key !== 'Enter') return; + + send(); + } + + useEffect(() => + { + const linkTracker: ILinkEventTracker = { + linkReceived: (url: string) => + { + const parts = url.split('/'); + + if(parts.length === 2) + { + if(parts[1] === 'open') + { + setIsVisible(true); + + return; + } + + if(parts[1] === 'toggle') + { + setIsVisible(prevValue => !prevValue); + + return; + } + + const thread = getMessageThread(parseInt(parts[1])); + + if(!thread) return; + + setActiveThreadId(thread.threadId); + setIsVisible(true); + } + }, + eventUrlPrefix: 'friends-messenger/' + }; + + AddEventLinkTracker(linkTracker); + + return () => RemoveLinkEventTracker(linkTracker); + }, [ getMessageThread, setActiveThreadId ]); + + useEffect(() => + { + if(!isVisible || !activeThread) return; + + messagesBox.current.scrollTop = messagesBox.current.scrollHeight; + }, [ isVisible, activeThread ]); + + useEffect(() => + { + if(isVisible && !activeThread) + { + if(lastThreadId > 0) + { + setActiveThreadId(lastThreadId); + } + else + { + if(visibleThreads.length > 0) setActiveThreadId(visibleThreads[0].threadId); + } + + return; + } + + if(!isVisible && activeThread) + { + setLastThreadId(activeThread.threadId); + setActiveThreadId(-1); + } + }, [ isVisible, activeThread, lastThreadId, visibleThreads, setActiveThreadId ]); + + if(!isVisible) return null; + + return ( + + setIsVisible(false) } /> + + + + { LocalizeText('toolbar.icon.label.messenger') } + + + { visibleThreads && (visibleThreads.length > 0) && visibleThreads.map(thread => + { + return ( + setActiveThreadId(thread.threadId) }> + { thread.unread && + } + + + { (thread.participant.id > 0) && + } + { (thread.participant.id <= 0) && + } + + { thread.participant.name } + + + ); + }) } + + + + + { activeThread && + <> + { LocalizeText('messenger.window.separator', [ 'FRIEND_NAME' ], [ activeThread.participant.name ]) } + + + + + + + + + + + + + + + + + setMessageText(event.target.value) } onKeyDown={ onKeyDown } /> + + + } + + + + + ); +} diff --git a/apps/frontend/src/components/friends/views/messenger/messenger-thread/FriendsMessengerThreadGroup.tsx b/apps/frontend/src/components/friends/views/messenger/messenger-thread/FriendsMessengerThreadGroup.tsx new file mode 100644 index 0000000..ad3d755 --- /dev/null +++ b/apps/frontend/src/components/friends/views/messenger/messenger-thread/FriendsMessengerThreadGroup.tsx @@ -0,0 +1,72 @@ +import { FC, useMemo } from 'react'; +import { GetGroupChatData, GetSessionDataManager, LocalizeText, MessengerGroupType, MessengerThread, MessengerThreadChat, MessengerThreadChatGroup } from '../../../../../api'; +import { Base, Flex, LayoutAvatarImageView } from '../../../../../common'; + +export const FriendsMessengerThreadGroup: FC<{ thread: MessengerThread, group: MessengerThreadChatGroup }> = props => +{ + const { thread = null, group = null } = props; + + const groupChatData = useMemo(() => ((group.type === MessengerGroupType.GROUP_CHAT) && GetGroupChatData(group.chats[0].extraData)), [ group ]); + + const isOwnChat = useMemo(() => + { + if(!thread || !group) return false; + + if((group.type === MessengerGroupType.PRIVATE_CHAT) && (group.userId === GetSessionDataManager().userId)) return true; + + if(groupChatData && group.chats.length && (groupChatData.userId === GetSessionDataManager().userId)) return true; + + return false; + }, [ thread, group, groupChatData ]); + + if(!thread || !group) return null; + + if(!group.userId) + { + return ( + <> + { group.chats.map((chat, index) => + { + return ( + + + { (chat.type === MessengerThreadChat.SECURITY_NOTIFICATION) && + + + { chat.message } + } + { (chat.type === MessengerThreadChat.ROOM_INVITE) && + + + { (LocalizeText('messenger.invitation') + ' ') }{ chat.message } + } + + + ); + }) } + + ); + } + + return ( + + + { ((group.type === MessengerGroupType.PRIVATE_CHAT) && !isOwnChat) && + } + { (groupChatData && !isOwnChat) && + } + + + + { isOwnChat && GetSessionDataManager().userName } + { !isOwnChat && (groupChatData ? groupChatData.username : thread.participant.name) } + + { group.chats.map((chat, index) => { chat.message }) } + + { isOwnChat && + + + } + + ); +} diff --git a/apps/frontend/src/components/friends/views/messenger/messenger-thread/FriendsMessengerThreadView.tsx b/apps/frontend/src/components/friends/views/messenger/messenger-thread/FriendsMessengerThreadView.tsx new file mode 100644 index 0000000..962a668 --- /dev/null +++ b/apps/frontend/src/components/friends/views/messenger/messenger-thread/FriendsMessengerThreadView.tsx @@ -0,0 +1,16 @@ +import { FC } from 'react'; +import { MessengerThread } from '../../../../../api'; +import { FriendsMessengerThreadGroup } from './FriendsMessengerThreadGroup'; + +export const FriendsMessengerThreadView: FC<{ thread: MessengerThread }> = props => +{ + const { thread = null } = props; + + thread.setRead(); + + return ( + <> + { (thread.groups.length > 0) && thread.groups.map((group, index) => ) } + + ); +} diff --git a/apps/frontend/src/components/game-center/GameCenterView.scss b/apps/frontend/src/components/game-center/GameCenterView.scss new file mode 100644 index 0000000..246278d --- /dev/null +++ b/apps/frontend/src/components/game-center/GameCenterView.scss @@ -0,0 +1,44 @@ +.game-center-stage { + width: 100%; + height: calc(100% - 55px); + background-color: black; + position: absolute; + inset: 0; +} +.game-center-main { + width: 1280px; + height: calc(100% - 55px); + background: #93d4f3; + + .game-view { + background-position: bottom center; + background-repeat: no-repeat; + + > div { + color: inherit; + } + } + + .gameList-container { + min-height: 107px; + + .game-icon { + width: 83px; + height: 83px; + background-position: center; + background-repeat: no-repeat; + + &.selected { + position: relative; + &::after { + content: ''; + background-image: url('@/assets/images/gamecenter/selectedIcon.png'); + width: 83px; + height: 83px; + position: absolute; + inset: 0; + } + } + } + } +} diff --git a/apps/frontend/src/components/game-center/GameCenterView.tsx b/apps/frontend/src/components/game-center/GameCenterView.tsx new file mode 100644 index 0000000..f0955ae --- /dev/null +++ b/apps/frontend/src/components/game-center/GameCenterView.tsx @@ -0,0 +1,50 @@ +import { ILinkEventTracker } from '@nitrots/nitro-renderer'; +import { useEffect } from 'react'; +import { AddEventLinkTracker, RemoveLinkEventTracker } from '../../api'; +import { Flex } from '../../common'; +import { useGameCenter } from '../../hooks'; +import { GameListView } from './views/GameListView'; +import { GameStageView } from './views/GameStageView'; +import { GameView } from './views/GameView'; + +export const GameCenterView = () => +{ + const{ isVisible, setIsVisible, games, accountStatus } = useGameCenter(); + + useEffect(() => + { + const toggleGameCenter = () => + { + setIsVisible(prev => !prev); + } + + const linkTracker: ILinkEventTracker = { + linkReceived: (url: string) => + { + const value = url.split('/'); + + switch(value[1]) + { + case 'toggle': + toggleGameCenter(); + break; + } + }, + eventUrlPrefix: 'games/' + }; + + AddEventLinkTracker(linkTracker); + + return () => RemoveLinkEventTracker(linkTracker); + }, [ ]); + + if(!isVisible || !games || !accountStatus) return; + + return + + + + + + +} diff --git a/apps/frontend/src/components/game-center/views/GameListView.tsx b/apps/frontend/src/components/game-center/views/GameListView.tsx new file mode 100644 index 0000000..1211bc7 --- /dev/null +++ b/apps/frontend/src/components/game-center/views/GameListView.tsx @@ -0,0 +1,32 @@ +import { GameConfigurationData } from '@nitrots/nitro-renderer'; +import { LocalizeText } from '../../../api'; +import { Base, Flex } from '../../../common'; +import { useGameCenter } from '../../../hooks'; + +export const GameListView = () => +{ + const { games,selectedGame, setSelectedGame } = useGameCenter(); + + const getClasses = (game: GameConfigurationData) => + { + let classes = [ 'game-icon' ]; + + if(selectedGame === game) classes.push('selected'); + + return classes.join(' '); + } + + const getIconImage = (game: GameConfigurationData): string => + { + return `url(${ game.assetUrl }${ game.gameNameId }_icon.png)` + } + + return + { LocalizeText('gamecenter.game_list_title') } + + { games && games.map((game,index) => + setSelectedGame(game) } style={ { backgroundImage: getIconImage(game) } }/> + ) } + + +} diff --git a/apps/frontend/src/components/game-center/views/GameStageView.tsx b/apps/frontend/src/components/game-center/views/GameStageView.tsx new file mode 100644 index 0000000..0a26fea --- /dev/null +++ b/apps/frontend/src/components/game-center/views/GameStageView.tsx @@ -0,0 +1,47 @@ +import { Game2ExitGameMessageComposer } from '@nitrots/nitro-renderer'; +import { useEffect, useRef, useState } from 'react'; +import { SendMessageComposer } from '../../../api'; +import { Base } from '../../../common'; +import { useGameCenter } from '../../../hooks'; + +export const GameStageView = () => +{ + const { gameURL,setGameURL } = useGameCenter(); + const [ loadTimes, setLoadTimes ] = useState(0); + const ref = useRef(); + + useEffect(()=> + { + if(!ref || ref && !ref.current) return; + + setLoadTimes(0); + + let frame: HTMLIFrameElement = document.createElement('iframe'); + + frame.src = gameURL; + frame.classList.add('game-center-stage'); + frame.classList.add('h-100'); + + frame.onload = () => + { + setLoadTimes(prev => prev += 1) + } + + ref.current.innerHTML = ''; + ref.current.appendChild(frame); + + },[ ref, gameURL ]); + + useEffect(()=> + { + if(loadTimes > 1) + { + setGameURL(null); + SendMessageComposer(new Game2ExitGameMessageComposer()); + } + },[ loadTimes,setGameURL ]) + + if(!gameURL) return null; + + return +} diff --git a/apps/frontend/src/components/game-center/views/GameView.tsx b/apps/frontend/src/components/game-center/views/GameView.tsx new file mode 100644 index 0000000..a9d641f --- /dev/null +++ b/apps/frontend/src/components/game-center/views/GameView.tsx @@ -0,0 +1,58 @@ +import { Game2GetAccountGameStatusMessageComposer, GetGameStatusMessageComposer, JoinQueueMessageComposer } from '@nitrots/nitro-renderer'; +import { useEffect } from 'react'; +import { ColorUtils, LocalizeText, SendMessageComposer } from '../../../api'; +import { Base, Button, Flex, LayoutItemCountView, Text } from '../../../common'; +import { useGameCenter } from '../../../hooks'; + +export const GameView = () => +{ + const { selectedGame, accountStatus } = useGameCenter(); + + useEffect(()=> + { + if(selectedGame) + { + SendMessageComposer(new GetGameStatusMessageComposer(selectedGame.gameId)); + SendMessageComposer(new Game2GetAccountGameStatusMessageComposer(selectedGame.gameId)); + } + },[ selectedGame ]) + + const getBgColour = (): string => + { + return ColorUtils.uintHexColor(selectedGame.bgColor) + } + + const getBgImage = (): string => + { + return `url(${ selectedGame.assetUrl }${ selectedGame.gameNameId }_theme.png)` + } + + const getColor = () => + { + return ColorUtils.uintHexColor(selectedGame.textColor); + } + + const onPlay = () => + { + SendMessageComposer(new JoinQueueMessageComposer(selectedGame.gameId)); + } + + return + + { LocalizeText(`gamecenter.${ selectedGame.gameNameId }.description_title`) } + + { (accountStatus.hasUnlimitedGames || accountStatus.freeGamesLeft > 0) && <> + + } + { LocalizeText(`gamecenter.${ selectedGame.gameNameId }.description_content`) } + + + + + + +} diff --git a/apps/frontend/src/components/groups/GroupView.scss b/apps/frontend/src/components/groups/GroupView.scss new file mode 100644 index 0000000..8882b93 --- /dev/null +++ b/apps/frontend/src/components/groups/GroupView.scss @@ -0,0 +1,190 @@ +.nitro-group-tab-image { + width: 122px; + height: 68px; + background: url('@/assets/images/groups/creator_images.png') no-repeat; + + &.tab-1 { + background-position: 0px 0px; + width: 99px; + height: 50px; + } + + &.tab-2 { + background-position: -99px 0px; + width: 98px; + height: 62px; + } + + &.tab-3 { + background-position: 0px -50px; + width: 96px; + height: 45px; + } + + &.tab-4, + &.tab-5 { + background-position: 0px -95px; + width: 114px; + height: 61px; + } +} + +.group-information { + width: 100%; + height: 100%; + display: flex; + + .group-badge { + width: 78px; + height: 78px; + + .badge-image { + background-size: contain; + } + } + + .group-description { + height: 55px; + } +} + +.nitro-group-information-standalone { + width: 500px; +} + +.nitro-group-members { + width: 400px; + max-height: 380px; + + .nitro-group-members-list-grid { + + .member-list-item { + height: 50px; + max-height: 50px; + + .avatar-head { + position: relative; + overflow: hidden; + width: 40px; + height: 50px; + + .avatar-image { + position: absolute; + left: -25px; + top: -20px; + } + } + } + } +} + +.group-badge-preview { + width: 42px; + height: 42px; + background-color: $grid-bg-color; + + &.active { + border-color: $grid-active-border-color !important; + background-color: $grid-active-bg-color; + + &:before { + position: absolute; + content: ' '; + width: 0; + height: 0; + border-top: 10px solid white; + border-left: 10px solid transparent; + border-right: 10px solid transparent; + bottom: -10px; + } + } +} + +.group-badge-color-swatch, +.group-badge-position-swatch { + position: relative; + border-radius: $border-radius; + width: 16px; + height: 16px; + background: $white; + border: 2px solid $white; + box-shadow: inset 3px 3px rgba(0, 0, 0, .1); + + &.active { + box-shadow: none; + } +} + +.group-badge-position-swatch { + box-shadow: inset 3px 3px rgba(0, 0, 0, .1); + + &.active { + background: $primary; + } +} + +.group-badge-color-swatch { + box-shadow: inset 2px 2px rgba(0, 0, 0, .2); +} + +.group-color-swatch { + width: 30px; + height: 40px; +} + +.nitro-group-manager { + height: $nitro-group-manager-height; + width: $nitro-group-manager-width; +} + +.nitro-group-creator { + height: $nitro-group-manager-height; + width: $nitro-group-manager-width; + + .creator-tabs { + + .tab { + position: relative; + margin-left: -6px; + background-image: url('@/assets/images/groups/creator_tabs.png'); + background-repeat: no-repeat; + + &:first-child { + margin-left: 0; + } + + &.tab-blue-flat { + width: 84px; + height: 24px; + background-position: 0px 0px; + + &.active { + height: 28px; + background-position: 0px -24px; + } + } + + &.tab-blue-arrow { + width: 83px; + height: 24px; + background-position: 0px -52px; + + &.active { + height: 28px; + background-position: 0px -76px; + } + } + + &.tab-yellow { + width: 133px; + height: 28px; + background-position: 0px -104px; + + &.active { + height: 33px; + background-position: 0px -132px; + } + } + } + } +} diff --git a/apps/frontend/src/components/groups/GroupsView.tsx b/apps/frontend/src/components/groups/GroupsView.tsx new file mode 100644 index 0000000..bbaee28 --- /dev/null +++ b/apps/frontend/src/components/groups/GroupsView.tsx @@ -0,0 +1,63 @@ +import { GroupPurchasedEvent, GroupSettingsComposer, ILinkEventTracker } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useState } from 'react'; +import { AddEventLinkTracker, RemoveLinkEventTracker, SendMessageComposer, TryVisitRoom } from '../../api'; +import { useGroup, useMessageEvent } from '../../hooks'; +import { GroupCreatorView } from './views/GroupCreatorView'; +import { GroupInformationStandaloneView } from './views/GroupInformationStandaloneView'; +import { GroupManagerView } from './views/GroupManagerView'; +import { GroupMembersView } from './views/GroupMembersView'; + +export const GroupsView: FC<{}> = props => +{ + const [ isCreatorVisible, setCreatorVisible ] = useState(false); + const {} = useGroup(); + + useMessageEvent(GroupPurchasedEvent, event => + { + const parser = event.getParser(); + + setCreatorVisible(false); + TryVisitRoom(parser.roomId); + }); + + useEffect(() => + { + const linkTracker: ILinkEventTracker = { + linkReceived: (url: string) => + { + const parts = url.split('/'); + + if(parts.length < 2) return; + + switch(parts[1]) + { + case 'create': + setCreatorVisible(true); + return; + case 'manage': + if(!parts[2]) return; + + setCreatorVisible(false); + SendMessageComposer(new GroupSettingsComposer(Number(parts[2]))); + return; + } + }, + eventUrlPrefix: 'groups/' + }; + + AddEventLinkTracker(linkTracker); + + return () => RemoveLinkEventTracker(linkTracker); + }, []); + + return ( + <> + { isCreatorVisible && + setCreatorVisible(false) } /> } + { !isCreatorVisible && + } + + + + ); +}; diff --git a/apps/frontend/src/components/groups/views/GroupBadgeCreatorView.tsx b/apps/frontend/src/components/groups/views/GroupBadgeCreatorView.tsx new file mode 100644 index 0000000..8e32c92 --- /dev/null +++ b/apps/frontend/src/components/groups/views/GroupBadgeCreatorView.tsx @@ -0,0 +1,83 @@ +import { Dispatch, FC, SetStateAction, useState } from 'react'; +import { FaPlus, FaTimes } from 'react-icons/fa'; +import { GroupBadgePart } from '../../../api'; +import { Base, Column, Flex, Grid, LayoutBadgeImageView } from '../../../common'; +import { useGroup } from '../../../hooks'; + +interface GroupBadgeCreatorViewProps +{ + badgeParts: GroupBadgePart[]; + setBadgeParts: Dispatch>; +} + +const POSITIONS: number[] = [ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]; + +export const GroupBadgeCreatorView: FC = props => +{ + const { badgeParts = [], setBadgeParts = null } = props; + const [ selectedIndex, setSelectedIndex ] = useState(-1); + const { groupCustomize = null } = useGroup(); + + const setPartProperty = (partIndex: number, property: string, value: number) => + { + const newBadgeParts = [ ...badgeParts ]; + + newBadgeParts[partIndex][property] = value; + + setBadgeParts(newBadgeParts); + + if(property === 'key') setSelectedIndex(-1); + } + + if(!badgeParts || !badgeParts.length) return null; + + return ( + <> + { ((selectedIndex < 0) && badgeParts && (badgeParts.length > 0)) && badgeParts.map((part, index) => + { + return ( + + setSelectedIndex(index) }> + { (badgeParts[index].code && (badgeParts[index].code.length > 0)) && + } + { (!badgeParts[index].code || !badgeParts[index].code.length) && + + + } + + { (part.type !== GroupBadgePart.BASE) && + + { POSITIONS.map((position, posIndex) => + { + return setPartProperty(index, 'position', position) }> + }) } + } + + { (groupCustomize.badgePartColors.length > 0) && groupCustomize.badgePartColors.map((item, colorIndex) => + { + return setPartProperty(index, 'color', (colorIndex + 1)) }> + }) } + + + ); + }) } + { (selectedIndex >= 0) && + + { (badgeParts[selectedIndex].type === GroupBadgePart.SYMBOL) && + setPartProperty(selectedIndex, 'key', 0) }> + + + + } + { ((badgeParts[selectedIndex].type === GroupBadgePart.BASE) ? groupCustomize.badgeBases : groupCustomize.badgeSymbols).map((item, index) => + { + return ( + setPartProperty(selectedIndex, 'key', item.id) }> + + + ); + }) } + } + + ); +} diff --git a/apps/frontend/src/components/groups/views/GroupCreatorView.tsx b/apps/frontend/src/components/groups/views/GroupCreatorView.tsx new file mode 100644 index 0000000..cd64ef3 --- /dev/null +++ b/apps/frontend/src/components/groups/views/GroupCreatorView.tsx @@ -0,0 +1,164 @@ +import { GroupBuyComposer, GroupBuyDataComposer, GroupBuyDataEvent } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useState } from 'react'; +import { HasHabboClub, IGroupData, LocalizeText, SendMessageComposer } from '../../../api'; +import { Base, Button, Column, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../common'; +import { useMessageEvent } from '../../../hooks'; +import { GroupTabBadgeView } from './tabs/GroupTabBadgeView'; +import { GroupTabColorsView } from './tabs/GroupTabColorsView'; +import { GroupTabCreatorConfirmationView } from './tabs/GroupTabCreatorConfirmationView'; +import { GroupTabIdentityView } from './tabs/GroupTabIdentityView'; + +interface GroupCreatorViewProps +{ + onClose: () => void; +} + +const TABS: number[] = [ 1, 2, 3, 4 ]; + +export const GroupCreatorView: FC = props => +{ + const { onClose = null } = props; + const [ currentTab, setCurrentTab ] = useState(1); + const [ closeAction, setCloseAction ] = useState<{ action: () => boolean }>(null); + const [ groupData, setGroupData ] = useState(null); + const [ availableRooms, setAvailableRooms ] = useState<{ id: number, name: string }[]>(null); + const [ purchaseCost, setPurchaseCost ] = useState(0); + + const onCloseClose = () => + { + setCloseAction(null); + setGroupData(null); + + if(onClose) onClose(); + } + + const buyGroup = () => + { + if(!groupData) return; + + const badge = []; + + groupData.groupBadgeParts.forEach(part => + { + if(part.code) + { + badge.push(part.key); + badge.push(part.color); + badge.push(part.position); + } + }); + + SendMessageComposer(new GroupBuyComposer(groupData.groupName, groupData.groupDescription, groupData.groupHomeroomId, groupData.groupColors[0], groupData.groupColors[1], badge)); + } + + const previousStep = () => + { + if(closeAction && closeAction.action) + { + if(!closeAction.action()) return; + } + + if(currentTab === 1) + { + onClose(); + + return; + } + + setCurrentTab(value => value - 1); + } + + const nextStep = () => + { + if(closeAction && closeAction.action) + { + if(!closeAction.action()) return; + } + + if(currentTab === 4) + { + buyGroup(); + + return; + } + + setCurrentTab(value => (value === 4 ? value : value + 1)); + } + + useMessageEvent(GroupBuyDataEvent, event => + { + const parser = event.getParser(); + + const rooms: { id: number, name: string }[] = []; + + parser.availableRooms.forEach((name, id) => rooms.push({ id, name })); + + setAvailableRooms(rooms); + setPurchaseCost(parser.groupCost); + }); + + useEffect(() => + { + setCurrentTab(1); + + setGroupData({ + groupId: -1, + groupName: null, + groupDescription: null, + groupHomeroomId: -1, + groupState: 1, + groupCanMembersDecorate: true, + groupColors: null, + groupBadgeParts: null + }); + + SendMessageComposer(new GroupBuyDataComposer()); + }, [ setGroupData ]); + + if(!groupData) return null; + + return ( + + + + + { TABS.map((tab, index) => + { + return ( + + { LocalizeText(`group.create.steplabel.${ tab }`) } + + ); + }) } + + + + + + { LocalizeText(`group.create.stepcaption.${ currentTab }`) } + { LocalizeText(`group.create.stepdesc.${ currentTab }`) } + + + + { (currentTab === 1) && + } + { (currentTab === 2) && + } + { (currentTab === 3) && + } + { (currentTab === 4) && + } + + + + + + + + + ); +}; diff --git a/apps/frontend/src/components/groups/views/GroupInformationStandaloneView.tsx b/apps/frontend/src/components/groups/views/GroupInformationStandaloneView.tsx new file mode 100644 index 0000000..d4206d7 --- /dev/null +++ b/apps/frontend/src/components/groups/views/GroupInformationStandaloneView.tsx @@ -0,0 +1,29 @@ +import { GroupInformationEvent, GroupInformationParser } from '@nitrots/nitro-renderer'; +import { FC, useState } from 'react'; +import { LocalizeText } from '../../../api'; +import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../common'; +import { useMessageEvent } from '../../../hooks'; +import { GroupInformationView } from './GroupInformationView'; + +export const GroupInformationStandaloneView: FC<{}> = props => +{ + const [ groupInformation, setGroupInformation ] = useState(null); + + useMessageEvent(GroupInformationEvent, event => + { + const parser = event.getParser(); + + if((groupInformation && (groupInformation.id === parser.id)) || parser.flag) setGroupInformation(parser); + }); + + if(!groupInformation) return null; + + return ( + + setGroupInformation(null) } /> + + setGroupInformation(null) } /> + + + ); +}; diff --git a/apps/frontend/src/components/groups/views/GroupInformationView.tsx b/apps/frontend/src/components/groups/views/GroupInformationView.tsx new file mode 100644 index 0000000..da6bdc8 --- /dev/null +++ b/apps/frontend/src/components/groups/views/GroupInformationView.tsx @@ -0,0 +1,146 @@ +import { GroupInformationParser, GroupRemoveMemberComposer } from '@nitrots/nitro-renderer'; +import { FC } from 'react'; +import { CatalogPageName, CreateLinkEvent, GetGroupManager, GetGroupMembers, GetSessionDataManager, GroupMembershipType, GroupType, LocalizeText, SendMessageComposer, TryJoinGroup, TryVisitRoom } from '../../../api'; +import { Button, Column, Flex, Grid, GridProps, LayoutBadgeImageView, Text } from '../../../common'; +import { useNotification } from '../../../hooks'; + +const STATES: string[] = [ 'regular', 'exclusive', 'private' ]; + +interface GroupInformationViewProps extends GridProps +{ + groupInformation: GroupInformationParser; + onJoin?: () => void; + onClose?: () => void; +} + +export const GroupInformationView: FC = props => +{ + const { groupInformation = null, onClose = null, overflow = 'hidden', ...rest } = props; + const { showConfirm = null } = useNotification(); + + const isRealOwner = (groupInformation && (groupInformation.ownerName === GetSessionDataManager().userName)); + + const joinGroup = () => (groupInformation && TryJoinGroup(groupInformation.id)); + + const leaveGroup = () => + { + showConfirm(LocalizeText('group.leaveconfirm.desc'), () => + { + SendMessageComposer(new GroupRemoveMemberComposer(groupInformation.id, GetSessionDataManager().userId)); + + if(onClose) onClose(); + }, null); + } + + const getRoleIcon = () => + { + if(groupInformation.membershipType === GroupMembershipType.NOT_MEMBER || groupInformation.membershipType === GroupMembershipType.REQUEST_PENDING) return null; + + if(isRealOwner) return ; + + if(groupInformation.isAdmin) return ; + + return ; + } + + const getButtonText = () => + { + if(isRealOwner) return 'group.youareowner'; + + if(groupInformation.type === GroupType.PRIVATE && groupInformation.membershipType !== GroupMembershipType.MEMBER) return ''; + + if(groupInformation.membershipType === GroupMembershipType.MEMBER) return 'group.leave'; + + if((groupInformation.membershipType === GroupMembershipType.NOT_MEMBER) && groupInformation.type === GroupType.REGULAR) return 'group.join'; + + if(groupInformation.membershipType === GroupMembershipType.REQUEST_PENDING) return 'group.membershippending'; + + if((groupInformation.membershipType === GroupMembershipType.NOT_MEMBER) && groupInformation.type === GroupType.EXCLUSIVE) return 'group.requestmembership'; + } + + const handleButtonClick = () => + { + if((groupInformation.type === GroupType.PRIVATE) && (groupInformation.membershipType === GroupMembershipType.NOT_MEMBER)) return; + + if(groupInformation.membershipType === GroupMembershipType.MEMBER) + { + leaveGroup(); + + return; + } + + joinGroup(); + } + + const handleAction = (action: string) => + { + switch(action) + { + case 'members': + GetGroupMembers(groupInformation.id); + break; + case 'members_pending': + GetGroupMembers(groupInformation.id, 2); + break; + case 'manage': + GetGroupManager(groupInformation.id); + break; + case 'homeroom': + TryVisitRoom(groupInformation.roomId); + break; + case 'furniture': + CreateLinkEvent('catalog/open/' + CatalogPageName.GUILD_CUSTOM_FURNI); + break; + case 'popular_groups': + CreateLinkEvent('navigator/search/groups'); + break; + } + } + + if(!groupInformation) return null; + + return ( + + + + + + + handleAction('members') }>{ LocalizeText('group.membercount', [ 'totalMembers' ], [ groupInformation.membersCount.toString() ]) } + { (groupInformation.pendingRequestsCount > 0) && + handleAction('members_pending') }>{ LocalizeText('group.pendingmembercount', [ 'amount' ], [ groupInformation.pendingRequestsCount.toString() ]) } } + { groupInformation.isOwner && + handleAction('manage') }>{ LocalizeText('group.manage') } } + + { getRoleIcon() } + + + + + + { groupInformation.title } + + + { groupInformation.canMembersDecorate && + } + + + { LocalizeText('group.created', [ 'date', 'owner' ], [ groupInformation.createdAt, groupInformation.ownerName ]) } + + { groupInformation.description } + + + + handleAction('homeroom') }>{ LocalizeText('group.linktobase') } + handleAction('furniture') }>{ LocalizeText('group.buyfurni') } + handleAction('popular_groups') }>{ LocalizeText('group.showgroups') } + + { (groupInformation.type !== GroupType.PRIVATE || groupInformation.type === GroupType.PRIVATE && groupInformation.membershipType === GroupMembershipType.MEMBER) && + } + + + + ); +}; diff --git a/apps/frontend/src/components/groups/views/GroupManagerView.tsx b/apps/frontend/src/components/groups/views/GroupManagerView.tsx new file mode 100644 index 0000000..9e1d625 --- /dev/null +++ b/apps/frontend/src/components/groups/views/GroupManagerView.tsx @@ -0,0 +1,119 @@ +import { GroupBadgePart, GroupInformationEvent, GroupSettingsEvent } from '@nitrots/nitro-renderer'; +import { FC, useState } from 'react'; +import { IGroupData, LocalizeText } from '../../../api'; +import { Base, Column, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView, Text } from '../../../common'; +import { useMessageEvent } from '../../../hooks'; +import { GroupTabBadgeView } from './tabs/GroupTabBadgeView'; +import { GroupTabColorsView } from './tabs/GroupTabColorsView'; +import { GroupTabIdentityView } from './tabs/GroupTabIdentityView'; +import { GroupTabSettingsView } from './tabs/GroupTabSettingsView'; + +const TABS: number[] = [ 1, 2, 3, 5 ]; + +export const GroupManagerView: FC<{}> = props => +{ + const [ currentTab, setCurrentTab ] = useState(1); + const [ closeAction, setCloseAction ] = useState<{ action: () => boolean }>(null); + const [ groupData, setGroupData ] = useState(null); + + const onClose = () => + { + setCloseAction(prevValue => + { + if(prevValue && prevValue.action) prevValue.action(); + + return null; + }); + + setGroupData(null); + } + + const changeTab = (tab: number) => + { + if(closeAction && closeAction.action) closeAction.action(); + + setCurrentTab(tab); + } + + useMessageEvent(GroupInformationEvent, event => + { + const parser = event.getParser(); + + if(!groupData || (groupData.groupId !== parser.id)) return; + + setGroupData(prevValue => + { + const newValue = { ...prevValue }; + + newValue.groupName = parser.title; + newValue.groupDescription = parser.description; + newValue.groupState = parser.type; + newValue.groupCanMembersDecorate = parser.canMembersDecorate; + + return newValue; + }); + }); + + useMessageEvent(GroupSettingsEvent, event => + { + const parser = event.getParser(); + + const groupBadgeParts: GroupBadgePart[] = []; + + parser.badgeParts.forEach((part, id) => + { + groupBadgeParts.push(new GroupBadgePart( + part.isBase ? GroupBadgePart.BASE : GroupBadgePart.SYMBOL, + part.key, + part.color, + part.position + )); + }); + + setGroupData({ + groupId: parser.id, + groupName: parser.title, + groupDescription: parser.description, + groupHomeroomId: parser.roomId, + groupState: parser.state, + groupCanMembersDecorate: parser.canMembersDecorate, + groupColors: [ parser.colorA, parser.colorB ], + groupBadgeParts + }); + }); + + if(!groupData || (groupData.groupId <= 0)) return null; + + return ( + + + + { TABS.map(tab => + { + return ( changeTab(tab) }> + { LocalizeText(`group.edit.tab.${ tab }`) } + ); + }) } + + + + + + { LocalizeText(`group.edit.tabcaption.${ currentTab }`) } + { LocalizeText(`group.edit.tabdesc.${ currentTab }`) } + + + + { (currentTab === 1) && + } + { (currentTab === 2) && + } + { (currentTab === 3) && + } + { (currentTab === 5) && + } + + + + ); +}; diff --git a/apps/frontend/src/components/groups/views/GroupMembersView.tsx b/apps/frontend/src/components/groups/views/GroupMembersView.tsx new file mode 100644 index 0000000..366821e --- /dev/null +++ b/apps/frontend/src/components/groups/views/GroupMembersView.tsx @@ -0,0 +1,210 @@ +import { GroupAdminGiveComposer, GroupAdminTakeComposer, GroupConfirmMemberRemoveEvent, GroupConfirmRemoveMemberComposer, GroupMemberParser, GroupMembersComposer, GroupMembersEvent, GroupMembershipAcceptComposer, GroupMembershipDeclineComposer, GroupMembersParser, GroupRank, GroupRemoveMemberComposer, ILinkEventTracker } from '@nitrots/nitro-renderer'; +import { FC, useCallback, useEffect, useState } from 'react'; +import { FaChevronLeft, FaChevronRight } from 'react-icons/fa'; +import { AddEventLinkTracker, GetSessionDataManager, GetUserProfile, LocalizeText, RemoveLinkEventTracker, SendMessageComposer } from '../../../api'; +import { Base, Button, Column, Flex, Grid, LayoutAvatarImageView, LayoutBadgeImageView, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../common'; +import { useMessageEvent, useNotification } from '../../../hooks'; + +export const GroupMembersView: FC<{}> = props => +{ + const [ groupId, setGroupId ] = useState(-1); + const [ levelId, setLevelId ] = useState(-1); + const [ membersData, setMembersData ] = useState(null); + const [ pageId, setPageId ] = useState(-1); + const [ totalPages, setTotalPages ] = useState(0); + const [ searchQuery, setSearchQuery ] = useState(''); + const [ removingMemberName, setRemovingMemberName ] = useState(null); + const { showConfirm = null } = useNotification(); + + const getRankDescription = (member: GroupMemberParser) => + { + if(member.rank === GroupRank.OWNER) return 'group.members.owner'; + + if(membersData.admin) + { + if(member.rank === GroupRank.ADMIN) return 'group.members.removerights'; + + if(member.rank === GroupRank.MEMBER) return 'group.members.giverights'; + } + + return ''; + } + + const refreshMembers = useCallback(() => + { + if((groupId === -1) || (levelId === -1) || (pageId === -1)) return; + + SendMessageComposer(new GroupMembersComposer(groupId, pageId, searchQuery, levelId)); + }, [ groupId, levelId, pageId, searchQuery ]); + + const toggleAdmin = (member: GroupMemberParser) => + { + if(!membersData.admin || (member.rank === GroupRank.OWNER)) return; + + if(member.rank !== GroupRank.ADMIN) SendMessageComposer(new GroupAdminGiveComposer(membersData.groupId, member.id)); + else SendMessageComposer(new GroupAdminTakeComposer(membersData.groupId, member.id)); + + refreshMembers(); + } + + const acceptMembership = (member: GroupMemberParser) => + { + if(!membersData.admin || (member.rank !== GroupRank.REQUESTED)) return; + + SendMessageComposer(new GroupMembershipAcceptComposer(membersData.groupId, member.id)); + + refreshMembers(); + } + + const removeMemberOrDeclineMembership = (member: GroupMemberParser) => + { + if(!membersData.admin) return; + + if(member.rank === GroupRank.REQUESTED) + { + SendMessageComposer(new GroupMembershipDeclineComposer(membersData.groupId, member.id)); + + refreshMembers(); + + return; + } + + setRemovingMemberName(member.name); + SendMessageComposer(new GroupConfirmRemoveMemberComposer(membersData.groupId, member.id)); + } + + useMessageEvent(GroupMembersEvent, event => + { + const parser = event.getParser(); + + setMembersData(parser); + setLevelId(parser.level); + setTotalPages(Math.ceil(parser.totalMembersCount / parser.pageSize)); + }); + + useMessageEvent(GroupConfirmMemberRemoveEvent, event => + { + const parser = event.getParser(); + + showConfirm(LocalizeText(((parser.furnitureCount > 0) ? 'group.kickconfirm.desc' : 'group.kickconfirm_nofurni.desc'), [ 'user', 'amount' ], [ removingMemberName, parser.furnitureCount.toString() ]), () => + { + SendMessageComposer(new GroupRemoveMemberComposer(membersData.groupId, parser.userId)); + + refreshMembers(); + }, null); + + setRemovingMemberName(null); + }); + + useEffect(() => + { + const linkTracker: ILinkEventTracker = { + linkReceived: (url: string) => + { + const parts = url.split('/'); + + if(parts.length < 2) return; + + const groupId = (parseInt(parts[1]) || -1); + const levelId = (parseInt(parts[2]) || 3); + + setGroupId(groupId); + setLevelId(levelId); + setPageId(0); + }, + eventUrlPrefix: 'group-members/' + }; + + AddEventLinkTracker(linkTracker); + + return () => RemoveLinkEventTracker(linkTracker); + }, []); + + useEffect(() => + { + setPageId(0); + }, [ groupId, levelId, searchQuery ]); + + useEffect(() => + { + if((groupId === -1) || (levelId === -1) || (pageId === -1)) return; + + SendMessageComposer(new GroupMembersComposer(groupId, pageId, searchQuery, levelId)); + }, [ groupId, levelId, pageId, searchQuery ]); + + useEffect(() => + { + if(groupId === -1) return; + + setLevelId(-1); + setMembersData(null); + setTotalPages(0); + setSearchQuery(''); + setRemovingMemberName(null); + }, [ groupId ]); + + if((groupId === -1) || !membersData) return null; + + return ( + + setGroupId(-1) } /> + + + + + + + setSearchQuery(event.target.value) } /> + + + + + { membersData.result.map((member, index) => + { + return ( + +
GetUserProfile(member.id) }> + +
+ + GetUserProfile(member.id) }>{ member.name } + { (member.rank !== GroupRank.REQUESTED) && + { LocalizeText('group.members.since', [ 'date' ], [ member.joinedAt ]) } } + + + { (member.rank !== GroupRank.REQUESTED) && + + toggleAdmin(member) } /> + } + { membersData.admin && (member.rank === GroupRank.REQUESTED) && + + acceptMembership(member) }> + } + { membersData.admin && (member.rank !== GroupRank.OWNER) && (member.id !== GetSessionDataManager().userId) && + + removeMemberOrDeclineMembership(member) }> + } + +
+ ); + }) } +
+ + + + { LocalizeText('group.members.pageinfo', [ 'amount', 'page', 'totalPages' ], [ membersData.totalMembersCount.toString(), (membersData.pageIndex + 1).toString(), totalPages.toString() ]) } + + + +
+
+ ); +}; diff --git a/apps/frontend/src/components/groups/views/GroupRoomInformationView.tsx b/apps/frontend/src/components/groups/views/GroupRoomInformationView.tsx new file mode 100644 index 0000000..9ac0ca2 --- /dev/null +++ b/apps/frontend/src/components/groups/views/GroupRoomInformationView.tsx @@ -0,0 +1,132 @@ +import { DesktopViewEvent, GetGuestRoomResultEvent, GroupInformationComposer, GroupInformationEvent, GroupInformationParser, GroupRemoveMemberComposer, HabboGroupDeactivatedMessageEvent, RoomEntryInfoMessageEvent } from '@nitrots/nitro-renderer'; +import { FC, useState } from 'react'; +import { FaChevronDown, FaChevronUp } from 'react-icons/fa'; +import { GetGroupInformation, GetGroupManager, GetSessionDataManager, GroupMembershipType, GroupType, LocalizeText, SendMessageComposer, TryJoinGroup } from '../../../api'; +import { Base, Button, Column, Flex, LayoutBadgeImageView, Text } from '../../../common'; +import { useMessageEvent, useNotification } from '../../../hooks'; + +export const GroupRoomInformationView: FC<{}> = props => +{ + const [ expectedGroupId, setExpectedGroupId ] = useState(0); + const [ groupInformation, setGroupInformation ] = useState(null); + const [ isOpen, setIsOpen ] = useState(true); + const { showConfirm = null } = useNotification(); + + useMessageEvent(DesktopViewEvent, event => + { + setExpectedGroupId(0); + setGroupInformation(null); + }); + + useMessageEvent(RoomEntryInfoMessageEvent, event => + { + setExpectedGroupId(0); + setGroupInformation(null); + }); + + useMessageEvent(GetGuestRoomResultEvent, event => + { + const parser = event.getParser(); + + if(!parser.roomEnter) return; + + if(parser.data.habboGroupId > 0) + { + setExpectedGroupId(parser.data.habboGroupId); + SendMessageComposer(new GroupInformationComposer(parser.data.habboGroupId, false)); + } + else + { + setExpectedGroupId(0); + setGroupInformation(null); + } + }); + + useMessageEvent(HabboGroupDeactivatedMessageEvent, event => + { + const parser = event.getParser(); + + if(!groupInformation || ((parser.groupId !== groupInformation.id) && (parser.groupId !== expectedGroupId))) return; + + setExpectedGroupId(0); + setGroupInformation(null); + }); + + useMessageEvent(GroupInformationEvent, event => + { + const parser = event.getParser(); + + if(parser.id !== expectedGroupId) return; + + setGroupInformation(parser); + }); + + const leaveGroup = () => + { + showConfirm(LocalizeText('group.leaveconfirm.desc'), () => + { + SendMessageComposer(new GroupRemoveMemberComposer(groupInformation.id, GetSessionDataManager().userId)); + }, null); + } + + const isRealOwner = (groupInformation && (groupInformation.ownerName === GetSessionDataManager().userName)); + + const getButtonText = () => + { + if(isRealOwner) return 'group.manage'; + + if(groupInformation.type === GroupType.PRIVATE) return ''; + + if(groupInformation.membershipType === GroupMembershipType.MEMBER) return 'group.leave'; + + if((groupInformation.membershipType === GroupMembershipType.NOT_MEMBER) && groupInformation.type === GroupType.REGULAR) return 'group.join'; + + if(groupInformation.membershipType === GroupMembershipType.REQUEST_PENDING) return 'group.membershippending'; + + if((groupInformation.membershipType === GroupMembershipType.NOT_MEMBER) && groupInformation.type === GroupType.EXCLUSIVE) return 'group.requestmembership'; + } + + const handleButtonClick = () => + { + if(isRealOwner) return GetGroupManager(groupInformation.id); + + if((groupInformation.type === GroupType.PRIVATE) && (groupInformation.membershipType === GroupMembershipType.NOT_MEMBER)) return; + + if(groupInformation.membershipType === GroupMembershipType.MEMBER) + { + leaveGroup(); + + return; + } + + TryJoinGroup(groupInformation.id); + } + + if(!groupInformation) return null; + + return ( + + + setIsOpen(value => !value) }> + { LocalizeText('group.homeroominfo.title') } + { isOpen && } + { !isOpen && } + + { isOpen && + <> + GetGroupInformation(groupInformation.id) }> + + + + { groupInformation.title } + + { (groupInformation.type !== GroupType.PRIVATE || isRealOwner) && + + } + } + + + ); +}; diff --git a/apps/frontend/src/components/groups/views/tabs/GroupTabBadgeView.tsx b/apps/frontend/src/components/groups/views/tabs/GroupTabBadgeView.tsx new file mode 100644 index 0000000..73d65c0 --- /dev/null +++ b/apps/frontend/src/components/groups/views/tabs/GroupTabBadgeView.tsx @@ -0,0 +1,120 @@ +import { GroupSaveBadgeComposer } from '@nitrots/nitro-renderer'; +import { Dispatch, FC, SetStateAction, useCallback, useEffect, useState } from 'react'; +import { GroupBadgePart, IGroupData, SendMessageComposer } from '../../../../api'; +import { Column, Flex, Grid, LayoutBadgeImageView } from '../../../../common'; +import { useGroup } from '../../../../hooks'; +import { GroupBadgeCreatorView } from '../GroupBadgeCreatorView'; + +interface GroupTabBadgeViewProps +{ + skipDefault?: boolean; + setCloseAction: Dispatch boolean }>>; + groupData: IGroupData; + setGroupData: Dispatch>; +} + +export const GroupTabBadgeView: FC = props => +{ + const { groupData = null, setGroupData = null, setCloseAction = null, skipDefault = null } = props; + const [ badgeParts, setBadgeParts ] = useState(null); + const { groupCustomize = null } = useGroup(); + + const getModifiedBadgeCode = () => + { + if(!badgeParts || !badgeParts.length) return ''; + + let badgeCode = ''; + + badgeParts.forEach(part => (part.code && (badgeCode += part.code))); + + return badgeCode; + } + + const saveBadge = useCallback(() => + { + if(!groupData || !badgeParts || !badgeParts.length) return false; + + if((groupData.groupBadgeParts === badgeParts)) return true; + + if(groupData.groupId <= 0) + { + setGroupData(prevValue => + { + const newValue = { ...prevValue }; + + newValue.groupBadgeParts = badgeParts; + + return newValue; + }); + + return true; + } + + const badge = []; + + badgeParts.forEach(part => + { + if(!part.code) return; + + badge.push(part.key); + badge.push(part.color); + badge.push(part.position); + }); + + SendMessageComposer(new GroupSaveBadgeComposer(groupData.groupId, badge)); + + return true; + }, [ groupData, badgeParts, setGroupData ]); + + useEffect(() => + { + if(groupData.groupBadgeParts) return; + + const badgeParts = [ + new GroupBadgePart(GroupBadgePart.BASE, groupCustomize.badgeBases[0].id, groupCustomize.badgePartColors[0].id), + new GroupBadgePart(GroupBadgePart.SYMBOL, 0, groupCustomize.badgePartColors[0].id), + new GroupBadgePart(GroupBadgePart.SYMBOL, 0, groupCustomize.badgePartColors[0].id), + new GroupBadgePart(GroupBadgePart.SYMBOL, 0, groupCustomize.badgePartColors[0].id), + new GroupBadgePart(GroupBadgePart.SYMBOL, 0, groupCustomize.badgePartColors[0].id) + ]; + + setGroupData(prevValue => + { + const groupBadgeParts = badgeParts; + + return { ...prevValue, groupBadgeParts }; + }); + }, [ groupData.groupBadgeParts, groupCustomize, setGroupData ]); + + useEffect(() => + { + if(groupData.groupId <= 0) + { + setBadgeParts(groupData.groupBadgeParts ? [ ...groupData.groupBadgeParts ] : null); + + return; + } + + setBadgeParts(groupData.groupBadgeParts); + }, [ groupData ]); + + useEffect(() => + { + setCloseAction({ action: saveBadge }); + + return () => setCloseAction(null); + }, [ setCloseAction, saveBadge ]); + + return ( + + + + + + + + + + + ); +}; diff --git a/apps/frontend/src/components/groups/views/tabs/GroupTabColorsView.tsx b/apps/frontend/src/components/groups/views/tabs/GroupTabColorsView.tsx new file mode 100644 index 0000000..61f3c86 --- /dev/null +++ b/apps/frontend/src/components/groups/views/tabs/GroupTabColorsView.tsx @@ -0,0 +1,127 @@ +import { GroupSaveColorsComposer } from '@nitrots/nitro-renderer'; +import { Dispatch, FC, SetStateAction, useCallback, useEffect, useState } from 'react'; +import { IGroupData, LocalizeText, SendMessageComposer } from '../../../../api'; +import { AutoGrid, Base, classNames, Column, Flex, Grid, Text } from '../../../../common'; +import { useGroup } from '../../../../hooks'; + +interface GroupTabColorsViewProps +{ + groupData: IGroupData; + setGroupData: Dispatch>; + setCloseAction: Dispatch boolean }>>; +} + +export const GroupTabColorsView: FC = props => +{ + const { groupData = null, setGroupData = null, setCloseAction = null } = props; + const [ colors, setColors ] = useState(null); + const { groupCustomize = null } = useGroup(); + + const getGroupColor = (colorIndex: number) => + { + if(colorIndex === 0) return groupCustomize.groupColorsA.find(color => (color.id === colors[colorIndex])).color; + + return groupCustomize.groupColorsB.find(color => (color.id === colors[colorIndex])).color; + } + + const selectColor = (colorIndex: number, colorId: number) => + { + setColors(prevValue => + { + const newColors = [ ...prevValue ]; + + newColors[colorIndex] = colorId; + + return newColors; + }); + } + + const saveColors = useCallback(() => + { + if(!groupData || !colors || !colors.length) return false; + + if(groupData.groupColors === colors) return true; + + if(groupData.groupId <= 0) + { + setGroupData(prevValue => + { + const newValue = { ...prevValue }; + + newValue.groupColors = [ ...colors ]; + + return newValue; + }); + + return true; + } + + SendMessageComposer(new GroupSaveColorsComposer(groupData.groupId, colors[0], colors[1])); + + return true; + }, [ groupData, colors, setGroupData ]); + + useEffect(() => + { + if(!groupCustomize.groupColorsA || !groupCustomize.groupColorsB || groupData.groupColors) return; + + const groupColors = [ groupCustomize.groupColorsA[0].id, groupCustomize.groupColorsB[0].id ]; + + setGroupData(prevValue => + { + return { ...prevValue, groupColors }; + }); + }, [ groupCustomize, groupData.groupColors, setGroupData ]); + + useEffect(() => + { + if(groupData.groupId <= 0) + { + setColors(groupData.groupColors ? [ ...groupData.groupColors ] : null); + + return; + } + + setColors(groupData.groupColors); + }, [ groupData ]); + + useEffect(() => + { + setCloseAction({ action: saveColors }); + + return () => setCloseAction(null); + }, [ setCloseAction, saveColors ]); + + if(!colors) return null; + + return ( + + + { LocalizeText('group.edit.color.guild.color') } + { groupData.groupColors && (groupData.groupColors.length > 0) && + + + + } + + + { LocalizeText('group.edit.color.primary.color') } + + { groupData.groupColors && groupCustomize.groupColorsA && groupCustomize.groupColorsA.map((item, index) => + { + return
selectColor(0, item.id) }>
+ }) } +
+
+ + { LocalizeText('group.edit.color.secondary.color') } + + { groupData.groupColors && groupCustomize.groupColorsB && groupCustomize.groupColorsB.map((item, index) => + { + return
selectColor(1, item.id) }>
+ }) } +
+
+
+ ); +}; diff --git a/apps/frontend/src/components/groups/views/tabs/GroupTabCreatorConfirmationView.tsx b/apps/frontend/src/components/groups/views/tabs/GroupTabCreatorConfirmationView.tsx new file mode 100644 index 0000000..3046038 --- /dev/null +++ b/apps/frontend/src/components/groups/views/tabs/GroupTabCreatorConfirmationView.tsx @@ -0,0 +1,67 @@ +import { Dispatch, FC, SetStateAction } from 'react'; +import { IGroupData, LocalizeText } from '../../../../api'; +import { Base, Column, Flex, Grid, LayoutBadgeImageView, Text } from '../../../../common'; +import { useGroup } from '../../../../hooks'; + +interface GroupTabCreatorConfirmationViewProps +{ + groupData: IGroupData; + setGroupData: Dispatch>; + purchaseCost: number; +} + +export const GroupTabCreatorConfirmationView: FC = props => +{ + const { groupData = null, setGroupData = null, purchaseCost = 0 } = props; + const { groupCustomize = null } = useGroup(); + + const getCompleteBadgeCode = () => + { + if(!groupData || !groupData.groupBadgeParts || !groupData.groupBadgeParts.length) return ''; + + let badgeCode = ''; + + groupData.groupBadgeParts.forEach(part => (part.code && (badgeCode += part.code))); + + return badgeCode; + } + + const getGroupColor = (colorIndex: number) => + { + if(colorIndex === 0) return groupCustomize.groupColorsA.find(c => c.id === groupData.groupColors[colorIndex]).color; + + return groupCustomize.groupColorsB.find(c => c.id === groupData.groupColors[colorIndex]).color; + } + + if(!groupData) return null; + + return ( + + + + { LocalizeText('group.create.confirm.guildbadge') } + + + + { LocalizeText('group.edit.color.guild.color') } + + + + + + + + + + { groupData.groupName } + { groupData.groupDescription } + + { LocalizeText('group.create.confirm.info') } + + + { LocalizeText('group.create.confirm.buyinfo', [ 'amount' ], [ purchaseCost.toString() ]) } + + + + ); +}; diff --git a/apps/frontend/src/components/groups/views/tabs/GroupTabIdentityView.tsx b/apps/frontend/src/components/groups/views/tabs/GroupTabIdentityView.tsx new file mode 100644 index 0000000..025babb --- /dev/null +++ b/apps/frontend/src/components/groups/views/tabs/GroupTabIdentityView.tsx @@ -0,0 +1,116 @@ +import { GroupDeleteComposer, GroupSaveInformationComposer } from '@nitrots/nitro-renderer'; +import { Dispatch, FC, SetStateAction, useCallback, useEffect, useState } from 'react'; +import { CreateLinkEvent, IGroupData, LocalizeText, SendMessageComposer } from '../../../../api'; +import { Base, Button, Column, Flex, Text } from '../../../../common'; +import { useNotification } from '../../../../hooks'; + +interface GroupTabIdentityViewProps +{ + groupData: IGroupData; + setGroupData: Dispatch>; + setCloseAction: Dispatch boolean }>>; + onClose: () => void; + isCreator?: boolean; + availableRooms?: { id: number, name: string }[]; +} + +export const GroupTabIdentityView: FC = props => +{ + const { groupData = null, setGroupData = null, setCloseAction = null, onClose = null, isCreator = false, availableRooms = [] } = props; + const [ groupName, setGroupName ] = useState(''); + const [ groupDescription, setGroupDescription ] = useState(''); + const [ groupHomeroomId, setGroupHomeroomId ] = useState(-1); + const { showConfirm = null } = useNotification(); + + const deleteGroup = () => + { + if(!groupData || (groupData.groupId <= 0)) return; + + showConfirm(LocalizeText('group.deleteconfirm.desc'), () => + { + SendMessageComposer(new GroupDeleteComposer(groupData.groupId)); + + if(onClose) onClose(); + }, null, null, null, LocalizeText('group.deleteconfirm.title')); + } + + const saveIdentity = useCallback(() => + { + if(!groupData || !groupName || !groupName.length) return false; + + if((groupName === groupData.groupName) && (groupDescription === groupData.groupDescription)) return true; + + if(groupData.groupId <= 0) + { + if(groupHomeroomId <= 0) return false; + + setGroupData(prevValue => + { + const newValue = { ...prevValue }; + + newValue.groupName = groupName; + newValue.groupDescription = groupDescription; + newValue.groupHomeroomId = groupHomeroomId; + + return newValue; + }); + + return true; + } + + SendMessageComposer(new GroupSaveInformationComposer(groupData.groupId, groupName, (groupDescription || ''))); + + return true; + }, [ groupData, groupName, groupDescription, groupHomeroomId, setGroupData ]); + + useEffect(() => + { + setGroupName(groupData.groupName || ''); + setGroupDescription(groupData.groupDescription || ''); + setGroupHomeroomId(groupData.groupHomeroomId); + }, [ groupData ]); + + useEffect(() => + { + setCloseAction({ action: saveIdentity }); + + return () => setCloseAction(null); + }, [ setCloseAction, saveIdentity ]); + + if(!groupData) return null; + + return ( + + + + { LocalizeText('group.edit.name') } + setGroupName(event.target.value) } /> + + + { LocalizeText('group.edit.desc') } + + + + ); +}; diff --git a/apps/frontend/src/components/guide-tool/views/GuideToolUserFeedbackView.tsx b/apps/frontend/src/components/guide-tool/views/GuideToolUserFeedbackView.tsx new file mode 100644 index 0000000..9ec7280 --- /dev/null +++ b/apps/frontend/src/components/guide-tool/views/GuideToolUserFeedbackView.tsx @@ -0,0 +1,43 @@ +import { GuideSessionFeedbackMessageComposer } from '@nitrots/nitro-renderer'; +import { FC } from 'react'; +import { LocalizeText, SendMessageComposer } from '../../../api'; +import { Button, Column, Flex, Text } from '../../../common'; + +interface GuideToolUserFeedbackViewProps +{ + userName: string; +} + +export const GuideToolUserFeedbackView: FC = props => +{ + const { userName = null } = props; + + const giveFeedback = (recommend: boolean) => SendMessageComposer(new GuideSessionFeedbackMessageComposer(recommend)); + + return ( + + + + { userName } + { LocalizeText('guide.help.request.user.feedback.guide.desc') } + + + + + { LocalizeText('guide.help.request.user.feedback.closed.title') } + { LocalizeText('guide.help.request.user.feedback.closed.desc') } + + { userName && (userName.length > 0) && + <> +
+ + { LocalizeText('guide.help.request.user.feedback.question') } + + + + + + } +
+ ); +}; diff --git a/apps/frontend/src/components/guide-tool/views/GuideToolUserNoHelpersView.tsx b/apps/frontend/src/components/guide-tool/views/GuideToolUserNoHelpersView.tsx new file mode 100644 index 0000000..6fcbfd5 --- /dev/null +++ b/apps/frontend/src/components/guide-tool/views/GuideToolUserNoHelpersView.tsx @@ -0,0 +1,13 @@ +import { FC } from 'react'; +import { LocalizeText } from '../../../api'; +import { Column, Text } from '../../../common'; + +export const GuideToolUserNoHelpersView: FC<{}> = props => +{ + return ( + + { LocalizeText('guide.help.request.no_tour_guides.title') } + { LocalizeText('guide.help.request.no_tour_guides.message') } + + ); +}; \ No newline at end of file diff --git a/apps/frontend/src/components/guide-tool/views/GuideToolUserPendingView.tsx b/apps/frontend/src/components/guide-tool/views/GuideToolUserPendingView.tsx new file mode 100644 index 0000000..81faaff --- /dev/null +++ b/apps/frontend/src/components/guide-tool/views/GuideToolUserPendingView.tsx @@ -0,0 +1,33 @@ +import { GuideSessionRequesterCancelsMessageComposer } from '@nitrots/nitro-renderer'; +import { FC } from 'react'; +import { LocalizeText, SendMessageComposer } from '../../../api'; +import { Button, Column, Text } from '../../../common'; + +interface GuideToolUserPendingViewProps +{ + helpRequestDescription: string; + helpRequestAverageTime: number; +} + +export const GuideToolUserPendingView: FC = props => +{ + const { helpRequestDescription = null, helpRequestAverageTime = 0 } = props; + + const cancelRequest = () => SendMessageComposer(new GuideSessionRequesterCancelsMessageComposer()); + + return ( + + + { LocalizeText('guide.help.request.guide.accept.request.title') } + { LocalizeText('guide.help.request.type.1') } + { helpRequestDescription } + + + { LocalizeText('guide.help.request.user.pending.info.title') } + { LocalizeText('guide.help.request.user.pending.info.message') } + { LocalizeText('guide.help.request.user.pending.info.waiting', [ 'waitingtime' ], [ helpRequestAverageTime.toString() ]) } + + + + ); +}; diff --git a/apps/frontend/src/components/guide-tool/views/GuideToolUserSomethingWrogView.tsx b/apps/frontend/src/components/guide-tool/views/GuideToolUserSomethingWrogView.tsx new file mode 100644 index 0000000..1e8ec5e --- /dev/null +++ b/apps/frontend/src/components/guide-tool/views/GuideToolUserSomethingWrogView.tsx @@ -0,0 +1,12 @@ +import { FC } from 'react'; +import { LocalizeText } from '../../../api'; +import { Column, Text } from '../../../common'; + +export const GuideToolUserSomethingWrogView: FC<{}> = props => +{ + return ( + + { LocalizeText('guide.help.request.user.guide.disconnected.error.desc') } + + ); +}; \ No newline at end of file diff --git a/apps/frontend/src/components/guide-tool/views/GuideToolUserThanksView.tsx b/apps/frontend/src/components/guide-tool/views/GuideToolUserThanksView.tsx new file mode 100644 index 0000000..4953b0f --- /dev/null +++ b/apps/frontend/src/components/guide-tool/views/GuideToolUserThanksView.tsx @@ -0,0 +1,13 @@ +import { FC } from 'react'; +import { LocalizeText } from '../../../api'; +import { Column, Text } from '../../../common'; + +export const GuideToolUserThanksView: FC<{}> = props => +{ + return ( + + { LocalizeText('guide.help.request.user.thanks.info.title') } + { LocalizeText('guide.help.request.user.thanks.info.desc') } + + ); +}; diff --git a/apps/frontend/src/components/hc-center/HcCenterView.scss b/apps/frontend/src/components/hc-center/HcCenterView.scss new file mode 100644 index 0000000..a6af9fa --- /dev/null +++ b/apps/frontend/src/components/hc-center/HcCenterView.scss @@ -0,0 +1,44 @@ +.nitro-hc-center { + width: 430px; + resize: none; + + .hc-logo { + width: 213px; + height: 37px; + background-image: url('@/assets/images/hc-center/hc_logo.gif'); + } + + .payday-special { + height: 128px; + } + + .payday { + width: 222px; + height: 150px; + background-image: url('@/assets/images/hc-center/payday.png'); + z-index: 3; + color: #6b3502; + } + + .clock { + width: 24px; + height: 24px; + background-image: url('@/assets/images/hc-center/clock.png'); + } + + .streak-info { + min-height: 64px; + line-height: 16px; + } + + .habbo-avatar { + z-index: 4; + } + + .benefits { + background-image: url('@/assets/images/hc-center/benefits.png'); + background-position: right top; + background-repeat: no-repeat; + height: 100%; + } +} diff --git a/apps/frontend/src/components/hc-center/HcCenterView.tsx b/apps/frontend/src/components/hc-center/HcCenterView.tsx new file mode 100644 index 0000000..2757016 --- /dev/null +++ b/apps/frontend/src/components/hc-center/HcCenterView.tsx @@ -0,0 +1,204 @@ +import { ClubGiftInfoEvent, FriendlyTime, GetClubGiftInfo, ILinkEventTracker, ScrGetKickbackInfoMessageComposer, ScrKickbackData, ScrSendKickbackInfoMessageEvent } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useState } from 'react'; +import { OverlayTrigger, Popover } from 'react-bootstrap'; +import { AddEventLinkTracker, ClubStatus, CreateLinkEvent, GetClubBadge, GetConfiguration, LocalizeText, RemoveLinkEventTracker, SendMessageComposer } from '../../api'; +import { Base, Button, Column, Flex, LayoutAvatarImageView, LayoutBadgeImageView, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common'; +import { useInventoryBadges, useMessageEvent, usePurse, useSessionInfo } from '../../hooks'; + + +export const HcCenterView: FC<{}> = props => +{ + const [ isVisible, setIsVisible ] = useState(false); + const [ kickbackData, setKickbackData ] = useState(null); + const [ unclaimedGifts, setUnclaimedGifts ] = useState(0); + const [ badgeCode, setBadgeCode ] = useState(null); + const { userFigure = null } = useSessionInfo(); + const { purse = null, clubStatus = null } = usePurse(); + const { badgeCodes = [], activate = null, deactivate = null } = useInventoryBadges(); + + const getClubText = () => + { + if(purse.clubDays <= 0) return LocalizeText('purse.clubdays.zero.amount.text'); + + if((purse.minutesUntilExpiration > -1) && (purse.minutesUntilExpiration < (60 * 24))) + { + return FriendlyTime.shortFormat(purse.minutesUntilExpiration * 60); + } + + return FriendlyTime.shortFormat(((purse.clubPeriods * 31) + purse.clubDays) * 86400); + } + + const getInfoText = () => + { + switch(clubStatus) + { + case ClubStatus.ACTIVE: + return LocalizeText(`hccenter.status.${ clubStatus }.info`, [ 'timeleft', 'joindate', 'streakduration' ], [ getClubText(), kickbackData?.firstSubscriptionDate, FriendlyTime.shortFormat(kickbackData?.currentHcStreak * 86400) ]); + case ClubStatus.EXPIRED: + return LocalizeText(`hccenter.status.${ clubStatus }.info`, [ 'joindate' ], [ kickbackData?.firstSubscriptionDate ]); + default: + return LocalizeText(`hccenter.status.${ clubStatus }.info`); + } + } + + const getHcPaydayTime = () => (!kickbackData || kickbackData.timeUntilPayday < 60) ? LocalizeText('hccenter.special.time.soon') : FriendlyTime.shortFormat(kickbackData.timeUntilPayday * 60); + const getHcPaydayAmount = () => LocalizeText('hccenter.special.sum', [ 'credits' ], [ (kickbackData?.creditRewardForStreakBonus + kickbackData?.creditRewardForMonthlySpent).toString() ]); + + useMessageEvent(ClubGiftInfoEvent, event => + { + const parser = event.getParser(); + + setUnclaimedGifts(parser.giftsAvailable); + }); + + useMessageEvent(ScrSendKickbackInfoMessageEvent, event => + { + const parser = event.getParser(); + + setKickbackData(parser.data); + }); + + useEffect(() => + { + const linkTracker: ILinkEventTracker = { + linkReceived: (url: string) => + { + const parts = url.split('/'); + + if(parts.length < 2) return; + + switch(parts[1]) + { + case 'open': + if(parts.length > 2) + { + switch(parts[2]) + { + case 'hccenter': + setIsVisible(true); + break; + } + } + return; + } + }, + eventUrlPrefix: 'habboUI/' + }; + + AddEventLinkTracker(linkTracker); + + return () => RemoveLinkEventTracker(linkTracker); + }, []); + + useEffect(() => + { + setBadgeCode(GetClubBadge(badgeCodes)); + }, [ badgeCodes ]); + + useEffect(() => + { + if(!isVisible) return; + + const id = activate(); + + return () => deactivate(id); + }, [ isVisible, activate, deactivate ]); + + useEffect(() => + { + SendMessageComposer(new GetClubGiftInfo()); + SendMessageComposer(new ScrGetKickbackInfoMessageComposer()); + }, []); + + if(!isVisible) return null; + + const popover = ( + + +
{ LocalizeText('hccenter.breakdown.title') }
+
{ LocalizeText('hccenter.breakdown.creditsspent', [ 'credits' ], [ kickbackData?.totalCreditsSpent.toString() ]) }
+
{ LocalizeText('hccenter.breakdown.paydayfactor.percent', [ 'percent' ], [ (kickbackData?.kickbackPercentage * 100).toString() ]) }
+
{ LocalizeText('hccenter.breakdown.streakbonus', [ 'credits' ], [ kickbackData?.creditRewardForStreakBonus.toString() ]) }
+
+
{ LocalizeText('hccenter.breakdown.total', [ 'credits', 'actual' ], [ getHcPaydayAmount(), ((((kickbackData?.kickbackPercentage * kickbackData?.totalCreditsSpent) + kickbackData?.creditRewardForStreakBonus) * 100) / 100).toString() ]) }
+
CreateLinkEvent('habbopages/' + GetConfiguration('hc.center')['payday.habbopage']) }> + { LocalizeText('hccenter.special.infolink') } +
+
+
+ ); + + return ( + + setIsVisible(false) } /> + + +
+ + + + + + + + + + + + + { LocalizeText('hccenter.status.' + clubStatus) } + + + + { GetConfiguration('hc.center')['payday.info'] && + + + +

{ LocalizeText('hccenter.special.title') }

+
{ LocalizeText('hccenter.special.info') }
+
CreateLinkEvent('habbopages/' + GetConfiguration('hc.center')['payday.habbopage']) }>{ LocalizeText('hccenter.special.infolink') }
+
+
+
{ LocalizeText('hccenter.special.time.title') }
+
+
+
{ getHcPaydayTime() }
+
+ { clubStatus === ClubStatus.ACTIVE && +
+
{ LocalizeText('hccenter.special.amount.title') }
+
+
{ getHcPaydayAmount() }
+ +
+ { LocalizeText('hccenter.breakdown.infolink') } +
+
+
+
} +
+ } + { GetConfiguration('hc.center')['gift.info'] && +
+
+

{ LocalizeText('hccenter.gift.title') }

+
0 ? LocalizeText('hccenter.unclaimedgifts', [ 'unclaimedgifts' ], [ unclaimedGifts.toString() ]) : LocalizeText('hccenter.gift.info') } }>
+
+ +
} + { GetConfiguration('hc.center')['benefits.info'] && +
+
{ LocalizeText('hccenter.general.title') }
+
+ +
} + + + ); +} diff --git a/apps/frontend/src/components/help/HelpView.scss b/apps/frontend/src/components/help/HelpView.scss new file mode 100644 index 0000000..abe4189 --- /dev/null +++ b/apps/frontend/src/components/help/HelpView.scss @@ -0,0 +1,18 @@ +.nitro-help { + height: $help-height; + width: $help-width; + + .index-image { + background: url('@/assets/images/help/help_index.png'); + width: 126px; + height: 105px; + } +} + +.nitro-cfh-sanction-status { + width: 400px; +} + +.nitro-change-username { + width: 300px; +} diff --git a/apps/frontend/src/components/help/HelpView.tsx b/apps/frontend/src/components/help/HelpView.tsx new file mode 100644 index 0000000..e0df417 --- /dev/null +++ b/apps/frontend/src/components/help/HelpView.tsx @@ -0,0 +1,116 @@ +import { ILinkEventTracker } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useState } from 'react'; +import { AddEventLinkTracker, LocalizeText, RemoveLinkEventTracker, ReportState } from '../../api'; +import { Base, Column, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../common'; +import { useHelp } from '../../hooks'; +import { DescribeReportView } from './views/DescribeReportView'; +import { HelpIndexView } from './views/HelpIndexView'; +import { NameChangeView } from './views/name-change/NameChangeView'; +import { ReportSummaryView } from './views/ReportSummaryView'; +import { SanctionSatusView } from './views/SanctionStatusView'; +import { SelectReportedChatsView } from './views/SelectReportedChatsView'; +import { SelectReportedUserView } from './views/SelectReportedUserView'; +import { SelectTopicView } from './views/SelectTopicView'; + +export const HelpView: FC<{}> = props => +{ + const [ isVisible, setIsVisible ] = useState(false); + const { activeReport = null, setActiveReport = null, report = null } = useHelp(); + + const onClose = () => + { + setActiveReport(null); + setIsVisible(false); + } + + 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(prevValue => !prevValue); + return; + case 'tour': + // todo: launch tour + return; + case 'report': + if((parts.length >= 5) && (parts[2] === 'room')) + { + const roomId = parseInt(parts[3]); + const unknown = unescape(parts.splice(4).join('/')); + //this.reportRoom(roomId, unknown, ""); + } + return; + } + }, + eventUrlPrefix: 'help/' + }; + + AddEventLinkTracker(linkTracker); + + return () => RemoveLinkEventTracker(linkTracker); + }, []); + + useEffect(() => + { + if(!activeReport) return; + + setIsVisible(true); + }, [ activeReport ]); + + const CurrentStepView = () => + { + if(activeReport) + { + switch(activeReport.currentStep) + { + case ReportState.SELECT_USER: + return ; + case ReportState.SELECT_CHATS: + return ; + case ReportState.SELECT_TOPICS: + return ; + case ReportState.INPUT_REPORT_MESSAGE: + return ; + case ReportState.REPORT_SUMMARY: + return ; + } + } + + return ; + } + + return ( + <> + { isVisible && + + + + + + + + + + + + + } + + + + ); +} diff --git a/apps/frontend/src/components/help/views/DescribeReportView.tsx b/apps/frontend/src/components/help/views/DescribeReportView.tsx new file mode 100644 index 0000000..5f231d3 --- /dev/null +++ b/apps/frontend/src/components/help/views/DescribeReportView.tsx @@ -0,0 +1,48 @@ +import { FC, useState } from 'react'; +import { LocalizeText, ReportState, ReportType } from '../../../api'; +import { Button, Column, Flex, Text } from '../../../common'; +import { useHelp } from '../../../hooks'; + +export const DescribeReportView: FC<{}> = props => +{ + const [ message, setMessage ] = useState(''); + const { activeReport = null, setActiveReport = null } = useHelp(); + + const submitMessage = () => + { + if(message.length < 15) return; + + setActiveReport(prevValue => + { + const currentStep = ReportState.REPORT_SUMMARY; + + return { ...prevValue, message, currentStep }; + }); + } + + const back = () => + { + setActiveReport(prevValue => + { + return { ...prevValue, currentStep: (prevValue.currentStep - 1) }; + }); + } + + return ( + <> + + { LocalizeText('help.emergency.chat_report.subtitle') } + { LocalizeText('help.cfh.input.text') } + + + + + + + + + ); +} diff --git a/apps/frontend/src/components/mod-tools/views/tickets/CfhChatlogView.tsx b/apps/frontend/src/components/mod-tools/views/tickets/CfhChatlogView.tsx new file mode 100644 index 0000000..c8bdd7b --- /dev/null +++ b/apps/frontend/src/components/mod-tools/views/tickets/CfhChatlogView.tsx @@ -0,0 +1,41 @@ +import { CfhChatlogData, CfhChatlogEvent, GetCfhChatlogMessageComposer } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useState } from 'react'; +import { SendMessageComposer } from '../../../../api'; +import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common'; +import { useMessageEvent } from '../../../../hooks'; +import { ChatlogView } from '../chatlog/ChatlogView'; + +interface CfhChatlogViewProps +{ + issueId: number; + onCloseClick(): void; +} + +export const CfhChatlogView: FC = props => +{ + const { onCloseClick = null, issueId = null } = props; + const [ chatlogData, setChatlogData ] = useState(null); + + useMessageEvent(CfhChatlogEvent, event => + { + const parser = event.getParser(); + + if(!parser || parser.data.issueId !== issueId) return; + + setChatlogData(parser.data); + }); + + useEffect(() => + { + SendMessageComposer(new GetCfhChatlogMessageComposer(issueId)); + }, [ issueId ]); + + return ( + + + + { chatlogData && } + + + ); +} diff --git a/apps/frontend/src/components/mod-tools/views/tickets/ModToolsIssueInfoView.tsx b/apps/frontend/src/components/mod-tools/views/tickets/ModToolsIssueInfoView.tsx new file mode 100644 index 0000000..1179d41 --- /dev/null +++ b/apps/frontend/src/components/mod-tools/views/tickets/ModToolsIssueInfoView.tsx @@ -0,0 +1,86 @@ +import { CloseIssuesMessageComposer, ReleaseIssuesMessageComposer } from '@nitrots/nitro-renderer'; +import { FC, useState } from 'react'; +import { GetIssueCategoryName, LocalizeText, SendMessageComposer } from '../../../../api'; +import { Button, Column, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common'; +import { useModTools } from '../../../../hooks'; +import { CfhChatlogView } from './CfhChatlogView'; + +interface IssueInfoViewProps +{ + issueId: number; + onIssueInfoClosed(issueId: number): void; +} + +export const ModToolsIssueInfoView: FC = props => +{ + const { issueId = null, onIssueInfoClosed = null } = props; + const [ cfhChatlogOpen, setcfhChatlogOpen ] = useState(false); + const { tickets = [], openUserInfo = null } = useModTools(); + const ticket = tickets.find(issue => (issue.issueId === issueId)); + + const releaseIssue = (issueId: number) => + { + SendMessageComposer(new ReleaseIssuesMessageComposer([ issueId ])); + + onIssueInfoClosed(issueId); + } + + const closeIssue = (resolutionType: number) => + { + SendMessageComposer(new CloseIssuesMessageComposer([ issueId ], resolutionType)); + + onIssueInfoClosed(issueId) + } + + return ( + <> + + onIssueInfoClosed(issueId) } /> + + Issue Information + + + + + + + + + + + + + + + + + + + + + + + + + +
Source{ GetIssueCategoryName(ticket.categoryId) }
Category{ LocalizeText('help.cfh.topic.' + ticket.reportedCategoryId) }
Description{ ticket.message }
Caller + openUserInfo(ticket.reporterUserId) }>{ ticket.reporterUserName } +
Reported User + openUserInfo(ticket.reportedUserId) }>{ ticket.reportedUserName } +
+
+ + + + + + + +
+
+
+ { cfhChatlogOpen && + setcfhChatlogOpen(false) }/> } + + ); +} diff --git a/apps/frontend/src/components/mod-tools/views/tickets/ModToolsMyIssuesTabView.tsx b/apps/frontend/src/components/mod-tools/views/tickets/ModToolsMyIssuesTabView.tsx new file mode 100644 index 0000000..2e1827a --- /dev/null +++ b/apps/frontend/src/components/mod-tools/views/tickets/ModToolsMyIssuesTabView.tsx @@ -0,0 +1,47 @@ +import { IssueMessageData, ReleaseIssuesMessageComposer } from '@nitrots/nitro-renderer'; +import { FC } from 'react'; +import { SendMessageComposer } from '../../../../api'; +import { Base, Button, Column, Grid } from '../../../../common'; + +interface ModToolsMyIssuesTabViewProps +{ + myIssues: IssueMessageData[]; + handleIssue: (issueId: number) => void; +} + +export const ModToolsMyIssuesTabView: FC = props => +{ + const { myIssues = null, handleIssue = null } = props; + + return ( + + + + Type + Room/Player + Opened + + + + + + { myIssues && (myIssues.length > 0) && myIssues.map(issue => + { + return ( + + { issue.categoryId } + { issue.reportedUserName } + { new Date(Date.now() - issue.issueAgeInMilliseconds).toLocaleTimeString() } + + + + + + + + ); + }) } + + + ); +} diff --git a/apps/frontend/src/components/mod-tools/views/tickets/ModToolsOpenIssuesTabView.tsx b/apps/frontend/src/components/mod-tools/views/tickets/ModToolsOpenIssuesTabView.tsx new file mode 100644 index 0000000..6ee23cd --- /dev/null +++ b/apps/frontend/src/components/mod-tools/views/tickets/ModToolsOpenIssuesTabView.tsx @@ -0,0 +1,42 @@ +import { IssueMessageData, PickIssuesMessageComposer } from '@nitrots/nitro-renderer'; +import { FC } from 'react'; +import { SendMessageComposer } from '../../../../api'; +import { Base, Button, Column, Grid } from '../../../../common'; + +interface ModToolsOpenIssuesTabViewProps +{ + openIssues: IssueMessageData[]; +} + +export const ModToolsOpenIssuesTabView: FC = props => +{ + const { openIssues = null } = props; + + return ( + + + + Type + Room/Player + Opened + + + + + { openIssues && (openIssues.length > 0) && openIssues.map(issue => + { + return ( + + { issue.categoryId } + { issue.reportedUserName } + { new Date(Date.now() - issue.issueAgeInMilliseconds).toLocaleTimeString() } + + + + + ); + }) } + + + ); +} diff --git a/apps/frontend/src/components/mod-tools/views/tickets/ModToolsPickedIssuesTabView.tsx b/apps/frontend/src/components/mod-tools/views/tickets/ModToolsPickedIssuesTabView.tsx new file mode 100644 index 0000000..19b899b --- /dev/null +++ b/apps/frontend/src/components/mod-tools/views/tickets/ModToolsPickedIssuesTabView.tsx @@ -0,0 +1,39 @@ +import { IssueMessageData } from '@nitrots/nitro-renderer'; +import { FC } from 'react'; +import { Base, Column, Grid } from '../../../../common'; + +interface ModToolsPickedIssuesTabViewProps +{ + pickedIssues: IssueMessageData[]; +} + +export const ModToolsPickedIssuesTabView: FC = props => +{ + const { pickedIssues = null } = props; + + return ( + + + + Type + Room/Player + Opened + Picker + + + + { pickedIssues && (pickedIssues.length > 0) && pickedIssues.map(issue => + { + return ( + + { issue.categoryId } + { issue.reportedUserName } + { new Date(Date.now() - issue.issueAgeInMilliseconds).toLocaleTimeString() } + { issue.pickerUserName } + + ); + }) } + + + ); +} diff --git a/apps/frontend/src/components/mod-tools/views/tickets/ModToolsTicketsView.tsx b/apps/frontend/src/components/mod-tools/views/tickets/ModToolsTicketsView.tsx new file mode 100644 index 0000000..d6597da --- /dev/null +++ b/apps/frontend/src/components/mod-tools/views/tickets/ModToolsTicketsView.tsx @@ -0,0 +1,91 @@ +import { IssueMessageData } from '@nitrots/nitro-renderer'; +import { FC, useState } from 'react'; +import { GetSessionDataManager } from '../../../../api'; +import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../../../common'; +import { useModTools } from '../../../../hooks'; +import { ModToolsIssueInfoView } from './ModToolsIssueInfoView'; +import { ModToolsMyIssuesTabView } from './ModToolsMyIssuesTabView'; +import { ModToolsOpenIssuesTabView } from './ModToolsOpenIssuesTabView'; +import { ModToolsPickedIssuesTabView } from './ModToolsPickedIssuesTabView'; + +interface ModToolsTicketsViewProps +{ + onCloseClick: () => void; +} + +const TABS: string[] = [ + 'Open Issues', + 'My Issues', + 'Picked Issues' +]; + +export const ModToolsTicketsView: FC = props => +{ + const { onCloseClick = null } = props; + const [ currentTab, setCurrentTab ] = useState(0); + const [ issueInfoWindows, setIssueInfoWindows ] = useState([]); + const { tickets = [] } = useModTools(); + + const openIssues = tickets.filter(issue => issue.state === IssueMessageData.STATE_OPEN); + const myIssues = tickets.filter(issue => (issue.state === IssueMessageData.STATE_PICKED) && (issue.pickerUserId === GetSessionDataManager().userId)); + const pickedIssues = tickets.filter(issue => issue.state === IssueMessageData.STATE_PICKED); + + const closeIssue = (issueId: number) => + { + setIssueInfoWindows(prevValue => + { + const newValue = [ ...prevValue ]; + const existingIndex = newValue.indexOf(issueId); + + if(existingIndex >= 0) newValue.splice(existingIndex, 1); + + return newValue; + }); + } + + const handleIssue = (issueId: number) => + { + setIssueInfoWindows(prevValue => + { + const newValue = [ ...prevValue ]; + const existingIndex = newValue.indexOf(issueId); + + if(existingIndex === -1) newValue.push(issueId); + else newValue.splice(existingIndex, 1); + + return newValue; + }) + } + + const CurrentTabComponent = () => + { + switch(currentTab) + { + case 0: return ; + case 1: return ; + case 2: return ; + } + + return null; + } + + return ( + <> + + + + { TABS.map((tab, index) => + { + return ( setCurrentTab(index) }> + { tab } + ); + }) } + + + + + + { issueInfoWindows && (issueInfoWindows.length > 0) && issueInfoWindows.map(issueId => ) } + + ); +} diff --git a/apps/frontend/src/components/mod-tools/views/user/ModToolsUserChatlogView.tsx b/apps/frontend/src/components/mod-tools/views/user/ModToolsUserChatlogView.tsx new file mode 100644 index 0000000..7bfb2e1 --- /dev/null +++ b/apps/frontend/src/components/mod-tools/views/user/ModToolsUserChatlogView.tsx @@ -0,0 +1,44 @@ +import { ChatRecordData, GetUserChatlogMessageComposer, UserChatlogEvent } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useState } from 'react'; +import { SendMessageComposer } from '../../../../api'; +import { DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common'; +import { useMessageEvent } from '../../../../hooks'; +import { ChatlogView } from '../chatlog/ChatlogView'; + +interface ModToolsUserChatlogViewProps +{ + userId: number; + onCloseClick: () => void; +} + +export const ModToolsUserChatlogView: FC = props => +{ + const { userId = null, onCloseClick = null } = props; + const [ userChatlog, setUserChatlog ] = useState(null); + const [ username, setUsername ] = useState(null); + + useMessageEvent(UserChatlogEvent, event => + { + const parser = event.getParser(); + + if(!parser || parser.data.userId !== userId) return; + + setUsername(parser.data.username); + setUserChatlog(parser.data.roomChatlogs); + }); + + useEffect(() => + { + SendMessageComposer(new GetUserChatlogMessageComposer(userId)); + }, [ userId ]); + + return ( + + + + { userChatlog && + } + + + ); +} diff --git a/apps/frontend/src/components/mod-tools/views/user/ModToolsUserModActionView.tsx b/apps/frontend/src/components/mod-tools/views/user/ModToolsUserModActionView.tsx new file mode 100644 index 0000000..bd4c2d5 --- /dev/null +++ b/apps/frontend/src/components/mod-tools/views/user/ModToolsUserModActionView.tsx @@ -0,0 +1,176 @@ +import { CallForHelpTopicData, DefaultSanctionMessageComposer, ModAlertMessageComposer, ModBanMessageComposer, ModKickMessageComposer, ModMessageMessageComposer, ModMuteMessageComposer, ModTradingLockMessageComposer } from '@nitrots/nitro-renderer'; +import { FC, useMemo, useState } from 'react'; +import { ISelectedUser, LocalizeText, ModActionDefinition, NotificationAlertType, SendMessageComposer } from '../../../../api'; +import { Button, Column, DraggableWindowPosition, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common'; +import { useModTools, useNotification } from '../../../../hooks'; + +interface ModToolsUserModActionViewProps +{ + user: ISelectedUser; + onCloseClick: () => void; +} + +const MOD_ACTION_DEFINITIONS = [ + new ModActionDefinition(1, 'Alert', ModActionDefinition.ALERT, 1, 0), + new ModActionDefinition(2, 'Mute 1h', ModActionDefinition.MUTE, 2, 0), + new ModActionDefinition(3, 'Ban 18h', ModActionDefinition.BAN, 3, 0), + new ModActionDefinition(4, 'Ban 7 days', ModActionDefinition.BAN, 4, 0), + new ModActionDefinition(5, 'Ban 30 days (step 1)', ModActionDefinition.BAN, 5, 0), + new ModActionDefinition(7, 'Ban 30 days (step 2)', ModActionDefinition.BAN, 7, 0), + new ModActionDefinition(6, 'Ban 100 years', ModActionDefinition.BAN, 6, 0), + new ModActionDefinition(106, 'Ban avatar-only 100 years', ModActionDefinition.BAN, 6, 0), + new ModActionDefinition(101, 'Kick', ModActionDefinition.KICK, 0, 0), + new ModActionDefinition(102, 'Lock trade 1 week', ModActionDefinition.TRADE_LOCK, 0, 168), + new ModActionDefinition(104, 'Lock trade permanent', ModActionDefinition.TRADE_LOCK, 0, 876000), + new ModActionDefinition(105, 'Message', ModActionDefinition.MESSAGE, 0, 0), +]; + +export const ModToolsUserModActionView: FC = props => +{ + const { user = null, onCloseClick = null } = props; + const [ selectedTopic, setSelectedTopic ] = useState(-1); + const [ selectedAction, setSelectedAction ] = useState(-1); + const [ message, setMessage ] = useState(''); + const { cfhCategories = null, settings = null } = useModTools(); + const { simpleAlert = null } = useNotification(); + + const topics = useMemo(() => + { + const values: CallForHelpTopicData[] = []; + + if(cfhCategories && cfhCategories.length) + { + for(const category of cfhCategories) + { + for(const topic of category.topics) values.push(topic); + } + } + + return values; + }, [ cfhCategories ]); + + const sendAlert = (message: string) => simpleAlert(message, NotificationAlertType.DEFAULT, null, null, 'Error'); + + const sendDefaultSanction = () => + { + let errorMessage: string = null; + + const category = topics[selectedTopic]; + + if(selectedTopic === -1) errorMessage = 'You must select a CFH topic'; + + if(errorMessage) return sendAlert(errorMessage); + + const messageOrDefault = (message.trim().length === 0) ? LocalizeText(`help.cfh.topic.${ category.id }`) : message; + + SendMessageComposer(new DefaultSanctionMessageComposer(user.userId, selectedTopic, messageOrDefault)); + + onCloseClick(); + } + + const sendSanction = () => + { + let errorMessage: string = null; + + const category = topics[selectedTopic]; + const sanction = MOD_ACTION_DEFINITIONS[selectedAction]; + + if((selectedTopic === -1) || (selectedAction === -1)) errorMessage = 'You must select a CFH topic and Sanction'; + else if(!settings || !settings.cfhPermission) errorMessage = 'You do not have permission to do this'; + else if(!category) errorMessage = 'You must select a CFH topic'; + else if(!sanction) errorMessage = 'You must select a sanction'; + + if(errorMessage) + { + sendAlert(errorMessage); + + return; + } + + const messageOrDefault = (message.trim().length === 0) ? LocalizeText(`help.cfh.topic.${ category.id }`) : message; + + switch(sanction.actionType) + { + case ModActionDefinition.ALERT: { + if(!settings.alertPermission) + { + sendAlert('You have insufficient permissions'); + + return; + } + + SendMessageComposer(new ModAlertMessageComposer(user.userId, messageOrDefault, category.id)); + break; + } + case ModActionDefinition.MUTE: + SendMessageComposer(new ModMuteMessageComposer(user.userId, messageOrDefault, category.id)); + break; + case ModActionDefinition.BAN: { + if(!settings.banPermission) + { + sendAlert('You have insufficient permissions'); + + return; + } + + SendMessageComposer(new ModBanMessageComposer(user.userId, messageOrDefault, category.id, selectedAction, (sanction.actionId === 106))); + break; + } + case ModActionDefinition.KICK: { + if(!settings.kickPermission) + { + sendAlert('You have insufficient permissions'); + return; + } + + SendMessageComposer(new ModKickMessageComposer(user.userId, messageOrDefault, category.id)); + break; + } + case ModActionDefinition.TRADE_LOCK: { + const numSeconds = (sanction.actionLengthHours * 60); + + SendMessageComposer(new ModTradingLockMessageComposer(user.userId, messageOrDefault, numSeconds, category.id)); + break; + } + case ModActionDefinition.MESSAGE: { + if(message.trim().length === 0) + { + sendAlert('Please write a message to user'); + + return; + } + + SendMessageComposer(new ModMessageMessageComposer(user.userId, message, category.id)); + break; + } + } + + onCloseClick(); + } + + if(!user) return null; + + return ( + + onCloseClick() } /> + + + + + Optional message type, overrides default + + + + + ); +} diff --git a/apps/frontend/src/components/mod-tools/views/user/ModToolsUserView.tsx b/apps/frontend/src/components/mod-tools/views/user/ModToolsUserView.tsx new file mode 100644 index 0000000..4454a0c --- /dev/null +++ b/apps/frontend/src/components/mod-tools/views/user/ModToolsUserView.tsx @@ -0,0 +1,156 @@ +import { FriendlyTime, GetModeratorUserInfoMessageComposer, ModeratorUserInfoData, ModeratorUserInfoEvent } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useMemo, useState } from 'react'; +import { CreateLinkEvent, LocalizeText, SendMessageComposer } from '../../../../api'; +import { Button, Column, DraggableWindowPosition, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common'; +import { useMessageEvent } from '../../../../hooks'; +import { ModToolsUserModActionView } from './ModToolsUserModActionView'; +import { ModToolsUserRoomVisitsView } from './ModToolsUserRoomVisitsView'; +import { ModToolsUserSendMessageView } from './ModToolsUserSendMessageView'; + +interface ModToolsUserViewProps +{ + userId: number; + onCloseClick: () => void; +} + +export const ModToolsUserView: FC = props => +{ + const { onCloseClick = null, userId = null } = props; + const [ userInfo, setUserInfo ] = useState(null); + const [ sendMessageVisible, setSendMessageVisible ] = useState(false); + const [ modActionVisible, setModActionVisible ] = useState(false); + const [ roomVisitsVisible, setRoomVisitsVisible ] = useState(false); + + const userProperties = useMemo(() => + { + if(!userInfo) return null; + + return [ + { + localeKey: 'modtools.userinfo.userName', + value: userInfo.userName, + showOnline: true + }, + { + localeKey: 'modtools.userinfo.cfhCount', + value: userInfo.cfhCount.toString() + }, + { + localeKey: 'modtools.userinfo.abusiveCfhCount', + value: userInfo.abusiveCfhCount.toString() + }, + { + localeKey: 'modtools.userinfo.cautionCount', + value: userInfo.cautionCount.toString() + }, + { + localeKey: 'modtools.userinfo.banCount', + value: userInfo.banCount.toString() + }, + { + localeKey: 'modtools.userinfo.lastSanctionTime', + value: userInfo.lastSanctionTime + }, + { + localeKey: 'modtools.userinfo.tradingLockCount', + value: userInfo.tradingLockCount.toString() + }, + { + localeKey: 'modtools.userinfo.tradingExpiryDate', + value: userInfo.tradingExpiryDate + }, + { + localeKey: 'modtools.userinfo.minutesSinceLastLogin', + value: FriendlyTime.format(userInfo.minutesSinceLastLogin * 60, '.ago', 2) + }, + { + localeKey: 'modtools.userinfo.lastPurchaseDate', + value: userInfo.lastPurchaseDate + }, + { + localeKey: 'modtools.userinfo.primaryEmailAddress', + value: userInfo.primaryEmailAddress + }, + { + localeKey: 'modtools.userinfo.identityRelatedBanCount', + value: userInfo.identityRelatedBanCount.toString() + }, + { + localeKey: 'modtools.userinfo.registrationAgeInMinutes', + value: FriendlyTime.format(userInfo.registrationAgeInMinutes * 60, '.ago', 2) + }, + { + localeKey: 'modtools.userinfo.userClassification', + value: userInfo.userClassification + } + ]; + }, [ userInfo ]); + + useMessageEvent(ModeratorUserInfoEvent, event => + { + const parser = event.getParser(); + + if(!parser || parser.data.userId !== userId) return; + + setUserInfo(parser.data); + }); + + useEffect(() => + { + SendMessageComposer(new GetModeratorUserInfoMessageComposer(userId)); + }, [ userId ]); + + if(!userInfo) return null; + + return ( + <> + + onCloseClick() } /> + + + + + + { userProperties.map( (property, index) => + { + + return ( + + + + + ); + }) } + +
{ LocalizeText(property.localeKey) } + { property.value } + { property.showOnline && + } +
+
+ + + + + + +
+
+
+ { sendMessageVisible && + setSendMessageVisible(false) } /> } + { modActionVisible && + setModActionVisible(false) } /> } + { roomVisitsVisible && + setRoomVisitsVisible(false) } /> } + + ); +} diff --git a/apps/frontend/src/components/navigator/NavigatorView.scss b/apps/frontend/src/components/navigator/NavigatorView.scss new file mode 100644 index 0000000..ef235bc --- /dev/null +++ b/apps/frontend/src/components/navigator/NavigatorView.scss @@ -0,0 +1,65 @@ +.nitro-navigator { + width: $navigator-width; + height: $navigator-height; + + .navigator-grid { + + .navigator-item { + + .badge { + width: 35px; + min-width: 35px; + } + } + + &:not(.two-columns) { + + .navigator-item { + + &:nth-child(odd) { + background-color: $grid-active-bg-color; + } + } + } + + &.two-columns { + + .navigator-item { + + &:nth-child(4n-2), + &:nth-child(4n-3) { + background: $grid-active-bg-color; + } + } + } + } +} + +.nitro-navigator-doorbell, +.nitro-navigator-password { + width: 250px; +} + +.nitro-room-info { + width: $room-info-width; +} + +.nitro-room-link { + width: 400px; +} + +.nitro-room-settings { + width: 400px; + + .list-container { + height: 100px; + + .list-item { + background-color: $grid-active-bg-color; + } + } +} + +.room-info { + width: 275px; +} diff --git a/apps/frontend/src/components/navigator/NavigatorView.tsx b/apps/frontend/src/components/navigator/NavigatorView.tsx new file mode 100644 index 0000000..aefe50b --- /dev/null +++ b/apps/frontend/src/components/navigator/NavigatorView.tsx @@ -0,0 +1,233 @@ +import { ConvertGlobalRoomIdMessageComposer, HabboWebTools, ILinkEventTracker, LegacyExternalInterface, NavigatorInitComposer, NavigatorSearchComposer, RoomSessionEvent } from '@nitrots/nitro-renderer'; +import { FC, useCallback, useEffect, useRef, useState } from 'react'; +import { FaPlus } from 'react-icons/fa'; +import { AddEventLinkTracker, LocalizeText, RemoveLinkEventTracker, SendMessageComposer, TryVisitRoom } from '../../api'; +import { Base, Column, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common'; +import { useNavigator, useRoomSessionManagerEvent } from '../../hooks'; +import { NavigatorDoorStateView } from './views/NavigatorDoorStateView'; +import { NavigatorRoomCreatorView } from './views/NavigatorRoomCreatorView'; +import { NavigatorRoomInfoView } from './views/NavigatorRoomInfoView'; +import { NavigatorRoomLinkView } from './views/NavigatorRoomLinkView'; +import { NavigatorRoomSettingsView } from './views/room-settings/NavigatorRoomSettingsView'; +import { NavigatorSearchResultView } from './views/search/NavigatorSearchResultView'; +import { NavigatorSearchView } from './views/search/NavigatorSearchView'; + +export const NavigatorView: FC<{}> = props => +{ + const [ isVisible, setIsVisible ] = useState(false); + const [ isReady, setIsReady ] = useState(false); + const [ isCreatorOpen, setCreatorOpen ] = useState(false); + const [ isRoomInfoOpen, setRoomInfoOpen ] = useState(false); + const [ isRoomLinkOpen, setRoomLinkOpen ] = useState(false); + const [ isLoading, setIsLoading ] = useState(false); + const [ needsInit, setNeedsInit ] = useState(true); + const [ needsSearch, setNeedsSearch ] = useState(false); + const { searchResult = null, topLevelContext = null, topLevelContexts = null, navigatorData = null } = useNavigator(); + const pendingSearch = useRef<{ value: string, code: string }>(null); + const elementRef = useRef(); + + useRoomSessionManagerEvent(RoomSessionEvent.CREATED, event => + { + setIsVisible(false); + setCreatorOpen(false); + }); + + const sendSearch = useCallback((searchValue: string, contextCode: string) => + { + setCreatorOpen(false); + + SendMessageComposer(new NavigatorSearchComposer(contextCode, searchValue)); + + setIsLoading(true); + }, []); + + const reloadCurrentSearch = useCallback(() => + { + if(!isReady) + { + setNeedsSearch(true); + + return; + } + + if(pendingSearch.current) + { + sendSearch(pendingSearch.current.value, pendingSearch.current.code); + + pendingSearch.current = null; + + return; + } + + if(searchResult) + { + sendSearch(searchResult.data, searchResult.code); + + return; + } + + if(!topLevelContext) return; + + sendSearch('', topLevelContext.code); + }, [ isReady, searchResult, topLevelContext, sendSearch ]); + + useEffect(() => + { + const linkTracker: ILinkEventTracker = { + linkReceived: (url: string) => + { + const parts = url.split('/'); + + if(parts.length < 2) return; + + switch(parts[1]) + { + case 'show': { + setIsVisible(true); + setNeedsSearch(true); + return; + } + case 'hide': + setIsVisible(false); + return; + case 'toggle': { + if(isVisible) + { + setIsVisible(false); + + return; + } + + setIsVisible(true); + setNeedsSearch(true); + return; + } + case 'toggle-room-info': + setRoomInfoOpen(value => !value); + return; + case 'toggle-room-link': + setRoomLinkOpen(value => !value); + return; + case 'goto': + if(parts.length <= 2) return; + + switch(parts[2]) + { + case 'home': + if(navigatorData.homeRoomId <= 0) return; + + TryVisitRoom(navigatorData.homeRoomId); + break; + default: { + const roomId = parseInt(parts[2]); + + TryVisitRoom(roomId); + } + } + return; + case 'create': + setIsVisible(true); + setCreatorOpen(true); + return; + case 'search': + if(parts.length > 2) + { + const topLevelContextCode = parts[2]; + + let searchValue = ''; + + if(parts.length > 3) searchValue = parts[3]; + + pendingSearch.current = { value: searchValue, code: topLevelContextCode }; + + setIsVisible(true); + setNeedsSearch(true); + } + return; + } + }, + eventUrlPrefix: 'navigator/' + }; + + AddEventLinkTracker(linkTracker); + + return () => RemoveLinkEventTracker(linkTracker); + }, [ isVisible, navigatorData ]); + + useEffect(() => + { + if(!searchResult) return; + + setIsLoading(false); + + if(elementRef && elementRef.current) elementRef.current.scrollTop = 0; + }, [ searchResult ]); + + useEffect(() => + { + if(!isVisible || !isReady || !needsSearch) return; + + reloadCurrentSearch(); + + setNeedsSearch(false); + }, [ isVisible, isReady, needsSearch, reloadCurrentSearch ]); + + useEffect(() => + { + if(isReady || !topLevelContext) return; + + setIsReady(true); + }, [ isReady, topLevelContext ]); + + useEffect(() => + { + if(!isVisible || !needsInit) return; + + SendMessageComposer(new NavigatorInitComposer()); + + setNeedsInit(false); + }, [ isVisible, needsInit ]); + + useEffect(() => + { + LegacyExternalInterface.addCallback(HabboWebTools.OPENROOM, (k: string, _arg_2: boolean = false, _arg_3: string = null) => SendMessageComposer(new ConvertGlobalRoomIdMessageComposer(k))); + }, []); + + return ( + <> + { isVisible && + + setIsVisible(false) } /> + + { topLevelContexts && (topLevelContexts.length > 0) && topLevelContexts.map((context, index) => + { + return ( + sendSearch('', context.code) }> + { LocalizeText(('navigator.toplevelview.' + context.code)) } + + ); + }) } + setCreatorOpen(true) }> + + + + + { isLoading && + } + { !isCreatorOpen && + <> + + + { (searchResult && searchResult.results.map((result, index) => )) } + + } + { isCreatorOpen && } + + } + + { isRoomInfoOpen && setRoomInfoOpen(false) } /> } + { isRoomLinkOpen && setRoomLinkOpen(false) } /> } + + + ); +} diff --git a/apps/frontend/src/components/navigator/views/NavigatorDoorStateView.tsx b/apps/frontend/src/components/navigator/views/NavigatorDoorStateView.tsx new file mode 100644 index 0000000..4c8a3e2 --- /dev/null +++ b/apps/frontend/src/components/navigator/views/NavigatorDoorStateView.tsx @@ -0,0 +1,110 @@ +import { FC, useEffect, useState } from 'react'; +import { CreateRoomSession, DoorStateType, GoToDesktop, LocalizeText } from '../../../api'; +import { Button, Column, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../common'; +import { useNavigator } from '../../../hooks'; + +const VISIBLE_STATES = [ DoorStateType.START_DOORBELL, DoorStateType.STATE_WAITING, DoorStateType.STATE_NO_ANSWER, DoorStateType.START_PASSWORD, DoorStateType.STATE_WRONG_PASSWORD ]; +const DOORBELL_STATES = [ DoorStateType.START_DOORBELL, DoorStateType.STATE_WAITING, DoorStateType.STATE_NO_ANSWER ]; +const PASSWORD_STATES = [ DoorStateType.START_PASSWORD, DoorStateType.STATE_WRONG_PASSWORD ]; + +export const NavigatorDoorStateView: FC<{}> = props => +{ + const [ password, setPassword ] = useState(''); + const { doorData = null, setDoorData = null } = useNavigator(); + + const onClose = () => + { + if(doorData && (doorData.state === DoorStateType.STATE_WAITING)) GoToDesktop(); + + setDoorData(null); + } + + const ring = () => + { + if(!doorData || !doorData.roomInfo) return; + + CreateRoomSession(doorData.roomInfo.roomId); + + setDoorData(prevValue => + { + const newValue = { ...prevValue }; + + newValue.state = DoorStateType.STATE_PENDING_SERVER; + + return newValue; + }); + } + + const tryEntering = () => + { + if(!doorData || !doorData.roomInfo) return; + + CreateRoomSession(doorData.roomInfo.roomId, password); + + setDoorData(prevValue => + { + const newValue = { ...prevValue }; + + newValue.state = DoorStateType.STATE_PENDING_SERVER; + + return newValue; + }); + } + + useEffect(() => + { + if(!doorData || (doorData.state !== DoorStateType.STATE_NO_ANSWER)) return; + + GoToDesktop(); + }, [ doorData ]); + + if(!doorData || (doorData.state === DoorStateType.NONE) || (VISIBLE_STATES.indexOf(doorData.state) === -1)) return null; + + const isDoorbell = (DOORBELL_STATES.indexOf(doorData.state) >= 0); + + return ( + + + + + { doorData && doorData.roomInfo && doorData.roomInfo.roomName } + { (doorData.state === DoorStateType.START_DOORBELL) && + { LocalizeText('navigator.doorbell.info') } } + { (doorData.state === DoorStateType.STATE_WAITING) && + { LocalizeText('navigator.doorbell.waiting') } } + { (doorData.state === DoorStateType.STATE_NO_ANSWER) && + { LocalizeText('navigator.doorbell.no.answer') } } + { (doorData.state === DoorStateType.START_PASSWORD) && + { LocalizeText('navigator.password.info') } } + { (doorData.state === DoorStateType.STATE_WRONG_PASSWORD) && + { LocalizeText('navigator.password.retryinfo') } } + + { isDoorbell && + + { (doorData.state === DoorStateType.START_DOORBELL) && + } + + } + { !isDoorbell && + <> + + { LocalizeText('navigator.password.enter') } + setPassword(event.target.value) } /> + + + + + + } + + + ); +} diff --git a/apps/frontend/src/components/navigator/views/NavigatorRoomCreatorView.tsx b/apps/frontend/src/components/navigator/views/NavigatorRoomCreatorView.tsx new file mode 100644 index 0000000..516fd09 --- /dev/null +++ b/apps/frontend/src/components/navigator/views/NavigatorRoomCreatorView.tsx @@ -0,0 +1,122 @@ +/* eslint-disable no-template-curly-in-string */ +import { CreateFlatMessageComposer, HabboClubLevelEnum } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useState } from 'react'; +import { GetClubMemberLevel, GetConfiguration, IRoomModel, LocalizeText, SendMessageComposer } from '../../../api'; +import { Button, Column, Flex, Grid, LayoutCurrencyIcon, LayoutGridItem, Text } from '../../../common'; +import { useNavigator } from '../../../hooks'; + +export const NavigatorRoomCreatorView: FC<{}> = props => +{ + const [ maxVisitorsList, setMaxVisitorsList ] = useState(null); + const [ name, setName ] = useState(null); + const [ description, setDescription ] = useState(null); + const [ category, setCategory ] = useState(null); + const [ visitorsCount, setVisitorsCount ] = useState(null); + const [ tradesSetting, setTradesSetting ] = useState(0); + const [ roomModels, setRoomModels ] = useState([]); + const [ selectedModelName, setSelectedModelName ] = useState(''); + const { categories = null } = useNavigator(); + + const hcDisabled = GetConfiguration('hc.disabled', false); + + const getRoomModelImage = (name: string) => GetConfiguration('images.url') + `/navigator/models/model_${ name }.png`; + + const selectModel = (model: IRoomModel, index: number) => + { + if(!model || (model.clubLevel > GetClubMemberLevel())) return; + + setSelectedModelName(roomModels[index].name); + }; + + const createRoom = () => + { + SendMessageComposer(new CreateFlatMessageComposer(name, description, 'model_' + selectedModelName, Number(category), Number(visitorsCount), tradesSetting)); + }; + + useEffect(() => + { + if(!maxVisitorsList) + { + const list = []; + + for(let i = 10; i <= 100; i = i + 10) list.push(i); + + setMaxVisitorsList(list); + setVisitorsCount(list[0]); + } + }, [ maxVisitorsList ]); + + useEffect(() => + { + if(categories && categories.length) setCategory(categories[0].id); + }, [ categories ]); + + useEffect(() => + { + const models = GetConfiguration('navigator.room.models'); + + if(models && models.length) + { + setRoomModels(models); + setSelectedModelName(models[0].name); + } + }, []); + + return ( + + + + + { LocalizeText('navigator.createroom.roomnameinfo') } + setName(event.target.value) } placeholder={ LocalizeText('navigator.createroom.roomnameinfo') } /> + + + { LocalizeText('navigator.createroom.roomdescinfo') } + +
+
+ + ); +} diff --git a/apps/frontend/src/components/room/widgets/furniture/FurnitureStackHeightView.tsx b/apps/frontend/src/components/room/widgets/furniture/FurnitureStackHeightView.tsx new file mode 100644 index 0000000..d4d1039 --- /dev/null +++ b/apps/frontend/src/components/room/widgets/furniture/FurnitureStackHeightView.tsx @@ -0,0 +1,58 @@ +import { FurnitureStackHeightComposer } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useState } from 'react'; +import ReactSlider from 'react-slider'; +import { LocalizeText, SendMessageComposer } from '../../../../api'; +import { Button, Column, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common'; +import { useFurnitureStackHeightWidget } from '../../../../hooks'; + +export const FurnitureStackHeightView: FC<{}> = props => +{ + const { objectId = -1, height = 0, maxHeight = 40, onClose = null, updateHeight = null } = useFurnitureStackHeightWidget(); + const [ tempHeight, setTempHeight ] = useState(''); + + const updateTempHeight = (value: string) => + { + setTempHeight(value); + + const newValue = parseFloat(value); + + if(isNaN(newValue) || (newValue === height)) return; + + updateHeight(newValue); + } + + useEffect(() => + { + setTempHeight(height.toString()); + }, [ height ]); + + if(objectId === -1) return null; + + return ( + + + + { LocalizeText('widget.custom.stack.height.text') } + + updateHeight(event) } + renderThumb={ (props, state) =>
{ state.valueNow }
} /> + updateTempHeight(event.target.value) } /> +
+ + + + +
+
+ ); +} diff --git a/apps/frontend/src/components/room/widgets/furniture/FurnitureStickieView.tsx b/apps/frontend/src/components/room/widgets/furniture/FurnitureStickieView.tsx new file mode 100644 index 0000000..2f0e0fe --- /dev/null +++ b/apps/frontend/src/components/room/widgets/furniture/FurnitureStickieView.tsx @@ -0,0 +1,66 @@ +import { FC, useEffect, useState } from 'react'; +import { ColorUtils } from '../../../../api'; +import { DraggableWindow, DraggableWindowPosition } from '../../../../common'; +import { useFurnitureStickieWidget } from '../../../../hooks'; + +const STICKIE_COLORS = [ '9CCEFF','FF9CFF', '9CFF9C','FFFF33' ]; +const STICKIE_COLOR_NAMES = [ 'blue', 'pink', 'green', 'yellow' ]; +const STICKIE_TYPES = [ 'post_it','post_it_shakesp', 'post_it_dreams','post_it_xmas', 'post_it_vd', 'post_it_juninas' ]; +const STICKIE_TYPE_NAMES = [ 'post_it', 'shakesp', 'dreams', 'christmas', 'heart', 'juninas' ]; + +const getStickieColorName = (color: string) => +{ + let index = STICKIE_COLORS.indexOf(color); + + if(index === -1) index = 0; + + return STICKIE_COLOR_NAMES[index]; +} + +const getStickieTypeName = (type: string) => +{ + let index = STICKIE_TYPES.indexOf(type); + + if(index === -1) index = 0; + + return STICKIE_TYPE_NAMES[index]; +} + +export const FurnitureStickieView: FC<{}> = props => +{ + const { objectId = -1, color = '0', text = '', type = '', canModify = false, updateColor = null, updateText = null, trash = null, onClose = null } = useFurnitureStickieWidget(); + const [ isEditing, setIsEditing ] = useState(false); + + useEffect(() => + { + setIsEditing(false); + }, [ objectId, color, text, type ]); + + if(objectId === -1) return null; + + return ( + +
+
+
+ { canModify && + <> +
+ { type == 'post_it' && + <> + { STICKIE_COLORS.map(color => + { + return
updateColor(color) } style={ { backgroundColor: ColorUtils.makeColorHex(color) } } /> + }) } + } + } +
+
+
+
+ { (!isEditing || !canModify) ?
(canModify && setIsEditing(true)) }>{ text }
: } +
+
+ + ); +} diff --git a/apps/frontend/src/components/room/widgets/furniture/FurnitureTrophyView.tsx b/apps/frontend/src/components/room/widgets/furniture/FurnitureTrophyView.tsx new file mode 100644 index 0000000..7977097 --- /dev/null +++ b/apps/frontend/src/components/room/widgets/furniture/FurnitureTrophyView.tsx @@ -0,0 +1,12 @@ +import { FC } from 'react'; +import { LayoutTrophyView } from '../../../../common'; +import { useFurnitureTrophyWidget } from '../../../../hooks'; + +export const FurnitureTrophyView: FC<{}> = props => +{ + const { objectId = -1, color = '1', senderName = '', date = '', message = '', onClose = null } = useFurnitureTrophyWidget(); + + if(objectId === -1) return null; + + return ; +} diff --git a/apps/frontend/src/components/room/widgets/furniture/FurnitureWidgets.scss b/apps/frontend/src/components/room/widgets/furniture/FurnitureWidgets.scss new file mode 100644 index 0000000..65b6559 --- /dev/null +++ b/apps/frontend/src/components/room/widgets/furniture/FurnitureWidgets.scss @@ -0,0 +1,526 @@ +.nitro-room-widgets { + pointer-events: none; +} + +.nitro-widget-custom-stack-height { + width: $nitro-widget-custom-stack-height-width; + height: $nitro-widget-custom-stack-height-height; +} + +.nitro-room-widget-toner { + width: 190px; +} + +.nitro-room-widget-dimmer { + width: 275px; + + .dimmer-banner { + width: 56px; + height: 79px; + background: url('@/assets/images/room-widgets/dimmer-widget/dimmer_banner.png') + center no-repeat; + } + + .color-swatch { + height: 30px; + border: 2px solid $white; + box-shadow: inset 3px 3px rgba(0, 0, 0, 0.2); + + &.active { + box-shadow: none; + } + } +} + +.nitro-widget-crafting { + width: $nitro-widget-crafting-width; + height: $nitro-widget-crafting-height; +} + +.nitro-widget-exchange-credit { + width: $nitro-widget-exchange-credit-width; + height: $nitro-widget-exchange-credit-height; + + .exchange-image { + background-image: url('@/assets/images/room-widgets/exchange-credit/exchange-credit-image.png'); + width: 103px; + height: 103px; + } +} + +.nitro-external-image-widget { + .picture-preview { + width: 320px; + height: 320px; + } + + .picture-preview-buttons { + display: flex; + align-items: center; + justify-content: space-between; + color: black; + } + + .picture-preview-buttons-previous, + .picture-preview-buttons-next { + color: #222; + background-color: white; + padding: 10px; + border-radius: 50%; + } +} + +.nitro-gift-opening { + width: 340px; + resize: none; +} + +.nitro-mannequin { + width: 300px; + + .mannequin-preview { + display: flex; + justify-content: center; + align-items: center; + width: 83px; + height: 130px; + background-image: url('@/assets/images/room-widgets/mannequin-widget/mannequin-spritesheet.png'); + overflow: hidden; + + .avatar-image { + background-position: unset; + top: -8px; + } + } +} + +.nitro-stickie { + position: relative; + width: 185px; + height: 178px; + top: 25px; + left: 25px; + padding: 1px; + pointer-events: all; + + .stickie-header { + width: 183px; + height: 18px; + padding: 0 7px; + + .header-trash, + .header-close { + cursor: pointer; + } + + .stickie-color { + width: 10px; + height: 10px; + cursor: pointer; + } + } + + .stickie-context { + width: 183px; + height: 145px; + padding: 2px 7px; + font-size: 12px; + color: $black; + + .context-text { + width: 100%; + height: 100%; + padding: 0; + overflow-wrap: break-word; + white-space: break-spaces; + overflow-y: auto; + } + + textarea { + background: transparent; + border: 0; + outline: none; + box-shadow: none; + resize: none; + font-style: italic; + + &:active { + border: 0; + outline: none; + box-shadow: none; + } + } + } +} + +.nitro-stickie-image { + background-image: url('@/assets/images/room-widgets/stickie-widget/stickie-spritesheet.png'); + + &.stickie-blue, + &.stickie-yellow, + &.stickie-green, + &.stickie-pink { + width: 185px; + height: 178px; + } + + &.stickie-blue { + background-position: -2px -2px; + } + + &.stickie-yellow { + background-image: url('@/assets/images/room-widgets/stickie-widget/stickie-yellow.png'); + //background-position: -191px -184px; + } + + &.stickie-green { + background-position: -191px -2px; + } + + &.stickie-pink { + background-position: -2px -184px; + } + + &.stickie-christmas { + background-image: url('@/assets/images/room-widgets/stickie-widget/stickie-christmas.png'); + } + + &.stickie-shakesp { + background-image: url('@/assets/images/room-widgets/stickie-widget/stickie-shakesp.png'); + } + + &.stickie-dreams { + background-image: url('@/assets/images/room-widgets/stickie-widget/stickie-dreams.png'); + } + + &.stickie-heart { + background-image: url('@/assets/images/room-widgets/stickie-widget/stickie-heart.png'); + } + + &.stickie-juninas { + background-image: url('@/assets/images/room-widgets/stickie-widget/stickie-juninas.png'); + } + + &.stickie-close { + width: 10px; + height: 10px; + background-position: -2px -366px; + } + + &.stickie-trash { + width: 9px; + height: 10px; + background-position: -16px -366px; + } +} + +.nitro-engraving-lock { + width: 300px; + + .engraving-lock-stage-1 { + width: 31px; + height: 39px; + background-position: -380px -43px; + background-image: url('@/assets/images/room-widgets/engraving-lock-widget/engraving-lock-spritesheet.png'); + } + + .engraving-lock-stage-2 { + width: 36px; + height: 43px; + background-position: -375px 0px; + background-image: url('@/assets/images/room-widgets/engraving-lock-widget/engraving-lock-spritesheet.png'); + } +} + +.nitro-engraving-lock-view { + width: 375px; + height: 210px; + background-position: 0px 0px; + background-image: url('@/assets/images/room-widgets/engraving-lock-widget/engraving-lock-spritesheet.png'); + + color: #622e54; + font-weight: bold; + font-size: 16px; + text-shadow: 0px 1px white; + + &.engraving-lock-3 { + background-position: 0px -210px; + color: #614110; + } + + &.engraving-lock-4 { + background-position: 0px -420px; + color: #f1dcc8; + text-shadow: 0px 2px rgba(0, 0, 0, 0.4); + + .engraving-lock-avatar { + margin-bottom: 10px; + } + } + + .engraving-lock-close { + position: absolute; + cursor: pointer; + width: 15px; + height: 15px; + top: 34px; + right: 27px; + } + + .engraving-lock-avatar { + width: 70px; + height: 120px; + + div { + position: absolute; + margin-top: -5px; + } + + &:nth-child(1) { + div { + margin-left: -10px; + } + } + + &:nth-child(2) { + div { + margin-left: -15px; + } + } + } +} + +.nitro-widget-high-score { + width: 250px; + max-width: 250px; + height: 200px; +} + +.youtube-tv-widget { + width: 600px; + height: 380px; + + .youtube-video-container { + .empty-video { + background-color: black; + color: white; + width: 100%; + height: 100%; + text-align: center; + } + + .youtubeContainer { + position: relative; + width: 100%; + height: 100%; + overflow: hidden; + margin-bottom: 50px; + } + + .youtubeContainer iframe { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + } + } + + .playlist-container { + overflow-y: auto; + margin-right: -10px; + color: black; + height: 100%; + + .playlist-controls { + width: 100%; + .icon { + margin-right: 10px; + margin-bottom: 10px; + } + } + + .playlist-grid { + height: 100%; + width: 100%; + } + } +} + +.nitro-playlist-editor-widget { + width: 625px; + height: 440px; + + img.my-music { + position: absolute; + top: -4px; + left: -4px; + z-index: 0; + } + + img.playlist-img { + position: absolute; + top: -4px; + left: 0; + z-index: 0; + } + + img.get-more, + img.add-songs { + position: absolute; + bottom: 0; + left: 0; + z-index: 0; + } + + .playlist-bottom { + z-index: 3; + } + + .move-disk { + width: 22px; + height: 18px; + background-image: url('@/assets/images/room-widgets/playlist-editor/move.png'); + } + + .disk-2, + .disk-image { + background-blend-mode: multiply; + } + + .disk-2 { + width: 38px; + height: 38px; + background-image: url('@/assets/images/room-widgets/playlist-editor/disk_2.png'); + background-position: center; + background-repeat: no-repeat; + + &.playing-song { + background-image: url('@/assets/images/room-widgets/playlist-editor/playing.png'); + } + + &.selected-song { + background-image: url('@/assets/images/room-widgets/playlist-editor/move.png'); + transform: scaleX(-1); + } + + &:not(.playing-song):not(.selected-song) { + -webkit-mask-image: url('@/assets/images/room-widgets/playlist-editor/disk_2.png'); + mask-image: url('@/assets/images/room-widgets/playlist-editor/disk_2.png'); + } + } + + .pause-song { + width: 18px; + height: 20px; + background-image: url('@/assets/images/room-widgets/playlist-editor/pause.png'); + } + + .pause-btn { + width: 16px; + height: 16px; + + background-image: url('@/assets/images/room-widgets/playlist-editor/pause-btn.png'); + } + + .music-note { + width: 38px; + height: 38px; + background-image: url('@/assets/images/room-widgets/playlist-editor/playing.png'); + } + + .preview-song { + width: 16px; + height: 16px; + background-image: url('@/assets/images/room-widgets/playlist-editor/preview.png'); + } + + .layout-grid-item { + min-height: 95px; + min-width: 95px; + position: relative; + + .disk-image { + background: url('@/assets/images/room-widgets/playlist-editor/disk_image.png'); + -webkit-mask-image: url('@/assets/images/room-widgets/playlist-editor/disk_image.png'); + mask-image: url('@/assets/images/room-widgets/playlist-editor/disk_image.png'); + height: 76px; + width: 76px; + } + } +} + +.nitro-mysterybox-dialog { + width: 375px; + height: 210px; + + .prize-container { + height: 80px; + width: 81px; + background-image: url('@/assets/images/prize/prize_background.png'); + background-repeat: no-repeat; + background-position: center; + } +} + +.nitro-mysterytrophy-dialog +{ + .mysterytrophy-dialog-top + { + width: 400px; + height: 120px; + border-radius: 2px; + background-color: #0E3F52; + + .mysterytrophy-image + { + width: 80px; + height: 84px; + position: relative; + background-image: url('@/assets/images/mysterytrophy/frank_mystery_trophy.png'); + background-repeat: no-repeat; + } + + .mysterytrophy-text-big + { + font-size: 16px; + } + } + + .mysterytrophy-dialog-bottom + { + display: flex; + justify-content: center; + width: 400px; + height: 120px; + border-radius: 2px; + background-color: #E9E9E1; + + .input-mysterytrophy-dialog + { + width: 380px; + border: 1px solid black; + + .input-mysterytrophy + { + width: 350px; + border: 0; + outline: 0; + } + + .mysterytrophy-pencil-image + { + width: 16px; + height: 16px; + position: relative; + background-image: url('@/assets/images/infostand/pencil-icon.png'); + background-repeat: no-repeat; + } + } + + .text-decoration + { + text-decoration: underline; + } + } +} diff --git a/apps/frontend/src/components/room/widgets/furniture/FurnitureWidgetsView.tsx b/apps/frontend/src/components/room/widgets/furniture/FurnitureWidgetsView.tsx new file mode 100644 index 0000000..d0e4066 --- /dev/null +++ b/apps/frontend/src/components/room/widgets/furniture/FurnitureWidgetsView.tsx @@ -0,0 +1,48 @@ +import { FC } from 'react'; +import { Base } from '../../../../common'; +import { FurnitureContextMenuView } from './context-menu/FurnitureContextMenuView'; +import { FurnitureBackgroundColorView } from './FurnitureBackgroundColorView'; +import { FurnitureBadgeDisplayView } from './FurnitureBadgeDisplayView'; +import { FurnitureCraftingView } from './FurnitureCraftingView'; +import { FurnitureDimmerView } from './FurnitureDimmerView'; +import { FurnitureExchangeCreditView } from './FurnitureExchangeCreditView'; +import { FurnitureExternalImageView } from './FurnitureExternalImageView'; +import { FurnitureFriendFurniView } from './FurnitureFriendFurniView'; +import { FurnitureGiftOpeningView } from './FurnitureGiftOpeningView'; +import { FurnitureHighScoreView } from './FurnitureHighScoreView'; +import { FurnitureInternalLinkView } from './FurnitureInternalLinkView'; +import { FurnitureMannequinView } from './FurnitureMannequinView'; +import { FurnitureRoomLinkView } from './FurnitureRoomLinkView'; +import { FurnitureSpamWallPostItView } from './FurnitureSpamWallPostItView'; +import { FurnitureStackHeightView } from './FurnitureStackHeightView'; +import { FurnitureStickieView } from './FurnitureStickieView'; +import { FurnitureTrophyView } from './FurnitureTrophyView'; +import { FurnitureYoutubeDisplayView } from './FurnitureYoutubeDisplayView'; +import { FurniturePlaylistEditorWidgetView } from './playlist-editor/FurniturePlaylistEditorWidgetView'; + +export const FurnitureWidgetsView: FC<{}> = props => +{ + return ( + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/apps/frontend/src/components/room/widgets/furniture/FurnitureYoutubeDisplayView.tsx b/apps/frontend/src/components/room/widgets/furniture/FurnitureYoutubeDisplayView.tsx new file mode 100644 index 0000000..cd7d43d --- /dev/null +++ b/apps/frontend/src/components/room/widgets/furniture/FurnitureYoutubeDisplayView.tsx @@ -0,0 +1,109 @@ +import { FC, useEffect, useState } from 'react'; +import YouTube, { Options } from 'react-youtube'; +import { YouTubePlayer } from 'youtube-player/dist/types'; +import { LocalizeText, YoutubeVideoPlaybackStateEnum } from '../../../../api'; +import { AutoGrid, AutoGridProps, LayoutGridItem, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common'; +import { useFurnitureYoutubeWidget } from '../../../../hooks'; + +interface FurnitureYoutubeDisplayViewProps extends AutoGridProps +{ + +} + +export const FurnitureYoutubeDisplayView: FC<{}> = FurnitureYoutubeDisplayViewProps => +{ + const [ player, setPlayer ] = useState(null); + const { objectId = -1, videoId = null, videoStart = 0, videoEnd = 0, currentVideoState = null, selectedVideo = null, playlists = [], onClose = null, previous = null, next = null, pause = null, play = null, selectVideo = null } = useFurnitureYoutubeWidget(); + + const onStateChange = (event: { target: YouTubePlayer; data: number }) => + { + setPlayer(event.target); + + if(objectId === -1) return; + + switch(event.target.getPlayerState()) + { + case -1: + case 1: + if(currentVideoState === 2) + { + //event.target.pauseVideo(); + } + + if(currentVideoState !== 1) play(); + return; + case 2: + if(currentVideoState !== 2) pause(); + } + } + + useEffect(() => + { + if((currentVideoState === null) || !player) return; + + if((currentVideoState === YoutubeVideoPlaybackStateEnum.PLAYING) && (player.getPlayerState() !== YoutubeVideoPlaybackStateEnum.PLAYING)) + { + player.playVideo(); + + return; + } + + if((currentVideoState === YoutubeVideoPlaybackStateEnum.PAUSED) && (player.getPlayerState() !== YoutubeVideoPlaybackStateEnum.PAUSED)) + { + player.pauseVideo(); + + return; + } + }, [ currentVideoState, player ]); + + if(objectId === -1) return null; + + const youtubeOptions: Options = { + height: '375', + width: '500', + playerVars: { + autoplay: 1, + disablekb: 1, + controls: 0, + origin: window.origin, + modestbranding: 1, + start: videoStart, + end: videoEnd + } + } + + return ( + + + +
+
+ { (videoId && videoId.length > 0) && + setPlayer(event.target) } onStateChange={ onStateChange } containerClassName={ 'youtubeContainer' } /> + } + { (!videoId || videoId.length === 0) && +
{ LocalizeText('widget.furni.video_viewer.no_videos') }
+ } +
+
+ + + + +
{ LocalizeText('widget.furni.video_viewer.playlists') }
+ + { playlists && playlists.map((entry, index) => + { + return ( + selectVideo(entry.video) } itemActive={ (entry.video === selectedVideo) }> + { entry.title } + + ) + }) } + +
+
+
+
+ ) +} diff --git a/apps/frontend/src/components/room/widgets/furniture/context-menu/EffectBoxConfirmView.tsx b/apps/frontend/src/components/room/widgets/furniture/context-menu/EffectBoxConfirmView.tsx new file mode 100644 index 0000000..f7e35b9 --- /dev/null +++ b/apps/frontend/src/components/room/widgets/furniture/context-menu/EffectBoxConfirmView.tsx @@ -0,0 +1,40 @@ +import { FC } from 'react'; +import { LocalizeText } from '../../../../../api'; +import { Button, Column, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../../common'; +import { useRoom } from '../../../../../hooks'; + +interface EffectBoxConfirmViewProps +{ + objectId: number; + onClose: () => void; +} + +export const EffectBoxConfirmView: FC = props => +{ + const { objectId = -1, onClose = null } = props; + const { roomSession = null } = useRoom(); + + const useProduct = () => + { + roomSession.useMultistateItem(objectId); + + onClose(); + } + + return ( + + + + + + { LocalizeText('effectbox.header.description') } + + + + + + + + + ); +} diff --git a/apps/frontend/src/components/room/widgets/furniture/context-menu/FurnitureContextMenuView.tsx b/apps/frontend/src/components/room/widgets/furniture/context-menu/FurnitureContextMenuView.tsx new file mode 100644 index 0000000..56ac66d --- /dev/null +++ b/apps/frontend/src/components/room/widgets/furniture/context-menu/FurnitureContextMenuView.tsx @@ -0,0 +1,130 @@ +import { ContextMenuEnum, CustomUserNotificationMessageEvent, RoomObjectCategory } from '@nitrots/nitro-renderer'; +import { FC } from 'react'; +import { GetGroupInformation, GetSessionDataManager, LocalizeText } from '../../../../../api'; +import { EFFECTBOX_OPEN, GROUP_FURNITURE, MONSTERPLANT_SEED_CONFIRMATION, MYSTERYTROPHY_OPEN_DIALOG, PURCHASABLE_CLOTHING_CONFIRMATION, useFurnitureContextMenuWidget, useMessageEvent, useNotification } from '../../../../../hooks'; +import { ContextMenuHeaderView } from '../../context-menu/ContextMenuHeaderView'; +import { ContextMenuListItemView } from '../../context-menu/ContextMenuListItemView'; +import { ContextMenuView } from '../../context-menu/ContextMenuView'; +import { FurnitureMysteryBoxOpenDialogView } from '../FurnitureMysteryBoxOpenDialogView'; +import { FurnitureMysteryTrophyOpenDialogView } from '../FurnitureMysteryTrophyOpenDialogView'; +import { EffectBoxConfirmView } from './EffectBoxConfirmView'; +import { MonsterPlantSeedConfirmView } from './MonsterPlantSeedConfirmView'; +import { PurchasableClothingConfirmView } from './PurchasableClothingConfirmView'; + +export const FurnitureContextMenuView: FC<{}> = props => +{ + const { closeConfirm = null, processAction = null, onClose = null, objectId = -1, mode = null, confirmMode = null, confirmingObjectId = -1, groupData = null, isGroupMember = false, objectOwnerId = -1 } = useFurnitureContextMenuWidget(); + const { simpleAlert = null } = useNotification(); + + useMessageEvent(CustomUserNotificationMessageEvent, event => + { + const parser = event.getParser(); + + if(!parser) return; + + // HOPPER_NO_COSTUME = 1; HOPPER_NO_HC = 2; GATE_NO_HC = 3; STARS_NOT_CANDIDATE = 4 (not coded in Emulator); STARS_NOT_ENOUGH_USERS = 5 (not coded in Emulator); + + switch(parser.count) + { + case 1: + simpleAlert(LocalizeText('costumehopper.costumerequired.bodytext'), null, 'catalog/open/temporary_effects' , LocalizeText('costumehopper.costumerequired.buy'), LocalizeText('costumehopper.costumerequired.header'), null); + break; + case 2: + simpleAlert(LocalizeText('viphopper.viprequired.bodytext'), null, 'catalog/open/habbo_club' , LocalizeText('viprequired.buy.vip'), LocalizeText('viprequired.header'), null); + break; + case 3: + simpleAlert(LocalizeText('gate.viprequired.bodytext'), null, 'catalog/open/habbo_club' , LocalizeText('viprequired.buy.vip'), LocalizeText('gate.viprequired.title'), null); + break; + } + }); + + const isOwner = GetSessionDataManager().userId === objectOwnerId; + + return ( + <> + { (confirmMode === MONSTERPLANT_SEED_CONFIRMATION) && + } + { (confirmMode === PURCHASABLE_CLOTHING_CONFIRMATION) && + } + { (confirmMode === EFFECTBOX_OPEN) && + } + { (confirmMode === MYSTERYTROPHY_OPEN_DIALOG) && + } + + { (objectId >= 0) && mode && + + { (mode === ContextMenuEnum.FRIEND_FURNITURE) && + <> + + { LocalizeText('friendfurni.context.title') } + + processAction('use_friend_furni') }> + { LocalizeText('friendfurni.context.use') } + + } + { (mode === ContextMenuEnum.MONSTERPLANT_SEED) && + <> + + { LocalizeText('furni.mnstr_seed.name') } + + processAction('use_monsterplant_seed') }> + { LocalizeText('widget.monsterplant_seed.button.use') } + + } + { (mode === ContextMenuEnum.RANDOM_TELEPORT) && + <> + + { LocalizeText('furni.random_teleport.name') } + + processAction('use_random_teleport') }> + { LocalizeText('widget.random_teleport.button.use') } + + } + { (mode === ContextMenuEnum.PURCHASABLE_CLOTHING) && + <> + + { LocalizeText('furni.generic_usable.name') } + + processAction('use_purchaseable_clothing') }> + { LocalizeText('widget.generic_usable.button.use') } + + } + { (mode === ContextMenuEnum.MYSTERY_BOX) && + <> + + { LocalizeText('mysterybox.context.title') } + + processAction('use_mystery_box') }> + { LocalizeText('mysterybox.context.' + ((isOwner) ? 'owner' : 'other') + '.use') } + + } + { (mode === ContextMenuEnum.MYSTERY_TROPHY) && + <> + + { LocalizeText('mysterytrophy.header.title') } + + processAction('use_mystery_trophy') }> + { LocalizeText('friendfurni.context.use') } + + } + { (mode === GROUP_FURNITURE) && groupData && + <> + GetGroupInformation(groupData.guildId) }> + { groupData.guildName } + + { !isGroupMember && + processAction('join_group') }> + { LocalizeText('widget.furniture.button.join.group') } + } + processAction('go_to_group_homeroom') }> + { LocalizeText('widget.furniture.button.go.to.group.home.room') } + + { groupData.guildHasReadableForum && + processAction('open_forum') }> + { LocalizeText('widget.furniture.button.open_group_forum') } + } + } + } + + ) +} diff --git a/apps/frontend/src/components/room/widgets/furniture/context-menu/MonsterPlantSeedConfirmView.tsx b/apps/frontend/src/components/room/widgets/furniture/context-menu/MonsterPlantSeedConfirmView.tsx new file mode 100644 index 0000000..b7163cc --- /dev/null +++ b/apps/frontend/src/components/room/widgets/furniture/context-menu/MonsterPlantSeedConfirmView.tsx @@ -0,0 +1,85 @@ +import { IFurnitureData, RoomObjectCategory } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useState } from 'react'; +import { FurniCategory, GetFurnitureDataForRoomObject, LocalizeText } from '../../../../../api'; +import { Base, Button, Column, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../../common'; +import { useRoom } from '../../../../../hooks'; + +interface MonsterPlantSeedConfirmViewProps +{ + objectId: number; + onClose: () => void; +} + +const MODE_DEFAULT: number = -1; +const MODE_MONSTERPLANT_SEED: number = 0; + +export const MonsterPlantSeedConfirmView: FC = props => +{ + const { objectId = -1, onClose = null } = props; + const [ furniData, setFurniData ] = useState(null); + const [ mode, setMode ] = useState(MODE_DEFAULT); + const { roomSession = null } = useRoom(); + + const useProduct = () => + { + roomSession.useMultistateItem(objectId); + + onClose(); + } + + useEffect(() => + { + if(!roomSession || (objectId === -1)) return; + + const furniData = GetFurnitureDataForRoomObject(roomSession.roomId, objectId, RoomObjectCategory.FLOOR); + + if(!furniData) return; + + setFurniData(furniData); + + let mode = MODE_DEFAULT; + + switch(furniData.specialType) + { + case FurniCategory.MONSTERPLANT_SEED: + mode = MODE_MONSTERPLANT_SEED; + break; + } + + if(mode === MODE_DEFAULT) + { + onClose(); + + return; + } + + setMode(mode); + }, [ roomSession, objectId, onClose ]); + + if(mode === MODE_DEFAULT) return null; + + return ( + + + + + + + + + + + + { LocalizeText('useproduct.widget.text.plant_seed', [ 'productName' ], [ furniData.name ] ) } + { LocalizeText('useproduct.widget.info.plant_seed') } + + + + + + + + + + ); +} diff --git a/apps/frontend/src/components/room/widgets/furniture/context-menu/PurchasableClothingConfirmView.tsx b/apps/frontend/src/components/room/widgets/furniture/context-menu/PurchasableClothingConfirmView.tsx new file mode 100644 index 0000000..d07d5e8 --- /dev/null +++ b/apps/frontend/src/components/room/widgets/furniture/context-menu/PurchasableClothingConfirmView.tsx @@ -0,0 +1,104 @@ +import { RedeemItemClothingComposer, RoomObjectCategory, UserFigureComposer } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useState } from 'react'; +import { FigureData, FurniCategory, GetAvatarRenderManager, GetConnection, GetFurnitureDataForRoomObject, GetSessionDataManager, LocalizeText } from '../../../../../api'; +import { Base, Button, Column, Flex, LayoutAvatarImageView, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../../common'; +import { useRoom } from '../../../../../hooks'; + +interface PurchasableClothingConfirmViewProps +{ + objectId: number; + onClose: () => void; +} + +const MODE_DEFAULT: number = -1; +const MODE_PURCHASABLE_CLOTHING: number = 0; + +export const PurchasableClothingConfirmView: FC = props => +{ + const { objectId = -1, onClose = null } = props; + const [ mode, setMode ] = useState(MODE_DEFAULT); + const [ gender, setGender ] = useState(FigureData.MALE); + const [ newFigure, setNewFigure ] = useState(null); + const { roomSession = null } = useRoom(); + + const useProduct = () => + { + GetConnection().send(new RedeemItemClothingComposer(objectId)); + GetConnection().send(new UserFigureComposer(gender, newFigure)); + + onClose(); + } + + useEffect(() => + { + let mode = MODE_DEFAULT; + + const figure = GetSessionDataManager().figure; + const gender = GetSessionDataManager().gender; + const validSets: number[] = []; + + if(roomSession && (objectId >= 0)) + { + const furniData = GetFurnitureDataForRoomObject(roomSession.roomId, objectId, RoomObjectCategory.FLOOR); + + if(furniData) + { + switch(furniData.specialType) + { + case FurniCategory.FIGURE_PURCHASABLE_SET: + mode = MODE_PURCHASABLE_CLOTHING; + + const setIds = furniData.customParams.split(',').map(part => parseInt(part)); + + for(const setId of setIds) + { + if(GetAvatarRenderManager().isValidFigureSetForGender(setId, gender)) validSets.push(setId); + } + + break; + } + } + } + + if(mode === MODE_DEFAULT) + { + onClose(); + + return; + } + + setGender(gender); + setNewFigure(GetAvatarRenderManager().getFigureStringWithFigureIds(figure, gender, validSets)); + + // if owns clothing, change to it + + setMode(mode); + }, [ roomSession, objectId, onClose ]); + + if(mode === MODE_DEFAULT) return null; + + return ( + + + + + + + + + + + + { LocalizeText('useproduct.widget.text.bind_clothing') } + { LocalizeText('useproduct.widget.info.bind_clothing') } + + + + + + + + + + ); +} diff --git a/apps/frontend/src/components/room/widgets/furniture/playlist-editor/DiskInventoryView.tsx b/apps/frontend/src/components/room/widgets/furniture/playlist-editor/DiskInventoryView.tsx new file mode 100644 index 0000000..11bb6da --- /dev/null +++ b/apps/frontend/src/components/room/widgets/furniture/playlist-editor/DiskInventoryView.tsx @@ -0,0 +1,94 @@ +import { IAdvancedMap, MusicPriorities } from '@nitrots/nitro-renderer'; +import { FC, MouseEvent, useCallback, useEffect, useState } from 'react'; +import { CatalogPageName, CreateLinkEvent, GetConfiguration, GetDiskColor, GetNitroInstance, LocalizeText } from '../../../../../api'; +import { AutoGrid, Base, Button, Flex, LayoutGridItem, Text } from '../../../../../common'; + +export interface DiskInventoryViewProps +{ + diskInventory: IAdvancedMap; + addToPlaylist: (diskId: number, slotNumber: number) => void; +} + +export const DiskInventoryView: FC = props => +{ + const { diskInventory = null, addToPlaylist = null } = props; + const [ selectedItem, setSelectedItem ] = useState(-1); + const [ previewSongId, setPreviewSongId ] = useState(-1); + + const previewSong = useCallback((event: MouseEvent, songId: number) => + { + event.stopPropagation(); + + setPreviewSongId(prevValue => (prevValue === songId) ? -1 : songId); + }, []); + + const addSong = useCallback((event: MouseEvent, diskId: number) => + { + event.stopPropagation(); + + addToPlaylist(diskId, GetNitroInstance().soundManager.musicController?.getRoomItemPlaylist()?.length) + }, [ addToPlaylist ]); + + const openCatalogPage = () => + { + CreateLinkEvent('catalog/open/' + CatalogPageName.TRAX_SONGS); + } + + useEffect(() => + { + if(previewSongId === -1) return; + + GetNitroInstance().soundManager.musicController?.playSong(previewSongId, MusicPriorities.PRIORITY_SONG_PLAY, 0, 0, 0, 0); + + return () => + { + GetNitroInstance().soundManager.musicController?.stop(MusicPriorities.PRIORITY_SONG_PLAY); + } + }, [ previewSongId ]); + + useEffect(() => + { + return () => setPreviewSongId(-1); + }, []); + + return (<> +
+ +

{ LocalizeText('playlist.editor.my.music') }

+
+
+ + { diskInventory && diskInventory.getKeys().map( (key, index) => + { + const diskId = diskInventory.getKey(index); + const songId = diskInventory.getWithIndex(index); + const songInfo = GetNitroInstance().soundManager.musicController?.getSongInfo(songId); + + return ( + setSelectedItem(prev => prev === index ? -1 : index) } classNames={ [ 'text-black' ] }> +
+
+ { songInfo?.name } + { (selectedItem === index) && + + + + + } +
) + }) } +
+
+
+
{ LocalizeText('playlist.editor.text.get.more.music') }
+
{ LocalizeText('playlist.editor.text.you.have.no.songdisks.available') }
+
{ LocalizeText('playlist.editor.text.you.can.buy.some.from.the.catalogue') }
+ +
+ + ); +} diff --git a/apps/frontend/src/components/room/widgets/furniture/playlist-editor/FurniturePlaylistEditorWidgetView.tsx b/apps/frontend/src/components/room/widgets/furniture/playlist-editor/FurniturePlaylistEditorWidgetView.tsx new file mode 100644 index 0000000..ceb0db9 --- /dev/null +++ b/apps/frontend/src/components/room/widgets/furniture/playlist-editor/FurniturePlaylistEditorWidgetView.tsx @@ -0,0 +1,29 @@ +import { FC } from 'react'; +import { LocalizeText } from '../../../../../api'; +import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../../common'; +import { useFurniturePlaylistEditorWidget } from '../../../../../hooks'; +import { DiskInventoryView } from './DiskInventoryView'; +import { SongPlaylistView } from './SongPlaylistView'; + +export const FurniturePlaylistEditorWidgetView: FC<{}> = props => +{ + const { objectId = -1, currentPlayingIndex = -1, playlist = null, diskInventory = null, onClose = null, togglePlayPause = null, removeFromPlaylist = null, addToPlaylist = null } = useFurniturePlaylistEditorWidget(); + + if(objectId === -1) return null; + + return ( + + + +
+
+ +
+
+ +
+
+
+
+ ); +} diff --git a/apps/frontend/src/components/room/widgets/furniture/playlist-editor/SongPlaylistView.tsx b/apps/frontend/src/components/room/widgets/furniture/playlist-editor/SongPlaylistView.tsx new file mode 100644 index 0000000..241bdc5 --- /dev/null +++ b/apps/frontend/src/components/room/widgets/furniture/playlist-editor/SongPlaylistView.tsx @@ -0,0 +1,79 @@ +import { ISongInfo } from '@nitrots/nitro-renderer'; +import { FC, useState } from 'react'; +import { GetConfiguration, GetDiskColor, LocalizeText } from '../../../../../api'; +import { Base, Button, Flex, Text } from '../../../../../common'; + +export interface SongPlaylistViewProps +{ + furniId: number; + playlist: ISongInfo[]; + currentPlayingIndex: number; + removeFromPlaylist(slotNumber: number): void; + togglePlayPause(furniId: number, position: number): void; +} + +export const SongPlaylistView: FC = props => +{ + const { furniId = -1, playlist = null, currentPlayingIndex = -1, removeFromPlaylist = null, togglePlayPause = null } = props; + const [ selectedItem, setSelectedItem ] = useState(-1); + + const action = (index: number) => + { + if(selectedItem === index) removeFromPlaylist(index); + } + + const playPause = (furniId: number, selectedItem: number) => + { + togglePlayPause(furniId, selectedItem !== -1 ? selectedItem : 0 ) + } + + return (<> +
+ +

{ LocalizeText('playlist.editor.playlist') }

+
+
+ + { playlist && playlist.map( (songInfo, index) => + { + return setSelectedItem(prev => prev === index ? -1 : index) }> + action(index) } className={ 'disk-2 ' + (selectedItem === index ? 'selected-song' : '') } style={ { backgroundColor: (selectedItem === index ? '' : GetDiskColor(songInfo.songData)) } }/> + { songInfo.name } + + }) } + + +
+ { (!playlist || playlist.length === 0 ) && + <>
+
{ LocalizeText('playlist.editor.add.songs.to.your.playlist') }
+
{ LocalizeText('playlist.editor.text.click.song.to.choose.click.again.to.move') }
+
+ + } + { (playlist && playlist.length > 0) && + <> + { (currentPlayingIndex === -1) && + + } + { (currentPlayingIndex !== -1) && + + + + { LocalizeText('playlist.editor.text.now.playing.in.your.room') } + + { playlist[currentPlayingIndex]?.name + ' - ' + playlist[currentPlayingIndex]?.creator } + + + + + } + + } + + ); +} diff --git a/apps/frontend/src/components/room/widgets/mysterybox/MysteryBoxExtensionView.scss b/apps/frontend/src/components/room/widgets/mysterybox/MysteryBoxExtensionView.scss new file mode 100644 index 0000000..254821c --- /dev/null +++ b/apps/frontend/src/components/room/widgets/mysterybox/MysteryBoxExtensionView.scss @@ -0,0 +1,52 @@ +.mysterybox-extension { + + .mysterybox-container { + max-width: 50px; + max-height: 50px; + width: 50px; + height: 50px; + + background-color: rgba(28, 28, 32, 0.95); + box-shadow: inset 0px 5px rgb(34 34 39 / 60%), inset 0 -4px rgb(18 18 21 / 60%); + border-color: #5b5a57; + } + + .box-image { + width: 31px; + height: 36px; + position: relative; + background-image: url('@/assets/images/mysterybox/mystery_box.png'); + -webkit-mask-image: url('@/assets/images/mysterybox/mystery_box.png'); + mask-image: url('@/assets/images/mysterybox/mystery_box.png'); + + .chain-overlay-image { + width: 31px; + height: 36px; + position: absolute; + background-image: url('@/assets/images/mysterybox/chain_mysterybox_box_overlay.png'); + } + } + + .key-image { + width: 39px; + height: 39px; + position: relative; + background-image: url('@/assets/images/mysterybox/mystery_box_key.png'); + -webkit-mask-image: url('@/assets/images/mysterybox/mystery_box_key.png'); + mask-image: url('@/assets/images/mysterybox/mystery_box_key.png'); + + .key-overlay-image { + width: 39px; + height: 39px; + position: absolute; + background-image: url('@/assets/images/mysterybox/key_overlay.png'); + } + } + + .box-image, + .key-image { + background-blend-mode: multiply; + background-position: center; + background-repeat: no-repeat; + } +} diff --git a/apps/frontend/src/components/room/widgets/mysterybox/MysteryBoxExtensionView.tsx b/apps/frontend/src/components/room/widgets/mysterybox/MysteryBoxExtensionView.tsx new file mode 100644 index 0000000..83ae06c --- /dev/null +++ b/apps/frontend/src/components/room/widgets/mysterybox/MysteryBoxExtensionView.tsx @@ -0,0 +1,67 @@ +import { MysteryBoxKeysUpdateEvent } from '@nitrots/nitro-renderer'; +import { FC, useState } from 'react'; +import { FaChevronDown, FaChevronUp } from 'react-icons/fa'; +import { ColorUtils, LocalizeText } from '../../../../api'; +import { Base, Column, Flex, LayoutGridItem, Text } from '../../../../common'; +import { useSessionDataManagerEvent } from '../../../../hooks'; + +const colorMap = { + 'purple': 9452386, + 'blue': 3891856, + 'green': 6459451, + 'yellow': 10658089, + 'lilac': 6897548, + 'orange': 10841125, + 'turquoise': 2661026, + 'red': 10104881 +} + +export const MysteryBoxExtensionView: FC<{}> = props => +{ + const [ isOpen, setIsOpen ] = useState(true); + const [ keyColor, setKeyColor ] = useState(''); + const [ boxColor, setBoxColor ] = useState(''); + + useSessionDataManagerEvent(MysteryBoxKeysUpdateEvent.MYSTERY_BOX_KEYS_UPDATE, event => + { + setKeyColor(event.keyColor); + setBoxColor(event.boxColor); + }); + + const getRgbColor = (color: string) => + { + const colorInt = colorMap[color]; + + return ColorUtils.int2rgb(colorInt); + } + + if(keyColor === '' && boxColor === '') return null; + + return ( + + + setIsOpen(value => !value) }> + { LocalizeText('mysterybox.tracker.title') } + { isOpen && } + { !isOpen && } + + { isOpen && + <> + { LocalizeText('mysterybox.tracker.description') } + + +
+
+
+ + +
+
+
+ + + } + + + ); +} diff --git a/apps/frontend/src/components/room/widgets/object-location/ObjectLocationView.tsx b/apps/frontend/src/components/room/widgets/object-location/ObjectLocationView.tsx new file mode 100644 index 0000000..237090b --- /dev/null +++ b/apps/frontend/src/components/room/widgets/object-location/ObjectLocationView.tsx @@ -0,0 +1,61 @@ +import { GetTicker } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useRef, useState } from 'react'; +import { GetRoomObjectBounds, GetRoomSession } from '../../../../api'; +import { Base, BaseProps } from '../../../../common'; + +interface ObjectLocationViewProps extends BaseProps +{ + objectId: number; + category: number; + noFollow?: boolean; +} + +export const ObjectLocationView: FC = props => +{ + const { objectId = -1, category = -1, noFollow = false, position = 'absolute', ...rest } = props; + const [ pos, setPos ] = useState<{ x: number, y: number }>({ x: -1, y: -1 }); + const elementRef = useRef(); + + useEffect(() => + { + let remove = false; + + const getObjectLocation = () => + { + const roomSession = GetRoomSession(); + const objectBounds = GetRoomObjectBounds(roomSession.roomId, objectId, category, 1); + + return objectBounds; + } + + const updatePosition = () => + { + const bounds = getObjectLocation(); + + if(!bounds || !elementRef.current) return; + + setPos({ + x: Math.round(((bounds.left + (bounds.width / 2)) - (elementRef.current.offsetWidth / 2))), + y: Math.round((bounds.top - elementRef.current.offsetHeight) + 10) + }); + } + + if(noFollow) + { + updatePosition(); + } + else + { + remove = true; + + GetTicker().add(updatePosition); + } + + return () => + { + if(remove) GetTicker().remove(updatePosition); + } + }, [ objectId, category, noFollow ]); + + return -1 } className="object-location" style={ { left: pos.x, top: pos.y } } { ...rest } />; +} diff --git a/apps/frontend/src/components/room/widgets/pet-package/PetPackageWidgetView.scss b/apps/frontend/src/components/room/widgets/pet-package/PetPackageWidgetView.scss new file mode 100644 index 0000000..97a6d2e --- /dev/null +++ b/apps/frontend/src/components/room/widgets/pet-package/PetPackageWidgetView.scss @@ -0,0 +1,106 @@ +.nitro-pet-package +{ + .pet-package-container-top + { + width: 400px; + height: 120px; + border-radius: 2px; + background-color: #0E3F52; + + .package-image-gnome_box + { + width: 80px; + height: 84px; + position: relative; + background-image: url('@/assets/images/pets/pet-package/gnome.png'); + background-repeat: no-repeat; + } + + .package-image-leprechaun_box + { + width: 80px; + height: 84px; + position: relative; + background-image: url('@/assets/images/pets/pet-package/leprechaun_box.png'); + background-repeat: no-repeat; + } + + .package-image-val11_present + { + width: 80px; + height: 84px; + position: relative; + background-image: url('@/assets/images/pets/pet-package/val11_present.png'); + background-repeat: no-repeat; + } + + .package-image-velociraptor_egg + { + width: 80px; + height: 84px; + position: relative; + background-image: url('@/assets/images/pets/pet-package/velociraptor_egg.png'); + background-repeat: no-repeat; + } + + .package-image-pterosaur_egg + { + width: 80px; + height: 84px; + position: relative; + background-image: url('@/assets/images/pets/pet-package/pterosaur_egg.png'); + background-repeat: no-repeat; + } + + .package-image-petbox_epic + { + width: 80px; + height: 84px; + position: relative; + background-image: url('@/assets/images/pets/pet-package/petbox_epic.png'); + background-repeat: no-repeat; + } + + .package-text-big + { + font-size: 16px; + } + } + + .pet-package-container-bottom + { + display: flex; + justify-content: center; + width: 400px; + height: 120px; + border-radius: 2px; + background-color: #E9E9E1; + + .input-pet-package-container + { + width: 380px; + border: 1px solid black; + + .input-pet-package + { + width: 350px; + border: 0; + outline: 0; + } + + .package-pencil-image + { + width: 16px; + height: 16px; + position: relative; + background-image: url('@/assets/images/infostand/pencil-icon.png'); + background-repeat: no-repeat; + } + } + + .text-decoration + { + text-decoration: underline; + } + } +} diff --git a/apps/frontend/src/components/room/widgets/pet-package/PetPackageWidgetView.tsx b/apps/frontend/src/components/room/widgets/pet-package/PetPackageWidgetView.tsx new file mode 100644 index 0000000..048c9aa --- /dev/null +++ b/apps/frontend/src/components/room/widgets/pet-package/PetPackageWidgetView.tsx @@ -0,0 +1,42 @@ +import { FC } from 'react'; +import { Button } from 'react-bootstrap'; +import { GetConfiguration, LocalizeText } from '../../../../api'; +import { Base, Column, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common'; +import { usePetPackageWidget } from '../../../../hooks'; + +export const PetPackageWidgetView: FC<{}> = props => +{ + const { isVisible = false, errorResult = null, petName = null, objectType = null, onChangePetName = null, onConfirm = null, onClose = null } = usePetPackageWidget(); + + return ( + <> + { isVisible && + + onClose() } /> + + +
+
+ { objectType === 'gnome_box' ? LocalizeText('widgets.gnomepackage.name.title') : LocalizeText('furni.petpackage') } +
+
+ + + + onChangePetName(event.target.value) } /> +
+
+ { (errorResult.length > 0) && + { errorResult } } + + onClose() }>{ LocalizeText('cancel') } + + +
+
+
+
+ } + + ); +} diff --git a/apps/frontend/src/components/room/widgets/room-filter-words/RoomFilterWordsWidgetView.tsx b/apps/frontend/src/components/room/widgets/room-filter-words/RoomFilterWordsWidgetView.tsx new file mode 100644 index 0000000..4f7825f --- /dev/null +++ b/apps/frontend/src/components/room/widgets/room-filter-words/RoomFilterWordsWidgetView.tsx @@ -0,0 +1,74 @@ +import { UpdateRoomFilterMessageComposer } from '@nitrots/nitro-renderer'; +import { FC, useState } from 'react'; +import { LocalizeText, SendMessageComposer } from '../../../../api'; +import { Button, classNames, Column, Flex, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common'; +import { useFilterWordsWidget, useNavigator } from '../../../../hooks'; + +export const RoomFilterWordsWidgetView: FC<{}> = props => +{ + const [ word, setWord ] = useState('bobba'); + const [ selectedWord, setSelectedWord ] = useState(''); + const [ isSelectingWord, setIsSelectingWord ] = useState(false); + const { wordsFilter = [], isVisible = null, setWordsFilter, onClose = null } = useFilterWordsWidget(); + const { navigatorData = null } = useNavigator(); + + const processAction = (isAddingWord: boolean) => + { + if ((isSelectingWord) ? (!selectedWord) : (!word)) return; + + SendMessageComposer(new UpdateRoomFilterMessageComposer(navigatorData.enteredGuestRoom.roomId, isAddingWord, (isSelectingWord ? selectedWord : word))); + setSelectedWord(''); + setWord('bobba'); + setIsSelectingWord(false); + + if (isAddingWord && wordsFilter.includes((isSelectingWord ? selectedWord : word))) return; + + setWordsFilter(prevValue => + { + const newWords = [ ...prevValue ]; + + isAddingWord ? newWords.push((isSelectingWord ? selectedWord : word)) : newWords.splice(newWords.indexOf((isSelectingWord ? selectedWord : word)), 1); + + return newWords; + }); + } + + const onTyping = (word: string) => + { + setWord(word); + setIsSelectingWord(false); + } + + const onSelectedWord = (word: string) => + { + setSelectedWord(word); + setIsSelectingWord(true); + } + + if (!isVisible) return null; + + return ( + + onClose() } /> + + + onTyping(event.target.value) } /> + + + + { wordsFilter && (wordsFilter.length > 0) && wordsFilter.map((word, index) => + { + return ( + onSelectedWord(word) }> + { word } + + ) + }) } + + + + + + + ); +}; diff --git a/apps/frontend/src/components/room/widgets/room-promotes/RoomPromotesWidgetView.tsx b/apps/frontend/src/components/room/widgets/room-promotes/RoomPromotesWidgetView.tsx new file mode 100644 index 0000000..bfd66fa --- /dev/null +++ b/apps/frontend/src/components/room/widgets/room-promotes/RoomPromotesWidgetView.tsx @@ -0,0 +1,56 @@ +import { DesktopViewEvent } from '@nitrots/nitro-renderer'; +import { FC, useState } from 'react'; +import { FaChevronDown, FaChevronUp } from 'react-icons/fa'; +import { GetSessionDataManager } from '../../../../api'; +import { Base, Column, Flex, Text } from '../../../../common'; +import { useMessageEvent, useRoomPromote } from '../../../../hooks'; +import { RoomPromoteEditWidgetView, RoomPromoteMyOwnEventWidgetView, RoomPromoteOtherEventWidgetView } from './views'; + +export const RoomPromotesWidgetView: FC<{}> = props => +{ + const [ isEditingPromote, setIsEditingPromote ] = useState(false); + const [ isOpen, setIsOpen ] = useState(true); + const { promoteInformation, setPromoteInformation } = useRoomPromote(); + + useMessageEvent(DesktopViewEvent, event => + { + setPromoteInformation(null); + }); + + if(!promoteInformation) return null; + + return ( + <> + { promoteInformation.data.adId !== -1 && + + + setIsOpen(value => !value) }> + { promoteInformation.data.eventName } + { isOpen && } + { !isOpen && } + + { (isOpen && GetSessionDataManager().userId !== promoteInformation.data.ownerAvatarId) && + + } + { (isOpen && GetSessionDataManager().userId === promoteInformation.data.ownerAvatarId) && + setIsEditingPromote(true) } + /> + } + { isEditingPromote && + setIsEditingPromote(false) } + /> + } + + + } + + ); +}; diff --git a/apps/frontend/src/components/room/widgets/room-promotes/views/RoomPromoteEditWidgetView.tsx b/apps/frontend/src/components/room/widgets/room-promotes/views/RoomPromoteEditWidgetView.tsx new file mode 100644 index 0000000..f9b8462 --- /dev/null +++ b/apps/frontend/src/components/room/widgets/room-promotes/views/RoomPromoteEditWidgetView.tsx @@ -0,0 +1,44 @@ +import { EditEventMessageComposer } from '@nitrots/nitro-renderer'; +import { FC, useState } from 'react'; +import { LocalizeText, SendMessageComposer } from '../../../../../api'; +import { Button, Column, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../../common'; + +interface RoomPromoteEditWidgetViewProps +{ + eventId: number; + eventName: string; + eventDescription: string; + setIsEditingPromote: (value: boolean) => void; +} + +export const RoomPromoteEditWidgetView: FC = props => +{ + const { eventId = -1, eventName = '', eventDescription = '', setIsEditingPromote = null } = props; + const [ newEventName, setNewEventName ] = useState(eventName); + const [ newEventDescription, setNewEventDescription ] = useState(eventDescription); + + const updatePromote = () => + { + SendMessageComposer(new EditEventMessageComposer(eventId, newEventName, newEventDescription)); + setIsEditingPromote(false); + } + + return ( + + setIsEditingPromote(false) } /> + + + { LocalizeText('navigator.eventsettings.name') } + setNewEventName(event.target.value) } /> + + + { LocalizeText('navigator.eventsettings.desc') } + + + + + + + + ); +}; diff --git a/apps/frontend/src/components/room/widgets/room-promotes/views/RoomPromoteMyOwnEventWidgetView.tsx b/apps/frontend/src/components/room/widgets/room-promotes/views/RoomPromoteMyOwnEventWidgetView.tsx new file mode 100644 index 0000000..169cc9b --- /dev/null +++ b/apps/frontend/src/components/room/widgets/room-promotes/views/RoomPromoteMyOwnEventWidgetView.tsx @@ -0,0 +1,35 @@ +import { FC } from 'react'; +import { CreateLinkEvent, LocalizeText } from '../../../../../api'; +import { Button, Flex, Grid, Text } from '../../../../../common'; +import { useRoomPromote } from '../../../../../hooks'; + +interface RoomPromoteMyOwnEventWidgetViewProps +{ + eventDescription: string; + setIsEditingPromote: (value: boolean) => void; +} + +export const RoomPromoteMyOwnEventWidgetView: FC = props => +{ + const { eventDescription = '', setIsEditingPromote = null } = props; + const { setIsExtended } = useRoomPromote(); + + const extendPromote = () => + { + setIsExtended(true); + CreateLinkEvent('catalog/open/room_event'); + } + + return ( + <> + + { eventDescription } + +

+ + + + + + ); +}; diff --git a/apps/frontend/src/components/room/widgets/room-promotes/views/RoomPromoteOtherEventWidgetView.tsx b/apps/frontend/src/components/room/widgets/room-promotes/views/RoomPromoteOtherEventWidgetView.tsx new file mode 100644 index 0000000..5adb517 --- /dev/null +++ b/apps/frontend/src/components/room/widgets/room-promotes/views/RoomPromoteOtherEventWidgetView.tsx @@ -0,0 +1,30 @@ +import { FC } from 'react'; +import { LocalizeText } from '../../../../../api'; +import { Base, Column, Flex, Text } from '../../../../../common'; + +interface RoomPromoteOtherEventWidgetViewProps +{ + eventDescription: string; +} + +export const RoomPromoteOtherEventWidgetView: FC = props => +{ + const { eventDescription = '' } = props; + + return ( + <> + + { eventDescription } + +

+ + + + { LocalizeText('navigator.eventinprogress') } + +   + + + + ); +}; diff --git a/apps/frontend/src/components/room/widgets/room-promotes/views/index.ts b/apps/frontend/src/components/room/widgets/room-promotes/views/index.ts new file mode 100644 index 0000000..da74691 --- /dev/null +++ b/apps/frontend/src/components/room/widgets/room-promotes/views/index.ts @@ -0,0 +1,3 @@ +export * from './RoomPromoteEditWidgetView'; +export * from './RoomPromoteMyOwnEventWidgetView'; +export * from './RoomPromoteOtherEventWidgetView'; diff --git a/apps/frontend/src/components/room/widgets/room-thumbnail/RoomThumbnailWidgetView.tsx b/apps/frontend/src/components/room/widgets/room-thumbnail/RoomThumbnailWidgetView.tsx new file mode 100644 index 0000000..055e734 --- /dev/null +++ b/apps/frontend/src/components/room/widgets/room-thumbnail/RoomThumbnailWidgetView.tsx @@ -0,0 +1,42 @@ +import { NitroRenderTexture } from '@nitrots/nitro-renderer'; +import { FC, useState } from 'react'; +import { GetRoomEngine } from '../../../../api'; +import { LayoutMiniCameraView } from '../../../../common'; +import { RoomWidgetThumbnailEvent } from '../../../../events'; +import { useRoom, useUiEvent } from '../../../../hooks'; + +export const RoomThumbnailWidgetView: FC<{}> = props => +{ + const [ isVisible, setIsVisible ] = useState(false); + const { roomSession = null } = useRoom(); + + useUiEvent([ + RoomWidgetThumbnailEvent.SHOW_THUMBNAIL, + RoomWidgetThumbnailEvent.HIDE_THUMBNAIL, + RoomWidgetThumbnailEvent.TOGGLE_THUMBNAIL ], event => + { + switch(event.type) + { + case RoomWidgetThumbnailEvent.SHOW_THUMBNAIL: + setIsVisible(true); + return; + case RoomWidgetThumbnailEvent.HIDE_THUMBNAIL: + setIsVisible(false); + return; + case RoomWidgetThumbnailEvent.TOGGLE_THUMBNAIL: + setIsVisible(value => !value); + return; + } + }); + + const receiveTexture = (texture: NitroRenderTexture) => + { + GetRoomEngine().saveTextureAsScreenshot(texture, true); + + setIsVisible(false); + } + + if(!isVisible) return null; + + return setIsVisible(false) } /> +}; diff --git a/apps/frontend/src/components/room/widgets/room-tools/RoomToolsWidgetView.tsx b/apps/frontend/src/components/room/widgets/room-tools/RoomToolsWidgetView.tsx new file mode 100644 index 0000000..0bd2656 --- /dev/null +++ b/apps/frontend/src/components/room/widgets/room-tools/RoomToolsWidgetView.tsx @@ -0,0 +1,100 @@ +import { GetGuestRoomResultEvent, NavigatorSearchComposer, RateFlatMessageComposer } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useState } from 'react'; +import { CreateLinkEvent, GetRoomEngine, LocalizeText, SendMessageComposer } from '../../../../api'; +import { Base, classNames, Column, Flex, Text, TransitionAnimation, TransitionAnimationTypes } from '../../../../common'; +import { useMessageEvent, useNavigator, useRoom } from '../../../../hooks'; + +export const RoomToolsWidgetView: FC<{}> = props => +{ + const [ isZoomedIn, setIsZoomedIn ] = useState(false); + const [ roomName, setRoomName ] = useState(null); + const [ roomOwner, setRoomOwner ] = useState(null); + const [ roomTags, setRoomTags ] = useState(null); + const [ isOpen, setIsOpen ] = useState(false); + const { navigatorData = null } = useNavigator(); + const { roomSession = null } = useRoom(); + + const handleToolClick = (action: string, value?: string) => + { + switch(action) + { + case 'settings': + CreateLinkEvent('navigator/toggle-room-info'); + return; + case 'zoom': + setIsZoomedIn(prevValue => + { + let scale = GetRoomEngine().getRoomInstanceRenderingCanvasScale(roomSession.roomId, 1); + + if(!prevValue) scale /= 2; + else scale *= 2; + + GetRoomEngine().setRoomInstanceRenderingCanvasScale(roomSession.roomId, 1, scale); + + return !prevValue; + }); + return; + case 'chat_history': + CreateLinkEvent('chat-history/toggle'); + return; + case 'like_room': + SendMessageComposer(new RateFlatMessageComposer(1)); + return; + case 'toggle_room_link': + CreateLinkEvent('navigator/toggle-room-link'); + return; + case 'navigator_search_tag': + CreateLinkEvent(`navigator/search/${ value }`); + SendMessageComposer(new NavigatorSearchComposer('hotel_view', `tag:${ value }`)); + return; + } + } + + useMessageEvent(GetGuestRoomResultEvent, event => + { + const parser = event.getParser(); + + if(!parser.roomEnter || (parser.data.roomId !== roomSession.roomId)) return; + + if(roomName !== parser.data.roomName) setRoomName(parser.data.roomName); + if(roomOwner !== parser.data.ownerName) setRoomOwner(parser.data.ownerName); + if(roomTags !== parser.data.tags) setRoomTags(parser.data.tags); + }); + + useEffect(() => + { + setIsOpen(true); + + const timeout = setTimeout(() => setIsOpen(false), 5000); + + return () => clearTimeout(timeout); + }, [ roomName, roomOwner, roomTags ]); + + return ( + + + handleToolClick('settings') } /> + handleToolClick('zoom') } className={ classNames('icon', (!isZoomedIn && 'icon-zoom-less'), (isZoomedIn && 'icon-zoom-more')) } /> + handleToolClick('chat_history') } className="icon icon-chat-history" /> + { navigatorData.canRate && + handleToolClick('like_room') } className="icon icon-like-room" /> } + + + + + + + { roomName } + { roomOwner } + + { roomTags && roomTags.length > 0 && + + { roomTags.map((tag, index) => handleToolClick('navigator_search_tag', tag) }>#{ tag }) } + } + + + + + + ); +} diff --git a/apps/frontend/src/components/room/widgets/user-location/UserLocationView.tsx b/apps/frontend/src/components/room/widgets/user-location/UserLocationView.tsx new file mode 100644 index 0000000..ca281ec --- /dev/null +++ b/apps/frontend/src/components/room/widgets/user-location/UserLocationView.tsx @@ -0,0 +1,24 @@ +import { RoomObjectCategory } from '@nitrots/nitro-renderer'; +import { FC } from 'react'; +import { BaseProps } from '../../../../common'; +import { useRoom } from '../../../../hooks'; +import { ObjectLocationView } from '../object-location/ObjectLocationView'; + +interface UserLocationViewProps extends BaseProps +{ + userId: number; +} + +export const UserLocationView: FC = props => +{ + const { userId = -1, ...rest } = props; + const { roomSession = null } = useRoom(); + + if((userId === -1) || !roomSession) return null; + + const userData = roomSession.userDataManager.getUserData(userId); + + if(!userData) return null; + + return ; +} diff --git a/apps/frontend/src/components/room/widgets/word-quiz/WordQuizQuestionView.tsx b/apps/frontend/src/components/room/widgets/word-quiz/WordQuizQuestionView.tsx new file mode 100644 index 0000000..3b4d051 --- /dev/null +++ b/apps/frontend/src/components/room/widgets/word-quiz/WordQuizQuestionView.tsx @@ -0,0 +1,44 @@ +import { FC } from 'react'; +import { VALUE_KEY_DISLIKE, VALUE_KEY_LIKE } from '../../../../api'; +import { Base, Column, Flex, Text } from '../../../../common'; + +interface WordQuizQuestionViewProps +{ + question: string; + canVote: boolean; + vote(value: string): void; + noVotes: number; + yesVotes: number; +} + +export const WordQuizQuestionView: FC = props => +{ + const { question = null, canVote = null, vote = null, noVotes = null, yesVotes = null } = props; + + return ( + + { !canVote && + + + { noVotes } + + { question } + + { yesVotes } + + } + { canVote && + + { question } + + vote(VALUE_KEY_DISLIKE) }> + + + vote(VALUE_KEY_LIKE) }> + + + + } + + ); +} diff --git a/apps/frontend/src/components/room/widgets/word-quiz/WordQuizVoteView.tsx b/apps/frontend/src/components/room/widgets/word-quiz/WordQuizVoteView.tsx new file mode 100644 index 0000000..8437d76 --- /dev/null +++ b/apps/frontend/src/components/room/widgets/word-quiz/WordQuizVoteView.tsx @@ -0,0 +1,24 @@ +import { RoomObjectCategory } from '@nitrots/nitro-renderer'; +import { FC } from 'react'; +import { VALUE_KEY_DISLIKE } from '../../../../api'; +import { Base, BaseProps, Flex } from '../../../../common'; +import { ObjectLocationView } from '../object-location/ObjectLocationView'; + +interface WordQuizVoteViewProps extends BaseProps +{ + userIndex: number; + vote: string; +} + +export const WordQuizVoteView: FC = props => +{ + const { userIndex = null, vote = null, ...rest } = props; + + return ( + + + + + + ); +} diff --git a/apps/frontend/src/components/room/widgets/word-quiz/WordQuizWidgetView.tsx b/apps/frontend/src/components/room/widgets/word-quiz/WordQuizWidgetView.tsx new file mode 100644 index 0000000..a04cfd8 --- /dev/null +++ b/apps/frontend/src/components/room/widgets/word-quiz/WordQuizWidgetView.tsx @@ -0,0 +1,19 @@ +import { FC } from 'react'; +import { VALUE_KEY_DISLIKE, VALUE_KEY_LIKE } from '../../../../api'; +import { useWordQuizWidget } from '../../../../hooks'; +import { WordQuizQuestionView } from './WordQuizQuestionView'; +import { WordQuizVoteView } from './WordQuizVoteView'; + +export const WordQuizWidgetView: FC<{}> = props => +{ + const { question = null, answerSent = false, answerCounts = null, userAnswers = null, vote = null } = useWordQuizWidget(); + + return ( + <> + { question && + } + { userAnswers && + Array.from(userAnswers.entries()).map(([ key, value ], index) => ) } + + ); +} diff --git a/apps/frontend/src/components/toolbar/ToolbarMeView.tsx b/apps/frontend/src/components/toolbar/ToolbarMeView.tsx new file mode 100644 index 0000000..8a66a91 --- /dev/null +++ b/apps/frontend/src/components/toolbar/ToolbarMeView.tsx @@ -0,0 +1,52 @@ +import { MouseEventType, RoomObjectCategory } from '@nitrots/nitro-renderer'; +import { Dispatch, FC, PropsWithChildren, SetStateAction, useEffect, useRef } from 'react'; +import { CreateLinkEvent, DispatchUiEvent, GetConfiguration, GetRoomEngine, GetRoomSession, GetSessionDataManager, GetUserProfile } from '../../api'; +import { Base, Flex, LayoutItemCountView } from '../../common'; +import { GuideToolEvent } from '../../events'; + +interface ToolbarMeViewProps +{ + useGuideTool: boolean; + unseenAchievementCount: number; + setMeExpanded: Dispatch>; +} + +export const ToolbarMeView: FC> = props => +{ + const { useGuideTool = false, unseenAchievementCount = 0, setMeExpanded = null, children = null, ...rest } = props; + const elementRef = useRef(); + + useEffect(() => + { + const roomSession = GetRoomSession(); + + if(!roomSession) return; + + GetRoomEngine().selectRoomObject(roomSession.roomId, roomSession.ownRoomIndex, RoomObjectCategory.UNIT); + }, []); + + useEffect(() => + { + const onClick = (event: MouseEvent) => setMeExpanded(false); + + document.addEventListener('click', onClick); + + return () => document.removeEventListener(MouseEventType.MOUSE_CLICK, onClick); + }, [ setMeExpanded ]); + + return ( + + { (GetConfiguration('guides.enabled') && useGuideTool) && + DispatchUiEvent(new GuideToolEvent(GuideToolEvent.TOGGLE_GUIDE_TOOL)) } /> } + CreateLinkEvent('achievements/toggle') }> + { (unseenAchievementCount > 0) && + } + + GetUserProfile(GetSessionDataManager().userId) } /> + CreateLinkEvent('navigator/search/myworld_view') } /> + CreateLinkEvent('avatar-editor/toggle') } /> + CreateLinkEvent('user-settings/toggle') } /> + { children } + + ); +} diff --git a/apps/frontend/src/components/toolbar/ToolbarView.scss b/apps/frontend/src/components/toolbar/ToolbarView.scss new file mode 100644 index 0000000..6e7f651 --- /dev/null +++ b/apps/frontend/src/components/toolbar/ToolbarView.scss @@ -0,0 +1,81 @@ +.nitro-toolbar { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: $toolbar-height; + z-index: $toolbar-zindex; + pointer-events: all; + background: rgba($dark, 0.95); + box-shadow: inset 0px 5px lighten(rgba($dark, 0.6), 2.5), + inset 0 -4px darken(rgba($dark, 0.6), 4); + + .navigation-item { + position: relative; + + &.item-avatar { + width: 50px; + height: 45px; + overflow: hidden; + + .avatar-image { + margin-left: -5px; + margin-top: 25px; + } + } + + &:hover { + -webkit-transform: translate(-1px, -1px); + transform: translate(-1px, -1px); + filter: drop-shadow(2px 2px 0 rgba($black, 0.8)); + } + + &.active, + &:active { + -webkit-transform: unset; + transform: unset; + filter: none; + } + } + + #toolbar-chat-input-container { + + @include media-breakpoint-down(sm) { + width: 0px; + height: 0px; + } + } +} + +.nitro-toolbar-me { + position: absolute; + bottom: 60px; + left: 15px; + z-index: $toolbar-memenu-zindex; + background: rgba(20, 20, 20, .95); + border: 1px solid #101010; + box-shadow: inset 2px 2px rgba(255, 255, 255, .1), inset -2px -2px rgba(255, 255, 255, .1); + border-radius: $border-radius; + + .navigation-item { + transition: filter .2s ease-out; + filter: grayscale(1); + + &:hover { + filter: grayscale(0) drop-shadow(2px 2px 0 rgba($black, 0.8)); + } + } +} + +.toolbar-icon-animation { + position: absolute; + object-fit: cover; + height: auto; + width: auto; + max-width: 120px; + max-height: 150px; + z-index: 500; + filter: drop-shadow(2px 1px 0 rgba($white, 1)) + drop-shadow(-2px 1px 0 rgba($white, 1)) + drop-shadow(0 -2px 0 rgba($white, 1)); +} diff --git a/apps/frontend/src/components/toolbar/ToolbarView.tsx b/apps/frontend/src/components/toolbar/ToolbarView.tsx new file mode 100644 index 0000000..0efab76 --- /dev/null +++ b/apps/frontend/src/components/toolbar/ToolbarView.tsx @@ -0,0 +1,111 @@ +import { Dispose, DropBounce, EaseOut, JumpBy, Motions, NitroToolbarAnimateIconEvent, PerkAllowancesMessageEvent, PerkEnum, Queue, Wait } from '@nitrots/nitro-renderer'; +import { FC, useState } from 'react'; +import { CreateLinkEvent, GetConfiguration, GetSessionDataManager, MessengerIconState, OpenMessengerChat, VisitDesktop } from '../../api'; +import { Base, Flex, LayoutAvatarImageView, LayoutItemCountView, TransitionAnimation, TransitionAnimationTypes } from '../../common'; +import { useAchievements, useFriends, useInventoryUnseenTracker, useMessageEvent, useMessenger, useRoomEngineEvent, useSessionInfo } from '../../hooks'; +import { ToolbarMeView } from './ToolbarMeView'; + +export const ToolbarView: FC<{ isInRoom: boolean }> = props => +{ + const { isInRoom } = props; + const [ isMeExpanded, setMeExpanded ] = useState(false); + const [ useGuideTool, setUseGuideTool ] = useState(false); + const { userFigure = null } = useSessionInfo(); + const { getFullCount = 0 } = useInventoryUnseenTracker(); + const { getTotalUnseen = 0 } = useAchievements(); + const { requests = [] } = useFriends(); + const { iconState = MessengerIconState.HIDDEN } = useMessenger(); + const isMod = GetSessionDataManager().isModerator; + + useMessageEvent(PerkAllowancesMessageEvent, event => + { + const parser = event.getParser(); + + setUseGuideTool(parser.isAllowed(PerkEnum.USE_GUIDE_TOOL)); + }); + + useRoomEngineEvent(NitroToolbarAnimateIconEvent.ANIMATE_ICON, event => + { + const animationIconToToolbar = (iconName: string, image: HTMLImageElement, x: number, y: number) => + { + const target = (document.body.getElementsByClassName(iconName)[0] as HTMLElement); + + if(!target) return; + + image.className = 'toolbar-icon-animation'; + image.style.visibility = 'visible'; + image.style.left = (x + 'px'); + image.style.top = (y + 'px'); + + document.body.append(image); + + const targetBounds = target.getBoundingClientRect(); + const imageBounds = image.getBoundingClientRect(); + + const left = (imageBounds.x - targetBounds.x); + const top = (imageBounds.y - targetBounds.y); + const squared = Math.sqrt(((left * left) + (top * top))); + const wait = (500 - Math.abs(((((1 / squared) * 100) * 500) * 0.5))); + const height = 20; + + const motionName = (`ToolbarBouncing[${ iconName }]`); + + if(!Motions.getMotionByTag(motionName)) + { + Motions.runMotion(new Queue(new Wait((wait + 8)), new DropBounce(target, 400, 12))).tag = motionName; + } + + const motion = new Queue(new EaseOut(new JumpBy(image, wait, ((targetBounds.x - imageBounds.x) + height), (targetBounds.y - imageBounds.y), 100, 1), 1), new Dispose(image)); + + Motions.runMotion(motion); + } + + animationIconToToolbar('icon-inventory', event.image, event.x, event.y); + }); + + return ( + <> + + + + + + + setMeExpanded(!isMeExpanded) }> + + { (getTotalUnseen > 0) && + } + + { isInRoom && + VisitDesktop() } /> } + { !isInRoom && + CreateLinkEvent('navigator/goto/home') } /> } + CreateLinkEvent('navigator/toggle') } /> + { GetConfiguration('game.center.enabled') && CreateLinkEvent('games/toggle') } /> } + CreateLinkEvent('catalog/toggle') } /> + CreateLinkEvent('inventory/toggle') }> + { (getFullCount > 0) && + } + + { isInRoom && + CreateLinkEvent('camera/toggle') } /> } + { isMod && + CreateLinkEvent('mod-tools/toggle') } /> } + + + + + + CreateLinkEvent('friends/toggle') }> + { (requests.length > 0) && + } + + { ((iconState === MessengerIconState.SHOW) || (iconState === MessengerIconState.UNREAD)) && + OpenMessengerChat() } /> } + + + + + + ); +} diff --git a/apps/frontend/src/components/user-profile/UserProfileVew.scss b/apps/frontend/src/components/user-profile/UserProfileVew.scss new file mode 100644 index 0000000..edcfde6 --- /dev/null +++ b/apps/frontend/src/components/user-profile/UserProfileVew.scss @@ -0,0 +1,67 @@ +.user-profile { + width: $user-profile-width; + height: $user-profile-height; + + .user-container { + border-right: 1px solid gray; + + .avatar-container { + width: 75px; + height: 120px; + } + } + + .rooms-button-container { + border-top: 1px solid gray; + border-bottom: 1px solid gray; + } + + .user-relationship { + height: 25px; + + .avatar-image-container { + width: 50px; + height: 50px; + + .avatar-image { + top: 20px; + right: -8pxpx; + } + } + } + + .user-relationship-count { + margin-top: 2px; + margin-left: 5px; + color: #939392 !important; + } + + .user-groups-container { + + .layout-grid-item { + width: 50px; + } + } + + .no-group-spritesheet { + background: transparent url('@/assets/images/groups/no-group-spritesheet.png') no-repeat; + + &.image-1 { + width: 95px; + height: 136px; + background-position: -3px -3px; + } + + &.image-2 { + width: 95px; + height: 136px; + background-position: -104px -3px; + } + + &.image-3 { + width: 95px; + height: 136px; + background-position: -205px -3px; + } + } +} diff --git a/apps/frontend/src/components/user-profile/UserProfileView.tsx b/apps/frontend/src/components/user-profile/UserProfileView.tsx new file mode 100644 index 0000000..6327c29 --- /dev/null +++ b/apps/frontend/src/components/user-profile/UserProfileView.tsx @@ -0,0 +1,122 @@ +import { ExtendedProfileChangedMessageEvent, RelationshipStatusInfoEvent, RelationshipStatusInfoMessageParser, RoomEngineObjectEvent, RoomObjectCategory, RoomObjectType, UserCurrentBadgesComposer, UserCurrentBadgesEvent, UserProfileEvent, UserProfileParser, UserRelationshipsComposer } from '@nitrots/nitro-renderer'; +import { FC, useState } from 'react'; +import { CreateLinkEvent, GetRoomSession, GetSessionDataManager, GetUserProfile, LocalizeText, SendMessageComposer } from '../../api'; +import { Column, Flex, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common'; +import { useMessageEvent, useRoomEngineEvent } from '../../hooks'; +import { BadgesContainerView } from './views/BadgesContainerView'; +import { FriendsContainerView } from './views/FriendsContainerView'; +import { GroupsContainerView } from './views/GroupsContainerView'; +import { UserContainerView } from './views/UserContainerView'; + +export const UserProfileView: FC<{}> = props => +{ + const [ userProfile, setUserProfile ] = useState(null); + const [ userBadges, setUserBadges ] = useState([]); + const [ userRelationships, setUserRelationships ] = useState(null); + + const onClose = () => + { + setUserProfile(null); + setUserBadges([]); + setUserRelationships(null); + } + + const onLeaveGroup = () => + { + if(!userProfile || (userProfile.id !== GetSessionDataManager().userId)) return; + + GetUserProfile(userProfile.id); + } + + useMessageEvent(UserCurrentBadgesEvent, event => + { + const parser = event.getParser(); + + if(!userProfile || (parser.userId !== userProfile.id)) return; + + setUserBadges(parser.badges); + }); + + useMessageEvent(RelationshipStatusInfoEvent, event => + { + const parser = event.getParser(); + + if(!userProfile || (parser.userId !== userProfile.id)) return; + + setUserRelationships(parser); + }); + + useMessageEvent(UserProfileEvent, event => + { + const parser = event.getParser(); + + let isSameProfile = false; + + setUserProfile(prevValue => + { + if(prevValue && prevValue.id) isSameProfile = (prevValue.id === parser.id); + + return parser; + }); + + if(!isSameProfile) + { + setUserBadges([]); + setUserRelationships(null); + } + + SendMessageComposer(new UserCurrentBadgesComposer(parser.id)); + SendMessageComposer(new UserRelationshipsComposer(parser.id)); + }); + + useMessageEvent(ExtendedProfileChangedMessageEvent, event => + { + const parser = event.getParser(); + + if(parser.userId != userProfile?.id) return; + + GetUserProfile(parser.userId); + }); + + useRoomEngineEvent(RoomEngineObjectEvent.SELECTED, event => + { + if(!userProfile) return; + + if(event.category !== RoomObjectCategory.UNIT) return; + + const userData = GetRoomSession().userDataManager.getUserDataByIndex(event.objectId); + + if(userData.type !== RoomObjectType.USER) return; + + GetUserProfile(userData.webID); + }); + + if(!userProfile) return null; + + return ( + + + + + + + + + + + + { userRelationships && + } + + + + CreateLinkEvent(`navigator/search/hotel_view/owner:${ userProfile.username }`) }> + + { LocalizeText('extendedprofile.rooms') } + + + + + + ) +} diff --git a/apps/frontend/src/components/user-profile/views/BadgesContainerView.tsx b/apps/frontend/src/components/user-profile/views/BadgesContainerView.tsx new file mode 100644 index 0000000..ca59fc2 --- /dev/null +++ b/apps/frontend/src/components/user-profile/views/BadgesContainerView.tsx @@ -0,0 +1,25 @@ +import { FC } from 'react'; +import { Column, FlexProps, LayoutBadgeImageView } from '../../../common'; + +interface BadgesContainerViewProps extends FlexProps +{ + badges: string[]; +} + +export const BadgesContainerView: FC = props => +{ + const { badges = null, gap = 1, justifyContent = 'between', ...rest } = props; + + return ( + <> + { badges && (badges.length > 0) && badges.map((badge, index) => + { + return ( + + + + ); + }) } + + ); +} diff --git a/apps/frontend/src/components/user-profile/views/FriendsContainerView.tsx b/apps/frontend/src/components/user-profile/views/FriendsContainerView.tsx new file mode 100644 index 0000000..f5449d1 --- /dev/null +++ b/apps/frontend/src/components/user-profile/views/FriendsContainerView.tsx @@ -0,0 +1,28 @@ +import { RelationshipStatusInfoMessageParser } from '@nitrots/nitro-renderer'; +import { FC } from 'react'; +import { LocalizeText } from '../../../api'; +import { Column, Text } from '../../../common'; +import { RelationshipsContainerView } from './RelationshipsContainerView'; + +interface FriendsContainerViewProps +{ + relationships: RelationshipStatusInfoMessageParser; + friendsCount: number; +} + +export const FriendsContainerView: FC = props => +{ + const { relationships = null, friendsCount = null } = props; + + return ( + + + { LocalizeText('extendedprofile.friends.count') } { friendsCount } + + { LocalizeText('extendedprofile.relstatus') } + + + + + ) +} diff --git a/apps/frontend/src/components/user-profile/views/GroupsContainerView.tsx b/apps/frontend/src/components/user-profile/views/GroupsContainerView.tsx new file mode 100644 index 0000000..bd598ee --- /dev/null +++ b/apps/frontend/src/components/user-profile/views/GroupsContainerView.tsx @@ -0,0 +1,90 @@ +import { GroupInformationComposer, GroupInformationEvent, GroupInformationParser, HabboGroupEntryData } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useState } from 'react'; +import { SendMessageComposer, ToggleFavoriteGroup } from '../../../api'; +import { AutoGrid, Base, Column, Flex, Grid, GridProps, LayoutBadgeImageView, LayoutGridItem } from '../../../common'; +import { useMessageEvent } from '../../../hooks'; +import { GroupInformationView } from '../../groups/views/GroupInformationView'; + +interface GroupsContainerViewProps extends GridProps +{ + itsMe: boolean; + groups: HabboGroupEntryData[]; + onLeaveGroup: () => void; +} + +export const GroupsContainerView: FC = props => +{ + const { itsMe = null, groups = null, onLeaveGroup = null, overflow = 'hidden', gap = 2, ...rest } = props; + const [ selectedGroupId, setSelectedGroupId ] = useState(null); + const [ groupInformation, setGroupInformation ] = useState(null); + + useMessageEvent(GroupInformationEvent, event => + { + const parser = event.getParser(); + + if(!selectedGroupId || (selectedGroupId !== parser.id) || parser.flag) return; + + setGroupInformation(parser); + }); + + useEffect(() => + { + if(!selectedGroupId) return; + + SendMessageComposer(new GroupInformationComposer(selectedGroupId, false)); + }, [ selectedGroupId ]); + + useEffect(() => + { + setGroupInformation(null); + + if(groups.length > 0) + { + setSelectedGroupId(prevValue => + { + if(prevValue === groups[0].groupId) + { + SendMessageComposer(new GroupInformationComposer(groups[0].groupId, false)); + } + + return groups[0].groupId; + }); + } + }, [ groups ]); + + if(!groups || !groups.length) + { + return ( + + + + + + + + ); + } + + return ( + + + + { groups.map((group, index) => + { + return ( + setSelectedGroupId(group.groupId) } className="p-1"> + { itsMe && + ToggleFavoriteGroup(group) } /> } + + + ) + }) } + + + + { groupInformation && + } + + + ); +} diff --git a/apps/frontend/src/components/user-profile/views/RelationshipsContainerView.tsx b/apps/frontend/src/components/user-profile/views/RelationshipsContainerView.tsx new file mode 100644 index 0000000..9b698ec --- /dev/null +++ b/apps/frontend/src/components/user-profile/views/RelationshipsContainerView.tsx @@ -0,0 +1,62 @@ +import { RelationshipStatusEnum, RelationshipStatusInfoMessageParser } from '@nitrots/nitro-renderer'; +import { FC } from 'react'; +import { GetUserProfile, LocalizeText } from '../../../api'; +import { Column, Flex, LayoutAvatarImageView, Text } from '../../../common'; + +interface RelationshipsContainerViewProps +{ + relationships: RelationshipStatusInfoMessageParser; +} + +interface RelationshipsContainerRelationshipViewProps +{ + type: number; +} + +export const RelationshipsContainerView: FC = props => +{ + const { relationships = null } = props; + + const RelationshipComponent = ({ type }: RelationshipsContainerRelationshipViewProps) => + { + const relationshipInfo = (relationships && relationships.relationshipStatusMap.hasKey(type)) ? relationships.relationshipStatusMap.getValue(type) : null; + const relationshipName = RelationshipStatusEnum.RELATIONSHIP_NAMES[type].toLocaleLowerCase(); + + return ( + + + + + + + (relationshipInfo && (relationshipInfo.randomFriendId >= 1) && GetUserProfile(relationshipInfo.randomFriendId)) }> + { (!relationshipInfo || (relationshipInfo.friendCount === 0)) && + LocalizeText('extendedprofile.add.friends') } + { (relationshipInfo && (relationshipInfo.friendCount >= 1)) && + relationshipInfo.randomFriendName } + + { (relationshipInfo && (relationshipInfo.friendCount >= 1)) && + + + } + + + { (!relationshipInfo || (relationshipInfo.friendCount === 0)) && + LocalizeText('extendedprofile.no.friends.in.this.category') } + { (relationshipInfo && (relationshipInfo.friendCount > 1)) && + LocalizeText(`extendedprofile.relstatus.others.${ relationshipName }`, [ 'count' ], [ (relationshipInfo.friendCount - 1).toString() ]) } +   + + + + ); + } + + return ( + <> + + + + + ); +} diff --git a/apps/frontend/src/components/user-profile/views/UserContainerView.tsx b/apps/frontend/src/components/user-profile/views/UserContainerView.tsx new file mode 100644 index 0000000..ceba4bf --- /dev/null +++ b/apps/frontend/src/components/user-profile/views/UserContainerView.tsx @@ -0,0 +1,74 @@ +import { FriendlyTime, RequestFriendComposer, UserProfileParser } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useState } from 'react'; +import { GetSessionDataManager, LocalizeText, SendMessageComposer } from '../../../api'; +import { Column, Flex, LayoutAvatarImageView, Text } from '../../../common'; + +interface UserContainerViewProps +{ + userProfile: UserProfileParser; +} + +export const UserContainerView: FC = props => +{ + const { userProfile = null } = props; + const [ requestSent, setRequestSent ] = useState(userProfile.requestSent); + const isOwnProfile = (userProfile.id === GetSessionDataManager().userId); + const canSendFriendRequest = !requestSent && (!isOwnProfile && !userProfile.isMyFriend && !userProfile.requestSent); + + const addFriend = () => + { + setRequestSent(true); + + SendMessageComposer(new RequestFriendComposer(userProfile.username)); + } + + useEffect(() => + { + setRequestSent(userProfile.requestSent); + }, [ userProfile ]) + + return ( + + + + + + + { userProfile.username } + { userProfile.motto }  + + + + { LocalizeText('extendedprofile.created') } { userProfile.registration } + + + { LocalizeText('extendedprofile.last.login') } { FriendlyTime.format(userProfile.secondsSinceLastVisit, '.ago', 2) } + + + { LocalizeText('extendedprofile.achievementscore') } { userProfile.achievementPoints } + + + + { userProfile.isOnline && + } + { !userProfile.isOnline && + } + + { canSendFriendRequest && + { LocalizeText('extendedprofile.addasafriend') } } + { !canSendFriendRequest && + <> + + { isOwnProfile && + { LocalizeText('extendedprofile.me') } } + { userProfile.isMyFriend && + { LocalizeText('extendedprofile.friend') } } + { (requestSent || userProfile.requestSent) && + { LocalizeText('extendedprofile.friendrequestsent') } } + } + + + + + ) +} diff --git a/apps/frontend/src/components/user-settings/UserSettingsView.tsx b/apps/frontend/src/components/user-settings/UserSettingsView.tsx new file mode 100644 index 0000000..45a4546 --- /dev/null +++ b/apps/frontend/src/components/user-settings/UserSettingsView.tsx @@ -0,0 +1,187 @@ +import { ILinkEventTracker, NitroSettingsEvent, UserSettingsCameraFollowComposer, UserSettingsEvent, UserSettingsOldChatComposer, UserSettingsRoomInvitesComposer, UserSettingsSoundComposer } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useState } from 'react'; +import { FaVolumeDown, FaVolumeMute, FaVolumeUp } from 'react-icons/fa'; +import { AddEventLinkTracker, DispatchMainEvent, DispatchUiEvent, LocalizeText, RemoveLinkEventTracker, SendMessageComposer } from '../../api'; +import { classNames, Column, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common'; +import { useCatalogPlaceMultipleItems, useCatalogSkipPurchaseConfirmation, useMessageEvent } from '../../hooks'; + +export const UserSettingsView: FC<{}> = props => +{ + const [ isVisible, setIsVisible ] = useState(false); + const [ userSettings, setUserSettings ] = useState(null); + const [ catalogPlaceMultipleObjects, setCatalogPlaceMultipleObjects ] = useCatalogPlaceMultipleItems(); + const [ catalogSkipPurchaseConfirmation, setCatalogSkipPurchaseConfirmation ] = useCatalogSkipPurchaseConfirmation(); + + const processAction = (type: string, value?: boolean | number | string) => + { + let doUpdate = true; + + const clone = userSettings.clone(); + + switch(type) + { + case 'close_view': + setIsVisible(false); + doUpdate = false; + return; + case 'oldchat': + clone.oldChat = value as boolean; + SendMessageComposer(new UserSettingsOldChatComposer(clone.oldChat)); + break; + case 'room_invites': + clone.roomInvites = value as boolean; + SendMessageComposer(new UserSettingsRoomInvitesComposer(clone.roomInvites)); + break; + case 'camera_follow': + clone.cameraFollow = value as boolean; + SendMessageComposer(new UserSettingsCameraFollowComposer(clone.cameraFollow)); + break; + case 'system_volume': + clone.volumeSystem = value as number; + clone.volumeSystem = Math.max(0, clone.volumeSystem); + clone.volumeSystem = Math.min(100, clone.volumeSystem); + break; + case 'furni_volume': + clone.volumeFurni = value as number; + clone.volumeFurni = Math.max(0, clone.volumeFurni); + clone.volumeFurni = Math.min(100, clone.volumeFurni); + break; + case 'trax_volume': + clone.volumeTrax = value as number; + clone.volumeTrax = Math.max(0, clone.volumeTrax); + clone.volumeTrax = Math.min(100, clone.volumeTrax); + break; + } + + if(doUpdate) setUserSettings(clone); + + DispatchMainEvent(clone) + } + + const saveRangeSlider = (type: string) => + { + switch(type) + { + case 'volume': + SendMessageComposer(new UserSettingsSoundComposer(Math.round(userSettings.volumeSystem), Math.round(userSettings.volumeFurni), Math.round(userSettings.volumeTrax))); + break; + } + } + + useMessageEvent(UserSettingsEvent, event => + { + const parser = event.getParser(); + const settingsEvent = new NitroSettingsEvent(); + + settingsEvent.volumeSystem = parser.volumeSystem; + settingsEvent.volumeFurni = parser.volumeFurni; + settingsEvent.volumeTrax = parser.volumeTrax; + settingsEvent.oldChat = parser.oldChat; + settingsEvent.roomInvites = parser.roomInvites; + settingsEvent.cameraFollow = parser.cameraFollow; + settingsEvent.flags = parser.flags; + settingsEvent.chatType = parser.chatType; + + setUserSettings(settingsEvent); + DispatchMainEvent(settingsEvent); + }); + + 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(prevValue => !prevValue); + return; + } + }, + eventUrlPrefix: 'user-settings/' + }; + + AddEventLinkTracker(linkTracker); + + return () => RemoveLinkEventTracker(linkTracker); + }, []); + + useEffect(() => + { + if(!userSettings) return; + + DispatchUiEvent(userSettings); + }, [ userSettings ]); + + if(!isVisible || !userSettings) return null; + + return ( + + processAction('close_view') } /> + + + + processAction('oldchat', event.target.checked) } /> + { LocalizeText('memenu.settings.chat.prefer.old.chat') } + + + processAction('room_invites', event.target.checked) } /> + { LocalizeText('memenu.settings.other.ignore.room.invites') } + + + processAction('camera_follow', event.target.checked) } /> + { LocalizeText('memenu.settings.other.disable.room.camera.follow') } + + + setCatalogPlaceMultipleObjects(event.target.checked) } /> + { LocalizeText('memenu.settings.other.place.multiple.objects') } + + + setCatalogSkipPurchaseConfirmation(event.target.checked) } /> + { LocalizeText('memenu.settings.other.skip.purchase.confirmation') } + + + + { LocalizeText('widget.memenu.settings.volume') } + + { LocalizeText('widget.memenu.settings.volume.ui') } + + { (userSettings.volumeSystem === 0) && = 50) && 'text-muted', 'fa-icon') } /> } + { (userSettings.volumeSystem > 0) && = 50) && 'text-muted', 'fa-icon') } /> } + processAction('system_volume', event.target.value) } onMouseUp={ () => saveRangeSlider('volume') }/> + + + + + { LocalizeText('widget.memenu.settings.volume.furni') } + + { (userSettings.volumeFurni === 0) && = 50) && 'text-muted', 'fa-icon') } /> } + { (userSettings.volumeFurni > 0) && = 50) && 'text-muted', 'fa-icon') } /> } + processAction('furni_volume', event.target.value) } onMouseUp={ () => saveRangeSlider('volume') }/> + + + + + { LocalizeText('widget.memenu.settings.volume.trax') } + + { (userSettings.volumeTrax === 0) && = 50) && 'text-muted', 'fa-icon') } /> } + { (userSettings.volumeTrax > 0) && = 50) && 'text-muted', 'fa-icon') } /> } + processAction('trax_volume', event.target.value) } onMouseUp={ () => saveRangeSlider('volume') }/> + + + + + + + ); +} diff --git a/apps/frontend/src/components/wired/WiredView.scss b/apps/frontend/src/components/wired/WiredView.scss new file mode 100644 index 0000000..06569ad --- /dev/null +++ b/apps/frontend/src/components/wired/WiredView.scss @@ -0,0 +1,175 @@ +.nitro-wired { + width: 300px; + + .icon { + background-repeat: no-repeat; + background-position: center; + + &.icon-mv-1 { + width: 16px; + height: 9px; + background-image: url('@/assets/images/wired/icon_wired_around.png'); + } + &.icon-mv-2 { + width: 16px; + height: 9px; + background-image: url('@/assets/images/wired/icon_wired_up_down.png'); + } + &.icon-mv-3 { + width: 16px; + height: 9px; + background-image: url('@/assets/images/wired/icon_wired_left_right.png'); + } + &.icon-ne { + width: 16px; + height: 9px; + background-image: url('@/assets/images/wired/icon_wired_north_east.png'); + } + &.icon-se { + width: 16px; + height: 9px; + background-image: url('@/assets/images/wired/icon_wired_south_east.png'); + } + &.icon-sw { + width: 16px; + height: 9px; + background-image: url('@/assets/images/wired/icon_wired_south_west.png'); + } + &.icon-nw { + width: 16px; + height: 9px; + background-image: url('@/assets/images/wired/icon_wired_north_west.png'); + } + &.icon-rot-1 { + width: 16px; + height: 9px; + background-image: url('@/assets/images/wired/icon_wired_rotate_clockwise.png'); + } + &.icon-rot-2 { + width: 16px; + height: 9px; + background-image: url('@/assets/images/wired/icon_wired_rotate_counter_clockwise.png'); + } + } + + .nitro-wired-header { + color: #000; + margin-bottom:3px; + + .nitro-wired-title, .nitro-wired-close { + border:1px solid rgba($black,.8); + background-image: linear-gradient(45deg, #00d9cb 25%, #00bdb0 25%, #00bdb0 50%, #00d9cb 50%, #00d9cb 75%, #00bdb0 75%, #00bdb0 100%); + background-size: 197.99px 197.99px; + animation: wiredSlider 3s linear infinite; + text-align: center; + box-shadow:inset 0 0 0 2px rgba($white,.6), 0 2px rgba($black,.4); + } + + .nitro-wired-title { + margin-right:3px; + } + + .nitro-wired-close { + min-width: 23px; + } + } + + &.nitro-wired-trigger { + background-color: #3b2516 !important; + border: 1px solid #000 !important; + box-shadow: inset 0px -2px #50321f, + inset 0px -3px #86583b, + inset 0 0 0 1px #86583b, + inset 0 0 0 3px #644029, + inset 0 0 0 4px rgba($black,.4) !important; + + .bg-light,.bg-primary { + background-color: transparent !important; + } + + .bg-dark { + background-color: #000 !important; + } + } + + &.nitro-wired-action { + background-color: #686868 !important; + border: 1px solid #000 !important; + box-shadow: inset 0px -2px #9d9d9d, + inset 0px -3px #c5c5c5, + inset 0 0 0 1px #c5c5c5, + inset 0 0 0 3px #9d9d9d, + inset 0 0 0 4px rgba($black,.4) !important; + + .bg-light,.bg-primary { + background-color: transparent !important; + } + + .bg-dark { + background-color: #000 !important; + } + + &::before, + &::after, + .content-area::before, + .content-area::after { + content: ''; + height: 6px; + width: 6px; + position: absolute; + background-image: url('@/assets/images/wired/card-action-corners.png'); + } + + &::before { + background-position: 0 0; + top: 0; + left: 0; + } + + &::after { + background-position: 6px 0; + top: 0; + right: 0; + } + + .content-area { + &::before { + background-position: 0 6px; + bottom: 0; + left: 0; + } + + &::after { + background-position: 6px 6px; + bottom: 0; + right: 0; + } + } + } + + &.nitro-wired-condition { + background-color: #cfd2dd !important; + border: 1px solid #000 !important; + box-shadow: inset 0 0 0 3px #efefef, inset 4px 4px #abaeb9 !important; + color: #000; + + .bg-light,.bg-primary { + background-color: transparent !important; + } + + .bg-dark { + background-color: #000 !important; + } + } +} + + +@keyframes wiredSlider { + 0% { + background-position: 0 0; + } + + 100% { + background-position: 0 -197.99px; + } +} diff --git a/apps/frontend/src/components/wired/WiredView.tsx b/apps/frontend/src/components/wired/WiredView.tsx new file mode 100644 index 0000000..cb3b4ba --- /dev/null +++ b/apps/frontend/src/components/wired/WiredView.tsx @@ -0,0 +1,21 @@ +import { ConditionDefinition, TriggerDefinition, WiredActionDefinition } from '@nitrots/nitro-renderer'; +import { FC } from 'react'; +import { useWired } from '../../hooks'; +import { WiredActionLayoutView } from './views/actions/WiredActionLayoutView'; +import { WiredConditionLayoutView } from './views/conditions/WiredConditionLayoutView'; +import { WiredTriggerLayoutView } from './views/triggers/WiredTriggerLayoutView'; + +export const WiredView: FC<{}> = props => +{ + const { trigger = null } = useWired(); + + if(!trigger) return null; + + if(trigger instanceof WiredActionDefinition) return WiredActionLayoutView(trigger.code); + + if(trigger instanceof TriggerDefinition) return WiredTriggerLayoutView(trigger.code); + + if(trigger instanceof ConditionDefinition) return WiredConditionLayoutView(trigger.code); + + return null; +}; diff --git a/apps/frontend/src/components/wired/views/WiredBaseView.tsx b/apps/frontend/src/components/wired/views/WiredBaseView.tsx new file mode 100644 index 0000000..92cc77f --- /dev/null +++ b/apps/frontend/src/components/wired/views/WiredBaseView.tsx @@ -0,0 +1,113 @@ +import { FC, PropsWithChildren, useEffect, useState } from 'react'; +import { GetSessionDataManager, LocalizeText, WiredFurniType, WiredSelectionVisualizer } from '../../../api'; +import { Button, Column, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../common'; +import { useWired } from '../../../hooks'; +import { WiredFurniSelectorView } from './WiredFurniSelectorView'; + +export interface WiredBaseViewProps +{ + wiredType: string; + requiresFurni: number; + hasSpecialInput: boolean; + save: () => void; + validate?: () => boolean; +} + +export const WiredBaseView: FC> = props => +{ + const { wiredType = '', requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_NONE, save = null, validate = null, children = null, hasSpecialInput = false } = props; + const [ wiredName, setWiredName ] = useState(null); + const [ wiredDescription, setWiredDescription ] = useState(null); + const [ needsSave, setNeedsSave ] = useState(false); + const { trigger = null, setTrigger = null, setIntParams = null, setStringParam = null, setFurniIds = null, setAllowsFurni = null, saveWired = null } = useWired(); + + const onClose = () => setTrigger(null); + + const onSave = () => + { + if(validate && !validate()) return; + + if(save) save(); + + setNeedsSave(true); + } + + useEffect(() => + { + if(!needsSave) return; + + saveWired(); + + setNeedsSave(false); + }, [ needsSave, saveWired ]); + + useEffect(() => + { + if(!trigger) return; + + const spriteId = (trigger.spriteId || -1); + const furniData = GetSessionDataManager().getFloorItemData(spriteId); + + if(!furniData) + { + setWiredName(('NAME: ' + spriteId)); + setWiredDescription(('NAME: ' + spriteId)); + } + else + { + setWiredName(furniData.name); + setWiredDescription(furniData.description); + } + + if(hasSpecialInput) + { + setIntParams(trigger.intData); + setStringParam(trigger.stringData); + } + + if(requiresFurni > WiredFurniType.STUFF_SELECTION_OPTION_NONE) + { + setFurniIds(prevValue => + { + if(prevValue && prevValue.length) WiredSelectionVisualizer.clearSelectionShaderFromFurni(prevValue); + + if(trigger.selectedItems && trigger.selectedItems.length) + { + WiredSelectionVisualizer.applySelectionShaderToFurni(trigger.selectedItems); + + return trigger.selectedItems; + } + + return []; + }); + } + + setAllowsFurni(requiresFurni); + }, [ trigger, hasSpecialInput, requiresFurni, setIntParams, setStringParam, setFurniIds, setAllowsFurni ]); + + return ( + + + + + + + { wiredName } + + { wiredDescription } + + { !!children &&
} + { children } + { (requiresFurni > WiredFurniType.STUFF_SELECTION_OPTION_NONE) && + <> +
+ + } + + + + +
+
+ ); +} diff --git a/apps/frontend/src/components/wired/views/WiredFurniSelectorView.tsx b/apps/frontend/src/components/wired/views/WiredFurniSelectorView.tsx new file mode 100644 index 0000000..9b38f97 --- /dev/null +++ b/apps/frontend/src/components/wired/views/WiredFurniSelectorView.tsx @@ -0,0 +1,16 @@ +import { FC } from 'react'; +import { LocalizeText } from '../../../api'; +import { Column, Text } from '../../../common'; +import { useWired } from '../../../hooks'; + +export const WiredFurniSelectorView: FC<{}> = props => +{ + const { trigger = null, furniIds = [] } = useWired(); + + return ( + + { LocalizeText('wiredfurni.pickfurnis.caption', [ 'count', 'limit' ], [ furniIds.length.toString(), trigger.maximumItemSelectionCount.toString() ]) } + { LocalizeText('wiredfurni.pickfurnis.desc') } + + ); +} diff --git a/apps/frontend/src/components/wired/views/actions/WiredActionBaseView.tsx b/apps/frontend/src/components/wired/views/actions/WiredActionBaseView.tsx new file mode 100644 index 0000000..6c7a86e --- /dev/null +++ b/apps/frontend/src/components/wired/views/actions/WiredActionBaseView.tsx @@ -0,0 +1,41 @@ +import { WiredActionDefinition } from '@nitrots/nitro-renderer'; +import { FC, PropsWithChildren, useEffect } from 'react'; +import ReactSlider from 'react-slider'; +import { GetWiredTimeLocale, LocalizeText, WiredFurniType } from '../../../../api'; +import { Column, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredBaseView } from '../WiredBaseView'; + +export interface WiredActionBaseViewProps +{ + hasSpecialInput: boolean; + requiresFurni: number; + save: () => void; +} + +export const WiredActionBaseView: FC> = props => +{ + const { requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_NONE, save = null, hasSpecialInput = false, children = null } = props; + const { trigger = null, actionDelay = 0, setActionDelay = null } = useWired(); + + useEffect(() => + { + setActionDelay((trigger as WiredActionDefinition).delayInPulses); + }, [ trigger, setActionDelay ]); + + return ( + + { children } + { !!children &&
} + + { LocalizeText('wiredfurni.params.delay', [ 'seconds' ], [ GetWiredTimeLocale(actionDelay) ]) } + setActionDelay(event) } /> + +
+ ); +} diff --git a/apps/frontend/src/components/wired/views/actions/WiredActionBotChangeFigureView.tsx b/apps/frontend/src/components/wired/views/actions/WiredActionBotChangeFigureView.tsx new file mode 100644 index 0000000..4289f95 --- /dev/null +++ b/apps/frontend/src/components/wired/views/actions/WiredActionBotChangeFigureView.tsx @@ -0,0 +1,37 @@ +import { FC, useEffect, useState } from 'react'; +import { GetSessionDataManager, LocalizeText, WiredFurniType, WIRED_STRING_DELIMETER } from '../../../../api'; +import { Button, Column, Flex, LayoutAvatarImageView, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredActionBaseView } from './WiredActionBaseView'; + +const DEFAULT_FIGURE: string = 'hd-180-1.ch-210-66.lg-270-82.sh-290-81'; + +export const WiredActionBotChangeFigureView: FC<{}> = props => +{ + const [ botName, setBotName ] = useState(''); + const [ figure, setFigure ] = useState(''); + const { trigger = null, setStringParam = null } = useWired(); + + const save = () => setStringParam((botName + WIRED_STRING_DELIMETER + figure)); + + useEffect(() => + { + const data = trigger.stringData.split(WIRED_STRING_DELIMETER); + + if(data.length > 0) setBotName(data[0]); + if(data.length > 1) setFigure(data[1].length > 0 ? data[1] : DEFAULT_FIGURE); + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.params.bot.name') } + setBotName(event.target.value) } /> + + + + + + + ); +} diff --git a/apps/frontend/src/components/wired/views/actions/WiredActionBotFollowAvatarView.tsx b/apps/frontend/src/components/wired/views/actions/WiredActionBotFollowAvatarView.tsx new file mode 100644 index 0000000..2acb5cc --- /dev/null +++ b/apps/frontend/src/components/wired/views/actions/WiredActionBotFollowAvatarView.tsx @@ -0,0 +1,43 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Column, Flex, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredActionBaseView } from './WiredActionBaseView'; + +export const WiredActionBotFollowAvatarView: FC<{}> = props => +{ + const [ botName, setBotName ] = useState(''); + const [ followMode, setFollowMode ] = useState(-1); + const { trigger = null, setStringParam = null, setIntParams = null } = useWired(); + + const save = () => + { + setStringParam(botName); + setIntParams([ followMode ]); + } + + useEffect(() => + { + setBotName(trigger.stringData); + setFollowMode((trigger.intData.length > 0) ? trigger.intData[0] : 0); + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.params.bot.name') } + setBotName(event.target.value) } /> + + + + setFollowMode(1) } /> + { LocalizeText('wiredfurni.params.start.following') } + + + setFollowMode(0) } /> + { LocalizeText('wiredfurni.params.stop.following') } + + + + ); +} diff --git a/apps/frontend/src/components/wired/views/actions/WiredActionBotGiveHandItemView.tsx b/apps/frontend/src/components/wired/views/actions/WiredActionBotGiveHandItemView.tsx new file mode 100644 index 0000000..c93d0de --- /dev/null +++ b/apps/frontend/src/components/wired/views/actions/WiredActionBotGiveHandItemView.tsx @@ -0,0 +1,42 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Column, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredActionBaseView } from './WiredActionBaseView'; + +const ALLOWED_HAND_ITEM_IDS: number[] = [ 2, 5, 7, 8, 9, 10, 27 ]; + +export const WiredActionBotGiveHandItemView: FC<{}> = props => +{ + const [ botName, setBotName ] = useState(''); + const [ handItemId, setHandItemId ] = useState(-1); + const { trigger = null, setStringParam = null, setIntParams = null } = useWired(); + + const save = () => + { + setStringParam(botName); + setIntParams([ handItemId ]); + } + + useEffect(() => + { + setBotName(trigger.stringData); + setHandItemId((trigger.intData.length > 0) ? trigger.intData[0] : 0); + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.params.bot.name') } + setBotName(event.target.value) } /> + + + { LocalizeText('wiredfurni.params.handitem') } + + + + ); +} diff --git a/apps/frontend/src/components/wired/views/actions/WiredActionBotMoveView.tsx b/apps/frontend/src/components/wired/views/actions/WiredActionBotMoveView.tsx new file mode 100644 index 0000000..d09a359 --- /dev/null +++ b/apps/frontend/src/components/wired/views/actions/WiredActionBotMoveView.tsx @@ -0,0 +1,27 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Column, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredActionBaseView } from './WiredActionBaseView'; + +export const WiredActionBotMoveView: FC<{}> = props => +{ + const [ botName, setBotName ] = useState(''); + const { trigger = null, setStringParam = null } = useWired(); + + const save = () => setStringParam(botName); + + useEffect(() => + { + setBotName(trigger.stringData); + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.params.bot.name') } + setBotName(event.target.value) } /> + + + ); +} diff --git a/apps/frontend/src/components/wired/views/actions/WiredActionBotTalkToAvatarView.tsx b/apps/frontend/src/components/wired/views/actions/WiredActionBotTalkToAvatarView.tsx new file mode 100644 index 0000000..ebebe2d --- /dev/null +++ b/apps/frontend/src/components/wired/views/actions/WiredActionBotTalkToAvatarView.tsx @@ -0,0 +1,52 @@ +import { FC, useEffect, useState } from 'react'; +import { GetConfiguration, LocalizeText, WiredFurniType, WIRED_STRING_DELIMETER } from '../../../../api'; +import { Column, Flex, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredActionBaseView } from './WiredActionBaseView'; + +export const WiredActionBotTalkToAvatarView: FC<{}> = props => +{ + const [ botName, setBotName ] = useState(''); + const [ message, setMessage ] = useState(''); + const [ talkMode, setTalkMode ] = useState(-1); + const { trigger = null, setStringParam = null, setIntParams = null } = useWired(); + + const save = () => + { + setStringParam(botName + WIRED_STRING_DELIMETER + message); + setIntParams([ talkMode ]); + } + + useEffect(() => + { + const data = trigger.stringData.split(WIRED_STRING_DELIMETER); + + if(data.length > 0) setBotName(data[0]); + if(data.length > 1) setMessage(data[1].length > 0 ? data[1] : ''); + + setTalkMode((trigger.intData.length > 0) ? trigger.intData[0] : 0); + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.params.bot.name') } + setBotName(event.target.value) } /> + + + { LocalizeText('wiredfurni.params.message') } + ('wired.action.bot.talk.to.avatar.max.length', 64) } value={ message } onChange={ event => setMessage(event.target.value) } /> + + + + setTalkMode(0) } /> + { LocalizeText('wiredfurni.params.talk') } + + + setTalkMode(1) } /> + { LocalizeText('wiredfurni.params.whisper') } + + + + ); +} diff --git a/apps/frontend/src/components/wired/views/actions/WiredActionBotTalkView.tsx b/apps/frontend/src/components/wired/views/actions/WiredActionBotTalkView.tsx new file mode 100644 index 0000000..cd2d28e --- /dev/null +++ b/apps/frontend/src/components/wired/views/actions/WiredActionBotTalkView.tsx @@ -0,0 +1,52 @@ +import { FC, useEffect, useState } from 'react'; +import { GetConfiguration, LocalizeText, WiredFurniType, WIRED_STRING_DELIMETER } from '../../../../api'; +import { Column, Flex, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredActionBaseView } from './WiredActionBaseView'; + +export const WiredActionBotTalkView: FC<{}> = props => +{ + const [ botName, setBotName ] = useState(''); + const [ message, setMessage ] = useState(''); + const [ talkMode, setTalkMode ] = useState(-1); + const { trigger = null, setStringParam = null, setIntParams = null } = useWired(); + + const save = () => + { + setStringParam(botName + WIRED_STRING_DELIMETER + message); + setIntParams([ talkMode ]); + } + + useEffect(() => + { + const data = trigger.stringData.split(WIRED_STRING_DELIMETER); + + if(data.length > 0) setBotName(data[0]); + if(data.length > 1) setMessage(data[1].length > 0 ? data[1] : ''); + + setTalkMode((trigger.intData.length > 0) ? trigger.intData[0] : 0); + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.params.bot.name') } + setBotName(event.target.value) } /> + + + { LocalizeText('wiredfurni.params.message') } + ('wired.action.bot.talk.max.length', 64) } value={ message } onChange={ event => setMessage(event.target.value) } /> + + + + setTalkMode(0) } /> + { LocalizeText('wiredfurni.params.talk') } + + + setTalkMode(1) } /> + { LocalizeText('wiredfurni.params.shout') } + + + + ); +} diff --git a/apps/frontend/src/components/wired/views/actions/WiredActionBotTeleportView.tsx b/apps/frontend/src/components/wired/views/actions/WiredActionBotTeleportView.tsx new file mode 100644 index 0000000..5d767b1 --- /dev/null +++ b/apps/frontend/src/components/wired/views/actions/WiredActionBotTeleportView.tsx @@ -0,0 +1,27 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Column, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredActionBaseView } from './WiredActionBaseView'; + +export const WiredActionBotTeleportView: FC<{}> = props => +{ + const [ botName, setBotName ] = useState(''); + const { trigger = null, setStringParam = null } = useWired(); + + const save = () => setStringParam(botName); + + useEffect(() => + { + setBotName(trigger.stringData); + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.params.bot.name') } + setBotName(event.target.value) } /> + + + ); +} diff --git a/apps/frontend/src/components/wired/views/actions/WiredActionCallAnotherStackView.tsx b/apps/frontend/src/components/wired/views/actions/WiredActionCallAnotherStackView.tsx new file mode 100644 index 0000000..ee6d547 --- /dev/null +++ b/apps/frontend/src/components/wired/views/actions/WiredActionCallAnotherStackView.tsx @@ -0,0 +1,8 @@ +import { FC } from 'react'; +import { WiredFurniType } from '../../../../api'; +import { WiredActionBaseView } from './WiredActionBaseView'; + +export const WiredActionCallAnotherStackView: FC<{}> = props => +{ + return ; +} diff --git a/apps/frontend/src/components/wired/views/actions/WiredActionChaseView.tsx b/apps/frontend/src/components/wired/views/actions/WiredActionChaseView.tsx new file mode 100644 index 0000000..c494503 --- /dev/null +++ b/apps/frontend/src/components/wired/views/actions/WiredActionChaseView.tsx @@ -0,0 +1,8 @@ +import { FC } from 'react'; +import { WiredFurniType } from '../../../../api'; +import { WiredActionBaseView } from './WiredActionBaseView'; + +export const WiredActionChaseView: FC<{}> = props => +{ + return ; +} diff --git a/apps/frontend/src/components/wired/views/actions/WiredActionChatView.tsx b/apps/frontend/src/components/wired/views/actions/WiredActionChatView.tsx new file mode 100644 index 0000000..ed256dc --- /dev/null +++ b/apps/frontend/src/components/wired/views/actions/WiredActionChatView.tsx @@ -0,0 +1,27 @@ +import { FC, useEffect, useState } from 'react'; +import { GetConfiguration, LocalizeText, WiredFurniType } from '../../../../api'; +import { Column, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredActionBaseView } from './WiredActionBaseView'; + +export const WiredActionChatView: FC<{}> = props => +{ + const [ message, setMessage ] = useState(''); + const { trigger = null, setStringParam = null } = useWired(); + + const save = () => setStringParam(message); + + useEffect(() => + { + setMessage(trigger.stringData); + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.params.message') } + setMessage(event.target.value) } maxLength={ GetConfiguration('wired.action.chat.max.length', 100) } /> + + + ); +} diff --git a/apps/frontend/src/components/wired/views/actions/WiredActionFleeView.tsx b/apps/frontend/src/components/wired/views/actions/WiredActionFleeView.tsx new file mode 100644 index 0000000..a80ab43 --- /dev/null +++ b/apps/frontend/src/components/wired/views/actions/WiredActionFleeView.tsx @@ -0,0 +1,8 @@ +import { FC } from 'react'; +import { WiredFurniType } from '../../../../api'; +import { WiredActionBaseView } from './WiredActionBaseView'; + +export const WiredActionFleeView: FC<{}> = props => +{ + return ; +} diff --git a/apps/frontend/src/components/wired/views/actions/WiredActionGiveRewardView.tsx b/apps/frontend/src/components/wired/views/actions/WiredActionGiveRewardView.tsx new file mode 100644 index 0000000..a255fe3 --- /dev/null +++ b/apps/frontend/src/components/wired/views/actions/WiredActionGiveRewardView.tsx @@ -0,0 +1,160 @@ +import { FC, useEffect, useState } from 'react'; +import { FaPlus, FaTrash } from 'react-icons/fa'; +import ReactSlider from 'react-slider'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Button, Column, Flex, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredActionBaseView } from './WiredActionBaseView'; + +export const WiredActionGiveRewardView: FC<{}> = props => +{ + const [ limitEnabled, setLimitEnabled ] = useState(false); + const [ rewardTime, setRewardTime ] = useState(1); + const [ uniqueRewards, setUniqueRewards ] = useState(false); + const [ rewardsLimit, setRewardsLimit ] = useState(1); + const [ limitationInterval, setLimitationInterval ] = useState(1); + const [ rewards, setRewards ] = useState<{ isBadge: boolean, itemCode: string, probability: number }[]>([]); + const { trigger = null, setIntParams = null, setStringParam = null } = useWired(); + + const addReward = () => setRewards(rewards => [ ...rewards, { isBadge: false, itemCode: '', probability: null } ]); + + const removeReward = (index: number) => + { + setRewards(prevValue => + { + const newValues = Array.from(prevValue); + + newValues.splice(index, 1); + + return newValues; + }); + } + + const updateReward = (index: number, isBadge: boolean, itemCode: string, probability: number) => + { + const rewardsClone = Array.from(rewards); + const reward = rewardsClone[index]; + + if(!reward) return; + + reward.isBadge = isBadge; + reward.itemCode = itemCode; + reward.probability = probability; + + setRewards(rewardsClone); + } + + const save = () => + { + let stringRewards = []; + + for(const reward of rewards) + { + if(!reward.itemCode) continue; + + const rewardsString = [ reward.isBadge ? '0' : '1', reward.itemCode, reward.probability.toString() ]; + stringRewards.push(rewardsString.join(',')); + } + + if(stringRewards.length > 0) + { + setStringParam(stringRewards.join(';')); + setIntParams([ rewardTime, uniqueRewards ? 1 : 0, rewardsLimit, limitationInterval ]); + } + } + + useEffect(() => + { + const readRewards: { isBadge: boolean, itemCode: string, probability: number }[] = []; + + if(trigger.stringData.length > 0 && trigger.stringData.includes(';')) + { + const splittedRewards = trigger.stringData.split(';'); + + for(const rawReward of splittedRewards) + { + const reward = rawReward.split(','); + + if(reward.length !== 3) continue; + + readRewards.push({ isBadge: reward[0] === '0', itemCode: reward[1], probability: Number(reward[2]) }); + } + } + + if(readRewards.length === 0) readRewards.push({ isBadge: false, itemCode: '', probability: null }); + + setRewardTime((trigger.intData.length > 0) ? trigger.intData[0] : 0); + setUniqueRewards((trigger.intData.length > 1) ? (trigger.intData[1] === 1) : false); + setRewardsLimit((trigger.intData.length > 2) ? trigger.intData[2] : 0); + setLimitationInterval((trigger.intData.length > 3) ? trigger.intData[3] : 0); + setLimitEnabled((trigger.intData.length > 3) ? trigger.intData[3] > 0 : false); + setRewards(readRewards); + }, [ trigger ]); + + return ( + + + setLimitEnabled(event.target.checked) } /> + { LocalizeText('wiredfurni.params.prizelimit', [ 'amount' ], [ limitEnabled ? rewardsLimit.toString() : '' ]) } + + { !limitEnabled && + + Reward limit not set. Make sure rewards are badges or non-tradeable items. + } + { limitEnabled && + setRewardsLimit(event) } /> } +
+ + How often can a user be rewarded? + + + { (rewardTime > 0) && setLimitationInterval(Number(event.target.value)) } /> } + + +
+ + setUniqueRewards(e.target.checked) } /> + Unique rewards + + + If checked each reward will be given once to each user. This will disable the probabilities option. + +
+ + Rewards + + + + { rewards && rewards.map((reward, index) => + { + return ( + + + updateReward(index, e.target.checked, reward.itemCode, reward.probability) } /> + Badge? + + updateReward(index, reward.isBadge, e.target.value, reward.probability) } placeholder="Item Code" /> + updateReward(index, reward.isBadge, reward.itemCode, Number(e.target.value)) } placeholder="Probability" /> + { (index > 0) && + } + + ) + }) } + +
+ ); +} diff --git a/apps/frontend/src/components/wired/views/actions/WiredActionGiveScoreToPredefinedTeamView.tsx b/apps/frontend/src/components/wired/views/actions/WiredActionGiveScoreToPredefinedTeamView.tsx new file mode 100644 index 0000000..43ab240 --- /dev/null +++ b/apps/frontend/src/components/wired/views/actions/WiredActionGiveScoreToPredefinedTeamView.tsx @@ -0,0 +1,67 @@ +import { FC, useEffect, useState } from 'react'; +import ReactSlider from 'react-slider'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Column, Flex, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredActionBaseView } from './WiredActionBaseView'; + +export const WiredActionGiveScoreToPredefinedTeamView: FC<{}> = props => +{ + const [ points, setPoints ] = useState(1); + const [ time, setTime ] = useState(1); + const [ selectedTeam, setSelectedTeam ] = useState(1); + const { trigger = null, setIntParams = null } = useWired(); + + const save = () => setIntParams([ points, time, selectedTeam ]); + + useEffect(() => + { + if(trigger.intData.length >= 2) + { + setPoints(trigger.intData[0]); + setTime(trigger.intData[1]); + setSelectedTeam(trigger.intData[2]); + } + else + { + setPoints(1); + setTime(1); + setSelectedTeam(1); + } + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.params.setpoints', [ 'points' ], [ points.toString() ]) } + setPoints(event) } /> + + + { LocalizeText('wiredfurni.params.settimesingame', [ 'times' ], [ time.toString() ]) } + setTime(event) } /> + + + { LocalizeText('wiredfurni.params.team') } + { [ 1, 2, 3, 4 ].map(value => + { + return ( + + setSelectedTeam(value) } /> + { LocalizeText('wiredfurni.params.team.' + value) } + + ); + }) } + + + ); +} diff --git a/apps/frontend/src/components/wired/views/actions/WiredActionGiveScoreView.tsx b/apps/frontend/src/components/wired/views/actions/WiredActionGiveScoreView.tsx new file mode 100644 index 0000000..28bfb0b --- /dev/null +++ b/apps/frontend/src/components/wired/views/actions/WiredActionGiveScoreView.tsx @@ -0,0 +1,52 @@ +import { FC, useEffect, useState } from 'react'; +import ReactSlider from 'react-slider'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Column, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredActionBaseView } from './WiredActionBaseView'; + +export const WiredActionGiveScoreView: FC<{}> = props => +{ + const [ points, setPoints ] = useState(1); + const [ time, setTime ] = useState(1); + const { trigger = null, setIntParams = null } = useWired(); + + const save = () => setIntParams([ points, time ]); + + useEffect(() => + { + if(trigger.intData.length >= 2) + { + setPoints(trigger.intData[0]); + setTime(trigger.intData[1]); + } + else + { + setPoints(1); + setTime(1); + } + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.params.setpoints', [ 'points' ], [ points.toString() ]) } + setPoints(event) } /> + + + { LocalizeText('wiredfurni.params.settimesingame', [ 'times' ], [ time.toString() ]) } + setTime(event) } /> + + + ); +} diff --git a/apps/frontend/src/components/wired/views/actions/WiredActionJoinTeamView.tsx b/apps/frontend/src/components/wired/views/actions/WiredActionJoinTeamView.tsx new file mode 100644 index 0000000..7580564 --- /dev/null +++ b/apps/frontend/src/components/wired/views/actions/WiredActionJoinTeamView.tsx @@ -0,0 +1,35 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Column, Flex, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredActionBaseView } from './WiredActionBaseView'; + +export const WiredActionJoinTeamView: FC<{}> = props => +{ + const [ selectedTeam, setSelectedTeam ] = useState(-1); + const { trigger = null, setIntParams = null } = useWired(); + + const save = () => setIntParams([ selectedTeam ]); + + useEffect(() => + { + setSelectedTeam((trigger.intData.length > 0) ? trigger.intData[0] : 0); + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.params.team') } + { [ 1, 2, 3, 4 ].map(team => + { + return ( + + setSelectedTeam(team) } /> + { LocalizeText(`wiredfurni.params.team.${ team }`) } + + ) + }) } + + + ); +} diff --git a/apps/frontend/src/components/wired/views/actions/WiredActionKickFromRoomView.tsx b/apps/frontend/src/components/wired/views/actions/WiredActionKickFromRoomView.tsx new file mode 100644 index 0000000..c9bd10c --- /dev/null +++ b/apps/frontend/src/components/wired/views/actions/WiredActionKickFromRoomView.tsx @@ -0,0 +1,27 @@ +import { FC, useEffect, useState } from 'react'; +import { GetConfiguration, LocalizeText, WiredFurniType } from '../../../../api'; +import { Column, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredActionBaseView } from './WiredActionBaseView'; + +export const WiredActionKickFromRoomView: FC<{}> = props => +{ + const [ message, setMessage ] = useState(''); + const { trigger = null, setStringParam = null } = useWired(); + + const save = () => setStringParam(message); + + useEffect(() => + { + setMessage(trigger.stringData); + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.params.message') } + setMessage(event.target.value) } maxLength={ GetConfiguration('wired.action.kick.from.room.max.length', 100) } /> + + + ); +} diff --git a/apps/frontend/src/components/wired/views/actions/WiredActionLayoutView.tsx b/apps/frontend/src/components/wired/views/actions/WiredActionLayoutView.tsx new file mode 100644 index 0000000..f43666a --- /dev/null +++ b/apps/frontend/src/components/wired/views/actions/WiredActionLayoutView.tsx @@ -0,0 +1,85 @@ +import { WiredActionLayoutCode } from '../../../../api'; +import { WiredActionBotChangeFigureView } from './WiredActionBotChangeFigureView'; +import { WiredActionBotFollowAvatarView } from './WiredActionBotFollowAvatarView'; +import { WiredActionBotGiveHandItemView } from './WiredActionBotGiveHandItemView'; +import { WiredActionBotMoveView } from './WiredActionBotMoveView'; +import { WiredActionBotTalkToAvatarView } from './WiredActionBotTalkToAvatarView'; +import { WiredActionBotTalkView } from './WiredActionBotTalkView'; +import { WiredActionBotTeleportView } from './WiredActionBotTeleportView'; +import { WiredActionCallAnotherStackView } from './WiredActionCallAnotherStackView'; +import { WiredActionChaseView } from './WiredActionChaseView'; +import { WiredActionChatView } from './WiredActionChatView'; +import { WiredActionFleeView } from './WiredActionFleeView'; +import { WiredActionGiveRewardView } from './WiredActionGiveRewardView'; +import { WiredActionGiveScoreToPredefinedTeamView } from './WiredActionGiveScoreToPredefinedTeamView'; +import { WiredActionGiveScoreView } from './WiredActionGiveScoreView'; +import { WiredActionJoinTeamView } from './WiredActionJoinTeamView'; +import { WiredActionKickFromRoomView } from './WiredActionKickFromRoomView'; +import { WiredActionLeaveTeamView } from './WiredActionLeaveTeamView'; +import { WiredActionMoveAndRotateFurniView } from './WiredActionMoveAndRotateFurniView'; +import { WiredActionMoveFurniToView } from './WiredActionMoveFurniToView'; +import { WiredActionMoveFurniView } from './WiredActionMoveFurniView'; +import { WiredActionMuteUserView } from './WiredActionMuteUserView'; +import { WiredActionResetView } from './WiredActionResetView'; +import { WiredActionSetFurniStateToView } from './WiredActionSetFurniStateToView'; +import { WiredActionTeleportView } from './WiredActionTeleportView'; +import { WiredActionToggleFurniStateView } from './WiredActionToggleFurniStateView'; + +export const WiredActionLayoutView = (code: number) => +{ + switch(code) + { + case WiredActionLayoutCode.BOT_CHANGE_FIGURE: + return ; + case WiredActionLayoutCode.BOT_FOLLOW_AVATAR: + return ; + case WiredActionLayoutCode.BOT_GIVE_HAND_ITEM: + return ; + case WiredActionLayoutCode.BOT_MOVE: + return ; + case WiredActionLayoutCode.BOT_TALK: + return ; + case WiredActionLayoutCode.BOT_TALK_DIRECT_TO_AVTR: + return ; + case WiredActionLayoutCode.BOT_TELEPORT: + return ; + case WiredActionLayoutCode.CALL_ANOTHER_STACK: + return ; + case WiredActionLayoutCode.CHASE: + return ; + case WiredActionLayoutCode.CHAT: + return ; + case WiredActionLayoutCode.FLEE: + return ; + case WiredActionLayoutCode.GIVE_REWARD: + return ; + case WiredActionLayoutCode.GIVE_SCORE: + return ; + case WiredActionLayoutCode.GIVE_SCORE_TO_PREDEFINED_TEAM: + return ; + case WiredActionLayoutCode.JOIN_TEAM: + return ; + case WiredActionLayoutCode.KICK_FROM_ROOM: + return ; + case WiredActionLayoutCode.LEAVE_TEAM: + return ; + case WiredActionLayoutCode.MOVE_FURNI: + return ; + case WiredActionLayoutCode.MOVE_AND_ROTATE_FURNI: + return ; + case WiredActionLayoutCode.MOVE_FURNI_TO: + return ; + case WiredActionLayoutCode.MUTE_USER: + return ; + case WiredActionLayoutCode.RESET: + return ; + case WiredActionLayoutCode.SET_FURNI_STATE: + return ; + case WiredActionLayoutCode.TELEPORT: + return ; + case WiredActionLayoutCode.TOGGLE_FURNI_STATE: + return ; + } + + return null; +} diff --git a/apps/frontend/src/components/wired/views/actions/WiredActionLeaveTeamView.tsx b/apps/frontend/src/components/wired/views/actions/WiredActionLeaveTeamView.tsx new file mode 100644 index 0000000..eaa834f --- /dev/null +++ b/apps/frontend/src/components/wired/views/actions/WiredActionLeaveTeamView.tsx @@ -0,0 +1,8 @@ +import { FC } from 'react'; +import { WiredFurniType } from '../../../../api'; +import { WiredActionBaseView } from './WiredActionBaseView'; + +export const WiredActionLeaveTeamView: FC<{}> = props => +{ + return ; +} diff --git a/apps/frontend/src/components/wired/views/actions/WiredActionMoveAndRotateFurniView.tsx b/apps/frontend/src/components/wired/views/actions/WiredActionMoveAndRotateFurniView.tsx new file mode 100644 index 0000000..62801af --- /dev/null +++ b/apps/frontend/src/components/wired/views/actions/WiredActionMoveAndRotateFurniView.tsx @@ -0,0 +1,82 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Column, Flex, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredActionBaseView } from './WiredActionBaseView'; + +const directionOptions: { value: number, icon: string }[] = [ + { + value: 0, + icon: 'ne' + }, + { + value: 2, + icon: 'se' + }, + { + value: 4, + icon: 'sw' + }, + { + value: 6, + icon: 'nw' + } +]; + +const rotationOptions: number[] = [ 0, 1, 2, 3, 4, 5, 6 ]; + +export const WiredActionMoveAndRotateFurniView: FC<{}> = props => +{ + const [ movement, setMovement ] = useState(-1); + const [ rotation, setRotation ] = useState(-1); + const { trigger = null, setIntParams = null } = useWired(); + + const save = () => setIntParams([ movement, rotation ]); + + useEffect(() => + { + if(trigger.intData.length >= 2) + { + setMovement(trigger.intData[0]); + setRotation(trigger.intData[1]); + } + else + { + setMovement(-1); + setRotation(-1); + } + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.params.startdir') } + + { directionOptions.map(option => + { + return ( + + setMovement(option.value) } /> + + + + + ) + }) } + + + + { LocalizeText('wiredfurni.params.turn') } + { rotationOptions.map(option => + { + return ( + + setRotation(option) } /> + { LocalizeText(`wiredfurni.params.turn.${ option }`) } + + ) + }) } + + + ); +} diff --git a/apps/frontend/src/components/wired/views/actions/WiredActionMoveFurniToView.tsx b/apps/frontend/src/components/wired/views/actions/WiredActionMoveFurniToView.tsx new file mode 100644 index 0000000..56bdb6b --- /dev/null +++ b/apps/frontend/src/components/wired/views/actions/WiredActionMoveFurniToView.tsx @@ -0,0 +1,76 @@ +import { FC, useEffect, useState } from 'react'; +import ReactSlider from 'react-slider'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Column, Flex, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredActionBaseView } from './WiredActionBaseView'; + +const directionOptions: { value: number, icon: string }[] = [ + { + value: 0, + icon: 'ne' + }, + { + value: 2, + icon: 'se' + }, + { + value: 4, + icon: 'sw' + }, + { + value: 6, + icon: 'nw' + } +]; + +export const WiredActionMoveFurniToView: FC<{}> = props => +{ + const [ spacing, setSpacing ] = useState(-1); + const [ movement, setMovement ] = useState(-1); + const { trigger = null, setIntParams = null } = useWired(); + + const save = () => setIntParams([ movement, spacing ]); + + useEffect(() => + { + if(trigger.intData.length >= 2) + { + setSpacing(trigger.intData[1]); + setMovement(trigger.intData[0]); + } + else + { + setSpacing(-1); + setMovement(-1); + } + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.params.emptytiles', [ 'tiles' ], [ spacing.toString() ]) } + setSpacing(event) } /> + + + { LocalizeText('wiredfurni.params.startdir') } + + { directionOptions.map(value => + { + return ( + + setMovement(value.value) } /> + + + ) + }) } + + + + ); +} diff --git a/apps/frontend/src/components/wired/views/actions/WiredActionMoveFurniView.tsx b/apps/frontend/src/components/wired/views/actions/WiredActionMoveFurniView.tsx new file mode 100644 index 0000000..b5a0888 --- /dev/null +++ b/apps/frontend/src/components/wired/views/actions/WiredActionMoveFurniView.tsx @@ -0,0 +1,100 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Column, Flex, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredActionBaseView } from './WiredActionBaseView'; + +const directionOptions: { value: number, icon: string }[] = [ + { + value: 4, + icon: 'ne' + }, + { + value: 5, + icon: 'se' + }, + { + value: 6, + icon: 'sw' + }, + { + value: 7, + icon: 'nw' + }, + { + value: 2, + icon: 'mv-2' + }, + { + value: 3, + icon: 'mv-3' + }, + { + value: 1, + icon: 'mv-1' + } +]; + +const rotationOptions: number[] = [ 0, 1, 2, 3 ]; + +export const WiredActionMoveFurniView: FC<{}> = props => +{ + const [ movement, setMovement ] = useState(-1); + const [ rotation, setRotation ] = useState(-1); + const { trigger = null, setIntParams = null } = useWired(); + + const save = () => setIntParams([ movement, rotation ]); + + useEffect(() => + { + if(trigger.intData.length >= 2) + { + setMovement(trigger.intData[0]); + setRotation(trigger.intData[1]); + } + else + { + setMovement(-1); + setRotation(-1); + } + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.params.movefurni') } + + setMovement(0) } /> + { LocalizeText('wiredfurni.params.movefurni.0') } + + + { directionOptions.map(option => + { + return ( + + setMovement(option.value) } /> + + + ) + }) } +
+ + + + { LocalizeText('wiredfurni.params.rotatefurni') } + { rotationOptions.map(option => + { + return ( + + setRotation(option) } /> + + { [ 1, 2 ].includes(option) && } + { LocalizeText(`wiredfurni.params.rotatefurni.${ option }`) } + + + ) + }) } + + + ); +} diff --git a/apps/frontend/src/components/wired/views/actions/WiredActionMuteUserView.tsx b/apps/frontend/src/components/wired/views/actions/WiredActionMuteUserView.tsx new file mode 100644 index 0000000..1665036 --- /dev/null +++ b/apps/frontend/src/components/wired/views/actions/WiredActionMuteUserView.tsx @@ -0,0 +1,43 @@ +import { FC, useEffect, useState } from 'react'; +import ReactSlider from 'react-slider'; +import { GetConfiguration, LocalizeText, WiredFurniType } from '../../../../api'; +import { Column, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredActionBaseView } from './WiredActionBaseView'; + +export const WiredActionMuteUserView: FC<{}> = props => +{ + const [ time, setTime ] = useState(-1); + const [ message, setMessage ] = useState(''); + const { trigger = null, setIntParams = null, setStringParam = null } = useWired(); + + const save = () => + { + setIntParams([ time ]); + setStringParam(message); + } + + useEffect(() => + { + setTime((trigger.intData.length > 0) ? trigger.intData[0] : 0); + setMessage(trigger.stringData); + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.params.length.minutes', [ 'minutes' ], [ time.toString() ]) } + setTime(event) } /> + + + { LocalizeText('wiredfurni.params.message') } + setMessage(event.target.value) } maxLength={ GetConfiguration('wired.action.mute.user.max.length', 100) } /> + + + ); +} diff --git a/apps/frontend/src/components/wired/views/actions/WiredActionResetView.tsx b/apps/frontend/src/components/wired/views/actions/WiredActionResetView.tsx new file mode 100644 index 0000000..9245c67 --- /dev/null +++ b/apps/frontend/src/components/wired/views/actions/WiredActionResetView.tsx @@ -0,0 +1,8 @@ +import { FC } from 'react'; +import { WiredFurniType } from '../../../../api'; +import { WiredActionBaseView } from './WiredActionBaseView'; + +export const WiredActionResetView: FC<{}> = props => +{ + return ; +} diff --git a/apps/frontend/src/components/wired/views/actions/WiredActionSetFurniStateToView.tsx b/apps/frontend/src/components/wired/views/actions/WiredActionSetFurniStateToView.tsx new file mode 100644 index 0000000..587254c --- /dev/null +++ b/apps/frontend/src/components/wired/views/actions/WiredActionSetFurniStateToView.tsx @@ -0,0 +1,42 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Column, Flex, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredActionBaseView } from './WiredActionBaseView'; + +export const WiredActionSetFurniStateToView: FC<{}> = props => +{ + const [ stateFlag, setStateFlag ] = useState(0); + const [ directionFlag, setDirectionFlag ] = useState(0); + const [ positionFlag, setPositionFlag ] = useState(0); + const { trigger = null, setIntParams = null } = useWired(); + + const save = () => setIntParams([ stateFlag, directionFlag, positionFlag ]); + + useEffect(() => + { + setStateFlag(trigger.getBoolean(0) ? 1 : 0); + setDirectionFlag(trigger.getBoolean(1) ? 1 : 0); + setPositionFlag(trigger.getBoolean(2) ? 1 : 0); + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.params.conditions') } + + setStateFlag(event.target.checked ? 1 : 0) } /> + { LocalizeText('wiredfurni.params.condition.state') } + + + setDirectionFlag(event.target.checked ? 1 : 0) } /> + { LocalizeText('wiredfurni.params.condition.direction') } + + + setPositionFlag(event.target.checked ? 1 : 0) } /> + { LocalizeText('wiredfurni.params.condition.position') } + + + + ); +} diff --git a/apps/frontend/src/components/wired/views/actions/WiredActionTeleportView.tsx b/apps/frontend/src/components/wired/views/actions/WiredActionTeleportView.tsx new file mode 100644 index 0000000..652da61 --- /dev/null +++ b/apps/frontend/src/components/wired/views/actions/WiredActionTeleportView.tsx @@ -0,0 +1,8 @@ +import { FC } from 'react'; +import { WiredFurniType } from '../../../../api'; +import { WiredActionBaseView } from './WiredActionBaseView'; + +export const WiredActionTeleportView: FC<{}> = props => +{ + return ; +} diff --git a/apps/frontend/src/components/wired/views/actions/WiredActionToggleFurniStateView.tsx b/apps/frontend/src/components/wired/views/actions/WiredActionToggleFurniStateView.tsx new file mode 100644 index 0000000..37b5d2e --- /dev/null +++ b/apps/frontend/src/components/wired/views/actions/WiredActionToggleFurniStateView.tsx @@ -0,0 +1,8 @@ +import { FC } from 'react'; +import { WiredFurniType } from '../../../../api'; +import { WiredActionBaseView } from './WiredActionBaseView'; + +export const WiredActionToggleFurniStateView: FC<{}> = props => +{ + return ; +} diff --git a/apps/frontend/src/components/wired/views/conditions/WiredConditionActorHasHandItem.tsx b/apps/frontend/src/components/wired/views/conditions/WiredConditionActorHasHandItem.tsx new file mode 100644 index 0000000..24e89ed --- /dev/null +++ b/apps/frontend/src/components/wired/views/conditions/WiredConditionActorHasHandItem.tsx @@ -0,0 +1,34 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Column, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +const ALLOWED_HAND_ITEM_IDS: number[] = [ 2, 5, 7, 8, 9, 10, 27 ]; + +export const WiredConditionActorHasHandItemView: FC<{}> = props => +{ + const [ handItemId, setHandItemId ] = useState(-1); + const { trigger = null, setIntParams = null } = useWired(); + + const save = () => setIntParams([ handItemId ]); + + useEffect(() => + { + setHandItemId((trigger.intData.length > 0) ? trigger.intData[0] : 0); + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.params.handitem') } + + + + ); +} diff --git a/apps/frontend/src/components/wired/views/conditions/WiredConditionActorIsGroupMemberView.tsx b/apps/frontend/src/components/wired/views/conditions/WiredConditionActorIsGroupMemberView.tsx new file mode 100644 index 0000000..e278eab --- /dev/null +++ b/apps/frontend/src/components/wired/views/conditions/WiredConditionActorIsGroupMemberView.tsx @@ -0,0 +1,8 @@ +import { FC } from 'react'; +import { WiredFurniType } from '../../../../api'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +export const WiredConditionActorIsGroupMemberView: FC<{}> = props => +{ + return ; +} diff --git a/apps/frontend/src/components/wired/views/conditions/WiredConditionActorIsOnFurniView.tsx b/apps/frontend/src/components/wired/views/conditions/WiredConditionActorIsOnFurniView.tsx new file mode 100644 index 0000000..10cddf5 --- /dev/null +++ b/apps/frontend/src/components/wired/views/conditions/WiredConditionActorIsOnFurniView.tsx @@ -0,0 +1,8 @@ +import { FC } from 'react'; +import { WiredFurniType } from '../../../../api'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +export const WiredConditionActorIsOnFurniView: FC<{}> = props => +{ + return ; +} diff --git a/apps/frontend/src/components/wired/views/conditions/WiredConditionActorIsTeamMemberView.tsx b/apps/frontend/src/components/wired/views/conditions/WiredConditionActorIsTeamMemberView.tsx new file mode 100644 index 0000000..20db3ac --- /dev/null +++ b/apps/frontend/src/components/wired/views/conditions/WiredConditionActorIsTeamMemberView.tsx @@ -0,0 +1,37 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Column, Flex, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +const teamIds: number[] = [ 1, 2, 3, 4 ]; + +export const WiredConditionActorIsTeamMemberView: FC<{}> = props => +{ + const [ selectedTeam, setSelectedTeam ] = useState(-1); + const { trigger = null, setIntParams = null } = useWired(); + + const save = () => setIntParams([ selectedTeam ]); + + useEffect(() => + { + setSelectedTeam((trigger.intData.length > 0) ? trigger.intData[0] : 0); + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.params.team') } + { teamIds.map(value => + { + return ( + + setSelectedTeam(value) } /> + { LocalizeText(`wiredfurni.params.team.${ value }`) } + + ) + }) } + + + ); +} diff --git a/apps/frontend/src/components/wired/views/conditions/WiredConditionActorIsWearingBadgeView.tsx b/apps/frontend/src/components/wired/views/conditions/WiredConditionActorIsWearingBadgeView.tsx new file mode 100644 index 0000000..01e6d3b --- /dev/null +++ b/apps/frontend/src/components/wired/views/conditions/WiredConditionActorIsWearingBadgeView.tsx @@ -0,0 +1,27 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Column, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +export const WiredConditionActorIsWearingBadgeView: FC<{}> = props => +{ + const [ badge, setBadge ] = useState(''); + const { trigger = null, setStringParam = null } = useWired(); + + const save = () => setStringParam(badge); + + useEffect(() => + { + setBadge(trigger.stringData); + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.params.badgecode') } + setBadge(event.target.value) } /> + + + ); +} diff --git a/apps/frontend/src/components/wired/views/conditions/WiredConditionActorIsWearingEffectView.tsx b/apps/frontend/src/components/wired/views/conditions/WiredConditionActorIsWearingEffectView.tsx new file mode 100644 index 0000000..00d7fa6 --- /dev/null +++ b/apps/frontend/src/components/wired/views/conditions/WiredConditionActorIsWearingEffectView.tsx @@ -0,0 +1,27 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Column, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +export const WiredConditionActorIsWearingEffectView: FC<{}> = props => +{ + const [ effect, setEffect ] = useState(-1); + const { trigger = null, setIntParams = null } = useWired(); + + const save = () => setIntParams([ effect ]); + + useEffect(() => + { + setEffect(trigger?.intData[0] ?? 0); + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.tooltip.effectid') } + setEffect(parseInt(event.target.value)) } /> + + + ); +} diff --git a/apps/frontend/src/components/wired/views/conditions/WiredConditionBaseView.tsx b/apps/frontend/src/components/wired/views/conditions/WiredConditionBaseView.tsx new file mode 100644 index 0000000..fc49215 --- /dev/null +++ b/apps/frontend/src/components/wired/views/conditions/WiredConditionBaseView.tsx @@ -0,0 +1,23 @@ +import { FC, PropsWithChildren } from 'react'; +import { WiredFurniType } from '../../../../api'; +import { WiredBaseView } from '../WiredBaseView'; + +export interface WiredConditionBaseViewProps +{ + hasSpecialInput: boolean; + requiresFurni: number; + save: () => void; +} + +export const WiredConditionBaseView: FC> = props => +{ + const { requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_NONE, save = null, hasSpecialInput = false, children = null } = props; + + const onSave = () => (save && save()); + + return ( + + { children } + + ); +} diff --git a/apps/frontend/src/components/wired/views/conditions/WiredConditionDateRangeView.tsx b/apps/frontend/src/components/wired/views/conditions/WiredConditionDateRangeView.tsx new file mode 100644 index 0000000..36832e1 --- /dev/null +++ b/apps/frontend/src/components/wired/views/conditions/WiredConditionDateRangeView.tsx @@ -0,0 +1,58 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredDateToString, WiredFurniType } from '../../../../api'; +import { Column, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +export const WiredConditionDateRangeView: FC<{}> = props => +{ + const [ startDate, setStartDate ] = useState(''); + const [ endDate, setEndDate ] = useState(''); + const { trigger = null, setIntParams = null } = useWired(); + + const save = () => + { + let startDateMili = 0; + let endDateMili = 0; + + const startDateInstance = new Date(startDate); + const endDateInstance = new Date(endDate); + + if(startDateInstance && endDateInstance) + { + startDateMili = startDateInstance.getTime() / 1000; + endDateMili = endDateInstance.getTime() / 1000; + } + + setIntParams([ startDateMili, endDateMili ]); + } + + useEffect(() => + { + if(trigger.intData.length >= 2) + { + let startDate = new Date(); + let endDate = new Date(); + + if(trigger.intData[0] > 0) startDate = new Date((trigger.intData[0] * 1000)); + + if(trigger.intData[1] > 0) endDate = new Date((trigger.intData[1] * 1000)); + + setStartDate(WiredDateToString(startDate)); + setEndDate(WiredDateToString(endDate)); + } + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.params.startdate') } + setStartDate(e.target.value) } /> + + + { LocalizeText('wiredfurni.params.enddate') } + setEndDate(e.target.value) } /> + + + ); +} diff --git a/apps/frontend/src/components/wired/views/conditions/WiredConditionFurniHasAvatarOnView.tsx b/apps/frontend/src/components/wired/views/conditions/WiredConditionFurniHasAvatarOnView.tsx new file mode 100644 index 0000000..aa3ba14 --- /dev/null +++ b/apps/frontend/src/components/wired/views/conditions/WiredConditionFurniHasAvatarOnView.tsx @@ -0,0 +1,8 @@ +import { FC } from 'react'; +import { WiredFurniType } from '../../../../api'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +export const WiredConditionFurniHasAvatarOnView: FC<{}> = props => +{ + return ; +} diff --git a/apps/frontend/src/components/wired/views/conditions/WiredConditionFurniHasFurniOnView.tsx b/apps/frontend/src/components/wired/views/conditions/WiredConditionFurniHasFurniOnView.tsx new file mode 100644 index 0000000..1a9d0ef --- /dev/null +++ b/apps/frontend/src/components/wired/views/conditions/WiredConditionFurniHasFurniOnView.tsx @@ -0,0 +1,35 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Column, Flex, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +export const WiredConditionFurniHasFurniOnView: FC<{}> = props => +{ + const [ requireAll, setRequireAll ] = useState(-1); + const { trigger = null, setIntParams = null } = useWired(); + + const save = () => setIntParams([ requireAll ]); + + useEffect(() => + { + setRequireAll((trigger.intData.length > 0) ? trigger.intData[0] : 0); + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.params.requireall') } + { [ 0, 1 ].map(value => + { + return ( + + setRequireAll(value) } /> + { LocalizeText('wiredfurni.params.requireall.' + value) } + + ) + }) } + + + ); +} diff --git a/apps/frontend/src/components/wired/views/conditions/WiredConditionFurniHasNotFurniOnView.tsx b/apps/frontend/src/components/wired/views/conditions/WiredConditionFurniHasNotFurniOnView.tsx new file mode 100644 index 0000000..031ec08 --- /dev/null +++ b/apps/frontend/src/components/wired/views/conditions/WiredConditionFurniHasNotFurniOnView.tsx @@ -0,0 +1,35 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Column, Flex, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +export const WiredConditionFurniHasNotFurniOnView: FC<{}> = props => +{ + const [ requireAll, setRequireAll ] = useState(-1); + const { trigger = null, setIntParams = null } = useWired(); + + const save = () => setIntParams([ requireAll ]); + + useEffect(() => + { + setRequireAll((trigger.intData.length > 0) ? trigger.intData[0] : 0); + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.params.not_requireall') } + { [ 0, 1 ].map(value => + { + return ( + + setRequireAll(value) } /> + { LocalizeText(`wiredfurni.params.not_requireall.${ value }`) } + + ) + }) } + + + ); +} diff --git a/apps/frontend/src/components/wired/views/conditions/WiredConditionFurniIsOfTypeView.tsx b/apps/frontend/src/components/wired/views/conditions/WiredConditionFurniIsOfTypeView.tsx new file mode 100644 index 0000000..2b0ddf4 --- /dev/null +++ b/apps/frontend/src/components/wired/views/conditions/WiredConditionFurniIsOfTypeView.tsx @@ -0,0 +1,8 @@ +import { FC } from 'react'; +import { WiredFurniType } from '../../../../api'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +export const WiredConditionFurniIsOfTypeView: FC<{}> = props => +{ + return ; +} diff --git a/apps/frontend/src/components/wired/views/conditions/WiredConditionFurniMatchesSnapshotView.tsx b/apps/frontend/src/components/wired/views/conditions/WiredConditionFurniMatchesSnapshotView.tsx new file mode 100644 index 0000000..47a555e --- /dev/null +++ b/apps/frontend/src/components/wired/views/conditions/WiredConditionFurniMatchesSnapshotView.tsx @@ -0,0 +1,42 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Column, Flex, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +export const WiredConditionFurniMatchesSnapshotView: FC<{}> = props => +{ + const [ stateFlag, setStateFlag ] = useState(0); + const [ directionFlag, setDirectionFlag ] = useState(0); + const [ positionFlag, setPositionFlag ] = useState(0); + const { trigger = null, setIntParams = null } = useWired(); + + const save = () => setIntParams([ stateFlag, directionFlag, positionFlag ]); + + useEffect(() => + { + setStateFlag(trigger.getBoolean(0) ? 1 : 0); + setDirectionFlag(trigger.getBoolean(1) ? 1 : 0); + setPositionFlag(trigger.getBoolean(2) ? 1 : 0); + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.params.conditions') } + + setStateFlag(event.target.checked ? 1 : 0) } /> + { LocalizeText('wiredfurni.params.condition.state') } + + + setDirectionFlag(event.target.checked ? 1 : 0) } /> + { LocalizeText('wiredfurni.params.condition.direction') } + + + setPositionFlag(event.target.checked ? 1 : 0) } /> + { LocalizeText('wiredfurni.params.condition.position') } + + + + ); +} diff --git a/apps/frontend/src/components/wired/views/conditions/WiredConditionLayoutView.tsx b/apps/frontend/src/components/wired/views/conditions/WiredConditionLayoutView.tsx new file mode 100644 index 0000000..c7c49b2 --- /dev/null +++ b/apps/frontend/src/components/wired/views/conditions/WiredConditionLayoutView.tsx @@ -0,0 +1,64 @@ +import { WiredConditionlayout } from '../../../../api'; +import { WiredConditionActorHasHandItemView } from './WiredConditionActorHasHandItem'; +import { WiredConditionActorIsGroupMemberView } from './WiredConditionActorIsGroupMemberView'; +import { WiredConditionActorIsOnFurniView } from './WiredConditionActorIsOnFurniView'; +import { WiredConditionActorIsTeamMemberView } from './WiredConditionActorIsTeamMemberView'; +import { WiredConditionActorIsWearingBadgeView } from './WiredConditionActorIsWearingBadgeView'; +import { WiredConditionActorIsWearingEffectView } from './WiredConditionActorIsWearingEffectView'; +import { WiredConditionDateRangeView } from './WiredConditionDateRangeView'; +import { WiredConditionFurniHasAvatarOnView } from './WiredConditionFurniHasAvatarOnView'; +import { WiredConditionFurniHasFurniOnView } from './WiredConditionFurniHasFurniOnView'; +import { WiredConditionFurniHasNotFurniOnView } from './WiredConditionFurniHasNotFurniOnView'; +import { WiredConditionFurniIsOfTypeView } from './WiredConditionFurniIsOfTypeView'; +import { WiredConditionFurniMatchesSnapshotView } from './WiredConditionFurniMatchesSnapshotView'; +import { WiredConditionTimeElapsedLessView } from './WiredConditionTimeElapsedLessView'; +import { WiredConditionTimeElapsedMoreView } from './WiredConditionTimeElapsedMoreView'; +import { WiredConditionUserCountInRoomView } from './WiredConditionUserCountInRoomView'; + +export const WiredConditionLayoutView = (code: number) => +{ + switch(code) + { + case WiredConditionlayout.ACTOR_HAS_HANDITEM: + return ; + case WiredConditionlayout.ACTOR_IS_GROUP_MEMBER: + case WiredConditionlayout.NOT_ACTOR_IN_GROUP: + return ; + case WiredConditionlayout.ACTOR_IS_ON_FURNI: + case WiredConditionlayout.NOT_ACTOR_ON_FURNI: + return ; + case WiredConditionlayout.ACTOR_IS_IN_TEAM: + case WiredConditionlayout.NOT_ACTOR_IN_TEAM: + return ; + case WiredConditionlayout.ACTOR_IS_WEARING_BADGE: + case WiredConditionlayout.NOT_ACTOR_WEARS_BADGE: + return ; + case WiredConditionlayout.ACTOR_IS_WEARING_EFFECT: + case WiredConditionlayout.NOT_ACTOR_WEARING_EFFECT: + return ; + case WiredConditionlayout.DATE_RANGE_ACTIVE: + return ; + case WiredConditionlayout.FURNIS_HAVE_AVATARS: + case WiredConditionlayout.FURNI_NOT_HAVE_HABBO: + return ; + case WiredConditionlayout.HAS_STACKED_FURNIS: + return ; + case WiredConditionlayout.NOT_HAS_STACKED_FURNIS: + return ; + case WiredConditionlayout.STUFF_TYPE_MATCHES: + case WiredConditionlayout.NOT_FURNI_IS_OF_TYPE: + return ; + case WiredConditionlayout.STATES_MATCH: + case WiredConditionlayout.NOT_STATES_MATCH: + return ; + case WiredConditionlayout.TIME_ELAPSED_LESS: + return ; + case WiredConditionlayout.TIME_ELAPSED_MORE: + return ; + case WiredConditionlayout.USER_COUNT_IN: + case WiredConditionlayout.NOT_USER_COUNT_IN: + return ; + } + + return null; +} diff --git a/apps/frontend/src/components/wired/views/conditions/WiredConditionTimeElapsedLessView.tsx b/apps/frontend/src/components/wired/views/conditions/WiredConditionTimeElapsedLessView.tsx new file mode 100644 index 0000000..31d6d79 --- /dev/null +++ b/apps/frontend/src/components/wired/views/conditions/WiredConditionTimeElapsedLessView.tsx @@ -0,0 +1,33 @@ +import { FC, useEffect, useState } from 'react'; +import ReactSlider from 'react-slider'; +import { GetWiredTimeLocale, LocalizeText, WiredFurniType } from '../../../../api'; +import { Column, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +export const WiredConditionTimeElapsedLessView: FC<{}> = props => +{ + const [ time, setTime ] = useState(-1); + const { trigger = null, setIntParams = null } = useWired(); + + const save = () => setIntParams([ time ]); + + useEffect(() => + { + setTime((trigger.intData.length > 0) ? trigger.intData[0] : 0); + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.params.allowbefore', [ 'seconds' ], [ GetWiredTimeLocale(time) ]) } + setTime(event) } /> + + + ); +} diff --git a/apps/frontend/src/components/wired/views/conditions/WiredConditionTimeElapsedMoreView.tsx b/apps/frontend/src/components/wired/views/conditions/WiredConditionTimeElapsedMoreView.tsx new file mode 100644 index 0000000..cb158e6 --- /dev/null +++ b/apps/frontend/src/components/wired/views/conditions/WiredConditionTimeElapsedMoreView.tsx @@ -0,0 +1,33 @@ +import { FC, useEffect, useState } from 'react'; +import ReactSlider from 'react-slider'; +import { GetWiredTimeLocale, LocalizeText, WiredFurniType } from '../../../../api'; +import { Column, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +export const WiredConditionTimeElapsedMoreView: FC<{}> = props => +{ + const [ time, setTime ] = useState(-1); + const { trigger = null, setIntParams = null } = useWired(); + + const save = () => setIntParams([ time ]); + + useEffect(() => + { + setTime((trigger.intData.length > 0) ? trigger.intData[0] : 0); + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.params.allowafter', [ 'seconds' ], [ GetWiredTimeLocale(time) ]) } + setTime(event) } /> + + + ); +} diff --git a/apps/frontend/src/components/wired/views/conditions/WiredConditionUserCountInRoomView.tsx b/apps/frontend/src/components/wired/views/conditions/WiredConditionUserCountInRoomView.tsx new file mode 100644 index 0000000..824d7ae --- /dev/null +++ b/apps/frontend/src/components/wired/views/conditions/WiredConditionUserCountInRoomView.tsx @@ -0,0 +1,52 @@ +import { FC, useEffect, useState } from 'react'; +import ReactSlider from 'react-slider'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Column, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredConditionBaseView } from './WiredConditionBaseView'; + +export const WiredConditionUserCountInRoomView: FC<{}> = props => +{ + const [ min, setMin ] = useState(1); + const [ max, setMax ] = useState(1); + const { trigger = null, setIntParams = null } = useWired(); + + const save = () => setIntParams([ min, max ]); + + useEffect(() => + { + if(trigger.intData.length >= 2) + { + setMin(trigger.intData[0]); + setMax(trigger.intData[1]); + } + else + { + setMin(1); + setMax(1); + } + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.params.usercountmin', [ 'value' ], [ min.toString() ]) } + setMin(event) } /> + + + { LocalizeText('wiredfurni.params.usercountmax', [ 'value' ], [ max.toString() ]) } + setMax(event) } /> + + + ); +} diff --git a/apps/frontend/src/components/wired/views/triggers/WiredTriggerAvatarEnterRoomView.tsx b/apps/frontend/src/components/wired/views/triggers/WiredTriggerAvatarEnterRoomView.tsx new file mode 100644 index 0000000..b14eafb --- /dev/null +++ b/apps/frontend/src/components/wired/views/triggers/WiredTriggerAvatarEnterRoomView.tsx @@ -0,0 +1,38 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Column, Flex, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredTriggerBaseView } from './WiredTriggerBaseView'; + +export const WiredTriggerAvatarEnterRoomView: FC<{}> = props => +{ + const [ username, setUsername ] = useState(''); + const [ avatarMode, setAvatarMode ] = useState(0); + const { trigger = null, setStringParam = null } = useWired(); + + const save = () => setStringParam((avatarMode === 1) ? username : ''); + + useEffect(() => + { + setUsername(trigger.stringData); + setAvatarMode(trigger.stringData ? 1 : 0); + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.params.picktriggerer') } + + setAvatarMode(0) } /> + { LocalizeText('wiredfurni.params.anyavatar') } + + + setAvatarMode(1) } /> + { LocalizeText('wiredfurni.params.certainavatar') } + + { (avatarMode === 1) && + setUsername(event.target.value) } /> } + + + ); +} diff --git a/apps/frontend/src/components/wired/views/triggers/WiredTriggerAvatarSaysSomethingView.tsx b/apps/frontend/src/components/wired/views/triggers/WiredTriggerAvatarSaysSomethingView.tsx new file mode 100644 index 0000000..d8bc621 --- /dev/null +++ b/apps/frontend/src/components/wired/views/triggers/WiredTriggerAvatarSaysSomethingView.tsx @@ -0,0 +1,44 @@ +import { FC, useEffect, useState } from 'react'; +import { GetSessionDataManager, LocalizeText, WiredFurniType } from '../../../../api'; +import { Column, Flex, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredTriggerBaseView } from './WiredTriggerBaseView'; + +export const WiredTriggerAvatarSaysSomethingView: FC<{}> = props => +{ + const [ message, setMessage ] = useState(''); + const [ triggererAvatar, setTriggererAvatar ] = useState(-1); + const { trigger = null, setStringParam = null, setIntParams = null } = useWired(); + + const save = () => + { + setStringParam(message); + setIntParams([ triggererAvatar ]); + } + + useEffect(() => + { + setMessage(trigger.stringData); + setTriggererAvatar((trigger.intData.length > 0) ? trigger.intData[0] : 0); + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.params.whatissaid') } + setMessage(event.target.value) } /> + + + { LocalizeText('wiredfurni.params.picktriggerer') } + + setTriggererAvatar(0) } /> + { LocalizeText('wiredfurni.params.anyavatar') } + + + setTriggererAvatar(1) } /> + { GetSessionDataManager().userName } + + + + ); +} diff --git a/apps/frontend/src/components/wired/views/triggers/WiredTriggerAvatarWalksOffFurniView.tsx b/apps/frontend/src/components/wired/views/triggers/WiredTriggerAvatarWalksOffFurniView.tsx new file mode 100644 index 0000000..0b98a56 --- /dev/null +++ b/apps/frontend/src/components/wired/views/triggers/WiredTriggerAvatarWalksOffFurniView.tsx @@ -0,0 +1,8 @@ +import { FC } from 'react'; +import { WiredFurniType } from '../../../../api'; +import { WiredTriggerBaseView } from './WiredTriggerBaseView'; + +export const WiredTriggerAvatarWalksOffFurniView: FC<{}> = props => +{ + return ; +} diff --git a/apps/frontend/src/components/wired/views/triggers/WiredTriggerAvatarWalksOnFurni.tsx b/apps/frontend/src/components/wired/views/triggers/WiredTriggerAvatarWalksOnFurni.tsx new file mode 100644 index 0000000..25572e7 --- /dev/null +++ b/apps/frontend/src/components/wired/views/triggers/WiredTriggerAvatarWalksOnFurni.tsx @@ -0,0 +1,8 @@ +import { FC } from 'react'; +import { WiredFurniType } from '../../../../api'; +import { WiredTriggerBaseView } from './WiredTriggerBaseView'; + +export const WiredTriggerAvatarWalksOnFurniView: FC<{}> = props => +{ + return ; +} diff --git a/apps/frontend/src/components/wired/views/triggers/WiredTriggerBaseView.tsx b/apps/frontend/src/components/wired/views/triggers/WiredTriggerBaseView.tsx new file mode 100644 index 0000000..f8f1575 --- /dev/null +++ b/apps/frontend/src/components/wired/views/triggers/WiredTriggerBaseView.tsx @@ -0,0 +1,23 @@ +import { FC, PropsWithChildren } from 'react'; +import { WiredFurniType } from '../../../../api'; +import { WiredBaseView } from '../WiredBaseView'; + +export interface WiredTriggerBaseViewProps +{ + hasSpecialInput: boolean; + requiresFurni: number; + save: () => void; +} + +export const WiredTriggerBaseView: FC> = props => +{ + const { requiresFurni = WiredFurniType.STUFF_SELECTION_OPTION_NONE, save = null, hasSpecialInput = false, children = null } = props; + + const onSave = () => (save && save()); + + return ( + + { children } + + ); +} diff --git a/apps/frontend/src/components/wired/views/triggers/WiredTriggerBotReachedAvatarView.tsx b/apps/frontend/src/components/wired/views/triggers/WiredTriggerBotReachedAvatarView.tsx new file mode 100644 index 0000000..6984dc6 --- /dev/null +++ b/apps/frontend/src/components/wired/views/triggers/WiredTriggerBotReachedAvatarView.tsx @@ -0,0 +1,27 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Column, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredTriggerBaseView } from './WiredTriggerBaseView'; + +export const WiredTriggerBotReachedAvatarView: FC<{}> = props => +{ + const [ botName, setBotName ] = useState(''); + const { trigger = null, setStringParam = null } = useWired(); + + const save = () => setStringParam(botName); + + useEffect(() => + { + setBotName(trigger.stringData); + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.params.bot.name') } + setBotName(event.target.value) } /> + + + ); +} diff --git a/apps/frontend/src/components/wired/views/triggers/WiredTriggerBotReachedStuffView.tsx b/apps/frontend/src/components/wired/views/triggers/WiredTriggerBotReachedStuffView.tsx new file mode 100644 index 0000000..9cde173 --- /dev/null +++ b/apps/frontend/src/components/wired/views/triggers/WiredTriggerBotReachedStuffView.tsx @@ -0,0 +1,27 @@ +import { FC, useEffect, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Column, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredTriggerBaseView } from './WiredTriggerBaseView'; + +export const WiredTriggerBotReachedStuffView: FC<{}> = props => +{ + const [ botName, setBotName ] = useState(''); + const { trigger = null, setStringParam = null } = useWired(); + + const save = () => setStringParam(botName); + + useEffect(() => + { + setBotName(trigger.stringData); + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.params.bot.name') } + setBotName(event.target.value) } /> + + + ); +} diff --git a/apps/frontend/src/components/wired/views/triggers/WiredTriggerCollisionView.tsx b/apps/frontend/src/components/wired/views/triggers/WiredTriggerCollisionView.tsx new file mode 100644 index 0000000..be23a6b --- /dev/null +++ b/apps/frontend/src/components/wired/views/triggers/WiredTriggerCollisionView.tsx @@ -0,0 +1,8 @@ +import { FC } from 'react'; +import { WiredFurniType } from '../../../../api'; +import { WiredTriggerBaseView } from './WiredTriggerBaseView'; + +export const WiredTriggerCollisionView: FC<{}> = props => +{ + return ; +} diff --git a/apps/frontend/src/components/wired/views/triggers/WiredTriggerExecuteOnceView.tsx b/apps/frontend/src/components/wired/views/triggers/WiredTriggerExecuteOnceView.tsx new file mode 100644 index 0000000..ea3d61b --- /dev/null +++ b/apps/frontend/src/components/wired/views/triggers/WiredTriggerExecuteOnceView.tsx @@ -0,0 +1,33 @@ +import { FC, useEffect, useState } from 'react'; +import ReactSlider from 'react-slider'; +import { GetWiredTimeLocale, LocalizeText, WiredFurniType } from '../../../../api'; +import { Column, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredTriggerBaseView } from './WiredTriggerBaseView'; + +export const WiredTriggeExecuteOnceView: FC<{}> = props => +{ + const [ time, setTime ] = useState(1); + const { trigger = null, setIntParams = null } = useWired(); + + const save = () => setIntParams([ time ]); + + useEffect(() => + { + setTime((trigger.intData.length > 0) ? trigger.intData[0] : 0); + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.params.settime', [ 'seconds' ], [ GetWiredTimeLocale(time) ]) } + setTime(event) } /> + + + ); +} diff --git a/apps/frontend/src/components/wired/views/triggers/WiredTriggerExecutePeriodicallyLongView.tsx b/apps/frontend/src/components/wired/views/triggers/WiredTriggerExecutePeriodicallyLongView.tsx new file mode 100644 index 0000000..d9bc6bc --- /dev/null +++ b/apps/frontend/src/components/wired/views/triggers/WiredTriggerExecutePeriodicallyLongView.tsx @@ -0,0 +1,34 @@ +import { FriendlyTime } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useState } from 'react'; +import ReactSlider from 'react-slider'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Column, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredTriggerBaseView } from './WiredTriggerBaseView'; + +export const WiredTriggeExecutePeriodicallyLongView: FC<{}> = props => +{ + const [ time, setTime ] = useState(1); + const { trigger = null, setIntParams = null } = useWired(); + + const save = () => setIntParams([ time ]); + + useEffect(() => + { + setTime((trigger.intData.length > 0) ? trigger.intData[0] : 0); + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.params.setlongtime', [ 'time' ], [ FriendlyTime.format(time * 5).toString() ]) } + setTime(event) } /> + + + ); +} diff --git a/apps/frontend/src/components/wired/views/triggers/WiredTriggerExecutePeriodicallyView.tsx b/apps/frontend/src/components/wired/views/triggers/WiredTriggerExecutePeriodicallyView.tsx new file mode 100644 index 0000000..b77e642 --- /dev/null +++ b/apps/frontend/src/components/wired/views/triggers/WiredTriggerExecutePeriodicallyView.tsx @@ -0,0 +1,33 @@ +import { FC, useEffect, useState } from 'react'; +import ReactSlider from 'react-slider'; +import { GetWiredTimeLocale, LocalizeText, WiredFurniType } from '../../../../api'; +import { Column, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredTriggerBaseView } from './WiredTriggerBaseView'; + +export const WiredTriggeExecutePeriodicallyView: FC<{}> = props => +{ + const [ time, setTime ] = useState(1); + const { trigger = null, setIntParams = null } = useWired(); + + const save = () => setIntParams([ time ]); + + useEffect(() => + { + setTime((trigger.intData.length > 0) ? trigger.intData[0] : 0); + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.params.settime', [ 'seconds' ], [ GetWiredTimeLocale(time) ]) } + setTime(event) } /> + + + ); +} diff --git a/apps/frontend/src/components/wired/views/triggers/WiredTriggerGameEndsView.tsx b/apps/frontend/src/components/wired/views/triggers/WiredTriggerGameEndsView.tsx new file mode 100644 index 0000000..c6f1c67 --- /dev/null +++ b/apps/frontend/src/components/wired/views/triggers/WiredTriggerGameEndsView.tsx @@ -0,0 +1,8 @@ +import { FC } from 'react'; +import { WiredFurniType } from '../../../../api'; +import { WiredTriggerBaseView } from './WiredTriggerBaseView'; + +export const WiredTriggerGameEndsView: FC<{}> = props => +{ + return ; +} diff --git a/apps/frontend/src/components/wired/views/triggers/WiredTriggerGameStartsView.tsx b/apps/frontend/src/components/wired/views/triggers/WiredTriggerGameStartsView.tsx new file mode 100644 index 0000000..6e43a2c --- /dev/null +++ b/apps/frontend/src/components/wired/views/triggers/WiredTriggerGameStartsView.tsx @@ -0,0 +1,8 @@ +import { FC } from 'react'; +import { WiredFurniType } from '../../../../api'; +import { WiredTriggerBaseView } from './WiredTriggerBaseView'; + +export const WiredTriggerGameStartsView: FC<{}> = props => +{ + return ; +} diff --git a/apps/frontend/src/components/wired/views/triggers/WiredTriggerLayoutView.tsx b/apps/frontend/src/components/wired/views/triggers/WiredTriggerLayoutView.tsx new file mode 100644 index 0000000..e45b2ea --- /dev/null +++ b/apps/frontend/src/components/wired/views/triggers/WiredTriggerLayoutView.tsx @@ -0,0 +1,52 @@ +import { WiredTriggerLayout } from '../../../../api'; +import { WiredTriggerAvatarEnterRoomView } from './WiredTriggerAvatarEnterRoomView'; +import { WiredTriggerAvatarSaysSomethingView } from './WiredTriggerAvatarSaysSomethingView'; +import { WiredTriggerAvatarWalksOffFurniView } from './WiredTriggerAvatarWalksOffFurniView'; +import { WiredTriggerAvatarWalksOnFurniView } from './WiredTriggerAvatarWalksOnFurni'; +import { WiredTriggerBotReachedAvatarView } from './WiredTriggerBotReachedAvatarView'; +import { WiredTriggerBotReachedStuffView } from './WiredTriggerBotReachedStuffView'; +import { WiredTriggerCollisionView } from './WiredTriggerCollisionView'; +import { WiredTriggeExecuteOnceView } from './WiredTriggerExecuteOnceView'; +import { WiredTriggeExecutePeriodicallyLongView } from './WiredTriggerExecutePeriodicallyLongView'; +import { WiredTriggeExecutePeriodicallyView } from './WiredTriggerExecutePeriodicallyView'; +import { WiredTriggerGameEndsView } from './WiredTriggerGameEndsView'; +import { WiredTriggerGameStartsView } from './WiredTriggerGameStartsView'; +import { WiredTriggeScoreAchievedView } from './WiredTriggerScoreAchievedView'; +import { WiredTriggerToggleFurniView } from './WiredTriggerToggleFurniView'; + +export const WiredTriggerLayoutView = (code: number) => +{ + switch(code) + { + case WiredTriggerLayout.AVATAR_ENTERS_ROOM: + return ; + case WiredTriggerLayout.AVATAR_SAYS_SOMETHING: + return ; + case WiredTriggerLayout.AVATAR_WALKS_OFF_FURNI: + return ; + case WiredTriggerLayout.AVATAR_WALKS_ON_FURNI: + return ; + case WiredTriggerLayout.BOT_REACHED_AVATAR: + return ; + case WiredTriggerLayout.BOT_REACHED_STUFF: + return ; + case WiredTriggerLayout.COLLISION: + return ; + case WiredTriggerLayout.EXECUTE_ONCE: + return ; + case WiredTriggerLayout.EXECUTE_PERIODICALLY: + return ; + case WiredTriggerLayout.EXECUTE_PERIODICALLY_LONG: + return ; + case WiredTriggerLayout.GAME_ENDS: + return ; + case WiredTriggerLayout.GAME_STARTS: + return ; + case WiredTriggerLayout.SCORE_ACHIEVED: + return ; + case WiredTriggerLayout.TOGGLE_FURNI: + return ; + } + + return null; +} diff --git a/apps/frontend/src/components/wired/views/triggers/WiredTriggerScoreAchievedView.tsx b/apps/frontend/src/components/wired/views/triggers/WiredTriggerScoreAchievedView.tsx new file mode 100644 index 0000000..883503b --- /dev/null +++ b/apps/frontend/src/components/wired/views/triggers/WiredTriggerScoreAchievedView.tsx @@ -0,0 +1,33 @@ +import { FC, useEffect, useState } from 'react'; +import ReactSlider from 'react-slider'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Column, Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { WiredTriggerBaseView } from './WiredTriggerBaseView'; + +export const WiredTriggeScoreAchievedView: FC<{}> = props => +{ + const [ points, setPoints ] = useState(1); + const { trigger = null, setIntParams = null } = useWired(); + + const save = () => setIntParams([ points ]); + + useEffect(() => + { + setPoints((trigger.intData.length > 0) ? trigger.intData[0] : 0); + }, [ trigger ]); + + return ( + + + { LocalizeText('wiredfurni.params.setscore', [ 'points' ], [ points.toString() ]) } + setPoints(event) } /> + + + ); +} diff --git a/apps/frontend/src/components/wired/views/triggers/WiredTriggerToggleFurniView.tsx b/apps/frontend/src/components/wired/views/triggers/WiredTriggerToggleFurniView.tsx new file mode 100644 index 0000000..865006b --- /dev/null +++ b/apps/frontend/src/components/wired/views/triggers/WiredTriggerToggleFurniView.tsx @@ -0,0 +1,8 @@ +import { FC } from 'react'; +import { WiredFurniType } from '../../../../api'; +import { WiredTriggerBaseView } from './WiredTriggerBaseView'; + +export const WiredTriggerToggleFurniView: FC<{}> = props => +{ + return ; +} diff --git a/apps/frontend/src/events/catalog/CatalogEvent.ts b/apps/frontend/src/events/catalog/CatalogEvent.ts new file mode 100644 index 0000000..893775a --- /dev/null +++ b/apps/frontend/src/events/catalog/CatalogEvent.ts @@ -0,0 +1,14 @@ +import { NitroEvent } from '@nitrots/nitro-renderer'; + +export class CatalogEvent extends NitroEvent +{ + public static SHOW_CATALOG: string = 'CE_SHOW_CATALOG'; + public static HIDE_CATALOG: string = 'CE_HIDE_CATALOG'; + public static TOGGLE_CATALOG: string = 'CE_TOGGLE_CATALOG'; + public static SOLD_OUT: string = 'CE_SOLD_OUT'; + public static APPROVE_NAME_RESULT: string = 'CE_APPROVE_NAME_RESULT'; + public static PURCHASE_APPROVED: string = 'CE_PURCHASE_APPROVED'; + public static INIT_GIFT: string = 'CE_INIT_GIFT'; + public static CATALOG_RESET: string = 'CE_RESET'; + public static CATALOG_INVISIBLE_PAGE_VISITED: string = 'CE_CATALOG_INVISIBLE_PAGE_VISITED'; +} diff --git a/apps/frontend/src/events/catalog/CatalogInitGiftEvent.ts b/apps/frontend/src/events/catalog/CatalogInitGiftEvent.ts new file mode 100644 index 0000000..fd9c926 --- /dev/null +++ b/apps/frontend/src/events/catalog/CatalogInitGiftEvent.ts @@ -0,0 +1,32 @@ +import { CatalogEvent } from './CatalogEvent'; + +export class CatalogInitGiftEvent extends CatalogEvent +{ + private _pageId: number; + private _offerId: number; + private _extraData: string; + + constructor(pageId: number, offerId: number, extraData: string) + { + super(CatalogEvent.INIT_GIFT); + + this._pageId = pageId; + this._offerId = offerId; + this._extraData = extraData; + } + + public get pageId(): number + { + return this._pageId; + } + + public get offerId(): number + { + return this._offerId; + } + + public get extraData(): string + { + return this._extraData; + } +} diff --git a/apps/frontend/src/events/catalog/CatalogPostMarketplaceOfferEvent.ts b/apps/frontend/src/events/catalog/CatalogPostMarketplaceOfferEvent.ts new file mode 100644 index 0000000..d54c6c4 --- /dev/null +++ b/apps/frontend/src/events/catalog/CatalogPostMarketplaceOfferEvent.ts @@ -0,0 +1,20 @@ +import { CatalogEvent } from '.'; +import { FurnitureItem } from '../../api'; + +export class CatalogPostMarketplaceOfferEvent extends CatalogEvent +{ + public static readonly POST_MARKETPLACE = 'CE_POST_MARKETPLACE'; + + private _item: FurnitureItem; + + constructor(item: FurnitureItem) + { + super(CatalogPostMarketplaceOfferEvent.POST_MARKETPLACE); + this._item = item; + } + + public get item(): FurnitureItem + { + return this._item; + } +} diff --git a/apps/frontend/src/events/catalog/CatalogPurchaseFailureEvent.ts b/apps/frontend/src/events/catalog/CatalogPurchaseFailureEvent.ts new file mode 100644 index 0000000..59f0633 --- /dev/null +++ b/apps/frontend/src/events/catalog/CatalogPurchaseFailureEvent.ts @@ -0,0 +1,20 @@ +import { NitroEvent } from '@nitrots/nitro-renderer'; + +export class CatalogPurchaseFailureEvent extends NitroEvent +{ + public static PURCHASE_FAILED: string = 'CPFE_PURCHASE_FAILED'; + + private _code: number; + + constructor(code: number) + { + super(CatalogPurchaseFailureEvent.PURCHASE_FAILED); + + this._code = code; + } + + public get code(): number + { + return this._code; + } +} diff --git a/apps/frontend/src/events/catalog/CatalogPurchaseNotAllowedEvent.ts b/apps/frontend/src/events/catalog/CatalogPurchaseNotAllowedEvent.ts new file mode 100644 index 0000000..78d012b --- /dev/null +++ b/apps/frontend/src/events/catalog/CatalogPurchaseNotAllowedEvent.ts @@ -0,0 +1,20 @@ +import { NitroEvent } from '@nitrots/nitro-renderer'; + +export class CatalogPurchaseNotAllowedEvent extends NitroEvent +{ + public static NOT_ALLOWED: string = 'CPNAE_NOT_ALLOWED'; + + private _code: number; + + constructor(code: number) + { + super(CatalogPurchaseNotAllowedEvent.NOT_ALLOWED); + + this._code = code; + } + + public get code(): number + { + return this._code; + } +} diff --git a/apps/frontend/src/events/catalog/CatalogPurchaseOverrideEvent.ts b/apps/frontend/src/events/catalog/CatalogPurchaseOverrideEvent.ts new file mode 100644 index 0000000..7c0b8b5 --- /dev/null +++ b/apps/frontend/src/events/catalog/CatalogPurchaseOverrideEvent.ts @@ -0,0 +1,19 @@ +import { NitroEvent } from '@nitrots/nitro-renderer'; +import { CatalogWidgetEvent } from './CatalogWidgetEvent'; + +export class CatalogPurchaseOverrideEvent extends NitroEvent +{ + private _callback: Function; + + constructor(callback: Function) + { + super(CatalogWidgetEvent.PURCHASE_OVERRIDE); + + this._callback = callback; + } + + public get callback(): Function + { + return this._callback; + } +} diff --git a/apps/frontend/src/events/catalog/CatalogPurchaseSoldOutEvent.ts b/apps/frontend/src/events/catalog/CatalogPurchaseSoldOutEvent.ts new file mode 100644 index 0000000..a3a43a3 --- /dev/null +++ b/apps/frontend/src/events/catalog/CatalogPurchaseSoldOutEvent.ts @@ -0,0 +1,11 @@ +import { NitroEvent } from '@nitrots/nitro-renderer'; + +export class CatalogPurchaseSoldOutEvent extends NitroEvent +{ + public static SOLD_OUT: string = 'CPSOE_SOLD_OUT'; + + constructor() + { + super(CatalogPurchaseSoldOutEvent.SOLD_OUT); + } +} diff --git a/apps/frontend/src/events/catalog/CatalogPurchasedEvent.ts b/apps/frontend/src/events/catalog/CatalogPurchasedEvent.ts new file mode 100644 index 0000000..dedba46 --- /dev/null +++ b/apps/frontend/src/events/catalog/CatalogPurchasedEvent.ts @@ -0,0 +1,20 @@ +import { NitroEvent, PurchaseOKMessageOfferData } from '@nitrots/nitro-renderer'; + +export class CatalogPurchasedEvent extends NitroEvent +{ + public static PURCHASE_SUCCESS: string = 'CPE_PURCHASE_SUCCESS'; + + private _purchase: PurchaseOKMessageOfferData; + + constructor(purchase: PurchaseOKMessageOfferData) + { + super(CatalogPurchasedEvent.PURCHASE_SUCCESS); + + this._purchase = purchase; + } + + public get purchase(): PurchaseOKMessageOfferData + { + return this._purchase; + } +} diff --git a/apps/frontend/src/events/catalog/CatalogSetRoomPreviewerStuffDataEvent.ts b/apps/frontend/src/events/catalog/CatalogSetRoomPreviewerStuffDataEvent.ts new file mode 100644 index 0000000..24e91a7 --- /dev/null +++ b/apps/frontend/src/events/catalog/CatalogSetRoomPreviewerStuffDataEvent.ts @@ -0,0 +1,19 @@ +import { IObjectData, NitroEvent } from '@nitrots/nitro-renderer'; +import { CatalogWidgetEvent } from './CatalogWidgetEvent'; + +export class CatalogSetRoomPreviewerStuffDataEvent extends NitroEvent +{ + private _stuffData: IObjectData; + + constructor(stuffData: IObjectData) + { + super(CatalogWidgetEvent.SET_PREVIEWER_STUFFDATA); + + this._stuffData = stuffData; + } + + public get stuffData(): IObjectData + { + return this._stuffData; + } +} diff --git a/apps/frontend/src/events/catalog/CatalogWidgetEvent.ts b/apps/frontend/src/events/catalog/CatalogWidgetEvent.ts new file mode 100644 index 0000000..fd0e602 --- /dev/null +++ b/apps/frontend/src/events/catalog/CatalogWidgetEvent.ts @@ -0,0 +1,26 @@ +import { NitroEvent } from '@nitrots/nitro-renderer'; + +export class CatalogWidgetEvent extends NitroEvent +{ + public static WIDGETS_INITIALIZED: string = 'CWE_CWE_WIDGETS_INITIALIZED'; + public static SELECT_PRODUCT: string = 'CWE_SELECT_PRODUCT'; + public static SET_EXTRA_PARM: string = 'CWE_CWE_SET_EXTRA_PARM'; + public static PURCHASE: string = 'CWE_PURCHASE'; + public static COLOUR_ARRAY: string = 'CWE_COLOUR_ARRAY'; + public static MULTI_COLOUR_ARRAY: string = 'CWE_MULTI_COLOUR_ARRAY'; + public static COLOUR_INDEX: string = 'CWE_COLOUR_INDEX'; + public static TEXT_INPUT: string = 'CWE_TEXT_INPUT'; + public static DROPMENU_SELECT: string = 'CWE_CWE_DROPMENU_SELECT'; + public static PURCHASE_OVERRIDE: string = 'CWE_PURCHASE_OVERRIDE'; + public static SELLABLE_PET_PALETTES: string = 'CWE_SELLABLE_PET_PALETTES'; + public static UPDATE_ROOM_PREVIEW: string = 'CWE_UPDATE_ROOM_PREVIEW'; + public static GUILD_SELECTED: string = 'CWE_GUILD_SELECTED'; + public static TOTAL_PRICE_WIDGET_INITIALIZED: string = 'CWE_TOTAL_PRICE_WIDGET_INITIALIZED'; + public static PRODUCT_OFFER_UPDATED: string = 'CWE_CWE_PRODUCT_OFFER_UPDATED'; + public static SET_PREVIEWER_STUFFDATA: string = 'CWE_CWE_SET_PREVIEWER_STUFFDATA'; + public static EXTRA_PARAM_REQUIRED_FOR_BUY: string = 'CWE_CWE_EXTRA_PARAM_REQUIRED_FOR_BUY'; + public static TOGGLE: string = 'CWE_CWE_TOGGLE'; + public static BUILDER_SUBSCRIPTION_UPDATED: string = 'CWE_CWE_BUILDER_SUBSCRIPTION_UPDATED'; + public static ROOM_CHANGED: string = 'CWE_CWE_ROOM_CHANGED'; + public static SHOW_WARNING_TEXT: string = 'CWE_CWE_SHOW_WARNING_TEXT'; +} diff --git a/apps/frontend/src/events/catalog/SetRoomPreviewerStuffDataEvent.ts b/apps/frontend/src/events/catalog/SetRoomPreviewerStuffDataEvent.ts new file mode 100644 index 0000000..5332dfd --- /dev/null +++ b/apps/frontend/src/events/catalog/SetRoomPreviewerStuffDataEvent.ts @@ -0,0 +1,28 @@ +import { IObjectData, NitroEvent } from '@nitrots/nitro-renderer'; +import { IPurchasableOffer } from '../../api'; + +export class SetRoomPreviewerStuffDataEvent extends NitroEvent +{ + public static UPDATE_STUFF_DATA: string = 'SRPSA_UPDATE_STUFF_DATA'; + + private _offer: IPurchasableOffer; + private _stuffData: IObjectData; + + constructor(offer: IPurchasableOffer, stuffData: IObjectData) + { + super(SetRoomPreviewerStuffDataEvent.UPDATE_STUFF_DATA); + + this._offer = offer; + this._stuffData = stuffData; + } + + public get offer(): IPurchasableOffer + { + return this._offer; + } + + public get stuffData(): IObjectData + { + return this._stuffData; + } +} diff --git a/apps/frontend/src/events/catalog/index.ts b/apps/frontend/src/events/catalog/index.ts new file mode 100644 index 0000000..a7c1572 --- /dev/null +++ b/apps/frontend/src/events/catalog/index.ts @@ -0,0 +1,11 @@ +export * from './CatalogEvent'; +export * from './CatalogInitGiftEvent'; +export * from './CatalogPostMarketplaceOfferEvent'; +export * from './CatalogPurchasedEvent'; +export * from './CatalogPurchaseFailureEvent'; +export * from './CatalogPurchaseNotAllowedEvent'; +export * from './CatalogPurchaseOverrideEvent'; +export * from './CatalogPurchaseSoldOutEvent'; +export * from './CatalogSetRoomPreviewerStuffDataEvent'; +export * from './CatalogWidgetEvent'; +export * from './SetRoomPreviewerStuffDataEvent'; diff --git a/apps/frontend/src/events/guide-tool/GuideToolEvent.ts b/apps/frontend/src/events/guide-tool/GuideToolEvent.ts new file mode 100644 index 0000000..f77cec3 --- /dev/null +++ b/apps/frontend/src/events/guide-tool/GuideToolEvent.ts @@ -0,0 +1,10 @@ +import { NitroEvent } from '@nitrots/nitro-renderer'; + +export class GuideToolEvent extends NitroEvent +{ + public static readonly SHOW_GUIDE_TOOL: string = 'GTE_SHOW_GUIDE_TOOL'; + public static readonly HIDE_GUIDE_TOOL: string = 'GTE_HIDE_GUIDE_TOOL'; + public static readonly TOGGLE_GUIDE_TOOL: string = 'GTE_TOGGLE_GUIDE_TOOL'; + public static readonly CREATE_HELP_REQUEST: string = 'GTE_CREATE_HELP_REQUEST'; + public static readonly CREATE_BULLY_REQUEST: string = 'GTE_CREATE_BULLY_REQUEST'; +} diff --git a/apps/frontend/src/events/guide-tool/index.ts b/apps/frontend/src/events/guide-tool/index.ts new file mode 100644 index 0000000..6cbff54 --- /dev/null +++ b/apps/frontend/src/events/guide-tool/index.ts @@ -0,0 +1 @@ +export * from './GuideToolEvent'; diff --git a/apps/frontend/src/events/help/HelpNameChangeEvent.ts b/apps/frontend/src/events/help/HelpNameChangeEvent.ts new file mode 100644 index 0000000..44597d0 --- /dev/null +++ b/apps/frontend/src/events/help/HelpNameChangeEvent.ts @@ -0,0 +1,6 @@ +import { NitroEvent } from '@nitrots/nitro-renderer'; + +export class HelpNameChangeEvent extends NitroEvent +{ + public static INIT: string = 'HC_NAME_CHANGE_INIT'; +} diff --git a/apps/frontend/src/events/help/index.ts b/apps/frontend/src/events/help/index.ts new file mode 100644 index 0000000..e89f307 --- /dev/null +++ b/apps/frontend/src/events/help/index.ts @@ -0,0 +1 @@ +export * from './HelpNameChangeEvent'; diff --git a/apps/frontend/src/events/index.ts b/apps/frontend/src/events/index.ts new file mode 100644 index 0000000..4c5e757 --- /dev/null +++ b/apps/frontend/src/events/index.ts @@ -0,0 +1,6 @@ +export * from './catalog'; +export * from './guide-tool'; +export * from './help'; +export * from './inventory'; +export * from './room-widgets'; +export * from './room-widgets/thumbnail'; diff --git a/apps/frontend/src/events/inventory/InventoryFurniAddedEvent.ts b/apps/frontend/src/events/inventory/InventoryFurniAddedEvent.ts new file mode 100644 index 0000000..409f0be --- /dev/null +++ b/apps/frontend/src/events/inventory/InventoryFurniAddedEvent.ts @@ -0,0 +1,14 @@ +import { NitroEvent } from '@nitrots/nitro-renderer'; + +export class InventoryFurniAddedEvent extends NitroEvent +{ + public static FURNI_ADDED: string = 'IFAE_FURNI_ADDED'; + + constructor( + public readonly id: number, + public readonly spriteId: number, + public readonly category: number) + { + super(InventoryFurniAddedEvent.FURNI_ADDED); + } +} diff --git a/apps/frontend/src/events/inventory/index.ts b/apps/frontend/src/events/inventory/index.ts new file mode 100644 index 0000000..58503ea --- /dev/null +++ b/apps/frontend/src/events/inventory/index.ts @@ -0,0 +1 @@ +export * from './InventoryFurniAddedEvent'; diff --git a/apps/frontend/src/events/room-widgets/index.ts b/apps/frontend/src/events/room-widgets/index.ts new file mode 100644 index 0000000..d4ab7a5 --- /dev/null +++ b/apps/frontend/src/events/room-widgets/index.ts @@ -0,0 +1 @@ +export * from './thumbnail'; diff --git a/apps/frontend/src/events/room-widgets/thumbnail/RoomWidgetThumbnailEvent.ts b/apps/frontend/src/events/room-widgets/thumbnail/RoomWidgetThumbnailEvent.ts new file mode 100644 index 0000000..dc62c6f --- /dev/null +++ b/apps/frontend/src/events/room-widgets/thumbnail/RoomWidgetThumbnailEvent.ts @@ -0,0 +1,8 @@ +import { NitroEvent } from '@nitrots/nitro-renderer'; + +export class RoomWidgetThumbnailEvent extends NitroEvent +{ + public static SHOW_THUMBNAIL: string = 'NE_SHOW_THUMBNAIL'; + public static HIDE_THUMBNAIL: string = 'NE_HIDE_THUMBNAIL'; + public static TOGGLE_THUMBNAIL: string = 'NE_TOGGLE_THUMBNAIL'; +} diff --git a/apps/frontend/src/events/room-widgets/thumbnail/index.ts b/apps/frontend/src/events/room-widgets/thumbnail/index.ts new file mode 100644 index 0000000..56f116d --- /dev/null +++ b/apps/frontend/src/events/room-widgets/thumbnail/index.ts @@ -0,0 +1 @@ +export * from './RoomWidgetThumbnailEvent'; diff --git a/apps/frontend/src/hooks/UseMountEffect.tsx b/apps/frontend/src/hooks/UseMountEffect.tsx new file mode 100644 index 0000000..0ead14b --- /dev/null +++ b/apps/frontend/src/hooks/UseMountEffect.tsx @@ -0,0 +1,7 @@ +import { EffectCallback, useEffect } from 'react'; + + +// eslint-disable-next-line react-hooks/exhaustive-deps +const useEffectOnce = (effect: EffectCallback) => useEffect(effect, []); + +export const UseMountEffect = (fn: Function) => useEffectOnce(() => fn()); diff --git a/apps/frontend/src/hooks/achievements/index.ts b/apps/frontend/src/hooks/achievements/index.ts new file mode 100644 index 0000000..1a2a81e --- /dev/null +++ b/apps/frontend/src/hooks/achievements/index.ts @@ -0,0 +1 @@ +export * from './useAchievements'; diff --git a/apps/frontend/src/hooks/achievements/useAchievements.ts b/apps/frontend/src/hooks/achievements/useAchievements.ts new file mode 100644 index 0000000..a067ff7 --- /dev/null +++ b/apps/frontend/src/hooks/achievements/useAchievements.ts @@ -0,0 +1,185 @@ +import { AchievementData, AchievementEvent, AchievementsEvent, AchievementsScoreEvent, RequestAchievementsMessageComposer } from '@nitrots/nitro-renderer'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useBetween } from 'use-between'; +import { AchievementCategory, AchievementUtilities, CloneObject, SendMessageComposer } from '../../api'; +import { useMessageEvent } from '../events'; + +const useAchievementsState = () => +{ + const [ needsUpdate, setNeedsUpdate ] = useState(true); + const [ achievementCategories, setAchievementCategories ] = useState([]); + const [ selectedCategoryCode, setSelectedCategoryCode ] = useState(null); + const [ selectedAchievementId, setSelectedAchievementId ] = useState(-1); + const [ achievementScore, setAchievementScore ] = useState(0); + + const getTotalUnseen = useMemo(() => + { + let unseen = 0; + + achievementCategories.forEach(category => unseen += AchievementUtilities.getAchievementCategoryTotalUnseen(category)); + + return unseen; + }, [ achievementCategories ]); + + const getProgress = useMemo(() => + { + let progress = 0; + + achievementCategories.forEach(category => (progress += category.getProgress())); + + return progress; + }, [ achievementCategories ]); + + const getMaxProgress = useMemo(() => + { + let progress = 0; + + achievementCategories.forEach(category => (progress += category.getMaxProgress())); + + return progress; + }, [ achievementCategories ]); + + const scaledProgressPercent = useMemo(() => + { + return ~~((((getProgress - 0) * (100 - 0)) / (getMaxProgress - 0)) + 0); + }, [ getProgress, getMaxProgress ]); + + const selectedCategory = useMemo(() => + { + if(selectedCategoryCode === null) return null; + + return achievementCategories.find(category => (category.code === selectedCategoryCode)); + }, [ achievementCategories, selectedCategoryCode ]); + + const selectedAchievement = useMemo(() => + { + if((selectedAchievementId === -1) || !selectedCategory) return null; + + return selectedCategory.achievements.find(achievement => (achievement.achievementId === selectedAchievementId)); + }, [ selectedCategory, selectedAchievementId ]); + + const setAchievementSeen = useCallback((categoryCode: string, achievementId: number) => + { + setAchievementCategories(prevValue => + { + const newValue = [ ...prevValue ]; + + for(const category of newValue) + { + if(category.code !== categoryCode) continue; + + for(const achievement of category.achievements) + { + if(achievement.achievementId !== achievementId) continue; + + achievement.unseen = 0; + } + } + + return newValue; + }); + }, []); + + useMessageEvent(AchievementEvent, event => + { + const parser = event.getParser(); + const achievement = parser.achievement; + + setAchievementCategories(prevValue => + { + const newValue = [ ...prevValue ]; + const categoryIndex = newValue.findIndex(existing => (existing.code === achievement.category)); + + if(categoryIndex === -1) + { + const category = new AchievementCategory(achievement.category); + + category.achievements.push(achievement); + + newValue.push(category); + } + else + { + const category = CloneObject(newValue[categoryIndex]); + const newAchievements = [ ...category.achievements ]; + const achievementIndex = newAchievements.findIndex(existing => (existing.achievementId === achievement.achievementId)); + let previousAchievement: AchievementData = null; + + if(achievementIndex === -1) + { + newAchievements.push(achievement); + } + else + { + previousAchievement = newAchievements[achievementIndex]; + + newAchievements[achievementIndex] = achievement; + } + + if(!AchievementUtilities.getAchievementIsIgnored(achievement)) + { + achievement.unseen++; + + if(previousAchievement) achievement.unseen += previousAchievement.unseen; + } + + category.achievements = newAchievements; + + newValue[categoryIndex] = category; + } + + return newValue; + }); + }); + + useMessageEvent(AchievementsEvent, event => + { + const parser = event.getParser(); + const categories: AchievementCategory[] = []; + + for(const achievement of parser.achievements) + { + const categoryName = achievement.category; + + let existing = categories.find(category => (category.code === categoryName)); + + if(!existing) + { + existing = new AchievementCategory(categoryName); + + categories.push(existing); + } + + existing.achievements.push(achievement); + } + + setAchievementCategories(categories); + }); + + useMessageEvent(AchievementsScoreEvent, event => + { + const parser = event.getParser(); + + setAchievementScore(parser.score); + }); + + useEffect(() => + { + if(!needsUpdate) return; + + SendMessageComposer(new RequestAchievementsMessageComposer()); + + setNeedsUpdate(false); + }, [ needsUpdate ]); + + useEffect(() => + { + if(!selectedCategoryCode || (selectedAchievementId === -1)) return; + + setAchievementSeen(selectedCategoryCode, selectedAchievementId); + }, [ selectedCategoryCode, selectedAchievementId, setAchievementSeen ]); + + return { achievementCategories, selectedCategoryCode, setSelectedCategoryCode, selectedAchievementId, setSelectedAchievementId, achievementScore, getTotalUnseen, getProgress, getMaxProgress, scaledProgressPercent, selectedCategory, selectedAchievement, setAchievementSeen }; +} + +export const useAchievements = () => useBetween(useAchievementsState); diff --git a/apps/frontend/src/hooks/camera/index.ts b/apps/frontend/src/hooks/camera/index.ts new file mode 100644 index 0000000..20bea9c --- /dev/null +++ b/apps/frontend/src/hooks/camera/index.ts @@ -0,0 +1 @@ +export * from './useCamera'; diff --git a/apps/frontend/src/hooks/camera/useCamera.ts b/apps/frontend/src/hooks/camera/useCamera.ts new file mode 100644 index 0000000..e2449c8 --- /dev/null +++ b/apps/frontend/src/hooks/camera/useCamera.ts @@ -0,0 +1,42 @@ +import { InitCameraMessageEvent, IRoomCameraWidgetEffect, RequestCameraConfigurationComposer, RoomCameraWidgetManagerEvent } from '@nitrots/nitro-renderer'; +import { useEffect, useState } from 'react'; +import { useBetween } from 'use-between'; +import { CameraPicture, GetRoomCameraWidgetManager, SendMessageComposer } from '../../api'; +import { useCameraEvent, useMessageEvent } from '../events'; + +const useCameraState = () => +{ + const [ availableEffects, setAvailableEffects ] = useState([]); + const [ cameraRoll, setCameraRoll ] = useState([]); + const [ selectedPictureIndex, setSelectedPictureIndex ] = useState(-1); + const [ myLevel, setMyLevel ] = useState(10); + const [ price, setPrice ] = useState<{ credits: number, duckets: number, publishDucketPrice: number }>(null); + + useCameraEvent(RoomCameraWidgetManagerEvent.INITIALIZED, event => + { + setAvailableEffects(Array.from(GetRoomCameraWidgetManager().effects.values())); + }); + + useMessageEvent(InitCameraMessageEvent, event => + { + const parser = event.getParser(); + + setPrice({ credits: parser.creditPrice, duckets: parser.ducketPrice, publishDucketPrice: parser.publishDucketPrice }); + }); + + useEffect(() => + { + if(!GetRoomCameraWidgetManager().isLoaded) + { + GetRoomCameraWidgetManager().init(); + + SendMessageComposer(new RequestCameraConfigurationComposer()); + + return; + } + }, []); + + return { availableEffects, cameraRoll, setCameraRoll, selectedPictureIndex, setSelectedPictureIndex, myLevel, price }; +} + +export const useCamera = () => useBetween(useCameraState); diff --git a/apps/frontend/src/hooks/catalog/index.ts b/apps/frontend/src/hooks/catalog/index.ts new file mode 100644 index 0000000..75d2984 --- /dev/null +++ b/apps/frontend/src/hooks/catalog/index.ts @@ -0,0 +1,3 @@ +export * from './useCatalog'; +export * from './useCatalogPlaceMultipleItems'; +export * from './useCatalogSkipPurchaseConfirmation'; diff --git a/apps/frontend/src/hooks/catalog/useCatalog.ts b/apps/frontend/src/hooks/catalog/useCatalog.ts new file mode 100644 index 0000000..6572baa --- /dev/null +++ b/apps/frontend/src/hooks/catalog/useCatalog.ts @@ -0,0 +1,913 @@ +import { BuildersClubFurniCountMessageEvent, BuildersClubPlaceRoomItemMessageComposer, BuildersClubPlaceWallItemMessageComposer, BuildersClubQueryFurniCountMessageComposer, BuildersClubSubscriptionStatusMessageEvent, CatalogPageMessageEvent, CatalogPagesListEvent, CatalogPublishedMessageEvent, ClubGiftInfoEvent, FrontPageItem, FurniturePlaceComposer, FurniturePlacePaintComposer, GetCatalogIndexComposer, GetCatalogPageComposer, GetClubGiftInfo, GetGiftWrappingConfigurationComposer, GetTickerTime, GiftWrappingConfigurationEvent, GuildMembershipsMessageEvent, HabboClubOffersMessageEvent, LegacyDataType, LimitedEditionSoldOutEvent, MarketplaceMakeOfferResult, NodeData, ProductOfferEvent, PurchaseErrorMessageEvent, PurchaseFromCatalogComposer, PurchaseNotAllowedMessageEvent, PurchaseOKMessageEvent, RoomControllerLevel, RoomEngineObjectPlacedEvent, RoomObjectCategory, RoomObjectPlacementSource, RoomObjectType, RoomObjectVariable, RoomPreviewer, SellablePetPalettesMessageEvent, Vector3d } from '@nitrots/nitro-renderer'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useBetween } from 'use-between'; +import { BuilderFurniPlaceableStatus, CatalogNode, CatalogPage, CatalogPetPalette, CatalogType, CreateLinkEvent, DispatchUiEvent, FurniCategory, GetFurnitureData, GetProductDataForLocalization, GetRoomEngine, GetRoomSession, GiftWrappingConfiguration, ICatalogNode, ICatalogOptions, ICatalogPage, IPageLocalization, IProduct, IPurchasableOffer, IPurchaseOptions, LocalizeText, NotificationAlertType, Offer, PageLocalization, PlacedObjectPurchaseData, PlaySound, Product, ProductTypeEnum, RequestedPage, SearchResult, SendMessageComposer, SoundNames } from '../../api'; +import { CatalogPurchasedEvent, CatalogPurchaseFailureEvent, CatalogPurchaseNotAllowedEvent, CatalogPurchaseSoldOutEvent, InventoryFurniAddedEvent } from '../../events'; +import { useMessageEvent, useRoomEngineEvent, useUiEvent } from '../events'; +import { useNotification } from '../notification'; +import { useCatalogPlaceMultipleItems } from './useCatalogPlaceMultipleItems'; +import { useCatalogSkipPurchaseConfirmation } from './useCatalogSkipPurchaseConfirmation'; + +const DUMMY_PAGE_ID_FOR_OFFER_SEARCH = -12345678; +const DRAG_AND_DROP_ENABLED = true; + +const useCatalogState = () => +{ + const [ isVisible, setIsVisible ] = useState(false); + const [ isBusy, setIsBusy ] = useState(false); + const [ pageId, setPageId ] = useState(-1); + const [ previousPageId, setPreviousPageId ] = useState(-1); + const [ currentType, setCurrentType ] = useState(CatalogType.NORMAL); + const [ rootNode, setRootNode ] = useState(null); + const [ offersToNodes, setOffersToNodes ] = useState>(null); + const [ currentPage, setCurrentPage ] = useState(null); + const [ currentOffer, setCurrentOffer ] = useState(null); + const [ activeNodes, setActiveNodes ] = useState([]); + const [ searchResult, setSearchResult ] = useState(null); + const [ frontPageItems, setFrontPageItems ] = useState([]); + const [ roomPreviewer, setRoomPreviewer ] = useState(null); + const [ navigationHidden, setNavigationHidden ] = useState(false); + const [ purchaseOptions, setPurchaseOptions ] = useState({ quantity: 1, extraData: null, extraParamRequired: false, previewStuffData: null }); + const [ catalogOptions, setCatalogOptions ] = useState({}); + const [ objectMoverRequested, setObjectMoverRequested ] = useState(false); + const [ catalogPlaceMultipleObjects, setCatalogPlaceMultipleObjects ] = useCatalogPlaceMultipleItems(); + const [ catalogSkipPurchaseConfirmation, setCatalogSkipPurchaseConfirmation ] = useCatalogSkipPurchaseConfirmation(); + const [ purchasableOffer, setPurchaseableOffer ] = useState(null); + const [ placedObjectPurchaseData, setPlacedObjectPurchaseData ] = useState(null); + const [ furniCount, setFurniCount ] = useState(0); + const [ furniLimit, setFurniLimit ] = useState(0); + const [ maxFurniLimit, setMaxFurniLimit ] = useState(0); + const [ secondsLeft, setSecondsLeft ] = useState(0); + const [ updateTime, setUpdateTime ] = useState(0); + const [ secondsLeftWithGrace, setSecondsLeftWithGrace ] = useState(0); + const { simpleAlert = null } = useNotification(); + const requestedPage = useRef(new RequestedPage()); + + const resetState = useCallback(() => + { + setPageId(-1); + setPreviousPageId(-1); + setRootNode(null); + setOffersToNodes(null); + setCurrentPage(null); + setCurrentOffer(null); + setActiveNodes([]); + setSearchResult(null); + setFrontPageItems([]); + setIsVisible(false); + }, []); + + const getBuilderFurniPlaceableStatus = useCallback((offer: IPurchasableOffer) => + { + if(!offer) return BuilderFurniPlaceableStatus.MISSING_OFFER; + + if((furniCount < 0) || (furniCount >= furniLimit)) return BuilderFurniPlaceableStatus.FURNI_LIMIT_REACHED; + + const roomSession = GetRoomSession(); + + if(!roomSession) return BuilderFurniPlaceableStatus.NOT_IN_ROOM; + + if(!roomSession.isRoomOwner) return BuilderFurniPlaceableStatus.NOT_ROOM_OWNER; + + if(secondsLeft <= 0) + { + const roomEngine = GetRoomEngine(); + + let objectCount = roomEngine.getRoomObjectCount(roomSession.roomId, RoomObjectCategory.UNIT); + + while(objectCount > 0) + { + const roomObject = roomEngine.getRoomObjectByIndex(roomSession.roomId, objectCount, RoomObjectCategory.UNIT); + const userData = roomSession.userDataManager.getUserDataByIndex(roomObject.id); + + if(userData && (userData.type === RoomObjectType.USER) && (userData.roomIndex !== roomSession.ownRoomIndex) && !userData.isModerator) return BuilderFurniPlaceableStatus.VISITORS_IN_ROOM; + + objectCount--; + } + } + + return BuilderFurniPlaceableStatus.OKAY; + }, [ furniCount, furniLimit, secondsLeft ]); + + const isDraggable = useCallback((offer: IPurchasableOffer) => + { + const roomSession = GetRoomSession(); + + if(((DRAG_AND_DROP_ENABLED && roomSession && offer.page && (offer.page.layoutCode !== 'sold_ltd_items') && (currentType === CatalogType.NORMAL) && (roomSession.isRoomOwner || (roomSession.isGuildRoom && (roomSession.controllerLevel >= RoomControllerLevel.GUILD_MEMBER)))) || ((currentType === CatalogType.BUILDER) && (getBuilderFurniPlaceableStatus(offer) === BuilderFurniPlaceableStatus.OKAY))) && (offer.pricingModel !== Offer.PRICING_MODEL_BUNDLE) && (offer.product.productType !== ProductTypeEnum.EFFECT) && (offer.product.productType !== ProductTypeEnum.HABBO_CLUB)) return true; + + return false; + }, [ currentType, getBuilderFurniPlaceableStatus ]); + + const requestOfferToMover = useCallback((offer: IPurchasableOffer) => + { + if(!isDraggable(offer)) return; + + const product = offer.product; + + if(!product) return; + + let category = 0; + + switch(product.productType) + { + case ProductTypeEnum.FLOOR: + category = RoomObjectCategory.FLOOR; + break; + case ProductTypeEnum.WALL: + category = RoomObjectCategory.WALL; + break; + } + + if(GetRoomEngine().processRoomObjectPlacement(RoomObjectPlacementSource.CATALOG, -(offer.offerId), category, product.productClassId, product.extraParam)) + { + setPurchaseableOffer(offer); + setObjectMoverRequested(true); + + setIsVisible(false); + } + }, [ isDraggable ]); + + const resetRoomPaint = useCallback((planeType: string, type: string) => + { + const roomEngine = GetRoomEngine(); + + let wallType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_WALL_TYPE); + let floorType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_FLOOR_TYPE); + let landscapeType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_LANDSCAPE_TYPE); + + wallType = (wallType && wallType.length) ? wallType : '101'; + floorType = (floorType && floorType.length) ? floorType : '101'; + landscapeType = (landscapeType && landscapeType.length) ? landscapeType : '1.1'; + + switch(planeType) + { + case 'floor': + roomEngine.updateRoomInstancePlaneType(roomEngine.activeRoomId, type, wallType, landscapeType, true); + return; + case 'wallpaper': + roomEngine.updateRoomInstancePlaneType(roomEngine.activeRoomId, floorType, type, landscapeType, true); + return; + case 'landscape': + roomEngine.updateRoomInstancePlaneType(roomEngine.activeRoomId, floorType, wallType, type, true); + return; + default: + roomEngine.updateRoomInstancePlaneType(roomEngine.activeRoomId, floorType, wallType, landscapeType, true); + return; + } + }, []); + + const cancelObjectMover = useCallback(() => + { + if(!purchasableOffer) return; + + GetRoomEngine().cancelRoomObjectInsert(); + + setObjectMoverRequested(false); + setPurchaseableOffer(null); + }, [ purchasableOffer ]); + + const resetObjectMover = useCallback((flag: boolean = true) => + { + setObjectMoverRequested(prevValue => + { + if(prevValue && flag) + { + CreateLinkEvent('catalog/open'); + } + + return false; + }); + }, []); + + const resetPlacedOfferData = useCallback((flag: boolean = false) => + { + if(!flag) resetObjectMover(); + + setPlacedObjectPurchaseData(prevValue => + { + if(prevValue) + { + switch(prevValue.category) + { + case RoomObjectCategory.FLOOR: + GetRoomEngine().removeRoomObjectFloor(prevValue.roomId, prevValue.objectId); + break; + case RoomObjectCategory.WALL: { + + switch(prevValue.furniData.className) + { + case 'floor': + case 'wallpaper': + case 'landscape': + resetRoomPaint('reset', ''); + break; + default: + GetRoomEngine().removeRoomObjectWall(prevValue.roomId, prevValue.objectId); + break; + } + break; + } + default: + GetRoomEngine().deleteRoomObject(prevValue.objectId, prevValue.category); + break; + } + } + + return null; + }); + }, [ resetObjectMover, resetRoomPaint ]); + + const getNodeById = useCallback((id: number, node: ICatalogNode) => + { + if((node.pageId === id) && (node !== rootNode)) return node; + + for(const child of node.children) + { + const found = (getNodeById(id, child) as ICatalogNode); + + if(found) return found; + } + + return null; + }, [ rootNode ]); + + const getNodeByName = useCallback((name: string, node: ICatalogNode) => + { + if((node.pageName === name) && (node !== rootNode)) return node; + + for(const child of node.children) + { + const found = (getNodeByName(name, child) as ICatalogNode); + + if(found) return found; + } + + return null; + }, [ rootNode ]); + + const getNodesByOfferId = useCallback((offerId: number, flag: boolean = false) => + { + if(!offersToNodes || !offersToNodes.size) return null; + + if(flag) + { + const nodes: ICatalogNode[] = []; + const offers = offersToNodes.get(offerId); + + if(offers && offers.length) for(const offer of offers) (offer.isVisible && nodes.push(offer)); + + if(nodes.length) return nodes; + } + + return offersToNodes.get(offerId); + }, [ offersToNodes ]); + + const loadCatalogPage = useCallback((pageId: number, offerId: number) => + { + if(pageId < 0) return; + + setIsBusy(true); + setPageId(pageId); + + if(pageId > -1) SendMessageComposer(new GetCatalogPageComposer(pageId, offerId, currentType)); + }, [ currentType ]); + + const showCatalogPage = useCallback((pageId: number, layoutCode: string, localization: IPageLocalization, offers: IPurchasableOffer[], offerId: number, acceptSeasonCurrencyAsCredits: boolean) => + { + const catalogPage = (new CatalogPage(pageId, layoutCode, localization, offers, acceptSeasonCurrencyAsCredits) as ICatalogPage); + + setCurrentPage(catalogPage); + setPreviousPageId(prevValue => ((pageId !== -1) ? pageId : prevValue)); + setNavigationHidden(false); + + if((offerId > -1) && catalogPage.offers.length) + { + for(const offer of catalogPage.offers) + { + if(offer.offerId !== offerId) continue; + + setCurrentOffer(offer) + + break; + } + } + }, []); + + const activateNode = useCallback((targetNode: ICatalogNode, offerId: number = -1) => + { + cancelObjectMover(); + + if(targetNode.parent.pageName === 'root') + { + if(targetNode.children.length) + { + for(const child of targetNode.children) + { + if(!child.isVisible) continue; + + targetNode = child; + + break; + } + } + } + + const nodes: ICatalogNode[] = []; + + let node = targetNode; + + while(node && (node.pageName !== 'root')) + { + nodes.push(node); + + node = node.parent; + } + + nodes.reverse(); + + setActiveNodes(prevValue => + { + const isActive = (prevValue.indexOf(targetNode) >= 0); + const isOpen = targetNode.isOpen; + + for(const existing of prevValue) + { + existing.deactivate(); + + if(nodes.indexOf(existing) === -1) existing.close(); + } + + for(const n of nodes) + { + n.activate(); + + if(n.parent) n.open(); + + if((n === targetNode.parent) && n.children.length) n.open(); + } + + if(isActive && isOpen) targetNode.close(); + else targetNode.open(); + + return nodes; + }); + + if(targetNode.pageId > -1) loadCatalogPage(targetNode.pageId, offerId); + }, [ setActiveNodes, loadCatalogPage, cancelObjectMover ]); + + const openPageById = useCallback((id: number) => + { + if(id !== -1) setSearchResult(null); + + if(!isVisible) + { + requestedPage.current.requestById = id; + + setIsVisible(true); + } + else + { + const node = getNodeById(id, rootNode); + + if(node) activateNode(node); + } + }, [ isVisible, rootNode, getNodeById, activateNode ]); + + const openPageByName = useCallback((name: string) => + { + setSearchResult(null); + + if(!isVisible) + { + requestedPage.current.requestByName = name; + + setIsVisible(true); + } + else + { + const node = getNodeByName(name, rootNode); + + if(node) activateNode(node); + } + }, [ isVisible, rootNode, getNodeByName, activateNode ]); + + const openPageByOfferId = useCallback((offerId: number) => + { + setSearchResult(null); + + if(!isVisible) + { + requestedPage.current.requestedByOfferId = offerId; + + setIsVisible(true); + } + else + { + const nodes = getNodesByOfferId(offerId); + + if(!nodes || !nodes.length) return; + + activateNode(nodes[0], offerId); + } + }, [ isVisible, getNodesByOfferId, activateNode ]); + + const refreshBuilderStatus = useCallback(() => + { + + }, []); + + useMessageEvent(CatalogPagesListEvent, event => + { + const parser = event.getParser(); + const offers: Map = new Map(); + + const getCatalogNode = (node: NodeData, depth: number, parent: ICatalogNode) => + { + const catalogNode = (new CatalogNode(node, depth, parent) as ICatalogNode); + + for(const offerId of catalogNode.offerIds) + { + if(offers.has(offerId)) offers.get(offerId).push(catalogNode); + else offers.set(offerId, [ catalogNode ]); + } + + depth++; + + for(const child of node.children) catalogNode.addChild(getCatalogNode(child, depth, catalogNode)); + + return catalogNode; + } + + setRootNode(getCatalogNode(parser.root, 0, null)); + setOffersToNodes(offers); + }); + + useMessageEvent(CatalogPageMessageEvent, event => + { + const parser = event.getParser(); + + if(parser.catalogType !== currentType) return; + + const purchasableOffers: IPurchasableOffer[] = []; + + for(const offer of parser.offers) + { + const products: IProduct[] = []; + const productData = GetProductDataForLocalization(offer.localizationId); + + for(const product of offer.products) + { + const furnitureData = GetFurnitureData(product.furniClassId, product.productType); + + products.push(new Product(product.productType, product.furniClassId, product.extraParam, product.productCount, productData, furnitureData, product.uniqueLimitedItem, product.uniqueLimitedSeriesSize, product.uniqueLimitedItemsLeft)); + } + + if(!products.length) continue; + + const purchasableOffer = new Offer(offer.offerId, offer.localizationId, offer.rent, offer.priceCredits, offer.priceActivityPoints, offer.priceActivityPointsType, offer.giftable, offer.clubLevel, products, offer.bundlePurchaseAllowed); + + if((currentType === CatalogType.NORMAL) || ((purchasableOffer.pricingModel !== Offer.PRICING_MODEL_BUNDLE) && (purchasableOffer.pricingModel !== Offer.PRICING_MODEL_MULTI))) purchasableOffers.push(purchasableOffer); + } + + if(parser.frontPageItems && parser.frontPageItems.length) setFrontPageItems(parser.frontPageItems); + + setIsBusy(false); + + if(pageId === parser.pageId) + { + showCatalogPage(parser.pageId, parser.layoutCode, new PageLocalization(parser.localization.images.concat(), parser.localization.texts.concat()), purchasableOffers, parser.offerId, parser.acceptSeasonCurrencyAsCredits); + } + }); + + useMessageEvent(PurchaseOKMessageEvent, event => + { + const parser = event.getParser(); + + DispatchUiEvent(new CatalogPurchasedEvent(parser.offer)); + }); + + useMessageEvent(PurchaseErrorMessageEvent, event => + { + const parser = event.getParser(); + + DispatchUiEvent(new CatalogPurchaseFailureEvent(parser.code)); + }); + + useMessageEvent(PurchaseNotAllowedMessageEvent, event => + { + const parser = event.getParser(); + + DispatchUiEvent(new CatalogPurchaseNotAllowedEvent(parser.code)); + }); + + useMessageEvent(LimitedEditionSoldOutEvent, event => + { + const parser = event.getParser(); + + DispatchUiEvent(new CatalogPurchaseSoldOutEvent()); + }); + + useMessageEvent(ProductOfferEvent, event => + { + const parser = event.getParser(); + const offerData = parser.offer; + + if(!offerData || !offerData.products.length) return; + + const offerProductData = offerData.products[0]; + + if(offerProductData.uniqueLimitedItem) + { + // update unique + } + + const products: IProduct[] = []; + const productData = GetProductDataForLocalization(offerData.localizationId); + + for(const product of offerData.products) + { + const furnitureData = GetFurnitureData(product.furniClassId, product.productType); + + products.push(new Product(product.productType, product.furniClassId, product.extraParam, product.productCount, productData, furnitureData, product.uniqueLimitedItem, product.uniqueLimitedSeriesSize, product.uniqueLimitedItemsLeft)); + } + + const offer = new Offer(offerData.offerId, offerData.localizationId, offerData.rent, offerData.priceCredits, offerData.priceActivityPoints, offerData.priceActivityPointsType, offerData.giftable, offerData.clubLevel, products, offerData.bundlePurchaseAllowed); + + if(!((currentType === CatalogType.NORMAL) || ((offer.pricingModel !== Offer.PRICING_MODEL_BUNDLE) && (offer.pricingModel !== Offer.PRICING_MODEL_MULTI)))) return; + + offer.page = currentPage; + + setCurrentOffer(offer); + + if(offer.product && (offer.product.productType === ProductTypeEnum.WALL)) + { + setPurchaseOptions(prevValue => + { + const newValue = { ...prevValue }; + + newValue.extraData =( offer.product.extraParam || null); + + return newValue; + }); + } + + // (this._isObjectMoverRequested) && (this._purchasableOffer) + }); + + useMessageEvent(SellablePetPalettesMessageEvent, event => + { + const parser = event.getParser(); + const petPalette = new CatalogPetPalette(parser.productCode, parser.palettes.slice()); + + setCatalogOptions(prevValue => + { + const petPalettes = []; + + if(prevValue.petPalettes) petPalettes.push(...prevValue.petPalettes); + + for(let i = 0; i < petPalettes.length; i++) + { + const palette = petPalettes[i]; + + if(palette.breed === petPalette.breed) + { + petPalettes.splice(i, 1); + + break; + } + } + + petPalettes.push(petPalette); + + return { ...prevValue, petPalettes }; + }); + }); + + useMessageEvent(HabboClubOffersMessageEvent, event => + { + const parser = event.getParser(); + + setCatalogOptions(prevValue => + { + const clubOffers = parser.offers; + + return { ...prevValue, clubOffers }; + }); + }); + + useMessageEvent(GuildMembershipsMessageEvent, event => + { + const parser = event.getParser(); + + setCatalogOptions(prevValue => + { + const groups = parser.groups; + + return { ...prevValue, groups }; + }); + }); + + useMessageEvent(GiftWrappingConfigurationEvent, event => + { + const parser = event.getParser(); + + setCatalogOptions(prevValue => + { + const giftConfiguration = new GiftWrappingConfiguration(parser); + + return { ...prevValue, giftConfiguration }; + }); + }); + + useMessageEvent(MarketplaceMakeOfferResult, event => + { + const parser = event.getParser(); + + if(!parser) return; + + let title = ''; + if(parser.result === 1) + { + title = LocalizeText('inventory.marketplace.result.title.success'); + } + else + { + title = LocalizeText('inventory.marketplace.result.title.failure'); + } + + const message = LocalizeText(`inventory.marketplace.result.${ parser.result }`); + + simpleAlert(message, NotificationAlertType.DEFAULT, null, null, title); + }); + + useMessageEvent(ClubGiftInfoEvent, event => + { + const parser = event.getParser(); + + setCatalogOptions(prevValue => + { + const clubGifts = parser; + + return { ...prevValue, clubGifts }; + }); + }); + + useMessageEvent(CatalogPublishedMessageEvent, event => + { + const wasVisible = isVisible; + + resetState(); + + if(wasVisible) simpleAlert(LocalizeText('catalog.alert.published.description'), NotificationAlertType.ALERT, null, null, LocalizeText('catalog.alert.published.title')); + }); + + useMessageEvent(BuildersClubFurniCountMessageEvent, event => + { + const parser = event.getParser(); + + setFurniCount(parser.furniCount); + + refreshBuilderStatus(); + }); + + useMessageEvent(BuildersClubSubscriptionStatusMessageEvent, event => + { + const parser = event.getParser(); + + setFurniLimit(parser.furniLimit); + setMaxFurniLimit(parser.maxFurniLimit); + setSecondsLeft(parser.secondsLeft); + setUpdateTime(GetTickerTime()); + setSecondsLeftWithGrace(parser.secondsLeftWithGrace); + + refreshBuilderStatus(); + }); + + useUiEvent(CatalogPurchasedEvent.PURCHASE_SUCCESS, event => PlaySound(SoundNames.CREDITS)); + + useRoomEngineEvent(RoomEngineObjectPlacedEvent.PLACED, event => + { + if(!objectMoverRequested || (event.type !== RoomEngineObjectPlacedEvent.PLACED)) return; + + resetPlacedOfferData(true); + + if(!purchasableOffer) + { + resetObjectMover(); + + return; + } + + let placed = false; + + const product = purchasableOffer.product; + + if(event.category === RoomObjectCategory.WALL) + { + switch(product.furnitureData.className) + { + case 'floor': + case 'wallpaper': + case 'landscape': + placed = (event.placedOnFloor || event.placedOnWall); + break; + default: + placed = event.placedInRoom; + break; + } + } + else + { + placed = event.placedInRoom; + } + + if(!placed) + { + resetObjectMover(); + + return; + } + + setPlacedObjectPurchaseData(new PlacedObjectPurchaseData(event.roomId, event.objectId, event.category, event.wallLocation, event.x, event.y, event.direction, purchasableOffer)); + + switch(currentType) + { + case CatalogType.NORMAL: { + switch(event.category) + { + case RoomObjectCategory.FLOOR: + GetRoomEngine().addFurnitureFloor(event.roomId, event.objectId, product.productClassId, new Vector3d(event.x, event.y, event.z), new Vector3d(event.direction), 0, new LegacyDataType()); + break; + case RoomObjectCategory.WALL: { + switch(product.furnitureData.className) + { + case 'floor': + case 'wallpaper': + case 'landscape': + resetRoomPaint(product.furnitureData.className, product.extraParam); + break; + default: + GetRoomEngine().addFurnitureWall(event.roomId, event.objectId, product.productClassId, new Vector3d(event.x, event.y, event.z), new Vector3d(event.direction * 45), 0, event.instanceData, 0); + break; + } + } + } + + const roomObject = GetRoomEngine().getRoomObject(event.roomId, event.objectId, event.category); + + if(roomObject) roomObject.model.setValue(RoomObjectVariable.FURNITURE_ALPHA_MULTIPLIER, 0.5); + + if(catalogSkipPurchaseConfirmation) + { + SendMessageComposer(new PurchaseFromCatalogComposer(pageId, purchasableOffer.offerId, product.extraParam, 1)); + + if(catalogPlaceMultipleObjects) requestOfferToMover(purchasableOffer); + } + else + { + // confirm + + if(catalogPlaceMultipleObjects) requestOfferToMover(purchasableOffer); + } + break; + } + case CatalogType.BUILDER: { + let pageId = purchasableOffer.page.pageId; + + if(pageId === DUMMY_PAGE_ID_FOR_OFFER_SEARCH) + { + pageId = -1; + } + + switch(event.category) + { + case RoomObjectCategory.FLOOR: + SendMessageComposer(new BuildersClubPlaceRoomItemMessageComposer(pageId, purchasableOffer.offerId, product.extraParam, event.x, event.y, event.direction)); + break; + case RoomObjectCategory.WALL: + SendMessageComposer(new BuildersClubPlaceWallItemMessageComposer(pageId, purchasableOffer.offerId, product.extraParam, event.wallLocation)); + break; + } + + if(catalogPlaceMultipleObjects) requestOfferToMover(purchasableOffer); + break; + } + } + }); + + useUiEvent(InventoryFurniAddedEvent.FURNI_ADDED, event => + { + const roomEngine = GetRoomEngine(); + + if(!placedObjectPurchaseData || (placedObjectPurchaseData.productClassId !== event.spriteId) || (placedObjectPurchaseData.roomId !== roomEngine.activeRoomId)) return; + + switch(event.category) + { + case FurniCategory.FLOOR: { + const floorType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_FLOOR_TYPE); + + if(placedObjectPurchaseData.extraParam !== floorType) SendMessageComposer(new FurniturePlacePaintComposer(event.id)); + break; + } + case FurniCategory.WALL_PAPER: { + const wallType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_WALL_TYPE); + + if(placedObjectPurchaseData.extraParam !== wallType) SendMessageComposer(new FurniturePlacePaintComposer(event.id)); + break; + } + case FurniCategory.LANDSCAPE: { + const landscapeType = roomEngine.getRoomInstanceVariable(roomEngine.activeRoomId, RoomObjectVariable.ROOM_LANDSCAPE_TYPE); + + if(placedObjectPurchaseData.extraParam !== landscapeType) SendMessageComposer(new FurniturePlacePaintComposer(event.id)); + break; + } + default: + SendMessageComposer(new FurniturePlaceComposer(event.id, placedObjectPurchaseData.category, placedObjectPurchaseData.wallLocation, placedObjectPurchaseData.x, placedObjectPurchaseData.y, placedObjectPurchaseData.direction)); + } + + if(!catalogPlaceMultipleObjects) resetPlacedOfferData(); + }); + + useEffect(() => + { + return () => setCurrentOffer(null); + }, [ currentPage ]); + + useEffect(() => + { + if(!isVisible || !rootNode || !offersToNodes || !requestedPage.current) return; + + switch(requestedPage.current.requestType) + { + case RequestedPage.REQUEST_TYPE_NONE: + if(currentPage) return; + + if(rootNode.isBranch) + { + for(const child of rootNode.children) + { + if(child && child.isVisible) + { + activateNode(child); + + return; + } + } + } + return; + case RequestedPage.REQUEST_TYPE_ID: + openPageById(requestedPage.current.requestById); + requestedPage.current.resetRequest(); + return; + case RequestedPage.REQUEST_TYPE_OFFER: + openPageByOfferId(requestedPage.current.requestedByOfferId); + requestedPage.current.resetRequest(); + return; + case RequestedPage.REQUEST_TYPE_NAME: + openPageByName(requestedPage.current.requestByName); + requestedPage.current.resetRequest(); + return; + } + }, [ isVisible, rootNode, offersToNodes, currentPage, activateNode, openPageById, openPageByOfferId, openPageByName ]); + + useEffect(() => + { + if(!searchResult && currentPage && (currentPage.pageId === -1)) openPageById(previousPageId); + }, [ searchResult, currentPage, previousPageId, openPageById ]); + + useEffect(() => + { + if(!currentOffer) return; + + setPurchaseOptions({ quantity: 1, extraData: null, extraParamRequired: false, previewStuffData: null }); + }, [ currentOffer ]); + + useEffect(() => + { + if(!isVisible || rootNode) return; + + SendMessageComposer(new GetGiftWrappingConfigurationComposer()); + SendMessageComposer(new GetClubGiftInfo()); + SendMessageComposer(new GetCatalogIndexComposer(currentType)); + SendMessageComposer(new BuildersClubQueryFurniCountMessageComposer()); + }, [ isVisible, rootNode, currentType ]); + + useEffect(() => + { + setRoomPreviewer(new RoomPreviewer(GetRoomEngine(), ++RoomPreviewer.PREVIEW_COUNTER)); + + return () => + { + setRoomPreviewer(prevValue => + { + prevValue.dispose(); + + return null; + }); + } + }, []); + + return { isVisible, setIsVisible, isBusy, pageId, previousPageId, currentType, rootNode, offersToNodes, currentPage, setCurrentPage, currentOffer, setCurrentOffer, activeNodes, searchResult, setSearchResult, frontPageItems, roomPreviewer, navigationHidden, setNavigationHidden, purchaseOptions, setPurchaseOptions, catalogOptions, setCatalogOptions, getNodeById, getNodeByName, activateNode, openPageById, openPageByName, openPageByOfferId, requestOfferToMover }; +} + +export const useCatalog = () => useBetween(useCatalogState); diff --git a/apps/frontend/src/hooks/catalog/useCatalogPlaceMultipleItems.ts b/apps/frontend/src/hooks/catalog/useCatalogPlaceMultipleItems.ts new file mode 100644 index 0000000..39cfd28 --- /dev/null +++ b/apps/frontend/src/hooks/catalog/useCatalogPlaceMultipleItems.ts @@ -0,0 +1,7 @@ +import { useBetween } from 'use-between'; +import { LocalStorageKeys } from '../../api'; +import { useLocalStorage } from '../useLocalStorage'; + +const useCatalogPlaceMultipleItemsState = () => useLocalStorage(LocalStorageKeys.CATALOG_PLACE_MULTIPLE_OBJECTS, false); + +export const useCatalogPlaceMultipleItems = () => useBetween(useCatalogPlaceMultipleItemsState); diff --git a/apps/frontend/src/hooks/catalog/useCatalogSkipPurchaseConfirmation.ts b/apps/frontend/src/hooks/catalog/useCatalogSkipPurchaseConfirmation.ts new file mode 100644 index 0000000..b2d69a2 --- /dev/null +++ b/apps/frontend/src/hooks/catalog/useCatalogSkipPurchaseConfirmation.ts @@ -0,0 +1,7 @@ +import { useBetween } from 'use-between'; +import { LocalStorageKeys } from '../../api'; +import { useLocalStorage } from '../useLocalStorage'; + +const useCatalogSkipPurchaseConfirmationState = () => useLocalStorage(LocalStorageKeys.CATALOG_SKIP_PURCHASE_CONFIRMATION, false); + +export const useCatalogSkipPurchaseConfirmation = () => useBetween(useCatalogSkipPurchaseConfirmationState); diff --git a/apps/frontend/src/hooks/chat-history/index.ts b/apps/frontend/src/hooks/chat-history/index.ts new file mode 100644 index 0000000..970d90f --- /dev/null +++ b/apps/frontend/src/hooks/chat-history/index.ts @@ -0,0 +1 @@ +export * from './useChatHistory'; diff --git a/apps/frontend/src/hooks/chat-history/useChatHistory.ts b/apps/frontend/src/hooks/chat-history/useChatHistory.ts new file mode 100644 index 0000000..a2f3c34 --- /dev/null +++ b/apps/frontend/src/hooks/chat-history/useChatHistory.ts @@ -0,0 +1,104 @@ +import { GetGuestRoomResultEvent, NewConsoleMessageEvent, RoomInviteEvent, RoomSessionEvent } from '@nitrots/nitro-renderer'; +import { useState } from 'react'; +import { useBetween } from 'use-between'; +import { ChatEntryType, ChatHistoryCurrentDate, IChatEntry, IRoomHistoryEntry, MessengerHistoryCurrentDate } from '../../api'; +import { useMessageEvent, useRoomSessionManagerEvent } from '../events'; + +const CHAT_HISTORY_MAX = 1000; +const ROOM_HISTORY_MAX = 10; +const MESSENGER_HISTORY_MAX = 1000; + +let CHAT_HISTORY_COUNTER: number = 0; +let MESSENGER_HISTORY_COUNTER: number = 0; + +const useChatHistoryState = () => +{ + const [ chatHistory, setChatHistory ] = useState([]); + const [ roomHistory, setRoomHistory ] = useState([]); + const [ messengerHistory, setMessengerHistory ] = useState([]); + const [ needsRoomInsert, setNeedsRoomInsert ] = useState(false); + + const addChatEntry = (entry: IChatEntry) => + { + entry.id = CHAT_HISTORY_COUNTER++; + + setChatHistory(prevValue => + { + const newValue = [ ...prevValue ]; + + newValue.push(entry); + + if(newValue.length > CHAT_HISTORY_MAX) newValue.shift(); + + return newValue; + }); + } + + const addRoomHistoryEntry = (entry: IRoomHistoryEntry) => + { + setRoomHistory(prevValue => + { + const newValue = [ ...prevValue ]; + + newValue.push(entry); + + if(newValue.length > ROOM_HISTORY_MAX) newValue.shift(); + + return newValue; + }); + } + + const addMessengerEntry = (entry: IChatEntry) => + { + entry.id = MESSENGER_HISTORY_COUNTER++; + + setMessengerHistory(prevValue => + { + const newValue = [ ...prevValue ]; + + newValue.push(entry); + + if(newValue.length > MESSENGER_HISTORY_MAX) newValue.shift(); + + return newValue; + }); + } + + useRoomSessionManagerEvent(RoomSessionEvent.STARTED, event => setNeedsRoomInsert(true)); + + useMessageEvent(GetGuestRoomResultEvent, event => + { + if(!needsRoomInsert) return; + + const parser = event.getParser(); + + if(roomHistory.length) + { + if(roomHistory[(roomHistory.length - 1)].id === parser.data.roomId) return; + } + + addChatEntry({ id: -1, webId: -1, entityId: -1, name: parser.data.roomName, timestamp: ChatHistoryCurrentDate(), type: ChatEntryType.TYPE_ROOM_INFO, roomId: parser.data.roomId }); + + addRoomHistoryEntry({ id: parser.data.roomId, name: parser.data.roomName }); + + setNeedsRoomInsert(false); + }); + + useMessageEvent(NewConsoleMessageEvent, event => + { + const parser = event.getParser(); + + addMessengerEntry({ id: -1, webId: parser.senderId, entityId: -1, name: '', message: parser.messageText, roomId: -1, timestamp: MessengerHistoryCurrentDate(parser.secondsSinceSent), type: ChatEntryType.TYPE_IM }); + }); + + useMessageEvent(RoomInviteEvent, event => + { + const parser = event.getParser(); + + addMessengerEntry({ id: -1, webId: parser.senderId, entityId: -1, name: '', message: parser.messageText, roomId: -1, timestamp: MessengerHistoryCurrentDate(), type: ChatEntryType.TYPE_IM }); + }); + + return { addChatEntry, chatHistory, roomHistory, messengerHistory }; +} + +export const useChatHistory = () => useBetween(useChatHistoryState); diff --git a/apps/frontend/src/hooks/events/core/index.ts b/apps/frontend/src/hooks/events/core/index.ts new file mode 100644 index 0000000..b69a57e --- /dev/null +++ b/apps/frontend/src/hooks/events/core/index.ts @@ -0,0 +1,2 @@ +export * from './useCommunicationEvent'; +export * from './useConfigurationEvent'; diff --git a/apps/frontend/src/hooks/events/core/useCommunicationEvent.tsx b/apps/frontend/src/hooks/events/core/useCommunicationEvent.tsx new file mode 100644 index 0000000..83d6ed5 --- /dev/null +++ b/apps/frontend/src/hooks/events/core/useCommunicationEvent.tsx @@ -0,0 +1,5 @@ +import { NitroEvent } from '@nitrots/nitro-renderer'; +import { GetCommunication } from '../../../api'; +import { useEventDispatcher } from '../useEventDispatcher'; + +export const useCommunicationEvent = (type: string | string[], handler: (event: T) => void) => useEventDispatcher(type, GetCommunication().events, handler); diff --git a/apps/frontend/src/hooks/events/core/useConfigurationEvent.tsx b/apps/frontend/src/hooks/events/core/useConfigurationEvent.tsx new file mode 100644 index 0000000..617e9a2 --- /dev/null +++ b/apps/frontend/src/hooks/events/core/useConfigurationEvent.tsx @@ -0,0 +1,5 @@ +import { NitroEvent } from '@nitrots/nitro-renderer'; +import { GetConfigurationManager } from '../../../api'; +import { useEventDispatcher } from '../useEventDispatcher'; + +export const useConfigurationEvent = (type: string | string[], handler: (event: T) => void) => useEventDispatcher(type, GetConfigurationManager().events, handler); diff --git a/apps/frontend/src/hooks/events/index.ts b/apps/frontend/src/hooks/events/index.ts new file mode 100644 index 0000000..cafd793 --- /dev/null +++ b/apps/frontend/src/hooks/events/index.ts @@ -0,0 +1,5 @@ +export * from './core'; +export * from './nitro'; +export * from './useEventDispatcher'; +export * from './useMessageEvent'; +export * from './useUiEvent'; diff --git a/apps/frontend/src/hooks/events/nitro/index.ts b/apps/frontend/src/hooks/events/nitro/index.ts new file mode 100644 index 0000000..2248da9 --- /dev/null +++ b/apps/frontend/src/hooks/events/nitro/index.ts @@ -0,0 +1,8 @@ +export * from './useAvatarEvent'; +export * from './useCameraEvent'; +export * from './useLocalizationEvent'; +export * from './useMainEvent'; +export * from './useRoomEngineEvent'; +export * from './useRoomSessionManagerEvent'; +export * from './useSessionDataManagerEvent'; +export * from './useSoundEvent'; diff --git a/apps/frontend/src/hooks/events/nitro/useAvatarEvent.tsx b/apps/frontend/src/hooks/events/nitro/useAvatarEvent.tsx new file mode 100644 index 0000000..9212a68 --- /dev/null +++ b/apps/frontend/src/hooks/events/nitro/useAvatarEvent.tsx @@ -0,0 +1,5 @@ +import { NitroEvent } from '@nitrots/nitro-renderer'; +import { GetAvatarRenderManager } from '../../../api'; +import { useEventDispatcher } from '../useEventDispatcher'; + +export const useAvatarEvent = (type: string | string[], handler: (event: T) => void) => useEventDispatcher(type, GetAvatarRenderManager().events, handler); diff --git a/apps/frontend/src/hooks/events/nitro/useCameraEvent.tsx b/apps/frontend/src/hooks/events/nitro/useCameraEvent.tsx new file mode 100644 index 0000000..42bee39 --- /dev/null +++ b/apps/frontend/src/hooks/events/nitro/useCameraEvent.tsx @@ -0,0 +1,5 @@ +import { NitroEvent } from '@nitrots/nitro-renderer'; +import { GetNitroInstance } from '../../../api'; +import { useEventDispatcher } from '../useEventDispatcher'; + +export const useCameraEvent = (type: string | string[], handler: (event: T) => void) => useEventDispatcher(type, GetNitroInstance().cameraManager.events, handler); diff --git a/apps/frontend/src/hooks/events/nitro/useLocalizationEvent.tsx b/apps/frontend/src/hooks/events/nitro/useLocalizationEvent.tsx new file mode 100644 index 0000000..0dccbdc --- /dev/null +++ b/apps/frontend/src/hooks/events/nitro/useLocalizationEvent.tsx @@ -0,0 +1,5 @@ +import { NitroEvent } from '@nitrots/nitro-renderer'; +import { GetNitroInstance } from '../../../api'; +import { useEventDispatcher } from '../useEventDispatcher'; + +export const useLocalizationEvent = (type: string | string[], handler: (event: T) => void) => useEventDispatcher(type, GetNitroInstance().localization.events, handler); diff --git a/apps/frontend/src/hooks/events/nitro/useMainEvent.tsx b/apps/frontend/src/hooks/events/nitro/useMainEvent.tsx new file mode 100644 index 0000000..8a2ad8b --- /dev/null +++ b/apps/frontend/src/hooks/events/nitro/useMainEvent.tsx @@ -0,0 +1,5 @@ +import { NitroEvent } from '@nitrots/nitro-renderer'; +import { GetNitroInstance } from '../../../api'; +import { useEventDispatcher } from '../useEventDispatcher'; + +export const useMainEvent = (type: string | string[], handler: (event: T) => void) => useEventDispatcher(type, GetNitroInstance().events, handler); diff --git a/apps/frontend/src/hooks/events/nitro/useRoomEngineEvent.tsx b/apps/frontend/src/hooks/events/nitro/useRoomEngineEvent.tsx new file mode 100644 index 0000000..6d2d419 --- /dev/null +++ b/apps/frontend/src/hooks/events/nitro/useRoomEngineEvent.tsx @@ -0,0 +1,5 @@ +import { NitroEvent } from '@nitrots/nitro-renderer'; +import { GetRoomEngine } from '../../../api'; +import { useEventDispatcher } from '../useEventDispatcher'; + +export const useRoomEngineEvent = (type: string | string[], handler: (event: T) => void) => useEventDispatcher(type, GetRoomEngine().events, handler); diff --git a/apps/frontend/src/hooks/events/nitro/useRoomSessionManagerEvent.tsx b/apps/frontend/src/hooks/events/nitro/useRoomSessionManagerEvent.tsx new file mode 100644 index 0000000..5cccb98 --- /dev/null +++ b/apps/frontend/src/hooks/events/nitro/useRoomSessionManagerEvent.tsx @@ -0,0 +1,5 @@ +import { NitroEvent } from '@nitrots/nitro-renderer'; +import { GetRoomSessionManager } from '../../../api'; +import { useEventDispatcher } from '../useEventDispatcher'; + +export const useRoomSessionManagerEvent = (type: string | string[], handler: (event: T) => void) => useEventDispatcher(type, GetRoomSessionManager().events, handler); diff --git a/apps/frontend/src/hooks/events/nitro/useSessionDataManagerEvent.tsx b/apps/frontend/src/hooks/events/nitro/useSessionDataManagerEvent.tsx new file mode 100644 index 0000000..fa49271 --- /dev/null +++ b/apps/frontend/src/hooks/events/nitro/useSessionDataManagerEvent.tsx @@ -0,0 +1,5 @@ +import { NitroEvent } from '@nitrots/nitro-renderer'; +import { GetSessionDataManager } from '../../../api'; +import { useEventDispatcher } from '../useEventDispatcher'; + +export const useSessionDataManagerEvent = (type: string | string, handler: (event: T) => void) => useEventDispatcher(type, GetSessionDataManager().events, handler); diff --git a/apps/frontend/src/hooks/events/nitro/useSoundEvent.tsx b/apps/frontend/src/hooks/events/nitro/useSoundEvent.tsx new file mode 100644 index 0000000..4bfbd5d --- /dev/null +++ b/apps/frontend/src/hooks/events/nitro/useSoundEvent.tsx @@ -0,0 +1,5 @@ +import { NitroEvent } from '@nitrots/nitro-renderer'; +import { GetNitroInstance } from '../../../api'; +import { useEventDispatcher } from '../useEventDispatcher'; + +export const useSoundEvent = (type: string | string[], handler: (event: T) => void, enabled = true) => useEventDispatcher(type, GetNitroInstance().soundManager.events, handler, enabled); diff --git a/apps/frontend/src/hooks/events/useEventDispatcher.tsx b/apps/frontend/src/hooks/events/useEventDispatcher.tsx new file mode 100644 index 0000000..60555f2 --- /dev/null +++ b/apps/frontend/src/hooks/events/useEventDispatcher.tsx @@ -0,0 +1,31 @@ +import { IEventDispatcher, NitroEvent } from '@nitrots/nitro-renderer'; +import { useEffect } from 'react'; + +export const useEventDispatcher = (type: string | string[], eventDispatcher: IEventDispatcher, handler: (event: T) => void, enabled: boolean = true) => +{ + useEffect(() => + { + if(!enabled) return; + + if(Array.isArray(type)) + { + type.map(name => eventDispatcher.addEventListener(name, handler)); + } + else + { + eventDispatcher.addEventListener(type, handler); + } + + return () => + { + if(Array.isArray(type)) + { + type.map(name => eventDispatcher.removeEventListener(name, handler)); + } + else + { + eventDispatcher.removeEventListener(type, handler); + } + } + }, [ type, eventDispatcher, enabled, handler ]); +} diff --git a/apps/frontend/src/hooks/events/useMessageEvent.tsx b/apps/frontend/src/hooks/events/useMessageEvent.tsx new file mode 100644 index 0000000..3a62ebe --- /dev/null +++ b/apps/frontend/src/hooks/events/useMessageEvent.tsx @@ -0,0 +1,16 @@ +import { IMessageEvent, MessageEvent } from '@nitrots/nitro-renderer'; +import { useEffect } from 'react'; +import { GetCommunication } from '../../api'; + +export const useMessageEvent = (eventType: typeof MessageEvent, handler: (event: T) => void) => +{ + useEffect(() => + { + //@ts-ignore + const event = new eventType(handler); + + GetCommunication().registerMessageEvent(event); + + return () => GetCommunication().removeMessageEvent(event); + }, [ eventType, handler ]); +} diff --git a/apps/frontend/src/hooks/events/useUiEvent.tsx b/apps/frontend/src/hooks/events/useUiEvent.tsx new file mode 100644 index 0000000..02c0aff --- /dev/null +++ b/apps/frontend/src/hooks/events/useUiEvent.tsx @@ -0,0 +1,5 @@ +import { NitroEvent } from '@nitrots/nitro-renderer'; +import { UI_EVENT_DISPATCHER } from '../../api'; +import { useEventDispatcher } from './useEventDispatcher'; + +export const useUiEvent = (type: string | string[], handler: (event: T) => void, enabled: boolean = true) => useEventDispatcher(type, UI_EVENT_DISPATCHER, handler, enabled); diff --git a/apps/frontend/src/hooks/friends/index.ts b/apps/frontend/src/hooks/friends/index.ts new file mode 100644 index 0000000..45c983f --- /dev/null +++ b/apps/frontend/src/hooks/friends/index.ts @@ -0,0 +1,2 @@ +export * from './useFriends'; +export * from './useMessenger'; diff --git a/apps/frontend/src/hooks/friends/useFriends.ts b/apps/frontend/src/hooks/friends/useFriends.ts new file mode 100644 index 0000000..05437b2 --- /dev/null +++ b/apps/frontend/src/hooks/friends/useFriends.ts @@ -0,0 +1,266 @@ +import { AcceptFriendMessageComposer, DeclineFriendMessageComposer, FollowFriendMessageComposer, FriendListFragmentEvent, FriendListUpdateComposer, FriendListUpdateEvent, FriendParser, FriendRequestsEvent, GetFriendRequestsComposer, MessengerInitComposer, MessengerInitEvent, NewFriendRequestEvent, RequestFriendComposer, SetRelationshipStatusComposer } from '@nitrots/nitro-renderer'; +import { useEffect, useMemo, useState } from 'react'; +import { useBetween } from 'use-between'; +import { CloneObject, GetSessionDataManager, MessengerFriend, MessengerRequest, MessengerSettings, SendMessageComposer } from '../../api'; +import { useMessageEvent } from '../events'; + +const useFriendsState = () => +{ + const [ friends, setFriends ] = useState([]); + const [ requests, setRequests ] = useState([]); + const [ sentRequests, setSentRequests ] = useState([]); + const [ dismissedRequestIds, setDismissedRequestIds ] = useState([]); + const [ settings, setSettings ] = useState(null); + + const onlineFriends = useMemo(() => + { + const onlineFriends = friends.filter(friend => friend.online); + + onlineFriends.sort((a, b) => + { + if( a.name < b.name ) return -1; + + if( a.name > b.name ) return 1; + + return 0; + }); + + return onlineFriends; + }, [ friends ]); + + const offlineFriends = useMemo(() => + { + const offlineFriends = friends.filter(friend => !friend.online); + + offlineFriends.sort((a, b) => + { + if( a.name < b.name ) return -1; + + if( a.name > b.name ) return 1; + + return 0; + }); + + return offlineFriends; + }, [ friends ]); + + const followFriend = (friend: MessengerFriend) => SendMessageComposer(new FollowFriendMessageComposer(friend.id)); + + const updateRelationship = (friend: MessengerFriend, type: number) => ((type !== friend.relationshipStatus) && SendMessageComposer(new SetRelationshipStatusComposer(friend.id, type))); + + const getFriend = (userId: number) => + { + for(const friend of friends) + { + if(friend.id === userId) return friend; + } + + return null; + } + + const canRequestFriend = (userId: number) => + { + if(userId === GetSessionDataManager().userId) return false; + + if(getFriend(userId)) return false; + + if(requests.find(request => (request.requesterUserId === userId))) return false; + + if(sentRequests.indexOf(userId) >= 0) return false; + + return true; + } + + const requestFriend = (userId: number, userName: string) => + { + if(!canRequestFriend(userId)) return false; + + setSentRequests(prevValue => + { + const newSentRequests = [ ...prevValue ]; + + newSentRequests.push(userId); + + return newSentRequests; + }); + + SendMessageComposer(new RequestFriendComposer(userName)); + } + + const requestResponse = (requestId: number, flag: boolean) => + { + if(requestId === -1 && !flag) + { + SendMessageComposer(new DeclineFriendMessageComposer(true)); + + setRequests([]); + } + else + { + setRequests(prevValue => + { + const newRequests = [ ...prevValue ]; + const index = newRequests.findIndex(request => (request.id === requestId)); + + if(index === -1) return prevValue; + + if(flag) + { + SendMessageComposer(new AcceptFriendMessageComposer(newRequests[index].id)); + } + else + { + SendMessageComposer(new DeclineFriendMessageComposer(false, newRequests[index].id)); + } + + newRequests.splice(index, 1); + + return newRequests; + }); + } + } + + useMessageEvent(MessengerInitEvent, event => + { + const parser = event.getParser(); + + setSettings(new MessengerSettings( + parser.userFriendLimit, + parser.normalFriendLimit, + parser.extendedFriendLimit, + parser.categories)); + + SendMessageComposer(new GetFriendRequestsComposer()); + }); + + useMessageEvent(FriendListFragmentEvent, event => + { + const parser = event.getParser(); + + setFriends(prevValue => + { + const newValue = [ ...prevValue ]; + + for(const friend of parser.fragment) + { + const index = newValue.findIndex(existingFriend => (existingFriend.id === friend.id)); + const newFriend = new MessengerFriend(); + newFriend.populate(friend); + + if(index > -1) newValue[index] = newFriend; + else newValue.push(newFriend); + } + + return newValue; + }); + }); + + useMessageEvent(FriendListUpdateEvent, event => + { + const parser = event.getParser(); + + setFriends(prevValue => + { + const newValue = [ ...prevValue ]; + + const processUpdate = (friend: FriendParser) => + { + const index = newValue.findIndex(existingFriend => (existingFriend.id === friend.id)); + + if(index === -1) + { + const newFriend = new MessengerFriend(); + newFriend.populate(friend); + + newValue.unshift(newFriend); + } + else + { + newValue[index].populate(friend); + } + } + + for(const friend of parser.addedFriends) processUpdate(friend); + + for(const friend of parser.updatedFriends) processUpdate(friend); + + for(const removedFriendId of parser.removedFriendIds) + { + const index = newValue.findIndex(existingFriend => (existingFriend.id === removedFriendId)); + + if(index > -1) newValue.splice(index, 1); + } + + return newValue; + }); + }); + + useMessageEvent(FriendRequestsEvent, event => + { + const parser = event.getParser(); + + setRequests(prevValue => + { + const newValue = [ ...prevValue ]; + + for(const request of parser.requests) + { + const index = newValue.findIndex(existing => (existing.requesterUserId === request.requesterUserId)); + + if(index > 0) + { + newValue[index] = CloneObject(newValue[index]); + newValue[index].populate(request); + } + else + { + const newRequest = new MessengerRequest(); + newRequest.populate(request); + + newValue.push(newRequest); + } + } + + return newValue; + }); + }); + + useMessageEvent(NewFriendRequestEvent, event => + { + const parser = event.getParser(); + const request = parser.request; + + setRequests(prevValue => + { + const newRequests = [ ...prevValue ]; + + const index = newRequests.findIndex(existing => (existing.requesterUserId === request.requesterUserId)); + + if(index === -1) + { + const newRequest = new MessengerRequest(); + newRequest.populate(request); + + newRequests.push(newRequest); + } + + return newRequests; + }); + }); + + useEffect(() => + { + SendMessageComposer(new MessengerInitComposer()); + + const interval = setInterval(() => SendMessageComposer(new FriendListUpdateComposer()), 120000); + + return () => + { + clearInterval(interval); + } + }, []); + + return { friends, requests, sentRequests, dismissedRequestIds, setDismissedRequestIds, settings, onlineFriends, offlineFriends, getFriend, canRequestFriend, requestFriend, requestResponse, followFriend, updateRelationship }; +} + +export const useFriends = () => useBetween(useFriendsState); diff --git a/apps/frontend/src/hooks/friends/useMessenger.ts b/apps/frontend/src/hooks/friends/useMessenger.ts new file mode 100644 index 0000000..a48b6cf --- /dev/null +++ b/apps/frontend/src/hooks/friends/useMessenger.ts @@ -0,0 +1,187 @@ +import { NewConsoleMessageEvent, RoomInviteErrorEvent, RoomInviteEvent, SendMessageComposer as SendMessageComposerPacket } from '@nitrots/nitro-renderer'; +import { useEffect, useMemo, useState } from 'react'; +import { useBetween } from 'use-between'; +import { CloneObject, GetSessionDataManager, LocalizeText, MessengerIconState, MessengerThread, MessengerThreadChat, NotificationAlertType, PlaySound, SendMessageComposer, SoundNames } from '../../api'; +import { useMessageEvent } from '../events'; +import { useNotification } from '../notification'; +import { useFriends } from './useFriends'; + +const useMessengerState = () => +{ + const [ messageThreads, setMessageThreads ] = useState([]); + const [ activeThreadId, setActiveThreadId ] = useState(-1); + const [ hiddenThreadIds, setHiddenThreadIds ] = useState([]); + const [ iconState, setIconState ] = useState(MessengerIconState.HIDDEN); + const { getFriend = null } = useFriends(); + const { simpleAlert = null } = useNotification(); + + const visibleThreads = useMemo(() => messageThreads.filter(thread => (hiddenThreadIds.indexOf(thread.threadId) === -1)), [ messageThreads, hiddenThreadIds ]); + const activeThread = useMemo(() => ((activeThreadId > 0) && visibleThreads.find(thread => (thread.threadId === activeThreadId) || null)), [ activeThreadId, visibleThreads ]); + + const getMessageThread = (userId: number) => + { + let thread = messageThreads.find(thread => (thread.participant && (thread.participant.id === userId))); + + if(!thread) + { + const friend = getFriend(userId); + + if(!friend) return null; + + thread = new MessengerThread(friend); + + thread.addMessage(null, LocalizeText('messenger.moderationinfo'), 0, null, MessengerThreadChat.SECURITY_NOTIFICATION); + + thread.setRead(); + + setMessageThreads(prevValue => + { + const newValue = [ ...prevValue ]; + + newValue.push(thread); + + return newValue; + }); + } + else + { + const hiddenIndex = hiddenThreadIds.indexOf(thread.threadId); + + if(hiddenIndex >= 0) + { + setHiddenThreadIds(prevValue => + { + const newValue = [ ...prevValue ]; + + newValue.splice(hiddenIndex, 1); + + return newValue; + }) + } + } + + return thread; + } + + const closeThread = (threadId: number) => + { + setHiddenThreadIds(prevValue => + { + const newValue = [ ...prevValue ]; + + if(newValue.indexOf(threadId) >= 0) return prevValue; + + newValue.push(threadId); + + return newValue; + }); + + if(activeThreadId === threadId) setActiveThreadId(-1); + } + + const sendMessage = (thread: MessengerThread, senderId: number, messageText: string, secondsSinceSent: number = 0, extraData: string = null, messageType: number = MessengerThreadChat.CHAT) => + { + if(!thread || !messageText || !messageText.length) return; + + const ownMessage = (senderId === GetSessionDataManager().userId); + + if(ownMessage && (messageText.length <= 255)) SendMessageComposer(new SendMessageComposerPacket(thread.participant.id, messageText)); + + setMessageThreads(prevValue => + { + const newValue = [ ...prevValue ]; + const index = newValue.findIndex(newThread => (newThread.threadId === thread.threadId)); + + if(index === -1) return prevValue; + + thread = CloneObject(newValue[index]); + + if(ownMessage && (thread.groups.length === 1)) PlaySound(SoundNames.MESSENGER_NEW_THREAD); + + thread.addMessage(((messageType === MessengerThreadChat.ROOM_INVITE) ? null : senderId), messageText, secondsSinceSent, extraData, messageType); + + if(activeThreadId === thread.threadId) thread.setRead(); + + newValue[index] = thread; + + if(!ownMessage && thread.unread) PlaySound(SoundNames.MESSENGER_MESSAGE_RECEIVED); + + return newValue; + }); + } + + useMessageEvent(NewConsoleMessageEvent, event => + { + const parser = event.getParser(); + const thread = getMessageThread(parser.senderId); + + if(!thread) return; + + sendMessage(thread, parser.senderId, parser.messageText, parser.secondsSinceSent, parser.extraData); + }); + + useMessageEvent(RoomInviteEvent, event => + { + const parser = event.getParser(); + const thread = getMessageThread(parser.senderId); + + if(!thread) return; + + sendMessage(thread, parser.senderId, parser.messageText, 0, null, MessengerThreadChat.ROOM_INVITE); + }); + + useMessageEvent(RoomInviteErrorEvent, event => + { + const parser = event.getParser(); + + simpleAlert(`Received room invite error: ${ parser.errorCode },recipients: ${ parser.failedRecipients }`, NotificationAlertType.DEFAULT, null, null, LocalizeText('friendlist.alert.title')); + }); + + useEffect(() => + { + if(activeThreadId <= 0) return; + + setMessageThreads(prevValue => + { + const newValue = [ ...prevValue ]; + const index = newValue.findIndex(newThread => (newThread.threadId === activeThreadId)); + + if(index >= 0) + { + newValue[index] = CloneObject(newValue[index]); + + newValue[index].setRead(); + } + + return newValue; + }); + }, [ activeThreadId ]); + + useEffect(() => + { + setIconState(prevValue => + { + if(!visibleThreads.length) return MessengerIconState.HIDDEN; + + let isUnread = false; + + for(const thread of visibleThreads) + { + if(thread.unreadCount > 0) + { + isUnread = true; + + break; + } + } + + if(isUnread) return MessengerIconState.UNREAD; + + return MessengerIconState.SHOW; + }); + }, [ visibleThreads ]); + + return { messageThreads, activeThread, iconState, visibleThreads, getMessageThread, setActiveThreadId, closeThread, sendMessage }; +} + +export const useMessenger = () => useBetween(useMessengerState); diff --git a/apps/frontend/src/hooks/game-center/index.ts b/apps/frontend/src/hooks/game-center/index.ts new file mode 100644 index 0000000..59f0472 --- /dev/null +++ b/apps/frontend/src/hooks/game-center/index.ts @@ -0,0 +1 @@ +export * from './useGameCenter'; diff --git a/apps/frontend/src/hooks/game-center/useGameCenter.ts b/apps/frontend/src/hooks/game-center/useGameCenter.ts new file mode 100644 index 0000000..0b9dbcb --- /dev/null +++ b/apps/frontend/src/hooks/game-center/useGameCenter.ts @@ -0,0 +1,83 @@ +import { Game2AccountGameStatusMessageEvent, Game2AccountGameStatusMessageParser, GameConfigurationData, GameListMessageEvent, GameStatusMessageEvent, GetGameListMessageComposer, LoadGameUrlEvent } from '@nitrots/nitro-renderer'; +import { useEffect, useState } from 'react'; +import { useBetween } from 'use-between'; +import { SendMessageComposer, VisitDesktop } from '../../api'; +import { useMessageEvent } from '../events'; + +const useGameCenterState = () => +{ + const [ isVisible, setIsVisible ] = useState(false); + const [ games, setGames ] = useState(null); + const [ selectedGame, setSelectedGame ] = useState(null); + const [ accountStatus, setAccountStatus ] = useState(null); + const [ gameOffline, setGameOffline ] = useState(false); + const [ gameURL, setGameURL ] = useState(null); + + useMessageEvent(GameListMessageEvent, event => + { + let parser = event.getParser(); + + if(!parser || parser && !parser.games.length) return; + + setSelectedGame(parser.games[0]); + + setGames(parser.games); + }); + + useMessageEvent(Game2AccountGameStatusMessageEvent, event => + { + let parser = event.getParser(); + + if(!parser) return; + + setAccountStatus(parser); + }); + + useMessageEvent(GameStatusMessageEvent, event => + { + let parser = event.getParser(); + + if(!parser) return; + + setGameOffline(parser.isInMaintenance); + }) + + useMessageEvent(LoadGameUrlEvent, event => + { + let parser = event.getParser(); + + if(!parser) return; + + switch(parser.gameTypeId) + { + case 2: + return console.log('snowwar') + default: + return setGameURL(parser.url); + } + }); + + useEffect(()=> + { + if(isVisible) + { + SendMessageComposer(new GetGameListMessageComposer()); + VisitDesktop(); + } + else + { + // dispose or wtv + } + },[ isVisible ]); + + return { + isVisible, setIsVisible, + games, + accountStatus, + selectedGame, setSelectedGame, + gameOffline, + gameURL, setGameURL + } +} + +export const useGameCenter = () => useBetween(useGameCenterState); diff --git a/apps/frontend/src/hooks/groups/index.ts b/apps/frontend/src/hooks/groups/index.ts new file mode 100644 index 0000000..95ef733 --- /dev/null +++ b/apps/frontend/src/hooks/groups/index.ts @@ -0,0 +1 @@ +export * from './useGroup'; diff --git a/apps/frontend/src/hooks/groups/useGroup.ts b/apps/frontend/src/hooks/groups/useGroup.ts new file mode 100644 index 0000000..7a9fd22 --- /dev/null +++ b/apps/frontend/src/hooks/groups/useGroup.ts @@ -0,0 +1,55 @@ +import { GroupBadgePartsComposer, GroupBadgePartsEvent } from '@nitrots/nitro-renderer'; +import { useEffect, useState } from 'react'; +import { useBetween } from 'use-between'; +import { IGroupCustomize, SendMessageComposer } from '../../api'; +import { useMessageEvent } from '../events'; + +const useGroupState = () => +{ + const [ groupCustomize, setGroupCustomize ] = useState(null); + + useMessageEvent(GroupBadgePartsEvent, event => + { + const parser = event.getParser(); + + const customize: IGroupCustomize = { + badgeBases: [], + badgeSymbols: [], + badgePartColors: [], + groupColorsA: [], + groupColorsB: [] + }; + + parser.bases.forEach((images, id) => customize.badgeBases.push({ id, images })); + parser.symbols.forEach((images, id) => customize.badgeSymbols.push({ id, images })); + parser.partColors.forEach((color, id) => customize.badgePartColors.push({ id, color })); + parser.colorsA.forEach((color, id) => customize.groupColorsA.push({ id, color })); + parser.colorsB.forEach((color, id) => customize.groupColorsB.push({ id, color })); + + const CompareId = (a: { id: number }, b: { id: number }) => + { + if(a.id < b.id) return -1; + + if(a.id > b.id) return 1; + + return 0; + } + + customize.badgeBases.sort(CompareId); + customize.badgeSymbols.sort(CompareId); + customize.badgePartColors.sort(CompareId); + customize.groupColorsA.sort(CompareId); + customize.groupColorsB.sort(CompareId); + + setGroupCustomize(customize); + }); + + useEffect(() => + { + SendMessageComposer(new GroupBadgePartsComposer()); + }, []); + + return { groupCustomize }; +} + +export const useGroup = () => useBetween(useGroupState); diff --git a/apps/frontend/src/hooks/help/index.ts b/apps/frontend/src/hooks/help/index.ts new file mode 100644 index 0000000..f03e754 --- /dev/null +++ b/apps/frontend/src/hooks/help/index.ts @@ -0,0 +1 @@ +export * from './useHelp'; diff --git a/apps/frontend/src/hooks/help/useHelp.ts b/apps/frontend/src/hooks/help/useHelp.ts new file mode 100644 index 0000000..1c74b62 --- /dev/null +++ b/apps/frontend/src/hooks/help/useHelp.ts @@ -0,0 +1,149 @@ +import { CallForHelpDisabledNotifyMessageEvent, CallForHelpPendingCallsDeletedMessageEvent, CallForHelpPendingCallsMessageEvent, CallForHelpReplyMessageEvent, CallForHelpResultMessageEvent, DeletePendingCallsForHelpMessageComposer, GetPendingCallsForHelpMessageComposer, IssueCloseNotificationMessageEvent, SanctionStatusEvent, SanctionStatusMessageParser } from '@nitrots/nitro-renderer'; +import { useState } from 'react'; +import { useBetween } from 'use-between'; +import { CallForHelpResult, GetCloseReasonKey, IHelpReport, LocalizeText, NotificationAlertType, ReportState, ReportType, SendMessageComposer } from '../../api'; +import { useMessageEvent } from '../events'; +import { useNotification } from '../notification'; + +const useHelpState = () => +{ + const [ activeReport, setActiveReport ] = useState(null); + const [ sanctionInfo, setSanctionInfo ] = useState(null); + const { simpleAlert = null, showConfirm = null } = useNotification(); + + const report = (type: number, options: Partial) => + { + const newReport: IHelpReport = { + reportType: type, + reportedUserId: -1, + reportedChats: [], + cfhCategory: -1, + cfhTopic: -1, + roomId: -1, + roomName: '', + messageId: -1, + threadId: -1, + groupId: -1, + extraData: '', + roomObjectId: -1, + message: '', + currentStep: 0 + }; + + switch(type) + { + case ReportType.BULLY: + case ReportType.EMERGENCY: + case ReportType.IM: + newReport.reportedUserId = options.reportedUserId; + newReport.currentStep = ReportState.SELECT_CHATS; + break; + case ReportType.ROOM: + newReport.roomId = options.roomId; + newReport.roomName = options.roomName; + newReport.currentStep = ReportState.SELECT_TOPICS; + break; + case ReportType.THREAD: + newReport.groupId = options.groupId; + newReport.threadId = options.threadId; + newReport.currentStep = ReportState.SELECT_TOPICS; + break; + case ReportType.MESSAGE: + newReport.groupId = options.groupId; + newReport.threadId = options.threadId; + newReport.messageId = options.messageId; + newReport.currentStep = ReportState.SELECT_TOPICS; + break; + case ReportType.PHOTO: + newReport.extraData = options.extraData; + newReport.roomId = options.roomId; + newReport.reportedUserId = options.reportedUserId; + newReport.roomObjectId = options.roomObjectId; + newReport.currentStep = ReportState.SELECT_TOPICS; + break; + case ReportType.GUIDE: + break; + } + + setActiveReport(newReport); + } + + useMessageEvent(CallForHelpResultMessageEvent, event => + { + const parser = event.getParser(); + + let message = parser.messageText; + + switch(parser.resultType) + { + case CallForHelpResult.TOO_MANY_PENDING_CALLS_CODE: + SendMessageComposer(new GetPendingCallsForHelpMessageComposer()); + simpleAlert(LocalizeText('help.cfh.error.pending'), NotificationAlertType.MODERATION, null, null, LocalizeText('help.cfh.error.title')); + break; + case CallForHelpResult.HAS_ABUSIVE_CALL_CODE: + simpleAlert(LocalizeText('help.cfh.error.abusive'), NotificationAlertType.MODERATION, null, null, LocalizeText('help.cfh.error.title')); + break; + default: + if(message.trim().length === 0) + { + message = LocalizeText('help.cfh.sent.text'); + } + + simpleAlert(message, NotificationAlertType.MODERATION, null, null, LocalizeText('help.cfh.sent.title')); + } + }); + + useMessageEvent(IssueCloseNotificationMessageEvent, event => + { + const parser = event.getParser(); + + const message = parser.messageText.length === 0 ? LocalizeText('help.cfh.closed.' + GetCloseReasonKey(parser.closeReason)) : parser.messageText; + + simpleAlert(message, NotificationAlertType.MODERATION, null, null, LocalizeText('mod.alert.title')); + }); + + useMessageEvent(CallForHelpPendingCallsMessageEvent, event => + { + const parser = event.getParser(); + + if(parser.count > 0) + { + showConfirm(LocalizeText('help.emergency.pending.title') + '\n' + parser.pendingCalls[0].message, () => + { + SendMessageComposer(new DeletePendingCallsForHelpMessageComposer()); + }, null, LocalizeText('help.emergency.pending.button.discard'), LocalizeText('help.emergency.pending.button.keep'), LocalizeText('help.emergency.pending.message.subtitle')); + } + }); + + useMessageEvent(CallForHelpPendingCallsDeletedMessageEvent, event => + { + const message = 'Your pending calls were deleted'; // todo: add localization + + simpleAlert(message, NotificationAlertType.MODERATION, null, null, LocalizeText('mod.alert.title')); + }); + + useMessageEvent(CallForHelpReplyMessageEvent, event => + { + const parser = event.getParser(); + + simpleAlert(parser.message, NotificationAlertType.MODERATION, null, null, LocalizeText('help.cfh.reply.title')); + }); + + useMessageEvent(CallForHelpDisabledNotifyMessageEvent, event => + { + const parser = event.getParser(); + + simpleAlert(LocalizeText('help.emergency.global_mute.message'), NotificationAlertType.MODERATION, parser.infoUrl, LocalizeText('help.emergency.global_mute.link'), LocalizeText('help.emergency.global_mute.subtitle')) + }); + + useMessageEvent(SanctionStatusEvent, event => + { + const parser = event.getParser(); + + setSanctionInfo(parser); + }); + + return { activeReport, setActiveReport, sanctionInfo, setSanctionInfo, report }; +} + +export const useHelp = () => useBetween(useHelpState); diff --git a/apps/frontend/src/hooks/index.ts b/apps/frontend/src/hooks/index.ts new file mode 100644 index 0000000..a551574 --- /dev/null +++ b/apps/frontend/src/hooks/index.ts @@ -0,0 +1,22 @@ +export * from './achievements'; +export * from './camera'; +export * from './catalog'; +export * from './chat-history'; +export * from './events'; +export * from './events/core'; +export * from './events/nitro'; +export * from './friends'; +export * from './game-center'; +export * from './groups'; +export * from './help'; +export * from './inventory'; +export * from './mod-tools'; +export * from './navigator'; +export * from './notification'; +export * from './purse'; +export * from './rooms'; +export * from './session'; +export * from './useLocalStorage'; +export * from './UseMountEffect'; +export * from './useSharedVisibility'; +export * from './wired'; diff --git a/apps/frontend/src/hooks/inventory/index.ts b/apps/frontend/src/hooks/inventory/index.ts new file mode 100644 index 0000000..4e70819 --- /dev/null +++ b/apps/frontend/src/hooks/inventory/index.ts @@ -0,0 +1,6 @@ +export * from './useInventoryBadges'; +export * from './useInventoryBots'; +export * from './useInventoryFurni'; +export * from './useInventoryPets'; +export * from './useInventoryTrade'; +export * from './useInventoryUnseenTracker'; diff --git a/apps/frontend/src/hooks/inventory/useInventoryBadges.ts b/apps/frontend/src/hooks/inventory/useInventoryBadges.ts new file mode 100644 index 0000000..000e4e3 --- /dev/null +++ b/apps/frontend/src/hooks/inventory/useInventoryBadges.ts @@ -0,0 +1,152 @@ +import { BadgeReceivedEvent, BadgesEvent, RequestBadgesComposer, SetActivatedBadgesComposer } from '@nitrots/nitro-renderer'; +import { useEffect, useState } from 'react'; +import { useBetween } from 'use-between'; +import { GetConfiguration, SendMessageComposer, UnseenItemCategory } from '../../api'; +import { useMessageEvent } from '../events'; +import { useSharedVisibility } from '../useSharedVisibility'; +import { useInventoryUnseenTracker } from './useInventoryUnseenTracker'; + +const useInventoryBadgesState = () => +{ + const [ needsUpdate, setNeedsUpdate ] = useState(true); + const [ badgeCodes, setBadgeCodes ] = useState([]); + const [ badgeIds, setBadgeIds ] = useState>(new Map()); + const [ activeBadgeCodes, setActiveBadgeCodes ] = useState([]); + const [ selectedBadgeCode, setSelectedBadgeCode ] = useState(null); + const { isVisible = false, activate = null, deactivate = null } = useSharedVisibility(); + const { isUnseen = null, resetCategory = null } = useInventoryUnseenTracker(); + + const maxBadgeCount = GetConfiguration('user.badges.max.slots', 5); + const isWearingBadge = (badgeCode: string) => (activeBadgeCodes.indexOf(badgeCode) >= 0); + const canWearBadges = () => (activeBadgeCodes.length < maxBadgeCount); + + const toggleBadge = (badgeCode: string) => + { + setActiveBadgeCodes(prevValue => + { + const newValue = [ ...prevValue ]; + + const index = newValue.indexOf(badgeCode); + + if(index === -1) + { + if(!canWearBadges()) return prevValue; + + newValue.push(badgeCode); + } + else + { + newValue.splice(index, 1); + } + + const composer = new SetActivatedBadgesComposer(); + + for(let i = 0; i < maxBadgeCount; i++) composer.addActivatedBadge(newValue[i] ?? ''); + + SendMessageComposer(composer); + + return newValue; + }); + } + + const getBadgeId = (badgeCode: string) => + { + const index = badgeCodes.indexOf(badgeCode); + + if(index === -1) return 0; + + return (badgeIds.get(badgeCode) ?? 0); + } + + useMessageEvent(BadgesEvent, event => + { + const parser = event.getParser(); + const badgesToAdd: string[] = []; + + setBadgeIds(prevValue => + { + const newValue = new Map(prevValue); + + parser.getAllBadgeCodes().forEach(code => + { + const exists = badgeCodes.indexOf(code) >= 0; + const badgeId = parser.getBadgeId(code); + + newValue.set(code, badgeId); + + if(exists) return; + + badgesToAdd.push(code); + }); + + return newValue; + }); + + setActiveBadgeCodes(parser.getActiveBadgeCodes()); + setBadgeCodes(prev => [ ...prev, ...badgesToAdd ]); + }); + + useMessageEvent(BadgeReceivedEvent, event => + { + const parser = event.getParser(); + const unseen = isUnseen(UnseenItemCategory.BADGE, parser.badgeId); + + setBadgeCodes(prevValue => + { + const newValue = [ ...prevValue ]; + + if(unseen) newValue.unshift(parser.badgeCode) + else newValue.push(parser.badgeCode); + + return newValue; + }); + + setBadgeIds(prevValue => + { + const newValue = new Map(prevValue); + + newValue.set(parser.badgeCode, parser.badgeId); + + return newValue; + }); + }); + + useEffect(() => + { + if(!badgeCodes || !badgeCodes.length) return; + + setSelectedBadgeCode(prevValue => + { + let newValue = prevValue; + + if(newValue && (badgeCodes.indexOf(newValue) === -1)) newValue = null; + + if(!newValue) newValue = badgeCodes[0]; + + return newValue; + }); + }, [ badgeCodes ]); + + useEffect(() => + { + if(!isVisible) return; + + return () => + { + resetCategory(UnseenItemCategory.BADGE); + } + }, [ isVisible, resetCategory ]); + + useEffect(() => + { + if(!isVisible || !needsUpdate) return; + + SendMessageComposer(new RequestBadgesComposer()); + + setNeedsUpdate(false); + }, [ isVisible, needsUpdate ]); + + return { badgeCodes, activeBadgeCodes, selectedBadgeCode, setSelectedBadgeCode, isWearingBadge, canWearBadges, toggleBadge, getBadgeId, activate, deactivate }; +} + +export const useInventoryBadges = () => useBetween(useInventoryBadgesState); diff --git a/apps/frontend/src/hooks/inventory/useInventoryBots.ts b/apps/frontend/src/hooks/inventory/useInventoryBots.ts new file mode 100644 index 0000000..aba5d39 --- /dev/null +++ b/apps/frontend/src/hooks/inventory/useInventoryBots.ts @@ -0,0 +1,158 @@ +import { BotAddedToInventoryEvent, BotData, BotInventoryMessageEvent, BotRemovedFromInventoryEvent, GetBotInventoryComposer } from '@nitrots/nitro-renderer'; +import { useEffect, useState } from 'react'; +import { useBetween } from 'use-between'; +import { cancelRoomObjectPlacement, CreateLinkEvent, getPlacingItemId, IBotItem, SendMessageComposer, UnseenItemCategory } from '../../api'; +import { useMessageEvent } from '../events'; +import { useSharedVisibility } from '../useSharedVisibility'; +import { useInventoryUnseenTracker } from './useInventoryUnseenTracker'; + +const useInventoryBotsState = () => +{ + const [ needsUpdate, setNeedsUpdate ] = useState(true); + const [ botItems, setBotItems ] = useState([]); + const [ selectedBot, setSelectedBot ] = useState(null); + const { isVisible = false, activate = null, deactivate = null } = useSharedVisibility(); + const { isUnseen = null, resetCategory = null } = useInventoryUnseenTracker(); + + useMessageEvent(BotInventoryMessageEvent, event => + { + const parser = event.getParser(); + + setBotItems(prevValue => + { + const newValue = [ ...prevValue ]; + const existingIds = newValue.map(item => item.botData.id); + const addedDatas: BotData[] = []; + + for(const botData of parser.items.values()) ((existingIds.indexOf(botData.id) === -1) && addedDatas.push(botData)); + + for(const existingId of existingIds) + { + let remove = true; + + for(const botData of parser.items.values()) + { + if(botData.id === existingId) + { + remove = false; + + break; + } + } + + if(!remove) continue; + + const index = newValue.findIndex(item => (item.botData.id === existingId)); + const botItem = newValue[index]; + + if((index === -1) || !botItem) continue; + + if(getPlacingItemId() === botItem.botData.id) + { + cancelRoomObjectPlacement(); + + CreateLinkEvent('inventory/open'); + } + + newValue.splice(index, 1); + } + + for(const botData of addedDatas) + { + const botItem = { botData } as IBotItem; + const unseen = isUnseen(UnseenItemCategory.BOT, botData.id); + + if(unseen) newValue.unshift(botItem); + else newValue.push(botItem); + } + + return newValue; + }); + }); + + useMessageEvent(BotAddedToInventoryEvent, event => + { + const parser = event.getParser(); + + setBotItems(prevValue => + { + const newValue = [ ...prevValue ]; + + const index = newValue.findIndex(item => (item.botData.id === parser.item.id)); + + if(index >= 0) return prevValue; + + const botItem = { botData: parser.item } as IBotItem; + const unseen = isUnseen(UnseenItemCategory.BOT, botItem.botData.id); + + if(unseen) newValue.unshift(botItem); + else newValue.push(botItem); + + return newValue; + }); + }); + + useMessageEvent(BotRemovedFromInventoryEvent, event => + { + const parser = event.getParser(); + + setBotItems(prevValue => + { + const newValue = [ ...prevValue ]; + + const index = newValue.findIndex(item => (item.botData.id === parser.itemId)); + + if(index === -1) return prevValue; + + newValue.splice(index, 1); + + if(getPlacingItemId() === parser.itemId) + { + cancelRoomObjectPlacement(); + + CreateLinkEvent('inventory/show'); + } + + return newValue; + }); + }); + + useEffect(() => + { + if(!botItems || !botItems.length) return; + + setSelectedBot(prevValue => + { + let newValue = prevValue; + + if(newValue && (botItems.indexOf(newValue) === -1)) newValue = null; + + if(!newValue) newValue = botItems[0]; + + return newValue; + }); + }, [ botItems ]); + + useEffect(() => + { + if(!isVisible) return; + + return () => + { + resetCategory(UnseenItemCategory.BOT); + } + }, [ isVisible, resetCategory ]); + + useEffect(() => + { + if(!isVisible || !needsUpdate) return; + + SendMessageComposer(new GetBotInventoryComposer()); + + setNeedsUpdate(false); + }, [ isVisible, needsUpdate ]); + + return { botItems, selectedBot, setSelectedBot, activate, deactivate }; +} + +export const useInventoryBots = () => useBetween(useInventoryBotsState); diff --git a/apps/frontend/src/hooks/inventory/useInventoryFurni.ts b/apps/frontend/src/hooks/inventory/useInventoryFurni.ts new file mode 100644 index 0000000..d57bd8d --- /dev/null +++ b/apps/frontend/src/hooks/inventory/useInventoryFurni.ts @@ -0,0 +1,298 @@ +import { FurnitureListAddOrUpdateEvent, FurnitureListComposer, FurnitureListEvent, FurnitureListInvalidateEvent, FurnitureListItemParser, FurnitureListRemovedEvent, FurniturePostItPlacedEvent } from '@nitrots/nitro-renderer'; +import { useEffect, useState } from 'react'; +import { useBetween } from 'use-between'; +import { addFurnitureItem, attemptItemPlacement, cancelRoomObjectPlacement, CloneObject, CreateLinkEvent, DispatchUiEvent, FurnitureItem, getAllItemIds, getPlacingItemId, GroupItem, mergeFurniFragments, SendMessageComposer, UnseenItemCategory } from '../../api'; +import { InventoryFurniAddedEvent } from '../../events'; +import { useMessageEvent } from '../events'; +import { useSharedVisibility } from '../useSharedVisibility'; +import { useInventoryUnseenTracker } from './useInventoryUnseenTracker'; + +let furniMsgFragments: Map[] = null; + +const useInventoryFurniState = () => +{ + const [ needsUpdate, setNeedsUpdate ] = useState(true); + const [ groupItems, setGroupItems ] = useState([]); + const [ selectedItem, setSelectedItem ] = useState(null); + const { isVisible = false, activate = null, deactivate = null } = useSharedVisibility(); + const { isUnseen = null, resetCategory = null } = useInventoryUnseenTracker(); + + const getItemsByType = (type: number) => + { + if(!groupItems || !groupItems.length) return; + + return groupItems.filter((i) => i.type === type); + } + + const getWallItemById = (id: number) => + { + if(!groupItems || !groupItems.length) return; + + for(const groupItem of groupItems) + { + const item = groupItem.getItemById(id); + + if(item && item.isWallItem) return groupItem; + } + + return null; + } + + const getFloorItemById = (id: number) => + { + if(!groupItems || !groupItems.length) return; + + for(const groupItem of groupItems) + { + const item = groupItem.getItemById(id); + + if(item && !item.isWallItem) return groupItem; + } + + return null; + } + + useMessageEvent(FurnitureListAddOrUpdateEvent, event => + { + const parser = event.getParser(); + + setGroupItems(prevValue => + { + const newValue = [ ...prevValue ]; + + for(const item of parser.items) + { + let i = 0; + let groupItem: GroupItem = null; + + while(i < newValue.length) + { + const group = newValue[i]; + + let j = 0; + + while(j < group.items.length) + { + const furniture = group.items[j]; + + if(furniture.id === item.itemId) + { + furniture.update(item); + + const newFurniture = [ ...group.items ]; + + newFurniture[j] = furniture; + + group.items = newFurniture; + + groupItem = group; + + break; + } + + j++ + } + + if(groupItem) break; + + i++; + } + + if(groupItem) + { + groupItem.hasUnseenItems = true; + + newValue[i] = CloneObject(groupItem); + } + else + { + const furniture = new FurnitureItem(item); + + addFurnitureItem(newValue, furniture, isUnseen(UnseenItemCategory.FURNI, item.itemId)); + + DispatchUiEvent(new InventoryFurniAddedEvent(furniture.id, furniture.type, furniture.category)); + } + } + + return newValue; + }); + }); + + useMessageEvent(FurnitureListEvent, event => + { + const parser = event.getParser(); + + if(!furniMsgFragments) furniMsgFragments = new Array(parser.totalFragments); + + const fragment = mergeFurniFragments(parser.fragment, parser.totalFragments, parser.fragmentNumber, furniMsgFragments); + + if(!fragment) return; + + setGroupItems(prevValue => + { + const newValue = [ ...prevValue ]; + const existingIds = getAllItemIds(newValue); + + for(const existingId of existingIds) + { + if(fragment.get(existingId)) continue; + + let index = 0; + + while(index < newValue.length) + { + const group = newValue[index]; + const item = group.remove(existingId); + + if(!item) + { + index++; + + continue; + } + + if(getPlacingItemId() === item.ref) + { + cancelRoomObjectPlacement(); + + if(!attemptItemPlacement(group)) + { + CreateLinkEvent('inventory/show'); + } + } + + if(group.getTotalCount() <= 0) + { + newValue.splice(index, 1); + + group.dispose(); + } + + break; + } + } + + for(const itemId of fragment.keys()) + { + if(existingIds.indexOf(itemId) >= 0) continue; + + const parser = fragment.get(itemId); + + if(!parser) continue; + + const item = new FurnitureItem(parser); + + addFurnitureItem(newValue, item, isUnseen(UnseenItemCategory.FURNI, itemId)); + + DispatchUiEvent(new InventoryFurniAddedEvent(item.id, item.type, item.category)); + + } + + return newValue; + }); + + furniMsgFragments = null; + }); + + useMessageEvent(FurnitureListInvalidateEvent, event => + { + setNeedsUpdate(true); + }); + + useMessageEvent(FurnitureListRemovedEvent, event => + { + const parser = event.getParser(); + + setGroupItems(prevValue => + { + const newValue = [ ...prevValue ]; + + let index = 0; + + while(index < newValue.length) + { + const group = newValue[index]; + const item = group.remove(parser.itemId); + + if(!item) + { + index++; + + continue; + } + + if(getPlacingItemId() === item.ref) + { + cancelRoomObjectPlacement(); + + if(!attemptItemPlacement(group)) CreateLinkEvent('inventory/show'); + } + + if(group.getTotalCount() <= 0) + { + newValue.splice(index, 1); + + group.dispose(); + } + + break; + } + + return newValue; + }); + }); + + useMessageEvent(FurniturePostItPlacedEvent, event => + { + + }); + + useEffect(() => + { + if(!groupItems || !groupItems.length) return; + + setSelectedItem(prevValue => + { + let newValue = prevValue; + + if(newValue && (groupItems.indexOf(newValue) === -1)) newValue = null; + + if(!newValue) newValue = groupItems[0]; + + return newValue; + }); + }, [ groupItems ]); + + useEffect(() => + { + if(!isVisible) return; + + return () => + { + if(resetCategory(UnseenItemCategory.FURNI)) + { + setGroupItems(prevValue => + { + const newValue = [ ...prevValue ]; + + for(const newGroup of newValue) newGroup.hasUnseenItems = false; + + return newValue; + }); + } + } + }, [ isVisible, resetCategory ]); + + useEffect(() => + { + if(!isVisible || !needsUpdate) return; + + SendMessageComposer(new FurnitureListComposer()); + + setNeedsUpdate(false); + }, [ isVisible, needsUpdate ]); + + return { isVisible, groupItems, setGroupItems, selectedItem, setSelectedItem, activate, deactivate, getWallItemById, getFloorItemById, getItemsByType }; +} + +export const useInventoryFurni = () => useBetween(useInventoryFurniState); diff --git a/apps/frontend/src/hooks/inventory/useInventoryPets.ts b/apps/frontend/src/hooks/inventory/useInventoryPets.ts new file mode 100644 index 0000000..d95e8bf --- /dev/null +++ b/apps/frontend/src/hooks/inventory/useInventoryPets.ts @@ -0,0 +1,107 @@ +import { PetAddedToInventoryEvent, PetData, PetInventoryEvent, PetRemovedFromInventory, RequestPetsComposer } from '@nitrots/nitro-renderer'; +import { useEffect, useState } from 'react'; +import { useBetween } from 'use-between'; +import { addSinglePetItem, IPetItem, mergePetFragments, processPetFragment, removePetItemById, SendMessageComposer, UnseenItemCategory } from '../../api'; +import { useMessageEvent } from '../events'; +import { useSharedVisibility } from '../useSharedVisibility'; +import { useInventoryUnseenTracker } from './useInventoryUnseenTracker'; + +let petMsgFragments: Map[] = null; + +const useInventoryPetsState = () => +{ + const [ needsUpdate, setNeedsUpdate ] = useState(true); + const [ petItems, setPetItems ] = useState([]); + const [ selectedPet, setSelectedPet ] = useState(null); + const { isVisible = false, activate = null, deactivate = null } = useSharedVisibility(); + const { isUnseen = null, resetCategory = null } = useInventoryUnseenTracker(); + + useMessageEvent(PetInventoryEvent, event => + { + const parser = event.getParser(); + + if(!petMsgFragments) petMsgFragments = new Array(parser.totalFragments); + + const fragment = mergePetFragments(parser.fragment, parser.totalFragments, parser.fragmentNumber, petMsgFragments); + + if(!fragment) return; + + setPetItems(prevValue => + { + const newValue = [ ...prevValue ]; + + processPetFragment(newValue, fragment, isUnseen); + + return newValue; + }); + + petMsgFragments = null; + }); + + useMessageEvent(PetAddedToInventoryEvent, event => + { + const parser = event.getParser(); + + setPetItems(prevValue => + { + const newValue = [ ...prevValue ]; + + addSinglePetItem(parser.pet, newValue, isUnseen(UnseenItemCategory.PET, parser.pet.id)); + + return newValue; + }); + }); + + useMessageEvent(PetRemovedFromInventory, event => + { + const parser = event.getParser(); + + setPetItems(prevValue => + { + const newValue = [ ...prevValue ]; + + removePetItemById(parser.petId, newValue); + + return newValue; + }); + }); + + useEffect(() => + { + if(!petItems || !petItems.length) return; + + setSelectedPet(prevValue => + { + let newValue = prevValue; + + if(newValue && (petItems.indexOf(newValue) === -1)) newValue = null; + + if(!newValue) newValue = petItems[0]; + + return newValue; + }); + }, [ petItems ]); + + useEffect(() => + { + if(!isVisible) return; + + return () => + { + resetCategory(UnseenItemCategory.PET); + } + }, [ isVisible, resetCategory ]); + + useEffect(() => + { + if(!isVisible || !needsUpdate) return; + + SendMessageComposer(new RequestPetsComposer()); + + setNeedsUpdate(false); + }, [ isVisible, needsUpdate ]); + + return { petItems, selectedPet, setSelectedPet, activate, deactivate }; +} + +export const useInventoryPets = () => useBetween(useInventoryPetsState); diff --git a/apps/frontend/src/hooks/inventory/useInventoryTrade.ts b/apps/frontend/src/hooks/inventory/useInventoryTrade.ts new file mode 100644 index 0000000..7eb7481 --- /dev/null +++ b/apps/frontend/src/hooks/inventory/useInventoryTrade.ts @@ -0,0 +1,288 @@ +import { AdvancedMap, TradingAcceptComposer, TradingAcceptEvent, TradingCancelComposer, TradingCloseComposer, TradingCloseEvent, TradingCloseParser, TradingCompletedEvent, TradingConfirmationComposer, TradingConfirmationEvent, TradingListItemEvent, TradingListItemRemoveComposer, TradingNotOpenEvent, TradingOpenEvent, TradingOpenFailedEvent, TradingOtherNotAllowedEvent, TradingUnacceptComposer, TradingYouAreNotAllowedEvent } from '@nitrots/nitro-renderer'; +import { useEffect, useState } from 'react'; +import { useBetween } from 'use-between'; +import { CloneObject, GetRoomSession, GetSessionDataManager, GroupItem, LocalizeText, parseTradeItems, SendMessageComposer, TradeState, TradeUserData, TradingNotificationType } from '../../api'; +import { useMessageEvent } from '../events'; +import { useNotification } from '../notification'; +import { useInventoryFurni } from './useInventoryFurni'; + +const useInventoryTradeState = () => +{ + const [ ownUser, setOwnUser ] = useState(null); + const [ otherUser, setOtherUser ] = useState(null); + const [ tradeState, setTradeState ] = useState(TradeState.TRADING_STATE_READY); + const { groupItems = [], setGroupItems = null, activate = null, deactivate = null } = useInventoryFurni(); + const { simpleAlert = null, showTradeAlert = null } = useNotification(); + const isTrading = (tradeState >= TradeState.TRADING_STATE_RUNNING); + + const progressTrade = () => + { + switch(tradeState) + { + case TradeState.TRADING_STATE_RUNNING: + if(!otherUser.itemCount && !ownUser.accepts) + { + simpleAlert(LocalizeText('inventory.trading.warning.other_not_offering'), null, null, null); + } + + if(ownUser.accepts) + { + SendMessageComposer(new TradingUnacceptComposer()); + } + else + { + SendMessageComposer(new TradingAcceptComposer()); + } + return; + case TradeState.TRADING_STATE_CONFIRMING: + SendMessageComposer(new TradingConfirmationComposer()); + + setTradeState(TradeState.TRADING_STATE_CONFIRMED); + return; + } + } + + const removeItem = (group: GroupItem) => + { + const item = group.getLastItem(); + + if(!item) return; + + SendMessageComposer(new TradingListItemRemoveComposer(item.id)); + } + + const stopTrading = () => + { + if(!isTrading) return; + + switch(tradeState) + { + case TradeState.TRADING_STATE_RUNNING: + SendMessageComposer(new TradingCloseComposer()); + return; + default: + SendMessageComposer(new TradingCancelComposer()); + return; + } + } + + useMessageEvent(TradingAcceptEvent, event => + { + const parser = event.getParser(); + + if(!ownUser || !otherUser) return; + + if(ownUser.userId === parser.userID) + { + setOwnUser(prevValue => + { + const newValue = CloneObject(prevValue); + + newValue.accepts = parser.userAccepts; + + return newValue; + }); + } + + else if(otherUser.userId === parser.userID) + { + setOtherUser(prevValue => + { + const newValue = CloneObject(prevValue); + + newValue.accepts = parser.userAccepts; + + return newValue; + }); + } + }); + + useMessageEvent(TradingCloseEvent, event => + { + const parser = event.getParser(); + + if(parser.reason === TradingCloseParser.ERROR_WHILE_COMMIT) + { + showTradeAlert(TradingNotificationType.ERROR_WHILE_COMMIT); + } + else + { + if(ownUser && (parser.userID !== ownUser.userId)) + { + showTradeAlert(TradingNotificationType.THEY_CANCELLED); + } + } + + setOwnUser(null); + setOtherUser(null); + setTradeState(TradeState.TRADING_STATE_READY); + }); + + useMessageEvent(TradingCompletedEvent, event => + { + const parser = event.getParser(); + + setOwnUser(null); + setOtherUser(null); + setTradeState(TradeState.TRADING_STATE_READY); + }); + + useMessageEvent(TradingConfirmationEvent, event => + { + const parser = event.getParser(); + + setTradeState(TradeState.TRADING_STATE_COUNTDOWN); + }); + + useMessageEvent(TradingListItemEvent, event => + { + const parser = event.getParser(); + const firstUserItems = parseTradeItems(parser.firstUserItemArray); + const secondUserItems = parseTradeItems(parser.secondUserItemArray); + + setOwnUser(prevValue => + { + const newValue = CloneObject(prevValue); + + if(newValue.userId === parser.firstUserID) + { + newValue.creditsCount = parser.firstUserNumCredits; + newValue.itemCount = parser.firstUserNumItems; + newValue.userItems = firstUserItems; + } + else + { + newValue.creditsCount = parser.secondUserNumCredits; + newValue.itemCount = parser.secondUserNumItems; + newValue.userItems = secondUserItems; + } + + const tradeIds: number[] = []; + + for(const groupItem of newValue.userItems.getValues()) + { + let i = 0; + + while(i < groupItem.getTotalCount()) + { + const item = groupItem.getItemByIndex(i); + + if(item) tradeIds.push(item.ref); + + i++; + } + } + + setGroupItems(prevValue => + { + const newValue = [ ...prevValue ]; + + for(const groupItem of newValue) groupItem.lockItemIds(tradeIds); + + return newValue; + }); + + return newValue; + }); + + setOtherUser(prevValue => + { + const newValue = CloneObject(prevValue); + + if(newValue.userId === parser.firstUserID) + { + newValue.creditsCount = parser.firstUserNumCredits; + newValue.itemCount = parser.firstUserNumItems; + newValue.userItems = firstUserItems; + } + else + { + newValue.creditsCount = parser.secondUserNumCredits; + newValue.itemCount = parser.secondUserNumItems; + newValue.userItems = secondUserItems; + } + + return newValue; + }); + }); + + useMessageEvent(TradingNotOpenEvent, event => + { + const parser = event.getParser(); + }); + + useMessageEvent(TradingOpenEvent, event => + { + const parser = event.getParser(); + + const firstUser = new TradeUserData(); + const firstUserData = GetRoomSession().userDataManager.getUserData(parser.userID); + + firstUser.userItems = new AdvancedMap(); + + const secondUser = new TradeUserData(); + const secondUserData = GetRoomSession().userDataManager.getUserData(parser.otherUserID); + + secondUser.userItems = new AdvancedMap(); + + if(firstUserData.webID === GetSessionDataManager().userId) + { + firstUser.userId = firstUserData.webID; + firstUser.userName = firstUserData.name; + firstUser.canTrade = parser.userCanTrade; + + secondUser.userId = secondUserData.webID; + secondUser.userName = secondUserData.name; + secondUser.canTrade = parser.otherUserCanTrade; + } + + else if(secondUserData.webID === GetSessionDataManager().userId) + { + firstUser.userId = secondUserData.webID; + firstUser.userName = secondUserData.name; + firstUser.canTrade = parser.otherUserCanTrade; + + secondUser.userId = firstUserData.webID; + secondUser.userName = firstUserData.name; + secondUser.canTrade = parser.userCanTrade; + } + + setOwnUser(firstUser); + setOtherUser(secondUser); + setTradeState(TradeState.TRADING_STATE_RUNNING); + }); + + useMessageEvent(TradingOpenFailedEvent, event => + { + const parser = event.getParser(); + + showTradeAlert(parser.reason, parser.otherUserName); + }); + + useMessageEvent(TradingOtherNotAllowedEvent, event => + { + const parser = event.getParser(); + + showTradeAlert(TradingNotificationType.THEY_NOT_ALLOWED); + }); + + useMessageEvent(TradingYouAreNotAllowedEvent, event => + { + const parser = event.getParser(); + + showTradeAlert(TradingNotificationType.YOU_NOT_ALLOWED); + }); + + useEffect(() => + { + if(tradeState === TradeState.TRADING_STATE_READY) return; + + const id = activate(); + + return () => deactivate(id); + }, [ tradeState, activate, deactivate ]); + + return { ownUser, otherUser, tradeState, setTradeState, isTrading, groupItems, progressTrade, removeItem, stopTrading }; +} + +export const useInventoryTrade = () => useBetween(useInventoryTradeState); diff --git a/apps/frontend/src/hooks/inventory/useInventoryUnseenTracker.ts b/apps/frontend/src/hooks/inventory/useInventoryUnseenTracker.ts new file mode 100644 index 0000000..90ba66e --- /dev/null +++ b/apps/frontend/src/hooks/inventory/useInventoryUnseenTracker.ts @@ -0,0 +1,132 @@ +import { UnseenItemsEvent, UnseenResetCategoryComposer, UnseenResetItemsComposer } from '@nitrots/nitro-renderer'; +import { useCallback, useMemo, useState } from 'react'; +import { useBetween } from 'use-between'; +import { SendMessageComposer } from '../../api'; +import { useMessageEvent } from '../events'; + +const sendResetCategoryMessage = (category: number) => SendMessageComposer(new UnseenResetCategoryComposer(category)); +const sendResetItemsMessage = (category: number, itemIds: number[]) => SendMessageComposer(new UnseenResetItemsComposer(category, ...itemIds)); + +const useInventoryUnseenTrackerState = () => +{ + const [ unseenItems, setUnseenItems ] = useState>(new Map()); + + const getCount = useCallback((category: number) => (unseenItems.get(category)?.length || 0), [ unseenItems ]); + + const getFullCount = useMemo(() => + { + let count = 0; + + for(const key of unseenItems.keys()) count += getCount(key); + + return count; + }, [ unseenItems, getCount ]); + + const resetCategory = useCallback((category: number) => + { + let didReset = true; + + setUnseenItems(prevValue => + { + if(!prevValue.has(category)) + { + didReset = false; + + return prevValue; + } + + const newValue = new Map(prevValue); + + newValue.delete(category); + + sendResetCategoryMessage(category); + + return newValue; + }); + + return didReset; + }, []); + + const resetItems = useCallback((category: number, itemIds: number[]) => + { + let didReset = true; + + setUnseenItems(prevValue => + { + if(!prevValue.has(category)) + { + didReset = false; + + return prevValue; + } + + const newValue = new Map(prevValue); + const existing = newValue.get(category); + + if(existing) for(const itemId of itemIds) existing.splice(existing.indexOf(itemId), 1); + + sendResetItemsMessage(category, itemIds); + + return newValue; + }); + + return didReset; + }, []); + + const isUnseen = useCallback((category: number, itemId: number) => + { + if(!unseenItems.has(category)) return false; + + const items = unseenItems.get(category); + + return (items.indexOf(itemId) >= 0); + }, [ unseenItems ]); + + const removeUnseen = useCallback((category: number, itemId: number) => + { + setUnseenItems(prevValue => + { + if(!prevValue.has(category)) return prevValue; + + const newValue = new Map(prevValue); + const items = newValue.get(category); + const index = items.indexOf(itemId); + + if(index >= 0) items.splice(index, 1); + + return newValue; + }); + }, []); + + useMessageEvent(UnseenItemsEvent, event => + { + const parser = event.getParser(); + + setUnseenItems(prevValue => + { + const newValue = new Map(prevValue); + + for(const category of parser.categories) + { + let existing = newValue.get(category); + + if(!existing) + { + existing = []; + + newValue.set(category, existing); + } + + const itemIds = parser.getItemsByCategory(category); + + for(const itemId of itemIds) ((existing.indexOf(itemId) === -1) && existing.push(itemId)); + } + + return newValue; + }); + }); + + return { getCount, getFullCount, resetCategory, resetItems, isUnseen, removeUnseen }; +} + +export const useInventoryUnseenTracker = () => useBetween(useInventoryUnseenTrackerState); diff --git a/apps/frontend/src/hooks/mod-tools/index.ts b/apps/frontend/src/hooks/mod-tools/index.ts new file mode 100644 index 0000000..26546fd --- /dev/null +++ b/apps/frontend/src/hooks/mod-tools/index.ts @@ -0,0 +1 @@ +export * from './useModTools'; diff --git a/apps/frontend/src/hooks/mod-tools/useModTools.ts b/apps/frontend/src/hooks/mod-tools/useModTools.ts new file mode 100644 index 0000000..93628f1 --- /dev/null +++ b/apps/frontend/src/hooks/mod-tools/useModTools.ts @@ -0,0 +1,207 @@ +import { CallForHelpCategoryData, CfhSanctionMessageEvent, CfhTopicsInitEvent, IssueDeletedMessageEvent, IssueInfoMessageEvent, IssueMessageData, IssuePickFailedMessageEvent, ModeratorActionResultMessageEvent, ModeratorInitData, ModeratorInitMessageEvent, ModeratorToolPreferencesEvent } from '@nitrots/nitro-renderer'; +import { useState } from 'react'; +import { useBetween } from 'use-between'; +import { NotificationAlertType, PlaySound, SoundNames } from '../../api'; +import { useMessageEvent } from '../events'; +import { useNotification } from '../notification'; + +const useModToolsState = () => +{ + const [ settings, setSettings ] = useState(null); + const [ openRooms, setOpenRooms ] = useState([]); + const [ openRoomChatlogs, setOpenRoomChatlogs ] = useState([]); + const [ openUserInfos, setOpenUserInfos ] = useState([]); + const [ openUserChatlogs, setOpenUserChatlogs ] = useState([]); + const [ tickets, setTickets ] = useState([]); + const [ cfhCategories, setCfhCategories ] = useState([]); + const { simpleAlert = null } = useNotification(); + + const openRoomInfo = (roomId: number) => + { + if(openRooms.indexOf(roomId) >= 0) return; + + setOpenRooms(prevValue => [ ...prevValue, roomId ]); + } + + const closeRoomInfo = (roomId: number) => + { + setOpenRooms(prevValue => + { + const newValue = [ ...prevValue ]; + const existingIndex = newValue.indexOf(roomId); + + if(existingIndex >= 0) newValue.splice(existingIndex); + + return newValue; + }); + } + + const toggleRoomInfo = (roomId: number) => + { + if(openRooms.indexOf(roomId) >= 0) closeRoomInfo(roomId); + else openRoomInfo(roomId); + } + + const openRoomChatlog = (roomId: number) => + { + if(openRoomChatlogs.indexOf(roomId) >= 0) return; + + setOpenRoomChatlogs(prevValue => [ ...prevValue, roomId ]); + } + + const closeRoomChatlog = (roomId: number) => + { + setOpenRoomChatlogs(prevValue => + { + const newValue = [ ...prevValue ]; + const existingIndex = newValue.indexOf(roomId); + + if(existingIndex >= 0) newValue.splice(existingIndex); + + return newValue; + }); + } + + const toggleRoomChatlog = (roomId: number) => + { + if(openRoomChatlogs.indexOf(roomId) >= 0) closeRoomChatlog(roomId); + else openRoomChatlog(roomId); + } + + const openUserInfo = (userId: number) => + { + if(openUserInfos.indexOf(userId) >= 0) return; + + setOpenUserInfos(prevValue => [ ...prevValue, userId ]); + } + + const closeUserInfo = (userId: number) => + { + setOpenUserInfos(prevValue => + { + const newValue = [ ...prevValue ]; + const existingIndex = newValue.indexOf(userId); + + if(existingIndex >= 0) newValue.splice(existingIndex); + + return newValue; + }); + } + + const toggleUserInfo = (userId: number) => + { + if(openUserInfos.indexOf(userId) >= 0) closeUserInfo(userId); + else openUserInfo(userId); + } + + const openUserChatlog = (userId: number) => + { + if(openUserChatlogs.indexOf(userId) >= 0) return; + + setOpenUserChatlogs(prevValue => [ ...prevValue, userId ]); + } + + const closeUserChatlog = (userId: number) => + { + setOpenUserChatlogs(prevValue => + { + const newValue = [ ...prevValue ]; + const existingIndex = newValue.indexOf(userId); + + if(existingIndex >= 0) newValue.splice(existingIndex); + + return newValue; + }); + } + + const toggleUserChatlog = (userId: number) => + { + if(openRoomChatlogs.indexOf(userId) >= 0) closeUserChatlog(userId); + else openUserChatlog(userId); + } + + useMessageEvent(ModeratorInitMessageEvent, event => + { + const parser = event.getParser(); + const data = parser.data; + + setSettings(data); + setTickets(data.issues); + }); + + useMessageEvent(IssueInfoMessageEvent, event => + { + const parser = event.getParser(); + + setTickets(prevValue => + { + const newValue = [ ...prevValue ]; + const existingIndex = newValue.findIndex(ticket => (ticket.issueId === parser.issueData.issueId)); + + if(existingIndex >= 0) newValue[existingIndex] = parser.issueData; + else + { + newValue.push(parser.issueData); + + PlaySound(SoundNames.MODTOOLS_NEW_TICKET); + } + + return newValue; + }); + }); + + useMessageEvent(ModeratorToolPreferencesEvent, event => + { + const parser = event.getParser(); + }); + + useMessageEvent(IssuePickFailedMessageEvent, event => + { + const parser = event.getParser(); + + if(!parser) return; + + simpleAlert('Failed to pick issue', NotificationAlertType.DEFAULT, null, null, 'Error') + }); + + useMessageEvent(IssueDeletedMessageEvent, event => + { + const parser = event.getParser(); + + setTickets(prevValue => + { + const newValue = [ ...prevValue ]; + const existingIndex = newValue.findIndex(ticket => (ticket.issueId === parser.issueId)); + + if(existingIndex >= 0) newValue.splice(existingIndex, 1); + + return newValue; + }); + }); + + useMessageEvent(ModeratorActionResultMessageEvent, event => + { + const parser = event.getParser(); + + if(parser.success) simpleAlert('Moderation action was successfull', NotificationAlertType.MODERATION, null, null, 'Success'); + else simpleAlert('There was a problem applying tht moderation action', NotificationAlertType.MODERATION, null, null, 'Error'); + }); + + useMessageEvent(CfhTopicsInitEvent, event => + { + const parser = event.getParser(); + + setCfhCategories(parser.callForHelpCategories); + }); + + useMessageEvent(CfhSanctionMessageEvent, event => + { + const parser = event.getParser(); + + // todo: update sanction data + }); + + return { settings, openRooms, openRoomChatlogs, openUserChatlogs, openUserInfos, cfhCategories, tickets, openRoomInfo, closeRoomInfo, toggleRoomInfo, openRoomChatlog, closeRoomChatlog, toggleRoomChatlog, openUserInfo, closeUserInfo, toggleUserInfo, openUserChatlog, closeUserChatlog, toggleUserChatlog }; +} + +export const useModTools = () => useBetween(useModToolsState); diff --git a/apps/frontend/src/hooks/navigator/index.ts b/apps/frontend/src/hooks/navigator/index.ts new file mode 100644 index 0000000..1f6f053 --- /dev/null +++ b/apps/frontend/src/hooks/navigator/index.ts @@ -0,0 +1 @@ +export * from './useNavigator'; diff --git a/apps/frontend/src/hooks/navigator/useNavigator.ts b/apps/frontend/src/hooks/navigator/useNavigator.ts new file mode 100644 index 0000000..18d6ebd --- /dev/null +++ b/apps/frontend/src/hooks/navigator/useNavigator.ts @@ -0,0 +1,442 @@ +import { CanCreateRoomEventEvent, CantConnectMessageParser, DoorbellMessageEvent, FlatAccessDeniedMessageEvent, FlatCreatedEvent, FollowFriendMessageComposer, GenericErrorEvent, GetGuestRoomMessageComposer, GetGuestRoomResultEvent, GetUserEventCatsMessageComposer, GetUserFlatCatsMessageComposer, HabboWebTools, LegacyExternalInterface, NavigatorCategoryDataParser, NavigatorEventCategoryDataParser, NavigatorHomeRoomEvent, NavigatorMetadataEvent, NavigatorOpenRoomCreatorEvent, NavigatorSearchEvent, NavigatorSearchResultSet, NavigatorTopLevelContext, RoomDataParser, RoomDoorbellAcceptedEvent, RoomEnterErrorEvent, RoomEntryInfoMessageEvent, RoomForwardEvent, RoomScoreEvent, RoomSettingsUpdatedEvent, SecurityLevel, UserEventCatsEvent, UserFlatCatsEvent, UserInfoEvent, UserPermissionsEvent } from '@nitrots/nitro-renderer'; +import { useState } from 'react'; +import { useBetween } from 'use-between'; +import { CreateLinkEvent, CreateRoomSession, DoorStateType, GetConfiguration, GetSessionDataManager, INavigatorData, LocalizeText, NotificationAlertType, SendMessageComposer, TryVisitRoom, VisitDesktop } from '../../api'; +import { useMessageEvent } from '../events'; +import { useNotification } from '../notification'; + +const useNavigatorState = () => +{ + const [ categories, setCategories ] = useState(null); + const [ eventCategories, setEventCategories ] = useState(null); + const [ topLevelContext, setTopLevelContext ] = useState(null); + const [ topLevelContexts, setTopLevelContexts ] = useState(null); + const [ doorData, setDoorData ] = useState<{ roomInfo: RoomDataParser, state: number }>({ roomInfo: null, state: DoorStateType.NONE }); + const [ searchResult, setSearchResult ] = useState(null); + const [ navigatorData, setNavigatorData ] = useState({ + settingsReceived: false, + homeRoomId: 0, + enteredGuestRoom: null, + currentRoomOwner: false, + currentRoomId: 0, + currentRoomIsStaffPick: false, + createdFlatId: 0, + avatarId: 0, + roomPicker: false, + eventMod: false, + currentRoomRating: 0, + canRate: true + }); + const { simpleAlert = null } = useNotification(); + + useMessageEvent(RoomSettingsUpdatedEvent, event => + { + const parser = event.getParser(); + + SendMessageComposer(new GetGuestRoomMessageComposer(parser.roomId, false, false)); + }); + + useMessageEvent(CanCreateRoomEventEvent, event => + { + const parser = event.getParser(); + + if(parser.canCreate) + { + // show room event cvreate + + return; + } + + simpleAlert(LocalizeText(`navigator.cannotcreateevent.error.${ parser.errorCode }`), null, null, null, LocalizeText('navigator.cannotcreateevent.title')); + }); + + useMessageEvent(UserInfoEvent, event => + { + SendMessageComposer(new GetUserFlatCatsMessageComposer()); + SendMessageComposer(new GetUserEventCatsMessageComposer()); + }); + + useMessageEvent(UserPermissionsEvent, event => + { + const parser = event.getParser(); + + setNavigatorData(prevValue => + { + const newValue = { ...prevValue }; + + newValue.eventMod = (parser.securityLevel >= SecurityLevel.MODERATOR); + newValue.roomPicker = (parser.securityLevel >= SecurityLevel.COMMUNITY); + + return newValue; + }); + }); + + useMessageEvent(RoomForwardEvent, event => + { + const parser = event.getParser(); + + TryVisitRoom(parser.roomId); + }); + + useMessageEvent(RoomEntryInfoMessageEvent, event => + { + const parser = event.getParser(); + + setNavigatorData(prevValue => + { + const newValue = { ...prevValue }; + + newValue.enteredGuestRoom = null; + newValue.currentRoomOwner = parser.isOwner; + newValue.currentRoomId = parser.roomId; + + return newValue; + }); + + // close room info + // close room settings + // close room filter + + SendMessageComposer(new GetGuestRoomMessageComposer(parser.roomId, true, false)); + + if(LegacyExternalInterface.available) LegacyExternalInterface.call('legacyTrack', 'navigator', 'private', [ parser.roomId ]); + }); + + useMessageEvent(GetGuestRoomResultEvent, event => + { + const parser = event.getParser(); + + if(parser.roomEnter) + { + setDoorData({ roomInfo: null, state: DoorStateType.NONE }); + + setNavigatorData(prevValue => + { + const newValue = { ...prevValue }; + + newValue.enteredGuestRoom = parser.data; + newValue.currentRoomIsStaffPick = parser.staffPick; + + const isCreated = (newValue.createdFlatId === parser.data.roomId); + + if(!isCreated && parser.data.displayRoomEntryAd) + { + if(GetConfiguration('roomenterad.habblet.enabled', false)) HabboWebTools.openRoomEnterAd(); + } + + newValue.createdFlatId = 0; + + if(newValue.enteredGuestRoom && (newValue.enteredGuestRoom.habboGroupId > 0)) + { + // close event info + } + + return newValue; + }); + } + else if(parser.roomForward) + { + if((parser.data.ownerName !== GetSessionDataManager().userName) && !parser.isGroupMember) + { + switch(parser.data.doorMode) + { + case RoomDataParser.DOORBELL_STATE: + setDoorData(prevValue => + { + const newValue = { ...prevValue }; + + newValue.roomInfo = parser.data; + newValue.state = DoorStateType.START_DOORBELL; + + return newValue; + }); + return; + case RoomDataParser.PASSWORD_STATE: + setDoorData(prevValue => + { + const newValue = { ...prevValue }; + + newValue.roomInfo = parser.data; + newValue.state = DoorStateType.START_PASSWORD; + + return newValue; + }); + return; + } + } + + if((parser.data.doorMode === RoomDataParser.NOOB_STATE) && !GetSessionDataManager().isAmbassador && !GetSessionDataManager().isRealNoob && !GetSessionDataManager().isModerator) return; + + CreateRoomSession(parser.data.roomId); + } + else + { + setNavigatorData(prevValue => + { + const newValue = { ...prevValue }; + + newValue.enteredGuestRoom = parser.data; + newValue.currentRoomIsStaffPick = parser.staffPick; + + return newValue; + }); + } + }); + + useMessageEvent(RoomScoreEvent, event => + { + const parser = event.getParser(); + + setNavigatorData(prevValue => + { + const newValue = { ...prevValue }; + + newValue.currentRoomRating = parser.totalLikes; + newValue.canRate = parser.canLike; + + return newValue; + }); + }); + + useMessageEvent(DoorbellMessageEvent, event => + { + const parser = event.getParser(); + + if(!parser.userName || (parser.userName.length === 0)) + { + setDoorData(prevValue => + { + const newValue = { ...prevValue }; + + newValue.state = DoorStateType.STATE_WAITING; + + return newValue; + }); + } + }); + + useMessageEvent(RoomDoorbellAcceptedEvent, event => + { + const parser = event.getParser(); + + if(!parser.userName || (parser.userName.length === 0)) + { + setDoorData(prevValue => + { + const newValue = { ...prevValue }; + + newValue.state = DoorStateType.STATE_ACCEPTED; + + return newValue; + }); + } + }); + + useMessageEvent(FlatAccessDeniedMessageEvent, event => + { + const parser = event.getParser(); + + if(!parser.userName || (parser.userName.length === 0)) + { + setDoorData(prevValue => + { + const newValue = { ...prevValue }; + + newValue.state = DoorStateType.STATE_NO_ANSWER; + + return newValue; + }); + } + }); + + useMessageEvent(GenericErrorEvent, event => + { + const parser = event.getParser(); + + switch(parser.errorCode) + { + case -100002: + setDoorData(prevValue => + { + const newValue = { ...prevValue }; + + newValue.state = DoorStateType.STATE_WRONG_PASSWORD; + + return newValue; + }); + return; + case 4009: + simpleAlert(LocalizeText('navigator.alert.need.to.be.vip'), NotificationAlertType.DEFAULT, null, null, LocalizeText('generic.alert.title')); + + return; + case 4010: + simpleAlert(LocalizeText('navigator.alert.invalid_room_name'), NotificationAlertType.DEFAULT, null, null, LocalizeText('generic.alert.title')); + + return; + case 4011: + simpleAlert(LocalizeText('navigator.alert.cannot_perm_ban'), NotificationAlertType.DEFAULT, null, null, LocalizeText('generic.alert.title')); + + return; + case 4013: + simpleAlert(LocalizeText('navigator.alert.room_in_maintenance'), NotificationAlertType.DEFAULT, null, null, LocalizeText('generic.alert.title')); + + return; + } + }); + + useMessageEvent(NavigatorMetadataEvent, event => + { + const parser = event.getParser(); + + setTopLevelContexts(parser.topLevelContexts); + setTopLevelContext(parser.topLevelContexts.length ? parser.topLevelContexts[0] : null); + }); + + useMessageEvent(NavigatorSearchEvent, event => + { + const parser = event.getParser(); + + setTopLevelContext(prevValue => + { + let newValue = prevValue; + + if(!newValue) newValue = ((topLevelContexts && topLevelContexts.length && topLevelContexts[0]) || null); + + if(!newValue) return null; + + if((parser.result.code !== newValue.code) && topLevelContexts && topLevelContexts.length) + { + for(const context of topLevelContexts) + { + if(context.code !== parser.result.code) continue; + + newValue = context; + } + } + + for(const context of topLevelContexts) + { + if(context.code !== parser.result.code) continue; + + newValue = context; + } + + return newValue; + }); + + setSearchResult(parser.result); + }); + + useMessageEvent(UserFlatCatsEvent, event => + { + const parser = event.getParser(); + + setCategories(parser.categories); + }); + + useMessageEvent(UserEventCatsEvent, event => + { + const parser = event.getParser(); + + setEventCategories(parser.categories); + }); + + useMessageEvent(FlatCreatedEvent, event => + { + const parser = event.getParser(); + + CreateRoomSession(parser.roomId); + }); + + useMessageEvent(NavigatorHomeRoomEvent, event => + { + const parser = event.getParser(); + + let prevSettingsReceived = false; + + setNavigatorData(prevValue => + { + prevSettingsReceived = prevValue.settingsReceived; + + const newValue = { ...prevValue }; + + newValue.homeRoomId = parser.homeRoomId; + newValue.settingsReceived = true; + + return newValue; + }); + + if(prevSettingsReceived) + { + // refresh room info window + return; + } + + let forwardType = -1; + let forwardId = -1; + + if((GetConfiguration('friend.id') !== undefined) && (parseInt(GetConfiguration('friend.id')) > 0)) + { + forwardType = 0; + SendMessageComposer(new FollowFriendMessageComposer(parseInt(GetConfiguration('friend.id')))); + } + + if((GetConfiguration('forward.type') !== undefined) && (GetConfiguration('forward.id') !== undefined)) + { + forwardType = parseInt(GetConfiguration('forward.type')); + forwardId = parseInt(GetConfiguration('forward.id')) + } + + if(forwardType === 2) + { + TryVisitRoom(forwardId); + } + + else if((forwardType === -1) && (parser.roomIdToEnter > 0)) + { + CreateLinkEvent('navigator/close'); + + if(parser.roomIdToEnter !== parser.homeRoomId) + { + CreateRoomSession(parser.roomIdToEnter); + } + else + { + CreateRoomSession(parser.homeRoomId); + } + } + }); + + useMessageEvent(RoomEnterErrorEvent, event => + { + const parser = event.getParser(); + + switch(parser.reason) + { + case CantConnectMessageParser.REASON_FULL: + simpleAlert(LocalizeText('navigator.guestroomfull.text'), NotificationAlertType.DEFAULT, null, null, LocalizeText('navigator.guestroomfull.title')); + + break; + case CantConnectMessageParser.REASON_QUEUE_ERROR: + simpleAlert(LocalizeText(`room.queue.error.${ parser.parameter }`), NotificationAlertType.DEFAULT, null, null, LocalizeText('room.queue.error.title')); + + break; + case CantConnectMessageParser.REASON_BANNED: + simpleAlert(LocalizeText('navigator.banned.text'), NotificationAlertType.DEFAULT, null, null, LocalizeText('navigator.banned.title')); + + break; + default: + simpleAlert(LocalizeText('room.queue.error.title'), NotificationAlertType.DEFAULT, null, null, LocalizeText('room.queue.error.title')); + + break; + } + + VisitDesktop(); + }); + + useMessageEvent(NavigatorOpenRoomCreatorEvent, event => CreateLinkEvent('navigator/show')); + + return { categories, doorData, setDoorData, topLevelContext, topLevelContexts, searchResult, navigatorData }; +} + +export const useNavigator = () => useBetween(useNavigatorState); diff --git a/apps/frontend/src/hooks/notification/index.ts b/apps/frontend/src/hooks/notification/index.ts new file mode 100644 index 0000000..b5a8561 --- /dev/null +++ b/apps/frontend/src/hooks/notification/index.ts @@ -0,0 +1 @@ +export * from './useNotification'; diff --git a/apps/frontend/src/hooks/notification/useNotification.ts b/apps/frontend/src/hooks/notification/useNotification.ts new file mode 100644 index 0000000..0089b86 --- /dev/null +++ b/apps/frontend/src/hooks/notification/useNotification.ts @@ -0,0 +1,432 @@ +import { AchievementNotificationMessageEvent, ActivityPointNotificationMessageEvent, ClubGiftNotificationEvent, ClubGiftSelectedEvent, ConnectionErrorEvent, HabboBroadcastMessageEvent, HotelClosedAndOpensEvent, HotelClosesAndWillOpenAtEvent, HotelWillCloseInMinutesEvent, InfoFeedEnableMessageEvent, MaintenanceStatusMessageEvent, ModeratorCautionEvent, ModeratorMessageEvent, MOTDNotificationEvent, NotificationDialogMessageEvent, PetLevelNotificationEvent, PetReceivedMessageEvent, RespectReceivedEvent, RoomEnterEffect, RoomEnterEvent, SimpleAlertMessageEvent, UserBannedMessageEvent, Vector3d } from '@nitrots/nitro-renderer'; +import { useCallback, useState } from 'react'; +import { useBetween } from 'use-between'; +import { GetConfiguration, GetNitroInstance, GetRoomEngine, GetSessionDataManager, LocalizeBadgeName, LocalizeText, NotificationAlertItem, NotificationAlertType, NotificationBubbleItem, NotificationBubbleType, NotificationConfirmItem, PlaySound, ProductImageUtility, TradingNotificationType } from '../../api'; +import { useMessageEvent } from '../events'; + +const cleanText = (text: string) => (text && text.length) ? text.replace(/\\r/g, '\r') : ''; + +const getTimeZeroPadded = (time: number) => +{ + const text = ('0' + time); + + return text.substr((text.length - 2), text.length); +} + +let modDisclaimerTimeout: ReturnType = null; + +const useNotificationState = () => +{ + const [ alerts, setAlerts ] = useState([]); + const [ bubbleAlerts, setBubbleAlerts ] = useState([]); + const [ confirms, setConfirms ] = useState([]); + const [ bubblesDisabled, setBubblesDisabled ] = useState(false); + const [ modDisclaimerShown, setModDisclaimerShown ] = useState(false); + + const getMainNotificationConfig = () => GetConfiguration<{ [key: string]: { delivery?: string, display?: string; title?: string; image?: string }}>('notification', {}); + + const getNotificationConfig = (key: string) => + { + const mainConfig = getMainNotificationConfig(); + + if(!mainConfig) return null; + + return mainConfig[key]; + } + + const getNotificationPart = (options: Map, type: string, key: string, localize: boolean) => + { + if(options.has(key)) return options.get(key); + + const localizeKey = [ 'notification', type, key ].join('.'); + + if(GetNitroInstance().localization.hasValue(localizeKey) || localize) return LocalizeText(localizeKey, Array.from(options.keys()), Array.from(options.values())); + + return null; + } + + const getNotificationImageUrl = (options: Map, type: string) => + { + let imageUrl = options.get('image'); + + if(!imageUrl) imageUrl = GetConfiguration('image.library.notifications.url', '').replace('%image%', type.replace(/\./g, '_')); + + return LocalizeText(imageUrl); + } + + const simpleAlert = useCallback((message: string, type: string = null, clickUrl: string = null, clickUrlText: string = null, title: string = null, imageUrl: string = null) => + { + if(!title || !title.length) title = LocalizeText('notifications.broadcast.title'); + + if(!type || !type.length) type = NotificationAlertType.DEFAULT; + + const alertItem = new NotificationAlertItem([ cleanText(message) ], type, clickUrl, clickUrlText, title, imageUrl); + + setAlerts(prevValue => [ alertItem, ...prevValue ]); + }, []); + + const showNitroAlert = useCallback(() => simpleAlert(null, NotificationAlertType.NITRO), [ simpleAlert ]); + + const showSingleBubble = useCallback((message: string, type: string, imageUrl: string = null, internalLink: string = null) => + { + if(bubblesDisabled) return; + + const notificationItem = new NotificationBubbleItem(message, type, imageUrl, internalLink); + + setBubbleAlerts(prevValue => [ notificationItem, ...prevValue ]); + }, [ bubblesDisabled ]); + + const showNotification = (type: string, options: Map = null) => + { + if(!options) options = new Map(); + + const configuration = getNotificationConfig(('notification.' + type)); + + if(configuration) for(const key in configuration) options.set(key, configuration[key]); + + if (type === 'floorplan_editor.error') options.set('message', options.get('message').replace(/[^a-zA-Z._ ]/g, '')); + + const title = getNotificationPart(options, type, 'title', true); + const message = getNotificationPart(options, type, 'message', true).replace(/\\r/g, '\r'); + const linkTitle = getNotificationPart(options, type, 'linkTitle', false); + const linkUrl = getNotificationPart(options, type, 'linkUrl', false); + const image = getNotificationImageUrl(options, type); + + if(options.get('display') === 'BUBBLE') + { + showSingleBubble(LocalizeText(message), NotificationBubbleType.INFO, image, linkUrl); + } + else + { + simpleAlert(LocalizeText(message), type, linkUrl, linkTitle, title, image); + } + + if(options.get('sound')) PlaySound(options.get('sound')); + } + + const showConfirm = useCallback((message: string, onConfirm: () => void, onCancel: () => void, confirmText: string = null, cancelText: string = null, title: string = null, type: string = null) => + { + if(!confirmText || !confirmText.length) confirmText = LocalizeText('generic.confirm'); + + if(!cancelText || !cancelText.length) cancelText = LocalizeText('generic.cancel'); + + if(!title || !title.length) title = LocalizeText('notifications.broadcast.title'); + + const confirmItem = new NotificationConfirmItem(type, message, onConfirm, onCancel, confirmText, cancelText, title); + + setConfirms(prevValue => [ confirmItem, ...prevValue ]); + }, []); + + const showModeratorMessage = (message: string, url: string = null, showHabboWay: boolean = true) => + { + simpleAlert(message, NotificationAlertType.DEFAULT, url, LocalizeText('mod.alert.link'), LocalizeText('mod.alert.title')); + } + + const showTradeAlert = useCallback((type: number, otherUsername: string = '') => + { + switch(type) + { + case TradingNotificationType.ALERT_SCAM: + simpleAlert(LocalizeText('inventory.trading.warning.other_not_offering'), null, null, null, LocalizeText('inventory.trading.notification.title')); + return; + case TradingNotificationType.HOTEL_TRADING_DISABLED: + case TradingNotificationType.YOU_NOT_ALLOWED: + case TradingNotificationType.THEY_NOT_ALLOWED: + case TradingNotificationType.ROOM_DISABLED: + case TradingNotificationType.YOU_OPEN: + case TradingNotificationType.THEY_OPEN: + simpleAlert(LocalizeText(`inventory.trading.openfail.${ type }`, [ 'otherusername' ], [ otherUsername ]), null, null, null, LocalizeText('inventory.trading.openfail.title')); + return; + case TradingNotificationType.ERROR_WHILE_COMMIT: + simpleAlert(`${ LocalizeText('inventory.trading.notification.caption') }, ${ LocalizeText('inventory.trading.notification.commiterror.info') }`, null, null, null, LocalizeText('inventory.trading.notification.title')); + return; + case TradingNotificationType.THEY_CANCELLED: + simpleAlert(LocalizeText('inventory.trading.info.closed'), null, null, null, LocalizeText('inventory.trading.notification.title')); + return; + } + }, [ simpleAlert ]); + + const closeAlert = useCallback((alert: NotificationAlertItem) => + { + setAlerts(prevValue => + { + const newAlerts = [ ...prevValue ]; + const index = newAlerts.findIndex(value => (alert === value)); + + if(index >= 0) newAlerts.splice(index, 1); + + return newAlerts; + }); + }, []); + + const closeBubbleAlert = useCallback((item: NotificationBubbleItem) => + { + setBubbleAlerts(prevValue => + { + const newAlerts = [ ...prevValue ]; + const index = newAlerts.findIndex(value => (item === value)); + + if(index >= 0) newAlerts.splice(index, 1); + + return newAlerts; + }) + }, []); + + const closeConfirm = useCallback((item: NotificationConfirmItem) => + { + setConfirms(prevValue => + { + const newConfirms = [ ...prevValue ]; + const index = newConfirms.findIndex(value => (item === value)); + + if(index >= 0) newConfirms.splice(index, 1); + + return newConfirms; + }) + }, []); + + useMessageEvent(RespectReceivedEvent, event => + { + const parser = event.getParser(); + + if(parser.userId !== GetSessionDataManager().userId) return; + + const text1 = LocalizeText('notifications.text.respect.1'); + const text2 = LocalizeText('notifications.text.respect.2', [ 'count' ], [ parser.respectsReceived.toString() ]); + + showSingleBubble(text1, NotificationBubbleType.RESPECT); + showSingleBubble(text2, NotificationBubbleType.RESPECT); + }); + + useMessageEvent(HabboBroadcastMessageEvent, event => + { + const parser = event.getParser(); + + simpleAlert(parser.message.replace(/\\r/g, '\r'), null, null, LocalizeText('notifications.broadcast.title')); + }); + + useMessageEvent(AchievementNotificationMessageEvent, event => + { + const parser = event.getParser(); + + const text1 = LocalizeText('achievements.levelup.desc'); + const badgeName = LocalizeBadgeName(parser.data.badgeCode); + const badgeImage = GetSessionDataManager().getBadgeUrl(parser.data.badgeCode); + const internalLink = 'questengine/achievements/' + parser.data.category; + + showSingleBubble((text1 + ' ' + badgeName), NotificationBubbleType.ACHIEVEMENT, badgeImage, internalLink); + }); + + useMessageEvent(ClubGiftNotificationEvent, event => + { + const parser = event.getParser(); + + if(parser.numGifts <= 0) return; + + showSingleBubble(parser.numGifts.toString(), NotificationBubbleType.CLUBGIFT, null, ('catalog/open/' + GetConfiguration('catalog.links')['hc.hc_gifts'])); + }); + + useMessageEvent(ModeratorMessageEvent, event => + { + const parser = event.getParser(); + + showModeratorMessage(parser.message, parser.url, false); + }); + + useMessageEvent(ActivityPointNotificationMessageEvent, event => + { + const parser = event.getParser(); + + if((parser.amountChanged <= 0) || (parser.type !== 5)) return; + + const imageUrl = GetConfiguration('currency.asset.icon.url', '').replace('%type%', parser.type.toString()); + + showSingleBubble(LocalizeText('notifications.text.loyalty.received', [ 'AMOUNT' ], [ parser.amountChanged.toString() ]), NotificationBubbleType.INFO, imageUrl); + }); + + useMessageEvent(UserBannedMessageEvent, event => + { + const parser = event.getParser(); + + showModeratorMessage(parser.message); + }); + + useMessageEvent(HotelClosesAndWillOpenAtEvent, event => + { + const parser = event.getParser(); + + simpleAlert( LocalizeText(('opening.hours.' + (parser.userThrowOutAtClose ? 'disconnected' : 'closed')), [ 'h', 'm' ], [ getTimeZeroPadded(parser.openHour), getTimeZeroPadded(parser.openMinute) ]), NotificationAlertType.DEFAULT, null, null, LocalizeText('opening.hours.title')); + }); + + useMessageEvent(PetReceivedMessageEvent, event => + { + const parser = event.getParser(); + + const text = LocalizeText('notifications.text.' + (parser.boughtAsGift ? 'petbought' : 'petreceived')); + + let imageUrl: string = null; + + const imageResult = GetRoomEngine().getRoomObjectPetImage(parser.pet.typeId, parser.pet.paletteId, parseInt(parser.pet.color, 16), new Vector3d(45 * 3), 64, null, true); + + if(imageResult) imageUrl = imageResult.getImage().src; + + showSingleBubble(text, NotificationBubbleType.PETLEVEL, imageUrl); + }); + + useMessageEvent(MOTDNotificationEvent, event => + { + const parser = event.getParser(); + + const messages = parser.messages.map(message => cleanText(message)); + + const alertItem = new NotificationAlertItem(messages, NotificationAlertType.MOTD, null, null, LocalizeText('notifications.motd.title')); + + setAlerts(prevValue => [ alertItem, ...prevValue ]); + }); + + useMessageEvent(PetLevelNotificationEvent, event => + { + const parser = event.getParser(); + + let imageUrl: string = null; + + const imageResult = GetRoomEngine().getRoomObjectPetImage(parser.figureData.typeId, parser.figureData.paletteId, parseInt(parser.figureData.color, 16), new Vector3d(45 * 3), 64, null, true); + + if(imageResult) imageUrl = imageResult.getImage().src; + + showSingleBubble(LocalizeText('notifications.text.petlevel', [ 'pet_name', 'level' ], [ parser.petName, parser.level.toString() ]), NotificationBubbleType.PETLEVEL, imageUrl); + }); + + useMessageEvent(InfoFeedEnableMessageEvent, event => + { + const parser = event.getParser(); + + setBubblesDisabled(!parser.enabled); + }); + + useMessageEvent(ClubGiftSelectedEvent, event => + { + const parser = event.getParser(); + + if(!parser.products || !parser.products.length) return; + + const productData = parser.products[0]; + + if(!productData) return; + + showSingleBubble(LocalizeText('notifications.text.club_gift.selected'), NotificationBubbleType.INFO, ProductImageUtility.getProductImageUrl(productData.productType, productData.furniClassId, productData.extraParam)); + }); + + useMessageEvent(MaintenanceStatusMessageEvent, event => + { + const parser = event.getParser(); + + simpleAlert(LocalizeText('maintenance.shutdown', [ 'm', 'd' ], [ parser.minutesUntilMaintenance.toString(), parser.duration.toString() ]), NotificationAlertType.DEFAULT, null, null, LocalizeText('opening.hours.title')); + }); + + useMessageEvent(ModeratorCautionEvent, event => + { + const parser = event.getParser(); + + showModeratorMessage(parser.message, parser.url); + }); + + useMessageEvent(NotificationDialogMessageEvent, event => + { + const parser = event.getParser(); + + showNotification(parser.type, parser.parameters); + }); + + useMessageEvent(HotelWillCloseInMinutesEvent, event => + { + const parser = event.getParser(); + + simpleAlert(LocalizeText('opening.hours.shutdown', [ 'm' ], [ parser.openMinute.toString() ]), NotificationAlertType.DEFAULT, null, null, LocalizeText('opening.hours.title')); + }); + + useMessageEvent(HotelClosedAndOpensEvent, event => + { + const parser = event.getParser(); + + simpleAlert(LocalizeText('opening.hours.disconnected', [ 'h', 'm' ], [ parser.openHour.toString(), parser.openMinute.toString() ]), NotificationAlertType.DEFAULT, null, null, LocalizeText('opening.hours.title')); + }); + + useMessageEvent(ConnectionErrorEvent, event => + { + const parser = event.getParser(); + + switch(parser.errorCode) + { + default: + case 0: + simpleAlert(LocalizeText('connection.server.error.desc', [ 'errorCode' ], [ parser.errorCode.toString() ]), NotificationAlertType.ALERT, null, null, LocalizeText('connection.server.error.title')); + break; + case 1001: + case 1002: + case 1003: + case 1004: + case 1005: + case 1006: + case 1007: + case 1008: + case 1009: + case 1010: + case 1011: + case 1012: + case 1013: + case 1014: + case 1015: + case 1016: + case 1017: + case 1018: + case 1019: + event.connection.dispose(); + break; + case 4013: + simpleAlert(LocalizeText('connection.room.maintenance.desc'), NotificationAlertType.ALERT, null, null, LocalizeText('connection.room.maintenance.title')); + break; + } + }); + + useMessageEvent(SimpleAlertMessageEvent, event => + { + const parser = event.getParser(); + + simpleAlert(LocalizeText(parser.alertMessage), NotificationAlertType.DEFAULT, null, null, LocalizeText(parser.titleMessage ? parser.titleMessage : 'notifications.broadcast.title')); + }); + + const onRoomEnterEvent = useCallback(() => + { + if(modDisclaimerShown) return; + + if(RoomEnterEffect.isRunning()) + { + if(modDisclaimerTimeout) return; + + modDisclaimerTimeout = setTimeout(() => + { + onRoomEnterEvent(); + }, (RoomEnterEffect.totalRunningTime + 5000)); + } + else + { + if(modDisclaimerTimeout) + { + clearTimeout(modDisclaimerTimeout); + + modDisclaimerTimeout = null; + } + + showSingleBubble(LocalizeText('mod.chatdisclaimer'), NotificationBubbleType.INFO); + + setModDisclaimerShown(true); + } + }, [ modDisclaimerShown, showSingleBubble ]); + + useMessageEvent(RoomEnterEvent, onRoomEnterEvent); + + return { alerts, bubbleAlerts, confirms, simpleAlert, showNitroAlert, showTradeAlert, showConfirm, showSingleBubble, closeAlert, closeBubbleAlert, closeConfirm }; +} + +export const useNotification = () => useBetween(useNotificationState); diff --git a/apps/frontend/src/hooks/purse/index.ts b/apps/frontend/src/hooks/purse/index.ts new file mode 100644 index 0000000..d9d1ff7 --- /dev/null +++ b/apps/frontend/src/hooks/purse/index.ts @@ -0,0 +1 @@ +export * from './usePurse'; diff --git a/apps/frontend/src/hooks/purse/usePurse.ts b/apps/frontend/src/hooks/purse/usePurse.ts new file mode 100644 index 0000000..6bb5a54 --- /dev/null +++ b/apps/frontend/src/hooks/purse/usePurse.ts @@ -0,0 +1,126 @@ +import { ActivityPointNotificationMessageEvent, UserCreditsEvent, UserCurrencyComposer, UserCurrencyEvent, UserSubscriptionComposer, UserSubscriptionEvent, UserSubscriptionParser } from '@nitrots/nitro-renderer'; +import { useEffect, useMemo, useState } from 'react'; +import { useBetween } from 'use-between'; +import { CloneObject, ClubStatus, GetConfiguration, IPurse, PlaySound, Purse, SendMessageComposer, SoundNames } from '../../api'; +import { useMessageEvent } from '../events'; + +const usePurseState = () => +{ + const [ purse, setPurse ] = useState(new Purse()); + const hcDisabled = useMemo(() => GetConfiguration('hc.disabled', false), []); + + const clubStatus = useMemo(() => + { + if(hcDisabled || (purse.clubDays > 0)) return ClubStatus.ACTIVE; + + if((purse.pastVipDays > 0) || (purse.pastVipDays > 0)) return ClubStatus.EXPIRED; + + return ClubStatus.NONE; + }, [ purse, hcDisabled ]); + + const getCurrencyAmount = (type: number) => + { + if(type === -1) return purse.credits; + + for(const [ key, value ] of purse.activityPoints.entries()) + { + if(key !== type) continue; + + return value; + } + + return 0; + } + + useMessageEvent(UserCreditsEvent, event => + { + const parser = event.getParser(); + + setPurse(prevValue => + { + const newValue = CloneObject(prevValue); + + newValue.credits = parseFloat(parser.credits); + + if(prevValue.credits !== newValue.credits) PlaySound(SoundNames.CREDITS); + + return newValue; + }); + }); + + useMessageEvent(UserCurrencyEvent, event => + { + const parser = event.getParser(); + + setPurse(prevValue => + { + const newValue = CloneObject(prevValue); + + newValue.activityPoints = parser.currencies; + + return newValue; + }); + }); + + useMessageEvent(ActivityPointNotificationMessageEvent, event => + { + const parser = event.getParser(); + + setPurse(prevValue => + { + const newValue = CloneObject(prevValue); + + newValue.activityPoints = new Map(newValue.activityPoints); + + newValue.activityPoints.set(parser.type, parser.amount); + + if(parser.type === 0) PlaySound(SoundNames.DUCKETS) + + return newValue; + }); + }); + + useMessageEvent(UserSubscriptionEvent, event => + { + const parser = event.getParser(); + const productName = parser.productName; + + if((productName !== 'club_habbo') && (productName !== 'habbo_club')) return; + + setPurse(prevValue => + { + const newValue = CloneObject(prevValue); + + newValue.clubDays = Math.max(0, parser.daysToPeriodEnd); + newValue.clubPeriods = Math.max(0, parser.periodsSubscribedAhead); + newValue.isVip = parser.isVip; + newValue.pastClubDays = parser.pastClubDays; + newValue.pastVipDays = parser.pastVipDays; + newValue.isExpiring = ((parser.responseType === UserSubscriptionParser.RESPONSE_TYPE_DISCOUNT_AVAILABLE) ? true : false); + newValue.minutesUntilExpiration = parser.minutesUntilExpiration; + newValue.minutesSinceLastModified = parser.minutesSinceLastModified; + + return newValue; + }); + }); + + useEffect(() => + { + if(hcDisabled) return; + + SendMessageComposer(new UserSubscriptionComposer('habbo_club')); + + const interval = setInterval(() => SendMessageComposer(new UserSubscriptionComposer('habbo_club')), 50000); + + return () => clearInterval(interval); + }, [ hcDisabled ]); + + useEffect(() => + { + SendMessageComposer(new UserCurrencyComposer()); + }, []); + + return { purse, hcDisabled, clubStatus, getCurrencyAmount }; +} + +export const usePurse = () => useBetween(usePurseState); diff --git a/apps/frontend/src/hooks/rooms/engine/index.ts b/apps/frontend/src/hooks/rooms/engine/index.ts new file mode 100644 index 0000000..42364b6 --- /dev/null +++ b/apps/frontend/src/hooks/rooms/engine/index.ts @@ -0,0 +1,9 @@ +export * from './useFurniAddedEvent'; +export * from './useFurniRemovedEvent'; +export * from './useObjectDeselectedEvent'; +export * from './useObjectDoubleClickedEvent'; +export * from './useObjectRollOutEvent'; +export * from './useObjectRollOverEvent'; +export * from './useObjectSelectedEvent'; +export * from './useUserAddedEvent'; +export * from './useUserRemovedEvent'; diff --git a/apps/frontend/src/hooks/rooms/engine/useFurniAddedEvent.ts b/apps/frontend/src/hooks/rooms/engine/useFurniAddedEvent.ts new file mode 100644 index 0000000..f5422c6 --- /dev/null +++ b/apps/frontend/src/hooks/rooms/engine/useFurniAddedEvent.ts @@ -0,0 +1,19 @@ +import { useEffect } from 'react'; +import { RoomWidgetUpdateRoomObjectEvent, UI_EVENT_DISPATCHER } from '../../../api'; + +export const useFurniAddedEvent = (isActive: boolean, handler: (event: RoomWidgetUpdateRoomObjectEvent) => void) => +{ + useEffect(() => + { + if(!isActive) return; + + const onRoomWidgetUpdateRoomObjectEvent = (event: RoomWidgetUpdateRoomObjectEvent) => handler(event); + + UI_EVENT_DISPATCHER.addEventListener(RoomWidgetUpdateRoomObjectEvent.FURNI_ADDED, onRoomWidgetUpdateRoomObjectEvent); + + return () => + { + UI_EVENT_DISPATCHER.removeEventListener(RoomWidgetUpdateRoomObjectEvent.FURNI_ADDED, onRoomWidgetUpdateRoomObjectEvent); + } + }, [ isActive, handler ]); +} diff --git a/apps/frontend/src/hooks/rooms/engine/useFurniRemovedEvent.ts b/apps/frontend/src/hooks/rooms/engine/useFurniRemovedEvent.ts new file mode 100644 index 0000000..ddd1be2 --- /dev/null +++ b/apps/frontend/src/hooks/rooms/engine/useFurniRemovedEvent.ts @@ -0,0 +1,19 @@ +import { useEffect } from 'react'; +import { RoomWidgetUpdateRoomObjectEvent, UI_EVENT_DISPATCHER } from '../../../api'; + +export const useFurniRemovedEvent = (isActive: boolean, handler: (event: RoomWidgetUpdateRoomObjectEvent) => void) => +{ + useEffect(() => + { + if(!isActive) return; + + const onRoomWidgetUpdateRoomObjectEvent = (event: RoomWidgetUpdateRoomObjectEvent) => handler(event); + + UI_EVENT_DISPATCHER.addEventListener(RoomWidgetUpdateRoomObjectEvent.FURNI_REMOVED, onRoomWidgetUpdateRoomObjectEvent); + + return () => + { + UI_EVENT_DISPATCHER.removeEventListener(RoomWidgetUpdateRoomObjectEvent.FURNI_REMOVED, onRoomWidgetUpdateRoomObjectEvent); + } + }, [ isActive, handler ]); +} diff --git a/apps/frontend/src/hooks/rooms/engine/useObjectDeselectedEvent.ts b/apps/frontend/src/hooks/rooms/engine/useObjectDeselectedEvent.ts new file mode 100644 index 0000000..ba54981 --- /dev/null +++ b/apps/frontend/src/hooks/rooms/engine/useObjectDeselectedEvent.ts @@ -0,0 +1,7 @@ +import { RoomWidgetUpdateRoomObjectEvent } from '../../../api'; +import { useUiEvent } from '../../events'; + +export const useObjectDeselectedEvent = (handler: (event: RoomWidgetUpdateRoomObjectEvent) => void) => +{ + useUiEvent(RoomWidgetUpdateRoomObjectEvent.OBJECT_DESELECTED, handler); +} diff --git a/apps/frontend/src/hooks/rooms/engine/useObjectDoubleClickedEvent.ts b/apps/frontend/src/hooks/rooms/engine/useObjectDoubleClickedEvent.ts new file mode 100644 index 0000000..66e3673 --- /dev/null +++ b/apps/frontend/src/hooks/rooms/engine/useObjectDoubleClickedEvent.ts @@ -0,0 +1,7 @@ +import { RoomWidgetUpdateRoomObjectEvent } from '../../../api'; +import { useUiEvent } from '../../events'; + +export const useObjectDoubleClickedEvent = (handler: (event: RoomWidgetUpdateRoomObjectEvent) => void) => +{ + useUiEvent(RoomWidgetUpdateRoomObjectEvent.OBJECT_DOUBLE_CLICKED, handler); +} diff --git a/apps/frontend/src/hooks/rooms/engine/useObjectRollOutEvent.ts b/apps/frontend/src/hooks/rooms/engine/useObjectRollOutEvent.ts new file mode 100644 index 0000000..433ca91 --- /dev/null +++ b/apps/frontend/src/hooks/rooms/engine/useObjectRollOutEvent.ts @@ -0,0 +1,7 @@ +import { RoomWidgetUpdateRoomObjectEvent } from '../../../api'; +import { useUiEvent } from '../../events'; + +export const useObjectRollOutEvent = (handler: (event: RoomWidgetUpdateRoomObjectEvent) => void) => +{ + useUiEvent(RoomWidgetUpdateRoomObjectEvent.OBJECT_ROLL_OUT, handler); +} diff --git a/apps/frontend/src/hooks/rooms/engine/useObjectRollOverEvent.ts b/apps/frontend/src/hooks/rooms/engine/useObjectRollOverEvent.ts new file mode 100644 index 0000000..2774ef2 --- /dev/null +++ b/apps/frontend/src/hooks/rooms/engine/useObjectRollOverEvent.ts @@ -0,0 +1,7 @@ +import { RoomWidgetUpdateRoomObjectEvent } from '../../../api'; +import { useUiEvent } from '../../events'; + +export const useObjectRollOverEvent = (handler: (event: RoomWidgetUpdateRoomObjectEvent) => void) => +{ + useUiEvent(RoomWidgetUpdateRoomObjectEvent.OBJECT_ROLL_OVER, handler); +} diff --git a/apps/frontend/src/hooks/rooms/engine/useObjectSelectedEvent.ts b/apps/frontend/src/hooks/rooms/engine/useObjectSelectedEvent.ts new file mode 100644 index 0000000..38db66f --- /dev/null +++ b/apps/frontend/src/hooks/rooms/engine/useObjectSelectedEvent.ts @@ -0,0 +1,7 @@ +import { RoomWidgetUpdateRoomObjectEvent } from '../../../api'; +import { useUiEvent } from '../../events'; + +export const useObjectSelectedEvent = (handler: (event: RoomWidgetUpdateRoomObjectEvent) => void) => +{ + useUiEvent(RoomWidgetUpdateRoomObjectEvent.OBJECT_SELECTED, handler); +} diff --git a/apps/frontend/src/hooks/rooms/engine/useUserAddedEvent.ts b/apps/frontend/src/hooks/rooms/engine/useUserAddedEvent.ts new file mode 100644 index 0000000..d3c761b --- /dev/null +++ b/apps/frontend/src/hooks/rooms/engine/useUserAddedEvent.ts @@ -0,0 +1,19 @@ +import { useEffect } from 'react'; +import { RoomWidgetUpdateRoomObjectEvent, UI_EVENT_DISPATCHER } from '../../../api'; + +export const useUserAddedEvent = (isActive: boolean, handler: (event: RoomWidgetUpdateRoomObjectEvent) => void) => +{ + useEffect(() => + { + if(!isActive) return; + + const onRoomWidgetUpdateRoomObjectEvent = (event: RoomWidgetUpdateRoomObjectEvent) => handler(event); + + UI_EVENT_DISPATCHER.addEventListener(RoomWidgetUpdateRoomObjectEvent.USER_ADDED, onRoomWidgetUpdateRoomObjectEvent); + + return () => + { + UI_EVENT_DISPATCHER.removeEventListener(RoomWidgetUpdateRoomObjectEvent.USER_ADDED, onRoomWidgetUpdateRoomObjectEvent); + } + }, [ isActive, handler ]); +} diff --git a/apps/frontend/src/hooks/rooms/engine/useUserRemovedEvent.ts b/apps/frontend/src/hooks/rooms/engine/useUserRemovedEvent.ts new file mode 100644 index 0000000..2820241 --- /dev/null +++ b/apps/frontend/src/hooks/rooms/engine/useUserRemovedEvent.ts @@ -0,0 +1,19 @@ +import { useEffect } from 'react'; +import { RoomWidgetUpdateRoomObjectEvent, UI_EVENT_DISPATCHER } from '../../../api'; + +export const useUserRemovedEvent = (isActive: boolean, handler: (event: RoomWidgetUpdateRoomObjectEvent) => void) => +{ + useEffect(() => + { + if(!isActive) return; + + const onRoomWidgetUpdateRoomObjectEvent = (event: RoomWidgetUpdateRoomObjectEvent) => handler(event); + + UI_EVENT_DISPATCHER.addEventListener(RoomWidgetUpdateRoomObjectEvent.USER_REMOVED, onRoomWidgetUpdateRoomObjectEvent); + + return () => + { + UI_EVENT_DISPATCHER.removeEventListener(RoomWidgetUpdateRoomObjectEvent.USER_REMOVED, onRoomWidgetUpdateRoomObjectEvent); + } + }, [ isActive, handler ]); +} diff --git a/apps/frontend/src/hooks/rooms/index.ts b/apps/frontend/src/hooks/rooms/index.ts new file mode 100644 index 0000000..e57ecfb --- /dev/null +++ b/apps/frontend/src/hooks/rooms/index.ts @@ -0,0 +1,4 @@ +export * from './engine'; +export * from './promotes'; +export * from './useRoom'; +export * from './widgets'; diff --git a/apps/frontend/src/hooks/rooms/promotes/index.ts b/apps/frontend/src/hooks/rooms/promotes/index.ts new file mode 100644 index 0000000..b1fd0d3 --- /dev/null +++ b/apps/frontend/src/hooks/rooms/promotes/index.ts @@ -0,0 +1 @@ +export * from './useRoomPromote'; diff --git a/apps/frontend/src/hooks/rooms/promotes/useRoomPromote.ts b/apps/frontend/src/hooks/rooms/promotes/useRoomPromote.ts new file mode 100644 index 0000000..e534d18 --- /dev/null +++ b/apps/frontend/src/hooks/rooms/promotes/useRoomPromote.ts @@ -0,0 +1,23 @@ +import { RoomEventEvent, RoomEventMessageParser } from '@nitrots/nitro-renderer'; +import { useState } from 'react'; +import { useBetween } from 'use-between'; +import { useMessageEvent } from '../../events'; + +const useRoomPromoteState = () => +{ + const [ promoteInformation, setPromoteInformation ] = useState(null); + const [ isExtended, setIsExtended ] = useState(false); + + useMessageEvent(RoomEventEvent, event => + { + const parser = event.getParser(); + + if (!parser) return; + + setPromoteInformation(parser); + }); + + return { promoteInformation, isExtended, setPromoteInformation, setIsExtended }; +} + +export const useRoomPromote = () => useBetween(useRoomPromoteState); diff --git a/apps/frontend/src/hooks/rooms/useRoom.ts b/apps/frontend/src/hooks/rooms/useRoom.ts new file mode 100644 index 0000000..af1c12a --- /dev/null +++ b/apps/frontend/src/hooks/rooms/useRoom.ts @@ -0,0 +1,298 @@ +import { AdjustmentFilter, ColorConverter, IRoomSession, NitroContainer, NitroSprite, NitroTexture, RoomBackgroundColorEvent, RoomEngineEvent, RoomEngineObjectEvent, RoomGeometry, RoomId, RoomObjectCategory, RoomObjectHSLColorEnabledEvent, RoomObjectOperationType, RoomSessionEvent, RoomVariableEnum, Vector3d } from '@nitrots/nitro-renderer'; +import { useEffect, useState } from 'react'; +import { useBetween } from 'use-between'; +import { CanManipulateFurniture, DispatchUiEvent, GetNitroInstance, GetRoomEngine, GetRoomSession, InitializeRoomInstanceRenderingCanvas, IsFurnitureSelectionDisabled, ProcessRoomObjectOperation, RoomWidgetUpdateBackgroundColorPreviewEvent, RoomWidgetUpdateRoomObjectEvent, SetActiveRoomId, StartRoomSession } from '../../api'; +import { useRoomEngineEvent, useRoomSessionManagerEvent, useUiEvent } from '../events'; + +const useRoomState = () => +{ + const [ roomSession, setRoomSession ] = useState(null); + const [ roomBackground, setRoomBackground ] = useState(null); + const [ roomFilter, setRoomFilter ] = useState(null); + const [ originalRoomBackgroundColor, setOriginalRoomBackgroundColor ] = useState(0); + + const updateRoomBackgroundColor = (hue: number, saturation: number, lightness: number, original: boolean = false) => + { + if(!roomBackground) return; + + const newColor = ColorConverter.hslToRGB(((((hue & 0xFF) << 16) + ((saturation & 0xFF) << 8)) + (lightness & 0xFF))); + + if(original) setOriginalRoomBackgroundColor(newColor); + + if(!hue && !saturation && !lightness) + { + roomBackground.tint = 0; + } + else + { + roomBackground.tint = newColor; + } + } + + const updateRoomFilter = (color: number) => + { + if(!roomFilter) return; + + const r = ((color >> 16) & 0xFF); + const g = ((color >> 8) & 0xFF); + const b = (color & 0xFF); + + roomFilter.red = (r / 255); + roomFilter.green = (g / 255); + roomFilter.blue = (b / 255); + } + + useUiEvent(RoomWidgetUpdateBackgroundColorPreviewEvent.PREVIEW, event => updateRoomBackgroundColor(event.hue, event.saturation, event.lightness)); + + useUiEvent(RoomWidgetUpdateBackgroundColorPreviewEvent.CLEAR_PREVIEW, event => + { + if(!roomBackground) return; + + roomBackground.tint = originalRoomBackgroundColor; + }); + + useRoomEngineEvent(RoomObjectHSLColorEnabledEvent.ROOM_BACKGROUND_COLOR, event => + { + if(RoomId.isRoomPreviewerId(event.roomId)) return; + + if(event.enable) updateRoomBackgroundColor(event.hue, event.saturation, event.lightness, true); + else updateRoomBackgroundColor(0, 0, 0, true); + }); + + useRoomEngineEvent(RoomBackgroundColorEvent.ROOM_COLOR, event => + { + if(RoomId.isRoomPreviewerId(event.roomId)) return; + + let color = 0x000000; + let brightness = 0xFF; + + if(!event.bgOnly) + { + color = event.color; + brightness = event.brightness; + } + + updateRoomFilter(ColorConverter.hslToRGB(((ColorConverter.rgbToHSL(color) & 0xFFFF00) + brightness))); + }); + + useRoomEngineEvent([ + RoomEngineEvent.INITIALIZED, + RoomEngineEvent.DISPOSED + ], event => + { + if(RoomId.isRoomPreviewerId(event.roomId)) return; + + const session = GetRoomSession(); + + if(!session) return; + + switch(event.type) + { + case RoomEngineEvent.INITIALIZED: + SetActiveRoomId(event.roomId); + setRoomSession(session); + return; + case RoomEngineEvent.DISPOSED: + setRoomSession(null); + return; + } + }); + + useRoomSessionManagerEvent([ + RoomSessionEvent.CREATED, + RoomSessionEvent.ENDED + ], event => + { + switch(event.type) + { + case RoomSessionEvent.CREATED: + StartRoomSession(event.session); + return; + case RoomSessionEvent.ENDED: + setRoomSession(null); + return; + } + }); + + useRoomEngineEvent([ + RoomEngineObjectEvent.SELECTED, + RoomEngineObjectEvent.DESELECTED, + RoomEngineObjectEvent.ADDED, + RoomEngineObjectEvent.REMOVED, + RoomEngineObjectEvent.PLACED, + RoomEngineObjectEvent.REQUEST_MOVE, + RoomEngineObjectEvent.REQUEST_ROTATE, + RoomEngineObjectEvent.MOUSE_ENTER, + RoomEngineObjectEvent.MOUSE_LEAVE, + RoomEngineObjectEvent.DOUBLE_CLICK + ], event => + { + if(RoomId.isRoomPreviewerId(event.roomId)) return; + + let updateEvent: RoomWidgetUpdateRoomObjectEvent = null; + + switch(event.type) + { + case RoomEngineObjectEvent.SELECTED: + if(!IsFurnitureSelectionDisabled(event)) updateEvent = new RoomWidgetUpdateRoomObjectEvent(RoomWidgetUpdateRoomObjectEvent.OBJECT_SELECTED, event.objectId, event.category, event.roomId); + break; + case RoomEngineObjectEvent.DESELECTED: + updateEvent = new RoomWidgetUpdateRoomObjectEvent(RoomWidgetUpdateRoomObjectEvent.OBJECT_DESELECTED, event.objectId, event.category, event.roomId); + break; + case RoomEngineObjectEvent.ADDED: { + let addedEventType: string = null; + + switch(event.category) + { + case RoomObjectCategory.FLOOR: + case RoomObjectCategory.WALL: + addedEventType = RoomWidgetUpdateRoomObjectEvent.FURNI_ADDED; + break; + case RoomObjectCategory.UNIT: + addedEventType = RoomWidgetUpdateRoomObjectEvent.USER_ADDED; + break; + } + + if(addedEventType) updateEvent = new RoomWidgetUpdateRoomObjectEvent(addedEventType, event.objectId, event.category, event.roomId); + break; + } + case RoomEngineObjectEvent.REMOVED: { + let removedEventType: string = null; + + switch(event.category) + { + case RoomObjectCategory.FLOOR: + case RoomObjectCategory.WALL: + removedEventType = RoomWidgetUpdateRoomObjectEvent.FURNI_REMOVED; + break; + case RoomObjectCategory.UNIT: + removedEventType = RoomWidgetUpdateRoomObjectEvent.USER_REMOVED; + break; + } + + if(removedEventType) updateEvent = new RoomWidgetUpdateRoomObjectEvent(removedEventType, event.objectId, event.category, event.roomId); + break; + } + case RoomEngineObjectEvent.REQUEST_MOVE: + if(CanManipulateFurniture(roomSession, event.objectId, event.category)) ProcessRoomObjectOperation(event.objectId, event.category, RoomObjectOperationType.OBJECT_MOVE); + break; + case RoomEngineObjectEvent.REQUEST_ROTATE: + if(CanManipulateFurniture(roomSession, event.objectId, event.category)) ProcessRoomObjectOperation(event.objectId, event.category, RoomObjectOperationType.OBJECT_ROTATE_POSITIVE); + break; + case RoomEngineObjectEvent.MOUSE_ENTER: + updateEvent = new RoomWidgetUpdateRoomObjectEvent(RoomWidgetUpdateRoomObjectEvent.OBJECT_ROLL_OVER, event.objectId, event.category, event.roomId); + break; + case RoomEngineObjectEvent.MOUSE_LEAVE: + updateEvent = new RoomWidgetUpdateRoomObjectEvent(RoomWidgetUpdateRoomObjectEvent.OBJECT_ROLL_OUT, event.objectId, event.category, event.roomId); + break; + case RoomEngineObjectEvent.DOUBLE_CLICK: + updateEvent = new RoomWidgetUpdateRoomObjectEvent(RoomWidgetUpdateRoomObjectEvent.OBJECT_DOUBLE_CLICKED, event.objectId, event.category, event.roomId); + break; + } + + if(updateEvent) DispatchUiEvent(updateEvent); + }); + + useEffect(() => + { + if(!roomSession) return; + + const nitroInstance = GetNitroInstance(); + const roomEngine = GetRoomEngine(); + const roomId = roomSession.roomId; + const canvasId = 1; + const width = Math.floor(window.innerWidth); + const height = Math.floor(window.innerHeight); + const renderer = nitroInstance.application.renderer; + + if(renderer) + { + renderer.view.style.width = `${ width }px`; + renderer.view.style.height = `${ height }px`; + renderer.resolution = window.devicePixelRatio; + renderer.resize(width, height); + } + + const displayObject = roomEngine.getRoomInstanceDisplay(roomId, canvasId, width, height, RoomGeometry.SCALE_ZOOMED_IN); + const canvas = GetRoomEngine().getRoomInstanceRenderingCanvas(roomId, canvasId); + + if(!displayObject || !canvas) return; + + const background = new NitroSprite(NitroTexture.WHITE); + const filter = new AdjustmentFilter(); + const master = (canvas.master as NitroContainer); + + background.tint = 0; + background.width = width; + background.height = height; + + master.addChildAt(background, 0); + master.filters = [ filter ]; + + setRoomBackground(background); + setRoomFilter(filter); + + const geometry = (roomEngine.getRoomInstanceGeometry(roomId, canvasId) as RoomGeometry); + + if(geometry) + { + const minX = (roomEngine.getRoomInstanceVariable(roomId, RoomVariableEnum.ROOM_MIN_X) || 0); + const maxX = (roomEngine.getRoomInstanceVariable(roomId, RoomVariableEnum.ROOM_MAX_X) || 0); + const minY = (roomEngine.getRoomInstanceVariable(roomId, RoomVariableEnum.ROOM_MIN_Y) || 0); + const maxY = (roomEngine.getRoomInstanceVariable(roomId, RoomVariableEnum.ROOM_MAX_Y) || 0); + + let x = ((minX + maxX) / 2); + let y = ((minY + maxY) / 2); + + const offset = 20; + + x = (x + (offset - 1)); + y = (y + (offset - 1)); + + const z = (Math.sqrt(((offset * offset) + (offset * offset))) * Math.tan(((30 / 180) * Math.PI))); + + geometry.location = new Vector3d(x, y, z); + } + + const stage = nitroInstance.application.stage; + + if(!stage) return; + + stage.addChild(displayObject); + + SetActiveRoomId(roomSession.roomId); + + const resize = (event: UIEvent) => + { + const width = Math.floor(window.innerWidth); + const height = Math.floor(window.innerHeight); + + renderer.view.style.width = `${ width }px`; + renderer.view.style.height = `${ height }px`; + renderer.resolution = window.devicePixelRatio; + renderer.resize(width, height); + + background.width = width; + background.height = height; + + InitializeRoomInstanceRenderingCanvas(width, height, 1); + + nitroInstance.application.render(); + } + + window.addEventListener('resize', resize); + + return () => + { + setRoomBackground(null); + setRoomFilter(null); + setOriginalRoomBackgroundColor(0); + + window.removeEventListener('resize', resize); + } + }, [ roomSession ]); + + return { roomSession }; +} + +export const useRoom = () => useBetween(useRoomState); diff --git a/apps/frontend/src/hooks/rooms/widgets/furniture/index.ts b/apps/frontend/src/hooks/rooms/widgets/furniture/index.ts new file mode 100644 index 0000000..37fa573 --- /dev/null +++ b/apps/frontend/src/hooks/rooms/widgets/furniture/index.ts @@ -0,0 +1,19 @@ +export * from './useFurnitureBackgroundColorWidget'; +export * from './useFurnitureBadgeDisplayWidget'; +export * from './useFurnitureContextMenuWidget'; +export * from './useFurnitureCraftingWidget'; +export * from './useFurnitureDimmerWidget'; +export * from './useFurnitureExchangeWidget'; +export * from './useFurnitureExternalImageWidget'; +export * from './useFurnitureFriendFurniWidget'; +export * from './useFurnitureHighScoreWidget'; +export * from './useFurnitureInternalLinkWidget'; +export * from './useFurnitureMannequinWidget'; +export * from './useFurniturePlaylistEditorWidget'; +export * from './useFurniturePresentWidget'; +export * from './useFurnitureRoomLinkWidget'; +export * from './useFurnitureSpamWallPostItWidget'; +export * from './useFurnitureStackHeightWidget'; +export * from './useFurnitureStickieWidget'; +export * from './useFurnitureTrophyWidget'; +export * from './useFurnitureYoutubeWidget'; diff --git a/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureBackgroundColorWidget.ts b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureBackgroundColorWidget.ts new file mode 100644 index 0000000..273a037 --- /dev/null +++ b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureBackgroundColorWidget.ts @@ -0,0 +1,71 @@ +import { ApplyTonerComposer, ColorConverter, RoomEngineTriggerWidgetEvent, RoomObjectVariable } from '@nitrots/nitro-renderer'; +import { useEffect, useState } from 'react'; +import { CanManipulateFurniture, ColorUtils, DispatchUiEvent, GetRoomEngine, RoomWidgetUpdateBackgroundColorPreviewEvent, SendMessageComposer } from '../../../../api'; +import { useRoomEngineEvent } from '../../../events'; +import { useFurniRemovedEvent } from '../../engine'; +import { useRoom } from '../../useRoom'; + +const useFurnitureBackgroundColorWidgetState = () => +{ + const [ objectId, setObjectId ] = useState(-1); + const [ category, setCategory ] = useState(-1); + const [ color, setColor ] = useState(0); + const { roomSession = null } = useRoom(); + + const applyToner = () => + { + const hsl = ColorConverter.rgbToHSL(color); + const [ _, hue, saturation, lightness ] = ColorUtils.int_to_8BitVals(hsl); + SendMessageComposer(new ApplyTonerComposer(objectId, hue, saturation, lightness)); + } + + const toggleToner = () => roomSession.useMultistateItem(objectId); + + const onClose = () => + { + DispatchUiEvent(new RoomWidgetUpdateBackgroundColorPreviewEvent(RoomWidgetUpdateBackgroundColorPreviewEvent.CLEAR_PREVIEW)); + + setObjectId(-1); + setCategory(-1); + setColor(0); + } + + useRoomEngineEvent(RoomEngineTriggerWidgetEvent.REQUEST_BACKGROUND_COLOR, event => + { + if(!CanManipulateFurniture(roomSession, event.objectId, event.category)) return; + + const roomObject = GetRoomEngine().getRoomObject(event.roomId, event.objectId, event.category); + const model = roomObject.model; + + setObjectId(event.objectId); + setCategory(event.category) + const hue = parseInt(model.getValue(RoomObjectVariable.FURNITURE_ROOM_BACKGROUND_COLOR_HUE)); + const saturation = parseInt(model.getValue(RoomObjectVariable.FURNITURE_ROOM_BACKGROUND_COLOR_SATURATION)); + const light = parseInt(model.getValue(RoomObjectVariable.FURNITURE_ROOM_BACKGROUND_COLOR_LIGHTNESS)); + + const hsl = ColorUtils.eight_bitVals_to_int(0, hue,saturation,light); + + const rgbColor = ColorConverter.hslToRGB(hsl); + setColor(rgbColor); + }); + + useFurniRemovedEvent(((objectId !== -1) && (category !== -1)), event => + { + if((event.id !== objectId) || (event.category !== category)) return; + + onClose(); + }); + + useEffect(() => + { + if((objectId === -1) || (category === -1)) return; + + const hls = ColorConverter.rgbToHSL(color); + const [ _, hue, saturation, lightness ] = ColorUtils.int_to_8BitVals(hls); + DispatchUiEvent(new RoomWidgetUpdateBackgroundColorPreviewEvent(RoomWidgetUpdateBackgroundColorPreviewEvent.PREVIEW, hue, saturation, lightness)); + }, [ objectId, category, color ]); + + return { objectId, color, setColor, applyToner, toggleToner, onClose }; +} + +export const useFurnitureBackgroundColorWidget = useFurnitureBackgroundColorWidgetState; diff --git a/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureBadgeDisplayWidget.ts b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureBadgeDisplayWidget.ts new file mode 100644 index 0000000..5ef2bfc --- /dev/null +++ b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureBadgeDisplayWidget.ts @@ -0,0 +1,75 @@ +import { RoomEngineTriggerWidgetEvent, RoomObjectVariable, StringDataType } from '@nitrots/nitro-renderer'; +import { useState } from 'react'; +import { GetRoomEngine, GetSessionDataManager, LocalizeBadgeDescription, LocalizeBadgeName, LocalizeText } from '../../../../api'; +import { useRoomEngineEvent } from '../../../events'; +import { useNotification } from '../../../notification'; +import { useFurniRemovedEvent } from '../../engine'; + +const useFurnitureBadgeDisplayWidgetState = () => +{ + const [ objectId, setObjectId ] = useState(-1); + const [ category, setCategory ] = useState(-1); + const [ color, setColor ] = useState('1'); + const [ badgeName, setBadgeName ] = useState(''); + const [ badgeDesc, setBadgeDesc ] = useState(''); + const [ date, setDate ] = useState(''); + const [ senderName, setSenderName ] = useState(''); + const { simpleAlert = null } = useNotification(); + + const onClose = () => + { + setObjectId(-1); + setCategory(-1); + setColor('1'); + setBadgeName(''); + setBadgeDesc(''); + setDate(''); + setSenderName(''); + } + + useRoomEngineEvent([ + RoomEngineTriggerWidgetEvent.REQUEST_BADGE_DISPLAY_ENGRAVING, + RoomEngineTriggerWidgetEvent.REQUEST_ACHIEVEMENT_RESOLUTION_ENGRAVING + ], event => + { + const roomObject = GetRoomEngine().getRoomObject(event.roomId, event.objectId, event.category); + + if(!roomObject) return; + + const stringStuff = new StringDataType(); + + stringStuff.initializeFromRoomObjectModel(roomObject.model); + + setObjectId(event.objectId); + setCategory(event.category); + setColor('1'); + setBadgeName(LocalizeBadgeName(stringStuff.getValue(1))); + setBadgeDesc(LocalizeBadgeDescription(stringStuff.getValue(1))); + setDate(stringStuff.getValue(2)); + setSenderName(stringStuff.getValue(3)); + }); + + useRoomEngineEvent(RoomEngineTriggerWidgetEvent.REQUEST_ACHIEVEMENT_RESOLUTION_FAILED, event => + { + const roomObject = GetRoomEngine().getRoomObject(event.roomId, event.objectId, event.category); + + if(!roomObject) return; + + const ownerId = roomObject.model.getValue(RoomObjectVariable.FURNITURE_OWNER_ID); + + if(ownerId !== GetSessionDataManager().userId) return; + + simpleAlert(`${ LocalizeText('resolution.failed.subtitle') } ${ LocalizeText('resolution.failed.text') }`, null, null, null, LocalizeText('resolution.failed.title')); + }); + + useFurniRemovedEvent(((objectId !== -1) && (category !== -1)), event => + { + if((event.id !== objectId) || (event.category !== category)) return; + + onClose(); + }); + + return { objectId, category, color, badgeName, badgeDesc, date, senderName, onClose }; +} + +export const useFurnitureBadgeDisplayWidget = useFurnitureBadgeDisplayWidgetState; diff --git a/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureContextMenuWidget.ts b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureContextMenuWidget.ts new file mode 100644 index 0000000..74aecb9 --- /dev/null +++ b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureContextMenuWidget.ts @@ -0,0 +1,179 @@ +import { ContextMenuEnum, GroupFurniContextMenuInfoMessageEvent, GroupFurniContextMenuInfoMessageParser, RoomEngineTriggerWidgetEvent, RoomObjectCategory, RoomObjectVariable } from '@nitrots/nitro-renderer'; +import { useState } from 'react'; +import { GetRoomEngine, IsOwnerOfFurniture, TryJoinGroup, TryVisitRoom } from '../../../../api'; +import { useMessageEvent, useRoomEngineEvent } from '../../../events'; +import { useRoom } from '../../useRoom'; + +export const MONSTERPLANT_SEED_CONFIRMATION: string = 'MONSTERPLANT_SEED_CONFIRMATION'; +export const PURCHASABLE_CLOTHING_CONFIRMATION: string = 'PURCHASABLE_CLOTHING_CONFIRMATION'; +export const GROUP_FURNITURE: string = 'GROUP_FURNITURE'; +export const EFFECTBOX_OPEN: string = 'EFFECTBOX_OPEN'; +export const MYSTERYTROPHY_OPEN_DIALOG: string = 'MYSTERYTROPHY_OPEN_DIALOG'; + +const useFurnitureContextMenuWidgetState = () => +{ + const [ objectId, setObjectId ] = useState(-1); + const [ mode, setMode ] = useState(null); + const [ confirmMode, setConfirmMode ] = useState(null); + const [ confirmingObjectId, setConfirmingObjectId ] = useState(-1); + const [ groupData, setGroupData ] = useState(null); + const [ isGroupMember, setIsGroupMember ] = useState(false); + const [ objectOwnerId, setObjectOwnerId ] = useState(-1); + const { roomSession = null } = useRoom(); + + const onClose = () => + { + setObjectId(-1); + setGroupData(null); + setIsGroupMember(false); + setMode(null); + } + + const closeConfirm = () => + { + setConfirmMode(null); + setConfirmingObjectId(-1); + } + + const processAction = (name: string) => + { + if(name) + { + switch(name) + { + case 'use_friend_furni': + roomSession.useMultistateItem(objectId); + break; + case 'use_monsterplant_seed': + setConfirmMode(MONSTERPLANT_SEED_CONFIRMATION); + setConfirmingObjectId(objectId); + break; + case 'use_random_teleport': + GetRoomEngine().useRoomObject(objectId, RoomObjectCategory.FLOOR); + break; + case 'use_purchaseable_clothing': + setConfirmMode(PURCHASABLE_CLOTHING_CONFIRMATION); + setConfirmingObjectId(objectId); + break; + case 'use_mystery_box': + roomSession.useMultistateItem(objectId); + break; + case 'use_mystery_trophy': + setConfirmMode(MYSTERYTROPHY_OPEN_DIALOG); + setConfirmingObjectId(objectId); + break; + case 'join_group': + TryJoinGroup(groupData.guildId); + setIsGroupMember(true); + return; + case 'go_to_group_homeroom': + if(groupData) TryVisitRoom(groupData.guildHomeRoomId); + break; + } + } + + onClose(); + } + + useRoomEngineEvent([ + RoomEngineTriggerWidgetEvent.OPEN_FURNI_CONTEXT_MENU, + RoomEngineTriggerWidgetEvent.CLOSE_FURNI_CONTEXT_MENU, + RoomEngineTriggerWidgetEvent.REQUEST_MONSTERPLANT_SEED_PLANT_CONFIRMATION_DIALOG, + RoomEngineTriggerWidgetEvent.REQUEST_PURCHASABLE_CLOTHING_CONFIRMATION_DIALOG, + RoomEngineTriggerWidgetEvent.REQUEST_EFFECTBOX_OPEN_DIALOG, + RoomEngineTriggerWidgetEvent.REQUEST_MYSTERYBOX_OPEN_DIALOG, + RoomEngineTriggerWidgetEvent.REQUEST_MYSTERYTROPHY_OPEN_DIALOG + ], event => + { + const object = GetRoomEngine().getRoomObject(roomSession.roomId, event.objectId, event.category); + + if(!object) return; + + setObjectOwnerId(object.model.getValue(RoomObjectVariable.FURNITURE_OWNER_ID)); + + switch(event.type) + { + case RoomEngineTriggerWidgetEvent.REQUEST_MONSTERPLANT_SEED_PLANT_CONFIRMATION_DIALOG: + if(!IsOwnerOfFurniture(object)) return; + + setConfirmingObjectId(object.id); + setConfirmMode(MONSTERPLANT_SEED_CONFIRMATION); + + onClose(); + return; + case RoomEngineTriggerWidgetEvent.REQUEST_EFFECTBOX_OPEN_DIALOG: + if(!IsOwnerOfFurniture(object)) return; + + setConfirmingObjectId(object.id); + setConfirmMode(EFFECTBOX_OPEN); + + onClose(); + return; + case RoomEngineTriggerWidgetEvent.REQUEST_PURCHASABLE_CLOTHING_CONFIRMATION_DIALOG: + if(!IsOwnerOfFurniture(object)) return; + + setConfirmingObjectId(object.id); + setConfirmMode(PURCHASABLE_CLOTHING_CONFIRMATION); + + onClose(); + return; + case RoomEngineTriggerWidgetEvent.REQUEST_MYSTERYBOX_OPEN_DIALOG: + roomSession.useMultistateItem(object.id); + + onClose(); + return; + case RoomEngineTriggerWidgetEvent.REQUEST_MYSTERYTROPHY_OPEN_DIALOG: + if(!IsOwnerOfFurniture(object)) return; + + setConfirmingObjectId(object.id); + setConfirmMode(MYSTERYTROPHY_OPEN_DIALOG); + + onClose(); + return; + case RoomEngineTriggerWidgetEvent.OPEN_FURNI_CONTEXT_MENU: + + setObjectId(object.id); + + switch(event.contextMenu) + { + case ContextMenuEnum.FRIEND_FURNITURE: + setMode(ContextMenuEnum.FRIEND_FURNITURE); + return; + case ContextMenuEnum.MONSTERPLANT_SEED: + if(IsOwnerOfFurniture(object)) setMode(ContextMenuEnum.MONSTERPLANT_SEED); + return; + case ContextMenuEnum.MYSTERY_BOX: + setMode(ContextMenuEnum.MYSTERY_BOX); + return; + case ContextMenuEnum.MYSTERY_TROPHY: + if(IsOwnerOfFurniture(object)) setMode(ContextMenuEnum.MYSTERY_TROPHY); + return; + case ContextMenuEnum.RANDOM_TELEPORT: + setMode(ContextMenuEnum.RANDOM_TELEPORT); + return; + case ContextMenuEnum.PURCHASABLE_CLOTHING: + if(IsOwnerOfFurniture(object)) setMode(ContextMenuEnum.PURCHASABLE_CLOTHING); + return; + } + + return; + case RoomEngineTriggerWidgetEvent.CLOSE_FURNI_CONTEXT_MENU: + if(object.id === objectId) onClose(); + return; + } + }); + + useMessageEvent(GroupFurniContextMenuInfoMessageEvent, event => + { + const parser = event.getParser(); + + setObjectId(parser.objectId); + setGroupData(parser); + setIsGroupMember(parser.userIsMember); + setMode(GROUP_FURNITURE); + }); + + return { objectId, mode, confirmMode, confirmingObjectId, groupData, isGroupMember, objectOwnerId, closeConfirm, processAction, onClose }; +} + +export const useFurnitureContextMenuWidget = useFurnitureContextMenuWidgetState; diff --git a/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureCraftingWidget.ts b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureCraftingWidget.ts new file mode 100644 index 0000000..fc73c7d --- /dev/null +++ b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureCraftingWidget.ts @@ -0,0 +1,166 @@ +import { CraftableProductsEvent, CraftComposer, CraftingRecipeEvent, CraftingRecipeIngredientParser, CraftingRecipesAvailableEvent, CraftingResultEvent, GetCraftableProductsComposer, GetCraftingRecipeComposer, RoomEngineTriggerWidgetEvent, RoomWidgetEnum } from '@nitrots/nitro-renderer'; +import { useEffect, useState } from 'react'; +import { GetRoomEngine, ICraftingIngredient, ICraftingRecipe, LocalizeText, SendMessageComposer } from '../../../../api'; +import { useMessageEvent, useRoomEngineEvent } from '../../../events'; +import { useInventoryFurni } from '../../../inventory'; +import { useNotification } from './../../../notification'; + +const useFurnitureCraftingWidgetState = () => +{ + const [ objectId, setObjectId ] = useState(-1); + const [ recipes, setRecipes ] = useState([]); + const [ selectedRecipe, setSelectedRecipe ] = useState(null); + const [ ingredients, setIngredients ] = useState([]); + const [ ingredientNames, setIngredientNames ] = useState(null); + const [ cachedIngredients, setCachedIngredients ] = useState>(new Map()); + const [ isCrafting, setIsCrafting ] = useState(false); + const { groupItems = [], getItemsByType = null, activate = null, deactivate = null } = useInventoryFurni(); + const { simpleAlert = null } = useNotification(); + + const requiredIngredients = ((selectedRecipe && cachedIngredients.get(selectedRecipe.name) || null)); + + const resetData = () => + { + setRecipes([]); + setSelectedRecipe(null); + setIngredients([]); + setCachedIngredients(new Map()); + }; + + const onClose = () => + { + setObjectId(-1); + resetData(); + }; + + const craft = () => + { + setIsCrafting(true); + + SendMessageComposer(new CraftComposer(objectId, selectedRecipe.name)); + }; + + const selectRecipe = (recipe: ICraftingRecipe) => + { + setSelectedRecipe(recipe); + + const cache = cachedIngredients.get(recipe.name); + + if(!cache) SendMessageComposer(new GetCraftingRecipeComposer(recipe.name)); + } + + useRoomEngineEvent(RoomEngineTriggerWidgetEvent.OPEN_WIDGET, event => + { + if (event.widget !== RoomWidgetEnum.CRAFTING) return; + + setObjectId(event.objectId); + resetData(); + SendMessageComposer(new GetCraftableProductsComposer(event.objectId)); + }); + + useMessageEvent(CraftableProductsEvent, event => + { + const parser = event.getParser(); + + if (!parser.isActive()) + { + setObjectId(-1); + + return; + } + + setRecipes(prevValue => + { + const newValue: ICraftingRecipe[] = []; + + for(const recipe of parser.recipes) + { + //@ts-ignore + const itemId = GetRoomEngine().roomContentLoader._activeObjectTypeIds.get(recipe.itemName); + const iconUrl = GetRoomEngine().getFurnitureFloorIconUrl(itemId); + + newValue.push({ + name: recipe.recipeName, + localizedName: LocalizeText('roomItem.name.' + itemId), + iconUrl + }); + } + + return newValue; + }); + + setIngredientNames(parser.ingredients); + }); + + useMessageEvent(CraftingRecipeEvent, event => + { + const parser = event.getParser(); + + setCachedIngredients(prevValue => + { + const newValue = new Map(prevValue); + + newValue.set(selectedRecipe.name, parser.ingredients); + + return newValue; + }); + }); + + useMessageEvent(CraftingResultEvent, event => + { + setSelectedRecipe(null); + setIsCrafting(false); + + const parser = event.getParser(); + + if(parser.result) simpleAlert(LocalizeText('crafting.info.result.ok')); + }); + + useMessageEvent(CraftingRecipesAvailableEvent, event => + { + }); + + useEffect(() => + { + if(!ingredientNames || !ingredientNames.length) return; + + setIngredients(prevValue => + { + const newValue: ICraftingIngredient[] = []; + + for(const name of ingredientNames) + { + //@ts-ignore + const itemId = GetRoomEngine().roomContentLoader._activeObjectTypeIds.get(name); + const iconUrl = GetRoomEngine().getFurnitureFloorIconUrl(itemId); + + const inventoryItems = getItemsByType(itemId); + + let amountAvailable = 0; + + if (inventoryItems) for (const inventoryItem of inventoryItems) amountAvailable += inventoryItem.items.length; + + newValue.push({ + name: name, + iconUrl, + count: amountAvailable + }); + } + + return newValue; + }); + }, [ groupItems, ingredientNames, getItemsByType ]); + + useEffect(() => + { + if((objectId === -1)) return; + + const id = activate(); + + return () => deactivate(id); + }, [ objectId, activate, deactivate ]); + + return { objectId, recipes, ingredients, selectedRecipe, requiredIngredients, isCrafting, selectRecipe, craft, onClose }; +} + +export const useFurnitureCraftingWidget = useFurnitureCraftingWidgetState; diff --git a/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureDimmerWidget.ts b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureDimmerWidget.ts new file mode 100644 index 0000000..6ac8f89 --- /dev/null +++ b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureDimmerWidget.ts @@ -0,0 +1,110 @@ +import { RoomControllerLevel, RoomEngineDimmerStateEvent, RoomEngineTriggerWidgetEvent, RoomId, RoomSessionDimmerPresetsEvent } from '@nitrots/nitro-renderer'; +import { useEffect, useState } from 'react'; +import { DimmerFurnitureWidgetPresetItem, FurnitureDimmerUtilities, GetSessionDataManager } from '../../../../api'; +import { useRoomEngineEvent, useRoomSessionManagerEvent } from '../../../events'; +import { useRoom } from '../../useRoom'; + +const useFurnitureDimmerWidgetState = () => +{ + const [ presets, setPresets ] = useState([]); + const [ selectedPresetId, setSelectedPresetId ] = useState(0); + const [ dimmerState, setDimmerState ] = useState(0); + const [ lastDimmerState, setLastDimmerState ] = useState(0); + const [ effectId, setEffectId ] = useState(0); + const [ color, setColor ] = useState(0xFFFFFF); + const [ brightness, setBrightness ] = useState(0xFF); + const [ selectedEffectId, setSelectedEffectId ] = useState(0); + const [ selectedColor, setSelectedColor ] = useState(0); + const [ selectedBrightness, setSelectedBrightness ] = useState(0); + const { roomSession = null } = useRoom(); + + const canOpenWidget = () => (roomSession.isRoomOwner || (roomSession.controllerLevel >= RoomControllerLevel.GUEST) || GetSessionDataManager().isModerator); + + const selectPresetId = (id: number) => + { + const preset = presets[(id - 1)]; + + if(!preset) return; + + setSelectedPresetId(preset.id); + setSelectedEffectId(preset.type); + setSelectedColor(preset.color); + setSelectedBrightness(preset.light); + } + + const applyChanges = () => + { + if(dimmerState === 0) return; + + const selectedPresetIndex = (selectedPresetId - 1); + + if((selectedPresetId < 1) || (selectedPresetId > presets.length)) return; + + const preset = presets[selectedPresetIndex]; + + if(!preset || ((selectedEffectId === preset.type) && (selectedColor === preset.color) && (selectedBrightness === preset.light))) return; + + setPresets(prevValue => + { + const newValue = [ ...prevValue ]; + + newValue[selectedPresetIndex] = new DimmerFurnitureWidgetPresetItem(preset.id, selectedEffectId, selectedColor, selectedBrightness); + + return newValue; + }); + + FurnitureDimmerUtilities.savePreset(preset.id, selectedEffectId, selectedColor, selectedBrightness, true); + } + + useRoomEngineEvent(RoomEngineTriggerWidgetEvent.REQUEST_DIMMER, event => + { + if(!canOpenWidget()) return; + + roomSession.requestMoodlightSettings(); + }); + + useRoomSessionManagerEvent(RoomSessionDimmerPresetsEvent.ROOM_DIMMER_PRESETS, event => + { + const presets: DimmerFurnitureWidgetPresetItem[] = []; + + let i = 0; + + while(i < event.presetCount) + { + const preset = event.getPreset(i); + + if(preset) presets.push(new DimmerFurnitureWidgetPresetItem(preset.id, preset.type, preset.color, preset.brightness)); + + i++; + } + + setPresets(presets); + setSelectedPresetId(event.selectedPresetId); + }); + + useRoomEngineEvent(RoomEngineDimmerStateEvent.ROOM_COLOR, event => + { + if(RoomId.isRoomPreviewerId(event.roomId)) return; + + setLastDimmerState(dimmerState); + setDimmerState(event.state); + setSelectedPresetId(event.presetId); + setEffectId(event.effectId); + setSelectedEffectId(event.effectId); + setColor(event.color); + setSelectedColor(event.color); + setBrightness(event.brightness); + setSelectedBrightness(event.brightness); + }); + + useEffect(() => + { + if((dimmerState === 0) && (lastDimmerState === 0)) return; + + FurnitureDimmerUtilities.previewDimmer(selectedColor, selectedBrightness, (selectedEffectId === 2)); + }, [ dimmerState, lastDimmerState, selectedColor, selectedBrightness, selectedEffectId ]); + + return { presets, selectedPresetId, dimmerState, lastDimmerState, effectId, color, brightness, selectedEffectId, setSelectedEffectId, selectedColor, setSelectedColor, selectedBrightness, setSelectedBrightness, selectPresetId, applyChanges }; +} + +export const useFurnitureDimmerWidget = useFurnitureDimmerWidgetState; diff --git a/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureExchangeWidget.ts b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureExchangeWidget.ts new file mode 100644 index 0000000..0dbb3d4 --- /dev/null +++ b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureExchangeWidget.ts @@ -0,0 +1,48 @@ +import { FurnitureExchangeComposer, RoomEngineTriggerWidgetEvent, RoomObjectVariable } from '@nitrots/nitro-renderer'; +import { useState } from 'react'; +import { GetRoomEngine, GetRoomSession, IsOwnerOfFurniture } from '../../../../api'; +import { useRoomEngineEvent } from '../../../events'; +import { useFurniRemovedEvent } from '../../engine'; + +const useFurnitureExchangeWidgetState = () => +{ + const [ objectId, setObjectId ] = useState(-1); + const [ category, setCategory ] = useState(-1); + const [ value, setValue ] = useState(0); + + const onClose = () => + { + setObjectId(-1); + setCategory(-1); + setValue(0); + } + + const redeem = () => + { + GetRoomSession().connection.send(new FurnitureExchangeComposer(objectId)); + + onClose(); + } + + useRoomEngineEvent(RoomEngineTriggerWidgetEvent.REQUEST_CREDITFURNI, event => + { + const roomObject = GetRoomEngine().getRoomObject(event.roomId, event.objectId, event.category); + + if(!roomObject || !IsOwnerOfFurniture(roomObject)) return; + + setObjectId(event.objectId); + setCategory(event.category); + setValue(roomObject.model.getValue(RoomObjectVariable.FURNITURE_CREDIT_VALUE) || 0); + }); + + useFurniRemovedEvent(((objectId !== -1) && (category !== -1)), event => + { + if((event.id !== objectId) || (event.category !== category)) return; + + onClose(); + }); + + return { objectId, value, redeem, onClose }; +} + +export const useFurnitureExchangeWidget = useFurnitureExchangeWidgetState; diff --git a/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureExternalImageWidget.ts b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureExternalImageWidget.ts new file mode 100644 index 0000000..a628eb0 --- /dev/null +++ b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureExternalImageWidget.ts @@ -0,0 +1,74 @@ +import { RoomEngineTriggerWidgetEvent, RoomObjectCategory, RoomObjectVariable } from '@nitrots/nitro-renderer'; +import { useState } from 'react'; +import { GetRoomEngine, IPhotoData } from '../../../../api'; +import { useRoomEngineEvent } from '../../../events'; +import { useFurniRemovedEvent } from '../../engine'; +import { useRoom } from '../../useRoom'; + +const useFurnitureExternalImageWidgetState = () => +{ + const [ objectId, setObjectId ] = useState(-1); + const [ category, setCategory ] = useState(-1); + const [ currentPhotoIndex, setCurrentPhotoIndex ] = useState(-1); + const [ currentPhotos, setCurrentPhotos ] = useState([]); + const { roomSession = null } = useRoom(); + + const onClose = () => + { + setObjectId(-1); + setCategory(-1); + setCurrentPhotoIndex(-1); + setCurrentPhotos([]); + } + + useRoomEngineEvent(RoomEngineTriggerWidgetEvent.REQUEST_EXTERNAL_IMAGE, event => + { + const roomObject = GetRoomEngine().getRoomObject(event.roomId, event.objectId, event.category); + const roomTotalImages = GetRoomEngine().getRoomObjects(roomSession?.roomId, RoomObjectCategory.WALL); + + if(!roomObject) return; + + const datas: IPhotoData[] = []; + + roomTotalImages.forEach(object => + { + if (object.type !== 'external_image_wallitem_poster_small') return null; + + const data = object.model.getValue(RoomObjectVariable.FURNITURE_DATA); + const jsonData: IPhotoData = JSON.parse(data); + + datas.push(jsonData); + }); + + setObjectId(event.objectId); + setCategory(event.category); + setCurrentPhotos(datas); + + const roomObjectPhotoData = (JSON.parse(roomObject.model.getValue(RoomObjectVariable.FURNITURE_DATA)) as IPhotoData); + + setCurrentPhotoIndex(prevValue => + { + let index = 0; + + if(roomObjectPhotoData) + { + index = datas.findIndex(data => (data.u === roomObjectPhotoData.u)) + } + + if(index < 0) index = 0; + + return index; + }); + }); + + useFurniRemovedEvent(((objectId !== -1) && (category !== -1)), event => + { + if((event.id !== objectId) || (event.category !== category)) return; + + onClose(); + }); + + return { objectId, currentPhotoIndex, currentPhotos, onClose }; +} + +export const useFurnitureExternalImageWidget = useFurnitureExternalImageWidgetState; diff --git a/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureFriendFurniWidget.ts b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureFriendFurniWidget.ts new file mode 100644 index 0000000..e658189 --- /dev/null +++ b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureFriendFurniWidget.ts @@ -0,0 +1,75 @@ +import { FriendFurniConfirmLockMessageComposer, LoveLockFurniFinishedEvent, LoveLockFurniFriendConfirmedEvent, LoveLockFurniStartEvent, RoomEngineTriggerWidgetEvent, RoomObjectVariable } from '@nitrots/nitro-renderer'; +import { useState } from 'react'; +import { GetRoomEngine, GetRoomSession } from '../../../../api'; +import { useMessageEvent, useRoomEngineEvent } from '../../../events'; +import { useFurniRemovedEvent } from '../../engine'; + +const useFurnitureFriendFurniWidgetState = () => +{ + const [ objectId, setObjectId ] = useState(-1); + const [ category, setCategory ] = useState(-1); + const [ type, setType ] = useState(0); + const [ usernames, setUsernames ] = useState([]); + const [ figures, setFigures ] = useState([]); + const [ date, setDate ] = useState(null); + const [ stage, setStage ] = useState(0); + + const onClose = () => + { + setObjectId(-1); + setCategory(-1); + setType(0); + setUsernames([]); + setFigures([]); + setDate(null); + } + + const respond = (flag: boolean) => + { + GetRoomSession().connection.send(new FriendFurniConfirmLockMessageComposer(objectId, flag)); + + onClose(); + } + + useMessageEvent(LoveLockFurniStartEvent, event => + { + const parser = event.getParser(); + + setObjectId(parser.furniId); + setStage(parser.start ? 1 : 2); + }); + + useMessageEvent(LoveLockFurniFinishedEvent, event => onClose()); + useMessageEvent(LoveLockFurniFriendConfirmedEvent, event => onClose()); + + useRoomEngineEvent(RoomEngineTriggerWidgetEvent.REQUEST_FRIEND_FURNITURE_ENGRAVING, event => + { + const roomObject = GetRoomEngine().getRoomObject(event.roomId, event.objectId, event.category); + + if(!roomObject) return; + + const data = roomObject.model.getValue(RoomObjectVariable.FURNITURE_DATA); + const type = roomObject.model.getValue(RoomObjectVariable.FURNITURE_FRIENDFURNI_ENGRAVING); + + if((data[0] !== '1') || (data.length !== 6)) return; + + setObjectId(event.objectId); + setCategory(event.category); + setType(type); + setUsernames([ data[1], data[2] ]); + setFigures([ data[3], data[4] ]); + setDate(data[5]); + setStage(0); + }); + + useFurniRemovedEvent(((objectId !== -1) && (category !== -1)), event => + { + if((event.id !== objectId) || (event.category !== category)) return; + + onClose(); + }); + + return { objectId, type, usernames, figures, date, stage, onClose, respond }; +} + +export const useFurnitureFriendFurniWidget = useFurnitureFriendFurniWidgetState; diff --git a/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureHighScoreWidget.ts b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureHighScoreWidget.ts new file mode 100644 index 0000000..0d986d5 --- /dev/null +++ b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureHighScoreWidget.ts @@ -0,0 +1,56 @@ +import { HighScoreDataType, ObjectDataFactory, RoomEngineTriggerWidgetEvent, RoomObjectVariable } from '@nitrots/nitro-renderer'; +import { useState } from 'react'; +import { GetRoomEngine } from '../../../../api'; +import { useRoomEngineEvent } from '../../../events'; +import { useRoom } from '../../useRoom'; + +const SCORE_TYPES = [ 'perteam', 'mostwins', 'classic' ]; +const CLEAR_TYPES = [ 'alltime', 'daily', 'weekly', 'monthly' ]; + +const useFurnitureHighScoreWidgetState = () => +{ + const [ stuffDatas, setStuffDatas ] = useState>(new Map()); + const { roomSession = null } = useRoom(); + + const getScoreType = (type: number) => SCORE_TYPES[type]; + const getClearType = (type: number) => CLEAR_TYPES[type]; + + useRoomEngineEvent(RoomEngineTriggerWidgetEvent.REQUEST_HIGH_SCORE_DISPLAY, event => + { + const roomObject = GetRoomEngine().getRoomObject(event.roomId, event.objectId, event.category); + + if(!roomObject) return; + + const formatKey = roomObject.model.getValue(RoomObjectVariable.FURNITURE_DATA_FORMAT); + const stuffData = (ObjectDataFactory.getData(formatKey) as HighScoreDataType); + + stuffData.initializeFromRoomObjectModel(roomObject.model); + + setStuffDatas(prevValue => + { + const newValue = new Map(prevValue); + + newValue.set(roomObject.id, stuffData); + + return newValue; + }); + }); + + useRoomEngineEvent(RoomEngineTriggerWidgetEvent.REQUEST_HIDE_HIGH_SCORE_DISPLAY, event => + { + if(event.roomId !== roomSession.roomId) return; + + setStuffDatas(prevValue => + { + const newValue = new Map(prevValue); + + newValue.delete(event.objectId); + + return newValue; + }); + }); + + return { stuffDatas, getScoreType, getClearType }; +} + +export const useFurnitureHighScoreWidget = useFurnitureHighScoreWidgetState; diff --git a/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureInternalLinkWidget.ts b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureInternalLinkWidget.ts new file mode 100644 index 0000000..c1b6880 --- /dev/null +++ b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureInternalLinkWidget.ts @@ -0,0 +1,27 @@ +import { RoomEngineTriggerWidgetEvent, RoomObjectVariable } from '@nitrots/nitro-renderer'; +import { CreateLinkEvent, GetRoomEngine } from '../../../../api'; +import { useRoomEngineEvent } from '../../../events'; + +const INTERNALLINK = 'internalLink'; + +const useFurnitureInternalLinkWidgetState = () => +{ + useRoomEngineEvent(RoomEngineTriggerWidgetEvent.REQUEST_INTERNAL_LINK, event => + { + const roomObject = GetRoomEngine().getRoomObject(event.roomId, event.objectId, event.category); + + if(!roomObject) return; + + const data = roomObject.model.getValue(RoomObjectVariable.FURNITURE_DATA); + + let link = data[INTERNALLINK]; + + if(!link || !link.length) link = roomObject.model.getValue(RoomObjectVariable.FURNITURE_INTERNAL_LINK); + + if(link && link.length) CreateLinkEvent(link); + }); + + return {}; +} + +export const useFurnitureInternalLinkWidget = useFurnitureInternalLinkWidgetState; diff --git a/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureMannequinWidget.ts b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureMannequinWidget.ts new file mode 100644 index 0000000..6042975 --- /dev/null +++ b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureMannequinWidget.ts @@ -0,0 +1,80 @@ +import { FurnitureMannequinSaveLookComposer, FurnitureMannequinSaveNameComposer, FurnitureMultiStateComposer, HabboClubLevelEnum, RoomEngineTriggerWidgetEvent, RoomObjectVariable } from '@nitrots/nitro-renderer'; +import { useState } from 'react'; +import { GetAvatarRenderManager, GetRoomEngine, MannequinUtilities, SendMessageComposer } from '../../../../api'; +import { useRoomEngineEvent } from '../../../events'; +import { useFurniRemovedEvent } from '../../engine'; + +const useFurnitureMannequinWidgetState = () => +{ + const [ objectId, setObjectId ] = useState(-1); + const [ category, setCategory ] = useState(-1); + const [ figure, setFigure ] = useState(null); + const [ gender, setGender ] = useState(null); + const [ clubLevel, setClubLevel ] = useState(HabboClubLevelEnum.NO_CLUB); + const [ name, setName ] = useState(null); + + const onClose = () => + { + setObjectId(-1); + setCategory(-1); + setFigure(null); + setGender(null); + setName(null); + } + + const saveFigure = () => + { + if(objectId === -1) return; + + SendMessageComposer(new FurnitureMannequinSaveLookComposer(objectId)); + + onClose(); + } + + const wearFigure = () => + { + if(objectId === -1) return; + + SendMessageComposer(new FurnitureMultiStateComposer(objectId)); + + onClose(); + } + + const saveName = () => + { + if(objectId === -1) return; + + SendMessageComposer(new FurnitureMannequinSaveNameComposer(objectId, name)); + } + + useRoomEngineEvent(RoomEngineTriggerWidgetEvent.REQUEST_MANNEQUIN, event => + { + const roomObject = GetRoomEngine().getRoomObject(event.roomId, event.objectId, event.category); + + if(!roomObject) return; + + const model = roomObject.model; + const figure = (model.getValue(RoomObjectVariable.FURNITURE_MANNEQUIN_FIGURE) || null); + const gender = (model.getValue(RoomObjectVariable.FURNITURE_MANNEQUIN_GENDER) || null); + const figureContainer = GetAvatarRenderManager().createFigureContainer(figure); + const figureClubLevel = GetAvatarRenderManager().getFigureClubLevel(figureContainer, gender, MannequinUtilities.MANNEQUIN_CLOTHING_PART_TYPES); + + setObjectId(event.objectId); + setCategory(event.category); + setFigure(figure); + setGender(gender); + setClubLevel(figureClubLevel); + setName(model.getValue(RoomObjectVariable.FURNITURE_MANNEQUIN_NAME) || null); + }); + + useFurniRemovedEvent(((objectId !== -1) && (category !== -1)), event => + { + if((event.id !== objectId) || (event.category !== category)) return; + + onClose(); + }); + + return { objectId, figure, gender, clubLevel, name, setName, saveFigure, wearFigure, saveName, onClose }; +} + +export const useFurnitureMannequinWidget = useFurnitureMannequinWidgetState; diff --git a/apps/frontend/src/hooks/rooms/widgets/furniture/useFurniturePlaylistEditorWidget.ts b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurniturePlaylistEditorWidget.ts new file mode 100644 index 0000000..05607f0 --- /dev/null +++ b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurniturePlaylistEditorWidget.ts @@ -0,0 +1,108 @@ +import { AddJukeboxDiskComposer, AdvancedMap, FurnitureListAddOrUpdateEvent, FurnitureListEvent, FurnitureListRemovedEvent, FurnitureMultiStateComposer, IAdvancedMap, IMessageEvent, ISongInfo, NotifyPlayedSongEvent, NowPlayingEvent, PlayListStatusEvent, RemoveJukeboxDiskComposer, RoomControllerLevel, RoomEngineTriggerWidgetEvent, SongDiskInventoryReceivedEvent } from '@nitrots/nitro-renderer'; +import { useCallback, useState } from 'react'; +import { GetNitroInstance, GetRoomEngine, GetSessionDataManager, IsOwnerOfFurniture, LocalizeText, NotificationAlertType, NotificationBubbleType, SendMessageComposer } from '../../../../api'; +import { useMessageEvent, useRoomEngineEvent, useSoundEvent } from '../../../events'; +import { useNotification } from '../../../notification'; +import { useFurniRemovedEvent } from '../../engine'; +import { useRoom } from '../../useRoom'; + +const useFurniturePlaylistEditorWidgetState = () => +{ + const [ objectId, setObjectId ] = useState(-1); + const [ category, setCategory ] = useState(-1); + const [ currentPlayingIndex, setCurrentPlayingIndex ] = useState(-1); + const [ diskInventory, setDiskInventory ] = useState>(new AdvancedMap()); + const [ playlist, setPlaylist ] = useState([]); + const { roomSession = null } = useRoom(); + const { showSingleBubble = null, simpleAlert = null } = useNotification(); + + const onClose = () => + { + setObjectId(-1); + setCategory(-1); + } + + const addToPlaylist = useCallback((diskId: number, slotNumber: number) => SendMessageComposer(new AddJukeboxDiskComposer(diskId, slotNumber)), []); + + const removeFromPlaylist = useCallback((slotNumber: number) => SendMessageComposer(new RemoveJukeboxDiskComposer(slotNumber)), []); + + const togglePlayPause = useCallback((furniId: number, position: number) => SendMessageComposer(new FurnitureMultiStateComposer(furniId, position)), []); + + useRoomEngineEvent(RoomEngineTriggerWidgetEvent.REQUEST_PLAYLIST_EDITOR, event => + { + const roomObject = GetRoomEngine().getRoomObject(event.roomId, event.objectId, event.category); + + if(!roomObject) return; + + if(IsOwnerOfFurniture(roomObject)) + { + // show the editor + setObjectId(event.objectId); + setCategory(event.category); + + GetNitroInstance().soundManager.musicController?.requestUserSongDisks(); + GetNitroInstance().soundManager.musicController?.getRoomItemPlaylist()?.requestPlayList(); + + return; + } + + if(roomSession.isRoomOwner || (roomSession.controllerLevel >= RoomControllerLevel.GUEST) || GetSessionDataManager().isModerator) SendMessageComposer(new FurnitureMultiStateComposer(event.objectId, -2)); + }); + + useFurniRemovedEvent(((objectId !== -1) && (category !== -1)), event => + { + if((event.id !== objectId) || (event.category !== category)) return; + + onClose(); + }); + + useSoundEvent(NowPlayingEvent.NPE_SONG_CHANGED, event => + { + setCurrentPlayingIndex(event.position); + }); + + useSoundEvent(NotifyPlayedSongEvent.NOTIFY_PLAYED_SONG, event => + { + showSingleBubble(LocalizeText('soundmachine.notification.playing', [ 'songname', 'songauthor' ], [ event.name, event.creator ]), NotificationBubbleType.SOUNDMACHINE) + }); + + useSoundEvent(SongDiskInventoryReceivedEvent.SDIR_SONG_DISK_INVENTORY_RECEIVENT_EVENT, event => + { + setDiskInventory(GetNitroInstance().soundManager.musicController?.songDiskInventory.clone()); + }); + + useSoundEvent(PlayListStatusEvent.PLUE_PLAY_LIST_UPDATED, event => + { + setPlaylist(GetNitroInstance().soundManager.musicController?.getRoomItemPlaylist()?.entries.concat()) + }); + + useSoundEvent(PlayListStatusEvent.PLUE_PLAY_LIST_FULL, event => + { + simpleAlert(LocalizeText('playlist.editor.alert.playlist.full'), NotificationAlertType.ALERT, '', '', LocalizeText('playlist.editor.alert.playlist.full.title')); + }); + + const onFurniListUpdated = (event : IMessageEvent) => + { + if(objectId === -1) return; + + if(event instanceof FurnitureListEvent) + { + if(event.getParser().fragmentNumber === 0) + { + GetNitroInstance().soundManager.musicController?.requestUserSongDisks(); + } + } + else + { + GetNitroInstance().soundManager.musicController?.requestUserSongDisks(); + } + } + + useMessageEvent(FurnitureListEvent, onFurniListUpdated); + useMessageEvent(FurnitureListRemovedEvent, onFurniListUpdated); + useMessageEvent(FurnitureListAddOrUpdateEvent, onFurniListUpdated); + + return { objectId, diskInventory, playlist, currentPlayingIndex, onClose, addToPlaylist, removeFromPlaylist, togglePlayPause }; +} + +export const useFurniturePlaylistEditorWidget = useFurniturePlaylistEditorWidgetState; diff --git a/apps/frontend/src/hooks/rooms/widgets/furniture/useFurniturePresentWidget.ts b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurniturePresentWidget.ts new file mode 100644 index 0000000..8342b51 --- /dev/null +++ b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurniturePresentWidget.ts @@ -0,0 +1,225 @@ +import { IFurnitureData, IGetImageListener, PetFigureData, RoomEngineTriggerWidgetEvent, RoomObjectCategory, RoomObjectVariable, RoomSessionPresentEvent, TextureUtils, Vector3d } from '@nitrots/nitro-renderer'; +import { useMemo, useState } from 'react'; +import { useRoom } from '../../..'; +import { GetRoomEngine, GetSessionDataManager, IsOwnerOfFurniture, LocalizeText, ProductTypeEnum } from '../../../../api'; +import { useRoomEngineEvent, useRoomSessionManagerEvent } from '../../../events'; +import { useFurniRemovedEvent } from '../../engine'; + +const FLOOR: string = 'floor'; +const WALLPAPER: string = 'wallpaper'; +const LANDSCAPE: string = 'landscape'; +const POSTER: string = 'poster'; + +const useFurniturePresentWidgetState = () => +{ + const [ objectId, setObjectId ] = useState(-1); + const [ classId, setClassId ] = useState(-1); + const [ itemType, setItemType ] = useState(null); + const [ text, setText ] = useState(null); + const [ isOwnerOfFurniture, setIsOwnerOfFurniture ] = useState(false); + const [ senderName, setSenderName ] = useState(null); + const [ senderFigure, setSenderFigure ] = useState(null); + const [ placedItemId, setPlacedItemId ] = useState(-1); + const [ placedItemType, setPlacedItemType ] = useState(null); + const [ placedInRoom, setPlacedInRoom ] = useState(false); + const [ imageUrl, setImageUrl ] = useState(null); + const { roomSession = null } = useRoom(); + + const onClose = () => + { + setObjectId(-1); + setClassId(-1); + setItemType(null); + setText(null); + setIsOwnerOfFurniture(false); + setSenderName(null); + setSenderFigure(null); + setPlacedItemId(-1); + setPlacedItemType(null); + setPlacedInRoom(false); + setImageUrl(null); + } + + const openPresent = () => + { + if(objectId === -1) return; + + roomSession.openGift(objectId); + + GetRoomEngine().changeObjectModelData(GetRoomEngine().activeRoomId, objectId, RoomObjectCategory.FLOOR, RoomObjectVariable.FURNITURE_DISABLE_PICKING_ANIMATION, 1); + } + + const imageListener: IGetImageListener = useMemo(() => + { + return { + imageReady: (id, texture, image) => + { + if(!image && texture) + { + image = TextureUtils.generateImage(texture); + } + + setImageUrl(image.src); + }, + imageFailed: null + } + }, []); + + useRoomSessionManagerEvent(RoomSessionPresentEvent.RSPE_PRESENT_OPENED, event => + { + let furniData: IFurnitureData = null; + + if(event.itemType === ProductTypeEnum.FLOOR) + { + furniData = GetSessionDataManager().getFloorItemData(event.classId); + } + else if(event.itemType === ProductTypeEnum.WALL) + { + furniData = GetSessionDataManager().getWallItemData(event.classId); + } + + let isOwnerOfFurni = false; + + if(event.placedInRoom) + { + const roomObject = GetRoomEngine().getRoomObject(roomSession.roomId, event.placedItemId, RoomObjectCategory.FLOOR); + + if(roomObject) isOwnerOfFurni = IsOwnerOfFurniture(roomObject); + } + + let giftImage: string = null; + + switch(event.itemType) + { + case ProductTypeEnum.WALL: { + if(furniData) + { + switch(furniData.className) + { + case FLOOR: + case LANDSCAPE: + case WALLPAPER: + let imageType = null; + let message = null; + + if(furniData.className === FLOOR) + { + imageType = 'packagecard_icon_floor'; + message = LocalizeText('inventory.furni.item.floor.name'); + } + + else if(furniData.className === LANDSCAPE) + { + imageType = 'packagecard_icon_landscape'; + message = LocalizeText('inventory.furni.item.landscape.name'); + } + + else + { + imageType = 'packagecard_icon_wallpaper'; + message = LocalizeText('inventory.furni.item.wallpaper.name'); + } + + setText(message); + //setImageUrl(getGiftImageUrl(imageType)); + break; + case POSTER: { + const productCode = event.productCode; + + let extras: string = null; + + if(productCode.indexOf('poster') === 0) extras = productCode.replace('poster', ''); + + const productData = GetSessionDataManager().getProductData(productCode); + + let name: string = null; + + if(productData) name = productData.name; + else if(furniData) name = furniData.name; + + setText(name); + setImageUrl(GetRoomEngine().getFurnitureWallIconUrl(event.classId, extras)); + + break; + } + default: { + setText(furniData.name || null); + setImageUrl(GetRoomEngine().getFurnitureWallIconUrl(event.classId)); + break; + } + } + } + + break; + } + case ProductTypeEnum.HABBO_CLUB: + setText(LocalizeText('widget.furni.present.hc')); + //setImageUrl(getGiftImageUrl('packagecard_icon_hc')); + break; + default: { + if(event.placedItemType === ProductTypeEnum.PET) + { + const petfigureString = event.petFigureString; + + if(petfigureString && petfigureString.length) + { + const petFigureData = new PetFigureData(petfigureString); + + const petImage = GetRoomEngine().getRoomObjectPetImage(petFigureData.typeId, petFigureData.paletteId, petFigureData.color, new Vector3d(90), 64, imageListener, true, 0, petFigureData.customParts); + + if(petImage) setImageUrl(petImage.getImage().src); + } + } + else + { + const furniImage = GetRoomEngine().getFurnitureFloorImage(event.classId, new Vector3d(90), 64, imageListener); + + if(furniImage) setImageUrl(furniImage.getImage().src); + } + + const productData = GetSessionDataManager().getProductData(event.productCode); + + setText((productData && productData.name) || furniData.name); + break; + } + } + + setObjectId(0); + setClassId(event.classId); + setItemType(event.itemType); + setIsOwnerOfFurniture(isOwnerOfFurni); + setPlacedItemId(event.placedItemId); + setPlacedItemType(event.placedItemType); + setPlacedInRoom(event.placedInRoom); + }); + + useRoomEngineEvent(RoomEngineTriggerWidgetEvent.REQUEST_PRESENT, event => + { + const roomObject = GetRoomEngine().getRoomObject(event.roomId, event.objectId, event.category); + + if(!roomObject) return null; + + onClose(); + + setObjectId(event.objectId); + setClassId(-1); + setText((roomObject.model.getValue(RoomObjectVariable.FURNITURE_DATA) || '')); + setIsOwnerOfFurniture(IsOwnerOfFurniture(roomObject)); + setSenderName((roomObject.model.getValue(RoomObjectVariable.FURNITURE_PURCHASER_NAME) || null)); + setSenderFigure((roomObject.model.getValue(RoomObjectVariable.FURNITURE_PURCHASER_FIGURE) || null)); + }); + + useFurniRemovedEvent((objectId !== -1), event => + { + if(event.id === objectId) onClose(); + + if(event.id === placedItemId) + { + if(placedInRoom) setPlacedInRoom(false); + } + }); + + return { objectId, classId, itemType, text, isOwnerOfFurniture, senderName, senderFigure, placedItemId, placedItemType, placedInRoom, imageUrl, openPresent, onClose }; +} + +export const useFurniturePresentWidget = useFurniturePresentWidgetState; diff --git a/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureRoomLinkWidget.ts b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureRoomLinkWidget.ts new file mode 100644 index 0000000..aa21127 --- /dev/null +++ b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureRoomLinkWidget.ts @@ -0,0 +1,49 @@ +import { GetGuestRoomMessageComposer, GetGuestRoomResultEvent, RoomEngineTriggerWidgetEvent, RoomObjectVariable } from '@nitrots/nitro-renderer'; +import { useState } from 'react'; +import { GetRoomEngine, SendMessageComposer } from '../../../../api'; +import { useMessageEvent, useRoomEngineEvent } from '../../../events'; + +const INTERNALLINK = 'internalLink'; + +const useFurnitureRoomLinkWidgetState = () => +{ + const [ roomIdToEnter, setRoomIdToEnter ] = useState(0); + + useRoomEngineEvent(RoomEngineTriggerWidgetEvent.REQUEST_ROOM_LINK, event => + { + const roomObject = GetRoomEngine().getRoomObject(event.roomId, event.objectId, event.category); + + if(!roomObject) return; + + const data = roomObject.model.getValue(RoomObjectVariable.FURNITURE_DATA); + + let roomId = data[INTERNALLINK]; + + if(!roomId || !roomId.length) roomId = roomObject.model.getValue(RoomObjectVariable.FURNITURE_INTERNAL_LINK); + + if(!roomId || !roomId.length) return; + + roomId = parseInt(roomId, 10); + + if(isNaN(roomId)) return; + + setRoomIdToEnter(roomId); + + SendMessageComposer(new GetGuestRoomMessageComposer(roomId, false, false)); + }); + + useMessageEvent(GetGuestRoomResultEvent, event => + { + if(!roomIdToEnter) return; + + const parser = event.getParser(); + + if(parser.data.roomId !== roomIdToEnter) return; + + setRoomIdToEnter(0); + }); + + return {}; +} + +export const useFurnitureRoomLinkWidget = useFurnitureRoomLinkWidgetState; diff --git a/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureSpamWallPostItWidget.ts b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureSpamWallPostItWidget.ts new file mode 100644 index 0000000..64f89b0 --- /dev/null +++ b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureSpamWallPostItWidget.ts @@ -0,0 +1,59 @@ +import { AddSpamWallPostItMessageComposer, RequestSpamWallPostItMessageEvent, RoomObjectCategory } from '@nitrots/nitro-renderer'; +import { useState } from 'react'; +import { GetRoomEngine, SendMessageComposer } from '../../../../api'; +import { useMessageEvent } from '../../../events'; +import { useInventoryFurni } from '../../../inventory'; + +const useFurnitureSpamWallPostItWidgetState = () => +{ + const [ objectId, setObjectId ] = useState(-1); + const [ category, setCategory ] = useState(-1); + const [ itemType, setItemType ] = useState(''); + const [ location, setLocation ] = useState(''); + const [ color, setColor ] = useState('0'); + const [ text, setText ] = useState(''); + const [ canModify, setCanModify ] = useState(false); + const { getWallItemById = null } = useInventoryFurni(); + + const onClose = () => + { + SendMessageComposer(new AddSpamWallPostItMessageComposer(objectId, location, color, text)); + + setObjectId(-1); + setCategory(-1); + setItemType(''); + setLocation(''); + setColor('0'); + setText(''); + setCanModify(false); + } + + useMessageEvent(RequestSpamWallPostItMessageEvent, event => + { + const parser = event.getParser(); + + setObjectId(parser.itemId); + setCategory(RoomObjectCategory.WALL); + + const inventoryItem = getWallItemById(parser.itemId); + + let itemType = 'post_it'; + + if(inventoryItem) + { + const wallItemType = GetRoomEngine().getFurnitureWallName(inventoryItem.type); + + if(wallItemType.match('post_it_')) itemType = wallItemType; + } + + setItemType(itemType); + setLocation(parser.location); + setColor('FFFF33'); + setText(''); + setCanModify(true); + }); + + return { objectId, color, setColor, text, setText, canModify, onClose }; +} + +export const useFurnitureSpamWallPostItWidget = useFurnitureSpamWallPostItWidgetState; diff --git a/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureStackHeightWidget.ts b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureStackHeightWidget.ts new file mode 100644 index 0000000..3f8c851 --- /dev/null +++ b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureStackHeightWidget.ts @@ -0,0 +1,79 @@ +import { FurnitureStackHeightComposer, FurnitureStackHeightEvent, RoomEngineTriggerWidgetEvent } from '@nitrots/nitro-renderer'; +import { useEffect, useState } from 'react'; +import { CanManipulateFurniture, GetRoomEngine, GetRoomSession, SendMessageComposer } from '../../../../api'; +import { useMessageEvent, useRoomEngineEvent } from '../../../events'; +import { useFurniRemovedEvent } from '../../engine'; + +const MAX_HEIGHT: number = 40; + +const useFurnitureStackHeightWidgetState = () => +{ + const [ objectId, setObjectId ] = useState(-1); + const [ category, setCategory ] = useState(-1); + const [ height, setHeight ] = useState(0); + const [ pendingHeight, setPendingHeight ] = useState(-1); + + const onClose = () => + { + setObjectId(-1); + setCategory(-1); + setHeight(0); + setPendingHeight(-1); + } + + const updateHeight = (height: number, server: boolean = false) => + { + if(!height) height = 0; + + height = Math.abs(height); + + if(!server) ((height > MAX_HEIGHT) && (height = MAX_HEIGHT)); + + setHeight(parseFloat(height.toFixed(2))); + + if(!server) setPendingHeight(height * 100); + } + + useMessageEvent(FurnitureStackHeightEvent, event => + { + const parser = event.getParser(); + + if(objectId !== parser.furniId) return; + + updateHeight(parser.height, true); + }); + + useRoomEngineEvent(RoomEngineTriggerWidgetEvent.REQUEST_STACK_HEIGHT, event => + { + if(!CanManipulateFurniture(GetRoomSession(), event.objectId, event.category)) return; + + const roomObject = GetRoomEngine().getRoomObject(event.roomId, event.objectId, event.category); + + if(!roomObject) return; + + setObjectId(event.objectId); + setCategory(event.category); + setHeight(roomObject.getLocation().z); + setPendingHeight(-1); + }); + + useFurniRemovedEvent(((objectId !== -1) && (category !== -1)), event => + { + if((event.id !== objectId) || (event.category !== category)) return; + + onClose(); + }); + + useEffect(() => + { + if((objectId === -1) || (pendingHeight === -1)) return; + + const timeout = setTimeout(() => SendMessageComposer(new FurnitureStackHeightComposer(objectId, ~~(pendingHeight))), 10); + + return () => clearTimeout(timeout); + }, [ objectId, pendingHeight ]); + + return { objectId, height, maxHeight: MAX_HEIGHT, onClose, updateHeight }; +} + +export const useFurnitureStackHeightWidget = useFurnitureStackHeightWidgetState; diff --git a/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureStickieWidget.ts b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureStickieWidget.ts new file mode 100644 index 0000000..7cc1597 --- /dev/null +++ b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureStickieWidget.ts @@ -0,0 +1,85 @@ +import { RoomEngineTriggerWidgetEvent, RoomObjectVariable } from '@nitrots/nitro-renderer'; +import { useState } from 'react'; +import { GetRoomEngine, GetRoomSession, GetSessionDataManager, IsOwnerOfFurniture } from '../../../../api'; +import { useRoomEngineEvent } from '../../../events'; +import { useFurniRemovedEvent } from '../../engine'; + +const useFurnitureStickieWidgetState = () => +{ + const [ objectId, setObjectId ] = useState(-1); + const [ category, setCategory ] = useState(-1); + const [ color, setColor ] = useState('0'); + const [ text, setText ] = useState(''); + const [ type, setType ] = useState(''); + const [ canModify, setCanModify ] = useState(false); + + const onClose = () => + { + setObjectId(-1); + setCategory(-1); + setColor('0'); + setText(''); + setType(''); + setCanModify(false); + } + + const updateColor = (newColor: string) => + { + if(newColor === color) return; + + setColor(newColor); + + GetRoomEngine().modifyRoomObjectData(objectId, category, newColor, text); + } + + const updateText = (newText: string) => + { + setText(newText); + + GetRoomEngine().modifyRoomObjectData(objectId, category, color, newText); + } + + const trash = () => GetRoomEngine().deleteRoomObject(objectId, category); + + useRoomEngineEvent(RoomEngineTriggerWidgetEvent.REQUEST_STICKIE, event => + { + const roomObject = GetRoomEngine().getRoomObject(event.roomId, event.objectId, event.category); + + if(!roomObject) return; + + const data = roomObject.model.getValue(RoomObjectVariable.FURNITURE_ITEMDATA); + + if(data.length < 6) return; + + let color: string = null; + let text: string = null; + + if(data.indexOf(' ') > 0) + { + color = data.slice(0, data.indexOf(' ')); + text = data.slice((data.indexOf(' ') + 1), data.length); + } + else + { + color = data; + } + + setObjectId(event.objectId); + setCategory(event.category); + setColor(color || '0'); + setText(text || ''); + setType(roomObject.type || 'post_it'); + setCanModify(GetRoomSession().isRoomOwner || GetSessionDataManager().isModerator || IsOwnerOfFurniture(roomObject)); + }); + + useFurniRemovedEvent(((objectId !== -1) && (category !== -1)), event => + { + if((event.id !== objectId) || (event.category !== category)) return; + + onClose(); + }); + + return { objectId, color, text, type, canModify, updateColor, updateText, trash, onClose }; +} + +export const useFurnitureStickieWidget = useFurnitureStickieWidgetState; diff --git a/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureTrophyWidget.ts b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureTrophyWidget.ts new file mode 100644 index 0000000..d5a1e33 --- /dev/null +++ b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureTrophyWidget.ts @@ -0,0 +1,63 @@ +import { RoomEngineTriggerWidgetEvent, RoomObjectVariable } from '@nitrots/nitro-renderer'; +import { useState } from 'react'; +import { GetRoomEngine } from '../../../../api'; +import { useRoomEngineEvent } from '../../../events'; +import { useFurniRemovedEvent } from '../../engine'; + +const useFurnitureTrophyWidgetState = () => +{ + const [ objectId, setObjectId ] = useState(-1); + const [ category, setCategory ] = useState(-1); + const [ color, setColor ] = useState('1'); + const [ senderName, setSenderName ] = useState(''); + const [ date, setDate ] = useState(''); + const [ message, setMessage ] = useState(''); + + const onClose = () => + { + setObjectId(-1); + setCategory(-1); + setColor('1'); + setSenderName(''); + setDate(''); + setMessage(''); + } + + useRoomEngineEvent(RoomEngineTriggerWidgetEvent.REQUEST_TROPHY, event => + { + const roomObject = GetRoomEngine().getRoomObject(event.roomId, event.objectId, event.category); + + if(!roomObject) return; + + let data = roomObject.model.getValue(RoomObjectVariable.FURNITURE_DATA); + let extra = roomObject.model.getValue(RoomObjectVariable.FURNITURE_EXTRAS); + + if(!extra) extra = '0'; + + setObjectId(event.objectId); + setCategory(event.category); + setColor(roomObject.model.getValue(RoomObjectVariable.FURNITURE_COLOR) || '1'); + + const senderName = data.substring(0, data.indexOf('\t')); + + data = data.substring((senderName.length + 1), data.length); + + const trophyDate = data.substring(0, data.indexOf('\t')); + const trophyText = data.substr((trophyDate.length + 1), data.length); + + setSenderName(senderName); + setDate(trophyDate); + setMessage(trophyText); + }); + + useFurniRemovedEvent(((objectId !== -1) && (category !== -1)), event => + { + if((event.id !== objectId) || (event.category !== category)) return; + + onClose(); + }); + + return { objectId, color, senderName, date, message, onClose }; +} + +export const useFurnitureTrophyWidget = useFurnitureTrophyWidgetState; diff --git a/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureYoutubeWidget.ts b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureYoutubeWidget.ts new file mode 100644 index 0000000..79548e7 --- /dev/null +++ b/apps/frontend/src/hooks/rooms/widgets/furniture/useFurnitureYoutubeWidget.ts @@ -0,0 +1,127 @@ +import { ControlYoutubeDisplayPlaybackMessageComposer, GetYoutubeDisplayStatusMessageComposer, RoomEngineTriggerWidgetEvent, RoomId, SecurityLevel, SetYoutubeDisplayPlaylistMessageComposer, YoutubeControlVideoMessageEvent, YoutubeDisplayPlaylist, YoutubeDisplayPlaylistsEvent, YoutubeDisplayVideoMessageEvent } from '@nitrots/nitro-renderer'; +import { useState } from 'react'; +import { GetRoomEngine, GetSessionDataManager, IsOwnerOfFurniture, SendMessageComposer, YoutubeVideoPlaybackStateEnum } from '../../../../api'; +import { useMessageEvent, useRoomEngineEvent } from '../../../events'; +import { useFurniRemovedEvent } from '../../engine'; + +const CONTROL_COMMAND_PREVIOUS_VIDEO = 0; +const CONTROL_COMMAND_NEXT_VIDEO = 1; +const CONTROL_COMMAND_PAUSE_VIDEO = 2; +const CONTROL_COMMAND_CONTINUE_VIDEO = 3; + +const useFurnitureYoutubeWidgetState = () => +{ + const [ objectId, setObjectId ] = useState(-1); + const [ category, setCategory ] = useState(-1); + const [ videoId, setVideoId ] = useState(null); + const [ videoStart, setVideoStart ] = useState(null); + const [ videoEnd, setVideoEnd ] = useState(null); + const [ currentVideoState, setCurrentVideoState ] = useState(-1); + const [ selectedVideo, setSelectedVideo ] = useState(null); + const [ playlists, setPlaylists ] = useState(null); + const [ hasControl, setHasControl ] = useState(false); + + const onClose = () => + { + setObjectId(-1); + setCategory(-1); + setVideoId(null); + setVideoStart(null); + setVideoEnd(null); + setCurrentVideoState(-1); + setSelectedVideo(null); + setPlaylists(null); + setHasControl(false); + } + + const previous = () => SendMessageComposer(new ControlYoutubeDisplayPlaybackMessageComposer(objectId, CONTROL_COMMAND_PREVIOUS_VIDEO)); + + const next = () => SendMessageComposer(new ControlYoutubeDisplayPlaybackMessageComposer(objectId, CONTROL_COMMAND_NEXT_VIDEO)); + + const pause = () => (hasControl && videoId && videoId.length) && SendMessageComposer(new ControlYoutubeDisplayPlaybackMessageComposer(objectId, CONTROL_COMMAND_PAUSE_VIDEO)); + + const play = () => (hasControl && videoId && videoId.length) && SendMessageComposer(new ControlYoutubeDisplayPlaybackMessageComposer(objectId, CONTROL_COMMAND_CONTINUE_VIDEO)); + + const selectVideo = (video: string) => + { + if(selectedVideo === video) + { + setSelectedVideo(null); + SendMessageComposer(new SetYoutubeDisplayPlaylistMessageComposer(objectId, '')); + + return; + } + + setSelectedVideo(video); + SendMessageComposer(new SetYoutubeDisplayPlaylistMessageComposer(objectId, video)); + } + + useRoomEngineEvent(RoomEngineTriggerWidgetEvent.REQUEST_YOUTUBE, event => + { + if(RoomId.isRoomPreviewerId(event.roomId)) return; + + const roomObject = GetRoomEngine().getRoomObject(event.roomId, event.objectId, event.category); + + if(!roomObject) return; + + setObjectId(event.objectId); + setCategory(event.category); + setHasControl(GetSessionDataManager().hasSecurity(SecurityLevel.EMPLOYEE) || IsOwnerOfFurniture(roomObject)); + + SendMessageComposer(new GetYoutubeDisplayStatusMessageComposer(event.objectId)); + }); + + useMessageEvent(YoutubeDisplayVideoMessageEvent, event => + { + const parser = event.getParser(); + + if((objectId === -1) || (objectId !== parser.furniId)) return; + + setVideoId(parser.videoId); + setVideoStart(parser.startAtSeconds); + setVideoEnd(parser.endAtSeconds); + setCurrentVideoState(parser.state); + }); + + useMessageEvent(YoutubeDisplayPlaylistsEvent, event => + { + const parser = event.getParser(); + + if((objectId === -1) || (objectId !== parser.furniId)) return; + + setPlaylists(parser.playlists); + setSelectedVideo(parser.selectedPlaylistId); + setVideoId(null); + setCurrentVideoState(-1); + setVideoEnd(null); + setVideoStart(null); + }); + + useMessageEvent(YoutubeControlVideoMessageEvent, event => + { + const parser = event.getParser(); + + if((objectId === -1) || (objectId !== parser.furniId)) return; + + switch(parser.commandId) + { + case 1: + setCurrentVideoState(YoutubeVideoPlaybackStateEnum.PLAYING); + break; + case 2: + setCurrentVideoState(YoutubeVideoPlaybackStateEnum.PAUSED); + break; + } + }); + + useFurniRemovedEvent(((objectId !== -1) && (category !== -1)), event => + { + if((event.id !== objectId) || (event.category !== category)) return; + + onClose(); + }); + + return { objectId, videoId, videoStart, videoEnd, currentVideoState, selectedVideo, playlists, onClose, previous, next, pause, play, selectVideo }; +} + +export const useFurnitureYoutubeWidget = useFurnitureYoutubeWidgetState; diff --git a/apps/frontend/src/hooks/rooms/widgets/index.ts b/apps/frontend/src/hooks/rooms/widgets/index.ts new file mode 100644 index 0000000..9984450 --- /dev/null +++ b/apps/frontend/src/hooks/rooms/widgets/index.ts @@ -0,0 +1,12 @@ +export * from './furniture'; +export * from './useAvatarInfoWidget'; +export * from './useChatInputWidget'; +export * from './useChatWidget'; +export * from './useDoorbellWidget'; +export * from './useFilterWordsWidget'; +export * from './useFriendRequestWidget'; +export * from './useFurniChooserWidget'; +export * from './usePetPackageWidget'; +export * from './usePollWidget'; +export * from './useUserChooserWidget'; +export * from './useWordQuizWidget'; diff --git a/apps/frontend/src/hooks/rooms/widgets/useAvatarInfoWidget.ts b/apps/frontend/src/hooks/rooms/widgets/useAvatarInfoWidget.ts new file mode 100644 index 0000000..9545362 --- /dev/null +++ b/apps/frontend/src/hooks/rooms/widgets/useAvatarInfoWidget.ts @@ -0,0 +1,355 @@ +import { RoomEngineObjectEvent, RoomEngineUseProductEvent, RoomObjectCategory, RoomObjectType, RoomObjectVariable, RoomSessionPetInfoUpdateEvent, RoomSessionPetStatusUpdateEvent, RoomSessionUserDataUpdateEvent } from '@nitrots/nitro-renderer'; +import { useEffect, useState } from 'react'; +import { AvatarInfoFurni, AvatarInfoName, AvatarInfoPet, AvatarInfoRentableBot, AvatarInfoUser, AvatarInfoUtilities, CanManipulateFurniture, FurniCategory, GetRoomEngine, GetSessionDataManager, IAvatarInfo, IsOwnerOfFurniture, RoomWidgetUpdateRoomObjectEvent, UseProductItem } from '../../../api'; +import { useRoomEngineEvent, useRoomSessionManagerEvent, useUiEvent } from '../../events'; +import { useFriends } from '../../friends'; +import { useWired } from '../../wired'; +import { useObjectDeselectedEvent, useObjectRollOutEvent, useObjectRollOverEvent, useObjectSelectedEvent } from '../engine'; +import { useRoom } from '../useRoom'; + +const useAvatarInfoWidgetState = () => +{ + const [ avatarInfo, setAvatarInfo ] = useState(null); + const [ activeNameBubble, setActiveNameBubble ] = useState(null); + const [ nameBubbles, setNameBubbles ] = useState([]); + const [ productBubbles, setProductBubbles ] = useState([]); + const [ confirmingProduct, setConfirmingProduct ] = useState(null); + const [ pendingPetId, setPendingPetId ] = useState(-1); + const [ isDecorating, setIsDecorating ] = useState(false); + const { friends = [] } = useFriends(); + const { selectObjectForWired = null } = useWired(); + const { roomSession = null } = useRoom(); + + const removeNameBubble = (index: number) => + { + setNameBubbles(prevValue => + { + const newValue = [ ...prevValue ]; + + newValue.splice(index, 1); + + return newValue; + }); + } + + const removeProductBubble = (index: number) => + { + setProductBubbles(prevValue => + { + const newValue = [ ...prevValue ]; + const item = newValue.splice(index, 1)[0]; + + if(confirmingProduct === item) setConfirmingProduct(null); + + return newValue; + }); + } + + const updateConfirmingProduct = (product: UseProductItem) => + { + setConfirmingProduct(product); + setProductBubbles([]); + } + + const getObjectName = (objectId: number, category: number) => + { + const name = AvatarInfoUtilities.getObjectName(objectId, category); + + if(!name) return; + + setActiveNameBubble(name); + + if(category !== RoomObjectCategory.UNIT) setProductBubbles([]); + } + + const getObjectInfo = (objectId: number, category: number) => + { + let info: IAvatarInfo = null; + + switch(category) + { + case RoomObjectCategory.FLOOR: + case RoomObjectCategory.WALL: + info = AvatarInfoUtilities.getFurniInfo(objectId, category); + + if(info) selectObjectForWired(objectId, category); + break; + case RoomObjectCategory.UNIT: { + const userData = roomSession.userDataManager.getUserDataByIndex(objectId); + + if(!userData) break; + + switch(userData.type) + { + case RoomObjectType.PET: + roomSession.userDataManager.requestPetInfo(userData.webID); + setPendingPetId(userData.webID); + break; + case RoomObjectType.USER: + info = AvatarInfoUtilities.getUserInfo(category, userData); + break; + case RoomObjectType.BOT: + info = AvatarInfoUtilities.getBotInfo(category, userData); + break; + case RoomObjectType.RENTABLE_BOT: + info = AvatarInfoUtilities.getRentableBotInfo(category, userData); + break; + } + } + + } + + if(!info) return; + + setAvatarInfo(info); + } + + const processUsableRoomObject = (objectId: number) => + { + } + + const refreshPetInfo = () => + { + // roomSession.userDataManager.requestPetInfo(petData.id); + } + + useRoomSessionManagerEvent(RoomSessionUserDataUpdateEvent.USER_DATA_UPDATED, event => + { + if(!event.addedUsers.length) return; + + let addedNameBubbles: AvatarInfoName[] = []; + + event.addedUsers.forEach(user => + { + if(user.webID === GetSessionDataManager().userId) return; + + if(friends.find(friend => (friend.id === user.webID))) + { + addedNameBubbles.push(new AvatarInfoName(user.roomIndex, RoomObjectCategory.UNIT, user.webID, user.name, user.type, true)); + } + }); + + if(!addedNameBubbles.length) return; + + setNameBubbles(prevValue => + { + const newValue = [ ...prevValue ]; + + addedNameBubbles.forEach(bubble => + { + const oldIndex = newValue.findIndex(oldBubble => (oldBubble.id === bubble.id)); + + if(oldIndex > -1) newValue.splice(oldIndex, 1); + + newValue.push(bubble); + }); + + return newValue; + }); + }); + + useRoomSessionManagerEvent(RoomSessionPetInfoUpdateEvent.PET_INFO, event => + { + const petData = event.petInfo; + + if(!petData) return; + + if(petData.id !== pendingPetId) return; + + const petInfo = AvatarInfoUtilities.getPetInfo(petData); + + if(!petInfo) return; + + setAvatarInfo(petInfo); + setPendingPetId(-1); + }); + + useRoomSessionManagerEvent(RoomSessionPetStatusUpdateEvent.PET_STATUS_UPDATE, event => + { + /* var _local_2:Boolean; + var _local_3:Boolean; + var _local_4:Boolean; + var _local_5:Boolean; + var _local_6:RoomUserData; + var _local_7:_Str_4828; + if (((!(this._container == null)) && (!(this._container.events == null)))) + { + _local_2 = k.canBreed; + _local_3 = k.canHarvest; + _local_4 = k.canRevive; + _local_5 = k.hasBreedingPermission; + _local_6 = this._Str_19958(k.petId); + if (_local_6 == null) + { + Logger.log((("Could not find pet with the id: " + k.petId) + " given by petStatusUpdate")); + return; + } + _local_7 = new _Str_4828(_local_6.roomObjectId, _local_2, _local_3, _local_4, _local_5); + this._container.events.dispatchEvent(_local_7); */ + }); + + useRoomEngineEvent(RoomEngineUseProductEvent.USE_PRODUCT_FROM_INVENTORY, event => + { + // this._Str_23199((k as RoomEngineUseProductEvent).inventoryStripId, (k as RoomEngineUseProductEvent).furnitureTypeId); + }); + + useRoomEngineEvent(RoomEngineUseProductEvent.USE_PRODUCT_FROM_ROOM, event => + { + const roomObject = GetRoomEngine().getRoomObject(roomSession.roomId, event.objectId, RoomObjectCategory.FLOOR); + + if(!roomObject || !IsOwnerOfFurniture(roomObject)) return; + + const ownerId = roomObject.model.getValue(RoomObjectVariable.FURNITURE_OWNER_ID); + const typeId = roomObject.model.getValue(RoomObjectVariable.FURNITURE_TYPE_ID); + const furniData = GetSessionDataManager().getFloorItemData(typeId); + const parts = furniData.customParams.split(' '); + const part = (parts.length ? parseInt(parts[0]) : -1); + + if(part === -1) return; + + const useProductBubbles: UseProductItem[] = []; + const roomObjects = GetRoomEngine().getRoomObjects(roomSession.roomId, RoomObjectCategory.UNIT); + + for(const roomObject of roomObjects) + { + const userData = roomSession.userDataManager.getUserDataByIndex(roomObject.id); + + let replace = false; + + if(!userData || (userData.type !== RoomObjectType.PET)) + { + + } + else + { + if(userData.ownerId === ownerId) + { + if(userData.hasSaddle && (furniData.specialType === FurniCategory.PET_SADDLE)) replace = true; + + const figureParts = userData.figure.split(' '); + const figurePart = (figureParts.length ? parseInt(figureParts[0]) : -1); + + if(figurePart === part) + { + if(furniData.specialType === FurniCategory.MONSTERPLANT_REVIVAL) + { + if(!userData.canRevive) continue; + } + + if(furniData.specialType === FurniCategory.MONSTERPLANT_REBREED) + { + if((userData.petLevel < 7) || userData.canRevive || userData.canBreed) continue; + } + + if(furniData.specialType === FurniCategory.MONSTERPLANT_FERTILIZE) + { + if((userData.petLevel >= 7) || userData.canRevive) continue; + } + + useProductBubbles.push(new UseProductItem(userData.roomIndex, RoomObjectCategory.UNIT, userData.name, event.objectId, roomObject.id, -1, replace)); + } + } + } + } + + setConfirmingProduct(null); + + if(useProductBubbles.length) setProductBubbles(useProductBubbles); + }); + + useRoomEngineEvent(RoomEngineObjectEvent.REQUEST_MANIPULATION, event => + { + if(!CanManipulateFurniture(roomSession, event.objectId, event.category)) return; + + setIsDecorating(true); + }); + + useObjectSelectedEvent(event => + { + getObjectInfo(event.id, event.category); + }); + + useObjectDeselectedEvent(event => + { + setAvatarInfo(null); + setProductBubbles([]); + }); + + useObjectRollOverEvent(event => + { + if(avatarInfo || (event.category !== RoomObjectCategory.UNIT)) return; + + getObjectName(event.id, event.category); + }); + + useObjectRollOutEvent(event => + { + if(!activeNameBubble || (event.category !== RoomObjectCategory.UNIT) || (activeNameBubble.roomIndex !== event.id)) return; + + setActiveNameBubble(null); + }); + + useUiEvent([ + RoomWidgetUpdateRoomObjectEvent.FURNI_REMOVED, + RoomWidgetUpdateRoomObjectEvent.USER_REMOVED + ], event => + { + if(activeNameBubble && (activeNameBubble.category === event.category) && (activeNameBubble.roomIndex === event.id)) setActiveNameBubble(null); + + if(event.category === RoomObjectCategory.UNIT) + { + let index = nameBubbles.findIndex(bubble => (bubble.roomIndex === event.id)); + + if(index > -1) setNameBubbles(prevValue => prevValue.filter(bubble => (bubble.roomIndex === event.id))); + + index = productBubbles.findIndex(bubble => (bubble.id === event.id)); + + if(index > -1) setProductBubbles(prevValue => prevValue.filter(bubble => (bubble.id !== event.id))); + } + + else if(event.category === RoomObjectCategory.FLOOR) + { + const index = productBubbles.findIndex(bubble => (bubble.id === event.id)); + + if(index > -1) setProductBubbles(prevValue => prevValue.filter(bubble => (bubble.requestRoomObjectId !== event.id))); + } + + if(avatarInfo) + { + if(avatarInfo instanceof AvatarInfoFurni) + { + if(avatarInfo.id === event.id) setAvatarInfo(null); + } + + else if((avatarInfo instanceof AvatarInfoUser) || (avatarInfo instanceof AvatarInfoRentableBot) || (avatarInfo instanceof AvatarInfoPet)) + { + if(avatarInfo.roomIndex === event.id) setAvatarInfo(null); + } + } + }); + + useEffect(() => + { + if(!avatarInfo) return; + + setActiveNameBubble(null); + setNameBubbles([]); + setProductBubbles([]); + }, [ avatarInfo ]); + + useEffect(() => + { + if(!activeNameBubble) return; + + setNameBubbles([]); + }, [ activeNameBubble ]); + + useEffect(() => + { + roomSession.isDecorating = isDecorating; + }, [ roomSession, isDecorating ]); + + return { avatarInfo, setAvatarInfo, activeNameBubble, setActiveNameBubble, nameBubbles, productBubbles, confirmingProduct, isDecorating, setIsDecorating, removeNameBubble, removeProductBubble, updateConfirmingProduct, getObjectName }; +} + +export const useAvatarInfoWidget = useAvatarInfoWidgetState; diff --git a/apps/frontend/src/hooks/rooms/widgets/useChatInputWidget.ts b/apps/frontend/src/hooks/rooms/widgets/useChatInputWidget.ts new file mode 100644 index 0000000..5b11ecf --- /dev/null +++ b/apps/frontend/src/hooks/rooms/widgets/useChatInputWidget.ts @@ -0,0 +1,279 @@ +import { AvatarExpressionEnum, GetTicker, HabboClubLevelEnum, RoomControllerLevel, RoomEngineObjectEvent, RoomObjectCategory, RoomRotatingEffect, RoomSessionChatEvent, RoomSettingsComposer, RoomShakingEffect, RoomZoomEvent, TextureUtils } from '@nitrots/nitro-renderer'; +import { useEffect, useState } from 'react'; +import { ChatMessageTypeEnum, CreateLinkEvent, GetClubMemberLevel, GetConfiguration, GetRoomEngine, GetSessionDataManager, LocalizeText, SendMessageComposer } from '../../../api'; +import { useRoomEngineEvent, useRoomSessionManagerEvent } from '../../events'; +import { useNotification } from '../../notification'; +import { useObjectSelectedEvent } from '../engine'; +import { useRoom } from '../useRoom'; + +const useChatInputWidgetState = () => +{ + const [ selectedUsername, setSelectedUsername ] = useState(''); + const [ isTyping, setIsTyping ] = useState(false); + const [ typingStartedSent, setTypingStartedSent ] = useState(false); + const [ isIdle, setIsIdle ] = useState(false); + const [ floodBlocked, setFloodBlocked ] = useState(false); + const [ floodBlockedSeconds, setFloodBlockedSeconds ] = useState(0); + const { showNitroAlert = null, showConfirm = null } = useNotification(); + const { roomSession = null } = useRoom(); + + const sendChat = (text: string, chatType: number, recipientName: string = '', styleId: number = 0) => + { + if(text === '') return null; + + const parts = text.split(' '); + + if(parts.length > 0) + { + const firstPart = parts[0]; + let secondPart = ''; + + if(parts.length > 1) secondPart = parts[1]; + + if((firstPart.charAt(0) === ':') && (secondPart === 'x')) + { + const selectedAvatarId = GetRoomEngine().selectedAvatarId; + + if(selectedAvatarId > -1) + { + const userData = roomSession.userDataManager.getUserDataByIndex(selectedAvatarId); + + if(userData) + { + secondPart = userData.name; + text = text.replace(' x', (' ' + userData.name)); + } + } + } + + switch(firstPart.toLowerCase()) + { + case ':shake': + RoomShakingEffect.init(2500, 5000); + RoomShakingEffect.turnVisualizationOn(); + + return null; + + case ':rotate': + RoomRotatingEffect.init(2500, 5000); + RoomRotatingEffect.turnVisualizationOn(); + + return null; + case ':d': + case ';d': + if(GetClubMemberLevel() === HabboClubLevelEnum.VIP) + { + roomSession.sendExpressionMessage(AvatarExpressionEnum.LAUGH.ordinal); + } + + break; + case 'o/': + case '_o/': + roomSession.sendExpressionMessage(AvatarExpressionEnum.WAVE.ordinal); + + return null; + case ':kiss': + if(GetClubMemberLevel() === HabboClubLevelEnum.VIP) + { + roomSession.sendExpressionMessage(AvatarExpressionEnum.BLOW.ordinal); + + return null; + } + + break; + case ':jump': + if(GetClubMemberLevel() === HabboClubLevelEnum.VIP) + { + roomSession.sendExpressionMessage(AvatarExpressionEnum.JUMP.ordinal); + + return null; + } + + break; + case ':idle': + roomSession.sendExpressionMessage(AvatarExpressionEnum.IDLE.ordinal); + + return null; + case '_b': + roomSession.sendExpressionMessage(AvatarExpressionEnum.RESPECT.ordinal); + + return null; + case ':sign': + roomSession.sendSignMessage(parseInt(secondPart)); + + return null; + case ':iddqd': + case ':flip': + GetRoomEngine().events.dispatchEvent(new RoomZoomEvent(roomSession.roomId, -1, true)); + + return null; + case ':zoom': + GetRoomEngine().events.dispatchEvent(new RoomZoomEvent(roomSession.roomId, parseFloat(secondPart), false)); + + return null; + case ':screenshot': + const texture = GetRoomEngine().createTextureFromRoom(roomSession.roomId, 1); + + const image = new Image(); + + image.src = TextureUtils.generateImageUrl(texture); + + const newWindow = window.open(''); + newWindow.document.write(image.outerHTML); + return null; + case ':pickall': + if(roomSession.isRoomOwner || GetSessionDataManager().isModerator) + { + showConfirm(LocalizeText('room.confirm.pick_all'), () => + { + GetSessionDataManager().sendSpecialCommandMessage(':pickall'); + }, + null, null, null, LocalizeText('generic.alert.title')); + } + + return null; + case ':ejectall': + if (roomSession.isRoomOwner || GetSessionDataManager().isModerator || roomSession.controllerLevel >= RoomControllerLevel.GUEST) + { + showConfirm(LocalizeText('room.confirm.eject_all'), () => + { + GetSessionDataManager().sendSpecialCommandMessage(':ejectall'); + }, + null, null, null, LocalizeText('generic.alert.title')); + } + return null; + case ':furni': + CreateLinkEvent('furni-chooser/'); + return null; + case ':chooser': + CreateLinkEvent('user-chooser/'); + return null; + case ':floor': + case ':bcfloor': + if(roomSession.controllerLevel >= RoomControllerLevel.ROOM_OWNER) CreateLinkEvent('floor-editor/show'); + + return null; + case ':togglefps': { + if(GetTicker().maxFPS > 0) GetTicker().maxFPS = 0; + else GetTicker().maxFPS = GetConfiguration('system.animation.fps'); + + return null; + } + case ':client': + case ':nitro': + case ':billsonnn': + showNitroAlert(); + return null; + case ':settings': + if(roomSession.isRoomOwner || GetSessionDataManager().isModerator) + { + SendMessageComposer(new RoomSettingsComposer(roomSession.roomId)); + } + + return null; + } + } + + switch(chatType) + { + case ChatMessageTypeEnum.CHAT_DEFAULT: + roomSession.sendChatMessage(text, styleId); + break; + case ChatMessageTypeEnum.CHAT_SHOUT: + roomSession.sendShoutMessage(text, styleId); + break; + case ChatMessageTypeEnum.CHAT_WHISPER: + roomSession.sendWhisperMessage(recipientName, text, styleId); + break; + } + } + + useRoomSessionManagerEvent(RoomSessionChatEvent.FLOOD_EVENT, event => + { + setFloodBlocked(true); + setFloodBlockedSeconds(parseFloat(event.message)); + }); + + useObjectSelectedEvent(event => + { + if(event.category !== RoomObjectCategory.UNIT) return; + + const userData = roomSession.userDataManager.getUserDataByIndex(event.id); + + if(!userData) return; + + setSelectedUsername(userData.name); + }); + + useRoomEngineEvent(RoomEngineObjectEvent.DESELECTED, event => setSelectedUsername('')); + + useEffect(() => + { + if(!floodBlocked) return; + + let seconds = 0; + + const interval = setInterval(() => + { + setFloodBlockedSeconds(prevValue => + { + seconds = ((prevValue || 0) - 1); + + return seconds; + }); + + if(seconds < 0) + { + clearInterval(interval); + + setFloodBlocked(false); + } + }, 1000); + + return () => clearInterval(interval); + }, [ floodBlocked ]); + + useEffect(() => + { + if(!isIdle) return; + + let timeout: ReturnType = null; + + if(isIdle) + { + timeout = setTimeout(() => + { + setIsIdle(false); + setIsTyping(false) + }, 10000); + } + + return () => clearTimeout(timeout); + }, [ isIdle ]); + + useEffect(() => + { + if(isTyping) + { + if(!typingStartedSent) + { + setTypingStartedSent(true); + + roomSession.sendChatTypingMessage(isTyping); + } + } + else + { + if(typingStartedSent) + { + setTypingStartedSent(false); + + roomSession.sendChatTypingMessage(isTyping); + } + } + }, [ roomSession, isTyping, typingStartedSent ]); + + return { selectedUsername, floodBlocked, floodBlockedSeconds, setIsTyping, setIsIdle, sendChat }; +} + +export const useChatInputWidget = useChatInputWidgetState; diff --git a/apps/frontend/src/hooks/rooms/widgets/useChatWidget.ts b/apps/frontend/src/hooks/rooms/widgets/useChatWidget.ts new file mode 100644 index 0000000..0edfb5f --- /dev/null +++ b/apps/frontend/src/hooks/rooms/widgets/useChatWidget.ts @@ -0,0 +1,252 @@ +import { AvatarFigurePartType, AvatarScaleType, AvatarSetType, GetGuestRoomResultEvent, NitroPoint, PetFigureData, RoomChatSettings, RoomChatSettingsEvent, RoomDragEvent, RoomObjectCategory, RoomObjectType, RoomObjectVariable, RoomSessionChatEvent, RoomUserData, SystemChatStyleEnum, TextureUtils, Vector3d } from '@nitrots/nitro-renderer'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import { ChatBubbleMessage, ChatEntryType, ChatHistoryCurrentDate, GetAvatarRenderManager, GetConfiguration, GetRoomEngine, GetRoomObjectScreenLocation, IRoomChatSettings, LocalizeText, PlaySound, RoomChatFormatter } from '../../../api'; +import { useMessageEvent, useRoomEngineEvent, useRoomSessionManagerEvent } from '../../events'; +import { useRoom } from '../useRoom'; +import { useChatHistory } from './../../chat-history'; + +const avatarColorCache: Map = new Map(); +const avatarImageCache: Map = new Map(); +const petImageCache: Map = new Map(); + +const useChatWidgetState = () => +{ + const [ chatMessages, setChatMessages ] = useState([]); + const [ chatSettings, setChatSettings ] = useState({ + mode: RoomChatSettings.CHAT_MODE_FREE_FLOW, + weight: RoomChatSettings.CHAT_BUBBLE_WIDTH_NORMAL, + speed: RoomChatSettings.CHAT_SCROLL_SPEED_NORMAL, + distance: 50, + protection: RoomChatSettings.FLOOD_FILTER_NORMAL + }); + const { roomSession = null } = useRoom(); + const { addChatEntry } = useChatHistory(); + const isDisposed = useRef(false); + + const getScrollSpeed = useMemo(() => + { + if(!chatSettings) return 6000; + + switch(chatSettings.speed) + { + case RoomChatSettings.CHAT_SCROLL_SPEED_FAST: + return 3000; + case RoomChatSettings.CHAT_SCROLL_SPEED_NORMAL: + return 6000; + case RoomChatSettings.CHAT_SCROLL_SPEED_SLOW: + return 12000; + } + }, [ chatSettings ]); + + const setFigureImage = (figure: string) => + { + const avatarImage = GetAvatarRenderManager().createAvatarImage(figure, AvatarScaleType.LARGE, null, { + resetFigure: figure => + { + if(isDisposed.current) return; + + setFigureImage(figure); + }, + dispose: () => + {}, + disposed: false + }); + + if(!avatarImage) return; + + const image = avatarImage.getCroppedImage(AvatarSetType.HEAD); + const color = avatarImage.getPartColor(AvatarFigurePartType.CHEST); + + avatarColorCache.set(figure, ((color && color.rgb) || 16777215)); + + avatarImage.dispose(); + + avatarImageCache.set(figure, image.src); + + return image.src; + } + + const getUserImage = (figure: string) => + { + let existing = avatarImageCache.get(figure); + + if(!existing) existing = setFigureImage(figure); + + return existing; + } + + const getPetImage = (figure: string, direction: number, _arg_3: boolean, scale: number = 64, posture: string = null) => + { + let existing = petImageCache.get((figure + posture)); + + if(existing) return existing; + + const figureData = new PetFigureData(figure); + const typeId = figureData.typeId; + const image = GetRoomEngine().getRoomObjectPetImage(typeId, figureData.paletteId, figureData.color, new Vector3d((direction * 45)), scale, null, false, 0, figureData.customParts, posture); + + if(image) + { + existing = TextureUtils.generateImageUrl(image.data); + + petImageCache.set((figure + posture), existing); + } + + return existing; + } + + useRoomSessionManagerEvent(RoomSessionChatEvent.CHAT_EVENT, event => + { + const roomObject = GetRoomEngine().getRoomObject(roomSession.roomId, event.objectId, RoomObjectCategory.UNIT); + const bubbleLocation = roomObject ? GetRoomObjectScreenLocation(roomSession.roomId, roomObject?.id, RoomObjectCategory.UNIT) : new NitroPoint(); + const userData = roomObject ? roomSession.userDataManager.getUserDataByIndex(event.objectId) : new RoomUserData(-1); + + let username = ''; + let avatarColor = 0; + let imageUrl: string = null; + let chatType = event.chatType; + let styleId = event.style; + let userType = 0; + let petType = -1; + let text = event.message; + + if(userData) + { + userType = userData.type; + + const figure = userData.figure; + + switch(userType) + { + case RoomObjectType.PET: + imageUrl = getPetImage(figure, 2, true, 64, roomObject.model.getValue(RoomObjectVariable.FIGURE_POSTURE)); + petType = new PetFigureData(figure).typeId; + break; + case RoomObjectType.USER: + imageUrl = getUserImage(figure); + break; + case RoomObjectType.RENTABLE_BOT: + case RoomObjectType.BOT: + styleId = SystemChatStyleEnum.BOT; + break; + } + + avatarColor = avatarColorCache.get(figure); + username = userData.name; + } + + switch(chatType) + { + case RoomSessionChatEvent.CHAT_TYPE_RESPECT: + text = LocalizeText('widgets.chatbubble.respect', [ 'username' ], [ username ]); + + if(GetConfiguration('respect.options')['enabled']) PlaySound(GetConfiguration('respect.options')['sound']); + + break; + case RoomSessionChatEvent.CHAT_TYPE_PETREVIVE: + case RoomSessionChatEvent.CHAT_TYPE_PET_REBREED_FERTILIZE: + case RoomSessionChatEvent.CHAT_TYPE_PET_SPEED_FERTILIZE: { + let textKey = 'widget.chatbubble.petrevived'; + + if(chatType === RoomSessionChatEvent.CHAT_TYPE_PET_REBREED_FERTILIZE) + { + textKey = 'widget.chatbubble.petrefertilized;'; + } + + else if(chatType === RoomSessionChatEvent.CHAT_TYPE_PET_SPEED_FERTILIZE) + { + textKey = 'widget.chatbubble.petspeedfertilized'; + } + + let targetUserName: string = null; + + const newRoomObject = GetRoomEngine().getRoomObject(roomSession.roomId, event.extraParam, RoomObjectCategory.UNIT); + + if(newRoomObject) + { + const newUserData = roomSession.userDataManager.getUserDataByIndex(roomObject.id); + + if(newUserData) targetUserName = newUserData.name; + } + + text = LocalizeText(textKey, [ 'petName', 'userName' ], [ username, targetUserName ]); + break; + } + case RoomSessionChatEvent.CHAT_TYPE_PETRESPECT: + text = LocalizeText('widget.chatbubble.petrespect', [ 'petname' ], [ username ]); + break; + case RoomSessionChatEvent.CHAT_TYPE_PETTREAT: + text = LocalizeText('widget.chatbubble.pettreat', [ 'petname' ], [ username ]); + break; + case RoomSessionChatEvent.CHAT_TYPE_HAND_ITEM_RECEIVED: + text = LocalizeText('widget.chatbubble.handitem', [ 'username', 'handitem' ], [ username, LocalizeText(('handitem' + event.extraParam)) ]); + break; + case RoomSessionChatEvent.CHAT_TYPE_MUTE_REMAINING: { + const hours = ((event.extraParam > 0) ? Math.floor((event.extraParam / 3600)) : 0).toString(); + const minutes = ((event.extraParam > 0) ? Math.floor((event.extraParam % 3600) / 60) : 0).toString(); + const seconds = (event.extraParam % 60).toString(); + + text = LocalizeText('widget.chatbubble.mutetime', [ 'hours', 'minutes', 'seconds' ], [ hours, minutes, seconds ]); + break; + } + } + + const formattedText = RoomChatFormatter(text); + const color = (avatarColor && (('#' + (avatarColor.toString(16).padStart(6, '0'))) || null)); + + const chatMessage = new ChatBubbleMessage( + userData.roomIndex, + RoomObjectCategory.UNIT, + roomSession.roomId, + text, + formattedText, + username, + new NitroPoint(bubbleLocation.x, bubbleLocation.y), + chatType, + styleId, + imageUrl, + color); + + setChatMessages(prevValue => [ ...prevValue, chatMessage ]); + addChatEntry({ id: -1, webId: userData.webID, entityId: userData.roomIndex, name: username, imageUrl, style: styleId, chatType: chatType, entityType: userData.type, message: formattedText, timestamp: ChatHistoryCurrentDate(), type: ChatEntryType.TYPE_CHAT, roomId: roomSession.roomId, color }); + }); + + useRoomEngineEvent(RoomDragEvent.ROOM_DRAG, event => + { + if(!chatMessages.length || (event.roomId !== roomSession.roomId)) return; + + const offsetX = event.offsetX; + + chatMessages.forEach(chat => (chat.elementRef && (chat.left += offsetX))); + }); + + useMessageEvent(GetGuestRoomResultEvent, event => + { + const parser = event.getParser(); + + if(!parser.roomEnter) return; + + setChatSettings(parser.chat); + }); + + useMessageEvent(RoomChatSettingsEvent, event => + { + const parser = event.getParser(); + + setChatSettings(parser.chat); + }); + + useEffect(() => + { + isDisposed.current = false; + + return () => + { + isDisposed.current = true; + } + }, []); + + return { chatMessages, setChatMessages, chatSettings, getScrollSpeed }; +} + +export const useChatWidget = useChatWidgetState; diff --git a/apps/frontend/src/hooks/rooms/widgets/useDoorbellWidget.ts b/apps/frontend/src/hooks/rooms/widgets/useDoorbellWidget.ts new file mode 100644 index 0000000..4ae5142 --- /dev/null +++ b/apps/frontend/src/hooks/rooms/widgets/useDoorbellWidget.ts @@ -0,0 +1,44 @@ +import { RoomSessionDoorbellEvent } from '@nitrots/nitro-renderer'; +import { useState } from 'react'; +import { GetRoomSession } from '../../../api'; +import { useRoomSessionManagerEvent } from '../../events'; + +const useDoorbellWidgetState = () => +{ + const [ users, setUsers ] = useState([]); + + const addUser = (userName: string) => + { + if(users.indexOf(userName) >= 0) return; + + setUsers([ ...users, userName ]); + } + + const removeUser = (userName: string) => + { + const index = users.indexOf(userName); + + if(index === -1) return; + + const newUsers = [ ...users ]; + + newUsers.splice(index, 1); + + setUsers(newUsers); + } + + const answer = (userName: string, flag: boolean) => + { + GetRoomSession().sendDoorbellApprovalMessage(userName, flag); + + removeUser(userName); + } + + useRoomSessionManagerEvent(RoomSessionDoorbellEvent.DOORBELL, event => addUser(event.userName)); + useRoomSessionManagerEvent(RoomSessionDoorbellEvent.RSDE_REJECTED, event => removeUser(event.userName)); + useRoomSessionManagerEvent(RoomSessionDoorbellEvent.RSDE_ACCEPTED, event => removeUser(event.userName)); + + return { users, addUser, removeUser, answer }; +} + +export const useDoorbellWidget = useDoorbellWidgetState; diff --git a/apps/frontend/src/hooks/rooms/widgets/useFilterWordsWidget.ts b/apps/frontend/src/hooks/rooms/widgets/useFilterWordsWidget.ts new file mode 100644 index 0000000..3811682 --- /dev/null +++ b/apps/frontend/src/hooks/rooms/widgets/useFilterWordsWidget.ts @@ -0,0 +1,23 @@ +import { RoomFilterSettingsMessageEvent } from '@nitrots/nitro-renderer'; +import { useState } from 'react'; +import { useMessageEvent } from '../../events'; + +const useFilterWordsWidgetState = () => +{ + const [ wordsFilter, setWordsFilter ] = useState(null); + const [ isVisible, setIsVisible ] = useState(false); + + const onClose = () => setIsVisible(false); + + useMessageEvent(RoomFilterSettingsMessageEvent, event => + { + const parser = event.getParser(); + + setIsVisible(true); + setWordsFilter(parser.words); + }); + + return { wordsFilter, isVisible, setWordsFilter, onClose }; +} + +export const useFilterWordsWidget = useFilterWordsWidgetState; diff --git a/apps/frontend/src/hooks/rooms/widgets/useFriendRequestWidget.ts b/apps/frontend/src/hooks/rooms/widgets/useFriendRequestWidget.ts new file mode 100644 index 0000000..82bf6a2 --- /dev/null +++ b/apps/frontend/src/hooks/rooms/widgets/useFriendRequestWidget.ts @@ -0,0 +1,81 @@ +import { RoomObjectCategory, RoomObjectUserType } from '@nitrots/nitro-renderer'; +import { useEffect, useMemo, useState } from 'react'; +import { GetRoomSession, MessengerRequest } from '../../../api'; +import { useFriends } from '../../friends'; +import { useUserAddedEvent, useUserRemovedEvent } from '../engine'; + +const useFriendRequestWidgetState = () => +{ + const [ activeRequests, setActiveRequests ] = useState<{ roomIndex: number, request: MessengerRequest }[]>([]); + const { requests = [], dismissedRequestIds = [], setDismissedRequestIds = null } = useFriends(); + + const displayedRequests = useMemo(() => activeRequests.filter(request => (dismissedRequestIds.indexOf(request.request.requesterUserId) === -1)), [ activeRequests, dismissedRequestIds ]); + + const hideFriendRequest = (userId: number) => + { + setDismissedRequestIds(prevValue => + { + if(prevValue.indexOf(userId) >= 0) return prevValue; + + const newValue = [ ...prevValue ]; + + newValue.push(userId); + + return newValue; + }); + } + + useUserAddedEvent(true, event => + { + if(event.category !== RoomObjectCategory.UNIT) return; + + const userData = GetRoomSession().userDataManager.getUserDataByIndex(event.id); + + if(!userData || (userData.type !== RoomObjectUserType.getTypeNumber(RoomObjectUserType.USER))) return; + + const request = requests.find(request => (request.requesterUserId === userData.webID)); + + if(!request || activeRequests.find(request => (request.request.requesterUserId === userData.webID))) return; + + const newValue = [ ...activeRequests ]; + + newValue.push({ roomIndex: userData.roomIndex, request }); + + setActiveRequests(newValue); + }); + + useUserRemovedEvent(true, event => + { + if(event.category !== RoomObjectCategory.UNIT) return; + + const index = activeRequests.findIndex(request => (request.roomIndex === event.id)); + + if(index === -1) return; + + const newValue = [ ...activeRequests ]; + + newValue.splice(index, 1); + + setActiveRequests(newValue); + }); + + useEffect(() => + { + const newDisplayedRequests: { roomIndex: number, request: MessengerRequest }[] = []; + + for(const request of requests) + { + const userData = GetRoomSession().userDataManager.getUserData(request.requesterUserId); + + if(!userData) continue; + + newDisplayedRequests.push({ roomIndex: userData.roomIndex, request }); + } + + setActiveRequests(newDisplayedRequests); + }, [ requests ]); + + return { displayedRequests, hideFriendRequest }; +} + +export const useFriendRequestWidget = useFriendRequestWidgetState; diff --git a/apps/frontend/src/hooks/rooms/widgets/useFurniChooserWidget.ts b/apps/frontend/src/hooks/rooms/widgets/useFurniChooserWidget.ts new file mode 100644 index 0000000..4542dba --- /dev/null +++ b/apps/frontend/src/hooks/rooms/widgets/useFurniChooserWidget.ts @@ -0,0 +1,132 @@ +import { RoomObjectCategory, RoomObjectVariable } from '@nitrots/nitro-renderer'; +import { useState } from 'react'; +import { GetRoomEngine, GetRoomSession, GetSessionDataManager, LocalizeText, RoomObjectItem } from '../../../api'; +import { useFurniAddedEvent, useFurniRemovedEvent } from '../engine'; +import { useRoom } from '../useRoom'; + +const useFurniChooserWidgetState = () => +{ + const [ items, setItems ] = useState(null); + const { roomSession = null } = useRoom(); + + const onClose = () => setItems(null); + + const selectItem = (item: RoomObjectItem) => item && GetRoomEngine().selectRoomObject(GetRoomSession().roomId, item.id, item.category); + + const populateChooser = () => + { + const sessionDataManager = GetSessionDataManager(); + const wallObjects = GetRoomEngine().getRoomObjects(roomSession.roomId, RoomObjectCategory.WALL); + const floorObjects = GetRoomEngine().getRoomObjects(roomSession.roomId, RoomObjectCategory.FLOOR); + + const wallItems = wallObjects.map(roomObject => + { + if(roomObject.id < 0) return null; + + let name = roomObject.type; + + if(name.startsWith('poster')) + { + name = LocalizeText(`poster_${ name.replace('poster', '') }_name`); + } + else + { + const typeId = roomObject.model.getValue(RoomObjectVariable.FURNITURE_TYPE_ID); + const furniData = sessionDataManager.getWallItemData(typeId); + + if(furniData && furniData.name.length) name = furniData.name; + } + + return new RoomObjectItem(roomObject.id, RoomObjectCategory.WALL, name); + }); + + const floorItems = floorObjects.map(roomObject => + { + if(roomObject.id < 0) return null; + + let name = roomObject.type; + + const typeId = roomObject.model.getValue(RoomObjectVariable.FURNITURE_TYPE_ID); + const furniData = sessionDataManager.getFloorItemData(typeId); + + if(furniData && furniData.name.length) name = furniData.name; + + return new RoomObjectItem(roomObject.id, RoomObjectCategory.FLOOR, name); + }); + + setItems([ ...wallItems, ...floorItems ].sort((a, b) => ((a.name < b.name) ? -1 : 1))); + } + + useFurniAddedEvent(!!items, event => + { + if(event.id < 0) return; + + const roomObject = GetRoomEngine().getRoomObject(GetRoomSession().roomId, event.id, event.category); + + if(!roomObject) return; + + let item: RoomObjectItem = null; + + switch(event.category) + { + case RoomObjectCategory.WALL: { + let name = roomObject.type; + + if(name.startsWith('poster')) + { + name = LocalizeText(`poster_${ name.replace('poster', '') }_name`); + } + else + { + const typeId = roomObject.model.getValue(RoomObjectVariable.FURNITURE_TYPE_ID); + const furniData = GetSessionDataManager().getWallItemData(typeId); + + if(furniData && furniData.name.length) name = furniData.name; + } + + item = new RoomObjectItem(roomObject.id, RoomObjectCategory.WALL, name); + + break; + } + case RoomObjectCategory.FLOOR: { + let name = roomObject.type; + + const typeId = roomObject.model.getValue(RoomObjectVariable.FURNITURE_TYPE_ID); + const furniData = GetSessionDataManager().getFloorItemData(typeId); + + if(furniData && furniData.name.length) name = furniData.name; + + item = new RoomObjectItem(roomObject.id, RoomObjectCategory.FLOOR, name); + } + } + + setItems(prevValue => [ ...prevValue, item ].sort((a, b) => ((a.name < b.name) ? -1 : 1))); + }); + + useFurniRemovedEvent(!!items, event => + { + if(event.id < 0) return; + + setItems(prevValue => + { + const newValue = [ ...prevValue ]; + + for(let i = 0; i < newValue.length; i++) + { + const existingValue = newValue[i]; + + if((existingValue.id !== event.id) || (existingValue.category !== event.category)) continue; + + newValue.splice(i, 1); + + break; + } + + return newValue; + }); + }); + + return { items, onClose, selectItem, populateChooser }; +} + +export const useFurniChooserWidget = useFurniChooserWidgetState; diff --git a/apps/frontend/src/hooks/rooms/widgets/usePetPackageWidget.ts b/apps/frontend/src/hooks/rooms/widgets/usePetPackageWidget.ts new file mode 100644 index 0000000..55b0c3a --- /dev/null +++ b/apps/frontend/src/hooks/rooms/widgets/usePetPackageWidget.ts @@ -0,0 +1,75 @@ +import { OpenPetPackageMessageComposer, RoomObjectCategory, RoomSessionPetPackageEvent } from '@nitrots/nitro-renderer'; +import { useState } from 'react'; +import { GetRoomEngine, LocalizeText, SendMessageComposer } from '../../../api'; +import { useRoomSessionManagerEvent } from '../../events'; + +const usePetPackageWidgetState = () => +{ + const [ isVisible, setIsVisible ] = useState(false); + const [ objectId, setObjectId ] = useState(-1); + const [ objectType, setObjectType ] = useState(''); + const [ petName, setPetName ] = useState(''); + const [ errorResult, setErrorResult ] = useState(''); + + const onClose = () => + { + setErrorResult(''); + setPetName(''); + setObjectType(''); + setObjectId(-1); + setIsVisible(false); + } + + const onConfirm = () => + { + SendMessageComposer(new OpenPetPackageMessageComposer(objectId, petName)); + } + + const onChangePetName = (petName: string) => + { + setPetName(petName); + if (errorResult.length > 0) setErrorResult(''); + } + + const getErrorResultForCode = (errorCode: number) => + { + if (!errorCode || errorCode === 0) return; + + switch(errorCode) + { + case 1: + return LocalizeText('catalog.alert.petname.long'); + case 2: + return LocalizeText('catalog.alert.petname.short'); + case 3: + return LocalizeText('catalog.alert.petname.chars'); + case 4: + default: + return LocalizeText('catalog.alert.petname.bobba'); + } + } + + useRoomSessionManagerEvent(RoomSessionPetPackageEvent.RSOPPE_OPEN_PET_PACKAGE_REQUESTED, event => + { + if (!event) return; + + const roomObject = GetRoomEngine().getRoomObject(event.session.roomId, event.objectId, RoomObjectCategory.FLOOR); + + setObjectId(event.objectId); + setObjectType(roomObject.type); + setIsVisible(true); + }); + + useRoomSessionManagerEvent(RoomSessionPetPackageEvent.RSOPPE_OPEN_PET_PACKAGE_RESULT, event => + { + if (!event) return; + + if (event.nameValidationStatus === 0) onClose(); + + if (event.nameValidationStatus !== 0) setErrorResult(getErrorResultForCode(event.nameValidationStatus)); + }); + + return { isVisible, errorResult, petName, objectType, onChangePetName, onConfirm, onClose }; +} + +export const usePetPackageWidget = usePetPackageWidgetState; diff --git a/apps/frontend/src/hooks/rooms/widgets/usePollWidget.ts b/apps/frontend/src/hooks/rooms/widgets/usePollWidget.ts new file mode 100644 index 0000000..138a56e --- /dev/null +++ b/apps/frontend/src/hooks/rooms/widgets/usePollWidget.ts @@ -0,0 +1,52 @@ +import { RoomSessionPollEvent } from '@nitrots/nitro-renderer'; +import { DispatchUiEvent, RoomWidgetPollUpdateEvent } from '../../../api'; +import { useRoomSessionManagerEvent } from '../../events'; +import { useRoom } from '../useRoom'; + +const usePollWidgetState = () => +{ + const { roomSession = null } = useRoom(); + + const startPoll = (pollId: number) => roomSession.sendPollStartMessage(pollId); + + const rejectPoll = (pollId: number) => roomSession.sendPollRejectMessage(pollId); + + const answerPoll = (pollId: number, questionId: number, answers: string[]) => roomSession.sendPollAnswerMessage(pollId, questionId, answers); + + useRoomSessionManagerEvent(RoomSessionPollEvent.OFFER, event => + { + const pollEvent = new RoomWidgetPollUpdateEvent(RoomWidgetPollUpdateEvent.OFFER, event.id); + + pollEvent.summary = event.summary; + pollEvent.headline = event.headline; + + DispatchUiEvent(pollEvent); + }); + + useRoomSessionManagerEvent(RoomSessionPollEvent.ERROR, event => + { + const pollEvent = new RoomWidgetPollUpdateEvent(RoomWidgetPollUpdateEvent.ERROR, event.id); + + pollEvent.summary = event.summary; + pollEvent.headline = event.headline; + + DispatchUiEvent(pollEvent); + }); + + useRoomSessionManagerEvent(RoomSessionPollEvent.CONTENT, event => + { + const pollEvent = new RoomWidgetPollUpdateEvent(RoomWidgetPollUpdateEvent.CONTENT, event.id); + + pollEvent.startMessage = event.startMessage; + pollEvent.endMessage = event.endMessage; + pollEvent.numQuestions = event.numQuestions; + pollEvent.questionArray = event.questionArray; + pollEvent.npsPoll = event.npsPoll; + + DispatchUiEvent(pollEvent); + }); + + return { startPoll, rejectPoll, answerPoll }; +} + +export const usePollWidget = usePollWidgetState; diff --git a/apps/frontend/src/hooks/rooms/widgets/useUserChooserWidget.ts b/apps/frontend/src/hooks/rooms/widgets/useUserChooserWidget.ts new file mode 100644 index 0000000..857922a --- /dev/null +++ b/apps/frontend/src/hooks/rooms/widgets/useUserChooserWidget.ts @@ -0,0 +1,80 @@ +import { RoomObjectCategory } from '@nitrots/nitro-renderer'; +import { useState } from 'react'; +import { GetRoomEngine, GetRoomSession, RoomObjectItem } from '../../../api'; +import { useUserAddedEvent, useUserRemovedEvent } from '../engine'; +import { useRoom } from '../useRoom'; + +const useUserChooserWidgetState = () => +{ + const [ items, setItems ] = useState(null); + const { roomSession = null } = useRoom(); + + const onClose = () => setItems(null); + + const selectItem = (item: RoomObjectItem) => item && GetRoomEngine().selectRoomObject(GetRoomSession().roomId, item.id, item.category); + + const populateChooser = () => + { + const roomSession = GetRoomSession(); + const roomObjects = GetRoomEngine().getRoomObjects(roomSession.roomId, RoomObjectCategory.UNIT); + + setItems(roomObjects + .map(roomObject => + { + if(roomObject.id < 0) return null; + + const userData = roomSession.userDataManager.getUserDataByIndex(roomObject.id); + + if(!userData) return null; + + return new RoomObjectItem(userData.roomIndex, RoomObjectCategory.UNIT, userData.name); + }) + .sort((a, b) => ((a.name < b.name) ? -1 : 1))); + } + + useUserAddedEvent(!!items, event => + { + if(event.id < 0) return; + + const userData = GetRoomSession().userDataManager.getUserDataByIndex(event.id); + + if(!userData) return; + + setItems(prevValue => + { + const newValue = [ ...prevValue ]; + + newValue.push(new RoomObjectItem(userData.roomIndex, RoomObjectCategory.UNIT, userData.name)); + newValue.sort((a, b) => ((a.name < b.name) ? -1 : 1)); + + return newValue; + }); + }); + + useUserRemovedEvent(!!items, event => + { + if(event.id < 0) return; + + setItems(prevValue => + { + const newValue = [ ...prevValue ]; + + for(let i = 0; i < newValue.length; i++) + { + const existingValue = newValue[i]; + + if((existingValue.id !== event.id) || (existingValue.category !== event.category)) continue; + + newValue.splice(i, 1); + + break; + } + + return newValue; + }); + }); + + return { items, onClose, selectItem, populateChooser }; +} + +export const useUserChooserWidget = useUserChooserWidgetState; diff --git a/apps/frontend/src/hooks/rooms/widgets/useWordQuizWidget.ts b/apps/frontend/src/hooks/rooms/widgets/useWordQuizWidget.ts new file mode 100644 index 0000000..3c5f5ac --- /dev/null +++ b/apps/frontend/src/hooks/rooms/widgets/useWordQuizWidget.ts @@ -0,0 +1,149 @@ +import { AvatarAction, IQuestion, RoomSessionWordQuizEvent } from '@nitrots/nitro-renderer'; +import { useEffect, useState } from 'react'; +import { GetRoomEngine, VoteValue } from '../../../api'; +import { useRoomSessionManagerEvent } from '../../events'; +import { useRoom } from '../useRoom'; +import { usePollWidget } from './usePollWidget'; + +const DEFAULT_DISPLAY_DELAY = 4000; +const SIGN_FADE_DELAY = 3; + +const useWordQuizWidgetState = () => +{ + const [ pollId, setPollId ] = useState(-1); + const [ question, setQuestion ] = useState(null); + const [ answerSent, setAnswerSent ] = useState(false); + const [ questionClearTimeout, setQuestionClearTimeout ] = useState>(null); + const [ answerCounts, setAnswerCounts ] = useState>(new Map()); + const [ userAnswers, setUserAnswers ] = useState>(new Map()); + const { answerPoll = null } = usePollWidget(); + const { roomSession = null } = useRoom(); + + const clearQuestion = () => + { + setPollId(-1); + setQuestion(null); + } + + const vote = (vote: string) => + { + if(answerSent || !question) return; + + answerPoll(pollId, question.id, [ vote ]); + + setAnswerSent(true); + } + + useRoomSessionManagerEvent(RoomSessionWordQuizEvent.ANSWERED, event => + { + const userData = roomSession.userDataManager.getUserData(event.userId); + + if(!userData) return; + + setAnswerCounts(event.answerCounts); + + setUserAnswers(prevValue => + { + if(!prevValue.has(userData.roomIndex)) + { + const newValue = new Map(userAnswers); + + newValue.set(userData.roomIndex, { value: event.value, secondsLeft: SIGN_FADE_DELAY }); + + return newValue; + } + + return prevValue; + }); + + GetRoomEngine().updateRoomObjectUserGesture(roomSession.roomId, userData.roomIndex, AvatarAction.getGestureId((event.value === '0') ? AvatarAction.GESTURE_SAD : AvatarAction.GESTURE_SMILE)); + }); + + useRoomSessionManagerEvent(RoomSessionWordQuizEvent.FINISHED, event => + { + if(question && (question.id === event.questionId)) + { + setAnswerCounts(event.answerCounts); + setAnswerSent(true); + + setQuestionClearTimeout(prevValue => + { + if(prevValue) clearTimeout(prevValue); + + return setTimeout(() => clearQuestion(), DEFAULT_DISPLAY_DELAY); + }); + } + + setUserAnswers(new Map()); + }); + + useRoomSessionManagerEvent(RoomSessionWordQuizEvent.QUESTION, event => + { + setPollId(event.id); + setQuestion(event.question); + setAnswerSent(false); + setAnswerCounts(new Map()); + setUserAnswers(new Map()); + + setQuestionClearTimeout(prevValue => + { + if(prevValue) clearTimeout(prevValue); + + if(event.duration > 0) + { + const delay = event.duration < 1000 ? DEFAULT_DISPLAY_DELAY : event.duration; + + return setTimeout(() => clearQuestion(), delay); + } + + return null; + }); + }); + + useEffect(() => + { + const checkSignFade = () => + { + setUserAnswers(prevValue => + { + const keysToRemove: number[] = []; + + prevValue.forEach((value, key) => + { + value.secondsLeft--; + + if(value.secondsLeft <= 0) keysToRemove.push(key); + }); + + if(keysToRemove.length === 0) return prevValue; + + const copy = new Map(prevValue); + + keysToRemove.forEach(key => copy.delete(key)); + + return copy; + }); + } + + const interval = setInterval(() => checkSignFade(), 1000); + + return () => clearInterval(interval); + }, []); + + useEffect(() => + { + return () => + { + setQuestionClearTimeout(prevValue => + { + if(prevValue) clearTimeout(prevValue); + + return null; + }); + } + }, []); + + return { question, answerSent, answerCounts, userAnswers, vote }; +} + +export const useWordQuizWidget = useWordQuizWidgetState; diff --git a/apps/frontend/src/hooks/session/index.ts b/apps/frontend/src/hooks/session/index.ts new file mode 100644 index 0000000..e61c7f3 --- /dev/null +++ b/apps/frontend/src/hooks/session/index.ts @@ -0,0 +1 @@ +export * from './useSessionInfo'; diff --git a/apps/frontend/src/hooks/session/useSessionInfo.ts b/apps/frontend/src/hooks/session/useSessionInfo.ts new file mode 100644 index 0000000..6f480fe --- /dev/null +++ b/apps/frontend/src/hooks/session/useSessionInfo.ts @@ -0,0 +1,93 @@ +import { FigureUpdateEvent, RoomUnitChatStyleComposer, UserInfoDataParser, UserInfoEvent, UserSettingsEvent } from '@nitrots/nitro-renderer'; +import { useEffect, useState } from 'react'; +import { useBetween } from 'use-between'; +import { GetLocalStorage, GetSessionDataManager, SendMessageComposer } from '../../api'; +import { useMessageEvent } from '../events'; +import { useLocalStorage } from '../useLocalStorage'; + +const useSessionInfoState = () => +{ + const [ userInfo, setUserInfo ] = useState(null); + const [ userFigure, setUserFigure ] = useState(null); + const [ chatStyleId, setChatStyleId ] = useState(0); + const [ userRespectRemaining, setUserRespectRemaining ] = useState(0); + const [ petRespectRemaining, setPetRespectRemaining ] = useState(0); + const [ screenSize, setScreenSize ] = useLocalStorage('nitro.screensize', { width: window.innerWidth, height: window.innerHeight }); + + const updateChatStyleId = (styleId: number) => + { + setChatStyleId(styleId); + + SendMessageComposer(new RoomUnitChatStyleComposer(styleId)); + } + + const respectUser = (userId: number) => + { + GetSessionDataManager().giveRespect(userId); + + setUserRespectRemaining(GetSessionDataManager().respectsLeft); + } + + const respectPet = (petId: number) => + { + GetSessionDataManager().givePetRespect(petId); + + setPetRespectRemaining(GetSessionDataManager().respectsPetLeft); + } + + useMessageEvent(UserInfoEvent, event => + { + const parser = event.getParser(); + + setUserInfo(parser.userInfo); + setUserFigure(parser.userInfo.figure); + setUserRespectRemaining(parser.userInfo.respectsRemaining); + setPetRespectRemaining(parser.userInfo.respectsPetRemaining); + }); + + useMessageEvent(FigureUpdateEvent, event => + { + const parser = event.getParser(); + + setUserFigure(parser.figure); + }); + + useMessageEvent(UserSettingsEvent, event => + { + const parser = event.getParser(); + + setChatStyleId(parser.chatType); + }); + + useEffect(() => + { + const currentScreenSize = <{ width: number, height: number }>GetLocalStorage('nitro.screensize'); + + if(currentScreenSize && ((currentScreenSize.width !== window.innerWidth) || (currentScreenSize.height !== window.innerHeight))) + { + let i = window.localStorage.length; + + while(i > 0) + { + const key = window.localStorage.key(i); + + if(key && key.startsWith('nitro.window')) window.localStorage.removeItem(key); + + i--; + } + } + + const onResize = (event: UIEvent) => setScreenSize({ width: window.innerWidth, height: window.innerHeight }); + + window.addEventListener('resize', onResize); + + return () => + { + window.removeEventListener('resize', onResize); + } + }, [ setScreenSize ]); + + return { userInfo, userFigure, chatStyleId, userRespectRemaining, petRespectRemaining, respectUser, respectPet, updateChatStyleId }; +} + +export const useSessionInfo = () => useBetween(useSessionInfoState); diff --git a/apps/frontend/src/hooks/useLocalStorage.ts b/apps/frontend/src/hooks/useLocalStorage.ts new file mode 100644 index 0000000..1e55fb9 --- /dev/null +++ b/apps/frontend/src/hooks/useLocalStorage.ts @@ -0,0 +1,44 @@ +import { NitroLogger } from '@nitrots/nitro-renderer'; +import { Dispatch, SetStateAction, useState } from 'react'; +import { GetLocalStorage, SetLocalStorage } from '../api'; + +const useLocalStorageState = (key: string, initialValue: T): [ T, Dispatch>] => +{ + const [ storedValue, setStoredValue ] = useState(() => + { + if(typeof window === 'undefined') return initialValue; + + try + { + const item = GetLocalStorage(key); + + return item ?? initialValue; + } + + catch(error) + { + return initialValue; + } + }); + + const setValue = (value: T) => + { + try + { + const valueToStore = value instanceof Function ? value(storedValue) : value; + + setStoredValue(valueToStore); + + if(typeof window !== 'undefined') SetLocalStorage(key, valueToStore); + } + + catch(error) + { + NitroLogger.error(error); + } + } + + return [ storedValue, setValue ]; +} + +export const useLocalStorage = useLocalStorageState; diff --git a/apps/frontend/src/hooks/useSharedVisibility.ts b/apps/frontend/src/hooks/useSharedVisibility.ts new file mode 100644 index 0000000..b55855b --- /dev/null +++ b/apps/frontend/src/hooks/useSharedVisibility.ts @@ -0,0 +1,44 @@ +import { useCallback, useMemo, useState } from 'react'; + +export const useSharedVisibility = () => +{ + const [ activeIds, setActiveIds ] = useState([]); + + const isVisible = useMemo(() => !!activeIds.length, [ activeIds ]); + + const activate = useCallback(() => + { + let id = -1; + + setActiveIds(prevValue => + { + const newValue = [ ...prevValue ]; + + id = newValue.length ? (newValue[(newValue.length - 1)] + 1) : 0; + + newValue.push(id); + + return newValue; + }); + + return id; + }, []); + + const deactivate = useCallback((id: number) => + { + setActiveIds(prevValue => + { + const newValue = [ ...prevValue ]; + + const index = newValue.indexOf(id); + + if(index === -1) return prevValue; + + newValue.splice(index, 1); + + return newValue; + }); + }, []); + + return { isVisible, activate, deactivate }; +} diff --git a/apps/frontend/src/hooks/wired/index.ts b/apps/frontend/src/hooks/wired/index.ts new file mode 100644 index 0000000..dc79020 --- /dev/null +++ b/apps/frontend/src/hooks/wired/index.ts @@ -0,0 +1 @@ +export * from './useWired'; diff --git a/apps/frontend/src/hooks/wired/useWired.ts b/apps/frontend/src/hooks/wired/useWired.ts new file mode 100644 index 0000000..c4628f4 --- /dev/null +++ b/apps/frontend/src/hooks/wired/useWired.ts @@ -0,0 +1,133 @@ +import { ConditionDefinition, Triggerable, TriggerDefinition, UpdateActionMessageComposer, UpdateConditionMessageComposer, UpdateTriggerMessageComposer, WiredActionDefinition, WiredFurniActionEvent, WiredFurniConditionEvent, WiredFurniTriggerEvent, WiredSaveSuccessEvent } from '@nitrots/nitro-renderer'; +import { useEffect, useState } from 'react'; +import { useBetween } from 'use-between'; +import { IsOwnerOfFloorFurniture, LocalizeText, SendMessageComposer, WiredFurniType, WiredSelectionVisualizer } from '../../api'; +import { useMessageEvent } from '../events'; +import { useNotification } from '../notification'; + +const useWiredState = () => +{ + const [ trigger, setTrigger ] = useState(null); + const [ intParams, setIntParams ] = useState([]); + const [ stringParam, setStringParam ] = useState(''); + const [ furniIds, setFurniIds ] = useState([]); + const [ actionDelay, setActionDelay ] = useState(0); + const [ allowsFurni, setAllowsFurni ] = useState(WiredFurniType.STUFF_SELECTION_OPTION_NONE); + const { showConfirm = null } = useNotification(); + + const saveWired = () => + { + const save = (trigger: Triggerable) => + { + if(!trigger) return; + + if(trigger instanceof WiredActionDefinition) + { + SendMessageComposer(new UpdateActionMessageComposer(trigger.id, intParams, stringParam, furniIds, actionDelay, trigger.stuffTypeSelectionCode)); + } + + else if(trigger instanceof TriggerDefinition) + { + SendMessageComposer(new UpdateTriggerMessageComposer(trigger.id, intParams, stringParam, furniIds, trigger.stuffTypeSelectionCode)); + } + + else if(trigger instanceof ConditionDefinition) + { + SendMessageComposer(new UpdateConditionMessageComposer(trigger.id, intParams, stringParam, furniIds, trigger.stuffTypeSelectionCode)); + } + } + + if(!IsOwnerOfFloorFurniture(trigger.id)) + { + showConfirm(LocalizeText('wiredfurni.nonowner.change.confirm.body'), () => + { + save(trigger) + }, null, null, null, LocalizeText('wiredfurni.nonowner.change.confirm.title')); + } + else + { + save(trigger); + } + } + + const selectObjectForWired = (objectId: number, category: number) => + { + if(!trigger || !allowsFurni) return; + + if(objectId <= 0) return; + + setFurniIds(prevValue => + { + const newFurniIds = [ ...prevValue ]; + + const index = prevValue.indexOf(objectId); + + if(index >= 0) + { + newFurniIds.splice(index, 1); + + WiredSelectionVisualizer.hide(objectId); + } + + else if(newFurniIds.length < trigger.maximumItemSelectionCount) + { + newFurniIds.push(objectId); + + WiredSelectionVisualizer.show(objectId); + } + + return newFurniIds; + }); + } + + useMessageEvent(WiredSaveSuccessEvent, event => + { + const parser = event.getParser(); + + setTrigger(null); + }); + + useMessageEvent(WiredFurniActionEvent, event => + { + const parser = event.getParser(); + + setTrigger(parser.definition); + }); + + useMessageEvent(WiredFurniConditionEvent, event => + { + const parser = event.getParser(); + + setTrigger(parser.definition); + }); + + useMessageEvent(WiredFurniTriggerEvent, event => + { + const parser = event.getParser(); + + setTrigger(parser.definition); + }); + + useEffect(() => + { + if(!trigger) return; + + return () => + { + setIntParams([]); + setStringParam(''); + setActionDelay(0); + setFurniIds(prevValue => + { + if(prevValue && prevValue.length) WiredSelectionVisualizer.clearSelectionShaderFromFurni(prevValue); + + return []; + }); + setAllowsFurni(WiredFurniType.STUFF_SELECTION_OPTION_NONE); + } + }, [ trigger ]); + + return { trigger, setTrigger, intParams, setIntParams, stringParam, setStringParam, furniIds, setFurniIds, actionDelay, setActionDelay, setAllowsFurni, saveWired, selectObjectForWired }; +} + +export const useWired = () => useBetween(useWiredState); diff --git a/apps/frontend/src/index.scss b/apps/frontend/src/index.scss new file mode 100644 index 0000000..5f3ecbb --- /dev/null +++ b/apps/frontend/src/index.scss @@ -0,0 +1,26 @@ +@import './assets/styles'; + +html, +body { + padding: 0; + width: 100%; + height: 100%; + overflow: hidden; + user-select: none; + scrollbar-width: thin; + + .image-rendering-pixelated { + image-rendering: pixelated; + image-rendering: -moz-crisp-edges; + } + + * { + -webkit-font-smoothing: antialiased; + } +} + +img { + object-fit: none; +} + +@import './App'; diff --git a/apps/frontend/src/index.tsx b/apps/frontend/src/index.tsx new file mode 100644 index 0000000..196309d --- /dev/null +++ b/apps/frontend/src/index.tsx @@ -0,0 +1,5 @@ +import { createRoot } from 'react-dom/client'; +import { App } from './App'; +import './index.scss'; + +createRoot(document.getElementById('root')).render(); diff --git a/apps/frontend/src/main.tsx b/apps/frontend/src/main.tsx deleted file mode 100644 index 5b0ece9..0000000 --- a/apps/frontend/src/main.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { StrictMode } from 'react'; -import * as ReactDOM from 'react-dom/client'; - -import App from './app/app'; - -const root = ReactDOM.createRoot( - document.getElementById('root') as HTMLElement -); -root.render( - - - -); diff --git a/apps/frontend/src/styles.scss b/apps/frontend/src/styles.scss deleted file mode 100644 index 90d4ee0..0000000 --- a/apps/frontend/src/styles.scss +++ /dev/null @@ -1 +0,0 @@ -/* You can add global styles to this file, and also import other style files */ diff --git a/apps/frontend/src/workers/IntervalWebWorker.ts b/apps/frontend/src/workers/IntervalWebWorker.ts new file mode 100644 index 0000000..978c80a --- /dev/null +++ b/apps/frontend/src/workers/IntervalWebWorker.ts @@ -0,0 +1,26 @@ +export default () => +{ + let interval: ReturnType = null; + + // eslint-disable-next-line no-restricted-globals + self.onmessage = (message: MessageEvent) => + { + if(!message) return; + + const data: { [index: string]: any } = message.data; + + switch(data.action) + { + case 'START': + interval = setInterval(() => postMessage(null), data.content); + break; + case 'STOP': + if(interval) + { + clearInterval(interval); + interval = null; + } + break; + } + } +} diff --git a/apps/frontend/src/workers/WorkerBuilder.ts b/apps/frontend/src/workers/WorkerBuilder.ts new file mode 100644 index 0000000..b848893 --- /dev/null +++ b/apps/frontend/src/workers/WorkerBuilder.ts @@ -0,0 +1,10 @@ +export class WorkerBuilder extends Worker +{ + constructor(worker) + { + const code = worker.toString(); + const blob = new Blob([ `(${ code })()` ]); + + super(URL.createObjectURL(blob)); + } +} diff --git a/apps/frontend/vite.config.ts b/apps/frontend/vite.config.ts index a091cd9..45f12b2 100644 --- a/apps/frontend/vite.config.ts +++ b/apps/frontend/vite.config.ts @@ -1,11 +1,19 @@ /// import { defineConfig } from 'vite'; +import { resolve } from 'path'; import react from '@vitejs/plugin-react'; import viteTsConfigPaths from 'vite-tsconfig-paths'; export default defineConfig({ cacheDir: '../../node_modules/.vite/frontend', + resolve: { + alias: { + '@': resolve(__dirname, 'src'), + '~': resolve(__dirname, 'node_modules') + } + }, + server: { port: 4200, host: 'localhost', diff --git a/libs/renderer/.eslintrc.json b/libs/renderer/.eslintrc.json new file mode 100644 index 0000000..9d9c0db --- /dev/null +++ b/libs/renderer/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/renderer/README.md b/libs/renderer/README.md new file mode 100644 index 0000000..4381c7e --- /dev/null +++ b/libs/renderer/README.md @@ -0,0 +1,7 @@ +# renderer + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build renderer` to build the library. diff --git a/libs/renderer/package.json b/libs/renderer/package.json new file mode 100644 index 0000000..6082140 --- /dev/null +++ b/libs/renderer/package.json @@ -0,0 +1,5 @@ +{ + "name": "@nitro/renderer", + "version": "0.0.1", + "type": "commonjs" +} diff --git a/libs/renderer/project.json b/libs/renderer/project.json new file mode 100644 index 0000000..d838ba9 --- /dev/null +++ b/libs/renderer/project.json @@ -0,0 +1,26 @@ +{ + "name": "renderer", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/renderer/src", + "projectType": "library", + "targets": { + "build": { + "executor": "@nrwl/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/renderer", + "main": "libs/renderer/src/index.ts", + "tsConfig": "libs/renderer/tsconfig.lib.json", + "assets": ["libs/renderer/*.md"] + } + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/renderer/**/*.ts"] + } + } + }, + "tags": [] +} diff --git a/libs/renderer/src/index.ts b/libs/renderer/src/index.ts new file mode 100644 index 0000000..2be951f --- /dev/null +++ b/libs/renderer/src/index.ts @@ -0,0 +1 @@ +export * from './lib/renderer'; diff --git a/libs/renderer/src/lib/renderer.ts b/libs/renderer/src/lib/renderer.ts new file mode 100644 index 0000000..a747f31 --- /dev/null +++ b/libs/renderer/src/lib/renderer.ts @@ -0,0 +1,3 @@ +export function renderer(): string { + return 'renderer'; +} diff --git a/libs/renderer/tsconfig.json b/libs/renderer/tsconfig.json new file mode 100644 index 0000000..db7b566 --- /dev/null +++ b/libs/renderer/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/libs/renderer/tsconfig.lib.json b/libs/renderer/tsconfig.lib.json new file mode 100644 index 0000000..33eca2c --- /dev/null +++ b/libs/renderer/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/package-lock.json b/package-lock.json index 732b43a..60b7096 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,15 @@ "version": "0.0.0", "license": "MIT", "dependencies": { + "@nitrots/nitro-renderer": "^1.6.6", + "@tanstack/react-virtual": "^3.0.0-alpha.0", "react": "18.2.0", - "react-dom": "18.2.0" + "react-bootstrap": "^2.7.2", + "react-dom": "18.2.0", + "react-icons": "^4.8.0", + "react-slider": "^2.0.4", + "react-youtube": "^10.1.0", + "use-between": "^1.3.5" }, "devDependencies": { "@nrwl/cypress": "15.8.6", @@ -1840,7 +1847,6 @@ "version": "7.21.0", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==", - "dev": true, "dependencies": { "regenerator-runtime": "^0.13.11" }, @@ -2489,6 +2495,48 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, + "node_modules/@nitrots/nitro-renderer": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@nitrots/nitro-renderer/-/nitro-renderer-1.6.6.tgz", + "integrity": "sha512-VMxn4gAV49G1nnOrtL6koLnJglHdp83zILcKe8DTZsZXX6GOGU2wST1sSnHvdcH28KpesqrCP5dyJGKC/0ylYQ==", + "dependencies": { + "@pixi/app": "~6.5.0", + "@pixi/basis": "~6.5.0", + "@pixi/canvas-display": "~6.5.0", + "@pixi/canvas-extract": "~6.5.0", + "@pixi/canvas-renderer": "~6.5.0", + "@pixi/constants": "~6.5.0", + "@pixi/core": "~6.5.0", + "@pixi/display": "~6.5.0", + "@pixi/events": "~6.5.0", + "@pixi/extensions": "~6.5.0", + "@pixi/extract": "~6.5.0", + "@pixi/filter-alpha": "~6.5.0", + "@pixi/filter-color-matrix": "~6.5.0", + "@pixi/graphics": "~6.5.0", + "@pixi/graphics-extras": "~6.5.0", + "@pixi/interaction": "~6.5.0", + "@pixi/loaders": "~6.5.0", + "@pixi/math": "~6.5.0", + "@pixi/math-extras": "~6.5.0", + "@pixi/mixin-cache-as-bitmap": "~6.5.0", + "@pixi/mixin-get-child-by-name": "~6.5.0", + "@pixi/mixin-get-global-position": "~6.5.0", + "@pixi/polyfill": "~6.5.0", + "@pixi/runner": "~6.5.0", + "@pixi/settings": "~6.5.0", + "@pixi/sprite": "~6.5.0", + "@pixi/sprite-tiling": "~6.5.0", + "@pixi/spritesheet": "~6.5.0", + "@pixi/text": "~6.5.0", + "@pixi/ticker": "~6.5.0", + "@pixi/tilemap": "^3.2.2", + "@pixi/utils": "~6.5.0", + "gifuct-js": "^2.1.2", + "howler": "^2.2.3", + "pako": "^2.0.4" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2904,12 +2952,412 @@ "typescript": "^3 || ^4" } }, + "node_modules/@pixi/app": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/app/-/app-6.5.9.tgz", + "integrity": "sha512-RDFR8ea86eykTmxlQPb1PMdXqYaeLmf1BKprcEKOOr6vmNLykzn+UEaal4OJtmpgtAsHt6hkpW7nUeZ8idbWZA==", + "peerDependencies": { + "@pixi/core": "6.5.9", + "@pixi/display": "6.5.9", + "@pixi/math": "6.5.9", + "@pixi/utils": "6.5.9" + } + }, + "node_modules/@pixi/basis": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/basis/-/basis-6.5.9.tgz", + "integrity": "sha512-mWuqq3ZmNo2eZw6rapOAQdafd+LaHLOBaa/Iy/PqfmuB8aFYaBgzEyF4FjnHGK1TYn+2PKO85G3ptIoAh9c67g==", + "peerDependencies": { + "@pixi/compressed-textures": "6.5.9", + "@pixi/constants": "6.5.9", + "@pixi/core": "6.5.9", + "@pixi/loaders": "6.5.9", + "@pixi/runner": "6.5.9", + "@pixi/settings": "6.5.9" + } + }, + "node_modules/@pixi/canvas-display": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/canvas-display/-/canvas-display-6.5.9.tgz", + "integrity": "sha512-ZxVBkyqDOPJKTkF9onGbGtwToICQEVcx/PTHh03LU6lkJ4/fenCd25f4265tdXarIFuu3k03liFKVCCy9sRU0g==", + "peerDependencies": { + "@pixi/display": "6.5.9" + } + }, + "node_modules/@pixi/canvas-extract": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/canvas-extract/-/canvas-extract-6.5.9.tgz", + "integrity": "sha512-EpOc1nHOhG4Ph6JsliXT0ZHxXzXicUezpXf/xuxjz0Td9A01pkzljI1gKI6qc5n/viD8Y2uaCKDnUSftdhiRsA==", + "peerDependencies": { + "@pixi/canvas-renderer": "6.5.9", + "@pixi/core": "6.5.9", + "@pixi/display": "6.5.9", + "@pixi/math": "6.5.9", + "@pixi/utils": "6.5.9" + } + }, + "node_modules/@pixi/canvas-renderer": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/canvas-renderer/-/canvas-renderer-6.5.9.tgz", + "integrity": "sha512-bs97ub6ZfEopGqKXBpNntuONmSsWs1UEJ06yWfFUfkLzhXrWNsaFtF5CbaELDzB8AMqH0HY5QgNNWylj8VQ0tQ==", + "peerDependencies": { + "@pixi/constants": "6.5.9", + "@pixi/core": "6.5.9", + "@pixi/math": "6.5.9", + "@pixi/settings": "6.5.9", + "@pixi/utils": "6.5.9" + } + }, + "node_modules/@pixi/compressed-textures": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/compressed-textures/-/compressed-textures-6.5.9.tgz", + "integrity": "sha512-7FbgA6fVjhhoWrIHjEkTTZBZIr4FlQ7bWQzpSy3i8J0lGFTFp1p6n17i0t8xxqrJ1SWAJud8WOESsiAHWUHLDQ==", + "peer": true, + "peerDependencies": { + "@pixi/constants": "6.5.9", + "@pixi/core": "6.5.9", + "@pixi/loaders": "6.5.9", + "@pixi/settings": "6.5.9", + "@pixi/utils": "6.5.9" + } + }, + "node_modules/@pixi/constants": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/constants/-/constants-6.5.9.tgz", + "integrity": "sha512-749Vv+DUh4Tguku6uouXUIAUHThYU/cDZzWW4lYNv2UrqUrPxE1a7b8Ca0GakFjt6HZIenl6DnUYLP4yE6PWiQ==" + }, + "node_modules/@pixi/core": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/core/-/core-6.5.9.tgz", + "integrity": "sha512-NQGaEYtUIKNAQNeqLsfHSkx1BYuOWJzAYDpb63QEZFvV8gTRf2t3SBuyvSxvMFAGakNrqYefIXkfJXpmHOrk7A==", + "dependencies": { + "@types/offscreencanvas": "^2019.6.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/pixijs" + }, + "peerDependencies": { + "@pixi/constants": "6.5.9", + "@pixi/extensions": "6.5.9", + "@pixi/math": "6.5.9", + "@pixi/runner": "6.5.9", + "@pixi/settings": "6.5.9", + "@pixi/ticker": "6.5.9", + "@pixi/utils": "6.5.9" + } + }, + "node_modules/@pixi/display": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/display/-/display-6.5.9.tgz", + "integrity": "sha512-85eODYWsOM/kIt2N/L51lsAl3DLJA+1Eed+Nl6ZeF/pEvQnXf7jDZzGwVmUKJurpPWhjkA5OnzWabFw3De2qZg==", + "peerDependencies": { + "@pixi/constants": "6.5.9", + "@pixi/math": "6.5.9", + "@pixi/settings": "6.5.9", + "@pixi/utils": "6.5.9" + } + }, + "node_modules/@pixi/events": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/events/-/events-6.5.9.tgz", + "integrity": "sha512-f5hiyy0nZtgIKrarbeO+DsZI441WvGz1W823D9G3g+FKDHXIoX72orXT5FvSIGJNNtuoS5LZKszKUQGNqJh/kQ==", + "peerDependencies": { + "@pixi/display": "6.5.9", + "@pixi/math": "6.5.9", + "@pixi/utils": "6.5.9" + } + }, + "node_modules/@pixi/extensions": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/extensions/-/extensions-6.5.9.tgz", + "integrity": "sha512-vwzEhLkGiiCw9e7QmXBKHuJzX1DzaA2JcFw0Kl1DTI0lH1cIZccE3rVBbuVY8+Zvb33WV5XxwQC03/qyx4DUbw==" + }, + "node_modules/@pixi/extract": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/extract/-/extract-6.5.9.tgz", + "integrity": "sha512-fqnGfJFC6OJ63Js+lkt2YjTCLpzMnCETB3YTpty/DUM9K/0WzqZGHbWVyNmLo4XDHlG3qqgkXW2hmZQdY9BQAw==", + "peerDependencies": { + "@pixi/constants": "6.5.9", + "@pixi/core": "6.5.9", + "@pixi/math": "6.5.9", + "@pixi/utils": "6.5.9" + } + }, + "node_modules/@pixi/filter-alpha": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/filter-alpha/-/filter-alpha-6.5.9.tgz", + "integrity": "sha512-p87mGgMXX64CKUmTSadIOUzA7Q7MxybmsYPZbxFIFWsH2ML07RZChEaZWL2Bzql2CwgfejzxJPkCTXB/Qn5IRQ==", + "peerDependencies": { + "@pixi/core": "6.5.9" + } + }, + "node_modules/@pixi/filter-color-matrix": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/filter-color-matrix/-/filter-color-matrix-6.5.9.tgz", + "integrity": "sha512-ycx1SO3USLLbGHkqwo+3RwtvxnlffKinFuKQR59LrhuvULhrwLD9GVdB6e7wKgx7CrMtJe5kcED9ZTitLL7QbA==", + "peerDependencies": { + "@pixi/core": "6.5.9" + } + }, + "node_modules/@pixi/graphics": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/graphics/-/graphics-6.5.9.tgz", + "integrity": "sha512-+b7Ke6MkngftcRq2WweqsEWtV4ttRRurCiiPYeOhM5kGuAwDoyWGhXnWltiBQUHAE026uEep8wFi3vmlAzlXTQ==", + "peerDependencies": { + "@pixi/constants": "6.5.9", + "@pixi/core": "6.5.9", + "@pixi/display": "6.5.9", + "@pixi/math": "6.5.9", + "@pixi/sprite": "6.5.9", + "@pixi/utils": "6.5.9" + } + }, + "node_modules/@pixi/graphics-extras": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/graphics-extras/-/graphics-extras-6.5.9.tgz", + "integrity": "sha512-MP5zAC1glGw6iIb3yfDH5IULbvgifgK6To0QiJ7EjRif8p/hjI7vgu1C0TLFqN3Zuhp9QbJVYrHCmmxEmXzFXw==", + "peerDependencies": { + "@pixi/graphics": "6.5.9", + "@pixi/math": "6.5.9" + } + }, + "node_modules/@pixi/interaction": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/interaction/-/interaction-6.5.9.tgz", + "integrity": "sha512-PXWPPpOBwZdf/VtrstYaKqtUfJcJR57oRGdSXZ0mtvN8jEhsWUe0GlmlHEp6PxTwtn5ECKDy8+i9V0CcqLKgug==", + "peerDependencies": { + "@pixi/core": "6.5.9", + "@pixi/display": "6.5.9", + "@pixi/math": "6.5.9", + "@pixi/ticker": "6.5.9", + "@pixi/utils": "6.5.9" + } + }, + "node_modules/@pixi/loaders": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/loaders/-/loaders-6.5.9.tgz", + "integrity": "sha512-wHza2gnDEkfz1xmlLrsrxBzkEIWOufS4DFR/i1gl9lyzDJs5be1UB6zLbp8r7gxAYhNXHTbqU+CODYaJq/1TAQ==", + "peerDependencies": { + "@pixi/constants": "6.5.9", + "@pixi/core": "6.5.9", + "@pixi/utils": "6.5.9" + } + }, + "node_modules/@pixi/math": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/math/-/math-6.5.9.tgz", + "integrity": "sha512-L6EARDZiMXXqyqrgvc4lTVpMppRhkeJcCCg+6XAilp73ZAehmcCKt1fuCENbscpJgdX8EDBDWlGVrDOq6Yfa3Q==" + }, + "node_modules/@pixi/math-extras": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/math-extras/-/math-extras-6.5.9.tgz", + "integrity": "sha512-ObwX1u6AWpp2qQpun7xychY5dFkhoHi4/lXhqyOF0BKOeN61fuIcP6Ro5W9jWTcTRgJJZYRC18RH3QoM5NqtGQ==", + "peerDependencies": { + "@pixi/math": "6.5.9" + } + }, + "node_modules/@pixi/mixin-cache-as-bitmap": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/mixin-cache-as-bitmap/-/mixin-cache-as-bitmap-6.5.9.tgz", + "integrity": "sha512-nhBRLp5f4bxnf/q+3DrVWD4MNWn8kymi6V7AFr+ItDROnCurAg96fefOZlUcxOs9hXWKM6QXkR9XQSHeXKNq+Q==", + "peerDependencies": { + "@pixi/constants": "6.5.9", + "@pixi/core": "6.5.9", + "@pixi/display": "6.5.9", + "@pixi/math": "6.5.9", + "@pixi/settings": "6.5.9", + "@pixi/sprite": "6.5.9", + "@pixi/utils": "6.5.9" + } + }, + "node_modules/@pixi/mixin-get-child-by-name": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/mixin-get-child-by-name/-/mixin-get-child-by-name-6.5.9.tgz", + "integrity": "sha512-Co1exHIPACW3dURze2KKDi7TnBa7CwyhI1SuEflynopN2CkMEhJ9VQJDCvd5FNzkhmc14lIdIEqtN19w9EEOYw==", + "peerDependencies": { + "@pixi/display": "6.5.9" + } + }, + "node_modules/@pixi/mixin-get-global-position": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/mixin-get-global-position/-/mixin-get-global-position-6.5.9.tgz", + "integrity": "sha512-lwwbI4qVwlrknZjE8cVdgqsiIHdDyV4MdCL2wO7+zw5aW4EofPlyRb2av7za5onPagaFL/Jgj4WkUlZta40WaQ==", + "peerDependencies": { + "@pixi/display": "6.5.9", + "@pixi/math": "6.5.9" + } + }, + "node_modules/@pixi/polyfill": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/polyfill/-/polyfill-6.5.9.tgz", + "integrity": "sha512-S8ETjbGlW+YtJcC3Ysg9pSAHUsuyU3AvJfCL9PaQFG4/C39J36TqRLufB/9+WzUZ4TBI/CcsEWCh7InHpogT4Q==", + "dependencies": { + "object-assign": "^4.1.1", + "promise-polyfill": "^8.2.0" + } + }, + "node_modules/@pixi/runner": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/runner/-/runner-6.5.9.tgz", + "integrity": "sha512-xIfmhflbhrDw9ZEDezL46K+/L3pz79KU0qvtmg82eXgJdpsp9irDY2+QcEYgOO1AnYmqO9E1ygZd/RofCxRM1g==" + }, + "node_modules/@pixi/settings": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/settings/-/settings-6.5.9.tgz", + "integrity": "sha512-cOODlDuToO3uixgDRHlsxGbzlgZKNyZn+AeZKHyo6z8JpLh5mYrC4wEgLyHoKSOX0VgNzlSY6VNLthmgpu2gAg==", + "peerDependencies": { + "@pixi/constants": "6.5.9" + } + }, + "node_modules/@pixi/sprite": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/sprite/-/sprite-6.5.9.tgz", + "integrity": "sha512-pgYHrIES9vZ1HfcFVpvDpdI8sMwzNRhInDkfRCfJX0K3NaAW8AWzu1DPPsn+eYzIF14gpi9JZXS3lT8JtD8lug==", + "peerDependencies": { + "@pixi/constants": "6.5.9", + "@pixi/core": "6.5.9", + "@pixi/display": "6.5.9", + "@pixi/math": "6.5.9", + "@pixi/settings": "6.5.9", + "@pixi/utils": "6.5.9" + } + }, + "node_modules/@pixi/sprite-tiling": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/sprite-tiling/-/sprite-tiling-6.5.9.tgz", + "integrity": "sha512-+I7iQfp/xhosyNCGx0JmOk+QGIPHC1kjq/QEhzaMwvFnw7rsoUdhy4B13fF38DMPdzrFpGuyWfdZW5xezRA3Ww==", + "peerDependencies": { + "@pixi/constants": "6.5.9", + "@pixi/core": "6.5.9", + "@pixi/display": "6.5.9", + "@pixi/math": "6.5.9", + "@pixi/sprite": "6.5.9", + "@pixi/utils": "6.5.9" + } + }, + "node_modules/@pixi/spritesheet": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/spritesheet/-/spritesheet-6.5.9.tgz", + "integrity": "sha512-jf27xXl1/v2kA+Vr8E4/xLAMMO3xxNOk/blZCVr/RwKILS9T3R1Y7f4FICW2Gv4jLreBLvWwYM41NPon9/N3/g==", + "peerDependencies": { + "@pixi/core": "6.5.9", + "@pixi/loaders": "6.5.9", + "@pixi/math": "6.5.9", + "@pixi/utils": "6.5.9" + } + }, + "node_modules/@pixi/text": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/text/-/text-6.5.9.tgz", + "integrity": "sha512-nhIQTplpO9e4bjw32/A0mGYtx9yMV7TeL5PQ+pXKUJjvMKxNiqzK4ULLNvGd8bZm/RED1FpFtxGhuw5x4r+0qQ==", + "peerDependencies": { + "@pixi/core": "6.5.9", + "@pixi/math": "6.5.9", + "@pixi/settings": "6.5.9", + "@pixi/sprite": "6.5.9", + "@pixi/utils": "6.5.9" + } + }, + "node_modules/@pixi/ticker": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/ticker/-/ticker-6.5.9.tgz", + "integrity": "sha512-y7bpdSXc+UkfH2HPvOCV7XBk1eFsmoexsvVGqlRNd9r0sb/OXqcYLvnW4+BEyt5xKp7TpQibNBEKJCNih4dcMQ==", + "peerDependencies": { + "@pixi/extensions": "6.5.9", + "@pixi/settings": "6.5.9" + } + }, + "node_modules/@pixi/tilemap": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@pixi/tilemap/-/tilemap-3.2.2.tgz", + "integrity": "sha512-svdmMyJP63vdae3t66tCmE8IWeO/6lD1xXU+5gzfxqxJS5seTp2bm8mQok2c8PF0O6l/NYlLz6BRklOuEuHboQ==", + "peerDependencies": { + "@pixi/constants": "^6.0.4", + "@pixi/core": "^6.0.4", + "@pixi/display": "^6.0.4", + "@pixi/graphics": "^6.0.4", + "@pixi/math": "^6.0.4", + "@pixi/utils": "^6.0.4" + } + }, + "node_modules/@pixi/utils": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/utils/-/utils-6.5.9.tgz", + "integrity": "sha512-eLYZihYs9gEyPscoNvxgpZtKTXeCskoZ7TFmI23gAoegOIA3SWUsCudi/DJuQwGJSulitQ0M2BDJoVoSEoonEA==", + "dependencies": { + "@types/earcut": "^2.1.0", + "earcut": "^2.2.4", + "eventemitter3": "^3.1.0", + "url": "^0.11.0" + }, + "peerDependencies": { + "@pixi/constants": "6.5.9", + "@pixi/settings": "6.5.9" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.21", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", "dev": true }, + "node_modules/@popperjs/core": { + "version": "2.11.6", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", + "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@reach/observe-rect": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@reach/observe-rect/-/observe-rect-1.2.0.tgz", + "integrity": "sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ==" + }, + "node_modules/@react-aria/ssr": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.5.0.tgz", + "integrity": "sha512-h0MJdSWOd1qObLnJ8mprU31wI8tmKFJMuwT22MpWq6psisOOZaga6Ml4u6Ee6M6duWWISjXvqO4Sb/J0PBA+nQ==", + "dependencies": { + "@swc/helpers": "^0.4.14" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@restart/hooks": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.9.tgz", + "integrity": "sha512-3BekqcwB6Umeya+16XPooARn4qEPW6vNvwYnlofIYe6h9qG1/VeD7UvShCWx11eFz5ELYmwIEshz+MkPX3wjcQ==", + "dependencies": { + "dequal": "^2.0.2" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@restart/ui": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@restart/ui/-/ui-1.6.1.tgz", + "integrity": "sha512-cMI9DdqZV5VGEyANYM4alHK9/2Lh/mKZAMydztMl6PBLm6EetFbwE2RfYqliloR+EtEULlI4TiZk/XPhQAovxw==", + "dependencies": { + "@babel/runtime": "^7.20.7", + "@popperjs/core": "^2.11.6", + "@react-aria/ssr": "^3.4.1", + "@restart/hooks": "^0.4.7", + "@types/warning": "^3.0.0", + "dequal": "^2.0.3", + "dom-helpers": "^5.2.0", + "uncontrollable": "^7.2.1", + "warning": "^4.0.3" + }, + "peerDependencies": { + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + } + }, "node_modules/@rollup/pluginutils": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", @@ -2927,11 +3375,29 @@ "version": "0.4.14", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.14.tgz", "integrity": "sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==", - "dev": true, "dependencies": { "tslib": "^2.4.0" } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.0.0-alpha.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.0.0-alpha.0.tgz", + "integrity": "sha512-WpHU/dt34NwZZ8qtiE05TF+nX/b1W6qrWZarO+s8jJFpPVicrTbJKp5Bjt4eSJuk7aYw272oEfsH3ABBRgj+3A==", + "dependencies": { + "@babel/runtime": "^7.16.7", + "@reach/observe-rect": "^1.1.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16" + } + }, "node_modules/@testing-library/dom": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.0.1.tgz", @@ -2999,6 +3465,11 @@ "@types/chai": "*" } }, + "node_modules/@types/earcut": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-2.1.1.tgz", + "integrity": "sha512-w8oigUCDjElRHRRrMvn/spybSMyX8MTkKA5Dv+tS1IE/TgmNZPqUYtvYBXGY8cieSE66gm+szeK+bnbxC2xHTQ==" + }, "node_modules/@types/eslint": { "version": "8.21.2", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.21.2.tgz", @@ -3039,6 +3510,11 @@ "integrity": "sha512-1uEQxww3DaghA0RxqHx0O0ppVlo43pJhepY51OxuQIKHpjbnYLA7vcdwioNPzIqmC2u3I/dmylcqjlh0e7AyUA==", "dev": true }, + "node_modules/@types/offscreencanvas": { + "version": "2019.7.0", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.0.tgz", + "integrity": "sha512-PGcyveRIpL1XIqK8eBsmRBt76eFgtzuPiSTyKHZxnGemp2yzGzWpjYKAfK3wIMiU7eH+851yEpiuP8JZerTmWg==" + }, "node_modules/@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -3048,14 +3524,12 @@ "node_modules/@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "dev": true + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" }, "node_modules/@types/react": { "version": "18.0.28", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.28.tgz", "integrity": "sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==", - "dev": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -3071,11 +3545,18 @@ "@types/react": "*" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", - "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", - "dev": true + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" }, "node_modules/@types/semver": { "version": "7.3.13", @@ -3095,6 +3576,11 @@ "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==", "dev": true }, + "node_modules/@types/warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz", + "integrity": "sha512-t/Tvs5qR47OLOr+4E9ckN8AmP2Tf16gWq+/qA4iUGS/OOyHVO8wv2vjJuX8SNOUTJyWb+2t7wJm6cXILFnOROA==" + }, "node_modules/@types/yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", @@ -4316,6 +4802,11 @@ "node": ">=8" } }, + "node_modules/classnames": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", + "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -4539,8 +5030,7 @@ "node_modules/csstype": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", - "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==", - "dev": true + "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" }, "node_modules/cypress": { "version": "12.8.1", @@ -4782,6 +5272,14 @@ "node": ">=0.4.0" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -4812,6 +5310,15 @@ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/domexception": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", @@ -4839,6 +5346,11 @@ "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", "dev": true }, + "node_modules/earcut": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", + "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==" + }, "node_modules/ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -5737,6 +6249,11 @@ "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==", "dev": true }, + "node_modules/eventemitter3": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", + "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" + }, "node_modules/execa": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", @@ -5810,8 +6327,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { "version": "3.2.7", @@ -6206,6 +6722,14 @@ "assert-plus": "^1.0.0" } }, + "node_modules/gifuct-js": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/gifuct-js/-/gifuct-js-2.1.2.tgz", + "integrity": "sha512-rI2asw77u0mGgwhV3qA+OEgYqaDn5UNqgs+Bx0FGwSpuqfYn+Ir6RQY5ENNQ8SbIiG/m5gVa7CD5RriO4f4Lsg==", + "dependencies": { + "js-binary-schema-parser": "^2.0.3" + } + }, "node_modules/glob": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", @@ -6421,6 +6945,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/howler": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/howler/-/howler-2.2.3.tgz", + "integrity": "sha512-QM0FFkw0LRX1PR8pNzJVAY25JhIWvbKMBFM4gqk+QdV+kPXOhleWGCB6AiAF/goGjIHK2e/nIElplvjQwhr0jg==" + }, "node_modules/html-encoding-sniffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", @@ -6609,6 +7138,14 @@ "node": ">= 0.4" } }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/is-arguments": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", @@ -7091,6 +7628,11 @@ "node": ">=10" } }, + "node_modules/js-binary-schema-parser": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/js-binary-schema-parser/-/js-binary-schema-parser-2.0.3.tgz", + "integrity": "sha512-xezGJmOb4lk/M1ZZLTR/jaBHQ4gG/lqQnJqdIv4721DMggsa1bDVlHXNeHYogaIEHD9vCRv0fcL4hMA+Coarkg==" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -7365,6 +7907,11 @@ "tslib": "^2.1.0" } }, + "node_modules/load-script": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz", + "integrity": "sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==" + }, "node_modules/local-pkg": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", @@ -7797,7 +8344,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -8024,6 +8570,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -8248,22 +8799,42 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/promise-polyfill": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz", + "integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, + "node_modules/prop-types-extra": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz", + "integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==", + "dependencies": { + "react-is": "^16.3.2", + "warning": "^4.0.0" + }, + "peerDependencies": { + "react": ">=0.14.0" + } + }, + "node_modules/prop-types-extra/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/proxy-from-env": { "version": "1.1.0", @@ -8311,6 +8882,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -8348,6 +8928,35 @@ "node": ">=0.10.0" } }, + "node_modules/react-bootstrap": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.7.2.tgz", + "integrity": "sha512-WDSln+mG4RLLFO01stkj2bEx/3MF4YihK9D/dWnHaSxOiQZLbhhlf95D2Jb20X3t2m7vMxRe888FVrfLJoGmmA==", + "dependencies": { + "@babel/runtime": "^7.17.2", + "@restart/hooks": "^0.4.6", + "@restart/ui": "^1.4.1", + "@types/react-transition-group": "^4.4.4", + "classnames": "^2.3.1", + "dom-helpers": "^5.2.1", + "invariant": "^2.2.4", + "prop-types": "^15.8.1", + "prop-types-extra": "^1.1.0", + "react-transition-group": "^4.4.2", + "uncontrollable": "^7.2.1", + "warning": "^4.0.3" + }, + "peerDependencies": { + "@types/react": ">=16.14.8", + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -8360,12 +8969,25 @@ "react": "^18.2.0" } }, + "node_modules/react-icons": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.8.0.tgz", + "integrity": "sha512-N6+kOLcihDiAnj5Czu637waJqSnwlMNROzVZMhfX68V/9bu9qHaMIJC4UdozWoOk57gahFCNHwVvWzm0MTzRjg==", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, "node_modules/react-refresh": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", @@ -8388,6 +9010,17 @@ "react": "^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-slider": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/react-slider/-/react-slider-2.0.4.tgz", + "integrity": "sha512-sWwQD01n6v+MbeLCYthJGZPc0kzOyhQHyd0bSo0edg+IAxTVQmj3Oy4SBK65eX6gNwS9meUn6Z5sIBUVmwAd9g==", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18" + } + }, "node_modules/react-test-renderer": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-18.2.0.tgz", @@ -8408,6 +9041,37 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/react-youtube": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-youtube/-/react-youtube-10.1.0.tgz", + "integrity": "sha512-ZfGtcVpk0SSZtWCSTYOQKhfx5/1cfyEW1JN/mugGNfAxT3rmVJeMbGpA9+e78yG21ls5nc/5uZJETE3cm3knBg==", + "dependencies": { + "fast-deep-equal": "3.1.3", + "prop-types": "15.8.1", + "youtube-player": "5.5.2" + }, + "engines": { + "node": ">= 14.x" + }, + "peerDependencies": { + "react": ">=0.14.1" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -8455,8 +9119,7 @@ "node_modules/regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", - "dev": true + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, "node_modules/regenerator-transform": { "version": "0.15.1", @@ -8850,6 +9513,11 @@ "node": ">= 10" } }, + "node_modules/sister": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sister/-/sister-3.0.2.tgz", + "integrity": "sha512-p19rtTs+NksBRKW9qn0UhZ8/TUI9BPw9lmtHny+Y3TinWlOa9jWh9xB0AtPSdmOy49NJJJSSe0Ey4C7h0TrcYA==" + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -9317,8 +9985,7 @@ "node_modules/tslib": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", - "dev": true + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" }, "node_modules/tsutils": { "version": "3.21.0", @@ -9434,6 +10101,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/uncontrollable": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", + "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", + "dependencies": { + "@babel/runtime": "^7.6.3", + "@types/react": ">=16.9.11", + "invariant": "^2.2.4", + "react-lifecycles-compat": "^3.0.4" + }, + "peerDependencies": { + "react": ">=15.0.0" + } + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", @@ -9527,6 +10208,15 @@ "punycode": "^2.1.0" } }, + "node_modules/url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, "node_modules/url-parse": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", @@ -9537,6 +10227,19 @@ "requires-port": "^1.0.0" } }, + "node_modules/url/node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" + }, + "node_modules/use-between": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/use-between/-/use-between-1.3.5.tgz", + "integrity": "sha512-IP9eJfszZr0aah/6i/pzaM7n/QgMPwWKJ+mnWqT5O0qFhLnztPbkVC6L7zI6ygeBIMJHfmUGvsw0b28pyrEGSA==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -9751,6 +10454,14 @@ "node": ">=14" } }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -10014,6 +10725,29 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/youtube-player": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/youtube-player/-/youtube-player-5.5.2.tgz", + "integrity": "sha512-ZGtsemSpXnDky2AUYWgxjaopgB+shFHgXVpiJFeNB5nWEugpW1KWYDaHKuLqh2b67r24GtP6HoSW5swvf0fFIQ==", + "dependencies": { + "debug": "^2.6.6", + "load-script": "^1.0.0", + "sister": "^3.0.0" + } + }, + "node_modules/youtube-player/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/youtube-player/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" } }, "dependencies": { @@ -11269,7 +12003,6 @@ "version": "7.21.0", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==", - "dev": true, "requires": { "regenerator-runtime": "^0.13.11" } @@ -11660,6 +12393,48 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, + "@nitrots/nitro-renderer": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@nitrots/nitro-renderer/-/nitro-renderer-1.6.6.tgz", + "integrity": "sha512-VMxn4gAV49G1nnOrtL6koLnJglHdp83zILcKe8DTZsZXX6GOGU2wST1sSnHvdcH28KpesqrCP5dyJGKC/0ylYQ==", + "requires": { + "@pixi/app": "~6.5.0", + "@pixi/basis": "~6.5.0", + "@pixi/canvas-display": "~6.5.0", + "@pixi/canvas-extract": "~6.5.0", + "@pixi/canvas-renderer": "~6.5.0", + "@pixi/constants": "~6.5.0", + "@pixi/core": "~6.5.0", + "@pixi/display": "~6.5.0", + "@pixi/events": "~6.5.0", + "@pixi/extensions": "~6.5.0", + "@pixi/extract": "~6.5.0", + "@pixi/filter-alpha": "~6.5.0", + "@pixi/filter-color-matrix": "~6.5.0", + "@pixi/graphics": "~6.5.0", + "@pixi/graphics-extras": "~6.5.0", + "@pixi/interaction": "~6.5.0", + "@pixi/loaders": "~6.5.0", + "@pixi/math": "~6.5.0", + "@pixi/math-extras": "~6.5.0", + "@pixi/mixin-cache-as-bitmap": "~6.5.0", + "@pixi/mixin-get-child-by-name": "~6.5.0", + "@pixi/mixin-get-global-position": "~6.5.0", + "@pixi/polyfill": "~6.5.0", + "@pixi/runner": "~6.5.0", + "@pixi/settings": "~6.5.0", + "@pixi/sprite": "~6.5.0", + "@pixi/sprite-tiling": "~6.5.0", + "@pixi/spritesheet": "~6.5.0", + "@pixi/text": "~6.5.0", + "@pixi/ticker": "~6.5.0", + "@pixi/tilemap": "^3.2.2", + "@pixi/utils": "~6.5.0", + "gifuct-js": "^2.1.2", + "howler": "^2.2.3", + "pako": "^2.0.4" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -11931,12 +12706,259 @@ "esquery": "^1.0.1" } }, + "@pixi/app": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/app/-/app-6.5.9.tgz", + "integrity": "sha512-RDFR8ea86eykTmxlQPb1PMdXqYaeLmf1BKprcEKOOr6vmNLykzn+UEaal4OJtmpgtAsHt6hkpW7nUeZ8idbWZA==", + "requires": {} + }, + "@pixi/basis": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/basis/-/basis-6.5.9.tgz", + "integrity": "sha512-mWuqq3ZmNo2eZw6rapOAQdafd+LaHLOBaa/Iy/PqfmuB8aFYaBgzEyF4FjnHGK1TYn+2PKO85G3ptIoAh9c67g==", + "requires": {} + }, + "@pixi/canvas-display": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/canvas-display/-/canvas-display-6.5.9.tgz", + "integrity": "sha512-ZxVBkyqDOPJKTkF9onGbGtwToICQEVcx/PTHh03LU6lkJ4/fenCd25f4265tdXarIFuu3k03liFKVCCy9sRU0g==", + "requires": {} + }, + "@pixi/canvas-extract": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/canvas-extract/-/canvas-extract-6.5.9.tgz", + "integrity": "sha512-EpOc1nHOhG4Ph6JsliXT0ZHxXzXicUezpXf/xuxjz0Td9A01pkzljI1gKI6qc5n/viD8Y2uaCKDnUSftdhiRsA==", + "requires": {} + }, + "@pixi/canvas-renderer": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/canvas-renderer/-/canvas-renderer-6.5.9.tgz", + "integrity": "sha512-bs97ub6ZfEopGqKXBpNntuONmSsWs1UEJ06yWfFUfkLzhXrWNsaFtF5CbaELDzB8AMqH0HY5QgNNWylj8VQ0tQ==", + "requires": {} + }, + "@pixi/compressed-textures": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/compressed-textures/-/compressed-textures-6.5.9.tgz", + "integrity": "sha512-7FbgA6fVjhhoWrIHjEkTTZBZIr4FlQ7bWQzpSy3i8J0lGFTFp1p6n17i0t8xxqrJ1SWAJud8WOESsiAHWUHLDQ==", + "peer": true, + "requires": {} + }, + "@pixi/constants": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/constants/-/constants-6.5.9.tgz", + "integrity": "sha512-749Vv+DUh4Tguku6uouXUIAUHThYU/cDZzWW4lYNv2UrqUrPxE1a7b8Ca0GakFjt6HZIenl6DnUYLP4yE6PWiQ==" + }, + "@pixi/core": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/core/-/core-6.5.9.tgz", + "integrity": "sha512-NQGaEYtUIKNAQNeqLsfHSkx1BYuOWJzAYDpb63QEZFvV8gTRf2t3SBuyvSxvMFAGakNrqYefIXkfJXpmHOrk7A==", + "requires": { + "@types/offscreencanvas": "^2019.6.4" + } + }, + "@pixi/display": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/display/-/display-6.5.9.tgz", + "integrity": "sha512-85eODYWsOM/kIt2N/L51lsAl3DLJA+1Eed+Nl6ZeF/pEvQnXf7jDZzGwVmUKJurpPWhjkA5OnzWabFw3De2qZg==", + "requires": {} + }, + "@pixi/events": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/events/-/events-6.5.9.tgz", + "integrity": "sha512-f5hiyy0nZtgIKrarbeO+DsZI441WvGz1W823D9G3g+FKDHXIoX72orXT5FvSIGJNNtuoS5LZKszKUQGNqJh/kQ==", + "requires": {} + }, + "@pixi/extensions": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/extensions/-/extensions-6.5.9.tgz", + "integrity": "sha512-vwzEhLkGiiCw9e7QmXBKHuJzX1DzaA2JcFw0Kl1DTI0lH1cIZccE3rVBbuVY8+Zvb33WV5XxwQC03/qyx4DUbw==" + }, + "@pixi/extract": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/extract/-/extract-6.5.9.tgz", + "integrity": "sha512-fqnGfJFC6OJ63Js+lkt2YjTCLpzMnCETB3YTpty/DUM9K/0WzqZGHbWVyNmLo4XDHlG3qqgkXW2hmZQdY9BQAw==", + "requires": {} + }, + "@pixi/filter-alpha": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/filter-alpha/-/filter-alpha-6.5.9.tgz", + "integrity": "sha512-p87mGgMXX64CKUmTSadIOUzA7Q7MxybmsYPZbxFIFWsH2ML07RZChEaZWL2Bzql2CwgfejzxJPkCTXB/Qn5IRQ==", + "requires": {} + }, + "@pixi/filter-color-matrix": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/filter-color-matrix/-/filter-color-matrix-6.5.9.tgz", + "integrity": "sha512-ycx1SO3USLLbGHkqwo+3RwtvxnlffKinFuKQR59LrhuvULhrwLD9GVdB6e7wKgx7CrMtJe5kcED9ZTitLL7QbA==", + "requires": {} + }, + "@pixi/graphics": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/graphics/-/graphics-6.5.9.tgz", + "integrity": "sha512-+b7Ke6MkngftcRq2WweqsEWtV4ttRRurCiiPYeOhM5kGuAwDoyWGhXnWltiBQUHAE026uEep8wFi3vmlAzlXTQ==", + "requires": {} + }, + "@pixi/graphics-extras": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/graphics-extras/-/graphics-extras-6.5.9.tgz", + "integrity": "sha512-MP5zAC1glGw6iIb3yfDH5IULbvgifgK6To0QiJ7EjRif8p/hjI7vgu1C0TLFqN3Zuhp9QbJVYrHCmmxEmXzFXw==", + "requires": {} + }, + "@pixi/interaction": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/interaction/-/interaction-6.5.9.tgz", + "integrity": "sha512-PXWPPpOBwZdf/VtrstYaKqtUfJcJR57oRGdSXZ0mtvN8jEhsWUe0GlmlHEp6PxTwtn5ECKDy8+i9V0CcqLKgug==", + "requires": {} + }, + "@pixi/loaders": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/loaders/-/loaders-6.5.9.tgz", + "integrity": "sha512-wHza2gnDEkfz1xmlLrsrxBzkEIWOufS4DFR/i1gl9lyzDJs5be1UB6zLbp8r7gxAYhNXHTbqU+CODYaJq/1TAQ==", + "requires": {} + }, + "@pixi/math": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/math/-/math-6.5.9.tgz", + "integrity": "sha512-L6EARDZiMXXqyqrgvc4lTVpMppRhkeJcCCg+6XAilp73ZAehmcCKt1fuCENbscpJgdX8EDBDWlGVrDOq6Yfa3Q==" + }, + "@pixi/math-extras": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/math-extras/-/math-extras-6.5.9.tgz", + "integrity": "sha512-ObwX1u6AWpp2qQpun7xychY5dFkhoHi4/lXhqyOF0BKOeN61fuIcP6Ro5W9jWTcTRgJJZYRC18RH3QoM5NqtGQ==", + "requires": {} + }, + "@pixi/mixin-cache-as-bitmap": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/mixin-cache-as-bitmap/-/mixin-cache-as-bitmap-6.5.9.tgz", + "integrity": "sha512-nhBRLp5f4bxnf/q+3DrVWD4MNWn8kymi6V7AFr+ItDROnCurAg96fefOZlUcxOs9hXWKM6QXkR9XQSHeXKNq+Q==", + "requires": {} + }, + "@pixi/mixin-get-child-by-name": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/mixin-get-child-by-name/-/mixin-get-child-by-name-6.5.9.tgz", + "integrity": "sha512-Co1exHIPACW3dURze2KKDi7TnBa7CwyhI1SuEflynopN2CkMEhJ9VQJDCvd5FNzkhmc14lIdIEqtN19w9EEOYw==", + "requires": {} + }, + "@pixi/mixin-get-global-position": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/mixin-get-global-position/-/mixin-get-global-position-6.5.9.tgz", + "integrity": "sha512-lwwbI4qVwlrknZjE8cVdgqsiIHdDyV4MdCL2wO7+zw5aW4EofPlyRb2av7za5onPagaFL/Jgj4WkUlZta40WaQ==", + "requires": {} + }, + "@pixi/polyfill": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/polyfill/-/polyfill-6.5.9.tgz", + "integrity": "sha512-S8ETjbGlW+YtJcC3Ysg9pSAHUsuyU3AvJfCL9PaQFG4/C39J36TqRLufB/9+WzUZ4TBI/CcsEWCh7InHpogT4Q==", + "requires": { + "object-assign": "^4.1.1", + "promise-polyfill": "^8.2.0" + } + }, + "@pixi/runner": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/runner/-/runner-6.5.9.tgz", + "integrity": "sha512-xIfmhflbhrDw9ZEDezL46K+/L3pz79KU0qvtmg82eXgJdpsp9irDY2+QcEYgOO1AnYmqO9E1ygZd/RofCxRM1g==" + }, + "@pixi/settings": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/settings/-/settings-6.5.9.tgz", + "integrity": "sha512-cOODlDuToO3uixgDRHlsxGbzlgZKNyZn+AeZKHyo6z8JpLh5mYrC4wEgLyHoKSOX0VgNzlSY6VNLthmgpu2gAg==", + "requires": {} + }, + "@pixi/sprite": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/sprite/-/sprite-6.5.9.tgz", + "integrity": "sha512-pgYHrIES9vZ1HfcFVpvDpdI8sMwzNRhInDkfRCfJX0K3NaAW8AWzu1DPPsn+eYzIF14gpi9JZXS3lT8JtD8lug==", + "requires": {} + }, + "@pixi/sprite-tiling": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/sprite-tiling/-/sprite-tiling-6.5.9.tgz", + "integrity": "sha512-+I7iQfp/xhosyNCGx0JmOk+QGIPHC1kjq/QEhzaMwvFnw7rsoUdhy4B13fF38DMPdzrFpGuyWfdZW5xezRA3Ww==", + "requires": {} + }, + "@pixi/spritesheet": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/spritesheet/-/spritesheet-6.5.9.tgz", + "integrity": "sha512-jf27xXl1/v2kA+Vr8E4/xLAMMO3xxNOk/blZCVr/RwKILS9T3R1Y7f4FICW2Gv4jLreBLvWwYM41NPon9/N3/g==", + "requires": {} + }, + "@pixi/text": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/text/-/text-6.5.9.tgz", + "integrity": "sha512-nhIQTplpO9e4bjw32/A0mGYtx9yMV7TeL5PQ+pXKUJjvMKxNiqzK4ULLNvGd8bZm/RED1FpFtxGhuw5x4r+0qQ==", + "requires": {} + }, + "@pixi/ticker": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/ticker/-/ticker-6.5.9.tgz", + "integrity": "sha512-y7bpdSXc+UkfH2HPvOCV7XBk1eFsmoexsvVGqlRNd9r0sb/OXqcYLvnW4+BEyt5xKp7TpQibNBEKJCNih4dcMQ==", + "requires": {} + }, + "@pixi/tilemap": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@pixi/tilemap/-/tilemap-3.2.2.tgz", + "integrity": "sha512-svdmMyJP63vdae3t66tCmE8IWeO/6lD1xXU+5gzfxqxJS5seTp2bm8mQok2c8PF0O6l/NYlLz6BRklOuEuHboQ==", + "requires": {} + }, + "@pixi/utils": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/@pixi/utils/-/utils-6.5.9.tgz", + "integrity": "sha512-eLYZihYs9gEyPscoNvxgpZtKTXeCskoZ7TFmI23gAoegOIA3SWUsCudi/DJuQwGJSulitQ0M2BDJoVoSEoonEA==", + "requires": { + "@types/earcut": "^2.1.0", + "earcut": "^2.2.4", + "eventemitter3": "^3.1.0", + "url": "^0.11.0" + } + }, "@polka/url": { "version": "1.0.0-next.21", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", "dev": true }, + "@popperjs/core": { + "version": "2.11.6", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", + "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==" + }, + "@reach/observe-rect": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@reach/observe-rect/-/observe-rect-1.2.0.tgz", + "integrity": "sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ==" + }, + "@react-aria/ssr": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.5.0.tgz", + "integrity": "sha512-h0MJdSWOd1qObLnJ8mprU31wI8tmKFJMuwT22MpWq6psisOOZaga6Ml4u6Ee6M6duWWISjXvqO4Sb/J0PBA+nQ==", + "requires": { + "@swc/helpers": "^0.4.14" + } + }, + "@restart/hooks": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.9.tgz", + "integrity": "sha512-3BekqcwB6Umeya+16XPooARn4qEPW6vNvwYnlofIYe6h9qG1/VeD7UvShCWx11eFz5ELYmwIEshz+MkPX3wjcQ==", + "requires": { + "dequal": "^2.0.2" + } + }, + "@restart/ui": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@restart/ui/-/ui-1.6.1.tgz", + "integrity": "sha512-cMI9DdqZV5VGEyANYM4alHK9/2Lh/mKZAMydztMl6PBLm6EetFbwE2RfYqliloR+EtEULlI4TiZk/XPhQAovxw==", + "requires": { + "@babel/runtime": "^7.20.7", + "@popperjs/core": "^2.11.6", + "@react-aria/ssr": "^3.4.1", + "@restart/hooks": "^0.4.7", + "@types/warning": "^3.0.0", + "dequal": "^2.0.3", + "dom-helpers": "^5.2.0", + "uncontrollable": "^7.2.1", + "warning": "^4.0.3" + } + }, "@rollup/pluginutils": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", @@ -11951,11 +12973,19 @@ "version": "0.4.14", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.14.tgz", "integrity": "sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==", - "dev": true, "requires": { "tslib": "^2.4.0" } }, + "@tanstack/react-virtual": { + "version": "3.0.0-alpha.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.0.0-alpha.0.tgz", + "integrity": "sha512-WpHU/dt34NwZZ8qtiE05TF+nX/b1W6qrWZarO+s8jJFpPVicrTbJKp5Bjt4eSJuk7aYw272oEfsH3ABBRgj+3A==", + "requires": { + "@babel/runtime": "^7.16.7", + "@reach/observe-rect": "^1.1.0" + } + }, "@testing-library/dom": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.0.1.tgz", @@ -12010,6 +13040,11 @@ "@types/chai": "*" } }, + "@types/earcut": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-2.1.1.tgz", + "integrity": "sha512-w8oigUCDjElRHRRrMvn/spybSMyX8MTkKA5Dv+tS1IE/TgmNZPqUYtvYBXGY8cieSE66gm+szeK+bnbxC2xHTQ==" + }, "@types/eslint": { "version": "8.21.2", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.21.2.tgz", @@ -12050,6 +13085,11 @@ "integrity": "sha512-1uEQxww3DaghA0RxqHx0O0ppVlo43pJhepY51OxuQIKHpjbnYLA7vcdwioNPzIqmC2u3I/dmylcqjlh0e7AyUA==", "dev": true }, + "@types/offscreencanvas": { + "version": "2019.7.0", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.0.tgz", + "integrity": "sha512-PGcyveRIpL1XIqK8eBsmRBt76eFgtzuPiSTyKHZxnGemp2yzGzWpjYKAfK3wIMiU7eH+851yEpiuP8JZerTmWg==" + }, "@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -12059,14 +13099,12 @@ "@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "dev": true + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" }, "@types/react": { "version": "18.0.28", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.28.tgz", "integrity": "sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==", - "dev": true, "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -12082,11 +13120,18 @@ "@types/react": "*" } }, + "@types/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==", + "requires": { + "@types/react": "*" + } + }, "@types/scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", - "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", - "dev": true + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" }, "@types/semver": { "version": "7.3.13", @@ -12106,6 +13151,11 @@ "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==", "dev": true }, + "@types/warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz", + "integrity": "sha512-t/Tvs5qR47OLOr+4E9ckN8AmP2Tf16gWq+/qA4iUGS/OOyHVO8wv2vjJuX8SNOUTJyWb+2t7wJm6cXILFnOROA==" + }, "@types/yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", @@ -12397,7 +13447,8 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true + "dev": true, + "requires": {} }, "acorn-walk": { "version": "8.2.0", @@ -12966,6 +14017,11 @@ "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", "dev": true }, + "classnames": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", + "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" + }, "clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -13143,8 +14199,7 @@ "csstype": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", - "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==", - "dev": true + "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" }, "cypress": { "version": "12.8.1", @@ -13340,6 +14395,11 @@ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true }, + "dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==" + }, "dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -13364,6 +14424,15 @@ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true }, + "dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "requires": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "domexception": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", @@ -13385,6 +14454,11 @@ "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", "dev": true }, + "earcut": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", + "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==" + }, "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -13733,7 +14807,8 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.1.0.tgz", "integrity": "sha512-oKMhGv3ihGbCIimCAjqkdzx2Q+jthoqnXSP+d86M9tptwugycmTFdVR4IpLgq2c4SHifbwO90z2fQ8/Aio73yw==", - "dev": true + "dev": true, + "requires": {} }, "eslint-import-resolver-node": { "version": "0.3.7", @@ -13976,7 +15051,8 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", - "dev": true + "dev": true, + "requires": {} }, "eslint-scope": { "version": "5.1.1", @@ -14078,6 +15154,11 @@ "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==", "dev": true }, + "eventemitter3": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", + "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" + }, "execa": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", @@ -14131,8 +15212,7 @@ "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-glob": { "version": "3.2.7", @@ -14433,6 +15513,14 @@ "assert-plus": "^1.0.0" } }, + "gifuct-js": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/gifuct-js/-/gifuct-js-2.1.2.tgz", + "integrity": "sha512-rI2asw77u0mGgwhV3qA+OEgYqaDn5UNqgs+Bx0FGwSpuqfYn+Ir6RQY5ENNQ8SbIiG/m5gVa7CD5RriO4f4Lsg==", + "requires": { + "js-binary-schema-parser": "^2.0.3" + } + }, "glob": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", @@ -14587,6 +15675,11 @@ "has-symbols": "^1.0.2" } }, + "howler": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/howler/-/howler-2.2.3.tgz", + "integrity": "sha512-QM0FFkw0LRX1PR8pNzJVAY25JhIWvbKMBFM4gqk+QdV+kPXOhleWGCB6AiAF/goGjIHK2e/nIElplvjQwhr0jg==" + }, "html-encoding-sniffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", @@ -14722,6 +15815,14 @@ "side-channel": "^1.0.4" } }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "requires": { + "loose-envify": "^1.0.0" + } + }, "is-arguments": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", @@ -15051,6 +16152,11 @@ "minimatch": "^3.0.4" } }, + "js-binary-schema-parser": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/js-binary-schema-parser/-/js-binary-schema-parser-2.0.3.tgz", + "integrity": "sha512-xezGJmOb4lk/M1ZZLTR/jaBHQ4gG/lqQnJqdIv4721DMggsa1bDVlHXNeHYogaIEHD9vCRv0fcL4hMA+Coarkg==" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -15269,6 +16375,11 @@ } } }, + "load-script": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz", + "integrity": "sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==" + }, "local-pkg": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", @@ -15594,8 +16705,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, "object-inspect": { "version": "1.12.3", @@ -15750,6 +16860,11 @@ "aggregate-error": "^3.0.0" } }, + "pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -15902,11 +17017,15 @@ } } }, + "promise-polyfill": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz", + "integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==" + }, "prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "requires": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -15916,8 +17035,23 @@ "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + } + } + }, + "prop-types-extra": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz", + "integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==", + "requires": { + "react-is": "^16.3.2", + "warning": "^4.0.0" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" } } }, @@ -15958,6 +17092,11 @@ "side-channel": "^1.0.4" } }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==" + }, "querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -15978,6 +17117,25 @@ "loose-envify": "^1.1.0" } }, + "react-bootstrap": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.7.2.tgz", + "integrity": "sha512-WDSln+mG4RLLFO01stkj2bEx/3MF4YihK9D/dWnHaSxOiQZLbhhlf95D2Jb20X3t2m7vMxRe888FVrfLJoGmmA==", + "requires": { + "@babel/runtime": "^7.17.2", + "@restart/hooks": "^0.4.6", + "@restart/ui": "^1.4.1", + "@types/react-transition-group": "^4.4.4", + "classnames": "^2.3.1", + "dom-helpers": "^5.2.1", + "invariant": "^2.2.4", + "prop-types": "^15.8.1", + "prop-types-extra": "^1.1.0", + "react-transition-group": "^4.4.2", + "uncontrollable": "^7.2.1", + "warning": "^4.0.3" + } + }, "react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -15987,12 +17145,23 @@ "scheduler": "^0.23.0" } }, + "react-icons": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.8.0.tgz", + "integrity": "sha512-N6+kOLcihDiAnj5Czu637waJqSnwlMNROzVZMhfX68V/9bu9qHaMIJC4UdozWoOk57gahFCNHwVvWzm0MTzRjg==", + "requires": {} + }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true }, + "react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, "react-refresh": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", @@ -16009,6 +17178,14 @@ "react-is": "^16.12.0 || ^17.0.0 || ^18.0.0" } }, + "react-slider": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/react-slider/-/react-slider-2.0.4.tgz", + "integrity": "sha512-sWwQD01n6v+MbeLCYthJGZPc0kzOyhQHyd0bSo0edg+IAxTVQmj3Oy4SBK65eX6gNwS9meUn6Z5sIBUVmwAd9g==", + "requires": { + "prop-types": "^15.8.1" + } + }, "react-test-renderer": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-18.2.0.tgz", @@ -16028,6 +17205,27 @@ } } }, + "react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "requires": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + } + }, + "react-youtube": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-youtube/-/react-youtube-10.1.0.tgz", + "integrity": "sha512-ZfGtcVpk0SSZtWCSTYOQKhfx5/1cfyEW1JN/mugGNfAxT3rmVJeMbGpA9+e78yG21ls5nc/5uZJETE3cm3knBg==", + "requires": { + "fast-deep-equal": "3.1.3", + "prop-types": "15.8.1", + "youtube-player": "5.5.2" + } + }, "readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -16066,8 +17264,7 @@ "regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", - "dev": true + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, "regenerator-transform": { "version": "0.15.1", @@ -16350,6 +17547,11 @@ "totalist": "^3.0.0" } }, + "sister": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sister/-/sister-3.0.2.tgz", + "integrity": "sha512-p19rtTs+NksBRKW9qn0UhZ8/TUI9BPw9lmtHny+Y3TinWlOa9jWh9xB0AtPSdmOy49NJJJSSe0Ey4C7h0TrcYA==" + }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -16677,7 +17879,8 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-2.1.0.tgz", "integrity": "sha512-lztI9ohwclQHISVWrM/hlcgsRpphsii94DV9AQtAw2XJSVNiv+3ppdEsrL5J+xc5oTeHXe1qDqlOAGw8VSa9+Q==", - "dev": true + "dev": true, + "requires": {} }, "tsconfig-paths": { "version": "4.1.2", @@ -16693,8 +17896,7 @@ "tslib": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", - "dev": true + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" }, "tsutils": { "version": "3.21.0", @@ -16778,6 +17980,17 @@ "which-boxed-primitive": "^1.0.2" } }, + "uncontrollable": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", + "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", + "requires": { + "@babel/runtime": "^7.6.3", + "@types/react": ">=16.9.11", + "invariant": "^2.2.4", + "react-lifecycles-compat": "^3.0.4" + } + }, "unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", @@ -16837,6 +18050,22 @@ "punycode": "^2.1.0" } }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" + } + } + }, "url-parse": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", @@ -16847,6 +18076,12 @@ "requires-port": "^1.0.0" } }, + "use-between": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/use-between/-/use-between-1.3.5.tgz", + "integrity": "sha512-IP9eJfszZr0aah/6i/pzaM7n/QgMPwWKJ+mnWqT5O0qFhLnztPbkVC6L7zI6ygeBIMJHfmUGvsw0b28pyrEGSA==", + "requires": {} + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -16964,6 +18199,14 @@ "xml-name-validator": "^4.0.0" } }, + "warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "requires": { + "loose-envify": "^1.0.0" + } + }, "webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -17070,7 +18313,8 @@ "version": "8.13.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", - "dev": true + "dev": true, + "requires": {} }, "xml-name-validator": { "version": "4.0.0", @@ -17151,6 +18395,31 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true + }, + "youtube-player": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/youtube-player/-/youtube-player-5.5.2.tgz", + "integrity": "sha512-ZGtsemSpXnDky2AUYWgxjaopgB+shFHgXVpiJFeNB5nWEugpW1KWYDaHKuLqh2b67r24GtP6HoSW5swvf0fFIQ==", + "requires": { + "debug": "^2.6.6", + "load-script": "^1.0.0", + "sister": "^3.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } } } } diff --git a/package.json b/package.json index 0c0730f..c6383ed 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,15 @@ "scripts": {}, "private": true, "dependencies": { + "@nitrots/nitro-renderer": "^1.6.6", + "@tanstack/react-virtual": "^3.0.0-alpha.0", "react": "18.2.0", - "react-dom": "18.2.0" + "react-bootstrap": "^2.7.2", + "react-dom": "18.2.0", + "react-icons": "^4.8.0", + "react-slider": "^2.0.4", + "react-youtube": "^10.1.0", + "use-between": "^1.3.5" }, "devDependencies": { "@nrwl/cypress": "15.8.6", diff --git a/tsconfig.base.json b/tsconfig.base.json index 11253ac..c0e83f6 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -14,7 +14,9 @@ "skipLibCheck": true, "skipDefaultLibCheck": true, "baseUrl": ".", - "paths": {} + "paths": { + "@nitro/renderer": ["libs/renderer/src/index.ts"] + } }, "exclude": ["node_modules", "tmp"] }