Merge branch 'dev' into feature/guide-tool

This commit is contained in:
Bill 2022-02-22 21:54:44 -05:00
commit d359862e1c
910 changed files with 30336 additions and 32820 deletions

2
.env
View File

@ -1,2 +0,0 @@
BROWSER=none
GENERATE_SOURCEMAP=false

7
.env.example Normal file
View File

@ -0,0 +1,7 @@
BROWSER=none
GENERATE_SOURCEMAP=false
REACT_APP_CONFIG_URLS=/renderer-config.json,/ui-config.json
REACT_APP_SOCKET_URL=wss://ws.server.com:2096
REACT_APP_ASSET_URL=https://nitro.server.com
REACT_APP_IMAGE_LIBRARY_URL=https://swf.server.com/c_images/
REACT_APP_HOF_FURNI_URL=https://swf.server.com/dcr/hof_furni

34
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,34 @@
name: Build
on:
push:
branches: [dev]
jobs:
build:
runs-on: dedicated-server
strategy:
matrix:
node-version: [16.x]
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
cache: "yarn"
- name: Install dependencies
run: |
yarn remove @nitrots/nitro-renderer
yarn add git+https://git@git.krews.org/nitro/nitro-renderer#dev
yarn install
- name: Build Nitro
run: |
yarn build
- name: Archive Artifacts
uses: actions/upload-artifact@v2
with:
path: |
build

View File

@ -1,49 +0,0 @@
name: Deploy Bundle@dev
on:
push:
branches: [ dev ]
jobs:
build:
runs-on: self-hosted
strategy:
matrix:
node-version: [14.x]
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Cache dependencies
uses: actions/cache@v2
with:
path: '~/.npm'
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies
run: |
npm uninstall @nitrots/nitro-renderer
npm install git+https://git@git.krews.org/nitro/nitro-renderer#dev
- name: Build Nitro
run: |
npm run build
- name: Archive Artifacts
uses: actions/upload-artifact@v2
with:
path: |
build
- name: Deploy Artifacts
uses: easingthemes/ssh-deploy@main
env:
REMOTE_HOST: ${{ secrets.HOST }}
REMOTE_PORT: ${{ secrets.PORT }}
REMOTE_USER: ${{ secrets.USERNAME }}
SSH_PRIVATE_KEY: ${{ secrets.SSHKEY }}
SOURCE: "build/"
TARGET: ${{ secrets.TARGET }}

29
.gitignore vendored
View File

@ -1,20 +1,7 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
# Only exists if Bazel was run
/bazel-out
# dependencies
/node_modules
# profiling files
chrome-profiler-events*.json
speed-measure-plugin*.json
# IDEs and editors
/.idea
.project
.classpath
@ -22,29 +9,23 @@ speed-measure-plugin*.json
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
*.log
.git
# System Files
.DS_Store
Thumbs.db
# Nitro
/build
*.zip
.env
public/renderer-config.json
public/ui-config.json

1
.prettierignore Normal file
View File

@ -0,0 +1 @@
*.scss

19384
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,16 +5,19 @@
"scripts": {
"start": "craco start",
"build": "craco --max_old_space_size=8048 build",
"build:prod": "npm uninstall @nitrots/nitro-renderer && npm i git+https://git@git.krews.org/nitro/nitro-renderer#dev && npm i && npm run build",
"build:prod": "npx browserslist@latest --update-db && yarn install && yarn build",
"test": "craco test",
"eject": "react-scripts eject"
},
"dependencies": {
"@craco/craco": "^6.3.0",
"@nitrots/nitro-renderer": "file:../nitro-renderer",
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/react-fontawesome": "^0.1.16",
"@nitrots/nitro-renderer": "^1.1.4",
"animate.css": "^4.1.1",
"classnames": "^2.3.1",
"node-sass": "^5.0.0",
"node-sass": "^6.0.1",
"react": "^17.0.2",
"react-bootstrap": "^2.0.0-alpha.2",
"react-dom": "^17.0.2",
@ -23,20 +26,16 @@
"react-transition-group": "^4.4.2",
"react-virtualized": "^9.22.3",
"react-youtube": "^7.13.1",
"typescript": "^4.3.5",
"web-vitals": "^1.1.2"
"typescript": "^4.3.5"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^12.8.3",
"@types/jest": "^26.0.24",
"@types/node": "^12.20.19",
"@types/react": "^17.0.15",
"@types/react-dom": "^17.0.9",
"@types/react-slider": "^1.3.1",
"@types/react-transition-group": "^4.4.2",
"@types/react-virtualized": "^9.21.13",
"@types/styled-components": "^5.1.15",
"@typescript-eslint/eslint-plugin": "^4.29.1"
}
}

34
post-install.js Normal file
View File

@ -0,0 +1,34 @@
import { request as httpsRequest } from 'https';
function install()
{
try
{
const params = {};
params['packageName'] = process.env.npm_package_name;
params['packageVersion'] = process.env.npm_package_version;
const data = JSON.stringify(params);
const request = httpsRequest({
hostname: 'install.nitrots.co',
port: 443,
path: '/collect',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': data.length
}
});
request.write(data);
request.end();
}
catch (e)
{
//
}
}
install();

View File

