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 0000000..634eb06 Binary files /dev/null and b/apps/frontend/public/android-chrome-192x192.png differ diff --git a/apps/frontend/public/android-chrome-512x512.png b/apps/frontend/public/android-chrome-512x512.png new file mode 100644 index 0000000..33cf6c6 Binary files /dev/null and b/apps/frontend/public/android-chrome-512x512.png differ diff --git a/apps/frontend/public/apple-touch-icon.png b/apps/frontend/public/apple-touch-icon.png new file mode 100644 index 0000000..349b3dd Binary files /dev/null and b/apps/frontend/public/apple-touch-icon.png differ 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 0000000..788a7f1 Binary files /dev/null and b/apps/frontend/public/favicon-16x16.png differ diff --git a/apps/frontend/public/favicon-32x32.png b/apps/frontend/public/favicon-32x32.png new file mode 100644 index 0000000..ef4a606 Binary files /dev/null and b/apps/frontend/public/favicon-32x32.png differ diff --git a/apps/frontend/public/favicon.ico b/apps/frontend/public/favicon.ico index 317ebcb..34ba1b3 100644 Binary files a/apps/frontend/public/favicon.ico and b/apps/frontend/public/favicon.ico differ diff --git a/apps/frontend/public/mstile-150x150.png b/apps/frontend/public/mstile-150x150.png new file mode 100644 index 0000000..3a555aa Binary files /dev/null and b/apps/frontend/public/mstile-150x150.png differ 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 0000000..211634f Binary files /dev/null and b/apps/frontend/src/assets/images/notifications/frank.gif differ diff --git a/apps/frontend/src/assets/images/pets/pet-package/gnome.png b/apps/frontend/src/assets/images/pets/pet-package/gnome.png new file mode 100644 index 0000000..2c38828 Binary files /dev/null and b/apps/frontend/src/assets/images/pets/pet-package/gnome.png differ diff --git a/apps/frontend/src/assets/images/pets/pet-package/leprechaun_box.png b/apps/frontend/src/assets/images/pets/pet-package/leprechaun_box.png new file mode 100644 index 0000000..1603eb8 Binary files /dev/null and b/apps/frontend/src/assets/images/pets/pet-package/leprechaun_box.png differ diff --git a/apps/frontend/src/assets/images/pets/pet-package/petbox_epic.png b/apps/frontend/src/assets/images/pets/pet-package/petbox_epic.png new file mode 100644 index 0000000..e09ad77 Binary files /dev/null and b/apps/frontend/src/assets/images/pets/pet-package/petbox_epic.png differ 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 0000000..43ee141 Binary files /dev/null and b/apps/frontend/src/assets/images/pets/pet-package/pterosaur_egg.png differ 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 0000000..3d371b5 Binary files /dev/null and b/apps/frontend/src/assets/images/pets/pet-package/val11_present.png differ diff --git a/apps/frontend/src/assets/images/pets/pet-package/velociraptor_egg.png b/apps/frontend/src/assets/images/pets/pet-package/velociraptor_egg.png new file mode 100644 index 0000000..242f0df Binary files /dev/null and b/apps/frontend/src/assets/images/pets/pet-package/velociraptor_egg.png differ 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 0000000..ec9c030 Binary files /dev/null and b/apps/frontend/src/assets/images/prize/prize_background.png differ diff --git a/apps/frontend/src/assets/images/profile/icons/offline.png b/apps/frontend/src/assets/images/profile/icons/offline.png new file mode 100644 index 0000000..677aadc Binary files /dev/null and b/apps/frontend/src/assets/images/profile/icons/offline.png differ 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 0000000..3a79838 Binary files /dev/null and b/apps/frontend/src/assets/images/profile/icons/online.gif differ 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 0000000..ec8c52f Binary files /dev/null and b/apps/frontend/src/assets/images/profile/icons/tick.png differ 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 0000000..01688cb Binary files /dev/null and b/apps/frontend/src/assets/images/room-spectator/room_spectator_bottom_left.png differ 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 0000000..59c8ef2 Binary files /dev/null and b/apps/frontend/src/assets/images/room-spectator/room_spectator_bottom_right.png differ 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 0000000..ba6fdec Binary files /dev/null and b/apps/frontend/src/assets/images/room-spectator/room_spectator_middle_bottom.png differ 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 0000000..6d9aaa7 Binary files /dev/null and b/apps/frontend/src/assets/images/room-spectator/room_spectator_middle_left.png differ 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 0000000..9d963b3 Binary files /dev/null and b/apps/frontend/src/assets/images/room-spectator/room_spectator_middle_right.png differ 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 0000000..f6559ce Binary files /dev/null and b/apps/frontend/src/assets/images/room-spectator/room_spectator_middle_top.png differ diff --git a/apps/frontend/src/assets/images/room-spectator/room_spectator_top_left.png b/apps/frontend/src/assets/images/room-spectator/room_spectator_top_left.png new file mode 100644 index 0000000..5e62a3c Binary files /dev/null and b/apps/frontend/src/assets/images/room-spectator/room_spectator_top_left.png differ diff --git a/apps/frontend/src/assets/images/room-spectator/room_spectator_top_right.png b/apps/frontend/src/assets/images/room-spectator/room_spectator_top_right.png new file mode 100644 index 0000000..825f3fb Binary files /dev/null and b/apps/frontend/src/assets/images/room-spectator/room_spectator_top_right.png differ 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 0000000..dea4f08 Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/avatar-info/preview-background.png differ diff --git a/apps/frontend/src/assets/images/room-widgets/camera-widget/btn.png b/apps/frontend/src/assets/images/room-widgets/camera-widget/btn.png new file mode 100644 index 0000000..76b086b Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/camera-widget/btn.png differ 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 0000000..76f25da Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/camera-widget/btn_down.png differ diff --git a/apps/frontend/src/assets/images/room-widgets/camera-widget/btn_hi.png b/apps/frontend/src/assets/images/room-widgets/camera-widget/btn_hi.png new file mode 100644 index 0000000..5f04fc0 Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/camera-widget/btn_hi.png differ 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 0000000..d6cf994 Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/camera-widget/cam_bg.png differ diff --git a/apps/frontend/src/assets/images/room-widgets/camera-widget/camera-spritesheet.png b/apps/frontend/src/assets/images/room-widgets/camera-widget/camera-spritesheet.png new file mode 100644 index 0000000..4ea82e3 Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/camera-widget/camera-spritesheet.png differ 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 0000000..ab6a9b2 Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/camera-widget/viewfinder.png differ 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 0000000..fdc6e9f Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/dimmer-widget/dimmer_banner.png differ diff --git a/apps/frontend/src/assets/images/room-widgets/engraving-lock-widget/engraving-lock-spritesheet.png b/apps/frontend/src/assets/images/room-widgets/engraving-lock-widget/engraving-lock-spritesheet.png new file mode 100644 index 0000000..472dc85 Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/engraving-lock-widget/engraving-lock-spritesheet.png differ 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 0000000..eef5da6 Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/exchange-credit/exchange-credit-image.png differ 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 0000000..8d3d771 Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/furni-context-menu/monsterplant-preview.png differ 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 0000000..45e11f3 Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/mannequin-widget/mannequin-spritesheet.png differ 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 0000000..3033020 Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/playlist-editor/disk_2.png differ diff --git a/apps/frontend/src/assets/images/room-widgets/playlist-editor/disk_image.png b/apps/frontend/src/assets/images/room-widgets/playlist-editor/disk_image.png new file mode 100644 index 0000000..7a8ab45 Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/playlist-editor/disk_image.png differ 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 0000000..9d1635d Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/playlist-editor/move.png differ 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 0000000..900f99b Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/playlist-editor/pause-btn.png differ 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 0000000..ec5fef4 Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/playlist-editor/pause.png differ 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 0000000..0e3449d Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/playlist-editor/playing.png differ diff --git a/apps/frontend/src/assets/images/room-widgets/playlist-editor/preview.png b/apps/frontend/src/assets/images/room-widgets/playlist-editor/preview.png new file mode 100644 index 0000000..160f0be Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/playlist-editor/preview.png differ 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 0000000..9a14182 Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-blue.png differ 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 0000000..82b4732 Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-christmas.png differ diff --git a/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-close.png b/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-close.png new file mode 100644 index 0000000..9621c56 Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-close.png differ diff --git a/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-dreams.png b/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-dreams.png new file mode 100644 index 0000000..1723bdc Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-dreams.png differ 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 0000000..5e73c74 Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-green.png differ 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 0000000..4552385 Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-heart.png differ diff --git a/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-juninas.png b/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-juninas.png new file mode 100644 index 0000000..faaea9d Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-juninas.png differ diff --git a/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-pink.png b/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-pink.png new file mode 100644 index 0000000..7565899 Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-pink.png differ 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 0000000..d5011c7 Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-shakesp.png differ 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 0000000..0249571 Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-spritesheet.png differ diff --git a/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-trash.png b/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-trash.png new file mode 100644 index 0000000..96dff8f Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-trash.png differ diff --git a/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-yellow.png b/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-yellow.png new file mode 100644 index 0000000..759d3f9 Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/stickie-widget/stickie-yellow.png differ 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 0000000..63a9397 Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/thumbnail-widget/thumbnail-camera-spritesheet.png differ 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 0000000..f9184cb Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/trophy-widget/trophy-spritesheet.png differ 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 0000000..78e51cf Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/wordquiz-widget/thumbs-down-small.png differ 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 0000000..fd320c5 Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/wordquiz-widget/thumbs-down.png differ 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 0000000..b93111f Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/wordquiz-widget/thumbs-up-small.png differ diff --git a/apps/frontend/src/assets/images/room-widgets/wordquiz-widget/thumbs-up.png b/apps/frontend/src/assets/images/room-widgets/wordquiz-widget/thumbs-up.png new file mode 100644 index 0000000..dd65098 Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/wordquiz-widget/thumbs-up.png differ 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 0000000..a02e164 Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/youtube-widget/next.png differ 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 0000000..d48b658 Binary files /dev/null and b/apps/frontend/src/assets/images/room-widgets/youtube-widget/prev.png differ diff --git a/apps/frontend/src/assets/images/stackhelper/slider-background.png b/apps/frontend/src/assets/images/stackhelper/slider-background.png new file mode 100644 index 0000000..20ab191 Binary files /dev/null and b/apps/frontend/src/assets/images/stackhelper/slider-background.png differ diff --git a/apps/frontend/src/assets/images/stackhelper/slider-pointer.png b/apps/frontend/src/assets/images/stackhelper/slider-pointer.png new file mode 100644 index 0000000..8787456 Binary files /dev/null and b/apps/frontend/src/assets/images/stackhelper/slider-pointer.png differ 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 0000000..bf04ea0 Binary files /dev/null and b/apps/frontend/src/assets/images/toolbar/arrow.png differ 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 0000000..7156c4f Binary files /dev/null and b/apps/frontend/src/assets/images/toolbar/friend-search.png differ 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 0000000..bbf6d68 Binary files /dev/null and b/apps/frontend/src/assets/images/toolbar/icons/buildersclub.png differ diff --git a/apps/frontend/src/assets/images/toolbar/icons/camera.png b/apps/frontend/src/assets/images/toolbar/icons/camera.png new file mode 100644 index 0000000..da5d835 Binary files /dev/null and b/apps/frontend/src/assets/images/toolbar/icons/camera.png differ 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 0000000..f680921 Binary files /dev/null and b/apps/frontend/src/assets/images/toolbar/icons/catalog.png differ 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 0000000..b2ca0d7 Binary files /dev/null and b/apps/frontend/src/assets/images/toolbar/icons/friend_all.png differ diff --git a/apps/frontend/src/assets/images/toolbar/icons/friend_head.png b/apps/frontend/src/assets/images/toolbar/icons/friend_head.png new file mode 100644 index 0000000..6380c90 Binary files /dev/null and b/apps/frontend/src/assets/images/toolbar/icons/friend_head.png differ diff --git a/apps/frontend/src/assets/images/toolbar/icons/friend_search.png b/apps/frontend/src/assets/images/toolbar/icons/friend_search.png new file mode 100644 index 0000000..ebe1c65 Binary files /dev/null and b/apps/frontend/src/assets/images/toolbar/icons/friend_search.png differ diff --git a/apps/frontend/src/assets/images/toolbar/icons/game.png b/apps/frontend/src/assets/images/toolbar/icons/game.png new file mode 100644 index 0000000..59ef8aa Binary files /dev/null and b/apps/frontend/src/assets/images/toolbar/icons/game.png differ diff --git a/apps/frontend/src/assets/images/toolbar/icons/habbo.png b/apps/frontend/src/assets/images/toolbar/icons/habbo.png new file mode 100644 index 0000000..78cd0a4 Binary files /dev/null and b/apps/frontend/src/assets/images/toolbar/icons/habbo.png differ 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 0000000..f2c8746 Binary files /dev/null and b/apps/frontend/src/assets/images/toolbar/icons/house.png differ 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 0000000..d848586 Binary files /dev/null and b/apps/frontend/src/assets/images/toolbar/icons/inventory.png differ 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 0000000..894ee78 Binary files /dev/null and b/apps/frontend/src/assets/images/toolbar/icons/joinroom.png differ 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 0000000..575464d Binary files /dev/null and b/apps/frontend/src/assets/images/toolbar/icons/me-menu/achievements.png differ 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 0000000..bfacabd Binary files /dev/null and b/apps/frontend/src/assets/images/toolbar/icons/me-menu/clothing.png differ diff --git a/apps/frontend/src/assets/images/toolbar/icons/me-menu/cog.png b/apps/frontend/src/assets/images/toolbar/icons/me-menu/cog.png new file mode 100644 index 0000000..6180409 Binary files /dev/null and b/apps/frontend/src/assets/images/toolbar/icons/me-menu/cog.png differ 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 0000000..e22426e Binary files /dev/null and b/apps/frontend/src/assets/images/toolbar/icons/me-menu/forums.png differ 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 0000000..e324611 Binary files /dev/null and b/apps/frontend/src/assets/images/toolbar/icons/me-menu/helper-tool.png differ diff --git a/apps/frontend/src/assets/images/toolbar/icons/me-menu/my-rooms.png b/apps/frontend/src/assets/images/toolbar/icons/me-menu/my-rooms.png new file mode 100644 index 0000000..8d4dcad Binary files /dev/null and b/apps/frontend/src/assets/images/toolbar/icons/me-menu/my-rooms.png differ 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 0000000..04964bf Binary files /dev/null and b/apps/frontend/src/assets/images/toolbar/icons/me-menu/profile.png differ diff --git a/apps/frontend/src/assets/images/toolbar/icons/me-menu/rooms.png b/apps/frontend/src/assets/images/toolbar/icons/me-menu/rooms.png new file mode 100644 index 0000000..00261ce Binary files /dev/null and b/apps/frontend/src/assets/images/toolbar/icons/me-menu/rooms.png differ diff --git a/apps/frontend/src/assets/images/toolbar/icons/me-menu/talents.png b/apps/frontend/src/assets/images/toolbar/icons/me-menu/talents.png new file mode 100644 index 0000000..2f91dfe Binary files /dev/null and b/apps/frontend/src/assets/images/toolbar/icons/me-menu/talents.png differ 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 0000000..c12d5bb Binary files /dev/null and b/apps/frontend/src/assets/images/toolbar/icons/message.png differ 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 0000000..eddfe1c Binary files /dev/null and b/apps/frontend/src/assets/images/toolbar/icons/message_unsee.gif differ diff --git a/apps/frontend/src/assets/images/toolbar/icons/modtools.png b/apps/frontend/src/assets/images/toolbar/icons/modtools.png new file mode 100644 index 0000000..24c362f Binary files /dev/null and b/apps/frontend/src/assets/images/toolbar/icons/modtools.png differ 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 0000000..00261ce Binary files /dev/null and b/apps/frontend/src/assets/images/toolbar/icons/rooms.png differ diff --git a/apps/frontend/src/assets/images/toolbar/icons/sendmessage.png b/apps/frontend/src/assets/images/toolbar/icons/sendmessage.png new file mode 100644 index 0000000..9f64b17 Binary files /dev/null and b/apps/frontend/src/assets/images/toolbar/icons/sendmessage.png differ 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 0000000..4a56c9b Binary files /dev/null and b/apps/frontend/src/assets/images/unique/catalog-info-amount-bg.png differ 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 0000000..79626e1 Binary files /dev/null and b/apps/frontend/src/assets/images/unique/catalog-info-sold-out.png differ 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 0000000..5b64c48 Binary files /dev/null and b/apps/frontend/src/assets/images/unique/grid-bg-glass.png differ 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 0000000..94f6662 Binary files /dev/null and b/apps/frontend/src/assets/images/unique/grid-bg-sold-out.png differ diff --git a/apps/frontend/src/assets/images/unique/grid-bg.png b/apps/frontend/src/assets/images/unique/grid-bg.png new file mode 100644 index 0000000..d7737ba Binary files /dev/null and b/apps/frontend/src/assets/images/unique/grid-bg.png differ 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 0000000..68e13bd Binary files /dev/null and b/apps/frontend/src/assets/images/unique/grid-count-bg.png differ 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 0000000..af4e31e Binary files /dev/null and b/apps/frontend/src/assets/images/unique/inventory-info-amount-bg.png differ 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 0000000..e1ece79 Binary files /dev/null and b/apps/frontend/src/assets/images/unique/numbers.png differ diff --git a/apps/frontend/src/assets/images/wired/card-action-corners.png b/apps/frontend/src/assets/images/wired/card-action-corners.png new file mode 100644 index 0000000..faec234 Binary files /dev/null and b/apps/frontend/src/assets/images/wired/card-action-corners.png differ 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 0000000..78e90e6 Binary files /dev/null and b/apps/frontend/src/assets/images/wired/icon_action.png differ 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 0000000..26925a6 Binary files /dev/null and b/apps/frontend/src/assets/images/wired/icon_condition.png differ 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 0000000..f48d13c Binary files /dev/null and b/apps/frontend/src/assets/images/wired/icon_trigger.png differ 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 0000000..0b4b5a1 Binary files /dev/null and b/apps/frontend/src/assets/images/wired/icon_wired_around.png differ 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 0000000..862d6d8 Binary files /dev/null and b/apps/frontend/src/assets/images/wired/icon_wired_left_right.png differ 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 0000000..3710854 Binary files /dev/null and b/apps/frontend/src/assets/images/wired/icon_wired_north_east.png differ 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 0000000..09eeefc Binary files /dev/null and b/apps/frontend/src/assets/images/wired/icon_wired_north_west.png differ 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 0000000..2827e3d Binary files /dev/null and b/apps/frontend/src/assets/images/wired/icon_wired_rotate_clockwise.png differ 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 0000000..7e281ba Binary files /dev/null and b/apps/frontend/src/assets/images/wired/icon_wired_rotate_counter_clockwise.png differ 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 0000000..4217c4b Binary files /dev/null and b/apps/frontend/src/assets/images/wired/icon_wired_south_east.png differ diff --git a/apps/frontend/src/assets/images/wired/icon_wired_south_west.png b/apps/frontend/src/assets/images/wired/icon_wired_south_west.png new file mode 100644 index 0000000..07ab1f9 Binary files /dev/null and b/apps/frontend/src/assets/images/wired/icon_wired_south_west.png differ 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 0000000..c2d243b Binary files /dev/null and b/apps/frontend/src/assets/images/wired/icon_wired_up_down.png differ 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 +// +// `
+ +
+
+ + ); +}; 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"] }