Feature/mystery box (#85)

* create furniture interaction view

* add right-side view for mysterybox

* update dialogue view

* style the mysterybox extension

* fix box overlay blending

* convert mode to enum

Co-authored-by: Bill <billsonnn@users.noreply.github.com>
This commit is contained in:
dank074 2023-01-03 01:04:27 -06:00 committed by GitHub
parent 44f3981ec5
commit 6dc2bbbf8c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 295 additions and 16 deletions

View File

@ -51,4 +51,15 @@ export class ColorUtils
{
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(',') + ')';
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@ -71,7 +71,7 @@ export const LayoutFurniImageView: FC<LayoutFurniImageViewProps> = props =>
if(imageResult)
{
const image = imageResult.getImage();
image.onload = () => setImageElement(image);
}
}, [ productType, productClassId, direction, extraData ]);

View File

@ -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<LayoutPrizeProductImageViewProps> = props =>
{
const { productType = ProductTypeEnum.FLOOR, classId = -1, extraParam = undefined } = props;
switch(productType)
{
case ProductTypeEnum.WALL:
case ProductTypeEnum.FLOOR:
return <LayoutFurniImageView productType={ productType } productClassId={ classId } />
case ProductTypeEnum.BADGE:
return <LayoutBadgeImageView badgeCode={ extraParam }/>
case ProductTypeEnum.HABBO_CLUB:
return <LayoutCurrencyIcon type="hc" />
}
return null;
}

View File

@ -4,6 +4,7 @@ import { OfferView } from '../catalog/views/targeted-offer/OfferView';
import { GroupRoomInformationView } from '../groups/views/GroupRoomInformationView';
import { NotificationCenterView } from '../notification-center/NotificationCenterView';
import { PurseView } from '../purse/PurseView';
import { MysteryBoxExtensionView } from '../room/widgets/mysterybox/MysteryBoxExtensionView';
import { RoomPromotesWidgetView } from '../room/widgets/room-promotes/RoomPromotesWidgetView';
export const RightSideView: FC<{}> = props =>
@ -13,6 +14,7 @@ export const RightSideView: FC<{}> = props =>
<Column position="relative" gap={ 1 }>
<PurseView />
<GroupRoomInformationView />
<MysteryBoxExtensionView />
<OfferView/>
<RoomPromotesWidgetView />
<NotificationCenterView />

View File

@ -106,3 +106,4 @@
@import './context-menu/ContextMenu';
@import './friend-request/FriendRequestDialogView';
@import './furniture/FurnitureWidgets';
@import './mysterybox/MysteryBoxExtensionView';

View File

@ -0,0 +1,81 @@
import { CancelMysteryBoxWaitMessageEvent, GotMysteryBoxPrizeMessageEvent, MysteryBoxWaitingCanceledMessageComposer, ShowMysteryBoxWaitMessageEvent } from '@nitrots/nitro-renderer';
import { FC, useState } from 'react';
import { GetSessionDataManager, LocalizeText, SendMessageComposer } from '../../../../api';
import { Button, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
import { LayoutPrizeProductImageView } from '../../../../common/layout/LayoutPrizeProductImageView';
import { useMessageEvent } from '../../../../hooks';
interface FurnitureMysteryBoxOpenDialogViewProps
{
ownerId: number;
}
type PrizeData = {
contentType:string;
classId:number;
}
enum ViewMode {
HIDDEN,
WAITING,
PRIZE
}
export const FurnitureMysteryBoxOpenDialogView: FC<FurnitureMysteryBoxOpenDialogViewProps> = props =>
{
const { ownerId = -1 } = props;
const [ mode, setMode ] = useState<ViewMode>(ViewMode.HIDDEN);
const [ prizeData, setPrizeData ] = useState<PrizeData>(undefined);
const close = () =>
{
if(mode === ViewMode.WAITING) SendMessageComposer(new MysteryBoxWaitingCanceledMessageComposer(ownerId));
setMode(ViewMode.HIDDEN);
setPrizeData(undefined);
}
useMessageEvent<ShowMysteryBoxWaitMessageEvent>(ShowMysteryBoxWaitMessageEvent, event =>
{
setMode(ViewMode.WAITING);
});
useMessageEvent<CancelMysteryBoxWaitMessageEvent>(CancelMysteryBoxWaitMessageEvent, event =>
{
setMode(ViewMode.HIDDEN);
setPrizeData(undefined);
});
useMessageEvent<GotMysteryBoxPrizeMessageEvent>(GotMysteryBoxPrizeMessageEvent, event =>
{
const parser = event.getParser();
setPrizeData({ contentType: parser.contentType, classId: parser.classId });
setMode(ViewMode.PRIZE);
});
const isOwner = GetSessionDataManager().userId === ownerId;
if(mode === ViewMode.HIDDEN) return null;
return (
<NitroCardView className="nitro-mysterybox-dialog" theme="primary-slim">
<NitroCardHeaderView headerText={ mode === ViewMode.WAITING ? LocalizeText(`mysterybox.dialog.${ isOwner ? 'owner' : 'other' }.title`) : LocalizeText('mysterybox.reward.title') } onCloseClick={ close } />
<NitroCardContentView>
{ mode === ViewMode.WAITING && <>
<Text variant="primary"> { LocalizeText(`mysterybox.dialog.${ isOwner ? 'owner' : 'other' }.subtitle`) } </Text>
<Text> { LocalizeText(`mysterybox.dialog.${ isOwner ? 'owner' : 'other' }.description`) } </Text>
<Text> { LocalizeText(`mysterybox.dialog.${ isOwner ? 'owner' : 'other' }.waiting`) }</Text>
<Button variant="danger" onClick={ close } className="mt-auto"> { LocalizeText(`mysterybox.dialog.${ isOwner ? 'owner' : 'other' }.cancel`) } </Button>
</>
}
{ mode === ViewMode.PRIZE && prizeData && <>
<Text variant="black"> { LocalizeText('mysterybox.reward.text') } </Text>
<Flex className="prize-container justify-content-center mx-auto">
<LayoutPrizeProductImageView classId={ prizeData.classId } productType={ prizeData.contentType }/>
</Flex>
<Button variant="success" onClick={ close } className="mt-auto"> { LocalizeText('mysterybox.reward.close') } </Button>
</>
}
</NitroCardContentView>
</NitroCardView>
);
}

View File

@ -406,6 +406,7 @@
&: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");
}
}
@ -442,8 +443,22 @@
.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;
}
}