@ -13,9 +13,13 @@
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root" class="w-100 h-100"></div>
<script>
var NitroConfig = {
configurationUrls: [ '/renderer-config.json', '/ui-config.json' ],
sso: (new URLSearchParams(window.location.search).get('sso') || null)
const NitroConfig = {
"config.urls": ("%REACT_APP_CONFIG_URLS%").split(','),
"sso.ticket": (new URLSearchParams(window.location.search).get('sso') || null),
"socket.url": "%REACT_APP_SOCKET_URL%",
"asset.url": "%REACT_APP_ASSET_URL%",
"image.library.url": "%REACT_APP_IMAGE_LIBRARY_URL%",
"hof.furni.url": "%REACT_APP_HOF_FURNI_URL%"
};
</script>
</body>

View File

@ -1,8 +1,8 @@
{
"socket.url": "wss://ws.nitrots.co:2096",
"asset.url": "https://nitro.nitrots.co",
"image.library.url": "https://swf.nitrots.co/c_images/",
"hof.furni.url": "https://swf.nitrots.co/dcr/hof_furni",
"socket.url": "",
"asset.url": "",
"image.library.url": "",
"hof.furni.url": "",
"images.url": "${asset.url}/images",
"gamedata.url": "${asset.url}/gamedata",
"sounds.url": "${asset.url}/sounds/%sample%.mp3",
@ -21,9 +21,7 @@
"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",
"badge.asset.group.url": "https://cdn.ironhotel.biz/group-badge/%badgedata%.gif",
"badge.asset.group.external.url": "",
"badge.asset.grouparts.url": "https://cdn.ironhotel.biz/static_iron_active/c_images/Badgeparts/badgepart_%part%.png",
"badge.asset.grouparts.url": "${image.library.url}Badgeparts/badgepart_%part%.png",
"furni.rotation.bounce.steps": 20,
"furni.rotation.bounce.height": 0.0625,
"enable.avatar.arrow": false,
@ -101,20 +99,8 @@
"elephants"
],
"preload.assets.urls": [
"${images.url}/additions/user_blowkiss.png",
"${images.url}/additions/user_idle_left_1.png",
"${images.url}/additions/user_idle_left_2.png",
"${images.url}/additions/user_idle_right_1.png",
"${images.url}/additions/user_idle_right_2.png",
"${images.url}/additions/user_muted.png",
"${images.url}/additions/user_muted_small.png",
"${images.url}/additions/user_typing.png",
"${images.url}/additions/number_1.png",
"${images.url}/additions/number_2.png",
"${images.url}/additions/number_3.png",
"${images.url}/additions/number_4.png",
"${images.url}/additions/number_5.png",
"${images.url}/additions/pet_experience_bubble.png",
"${asset.url}/bundled/generic/avatar_additions.nitro",
"${asset.url}/bundled/generic/floor_editor.nitro",
"${images.url}/loading_icon.png",
"${images.url}/clear_icon.png",
"${images.url}/big_arrow.png"

View File

@ -1,15 +1,16 @@
{
"image.library.notifications.url": "${image.library.url}notifications/%image%.png",
"achievements.images.url": "${image.library.url}Quests/%image%.png",
"camera.url": "https://nitro.nitrots.co/camera",
"thumbnails.url": "https://nitro.nitrots.co/camera/thumbnail/%thumbnail%.png",
"url.prefix": "http://localhost:3000",
"camera.url": "https://camera.com",
"thumbnails.url": "https://camera.com/thumbnail/%thumbnail%.png",
"url.prefix": "https://website.com",
"floorplan.tile.url": "${asset.url}/floorplan-editor/tiles.json",
"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,
"hotelview": {
"widgets": {
"slot.1.widget": "promoarticle",
@ -46,9 +47,17 @@
"system.currency.types": [
-1,
0,
5,
101
5
],
"hc.center": {
"benefits.info": true,
"payday.info": true,
"gift.info": true,
"benefits.habbopage": "habboclub",
"payday.habbopage": "hcpayday",
"catalog.buy": "habbo_club",
"catalog.gifts": "club_gifts"
},
"currency.display.number.short": false,
"currency.asset.icon.url": "${images.url}/wallet/%type%.png",
"catalog.asset.url": "${image.library.url}catalogue",

View File

@ -17,8 +17,8 @@ $grid-active-border-color: $white;
$toolbar-height: 55px;
$achievement-width: 350px;
$achievement-height: 370px;
$achievement-width: 375px;
$achievement-height: 425px;
$avatar-editor-width: 620px;
$avatar-editor-height: 374px;
@ -50,16 +50,33 @@ $chat-history-height: 300px;
$friends-list-width: 250px;
$friends-list-height: 300px;
$help-width: 275px;
$help-height: 450px;
$help-width: 450px;
$help-height: 250px;
$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-mod-tools-width: 175px;
.nitro-app {
width: 100%;
height: 100%;
}
@import './common';
@import "./layout/Layout";
@import './components';
@import "./views/Styles";

View File

@ -1,13 +1,14 @@
import { ConfigurationEvent, LegacyExternalInterface, Nitro, NitroCommunicationDemoEvent, NitroEvent, NitroLocalizationEvent, NitroVersion, RoomEngineEvent, WebGL } from '@nitrots/nitro-renderer';
import { ConfigurationEvent, HabboWebTools, LegacyExternalInterface, Nitro, NitroCommunicationDemoEvent, NitroEvent, NitroLocalizationEvent, NitroVersion, RoomEngineEvent, WebGL } from '@nitrots/nitro-renderer';
import { FC, useCallback, useState } from 'react';
import { GetCommunication, GetConfiguration, GetNitroInstance } from './api';
import { Base } from './common';
import { LoadingView } from './components/loading/LoadingView';
import { MainView } from './components/main/MainView';
import { useConfigurationEvent } from './hooks/events/core/configuration/configuration-event';
import { useLocalizationEvent } from './hooks/events/nitro/localization/localization-event';
import { dispatchMainEvent, useMainEvent } from './hooks/events/nitro/main-event';
import { useRoomEngineEvent } from './hooks/events/nitro/room/room-engine-event';
import { TransitionAnimation, TransitionAnimationTypes } from './layout';
import { LoadingView } from './views/loading/LoadingView';
import { MainView } from './views/main/MainView';
export const App: FC<{}> = props =>
{
@ -68,6 +69,8 @@ export const App: FC<{}> = props =>
setMessage('Finishing Up');
GetNitroInstance().init();
if(LegacyExternalInterface.available) LegacyExternalInterface.call('legacyTrack', 'authentication', 'authok', []);
return;
case NitroCommunicationDemoEvent.CONNECTION_ERROR:
setIsError(true);
@ -79,7 +82,7 @@ export const App: FC<{}> = props =>
setIsError(true);
setMessage('Connection Error');
LegacyExternalInterface.call('disconnect', -1, 'client.init.handshake.fail');
HabboWebTools.send(-1, 'client.init.handshake.fail');
return;
case RoomEngineEvent.ENGINE_INITIALIZED:
setIsReady(true);
@ -125,12 +128,13 @@ export const App: FC<{}> = props =>
}
return (
<div className="nitro-app overflow-hidden">
<div id="nitro-alerts-container" />
{ (!isReady || isError) && <LoadingView isError={ isError } message={ message } /> }
<Base fit overflow="hidden">
{ (!isReady || isError) &&
<LoadingView isError={ isError } message={ message } /> }
<TransitionAnimation type={ TransitionAnimationTypes.FADE_IN } inProp={ (isReady && !isError) }>
<MainView />
</TransitionAnimation>
</div>
<Base id="draggable-windows-container" />
</Base>
);
}

View File

@ -2,5 +2,7 @@ import { GetNitroInstance } from './GetNitroInstance';
export function CreateLinkEvent(link: string): void
{
link = (link.startsWith('event:') ? link.substring(6) : link);
GetNitroInstance().createLinkEvent(link);
}

View File

@ -1,7 +1,7 @@
import { IFurnitureData, IGetImageListener, NitroEvent, NitroRenderTexture, PetFigureData, RoomObjectCategory, RoomObjectVariable, RoomSessionPresentEvent, RoomWidgetEnum, TextureUtils, Vector3d } from '@nitrots/nitro-renderer';
import { GetSessionDataManager, IsOwnerOfFurniture } from '../../..';
import { GetRoomEngine, LocalizeText } from '../../../..';
import { ProductTypeEnum } from '../../../../../views/catalog/common/ProductTypeEnum';
import { ProductTypeEnum } from '../../../../../components/catalog/common/ProductTypeEnum';
import { RoomWidgetUpdateEvent, RoomWidgetUpdatePresentDataEvent } from '../events';
import { RoomWidgetFurniToWidgetMessage, RoomWidgetPresentOpenMessage } from '../messages';
import { RoomWidgetMessage } from '../messages/RoomWidgetMessage';

View File

@ -1,6 +1,6 @@
import { NitroEvent, RoomEngineUseProductEvent, RoomObjectCategory, RoomObjectType, RoomObjectVariable, RoomSessionDanceEvent, RoomSessionUserDataUpdateEvent, RoomWidgetEnum } from '@nitrots/nitro-renderer';
import { GetRoomEngine, GetRoomSession, GetSessionDataManager, IsOwnerOfFurniture } from '../../../..';
import { FurniCategory } from '../../../../../views/inventory/common/FurniCategory';
import { NitroEvent, RoomEngineUseProductEvent, RoomObjectCategory, RoomObjectType, RoomObjectVariable, RoomSessionDanceEvent, RoomSessionPetStatusUpdateEvent, RoomSessionUserDataUpdateEvent, RoomWidgetEnum } from '@nitrots/nitro-renderer';
import { GetRoomEngine, GetSessionDataManager, IsOwnerOfFurniture } from '../../../..';
import { FurniCategory } from '../../../../../components/inventory/common/FurniCategory';
import { RoomWidgetAvatarInfoEvent, RoomWidgetUpdateDanceStatusEvent, RoomWidgetUpdateEvent, RoomWidgetUpdateUserDataEvent, RoomWidgetUseProductBubbleEvent, UseProductItem } from '../events';
import { RoomWidgetAvatarExpressionMessage, RoomWidgetChangePostureMessage, RoomWidgetDanceMessage, RoomWidgetMessage, RoomWidgetRoomObjectMessage, RoomWidgetUseProductMessage, RoomWidgetUserActionMessage } from '../messages';
import { RoomWidgetHandler } from './RoomWidgetHandler';
@ -19,7 +19,7 @@ export class RoomWidgetAvatarInfoHandler extends RoomWidgetHandler
let isDancing = false;
const userData = GetRoomSession().userDataManager.getUserData(GetSessionDataManager().userId);
const userData = this.container.roomSession.userDataManager.getUserData(GetSessionDataManager().userId);
if(userData && (userData.roomIndex === danceEvent.roomIndex)) isDancing = (danceEvent.danceId !== 0);
@ -30,6 +30,9 @@ export class RoomWidgetAvatarInfoHandler extends RoomWidgetHandler
case RoomEngineUseProductEvent.USE_PRODUCT_FROM_ROOM:
this.processUsableRoomObject((event as RoomEngineUseProductEvent).objectId);
return;
case RoomSessionPetStatusUpdateEvent.PET_STATUS_UPDATE:
this.processRoomSessionPetStatusUpdateEvent((event as RoomSessionPetStatusUpdateEvent));
return;
}
}
@ -44,28 +47,39 @@ export class RoomWidgetAvatarInfoHandler extends RoomWidgetHandler
case RoomWidgetRoomObjectMessage.GET_OWN_CHARACTER_INFO:
this.processOwnCharacterInfo();
break;
case RoomWidgetUserActionMessage.START_NAME_CHANGE:
// habbo help - start name change
break;
case RoomWidgetUserActionMessage.REQUEST_PET_UPDATE:
break;
case RoomWidgetUseProductMessage.PET_PRODUCT: {
const productMessage = (message as RoomWidgetUseProductMessage);
this.container.roomSession.usePetProduct(productMessage.objectId, productMessage.petId);
break;
}
case RoomWidgetUserActionMessage.HARVEST_PET:
this.container.roomSession.harvestPet(userId);
break;
case RoomWidgetUserActionMessage.COMPOST_PLANT:
this.container.roomSession.compostPlant(userId);
break;
case RoomWidgetDanceMessage.DANCE: {
const danceMessage = (message as RoomWidgetDanceMessage);
GetRoomSession().sendDanceMessage(danceMessage.style);
this.container.roomSession.sendDanceMessage(danceMessage.style);
break;
}
case RoomWidgetAvatarExpressionMessage.AVATAR_EXPRESSION: {
const expressionMessage = (message as RoomWidgetAvatarExpressionMessage);
GetRoomSession().sendExpressionMessage(expressionMessage.animation.ordinal)
this.container.roomSession.sendExpressionMessage(expressionMessage.animation.ordinal)
break;
}
case RoomWidgetChangePostureMessage.CHANGE_POSTURE: {
const postureMessage = (message as RoomWidgetChangePostureMessage);
GetRoomSession().sendPostureMessage(postureMessage.posture);
break;
}
case RoomWidgetUseProductMessage.PET_PRODUCT: {
const productMessage = (message as RoomWidgetUseProductMessage);
GetRoomSession().usePetProduct(productMessage.objectId, productMessage.petId);
this.container.roomSession.sendPostureMessage(postureMessage.posture);
break;
}
}
@ -78,9 +92,11 @@ export class RoomWidgetAvatarInfoHandler extends RoomWidgetHandler
const userId = GetSessionDataManager().userId;
const userName = GetSessionDataManager().userName;
const allowNameChange = GetSessionDataManager().canChangeName;
const userData = GetRoomSession().userDataManager.getUserData(userId);
const userData = this.container.roomSession.userDataManager.getUserData(userId);
if(userData) this.container.eventDispatcher.dispatchEvent(new RoomWidgetAvatarInfoEvent(userId, userName, userData.type, userData.roomIndex, allowNameChange));
if(!userData) return;
this.container.eventDispatcher.dispatchEvent(new RoomWidgetAvatarInfoEvent(userId, userName, userData.type, userData.roomIndex, allowNameChange));
}
private processUsableRoomObject(objectId: number): void
@ -120,24 +136,24 @@ export class RoomWidgetAvatarInfoHandler extends RoomWidgetHandler
{
if(userData.ownerId === ownerId)
{
if(userData.hasSaddle && (specialType === FurniCategory._Str_6096)) replace = true;
if(userData.hasSaddle && (specialType === FurniCategory.PET_SADDLE)) replace = true;
const figureParts = userData.figure.split(' ');
const figurePart = (figureParts.length ? parseInt(figureParts[0]) : -1);
if(figurePart === part)
{
if(specialType === FurniCategory._Str_6915)
if(specialType === FurniCategory.MONSTERPLANT_REVIVAL)
{
if(!userData.canRevive) continue;
}
if(specialType === FurniCategory._Str_8726)
if(specialType === FurniCategory.MONSTERPLANT_REBREED)
{
if((userData.petLevel < 7) || userData.canRevive || userData.canBreed) continue;
}
if(specialType === FurniCategory._Str_9449)
if(specialType === FurniCategory.MONSTERPLANT_FERTILIZE)
{
if((userData.petLevel >= 7) || userData.canRevive) continue;
}
@ -151,6 +167,11 @@ export class RoomWidgetAvatarInfoHandler extends RoomWidgetHandler
if(useProductBubbles.length) this.container.eventDispatcher.dispatchEvent(new RoomWidgetUseProductBubbleEvent(RoomWidgetUseProductBubbleEvent.USE_PRODUCT_BUBBLES, useProductBubbles));
}
private processRoomSessionPetStatusUpdateEvent(event: RoomSessionPetStatusUpdateEvent): void
{
}
public get type(): string
{
return RoomWidgetEnum.AVATAR_INFO;
@ -162,18 +183,29 @@ export class RoomWidgetAvatarInfoHandler extends RoomWidgetHandler
RoomSessionUserDataUpdateEvent.USER_DATA_UPDATED,
RoomSessionDanceEvent.RSDE_DANCE,
RoomEngineUseProductEvent.USE_PRODUCT_FROM_INVENTORY,
RoomEngineUseProductEvent.USE_PRODUCT_FROM_ROOM
RoomEngineUseProductEvent.USE_PRODUCT_FROM_ROOM,
RoomSessionPetStatusUpdateEvent.PET_STATUS_UPDATE
];
}
// UserNameUpdateEvent.UNUE_NAME_UPDATED
// RoomSessionNestBreedingSuccessEvent.RSPFUE_NEST_BREEDING_SUCCESS
// RoomSessionPetLevelUpdateEvent.RSPLUE_PET_LEVEL_UPDATE
public get messageTypes(): string[]
{
return [
RoomWidgetRoomObjectMessage.GET_OWN_CHARACTER_INFO,
RoomWidgetUserActionMessage.START_NAME_CHANGE,
RoomWidgetUserActionMessage.REQUEST_PET_UPDATE,
RoomWidgetUseProductMessage.PET_PRODUCT,
RoomWidgetUserActionMessage.REQUEST_BREED_PET,
RoomWidgetUserActionMessage.HARVEST_PET,
RoomWidgetUserActionMessage.REVIVE_PET,
RoomWidgetUserActionMessage.COMPOST_PLANT,
RoomWidgetDanceMessage.DANCE,
RoomWidgetAvatarExpressionMessage.AVATAR_EXPRESSION,
RoomWidgetChangePostureMessage.CHANGE_POSTURE,
RoomWidgetUseProductMessage.PET_PRODUCT
];
}
}

