From 47e8338570611be4084273c8184a5a299a0c7603 Mon Sep 17 00:00:00 2001 From: duckietm Date: Thu, 28 May 2026 16:27:48 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=86=99=20Wheel=20of=20prizes=20!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/configuration/UITexts_nl.json5.example | 2 + .../FortuneWheelSettingsView.tsx | 147 +++++++++++++ .../fortune-wheel/FortuneWheelView.tsx | 13 +- src/components/rare-values/RareValuesView.tsx | 165 ++------------ .../FortuneWheelSettingsView.tsx | 147 +++++++++++++ .../fortune-wheel/FortuneWheelView.tsx | 202 ++++++++++++++++++ .../rare-values/RareValuesView.tsx | 115 ++++++++++ .../rooms/widgets/useChatCommandSelector.ts | 58 ++--- 8 files changed, 677 insertions(+), 172 deletions(-) create mode 100644 src/components/fortune-wheel/FortuneWheelSettingsView.tsx create mode 100644 src/components/user-settings/fortune-wheel/FortuneWheelSettingsView.tsx create mode 100644 src/components/user-settings/fortune-wheel/FortuneWheelView.tsx create mode 100644 src/components/user-settings/rare-values/RareValuesView.tsx diff --git a/public/configuration/UITexts_nl.json5.example b/public/configuration/UITexts_nl.json5.example index 68f003f..8bbedba 100644 --- a/public/configuration/UITexts_nl.json5.example +++ b/public/configuration/UITexts_nl.json5.example @@ -639,6 +639,8 @@ 'wheel.free.today': 'Je hebt vandaag %count% gratis draaibeurten!', 'wheel.extra': 'Extra draaibeurten: %count%', 'wheel.spin': 'DRAAIEN', + 'wheel.settings': 'Settings', + 'wheel.settings.title': 'Rad van Fortuin Settings', 'wheel.buy': 'Draaibeurt kopen', 'wheel.winners': 'Laatste winnaars', 'wheel.winners.empty': 'Nog geen winnaars', diff --git a/src/components/fortune-wheel/FortuneWheelSettingsView.tsx b/src/components/fortune-wheel/FortuneWheelSettingsView.tsx new file mode 100644 index 0000000..628eeb0 --- /dev/null +++ b/src/components/fortune-wheel/FortuneWheelSettingsView.tsx @@ -0,0 +1,147 @@ +import { IWheelAdminPrize, IWheelAdminPrizeEdit } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useState } from 'react'; +import { LocalizeText } from '../../api'; +import { Column, Flex, Text } from '../../common'; +import { useFortuneWheel } from '../../hooks'; +import { NitroCard } from '../../layout'; + +interface EditRow +{ + id: number; + category: string; + num: number; + weight: number; + label: string; +} + +interface CategoryDef +{ + key: string; + labelKey: string; +} + +const CATEGORIES: CategoryDef[] = [ + { key: 'item', labelKey: 'rarevalues.editor.cat.item' }, + { key: 'diamonds', labelKey: 'achievements.activitypoint.5' }, + { key: 'duckets', labelKey: 'achievements.activitypoint.0' }, + { key: 'credits', labelKey: 'credits' }, + { key: 'spins', labelKey: 'rarevalues.editor.cat.spin' }, + { key: 'nothing', labelKey: 'rarevalues.editor.cat.nothing' } +]; + +const prizeToCategory = (prize: IWheelAdminPrize): string => +{ + switch(prize.type) + { + case 'item': return 'item'; + case 'points': return (prize.pointsType === 5) ? 'diamonds' : 'duckets'; + case 'credits': return 'credits'; + case 'spin': return 'spins'; + default: return 'nothing'; + } +}; + +const prizeToNum = (prize: IWheelAdminPrize): number => + (prize.type === 'item') ? (parseInt(prize.value) || 0) : prize.amount; + +const rowToEdit = (row: EditRow): IWheelAdminPrizeEdit => +{ + const base = { id: row.id, weight: row.weight, label: row.label }; + + switch(row.category) + { + case 'item': return { ...base, type: 'item', value: String(row.num), amount: 1, pointsType: 0 }; + case 'diamonds': return { ...base, type: 'points', value: '', amount: row.num, pointsType: 5 }; + case 'duckets': return { ...base, type: 'points', value: '', amount: row.num, pointsType: 0 }; + case 'credits': return { ...base, type: 'credits', value: '', amount: row.num, pointsType: 0 }; + case 'spins': return { ...base, type: 'spin', value: '', amount: row.num, pointsType: 0 }; + default: return { ...base, type: 'nothing', value: '', amount: 0, pointsType: 0 }; + } +}; + +interface FortuneWheelSettingsViewProps +{ + onClose: () => void; +} + +export const FortuneWheelSettingsView: FC = ({ onClose }) => +{ + const { adminPrizes = [], loadAdminPrizes = null, saveAdminPrizes = null } = useFortuneWheel(); + const [ editRows, setEditRows ] = useState([]); + + useEffect(() => + { + if(loadAdminPrizes) loadAdminPrizes(); + }, [ loadAdminPrizes ]); + + useEffect(() => + { + setEditRows(adminPrizes.map(prize => ({ + id: prize.id, + category: prizeToCategory(prize), + num: prizeToNum(prize), + weight: prize.weight, + label: prize.label + }))); + }, [ adminPrizes ]); + + const updateRow = (id: number, patch: Partial) => + setEditRows(prev => prev.map(row => (row.id === id) ? { ...row, ...patch } : row)); + + return ( + + + + + + { LocalizeText('rarevalues.editor.type') } + { LocalizeText('rarevalues.editor.value') } + { LocalizeText('rarevalues.editor.weight') } + { LocalizeText('rarevalues.editor.label') } + + + { editRows.map(row => ( + + + updateRow(row.id, { num: parseInt(event.target.value) || 0 }) } + className="w-16 rounded border border-black/20 bg-white px-1 py-0.5 text-sm text-[#1f2d34] disabled:opacity-40" /> + updateRow(row.id, { weight: parseInt(event.target.value) || 0 }) } + className="w-12 rounded border border-black/20 bg-white px-1 py-0.5 text-sm text-[#1f2d34]" /> + updateRow(row.id, { label: event.target.value }) } + className="min-w-0 grow rounded border border-black/20 bg-white px-1 py-0.5 text-sm text-[#1f2d34]" /> + + )) } + { !editRows.length && + { LocalizeText('wheel.settings.empty') } } + + + + + + ); +}; diff --git a/src/components/fortune-wheel/FortuneWheelView.tsx b/src/components/fortune-wheel/FortuneWheelView.tsx index a66af12..37aa94b 100644 --- a/src/components/fortune-wheel/FortuneWheelView.tsx +++ b/src/components/fortune-wheel/FortuneWheelView.tsx @@ -2,8 +2,9 @@ import { AddLinkEventTracker, GetRoomEngine, ILinkEventTracker, IWheelPrize, Rem import { FC, useEffect, useMemo, useRef, useState } from 'react'; import { LocalizeText } from '../../api'; import { Column, Flex, LayoutAvatarImageView, LayoutBadgeImageView, LayoutCurrencyIcon, LayoutImage, Text } from '../../common'; -import { useFortuneWheel } from '../../hooks'; +import { useFortuneWheel, useHasPermission } from '../../hooks'; import { NitroCard } from '../../layout'; +import { FortuneWheelSettingsView } from './FortuneWheelSettingsView'; // Stock UI palette (white / light-blue / grey / black). const SLICE_COLORS = [ '#eef2f5', '#c3dcec' ]; @@ -42,7 +43,9 @@ const renderPrizeIcon = (prize: IWheelPrize) => export const FortuneWheelView: FC<{}> = () => { const [ isVisible, setIsVisible ] = useState(false); + const [ isSettingsOpen, setIsSettingsOpen ] = useState(false); const { freeSpins, extraSpins, spinCost, spinCostType, prizes, recentWins, pendingPrizeId, isSpinning, open, spin, buySpin, finishSpin } = useFortuneWheel(); + const canManage = useHasPermission('acc_wheeladmin'); const [ rotation, setRotation ] = useState(0); const rotationRef = useRef(0); const prizesRef = useRef([]); @@ -164,6 +167,12 @@ export const FortuneWheelView: FC<{}> = () => { LocalizeText('wheel.buy') } { spinCost } + { canManage && + } @@ -186,6 +195,8 @@ export const FortuneWheelView: FC<{}> = () => + { canManage && isSettingsOpen && + setIsSettingsOpen(false) } /> } ); }; diff --git a/src/components/rare-values/RareValuesView.tsx b/src/components/rare-values/RareValuesView.tsx index 5cb4ce7..c03f787 100644 --- a/src/components/rare-values/RareValuesView.tsx +++ b/src/components/rare-values/RareValuesView.tsx @@ -1,8 +1,8 @@ -import { AddLinkEventTracker, GetRoomEngine, GetSessionDataManager, ILinkEventTracker, IRareValue, IWheelAdminPrize, IWheelAdminPrizeEdit, RemoveLinkEventTracker } from '@nitrots/nitro-renderer'; +import { AddLinkEventTracker, GetRoomEngine, GetSessionDataManager, ILinkEventTracker, IRareValue, RemoveLinkEventTracker } from '@nitrots/nitro-renderer'; import { FC, useEffect, useMemo, useState } from 'react'; import { LocalizeFormattedNumber, LocalizeText } from '../../api'; import { Column, Flex, LayoutCurrencyIcon, LayoutImage, Text } from '../../common'; -import { useFortuneWheel, useHasPermission, useRareValues } from '../../hooks'; +import { useRareValues } from '../../hooks'; import { NitroCard, NitroInput } from '../../layout'; interface RareValueRow @@ -13,63 +13,11 @@ interface RareValueRow value: IRareValue; } -interface EditRow -{ - id: number; - category: string; - num: number; - weight: number; - label: string; -} - -const CATEGORIES: { key: string; label: string }[] = [ - { key: 'item', label: 'Raro (ID)' }, - { key: 'diamanti', label: 'Diamanti' }, - { key: 'duckets', label: 'Duckets' }, - { key: 'crediti', label: 'Crediti' }, - { key: 'giri', label: 'Giri extra' }, - { key: 'nulla', label: 'Nulla' } -]; - -const prizeToCategory = (prize: IWheelAdminPrize): string => -{ - switch(prize.type) - { - case 'item': return 'item'; - case 'points': return (prize.pointsType === 5) ? 'diamanti' : 'duckets'; - case 'credits': return 'crediti'; - case 'spin': return 'giri'; - default: return 'nulla'; - } -}; - -const prizeToNum = (prize: IWheelAdminPrize): number => - (prize.type === 'item') ? (parseInt(prize.value) || 0) : prize.amount; - -const rowToEdit = (row: EditRow): IWheelAdminPrizeEdit => -{ - const base = { id: row.id, weight: row.weight, label: row.label }; - - switch(row.category) - { - case 'item': return { ...base, type: 'item', value: String(row.num), amount: 1, pointsType: 0 }; - case 'diamanti': return { ...base, type: 'points', value: '', amount: row.num, pointsType: 5 }; - case 'duckets': return { ...base, type: 'points', value: '', amount: row.num, pointsType: 0 }; - case 'crediti': return { ...base, type: 'credits', value: '', amount: row.num, pointsType: 0 }; - case 'giri': return { ...base, type: 'spin', value: '', amount: row.num, pointsType: 0 }; - default: return { ...base, type: 'nothing', value: '', amount: 0, pointsType: 0 }; - } -}; - export const RareValuesView: FC<{}> = () => { const [ isVisible, setIsVisible ] = useState(false); - const [ tab, setTab ] = useState<'values' | 'editor'>('values'); const [ searchValue, setSearchValue ] = useState(''); const { values = null, loaded = false } = useRareValues(); - const { adminPrizes = [], loadAdminPrizes = null, saveAdminPrizes = null } = useFortuneWheel(); - const canEdit = useHasPermission('acc_supporttool'); - const [ editRows, setEditRows ] = useState([]); useEffect(() => { @@ -94,16 +42,6 @@ export const RareValuesView: FC<{}> = () => return () => RemoveLinkEventTracker(linkTracker); }, []); - useEffect(() => - { - if(isVisible && (tab === 'editor') && canEdit && loadAdminPrizes) loadAdminPrizes(); - }, [ isVisible, tab, canEdit, loadAdminPrizes ]); - - useEffect(() => - { - setEditRows(adminPrizes.map(prize => ({ id: prize.id, category: prizeToCategory(prize), num: prizeToNum(prize), weight: prize.weight, label: prize.label }))); - }, [ adminPrizes ]); - const rows = useMemo(() => { if(!values) return []; @@ -143,91 +81,34 @@ export const RareValuesView: FC<{}> = () => if(!isVisible) return null; - const updateRow = (id: number, patch: Partial) => - setEditRows(prev => prev.map(row => (row.id === id) ? { ...row, ...patch } : row)); - return ( setIsVisible(false) } /> - { canEdit && - - setTab('values') }> - { LocalizeText('rarevalues.title') } - - setTab('editor') }> - { LocalizeText('rarevalues.editor.tab') } - - } - { (tab === 'values' || !canEdit) && - - setSearchValue(event.target.value) } /> - - { !loaded && - { LocalizeText('rarevalues.loading') } } - { (loaded && !filtered.length) && - { LocalizeText('rarevalues.empty') } } - { filtered.map(row => ( - - - { row.name } - - { LocalizeFormattedNumber(row.value.points) } - - + + setSearchValue(event.target.value) } /> + + { !loaded && + { LocalizeText('rarevalues.loading') } } + { (loaded && !filtered.length) && + { LocalizeText('rarevalues.empty') } } + { filtered.map(row => ( + + + { row.name } + + { LocalizeFormattedNumber(row.value.points) } + - )) } - - } - - { (tab === 'editor' && canEdit) && - - - { LocalizeText('rarevalues.editor.type') } - { LocalizeText('rarevalues.editor.value') } - { LocalizeText('rarevalues.editor.weight') } - { LocalizeText('rarevalues.editor.label') } - - - { editRows.map(row => ( - - - updateRow(row.id, { num: parseInt(event.target.value) || 0 }) } - className="w-16 rounded border border-black/20 bg-white px-1 py-0.5 text-sm text-[#1f2d34] disabled:opacity-40" /> - updateRow(row.id, { weight: parseInt(event.target.value) || 0 }) } - className="w-12 rounded border border-black/20 bg-white px-1 py-0.5 text-sm text-[#1f2d34]" /> - updateRow(row.id, { label: event.target.value }) } - className="min-w-0 grow rounded border border-black/20 bg-white px-1 py-0.5 text-sm text-[#1f2d34]" /> - - )) } - - - } + + )) } + + ); diff --git a/src/components/user-settings/fortune-wheel/FortuneWheelSettingsView.tsx b/src/components/user-settings/fortune-wheel/FortuneWheelSettingsView.tsx new file mode 100644 index 0000000..91cc44c --- /dev/null +++ b/src/components/user-settings/fortune-wheel/FortuneWheelSettingsView.tsx @@ -0,0 +1,147 @@ +import { IWheelAdminPrize, IWheelAdminPrizeEdit } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useState } from 'react'; +import { LocalizeText } from '../../api'; +import { Column, Flex, Text } from '../../common'; +import { useFortuneWheel } from '../../hooks'; +import { NitroCard } from '../../layout'; + +interface EditRow +{ + id: number; + category: string; + num: number; + weight: number; + label: string; +} + +interface CategoryDef +{ + key: string; + labelKey: string; +} + +const CATEGORIES: CategoryDef[] = [ + { key: 'item', labelKey: 'rarevalues.editor.cat.item' }, + { key: 'diamanti', labelKey: 'achievements.activitypoint.5' }, + { key: 'duckets', labelKey: 'achievements.activitypoint.0' }, + { key: 'crediti', labelKey: 'credits' }, + { key: 'giri', labelKey: 'rarevalues.editor.cat.spin' }, + { key: 'nulla', labelKey: 'rarevalues.editor.cat.nothing' } +]; + +const prizeToCategory = (prize: IWheelAdminPrize): string => +{ + switch(prize.type) + { + case 'item': return 'item'; + case 'points': return (prize.pointsType === 5) ? 'diamanti' : 'duckets'; + case 'credits': return 'crediti'; + case 'spin': return 'giri'; + default: return 'nulla'; + } +}; + +const prizeToNum = (prize: IWheelAdminPrize): number => + (prize.type === 'item') ? (parseInt(prize.value) || 0) : prize.amount; + +const rowToEdit = (row: EditRow): IWheelAdminPrizeEdit => +{ + const base = { id: row.id, weight: row.weight, label: row.label }; + + switch(row.category) + { + case 'item': return { ...base, type: 'item', value: String(row.num), amount: 1, pointsType: 0 }; + case 'diamanti': return { ...base, type: 'points', value: '', amount: row.num, pointsType: 5 }; + case 'duckets': return { ...base, type: 'points', value: '', amount: row.num, pointsType: 0 }; + case 'crediti': return { ...base, type: 'credits', value: '', amount: row.num, pointsType: 0 }; + case 'giri': return { ...base, type: 'spin', value: '', amount: row.num, pointsType: 0 }; + default: return { ...base, type: 'nothing', value: '', amount: 0, pointsType: 0 }; + } +}; + +interface FortuneWheelSettingsViewProps +{ + onClose: () => void; +} + +export const FortuneWheelSettingsView: FC = ({ onClose }) => +{ + const { adminPrizes = [], loadAdminPrizes = null, saveAdminPrizes = null } = useFortuneWheel(); + const [ editRows, setEditRows ] = useState([]); + + useEffect(() => + { + if(loadAdminPrizes) loadAdminPrizes(); + }, [ loadAdminPrizes ]); + + useEffect(() => + { + setEditRows(adminPrizes.map(prize => ({ + id: prize.id, + category: prizeToCategory(prize), + num: prizeToNum(prize), + weight: prize.weight, + label: prize.label + }))); + }, [ adminPrizes ]); + + const updateRow = (id: number, patch: Partial) => + setEditRows(prev => prev.map(row => (row.id === id) ? { ...row, ...patch } : row)); + + return ( + + + + + + { LocalizeText('rarevalues.editor.type') } + { LocalizeText('rarevalues.editor.value') } + { LocalizeText('rarevalues.editor.weight') } + { LocalizeText('rarevalues.editor.label') } + + + { editRows.map(row => ( + + + updateRow(row.id, { num: parseInt(event.target.value) || 0 }) } + className="w-16 rounded border border-black/20 bg-white px-1 py-0.5 text-sm text-[#1f2d34] disabled:opacity-40" /> + updateRow(row.id, { weight: parseInt(event.target.value) || 0 }) } + className="w-12 rounded border border-black/20 bg-white px-1 py-0.5 text-sm text-[#1f2d34]" /> + updateRow(row.id, { label: event.target.value }) } + className="min-w-0 grow rounded border border-black/20 bg-white px-1 py-0.5 text-sm text-[#1f2d34]" /> + + )) } + { !editRows.length && + { LocalizeText('wheel.settings.empty') } } + + + + + + ); +}; diff --git a/src/components/user-settings/fortune-wheel/FortuneWheelView.tsx b/src/components/user-settings/fortune-wheel/FortuneWheelView.tsx new file mode 100644 index 0000000..37aa94b --- /dev/null +++ b/src/components/user-settings/fortune-wheel/FortuneWheelView.tsx @@ -0,0 +1,202 @@ +import { AddLinkEventTracker, GetRoomEngine, ILinkEventTracker, IWheelPrize, RemoveLinkEventTracker } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useMemo, useRef, useState } from 'react'; +import { LocalizeText } from '../../api'; +import { Column, Flex, LayoutAvatarImageView, LayoutBadgeImageView, LayoutCurrencyIcon, LayoutImage, Text } from '../../common'; +import { useFortuneWheel, useHasPermission } from '../../hooks'; +import { NitroCard } from '../../layout'; +import { FortuneWheelSettingsView } from './FortuneWheelSettingsView'; + +// Stock UI palette (white / light-blue / grey / black). +const SLICE_COLORS = [ '#eef2f5', '#c3dcec' ]; +const RIM = '#4c606c'; +const WHEEL_SIZE = 420; +const ICON_RADIUS = 150; +const FULL_TURNS = 5; + +const renderPrizeIcon = (prize: IWheelPrize) => +{ + switch(prize.type) + { + case 'item': + return ; + case 'badge': + return ; + case 'credits': + return ( + + + { prize.amount } + ); + case 'points': + return ( + + + { prize.amount } + ); + case 'spin': + return +{ prize.amount }; + default: + return ; + } +}; + +export const FortuneWheelView: FC<{}> = () => +{ + const [ isVisible, setIsVisible ] = useState(false); + const [ isSettingsOpen, setIsSettingsOpen ] = useState(false); + const { freeSpins, extraSpins, spinCost, spinCostType, prizes, recentWins, pendingPrizeId, isSpinning, open, spin, buySpin, finishSpin } = useFortuneWheel(); + const canManage = useHasPermission('acc_wheeladmin'); + const [ rotation, setRotation ] = useState(0); + const rotationRef = useRef(0); + const prizesRef = useRef([]); + prizesRef.current = prizes; + + useEffect(() => + { + const linkTracker: ILinkEventTracker = { + linkReceived: (url: string) => + { + const parts = url.split('/'); + if(parts.length < 2) return; + + switch(parts[1]) + { + case 'show': setIsVisible(true); return; + case 'hide': setIsVisible(false); return; + case 'toggle': setIsVisible(prev => !prev); return; + } + }, + eventUrlPrefix: 'fortune-wheel/', + }; + + AddLinkEventTracker(linkTracker); + + return () => RemoveLinkEventTracker(linkTracker); + }, []); + + useEffect(() => + { + if(isVisible) open(); + }, [ isVisible, open ]); + + // Drive the spin animation when the server reports the winning slice. + useEffect(() => + { + if(pendingPrizeId < 0) return; + + const list = prizesRef.current; + const idx = list.findIndex(prize => prize.id === pendingPrizeId); + + if(!list.length || (idx < 0)) + { + finishSpin(); + return; + } + + const sliceAngle = 360 / list.length; + const centerAngle = ((idx + 0.5) * sliceAngle); + const current = rotationRef.current; + const target = (current - (current % 360)) + (FULL_TURNS * 360) + (360 - centerAngle); + + rotationRef.current = target; + setRotation(target); + }, [ pendingPrizeId, finishSpin ]); + + const sliceAngle = prizes.length ? (360 / prizes.length) : 0; + + const background = useMemo(() => + { + if(!prizes.length) return SLICE_COLORS[0]; + + const stops = prizes.map((_, i) => `${ SLICE_COLORS[i % 2] } ${ i * sliceAngle }deg ${ (i + 1) * sliceAngle }deg`).join(', '); + return `conic-gradient(${ stops })`; + }, [ prizes, sliceAngle ]); + + if(!isVisible) return null; + + const canSpin = ((freeSpins + extraSpins) > 0) && !isSpinning && (prizes.length > 0); + + return ( + + setIsVisible(false) } /> + + + +
+
+
{ if(isSpinning) finishSpin(); } }> + { prizes.map((_, i) => ( +
+ )) } + { prizes.map((prize, i) => + { + const centerAngle = ((i + 0.5) * sliceAngle); + return ( +
+
+ { renderPrizeIcon(prize) } +
+
); + }) } +
+
+
+ { LocalizeText('wheel.free.today', [ 'count' ], [ freeSpins.toString() ]) } + { LocalizeText('wheel.extra', [ 'count' ], [ extraSpins.toString() ]) } + + + + { canManage && + } + + + + { LocalizeText('wheel.winners') } + + { recentWins.map((win, i) => ( + +
+ +
+ + { win.username } + { win.prizeLabel } + +
+ )) } + { !recentWins.length && + { LocalizeText('wheel.winners.empty') } } +
+
+ + + { canManage && isSettingsOpen && + setIsSettingsOpen(false) } /> } + + ); +}; diff --git a/src/components/user-settings/rare-values/RareValuesView.tsx b/src/components/user-settings/rare-values/RareValuesView.tsx new file mode 100644 index 0000000..c03f787 --- /dev/null +++ b/src/components/user-settings/rare-values/RareValuesView.tsx @@ -0,0 +1,115 @@ +import { AddLinkEventTracker, GetRoomEngine, GetSessionDataManager, ILinkEventTracker, IRareValue, RemoveLinkEventTracker } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useMemo, useState } from 'react'; +import { LocalizeFormattedNumber, LocalizeText } from '../../api'; +import { Column, Flex, LayoutCurrencyIcon, LayoutImage, Text } from '../../common'; +import { useRareValues } from '../../hooks'; +import { NitroCard, NitroInput } from '../../layout'; + +interface RareValueRow +{ + spriteId: number; + name: string; + iconUrl: string; + value: IRareValue; +} + +export const RareValuesView: FC<{}> = () => +{ + const [ isVisible, setIsVisible ] = useState(false); + const [ searchValue, setSearchValue ] = useState(''); + const { values = null, loaded = false } = useRareValues(); + + useEffect(() => + { + const linkTracker: ILinkEventTracker = { + linkReceived: (url: string) => + { + const parts = url.split('/'); + if(parts.length < 2) return; + + switch(parts[1]) + { + case 'show': setIsVisible(true); return; + case 'hide': setIsVisible(false); return; + case 'toggle': setIsVisible(prev => !prev); return; + } + }, + eventUrlPrefix: 'rare-values/', + }; + + AddLinkEventTracker(linkTracker); + + return () => RemoveLinkEventTracker(linkTracker); + }, []); + + const rows = useMemo(() => + { + if(!values) return []; + + const list: RareValueRow[] = []; + + values.forEach((value, spriteId) => + { + if(value.points <= 0) return; + + const floorData = GetSessionDataManager().getFloorItemData(spriteId); + const wallData = floorData ? null : GetSessionDataManager().getWallItemData(spriteId); + const data = (floorData ?? wallData); + + if(!data) return; + + const iconUrl = (floorData + ? GetRoomEngine().getFurnitureFloorIconUrl(spriteId) + : GetRoomEngine().getFurnitureWallIconUrl(spriteId)); + + list.push({ spriteId, name: (data.name || data.className || `#${ spriteId }`), iconUrl, value }); + }); + + list.sort((a, b) => (b.value.points - a.value.points)); + + return list; + }, [ values ]); + + const filtered = useMemo(() => + { + const query = searchValue.trim().toLocaleLowerCase(); + + if(!query) return rows; + + return rows.filter(row => row.name.toLocaleLowerCase().includes(query)); + }, [ rows, searchValue ]); + + if(!isVisible) return null; + + return ( + + setIsVisible(false) } /> + + + setSearchValue(event.target.value) } /> + + { !loaded && + { LocalizeText('rarevalues.loading') } } + { (loaded && !filtered.length) && + { LocalizeText('rarevalues.empty') } } + { filtered.map(row => ( + + + { row.name } + + { LocalizeFormattedNumber(row.value.points) } + + + + )) } + + + + + ); +}; diff --git a/src/hooks/rooms/widgets/useChatCommandSelector.ts b/src/hooks/rooms/widgets/useChatCommandSelector.ts index 9584e92..31dd495 100644 --- a/src/hooks/rooms/widgets/useChatCommandSelector.ts +++ b/src/hooks/rooms/widgets/useChatCommandSelector.ts @@ -1,36 +1,35 @@ import { AvailableCommandsEvent, GetCommunication } from '@nitrots/nitro-renderer'; import { useCallback, useEffect, useMemo, useState } from 'react'; -import { CommandDefinition } from '../../../api'; +import { CommandDefinition, LocalizeText } from '../../../api'; import { createNitroStore } from '../../../state/createNitroStore'; import { useMessageEvent } from '../../events'; -// Client-only commands are static; safe to keep at module scope. -const CLIENT_COMMANDS: CommandDefinition[] = [ - // Effetti stanza - { key: 'shake', description: 'Scuoti la stanza' }, - { key: 'rotate', description: 'Ruota la stanza' }, - { key: 'zoom', description: 'Zoom stanza' }, - { key: 'flip', description: 'Reset zoom' }, - { key: 'iddqd', description: 'Reset zoom' }, - { key: 'screenshot', description: 'Screenshot stanza' }, - { key: 'togglefps', description: 'Toggle FPS' }, - // Espressioni - { key: 'd', description: 'Ridi (VIP)' }, - { key: 'kiss', description: 'Manda un bacio (VIP)' }, - { key: 'jump', description: 'Salta (VIP)' }, - { key: 'idle', description: 'Vai in idle' }, - { key: 'sign', description: 'Mostra cartello' }, - // Gestione stanza - { key: 'furni', description: 'Furni chooser' }, - { key: 'chooser', description: 'User chooser' }, - { key: 'floor', description: 'Floor editor' }, - { key: 'bcfloor', description: 'Floor editor' }, - { key: 'pickall', description: 'Raccogli tutti i furni' }, - { key: 'ejectall', description: 'Espelli tutti i furni' }, - { key: 'settings', description: 'Impostazioni stanza' }, +const CLIENT_COMMANDS: { key: string; descriptionKey: string }[] = [ + // Room effects + { key: 'shake', descriptionKey: 'chatcmd.client.shake' }, + { key: 'rotate', descriptionKey: 'chatcmd.client.rotate' }, + { key: 'zoom', descriptionKey: 'chatcmd.client.zoom' }, + { key: 'flip', descriptionKey: 'chatcmd.client.flip' }, + { key: 'iddqd', descriptionKey: 'chatcmd.client.iddqd' }, + { key: 'screenshot', descriptionKey: 'chatcmd.client.screenshot' }, + { key: 'togglefps', descriptionKey: 'chatcmd.client.togglefps' }, + // Expressions + { key: 'd', descriptionKey: 'chatcmd.client.laugh' }, + { key: 'kiss', descriptionKey: 'chatcmd.client.kiss' }, + { key: 'jump', descriptionKey: 'chatcmd.client.jump' }, + { key: 'idle', descriptionKey: 'chatcmd.client.idle' }, + { key: 'sign', descriptionKey: 'chatcmd.client.sign' }, + // Room management + { key: 'furni', descriptionKey: 'chatcmd.client.furni' }, + { key: 'chooser', descriptionKey: 'chatcmd.client.chooser' }, + { key: 'floor', descriptionKey: 'chatcmd.client.floor' }, + { key: 'bcfloor', descriptionKey: 'chatcmd.client.floor' }, + { key: 'pickall', descriptionKey: 'chatcmd.client.pickall' }, + { key: 'ejectall', descriptionKey: 'chatcmd.client.ejectall' }, + { key: 'settings', descriptionKey: 'chatcmd.client.settings' }, // Info - { key: 'client', description: 'Info client' }, - { key: 'nitro', description: 'Info client' }, + { key: 'client', descriptionKey: 'chatcmd.client.info' }, + { key: 'nitro', descriptionKey: 'chatcmd.client.info' }, ]; /** @@ -110,11 +109,12 @@ export const useChatCommandSelector = (chatValue: string) => const allCommands = useMemo(() => { - const merged = [ ...serverCommands ]; + const merged: CommandDefinition[] = [ ...serverCommands ]; for(const clientCmd of CLIENT_COMMANDS) { - if(!merged.some(cmd => cmd.key === clientCmd.key)) merged.push(clientCmd); + if(merged.some(cmd => cmd.key === clientCmd.key)) continue; + merged.push({ key: clientCmd.key, description: LocalizeText(clientCmd.descriptionKey) }); } return merged.sort((a, b) => a.key.localeCompare(b.key));