From d15457b43c4a47d60f3cc650a62f56fc6e750f1a Mon Sep 17 00:00:00 2001 From: simoleo89 <11816867+simoleo89@users.noreply.github.com> Date: Mon, 15 Jun 2026 22:00:08 +0200 Subject: [PATCH] feat(vault): wire Guadagni window to earnings packets Request earnings on open (RequestEarningsCenterComposer), render real amounts/claimable per category from EarningsCenterEvent, per-row Riscatta + Richiedili Tutti send the claim composers, refresh on EarningsClaimResultEvent. Category keys aligned to the emulator contract; reward currencies derived from reward type; rows fall back to the static skeleton before data lands. --- src/components/vault/VaultView.tsx | 194 +++++++++++++++++++++++------ 1 file changed, 153 insertions(+), 41 deletions(-) diff --git a/src/components/vault/VaultView.tsx b/src/components/vault/VaultView.tsx index 7d62637..cae1964 100644 --- a/src/components/vault/VaultView.tsx +++ b/src/components/vault/VaultView.tsx @@ -1,6 +1,6 @@ -import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer'; -import { FC, useEffect, useState } from 'react'; -import { LocalizeText } from '../../api'; +import { AddLinkEventTracker, ClaimAllEarningsRewardsComposer, ClaimEarningsRewardComposer, EarningsCenterEvent, EarningsClaimResultEvent, IEarningsEntry, IEarningsReward, ILinkEventTracker, RemoveLinkEventTracker, RequestEarningsCenterComposer } from '@nitrots/nitro-renderer'; +import { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import { LocalizeText, SendMessageComposer } from '../../api'; import imgAchievements from '../../assets/images/vault/achievements.png'; import imgBonusbag from '../../assets/images/vault/bonusbag.png'; import imgDailygift from '../../assets/images/vault/dailygift.png'; @@ -12,6 +12,7 @@ import imgLevel from '../../assets/images/vault/levelprogression.png'; import imgMarketplace from '../../assets/images/vault/marketplace.png'; import imgSurprise from '../../assets/images/vault/surprise.png'; import { LayoutCurrencyIcon, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common'; +import { useMessageEvent } from '../../hooks'; const localizeWithFallback = (key: string, fallback: string) => { @@ -19,37 +20,56 @@ const localizeWithFallback = (key: string, fallback: string) => return (text && text !== key) ? text : fallback; }; -interface EarningRow +interface EarningCategory { + // Wire categoryKey — MUST match the emulator contract + // (emulatore/docs/earnings-packet-contract.md). key: string; // Standard gamedata localization key (ExternalTexts). 'label' is only the // fallback shown when the key is missing in the active texts. textKey: string; label: string; img: string; - currencies: number[]; + // Placeholder currency icons used only before the server entry arrives. + fallbackCurrencies: number[]; } -// Icons are the hotel's real earnings_icon_* assets. Amounts are placeholders -// (0) and claims are disabled until the emulator exposes the data + packets. -// 'games' and 'clubwork' have no standard earnings.*.label key — they use a -// custom key (add it to your texts) and fall back to the Italian label. -const EARNINGS: EarningRow[] = [ - { key: 'daily', textKey: 'earnings.dailygift.label', label: 'Regalo giornaliero', img: imgDailygift, currencies: [ 5 ] }, - { key: 'games', textKey: 'earnings.games.label', label: 'Giochi', img: imgGames, currencies: [ 0 ] }, - { key: 'achievements', textKey: 'earnings.achievements.label', label: 'Traguardi', img: imgAchievements, currencies: [ 5, 0 ] }, - { key: 'marketplace', textKey: 'earnings.marketplace.label', label: 'Mercatino', img: imgMarketplace, currencies: [ 0 ] }, - { key: 'hcpayday', textKey: 'earnings.hc.label', label: 'Bonus giorno di paga HC', img: imgHcpayday, currencies: [ 0 ] }, - { key: 'level', textKey: 'earnings.levelprogression.label', label: 'Progressione Livello', img: imgLevel, currencies: [ 5, 0 ] }, - { key: 'donations', textKey: 'earnings.donations.label', label: 'Donazioni', img: imgDonations, currencies: [ 0 ] }, - { key: 'bonusbag', textKey: 'earnings.bonusbag.label', label: 'Sacco Bonus', img: imgBonusbag, currencies: [ 0 ] }, - { key: 'surprise', textKey: 'earnings.surpriseboxes.label', label: 'Scatole Sorprese', img: imgSurprise, currencies: [ 5, 0 ] }, - { key: 'clubwork', textKey: 'earnings.clubwork.label', label: 'Club e Lavoro', img: imgGeneric, currencies: [ 0 ] } +// Fixed display order + icons/labels. Amounts, claimable state and the actual +// reward currencies come from the server (EarningsCenterEvent); these rows are +// the always-visible skeleton so the window matches the Habbo reference even +// before data lands. 'games' and 'club_job' have no standard earnings.*.label +// key — they use a custom key (add it to your texts) and fall back to Italian. +const CATEGORIES: EarningCategory[] = [ + { key: 'daily_gift', textKey: 'earnings.dailygift.label', label: 'Regalo giornaliero', img: imgDailygift, fallbackCurrencies: [ 5 ] }, + { key: 'games', textKey: 'earnings.games.label', label: 'Giochi', img: imgGames, fallbackCurrencies: [ 0 ] }, + { key: 'achievements', textKey: 'earnings.achievements.label', label: 'Traguardi', img: imgAchievements, fallbackCurrencies: [ 5, 0 ] }, + { key: 'marketplace', textKey: 'earnings.marketplace.label', label: 'Mercatino', img: imgMarketplace, fallbackCurrencies: [ 0 ] }, + { key: 'hc_payday', textKey: 'earnings.hc.label', label: 'Bonus giorno di paga HC', img: imgHcpayday, fallbackCurrencies: [ 0 ] }, + { key: 'level_progress', textKey: 'earnings.levelprogression.label', label: 'Progressione Livello', img: imgLevel, fallbackCurrencies: [ 5, 0 ] }, + { key: 'donations', textKey: 'earnings.donations.label', label: 'Donazioni', img: imgDonations, fallbackCurrencies: [ 0 ] }, + { key: 'bonus_bag', textKey: 'earnings.bonusbag.label', label: 'Sacco Bonus', img: imgBonusbag, fallbackCurrencies: [ 0 ] }, + { key: 'mystery_boxes', textKey: 'earnings.surpriseboxes.label', label: 'Scatole Sorprese', img: imgSurprise, fallbackCurrencies: [ 5, 0 ] }, + { key: 'club_job', textKey: 'earnings.clubwork.label', label: 'Club e Lavoro', img: imgGeneric, fallbackCurrencies: [ 0 ] } ]; +// Map a server reward type to a LayoutCurrencyIcon `type`. Returns null for +// rewards that aren't a currency (badge / item) — those show just the amount. +const rewardCurrencyType = (reward: IEarningsReward): number | string | null => +{ + switch(reward.type) + { + case 'credits': return -1; + case 'pixels': return 0; + case 'points': return reward.pointsType; + case 'hc_days': return 'hc'; + default: return null; + } +}; + // Scoped colour override for the Guadagni window only: classic blue header + // cool grey body (the shared 'primary-slim' theme is teal + cream). Higher -// specificity (.nitro-card.nitro-vault ...) than the theme so it wins. +// specificity (.nitro-card.nitro-vault ...) than the theme so it wins. The body +// element renders `.nitro-card-content-shell`, NOT `.content-area`. const VAULT_STYLES = ` .nitro-card.nitro-vault .nitro-card-header { background: linear-gradient(180deg, #5a80b8 0%, #3f63a0 100%); @@ -65,6 +85,51 @@ const VAULT_STYLES = ` export const VaultView: FC<{}> = props => { const [ isVisible, setIsVisible ] = useState(false); + const [ entries, setEntries ] = useState([]); + + const entriesByKey = useMemo(() => + { + const map = new Map(); + for(const entry of entries) map.set(entry.categoryKey, entry); + return map; + }, [ entries ]); + + const anyClaimable = useMemo(() => entries.some(entry => entry.enabled && entry.claimable), [ entries ]); + + useMessageEvent(EarningsCenterEvent, useCallback((event: EarningsCenterEvent) => + { + const parser = event.getParser(); + if(!parser) return; + setEntries(parser.entries ?? []); + }, [])); + + useMessageEvent(EarningsClaimResultEvent, useCallback((event: EarningsClaimResultEvent) => + { + const parser = event.getParser(); + if(!parser) return; + + setEntries(prev => + { + const next = prev.slice(); + + for(const result of parser.results) + { + if(result.hasEntry && result.entry) + { + const idx = next.findIndex(e => e.categoryKey === result.entry.categoryKey); + if(idx >= 0) next[idx] = result.entry; else next.push(result.entry); + } + else if(result.success) + { + // No refreshed entry but the claim worked — mark it spent. + const idx = next.findIndex(e => e.categoryKey === result.categoryKey); + if(idx >= 0) next[idx] = { ...next[idx], claimable: false }; + } + } + + return next; + }); + }, [])); useEffect(() => { @@ -97,6 +162,23 @@ export const VaultView: FC<{}> = props => return () => RemoveLinkEventTracker(linkTracker); }, []); + // Ask the server for fresh earnings every time the window opens. + useEffect(() => + { + if(!isVisible) return; + SendMessageComposer(new RequestEarningsCenterComposer()); + }, [ isVisible ]); + + const claimOne = useCallback((categoryKey: string) => + { + SendMessageComposer(new ClaimEarningsRewardComposer(categoryKey)); + }, []); + + const claimAll = useCallback(() => + { + SendMessageComposer(new ClaimAllEarningsRewardsComposer()); + }, []); + if(!isVisible) return null; return ( @@ -104,29 +186,59 @@ export const VaultView: FC<{}> = props => setIsVisible(false) } /> - { EARNINGS.map(row => ( -
-
- - - - { localizeWithFallback(row.textKey, row.label) } -
-
- { row.currencies.map((currency, index) => ( - - - 0 + { CATEGORIES.map(category => + { + const entry = entriesByKey.get(category.key) ?? null; + const canClaim = !!entry && entry.enabled && entry.claimable; + const rewards = entry?.rewards ?? []; + + return ( +
+
+ + - )) } + { localizeWithFallback(category.textKey, category.label) } +
+
+ { rewards.length > 0 + ? rewards.map((reward, index) => + { + const currencyType = rewardCurrencyType(reward); + return ( + + { currencyType !== null && } + { reward.amount } + + ); + }) + : category.fallbackCurrencies.map((currency, index) => ( + + + 0 + + )) } +
+
- -
- )) } + ); + }) }
-