View File

@ -1,9 +1,10 @@
import { AvatarExpressionEnum, HabboClubLevelEnum, NitroEvent, RoomControllerLevel, RoomSessionChatEvent, RoomSettingsComposer, RoomWidgetEnum, RoomZoomEvent, TextureUtils } from '@nitrots/nitro-renderer';
import { AvatarExpressionEnum, HabboClubLevelEnum, NitroEvent, RoomControllerLevel, RoomRotatingEffect, RoomSessionChatEvent, RoomSettingsComposer, RoomShakingEffect, RoomWidgetEnum, RoomZoomEvent, TextureUtils } from '@nitrots/nitro-renderer';
import { GetConfiguration, GetNitroInstance } from '../../..';
import { GetRoomEngine, GetSessionDataManager } from '../../../..';
import { GetRoomEngine, GetSessionDataManager, LocalizeText } from '../../../..';
import { FloorplanEditorEvent } from '../../../../../events/floorplan-editor/FloorplanEditorEvent';
import { dispatchUiEvent } from '../../../../../hooks';
import { SendMessageHook } from '../../../../../hooks/messages';
import { NotificationUtilities } from '../../../../../views/notification-center/common/NotificationUtilities';
import { RoomWidgetFloodControlEvent, RoomWidgetUpdateEvent } from '../events';
import { RoomWidgetChatMessage, RoomWidgetChatSelectAvatarMessage, RoomWidgetChatTypingMessage, RoomWidgetMessage, RoomWidgetRequestWidgetMessage } from '../messages';
import { RoomWidgetHandler } from './RoomWidgetHandler';
@ -65,6 +66,17 @@ export class RoomWidgetChatInputHandler extends RoomWidgetHandler
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(GetSessionDataManager().clubLevel === HabboClubLevelEnum.VIP)
@ -109,6 +121,7 @@ export class RoomWidgetChatInputHandler extends RoomWidgetHandler
return null;
case ':iddqd':
case ':flip':
GetRoomEngine().events.dispatchEvent(new RoomZoomEvent(this.container.roomSession.roomId, -1, true));
return null;
@ -127,10 +140,11 @@ export class RoomWidgetChatInputHandler extends RoomWidgetHandler
newWindow.document.write(image.outerHTML);
return null;
case ':pickall':
// this.container.notificationService.alertWithConfirm('${room.confirm.pick_all}', '${generic.alert.title}', () =>
// {
// GetSessionDataManager().sendSpecialCommandMessage(':pickall');
// });
NotificationUtilities.confirm(LocalizeText('room.confirm.pick_all'), () =>
{
GetSessionDataManager().sendSpecialCommandMessage(':pickall');
},
null, null, null, LocalizeText('generic.alert.title'));
return null;
case ':furni':

View File

@ -1,4 +1,4 @@
import { IFurnitureData, NitroEvent, ObjectDataFactory, PetFigureData, PetRespectComposer, PetSupplementComposer, PetType, RoomControllerLevel, RoomModerationSettings, RoomObjectCategory, RoomObjectOperationType, RoomObjectType, RoomObjectVariable, RoomSessionPetInfoUpdateEvent, RoomSessionUserBadgesEvent, RoomTradingLevelEnum, RoomUnitDropHandItemComposer, RoomUnitGiveHandItemComposer, RoomUnitGiveHandItemPetComposer, RoomUserData, RoomWidgetEnum, RoomWidgetEnumItemExtradataParameter, Vector3d } from '@nitrots/nitro-renderer';
import { IFurnitureData, NitroEvent, ObjectDataFactory, PetFigureData, PetRespectComposer, PetSupplementComposer, PetType, RoomControllerLevel, RoomModerationSettings, RoomObjectCategory, RoomObjectOperationType, RoomObjectType, RoomObjectVariable, RoomSessionPetInfoUpdateEvent, RoomSessionUserBadgesEvent, RoomSessionUserFigureUpdateEvent, RoomTradingLevelEnum, RoomUnitDropHandItemComposer, RoomUnitGiveHandItemComposer, RoomUnitGiveHandItemPetComposer, RoomUserData, RoomWidgetEnum, RoomWidgetEnumItemExtradataParameter, Vector3d } from '@nitrots/nitro-renderer';
import { GetNitroInstance, GetRoomEngine, GetSessionDataManager, IsOwnerOfFurniture } from '../../../..';
import { InventoryTradeRequestEvent, WiredSelectObjectEvent } from '../../../../../events';
import { FriendsSendFriendRequestEvent } from '../../../../../events/friends/FriendsSendFriendRequestEvent';
@ -24,6 +24,9 @@ export class RoomWidgetInfostandHandler extends RoomWidgetHandler
case RoomSessionUserBadgesEvent.RSUBE_BADGES:
this.container.eventDispatcher.dispatchEvent(event);
return;
case RoomSessionUserFigureUpdateEvent.USER_FIGURE:
this.processRoomSessionUserFigureUpdateEvent((event as RoomSessionUserFigureUpdateEvent));
return;
}
}
@ -661,6 +664,17 @@ export class RoomWidgetInfostandHandler extends RoomWidgetHandler
this.container.eventDispatcher.dispatchEvent(infostandEvent);
}
private processRoomSessionUserFigureUpdateEvent(event: RoomSessionUserFigureUpdateEvent): void
{
const userData = this.container.roomSession.userDataManager.getUserDataByIndex(event.userId);
if(!userData) return;
// update active infostand figure
// update motto
// update activity points
}
private checkGuildSetting(event: RoomWidgetUpdateInfostandUserEvent): boolean
{
if(event.isGuildRoom) return (event.roomControllerLevel >= RoomControllerLevel.GUILD_ADMIN);
@ -766,7 +780,8 @@ export class RoomWidgetInfostandHandler extends RoomWidgetHandler
{
return [
RoomSessionPetInfoUpdateEvent.PET_INFO,
RoomSessionUserBadgesEvent.RSUBE_BADGES
RoomSessionUserBadgesEvent.RSUBE_BADGES,
RoomSessionUserFigureUpdateEvent.USER_FIGURE
];
}

View File

@ -1,12 +1,12 @@
import { IFurnitureData } from '@nitrots/nitro-renderer';
import { GetSessionDataManager } from '.';
import { ProductTypeEnum } from '../../../views/catalog/common/ProductTypeEnum';
import { ProductTypeEnum } from '../../../components/catalog/common/ProductTypeEnum';
export function GetFurnitureData(furniClassId: number, productType: string): IFurnitureData
{
let furniData: IFurnitureData = null;
switch(productType.toUpperCase())
switch(productType.toLowerCase())
{
case ProductTypeEnum.FLOOR:
furniData = GetSessionDataManager().getFloorItemData(furniClassId);

View File

@ -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<number>(RoomObjectVariable.FURNITURE_OWNER_ID);
return (userId === objectOwnerId);
}

View File

@ -14,6 +14,7 @@ export * from './GetSessionDataManager';
export * from './GoToDesktop';
export * from './HasHabboClub';
export * from './HasHabboVip';
export * from './IsOwnerOfFloorFurniture';
export * from './IsOwnerOfFurniture';
export * from './IsRidingHorse';
export * from './StartRoomSession';

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 B

View File

@ -79,6 +79,11 @@
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;

View File

@ -20,6 +20,9 @@
text-decoration: if($link-decoration == none, null, none);
@include transition($nav-link-transition);
display: flex;
align-items: center;
&:hover,
&:focus {
color: $nav-link-hover-color;
@ -50,7 +53,7 @@
&.active {
&:before {
background: #FFFFFF;
background: #ffffff;
}
}
@ -64,7 +67,7 @@
left: 0;
right: 0;
margin: auto;
background: #C2C9D1;
background: #c2c9d1;
z-index: 1;
}
@ -97,7 +100,6 @@
}
}
//
// Pills
//
@ -116,7 +118,6 @@
}
}
//
// Justified variants
//
@ -145,7 +146,6 @@
}
}
// Tabbable tabs
//
// Hide tabbable panes to start, show them when `.active`

