5180 fichiers 38.5 Mo 0 téléchargements 246 vues mis à jour il y a 5 heures ui
VaultView.tsx 14.4 Ko Télécharger le fichier
import {
    AddLinkEventTracker,
    ClaimAllEarningsRewardsComposer,
    ClaimEarningsRewardComposer,
    EarningsCenterEvent,
    EarningsClaimResultEvent,
    IEarningsEntry,
    IncomeRewardNotificationEvent,
    IEarningsReward,
    ILinkEventTracker,
    RemoveLinkEventTracker,
    RequestEarningsCenterComposer
} from '@octane/renderer';
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { LocalizeText, NotificationBubbleType, 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, OctaneCardContentView, OctaneCardHeaderView, OctaneCardView, Text } from '../../common';
import { useMessageEvent, useNotification } from '../../hooks';
import { hasClaimableEarnings, useEarningsIndicatorStore } from '../../state/earningsIndicatorStore';
import { BaseButton } from '../../common/BaseButton';
import { NativeTextTree } from '../../common/native-text/NativeTextTree';

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[];
    // Only shown once the server sends the row enabled (AIR hides the agency row unless the hotel runs it).
    onlyWhenEnabled?: boolean;
}

// 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] },
    // AIR earnings category 13: the credits wired gives with that earnings category wait here (games is category 11).
    { key: 'agency', textKey: 'earnings.agency.label', label: 'Agenzia', img: imgGeneric, fallbackCurrencies: [-1], onlyWhenEnabled: true }
];

/** The rows the window shows: every fixed row, and a conditional row only once the server enabled it. */
const visibleEarningCategories =(entries: ReadonlyMap<string, IEarningsEntry>): EarningCategory[] =>
    CATEGORIES.filter((category) => !category.onlyWhenEnabled || !!entries.get(category.key)?.enabled);

// 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;
    }
};

export const VaultView: FC<{}> = (props) => {
    const [isVisible, setIsVisible] = useState(false);
    const [entries, setEntries] = useState<IEarningsEntry[]>([]);

    const entriesByKey = useMemo(() => {
        const map = new Map<string, IEarningsEntry>();
        for (const entry of entries) map.set(entry.categoryKey, entry);
        return map;
    }, [entries]);

    const anyClaimable = useMemo(() => entries.some((entry) => entry.enabled && entry.claimable), [entries]);
    const { showSingleBubble = null } = useNotification();
    const firstStatus = useRef(true);

    // AIR SessionDataManager asks for the income reward status at login, so the purse indicator knows of rewards
    // that were already waiting.
    useEffect(() => {
        SendMessageComposer(new RequestEarningsCenterComposer());
    }, []);

    useMessageEvent<EarningsCenterEvent>(
        EarningsCenterEvent,
        useCallback((event: EarningsCenterEvent) => {
            const parser = event.getParser();
            if (!parser) return;
            setEntries(parser.entries ?? []);

            // AIR EarningsController.onIncomeRewardStatus: only the first status lights the indicator.
            if (!firstStatus.current) return;
            firstStatus.current = false;
            if (hasClaimableEarnings(parser.entries ?? [])) useEarningsIndicatorStore.getState().setShowing(true);
        }, [])
    );

    // AIR EarningsController.onIncomeRewardNotification: the notification.earning.new bubble links to the vault; an
    // open window refreshes, a closed one lights the purse indicator.
    useMessageEvent<IncomeRewardNotificationEvent>(
        IncomeRewardNotificationEvent,
        useCallback(() => {
            showSingleBubble?.(
                localizeWithFallback('notification.earning.new', 'You have received new earnings! Click to see them'),
                NotificationBubbleType.EARNING,
                null,
                'habboUI/open/vault'
            );

            if (isVisible) {
                SendMessageComposer(new RequestEarningsCenterComposer());
                return;
            }

            useEarningsIndicatorStore.getState().setShowing(true);
        }, [isVisible, showSingleBubble])
    );

    // AIR EarningsController.showEarnings: opening the window clears the indicator.
    useEffect(() => {
        if (isVisible) useEarningsIndicatorStore.getState().setShowing(false);
    }, [isVisible]);

    useMessageEvent<EarningsClaimResultEvent>(
        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 (
        <NativeTextTree>
            <OctaneCardView
                className="octane-vault min-w-0 w-[min(430px,calc(100vw-16px))] max-w-[calc(100vw-16px)] max-h-[calc(100vh-16px)]"
                theme="primary-slim"
                uniqueKey="vault"
            >
                <OctaneCardHeaderView headerText={localizeWithFallback('earnings.title', 'Guadagni')} onCloseClick={() => setIsVisible(false)} />
                <OctaneCardContentView className="octane-vault-content flex flex-col gap-[3px] text-black">
                    {visibleEarningCategories(entriesByKey).map((category) => {
                        const entry = entriesByKey.get(category.key) ?? null;
                        const canClaim = !!entry && entry.enabled && entry.claimable;
                        const rewards = entry?.rewards ?? [];

                        return (
                            <div key={category.key} className="flex items-center gap-2">
                                <div className="flex min-w-0 flex-1 items-center gap-2 rounded-[5px] border border-[#9aa0a8] bg-white px-1.5 py-1">
                                    <span className="flex h-[24px] w-[24px] shrink-0 items-center justify-center rounded border border-black/15 bg-white">
                                        <img src={category.img} alt="" className="max-h-[20px] max-w-[20px] object-contain image-rendering-pixelated" />
                                    </span>
                                    <Text bold className="truncate octane-vault__label">
                                        {localizeWithFallback(category.textKey, category.label)}
                                    </Text>
                                </div>
                                <div className="flex min-w-[92px] shrink-0 items-center justify-end gap-2.5">
                                    {rewards.length > 0
                                        ? rewards.map((reward, index) => {
                                              const currencyType = rewardCurrencyType(reward);
                                              return (
                                                  <span key={index} className="flex items-center gap-1">
                                                      {currencyType !== null && <LayoutCurrencyIcon type={currencyType} />}
                                                      <Text bold className="octane-vault__value">
                                                          {reward.amount}
                                                      </Text>
                                                  </span>
                                              );
                                          })
                                        : category.fallbackCurrencies.map((currency, index) => (
                                              <span key={index} className="flex items-center gap-1">
                                                  <LayoutCurrencyIcon type={currency} />
                                                  <Text bold className="octane-vault__value">
                                                      0
                                                  </Text>
                                              </span>
                                          ))}
                                </div>
                                <BaseButton
                                    type="button"
                                    disabled={!canClaim}
                                    onClick={canClaim ? () => claimOne(category.key) : undefined}
                                    className="octane-vault__claim shrink-0"
                                >
                                    {localizeWithFallback('earnings.claim.button', 'Riscatta')}
                                </BaseButton>
                            </div>
                        );
                    })}
                    <div className="flex justify-center pt-1">
                        <BaseButton
                            type="button"
                            disabled={!anyClaimable}
                            onClick={anyClaimable ? claimAll : undefined}
                            className="octane-vault__claim-all"
                        >
                            {localizeWithFallback('earnings.claim.all', 'Richiedili Tutti')}
                        </BaseButton>
                    </div>
                </OctaneCardContentView>
            </OctaneCardView>
        </NativeTextTree>
    );
};
15Sujets
1Dépôts
20Membres
2En ligne
Profile
Inscrit
Dernière visite
Messages
0
Votes positifs
0
Votes négatifs
0

Nous utilisons des cookies pour assurer le bon fonctionnement du site et améliorer votre expérience. en savoir plus

Autoriser