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/public/configuration/renderer-config.example b/public/configuration/renderer-config.example
index 792be76..c40cdee 100644
--- a/public/configuration/renderer-config.example
+++ b/public/configuration/renderer-config.example
@@ -30,6 +30,9 @@
"pet.asset.url": "${asset.url}/pets/%libname%.nitro",
"generic.asset.url": "${asset.url}/generic/%libname%.nitro",
"badge.asset.url": "${image.library.url}album1584/%badgename%.gif",
+ "radio.url": "${gamedata.url}/radio-stations.json5?t=%timestamp%",
+ "soundboard.url": "${gamedata.url}/soundboard-sounds.json5?t=%timestamp%",
+ "radio_ui": false,
"furni.rotation.bounce.steps": 20,
"furni.rotation.bounce.height": 0.0625,
"enable.avatar.arrow": false,
diff --git a/src/components/MainView.tsx b/src/components/MainView.tsx
index aeb5d91..e155f05 100644
--- a/src/components/MainView.tsx
+++ b/src/components/MainView.tsx
@@ -1,6 +1,7 @@
import { AddLinkEventTracker, GetCommunication, GetRoomSessionManager, HabboWebTools, ILinkEventTracker, RemoveLinkEventTracker, RoomSessionEvent } from '@nitrots/nitro-renderer';
import { AnimatePresence, motion } from 'framer-motion';
import { FC, useEffect, useState } from 'react';
+import { GetConfigurationValue } from '../api';
import { useNitroEventReducer } from '../hooks';
import { AchievementsView } from './achievements/AchievementsView';
import { AvatarEditorView } from './avatar-editor';
@@ -183,7 +184,7 @@ export const MainView: FC<{}> = props =>
-
+ { GetConfigurationValue('radio_ui', true) && }
>
);
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/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));