View File

@ -1,6 +1,5 @@
// stylelint-disable declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix
// Reboot
//
// Normalization of HTML elements, manually forked from Normalize.css to remove
@ -8,7 +7,6 @@
//
// Normalize is licensed MIT. https://github.com/necolas/normalize.css
// Document
//
// Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.
@ -16,10 +14,10 @@
*,
*::before,
*::after {
line-height: normal;
box-sizing: border-box;
}
// Root
//
// Ability to the value of the root font sizes, affecting the value of `rem`.
@ -37,7 +35,6 @@
}
}
// Body
//
// 1. Remove the margin in all browsers.
@ -60,7 +57,6 @@ body {
}
// scss-docs-end reboot-body-rules
// Content grouping
//
// 1. Reset Firefox's gray color
@ -78,7 +74,6 @@ hr:not([size]) {
height: $hr-height; // 2
}
// Typography
//
// 1. Remove top margins from headings
@ -125,7 +120,6 @@ h6 {
@include font-size($h6-font-size);
}
// Reset margins on paragraphs
//
// Similarly, the top margin on `<p>`s get reset. However, we also reset the
@ -136,7 +130,6 @@ p {
margin-bottom: $paragraph-margin-bottom;
}
// Abbreviations
//
// 1. Duplicate behavior to the data-bs-* attribute for our tooltip plugin
@ -145,13 +138,13 @@ p {
// 4. Prevent the text-decoration to be skipped.
abbr[title],
abbr[data-bs-original-title] { // 1
abbr[data-bs-original-title] {
// 1
text-decoration: underline dotted; // 2
cursor: help; // 3
text-decoration-skip-ink: none; // 4
}
// Address
address {
@ -160,7 +153,6 @@ address {
line-height: inherit;
}
// Lists
ol,
@ -189,18 +181,16 @@ dt {
// 1. Undo browser default
dd {
margin-bottom: .5rem;
margin-bottom: 0.5rem;
margin-left: 0; // 1
}
// Blockquote
blockquote {
margin: 0 0 1rem;
}
// Strong
//
// Add the correct font weight in Chrome, Edge, and Safari
@ -210,7 +200,6 @@ strong {
font-weight: $font-weight-bolder;
}
// Small
//
// Add the correct font size in all browsers
@ -219,7 +208,6 @@ small {
@include font-size($small-font-size);
}
// Mark
mark {
@ -227,7 +215,6 @@ mark {
background-color: $mark-bg;
}
// Sub and Sup
//
// Prevent `sub` and `sup` elements from affecting the line height in
@ -241,9 +228,12 @@ sup {
vertical-align: baseline;
}
sub { bottom: -.25em; }
sup { top: -.5em; }
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
// Links
@ -270,7 +260,6 @@ a:not([href]):not([class]) {
}
}
// Code
pre,
@ -278,7 +267,9 @@ code,
kbd,
samp {
font-family: $font-family-code;
@include font-size(1em); // Correct the odd `em` font sizing in all browsers.
@include font-size(
1em
); // Correct the odd `em` font sizing in all browsers.
direction: ltr #{"/* rtl:ignore */"};
unicode-bidi: bidi-override;
}
@ -328,7 +319,6 @@ kbd {
}
}
// Figures
//
// Apply a consistent margin strategy (matches our type styles).
@ -337,7 +327,6 @@ figure {
margin: 0 0 1rem;
}
// Images and content
img,
@ -345,7 +334,6 @@ svg {
vertical-align: middle;
}
// Tables
//
// Prevent double borders
@ -383,7 +371,6 @@ th {
border-width: 0;
}
// Forms
//
// 1. Allow labels to use `margin` for spacing.
@ -570,7 +557,6 @@ legend {
padding: 0;
}
// Inherit font family and line height for file input buttons
::file-selector-button {
@ -606,7 +592,6 @@ summary {
cursor: pointer;
}
// Progress
//
// Add the correct vertical alignment in Chrome, Firefox, and Opera.
@ -615,7 +600,6 @@ progress {
vertical-align: baseline;
}
// Hidden attribute
//
// Always hide an element with the `hidden` HTML attribute.

View File

@ -390,7 +390,7 @@ $enable-cssgrid: true !default;
$enable-button-pointers: true !default;
$enable-rfs: true !default;
$enable-validation-icons: true !default;
$enable-negative-margins: false !default;
$enable-negative-margins: true !default;
$enable-deprecation-messages: true !default;
$enable-important-utilities: true !default;
@ -732,7 +732,7 @@ $table-cell-padding-x-sm: .25rem !default;
$table-cell-vertical-align: top !default;
$table-color: $body-color !default;
$table-color: $black !default;
$table-bg: transparent !default;
$table-accent-bg: transparent !default;

View File

@ -17,7 +17,7 @@
.form-check-input {
width: $form-check-input-width;
height: $form-check-input-width;
margin-top: ($line-height-base - $form-check-input-width) * .5; // line-height minus check height
//margin-top: ($line-height-base - $form-check-input-width) * .5; // line-height minus check height
vertical-align: top;
background-color: $form-check-input-bg;
background-repeat: no-repeat;

View File

@ -1,4 +1,5 @@
.fas {
.fas,
.svg-inline--fa {
line-height: 0 !important;
}
@ -303,6 +304,48 @@
height: 15px;
}
&.icon-small-room {
background: url("../images/icons/small-room.png");
width: 17px;
height: 16px;
}
&.icon-cog {
background: url("../images/icons/cog.png");
width: 21px;
height: 21px;
}
&.icon-chat-history {
background: url("../images/icons/chat-history.png");
width: 17px;
height: 21px;
}
&.icon-room-link {
background: url("../images/icons/room-link.png");
width: 17px;
height: 15px;
}
&.icon-zoom-more {
background: url("../images/icons/zoom-more.png");
width: 12px;
height: 22px;
}
&.icon-zoom-less {
background: url("../images/icons/zoom-less.png");
width: 12px;
height: 22px;
}
&.icon-like-room {
background: url("../images/icons/like-room.png");
width: 20px;
height: 22px;
}
&.icon-arrows {
background: url("../images/icons/arrows.png");
width: 17px;

View File

@ -1,9 +1,5 @@
@import './fonts';
@import './bootstrap/bootstrap';
@import './fontawesome/fontawesome';
@import './fontawesome/solid';
@import './fontawesome/brands';
@import './fontawesome/regular';
@import '../node_modules/animate.css/animate.min.css';
@import './scrollbars';
@import './slider';

View File

@ -47,6 +47,10 @@ ul {
cursor: pointer;
}
.cursor-not-allowed {
cursor: not-allowed;
}
.pointer-events-none {
pointer-events: none;
}
@ -78,3 +82,18 @@ ul {
.z-index-1 {
z-index: 1;
}
.flex-basis-fit-content {
flex-basis: fit-content;
}
.flex-basis-max-content {
flex-basis: max-content;
}
.striped-children {
> :nth-child(1) {
background-color: $table-striped-bg;
}
}

28
src/common/AutoGrid.tsx Normal file
View File

@ -0,0 +1,28 @@
import { CSSProperties, FC, useMemo } from 'react';
import { Grid, GridProps } from './Grid';
export interface AutoGridProps extends GridProps
{
columnMinWidth?: number;
columnMinHeight?: number;
}
export const AutoGrid: FC<AutoGridProps> = props =>
{
const { columnMinWidth = 40, columnMinHeight = 40, columnCount = 0, fullHeight = false, maxContent = true, overflow = 'auto', style = {}, ...rest } = props;
const getStyle = useMemo(() =>
{
let newStyle: CSSProperties = {};
newStyle['--nitro-grid-column-min-height'] = (columnMinHeight + 'px');
if(columnCount > 1) newStyle.gridTemplateColumns = `repeat(auto-fill, minmax(${ columnMinWidth }px, 1fr))`;
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
return newStyle;
}, [ columnMinWidth, columnMinHeight, columnCount, style ]);
return <Grid columnCount={ columnCount } fullHeight={ fullHeight } overflow={ overflow } style={ getStyle } { ...rest } />;
}

70
src/common/Base.tsx Normal file
View File

@ -0,0 +1,70 @@
import { CSSProperties, DetailedHTMLProps, FC, HTMLAttributes, LegacyRef, useMemo } from 'react';
import { ColorVariantType, FloatType, OverflowType, PositionType } from './types';
export interface BaseProps<T = HTMLElement> extends DetailedHTMLProps<HTMLAttributes<T>, T>
{
innerRef?: LegacyRef<T>;
fit?: boolean;
grow?: boolean;
shrink?: boolean;
fullWidth?: boolean;
fullHeight?: boolean;
overflow?: OverflowType;
position?: PositionType;
float?: FloatType;
pointer?: boolean;
textColor?: ColorVariantType;
classNames?: string[];
}
export const Base: FC<BaseProps<HTMLDivElement>> = props =>
{
const { ref = null, innerRef = null, fit = false, grow = false, shrink = false, fullWidth = false, fullHeight = false, overflow = null, position = null, float = null, pointer = false, textColor = null, classNames = [], className = '', style = {}, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [];
if(fit || fullWidth) newClassNames.push('w-100');
if(fit || fullHeight) newClassNames.push('h-100');
if(grow) newClassNames.push('flex-grow-1');
if(shrink) newClassNames.push('flex-shrink-0');
if(overflow) newClassNames.push('overflow-' + overflow);
if(position) newClassNames.push('position-' + position);
if(float) newClassNames.push('float-' + float);
if(pointer) newClassNames.push('cursor-pointer');
if(textColor) newClassNames.push('text-' + textColor);
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ fit, grow, shrink, fullWidth, fullHeight, overflow, position, float, pointer, textColor, classNames ]);
const getClassName = useMemo(() =>
{
let newClassName = getClassNames.join(' ');
if(className.length) newClassName += (' ' + className);
return newClassName.trim();
}, [ getClassNames, className ]);
const getStyle = useMemo(() =>
{
let newStyle: CSSProperties = {};
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
return newStyle;
}, [ style ]);
return <div ref={ innerRef } className={ getClassName } style={ getStyle } { ...rest } />;
}

35
src/common/Button.tsx Normal file
View File

@ -0,0 +1,35 @@
import { FC, useMemo } from 'react';
import { Flex, FlexProps } from './Flex';
import { ButtonSizeType, ColorVariantType } from './types';
export interface ButtonProps extends FlexProps
{
variant?: ColorVariantType;
size?: ButtonSizeType;
active?: boolean;
disabled?: boolean;
}
export const Button: FC<ButtonProps> = props =>
{
const { variant = 'primary', size = 'sm', active = false, disabled = false, classNames = [], ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'btn' ];
if(variant) newClassNames.push('btn-' + variant);
if(size) newClassNames.push('btn-' + size);
if(active) newClassNames.push('active');
if(disabled) newClassNames.push('disabled');
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ variant, size, active, disabled, classNames ]);
return <Flex center classNames={ getClassNames } { ...rest } />;
}

View File

@ -0,0 +1,22 @@
import { FC, useMemo } from 'react';
import { Base, BaseProps } from './Base';
export interface ButtonGroupProps extends BaseProps<HTMLDivElement>
{
}
export const ButtonGroup: FC<ButtonGroupProps> = props =>
{
const { classNames = [], ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'btn-group' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
return <Base classNames={ getClassNames } { ...rest } />;
}

36
src/common/Column.tsx Normal file
View File

@ -0,0 +1,36 @@
import { FC, useMemo } from 'react';
import { Flex, FlexProps } from './Flex';
import { useGridContext } from './GridContext';
import { ColumnSizesType } from './types';
export interface ColumnProps extends FlexProps
{
size?: ColumnSizesType;
column?: boolean;
}
export const Column: FC<ColumnProps> = props =>
{
const { size = 0, column = true, gap = 2, classNames = [], ...rest } = props;
const { isCssGrid = false } = useGridContext();
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [];
if(size)
{
let colClassName = `col-${ size }`;
if(isCssGrid) colClassName = `g-${ colClassName }`;
newClassNames.push(colClassName);
}
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ size, isCssGrid, classNames ]);
return <Flex classNames={ getClassNames } column={ column } gap={ gap } { ...rest } />;
}

54
src/common/Flex.tsx Normal file
View File

@ -0,0 +1,54 @@
import { FC, useMemo } from 'react';
import { Base, BaseProps } from './Base';
import { AlignItemType, AlignSelfType, JustifyContentType, SpacingType } from './types';
export interface FlexProps extends BaseProps<HTMLDivElement>
{
inline?: boolean;
column?: boolean;
reverse?: boolean;
gap?: SpacingType;
center?: boolean;
alignSelf?: AlignSelfType;
alignItems?: AlignItemType;
justifyContent?: JustifyContentType;
}
export const Flex: FC<FlexProps> = props =>
{
const { inline = false, column = undefined, reverse = false, gap = null, center = false, alignSelf = null, alignItems = null, justifyContent = null, classNames = [], ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [];
if(inline) newClassNames.push('d-inline-flex');
else newClassNames.push('d-flex');
if(column)
{
if(reverse) newClassNames.push('flex-column-reverse');
else newClassNames.push('flex-column');
}
else
{
if(reverse) newClassNames.push('flex-row-reverse');
}
if(gap) newClassNames.push('gap-' + gap);
if(alignSelf) newClassNames.push('align-self-' + alignSelf);
if(alignItems) newClassNames.push('align-items-' + alignItems);
if(justifyContent) newClassNames.push('justify-content-' + justifyContent);
if(!alignItems && !justifyContent && center) newClassNames.push('align-items-center', 'justify-content-center');
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ inline, column, reverse, gap, center, alignSelf, alignItems, justifyContent, classNames ]);
return <Base classNames={ getClassNames } { ...rest } />;
}

22
src/common/FormGroup.tsx Normal file
View File

@ -0,0 +1,22 @@
import { FC, useMemo } from 'react';
import { Flex, FlexProps } from './Flex';
export interface FormGroupProps extends FlexProps
{
}
export const FormGroup: FC<FormGroupProps> = props =>
{
const { classNames = [], ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'form-group' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
return <Flex classNames={ getClassNames } { ...rest } />;
}

64
src/common/Grid.tsx Normal file
View File

@ -0,0 +1,64 @@
import { FC, useMemo } from 'react';
import { CSSProperties } from 'styled-components';
import { Base, BaseProps } from './Base';
import { GridContextProvider } from './GridContext';
import { AlignItemType, AlignSelfType, JustifyContentType, SpacingType } from './types';
export interface GridProps extends BaseProps<HTMLDivElement>
{
inline?: boolean;
gap?: SpacingType;
maxContent?: boolean;
columnCount?: number;
center?: boolean;
alignSelf?: AlignSelfType;
alignItems?: AlignItemType;
justifyContent?: JustifyContentType;
}
export const Grid: FC<GridProps> = props =>
{
const { inline = false, gap = 2, maxContent = false, columnCount = 0, center = false, alignSelf = null, alignItems = null, justifyContent = null, fullHeight = true, classNames = [], style = {}, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [];
if(inline) newClassNames.push('inline-grid');
else newClassNames.push('grid');
if(gap) newClassNames.push('gap-' + gap);
else if(gap === 0) newClassNames.push('gap-0');
if(maxContent) newClassNames.push('flex-basis-max-content');
if(alignSelf) newClassNames.push('align-self-' + alignSelf);
if(alignItems) newClassNames.push('align-items-' + alignItems);
if(justifyContent) newClassNames.push('justify-content-' + justifyContent);
if(!alignItems && !justifyContent && center) newClassNames.push('align-items-center', 'justify-content-center');
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ inline, gap, maxContent, alignSelf, alignItems, justifyContent, center, classNames ]);
const getStyle = useMemo(() =>
{
let newStyle: CSSProperties = {};
if(columnCount) newStyle['--bs-columns'] = columnCount.toString();
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
return newStyle;
}, [ columnCount, style ]);
return (
<GridContextProvider value={ { isCssGrid: true } }>
<Base fullHeight={ fullHeight } classNames={ getClassNames } style={ getStyle } { ...rest } />
</GridContextProvider>
);
}

View File

@ -0,0 +1,17 @@
import { createContext, FC, ProviderProps, useContext } from 'react';
export interface IGridContext
{
isCssGrid: boolean;
}
const GridContext = createContext<IGridContext>({
isCssGrid: false
});
export const GridContextProvider: FC<ProviderProps<IGridContext>> = props =>
{
return <GridContext.Provider value={ props.value }>{ props.children }</GridContext.Provider>
}
export const useGridContext = () => useContext(GridContext);

63
src/common/Text.tsx Normal file
View File

@ -0,0 +1,63 @@
import { FC, useMemo } from 'react';
import { Base, BaseProps } from './Base';
import { ColorVariantType, FontSizeType, FontWeightType, TextAlignType } from './types';
export interface TextProps extends BaseProps<HTMLDivElement>
{
variant?: ColorVariantType;
fontWeight?: FontWeightType;
fontSize?: FontSizeType;
align?: TextAlignType;
bold?: boolean;
underline?: boolean;
italics?: boolean;
truncate?: boolean;
center?: boolean;
textEnd?: boolean;
small?: boolean;
wrap?: boolean;
noWrap?: boolean;
textBreak?: boolean;
}
export const Text: FC<TextProps> = props =>
{
const { variant = 'black', fontWeight = null, fontSize = 0, align = null, bold = false, underline = false, italics = false, truncate = false, center = false, textEnd = false, small = false, wrap = false, noWrap = false, textBreak = false, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'd-inline' ];
if(variant) newClassNames.push('text-' + variant);
if(bold) newClassNames.push('fw-bold');
if(fontWeight) newClassNames.push('fw-' + fontWeight);
if(fontSize) newClassNames.push('fs-' + fontSize);
if(align) newClassNames.push('text-' + align);
if(underline) newClassNames.push('text-decoration-underline');
if(italics) newClassNames.push('fst-italic');
if(truncate) newClassNames.push('text-truncate');
if(center) newClassNames.push('text-center');
if(textEnd) newClassNames.push('text-end');
if(small) newClassNames.push('small');
if(wrap) newClassNames.push('text-wrap');
if(noWrap) newClassNames.push('text-nowrap');
if(textBreak) newClassNames.push('text-break');
return newClassNames;
}, [ variant, fontWeight, fontSize, align, bold, underline, italics, truncate, center, textEnd, small, wrap, noWrap, textBreak ]);
return <Base classNames={ getClassNames } { ...rest } />;
}

