nitro-v36-with-renderer-v36
/
src /
components /
room /
widgets /
resolution /
AchievementResolutionView.tsx
AchievementResolutionView.tsx 11.6 Ko
Télécharger le fichier
import {
AchievementEvent,
AchievementResolutionCompletedMessageEvent,
AchievementResolutionData,
AchievementResolutionProgressMessageEvent,
AchievementResolutionsMessageEvent,
GetResolutionAchievementsMessageComposer,
ResetResolutionAchievementMessageComposer
} from '@octane/renderer';
import { FC, useEffect, useState } from 'react';
import { FriendlyTime, LocalizeBadgeDescription, LocalizeBadgeName, localizeWithFallback, SendMessageComposer } from '../../../../api';
import { Button, LayoutBadgeImageView, OctaneCardContentView, OctaneCardHeaderView, OctaneCardView, Text } from '../../../../common';
import { useMessageEvent, useNotification } from '../../../../hooks';
import { BaseButton } from '../../../../common/BaseButton';
import { Hint } from '../../../../common/Tooltip';
import '../../../../css/room/RoomWidgetsSweepAir.css';
import { NativeTextTree } from '../../../../common/native-text/NativeTextTree';
type ResolutionView =
| { kind: 'picker'; stuffId: number; achievements: AchievementResolutionData[]; endsAt: number }
| { kind: 'progress'; stuffId: number; badgeCode: string; progress: number; total: number; endsAt: number }
| { kind: 'completed'; badgeCode: string };
const STATE_SELECTABLE = 0;
/** Habbo's New Year resolution windows, opened by clicking a resolution furni. */
export const AchievementResolutionView: FC = () => {
const [view, setView] = useState<ResolutionView>(null);
const [selectedId, setSelectedId] = useState(-1);
const [now, setNow] = useState(() => Date.now());
const { showConfirm = null } = useNotification();
useMessageEvent<AchievementResolutionsMessageEvent>(AchievementResolutionsMessageEvent, (event) => {
const parser = event.getParser();
// Habbo keeps the picker closed when there is nothing to choose.
if (!parser || !parser.achievements?.length) return;
setSelectedId(-1);
setView({ kind: 'picker', stuffId: parser.stuffId, achievements: parser.achievements, endsAt: Date.now() + parser.endTime * 1000 });
});
useMessageEvent<AchievementResolutionProgressMessageEvent>(AchievementResolutionProgressMessageEvent, (event) => {
const parser = event.getParser();
if (!parser) return;
setView({
kind: 'progress',
stuffId: parser.stuffId,
badgeCode: parser.requiredLevelBadgeCode,
progress: parser.userProgress,
total: parser.totalProgress,
endsAt: Date.now() + parser.endTime * 1000
});
});
useMessageEvent<AchievementResolutionCompletedMessageEvent>(AchievementResolutionCompletedMessageEvent, (event) => {
const parser = event.getParser();
if (parser) setView({ kind: 'completed', badgeCode: parser.badgeCode });
});
// An achievement moved on while the progress window is open: ask for fresh numbers.
useMessageEvent<AchievementEvent>(AchievementEvent, () => {
if (view?.kind === 'progress') SendMessageComposer(new GetResolutionAchievementsMessageComposer(view.stuffId, 0));
});
useEffect(() => {
if (!view || view.kind === 'completed') return;
const timer = setInterval(() => setNow(Date.now()), 1000);
return () => clearInterval(timer);
}, [view]);
if (!view) return null;
const close = () => setView(null);
const timeLeft = (endsAt: number) => FriendlyTime.format(Math.max(0, Math.floor((endsAt - now) / 1000)));
if (view.kind === 'completed') {
return (
<NativeTextTree>
<OctaneCardView className="octane-resolution" theme="primary-slim">
<OctaneCardHeaderView headerText={localizeWithFallback('resolution.completed.title', 'New Year resolution')} onCloseClick={close} />
<OctaneCardContentView center gap={2}>
<LayoutBadgeImageView badgeCode={view.badgeCode} />
<Text bold className="octane-resolution__title">{localizeWithFallback('resolution.completed.header', 'Yay, you made it!')}</Text>
<Text center small>{localizeWithFallback('resolution.completed.description', '')}</Text>
<Button variant="secondary" onClick={close}>{localizeWithFallback('resolution.completed.close', 'Close window')}</Button>
</OctaneCardContentView>
</OctaneCardView>
</NativeTextTree>
);
}
if (view.kind === 'progress') {
const percent = view.total > 0 ? Math.min(100, Math.round((view.progress / view.total) * 100)) : 0;
const reset = () =>
showConfirm(
`${localizeWithFallback('resolution.reset.confirmation.text', 'Are you sure you want to re-select')} ${LocalizeBadgeName(view.badgeCode)}?`,
() => {
SendMessageComposer(new ResetResolutionAchievementMessageComposer(view.stuffId));
SendMessageComposer(new GetResolutionAchievementsMessageComposer(view.stuffId, 0));
},
null,
null,
null,
localizeWithFallback('resolution.reset.confirmation.title', 'Reset progress')
);
return (
<NativeTextTree>
<OctaneCardView className="octane-resolution" theme="primary-slim">
<OctaneCardHeaderView headerText={localizeWithFallback('resolution.progress.title', 'New Year resolution')} onCloseClick={close} />
<OctaneCardContentView gap={2}>
<div className="flex items-center gap-2">
<LayoutBadgeImageView badgeCode={view.badgeCode} />
<div className="min-w-0">
<Text bold className="octane-resolution__name">{LocalizeBadgeName(view.badgeCode)}</Text>
<Text small>{LocalizeBadgeDescription(view.badgeCode)}</Text>
</div>
</div>
<div className="h-[14px] rounded bg-black/20 overflow-hidden">
<div className="h-full bg-[#3c8a1e]" style={{ width: `${percent}%` }} />
</div>
<Text small className="octane-resolution__progress">
{localizeWithFallback('resolution.progress.progress', `Your progress ${view.progress}/${view.total}`, ['progress', 'total'], [view.progress.toString(), view.total.toString()])}
</Text>
<Text small className="octane-resolution__time-left">
{localizeWithFallback('resolution.progress.time.left', 'Time left:')} {timeLeft(view.endsAt)}
</Text>
<div className="flex justify-between mt-auto">
<Button variant="secondary" onClick={reset}>{localizeWithFallback('resolution.progress.reset', 'Re-select achievement')}</Button>
<Button variant="secondary" onClick={close}>{localizeWithFallback('generic.close', 'Close')}</Button>
</div>
</OctaneCardContentView>
</OctaneCardView>
</NativeTextTree>
);
}
const selected = view.achievements.find((achievement) => achievement.achievementId === selectedId) ?? null;
const selectedBadge = selected?.badgeId ?? '';
const canPick = !!selected && selected.state === STATE_SELECTABLE;
const pick = () => {
if (!canPick) return;
showConfirm(
localizeWithFallback('resolution.confirmation.text', 'You can\'t change your selection later on.'),
() => {
SendMessageComposer(new GetResolutionAchievementsMessageComposer(view.stuffId, selected.achievementId));
close();
},
null,
null,
null,
localizeWithFallback('resolution.confirmation.title', 'Are you sure you want to pick this achievement?')
);
};
return (
<NativeTextTree>
<OctaneCardView className="octane-resolution octane-resolution--picker" theme="primary-slim">
<OctaneCardHeaderView headerText={localizeWithFallback('resolution.title', 'Set yourself a New Year resolution!')} onCloseClick={close} />
<OctaneCardContentView gap={2} overflow="hidden">
<Text small>{localizeWithFallback('resolution.header', 'Pick an achievement and see what level you need to reach.')}</Text>
<div className="grid grid-cols-6 gap-1 overflow-auto max-h-[150px] p-1 rounded bg-black/10">
{view.achievements.map((achievement) => (
<Hint key={achievement.achievementId} content={LocalizeBadgeName(achievement.badgeId)}>
<BaseButton aria-label={LocalizeBadgeName(achievement.badgeId)}
className={`p-0.5 rounded border-2 bg-transparent cursor-pointer ${achievement.achievementId === selectedId ? 'border-[#1e7295]' : 'border-transparent'} ${achievement.state === STATE_SELECTABLE ? '' : 'opacity-40'}`}
type="button"
onClick={() => setSelectedId(achievement.achievementId)}
>
<LayoutBadgeImageView badgeCode={achievement.badgeId} />
</BaseButton>
</Hint>
))}
</div>
{selected && (
<div className="flex flex-col gap-0.5">
<Text bold className="octane-resolution__name">{LocalizeBadgeName(selectedBadge)}</Text>
<Text small>{LocalizeBadgeDescription(selectedBadge)}</Text>
<Text small>
{localizeWithFallback('resolution.achievement.level', 'Your current level')}:{' '}
{localizeWithFallback('resolution.achievement.level.value', `Level ${selected.level}`, ['level'], [selected.level.toString()])}
</Text>
{selected.state === STATE_SELECTABLE ? (
<Text small bold>
{localizeWithFallback('resolution.achievement.target', 'Target level')}:{' '}
{localizeWithFallback('resolution.achievement.target.value', `Level ${selected.requiredLevel}`, ['level'], [selected.requiredLevel.toString()])}
</Text>
) : (
<Text small className="octane-resolution__disabled text-[#a81a12]">{localizeWithFallback(`resolution.disabled.${selected.state}`, '')}</Text>
)}
</div>
)}
<Text small variant="muted">
{localizeWithFallback('resolution.progress.time.left', 'Time left:')} {timeLeft(view.endsAt)}
</Text>
<div className="flex justify-between mt-auto gap-1">
<Button variant="secondary" onClick={close}>{localizeWithFallback('resolution.button.cancel', 'No thanks, maybe later!')}</Button>
<Button variant="secondary" disabled={!canPick} onClick={pick}>{localizeWithFallback('resolution.button.ok', 'Make the resolution!')}</Button>
</div>
</OctaneCardContentView>
</OctaneCardView>
</NativeTextTree>
);
};
