From 97aae7170814081c0cb61e4b392d81f13e97b4d2 Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Wed, 18 Mar 2026 17:01:10 +0100 Subject: [PATCH] fix(wired-ui): clarify reward fields and mute alerts --- .../actions/WiredActionGiveRewardView.tsx | 226 +++++++++++++++--- src/hooks/notification/useNotification.ts | 24 +- src/hooks/rooms/widgets/useChatWidget.ts | 7 +- 3 files changed, 224 insertions(+), 33 deletions(-) diff --git a/src/components/wired/views/actions/WiredActionGiveRewardView.tsx b/src/components/wired/views/actions/WiredActionGiveRewardView.tsx index 91c1468..3fb239a 100644 --- a/src/components/wired/views/actions/WiredActionGiveRewardView.tsx +++ b/src/components/wired/views/actions/WiredActionGiveRewardView.tsx @@ -7,6 +7,131 @@ import { NitroInput } from '../../../../layout'; import { WiredActionBaseView } from './WiredActionBaseView'; import { WiredSourcesSelector } from '../WiredSourcesSelector'; +type RewardType = 'badge' | 'credits' | 'pixels' | 'diamonds' | 'points' | 'furni' | 'respect'; + +interface RewardEntry +{ + rewardType: RewardType; + rewardValue: string; + probability: number; + pointsType: number; +} + +const DEFAULT_PROBABILITY = 100; +const DEFAULT_POINTS_TYPE = 5; + +const REWARD_TYPES: { value: RewardType, label: string }[] = [ + { value: 'badge', label: 'Badge' }, + { value: 'credits', label: 'Credits' }, + { value: 'pixels', label: 'Pixels / Duckets' }, + { value: 'diamonds', label: 'Diamonds' }, + { value: 'points', label: 'Extra Currency' }, + { value: 'furni', label: 'Furni' }, + { value: 'respect', label: 'Respect' } +]; + +const SELECTABLE_REWARD_TYPES = REWARD_TYPES.filter(entry => (entry.value !== 'respect')); + +const createReward = (): RewardEntry => +({ + rewardType: 'furni', + rewardValue: '', + probability: DEFAULT_PROBABILITY, + pointsType: DEFAULT_POINTS_TYPE +}); + +const getRewardValuePlaceholder = (rewardType: RewardType) => +{ + switch(rewardType) + { + case 'badge': + return 'Badge code'; + case 'credits': + return 'Credits amount'; + case 'pixels': + return 'Pixels amount'; + case 'diamonds': + return 'Diamonds amount'; + case 'points': + return 'Amount'; + case 'furni': + return 'Furni base item id'; + case 'respect': + return 'Respect amount'; + } +}; + +const getExtraFieldLabel = (rewardType: RewardType) => +{ + switch(rewardType) + { + case 'points': + return 'Currency Type'; + case 'badge': + return 'Code'; + default: + return 'Info'; + } +}; + +const getExtraFieldPlaceholder = (rewardType: RewardType) => +{ + switch(rewardType) + { + case 'points': + return 'Type id (e.g. 105)'; + case 'badge': + return 'Badge'; + default: + return ''; + } +}; + +const parseRewardEntry = (rawType: string, rawCode: string, rawProbability: string): RewardEntry => +{ + const probability = Number(rawProbability); + const parsedProbability = Number.isFinite(probability) ? probability : DEFAULT_PROBABILITY; + + if(rawType === '0') + { + return { rewardType: 'badge', rewardValue: rawCode, probability: parsedProbability, pointsType: DEFAULT_POINTS_TYPE }; + } + + const separatorIndex = rawCode.indexOf('#'); + + if(separatorIndex === -1) + { + return { rewardType: 'furni', rewardValue: rawCode, probability: parsedProbability, pointsType: DEFAULT_POINTS_TYPE }; + } + + const rewardType = rawCode.slice(0, separatorIndex); + const rewardValue = rawCode.slice(separatorIndex + 1); + + if(rewardType.startsWith('points')) + { + const pointsType = Number(rewardType.slice('points'.length)); + + return { + rewardType: 'points', + rewardValue, + probability: parsedProbability, + pointsType: Number.isFinite(pointsType) ? pointsType : DEFAULT_POINTS_TYPE + }; + } + + if(REWARD_TYPES.some(entry => (entry.value === rewardType))) + { + return { rewardType: rewardType as RewardType, rewardValue, probability: parsedProbability, pointsType: DEFAULT_POINTS_TYPE }; + } + + if(rewardType === 'cata') + { + return { rewardType: 'furni', rewardValue, probability: parsedProbability, pointsType: DEFAULT_POINTS_TYPE }; + } + + return { rewardType: 'furni', rewardValue: rawCode, probability: parsedProbability, pointsType: DEFAULT_POINTS_TYPE }; +}; + export const WiredActionGiveRewardView: FC<{}> = props => { const [ limitEnabled, setLimitEnabled ] = useState(false); @@ -14,7 +139,7 @@ export const WiredActionGiveRewardView: FC<{}> = props => const [ uniqueRewards, setUniqueRewards ] = useState(false); const [ rewardsLimit, setRewardsLimit ] = useState(1); const [ limitationInterval, setLimitationInterval ] = useState(1); - const [ rewards, setRewards ] = useState<{ isBadge: boolean, itemCode: string, probability: number }[]>([]); + const [ rewards, setRewards ] = useState([]); const { trigger = null, setIntParams = null, setStringParam = null } = useWired(); const [ userSource, setUserSource ] = useState(() => { @@ -22,7 +147,8 @@ export const WiredActionGiveRewardView: FC<{}> = props => return 0; }); - const addReward = () => setRewards(rewards => [ ...rewards, { isBadge: false, itemCode: '', probability: null } ]); + const addReward = () => setRewards(rewards => [ ...rewards, createReward() ]); + const hasCustomCurrencyReward = rewards.some(reward => (reward.rewardType === 'points')); const removeReward = (index: number) => { @@ -36,18 +162,9 @@ export const WiredActionGiveRewardView: FC<{}> = props => }); }; - const updateReward = (index: number, isBadge: boolean, itemCode: string, probability: number) => + const updateReward = (index: number, updater: (reward: RewardEntry) => RewardEntry) => { - const rewardsClone = Array.from(rewards); - const reward = rewardsClone[index]; - - if(!reward) return; - - reward.isBadge = isBadge; - reward.itemCode = itemCode; - reward.probability = probability; - - setRewards(rewardsClone); + setRewards(prevValue => prevValue.map((reward, rewardIndex) => ((rewardIndex === index) ? updater(reward) : reward))); }; const save = () => @@ -56,9 +173,20 @@ export const WiredActionGiveRewardView: FC<{}> = props => for(const reward of rewards) { - if(!reward.itemCode) continue; + const rewardValue = reward.rewardValue.trim(); - const rewardsString = [ reward.isBadge ? '0' : '1', reward.itemCode, reward.probability.toString() ]; + if(!rewardValue) continue; + + const probability = Math.max(0, Number.isFinite(reward.probability) ? reward.probability : DEFAULT_PROBABILITY); + const rewardCode = (() => + { + if(reward.rewardType === 'badge') return rewardValue; + if(reward.rewardType === 'points') return `points${ Math.max(0, reward.pointsType) }#${ rewardValue }`; + + return `${ reward.rewardType }#${ rewardValue }`; + })(); + + const rewardsString = [ reward.rewardType === 'badge' ? '0' : '1', rewardCode, (uniqueRewards ? DEFAULT_PROBABILITY : probability).toString() ]; stringRewards.push(rewardsString.join(',')); } @@ -71,9 +199,9 @@ export const WiredActionGiveRewardView: FC<{}> = props => useEffect(() => { - const readRewards: { isBadge: boolean, itemCode: string, probability: number }[] = []; + const readRewards: RewardEntry[] = []; - if(trigger.stringData.length > 0 && trigger.stringData.includes(';')) + if(trigger.stringData.length > 0) { const splittedRewards = trigger.stringData.split(';'); @@ -83,11 +211,11 @@ export const WiredActionGiveRewardView: FC<{}> = props => if(reward.length !== 3) continue; - readRewards.push({ isBadge: reward[0] === '0', itemCode: reward[1], probability: Number(reward[2]) }); + readRewards.push(parseRewardEntry(reward[0], reward[1], reward[2])); } } - if(readRewards.length === 0) readRewards.push({ isBadge: false, itemCode: '', probability: null }); + if(readRewards.length === 0) readRewards.push(createReward()); setRewardTime((trigger.intData.length > 0) ? trigger.intData[0] : 0); setUniqueRewards((trigger.intData.length > 1) ? (trigger.intData[1] === 1) : false); @@ -147,24 +275,64 @@ export const WiredActionGiveRewardView: FC<{}> = props =>
+
+ Type + Amount / Value + { uniqueRewards ? 'Mode' : 'Chance %' } + { hasCustomCurrencyReward ? 'Currency Type' : 'Extra / Info' } + Action +
{ rewards && rewards.map((reward, index) => { + const rewardTypeOptions = (reward.rewardType === 'respect') + ? REWARD_TYPES + : SELECTABLE_REWARD_TYPES; + return ( -
-
- updateReward(index, e.target.checked, reward.itemCode, reward.probability) } /> - Badge? +
+ + updateReward(index, prevValue => ({ ...prevValue, rewardValue: event.target.value })) } /> + { uniqueRewards + ?
+ Unique +
+ : updateReward(index, prevValue => ({ ...prevValue, probability: Number(event.target.value) })) } /> } + { (reward.rewardType === 'points') + ? + updateReward(index, prevValue => ({ ...prevValue, pointsType: Number(event.target.value) })) } /> + :
+ { getExtraFieldLabel(reward.rewardType) } +
} +
+ { (index > 0) && + }
- updateReward(index, reward.isBadge, e.target.value, reward.probability) } /> - updateReward(index, reward.isBadge, reward.itemCode, Number(e.target.value)) } /> - { (index > 0) && - }
); }) }
+ + Extra Currency uses Amount as the quantity and Currency Type as the purse type id. Example: amount 200 + type 105. + ); }; diff --git a/src/hooks/notification/useNotification.ts b/src/hooks/notification/useNotification.ts index 4cd3f68..41e85d1 100644 --- a/src/hooks/notification/useNotification.ts +++ b/src/hooks/notification/useNotification.ts @@ -1,4 +1,4 @@ -import { AchievementNotificationMessageEvent, ActivityPointNotificationMessageEvent, ClubGiftNotificationEvent, ClubGiftSelectedEvent, ConnectionErrorEvent, GetLocalizationManager, GetRoomEngine, GetSessionDataManager, HabboBroadcastMessageEvent, HotelClosedAndOpensEvent, HotelClosesAndWillOpenAtEvent, HotelWillCloseInMinutesEvent, InfoFeedEnableMessageEvent, MaintenanceStatusMessageEvent, ModeratorCautionEvent, ModeratorMessageEvent, MOTDNotificationEvent, NotificationDialogMessageEvent, PetLevelNotificationEvent, PetReceivedMessageEvent, RespectReceivedEvent, RoomEnterEffect, RoomEnterEvent, SimpleAlertMessageEvent, UserBannedMessageEvent, Vector3d } from '@nitrots/nitro-renderer'; +import { AchievementNotificationMessageEvent, ActivityPointNotificationMessageEvent, ClubGiftNotificationEvent, ClubGiftSelectedEvent, ConnectionErrorEvent, GetLocalizationManager, GetRoomEngine, GetSessionDataManager, HabboBroadcastMessageEvent, HotelClosedAndOpensEvent, HotelClosesAndWillOpenAtEvent, HotelWillCloseInMinutesEvent, InfoFeedEnableMessageEvent, MaintenanceStatusMessageEvent, ModeratorCautionEvent, ModeratorMessageEvent, MOTDNotificationEvent, NotificationDialogMessageEvent, PetLevelNotificationEvent, PetReceivedMessageEvent, RespectReceivedEvent, RoomEnterEffect, RoomEnterEvent, SimpleAlertMessageEvent, UserBannedMessageEvent, Vector3d, WiredRewardResultMessageEvent } from '@nitrots/nitro-renderer'; import { useCallback, useState } from 'react'; import { useBetween } from 'use-between'; import { GetConfigurationValue, LocalizeBadgeName, LocalizeText, NotificationAlertItem, NotificationAlertType, NotificationBubbleItem, NotificationBubbleType, NotificationConfirmItem, PlaySound, ProductImageUtility, TradingNotificationType } from '../../api'; @@ -397,6 +397,28 @@ const useNotificationState = () => simpleAlert(LocalizeText(parser.alertMessage), NotificationAlertType.DEFAULT, null, null, LocalizeText(parser.titleMessage ? parser.titleMessage : 'notifications.broadcast.title')); }); + useMessageEvent(WiredRewardResultMessageEvent, event => + { + const parser = event.getParser(); + + switch(parser.reason) + { + case WiredRewardResultMessageEvent.PRODUCT_DONATED_CODE: + case WiredRewardResultMessageEvent.BADGE_DONATED_CODE: + simpleAlert(LocalizeText('wiredfurni.rewardsuccess.body'), NotificationAlertType.DEFAULT, null, null, LocalizeText('wiredfurni.rewardsuccess.title')); + return; + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 8: + simpleAlert(LocalizeText(`wiredfurni.rewardfailed.reason.${ parser.reason }`), NotificationAlertType.DEFAULT, null, null, LocalizeText('wiredfurni.rewardfailed.title')); + return; + } + }); + const onRoomEnterEvent = useCallback(() => { if(modDisclaimerShown) return; diff --git a/src/hooks/rooms/widgets/useChatWidget.ts b/src/hooks/rooms/widgets/useChatWidget.ts index d430a38..648a102 100644 --- a/src/hooks/rooms/widgets/useChatWidget.ts +++ b/src/hooks/rooms/widgets/useChatWidget.ts @@ -123,9 +123,10 @@ const useChatWidgetState = () => text = LocalizeText('widget.chatbubble.handitem', ['username', 'handitem'], [username, LocalizeText(('handitem' + event.extraParam))]); break; case RoomSessionChatEvent.CHAT_TYPE_MUTE_REMAINING: { - const hours = ((event.extraParam > 0) ? Math.floor((event.extraParam / 3600)) : 0).toString(); - const minutes = ((event.extraParam > 0) ? Math.floor((event.extraParam % 3600) / 60) : 0).toString(); - const seconds = (event.extraParam % 60).toString(); + const remainingSeconds = Math.max(0, event.extraParam); + const hours = Math.floor(remainingSeconds / 3600).toString(); + const minutes = Math.floor((remainingSeconds % 3600) / 60).toString(); + const seconds = (remainingSeconds % 60).toString(); text = LocalizeText('widget.chatbubble.mutetime', ['hours', 'minutes', 'seconds'], [hours, minutes, seconds]); break;