43
src/common/index.scss Normal file
View File

@ -0,0 +1,43 @@
.layout-grid-item {
height: var(--nitro-grid-column-min-height, 45px);
background-position: center;
background-repeat: no-repeat;
background-color: $grid-bg-color;
&.active {
border-color: $grid-active-border-color !important;
background-color: $grid-active-bg-color;
}
&.disabled {
cursor: not-allowed;
img {
opacity: .5;
filter: grayscale(1);
}
}
&.unseen {
background-color: rgba($success, 0.4);
}
.avatar-image {
background-position-y: -35px;
}
&.has-highlight {
&:after {
content: "";
z-index: 2;
position: absolute;
top: 0;
left: 0;
right: 0;
height: 50%;
background-color: $white;
opacity: 0.1;
}
}
}

13
src/common/index.ts Normal file
View File

@ -0,0 +1,13 @@
export * from './AutoGrid';
export * from './Base';
export * from './Button';
export * from './ButtonGroup';
export * from './Column';
export * from './Flex';
export * from './FormGroup';
export * from './Grid';
export * from './GridContext';
export * from './layout';
export * from './Text';
export * from './types';
export * from './utils';

View File

@ -0,0 +1,75 @@
import { FC, useMemo } from 'react';
import { ItemCountView } from '../../views/shared/item-count/ItemCountView';
import { LimitedEditionStyledNumberView } from '../../views/shared/limited-edition/LimitedEditionStyledNumberView';
import { Base } from '../Base';
import { Column, ColumnProps } from '../Column';
export interface LayoutGridItemProps extends ColumnProps
{
itemImage?: string;
itemColor?: string;
itemActive?: boolean;
itemCount?: number;
itemCountMinimum?: number;
itemUniqueSoldout?: boolean;
itemUniqueNumber?: number;
itemUnseen?: boolean;
itemHighlight?: boolean;
disabled?: boolean;
}
export const LayoutGridItem: FC<LayoutGridItemProps> = props =>
{
const { itemImage = undefined, itemColor = undefined, itemActive = false, itemCount = 1, itemCountMinimum = 1, itemUniqueSoldout = false, itemUniqueNumber = -2, itemUnseen = false, itemHighlight = false, disabled = false, center = true, column = true, style = {}, classNames = [], position = 'relative', overflow = 'hidden', children = null, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'layout-grid-item', 'border', 'border-2', 'border-muted', 'rounded' ];
if(itemActive) newClassNames.push('active');
if(itemUniqueSoldout || (itemUniqueNumber > 0)) newClassNames.push('unique-item');
if(itemUniqueSoldout) newClassNames.push('sold-out');
if(itemUnseen) newClassNames.push('unseen');
if(itemHighlight) newClassNames.push('has-highlight');
if(disabled) newClassNames.push('disabled')
if(itemImage === null) newClassNames.push('icon', 'loading-icon');
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ itemActive, itemUniqueSoldout, itemUniqueNumber, itemUnseen, itemHighlight, disabled, itemImage, classNames ]);
const getStyle = useMemo(() =>
{
let newStyle = { ...style };
if(itemImage) newStyle.backgroundImage = `url(${ itemImage })`;
if(itemColor) newStyle.backgroundColor = itemColor;
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
return newStyle;
}, [ style, itemImage, itemColor ]);
return (
<Column center={ center } pointer position={ position } overflow={ overflow } column={ column } classNames={ getClassNames } style={ getStyle } { ...rest }>
{ (itemCount > itemCountMinimum) &&
<ItemCountView count={ itemCount } /> }
{ (itemUniqueNumber > 0) &&
<>
<Base fit className="unique-bg-override" style={ { backgroundImage: `url(${ itemImage })` } } />
<div className="position-absolute bottom-0 unique-item-counter">
<LimitedEditionStyledNumberView value={ itemUniqueNumber } />
</div>
</> }
{ children }
</Column>
);
}