View File

@ -1,22 +1,18 @@
import { ContextMenuEnum, CustomUserNotificationMessageEvent, RoomObjectCategory } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { GetGroupInformation, LocalizeText } from '../../../../../api';
import { useFurnitureContextMenuWidget, useMessageEvent, useNotification } from '../../../../../hooks';
import { GetGroupInformation, GetSessionDataManager, LocalizeText } from '../../../../../api';
import { EFFECTBOX_OPEN, GROUP_FURNITURE, MONSTERPLANT_SEED_CONFIRMATION, 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 { EffectBoxConfirmView } from './EffectBoxConfirmView';
import { MonsterPlantSeedConfirmView } from './MonsterPlantSeedConfirmView';
import { PurchasableClothingConfirmView } from './PurchasableClothingConfirmView';
const MONSTERPLANT_SEED_CONFIRMATION: string = 'MONSTERPLANT_SEED_CONFIRMATION';
const PURCHASABLE_CLOTHING_CONFIRMATION: string = 'PURCHASABLE_CLOTHING_CONFIRMATION';
const GROUP_FURNITURE: string = 'GROUP_FURNITURE';
const EFFECTBOX_OPEN: string = 'EFFECTBOX_OPEN';
export const FurnitureContextMenuView: FC<{}> = props =>
{
const { closeConfirm = null, processAction = null, onClose = null, objectId = -1, mode = null, confirmMode = null, confirmingObjectId = -1, groupData = null, isGroupMember = false } = useFurnitureContextMenuWidget();
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>(CustomUserNotificationMessageEvent, event =>
@ -41,6 +37,8 @@ export const FurnitureContextMenuView: FC<{}> = props =>
}
});
const isOwner = GetSessionDataManager().userId === objectOwnerId;
return (
<>
{ (confirmMode === MONSTERPLANT_SEED_CONFIRMATION) &&
@ -49,6 +47,7 @@ export const FurnitureContextMenuView: FC<{}> = props =>
<PurchasableClothingConfirmView objectId={ confirmingObjectId } onClose={ closeConfirm } /> }
{ (confirmMode === EFFECTBOX_OPEN) &&
<EffectBoxConfirmView objectId={ confirmingObjectId } onClose={ closeConfirm } /> }
<FurnitureMysteryBoxOpenDialogView ownerId={ objectOwnerId } />
{ (objectId >= 0) && mode &&
<ContextMenuView objectId={ objectId } category={ RoomObjectCategory.FLOOR } onClose={ onClose } fades={ true }>
{ (mode === ContextMenuEnum.FRIEND_FURNITURE) &&
@ -87,6 +86,15 @@ export const FurnitureContextMenuView: FC<{}> = props =>
{ LocalizeText('widget.generic_usable.button.use') }
</ContextMenuListItemView>
</> }
{ (mode === ContextMenuEnum.MYSTERY_BOX) &&
<>
<ContextMenuHeaderView>
{ LocalizeText('mysterybox.context.title') }
</ContextMenuHeaderView>
<ContextMenuListItemView onClick={ event => processAction('use_mystery_box') }>
{ LocalizeText('mysterybox.context.' + ((isOwner) ? 'owner' : 'other') + '.use') }
</ContextMenuListItemView>
</> }
{ (mode === GROUP_FURNITURE) && groupData &&
<>
<ContextMenuHeaderView className="cursor-pointer text-truncate" onClick={ () => GetGroupInformation(groupData.guildId) }>

View File

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

View File

@ -0,0 +1,66 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { MysteryBoxKeysUpdateEvent } from '@nitrots/nitro-renderer';
import { FC, useState } from 'react';
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<boolean>(true);
const [ keyColor, setKeyColor ] = useState<string>('');
const [ boxColor, setBoxColor ] = useState<string>('');
useSessionDataManagerEvent<MysteryBoxKeysUpdateEvent>(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 (
<Base className="nitro-notification-bubble rounded mysterybox-extension">
<Column>
<Flex alignItems="center" justifyContent="between" pointer onClick={ event => setIsOpen(value => !value) }>
<Text variant="white">{ LocalizeText('mysterybox.tracker.title') }</Text>
<FontAwesomeIcon icon={ isOpen ? 'chevron-up' : 'chevron-down' } />
</Flex>
{ isOpen &&
<>
<Text variant="white">{ LocalizeText('mysterybox.tracker.description') }</Text>
<Flex justifyContent="center" alignItems="center" gap={ 2 }>
<LayoutGridItem className="mysterybox-container">
<div className="box-image flex-shrink-0" style={ { backgroundColor: getRgbColor(boxColor) } }>
<div className="chain-overlay-image" />
</div>
</LayoutGridItem>
<LayoutGridItem className="mysterybox-container">
<div className="key-image flex-shrink-0" style={ { backgroundColor: getRgbColor(keyColor ) } }>
<div className="key-overlay-image" />
</div>
</LayoutGridItem>
</Flex>
</> }
</Column>
</Base>
);
}

View File

@ -1,13 +1,13 @@
import { ContextMenuEnum, GroupFurniContextMenuInfoMessageEvent, GroupFurniContextMenuInfoMessageParser, RoomEngineTriggerWidgetEvent, RoomObjectCategory } from '@nitrots/nitro-renderer';
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';
const MONSTERPLANT_SEED_CONFIRMATION: string = 'MONSTERPLANT_SEED_CONFIRMATION';
const PURCHASABLE_CLOTHING_CONFIRMATION: string = 'PURCHASABLE_CLOTHING_CONFIRMATION';
const GROUP_FURNITURE: string = 'GROUP_FURNITURE';
const EFFECTBOX_OPEN: string = 'EFFECTBOX_OPEN';
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';
const useFurnitureContextMenuWidgetState = () =>
{
@ -17,6 +17,7 @@ const useFurnitureContextMenuWidgetState = () =>
const [ confirmingObjectId, setConfirmingObjectId ] = useState(-1);
const [ groupData, setGroupData ] = useState<GroupFurniContextMenuInfoMessageParser>(null);
const [ isGroupMember, setIsGroupMember ] = useState(false);
const [ objectOwnerId, setObjectOwnerId ] = useState(-1);
const { roomSession = null } = useRoom();
const onClose = () =>
@ -53,6 +54,9 @@ const useFurnitureContextMenuWidgetState = () =>
setConfirmMode(PURCHASABLE_CLOTHING_CONFIRMATION);
setConfirmingObjectId(objectId);
break;
case 'use_mystery_box':
roomSession.useMultistateItem(objectId);
break;
case 'join_group':
TryJoinGroup(groupData.guildId);
setIsGroupMember(true);
@ -71,13 +75,16 @@ const useFurnitureContextMenuWidgetState = () =>
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_EFFECTBOX_OPEN_DIALOG,
RoomEngineTriggerWidgetEvent.REQUEST_MYSTERYBOX_OPEN_DIALOG
], event =>
{
const object = GetRoomEngine().getRoomObject(roomSession.roomId, event.objectId, event.category);
if(!object) return;
setObjectOwnerId(object.model.getValue<number>(RoomObjectVariable.FURNITURE_OWNER_ID));
switch(event.type)
{
case RoomEngineTriggerWidgetEvent.REQUEST_MONSTERPLANT_SEED_PLANT_CONFIRMATION_DIALOG:
@ -102,6 +109,11 @@ const useFurnitureContextMenuWidgetState = () =>
setConfirmingObjectId(object.id);
setConfirmMode(PURCHASABLE_CLOTHING_CONFIRMATION);
onClose();
return;
case RoomEngineTriggerWidgetEvent.REQUEST_MYSTERYBOX_OPEN_DIALOG:
roomSession.useMultistateItem(object.id);
onClose();
return;
case RoomEngineTriggerWidgetEvent.OPEN_FURNI_CONTEXT_MENU:
@ -117,6 +129,7 @@ const useFurnitureContextMenuWidgetState = () =>
if(IsOwnerOfFurniture(object)) setMode(ContextMenuEnum.MONSTERPLANT_SEED);
return;
case ContextMenuEnum.MYSTERY_BOX:
setMode(ContextMenuEnum.MYSTERY_BOX);
return;
case ContextMenuEnum.RANDOM_TELEPORT:
setMode(ContextMenuEnum.RANDOM_TELEPORT);
@ -143,7 +156,7 @@ const useFurnitureContextMenuWidgetState = () =>
setMode(GROUP_FURNITURE);
});
return { objectId, mode, confirmMode, confirmingObjectId, groupData, isGroupMember, closeConfirm, processAction, onClose };
return { objectId, mode, confirmMode, confirmingObjectId, groupData, isGroupMember, objectOwnerId, closeConfirm, processAction, onClose };
}
export const useFurnitureContextMenuWidget = useFurnitureContextMenuWidgetState;