mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 15:36:18 +00:00
🆙 Init V3
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import { FC } from 'react';
|
||||
import { FaTimes } from 'react-icons/fa';
|
||||
import { AvatarInfoUser, LocalizeText } from '../../../../../api';
|
||||
import { Column, Flex, LayoutAvatarImageView, LayoutBadgeImageView, Text } from '../../../../../common';
|
||||
|
||||
interface InfoStandWidgetBotViewProps
|
||||
{
|
||||
avatarInfo: AvatarInfoUser;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const InfoStandWidgetBotView: FC<InfoStandWidgetBotViewProps> = props =>
|
||||
{
|
||||
const { avatarInfo = null, onClose = null } = props;
|
||||
|
||||
if(!avatarInfo) return null;
|
||||
|
||||
return (
|
||||
<Column className="nitro-infostand rounded">
|
||||
<Column className="container-fluid content-area" gap={ 1 } overflow="visible">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Flex alignItems="center" gap={ 1 } justifyContent="between">
|
||||
<Text small wrap variant="white">{ avatarInfo.name }</Text>
|
||||
<FaTimes className="cursor-pointer fa-icon" onClick={ onClose } />
|
||||
</Flex>
|
||||
<hr className="m-0" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex gap-1">
|
||||
<Column fullWidth className="body-image bot">
|
||||
<LayoutAvatarImageView direction={ 4 } figure={ avatarInfo.figure } />
|
||||
</Column>
|
||||
<Column center grow gap={ 0 }>
|
||||
{ (avatarInfo.badges.length > 0) && avatarInfo.badges.map(result =>
|
||||
{
|
||||
return <LayoutBadgeImageView key={ result } badgeCode={ result } showInfo={ true } />;
|
||||
}) }
|
||||
</Column>
|
||||
</div>
|
||||
<hr className="m-0" />
|
||||
</div>
|
||||
<Flex alignItems="center" className="bg-light-dark rounded py-1 px-2">
|
||||
<Text fullWidth small textBreak wrap className="min-h-[18px]" variant="white">{ avatarInfo.motto }</Text>
|
||||
</Flex>
|
||||
{ (avatarInfo.carryItem > 0) &&
|
||||
<div className="flex flex-col gap-1">
|
||||
<hr className="m-0" />
|
||||
<Text small wrap variant="white">
|
||||
{ LocalizeText('infostand.text.handitem', [ 'item' ], [ LocalizeText('handitem' + avatarInfo.carryItem) ]) }
|
||||
</Text>
|
||||
</div> }
|
||||
</Column>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,475 @@
|
||||
import { CrackableDataType, CreateLinkEvent, GetRoomEngine, GetSoundManager, GroupInformationComposer, GroupInformationEvent, NowPlayingEvent, RoomControllerLevel, RoomObjectCategory, RoomObjectOperationType, RoomObjectVariable, RoomWidgetEnumItemExtradataParameter, RoomWidgetFurniInfoUsagePolicyEnum, SetObjectDataMessageComposer, SongInfoReceivedEvent, StringDataType } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { FaTimes } from 'react-icons/fa';
|
||||
import { AvatarInfoFurni, GetGroupInformation, LocalizeText, SendMessageComposer } from '../../../../../api';
|
||||
import { Button, Column, Flex, LayoutBadgeImageView, LayoutLimitedEditionCompactPlateView, LayoutRarityLevelView, LayoutRoomObjectImageView, Text, UserProfileIconView } from '../../../../../common';
|
||||
import { useMessageEvent, useNitroEvent, useRoom } from '../../../../../hooks';
|
||||
import { NitroInput } from '../../../../../layout';
|
||||
|
||||
interface InfoStandWidgetFurniViewProps
|
||||
{
|
||||
avatarInfo: AvatarInfoFurni;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const PICKUP_MODE_NONE: number = 0;
|
||||
const PICKUP_MODE_EJECT: number = 1;
|
||||
const PICKUP_MODE_FULL: number = 2;
|
||||
|
||||
export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props =>
|
||||
{
|
||||
const { avatarInfo = null, onClose = null } = props;
|
||||
const { roomSession = null } = useRoom();
|
||||
|
||||
const [ pickupMode, setPickupMode ] = useState(0);
|
||||
const [ canMove, setCanMove ] = useState(false);
|
||||
const [ canRotate, setCanRotate ] = useState(false);
|
||||
const [ canUse, setCanUse ] = useState(false);
|
||||
const [ furniKeys, setFurniKeys ] = useState<string[]>([]);
|
||||
const [ furniValues, setFurniValues ] = useState<string[]>([]);
|
||||
const [ customKeys, setCustomKeys ] = useState<string[]>([]);
|
||||
const [ customValues, setCustomValues ] = useState<string[]>([]);
|
||||
const [ isCrackable, setIsCrackable ] = useState(false);
|
||||
const [ crackableHits, setCrackableHits ] = useState(0);
|
||||
const [ crackableTarget, setCrackableTarget ] = useState(0);
|
||||
const [ godMode, setGodMode ] = useState(false);
|
||||
const [ canSeeFurniId, setCanSeeFurniId ] = useState(false);
|
||||
const [ groupName, setGroupName ] = useState<string>(null);
|
||||
const [ isJukeBox, setIsJukeBox ] = useState<boolean>(false);
|
||||
const [ isSongDisk, setIsSongDisk ] = useState<boolean>(false);
|
||||
const [ songId, setSongId ] = useState<number>(-1);
|
||||
const [ songName, setSongName ] = useState<string>('');
|
||||
const [ songCreator, setSongCreator ] = useState<string>('');
|
||||
|
||||
useNitroEvent<NowPlayingEvent>(NowPlayingEvent.NPE_SONG_CHANGED, event =>
|
||||
{
|
||||
setSongId(event.id);
|
||||
}, (isJukeBox || isSongDisk));
|
||||
|
||||
useNitroEvent<NowPlayingEvent>(SongInfoReceivedEvent.SIR_TRAX_SONG_INFO_RECEIVED, event =>
|
||||
{
|
||||
if(event.id !== songId) return;
|
||||
|
||||
const songInfo = GetSoundManager().musicController.getSongInfo(event.id);
|
||||
|
||||
if(!songInfo) return;
|
||||
|
||||
setSongName(songInfo.name);
|
||||
setSongCreator(songInfo.creator);
|
||||
}, (isJukeBox || isSongDisk));
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
let pickupMode = PICKUP_MODE_NONE;
|
||||
let canMove = false;
|
||||
let canRotate = false;
|
||||
let canUse = false;
|
||||
let furniKeyss: string[] = [];
|
||||
let furniValuess: string[] = [];
|
||||
let customKeyss: string[] = [];
|
||||
let customValuess: string[] = [];
|
||||
let isCrackable = false;
|
||||
let crackableHits = 0;
|
||||
let crackableTarget = 0;
|
||||
let godMode = false;
|
||||
let canSeeFurniId = false;
|
||||
let furniIsJukebox = false;
|
||||
let furniIsSongDisk = false;
|
||||
let furniSongId = -1;
|
||||
|
||||
const isValidController = (avatarInfo.roomControllerLevel >= RoomControllerLevel.GUEST);
|
||||
|
||||
if(isValidController || avatarInfo.isOwner || avatarInfo.isRoomOwner || avatarInfo.isAnyRoomController)
|
||||
{
|
||||
canMove = true;
|
||||
canRotate = !avatarInfo.isWallItem;
|
||||
|
||||
if(avatarInfo.roomControllerLevel >= RoomControllerLevel.MODERATOR) godMode = true;
|
||||
}
|
||||
|
||||
if(avatarInfo.isAnyRoomController)
|
||||
{
|
||||
canSeeFurniId = true;
|
||||
}
|
||||
|
||||
if((((avatarInfo.usagePolicy === RoomWidgetFurniInfoUsagePolicyEnum.EVERYBODY) || ((avatarInfo.usagePolicy === RoomWidgetFurniInfoUsagePolicyEnum.CONTROLLER) && isValidController)) || ((avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.JUKEBOX) && isValidController)) || ((avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.USABLE_PRODUCT) && isValidController)) canUse = true;
|
||||
|
||||
if(avatarInfo.extraParam)
|
||||
{
|
||||
if(avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.CRACKABLE_FURNI)
|
||||
{
|
||||
const stuffData = (avatarInfo.stuffData as CrackableDataType);
|
||||
|
||||
canUse = true;
|
||||
isCrackable = true;
|
||||
crackableHits = stuffData.hits;
|
||||
crackableTarget = stuffData.target;
|
||||
}
|
||||
|
||||
else if(avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.JUKEBOX)
|
||||
{
|
||||
const playlist = GetSoundManager().musicController.getRoomItemPlaylist();
|
||||
|
||||
if(playlist)
|
||||
{
|
||||
furniSongId = playlist.currentSongId;
|
||||
}
|
||||
|
||||
furniIsJukebox = true;
|
||||
}
|
||||
|
||||
else if(avatarInfo.extraParam.indexOf(RoomWidgetEnumItemExtradataParameter.SONGDISK) === 0)
|
||||
{
|
||||
furniSongId = parseInt(avatarInfo.extraParam.substr(RoomWidgetEnumItemExtradataParameter.SONGDISK.length));
|
||||
|
||||
furniIsSongDisk = true;
|
||||
}
|
||||
|
||||
if(godMode)
|
||||
{
|
||||
const extraParam = avatarInfo.extraParam.substr(RoomWidgetEnumItemExtradataParameter.BRANDING_OPTIONS.length);
|
||||
|
||||
if(extraParam)
|
||||
{
|
||||
const parts = extraParam.split('\t');
|
||||
|
||||
for(const part of parts)
|
||||
{
|
||||
const value = part.split('=');
|
||||
|
||||
if(value && (value.length === 2))
|
||||
{
|
||||
furniKeyss.push(value[0]);
|
||||
furniValuess.push(value[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(godMode)
|
||||
{
|
||||
const roomObject = GetRoomEngine().getRoomObject(roomSession.roomId, avatarInfo.id, (avatarInfo.isWallItem) ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR);
|
||||
|
||||
if(roomObject)
|
||||
{
|
||||
const customVariables = roomObject.model.getValue<string[]>(RoomObjectVariable.FURNITURE_CUSTOM_VARIABLES);
|
||||
const furnitureData = roomObject.model.getValue<{ [index: string]: string }>(RoomObjectVariable.FURNITURE_DATA);
|
||||
|
||||
if(customVariables && customVariables.length)
|
||||
{
|
||||
for(const customVariable of customVariables)
|
||||
{
|
||||
customKeyss.push(customVariable);
|
||||
customValuess.push((furnitureData[customVariable]) || '');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(avatarInfo.isOwner || avatarInfo.isAnyRoomController) pickupMode = PICKUP_MODE_FULL;
|
||||
|
||||
else if(avatarInfo.isRoomOwner || (avatarInfo.roomControllerLevel >= RoomControllerLevel.GUILD_ADMIN)) pickupMode = PICKUP_MODE_EJECT;
|
||||
|
||||
if(avatarInfo.isStickie) pickupMode = PICKUP_MODE_NONE;
|
||||
|
||||
setPickupMode(pickupMode);
|
||||
setCanMove(canMove);
|
||||
setCanRotate(canRotate);
|
||||
setCanUse(canUse);
|
||||
setFurniKeys(furniKeyss);
|
||||
setFurniValues(furniValuess);
|
||||
setCustomKeys(customKeyss);
|
||||
setCustomValues(customValuess);
|
||||
setIsCrackable(isCrackable);
|
||||
setCrackableHits(crackableHits);
|
||||
setCrackableTarget(crackableTarget);
|
||||
setGodMode(godMode);
|
||||
setCanSeeFurniId(canSeeFurniId);
|
||||
setGroupName(null);
|
||||
setIsJukeBox(furniIsJukebox);
|
||||
setIsSongDisk(furniIsSongDisk);
|
||||
setSongId(furniSongId);
|
||||
|
||||
if(avatarInfo.groupId) SendMessageComposer(new GroupInformationComposer(avatarInfo.groupId, false));
|
||||
}, [ roomSession, avatarInfo ]);
|
||||
|
||||
useMessageEvent<GroupInformationEvent>(GroupInformationEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
if(!avatarInfo || avatarInfo.groupId !== parser.id || parser.flag) return;
|
||||
|
||||
if(groupName) setGroupName(null);
|
||||
|
||||
setGroupName(parser.title);
|
||||
});
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
const songInfo = GetSoundManager().musicController.getSongInfo(songId);
|
||||
|
||||
setSongName(songInfo?.name ?? '');
|
||||
setSongCreator(songInfo?.creator ?? '');
|
||||
}, [ songId ]);
|
||||
|
||||
const onFurniSettingChange = useCallback((index: number, value: string) =>
|
||||
{
|
||||
const clone = Array.from(furniValues);
|
||||
|
||||
clone[index] = value;
|
||||
|
||||
setFurniValues(clone);
|
||||
}, [ furniValues ]);
|
||||
|
||||
const onCustomVariableChange = useCallback((index: number, value: string) =>
|
||||
{
|
||||
const clone = Array.from(customValues);
|
||||
|
||||
clone[index] = value;
|
||||
|
||||
setCustomValues(clone);
|
||||
}, [ customValues ]);
|
||||
|
||||
const getFurniSettingsAsString = useCallback(() =>
|
||||
{
|
||||
if(furniKeys.length === 0 || furniValues.length === 0) return '';
|
||||
|
||||
let data = '';
|
||||
|
||||
let i = 0;
|
||||
|
||||
while(i < furniKeys.length)
|
||||
{
|
||||
const key = furniKeys[i];
|
||||
const value = furniValues[i];
|
||||
|
||||
data = (data + (key + '=' + value + '\t'));
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
return data;
|
||||
}, [ furniKeys, furniValues ]);
|
||||
|
||||
const processButtonAction = useCallback((action: string) =>
|
||||
{
|
||||
if(!action || (action === '')) return;
|
||||
|
||||
let objectData: string = null;
|
||||
|
||||
switch(action)
|
||||
{
|
||||
case 'buy_one':
|
||||
CreateLinkEvent(`catalog/open/offerId/${ avatarInfo.purchaseOfferId }`);
|
||||
return;
|
||||
case 'move':
|
||||
GetRoomEngine().processRoomObjectOperation(avatarInfo.id, avatarInfo.category, RoomObjectOperationType.OBJECT_MOVE);
|
||||
break;
|
||||
case 'rotate':
|
||||
GetRoomEngine().processRoomObjectOperation(avatarInfo.id, avatarInfo.category, RoomObjectOperationType.OBJECT_ROTATE_POSITIVE);
|
||||
break;
|
||||
case 'pickup':
|
||||
if(pickupMode === PICKUP_MODE_FULL)
|
||||
{
|
||||
GetRoomEngine().processRoomObjectOperation(avatarInfo.id, avatarInfo.category, RoomObjectOperationType.OBJECT_PICKUP);
|
||||
}
|
||||
else
|
||||
{
|
||||
GetRoomEngine().processRoomObjectOperation(avatarInfo.id, avatarInfo.category, RoomObjectOperationType.OBJECT_EJECT);
|
||||
}
|
||||
break;
|
||||
case 'use':
|
||||
GetRoomEngine().useRoomObject(avatarInfo.id, avatarInfo.category);
|
||||
break;
|
||||
case 'save_branding_configuration': {
|
||||
const mapData = new Map<string, string>();
|
||||
const dataParts = getFurniSettingsAsString().split('\t');
|
||||
|
||||
if(dataParts)
|
||||
{
|
||||
for(const part of dataParts)
|
||||
{
|
||||
const [ key, value ] = part.split('=', 2);
|
||||
|
||||
mapData.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
GetRoomEngine().modifyRoomObjectDataWithMap(avatarInfo.id, avatarInfo.category, RoomObjectOperationType.OBJECT_SAVE_STUFF_DATA, mapData);
|
||||
break;
|
||||
}
|
||||
case 'save_custom_variables': {
|
||||
const map = new Map();
|
||||
|
||||
for(let i = 0; i < customKeys.length; i++)
|
||||
{
|
||||
const key = customKeys[i];
|
||||
const value = customValues[i];
|
||||
|
||||
if((key && key.length) && (value && value.length)) map.set(key, value);
|
||||
}
|
||||
|
||||
SendMessageComposer(new SetObjectDataMessageComposer(avatarInfo.id, map));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [ avatarInfo, pickupMode, customKeys, customValues, getFurniSettingsAsString ]);
|
||||
|
||||
const getGroupBadgeCode = useCallback(() =>
|
||||
{
|
||||
const stringDataType = (avatarInfo.stuffData as StringDataType);
|
||||
|
||||
if(!stringDataType || !(stringDataType instanceof StringDataType)) return null;
|
||||
|
||||
return stringDataType.getValue(2);
|
||||
}, [ avatarInfo ]);
|
||||
|
||||
if(!avatarInfo) return null;
|
||||
|
||||
return (
|
||||
<Column alignItems="end" gap={ 1 }>
|
||||
<Column className="relative min-w-[190px] max-w-[190px] z-30 pointer-events-auto bg-[rgba(28,_28,_32,_.95)] [box-shadow:inset_0_5px_#22222799,_inset_0_-4px_#12121599] rounded">
|
||||
<Column className="h-full p-[8px] overflow-auto" gap={ 1 } overflow="visible">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Flex alignItems="center" gap={ 1 } justifyContent="between">
|
||||
<Text small wrap variant="white">{ avatarInfo.name }</Text>
|
||||
<FaTimes className="cursor-pointer fa-icon" onClick={ onClose } />
|
||||
</Flex>
|
||||
<hr className="m-0 bg-[#0003] border-[0] opacity-[.5] h-px" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Flex gap={ 1 } position="relative">
|
||||
{ avatarInfo.stuffData.isUnique &&
|
||||
<div className="absolute end-0">
|
||||
<LayoutLimitedEditionCompactPlateView uniqueNumber={ avatarInfo.stuffData.uniqueNumber } uniqueSeries={ avatarInfo.stuffData.uniqueSeries } />
|
||||
</div> }
|
||||
{ (avatarInfo.stuffData.rarityLevel > -1) &&
|
||||
<div className="absolute end-0">
|
||||
<LayoutRarityLevelView level={ avatarInfo.stuffData.rarityLevel } />
|
||||
</div> }
|
||||
<Flex center fullWidth>
|
||||
<LayoutRoomObjectImageView category={ avatarInfo.category } objectId={ avatarInfo.id } roomId={ roomSession.roomId } />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<hr className="m-0 bg-[#0003] border-[0] opacity-[.5] h-px" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text fullWidth small textBreak wrap variant="white">{ avatarInfo.description }</Text>
|
||||
<hr className="m-0 bg-[#0003] border-[0] opacity-[.5] h-px" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<UserProfileIconView userId={ avatarInfo.ownerId } />
|
||||
<Text small wrap variant="white">
|
||||
{ LocalizeText('furni.owner', [ 'name' ], [ avatarInfo.ownerName ]) }
|
||||
</Text>
|
||||
</div>
|
||||
{ (avatarInfo.purchaseOfferId > 0) &&
|
||||
<Flex>
|
||||
<Text pointer small underline variant="white" onClick={ event => processButtonAction('buy_one') }>
|
||||
{ LocalizeText('infostand.button.buy') }
|
||||
</Text>
|
||||
</Flex> }
|
||||
</div>
|
||||
{ (isJukeBox || isSongDisk) &&
|
||||
<div className="flex flex-col gap-1">
|
||||
<hr className="m-0 bg-[#0003] border-[0] opacity-[.5] h-px" />
|
||||
{ (songId === -1) &&
|
||||
<Text small wrap variant="white">
|
||||
{ LocalizeText('infostand.jukebox.text.not.playing') }
|
||||
</Text> }
|
||||
{ !!songName.length &&
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="icon disk-icon" />
|
||||
<Text small wrap variant="white">
|
||||
{ songName }
|
||||
</Text>
|
||||
</div> }
|
||||
{ !!songCreator.length &&
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="icon disk-creator" />
|
||||
<Text small wrap variant="white">
|
||||
{ songCreator }
|
||||
</Text>
|
||||
</div> }
|
||||
</div> }
|
||||
<div className="flex flex-col gap-1">
|
||||
{ isCrackable &&
|
||||
<>
|
||||
<hr className="m-0 bg-[#0003] border-[0] opacity-[.5] h-px" />
|
||||
<Text small wrap variant="white">{ LocalizeText('infostand.crackable_furni.hits_remaining', [ 'hits', 'target' ], [ crackableHits.toString(), crackableTarget.toString() ]) }</Text>
|
||||
</> }
|
||||
{ avatarInfo.groupId > 0 &&
|
||||
<>
|
||||
<hr className="m-0 bg-[#0003] border-[0] opacity-[.5] h-px" />
|
||||
<Flex pointer alignItems="center" gap={ 2 } onClick={ () => GetGroupInformation(avatarInfo.groupId) }>
|
||||
<LayoutBadgeImageView badgeCode={ getGroupBadgeCode() } isGroup={ true } />
|
||||
<Text underline variant="white">{ groupName }</Text>
|
||||
</Flex>
|
||||
</> }
|
||||
{ godMode &&
|
||||
<>
|
||||
<hr className="m-0 bg-[#0003] border-[0] opacity-[.5] h-px" />
|
||||
{ canSeeFurniId && <Text small wrap variant="white">ID: { avatarInfo.id }</Text> }
|
||||
{ (furniKeys.length > 0) &&
|
||||
<>
|
||||
<hr className="m-0 bg-[#0003] border-[0] opacity-[.5] h-px" />
|
||||
<div className="flex flex-col gap-1">
|
||||
{ furniKeys.map((key, index) =>
|
||||
{
|
||||
return (
|
||||
<Flex key={ index } alignItems="center" gap={ 1 }>
|
||||
<Text small wrap align="end" className="col-span-4" variant="white">{ key }</Text>
|
||||
<NitroInput type="text" value={ furniValues[index] } onChange={ event => onFurniSettingChange(index, event.target.value) } />
|
||||
</Flex>);
|
||||
}) }
|
||||
</div>
|
||||
</> }
|
||||
</> }
|
||||
{ (customKeys.length > 0) &&
|
||||
<>
|
||||
<hr className="m-0 bg-[#0003] border-[0] opacity-[.5] h-px" />
|
||||
<div className="flex flex-col gap-1">
|
||||
{ customKeys.map((key, index) =>
|
||||
{
|
||||
return (
|
||||
<Flex key={ index } alignItems="center" gap={ 1 }>
|
||||
<Text small wrap align="end" className="col-span-4" variant="white">{ key }</Text>
|
||||
<NitroInput type="text" value={ customValues[index] } onChange={ event => onCustomVariableChange(index, event.target.value) } />
|
||||
</Flex>);
|
||||
}) }
|
||||
</div>
|
||||
</> }
|
||||
</div>
|
||||
</Column>
|
||||
</Column>
|
||||
<Flex gap={ 1 } justifyContent="end">
|
||||
{ canMove &&
|
||||
<Button variant="dark" onClick={ event => processButtonAction('move') }>
|
||||
{ LocalizeText('infostand.button.move') }
|
||||
</Button> }
|
||||
{ canRotate &&
|
||||
<Button variant="dark" onClick={ event => processButtonAction('rotate') }>
|
||||
{ LocalizeText('infostand.button.rotate') }
|
||||
</Button> }
|
||||
{ (pickupMode !== PICKUP_MODE_NONE) &&
|
||||
<Button variant="dark" onClick={ event => processButtonAction('pickup') }>
|
||||
{ LocalizeText((pickupMode === PICKUP_MODE_EJECT) ? 'infostand.button.eject' : 'infostand.button.pickup') }
|
||||
</Button> }
|
||||
{ canUse &&
|
||||
<Button variant="dark" onClick={ event => processButtonAction('use') }>
|
||||
{ LocalizeText('infostand.button.use') }
|
||||
</Button> }
|
||||
{ ((furniKeys.length > 0 && furniValues.length > 0) && (furniKeys.length === furniValues.length)) &&
|
||||
<Button variant="dark" onClick={ () => processButtonAction('save_branding_configuration') }>
|
||||
{ LocalizeText('save') }
|
||||
</Button> }
|
||||
{ ((customKeys.length > 0 && customValues.length > 0) && (customKeys.length === customValues.length)) &&
|
||||
<Button variant="dark" onClick={ () => processButtonAction('save_custom_variables') }>
|
||||
{ LocalizeText('save') }
|
||||
</Button> }
|
||||
</Flex>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,343 @@
|
||||
import { CreateLinkEvent, PetRespectComposer, PetType } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useState, useCallback } from 'react';
|
||||
import { FaTimes } from 'react-icons/fa';
|
||||
import { AvatarInfoPet, ConvertSeconds, GetConfigurationValue, LocalizeText, SendMessageComposer } from '../../../../../api';
|
||||
import { Button, Column, Flex, LayoutCounterTimeView, LayoutPetImageView, LayoutRarityLevelView, Text, UserProfileIconView } from '../../../../../common';
|
||||
import { useRoom, useSessionInfo } from '../../../../../hooks';
|
||||
|
||||
// TypeScript interface for AvatarInfoPet
|
||||
interface AvatarInfoPet {
|
||||
id: number;
|
||||
name: string;
|
||||
petType: number;
|
||||
petBreed: number;
|
||||
petFigure: string;
|
||||
posture: string;
|
||||
level: number;
|
||||
maximumLevel: number;
|
||||
age: number;
|
||||
ownerId: number;
|
||||
ownerName: string;
|
||||
respect: number;
|
||||
dead?: boolean;
|
||||
energy?: number;
|
||||
maximumEnergy?: number;
|
||||
happyness?: number;
|
||||
maximumHappyness?: number;
|
||||
experience?: number;
|
||||
levelExperienceGoal?: number;
|
||||
remainingGrowTime?: number;
|
||||
remainingTimeToLive?: number;
|
||||
maximumTimeToLive?: number;
|
||||
rarityLevel?: number;
|
||||
isOwner?: boolean;
|
||||
}
|
||||
|
||||
interface InfoStandWidgetPetViewProps {
|
||||
avatarInfo: AvatarInfoPet;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const PetHeader: FC<{ name: string; petType: number; petBreed: number; onClose: () => void }> = ({ name, petType, petBreed, onClose }) => (
|
||||
<div className="flex flex-col gap-1">
|
||||
<Flex alignItems="center" gap={1} justifyContent="between">
|
||||
<Text small wrap variant="white">
|
||||
{name}
|
||||
</Text>
|
||||
<FaTimes
|
||||
className="cursor-pointer fa-icon"
|
||||
onClick={onClose}
|
||||
aria-label={LocalizeText('generic.close')}
|
||||
title={LocalizeText('generic.close')}
|
||||
/>
|
||||
</Flex>
|
||||
<Text small wrap variant="white">
|
||||
{LocalizeText(`pet.breed.${petType}.${petBreed}`)}
|
||||
</Text>
|
||||
<hr className="m-0" />
|
||||
</div>
|
||||
);
|
||||
|
||||
const MonsterplantStats: FC<{
|
||||
avatarInfo: AvatarInfoPet;
|
||||
remainingGrowTime: number;
|
||||
remainingTimeToLive: number;
|
||||
}> = ({ avatarInfo, remainingGrowTime, remainingTimeToLive }) => (
|
||||
<>
|
||||
<Column center gap={1}>
|
||||
<LayoutPetImageView direction={4} figure={avatarInfo.petFigure} posture={avatarInfo.posture} />
|
||||
<hr className="m-0" />
|
||||
</Column>
|
||||
<div className="flex flex-col gap-2">
|
||||
{!avatarInfo.dead && (
|
||||
<Column alignItems="center" gap={1}>
|
||||
<Text center small wrap variant="white">
|
||||
{LocalizeText('pet.level', ['level', 'maxlevel'], [avatarInfo.level.toString(), avatarInfo.maximumLevel.toString()])}
|
||||
</Text>
|
||||
</Column>
|
||||
)}
|
||||
<Column alignItems="center" gap={1}>
|
||||
<Text small truncate variant="white">
|
||||
{LocalizeText('infostand.pet.text.wellbeing')}
|
||||
</Text>
|
||||
<div className="bg-light-dark rounded relative overflow-hidden w-full">
|
||||
<div className="flex justify-center items-center size-full absolute">
|
||||
<Text small variant="white">
|
||||
{avatarInfo.dead || remainingTimeToLive <= 0
|
||||
? '00:00:00'
|
||||
: `${ConvertSeconds(remainingTimeToLive).split(':')[1]}:${ConvertSeconds(remainingTimeToLive).split(':')[2]}:${ConvertSeconds(remainingTimeToLive).split(':')[3]}`}
|
||||
</Text>
|
||||
</div>
|
||||
<div
|
||||
className="bg-success rounded pet-stats"
|
||||
style={{
|
||||
width: avatarInfo.dead || remainingTimeToLive <= 0 ? '0' : `${(remainingTimeToLive / avatarInfo.maximumTimeToLive) * 100}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Column>
|
||||
{remainingGrowTime > 0 && (
|
||||
<Column alignItems="center" gap={1}>
|
||||
<Text small truncate variant="white">
|
||||
{LocalizeText('infostand.pet.text.growth')}
|
||||
</Text>
|
||||
<LayoutCounterTimeView
|
||||
className="top-2 end-2"
|
||||
day={ConvertSeconds(remainingGrowTime).split(':')[0]}
|
||||
hour={ConvertSeconds(remainingGrowTime).split(':')[1]}
|
||||
minutes={ConvertSeconds(remainingGrowTime).split(':')[2]}
|
||||
seconds={ConvertSeconds(remainingGrowTime).split(':')[3]}
|
||||
/>
|
||||
</Column>
|
||||
)}
|
||||
<Column alignItems="center" gap={1}>
|
||||
<Text small truncate variant="white">
|
||||
{LocalizeText('infostand.pet.text.raritylevel', ['level'], [LocalizeText(`infostand.pet.raritylevel.${avatarInfo.rarityLevel}`)])}
|
||||
</Text>
|
||||
<LayoutRarityLevelView className="top-2 end-2" level={avatarInfo.rarityLevel} />
|
||||
</Column>
|
||||
<hr className="m-0" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text small wrap variant="white">
|
||||
{LocalizeText('pet.age', ['age'], [avatarInfo.age.toString()])}
|
||||
</Text>
|
||||
<hr className="m-0" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
// Sub-component: Regular Pet Stats
|
||||
const RegularPetStats: FC<{ avatarInfo: AvatarInfoPet }> = ({ avatarInfo }) => (
|
||||
<>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex gap-1">
|
||||
<Column fullWidth className="body-image pet p-1" overflow="hidden">
|
||||
<LayoutPetImageView direction={4} figure={avatarInfo.petFigure} posture={avatarInfo.posture} />
|
||||
</Column>
|
||||
<Column grow gap={1}>
|
||||
<Text center small wrap variant="white">
|
||||
{LocalizeText('pet.level', ['level', 'maxlevel'], [avatarInfo.level.toString(), avatarInfo.maximumLevel.toString()])}
|
||||
</Text>
|
||||
<Column alignItems="center" gap={1}>
|
||||
<Text small truncate variant="white">
|
||||
{LocalizeText('infostand.pet.text.happiness')}
|
||||
</Text>
|
||||
<div className="bg-light-dark rounded relative overflow-hidden w-full">
|
||||
<div className="flex justify-center items-center size-full absolute">
|
||||
<Text small variant="white">
|
||||
{avatarInfo.happyness + '/' + avatarInfo.maximumHappyness}
|
||||
</Text>
|
||||
</div>
|
||||
<div
|
||||
className="bg-info rounded pet-stats"
|
||||
style={{ width: (avatarInfo.happyness / avatarInfo.maximumHappyness) * 100 + '%' }}
|
||||
/>
|
||||
</div>
|
||||
</Column>
|
||||
<Column alignItems="center" gap={1}>
|
||||
<Text small truncate variant="white">
|
||||
{LocalizeText('infostand.pet.text.experience')}
|
||||
</Text>
|
||||
<div className="bg-light-dark rounded relative overflow-hidden w-full">
|
||||
<div className="flex justify-center items-center size-full absolute">
|
||||
<Text small variant="white">
|
||||
{avatarInfo.experience + '/' + avatarInfo.levelExperienceGoal}
|
||||
</Text>
|
||||
</div>
|
||||
<div
|
||||
className="bg-purple rounded pet-stats"
|
||||
style={{ width: (avatarInfo.experience / avatarInfo.levelExperienceGoal) * 100 + '%' }}
|
||||
/>
|
||||
</div>
|
||||
</Column>
|
||||
<Column alignItems="center" gap={1}>
|
||||
<Text small truncate variant="white">
|
||||
{LocalizeText('infostand.pet.text.energy')}
|
||||
</Text>
|
||||
<div className="bg-light-dark rounded relative overflow-hidden w-full">
|
||||
<div className="flex justify-center items-center size-full absolute">
|
||||
<Text small variant="white">
|
||||
{avatarInfo.energy + '/' + avatarInfo.maximumEnergy}
|
||||
</Text>
|
||||
</div>
|
||||
<div
|
||||
className="bg-success rounded pet-stats"
|
||||
style={{ width: (avatarInfo.energy / avatarInfo.maximumEnergy) * 100 + '%' }}
|
||||
/>
|
||||
</div>
|
||||
</Column>
|
||||
</Column>
|
||||
</div>
|
||||
<hr className="m-0" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text small wrap variant="white">
|
||||
{LocalizeText('infostand.text.petrespect', ['count'], [avatarInfo.respect.toString()])}
|
||||
</Text>
|
||||
<Text small wrap variant="white">
|
||||
{LocalizeText('pet.age', ['age'], [avatarInfo.age.toString()])}
|
||||
</Text>
|
||||
<hr className="m-0" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
export const InfoStandWidgetPetView: FC<InfoStandWidgetPetViewProps> = ({ avatarInfo, onClose }) => {
|
||||
const [remainingGrowTime, setRemainingGrowTime] = useState(0);
|
||||
const [remainingTimeToLive, setRemainingTimeToLive] = useState(0);
|
||||
const { roomSession = null } = useRoom();
|
||||
const { petRespectRemaining = 0, respectPet = null } = useSessionInfo();
|
||||
|
||||
useEffect(() => {
|
||||
setRemainingGrowTime(avatarInfo.remainingGrowTime || 0);
|
||||
setRemainingTimeToLive(avatarInfo.remainingTimeToLive || 0);
|
||||
}, [avatarInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
if (avatarInfo.petType !== PetType.MONSTERPLANT || avatarInfo.dead) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setRemainingGrowTime((prev) => (prev <= 0 ? 0 : prev - 1));
|
||||
setRemainingTimeToLive((prev) => (prev <= 0 ? 0 : prev - 1));
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [avatarInfo]);
|
||||
|
||||
const processButtonAction = useCallback(
|
||||
async (action: string) => {
|
||||
try {
|
||||
let hideMenu = true;
|
||||
if (!action) return;
|
||||
|
||||
switch (action) {
|
||||
case 'respect':
|
||||
await respectPet(avatarInfo.id);
|
||||
if (petRespectRemaining - 1 >= 1) hideMenu = false;
|
||||
break;
|
||||
case 'buyfood':
|
||||
CreateLinkEvent('catalog/open/' + GetConfigurationValue('catalog.links')['pets.buy_food']);
|
||||
break;
|
||||
case 'train':
|
||||
roomSession?.requestPetCommands(avatarInfo.id);
|
||||
break;
|
||||
case 'treat':
|
||||
SendMessageComposer(new PetRespectComposer(avatarInfo.id));
|
||||
break;
|
||||
case 'compost':
|
||||
roomSession?.compostPlant(avatarInfo.id);
|
||||
break;
|
||||
case 'pick_up':
|
||||
roomSession?.pickupPet(avatarInfo.id);
|
||||
break;
|
||||
}
|
||||
|
||||
if (hideMenu) onClose();
|
||||
} catch (error) {
|
||||
console.error(`Failed to process action ${action}:`, error);
|
||||
}
|
||||
},
|
||||
[avatarInfo, petRespectRemaining, respectPet, roomSession, onClose]
|
||||
);
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
action: 'buyfood',
|
||||
label: LocalizeText('infostand.button.buyfood'),
|
||||
condition: avatarInfo.petType !== PetType.MONSTERPLANT,
|
||||
},
|
||||
{
|
||||
action: 'train',
|
||||
label: LocalizeText('infostand.button.train'),
|
||||
condition: avatarInfo.isOwner && avatarInfo.petType !== PetType.MONSTERPLANT,
|
||||
},
|
||||
{
|
||||
action: 'treat',
|
||||
label: LocalizeText('infostand.button.pettreat'),
|
||||
condition:
|
||||
!avatarInfo.dead &&
|
||||
avatarInfo.petType === PetType.MONSTERPLANT &&
|
||||
avatarInfo.energy / avatarInfo.maximumEnergy < 0.98,
|
||||
},
|
||||
{
|
||||
action: 'compost',
|
||||
label: LocalizeText('infostand.button.compost'),
|
||||
condition: roomSession?.isRoomOwner && avatarInfo.petType === PetType.MONSTERPLANT,
|
||||
},
|
||||
{
|
||||
action: 'pick_up',
|
||||
label: LocalizeText('inventory.pets.pickup'),
|
||||
condition: avatarInfo.isOwner,
|
||||
},
|
||||
{
|
||||
action: 'respect',
|
||||
label: LocalizeText('infostand.button.petrespect', ['count'], [petRespectRemaining.toString()]),
|
||||
condition: petRespectRemaining > 0 && avatarInfo.petType !== PetType.MONSTERPLANT,
|
||||
},
|
||||
];
|
||||
|
||||
if (!avatarInfo) return <Text variant="white">{LocalizeText('generic.loading')}</Text>;
|
||||
|
||||
return (
|
||||
<Column alignItems="end" gap={1}>
|
||||
<Column className="nitro-infostand rounded">
|
||||
<Column className="container-fluid content-area" gap={1} overflow="visible">
|
||||
<PetHeader
|
||||
name={avatarInfo.name}
|
||||
petType={avatarInfo.petType}
|
||||
petBreed={avatarInfo.petBreed}
|
||||
onClose={onClose}
|
||||
/>
|
||||
{avatarInfo.petType === PetType.MONSTERPLANT ? (
|
||||
<MonsterplantStats
|
||||
avatarInfo={avatarInfo}
|
||||
remainingGrowTime={remainingGrowTime}
|
||||
remainingTimeToLive={remainingTimeToLive}
|
||||
/>
|
||||
) : (
|
||||
<RegularPetStats avatarInfo={avatarInfo} />
|
||||
)}
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<UserProfileIconView userId={avatarInfo.ownerId} />
|
||||
<Text small wrap variant="white">
|
||||
{LocalizeText('infostand.text.petowner', ['name'], [avatarInfo.ownerName])}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</Column>
|
||||
</Column>
|
||||
<Flex gap={1} justifyContent="end">
|
||||
{buttons.map(
|
||||
(button) =>
|
||||
button.condition && (
|
||||
<Button key={button.action} variant="dark" onClick={() => processButtonAction(button.action)}>
|
||||
{button.label}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
</Flex>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
import { BotRemoveComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useMemo } from 'react';
|
||||
import { FaTimes } from 'react-icons/fa';
|
||||
import { AvatarInfoRentableBot, BotSkillsEnum, LocalizeText, SendMessageComposer } from '../../../../../api';
|
||||
import { Button, Column, Flex, LayoutAvatarImageView, LayoutBadgeImageView, Text, UserProfileIconView } from '../../../../../common';
|
||||
|
||||
interface InfoStandWidgetRentableBotViewProps
|
||||
{
|
||||
avatarInfo: AvatarInfoRentableBot;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const InfoStandWidgetRentableBotView: FC<InfoStandWidgetRentableBotViewProps> = props =>
|
||||
{
|
||||
const { avatarInfo = null, onClose = null } = props;
|
||||
|
||||
const canPickup = useMemo(() =>
|
||||
{
|
||||
if(avatarInfo.botSkills.indexOf(BotSkillsEnum.NO_PICK_UP) >= 0) return false;
|
||||
|
||||
if(!avatarInfo.amIOwner && !avatarInfo.amIAnyRoomController) return false;
|
||||
|
||||
return true;
|
||||
}, [ avatarInfo ]);
|
||||
|
||||
const pickupBot = () => SendMessageComposer(new BotRemoveComposer(avatarInfo.webID));
|
||||
|
||||
if(!avatarInfo) return;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex flex-col nitro-infostand rounded">
|
||||
<div className="flex flex-col gap-1 overflow-visible container-fluid content-area">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Flex alignItems="center" gap={ 1 } justifyContent="between">
|
||||
<Text small wrap variant="white">{ avatarInfo.name }</Text>
|
||||
<FaTimes className="cursor-pointer fa-icon" onClick={ onClose } />
|
||||
</Flex>
|
||||
<hr className="m-0" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex gap-1">
|
||||
<Column fullWidth className="body-image bot">
|
||||
<LayoutAvatarImageView direction={ 4 } figure={ avatarInfo.figure } />
|
||||
</Column>
|
||||
<Column center grow gap={ 0 }>
|
||||
{ (avatarInfo.badges.length > 0) && avatarInfo.badges.map(result =>
|
||||
{
|
||||
return <LayoutBadgeImageView key={ result } badgeCode={ result } showInfo={ true } />;
|
||||
}) }
|
||||
</Column>
|
||||
</div>
|
||||
<hr className="m-0" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Flex alignItems="center" className="bg-light-dark rounded py-1 px-2">
|
||||
<Text fullWidth small textBreak wrap className="min-h-[18px]" variant="white">{ avatarInfo.motto }</Text>
|
||||
</Flex>
|
||||
<hr className="m-0" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<UserProfileIconView userId={ avatarInfo.ownerId } />
|
||||
<Text small wrap variant="white">
|
||||
{ LocalizeText('infostand.text.botowner', [ 'name' ], [ avatarInfo.ownerName ]) }
|
||||
</Text>
|
||||
</div>
|
||||
{ (avatarInfo.carryItem > 0) &&
|
||||
<>
|
||||
<hr className="m-0" />
|
||||
<Text small wrap variant="white">
|
||||
{ LocalizeText('infostand.text.handitem', [ 'item' ], [ LocalizeText('handitem' + avatarInfo.carryItem) ]) }
|
||||
</Text>
|
||||
</> }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{ canPickup &&
|
||||
<div className="flex justify-end">
|
||||
<Button variant="dark" onClick={ pickupBot }>{ LocalizeText('infostand.button.pickup') }</Button>
|
||||
</div> }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { RelationshipStatusEnum, RelationshipStatusInfo } from '@nitrots/nitro-renderer';
|
||||
import { FC } from 'react';
|
||||
import { GetUserProfile, LocalizeText } from '../../../../../api';
|
||||
import { Flex, Text } from '../../../../../common';
|
||||
|
||||
interface InfoStandWidgetUserRelationshipsRelationshipItemViewProps
|
||||
{
|
||||
type: number;
|
||||
relationship: RelationshipStatusInfo;
|
||||
}
|
||||
|
||||
export const InfoStandWidgetUserRelationshipsRelationshipItemView: FC<InfoStandWidgetUserRelationshipsRelationshipItemViewProps> = props =>
|
||||
{
|
||||
const { type = -1, relationship = null } = props;
|
||||
|
||||
if(!relationship) return null;
|
||||
|
||||
const relationshipName = RelationshipStatusEnum.RELATIONSHIP_NAMES[type].toLocaleLowerCase();
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<i className={ `nitro-friends-spritesheet icon-${ relationshipName }` } />
|
||||
<Flex alignItems="center" gap={ 0 }>
|
||||
<Text small variant="white" onClick={ event => GetUserProfile(relationship.randomFriendId) }>
|
||||
<u>{ relationship.randomFriendName }</u>
|
||||
{ (relationship.friendCount > 1) && (' ' + LocalizeText(`extendedprofile.relstatus.others.${ relationshipName }`, [ 'count' ], [ (relationship.friendCount - 1).toString() ])) }
|
||||
</Text>
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { RelationshipStatusEnum, RelationshipStatusInfoMessageParser } from '@nitrots/nitro-renderer';
|
||||
import { FC } from 'react';
|
||||
import { InfoStandWidgetUserRelationshipsRelationshipItemView } from './InfoStandWidgetUserRelationshipItemView';
|
||||
|
||||
interface InfoStandWidgetUserRelationshipsViewProps
|
||||
{
|
||||
relationships: RelationshipStatusInfoMessageParser;
|
||||
}
|
||||
|
||||
export const InfoStandWidgetUserRelationshipsView: FC<InfoStandWidgetUserRelationshipsViewProps> = props =>
|
||||
{
|
||||
const { relationships = null } = props;
|
||||
|
||||
if(!relationships || !relationships.relationshipStatusMap.length) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<InfoStandWidgetUserRelationshipsRelationshipItemView relationship={ relationships.relationshipStatusMap.getValue(RelationshipStatusEnum.HEART) } type={ RelationshipStatusEnum.HEART } />
|
||||
<InfoStandWidgetUserRelationshipsRelationshipItemView relationship={ relationships.relationshipStatusMap.getValue(RelationshipStatusEnum.SMILE) } type={ RelationshipStatusEnum.SMILE } />
|
||||
<InfoStandWidgetUserRelationshipsRelationshipItemView relationship={ relationships.relationshipStatusMap.getValue(RelationshipStatusEnum.BOBBA) } type={ RelationshipStatusEnum.BOBBA } />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { CreateLinkEvent, NavigatorSearchComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC } from 'react';
|
||||
import { SendMessageComposer } from '../../../../../api';
|
||||
import { Flex, Text } from '../../../../../common';
|
||||
|
||||
interface InfoStandWidgetUserTagsViewProps
|
||||
{
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
const processAction = (tag: string) =>
|
||||
{
|
||||
CreateLinkEvent(`navigator/search/${ tag }`);
|
||||
SendMessageComposer(new NavigatorSearchComposer('hotel_view', `tag:${ tag }`));
|
||||
};
|
||||
|
||||
export const InfoStandWidgetUserTagsView: FC<InfoStandWidgetUserTagsViewProps> = props =>
|
||||
{
|
||||
const { tags = null } = props;
|
||||
|
||||
if(!tags || !tags.length) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<hr className="m-0" />
|
||||
<Flex className="flex-tags">
|
||||
{ tags && (tags.length > 0) && tags.map((tag, index) => <Text key={ index } className="text-tags" variant="white" onClick={ event => processAction(tag) }>{ tag }</Text>) }
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,262 @@
|
||||
import { GetSessionDataManager, RelationshipStatusInfoEvent, RelationshipStatusInfoMessageParser, RoomSessionFavoriteGroupUpdateEvent, RoomSessionUserBadgesEvent, RoomSessionUserFigureUpdateEvent, UserRelationshipsComposer } from '@nitrots/nitro-renderer';
|
||||
import { Dispatch, FC, FocusEvent, KeyboardEvent, SetStateAction, useCallback, useEffect, useState } from 'react';
|
||||
import { FaPencilAlt, FaTimes } from 'react-icons/fa';
|
||||
import { AvatarInfoUser, CloneObject, GetConfigurationValue, GetGroupInformation, GetUserProfile, LocalizeText, SendMessageComposer } from '../../../../../api';
|
||||
import { Base, Column, Flex, LayoutAvatarImageView, LayoutBadgeImageView, Text, UserProfileIconView } from '../../../../../common';
|
||||
import { useMessageEvent, useNitroEvent, useRoom } from '../../../../../hooks';
|
||||
import { InfoStandWidgetUserRelationshipsView } from './InfoStandWidgetUserRelationshipsView';
|
||||
import { InfoStandWidgetUserTagsView } from './InfoStandWidgetUserTagsView';
|
||||
import { BackgroundsView } from '../../../../backgrounds/BackgroundsView';
|
||||
|
||||
interface InfoStandWidgetUserViewProps {
|
||||
avatarInfo: AvatarInfoUser;
|
||||
setAvatarInfo: Dispatch<SetStateAction<AvatarInfoUser>>;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props => {
|
||||
const { avatarInfo = null, setAvatarInfo = null, onClose = null } = props;
|
||||
const [motto, setMotto] = useState<string>(null);
|
||||
const [isEditingMotto, setIsEditingMotto] = useState(false);
|
||||
const [relationships, setRelationships] = useState<RelationshipStatusInfoMessageParser>(null);
|
||||
const [backgroundId, setBackgroundId] = useState<number>(null);
|
||||
const [standId, setStandId] = useState<number>(null);
|
||||
const [overlayId, setOverlayId] = useState<number>(null);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const { roomSession = null } = useRoom();
|
||||
|
||||
const infostandBackgroundClass = `background-${backgroundId ?? 'default'}`;
|
||||
const infostandStandClass = `stand-${standId ?? 'default'}`;
|
||||
const infostandOverlayClass = `overlay-${overlayId ?? 'default'}`;
|
||||
|
||||
const handleProfileClick = useCallback(() => { GetUserProfile(avatarInfo.webID); }, [avatarInfo.webID]);
|
||||
|
||||
const handleEditClick = useCallback((event: React.MouseEvent) => { event.stopPropagation(); setIsVisible(prev => !prev); }, []);
|
||||
|
||||
const saveMotto = (motto: string) => {
|
||||
if (!isEditingMotto || motto.length > GetConfigurationValue<number>('motto.max.length', 38) || !roomSession) return;
|
||||
|
||||
roomSession.sendMottoMessage(motto);
|
||||
setIsEditingMotto(false);
|
||||
};
|
||||
|
||||
const onMottoBlur = (event: FocusEvent<HTMLInputElement>) => saveMotto(event.target.value);
|
||||
|
||||
const onMottoKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||
event.stopPropagation();
|
||||
|
||||
switch (event.key) {
|
||||
case 'Enter':
|
||||
saveMotto((event.target as HTMLInputElement).value);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
useNitroEvent<RoomSessionUserBadgesEvent>(RoomSessionUserBadgesEvent.RSUBE_BADGES, event => {
|
||||
if (!avatarInfo || avatarInfo.webID !== event.userId) return;
|
||||
|
||||
const oldBadges = avatarInfo.badges.join('');
|
||||
|
||||
if (oldBadges === event.badges.join('')) return;
|
||||
|
||||
setAvatarInfo(prevValue => {
|
||||
const newValue = CloneObject(prevValue);
|
||||
newValue.badges = event.badges;
|
||||
return newValue;
|
||||
});
|
||||
});
|
||||
|
||||
useNitroEvent<RoomSessionUserFigureUpdateEvent>(RoomSessionUserFigureUpdateEvent.USER_FIGURE, event => {
|
||||
if (!avatarInfo || avatarInfo.roomIndex !== event.roomIndex) return;
|
||||
|
||||
setAvatarInfo(prevValue => {
|
||||
const newValue = CloneObject(prevValue);
|
||||
newValue.figure = event.figure;
|
||||
newValue.motto = event.customInfo;
|
||||
newValue.achievementScore = event.activityPoints;
|
||||
newValue.backgroundId = event.backgroundId;
|
||||
newValue.standId = event.standId;
|
||||
newValue.overlayId = event.overlayId;
|
||||
return newValue;
|
||||
});
|
||||
});
|
||||
|
||||
useNitroEvent<RoomSessionFavoriteGroupUpdateEvent>(RoomSessionFavoriteGroupUpdateEvent.FAVOURITE_GROUP_UPDATE, event => {
|
||||
if (!avatarInfo || avatarInfo.roomIndex !== event.roomIndex) return;
|
||||
|
||||
setAvatarInfo(prevValue => {
|
||||
const newValue = CloneObject(prevValue);
|
||||
const clearGroup = (event.status === -1) || (event.habboGroupId <= 0);
|
||||
|
||||
newValue.groupId = clearGroup ? -1 : event.habboGroupId;
|
||||
newValue.groupName = clearGroup ? null : event.habboGroupName;
|
||||
newValue.groupBadgeId = clearGroup ? null : GetSessionDataManager().getGroupBadge(event.habboGroupId);
|
||||
return newValue;
|
||||
});
|
||||
});
|
||||
|
||||
useMessageEvent<RelationshipStatusInfoEvent>(RelationshipStatusInfoEvent, event => {
|
||||
const parser = event.getParser();
|
||||
|
||||
if (!avatarInfo || avatarInfo.webID !== parser.userId) return;
|
||||
|
||||
setRelationships(parser);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setIsEditingMotto(false);
|
||||
setMotto(avatarInfo.motto);
|
||||
setBackgroundId(avatarInfo.backgroundId);
|
||||
setStandId(avatarInfo.standId);
|
||||
setOverlayId(avatarInfo.overlayId);
|
||||
|
||||
SendMessageComposer(new UserRelationshipsComposer(avatarInfo.webID));
|
||||
|
||||
return () => {
|
||||
setIsEditingMotto(false);
|
||||
setMotto(null);
|
||||
setRelationships(null);
|
||||
setBackgroundId(null);
|
||||
setStandId(null);
|
||||
setOverlayId(null);
|
||||
};
|
||||
}, [avatarInfo]);
|
||||
|
||||
if (!avatarInfo) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Column className="relative min-w-[190px] max-w-[190px] z-30 pointer-events-auto bg-[rgba(28,28,32,0.95)] [box-shadow:inset_0_5px_#22222799,_inset_0_-4px_#12121599] rounded">
|
||||
<Column className="h-full p-[8px] overflow-auto" gap={1} overflow="visible">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<UserProfileIconView userId={avatarInfo.webID} />
|
||||
<Text small wrap variant="white">{avatarInfo.name}</Text>
|
||||
</div>
|
||||
<FaTimes className="cursor-pointer fa-icon" onClick={onClose} />
|
||||
</div>
|
||||
<hr className="m-0 bg-[#0003] border-[0] opacity-[0.5] h-px" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex gap-1">
|
||||
<Column
|
||||
fullWidth
|
||||
className={`flex items-center w-full max-w-[68px] rounded-[0.25rem] profile-background ${infostandBackgroundClass}`}
|
||||
onClick={handleProfileClick}
|
||||
>
|
||||
<Base position="absolute" className={`profile-stand ${infostandStandClass}`} />
|
||||
<LayoutAvatarImageView direction={2} figure={avatarInfo.figure} />
|
||||
<Base position="absolute" className={`profile-overlay ${infostandOverlayClass}`} />
|
||||
{avatarInfo.type === AvatarInfoUser.OWN_USER && (
|
||||
<Base
|
||||
className="background-edit-icon background-edit-position"
|
||||
style={{ pointerEvents: 'auto', cursor: 'pointer' }}
|
||||
onClick={handleEditClick}
|
||||
aria-label="Edit profile background"
|
||||
/>
|
||||
)}
|
||||
</Column>
|
||||
<Column grow alignItems="center" gap={0}>
|
||||
<div className="flex gap-1">
|
||||
<div className="flex items-center justify-center relative w-[40px] h-[40px] bg-no-repeat bg-center">
|
||||
{avatarInfo.badges[0] && <LayoutBadgeImageView badgeCode={avatarInfo.badges[0]} showInfo={true} />}
|
||||
</div>
|
||||
<Flex center className="relative w-[40px] h-[40px] bg-no-repeat bg-center" pointer={avatarInfo.groupId > 0} onClick={event => GetGroupInformation(avatarInfo.groupId)}>
|
||||
{avatarInfo.groupId > 0 &&
|
||||
<LayoutBadgeImageView badgeCode={avatarInfo.groupBadgeId} customTitle={avatarInfo.groupName} isGroup={true} showInfo={true} />}
|
||||
</Flex>
|
||||
</div>
|
||||
<Flex center gap={1}>
|
||||
<div className="flex items-center justify-center relative w-[40px] h-[40px] bg-no-repeat bg-center">
|
||||
{avatarInfo.badges[1] && <LayoutBadgeImageView badgeCode={avatarInfo.badges[1]} showInfo={true} />}
|
||||
</div>
|
||||
<div className="flex items-center justify-center relative w-[40px] h-[40px] bg-no-repeat bg-center">
|
||||
{avatarInfo.badges[2] && <LayoutBadgeImageView badgeCode={avatarInfo.badges[2]} showInfo={true} />}
|
||||
</div>
|
||||
</Flex>
|
||||
<Flex center gap={1}>
|
||||
<div className="flex items-center justify-center relative w-[40px] h-[40px] bg-no-repeat bg-center">
|
||||
{avatarInfo.badges[3] && <LayoutBadgeImageView badgeCode={avatarInfo.badges[3]} showInfo={true} />}
|
||||
</div>
|
||||
<div className="flex items-center justify-center relative w-[40px] h-[40px] bg-no-repeat bg-center">
|
||||
{avatarInfo.badges[4] && <LayoutBadgeImageView badgeCode={avatarInfo.badges[4]} showInfo={true} />}
|
||||
</div>
|
||||
</Flex>
|
||||
</Column>
|
||||
</div>
|
||||
<hr className="m-0 bg-[#0003] border-[0] opacity-[0.5] h-px" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Flex alignItems="center" className="bg-light-dark rounded py-1 px-2">
|
||||
{avatarInfo.type !== AvatarInfoUser.OWN_USER && (
|
||||
<Flex grow alignItems="center" className="min-h-[18px]">
|
||||
<Text fullWidth pointer small textBreak wrap variant="white">{motto}</Text>
|
||||
</Flex>
|
||||
)}
|
||||
{avatarInfo.type === AvatarInfoUser.OWN_USER && (
|
||||
<Flex grow alignItems="center" gap={2}>
|
||||
<FaPencilAlt className="small fa-icon" />
|
||||
<Flex grow alignItems="center" className="min-h-[18px]">
|
||||
{!isEditingMotto && (
|
||||
<Text fullWidth pointer small textBreak wrap variant="white" onClick={event => setIsEditingMotto(true)}>
|
||||
{motto}
|
||||
</Text>
|
||||
)}
|
||||
{isEditingMotto && (
|
||||
<input
|
||||
autoFocus={true}
|
||||
className="w-full h-full text-[12px] p-0 outline-[0] border-[0] text-[#fff] relative bg-transparent resize-none focus:italic border-transparent focus:border-transparent focus:ring-0"
|
||||
maxLength={GetConfigurationValue<number>('motto.max.length', 38)}
|
||||
type="text"
|
||||
value={motto}
|
||||
onBlur={onMottoBlur}
|
||||
onChange={event => setMotto(event.target.value)}
|
||||
onKeyDown={onMottoKeyDown}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
<hr className="m-0 bg-[#0003] border-[0] opacity-[0.5] h-px" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text small wrap variant="white">
|
||||
{LocalizeText('infostand.text.achievement_score') + ' ' + avatarInfo.achievementScore}
|
||||
</Text>
|
||||
{avatarInfo.carryItem > 0 && (
|
||||
<>
|
||||
<hr className="m-0 bg-[#0003] border-[0] opacity-[0.5] h-px" />
|
||||
<Text small wrap variant="white">
|
||||
{LocalizeText('infostand.text.handitem', ['item'], [LocalizeText('handitem' + avatarInfo.carryItem)])}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<InfoStandWidgetUserRelationshipsView relationships={relationships} />
|
||||
</div>
|
||||
{GetConfigurationValue('user.tags.enabled') && (
|
||||
<Column className="mt-1" gap={1}>
|
||||
<InfoStandWidgetUserTagsView tags={GetSessionDataManager().tags} />
|
||||
</Column>
|
||||
)}
|
||||
</Column>
|
||||
</Column>
|
||||
{isVisible && avatarInfo.type === AvatarInfoUser.OWN_USER && (
|
||||
<div className="backgrounds-view-container">
|
||||
<BackgroundsView
|
||||
setIsVisible={setIsVisible}
|
||||
selectedBackground={backgroundId}
|
||||
setSelectedBackground={setBackgroundId}
|
||||
selectedStand={standId}
|
||||
setSelectedStand={setStandId}
|
||||
selectedOverlay={overlayId}
|
||||
setSelectedOverlay={setOverlayId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user