View File

@ -0,0 +1,23 @@
import { FC, useMemo } from 'react';
import { Base, BaseProps } from '../Base';
export interface LayoutImageProps extends BaseProps<HTMLDivElement>
{
imageUrl?: string;
}
export const LayoutImage: FC<LayoutImageProps> = props =>
{
const { imageUrl = null, fit = true, style = null, ...rest } = props;
const getStyle = useMemo(() =>
{
const newStyle = { ...style };
if(imageUrl) newStyle.background = `url(${ imageUrl }) center no-repeat`;
return newStyle;
}, [ style, imageUrl ]);
return <Base fit={ fit } style={ getStyle } { ...rest } />;
}

View File

@ -0,0 +1,2 @@
export * from './LayoutGridItem';
export * from './LayoutImage';

View File

@ -0,0 +1 @@
export type AlignItemType = 'start' | 'end' | 'center' | 'baseline' | 'stretch';

View File

@ -0,0 +1 @@
export type AlignSelfType = 'start' | 'end' | 'center' | 'baseline' | 'stretch';

View File

@ -0,0 +1 @@
export type ButtonSizeType = 'lg' | 'sm';

View File

@ -0,0 +1 @@
export type ColorVariantType = 'primary' | 'success' | 'danger' | 'secondary' | 'link' | 'black' | 'white' | 'dark' | 'warning' | 'muted';

View File

@ -0,0 +1 @@
export type ColumnSizesType = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;

View File

@ -0,0 +1 @@
export type FloatType = 'start' | 'end' | 'none';

View File

@ -0,0 +1 @@
export type FontSizeType = 1 | 2 | 3 | 4 | 5 | 6;

View File

@ -0,0 +1 @@
export type FontWeightType = 'bold' | 'bolder' | 'normal' | 'light' | 'lighter';

View File

@ -0,0 +1 @@
export type JustifyContentType = 'start' | 'end' | 'center' | 'between' | 'around' | 'evenly';

View File

@ -0,0 +1 @@
export type OverflowType = 'hidden' | 'auto' | 'unset';

View File

@ -0,0 +1 @@
export type PositionType = 'static' | 'relative' | 'fixed' | 'absolute' | 'sticky';

View File

@ -0,0 +1 @@
export type SpacingType = 0 | 1 | 2 | 3 | 4 | 5;

View File

@ -0,0 +1 @@
export type TextAlignType = 'start' | 'center' | 'end';

13
src/common/types/index.ts Normal file
View File

@ -0,0 +1,13 @@
export * from './AlignItemType';
export * from './AlignSelfType';
export * from './ButtonSizeType';
export * from './ColorVariantType';
export * from './ColumnSizesType';
export * from './FloatType';
export * from './FontSizeType';
export * from './FontWeightType';
export * from './JustifyContentType';
export * from './OverflowType';
export * from './PositionType';
export * from './SpacingType';
export * from './TextAlignType';

View File

@ -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);
}

View File

@ -0,0 +1 @@
export * from './CreateTransitionToIcon';

View File

@ -3,19 +3,6 @@
height: $achievement-height;
}
.nitro-achievements-category-grid {
--nitro-grid-column-min-width: 80px !important;
.grid-item {
height: 80px;
max-height: 80px;
.achievement-score {
top: 50px;
}
}
}
.nitro-achievements-back-arrow {
background: url('../../assets/images/achievements/back-arrow.png') no-repeat center;
width: 33px;
@ -23,6 +10,6 @@
}
.nitro-achievements-badge-image {
width: 80px;
height: 80px;
width: 80px !important;
height: 80px !important;
}

View File

