From f7e10b5f460df3ab8f69bb3b7c57ef7e8d8053b4 Mon Sep 17 00:00:00 2001 From: simoleo89 <11816867+simoleo89@users.noreply.github.com> Date: Mon, 15 Jun 2026 22:29:10 +0200 Subject: [PATCH] feat(vault): functional Guadagni (Earnings Center) window VaultView wired to the Earnings Center packets: requests data on open, renders real amounts/claimable per category from EarningsCenterEvent, Riscatta + Richiedili Tutti send the claim composers, refreshes on EarningsClaimResultEvent. Category keys match the emulator contract; reward currencies derived from reward type. Adds the real earnings_icon assets. Wired into MainView. --- src/assets/images/vault/achievements.png | Bin 0 -> 724 bytes src/assets/images/vault/bonusbag.png | Bin 0 -> 815 bytes src/assets/images/vault/dailygift.png | Bin 0 -> 524 bytes src/assets/images/vault/donations.png | Bin 0 -> 454 bytes src/assets/images/vault/games.png | Bin 0 -> 749 bytes src/assets/images/vault/generic.png | Bin 0 -> 162 bytes src/assets/images/vault/hcpayday.png | Bin 0 -> 776 bytes src/assets/images/vault/levelprogression.png | Bin 0 -> 525 bytes src/assets/images/vault/marketplace.png | Bin 0 -> 617 bytes src/assets/images/vault/surprise.png | Bin 0 -> 780 bytes src/components/MainView.tsx | 2 + src/components/vault/VaultView.tsx | 248 +++++++++++++++++++ 12 files changed, 250 insertions(+) create mode 100644 src/assets/images/vault/achievements.png create mode 100644 src/assets/images/vault/bonusbag.png create mode 100644 src/assets/images/vault/dailygift.png create mode 100644 src/assets/images/vault/donations.png create mode 100644 src/assets/images/vault/games.png create mode 100644 src/assets/images/vault/generic.png create mode 100644 src/assets/images/vault/hcpayday.png create mode 100644 src/assets/images/vault/levelprogression.png create mode 100644 src/assets/images/vault/marketplace.png create mode 100644 src/assets/images/vault/surprise.png create mode 100644 src/components/vault/VaultView.tsx diff --git a/src/assets/images/vault/achievements.png b/src/assets/images/vault/achievements.png new file mode 100644 index 0000000000000000000000000000000000000000..7c3d98a6f8182761c4b4dcc1b7dcf18cf9d4f511 GIT binary patch literal 724 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 fmg?j@f&=^qi|-8;d%F!$qGz=jyG~<|GE45 z`Tyr{&;PHt`Yhqn{L}n$eit&I@m#Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0-Z@jK~zXfos_Xp z6hRcmfA11BG$bl_U;>FIG~9Wj{{RUj(tt4*lAwhYQcA5fK%>1a7T6pK6iS3bNI4hm@HFsJjQ zty($g)ZO|Z6pBbd z6&I-zz=+tYlZi{QC4va77iJlldS_y%2vCFA+YE?R)m09#UYJ7!x`qZ3iMlw+GDsrc zD{dhoXeiTGtO|D?50c4e0oW^+XzxuBNFw%%B^0H-cL0Fx)kRd5jhVSB;uNq`ERo4) zBM0sT7Dk30u{}In$&ZxLTe|S-fJ2wh*z1xUIPR zWDwQx?gt1H1N6O~;8ODqFaphh7Z5I*CBgEInK>X@v4AU}9tdz9_=#}=!QtVEgBv3P zs?grs&+dngpb;g-pU*xb@O9-c58muV*&&?*ei;$kdiq%}%+fXV$W?6{D3j08a{J@~ zJOgR-bA3?dBDm`Tv54^LMerote;YK?{AjCL8X#7E+WhZ_8?Dv2aR&~t^l}2s!C_v_ zI=0C1h6S7p-zz5)TU=Ctu#1FuK7up0sHT8(k$|+l&gXHEvI7Hmqe5^u;v$GUz%*VG z_TvxZDX8fHLlpnEv3i_Da*`aJhFb38a7CV1fI37XJMbU-dPQwvCR?Kb0000Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0isDnK~zXftyD2i zLop0}2@4wwogz-aSXA{SY>bGJvoIn?I`t%|DkCRAVd}z0hb9cM^BpJo{|oV?v~}#f z7dv(h<|8WGD7%SSK@D=o31xmhC3 z%_(xm9Fc0HM}CqK0M6)pN<<4(A!(Bwp%>Hf=@5h0!Te|Oi=HT>=4U7Z}C_Fhcf10ao`Ia)B=h-myarK zRicVVWmuwi>Qc30N{srqnM&2FZ P00000NkvXXu0mjfqh{N~ literal 0 HcmV?d00001 diff --git a/src/assets/images/vault/marketplace.png b/src/assets/images/vault/marketplace.png new file mode 100644 index 0000000000000000000000000000000000000000..44f3b4b4e80739518d3562ca71aded82ed107da5 GIT binary patch literal 617 zcmV-v0+#)WP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0scuuK~zXfwUoV5 zQ$ZAfzZ293OQBK`geRLcD>XAu@`K z=QFU`lQSqTs;Uj=Y&2;x+J89{pSyH+_S0j-KGVFN(QGc$+1a=M#Iwf_IUHP~xV}3MKqsCh{l^BUrOi$x zRu^LJ(7MO_YNGBI1V4Q0lD9Js2ba8>l71O^JL7(H+4iLw%tGGIc(=8Y9vibbSV%@K z(%MWTHEO+R8nZY^YgtQAfII171%qT?OK+AM{1blxW$&+;ad2Wz00000NkvXXu0mjf DHvkhR literal 0 HcmV?d00001 diff --git a/src/assets/images/vault/surprise.png b/src/assets/images/vault/surprise.png new file mode 100644 index 0000000000000000000000000000000000000000..b4b1ce944fa3683796e82f405d1331e95baef4de GIT binary patch literal 780 zcmV+n1M~ceP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0-;GnK~zXfjgwDE z6j2<M~1e|@L_>M#I< zy=ROTv}LCIv5X?xbYae~$TWSI9SO2xTZ4k+kxsDMH1R^FzFv#`YmgfDjqf(KSH+VGjU(w?;yB2h)+*XXEaq_~-%v zT{mAM<5c@arsv+XxU|6SySgPG+&K+k##X>GPJj?P6A40jq}nc-0KqaALY`3pk!MnC z2Nt1~3vmHLC@6x^JKsrwuw78Bu@Dy^xIr$F#==el1UD$wSojZ!eIf7mRKN590000< KMNUMnLSTXycWmAO literal 0 HcmV?d00001 diff --git a/src/components/MainView.tsx b/src/components/MainView.tsx index 8c883c5..1d56034 100644 --- a/src/components/MainView.tsx +++ b/src/components/MainView.tsx @@ -45,6 +45,7 @@ import { TranslationSettingsView } from './translation/TranslationSettingsView'; import { UserProfileView } from './user-profile/UserProfileView'; import { UserAccountSettingsView } from './user-settings/UserAccountSettingsView'; import { UserSettingsView } from './user-settings/UserSettingsView'; +import { VaultView } from './vault/VaultView'; import { WiredView } from './wired/WiredView'; import { WiredCreatorToolsView } from './wired-tools/WiredCreatorToolsView'; import { MentionsView } from './mentions'; @@ -221,6 +222,7 @@ export const MainView: FC<{}> = props => + diff --git a/src/components/vault/VaultView.tsx b/src/components/vault/VaultView.tsx new file mode 100644 index 0000000..cae1964 --- /dev/null +++ b/src/components/vault/VaultView.tsx @@ -0,0 +1,248 @@ +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'; +import imgDonations from '../../assets/images/vault/donations.png'; +import imgGames from '../../assets/images/vault/games.png'; +import imgGeneric from '../../assets/images/vault/generic.png'; +import imgHcpayday from '../../assets/images/vault/hcpayday.png'; +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) => +{ + const text = LocalizeText(key); + return (text && text !== key) ? text : fallback; +}; + +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; + // Placeholder currency icons used only before the server entry arrives. + fallbackCurrencies: number[]; +} + +// 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. 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%); + border-color: #34548a; + } + .nitro-card.nitro-vault, + .nitro-card.nitro-vault .content-area, + .nitro-card.nitro-vault .nitro-card-content-shell { + background: #dde1e6 !important; + } +`; + +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(() => + { + const linkTracker: ILinkEventTracker = { + linkReceived: (url: string) => + { + const parts = url.split('/'); + + if(parts.length < 3) return; + if(parts[2] !== 'vault') return; + + switch(parts[1]) + { + case 'open': + setIsVisible(true); + return; + case 'close': + setIsVisible(false); + return; + case 'toggle': + setIsVisible(prevValue => !prevValue); + return; + } + }, + eventUrlPrefix: 'habboUI/' + }; + + AddLinkEventTracker(linkTracker); + + 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 ( + + setIsVisible(false) } /> + + + { 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 + + )) } +
+ +
+ ); + }) } +
+ +
+
+
+ ); +};