diff --git a/src/assets/images/vault/achievements.png b/src/assets/images/vault/achievements.png new file mode 100644 index 0000000..7c3d98a Binary files /dev/null and b/src/assets/images/vault/achievements.png differ diff --git a/src/assets/images/vault/bonusbag.png b/src/assets/images/vault/bonusbag.png new file mode 100644 index 0000000..085b2ee Binary files /dev/null and b/src/assets/images/vault/bonusbag.png differ diff --git a/src/assets/images/vault/dailygift.png b/src/assets/images/vault/dailygift.png new file mode 100644 index 0000000..3b52264 Binary files /dev/null and b/src/assets/images/vault/dailygift.png differ diff --git a/src/assets/images/vault/donations.png b/src/assets/images/vault/donations.png new file mode 100644 index 0000000..03474e2 Binary files /dev/null and b/src/assets/images/vault/donations.png differ diff --git a/src/assets/images/vault/games.png b/src/assets/images/vault/games.png new file mode 100644 index 0000000..d565c61 Binary files /dev/null and b/src/assets/images/vault/games.png differ diff --git a/src/assets/images/vault/generic.png b/src/assets/images/vault/generic.png new file mode 100644 index 0000000..171a5f1 Binary files /dev/null and b/src/assets/images/vault/generic.png differ diff --git a/src/assets/images/vault/hcpayday.png b/src/assets/images/vault/hcpayday.png new file mode 100644 index 0000000..e34f210 Binary files /dev/null and b/src/assets/images/vault/hcpayday.png differ diff --git a/src/assets/images/vault/levelprogression.png b/src/assets/images/vault/levelprogression.png new file mode 100644 index 0000000..2a8e8c5 Binary files /dev/null and b/src/assets/images/vault/levelprogression.png differ diff --git a/src/assets/images/vault/marketplace.png b/src/assets/images/vault/marketplace.png new file mode 100644 index 0000000..44f3b4b Binary files /dev/null and b/src/assets/images/vault/marketplace.png differ diff --git a/src/assets/images/vault/surprise.png b/src/assets/images/vault/surprise.png new file mode 100644 index 0000000..b4b1ce9 Binary files /dev/null and b/src/assets/images/vault/surprise.png differ 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 + + )) } +
+ +
+ ); + }) } +
+ +
+
+
+ ); +};