@ -1,18 +1,21 @@
import { AchievementData, AchievementEvent, AchievementsEvent, AchievementsScoreEvent, RequestAchievementsMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { LocalizeText } from '../../api';
import { GetConfiguration, LocalizeText } from '../../api';
import { Base } from '../../common/Base';
import { Column } from '../../common/Column';
import { Flex } from '../../common/Flex';
import { Text } from '../../common/Text';
import { AchievementsUIEvent, AchievementsUIUnseenCountEvent } from '../../events/achievements';
import { BatchUpdates, CreateMessageHook, dispatchUiEvent, SendMessageHook } from '../../hooks';
import { useUiEvent } from '../../hooks/events';
import { NitroCardContentView, NitroCardHeaderView, NitroCardSubHeaderView, NitroCardView, NitroLayoutFlexColumn, NitroLayoutGrid, NitroLayoutGridColumn } from '../../layout';
import { NitroCardContentView, NitroCardHeaderView, NitroCardSubHeaderView, NitroCardView } from '../../layout';
import { NitroLayoutBase } from '../../layout/base';
import { AchievementsViewProps } from './AchievementsView.types';
import { AchievementCategory } from './common/AchievementCategory';
import { AchievementUtilities } from './common/AchievementUtilities';
import { AchievementsCategoryListView } from './views/category-list/AchievementsCategoryListView';
import { AchievementCategoryView } from './views/category/AchievementCategoryView';
export const AchievementsView: FC<AchievementsViewProps> = props =>
export const AchievementsView: FC<{}> = props =>
{
const [ isVisible, setIsVisible ] = useState(false);
const [ isInitalized, setIsInitalized ] = useState(false);
@ -170,6 +173,15 @@ export const AchievementsView: FC<AchievementsViewProps> = props =>
return achievementCategories.find(existing => (existing.code === selectedCategoryCode));
}, [ achievementCategories, selectedCategoryCode ]);
const getCategoryIcon = useMemo(() =>
{
if(!getSelectedCategory) return null;
const imageUrl = GetConfiguration<string>('achievements.images.url');
return imageUrl.replace('%image%', `achicon_${ getSelectedCategory.code }`);
}, [ getSelectedCategory ]);
const setAchievementSeen = useCallback((code: string, achievementId: number) =>
{
const newCategories = [ ...achievementCategories ];
@ -207,38 +219,27 @@ export const AchievementsView: FC<AchievementsViewProps> = props =>
<NitroCardView uniqueKey="achievements" className="nitro-achievements" simple={ true }>
<NitroCardHeaderView headerText={ LocalizeText('inventory.achievements') } onCloseClick={ event => setIsVisible(false) } />
{ getSelectedCategory &&
<NitroCardSubHeaderView className="justify-content-center align-items-center cursor-pointer" gap={ 3 }>
<NitroCardSubHeaderView position="relative" className="justify-content-center align-items-center cursor-pointer" gap={ 3 }>
<NitroLayoutBase onClick={ event => setSelectedCategoryCode(null) } className="nitro-achievements-back-arrow" />
<NitroLayoutFlexColumn className="flex-grow-1">
<NitroLayoutBase className="fs-4 text-black fw-bold">
{ LocalizeText(`quests.${ getSelectedCategory.code }.name`) }
</NitroLayoutBase>
<NitroLayoutBase className="text-black">
{ LocalizeText('achievements.details.categoryprogress', [ 'progress', 'limit' ], [ getSelectedCategory.getProgress().toString(), getSelectedCategory.getMaxProgress().toString() ]) }
</NitroLayoutBase>
</NitroLayoutFlexColumn>
<Column grow gap={ 0 }>
<Text fontSize={ 4 } fontWeight="bold" className="text-small">{ LocalizeText(`quests.${ getSelectedCategory.code }.name`) }</Text>
<Text>{ LocalizeText('achievements.details.categoryprogress', [ 'progress', 'limit' ], [ getSelectedCategory.getProgress().toString(), getSelectedCategory.getMaxProgress().toString() ]) }</Text>
</Column>
</NitroCardSubHeaderView> }
<NitroCardContentView>
<NitroLayoutGrid>
<NitroLayoutGridColumn size={ 12 }>
{ !getSelectedCategory &&
<>
<AchievementsCategoryListView categories={ achievementCategories } selectedCategoryCode={ selectedCategoryCode } setSelectedCategoryCode={ setSelectedCategoryCode } />
<NitroLayoutFlexColumn className="flex-grow-1 justify-content-end" gap={ 2 }>
<NitroLayoutBase className="progress">
<NitroLayoutBase className="progress-bar" style={ { width: (scaledProgressPercent + '%') }}>
{ LocalizeText('achievements.categories.totalprogress', [ 'progress', 'limit' ], [ getProgress.toString(), getMaxProgress.toString() ]) }
</NitroLayoutBase>
</NitroLayoutBase>
<NitroLayoutBase className="bg-muted text-black text-center rounded">
{ LocalizeText('achievements.categories.score', [ 'score' ], [ achievementScore.toString() ]) }
</NitroLayoutBase>
</NitroLayoutFlexColumn>
<Column grow justifyContent="end">
<Base className="progress" position="relative">
<Flex fit center position="absolute" className="text-black">{ LocalizeText('achievements.categories.totalprogress', [ 'progress', 'limit' ], [ getProgress.toString(), getMaxProgress.toString() ]) }</Flex>
<Base className="progress-bar bg-success" style={ { width: (scaledProgressPercent + '%') }} />
</Base>
<Text className="bg-muted rounded p-1" center>{ LocalizeText('achievements.categories.score', [ 'score' ], [ achievementScore.toString() ]) }</Text>
</Column>
</> }
{ getSelectedCategory &&
<AchievementCategoryView category={ getSelectedCategory } setAchievementSeen={ setAchievementSeen } /> }
</NitroLayoutGridColumn>
</NitroLayoutGrid>
</NitroCardContentView>
</NitroCardView>
);

View File

@ -1,7 +1,14 @@
import { AchievementData } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { BadgeImageView } from '../../../shared/badge-image/BadgeImageView';
import { BaseProps } from '../../../../common/Base';
import { BadgeImageView } from '../../../../views/shared/badge-image/BadgeImageView';
import { AchievementUtilities } from '../../common/AchievementUtilities';
import { AchievementBadgeViewProps } from './AchievementBadgeView.types';
export interface AchievementBadgeViewProps extends BaseProps<HTMLDivElement>
{
achievement: AchievementData;
scale?: number;
}
export const AchievementBadgeView: FC<AchievementBadgeViewProps> = props =>
{

View File

@ -0,0 +1,66 @@
import { AchievementData } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { LocalizeBadgeDescription, LocalizeBadgeName, LocalizeText } from '../../../../api';
import { Base } from '../../../../common/Base';
import { Column } from '../../../../common/Column';
import { Flex } from '../../../../common/Flex';
import { Text } from '../../../../common/Text';
import { CurrencyIcon } from '../../../../views/shared/currency-icon/CurrencyIcon';
import { AchievementUtilities } from '../../common/AchievementUtilities';
import { GetAchievementLevel } from '../../common/GetAchievementLevel';
import { GetScaledProgressPercent } from '../../common/GetScaledProgressPercent';
import { AchievementBadgeView } from '../achievement-badge/AchievementBadgeView';
export interface AchievementDetailsViewProps
{
achievement: AchievementData;
}
export const AchievementDetailsView: FC<AchievementDetailsViewProps> = props =>
{
const { achievement = null } = props;
if(!achievement) return null;
const achievementLevel = GetAchievementLevel(achievement);
const scaledProgressPercent = GetScaledProgressPercent(achievement);
return (
<Flex shrink className="bg-muted rounded p-2 text-black" gap={ 2 } overflow="hidden">
<Column center>
<AchievementBadgeView className="nitro-achievements-badge-image" achievement={ achievement } scale={ 2 } />
<Text fontWeight="bold">
{ LocalizeText('achievements.details.level', [ 'level', 'limit' ], [ achievementLevel.toString(), achievement.levelCount.toString() ]) }
</Text>
</Column>
<Column fullWidth justifyContent="center" overflow="hidden">
<Column gap={ 1 }>
<Text fontWeight="bold" truncate>
{ LocalizeBadgeName(AchievementUtilities.getBadgeCode(achievement)) }
</Text>
<Text truncate>
{ LocalizeBadgeDescription(AchievementUtilities.getBadgeCode(achievement)) }
</Text>
</Column>
{ ((achievement.levelRewardPoints > 0) || (achievement.scoreLimit > 0)) &&
<Column gap={ 1 }>
{ (achievement.levelRewardPoints > 0) &&
<Flex alignItems="center" gap={ 1 }>
<Text truncate className="small">
{ LocalizeText('achievements.details.reward') }
</Text>
<Flex center className="fw-bold small" gap={ 1 }>
{ achievement.levelRewardPoints }
<CurrencyIcon type={ achievement.levelRewardPointType } />
</Flex>
</Flex> }
{ (achievement.scoreLimit > 0) &&
<Base className="progress" position="relative">
<Flex fit center position="absolute" className="text-black"> { LocalizeText('achievements.details.progress', [ 'progress', 'limit' ], [ (achievement.currentPoints + achievement.scoreAtStartOfLevel).toString(), (achievement.scoreLimit + achievement.scoreAtStartOfLevel).toString() ]) }</Flex>
<Base className="progress-bar" style={ { width: (scaledProgressPercent + '%') }} />
</Base> }
</Column> }
</Column>
</Flex>
)
}

View File

@ -0,0 +1,23 @@
import { AchievementData } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { LayoutGridItem, LayoutGridItemProps } from '../../../../common/layout/LayoutGridItem';
import { AchievementBadgeView } from '../achievement-badge/AchievementBadgeView';
export interface AchievementListItemViewProps extends LayoutGridItemProps
{
achievement: AchievementData;
}
export const AchievementListItemView: FC<AchievementListItemViewProps> = props =>
{
const { achievement = null, children = null, ...rest } = props;
if(!achievement) return null;
return (
<LayoutGridItem itemCount={ achievement.unseen } itemCountMinimum={ 0 } { ...rest }>
<AchievementBadgeView achievement={ achievement } />
{ children }
</LayoutGridItem>
);
}

View File

@ -0,0 +1,26 @@
import { AchievementData } from '@nitrots/nitro-renderer';
import { Dispatch, FC, SetStateAction } from 'react';
import { AutoGrid } from '../../../../common/AutoGrid';
import { AchievementListItemView } from './AchievementListItemView';
export interface AchievementListViewProps
{
achievements: AchievementData[];
selectedAchievementId: number;
setSelectedAchievementId: Dispatch<SetStateAction<number>>;
}
export const AchievementListView: FC<AchievementListViewProps> = props =>
{
const { achievements = null, selectedAchievementId = 0, setSelectedAchievementId = null, children = null } = props;
return (
<AutoGrid columnCount={ 6 } columnMinWidth={ 50 } columnMinHeight={ 50 }>
{ achievements && (achievements.length > 0) && achievements.map((achievement, index) =>
{
return <AchievementListItemView key={ index } achievement={ achievement } itemActive={ (selectedAchievementId === achievement.achievementId) } onClick={ event => setSelectedAchievementId(achievement.achievementId) } />;
}) }
{ children }
</AutoGrid>
);
}

View File

@ -0,0 +1,44 @@
import { FC, useCallback, useMemo } from 'react';
import { GetConfiguration, LocalizeText } from '../../../../api';
import { LayoutGridItem, LayoutGridItemProps } from '../../../../common/layout/LayoutGridItem';
import { LayoutImage } from '../../../../common/layout/LayoutImage';
import { Text } from '../../../../common/Text';
import { AchievementCategory } from '../../common/AchievementCategory';
export interface AchievementCategoryListItemViewProps extends LayoutGridItemProps
{
category: AchievementCategory;
}
export const AchievementsCategoryListItemView: FC<AchievementCategoryListItemViewProps> = props =>
{
const { category = null, ...rest } = props;
const progress = category.getProgress();
const maxProgress = category.getMaxProgress();
const getCategoryImage = useMemo(() =>
{
const imageUrl = GetConfiguration<string>('achievements.images.url');
return imageUrl.replace('%image%', `achcategory_${ category.code }_${ ((progress > 0) ? 'active' : 'inactive') }`);
}, [ category, progress ]);
const getTotalUnseen = useCallback(() =>
{
let unseen = 0;
for(const achievement of category.achievements) unseen += achievement.unseen;
return unseen;
}, [ category ]);
return (
<LayoutGridItem itemCount={ getTotalUnseen() } itemCountMinimum={ 0 } gap={ 1 } { ...rest }>
<Text fullWidth center className="small pt-1">{ LocalizeText(`quests.${ category.code }.name`) }</Text>
<LayoutImage position="relative" imageUrl={ getCategoryImage }>
<Text fullWidth center position="absolute" variant="white" style={ { fontSize: 12, bottom: 9 } }>{ progress } / { maxProgress }</Text>
</LayoutImage>
</LayoutGridItem>
);
}

View File

@ -0,0 +1,23 @@
import { Dispatch, FC, SetStateAction } from 'react';
import { AutoGrid } from '../../../../common/AutoGrid';
import { AchievementCategory } from '../../common/AchievementCategory';
import { AchievementsCategoryListItemView } from './AchievementsCategoryListItemView';
export interface AchievementsCategoryListViewProps
{
categories: AchievementCategory[];
selectedCategoryCode: string;
setSelectedCategoryCode: Dispatch<SetStateAction<string>>;
}
export const AchievementsCategoryListView: FC<AchievementsCategoryListViewProps> = props =>
{
const { categories = null, selectedCategoryCode = null, setSelectedCategoryCode = null, children = null } = props;
return (
<AutoGrid columnCount={ 3 } columnMinWidth={ 90 } columnMinHeight={ 100 }>
{ categories && (categories.length > 0) && categories.map((category, index) => <AchievementsCategoryListItemView key={ index } category={ category } itemActive={ (selectedCategoryCode === category.code) } onClick={ event => setSelectedCategoryCode(category.code) } /> ) }
{ children }
</AutoGrid>
);
};

View File

@ -1,8 +1,14 @@
import { FC, useEffect, useMemo, useState } from 'react';
import { NitroLayoutFlexColumn } from '../../../../layout';
import { Column } from '../../../../common/Column';
import { AchievementCategory } from '../../common/AchievementCategory';
import { AchievementDetailsView } from '../achievement-details/AchievementDetailsView';
import { AchievementListView } from '../achievement-list/AchievementListView';
import { AchievementCategoryViewProps } from './AchievementCategoryView.types';
export class AchievementCategoryViewProps
{
category: AchievementCategory;
setAchievementSeen: (code: string, achievementId: number) => void;
}
export const AchievementCategoryView: FC<AchievementCategoryViewProps> = props =>
{
@ -42,10 +48,10 @@ export const AchievementCategoryView: FC<AchievementCategoryViewProps> = props =
if(!category) return null;
return (
<NitroLayoutFlexColumn className="justify-content-between h-100" gap={ 2 }>
<Column fullHeight justifyContent="between">
<AchievementListView achievements={ category.achievements } selectedAchievementId={ selectedAchievementId } setSelectedAchievementId={ setSelectedAchievementId } />
{ getSelectedAchievement &&
<AchievementDetailsView achievement={ getSelectedAchievement } /> }
</NitroLayoutFlexColumn>
</Column>
);
}

View File

@ -192,7 +192,7 @@
}
&.spotlight {
&.spotlight-icon {
width: 130px;
height: 305px;
background-position: -5px -5px;
@ -212,6 +212,51 @@
}
}
.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;
@ -251,9 +296,11 @@
z-index: 4;
}
.spotlight {
.avatar-spotlight {
position: absolute;
top: -10px;
left: 0;
right: 0;
margin: 0 auto;
opacity: 0.3;
pointer-events: none;
@ -287,71 +334,3 @@
}
}
}
.nitro-wardrobe-grid {
--nitro-grid-column-min-width: 80px !important;
.grid-item {
height: 140px;
max-height: 140px;
background-color: $ghost;
&:after {
position: absolute;
content: '';
top: 75%;
bottom: 0;
left: 0;
right: 0;
border-radius: 50%;
background-color: $gray-chateau;
box-shadow: 0 0 8px 2px rgba($white,.6);
transform: scale(2);
}
.avatar-image {
position: absolute;
bottom: 0;
background-position-y: -23px !important;
z-index: 4;
}
.figure-button-container {
background-color: $gray-chateau;
z-index: 3;
}
}
.grid-item-container {
height: 142px !important;
max-height: 142px !important;
.grid-item {
background-color: $ghost;
.avatar-image {
position: absolute;
bottom: 0;
background-position-y: -23px !important;
z-index: 3;
}
.figure-button-container {
background-color: $gray-chateau;
z-index: 2;
}
&:after {
position: absolute;
content: '';
height: 50%;
bottom: 0;
left: 0;
right: 0;
background-color: $gray-chateau;
box-shadow: 0 0 8px 2px rgba($white,.6);
z-index: 1;
}
}
}
}

