From aee7ffb392ccf3bfb9059f33d2d835e1378c44a6 Mon Sep 17 00:00:00 2001 From: duckietm Date: Thu, 22 May 2025 10:29:50 +0200 Subject: [PATCH] :up: Fix Pet Infostand --- src/common/layout/LayoutBadgeImageView.tsx | 3 + .../infostand/InfoStandWidgetPetView.tsx | 512 +++++++++++------- src/css/room/InfoStand.css | 145 +++++ src/index.tsx | 1 + 4 files changed, 472 insertions(+), 189 deletions(-) create mode 100644 src/css/room/InfoStand.css diff --git a/src/common/layout/LayoutBadgeImageView.tsx b/src/common/layout/LayoutBadgeImageView.tsx index 7cfb838..bac40bc 100644 --- a/src/common/layout/LayoutBadgeImageView.tsx +++ b/src/common/layout/LayoutBadgeImageView.tsx @@ -68,6 +68,8 @@ export const LayoutBadgeImageView: FC = props => if(event.badgeId !== badgeCode) return; const element = await TextureUtils.generateImage(new NitroSprite(event.image)); + + console.log ('boe'); element.onload = () => setImageElement(element); @@ -85,6 +87,7 @@ export const LayoutBadgeImageView: FC = props => (async () => { const element = await TextureUtils.generateImage(new NitroSprite(texture)); + element.onload = () => setImageElement(element); })(); diff --git a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetPetView.tsx b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetPetView.tsx index d6d683d..1882d92 100644 --- a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetPetView.tsx +++ b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetPetView.tsx @@ -1,209 +1,343 @@ import { CreateLinkEvent, PetRespectComposer, PetType } from '@nitrots/nitro-renderer'; -import { FC, useEffect, useState } from 'react'; +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'; -interface InfoStandWidgetPetViewProps -{ - avatarInfo: AvatarInfoPet; - onClose: () => void; +// 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; } -export const InfoStandWidgetPetView: FC = props => -{ - const { avatarInfo = null, onClose = null } = props; - const [ remainingGrowTime, setRemainingGrowTime ] = useState(0); - const [ remainingTimeToLive, setRemainingTimeToLive ] = useState(0); - const { roomSession = null } = useRoom(); - const { petRespectRemaining = 0, respectPet = null } = useSessionInfo(); +interface InfoStandWidgetPetViewProps { + avatarInfo: AvatarInfoPet; + onClose: () => void; +} - useEffect(() => - { - setRemainingGrowTime(avatarInfo.remainingGrowTime); - setRemainingTimeToLive(avatarInfo.remainingTimeToLive); - }, [ avatarInfo ]); +const PetHeader: FC<{ name: string; petType: number; petBreed: number; onClose: () => void }> = ({ name, petType, petBreed, onClose }) => ( +
+ + + {name} + + + + + {LocalizeText(`pet.breed.${petType}.${petBreed}`)} + +
+
+); - useEffect(() => - { - if((avatarInfo.petType !== PetType.MONSTERPLANT) || avatarInfo.dead) return; +const MonsterplantStats: FC<{ + avatarInfo: AvatarInfoPet; + remainingGrowTime: number; + remainingTimeToLive: number; +}> = ({ avatarInfo, remainingGrowTime, remainingTimeToLive }) => ( + <> + + +
+
+
+ {!avatarInfo.dead && ( + + + {LocalizeText('pet.level', ['level', 'maxlevel'], [avatarInfo.level.toString(), avatarInfo.maximumLevel.toString()])} + + + )} + + + {LocalizeText('infostand.pet.text.wellbeing')} + +
+
+ + {avatarInfo.dead || remainingTimeToLive <= 0 + ? '00:00:00' + : `${ConvertSeconds(remainingTimeToLive).split(':')[1]}:${ConvertSeconds(remainingTimeToLive).split(':')[2]}:${ConvertSeconds(remainingTimeToLive).split(':')[3]}`} + +
+
+
+ + {remainingGrowTime > 0 && ( + + + {LocalizeText('infostand.pet.text.growth')} + + + + )} + + + {LocalizeText('infostand.pet.text.raritylevel', ['level'], [LocalizeText(`infostand.pet.raritylevel.${avatarInfo.rarityLevel}`)])} + + + +
+
+
+ + {LocalizeText('pet.age', ['age'], [avatarInfo.age.toString()])} + +
+
+ +); - const interval = setInterval(() => - { - setRemainingGrowTime(prevValue => (prevValue - 1)); - setRemainingTimeToLive(prevValue => (prevValue - 1)); - }, 1000); +// Sub-component: Regular Pet Stats +const RegularPetStats: FC<{ avatarInfo: AvatarInfoPet }> = ({ avatarInfo }) => ( + <> +
+
+ + + + + + {LocalizeText('pet.level', ['level', 'maxlevel'], [avatarInfo.level.toString(), avatarInfo.maximumLevel.toString()])} + + + + {LocalizeText('infostand.pet.text.happiness')} + +
+
+ + {avatarInfo.happyness + '/' + avatarInfo.maximumHappyness} + +
+
+
+ + + + {LocalizeText('infostand.pet.text.experience')} + +
+
+ + {avatarInfo.experience + '/' + avatarInfo.levelExperienceGoal} + +
+
+
+ + + + {LocalizeText('infostand.pet.text.energy')} + +
+
+ + {avatarInfo.energy + '/' + avatarInfo.maximumEnergy} + +
+
+
+ + +
+
+
+
+ + {LocalizeText('infostand.text.petrespect', ['count'], [avatarInfo.respect.toString()])} + + + {LocalizeText('pet.age', ['age'], [avatarInfo.age.toString()])} + +
+
+ +); - return () => clearInterval(interval); - }, [ avatarInfo ]); +export const InfoStandWidgetPetView: FC = ({ avatarInfo, onClose }) => { + const [remainingGrowTime, setRemainingGrowTime] = useState(0); + const [remainingTimeToLive, setRemainingTimeToLive] = useState(0); + const { roomSession = null } = useRoom(); + const { petRespectRemaining = 0, respectPet = null } = useSessionInfo(); - if(!avatarInfo) return null; + useEffect(() => { + setRemainingGrowTime(avatarInfo.remainingGrowTime || 0); + setRemainingTimeToLive(avatarInfo.remainingTimeToLive || 0); + }, [avatarInfo]); - const processButtonAction = (action: string) => - { + 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; - if(!action || action == '') return; - - switch(action) - { - case 'respect': - 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; + 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(); - }; + if (hideMenu) onClose(); + } catch (error) { + console.error(`Failed to process action ${action}:`, error); + } + }, + [avatarInfo, petRespectRemaining, respectPet, roomSession, onClose] + ); - return ( - - - -
- - { avatarInfo.name } - - - { LocalizeText(`pet.breed.${ avatarInfo.petType }.${ avatarInfo.petBreed }`) } -
-
- { (avatarInfo.petType === PetType.MONSTERPLANT) && - <> - - -
-
-
- { !avatarInfo.dead && - - { LocalizeText('pet.level', [ 'level', 'maxlevel' ], [ avatarInfo.level.toString(), avatarInfo.maximumLevel.toString() ]) } - } - - { LocalizeText('infostand.pet.text.wellbeing') } -
-
- { avatarInfo.dead ? '00:00:00' : ConvertSeconds((remainingTimeToLive == 0 ? avatarInfo.remainingTimeToLive : remainingTimeToLive)).split(':')[1] + ':' + ConvertSeconds((remainingTimeToLive == null || remainingTimeToLive == undefined ? 0 : remainingTimeToLive)).split(':')[2] + ':' + ConvertSeconds((remainingTimeToLive == null || remainingTimeToLive == undefined ? 0 : remainingTimeToLive)).split(':')[3] } -
-
-
- - { remainingGrowTime != 0 && remainingGrowTime > 0 && - - { LocalizeText('infostand.pet.text.growth') } - - } - - { LocalizeText('infostand.pet.text.raritylevel', [ 'level' ], [ LocalizeText(`infostand.pet.raritylevel.${ avatarInfo.rarityLevel }`) ]) } - - -
-
-
- { LocalizeText('pet.age', [ 'age' ], [ avatarInfo.age.toString() ]) } -
-
- } - { (avatarInfo.petType !== PetType.MONSTERPLANT) && - <> -
-
- - - - - { LocalizeText('pet.level', [ 'level', 'maxlevel' ], [ avatarInfo.level.toString(), avatarInfo.maximumLevel.toString() ]) } - - { LocalizeText('infostand.pet.text.happiness') } -
-
- { avatarInfo.happyness + '/' + avatarInfo.maximumHappyness } -
-
-
- - - { LocalizeText('infostand.pet.text.experience') } -
-
- { avatarInfo.experience + '/' + avatarInfo.levelExperienceGoal } -
-
-
- - - { LocalizeText('infostand.pet.text.energy') } -
-
- { avatarInfo.energy + '/' + avatarInfo.maximumEnergy } -
-
-
- - -
-
-
-
- { (avatarInfo.petType !== PetType.MONSTERPLANT) && - { LocalizeText('infostand.text.petrespect', [ 'count' ], [ avatarInfo.respect.toString() ]) } } - { LocalizeText('pet.age', [ 'age' ], [ avatarInfo.age.toString() ]) } -
-
- } -
-
- - - { LocalizeText('infostand.text.petowner', [ 'name' ], [ avatarInfo.ownerName ]) } - -
-
-
- - - { (avatarInfo.petType !== PetType.MONSTERPLANT) && - } - { avatarInfo.isOwner && (avatarInfo.petType !== PetType.MONSTERPLANT) && - } - { !avatarInfo.dead && ((avatarInfo.energy / avatarInfo.maximumEnergy) < 0.98) && (avatarInfo.petType === PetType.MONSTERPLANT) && - } - { roomSession?.isRoomOwner && (avatarInfo.petType === PetType.MONSTERPLANT) && - } - { avatarInfo.isOwner && - } - { (petRespectRemaining > 0) && (avatarInfo.petType !== PetType.MONSTERPLANT) && - } - + 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 {LocalizeText('generic.loading')}; + + return ( + + + + + {avatarInfo.petType === PetType.MONSTERPLANT ? ( + + ) : ( + + )} +
+
+ + + {LocalizeText('infostand.text.petowner', ['name'], [avatarInfo.ownerName])} + +
+
- ); -}; +
+ + {buttons.map( + (button) => + button.condition && ( + + ) + )} + +
+ ); +}; \ No newline at end of file diff --git a/src/css/room/InfoStand.css b/src/css/room/InfoStand.css new file mode 100644 index 0000000..e44b062 --- /dev/null +++ b/src/css/room/InfoStand.css @@ -0,0 +1,145 @@ +.nitro-use-product-confirmation { + width: 350px; + + .product-preview { + display: flex; + justify-content: center; + align-items: center; + width: 75px; /* Aligned with .body-image.pet */ + height: 80px; + background: url('@/assets/images/room-widgets/avatar-info/preview-background.png') no-repeat center; + + .pet-image { + width: 75px; + height: 80px; + } + + .monsterplant-image { + width: 75px; + height: 80px; + background: url('@/assets/images/room-widgets/furni-context-menu/monsterplant-preview.png') no-repeat center; + } + } +} + +.nitro-infostand { + position: relative; + width: clamp(160px, 20vw, 190px); /* Responsive width */ + z-index: 30; + pointer-events: auto; + background: #212131; + box-shadow: inset 0 5px rgba(38, 38, 57, 0.6), inset 0 -4px rgba(25, 25, 37, 0.6); + border-radius: 0.5rem; + padding: 10px; + + @media (max-width: 576px) { + top: -67px; + padding: 8px; + .text { + font-size: 0.85rem; + } + } + + .form-control-sm { + height: 25px; + min-height: 25px; + padding: 0.1rem 0.25rem; + } + + .body-image { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + max-width: 68px; + border-radius: 0.5rem; + + &.pet { + max-width: 75px; + } + + &.bot { + background-image: url('@/assets/images/infostand/bot_background.png'); + background-repeat: no-repeat; + background-position: center; + } + + &.furni { + background-color: transparent; + margin-right: 0; + } + } + + .body-image-plant { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + max-width: 68px; + height: 85px; + max-height: 90px; + border-radius: 0.5rem; + } + + .badge-image { + width: 45px; + height: 45px; + } + + .motto-content { + min-height: 18px; + } + + .motto-input { + width: 100%; + height: 100%; + font-size: 12px; + padding: 0; + outline: 0; + border: 0; + color: rgba(255, 255, 255, 1); + position: relative; + background: transparent; + resize: none; + + &:focus { + font-style: italic; + } + } + + .flex-tags { + flex-wrap: wrap; + margin-bottom: -10px; + + .text-tags { + padding: 2px; + border-radius: 3px; + background: #333; + margin-right: 5px; + margin-bottom: 10px; + cursor: pointer; + } + } + + .button-container { + pointer-events: auto; + } + + .pet-stats { + height: 18px; + transition: width 0.3s ease-in-out; /* Smooth progress bar animation */ + } +} + +.nitro-rarity-level { + width: 36px; + height: 28px; + background: url('@/assets/images/infostand/rarity-level.png'); + + div { + line-height: 28px; + text-align: center; + color: $black; + font-weight: bold; + } +} \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx index 80b6c54..fb3de22 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -29,6 +29,7 @@ import './css/notification/NotificationCenterView.css'; import './css/purse/PurseView.css'; +import './css/room/InfoStand.css'; import './css/room/NavigatorRoomSettings.css'; import './css/room/RoomWidgets.css';