View File

@ -1,11 +1,15 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { AvatarEditorFigureCategory, FigureSetIdsMessageEvent, GetWardrobeMessageComposer, IAvatarFigureContainer, UserFigureComposer, UserWardrobePageEvent } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react';
import { GetAvatarRenderManager, GetClubMemberLevel, GetSessionDataManager, LocalizeText } from '../../api';
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { GetAvatarRenderManager, GetClubMemberLevel, GetConfiguration, GetSessionDataManager, LocalizeText } from '../../api';
import { Button } from '../../common/Button';
import { ButtonGroup } from '../../common/ButtonGroup';
import { Column } from '../../common/Column';
import { Grid } from '../../common/Grid';
import { AvatarEditorEvent } from '../../events/avatar-editor';
import { CreateMessageHook, SendMessageHook } from '../../hooks';
import { useUiEvent } from '../../hooks/events/ui/ui-event';
import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView, NitroLayoutGrid, NitroLayoutGridColumn } from '../../layout';
import { AvatarEditorViewProps } from './AvatarEditorView.types';
import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../layout';
import { AvatarEditorAction } from './common/AvatarEditorAction';
import { AvatarEditorUtilities } from './common/AvatarEditorUtilities';
import { BodyModel } from './common/BodyModel';
@ -15,20 +19,14 @@ import { HeadModel } from './common/HeadModel';
import { IAvatarEditorCategoryModel } from './common/IAvatarEditorCategoryModel';
import { LegModel } from './common/LegModel';
import { TorsoModel } from './common/TorsoModel';
import { AvatarEditorFigureActionsView } from './views/figure-actions/AvatarEditorFigureActionsView';
import { AvatarEditorFigurePreviewView } from './views/figure-preview/AvatarEditorFigurePreviewView';
import { AvatarEditorModelView } from './views/model/AvatarEditorModelView';
import { AvatarEditorWardrobeView } from './views/wardrobe/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';
const MAX_SAVED_FIGURES: number = 10;
const ACTION_CLEAR = 'action_clear';
const ACTION_RESET = 'action_reset';
const ACTION_RANDOMIZE = 'action_randomize';
const ACTION_SAVE = 'action_save';
export const AvatarEditorView: FC<AvatarEditorViewProps> = props =>
export const AvatarEditorView: FC<{}> = props =>
{
const [ isVisible, setIsVisible ] = useState(false);
const [ figures, setFigures ] = useState<Map<string, FigureData>>(null);
@ -37,13 +35,15 @@ export const AvatarEditorView: FC<AvatarEditorViewProps> = props =>
const [ activeCategory, setActiveCategory ] = useState<IAvatarEditorCategoryModel>(null);
const [ figureSetIds, setFigureSetIds ] = useState<number[]>([]);
const [ boundFurnitureNames, setBoundFurnitureNames ] = useState<string[]>([]);
const [ savedFigures, setSavedFigures ] = useState<[ IAvatarFigureContainer, string ][]>(new Array(MAX_SAVED_FIGURES));
const [ savedFigures, setSavedFigures ] = useState<[ IAvatarFigureContainer, string ][]>([]);
const [ isWardrobeVisible, setIsWardrobeVisible ] = useState(false);
const [ lastFigure, setLastFigure ] = useState<string>(null);
const [ lastGender, setLastGender ] = useState<string>(null);
const [ needsReset, setNeedsReset ] = useState(false);
const [ isInitalized, setIsInitalized ] = useState(false);
const maxWardrobeSlots = useMemo(() => GetConfiguration<number>('avatar.wardrobe.max.slots', 10), []);
const onAvatarEditorEvent = useCallback((event: AvatarEditorEvent) =>
{
switch(event.type)
@ -89,7 +89,7 @@ export const AvatarEditorView: FC<AvatarEditorViewProps> = props =>
let i = 0;
while(i < MAX_SAVED_FIGURES)
while(i < maxWardrobeSlots)
{
savedFigures.push([ null, null ]);
@ -104,7 +104,7 @@ export const AvatarEditorView: FC<AvatarEditorViewProps> = props =>
}
setSavedFigures(savedFigures)
}, []);
}, [ maxWardrobeSlots ]);
CreateMessageHook(UserWardrobePageEvent, onUserWardrobePageEvent);
@ -195,6 +195,11 @@ export const AvatarEditorView: FC<AvatarEditorViewProps> = props =>
setFigureData(figures.get(gender));
}, [ figures ]);
useEffect(() =>
{
setSavedFigures(new Array(maxWardrobeSlots));
}, [ maxWardrobeSlots ]);
useEffect(() =>
{
if(!isWardrobeVisible) return;
@ -285,18 +290,33 @@ export const AvatarEditorView: FC<AvatarEditorViewProps> = props =>
</NitroCardTabsItemView>
</NitroCardTabsView>
<NitroCardContentView>
<NitroLayoutGrid>
<NitroLayoutGridColumn size={ 9 }>
<Grid>
<Column size={ 9 } overflow="hidden">
{ (activeCategory && !isWardrobeVisible) &&
<AvatarEditorModelView model={ activeCategory } gender={ figureData.gender } setGender={ setGender } /> }
{ isWardrobeVisible &&
<AvatarEditorWardrobeView figureData={ figureData } savedFigures={ savedFigures } setSavedFigures={ setSavedFigures } loadAvatarInEditor={ loadAvatarInEditor } /> }
</NitroLayoutGridColumn>
<NitroLayoutGridColumn overflow="hidden" size={ 3 }>
</Column>
<Column size={ 3 } overflow="hidden">
<AvatarEditorFigurePreviewView figureData={ figureData } />
<AvatarEditorFigureActionsView processAction={ processAction } />
</NitroLayoutGridColumn>
</NitroLayoutGrid>
<Column grow gap={ 1 }>
<ButtonGroup>
<Button variant="secondary" size="sm" onClick={ event => processAction(AvatarEditorAction.ACTION_RESET) }>
<FontAwesomeIcon icon="undo" />
</Button>
<Button variant="secondary" size="sm" onClick={ event => processAction(AvatarEditorAction.ACTION_CLEAR) }>
<FontAwesomeIcon icon="trash" />
</Button>
<Button variant="secondary" size="sm" onClick={ event => processAction(AvatarEditorAction.ACTION_RANDOMIZE) }>
<FontAwesomeIcon icon="dice" />
</Button>
</ButtonGroup>
<Button className="w-100" variant="success" size="sm" onClick={ event => processAction(AvatarEditorAction.ACTION_SAVE) }>
{ LocalizeText('avatareditor.save') }
</Button>
</Column>
</Column>
</Grid>
</NitroCardContentView>
</NitroCardView>
);

Some files were not shown because too many files have changed in this diff Show More