Merge pull request #24 from duckietm/main

Prod to Dev
This commit is contained in:
DuckieTM
2026-03-20 07:19:27 +01:00
committed by GitHub
97 changed files with 3365 additions and 4007 deletions
-1
View File
@@ -21,7 +21,6 @@
"framer-motion": "^11.2.12",
"react": "^19.2.4",
"react-bootstrap": "^2.10.10",
"react-colorful": "^5.6.1",
"react-dom": "^19.2.4",
"react-icons": "^5.5.0",
"react-slider": "^2.0.6",
+68 -8
View File
@@ -13,12 +13,72 @@
"widget.settings.interface.fps.warning": "Het zetten van FPS naar unlimited kan prestatie problemen veroorzaken!",
"widget.settings.interface.secondary": "Verander de window header kleur",
"widget.settings.interface.reset": "Reset header kleur naar default",
"widget.room.chat.hide_pets": "Verberg dieren",
"widget.room.chat.hide_avatars": "Verberg avatars",
"widget.room.chat.hide_balloon": "Verberg Spreekballon",
"widget.room.chat.show_balloon": "Spreekballon",
"widget.room.chat.clear_history": "leeg geschiedenis",
"widget.room.youtube.shared": "YouTube word gedeeld",
"widget.room.youtube.open_video": "Open de video",
"wiredfurni.params.area_selection.selected": "Geselecteerd gebied: Lengte=%x%, Breedte=%y%, breedte=%w%, hoogte=%h%"
"widget.room.chat.hide_pets": "Verberg dieren",
"widget.room.chat.hide_avatars": "Verberg avatars",
"widget.room.chat.hide_balloon": "Verberg Spreekballon",
"widget.room.chat.show_balloon": "Spreekballon",
"widget.room.chat.clear_history": "leeg geschiedenis",
"widget.room.youtube.shared": "YouTube word gedeeld",
"widget.room.youtube.open_video": "Open de video",
"wiredfurni.params.area_selection.selected": "Geselecteerd gebied: Lengte=%x%, Breedte=%y%, breedte=%w%, hoogte=%h%",
"wiredfurni.params.sources.collapse": "Nascondi le impostazioni avanzate",
"wiredfurni.params.sources.expand": "Mostra le impostazioni avanzate",
"wiredfurni.params.quantifier_selection": "Abbina condizione se:",
"wiredfurni.params.quantifier.users.0": "Tutti gli utenti corrispondono",
"wiredfurni.params.quantifier.users.1": "Uno qualsiasi degli utenti corrisponde",
"wiredfurni.params.quantifier.users.neg.0": "Uno qualsiasi degli utenti non corrisponde",
"wiredfurni.params.quantifier.users.neg.1": "Nessuno degli utenti corrisponde",
"wiredfurni.params.quantifier.furni.0": "Tutti i Furni corrispondono",
"wiredfurni.params.quantifier.furni.1": "Uno qualsiasi dei Furni corrisponde",
"wiredfurni.params.usertype.1": "Habbo",
"wiredfurni.params.usertype.2": "Cucciolo",
"wiredfurni.params.usertype.4": "Bot",
"wiredfurni.params.sources.users.title.match.0": "Gli utenti da abbinare:",
"wiredfurni.params.sources.users.title.match.1": "Utenti da comparare con:",
"wiredfurni.params.sources.users.101": "Usa l'utente specificato dal nome",
"wiredfurni.params.comparison.0": "Più basso di",
"wiredfurni.params.comparison.1": "È uguale a",
"wiredfurni.params.comparison.2": "Più alto di",
"wiredfurni.params.team": "Scegli una squadra",
"wiredfurni.params.team.1": "Rossa",
"wiredfurni.params.team.2": "Verde",
"wiredfurni.params.team.3": "Blu",
"wiredfurni.params.team.4": "Gialla",
"wiredfurni.params.team.triggerer": "Squadra dell'innescatore",
"wiredfurni.params.comparison_selection": "Scegli tipo:",
"wiredfurni.params.setscore2": "La squadra deve segnare:",
"wiredfurni.params.placement_selection": "Posizione:",
"wiredfurni.params.placement.1": "1.",
"wiredfurni.params.placement.2": "2.",
"wiredfurni.params.placement.3": "3.",
"wiredfurni.params.placement.4": "4.",
"wiredfurni.params.time.hour_selection": "Ore:",
"wiredfurni.params.time.minute_selection": "Minuti:",
"wiredfurni.params.time.second_selection": "Secondi:",
"wiredfurni.params.time.skip": "Non usare il filtro",
"wiredfurni.params.time.exact": "Esatto",
"wiredfurni.params.time.range": "Range",
"wiredfurni.params.time.weekday_selection": "Giorno della settimana:",
"wiredfurni.params.time.weekday.1": "Lunedì",
"wiredfurni.params.time.weekday.2": "Martedì",
"wiredfurni.params.time.weekday.3": "Mercoledì",
"wiredfurni.params.time.weekday.4": "Giovedì",
"wiredfurni.params.time.weekday.5": "Venerdì",
"wiredfurni.params.time.weekday.6": "Sabato",
"wiredfurni.params.time.weekday.7": "Domenica",
"wiredfurni.params.time.day_selection": "Giorno:",
"wiredfurni.params.time.month_selection": "Mese:",
"wiredfurni.params.time.month.10": "Ott.",
"wiredfurni.params.time.month.11": "Nov.",
"wiredfurni.params.time.month.12": "Dic.",
"wiredfurni.params.time.month.1": "Gen.",
"wiredfurni.params.time.month.2": "Feb.",
"wiredfurni.params.time.month.3": "Mar.",
"wiredfurni.params.time.month.4": "Apr.",
"wiredfurni.params.time.month.5": "Mag.",
"wiredfurni.params.time.month.6": "Giu.",
"wiredfurni.params.time.month.7": "Lug.",
"wiredfurni.params.time.month.8": "Ago.",
"wiredfurni.params.time.month.9": "Set.",
"wiredfurni.params.time.year_selection": "Anno:"
}
File diff suppressed because it is too large Load Diff
+9 -9
View File
@@ -1,9 +1,10 @@
import { GetAssetManager, GetAvatarRenderManager, GetCommunication, GetConfiguration, GetLocalizationManager, GetRoomEngine, GetRoomSessionManager, GetSessionDataManager, GetSoundManager, GetStage, GetTexturePool, GetTicker, HabboWebTools, LegacyExternalInterface, LoadGameUrlEvent, NitroLogger, NitroVersion, PrepareRenderer } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { GetUIVersion, UiSettingsProvider } from './api';
import { GetUIVersion } from './api';
import { Base } from './common';
import { LoadingView } from './components/loading/LoadingView';
import { MainView } from './components/MainView';
import { ReconnectView } from './components/reconnect/ReconnectView';
import { useMessageEvent } from './hooks';
NitroVersion.UI_VERSION = GetUIVersion();
@@ -89,13 +90,12 @@ export const App: FC<{}> = props =>
}, []);
return (
<UiSettingsProvider>
<Base fit overflow="hidden" className={ !(window.devicePixelRatio % 1) && 'image-rendering-pixelated' }>
{ !isReady &&
<LoadingView /> }
{ isReady && <MainView /> }
<Base id="draggable-windows-container" />
</Base>
</UiSettingsProvider>
<Base fit overflow="hidden" className={ !(window.devicePixelRatio % 1) && 'image-rendering-pixelated' }>
{ !isReady &&
<LoadingView /> }
{ isReady && <MainView /> }
<ReconnectView />
<Base id="draggable-windows-container" />
</Base>
);
};
-21
View File
@@ -1,21 +0,0 @@
export interface IUiSettings
{
colorMode: 'color' | 'image' | 'default';
headerColor: string;
headerImageUrl: string;
headerAlpha: number;
}
export const DEFAULT_UI_SETTINGS: IUiSettings = {
colorMode: 'default',
headerColor: '#1E7295',
headerImageUrl: '',
headerAlpha: 100
};
export const PRESET_COLORS: string[] = [
'#000000', '#444444', '#888888', '#CCCCCC', '#660000', '#CC3333', '#FF6666', '#CC6600',
'#FF3333', '#FF6633', '#FF9933', '#FFCC00', '#FFFF00', '#66FF00', '#00CC00', '#009900',
'#00FFCC', '#33CCFF', '#3366FF', '#0000CC', '#6633CC', '#9933FF', '#CC33FF', '#FF66CC',
'#FF99CC', '#1E7295', '#185D79', '#2DABC2', '#2B91A7', '#283F5D'
];
-164
View File
@@ -1,164 +0,0 @@
import { createContext, FC, PropsWithChildren, useCallback, useContext, useEffect, useState } from 'react';
import { DEFAULT_UI_SETTINGS, IUiSettings } from './IUiSettings';
const STORAGE_KEY = 'nitro.ui.settings';
interface IUiSettingsContext
{
settings: IUiSettings;
isCustomActive: boolean;
updateSettings: (partial: Partial<IUiSettings>) => void;
resetSettings: () => void;
getHeaderStyle: () => React.CSSProperties;
getTabsStyle: () => React.CSSProperties;
getAccentColor: () => string;
}
const UiSettingsContext = createContext<IUiSettingsContext>({
settings: DEFAULT_UI_SETTINGS,
isCustomActive: false,
updateSettings: () => {},
resetSettings: () => {},
getHeaderStyle: () => ({}),
getTabsStyle: () => ({}),
getAccentColor: () => DEFAULT_UI_SETTINGS.headerColor
});
const darkenColor = (hex: string, amount: number): string =>
{
const num = parseInt(hex.replace('#', ''), 16);
const r = Math.max(0, ((num >> 16) & 0xFF) - amount);
const g = Math.max(0, ((num >> 8) & 0xFF) - amount);
const b = Math.max(0, (num & 0xFF) - amount);
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
};
const loadSettings = (): IUiSettings =>
{
try
{
const stored = localStorage.getItem(STORAGE_KEY);
if(stored) return { ...DEFAULT_UI_SETTINGS, ...JSON.parse(stored) };
}
catch(e) {}
return { ...DEFAULT_UI_SETTINGS };
};
const saveSettings = (settings: IUiSettings): void =>
{
try
{
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
}
catch(e) {}
};
export const UiSettingsProvider: FC<PropsWithChildren> = ({ children }) =>
{
const [ settings, setSettings ] = useState<IUiSettings>(loadSettings);
const updateSettings = useCallback((partial: Partial<IUiSettings>) =>
{
setSettings(prev =>
{
const updated = { ...prev, ...partial };
saveSettings(updated);
return updated;
});
}, []);
const resetSettings = useCallback(() =>
{
setSettings({ ...DEFAULT_UI_SETTINGS });
saveSettings(DEFAULT_UI_SETTINGS);
}, []);
const getHeaderStyle = useCallback((): React.CSSProperties =>
{
if(settings.colorMode === 'color')
{
return { backgroundColor: settings.headerColor };
}
if(settings.colorMode === 'image' && settings.headerImageUrl)
{
return {
backgroundImage: `url(${ settings.headerImageUrl })`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'repeat'
};
}
return {};
}, [ settings ]);
const getTabsStyle = useCallback((): React.CSSProperties =>
{
if(settings.colorMode === 'color')
{
return { backgroundColor: darkenColor(settings.headerColor, 30) };
}
if(settings.colorMode === 'image' && settings.headerImageUrl)
{
return {
backgroundImage: `url(${ settings.headerImageUrl })`,
backgroundSize: 'cover',
backgroundPosition: 'center bottom',
backgroundRepeat: 'repeat'
};
}
return {};
}, [ settings ]);
const getAccentColor = useCallback((): string =>
{
if(settings.colorMode === 'color') return settings.headerColor;
return DEFAULT_UI_SETTINGS.headerColor;
}, [ settings ]);
const isCustomActive = settings.colorMode !== 'default';
const ALL_CSS_VARS = [
'--ui-accent-color', '--ui-accent-dark',
'--ui-ctx-bg', '--ui-ctx-header-bg', '--ui-ctx-item-bg1', '--ui-ctx-item-bg2',
'--ui-btn-primary-bg', '--ui-btn-primary-border',
'--ui-dark-bg', '--ui-dark-border'
];
useEffect(() =>
{
const root = document.documentElement;
if(settings.colorMode === 'color')
{
const c = settings.headerColor;
root.style.setProperty('--ui-accent-color', c);
root.style.setProperty('--ui-accent-dark', darkenColor(c, 30));
root.style.setProperty('--ui-ctx-bg', darkenColor(c, 50));
root.style.setProperty('--ui-ctx-header-bg', darkenColor(c, 20));
root.style.setProperty('--ui-ctx-item-bg1', darkenColor(c, 60));
root.style.setProperty('--ui-ctx-item-bg2', darkenColor(c, 70));
root.style.setProperty('--ui-btn-primary-bg', c);
root.style.setProperty('--ui-btn-primary-border', darkenColor(c, 20));
root.style.setProperty('--ui-dark-bg', darkenColor(c, 55));
root.style.setProperty('--ui-dark-border', darkenColor(c, 60));
}
else
{
ALL_CSS_VARS.forEach(v => root.style.removeProperty(v));
}
}, [ settings ]);
return (
<UiSettingsContext.Provider value={ { settings, isCustomActive, updateSettings, resetSettings, getHeaderStyle, getTabsStyle, getAccentColor } }>
{ children }
</UiSettingsContext.Provider>
);
};
export const useUiSettings = () => useContext(UiSettingsContext);
-2
View File
@@ -1,2 +0,0 @@
export * from './IUiSettings';
export * from './UiSettingsContext';
+5
View File
@@ -32,4 +32,9 @@ export class WiredActionLayoutCode
public static USERS_AREA_SELECTOR: number = 31;
public static USERS_NEIGHBORHOOD_SELECTOR: number = 32;
public static SEND_SIGNAL: number = 33;
public static FREEZE: number = 34;
public static UNFREEZE: number = 35;
public static FURNI_TO_USER: number = 36;
public static USER_TO_FURNI: number = 37;
public static FURNI_TO_FURNI: number = 38;
}
+11
View File
@@ -26,4 +26,15 @@ export class WiredConditionlayout
public static NOT_ACTOR_WEARING_EFFECT: number = 23;
public static DATE_RANGE_ACTIVE: number = 24;
public static ACTOR_HAS_HANDITEM: number = 25;
public static COUNTER_TIME_MATCHES: number = 27;
public static USER_PERFORMS_ACTION: number = 28;
public static HAS_ALTITUDE: number = 29;
public static NOT_USER_PERFORMS_ACTION: number = 30;
public static NOT_ACTOR_HAS_HANDITEM: number = 31;
public static TRIGGERER_MATCH: number = 32;
public static NOT_TRIGGERER_MATCH: number = 33;
public static TEAM_HAS_SCORE: number = 34;
public static TEAM_HAS_RANK: number = 35;
public static MATCH_TIME: number = 36;
public static MATCH_DATE: number = 37;
}
+61 -10
View File
@@ -3,25 +3,42 @@ import { GetRoomEngine, IRoomObject, IRoomObjectSpriteVisualization, RoomObjectC
export class WiredSelectionVisualizer
{
private static _selectionShader: WiredFilter = new WiredFilter({
lineColor: [ 1, 1, 1 ],
color: [ 0.6, 0.6, 0.6 ]
lineColor: [ 0.45, 0.95, 0.55 ],
color: [ 0.18, 0.78, 0.30 ]
});
private static _secondarySelectionShader: WiredFilter = new WiredFilter({
lineColor: [ 0.45, 0.78, 1 ],
color: [ 0.20, 0.52, 0.95 ]
});
public static show(furniId: number): void
{
WiredSelectionVisualizer.applySelectionShader(WiredSelectionVisualizer.getRoomObject(furniId));
WiredSelectionVisualizer.applySelectionShader(WiredSelectionVisualizer.getRoomObject(furniId), WiredSelectionVisualizer._selectionShader);
}
public static hide(furniId: number): void
{
WiredSelectionVisualizer.clearSelectionShader(WiredSelectionVisualizer.getRoomObject(furniId));
const roomObject = WiredSelectionVisualizer.getRoomObject(furniId);
WiredSelectionVisualizer.clearSelectionShader(roomObject, WiredSelectionVisualizer._selectionShader);
WiredSelectionVisualizer.clearSelectionShader(roomObject, WiredSelectionVisualizer._secondarySelectionShader);
}
public static showSecondary(furniId: number): void
{
WiredSelectionVisualizer.applySelectionShader(WiredSelectionVisualizer.getRoomObject(furniId), WiredSelectionVisualizer._secondarySelectionShader);
}
public static hideSecondary(furniId: number): void
{
WiredSelectionVisualizer.clearSelectionShader(WiredSelectionVisualizer.getRoomObject(furniId), WiredSelectionVisualizer._secondarySelectionShader);
}
public static clearSelectionShaderFromFurni(furniIds: number[]): void
{
for(const furniId of furniIds)
{
WiredSelectionVisualizer.clearSelectionShader(WiredSelectionVisualizer.getRoomObject(furniId));
WiredSelectionVisualizer.clearSelectionShader(WiredSelectionVisualizer.getRoomObject(furniId), WiredSelectionVisualizer._selectionShader);
}
}
@@ -29,7 +46,39 @@ export class WiredSelectionVisualizer
{
for(const furniId of furniIds)
{
WiredSelectionVisualizer.applySelectionShader(WiredSelectionVisualizer.getRoomObject(furniId));
WiredSelectionVisualizer.applySelectionShader(WiredSelectionVisualizer.getRoomObject(furniId), WiredSelectionVisualizer._selectionShader);
}
}
public static clearSecondarySelectionShaderFromFurni(furniIds: number[]): void
{
for(const furniId of furniIds)
{
WiredSelectionVisualizer.clearSelectionShader(WiredSelectionVisualizer.getRoomObject(furniId), WiredSelectionVisualizer._secondarySelectionShader);
}
}
public static applySecondarySelectionShaderToFurni(furniIds: number[]): void
{
for(const furniId of furniIds)
{
WiredSelectionVisualizer.applySelectionShader(WiredSelectionVisualizer.getRoomObject(furniId), WiredSelectionVisualizer._secondarySelectionShader);
}
}
public static clearAllSelectionShaders(): void
{
const roomEngine = GetRoomEngine();
const roomId = roomEngine.activeRoomId;
if(roomId < 0) return;
const roomObjects = roomEngine.getRoomObjects(roomId, RoomObjectCategory.FLOOR);
for(const roomObject of roomObjects)
{
WiredSelectionVisualizer.clearSelectionShader(roomObject, WiredSelectionVisualizer._selectionShader);
WiredSelectionVisualizer.clearSelectionShader(roomObject, WiredSelectionVisualizer._secondarySelectionShader);
}
}
@@ -40,7 +89,7 @@ export class WiredSelectionVisualizer
return roomEngine.getRoomObject(roomEngine.activeRoomId, objectId, RoomObjectCategory.FLOOR);
}
private static applySelectionShader(roomObject: IRoomObject): void
private static applySelectionShader(roomObject: IRoomObject, filter: WiredFilter): void
{
if(!roomObject) return;
@@ -54,13 +103,15 @@ export class WiredSelectionVisualizer
if(!sprite.filters) sprite.filters = [];
sprite.filters.push(WiredSelectionVisualizer._selectionShader);
if(sprite.filters.includes(filter)) continue;
sprite.filters.push(filter);
sprite.increaseUpdateCounter();
}
}
private static clearSelectionShader(roomObject: IRoomObject): void
private static clearSelectionShader(roomObject: IRoomObject, filter: WiredFilter): void
{
if(!roomObject) return;
@@ -72,7 +123,7 @@ export class WiredSelectionVisualizer
{
if(!sprite.filters) continue;
const index = sprite.filters.indexOf(WiredSelectionVisualizer._selectionShader);
const index = sprite.filters.indexOf(filter);
if(index >= 0)
{
+6
View File
@@ -15,4 +15,10 @@ export class WiredTriggerLayout
public static BOT_REACHED_STUFF: number = 13;
public static BOT_REACHED_AVATAR: number = 14;
public static RECEIVE_SIGNAL: number = 15;
public static AVATAR_LEAVES_ROOM: number = 16;
public static EXECUTE_PERIODICALLY_SHORT: number = 17;
public static CLICK_FURNI: number = 18;
public static CLICK_TILE: number = 19;
public static CLICK_USER: number = 20;
public static USER_PERFORMS_ACTION: number = 21;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

+11 -29
View File
@@ -12,16 +12,20 @@ export interface ButtonProps extends FlexProps
export const Button: FC<ButtonProps> = props =>
{
const { variant = 'primary', size = 'sm', active = false, disabled = false, classNames = [], style = {}, ...rest } = props;
const { variant = 'primary', size = 'sm', active = false, disabled = false, classNames = [], ...rest } = props;
const getClassNames = useMemo(() =>
{
// fucked up method i know (i dont have a clue what im doing because im a ninja)
const newClassNames: string[] = [ 'pointer-events-auto inline-block font-normal leading-normal text-[#fff] text-center no-underline align-middle cursor-pointer select-none border border-[solid] border-transparent px-[.75rem] py-[.375rem] text-[.9rem] rounded-[.25rem] [transition:color_.15s_ease-in-out,background-color_.15s_ease-in-out,border-color_.15s_ease-in-out,box-shadow_.15s_ease-in-out]' ];
if(variant)
{
if(variant == 'primary')
newClassNames.push('text-white [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white');
newClassNames.push('text-white bg-[#1e7295] border-[#1e7295] [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white hover:bg-[#1a617f] hover:border-[#185b77]');
if(variant == 'success')
newClassNames.push('text-white bg-[#00800b] border-[#00800b] [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white hover:bg-[#006d09] hover:border-[#006609]');
@@ -39,10 +43,11 @@ export const Button: FC<ButtonProps> = props =>
newClassNames.push('text-white bg-[#185d79] border-[#185d79] [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white hover:bg-[#144f67] hover:border-[#134a61]');
if(variant == 'dark')
newClassNames.push('text-white [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white');
newClassNames.push('text-white bg-dark [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white hover:bg-[#18181bfb] hover:border-[#161619fb]');
if(variant == 'gray')
newClassNames.push('text-white [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white');
newClassNames.push('text-white bg-[#1e7295] border-[#1e7295] [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white hover:bg-[#1a617f] hover:border-[#185b77]');
}
if(size)
@@ -62,28 +67,5 @@ export const Button: FC<ButtonProps> = props =>
return newClassNames;
}, [ variant, size, active, disabled, classNames ]);
const getStyle = useMemo(() =>
{
if(variant === 'primary' || variant === 'gray')
{
return {
backgroundColor: 'var(--ui-btn-primary-bg, #1e7295)',
borderColor: 'var(--ui-btn-primary-border, #1e7295)',
...style
};
}
if(variant === 'dark')
{
return {
backgroundColor: 'var(--ui-dark-bg, rgba(28, 28, 32, .98))',
borderColor: 'var(--ui-dark-border, rgba(28, 28, 32, .98))',
...style
};
}
return style;
}, [ variant, style ]);
return <Flex center classNames={ getClassNames } style={ getStyle } { ...rest } />;
return <Flex center classNames={ getClassNames } { ...rest } />;
};
+18 -4
View File
@@ -11,11 +11,25 @@ export interface SliderProps extends ReactSliderProps
export const Slider: FC<SliderProps> = props =>
{
const { disabledButton, max, min, value, onChange, ...rest } = props;
const { disabledButton, max, min, step, value, onChange, ...rest } = props;
const currentValue = Array.isArray(value) ? value[0] : ((typeof value === 'number') ? value : 0);
const minimum = (typeof min === 'number') ? min : 0;
const maximum = (typeof max === 'number') ? max : 0;
const buttonStep = ((typeof step === 'number') && (step > 0)) ? step : 1;
const roundToStep = (nextValue: number) =>
{
if(typeof buttonStep !== 'number') return nextValue;
const decimalStep = buttonStep.toString();
const precision = decimalStep.includes('.') ? (decimalStep.length - decimalStep.indexOf('.') - 1) : 0;
return parseFloat(nextValue.toFixed(precision));
};
return <Flex fullWidth gap={ 1 }>
{ !disabledButton && <Button disabled={ min >= value } onClick={ () => onChange(min < value ? value - 1 : min, 0) }><FaAngleLeft /></Button> }
<ReactSlider className={ 'nitro-slider' } max={ max } min={ min } value={ value } onChange={ onChange } { ...rest } />
{ !disabledButton && <Button disabled={ max <= value } onClick={ () => onChange(max > value ? value + 1 : max, 0) }><FaAngleRight /></Button> }
{ !disabledButton && <Button disabled={ minimum >= currentValue } onClick={ () => onChange(roundToStep(minimum < currentValue ? currentValue - buttonStep : minimum), 0) }><FaAngleLeft /></Button> }
<ReactSlider className={ 'nitro-slider' } max={ max } min={ min } step={ step } value={ value } onChange={ onChange } { ...rest } />
{ !disabledButton && <Button disabled={ maximum <= currentValue } onClick={ () => onChange(roundToStep(maximum > currentValue ? currentValue + buttonStep : maximum), 0) }><FaAngleRight /></Button> }
</Flex>;
}
+3 -7
View File
@@ -1,6 +1,5 @@
import { FC, MouseEvent } from 'react';
import { FaFlag } from 'react-icons/fa';
import { useUiSettings } from '../../api';
import { Base, Column, ColumnProps, Flex } from '..';
interface NitroCardHeaderViewProps extends ColumnProps
@@ -17,7 +16,8 @@ interface NitroCardHeaderViewProps extends ColumnProps
export const NitroCardHeaderView: FC<NitroCardHeaderViewProps> = props =>
{
const { headerText = null, isGalleryPhoto = false, noCloseButton = false, isInfoToHabboPages = false, onReportPhoto = null, onClickInfoHabboPages = null, onCloseClick = null, justifyContent = 'center', alignItems = 'center', classNames = [], children = null, ...rest } = props;
const { isCustomActive, getHeaderStyle } = useUiSettings();
const onMouseDown = (event: MouseEvent<HTMLDivElement>) =>
{
@@ -25,12 +25,8 @@ export const NitroCardHeaderView: FC<NitroCardHeaderViewProps> = props =>
event.nativeEvent.stopImmediatePropagation();
};
const headerClassName = isCustomActive
? 'relative flex items-center justify-center flex-col drag-handler min-h-card-header max-h-card-header'
: 'relative flex items-center justify-center flex-col drag-handler min-h-card-header max-h-card-header bg-card-header';
return (
<Column center className={ headerClassName } style={ getHeaderStyle() } { ...rest }>
<Column center className={ 'relative flex items-center justify-center flex-col drag-handler min-h-card-header max-h-card-header bg-card-header' } { ...rest }>
<Flex center fullWidth>
<span className="text-xl text-white drop-shadow-lg">{ headerText }</span>
{ isGalleryPhoto &&
+3 -9
View File
@@ -1,27 +1,21 @@
import { FC, useMemo } from 'react';
import { useUiSettings } from '../../../api';
import { Flex, FlexProps } from '../..';
export const NitroCardTabsView: FC<FlexProps> = props =>
{
const { justifyContent = 'center', gap = 1, classNames = [], children = null, ...rest } = props;
const { isCustomActive, getTabsStyle } = useUiSettings();
const getClassNames = useMemo(() =>
{
const base = isCustomActive
? 'justify-center gap-0.5 flex min-h-card-tabs max-h-card-tabs pt-1 border-b border-card-border px-2 -mt-px'
: 'justify-center gap-0.5 flex bg-card-tabs min-h-card-tabs max-h-card-tabs pt-1 border-b border-card-border px-2 -mt-px';
const newClassNames: string[] = [ base ];
const newClassNames: string[] = [ 'justify-center gap-0.5 flex bg-card-tabs min-h-card-tabs max-h-card-tabs pt-1 border-b border-card-border px-2 -mt-px' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames, isCustomActive ]);
}, [ classNames ]);
return (
<Flex classNames={ getClassNames } gap={ gap } justifyContent={ justifyContent } style={ getTabsStyle() } { ...rest }>
<Flex classNames={ getClassNames } gap={ gap } justifyContent={ justifyContent } { ...rest }>
{ children }
</Flex>
);
@@ -8,6 +8,7 @@ const CURRENT_WINDOWS: HTMLElement[] = [];
const POS_MEMORY: Map<Key, { x: number, y: number }> = new Map();
const BOUNDS_THRESHOLD_TOP: number = 0;
const BOUNDS_THRESHOLD_LEFT: number = 0;
const DRAG_OUTSIDE_PERCENT: number = 0.80;
export interface DraggableWindowProps {
uniqueKey?: Key;
@@ -80,8 +81,11 @@ export const DraggableWindow: FC<DraggableWindowProps> = props => {
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const clampedX = Math.max(BOUNDS_THRESHOLD_LEFT, Math.min(newX, viewportWidth - windowWidth));
const clampedY = Math.max(BOUNDS_THRESHOLD_TOP, Math.min(newY, viewportHeight - windowHeight));
const maxOutX = windowWidth * DRAG_OUTSIDE_PERCENT;
const maxOutY = windowHeight * DRAG_OUTSIDE_PERCENT;
const clampedX = Math.max(-maxOutX, Math.min(newX, viewportWidth - windowWidth + maxOutX));
const clampedY = Math.max(-maxOutY, Math.min(newY, viewportHeight - windowHeight + maxOutY));
return { x: clampedX, y: clampedY };
}, []);
+28 -9
View File
@@ -67,11 +67,20 @@ export const LayoutBadgeImageView: FC<LayoutBadgeImageViewProps> = props =>
{
if(event.badgeId !== badgeCode) return;
const element = await TextureUtils.generateImage(new NitroSprite(event.image));
console.log ('boe');
if(isGroup)
{
const element = await TextureUtils.generateImage(new NitroSprite(event.image));
element.onload = () => setImageElement(element);
element.onload = () => setImageElement(element);
}
else
{
const badgeUrl = GetConfigurationValue<string>('badge.asset.url').replace('%badgename%', badgeCode.toString());
const img = new Image();
img.onload = () => setImageElement(img);
img.src = badgeUrl;
}
didSetBadge = true;
@@ -84,13 +93,23 @@ export const LayoutBadgeImageView: FC<LayoutBadgeImageViewProps> = props =>
if(texture && !didSetBadge)
{
(async () =>
if(isGroup)
{
const element = await TextureUtils.generateImage(new NitroSprite(texture));
(async () =>
{
const element = await TextureUtils.generateImage(new NitroSprite(texture));
element.onload = () => setImageElement(element);
})();
element.onload = () => setImageElement(element);
})();
}
else
{
const badgeUrl = GetConfigurationValue<string>('badge.asset.url').replace('%badgename%', badgeCode.toString());
const img = new Image();
img.onload = () => setImageElement(img);
img.src = badgeUrl;
}
}
return () => GetEventDispatcher().removeEventListener(BadgeImageReadyEvent.IMAGE_READY, onBadgeImageReadyEvent);
+5 -8
View File
@@ -1,4 +1,4 @@
import { AddLinkEventTracker, GetCommunication, HabboWebTools, ILinkEventTracker, RemoveLinkEventTracker, RoomSessionEvent } from '@nitrots/nitro-renderer';
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 { useNitroEvent } from '../hooks';
@@ -9,7 +9,6 @@ import { CampaignView } from './campaign/CampaignView';
import { CatalogView } from './catalog/CatalogView';
import { ChatHistoryView } from './chat-history/ChatHistoryView';
import { FloorplanEditorView } from './floorplan-editor/FloorplanEditorView';
import { FurniEditorView } from './furni-editor/FurniEditorView';
import { FriendsView } from './friends/FriendsView';
import { GameCenterView } from './game-center/GameCenterView';
import { GroupsView } from './groups/GroupsView';
@@ -22,14 +21,13 @@ import { ModToolsView } from './mod-tools/ModToolsView';
import { NavigatorView } from './navigator/NavigatorView';
import { NitrobubbleHiddenView } from './nitrobubblehidden/NitrobubbleHiddenView';
import { NitropediaView } from './nitropedia/NitropediaView';
import { ExternalPluginLoader } from './plugins/ExternalPluginLoader';
import { RightSideView } from './right-side/RightSideView';
import { RoomView } from './room/RoomView';
import { ToolbarView } from './toolbar/ToolbarView';
import { UserProfileView } from './user-profile/UserProfileView';
import { InterfaceSettingsView } from './interface-settings/InterfaceSettingsView';
import { UserSettingsView } from './user-settings/UserSettingsView';
import { WiredView } from './wired/WiredView';
import { WiredCreatorToolsView } from './wired-tools/WiredCreatorToolsView';
import { YoutubeTvView } from './youtube-tv/YoutubeTvView';
export const MainView: FC<{}> = props =>
@@ -44,6 +42,8 @@ export const MainView: FC<{}> = props =>
{
setIsReady(true);
GetRoomSessionManager().tryRestoreSession();
GetCommunication().connection.ready();
}, []);
@@ -88,7 +88,6 @@ export const MainView: FC<{}> = props =>
<AnimatePresence>
{ landingViewVisible &&
<motion.div
className="w-full h-full"
initial={ { opacity: 0 }}
animate={ { opacity: 1 }}
exit={ { opacity: 0 }}>
@@ -97,6 +96,7 @@ export const MainView: FC<{}> = props =>
</AnimatePresence>
<ToolbarView isInRoom={ !landingViewVisible } />
<ModToolsView />
<WiredCreatorToolsView />
<RoomView />
<ChatHistoryView />
<WiredView />
@@ -109,7 +109,6 @@ export const MainView: FC<{}> = props =>
<FriendsView />
<RightSideView />
<UserSettingsView />
<InterfaceSettingsView />
<UserProfileView />
<GroupsView />
<CameraWidgetView />
@@ -120,9 +119,7 @@ export const MainView: FC<{}> = props =>
<CampaignView />
<GameCenterView />
<FloorplanEditorView />
<FurniEditorView />
<YoutubeTvView />
<ExternalPluginLoader />
</>
);
};
@@ -60,7 +60,7 @@ export const CameraWidgetCaptureView: FC<CameraWidgetCaptureViewProps> = props =
return (
<DraggableWindow uniqueKey="nitro-camera-capture">
<Column center className="relative" gap={ 0 }>
{ selectedPicture && <img alt="" className="absolute top-[37px] left-[10px] w-[320px] h-[320px]" src={ selectedPicture.imageUrl } /> }
{ selectedPicture && <img alt="" className="absolute top-[37px] left-[10px] w-[325px] h-[325px]" src={ selectedPicture.imageUrl } /> }
<div className="relative w-[340px] h-[462px] bg-[url('@/assets/images/room-widgets/camera-widget/camera-spritesheet.png')] bg-position-[-1px_-1px] drag-handler">
<div className="absolute top-[8px] right-[8px] rounded-[.25rem] [box-shadow:0_0_0_1.5px_#fff] border-2 border-[solid] border-[#921911] bg-[repeating-linear-gradient(rgb(245,80,65),rgb(245,80,65)_50%,rgb(194,48,39)_50%,rgb(194,48,39)_100%)] cursor-pointer leading-none px-[3px] py-px" onClick={ onClose }>
<FaTimes className="fa-icon" />
@@ -1,8 +1,8 @@
import { GetRoomCameraWidgetManager, IRoomCameraWidgetEffect, IRoomCameraWidgetSelectedEffect, NitroLogger, RoomCameraWidgetSelectedEffect } from '@nitrots/nitro-renderer';
import { GetRoomCameraWidgetManager, IRoomCameraWidgetEffect, IRoomCameraWidgetSelectedEffect, NitroLogger, NitroTexture, RoomCameraWidgetSelectedEffect } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { FaSave, FaSearchMinus, FaSearchPlus, FaTrash } from 'react-icons/fa';
import { CameraEditorTabs, CameraPicture, CameraPictureThumbnail, LocalizeText } from '../../../../api';
import { Button, Column, Flex, Grid, LayoutImage, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView, Slider, Text } from '../../../../common';
import { Button, Column, Flex, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView, Slider, Text } from '../../../../common';
import { CameraWidgetEffectListView } from './effect-list';
export interface CameraWidgetEditorViewProps {
@@ -23,10 +23,18 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
const [ selectedEffects, setSelectedEffects ] = useState<IRoomCameraWidgetSelectedEffect[]>([]);
const [ effectsThumbnails, setEffectsThumbnails ] = useState<CameraPictureThumbnail[]>([]);
const [ isZoomed, setIsZoomed ] = useState(false);
const [ currentPictureUrl, setCurrentPictureUrl ] = useState<string>('');
const [ currentPictureUrl, setCurrentPictureUrl ] = useState<string>(picture?.imageUrl ?? '');
const [ stableTexture, setStableTexture ] = useState<NitroTexture>(null);
const debounceTimerRef = useRef<ReturnType<typeof setTimeout>>(null);
const requestIdRef = useRef<number>(0);
useEffect(() =>
{
const img = new Image();
img.onload = () => setStableTexture(NitroTexture.from(img));
img.src = picture.imageUrl;
}, [ picture ]);
const getColorMatrixEffects = useMemo(() => {
return availableEffects.filter(effect => effect.colorMatrix);
}, [ availableEffects ]);
@@ -104,16 +112,27 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
return;
}
case 'clear_effects':
setSelectedEffectName(null);
setSelectedEffects([]);
onCancel();
return;
case 'download': {
(async () => {
const image = new Image();
image.src = currentPictureUrl;
const newWindow = window.open('');
newWindow.document.write(image.outerHTML);
})();
if(!currentPictureUrl) return;
const parts = currentPictureUrl.split(',');
const mime = parts[0].match(/:(.*?);/)?.[1] || 'image/png';
const binary = atob(parts[1]);
const bytes = new Uint8Array(binary.length);
for(let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
const blob = new Blob([ bytes ], { type: mime });
const blobUrl = URL.createObjectURL(blob);
const w = window.open('', '_blank');
if(w)
{
w.document.title = 'camera_photo.png';
w.document.body.style.margin = '0';
w.document.body.innerHTML = `<img src="${ blobUrl }" style="max-width:100%"/>`;
}
return;
}
case 'zoom':
@@ -123,25 +142,29 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
}, [ availableEffects, selectedEffectName, currentPictureUrl, getSelectedEffectIndex, onCancel, onCheckout, onClose ]);
useEffect(() => {
if(!stableTexture) return;
const processThumbnails = async () => {
const renderedEffects = await Promise.all(
availableEffects.map(effect =>
GetRoomCameraWidgetManager().applyEffects(picture.texture, [ new RoomCameraWidgetSelectedEffect(effect, 1) ], false)
GetRoomCameraWidgetManager().applyEffects(stableTexture, [ new RoomCameraWidgetSelectedEffect(effect, 1) ], false)
)
);
setEffectsThumbnails(renderedEffects.map((image, index) => new CameraPictureThumbnail(availableEffects[index].name, image.src)));
};
processThumbnails();
}, [ picture, availableEffects ]);
}, [ stableTexture, availableEffects ]);
useEffect(() => {
if(!stableTexture) return;
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
debounceTimerRef.current = setTimeout(() => {
const id = ++requestIdRef.current;
GetRoomCameraWidgetManager()
.applyEffects(picture.texture, selectedEffects, false)
.applyEffects(stableTexture, selectedEffects, false)
.then(imageElement => {
if (id !== requestIdRef.current) return;
setCurrentPictureUrl(imageElement.src);
@@ -152,7 +175,7 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
return () => {
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
};
}, [ picture, selectedEffects ]);
}, [ stableTexture, selectedEffects ]);
return (
<NitroCardView className="w-[600px] h-[500px]">
@@ -177,16 +200,14 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
</Column>
<Column justifyContent="between" overflow="hidden" size={ 7 }>
<Column center>
<LayoutImage
style={{
width: '320px',
height: '320px',
backgroundImage: `url(${currentPictureUrl})`,
backgroundPosition: isZoomed ? 'center' : 'top left',
backgroundSize: isZoomed ? 'contain' : 'auto', // Zoom only affects display
backgroundRepeat: 'no-repeat'
}}
/>
<div className="w-[325px] h-[325px] overflow-hidden">
{ currentPictureUrl && <img
alt=""
src={ currentPictureUrl }
className="w-[325px] h-[325px] [image-rendering:pixelated]"
style={ isZoomed ? { transform: 'scale(2)', transformOrigin: 'center' } : undefined }
/> }
</div>
{ selectedEffectName && (
<Column center fullWidth gap={ 1 }>
<Text>{ LocalizeText('camera.effect.name.' + selectedEffectName) }</Text>
@@ -7,6 +7,8 @@ import { CatalogEvent, CatalogInitGiftEvent, CatalogPurchasedEvent } from '../..
import { useCatalog, useFriends, useMessageEvent, useUiEvent } from '../../../../hooks';
import { classNames } from '../../../../layout';
let isBuyingGift = false;
export const CatalogGiftView: FC<{}> = props =>
{
const [ isVisible, setIsVisible ] = useState<boolean>(false);
@@ -32,6 +34,7 @@ export const CatalogGiftView: FC<{}> = props =>
const onClose = useCallback(() =>
{
isBuyingGift = false;
setIsVisible(false);
setPageId(0);
setOfferId(0);
@@ -122,6 +125,10 @@ export const CatalogGiftView: FC<{}> = props =>
return;
}
if(isBuyingGift) return;
isBuyingGift = true;
SendMessageComposer(new PurchaseFromCatalogAsGiftComposer(pageId, offerId, extraData, receiverName, message, colourId, selectedBoxIndex, selectedRibbonIndex, showMyFace));
return;
}
@@ -136,6 +143,7 @@ export const CatalogGiftView: FC<{}> = props =>
switch(event.type)
{
case CatalogPurchasedEvent.PURCHASE_SUCCESS:
isBuyingGift = false;
onClose();
return;
case CatalogEvent.INIT_GIFT:
@@ -6,6 +6,8 @@ import { useCatalog, useMessageEvent, useNavigator, useRoomPromote } from '../..
import { NitroInput } from '../../../../../layout';
import { CatalogLayoutProps } from './CatalogLayout.types';
let isPurchasingAd = false;
export const CatalogLayoutRoomAdsView: FC<CatalogLayoutProps> = props =>
{
const { page = null } = props;
@@ -45,6 +47,10 @@ export const CatalogLayoutRoomAdsView: FC<CatalogLayoutProps> = props =>
const purchaseAd = () =>
{
if(isPurchasingAd) return;
isPurchasingAd = true;
const pageId = page.pageId;
const offerId = page.offers.length >= 1 ? page.offers[0].offerId : -1;
const flatId = roomId;
@@ -1,5 +1,5 @@
import { ClubOfferData, GetClubOffersMessageComposer, PurchaseFromCatalogComposer } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { CatalogPurchaseState, LocalizeText, SendMessageComposer } from '../../../../../api';
import { AutoGrid, Button, Column, Flex, Grid, LayoutCurrencyIcon, LayoutGridItem, LayoutLoadingSpinnerView, Text } from '../../../../../common';
import { CatalogEvent, CatalogPurchaseFailureEvent, CatalogPurchasedEvent } from '../../../../../events';
@@ -13,15 +13,18 @@ export const CatalogLayoutVipBuyView: FC<CatalogLayoutProps> = props =>
const { currentPage = null, catalogOptions = null } = useCatalog();
const { purse = null, getCurrencyAmount = null } = usePurse();
const { clubOffers = null } = catalogOptions;
const isPurchasingRef = useRef<boolean>(false);
const onCatalogEvent = useCallback((event: CatalogEvent) =>
{
switch(event.type)
{
case CatalogPurchasedEvent.PURCHASE_SUCCESS:
isPurchasingRef.current = false;
setPurchaseState(CatalogPurchaseState.NONE);
return;
case CatalogPurchaseFailureEvent.PURCHASE_FAILED:
isPurchasingRef.current = false;
setPurchaseState(CatalogPurchaseState.FAILED);
return;
}
@@ -83,8 +86,9 @@ export const CatalogLayoutVipBuyView: FC<CatalogLayoutProps> = props =>
const purchaseSubscription = useCallback(() =>
{
if(!pendingOffer) return;
if(!pendingOffer || isPurchasingRef.current) return;
isPurchasingRef.current = true;
setPurchaseState(CatalogPurchaseState.PURCHASE);
SendMessageComposer(new PurchaseFromCatalogComposer(currentPage.pageId, pendingOffer.offerId, null, 1));
}, [ pendingOffer, currentPage ]);
@@ -1,5 +1,5 @@
import { CancelMarketplaceOfferMessageComposer, GetMarketplaceOwnOffersMessageComposer, MarketplaceCancelOfferResultEvent, MarketplaceOwnOffersEvent, RedeemMarketplaceOfferCreditsMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { LocalizeText, MarketplaceOfferData, MarketPlaceOfferState, NotificationAlertType, SendMessageComposer } from '../../../../../../api';
import { Button, Column, Text } from '../../../../../../common';
import { useMessageEvent, useNotification } from '../../../../../../hooks';
@@ -11,6 +11,8 @@ export const CatalogLayoutMarketplaceOwnItemsView: FC<CatalogLayoutProps> = prop
const [ creditsWaiting, setCreditsWaiting ] = useState(0);
const [ offers, setOffers ] = useState<MarketplaceOfferData[]>([]);
const { simpleAlert = null } = useNotification();
const isRedeemingRef = useRef<boolean>(false);
const pendingCancelsRef = useRef<Set<number>>(new Set());
useMessageEvent<MarketplaceOwnOffersEvent>(MarketplaceOwnOffersEvent, event =>
{
@@ -54,6 +56,10 @@ export const CatalogLayoutMarketplaceOwnItemsView: FC<CatalogLayoutProps> = prop
const redeemSoldOffers = useCallback(() =>
{
if(isRedeemingRef.current) return;
isRedeemingRef.current = true;
setOffers(prevValue =>
{
const idsToDelete = soldOffers.map(value => value.offerId);
@@ -62,11 +68,19 @@ export const CatalogLayoutMarketplaceOwnItemsView: FC<CatalogLayoutProps> = prop
});
SendMessageComposer(new RedeemMarketplaceOfferCreditsMessageComposer());
setTimeout(() => isRedeemingRef.current = false, 3000);
}, [ soldOffers ]);
const takeItemBack = (offerData: MarketplaceOfferData) =>
{
if(pendingCancelsRef.current.has(offerData.offerId)) return;
pendingCancelsRef.current.add(offerData.offerId);
SendMessageComposer(new CancelMarketplaceOfferMessageComposer(offerData.offerId));
setTimeout(() => pendingCancelsRef.current.delete(offerData.offerId), 2000);
};
useEffect(() =>
@@ -1,5 +1,5 @@
import { BuyMarketplaceOfferMessageComposer, GetMarketplaceOffersMessageComposer, MarketplaceBuyOfferResultEvent, MarketPlaceOffersEvent } from '@nitrots/nitro-renderer';
import { FC, useCallback, useMemo, useState } from 'react';
import { FC, useCallback, useMemo, useRef, useState } from 'react';
import { IMarketplaceSearchOptions, LocalizeText, MarketplaceOfferData, MarketplaceSearchType, NotificationAlertType, SendMessageComposer } from '../../../../../../api';
import { Button, Column, Text } from '../../../../../../common';
import { useMessageEvent, useNotification, usePurse } from '../../../../../../hooks';
@@ -23,6 +23,7 @@ export const CatalogLayoutMarketplacePublicItemsView: FC<CatalogLayoutMarketplac
const [ lastSearch, setLastSearch ] = useState<IMarketplaceSearchOptions>({ minPrice: -1, maxPrice: -1, query: '', type: 3 });
const { getCurrencyAmount = null } = usePurse();
const { simpleAlert = null, showConfirm = null } = useNotification();
const isBuyingRef = useRef<boolean>(false);
const requestOffers = useCallback((options: IMarketplaceSearchOptions) =>
{
@@ -56,6 +57,9 @@ export const CatalogLayoutMarketplacePublicItemsView: FC<CatalogLayoutMarketplac
showConfirm(LocalizeText('catalog.marketplace.confirm_header'), () =>
{
if(isBuyingRef.current) return;
isBuyingRef.current = true;
SendMessageComposer(new BuyMarketplaceOfferMessageComposer(offerId));
},
null, null, null, LocalizeText('catalog.marketplace.confirm_title'));
@@ -83,6 +87,8 @@ export const CatalogLayoutMarketplacePublicItemsView: FC<CatalogLayoutMarketplac
{
const parser = event.getParser();
isBuyingRef.current = false;
if(!parser) return;
switch(parser.result)
@@ -6,6 +6,8 @@ import { CatalogPostMarketplaceOfferEvent } from '../../../../../../events';
import { useCatalog, useMessageEvent, useNotification, useUiEvent } from '../../../../../../hooks';
import { NitroInput } from '../../../../../../layout';
let isPostingMarketplaceOffer = false;
export const MarketplacePostOfferView: FC<{}> = props =>
{
const [ item, setItem ] = useState<FurnitureItem>(null);
@@ -65,10 +67,15 @@ export const MarketplacePostOfferView: FC<{}> = props =>
const postItem = () =>
{
if(!item || (askingPrice < marketplaceConfiguration.minimumPrice)) return;
if(!item || (askingPrice < marketplaceConfiguration.minimumPrice) || isPostingMarketplaceOffer) return;
showConfirm(LocalizeText('inventory.marketplace.confirm_offer.info', [ 'furniname', 'price' ], [ getFurniTitle, askingPrice.toString() ]), () =>
{
if(isPostingMarketplaceOffer) return;
isPostingMarketplaceOffer = true;
setTimeout(() => isPostingMarketplaceOffer = false, 5000);
SendMessageComposer(new MakeOfferMessageComposer(askingPrice, item.isWallItem ? 2 : 1, item.id));
setItem(null);
},
@@ -6,6 +6,8 @@ import { useCatalog, useNotification, usePurse } from '../../../../../../hooks';
import { CatalogLayoutProps } from '../CatalogLayout.types';
import { VipGiftItem } from './VipGiftItemView';
let isSelectingGift = false;
export const CatalogLayoutVipGiftsView: FC<CatalogLayoutProps> = props =>
{
const { purse = null } = usePurse();
@@ -30,6 +32,10 @@ export const CatalogLayoutVipGiftsView: FC<CatalogLayoutProps> = props =>
{
showConfirm(LocalizeText('catalog.club_gift.confirm'), () =>
{
if(isSelectingGift) return;
isSelectingGift = true;
SendMessageComposer(new SelectClubGiftComposer(localizationId));
setCatalogOptions(prevValue =>
@@ -38,6 +44,8 @@ export const CatalogLayoutVipGiftsView: FC<CatalogLayoutProps> = props =>
return { ...prevValue };
});
setTimeout(() => isSelectingGift = false, 5000);
}, null);
}, [ setCatalogOptions, showConfirm ]);
@@ -11,6 +11,8 @@ interface CatalogPurchaseWidgetViewProps
purchaseCallback?: () => void;
}
let isPurchasingCatalogItem = false;
export const CatalogPurchaseWidgetView: FC<CatalogPurchaseWidgetViewProps> = props =>
{
const { noGiftOption = false, purchaseCallback = null } = props;
@@ -25,15 +27,19 @@ export const CatalogPurchaseWidgetView: FC<CatalogPurchaseWidgetViewProps> = pro
switch(event.type)
{
case CatalogPurchasedEvent.PURCHASE_SUCCESS:
isPurchasingCatalogItem = false;
setPurchaseState(CatalogPurchaseState.NONE);
return;
case CatalogPurchaseFailureEvent.PURCHASE_FAILED:
isPurchasingCatalogItem = false;
setPurchaseState(CatalogPurchaseState.FAILED);
return;
case CatalogPurchaseNotAllowedEvent.NOT_ALLOWED:
isPurchasingCatalogItem = false;
setPurchaseState(CatalogPurchaseState.FAILED);
return;
case CatalogPurchaseSoldOutEvent.SOLD_OUT:
isPurchasingCatalogItem = false;
setPurchaseState(CatalogPurchaseState.SOLD_OUT);
return;
}
@@ -62,7 +68,7 @@ export const CatalogPurchaseWidgetView: FC<CatalogPurchaseWidgetViewProps> = pro
const purchase = (isGift: boolean = false) =>
{
if(!currentOffer) return;
if(!currentOffer || isPurchasingCatalogItem) return;
if(GetClubMemberLevel() < currentOffer.clubLevel)
{
@@ -78,6 +84,7 @@ export const CatalogPurchaseWidgetView: FC<CatalogPurchaseWidgetViewProps> = pro
return;
}
isPurchasingCatalogItem = true;
setPurchaseState(CatalogPurchaseState.PURCHASE);
if(purchaseCallback)
@@ -4,6 +4,8 @@ import { FriendlyTime, GetConfigurationValue, LocalizeText, SendMessageComposer
import { Button, Column, Flex, LayoutCurrencyIcon, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
import { usePurse } from '../../../../hooks';
let isBuyingOffer = false;
export const OfferWindowView = (props: { offer: TargetedOfferData, setOpen: Dispatch<SetStateAction<boolean>> }) =>
{
const { offer = null, setOpen = null } = props;
@@ -37,8 +39,14 @@ export const OfferWindowView = (props: { offer: TargetedOfferData, setOpen: Disp
const buyOffer = () =>
{
if(isBuyingOffer) return;
isBuyingOffer = true;
SendMessageComposer(new PurchaseTargetedOfferComposer(offer.id, amount));
SendMessageComposer(new GetTargetedOfferComposer());
setTimeout(() => isBuyingOffer = false, 5000);
};
if(!offer) return;
@@ -8,25 +8,13 @@ interface IFloorplanEditorContext
setOriginalFloorplanSettings: Dispatch<SetStateAction<IFloorplanSettings>>;
visualizationSettings: IVisualizationSettings;
setVisualizationSettings: Dispatch<SetStateAction<IVisualizationSettings>>;
floorHeight: number;
setFloorHeight: Dispatch<SetStateAction<number>>;
floorAction: number;
setFloorAction: Dispatch<SetStateAction<number>>;
tilemapVersion: number;
areaInfo: { total: number; walkable: number };
}
const FloorplanEditorContext = createContext<IFloorplanEditorContext>({
originalFloorplanSettings: null,
setOriginalFloorplanSettings: null,
visualizationSettings: null,
setVisualizationSettings: null,
floorHeight: 0,
setFloorHeight: null,
floorAction: 3,
setFloorAction: null,
tilemapVersion: 0,
areaInfo: { total: 0, walkable: 0 }
setVisualizationSettings: null
});
export const FloorplanEditorContextProvider: FC<ProviderProps<IFloorplanEditorContext>> = props => <FloorplanEditorContext.Provider { ...props } />;
@@ -1,22 +1,19 @@
import { AddLinkEventTracker, FloorHeightMapEvent, ILinkEventTracker, RemoveLinkEventTracker, RoomEngineEvent, RoomVisualizationSettingsEvent, UpdateFloorPropertiesMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react';
import { FaCaretLeft, FaCaretRight } from 'react-icons/fa';
import { FC, useEffect, useState } from 'react';
import { LocalizeText, SendMessageComposer } from '../../api';
import { Button, ButtonGroup, Column, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common';
import { Button, ButtonGroup, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../common';
import { useMessageEvent, useNitroEvent } from '../../hooks';
import { FloorplanEditorContextProvider } from './FloorplanEditorContext';
import { FloorplanEditor } from '@nitrots/nitro-renderer';
import { IFloorplanSettings } from '@nitrots/nitro-renderer';
import { IVisualizationSettings } from '@nitrots/nitro-renderer';
import { convertNumbersForSaving, convertSettingToNumber, FloorAction, HEIGHT_SCHEME } from '@nitrots/nitro-renderer';
import { convertNumbersForSaving, convertSettingToNumber } from '@nitrots/nitro-renderer';
import { FloorplanCanvasView } from './views/FloorplanCanvasView';
import { FloorplanImportExportView } from './views/FloorplanImportExportView';
import { FloorplanOptionsView } from './views/FloorplanOptionsView';
import { FloorplanHeightSelector } from './views/FloorplanHeightSelector';
import { FloorplanPreviewView } from './views/FloorplanPreviewView';
const MIN_WALL_HEIGHT = 0;
const MAX_WALL_HEIGHT = 16;
type ScrollDirection = 'up' | 'down' | 'left' | 'right';
export const FloorplanEditorView: FC<{}> = props =>
{
@@ -37,65 +34,7 @@ export const FloorplanEditorView: FC<{}> = props =>
thicknessWall: 1,
thicknessFloor: 1
});
const [ floorHeight, setFloorHeight ] = useState(0);
const [ floorAction, setFloorAction ] = useState(FloorAction.SET);
const [ tilemapVersion, setTilemapVersion ] = useState(0);
const [ areaInfo, setAreaInfo ] = useState({ total: 0, walkable: 0 });
const calculateArea = useCallback(() =>
{
const tilemap = FloorplanEditor.instance.tilemap;
if(!tilemap || tilemap.length === 0)
{
setAreaInfo({ total: 0, walkable: 0 });
return;
}
let total = 0;
let walkable = 0;
for(let y = 0; y < tilemap.length; y++)
{
if(!tilemap[y]) continue;
for(let x = 0; x < tilemap[y].length; x++)
{
if(!tilemap[y][x] || tilemap[y][x].height === 'x') continue;
total++;
if(!tilemap[y][x].isBlocked) walkable++;
}
}
setAreaInfo({ total, walkable });
}, []);
// sync floorHeight/floorAction changes to the FloorplanEditor instance
useEffect(() =>
{
FloorplanEditor.instance.actionSettings.currentAction = floorAction;
FloorplanEditor.instance.actionSettings.currentHeight = floorHeight.toString(36);
}, [ floorHeight, floorAction ]);
// register onTilemapChange callback
useEffect(() =>
{
if(!isVisible) return;
FloorplanEditor.instance.onTilemapChange = () =>
{
setTilemapVersion(prev => prev + 1);
calculateArea();
};
return () =>
{
FloorplanEditor.instance.onTilemapChange = null;
};
}, [ isVisible, calculateArea ]);
const [ canvasScrollHandler, setCanvasScrollHandler ] = useState<((direction: ScrollDirection) => void) | null>(null);
const saveFloorChanges = () =>
{
@@ -108,50 +47,16 @@ export const FloorplanEditorView: FC<{}> = props =>
convertNumbersForSaving(visualizationSettings.thicknessFloor),
(visualizationSettings.wallHeight - 1)
));
};
}
const revertChanges = () =>
{
setVisualizationSettings({ wallHeight: originalFloorplanSettings.wallHeight, thicknessWall: originalFloorplanSettings.thicknessWall, thicknessFloor: originalFloorplanSettings.thicknessFloor, entryPointDir: originalFloorplanSettings.entryPointDir });
FloorplanEditor.instance.doorLocation = { x: originalFloorplanSettings.entryPoint[0], y: originalFloorplanSettings.entryPoint[1] };
FloorplanEditor.instance.setTilemap(originalFloorplanSettings.tilemap, originalFloorplanSettings.reservedTiles);
FloorplanEditor.instance.renderTiles();
};
const onWallHeightChange = (value: number) =>
{
if(isNaN(value) || (value <= 0)) value = MIN_WALL_HEIGHT;
if(value > MAX_WALL_HEIGHT) value = MAX_WALL_HEIGHT;
setVisualizationSettings(prevValue =>
{
const newValue = { ...prevValue };
newValue.wallHeight = value;
return newValue;
});
};
const increaseWallHeight = () =>
{
let height = (visualizationSettings.wallHeight + 1);
if(height > MAX_WALL_HEIGHT) height = MAX_WALL_HEIGHT;
onWallHeightChange(height);
};
const decreaseWallHeight = () =>
{
let height = (visualizationSettings.wallHeight - 1);
if(height <= 0) height = MIN_WALL_HEIGHT;
onWallHeightChange(height);
};
}
useNitroEvent<RoomEngineEvent>(RoomEngineEvent.DISPOSED, event => setIsVisible(false));
@@ -212,7 +117,7 @@ export const FloorplanEditorView: FC<{}> = props =>
const parts = url.split('/');
if(parts.length < 2) return;
switch(parts[1])
{
case 'show':
@@ -235,42 +140,17 @@ export const FloorplanEditorView: FC<{}> = props =>
}, []);
return (
<FloorplanEditorContextProvider value={ {
originalFloorplanSettings,
setOriginalFloorplanSettings,
visualizationSettings,
setVisualizationSettings,
floorHeight,
setFloorHeight,
floorAction,
setFloorAction,
tilemapVersion,
areaInfo
} }>
<FloorplanEditorContextProvider value={ { originalFloorplanSettings: originalFloorplanSettings, setOriginalFloorplanSettings: setOriginalFloorplanSettings, visualizationSettings: visualizationSettings, setVisualizationSettings: setVisualizationSettings } }>
{ isVisible &&
<NitroCardView uniqueKey="floorpan-editor" className="w-[1100px] h-[600px]" theme="primary-slim">
<NitroCardView uniqueKey="floorpan-editor" className="w-[760px] h-[500px]" theme="primary-slim">
<NitroCardHeaderView headerText={ LocalizeText('floor.plan.editor.title') } onCloseClick={ () => setIsVisible(false) } />
<NitroCardContentView overflow="hidden" className="flex flex-col">
<FloorplanOptionsView />
<Flex gap={ 2 } className="flex-1 min-h-0">
<FloorplanHeightSelector />
<FloorplanCanvasView overflow="hidden" />
<Column gap={ 2 } className="w-[380px] min-w-[380px]">
<FloorplanPreviewView />
<Flex gap={ 1 } alignItems="center">
<Text bold small>{ LocalizeText('floor.editor.wall.height') }</Text>
<FaCaretLeft className="cursor-pointer fa-icon" onClick={ decreaseWallHeight } />
<input type="number" className="form-control form-control-sm w-[49px]" value={ visualizationSettings.wallHeight } onChange={ event => onWallHeightChange(event.target.valueAsNumber) } />
<FaCaretRight className="cursor-pointer fa-icon" onClick={ increaseWallHeight } />
</Flex>
<Text bold small className="text-center">
Area: { areaInfo.total } ({ areaInfo.walkable } caselle)
</Text>
</Column>
</Flex>
<NitroCardContentView overflow="hidden">
<FloorplanOptionsView onCanvasScroll={ direction => canvasScrollHandler && canvasScrollHandler(direction) } />
<FloorplanCanvasView overflow="hidden" setScrollHandler={ setCanvasScrollHandler } />
<Flex justifyContent="between">
<Button variant="danger" onClick={ revertChanges }>{ LocalizeText('floor.plan.editor.reload') }</Button>
<Button onClick={ revertChanges }>{ LocalizeText('floor.plan.editor.reload') }</Button>
<ButtonGroup>
<Button disabled={ true }>{ LocalizeText('floor.plan.editor.preview') }</Button>
<Button onClick={ event => setImportExportVisible(true) }>{ LocalizeText('floor.plan.editor.import.export') }</Button>
<Button onClick={ saveFloorChanges }>{ LocalizeText('floor.plan.editor.save') }</Button>
</ButtonGroup>
@@ -281,4 +161,4 @@ export const FloorplanEditorView: FC<{}> = props =>
<FloorplanImportExportView onCloseClick={ () => setImportExportVisible(false) } /> }
</FloorplanEditorContextProvider>
);
};
}
@@ -1,25 +1,25 @@
import { GetOccupiedTilesMessageComposer, GetRoomEntryTileMessageComposer, RoomEntryTileMessageEvent, RoomOccupiedTilesMessageEvent } from '@nitrots/nitro-renderer';
import { FC, useEffect, useRef, useState } from 'react';
import { FaPlus, FaMinus } from 'react-icons/fa';
import { SendMessageComposer } from '../../../api';
import { Base, Column, ColumnProps } from '../../../common';
import { useMessageEvent } from '../../../hooks';
import { useFloorplanEditorContext } from '../FloorplanEditorContext';
import { FloorplanEditor } from '@nitrots/nitro-renderer';
type ScrollDirection = 'up' | 'down' | 'left' | 'right';
interface FloorplanCanvasViewProps extends ColumnProps
{
setScrollHandler(handler: ((direction: ScrollDirection) => void) | null): void;
}
export const FloorplanCanvasView: FC<FloorplanCanvasViewProps> = props =>
{
const { gap = 1, children = null, ...rest } = props;
const [ occupiedTilesReceived, setOccupiedTilesReceived ] = useState(false);
const { gap = 1, children = null, setScrollHandler = null, ...rest } = props;
const [ occupiedTilesReceived , setOccupiedTilesReceived ] = useState(false);
const [ entryTileReceived, setEntryTileReceived ] = useState(false);
const [ zoomLevel, setZoomLevel ] = useState(1.0);
const { originalFloorplanSettings = null, setOriginalFloorplanSettings = null, setVisualizationSettings = null } = useFloorplanEditorContext();
const elementRef = useRef<HTMLDivElement>(null);
const canvasWrapperRef = useRef<HTMLDivElement>(null);
useMessageEvent<RoomOccupiedTilesMessageEvent>(RoomOccupiedTilesMessageEvent, event =>
{
@@ -37,7 +37,7 @@ export const FloorplanCanvasView: FC<FloorplanCanvasViewProps> = props =>
});
setOccupiedTilesReceived(true);
elementRef.current.scrollTo((FloorplanEditor.instance.renderer.canvas.width / 3), 0);
});
@@ -63,16 +63,39 @@ export const FloorplanCanvasView: FC<FloorplanCanvasViewProps> = props =>
return newValue;
});
FloorplanEditor.instance.doorLocation = { x: parser.x, y: parser.y };
setEntryTileReceived(true);
});
const onClickArrowButton = (scrollDirection: ScrollDirection) =>
{
const element = elementRef.current;
if(!element) return;
switch(scrollDirection)
{
case 'up':
element.scrollBy({ top: -10 });
break;
case 'down':
element.scrollBy({ top: 10 });
break;
case 'left':
element.scrollBy({ left: -10 });
break;
case 'right':
element.scrollBy({ left: 10 });
break;
}
}
const onPointerEvent = (event: PointerEvent) =>
{
event.preventDefault();
switch(event.type)
{
case 'pointerout':
@@ -86,10 +109,7 @@ export const FloorplanCanvasView: FC<FloorplanCanvasViewProps> = props =>
FloorplanEditor.instance.onPointerMove(event);
break;
}
};
const zoomIn = () => setZoomLevel(prev => Math.min(prev + 0.25, 2.0));
const zoomOut = () => setZoomLevel(prev => Math.max(prev - 0.25, 0.5));
}
useEffect(() =>
{
@@ -104,15 +124,15 @@ export const FloorplanCanvasView: FC<FloorplanCanvasViewProps> = props =>
thicknessWall: originalFloorplanSettings.thicknessWall,
thicknessFloor: originalFloorplanSettings.thicknessFloor,
entryPointDir: prevValue.entryPointDir
};
}
});
};
}
}, [ originalFloorplanSettings.thicknessFloor, originalFloorplanSettings.thicknessWall, originalFloorplanSettings.wallHeight, setVisualizationSettings ]);
useEffect(() =>
{
if(!entryTileReceived || !occupiedTilesReceived) return;
FloorplanEditor.instance.renderTiles();
}, [ entryTileReceived, occupiedTilesReceived ]);
@@ -124,56 +144,45 @@ export const FloorplanCanvasView: FC<FloorplanCanvasViewProps> = props =>
const currentElement = elementRef.current;
if(!currentElement) return;
const wrapper = canvasWrapperRef.current;
if(wrapper) wrapper.appendChild(FloorplanEditor.instance.renderer.canvas);
currentElement.appendChild(FloorplanEditor.instance.renderer.canvas);
currentElement.addEventListener('pointerup', onPointerEvent);
currentElement.addEventListener('pointerout', onPointerEvent);
currentElement.addEventListener('pointerdown', onPointerEvent);
currentElement.addEventListener('pointermove', onPointerEvent);
return () =>
return () =>
{
if(currentElement)
{
currentElement.removeEventListener('pointerup', onPointerEvent);
currentElement.removeEventListener('pointerout', onPointerEvent);
currentElement.removeEventListener('pointerdown', onPointerEvent);
currentElement.removeEventListener('pointermove', onPointerEvent);
}
};
}
}, []);
useEffect(() =>
{
if(!setScrollHandler) return;
setScrollHandler(() => onClickArrowButton);
return () => setScrollHandler(null);
}, [ setScrollHandler ]);
return (
<Column gap={ gap } { ...rest } className="relative flex-1">
<Base overflow="auto" innerRef={ elementRef } className="flex-1">
<div
ref={ canvasWrapperRef }
style={ {
transform: `scale(${ zoomLevel })`,
transformOrigin: '0 0'
} }
/>
</Base>
<div className="absolute top-2 right-2 flex flex-col gap-1 z-10">
<button
className="w-[28px] h-[28px] flex items-center justify-center rounded bg-[#1e7295] text-white border border-transparent shadow cursor-pointer hover:brightness-110"
onClick={ zoomIn }
title="Zoom in"
>
<FaPlus size={ 10 } />
</button>
<button
className="w-[28px] h-[28px] flex items-center justify-center rounded bg-[#1e7295] text-white border border-transparent shadow cursor-pointer hover:brightness-110"
onClick={ zoomOut }
title="Zoom out"
>
<FaMinus size={ 10 } />
</button>
</div>
<Column gap={ gap } { ...rest }>
<Base overflow="auto" innerRef={ elementRef } />
{ children }
</Column>
);
};
}
@@ -1,54 +0,0 @@
import { FC } from 'react';
import { COLORMAP, FloorAction, HEIGHT_SCHEME } from '@nitrots/nitro-renderer';
import { FloorplanEditor } from '@nitrots/nitro-renderer';
import { Column, Text } from '../../../common';
import { useFloorplanEditorContext } from '../FloorplanEditorContext';
const colormap = COLORMAP as Record<string, string>;
export const FloorplanHeightSelector: FC<{}> = () =>
{
const { floorHeight, setFloorHeight, setFloorAction } = useFloorplanEditorContext();
const onSelectHeight = (height: number) =>
{
setFloorHeight(height);
setFloorAction(FloorAction.SET);
FloorplanEditor.instance.actionSettings.currentAction = FloorAction.SET;
FloorplanEditor.instance.actionSettings.currentHeight = height.toString(36);
};
const heights: number[] = [];
for(let i = 26; i >= 0; i--) heights.push(i);
return (
<Column className="h-full w-[30px] min-w-[30px] select-none">
<Text bold small center>{ floorHeight }</Text>
<div className="flex flex-col flex-1 rounded overflow-hidden border-2 border-muted">
{ heights.map(h =>
{
const char = HEIGHT_SCHEME[h + 1];
const color = colormap[char] || '101010';
const isActive = (floorHeight === h);
return (
<div
key={ h }
className="flex-1 cursor-pointer relative flex items-center justify-center"
style={ {
backgroundColor: `#${ color }`,
outline: isActive ? '2px solid #fff' : 'none',
outlineOffset: '-2px',
zIndex: isActive ? 1 : 0
} }
onClick={ () => onSelectHeight(h) }
title={ `${ h }` }
/>
);
}) }
</div>
</Column>
);
};
@@ -1,32 +1,45 @@
import { FC } from 'react';
import { FC, useState } from 'react';
import { FaArrowDown, FaArrowLeft, FaArrowRight, FaArrowUp, FaCaretLeft, FaCaretRight } from 'react-icons/fa';
import { LocalizeText } from '../../../api';
import { Flex, LayoutGridItem, Text } from '../../../common';
import { FloorAction } from '@nitrots/nitro-renderer';
import { Button, Column, Flex, LayoutGridItem, Slider, Text } from '../../../common';
import { COLORMAP, FloorAction } from '@nitrots/nitro-renderer';
import { FloorplanEditor } from '@nitrots/nitro-renderer';
import { useFloorplanEditorContext } from '../FloorplanEditorContext';
const MIN_WALL_HEIGHT: number = 0;
const MAX_WALL_HEIGHT: number = 16;
const MIN_FLOOR_HEIGHT: number = 0;
const MAX_FLOOR_HEIGHT: number = 26;
type ScrollDirection = 'up' | 'down' | 'left' | 'right';
interface FloorplanOptionsViewProps
{
onCanvasScroll?(direction: ScrollDirection): void;
}
export const FloorplanOptionsView: FC<FloorplanOptionsViewProps> = props =>
{
const { visualizationSettings = null, setVisualizationSettings = null, floorAction, setFloorAction } = useFloorplanEditorContext();
const isSquareSelectMode = FloorplanEditor.instance.isSquareSelectMode;
const { onCanvasScroll = () => {} } = props;
const { visualizationSettings = null, setVisualizationSettings = null } = useFloorplanEditorContext();
const [ floorAction, setFloorAction ] = useState(FloorAction.SET);
const [ floorHeight, setFloorHeight ] = useState(0);
const [ isSquareSelectMode, setSquareSelectMode ] = useState(false);
const selectAction = (action: number) =>
{
setFloorAction(action);
FloorplanEditor.instance.actionSettings.currentAction = action;
};
}
const toggleSquareSelectMode = () =>
{
FloorplanEditor.instance.toggleSquareSelectMode();
// force re-render by toggling action to same value
setFloorAction(prev => prev);
};
const nextValue = FloorplanEditor.instance.toggleSquareSelectMode();
setSquareSelectMode(nextValue);
}
const changeDoorDirection = () =>
{
@@ -45,19 +58,18 @@ export const FloorplanOptionsView: FC<FloorplanOptionsViewProps> = props =>
return newValue;
});
};
}
const onWallThicknessChange = (value: number) =>
const onFloorHeightChange = (value: number) =>
{
setVisualizationSettings(prevValue =>
{
const newValue = { ...prevValue };
if(isNaN(value) || (value <= 0)) value = 0;
newValue.thicknessWall = value;
if(value > 26) value = 26;
return newValue;
});
};
setFloorHeight(value);
FloorplanEditor.instance.actionSettings.currentHeight = value.toString(36);
}
const onFloorThicknessChange = (value: number) =>
{
@@ -69,54 +81,157 @@ export const FloorplanOptionsView: FC<FloorplanOptionsViewProps> = props =>
return newValue;
});
};
}
const onWallThicknessChange = (value: number) =>
{
setVisualizationSettings(prevValue =>
{
const newValue = { ...prevValue };
newValue.thicknessWall = value;
return newValue;
});
}
const onWallHeightChange = (value: number) =>
{
if(isNaN(value) || (value <= 0)) value = MIN_WALL_HEIGHT;
if(value > MAX_WALL_HEIGHT) value = MAX_WALL_HEIGHT;
setVisualizationSettings(prevValue =>
{
const newValue = { ...prevValue };
newValue.wallHeight = value;
return newValue;
});
}
const increaseWallHeight = () =>
{
let height = (visualizationSettings.wallHeight + 1);
if(height > MAX_WALL_HEIGHT) height = MAX_WALL_HEIGHT;
onWallHeightChange(height);
}
const decreaseWallHeight = () =>
{
let height = (visualizationSettings.wallHeight - 1);
if(height <= 0) height = MIN_WALL_HEIGHT;
onWallHeightChange(height);
}
return (
<Flex gap={ 2 } alignItems="center">
<Flex gap={ 1 } alignItems="center">
<Text bold small>{ LocalizeText('floor.plan.editor.draw.mode') }</Text>
<Flex gap={ 1 }>
<LayoutGridItem itemActive={ (floorAction === FloorAction.SET) } onClick={ () => selectAction(FloorAction.SET) }>
<i className="nitro-icon icon-set-tile" />
</LayoutGridItem>
<LayoutGridItem itemActive={ (floorAction === FloorAction.UNSET) } onClick={ () => selectAction(FloorAction.UNSET) }>
<i className="nitro-icon icon-unset-tile" />
</LayoutGridItem>
<LayoutGridItem itemActive={ (floorAction === FloorAction.UP) } onClick={ () => selectAction(FloorAction.UP) }>
<i className="nitro-icon icon-increase-height" />
</LayoutGridItem>
<LayoutGridItem itemActive={ (floorAction === FloorAction.DOWN) } onClick={ () => selectAction(FloorAction.DOWN) }>
<i className="nitro-icon icon-decrease-height" />
</LayoutGridItem>
<LayoutGridItem itemActive={ (floorAction === FloorAction.DOOR) } onClick={ () => selectAction(FloorAction.DOOR) }>
<i className="nitro-icon icon-set-door" />
</LayoutGridItem>
<LayoutGridItem onClick={ () => FloorplanEditor.instance.toggleSelectAll() }>
<i className={ `nitro-icon ${ floorAction === FloorAction.UNSET ? 'icon-set-deselect' : 'icon-set-select' }` } />
</LayoutGridItem>
<LayoutGridItem itemActive={ isSquareSelectMode } onClick={ toggleSquareSelectMode }>
<i className={ `nitro-icon ${ isSquareSelectMode ? 'icon-set-active-squaresselect' : 'icon-set-squaresselect' }` } />
</LayoutGridItem>
</Flex>
<Column>
<Flex gap={ 1 }>
<Column size={ 5 } gap={ 1 }>
<Text bold>{ LocalizeText('floor.plan.editor.draw.mode') }</Text>
<Flex gap={ 3 }>
<Flex gap={ 1 }>
<LayoutGridItem itemActive={ (floorAction === FloorAction.SET) } onClick={ event => selectAction(FloorAction.SET) }>
<i className="nitro-icon icon-set-tile" />
</LayoutGridItem>
<LayoutGridItem itemActive={ (floorAction === FloorAction.UNSET) } onClick={ event => selectAction(FloorAction.UNSET) }>
<i className="nitro-icon icon-unset-tile" />
</LayoutGridItem>
</Flex>
<Flex gap={ 1 }>
<LayoutGridItem itemActive={ (floorAction === FloorAction.UP) } onClick={ event => selectAction(FloorAction.UP) }>
<i className="nitro-icon icon-increase-height" />
</LayoutGridItem>
<LayoutGridItem itemActive={ (floorAction === FloorAction.DOWN) } onClick={ event => selectAction(FloorAction.DOWN) }>
<i className="nitro-icon icon-decrease-height" />
</LayoutGridItem>
</Flex>
<LayoutGridItem itemActive={ (floorAction === FloorAction.DOOR) } onClick={ event => selectAction(FloorAction.DOOR) }>
<i className="nitro-icon icon-set-door" />
</LayoutGridItem>
<LayoutGridItem onClick={ event => FloorplanEditor.instance.toggleSelectAll() }>
<i className={ `nitro-icon ${ floorAction === FloorAction.UNSET ? 'icon-set-deselect' : 'icon-set-select' }` } />
</LayoutGridItem>
<LayoutGridItem itemActive={ isSquareSelectMode } onClick={ toggleSquareSelectMode }>
<i className={ `nitro-icon ${ isSquareSelectMode ? 'icon-set-active-squaresselect' : 'icon-set-squaresselect' }` } />
</LayoutGridItem>
</Flex>
</Column>
<Column alignItems="center" size={ 4 }>
<Text bold>{ LocalizeText('floor.plan.editor.enter.direction') }</Text>
<i className={ `nitro-icon icon-door-direction-${ visualizationSettings.entryPointDir } cursor-pointer` } onClick={ changeDoorDirection } />
</Column>
<Column size={ 3 }>
<Text bold>{ LocalizeText('floor.editor.wall.height') }</Text>
<Flex alignItems="center" gap={ 1 }>
<FaCaretLeft className="cursor-pointer fa-icon" onClick={ decreaseWallHeight } />
<input type="number" className="form-control form-control-sm w-[49px]" value={ visualizationSettings.wallHeight } onChange={ event => onWallHeightChange(event.target.valueAsNumber) } />
<FaCaretRight className="cursor-pointer fa-icon" onClick={ increaseWallHeight } />
</Flex>
</Column>
<Column size={ 6 }>
<Text bold>{ LocalizeText('floor.plan.editor.room.options') }</Text>
<Flex className="align-items-center">
<select className="form-control form-control-sm" value={ visualizationSettings.thicknessWall } onChange={ event => onWallThicknessChange(parseInt(event.target.value)) }>
<option value={ 0 }>{ LocalizeText('navigator.roomsettings.wall_thickness.thinnest') }</option>
<option value={ 1 }>{ LocalizeText('navigator.roomsettings.wall_thickness.thin') }</option>
<option value={ 2 }>{ LocalizeText('navigator.roomsettings.wall_thickness.normal') }</option>
<option value={ 3 }>{ LocalizeText('navigator.roomsettings.wall_thickness.thick') }</option>
</select>
<select className="form-control form-control-sm" value={ visualizationSettings.thicknessFloor } onChange={ event => onFloorThicknessChange(parseInt(event.target.value)) }>
<option value={ 0 }>{ LocalizeText('navigator.roomsettings.floor_thickness.thinnest') }</option>
<option value={ 1 }>{ LocalizeText('navigator.roomsettings.floor_thickness.thin') }</option>
<option value={ 2 }>{ LocalizeText('navigator.roomsettings.floor_thickness.normal') }</option>
<option value={ 3 }>{ LocalizeText('navigator.roomsettings.floor_thickness.thick') }</option>
</select>
</Flex>
</Column>
</Flex>
<Flex gap={ 1 } alignItems="center">
<Text bold small>{ LocalizeText('floor.plan.editor.enter.direction') }</Text>
<i className={ `nitro-icon icon-door-direction-${ visualizationSettings.entryPointDir } cursor-pointer` } onClick={ changeDoorDirection } />
<Flex gap={ 2 } alignItems="center" justifyContent="between">
<Column size={ 6 }>
<Text bold>{ LocalizeText('floor.plan.editor.tile.height') }: { floorHeight }</Text>
<div style={ { width: '100%', maxWidth: 240 } }>
<Slider
min={ MIN_FLOOR_HEIGHT }
max={ MAX_FLOOR_HEIGHT }
step={ 1 }
value={ floorHeight }
onChange={ event => onFloorHeightChange(event) }
renderThumb={ (props, state) =>
{
const { key, style, ...rest } = (props as Record<string, any>);
return <div key={ key } style={ { backgroundColor: `#${ COLORMAP[state.valueNow.toString(33)] }`, ...style } } { ...rest }>{ state.valueNow }</div>;
} } />
</div>
</Column>
<Column gap={ 1 }>
<Flex justifyContent="center">
<Button shrink onClick={ event => onCanvasScroll('up') }>
<FaArrowUp className="fa-icon" />
</Button>
</Flex>
<Flex alignItems="center" justifyContent="center" gap={ 1 }>
<Button shrink onClick={ event => onCanvasScroll('left') }>
<FaArrowLeft className="fa-icon" />
</Button>
<div style={ { width: 28 } } />
<Button shrink onClick={ event => onCanvasScroll('right') }>
<FaArrowRight className="fa-icon" />
</Button>
</Flex>
<Flex justifyContent="center">
<Button shrink onClick={ event => onCanvasScroll('down') }>
<FaArrowDown className="fa-icon" />
</Button>
</Flex>
</Column>
</Flex>
<Flex gap={ 1 } alignItems="center" className="ml-auto">
<select className="form-control form-control-sm" value={ visualizationSettings.thicknessWall } onChange={ event => onWallThicknessChange(parseInt(event.target.value)) }>
<option value={ 0 }>{ LocalizeText('navigator.roomsettings.wall_thickness.thinnest') }</option>
<option value={ 1 }>{ LocalizeText('navigator.roomsettings.wall_thickness.thin') }</option>
<option value={ 2 }>{ LocalizeText('navigator.roomsettings.wall_thickness.normal') }</option>
<option value={ 3 }>{ LocalizeText('navigator.roomsettings.wall_thickness.thick') }</option>
</select>
<select className="form-control form-control-sm" value={ visualizationSettings.thicknessFloor } onChange={ event => onFloorThicknessChange(parseInt(event.target.value)) }>
<option value={ 0 }>{ LocalizeText('navigator.roomsettings.floor_thickness.thinnest') }</option>
<option value={ 1 }>{ LocalizeText('navigator.roomsettings.floor_thickness.thin') }</option>
<option value={ 2 }>{ LocalizeText('navigator.roomsettings.floor_thickness.normal') }</option>
<option value={ 3 }>{ LocalizeText('navigator.roomsettings.floor_thickness.thick') }</option>
</select>
</Flex>
</Flex>
</Column>
);
};
}
@@ -1,328 +0,0 @@
import { FC, useEffect, useRef } from 'react';
import { COLORMAP, HEIGHT_SCHEME, FloorplanEditor } from '@nitrots/nitro-renderer';
import { useFloorplanEditorContext } from '../FloorplanEditorContext';
const colormap = COLORMAP as Record<string, string>;
const PREVIEW_TILE_W = 16;
const PREVIEW_TILE_H = 8;
const PREVIEW_BLOCK_H = 5;
const WALL_HEIGHT_PX = 40;
const WALL_COLOR = '#6B7B5E';
const WALL_SIDE_COLOR = '#5A6A4F';
const WALL_TOP_COLOR = '#7D8E6F';
function hexToRgb(hex: string): [number, number, number]
{
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
return [ r, g, b ];
}
function rgbToHex(r: number, g: number, b: number): string
{
return `#${ ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1) }`;
}
function darken(hex: string, factor: number): string
{
const [ r, g, b ] = hexToRgb(hex);
return rgbToHex(
Math.floor(r * factor),
Math.floor(g * factor),
Math.floor(b * factor)
);
}
function getTilemapBounds(tilemap: any[][]): { minX: number; minY: number; maxX: number; maxY: number }
{
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
for(let y = 0; y < tilemap.length; y++)
{
if(!tilemap[y]) continue;
for(let x = 0; x < tilemap[y].length; x++)
{
if(!tilemap[y][x] || tilemap[y][x].height === 'x') continue;
if(x < minX) minX = x;
if(x > maxX) maxX = x;
if(y < minY) minY = y;
if(y > maxY) maxY = y;
}
}
if(minX === Infinity) return { minX: 0, minY: 0, maxX: 0, maxY: 0 };
return { minX, minY, maxX, maxY };
}
function renderPreview(canvas: HTMLCanvasElement, wallHeight: number): void
{
const ctx = canvas.getContext('2d');
const tilemap = FloorplanEditor.instance.tilemap;
if(!ctx || !tilemap || tilemap.length === 0)
{
if(ctx)
{
ctx.fillStyle = '#1a1a1a';
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
return;
}
const bounds = getTilemapBounds(tilemap);
const tilesW = bounds.maxX - bounds.minX + 1;
const tilesH = bounds.maxY - bounds.minY + 1;
// find max height for offset calculation
let maxTileHeight = 0;
for(let y = bounds.minY; y <= bounds.maxY; y++)
{
for(let x = bounds.minX; x <= bounds.maxX; x++)
{
if(!tilemap[y] || !tilemap[y][x] || tilemap[y][x].height === 'x') continue;
const hi = HEIGHT_SCHEME.indexOf(tilemap[y][x].height) - 1;
if(hi > maxTileHeight) maxTileHeight = hi;
}
}
// calculate isometric bounds
const isoW = (tilesW + tilesH) * PREVIEW_TILE_W;
const isoH = (tilesW + tilesH) * PREVIEW_TILE_H + maxTileHeight * PREVIEW_BLOCK_H + WALL_HEIGHT_PX;
// scale to fit canvas
const scaleX = (canvas.width - 20) / isoW;
const scaleY = (canvas.height - 20) / isoH;
const scale = Math.min(scaleX, scaleY, 3);
const offsetX = (canvas.width - isoW * scale) / 2;
const offsetY = (canvas.height - isoH * scale) / 2 + WALL_HEIGHT_PX * scale * 0.5;
ctx.fillStyle = '#1a1a1a';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.translate(offsetX, offsetY);
ctx.scale(scale, scale);
const tw = PREVIEW_TILE_W;
const th = PREVIEW_TILE_H;
function isoX(gx: number, gy: number): number
{
return (gx - bounds.minX - gy + bounds.minY) * tw + (tilesH - 1) * tw;
}
function isoY(gx: number, gy: number): number
{
return (gx - bounds.minX + gy - bounds.minY) * th;
}
function hasActiveTile(gx: number, gy: number): boolean
{
return tilemap[gy] && tilemap[gy][gx] && tilemap[gy][gx].height !== 'x';
}
function getTileHeight(gx: number, gy: number): number
{
if(!hasActiveTile(gx, gy)) return 0;
return Math.max(0, HEIGHT_SCHEME.indexOf(tilemap[gy][gx].height) - 1);
}
// draw walls on north and west edges
const wallH = wallHeight > 0 ? wallHeight * PREVIEW_BLOCK_H + WALL_HEIGHT_PX * 0.3 : WALL_HEIGHT_PX * 0.6;
for(let y = bounds.minY; y <= bounds.maxY; y++)
{
for(let x = bounds.minX; x <= bounds.maxX; x++)
{
if(!hasActiveTile(x, y)) continue;
const tileH = getTileHeight(x, y) * PREVIEW_BLOCK_H;
const cx = isoX(x, y);
const cy = isoY(x, y) - tileH;
// west wall (no tile to the left)
if(!hasActiveTile(x - 1, y))
{
ctx.beginPath();
ctx.moveTo(cx, cy + th);
ctx.lineTo(cx, cy + th - wallH);
ctx.lineTo(cx + tw, cy - wallH);
ctx.lineTo(cx + tw, cy);
ctx.closePath();
ctx.fillStyle = WALL_SIDE_COLOR;
ctx.fill();
ctx.strokeStyle = '#4A5A3F';
ctx.lineWidth = 0.5;
ctx.stroke();
}
// north wall (no tile above)
if(!hasActiveTile(x, y - 1))
{
ctx.beginPath();
ctx.moveTo(cx + tw, cy);
ctx.lineTo(cx + tw, cy - wallH);
ctx.lineTo(cx + tw * 2, cy + th - wallH);
ctx.lineTo(cx + tw * 2, cy + th);
ctx.closePath();
ctx.fillStyle = WALL_COLOR;
ctx.fill();
ctx.strokeStyle = '#4A5A3F';
ctx.lineWidth = 0.5;
ctx.stroke();
}
// wall top cap - corner
if(!hasActiveTile(x - 1, y) && !hasActiveTile(x, y - 1))
{
ctx.beginPath();
ctx.moveTo(cx + tw, cy - wallH);
ctx.lineTo(cx + tw + tw * 0.3, cy - wallH - th * 0.3);
ctx.lineTo(cx + tw, cy - wallH - th * 0.6);
ctx.lineTo(cx + tw - tw * 0.3, cy - wallH - th * 0.3);
ctx.closePath();
ctx.fillStyle = WALL_TOP_COLOR;
ctx.fill();
}
}
}
// draw tiles back-to-front
for(let y = bounds.minY; y <= bounds.maxY; y++)
{
for(let x = bounds.minX; x <= bounds.maxX; x++)
{
if(!hasActiveTile(x, y)) continue;
const tile = tilemap[y][x];
const heightIndex = HEIGHT_SCHEME.indexOf(tile.height) - 1;
const tileH = Math.max(0, heightIndex) * PREVIEW_BLOCK_H;
const cx = isoX(x, y);
const cy = isoY(x, y) - tileH;
const heightChar = tile.height;
const baseColor = colormap[heightChar] || 'aaaaaa';
const topColor = `#${ baseColor }`;
const leftColor = darken(baseColor, 0.65);
const rightColor = darken(baseColor, 0.80);
// draw side faces if tile has height
const blockH = Math.max(0, heightIndex) * PREVIEW_BLOCK_H;
// left face (visible when no neighbor to south or neighbor is shorter)
const southH = getTileHeight(x, y + 1);
const leftExpose = hasActiveTile(x, y + 1) ? Math.max(0, heightIndex - southH) * PREVIEW_BLOCK_H : blockH + PREVIEW_BLOCK_H;
if(leftExpose > 0)
{
ctx.beginPath();
ctx.moveTo(cx, cy + th);
ctx.lineTo(cx + tw, cy + th * 2);
ctx.lineTo(cx + tw, cy + th * 2 + leftExpose);
ctx.lineTo(cx, cy + th + leftExpose);
ctx.closePath();
ctx.fillStyle = leftColor;
ctx.fill();
}
// right face
const eastH = getTileHeight(x + 1, y);
const rightExpose = hasActiveTile(x + 1, y) ? Math.max(0, heightIndex - eastH) * PREVIEW_BLOCK_H : blockH + PREVIEW_BLOCK_H;
if(rightExpose > 0)
{
ctx.beginPath();
ctx.moveTo(cx + tw * 2, cy + th);
ctx.lineTo(cx + tw, cy + th * 2);
ctx.lineTo(cx + tw, cy + th * 2 + rightExpose);
ctx.lineTo(cx + tw * 2, cy + th + rightExpose);
ctx.closePath();
ctx.fillStyle = rightColor;
ctx.fill();
}
// top face
ctx.beginPath();
ctx.moveTo(cx + tw, cy);
ctx.lineTo(cx + tw * 2, cy + th);
ctx.lineTo(cx + tw, cy + th * 2);
ctx.lineTo(cx, cy + th);
ctx.closePath();
ctx.fillStyle = topColor;
ctx.fill();
// door indicator
const door = FloorplanEditor.instance.doorLocation;
if(door.x === x && door.y === y)
{
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
ctx.fill();
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 1;
ctx.stroke();
}
}
}
ctx.restore();
}
export const FloorplanPreviewView: FC<{}> = () =>
{
const { tilemapVersion, visualizationSettings } = useFloorplanEditorContext();
const canvasRef = useRef<HTMLCanvasElement>(null);
const rafRef = useRef<number>(0);
useEffect(() =>
{
if(!canvasRef.current) return;
if(rafRef.current) cancelAnimationFrame(rafRef.current);
rafRef.current = requestAnimationFrame(() =>
{
const canvas = canvasRef.current;
if(!canvas) return;
const parent = canvas.parentElement;
if(parent)
{
canvas.width = parent.clientWidth;
canvas.height = parent.clientHeight;
}
renderPreview(canvas, visualizationSettings?.wallHeight ?? 0);
});
return () =>
{
if(rafRef.current) cancelAnimationFrame(rafRef.current);
};
}, [ tilemapVersion, visualizationSettings?.wallHeight ]);
return (
<div className="flex-1 relative rounded overflow-hidden border-2 border-muted" style={ { minHeight: 200, backgroundColor: '#1a1a1a' } }>
<canvas
ref={ canvasRef }
className="w-full h-full"
/>
</div>
);
};
@@ -15,6 +15,8 @@ interface GroupCreatorViewProps
const TABS: number[] = [ 1, 2, 3, 4 ];
let isBuyingGroup = false;
export const GroupCreatorView: FC<GroupCreatorViewProps> = props =>
{
const { onClose = null } = props;
@@ -34,7 +36,10 @@ export const GroupCreatorView: FC<GroupCreatorViewProps> = props =>
const buyGroup = () =>
{
if(!groupData) return;
if(!groupData || isBuyingGroup) return;
isBuyingGroup = true;
setTimeout(() => isBuyingGroup = false, 5000);
const badge = [];
@@ -1,5 +1,5 @@
import { AddLinkEventTracker, GetSessionDataManager, GroupAdminGiveComposer, GroupAdminTakeComposer, GroupConfirmMemberRemoveEvent, GroupConfirmRemoveMemberComposer, GroupMemberParser, GroupMembersComposer, GroupMembersEvent, GroupMembershipAcceptComposer, GroupMembershipDeclineComposer, GroupMembersParser, GroupRank, GroupRemoveMemberComposer, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react';
import { FC, useCallback, useEffect, useRef, useState } from 'react';
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa';
import { GetUserProfile, LocalizeText, SendMessageComposer } from '../../../api';
import { Button, Column, Flex, Grid, LayoutAvatarImageView, LayoutBadgeImageView, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../common';
@@ -16,6 +16,7 @@ export const GroupMembersView: FC<{}> = props =>
const [ searchQuery, setSearchQuery ] = useState<string>('');
const [ removingMemberName, setRemovingMemberName ] = useState<string>(null);
const { showConfirm = null } = useNotification();
const pendingActionsRef = useRef<Set<string>>(new Set());
const getRankDescription = (member: GroupMemberParser) =>
{
@@ -42,6 +43,11 @@ export const GroupMembersView: FC<{}> = props =>
{
if(!membersData.admin || (member.rank === GroupRank.OWNER)) return;
const key = `admin_${member.id}`;
if(pendingActionsRef.current.has(key)) return;
pendingActionsRef.current.add(key);
setTimeout(() => pendingActionsRef.current.delete(key), 2000);
if(member.rank !== GroupRank.ADMIN) SendMessageComposer(new GroupAdminGiveComposer(membersData.groupId, member.id));
else SendMessageComposer(new GroupAdminTakeComposer(membersData.groupId, member.id));
@@ -52,6 +58,11 @@ export const GroupMembersView: FC<{}> = props =>
{
if(!membersData.admin || (member.rank !== GroupRank.REQUESTED)) return;
const key = `accept_${member.id}`;
if(pendingActionsRef.current.has(key)) return;
pendingActionsRef.current.add(key);
setTimeout(() => pendingActionsRef.current.delete(key), 2000);
SendMessageComposer(new GroupMembershipAcceptComposer(membersData.groupId, member.id));
refreshMembers();
@@ -61,6 +72,11 @@ export const GroupMembersView: FC<{}> = props =>
{
if(!membersData.admin) return;
const key = `remove_${member.id}`;
if(pendingActionsRef.current.has(key)) return;
pendingActionsRef.current.add(key);
setTimeout(() => pendingActionsRef.current.delete(key), 2000);
if(member.rank === GroupRank.REQUESTED)
{
SendMessageComposer(new GroupMembershipDeclineComposer(membersData.groupId, member.id));
+27 -18
View File
@@ -83,27 +83,36 @@ export const HotelView: FC<{}> = props =>
if(!container) return;
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight - 55;
const lobbyEl = container.querySelector<HTMLElement>('.nitro-hotel-view-lobby');
if(lobbyEl)
const centerView = () =>
{
const containerRect = container.getBoundingClientRect();
const lobbyRect = lobbyEl.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight - 55;
const lobbyCenterX = (lobbyRect.left - containerRect.left) + container.scrollLeft + lobbyRect.width / 2;
const lobbyCenterY = (lobbyRect.top - containerRect.top) + container.scrollTop + lobbyRect.height / 2;
const lobbyEl = container.querySelector<HTMLElement>('.nitro-hotel-view-lobby');
container.scrollLeft = Math.max(0, lobbyCenterX - viewportWidth / 2);
container.scrollTop = Math.max(0, lobbyCenterY - viewportHeight / 2);
}
else
{
container.scrollLeft = Math.max(0, (2600 - viewportWidth) / 2);
container.scrollTop = Math.max(0, (1425 - viewportHeight) / 2);
}
if(lobbyEl)
{
const containerRect = container.getBoundingClientRect();
const lobbyRect = lobbyEl.getBoundingClientRect();
const lobbyCenterX = (lobbyRect.left - containerRect.left) + container.scrollLeft + lobbyRect.width / 2;
const lobbyCenterY = (lobbyRect.top - containerRect.top) + container.scrollTop + lobbyRect.height / 2;
container.scrollLeft = Math.max(0, lobbyCenterX - viewportWidth / 2);
container.scrollTop = Math.max(0, lobbyCenterY - viewportHeight / 2);
}
else
{
container.scrollLeft = Math.max(0, (2600 - viewportWidth) / 2);
container.scrollTop = Math.max(0, (1425 - viewportHeight) / 2);
}
};
centerView();
window.addEventListener('resize', centerView);
return () => window.removeEventListener('resize', centerView);
}, []);
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) =>
@@ -1,179 +0,0 @@
import { RgbaColorPicker, RgbaColor } from 'react-colorful';
import { FC, useCallback, useMemo, useState } from 'react';
import { FaUndo, FaTrash, FaPen, FaFillDrip, FaSave } from 'react-icons/fa';
import { PRESET_COLORS, useUiSettings } from '../../api';
import { Flex, Text } from '../../common';
const hexToRgba = (hex: string, a = 1): RgbaColor =>
{
const num = parseInt(hex.replace('#', ''), 16);
return { r: (num >> 16) & 0xFF, g: (num >> 8) & 0xFF, b: num & 0xFF, a };
};
const rgbaToHex = (rgba: RgbaColor): string =>
{
return '#' + ((1 << 24) + (rgba.r << 16) + (rgba.g << 8) + rgba.b).toString(16).slice(1);
};
export const InterfaceColorTabView: FC<{}> = () =>
{
const { settings, updateSettings, resetSettings } = useUiSettings();
const [ color, setColor ] = useState<RgbaColor>(() => hexToRgba(settings.headerColor, settings.headerAlpha / 100));
const hexColor = useMemo(() => rgbaToHex(color), [ color ]);
const alphaPercent = useMemo(() => Math.round((color.a ?? 1) * 100), [ color ]);
const onHexInput = useCallback((value: string) =>
{
const clean = value.replace(/[^0-9a-fA-F]/g, '').slice(0, 6);
if(clean.length === 6)
{
const rgba = hexToRgba('#' + clean, color.a);
setColor(rgba);
}
}, [ color.a ]);
const onRgbInput = useCallback((channel: 'r' | 'g' | 'b', value: number) =>
{
const clamped = Math.max(0, Math.min(255, value || 0));
setColor(prev => ({ ...prev, [channel]: clamped }));
}, []);
const onAlphaInput = useCallback((value: number) =>
{
const clamped = Math.max(0, Math.min(100, value || 0));
setColor(prev => ({ ...prev, a: clamped / 100 }));
}, []);
const onPresetClick = useCallback((presetHex: string) =>
{
setColor(hexToRgba(presetHex, color.a));
}, [ color.a ]);
const onSave = useCallback(() =>
{
updateSettings({
colorMode: 'color',
headerColor: hexColor,
headerAlpha: alphaPercent
});
}, [ updateSettings, hexColor, alphaPercent ]);
const onReset = useCallback(() =>
{
resetSettings();
setColor(hexToRgba('#1E7295', 1));
}, [ resetSettings ]);
const onDelete = useCallback(() =>
{
updateSettings({ colorMode: 'default' });
setColor(hexToRgba('#1E7295', 1));
}, [ updateSettings ]);
return (
<Flex column gap={ 2 } className="items-center p-2">
<div className="w-[280px]">
<RgbaColorPicker color={ color } onChange={ setColor } style={ { width: '100%', height: '180px' } } />
</div>
<Flex gap={ 1 } className="items-center mt-1">
<Flex column className="items-center">
<input
className="form-control form-control-sm text-center w-[70px]"
value={ hexColor.replace('#', '').toUpperCase() }
onChange={ e => onHexInput(e.target.value) }
maxLength={ 6 }
/>
<Text small className="text-black">Hex</Text>
</Flex>
<Flex column className="items-center">
<input
type="number"
className="form-control form-control-sm text-center w-[45px]"
value={ color.r }
onChange={ e => onRgbInput('r', parseInt(e.target.value)) }
min={ 0 } max={ 255 }
/>
<Text small className="text-black">R</Text>
</Flex>
<Flex column className="items-center">
<input
type="number"
className="form-control form-control-sm text-center w-[45px]"
value={ color.g }
onChange={ e => onRgbInput('g', parseInt(e.target.value)) }
min={ 0 } max={ 255 }
/>
<Text small className="text-black">G</Text>
</Flex>
<Flex column className="items-center">
<input
type="number"
className="form-control form-control-sm text-center w-[45px]"
value={ color.b }
onChange={ e => onRgbInput('b', parseInt(e.target.value)) }
min={ 0 } max={ 255 }
/>
<Text small className="text-black">B</Text>
</Flex>
<Flex column className="items-center">
<input
type="number"
className="form-control form-control-sm text-center w-[45px]"
value={ alphaPercent }
onChange={ e => onAlphaInput(parseInt(e.target.value)) }
min={ 0 } max={ 100 }
/>
<Text small className="text-black">A</Text>
</Flex>
</Flex>
<div className="grid grid-cols-10 gap-0.5 mt-1">
{ PRESET_COLORS.map((presetHex, i) => (
<div
key={ i }
className="w-[24px] h-[24px] rounded cursor-pointer border border-black/20 hover:scale-110 transition-transform"
style={ { backgroundColor: presetHex } }
onClick={ () => onPresetClick(presetHex) }
/>
)) }
</div>
<Flex gap={ 1 } className="w-full mt-2">
<button
className="flex-1 flex items-center justify-center gap-1 py-2 rounded cursor-pointer text-white"
style={ { backgroundColor: '#5f9ea0' } }
onClick={ onReset }
>
<FaUndo size={ 14 } />
</button>
<button
className="flex-1 flex items-center justify-center gap-1 py-2 rounded cursor-pointer text-white"
style={ { backgroundColor: '#5f9ea0' } }
onClick={ onDelete }
>
<FaTrash size={ 14 } />
</button>
<button
className="flex-1 flex items-center justify-center gap-1 py-2 rounded cursor-pointer text-white"
style={ { backgroundColor: '#b0b0b0' } }
>
<FaPen size={ 14 } />
</button>
<button
className="flex-1 flex items-center justify-center gap-1 py-2 rounded cursor-pointer text-white"
style={ { backgroundColor: '#5f9ea0' } }
onClick={ onSave }
>
<FaFillDrip size={ 14 } />
</button>
</Flex>
<button
className="w-full py-2 rounded cursor-pointer text-white font-bold flex items-center justify-center gap-2"
style={ { backgroundColor: '#008000' } }
onClick={ onSave }
>
<FaSave size={ 14 } />
Salva colore
</button>
</Flex>
);
};
@@ -1,52 +0,0 @@
import { FC, useCallback, useMemo } from 'react';
import { GetConfigurationValue, useUiSettings } from '../../api';
export const InterfaceImageTabView: FC<{}> = () =>
{
const { settings, updateSettings } = useUiSettings();
const imageCount = useMemo(() =>
{
return GetConfigurationValue<number>('ui.header.images.count', 30);
}, []);
const baseUrl = useMemo(() =>
{
return GetConfigurationValue<string>('ui.header.images.url', 'https://image.webbo.city/image/headerImage/image{id}.gif');
}, []);
const images = useMemo(() =>
{
const result: string[] = [];
for(let i = 1; i <= imageCount; i++)
{
result.push(baseUrl.replace('{id}', String(i)));
}
return result;
}, [ imageCount, baseUrl ]);
const onImageSelect = useCallback((url: string) =>
{
updateSettings({
colorMode: 'image',
headerImageUrl: url
});
}, [ updateSettings ]);
return (
<div className="grid grid-cols-8 gap-1 p-2 overflow-auto max-h-[400px]">
{ images.map((url, i) => (
<div
key={ i }
className={ `w-[75px] h-[75px] rounded cursor-pointer border-2 transition-all hover:scale-105 ${ (settings.colorMode === 'image' && settings.headerImageUrl === url) ? 'border-white shadow-lg' : 'border-transparent' }` }
style={ {
backgroundImage: `url(${ url })`,
backgroundSize: 'cover',
backgroundPosition: 'center'
} }
onClick={ () => onImageSelect(url) }
/>
)) }
</div>
);
};
@@ -1,107 +0,0 @@
import { GetSessionDataManager, HabboClubLevelEnum } from '@nitrots/nitro-renderer';
import { FC, useCallback, useMemo, useState } from 'react';
import { GetClubMemberLevel, GetConfigurationValue } from '../../api';
import { Base, Flex, Grid, LayoutCurrencyIcon, NitroCardTabsItemView, NitroCardTabsView, Text } from '../../common';
import { useRoom } from '../../hooks';
interface ItemData
{
id: number;
isHcOnly: boolean;
minRank: number;
isAmbassadorOnly: boolean;
selectable: boolean;
}
const SUB_TABS = [ 'backgrounds', 'stands', 'overlays' ] as const;
type SubTabType = typeof SUB_TABS[number];
const SUB_TAB_LABELS: Record<SubTabType, string> = {
backgrounds: 'Sfondi',
stands: 'Basi',
overlays: 'Overlay'
};
export const InterfaceProfileTabView: FC<{}> = () =>
{
const [ activeSubTab, setActiveSubTab ] = useState<SubTabType>('backgrounds');
const [ selectedBackground, setSelectedBackground ] = useState<number>(0);
const [ selectedStand, setSelectedStand ] = useState<number>(0);
const [ selectedOverlay, setSelectedOverlay ] = useState<number>(0);
const { roomSession } = useRoom();
const userData = useMemo(() => ({
isHcMember: GetClubMemberLevel() >= HabboClubLevelEnum.CLUB,
securityLevel: GetSessionDataManager().canChangeName,
isAmbassador: GetSessionDataManager().isAmbassador
}), []);
const processData = useCallback((configData: any[], dataType: string): ItemData[] =>
{
if(!configData?.length) return [];
return configData
.filter(item =>
{
const meetsRank = userData.securityLevel >= item.minRank;
const ambassadorEligible = !item.isAmbassadorOnly || userData.isAmbassador;
return item.isHcOnly || (meetsRank && ambassadorEligible);
})
.map(item => ({ id: item[`${ dataType }Id`], ...item, selectable: !item.isHcOnly || userData.isHcMember }));
}, [ userData ]);
const allData = useMemo(() => ({
backgrounds: processData(GetConfigurationValue('backgrounds.data'), 'background'),
stands: processData(GetConfigurationValue('stands.data'), 'stand'),
overlays: processData(GetConfigurationValue('overlays.data'), 'overlay')
}), [ processData ]);
const handleSelection = useCallback((id: number) =>
{
if(!roomSession) return;
const setters = { backgrounds: setSelectedBackground, stands: setSelectedStand, overlays: setSelectedOverlay };
const currentValues = { backgrounds: selectedBackground, stands: selectedStand, overlays: selectedOverlay };
setters[activeSubTab](id);
const newValues = { ...currentValues, [activeSubTab]: id };
roomSession.sendBackgroundMessage(newValues.backgrounds, newValues.stands, newValues.overlays);
}, [ activeSubTab, roomSession, selectedBackground, selectedStand, selectedOverlay ]);
const renderItem = useCallback((item: ItemData, type: string) => (
<Flex
pointer
position="relative"
key={ item.id }
onClick={ () => item.selectable && handleSelection(item.id) }
className={ item.selectable ? '' : 'non-selectable' }
>
<Base className={ `profile-${ type } ${ type }-${ item.id }` } />
{ item.isHcOnly && <LayoutCurrencyIcon position="absolute" className="top-1 inset-e-1" type="hc" /> }
</Flex>
), [ handleSelection ]);
return (
<Flex column gap={ 1 }>
<Flex gap={ 1 } className="justify-center">
{ SUB_TABS.map(tab => (
<button
key={ tab }
className={ `px-3 py-1 rounded text-sm cursor-pointer transition-colors ${ activeSubTab === tab ? 'bg-primary text-white' : 'bg-card-grid-item text-black hover:bg-card-grid-item-active' }` }
onClick={ () => setActiveSubTab(tab) }
>
{ SUB_TAB_LABELS[tab] }
</button>
)) }
</Flex>
{ !roomSession && (
<Text bold center className="text-black py-4">Entra in una stanza per modificare il profilo</Text>
) }
{ roomSession && (
<Grid gap={ 1 } columnCount={ 7 } overflow="auto" className="max-h-[300px]">
{ allData[activeSubTab].map(item => renderItem(item, activeSubTab.slice(0, -1))) }
</Grid>
) }
</Flex>
);
};
@@ -1,74 +0,0 @@
import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsView, NitroCardTabsItemView, NitroCardView } from '../../common';
import { InterfaceColorTabView } from './InterfaceColorTabView';
import { InterfaceProfileTabView } from './InterfaceProfileTabView';
const TABS = [ 'color', 'profile' ] as const;
type TabType = typeof TABS[number];
const TAB_LABELS: Record<TabType, string> = {
color: 'Colore',
profile: 'Sfondo profilo'
};
export const InterfaceSettingsView: FC<{}> = () =>
{
const [ isVisible, setIsVisible ] = useState(false);
const [ currentTab, setCurrentTab ] = useState<TabType>('color');
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;
case 'profile':
setCurrentTab('profile');
setIsVisible(true);
return;
}
},
eventUrlPrefix: 'interface-settings/'
};
AddLinkEventTracker(linkTracker);
return () => RemoveLinkEventTracker(linkTracker);
}, []);
if(!isVisible) return null;
return (
<NitroCardView uniqueKey="interface-settings" className="min-w-[535px] max-w-[700px]">
<NitroCardHeaderView headerText="Interfaccia" onCloseClick={ () => setIsVisible(false) } />
<NitroCardTabsView>
{ TABS.map(tab => (
<NitroCardTabsItemView
key={ tab }
isActive={ currentTab === tab }
onClick={ () => setCurrentTab(tab) }
>
{ TAB_LABELS[tab] }
</NitroCardTabsItemView>
)) }
</NitroCardTabsView>
<NitroCardContentView>
{ currentTab === 'color' && <InterfaceColorTabView /> }
{ currentTab === 'profile' && <InterfaceProfileTabView /> }
</NitroCardContentView>
</NitroCardView>
);
};
+14 -6
View File
@@ -2,21 +2,29 @@ import { FC } from 'react';
import { Base, Column, Text } from '../../common';
interface LoadingViewProps {
isError: boolean;
message: string;
isError?: boolean;
message?: string;
}
export const LoadingView: FC<LoadingViewProps> = props => {
const { isError = false, message = '' } = props;
return (
<Column fullHeight position="relative" className="relative z-[100] bg-[radial-gradient(#1d1a24,#003a6b)]">
<Base fullHeight className="container h-100">
<Column fullHeight alignItems="center" justifyContent="center">
<Base className="absolute inset-0 m-auto w-[84px] h-[84px] [zoom:1.5] [image-rendering:pixelated] bg-[url('@/assets/images/loading/loading.gif')] bg-no-repeat bg-left-top" />
<Base className="absolute top-[20px] left-[20px] z-[2] w-[150px] h-[100px] bg-[url('@/assets/images/notifications/coolui.png')] bg-no-repeat bg-left-top" />
{ !isError &&
<Base className="absolute inset-0 m-auto w-[84px] h-[84px] [zoom:1.5] [image-rendering:pixelated] bg-[url('@/assets/images/loading/loading.gif')] bg-no-repeat bg-left-top" /> }
<Base className="absolute top-[20px] left-[20px] z-[2] w-[150px] h-[100px] bg-[url('@/assets/images/notifications/nitro_v3.png')] bg-no-repeat bg-left-top" />
{ isError && (message && message.length) ?
<Base className="fs-4 absolute bottom-[20px] left-1/2 z-[3] -translate-x-1/2 [text-shadow:0px_4px_4px_rgba(0,0,0,0.25)]">{ message }</Base>
<Column alignItems="center" className="absolute bottom-[20px] left-1/2 z-[3] -translate-x-1/2 max-w-[80%]" gap={ 2 }>
<Text fontSizeCustom={ 20 } variant="white" className="text-center [text-shadow:0px_4px_4px_rgba(0,0,0,0.25)]">
Something went wrong while loading
</Text>
<Base className="px-4 py-3 rounded-lg bg-black/40 text-[#ff6b6b] text-sm font-mono text-center break-words whitespace-pre-wrap max-w-[600px]">
{ message }
</Base>
</Column>
:
<Text fontSizeCustom={32} variant="white" className="absolute bottom-[20px] left-1/2 z-[3] -translate-x-1/2 [text-shadow:0px_4px_4px_rgba(0,0,0,0.25)]">
The hotel is loading ...
@@ -1,5 +1,5 @@
import { IssueMessageData, ReleaseIssuesMessageComposer } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { FC, useRef } from 'react';
import { SendMessageComposer } from '../../../../api';
import { Button, Column, Grid } from '../../../../common';
@@ -12,6 +12,17 @@ interface ModToolsMyIssuesTabViewProps
export const ModToolsMyIssuesTabView: FC<ModToolsMyIssuesTabViewProps> = props =>
{
const { myIssues = null, handleIssue = null } = props;
const pendingReleasesRef = useRef<Set<number>>(new Set());
const releaseIssue = (issueId: number) =>
{
if(pendingReleasesRef.current.has(issueId)) return;
pendingReleasesRef.current.add(issueId);
SendMessageComposer(new ReleaseIssuesMessageComposer([ issueId ]));
setTimeout(() => pendingReleasesRef.current.delete(issueId), 2000);
};
return (
<Column gap={ 0 } overflow="hidden">
@@ -36,7 +47,7 @@ export const ModToolsMyIssuesTabView: FC<ModToolsMyIssuesTabViewProps> = props =
<Button variant="primary" onClick={ event => handleIssue(issue.issueId) }>Handle</Button>
</div>
<div className="col-span-2">
<Button variant="danger" onClick={ event => SendMessageComposer(new ReleaseIssuesMessageComposer([ issue.issueId ])) }>Release</Button>
<Button variant="danger" onClick={ () => releaseIssue(issue.issueId) }>Release</Button>
</div>
</Grid>
);
@@ -1,5 +1,5 @@
import { IssueMessageData, PickIssuesMessageComposer } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { FC, useRef } from 'react';
import { SendMessageComposer } from '../../../../api';
import { Button, Column, Grid } from '../../../../common';
@@ -11,6 +11,17 @@ interface ModToolsOpenIssuesTabViewProps
export const ModToolsOpenIssuesTabView: FC<ModToolsOpenIssuesTabViewProps> = props =>
{
const { openIssues = null } = props;
const pendingPicksRef = useRef<Set<number>>(new Set());
const pickIssue = (issueId: number) =>
{
if(pendingPicksRef.current.has(issueId)) return;
pendingPicksRef.current.add(issueId);
SendMessageComposer(new PickIssuesMessageComposer([ issueId ], false, 0, 'pick issue button'));
setTimeout(() => pendingPicksRef.current.delete(issueId), 2000);
};
return (
<Column gap={ 0 } overflow="hidden">
@@ -31,7 +42,7 @@ export const ModToolsOpenIssuesTabView: FC<ModToolsOpenIssuesTabViewProps> = pro
<div className="col-span-3">{ issue.reportedUserName }</div>
<div className="col-span-4">{ new Date(Date.now() - issue.issueAgeInMilliseconds).toLocaleTimeString() }</div>
<div className="col-span-3">
<Button variant="success" onClick={ event => SendMessageComposer(new PickIssuesMessageComposer([ issue.issueId ], false, 0, 'pick issue button')) }>Pick Issue</Button>
<Button variant="success" onClick={ () => pickIssue(issue.issueId) }>Pick Issue</Button>
</div>
</Grid>
);
@@ -1,5 +1,5 @@
import { CallForHelpTopicData, DefaultSanctionMessageComposer, ModAlertMessageComposer, ModBanMessageComposer, ModKickMessageComposer, ModMessageMessageComposer, ModMuteMessageComposer, ModTradingLockMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useMemo, useState } from 'react';
import { FC, useMemo, useRef, useState } from 'react';
import { ISelectedUser, LocalizeText, ModActionDefinition, NotificationAlertType, SendMessageComposer } from '../../../../api';
import { Button, DraggableWindowPosition, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common';
import { useModTools, useNotification } from '../../../../hooks';
@@ -33,6 +33,7 @@ export const ModToolsUserModActionView: FC<ModToolsUserModActionViewProps> = pro
const [ message, setMessage ] = useState<string>('');
const { cfhCategories = null, settings = null } = useModTools();
const { simpleAlert = null } = useNotification();
const isSendingRef = useRef<boolean>(false);
const topics = useMemo(() =>
{
@@ -53,6 +54,8 @@ export const ModToolsUserModActionView: FC<ModToolsUserModActionViewProps> = pro
const sendDefaultSanction = () =>
{
if(isSendingRef.current) return;
let errorMessage: string = null;
const category = topics[selectedTopic];
@@ -63,6 +66,8 @@ export const ModToolsUserModActionView: FC<ModToolsUserModActionViewProps> = pro
const messageOrDefault = (message.trim().length === 0) ? LocalizeText(`help.cfh.topic.${ category.id }`) : message;
isSendingRef.current = true;
SendMessageComposer(new DefaultSanctionMessageComposer(user.userId, selectedTopic, messageOrDefault));
onCloseClick();
@@ -70,6 +75,8 @@ export const ModToolsUserModActionView: FC<ModToolsUserModActionViewProps> = pro
const sendSanction = () =>
{
if(isSendingRef.current) return;
let errorMessage: string = null;
const category = topics[selectedTopic];
@@ -145,6 +152,8 @@ export const ModToolsUserModActionView: FC<ModToolsUserModActionViewProps> = pro
}
}
isSendingRef.current = true;
onCloseClick();
};
@@ -6,6 +6,9 @@ import { Button, Flex, Grid, LayoutCurrencyIcon, LayoutGridItem, Text } from '..
import { useNavigator } from '../../../hooks';
import { NitroInput } from '../../../layout';
let isCreatingRoom = false;
let createRoomTimeout: ReturnType<typeof setTimeout> = null;
export const NavigatorRoomCreatorView: FC<{}> = props =>
{
const [ maxVisitorsList, setMaxVisitorsList ] = useState<number[]>(null);
@@ -16,6 +19,7 @@ export const NavigatorRoomCreatorView: FC<{}> = props =>
const [ tradesSetting, setTradesSetting ] = useState<number>(0);
const [ roomModels, setRoomModels ] = useState<IRoomModel[]>([]);
const [ selectedModelName, setSelectedModelName ] = useState<string>('');
const [ isCreating, setIsCreating ] = useState<boolean>(isCreatingRoom);
const { categories = null } = useNavigator();
const hcDisabled = GetConfigurationValue<boolean>('hc.disabled', false);
@@ -31,7 +35,19 @@ export const NavigatorRoomCreatorView: FC<{}> = props =>
const createRoom = () =>
{
if(isCreatingRoom) return;
isCreatingRoom = true;
setIsCreating(true);
SendMessageComposer(new CreateFlatMessageComposer(name, description, 'model_' + selectedModelName, Number(category), Number(visitorsCount), tradesSetting));
if(createRoomTimeout) clearTimeout(createRoomTimeout);
createRoomTimeout = setTimeout(() =>
{
isCreatingRoom = false;
setIsCreating(false);
}, 5000);
};
useEffect(() =>
@@ -117,7 +133,7 @@ export const NavigatorRoomCreatorView: FC<{}> = props =>
}
</div>
</Grid>
<Button fullWidth disabled={ (!name || (name.length < 3)) } variant={ (!name || (name.length < 3)) ? 'danger' : 'success' } onClick={ createRoom }>{ LocalizeText('navigator.createroom.create') }</Button>
<Button fullWidth disabled={ isCreating || !name || (name.length < 3) } variant={ (isCreating || !name || (name.length < 3)) ? 'danger' : 'success' } onClick={ createRoom }>{ LocalizeText('navigator.createroom.create') }</Button>
</div>
);
};
@@ -1,5 +1,5 @@
import { FlatControllerAddedEvent, FlatControllerRemovedEvent, FlatControllersEvent, RemoveAllRightsMessageComposer, RoomGiveRightsComposer, RoomTakeRightsComposer, RoomUsersWithRightsComposer } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { FC, useEffect, useRef, useState } from 'react';
import { IRoomData, LocalizeText, SendMessageComposer } from '../../../../api';
import { Button, Column, Flex, Grid, Text, UserProfileIconView } from '../../../../common';
import { useFriends, useMessageEvent } from '../../../../hooks';
@@ -18,6 +18,17 @@ export const NavigatorRoomSettingsRightsTabView: FC<NavigatorRoomSettingsTabView
const { roomData = null } = props;
const [ usersWithRights, setUsersWithRights ] = useState<Map<number, string>>(new Map());
const { onlineFriends = [], offlineFriends = [] } = useFriends();
const pendingActionsRef = useRef<Set<string>>(new Set());
const guardedSend = (key: string, composer: any) =>
{
if(pendingActionsRef.current.has(key)) return;
pendingActionsRef.current.add(key);
SendMessageComposer(composer);
setTimeout(() => pendingActionsRef.current.delete(key), 2000);
};
const allFriendsRaw = [ ...onlineFriends, ...offlineFriends ];
@@ -115,7 +126,7 @@ export const NavigatorRoomSettingsRightsTabView: FC<NavigatorRoomSettingsTabView
<Text
pointer
grow
onClick={ () => SendMessageComposer(new RoomTakeRightsComposer(id)) }>
onClick={ () => guardedSend(`take_${id}`, new RoomTakeRightsComposer(id)) }>
{ name }
</Text>
</Flex>
@@ -127,7 +138,7 @@ export const NavigatorRoomSettingsRightsTabView: FC<NavigatorRoomSettingsTabView
<Button
variant="danger"
disabled={ !filteredUsersWithRights.size }
onClick={ () => roomData && SendMessageComposer(new RemoveAllRightsMessageComposer(roomData.roomId)) }>
onClick={ () => roomData && guardedSend('removeAll', new RemoveAllRightsMessageComposer(roomData.roomId)) }>
{ LocalizeText('navigator.flatctrls.clear') }
</Button>
</Column>
@@ -154,7 +165,7 @@ export const NavigatorRoomSettingsRightsTabView: FC<NavigatorRoomSettingsTabView
<Text
pointer
grow
onClick={ () => SendMessageComposer(new RoomGiveRightsComposer(friend.id)) }>
onClick={ () => guardedSend(`give_${friend.id}`, new RoomGiveRightsComposer(friend.id)) }>
{ friend.name }
</Text>
</Flex>
@@ -10,6 +10,6 @@ export const GetConfirmLayout = (item: NotificationConfirmItem, onClose: () => v
switch(item.confirmType)
{
default:
return <NotificationDefaultConfirmView { ...props } />;
return <NotificationDefaultConfirmView key={ item.id } item={ item } onClose={ onClose } />;
}
};
+108
View File
@@ -0,0 +1,108 @@
import { NitroEventType, ReconnectEvent } from '@nitrots/nitro-renderer';
import { FC, useCallback, useState } from 'react';
import { Base, Column, Text } from '../../common';
import { useNitroEvent } from '../../hooks';
export const ReconnectView: FC<{}> = props =>
{
const [ isReconnecting, setIsReconnecting ] = useState(false);
const [ attempt, setAttempt ] = useState(0);
const [ maxAttempts, setMaxAttempts ] = useState(0);
const [ hasFailed, setHasFailed ] = useState(false);
const onReconnecting = useCallback((event: ReconnectEvent) =>
{
setIsReconnecting(true);
setHasFailed(false);
setAttempt(event.attempt);
setMaxAttempts(event.maxAttempts);
}, []);
const onReconnected = useCallback(() =>
{
setIsReconnecting(false);
setHasFailed(false);
setAttempt(0);
}, []);
const onReconnectFailed = useCallback(() =>
{
setIsReconnecting(false);
setHasFailed(true);
}, []);
useNitroEvent<ReconnectEvent>(NitroEventType.SOCKET_RECONNECTING, onReconnecting);
useNitroEvent(NitroEventType.SOCKET_RECONNECTED, onReconnected);
useNitroEvent(NitroEventType.SOCKET_RECONNECT_FAILED, onReconnectFailed);
const handleReload = useCallback(() =>
{
window.location.reload();
}, []);
const handleGoHome = useCallback(() =>
{
sessionStorage.removeItem('nitro_last_room');
sessionStorage.removeItem('nitro_last_room_password');
window.location.reload();
}, []);
if(!isReconnecting && !hasFailed) return null;
return (
<Column
fullHeight
position="fixed"
className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/70 backdrop-blur-sm"
>
<Column alignItems="center" gap={ 3 } className="p-6 rounded-xl bg-[#1a1a2e]/90 border border-white/10 shadow-2xl max-w-[400px]">
{ isReconnecting && (
<>
<Base className="w-[48px] h-[48px] border-4 border-white/20 border-t-[#4dabf7] rounded-full animate-spin" />
<Text fontSizeCustom={ 18 } variant="white" className="text-center font-semibold">
Connection lost
</Text>
<Text fontSizeCustom={ 14 } variant="white" className="text-center opacity-70">
Reconnecting to server... (attempt { attempt }/{ maxAttempts })
</Text>
<Base className="w-full h-[4px] rounded-full bg-white/10 overflow-hidden mt-1">
<Base
className="h-full bg-[#4dabf7] rounded-full transition-all duration-300"
style={ { width: `${ (attempt / maxAttempts) * 100 }%` } }
/>
</Base>
<Text fontSizeCustom={ 12 } variant="white" className="text-center opacity-50">
Please wait, your session will be restored automatically
</Text>
</>
) }
{ hasFailed && (
<>
<Text fontSizeCustom={ 36 } className="text-center text-red-500">&#9888;</Text>
<Text fontSizeCustom={ 18 } variant="white" className="text-center font-semibold">
Connection failed
</Text>
<Text fontSizeCustom={ 14 } variant="white" className="text-center opacity-70">
Unable to reconnect to the server after multiple attempts.
</Text>
<Base className="mt-2 flex gap-3">
<Base
className="px-6 py-2 rounded-lg bg-[#4dabf7] text-white font-semibold cursor-pointer hover:bg-[#339af0] transition-colors"
onClick={ handleReload }
>
Reload Page
</Base>
<Base
className="px-6 py-2 rounded-lg bg-white/10 text-white font-semibold cursor-pointer hover:bg-white/20 transition-colors"
onClick={ handleGoHome }
>
Go to Home
</Base>
</Base>
</>
) }
</Column>
</Column>
);
};
+2 -1
View File
@@ -43,10 +43,11 @@ export const RoomView: FC<{}> = (props) =>
<AnimatePresence>
{
<motion.div
className="w-full h-full"
initial={ { opacity: 0 }}
animate={ { opacity: 1 }}
exit={ { opacity: 0 }}>
<div ref={ elementRef } className="w-100 h-100">
<div ref={ elementRef } className="w-full h-full">
{ roomSession instanceof RoomSession &&
<>
<RoomWidgetsView />
@@ -45,8 +45,7 @@ const BadgeMiniPicker: FC<{
return (
<div
ref={ ref }
className="absolute right-[calc(100%+8px)] top-0 z-50 border border-white/20 rounded-md p-2 shadow-lg min-w-[160px]"
style={ { backgroundColor: 'var(--ui-dark-bg, rgba(28,28,32,0.97))' } }
className="absolute right-[calc(100%+8px)] top-0 z-50 bg-[rgba(28,28,32,0.97)] border border-white/20 rounded-md p-2 shadow-lg min-w-[160px]"
onClick={ e => e.stopPropagation() }>
<input
autoFocus
@@ -220,8 +220,8 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
canUse = true;
isCrackable = true;
crackableHits = stuffData?.hits ?? 0;
crackableTarget = stuffData?.target ?? 0;
crackableHits = stuffData.hits;
crackableTarget = stuffData.target;
}
else if(avatarInfo.extraParam === RoomWidgetEnumItemExtradataParameter.JUKEBOX)
@@ -458,7 +458,7 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
return (
<Column alignItems="end" gap={ 1 }>
<Column className="relative min-w-[190px] max-w-[190px] z-30 pointer-events-auto [box-shadow:inset_0_5px_#22222799,inset_0_-4px_#12121599] rounded" style={ { backgroundColor: 'var(--ui-dark-bg, rgba(28,28,32,.95))' } }>
<Column className="relative min-w-[190px] max-w-[190px] z-30 pointer-events-auto bg-[rgba(28,28,32,.95)] [box-shadow:inset_0_5px_#22222799,inset_0_-4px_#12121599] rounded">
<Column className="h-full p-[8px] overflow-auto" gap={ 1 } overflow="visible">
<div className="flex flex-col gap-1">
<Flex alignItems="center" gap={ 1 } justifyContent="between">
@@ -527,7 +527,7 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
{ isCrackable &&
<>
<hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" />
<Text small wrap variant="white">{ LocalizeText('infostand.crackable_furni.hits_remaining', [ 'hits', 'target' ], [ (crackableHits ?? 0).toString(), (crackableTarget ?? 0).toString() ]) }</Text>
<Text small wrap variant="white">{ LocalizeText('infostand.crackable_furni.hits_remaining', [ 'hits', 'target' ], [ crackableHits.toString(), crackableTarget.toString() ]) }</Text>
</> }
{ avatarInfo.groupId > 0 &&
<>
@@ -552,21 +552,7 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
{ godMode &&
<>
<hr className="m-0 bg-[#0003] border-0 opacity-[.5] h-px" />
{ canSeeFurniId &&
<div className="flex items-center gap-3">
<div className="flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-3 h-3 text-[#7ec8e3]">
<path fillRule="evenodd" d="M4.93 1.31a41.401 41.401 0 0 1 10.14 0C16.194 1.45 17 2.414 17 3.517V18.25a.75.75 0 0 1-1.075.676l-2.8-1.344-2.8 1.344a.75.75 0 0 1-.65 0l-2.8-1.344-2.8 1.344A.75.75 0 0 1 3 18.25V3.517c0-1.103.806-2.068 1.93-2.207Z" clipRule="evenodd" />
</svg>
<Text small wrap variant="white">ID: { avatarInfo.id }</Text>
</div>
<div className="flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-3 h-3 text-[#7ec8e3]">
<path d="M5.127 3.502 5.25 3.5h9.5c.041 0 .082 0 .123.002A2.251 2.251 0 0 0 12.75 2h-5.5a2.25 2.25 0 0 0-2.123 1.502ZM1 10.25A2.25 2.25 0 0 1 3.25 8h13.5A2.25 2.25 0 0 1 19 10.25v5.5A2.25 2.25 0 0 1 16.75 18H3.25A2.25 2.25 0 0 1 1 15.75v-5.5ZM3.25 6.5c-.04 0-.082 0-.123.002A2.25 2.25 0 0 1 5.25 5h9.5c.98 0 1.814.627 2.123 1.502a3.819 3.819 0 0 0-.123-.002H3.25Z" />
</svg>
<Text small wrap variant="white">Sprite: { (() => { const ro = GetRoomEngine().getRoomObject(roomSession.roomId, avatarInfo.id, avatarInfo.isWallItem ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR); return ro?.model?.getValue(RoomObjectVariable.FURNITURE_TYPE_ID) ?? '?'; })() }</Text>
</div>
</div> }
{ canSeeFurniId && <Text small wrap variant="white">ID: { avatarInfo.id }</Text> }
{ (!avatarInfo.isWallItem && canMove) &&
<>
<button
@@ -574,19 +560,6 @@ export const InfoStandWidgetFurniView: FC<InfoStandWidgetFurniViewProps> = props
onClick={ () => setDropdownOpen(!dropdownOpen) }>
{ dropdownOpen ? `${LocalizeText('widget.furni.present.close')} Buildtools` : `${LocalizeText('navigator.roomsettings.doormode.open')} Buildtools` }
</button>
<button
className="w-full text-white text-xs bg-[#1e7295] hover:bg-[#1a617f] border border-[#ffffff33] rounded px-2 py-1 cursor-pointer transition-colors"
onClick={ () =>
{
const roomObject = GetRoomEngine().getRoomObject(roomSession.roomId, avatarInfo.id, avatarInfo.isWallItem ? RoomObjectCategory.WALL : RoomObjectCategory.FLOOR);
const typeId = roomObject?.model?.getValue(RoomObjectVariable.FURNITURE_TYPE_ID);
CreateLinkEvent('furni-editor/show');
if(typeId) window.dispatchEvent(new CustomEvent('furni-editor:open', { detail: { spriteId: typeId } }));
} }>
Edit Furni
</button>
{ dropdownOpen &&
<div className="flex gap-[4px] w-full">
{ /* Left panel: position + rotation */ }
@@ -1,4 +1,4 @@
import { CreateLinkEvent, GetSessionDataManager, RelationshipStatusInfoEvent, RelationshipStatusInfoMessageParser, RoomSessionFavoriteGroupUpdateEvent, RoomSessionUserBadgesEvent, RoomSessionUserFigureUpdateEvent, UserRelationshipsComposer } from '@nitrots/nitro-renderer';
import { GetSessionDataManager, RelationshipStatusInfoEvent, RelationshipStatusInfoMessageParser, RoomSessionFavoriteGroupUpdateEvent, RoomSessionUserBadgesEvent, RoomSessionUserFigureUpdateEvent, UserRelationshipsComposer } from '@nitrots/nitro-renderer';
import { Dispatch, FC, FocusEvent, KeyboardEvent, SetStateAction, useCallback, useEffect, useState } from 'react';
import { FaPencilAlt, FaTimes } from 'react-icons/fa';
import { AvatarInfoUser, CloneObject, GetConfigurationValue, GetGroupInformation, GetUserProfile, LocalizeText, SendMessageComposer } from '../../../../../api';
@@ -7,6 +7,7 @@ import { useMessageEvent, useNitroEvent, useRoom } from '../../../../../hooks';
import { InfoStandBadgeSlotView } from './InfoStandBadgeSlotView';
import { InfoStandWidgetUserRelationshipsView } from './InfoStandWidgetUserRelationshipsView';
import { InfoStandWidgetUserTagsView } from './InfoStandWidgetUserTagsView';
import { BackgroundsView } from '../../../../backgrounds/BackgroundsView';
interface InfoStandWidgetUserViewProps {
avatarInfo: AvatarInfoUser;
@@ -31,7 +32,7 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
const handleProfileClick = useCallback(() => { GetUserProfile(avatarInfo.webID); }, [avatarInfo.webID]);
const handleEditClick = useCallback((event: React.MouseEvent) => { event.stopPropagation(); CreateLinkEvent('interface-settings/profile'); }, []);
const handleEditClick = useCallback((event: React.MouseEvent) => { event.stopPropagation(); setIsVisible(prev => !prev); }, []);
const saveMotto = (motto: string) => {
if (!isEditingMotto || motto.length > GetConfigurationValue<number>('motto.max.length', 38) || !roomSession) return;
@@ -126,7 +127,7 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
return (
<>
<Column className="relative min-w-[190px] max-w-[190px] z-30 pointer-events-auto [box-shadow:inset_0_5px_#22222799,inset_0_-4px_#12121599] rounded" style={ { backgroundColor: 'var(--ui-dark-bg, rgba(28,28,32,0.95))' } }>
<Column className="relative min-w-[190px] max-w-[190px] z-30 pointer-events-auto bg-[rgba(28,28,32,0.95)] [box-shadow:inset_0_5px_#22222799,inset_0_-4px_#12121599] rounded">
<Column className="h-full p-[8px] overflow-auto" gap={1} overflow="visible">
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between">
@@ -256,6 +257,19 @@ export const InfoStandWidgetUserView: FC<InfoStandWidgetUserViewProps> = props =
)}
</Column>
</Column>
{isVisible && avatarInfo.type === AvatarInfoUser.OWN_USER && (
<div className="backgrounds-view-container">
<BackgroundsView
setIsVisible={setIsVisible}
selectedBackground={backgroundId}
setSelectedBackground={setBackgroundId}
selectedStand={standId}
setSelectedStand={setStandId}
selectedOverlay={overlayId}
setSelectedOverlay={setOverlayId}
/>
</div>
)}
</>
);
};
@@ -3,21 +3,16 @@ import { Flex, FlexProps } from '../../../../common';
export const ContextMenuHeaderView: FC<FlexProps> = props =>
{
const { justifyContent = 'center', alignItems = 'center', classNames = [], style = {}, ...rest } = props;
const { justifyContent = 'center', alignItems = 'center', classNames = [], ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'text-[#fff] min-w-[117px] h-[25px] max-h-[25px] text-[16px] mb-[2px]', 'p-1' ];
const newClassNames: string[] = [ 'bg-[#3d5f6e] text-[#fff] min-w-[117px] h-[25px] max-h-[25px] text-[16px] mb-[2px]', 'p-1' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
const mergedStyle = useMemo(() => ({
backgroundColor: 'var(--ui-ctx-header-bg, #3d5f6e)',
...style
}), [ style ]);
return <Flex alignItems={ alignItems } classNames={ getClassNames } justifyContent={ justifyContent } style={ mergedStyle } { ...rest } />;
return <Flex alignItems={ alignItems } classNames={ getClassNames } justifyContent={ justifyContent } { ...rest } />;
};
@@ -8,7 +8,7 @@ interface ContextMenuListItemViewProps extends FlexProps
export const ContextMenuListItemView: FC<ContextMenuListItemViewProps> = props =>
{
const { disabled = false, fullWidth = true, justifyContent = 'center', alignItems = 'center', classNames = [], style = {}, onClick = null, ...rest } = props;
const { disabled = false, fullWidth = true, justifyContent = 'center', alignItems = 'center', classNames = [], onClick = null, ...rest } = props;
const handleClick = (event: MouseEvent<HTMLDivElement>) =>
{
@@ -19,7 +19,7 @@ export const ContextMenuListItemView: FC<ContextMenuListItemViewProps> = props =
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'relative mb-[2px] p-[3px] overflow-hidden', 'h-[24px] max-h-[24px] p-[3px] cursor-pointer' ];
const newClassNames: string[] = [ 'relative mb-[2px] p-[3px] overflow-hidden', 'h-[24px] max-h-[24px] p-[3px] bg-[repeating-linear-gradient(#131e25,#131e25_50%,#0d171d_50%,#0d171d_100%)] cursor-pointer' ];
if(disabled) newClassNames.push('disabled');
@@ -28,10 +28,5 @@ export const ContextMenuListItemView: FC<ContextMenuListItemViewProps> = props =
return newClassNames;
}, [ disabled, classNames ]);
const mergedStyle = useMemo(() => ({
background: 'repeating-linear-gradient(var(--ui-ctx-item-bg1, #131e25), var(--ui-ctx-item-bg1, #131e25) 50%, var(--ui-ctx-item-bg2, #0d171d) 50%, var(--ui-ctx-item-bg2, #0d171d) 100%)',
...style
}), [ style ]);
return <Flex alignItems={ alignItems } classNames={ getClassNames } fullWidth={ fullWidth } justifyContent={ justifyContent } onClick={ handleClick } style={ mergedStyle } { ...rest } />;
return <Flex alignItems={ alignItems } classNames={ getClassNames } fullWidth={ fullWidth } justifyContent={ justifyContent } onClick={ handleClick } { ...rest } />;
};
@@ -76,6 +76,7 @@ export const ContextMenuView: FC<ContextMenuViewProps> = ({
const getClassNames = useMemo(() => {
const classes = [
'p-[2px]!',
'bg-[#1c323f]',
'border-2',
'border-[solid]',
'border-[rgba(255,255,255,.5)]',
@@ -97,7 +98,6 @@ export const ContextMenuView: FC<ContextMenuViewProps> = ({
top: pos.y ?? 0,
transition: isFading ? 'opacity 75ms linear' : undefined,
opacity,
backgroundColor: 'var(--ui-ctx-bg, #1c323f)',
...style,
}),
[pos, opacity, isFading, style]
+30 -30
View File
@@ -69,38 +69,38 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props =>
<ToolbarMeView setMeExpanded={ setMeExpanded } unseenAchievementCount={ getTotalUnseen } useGuideTool={ useGuideTool } />
</motion.div> )}
</AnimatePresence>
<Flex alignItems="center" className="absolute bottom-0 left-0 w-full h-[55px] [box-shadow:inset_0_5px_#22222799,inset_0_-4px_#12121599] py-1 px-3" gap={ 2 } style={ { backgroundColor: 'var(--ui-dark-bg, rgba(28,28,32,.95))' } }>
<Flex alignItems="center" gap={ 2 } className="flex-shrink-0">
<Flex center pointer className={ 'relative w-[50px] h-[45px] overflow-hidden ' + (isMeExpanded ? 'active ' : '') } onClick={ event =>
{
setMeExpanded(!isMeExpanded);
event.stopPropagation();
} }>
<LayoutAvatarImageView className="-ml-[5px] mt-[25px]" direction={ 2 } figure={ userFigure } position="absolute" />
{ (getTotalUnseen > 0) &&
<LayoutItemCountView count={ getTotalUnseen } /> }
<Flex alignItems="center" className="absolute bottom-0 left-0 w-full h-[55px] bg-[rgba(28,28,32,.95)] [box-shadow:inset_0_5px_#22222799,inset_0_-4px_#12121599] py-1 px-3" gap={ 2 } justifyContent="between">
<Flex alignItems="center" gap={ 2 }>
<Flex alignItems="center" gap={ 2 }>
<Flex center pointer className={ 'relative w-[50px] h-[45px] overflow-hidden ' + (isMeExpanded ? 'active ' : '') } onClick={ event =>
{
setMeExpanded(!isMeExpanded);
event.stopPropagation();
} }>
<LayoutAvatarImageView className="-ml-[5px] mt-[25px]" direction={ 2 } figure={ userFigure } position="absolute" />
{ (getTotalUnseen > 0) &&
<LayoutItemCountView count={ getTotalUnseen } /> }
</Flex>
{ isInRoom &&
<ToolbarItemView icon="habbo" onClick={ event => VisitDesktop() } /> }
{ !isInRoom &&
<ToolbarItemView icon="house" onClick={ event => CreateLinkEvent('navigator/goto/home') } /> }
<ToolbarItemView icon="rooms" onClick={ event => CreateLinkEvent('navigator/toggle') } />
{ GetConfigurationValue('game.center.enabled') &&
<ToolbarItemView icon="game" onClick={ event => CreateLinkEvent('games/toggle') } /> }
<ToolbarItemView icon="catalog" onClick={ event => CreateLinkEvent('catalog/toggle') } />
<ToolbarItemView icon="inventory" onClick={ event => CreateLinkEvent('inventory/toggle') }>
{ (getFullCount > 0) &&
<LayoutItemCountView count={ getFullCount } /> }
</ToolbarItemView>
{ isInRoom &&
<ToolbarItemView icon="camera" onClick={ event => CreateLinkEvent('camera/toggle') } /> }
{ isMod &&
<ToolbarItemView icon="modtools" onClick={ event => CreateLinkEvent('mod-tools/toggle') } /> }
</Flex>
{ isInRoom &&
<ToolbarItemView icon="habbo" onClick={ event => VisitDesktop() } /> }
{ !isInRoom &&
<ToolbarItemView icon="house" onClick={ event => CreateLinkEvent('navigator/goto/home') } /> }
<ToolbarItemView icon="rooms" onClick={ event => CreateLinkEvent('navigator/toggle') } />
{ GetConfigurationValue('game.center.enabled') &&
<ToolbarItemView icon="game" onClick={ event => CreateLinkEvent('games/toggle') } /> }
<ToolbarItemView icon="catalog" onClick={ event => CreateLinkEvent('catalog/toggle') } />
<ToolbarItemView icon="inventory" onClick={ event => CreateLinkEvent('inventory/toggle') }>
{ (getFullCount > 0) &&
<LayoutItemCountView count={ getFullCount } /> }
</ToolbarItemView>
{ isInRoom &&
<ToolbarItemView icon="camera" onClick={ event => CreateLinkEvent('camera/toggle') } /> }
{ isMod &&
<ToolbarItemView icon="modtools" onClick={ event => CreateLinkEvent('mod-tools/toggle') } /> }
{ isMod &&
<ToolbarItemView icon="catalog" onClick={ event => CreateLinkEvent('furni-editor/toggle') } /> }
<Flex alignItems="center" id="toolbar-chat-input-container" />
</Flex>
<Flex alignItems="center" justifyContent="center" className="flex-1 min-w-0 max-w-[600px] mx-auto" id="toolbar-chat-input-container" />
<Flex alignItems="center" gap={ 2 } className="flex-shrink-0">
<Flex alignItems="center" gap={ 2 }>
<Flex gap={ 2 }>
<ToolbarItemView icon="friendall" onClick={ event => CreateLinkEvent('friends/toggle') }>
{ (requests.length > 0) &&
@@ -0,0 +1,176 @@
import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer';
import { FC, useEffect, useMemo, useState } from 'react';
import { Button, DraggableWindowPosition, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView, Text } from '../../common';
type WiredToolsTab = 'monitor' | 'variables' | 'inspection' | 'chests' | 'settings';
interface MonitorStat
{
label: string;
value: string;
}
interface MonitorLog
{
type: string;
category: string;
amount: string;
latest: string;
}
const TABS: Array<{ key: WiredToolsTab; label: string; }> = [
{ key: 'monitor', label: 'Monitor' },
{ key: 'variables', label: 'Variables' },
{ key: 'inspection', label: 'Inspection' },
{ key: 'chests', label: 'Chests' },
{ key: 'settings', label: 'Settings' }
];
const MONITOR_STATS: MonitorStat[] = [
{ label: 'Wired usage', value: '0/10000' },
{ label: 'Is heavy', value: 'No' },
{ label: 'Floor furni', value: '0/4000' },
{ label: 'Wall furni', value: '0/4000' },
{ label: 'Permanent furni vars', value: '0/60' }
];
const MONITOR_LOGS: MonitorLog[] = [
{ type: 'EXECUTION_CAP', category: 'ERROR', amount: '0', latest: '/' },
{ type: 'DELAYED_EVENTS_CAP', category: 'ERROR', amount: '0', latest: '/' },
{ type: 'EXECUTOR_OVERLOAD', category: 'ERROR', amount: '0', latest: '/' },
{ type: 'MARKED_AS_HEAVY', category: 'WARNING', amount: '0', latest: '/' },
{ type: 'KILLED', category: 'ERROR', amount: '0', latest: '/' },
{ type: 'RECURSION_TIMEOUT', category: 'ERROR', amount: '0', latest: '/' }
];
export const WiredCreatorToolsView: FC<{}> = props =>
{
const [ isVisible, setIsVisible ] = useState(false);
const [ activeTab, setActiveTab ] = useState<WiredToolsTab>('monitor');
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(prevValue => !prevValue);
return;
case 'tab':
if(parts.length > 2)
{
const tab = parts[2] as WiredToolsTab;
if(TABS.some(entry => entry.key === tab)) setActiveTab(tab);
}
setIsVisible(true);
return;
}
},
eventUrlPrefix: 'wired-tools/'
};
AddLinkEventTracker(linkTracker);
return () => RemoveLinkEventTracker(linkTracker);
}, []);
const currentTabLabel = useMemo(() => TABS.find(tab => tab.key === activeTab)?.label ?? 'Monitor', [ activeTab ]);
if(!isVisible) return null;
return (
<NitroCardView className="min-w-[520px] max-w-[520px]" theme="primary-slim" uniqueKey="wired-creator-tools" windowPosition={ DraggableWindowPosition.TOP_LEFT }>
<NitroCardHeaderView headerText="Wired Creator Tools (:wired)" onCloseClick={ () => setIsVisible(false) } />
<NitroCardTabsView justifyContent="start">
{ TABS.map(tab => (
<NitroCardTabsItemView key={ tab.key } isActive={ (activeTab === tab.key) } onClick={ () => setActiveTab(tab.key) }>
<Text>{ tab.label }</Text>
</NitroCardTabsItemView>
)) }
</NitroCardTabsView>
<NitroCardContentView className="text-black bg-[#e9e6d9]" gap={ 3 }>
<div className="rounded border border-[#2c5f73] overflow-hidden">
<div className="bg-[#24576d] text-white px-3 py-2 text-center">
<Text bold>{ currentTabLabel }</Text>
</div>
{ (activeTab === 'monitor') &&
<div className="p-3 flex flex-col gap-3">
<div className="text-[11px] text-[#666] italic">
This is the initial shell for the Wired Creator Tools. We can now build the real functionality tab by tab.
</div>
<div className="grid grid-cols-[190px_1fr] gap-3">
<div className="bg-white rounded border border-[#b9b3a5] p-2 flex flex-col gap-1">
<Text bold>Statistics:</Text>
{ MONITOR_STATS.map(stat => (
<div key={ stat.label } className="flex justify-between gap-2 text-[12px]">
<span>{ stat.label }:</span>
<span>{ stat.value }</span>
</div>
)) }
</div>
<div className="rounded border border-[#b9b3a5] bg-[linear-gradient(135deg,#2b5f73_0%,#1b4658_100%)] min-h-[140px] flex items-center justify-center text-center px-4">
<div className="text-white/90 text-sm">
<Text bold>Monitor Preview</Text>
<div className="mt-2 text-[12px] opacity-80">
Live statistics, executor health and diagnostics can be connected here next.
</div>
</div>
</div>
</div>
<div className="bg-white rounded border border-[#b9b3a5] p-2 flex flex-col gap-2">
<Text bold>Logs:</Text>
<div className="max-h-[180px] overflow-y-auto border border-[#d1ccbf] rounded">
<table className="w-full text-[12px]">
<thead className="bg-[#efede5] sticky top-0">
<tr>
<th className="text-left px-2 py-1">Type</th>
<th className="text-left px-2 py-1">Category</th>
<th className="text-left px-2 py-1">Amount</th>
<th className="text-left px-2 py-1">Latest occurrence</th>
</tr>
</thead>
<tbody>
{ MONITOR_LOGS.map((log, index) => (
<tr key={ log.type } className={ (index % 2 === 0) ? 'bg-white' : 'bg-[#f8f6f0]' }>
<td className="px-2 py-1 text-[#1b57b2]">{ log.type }</td>
<td className="px-2 py-1">{ log.category }</td>
<td className="px-2 py-1">{ log.amount }</td>
<td className="px-2 py-1">{ log.latest }</td>
</tr>
)) }
</tbody>
</table>
</div>
<div className="flex justify-between gap-2">
<Button disabled variant="danger">Clear all</Button>
<Button disabled variant="secondary">View full logs</Button>
</div>
</div>
</div> }
{ (activeTab !== 'monitor') &&
<div className="p-4 min-h-[360px] flex items-center justify-center text-center text-[#555]">
<div className="max-w-[320px]">
<Text bold>{ currentTabLabel }</Text>
<div className="mt-2 text-[12px]">
This tab is now ready to be wired into the new `:wired` tools flow.
</div>
</div>
</div> }
</div>
</NitroCardContentView>
</NitroCardView>
);
};
+7 -1
View File
@@ -24,7 +24,11 @@ export const WiredBaseView: FC<PropsWithChildren<WiredBaseViewProps>> = props =>
const [ needsSave, setNeedsSave ] = useState<boolean>(false);
const { trigger = null, setTrigger = null, setIntParams = null, setStringParam = null, setFurniIds = null, setAllowsFurni = null, saveWired = null } = useWired();
const onClose = () => setTrigger(null);
const onClose = () =>
{
WiredSelectionVisualizer.clearAllSelectionShaders();
setTrigger(null);
};
const onSave = () =>
{
@@ -48,6 +52,8 @@ export const WiredBaseView: FC<PropsWithChildren<WiredBaseViewProps>> = props =>
{
if(!trigger) return;
WiredSelectionVisualizer.clearAllSelectionShaders();
const spriteId = (trigger.spriteId || -1);
const furniData = GetSessionDataManager().getFloorItemData(spriteId);
@@ -16,45 +16,66 @@ export const USER_SOURCES = [
{ value: 201, label: 'wiredfurni.params.sources.users.201' }
];
export interface WiredSourceOption
{
value: number;
label: string;
}
interface WiredSourcesSelectorProps
{
showFurni?: boolean;
showUsers?: boolean;
furniSource?: number;
userSource?: number;
furniTitle?: string;
usersTitle?: string;
furniSources?: WiredSourceOption[];
userSources?: WiredSourceOption[];
onChangeFurni?: (source: number) => void;
onChangeUsers?: (source: number) => void;
}
export const WiredSourcesSelector: FC<WiredSourcesSelectorProps> = props =>
{
const { showFurni = false, showUsers = false, furniSource = 0, userSource = 0, onChangeFurni = null, onChangeUsers = null } = props;
const {
showFurni = false,
showUsers = false,
furniSource = 0,
userSource = 0,
furniTitle = 'wiredfurni.params.sources.furni.title',
usersTitle = 'wiredfurni.params.sources.users.title',
furniSources = FURNI_SOURCES,
userSources = USER_SOURCES,
onChangeFurni = null,
onChangeUsers = null
} = props;
const furniIndex = Math.max(0, FURNI_SOURCES.findIndex(s => s.value === furniSource));
const userIndex = Math.max(0, USER_SOURCES.findIndex(s => s.value === userSource));
const furniIndex = Math.max(0, furniSources.findIndex(s => s.value === furniSource));
const userIndex = Math.max(0, userSources.findIndex(s => s.value === userSource));
const prevFurni = () =>
{
const next = (furniIndex - 1 + FURNI_SOURCES.length) % FURNI_SOURCES.length;
onChangeFurni && onChangeFurni(FURNI_SOURCES[next].value);
const next = (furniIndex - 1 + furniSources.length) % furniSources.length;
onChangeFurni && onChangeFurni(furniSources[next].value);
};
const nextFurni = () =>
{
const next = (furniIndex + 1) % FURNI_SOURCES.length;
onChangeFurni && onChangeFurni(FURNI_SOURCES[next].value);
const next = (furniIndex + 1) % furniSources.length;
onChangeFurni && onChangeFurni(furniSources[next].value);
};
const prevUsers = () =>
{
const next = (userIndex - 1 + USER_SOURCES.length) % USER_SOURCES.length;
onChangeUsers && onChangeUsers(USER_SOURCES[next].value);
const next = (userIndex - 1 + userSources.length) % userSources.length;
onChangeUsers && onChangeUsers(userSources[next].value);
};
const nextUsers = () =>
{
const next = (userIndex + 1) % USER_SOURCES.length;
onChangeUsers && onChangeUsers(USER_SOURCES[next].value);
const next = (userIndex + 1) % userSources.length;
onChangeUsers && onChangeUsers(userSources[next].value);
};
if(!showFurni && !showUsers) return null;
@@ -63,11 +84,11 @@ export const WiredSourcesSelector: FC<WiredSourcesSelectorProps> = props =>
<div className="flex flex-col gap-2">
{ showFurni &&
<>
<Text bold>{ LocalizeText('wiredfurni.params.sources.furni.title') }</Text>
<Text bold>{ LocalizeText(furniTitle) }</Text>
<div className="flex items-center gap-2">
<Button variant="primary" className="px-2 py-1" onClick={ prevFurni }><FaChevronLeft /></Button>
<div className="flex flex-1 items-center justify-center">
<Text small>{ LocalizeText(FURNI_SOURCES[furniIndex].label) }</Text>
<Text small>{ LocalizeText(furniSources[furniIndex].label) }</Text>
</div>
<Button variant="primary" className="px-2 py-1" onClick={ nextFurni }><FaChevronRight /></Button>
</div>
@@ -77,11 +98,11 @@ export const WiredSourcesSelector: FC<WiredSourcesSelectorProps> = props =>
{ showUsers &&
<>
<Text bold>{ LocalizeText('wiredfurni.params.sources.users.title') }</Text>
<Text bold>{ LocalizeText(usersTitle) }</Text>
<div className="flex items-center gap-2">
<Button variant="primary" className="px-2 py-1" onClick={ prevUsers }><FaChevronLeft /></Button>
<div className="flex flex-1 items-center justify-center">
<Text small>{ LocalizeText(USER_SOURCES[userIndex].label) }</Text>
<Text small>{ LocalizeText(userSources[userIndex].label) }</Text>
</div>
<Button variant="primary" className="px-2 py-1" onClick={ nextUsers }><FaChevronRight /></Button>
</div>
@@ -89,4 +110,3 @@ export const WiredSourcesSelector: FC<WiredSourcesSelectorProps> = props =>
</div>
);
};
@@ -0,0 +1,54 @@
import { FC, useEffect, useState } from 'react';
import { LocalizeText, WiredFurniType } from '../../../../api';
import { Text } from '../../../../common';
import { useWired } from '../../../../hooks';
import { WiredActionBaseView } from './WiredActionBaseView';
import { WiredSourcesSelector } from '../WiredSourcesSelector';
const EFFECT_OPTIONS = [
{ value: 218, label: 'fx_218' },
{ value: 12, label: 'fx_12' },
{ value: 11, label: 'fx_11' },
{ value: 53, label: 'fx_53' },
{ value: 163, label: 'fx_163' }
];
export const WiredActionFreezeView: FC<{}> = () =>
{
const [ effectId, setEffectId ] = useState(218);
const [ cancelOnTeleport, setCancelOnTeleport ] = useState(false);
const [ userSource, setUserSource ] = useState(0);
const { trigger = null, setIntParams = null } = useWired();
const save = () => setIntParams([
effectId,
cancelOnTeleport ? 1 : 0,
userSource
]);
useEffect(() =>
{
setEffectId((trigger?.intData?.length > 0) ? trigger.intData[0] : 218);
setCancelOnTeleport((trigger?.intData?.length > 1) ? (trigger.intData[1] === 1) : false);
setUserSource((trigger?.intData?.length > 2) ? trigger.intData[2] : 0);
}, [ trigger ]);
return (
<WiredActionBaseView
hasSpecialInput={ true }
requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_NONE }
save={ save }
footer={ <WiredSourcesSelector showUsers={ true } userSource={ userSource } onChangeUsers={ setUserSource } /> }>
<div className="flex flex-col gap-1">
<Text bold>Effect</Text>
<select className="form-select form-select-sm" value={ effectId } onChange={ event => setEffectId(parseInt(event.target.value)) }>
{ EFFECT_OPTIONS.map(option => <option key={ option.value } value={ option.value }>{ LocalizeText(option.label) }</option>) }
</select>
</div>
<div className="flex items-center gap-1">
<input checked={ cancelOnTeleport } className="form-check-input" id="freezeCancelOnTeleport" type="checkbox" onChange={ event => setCancelOnTeleport(event.target.checked) } />
<Text>{ LocalizeText('wiredfurni.params.freeze.cancel_on_teleport') }</Text>
</div>
</WiredActionBaseView>
);
};
@@ -0,0 +1,222 @@
import { FC, useCallback, useEffect, useRef, useState } from 'react';
import { LocalizeText, WiredFurniType, WiredSelectionVisualizer } from '../../../../api';
import { Button, Text } from '../../../../common';
import { useWired } from '../../../../hooks';
import { WiredSourcesSelector, FURNI_SOURCES, WiredSourceOption } from '../WiredSourcesSelector';
import { WiredActionBaseView } from './WiredActionBaseView';
const SOURCE_TRIGGER = 0;
const SOURCE_SELECTED = 100;
const SOURCE_SECONDARY_SELECTED = 101;
const FURNI_DELIMITER = ';';
const TARGET_FURNI_SOURCES: WiredSourceOption[] = [
{ value: 0, label: 'wiredfurni.params.sources.furni.0' },
{ value: SOURCE_SECONDARY_SELECTED, label: 'wiredfurni.params.sources.furni.101' },
{ value: 200, label: 'wiredfurni.params.sources.furni.200' },
{ value: 201, label: 'wiredfurni.params.sources.furni.201' }
];
type SelectionMode = 'move' | 'target';
const parseIds = (data: string): number[] =>
{
if(!data || !data.length) return [];
const ids = new Set<number>();
for(const part of data.split(/[;,\t]/))
{
const trimmed = part.trim();
if(!trimmed.length) continue;
const value = parseInt(trimmed, 10);
if(!isNaN(value) && value > 0) ids.add(value);
}
return Array.from(ids);
};
const serializeIds = (ids: number[]): string =>
{
if(!ids || !ids.length) return '';
return ids.filter(id => (id > 0)).join(FURNI_DELIMITER);
};
export const WiredActionFurniToFurniView: FC<{}> = () =>
{
const [ moveSource, setMoveSource ] = useState<number>(SOURCE_TRIGGER);
const [ targetSource, setTargetSource ] = useState<number>(SOURCE_TRIGGER);
const [ moveFurniIds, setMoveFurniIds ] = useState<number[]>([]);
const [ targetFurniIds, setTargetFurniIds ] = useState<number[]>([]);
const [ selectionMode, setSelectionMode ] = useState<SelectionMode>('move');
const highlightedIds = useRef<number[]>([]);
const { trigger = null, furniIds = [], setFurniIds, setIntParams, setStringParam, setAllowsFurni } = useWired();
const syncHighlights = useCallback((nextMoveIds: number[], nextTargetIds: number[]) =>
{
if(highlightedIds.current.length)
{
WiredSelectionVisualizer.clearSelectionShaderFromFurni(highlightedIds.current);
WiredSelectionVisualizer.clearSecondarySelectionShaderFromFurni(highlightedIds.current);
}
const targetSet = new Set(nextTargetIds);
const moveOnlyIds = nextMoveIds.filter(id => !targetSet.has(id));
if(moveOnlyIds.length) WiredSelectionVisualizer.applySelectionShaderToFurni(moveOnlyIds);
if(nextTargetIds.length) WiredSelectionVisualizer.applySecondarySelectionShaderToFurni(nextTargetIds);
highlightedIds.current = Array.from(new Set([ ...nextMoveIds, ...nextTargetIds ]));
}, []);
const switchSelection = useCallback((mode: SelectionMode) =>
{
const canEditMove = (moveSource === SOURCE_SELECTED);
const canEditTarget = (targetSource === SOURCE_SECONDARY_SELECTED);
if(mode === 'move' && !canEditMove) return;
if(mode === 'target' && !canEditTarget) return;
setSelectionMode(mode);
setFurniIds([ ...(mode === 'move' ? moveFurniIds : targetFurniIds) ]);
}, [ moveSource, targetSource, moveFurniIds, targetFurniIds, setFurniIds ]);
useEffect(() =>
{
if(!trigger) return;
const nextMoveIds = trigger.selectedItems ?? [];
const nextTargetIds = parseIds(trigger.stringData);
const nextMoveSource = (trigger.intData.length >= 1)
? trigger.intData[0]
: (nextMoveIds.length ? SOURCE_SELECTED : SOURCE_TRIGGER);
const nextTargetSourceRaw = (trigger.intData.length >= 2)
? trigger.intData[1]
: (nextTargetIds.length ? SOURCE_SECONDARY_SELECTED : SOURCE_TRIGGER);
const nextTargetSource = (nextTargetSourceRaw === SOURCE_SELECTED) ? SOURCE_SECONDARY_SELECTED : nextTargetSourceRaw;
setMoveSource(nextMoveSource);
setTargetSource(nextTargetSource);
setMoveFurniIds(nextMoveIds);
setTargetFurniIds(nextTargetIds);
setSelectionMode('move');
setFurniIds([ ...nextMoveIds ]);
}, [ trigger, setFurniIds ]);
useEffect(() =>
{
if(selectionMode === 'move') setMoveFurniIds(furniIds);
else setTargetFurniIds(furniIds);
}, [ furniIds, selectionMode ]);
useEffect(() =>
{
syncHighlights(moveFurniIds, targetFurniIds);
}, [ moveFurniIds, targetFurniIds, syncHighlights ]);
useEffect(() =>
{
const canEditMove = (moveSource === SOURCE_SELECTED);
const canEditTarget = (targetSource === SOURCE_SECONDARY_SELECTED);
if(selectionMode === 'move' && !canEditMove && canEditTarget)
{
switchSelection('target');
return;
}
if(selectionMode === 'target' && !canEditTarget && canEditMove)
{
switchSelection('move');
return;
}
const canEditCurrent = ((selectionMode === 'move') ? canEditMove : canEditTarget);
setAllowsFurni(canEditCurrent ? WiredFurniType.STUFF_SELECTION_OPTION_BY_ID : WiredFurniType.STUFF_SELECTION_OPTION_NONE);
}, [ selectionMode, moveSource, targetSource, switchSelection, setAllowsFurni ]);
useEffect(() =>
{
return () =>
{
if(!highlightedIds.current.length) return;
WiredSelectionVisualizer.clearSelectionShaderFromFurni(highlightedIds.current);
WiredSelectionVisualizer.clearSecondarySelectionShaderFromFurni(highlightedIds.current);
highlightedIds.current = [];
};
}, []);
const save = useCallback(() =>
{
if(selectionMode === 'target')
{
setSelectionMode('move');
setFurniIds([ ...moveFurniIds ]);
}
setIntParams([
moveSource,
targetSource
]);
setStringParam(serializeIds(targetFurniIds));
}, [ selectionMode, moveFurniIds, moveSource, targetSource, targetFurniIds, setFurniIds, setIntParams, setStringParam ]);
const selectionLimit = trigger?.maximumItemSelectionCount ?? 0;
return (
<WiredActionBaseView
hasSpecialInput={ true }
requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_BY_ID }
save={ save }
footer={
<div className="flex flex-col gap-2">
<WiredSourcesSelector
showFurni={ true }
furniTitle="wiredfurni.params.sources.furni.title.mv.0"
furniSources={ FURNI_SOURCES }
furniSource={ moveSource }
onChangeFurni={ setMoveSource } />
<hr className="m-0 bg-dark" />
<WiredSourcesSelector
showFurni={ true }
furniTitle="wiredfurni.params.sources.furni.title.mv.1"
furniSources={ TARGET_FURNI_SOURCES }
furniSource={ targetSource }
onChangeFurni={ value => setTargetSource((value === SOURCE_SELECTED) ? SOURCE_SECONDARY_SELECTED : value) } />
</div>
}>
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-1">
<Text bold>{ LocalizeText('wiredfurni.params.sources.furni.title.mv.0') }</Text>
<div className="flex items-center gap-2">
<Button
variant={ (selectionMode === 'move') ? 'primary' : 'secondary' }
disabled={ moveSource !== SOURCE_SELECTED }
onClick={ () => switchSelection('move') }>
{ LocalizeText('wiredfurni.params.sources.furni.100') }
</Button>
<Text small>{ selectionLimit ? `${ moveFurniIds.length }/${ selectionLimit }` : moveFurniIds.length }</Text>
</div>
</div>
<div className="flex flex-col gap-1">
<Text bold>{ LocalizeText('wiredfurni.params.sources.furni.title.mv.1') }</Text>
<div className="flex items-center gap-2">
<Button
variant={ (selectionMode === 'target') ? 'primary' : 'secondary' }
disabled={ targetSource !== SOURCE_SECONDARY_SELECTED }
onClick={ () => switchSelection('target') }>
{ LocalizeText('wiredfurni.params.sources.furni.101') }
</Button>
<Text small>{ selectionLimit ? `${ targetFurniIds.length }/${ selectionLimit }` : targetFurniIds.length }</Text>
</div>
</div>
</div>
</WiredActionBaseView>
);
};
@@ -7,6 +7,131 @@ import { NitroInput } from '../../../../layout';
import { WiredActionBaseView } from './WiredActionBaseView';
import { WiredSourcesSelector } from '../WiredSourcesSelector';
type RewardType = 'badge' | 'credits' | 'pixels' | 'diamonds' | 'points' | 'furni' | 'respect';
interface RewardEntry
{
rewardType: RewardType;
rewardValue: string;
probability: number;
pointsType: number;
}
const DEFAULT_PROBABILITY = 100;
const DEFAULT_POINTS_TYPE = 5;
const REWARD_TYPES: { value: RewardType, label: string }[] = [
{ value: 'badge', label: 'Badge' },
{ value: 'credits', label: 'Credits' },
{ value: 'pixels', label: 'Pixels / Duckets' },
{ value: 'diamonds', label: 'Diamonds' },
{ value: 'points', label: 'Extra Currency' },
{ value: 'furni', label: 'Furni' },
{ value: 'respect', label: 'Respect' }
];
const SELECTABLE_REWARD_TYPES = REWARD_TYPES.filter(entry => (entry.value !== 'respect'));
const createReward = (): RewardEntry =>
({
rewardType: 'furni',
rewardValue: '',
probability: DEFAULT_PROBABILITY,
pointsType: DEFAULT_POINTS_TYPE
});
const getRewardValuePlaceholder = (rewardType: RewardType) =>
{
switch(rewardType)
{
case 'badge':
return 'Badge code';
case 'credits':
return 'Credits amount';
case 'pixels':
return 'Pixels amount';
case 'diamonds':
return 'Diamonds amount';
case 'points':
return 'Amount';
case 'furni':
return 'Furni base item id';
case 'respect':
return 'Respect amount';
}
};
const getExtraFieldLabel = (rewardType: RewardType) =>
{
switch(rewardType)
{
case 'points':
return 'Currency Type';
case 'badge':
return 'Code';
default:
return 'Info';
}
};
const getExtraFieldPlaceholder = (rewardType: RewardType) =>
{
switch(rewardType)
{
case 'points':
return 'Type id (e.g. 105)';
case 'badge':
return 'Badge';
default:
return '';
}
};
const parseRewardEntry = (rawType: string, rawCode: string, rawProbability: string): RewardEntry =>
{
const probability = Number(rawProbability);
const parsedProbability = Number.isFinite(probability) ? probability : DEFAULT_PROBABILITY;
if(rawType === '0')
{
return { rewardType: 'badge', rewardValue: rawCode, probability: parsedProbability, pointsType: DEFAULT_POINTS_TYPE };
}
const separatorIndex = rawCode.indexOf('#');
if(separatorIndex === -1)
{
return { rewardType: 'furni', rewardValue: rawCode, probability: parsedProbability, pointsType: DEFAULT_POINTS_TYPE };
}
const rewardType = rawCode.slice(0, separatorIndex);
const rewardValue = rawCode.slice(separatorIndex + 1);
if(rewardType.startsWith('points'))
{
const pointsType = Number(rewardType.slice('points'.length));
return {
rewardType: 'points',
rewardValue,
probability: parsedProbability,
pointsType: Number.isFinite(pointsType) ? pointsType : DEFAULT_POINTS_TYPE
};
}
if(REWARD_TYPES.some(entry => (entry.value === rewardType)))
{
return { rewardType: rewardType as RewardType, rewardValue, probability: parsedProbability, pointsType: DEFAULT_POINTS_TYPE };
}
if(rewardType === 'cata')
{
return { rewardType: 'furni', rewardValue, probability: parsedProbability, pointsType: DEFAULT_POINTS_TYPE };
}
return { rewardType: 'furni', rewardValue: rawCode, probability: parsedProbability, pointsType: DEFAULT_POINTS_TYPE };
};
export const WiredActionGiveRewardView: FC<{}> = props =>
{
const [ limitEnabled, setLimitEnabled ] = useState(false);
@@ -14,7 +139,7 @@ export const WiredActionGiveRewardView: FC<{}> = props =>
const [ uniqueRewards, setUniqueRewards ] = useState(false);
const [ rewardsLimit, setRewardsLimit ] = useState(1);
const [ limitationInterval, setLimitationInterval ] = useState(1);
const [ rewards, setRewards ] = useState<{ isBadge: boolean, itemCode: string, probability: number }[]>([]);
const [ rewards, setRewards ] = useState<RewardEntry[]>([]);
const { trigger = null, setIntParams = null, setStringParam = null } = useWired();
const [ userSource, setUserSource ] = useState<number>(() =>
{
@@ -22,7 +147,8 @@ export const WiredActionGiveRewardView: FC<{}> = props =>
return 0;
});
const addReward = () => setRewards(rewards => [ ...rewards, { isBadge: false, itemCode: '', probability: null } ]);
const addReward = () => setRewards(rewards => [ ...rewards, createReward() ]);
const hasCustomCurrencyReward = rewards.some(reward => (reward.rewardType === 'points'));
const removeReward = (index: number) =>
{
@@ -36,18 +162,9 @@ export const WiredActionGiveRewardView: FC<{}> = props =>
});
};
const updateReward = (index: number, isBadge: boolean, itemCode: string, probability: number) =>
const updateReward = (index: number, updater: (reward: RewardEntry) => RewardEntry) =>
{
const rewardsClone = Array.from(rewards);
const reward = rewardsClone[index];
if(!reward) return;
reward.isBadge = isBadge;
reward.itemCode = itemCode;
reward.probability = probability;
setRewards(rewardsClone);
setRewards(prevValue => prevValue.map((reward, rewardIndex) => ((rewardIndex === index) ? updater(reward) : reward)));
};
const save = () =>
@@ -56,9 +173,20 @@ export const WiredActionGiveRewardView: FC<{}> = props =>
for(const reward of rewards)
{
if(!reward.itemCode) continue;
const rewardValue = reward.rewardValue.trim();
const rewardsString = [ reward.isBadge ? '0' : '1', reward.itemCode, reward.probability.toString() ];
if(!rewardValue) continue;
const probability = Math.max(0, Number.isFinite(reward.probability) ? reward.probability : DEFAULT_PROBABILITY);
const rewardCode = (() =>
{
if(reward.rewardType === 'badge') return rewardValue;
if(reward.rewardType === 'points') return `points${ Math.max(0, reward.pointsType) }#${ rewardValue }`;
return `${ reward.rewardType }#${ rewardValue }`;
})();
const rewardsString = [ reward.rewardType === 'badge' ? '0' : '1', rewardCode, (uniqueRewards ? DEFAULT_PROBABILITY : probability).toString() ];
stringRewards.push(rewardsString.join(','));
}
@@ -71,9 +199,9 @@ export const WiredActionGiveRewardView: FC<{}> = props =>
useEffect(() =>
{
const readRewards: { isBadge: boolean, itemCode: string, probability: number }[] = [];
const readRewards: RewardEntry[] = [];
if(trigger.stringData.length > 0 && trigger.stringData.includes(';'))
if(trigger.stringData.length > 0)
{
const splittedRewards = trigger.stringData.split(';');
@@ -83,11 +211,11 @@ export const WiredActionGiveRewardView: FC<{}> = props =>
if(reward.length !== 3) continue;
readRewards.push({ isBadge: reward[0] === '0', itemCode: reward[1], probability: Number(reward[2]) });
readRewards.push(parseRewardEntry(reward[0], reward[1], reward[2]));
}
}
if(readRewards.length === 0) readRewards.push({ isBadge: false, itemCode: '', probability: null });
if(readRewards.length === 0) readRewards.push(createReward());
setRewardTime((trigger.intData.length > 0) ? trigger.intData[0] : 0);
setUniqueRewards((trigger.intData.length > 1) ? (trigger.intData[1] === 1) : false);
@@ -147,24 +275,64 @@ export const WiredActionGiveRewardView: FC<{}> = props =>
</Button>
</div>
<div className="flex flex-col gap-1">
<div className="grid grid-cols-[1.2fr_1fr_110px_150px_42px] gap-1 px-1">
<Text small bold>Type</Text>
<Text small bold>Amount / Value</Text>
<Text small bold>{ uniqueRewards ? 'Mode' : 'Chance %' }</Text>
<Text small bold>{ hasCustomCurrencyReward ? 'Currency Type' : 'Extra / Info' }</Text>
<Text small bold>Action</Text>
</div>
{ rewards && rewards.map((reward, index) =>
{
const rewardTypeOptions = (reward.rewardType === 'respect')
? REWARD_TYPES
: SELECTABLE_REWARD_TYPES;
return (
<div key={ index } className="flex gap-1">
<div className="flex items-center gap-1">
<input checked={ reward.isBadge } className="form-check-input" type="checkbox" onChange={ (e) => updateReward(index, e.target.checked, reward.itemCode, reward.probability) } />
<Text small>Badge?</Text>
<div key={ index } className="grid grid-cols-[1.2fr_1fr_110px_150px_42px] gap-1">
<select className="w-full form-select form-select-sm" value={ reward.rewardType } onChange={ event => updateReward(index, prevValue => ({ ...prevValue, rewardType: event.target.value as RewardType, rewardValue: '' })) }>
{ rewardTypeOptions.map(entry => <option key={ entry.value } value={ entry.value }>{ entry.label }</option>) }
</select>
<NitroInput
placeholder={ getRewardValuePlaceholder(reward.rewardType) }
type={ reward.rewardType === 'badge' ? 'text' : 'number' }
value={ reward.rewardValue }
onChange={ event => updateReward(index, prevValue => ({ ...prevValue, rewardValue: event.target.value })) } />
{ uniqueRewards
? <div className="flex items-center px-2 rounded bg-muted">
<Text small>Unique</Text>
</div>
: <NitroInput
min={ 0 }
max={ 100 }
placeholder="Chance %"
type="number"
value={ reward.probability }
onChange={ event => updateReward(index, prevValue => ({ ...prevValue, probability: Number(event.target.value) })) } /> }
{ (reward.rewardType === 'points')
?
<NitroInput
min={ 0 }
placeholder={ getExtraFieldPlaceholder(reward.rewardType) }
type="number"
value={ reward.pointsType }
onChange={ event => updateReward(index, prevValue => ({ ...prevValue, pointsType: Number(event.target.value) })) } />
: <div className="flex items-center px-2 rounded bg-muted">
<Text small>{ getExtraFieldLabel(reward.rewardType) }</Text>
</div> }
<div className="flex items-center justify-end">
{ (index > 0) &&
<Button variant="danger" onClick={ event => removeReward(index) }>
<FaTrash className="fa-icon" />
</Button> }
</div>
<NitroInput placeholder="Item Code" type="text" value={ reward.itemCode } onChange={ e => updateReward(index, reward.isBadge, e.target.value, reward.probability) } />
<NitroInput placeholder="Probability" type="number" value={ reward.probability } onChange={ e => updateReward(index, reward.isBadge, reward.itemCode, Number(e.target.value)) } />
{ (index > 0) &&
<Button variant="danger" onClick={ event => removeReward(index) }>
<FaTrash className="fa-icon" />
</Button> }
</div>
);
}) }
</div>
<Text center small className="p-1 rounded bg-muted">
Extra Currency uses Amount as the quantity and Currency Type as the purse type id. Example: amount 200 + type 105.
</Text>
</WiredActionBaseView>
);
};
@@ -1,5 +1,7 @@
import { WiredActionLayoutCode } from '../../../../api';
import { WiredActionBotChangeFigureView } from './WiredActionBotChangeFigureView';
import { WiredActionFreezeView } from './WiredActionFreezeView';
import { WiredActionFurniToFurniView } from './WiredActionFurniToFurniView';
import { WiredActionSendSignalView } from './WiredActionSendSignalView';
import { WiredActionFurniAreaView } from '../selectors/WiredActionFurniAreaView';
import { WiredSelectorFurniNeighborhoodView } from '../selectors/WiredSelectorFurniNeighborhoodView';
@@ -26,10 +28,12 @@ import { WiredActionMoveAndRotateFurniView } from './WiredActionMoveAndRotateFur
import { WiredActionMoveFurniToView } from './WiredActionMoveFurniToView';
import { WiredActionMoveFurniView } from './WiredActionMoveFurniView';
import { WiredActionMuteUserView } from './WiredActionMuteUserView';
import { WiredActionRelativeMoveView } from './WiredActionRelativeMoveView';
import { WiredActionResetView } from './WiredActionResetView';
import { WiredActionSetFurniStateToView } from './WiredActionSetFurniStateToView';
import { WiredActionTeleportView } from './WiredActionTeleportView';
import { WiredActionToggleFurniStateView } from './WiredActionToggleFurniStateView';
import { WiredActionUnfreezeView } from './WiredActionUnfreezeView';
export const WiredActionLayoutView = (code: number) =>
{
@@ -57,6 +61,12 @@ export const WiredActionLayoutView = (code: number) =>
return <WiredActionChatView />;
case WiredActionLayoutCode.FLEE:
return <WiredActionFleeView />;
case WiredActionLayoutCode.FREEZE:
return <WiredActionFreezeView />;
case WiredActionLayoutCode.FURNI_TO_USER:
return <WiredActionTeleportView />;
case WiredActionLayoutCode.FURNI_TO_FURNI:
return <WiredActionFurniToFurniView />;
case WiredActionLayoutCode.GIVE_REWARD:
return <WiredActionGiveRewardView />;
case WiredActionLayoutCode.GIVE_SCORE:
@@ -77,6 +87,8 @@ export const WiredActionLayoutView = (code: number) =>
return <WiredActionMoveFurniToView />;
case WiredActionLayoutCode.MUTE_USER:
return <WiredActionMuteUserView />;
case WiredActionLayoutCode.RELATIVE_MOVE:
return <WiredActionRelativeMoveView />;
case WiredActionLayoutCode.RESET:
return <WiredActionResetView />;
case WiredActionLayoutCode.SET_FURNI_STATE:
@@ -85,6 +97,10 @@ export const WiredActionLayoutView = (code: number) =>
return <WiredActionTeleportView />;
case WiredActionLayoutCode.TOGGLE_FURNI_STATE:
return <WiredActionToggleFurniStateView />;
case WiredActionLayoutCode.UNFREEZE:
return <WiredActionUnfreezeView />;
case WiredActionLayoutCode.USER_TO_FURNI:
return <WiredActionTeleportView />;
case WiredActionLayoutCode.FURNI_AREA_SELECTOR:
return <WiredActionFurniAreaView />;
case WiredActionLayoutCode.FURNI_NEIGHBORHOOD_SELECTOR:
@@ -0,0 +1,120 @@
import { FC, useEffect, useState } from 'react';
import { FaArrowDown, FaArrowLeft, FaArrowRight, FaArrowUp } from 'react-icons/fa';
import { LocalizeText, WiredFurniType } from '../../../../api';
import { Slider, Text } from '../../../../common';
import { useWired } from '../../../../hooks';
import { WiredSourcesSelector } from '../WiredSourcesSelector';
import { WiredActionBaseView } from './WiredActionBaseView';
const MAX_DISTANCE = 20;
const HORIZONTAL_OPTIONS = [
{ value: 0, icon: <FaArrowLeft /> },
{ value: 1, icon: <FaArrowRight /> }
];
const VERTICAL_OPTIONS = [
{ value: 0, icon: <FaArrowDown /> },
{ value: 1, icon: <FaArrowUp /> }
];
const normalizeDirection = (value: number, fallback = 1) =>
{
if(value === 0 || value === 1) return value;
return fallback;
};
const normalizeDistance = (value: number) =>
{
if(isNaN(value)) return 0;
return Math.max(0, Math.min(MAX_DISTANCE, value));
};
export const WiredActionRelativeMoveView: FC<{}> = () =>
{
const { trigger = null, setIntParams = null } = useWired();
const [horizontalDirection, setHorizontalDirection] = useState(1);
const [horizontalDistance, setHorizontalDistance] = useState(0);
const [verticalDirection, setVerticalDirection] = useState(1);
const [verticalDistance, setVerticalDistance] = useState(0);
const [ furniSource, setFurniSource ] = useState<number>(() =>
{
if(trigger?.intData?.length > 4) return trigger.intData[4];
return (trigger?.selectedItems?.length ?? 0) > 0 ? 100 : 0;
});
useEffect(() =>
{
if(!trigger) return;
setHorizontalDirection((trigger.intData.length > 0) ? normalizeDirection(trigger.intData[0], 1) : 1);
setHorizontalDistance((trigger.intData.length > 1) ? normalizeDistance(trigger.intData[1]) : 0);
setVerticalDirection((trigger.intData.length > 2) ? normalizeDirection(trigger.intData[2], 1) : 1);
setVerticalDistance((trigger.intData.length > 3) ? normalizeDistance(trigger.intData[3]) : 0);
if(trigger.intData.length > 4) setFurniSource(trigger.intData[4]);
else setFurniSource((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0);
}, [ trigger ]);
const save = () => setIntParams([
horizontalDirection,
horizontalDistance,
verticalDirection,
verticalDistance,
furniSource
]);
return (
<WiredActionBaseView
hasSpecialInput={ true }
requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_BY_TYPE_OR_FROM_CONTEXT }
save={ save }
footer={ <WiredSourcesSelector showFurni={ true } furniSource={ furniSource } onChangeFurni={ setFurniSource } /> }>
<div className="flex flex-col gap-2">
<Text bold>{ LocalizeText('wiredfurni.params.movement.horizontal.selection') }</Text>
<div className="flex gap-2">
{ HORIZONTAL_OPTIONS.map(option =>
{
return (
<label key={ option.value } className="flex items-center gap-1">
<input checked={ (horizontalDirection === option.value) } className="form-check-input" name="relativeMoveHorizontal" type="radio" onChange={ () => setHorizontalDirection(option.value) } />
<Text>{ option.icon }</Text>
</label>
);
}) }
</div>
<Text>{ LocalizeText('wiredfurni.params.movement.horizontal.distance', [ 'distance' ], [ horizontalDistance.toString() ]) }</Text>
<Slider
max={ MAX_DISTANCE }
min={ 0 }
step={ 1 }
value={ horizontalDistance }
onChange={ value => setHorizontalDistance(value as number) } />
</div>
<div className="flex flex-col gap-2">
<Text bold>{ LocalizeText('wiredfurni.params.movement.vertical.selection') }</Text>
<div className="flex gap-2">
{ VERTICAL_OPTIONS.map(option =>
{
return (
<label key={ option.value } className="flex items-center gap-1">
<input checked={ (verticalDirection === option.value) } className="form-check-input" name="relativeMoveVertical" type="radio" onChange={ () => setVerticalDirection(option.value) } />
<Text>{ option.icon }</Text>
</label>
);
}) }
</div>
<Text>{ LocalizeText('wiredfurni.params.movement.vertical.distance', [ 'distance' ], [ verticalDistance.toString() ]) }</Text>
<Slider
max={ MAX_DISTANCE }
min={ 0 }
step={ 1 }
value={ verticalDistance }
onChange={ value => setVerticalDistance(value as number) } />
</div>
</WiredActionBaseView>
);
};
@@ -0,0 +1,162 @@
import { FC, useEffect, useMemo, useState } from 'react';
import { LocalizeText, WiredFurniType } from '../../../../api';
import { Slider, Text } from '../../../../common';
import { useWired } from '../../../../hooks';
import { WiredSourcesSelector } from '../WiredSourcesSelector';
import { WiredActionBaseView } from './WiredActionBaseView';
const MIN_ALTITUDE = 0;
const MAX_ALTITUDE = 40;
const ALTITUDE_STEP = 0.01;
const ALTITUDE_PATTERN = /^\d*(\.\d{0,2})?$/;
const clampAltitude = (value: number) =>
{
if(isNaN(value)) return MIN_ALTITUDE;
const clamped = Math.min(MAX_ALTITUDE, Math.max(MIN_ALTITUDE, value));
return parseFloat(clamped.toFixed(2));
};
const formatAltitude = (value: number) =>
{
const normalized = clampAltitude(value);
const text = normalized.toFixed(2);
return text.replace(/\.00$/, '').replace(/(\.\d)0$/, '$1');
};
const parseAltitude = (value: string) =>
{
if(!value || !value.trim().length) return 0;
const parsed = parseFloat(value);
if(isNaN(parsed)) return 0;
return clampAltitude(parsed);
};
const OPERATOR_OPTIONS = [
{ value: 0, label: 'wiredfurni.params.operator.0' },
{ value: 1, label: 'wiredfurni.params.operator.1' },
{ value: 2, label: 'wiredfurni.params.operator.2' }
];
const normalizeOperator = (value: number) =>
{
if(value < 0 || value > 2) return 2;
return value;
};
export const WiredActionSetAltitudeView: FC<{}> = () =>
{
const { trigger = null, setIntParams = null, setStringParam = null } = useWired();
const [ operator, setOperator ] = useState(2);
const [ furniSource, setFurniSource ] = useState<number>(() =>
{
if(trigger?.intData?.length > 1) return trigger.intData[1];
return (trigger?.selectedItems?.length ?? 0) > 0 ? 100 : 0;
});
const [ altitude, setAltitude ] = useState(0);
const [ altitudeInput, setAltitudeInput ] = useState('0');
const normalizedAltitudeText = useMemo(() => formatAltitude(altitude), [ altitude ]);
useEffect(() =>
{
if(!trigger) return;
setOperator((trigger.intData.length > 0) ? normalizeOperator(trigger.intData[0]) : 2);
setFurniSource((trigger.intData.length > 1) ? trigger.intData[1] : ((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0));
const nextAltitude = parseAltitude(trigger.stringData);
setAltitude(nextAltitude);
setAltitudeInput(formatAltitude(nextAltitude));
}, [ trigger ]);
const updateAltitude = (value: number) =>
{
const nextValue = clampAltitude(value);
setAltitude(nextValue);
setAltitudeInput(formatAltitude(nextValue));
};
const updateAltitudeInput = (value: string) =>
{
if(!ALTITUDE_PATTERN.test(value)) return;
setAltitudeInput(value);
if(!value.length)
{
setAltitude(0);
return;
}
const parsedValue = parseFloat(value);
if(isNaN(parsedValue)) return;
if(parsedValue > MAX_ALTITUDE)
{
updateAltitude(MAX_ALTITUDE);
return;
}
setAltitude(clampAltitude(parsedValue));
};
const save = () =>
{
setIntParams([
operator,
furniSource
]);
setStringParam(normalizedAltitudeText);
};
return (
<WiredActionBaseView
hasSpecialInput={ true }
requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_BY_TYPE_OR_FROM_CONTEXT }
save={ save }
footer={ <WiredSourcesSelector showFurni={ true } furniSource={ furniSource } onChangeFurni={ setFurniSource } /> }>
<div className="flex flex-col gap-2">
{ OPERATOR_OPTIONS.map(option =>
{
return (
<div key={ option.value } className="flex items-center gap-1">
<input checked={ (operator === option.value) } className="form-check-input" id={ `setAltitudeOperator${ option.value }` } name="setAltitudeOperator" type="radio" onChange={ () => setOperator(option.value) } />
<Text>{ LocalizeText(option.label) }</Text>
</div>
);
}) }
</div>
<div className="flex flex-col gap-1">
<Text bold>{ LocalizeText('wiredfurni.params.setaltitude') }</Text>
<input
className="form-control form-control-sm"
inputMode="decimal"
type="text"
value={ altitudeInput }
onBlur={ () => setAltitudeInput(formatAltitude(altitude)) }
onChange={ event => updateAltitudeInput(event.target.value) } />
</div>
<div className="flex flex-col gap-1">
<Slider
max={ MAX_ALTITUDE }
min={ MIN_ALTITUDE }
step={ ALTITUDE_STEP }
value={ altitude }
onChange={ event => updateAltitude(event as number) } />
<Text small>{ normalizedAltitudeText }</Text>
</div>
</WiredActionBaseView>
);
};
@@ -0,0 +1,26 @@
import { FC, useEffect, useState } from 'react';
import { WiredFurniType } from '../../../../api';
import { useWired } from '../../../../hooks';
import { WiredActionBaseView } from './WiredActionBaseView';
import { WiredSourcesSelector } from '../WiredSourcesSelector';
export const WiredActionUnfreezeView: FC<{}> = () =>
{
const [ userSource, setUserSource ] = useState(0);
const { trigger = null, setIntParams = null } = useWired();
const save = () => setIntParams([ userSource ]);
useEffect(() =>
{
setUserSource((trigger?.intData?.length > 0) ? trigger.intData[0] : 0);
}, [ trigger ]);
return (
<WiredActionBaseView
hasSpecialInput={ true }
requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_NONE }
save={ save }
footer={ <WiredSourcesSelector showUsers={ true } userSource={ userSource } onChangeUsers={ setUserSource } /> } />
);
};
@@ -0,0 +1,185 @@
import { FC, useEffect, useState } from 'react';
import { LocalizeText, WiredFurniType } from '../../../../api';
import { Slider, Text } from '../../../../common';
import { useWired } from '../../../../hooks';
import { WiredSourcesSelector } from '../WiredSourcesSelector';
import { WiredConditionBaseView } from './WiredConditionBaseView';
const COUNTER_INTERACTION_TYPES = [ 'game_upcounter' ];
const MIN_ALTITUDE = 0;
const MAX_ALTITUDE = 40;
const ALTITUDE_STEP = 0.01;
const ALTITUDE_PATTERN = /^\d*(\.\d{0,2})?$/;
const clampAltitude = (value: number) =>
{
if(isNaN(value)) return MIN_ALTITUDE;
const clamped = Math.min(MAX_ALTITUDE, Math.max(MIN_ALTITUDE, value));
return parseFloat(clamped.toFixed(2));
};
const formatAltitude = (value: number) =>
{
const normalized = clampAltitude(value);
const text = normalized.toFixed(2);
return text.replace(/\.00$/, '').replace(/(\.\d)0$/, '$1');
};
const parseAltitude = (value: string) =>
{
if(!value || !value.trim().length) return 0;
const parsed = parseFloat(value);
if(isNaN(parsed)) return 0;
return clampAltitude(parsed);
};
export const WiredConditionHasAltitudeView: FC<{}> = () =>
{
const { trigger = null, setIntParams = null, setStringParam = null, setAllowedInteractionTypes = null, setAllowedInteractionErrorKey = null } = useWired();
const [ comparison, setComparison ] = useState(1);
const [ furniSource, setFurniSource ] = useState<number>(() =>
{
if(trigger?.intData?.length > 1) return trigger.intData[1];
return (trigger?.selectedItems?.length ?? 0) > 0 ? 100 : 0;
});
const [ quantifier, setQuantifier ] = useState(0);
const [ showAdvanced, setShowAdvanced ] = useState(false);
const [ altitude, setAltitude ] = useState(0);
const [ altitudeInput, setAltitudeInput ] = useState('0');
useEffect(() =>
{
setAllowedInteractionTypes(COUNTER_INTERACTION_TYPES);
setAllowedInteractionErrorKey('wiredfurni.error.require_counter_furni');
return () =>
{
setAllowedInteractionTypes(null);
setAllowedInteractionErrorKey(null);
};
}, [ setAllowedInteractionErrorKey, setAllowedInteractionTypes ]);
useEffect(() =>
{
if(!trigger) return;
setComparison((trigger.intData.length > 0) ? trigger.intData[0] : 1);
setFurniSource((trigger.intData.length > 1) ? trigger.intData[1] : ((trigger.selectedItems?.length ?? 0) > 0 ? 100 : 0));
setQuantifier((trigger.intData.length > 2) ? trigger.intData[2] : 0);
setShowAdvanced((trigger.intData.length > 1) ? (trigger.intData[1] !== 0 || trigger.intData[2] !== 0) : false);
const nextAltitude = parseAltitude(trigger.stringData);
setAltitude(nextAltitude);
setAltitudeInput(formatAltitude(nextAltitude));
}, [ trigger ]);
const updateAltitude = (value: number) =>
{
const nextValue = clampAltitude(value);
setAltitude(nextValue);
setAltitudeInput(formatAltitude(nextValue));
};
const updateAltitudeInput = (value: string) =>
{
if(!ALTITUDE_PATTERN.test(value)) return;
setAltitudeInput(value);
if(!value.length)
{
setAltitude(0);
return;
}
const parsedValue = parseFloat(value);
if(isNaN(parsedValue)) return;
if(parsedValue > MAX_ALTITUDE)
{
updateAltitude(MAX_ALTITUDE);
return;
}
setAltitude(clampAltitude(parsedValue));
};
const save = () =>
{
setIntParams([
comparison,
furniSource,
quantifier
]);
setStringParam(formatAltitude(altitude));
};
return (
<WiredConditionBaseView
hasSpecialInput={ true }
requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_BY_TYPE_OR_FROM_CONTEXT }
save={ save }
footer={
<div className="flex flex-col gap-2">
<button className="btn btn-link p-0 align-self-start" type="button" onClick={ () => setShowAdvanced(value => !value) }>
{ LocalizeText(showAdvanced ? 'wiredfurni.params.sources.collapse' : 'wiredfurni.params.sources.expand') }
</button>
{ showAdvanced &&
<>
<div className="flex flex-col gap-1">
<Text bold>{ LocalizeText('wiredfurni.params.quantifier_selection') }</Text>
{ [ 0, 1 ].map(value =>
{
return (
<div key={ value } className="flex items-center gap-1">
<input checked={ (quantifier === value) } className="form-check-input" id={ `altitudeQuantifier${ value }` } name="altitudeQuantifier" type="radio" onChange={ () => setQuantifier(value) } />
<Text>{ LocalizeText(`wiredfurni.params.quantifier.furni.${ value }`) }</Text>
</div>
);
}) }
</div>
<WiredSourcesSelector showFurni={ true } furniSource={ furniSource } onChangeFurni={ setFurniSource } />
</> }
</div>
}>
<div className="flex flex-col gap-2">
{ [ 0, 1, 2 ].map(value =>
{
return (
<div key={ value } className="flex items-center gap-1">
<input checked={ (comparison === value) } className="form-check-input" id={ `altitudeComparison${ value }` } name="altitudeComparison" type="radio" onChange={ () => setComparison(value) } />
<Text>{ LocalizeText(`wiredfurni.params.comparison.${ value }`) }</Text>
</div>
);
}) }
</div>
<div className="flex flex-col gap-1">
<Text bold>{ LocalizeText('wiredfurni.params.setaltitude') }</Text>
<input
className="form-control form-control-sm"
inputMode="decimal"
type="text"
value={ altitudeInput }
onBlur={ () => setAltitudeInput(formatAltitude(altitude)) }
onChange={ event => updateAltitudeInput(event.target.value) } />
</div>
<div className="flex flex-col gap-1">
<Slider
max={ MAX_ALTITUDE }
min={ MIN_ALTITUDE }
step={ ALTITUDE_STEP }
value={ altitude }
onChange={ event => updateAltitude(event as number) } />
<Text small>{ formatAltitude(altitude) }</Text>
</div>
</WiredConditionBaseView>
);
};
@@ -5,7 +5,11 @@ import { WiredConditionActorIsOnFurniView } from './WiredConditionActorIsOnFurni
import { WiredConditionActorIsTeamMemberView } from './WiredConditionActorIsTeamMemberView';
import { WiredConditionActorIsWearingBadgeView } from './WiredConditionActorIsWearingBadgeView';
import { WiredConditionActorIsWearingEffectView } from './WiredConditionActorIsWearingEffectView';
import { WiredConditionCounterTimeMatchesView } from './WiredConditionCounterTimeMatchesView';
import { WiredConditionDateRangeView } from './WiredConditionDateRangeView';
import { WiredConditionMatchDateView } from './WiredConditionMatchDateView';
import { WiredConditionMatchTimeView } from './WiredConditionMatchTimeView';
import { WiredConditionHasAltitudeView } from './WiredConditionHasAltitudeView';
import { WiredConditionFurniHasAvatarOnView } from './WiredConditionFurniHasAvatarOnView';
import { WiredConditionFurniHasFurniOnView } from './WiredConditionFurniHasFurniOnView';
import { WiredConditionFurniHasNotFurniOnView } from './WiredConditionFurniHasNotFurniOnView';
@@ -13,6 +17,10 @@ import { WiredConditionFurniIsOfTypeView } from './WiredConditionFurniIsOfTypeVi
import { WiredConditionFurniMatchesSnapshotView } from './WiredConditionFurniMatchesSnapshotView';
import { WiredConditionTimeElapsedLessView } from './WiredConditionTimeElapsedLessView';
import { WiredConditionTimeElapsedMoreView } from './WiredConditionTimeElapsedMoreView';
import { WiredConditionTeamHasRankView } from './WiredConditionTeamHasRankView';
import { WiredConditionTeamHasScoreView } from './WiredConditionTeamHasScoreView';
import { WiredConditionTriggererMatchView } from './WiredConditionTriggererMatchView';
import { WiredConditionUserPerformsActionView } from './WiredConditionUserPerformsActionView';
import { WiredConditionUserCountInRoomView } from './WiredConditionUserCountInRoomView';
export const WiredConditionLayoutView = (code: number) =>
@@ -20,7 +28,11 @@ export const WiredConditionLayoutView = (code: number) =>
switch(code)
{
case WiredConditionlayout.ACTOR_HAS_HANDITEM:
case WiredConditionlayout.NOT_ACTOR_HAS_HANDITEM:
return <WiredConditionActorHasHandItemView />;
case WiredConditionlayout.TRIGGERER_MATCH:
case WiredConditionlayout.NOT_TRIGGERER_MATCH:
return <WiredConditionTriggererMatchView />;
case WiredConditionlayout.ACTOR_IS_GROUP_MEMBER:
case WiredConditionlayout.NOT_ACTOR_IN_GROUP:
return <WiredConditionActorIsGroupMemberView />;
@@ -38,6 +50,10 @@ export const WiredConditionLayoutView = (code: number) =>
return <WiredConditionActorIsWearingEffectView />;
case WiredConditionlayout.DATE_RANGE_ACTIVE:
return <WiredConditionDateRangeView />;
case WiredConditionlayout.MATCH_TIME:
return <WiredConditionMatchTimeView />;
case WiredConditionlayout.MATCH_DATE:
return <WiredConditionMatchDateView />;
case WiredConditionlayout.FURNIS_HAVE_AVATARS:
case WiredConditionlayout.FURNI_NOT_HAVE_HABBO:
return <WiredConditionFurniHasAvatarOnView />;
@@ -58,6 +74,18 @@ export const WiredConditionLayoutView = (code: number) =>
case WiredConditionlayout.USER_COUNT_IN:
case WiredConditionlayout.NOT_USER_COUNT_IN:
return <WiredConditionUserCountInRoomView />;
case WiredConditionlayout.COUNTER_TIME_MATCHES:
return <WiredConditionCounterTimeMatchesView />;
case WiredConditionlayout.USER_PERFORMS_ACTION:
return <WiredConditionUserPerformsActionView />;
case WiredConditionlayout.NOT_USER_PERFORMS_ACTION:
return <WiredConditionUserPerformsActionView negative={ true } />;
case WiredConditionlayout.HAS_ALTITUDE:
return <WiredConditionHasAltitudeView />;
case WiredConditionlayout.TEAM_HAS_SCORE:
return <WiredConditionTeamHasScoreView />;
case WiredConditionlayout.TEAM_HAS_RANK:
return <WiredConditionTeamHasRankView />;
}
return null;
@@ -0,0 +1,195 @@
import { ChangeEvent, FC, useEffect, useMemo, useState } from 'react';
import { LocalizeText, WiredFurniType } from '../../../../api';
import { Text } from '../../../../common';
import { useWired } from '../../../../hooks';
import { WiredConditionBaseView } from './WiredConditionBaseView';
const MODE_SKIP = 0;
const MODE_EXACT = 1;
const MODE_RANGE = 2;
const MODE_OPTIONS = [ MODE_SKIP, MODE_EXACT, MODE_RANGE ];
const WEEKDAY_OPTIONS = [ 1, 2, 3, 4, 5, 6, 7 ];
const MONTH_OPTIONS = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
const createMask = (values: number[]) => values.reduce((mask, value) => (mask | (1 << value)), 0);
const ALL_WEEKDAYS_MASK = createMask(WEEKDAY_OPTIONS);
const ALL_MONTHS_MASK = createMask(MONTH_OPTIONS);
const clampValue = (value: number, min: number, max: number) =>
{
if(isNaN(value)) return min;
return Math.max(min, Math.min(max, Math.floor(value)));
};
const parseInputValue = (event: ChangeEvent<HTMLInputElement>, min: number, max: number) =>
{
return clampValue(parseInt(event.target.value || min.toString(), 10), min, max);
};
const toggleMaskValue = (mask: number, value: number, enabled: boolean) =>
{
if(enabled) return (mask | (1 << value));
return (mask & ~(1 << value));
};
const InlineNumberInput: FC<{ value: number; min: number; max: number; onChange: (value: number) => void }> = props =>
{
const { value = 0, min = 0, max = 0, onChange = null } = props;
return (
<input
className="form-control form-control-sm text-center"
max={ max }
min={ min }
style={ { width: 72 } }
type="number"
value={ value }
onChange={ event => onChange(parseInputValue(event, min, max)) } />
);
};
interface MatchDateSectionProps
{
sectionId: string;
titleKey: string;
mode: number;
fromValue: number;
toValue: number;
min: number;
max: number;
onModeChange: (value: number) => void;
onFromChange: (value: number) => void;
onToChange: (value: number) => void;
}
const MatchDateSection: FC<MatchDateSectionProps> = props =>
{
const { sectionId = '', titleKey = '', mode = MODE_SKIP, fromValue = 0, toValue = 0, min = 0, max = 0, onModeChange = null, onFromChange = null, onToChange = null } = props;
return (
<div className="flex flex-col gap-2">
<Text bold>{ LocalizeText(titleKey) }</Text>
<div className="flex items-center gap-1">
<input checked={ (mode === MODE_SKIP) } className="form-check-input" id={ `${ sectionId }0` } name={ sectionId } type="radio" onChange={ () => onModeChange(MODE_SKIP) } />
<Text>{ LocalizeText('wiredfurni.params.time.skip') }</Text>
</div>
<div className="flex items-center gap-2">
<input checked={ (mode === MODE_EXACT) } className="form-check-input" id={ `${ sectionId }1` } name={ sectionId } type="radio" onChange={ () => onModeChange(MODE_EXACT) } />
<Text>{ LocalizeText('wiredfurni.params.time.exact') }</Text>
<InlineNumberInput max={ max } min={ min } value={ fromValue } onChange={ onFromChange } />
</div>
<div className="flex items-center gap-2">
<input checked={ (mode === MODE_RANGE) } className="form-check-input" id={ `${ sectionId }2` } name={ sectionId } type="radio" onChange={ () => onModeChange(MODE_RANGE) } />
<Text>{ LocalizeText('wiredfurni.params.time.range') }</Text>
<InlineNumberInput max={ max } min={ min } value={ fromValue } onChange={ onFromChange } />
<Text>-</Text>
<InlineNumberInput max={ max } min={ min } value={ toValue } onChange={ onToChange } />
</div>
</div>
);
};
export const WiredConditionMatchDateView: FC<{}> = () =>
{
const { trigger = null, setIntParams = null } = useWired();
const currentYear = useMemo(() => new Date().getFullYear(), []);
const [ weekdayMask, setWeekdayMask ] = useState(ALL_WEEKDAYS_MASK);
const [ dayMode, setDayMode ] = useState(MODE_SKIP);
const [ dayFrom, setDayFrom ] = useState(1);
const [ dayTo, setDayTo ] = useState(31);
const [ monthMask, setMonthMask ] = useState(ALL_MONTHS_MASK);
const [ yearMode, setYearMode ] = useState(MODE_SKIP);
const [ yearFrom, setYearFrom ] = useState(currentYear);
const [ yearTo, setYearTo ] = useState(currentYear);
useEffect(() =>
{
if(!trigger) return;
setWeekdayMask((trigger.intData[0] && (trigger.intData[0] > 0)) ? trigger.intData[0] : ALL_WEEKDAYS_MASK);
setDayMode(MODE_OPTIONS.includes(trigger.intData[1]) ? trigger.intData[1] : MODE_SKIP);
setDayFrom(clampValue(trigger.intData[2] ?? 1, 1, 31));
setDayTo(clampValue(trigger.intData[3] ?? 31, 1, 31));
setMonthMask((trigger.intData[4] && (trigger.intData[4] > 0)) ? trigger.intData[4] : ALL_MONTHS_MASK);
setYearMode(MODE_OPTIONS.includes(trigger.intData[5]) ? trigger.intData[5] : MODE_SKIP);
setYearFrom(clampValue(trigger.intData[6] ?? currentYear, 1, 9999));
setYearTo(clampValue(trigger.intData[7] ?? currentYear, 1, 9999));
}, [ currentYear, trigger ]);
const save = () =>
{
setIntParams([
weekdayMask || ALL_WEEKDAYS_MASK,
dayMode,
clampValue(dayFrom, 1, 31),
clampValue(dayTo, 1, 31),
monthMask || ALL_MONTHS_MASK,
yearMode,
clampValue(yearFrom, 1, 9999),
clampValue(yearTo, 1, 9999)
]);
};
return (
<WiredConditionBaseView hasSpecialInput={ true } requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_NONE } save={ save }>
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-2">
<Text bold>{ LocalizeText('wiredfurni.params.time.weekday_selection') }</Text>
<div className="flex flex-wrap gap-2">
{ WEEKDAY_OPTIONS.map(value =>
{
const checked = ((weekdayMask & (1 << value)) !== 0);
return (
<label key={ value } className="flex items-center gap-1">
<input checked={ checked } className="form-check-input" type="checkbox" onChange={ event => setWeekdayMask(toggleMaskValue(weekdayMask, value, event.target.checked)) } />
<Text>{ LocalizeText(`wiredfurni.params.time.weekday.${ value }`) }</Text>
</label>
);
}) }
</div>
</div>
<MatchDateSection
fromValue={ dayFrom }
max={ 31 }
min={ 1 }
mode={ dayMode }
sectionId="matchDateDay"
titleKey="wiredfurni.params.time.day_selection"
toValue={ dayTo }
onFromChange={ value => setDayFrom(clampValue(value, 1, 31)) }
onModeChange={ setDayMode }
onToChange={ value => setDayTo(clampValue(value, 1, 31)) } />
<div className="flex flex-col gap-2">
<Text bold>{ LocalizeText('wiredfurni.params.time.month_selection') }</Text>
<div className="flex flex-wrap gap-2">
{ MONTH_OPTIONS.map(value =>
{
const checked = ((monthMask & (1 << value)) !== 0);
return (
<label key={ value } className="flex items-center gap-1">
<input checked={ checked } className="form-check-input" type="checkbox" onChange={ event => setMonthMask(toggleMaskValue(monthMask, value, event.target.checked)) } />
<Text>{ LocalizeText(`wiredfurni.params.time.month.${ value }`) }</Text>
</label>
);
}) }
</div>
</div>
<MatchDateSection
fromValue={ yearFrom }
max={ 9999 }
min={ 1 }
mode={ yearMode }
sectionId="matchDateYear"
titleKey="wiredfurni.params.time.year_selection"
toValue={ yearTo }
onFromChange={ value => setYearFrom(clampValue(value, 1, 9999)) }
onModeChange={ setYearMode }
onToChange={ value => setYearTo(clampValue(value, 1, 9999)) } />
</div>
</WiredConditionBaseView>
);
};
@@ -0,0 +1,163 @@
import { ChangeEvent, FC, useEffect, useState } from 'react';
import { LocalizeText, WiredFurniType } from '../../../../api';
import { Text } from '../../../../common';
import { useWired } from '../../../../hooks';
import { WiredConditionBaseView } from './WiredConditionBaseView';
const MODE_SKIP = 0;
const MODE_EXACT = 1;
const MODE_RANGE = 2;
const MODE_OPTIONS = [ MODE_SKIP, MODE_EXACT, MODE_RANGE ];
const clampValue = (value: number, min: number, max: number) =>
{
if(isNaN(value)) return min;
return Math.max(min, Math.min(max, Math.floor(value)));
};
interface TimeFilterSectionProps
{
sectionId: string;
titleKey: string;
min: number;
max: number;
mode: number;
fromValue: number;
toValue: number;
onModeChange: (value: number) => void;
onFromChange: (value: number) => void;
onToChange: (value: number) => void;
}
const parseInputValue = (event: ChangeEvent<HTMLInputElement>, min: number, max: number) =>
{
return clampValue(parseInt(event.target.value || min.toString(), 10), min, max);
};
const InlineNumberInput: FC<{ value: number; min: number; max: number; onChange: (value: number) => void }> = props =>
{
const { value = 0, min = 0, max = 0, onChange = null } = props;
return (
<input
className="form-control form-control-sm text-center"
max={ max }
min={ min }
style={ { width: 56 } }
type="number"
value={ value }
onChange={ event => onChange(parseInputValue(event, min, max)) } />
);
};
const TimeFilterSection: FC<TimeFilterSectionProps> = props =>
{
const { sectionId = '', titleKey = '', min = 0, max = 0, mode = MODE_SKIP, fromValue = 0, toValue = 0, onModeChange = null, onFromChange = null, onToChange = null } = props;
return (
<div className="d-flex flex-column gap-2">
<Text bold>{ LocalizeText(titleKey) }</Text>
<div className="flex items-center gap-1">
<input checked={ (mode === MODE_SKIP) } className="form-check-input" id={ `${ sectionId }0` } name={ sectionId } type="radio" onChange={ () => onModeChange(MODE_SKIP) } />
<Text>{ LocalizeText('wiredfurni.params.time.skip') }</Text>
</div>
<div className="flex items-center gap-2">
<input checked={ (mode === MODE_EXACT) } className="form-check-input" id={ `${ sectionId }1` } name={ sectionId } type="radio" onChange={ () => onModeChange(MODE_EXACT) } />
<Text>{ LocalizeText('wiredfurni.params.time.exact') }</Text>
<InlineNumberInput max={ max } min={ min } value={ fromValue } onChange={ onFromChange } />
</div>
<div className="flex items-center gap-2">
<input checked={ (mode === MODE_RANGE) } className="form-check-input" id={ `${ sectionId }2` } name={ sectionId } type="radio" onChange={ () => onModeChange(MODE_RANGE) } />
<Text>{ LocalizeText('wiredfurni.params.time.range') }</Text>
<InlineNumberInput max={ max } min={ min } value={ fromValue } onChange={ onFromChange } />
<Text>-</Text>
<InlineNumberInput max={ max } min={ min } value={ toValue } onChange={ onToChange } />
</div>
</div>
);
};
export const WiredConditionMatchTimeView: FC<{}> = () =>
{
const { trigger = null, setIntParams = null } = useWired();
const [ hourMode, setHourMode ] = useState(MODE_SKIP);
const [ hourFrom, setHourFrom ] = useState(0);
const [ hourTo, setHourTo ] = useState(0);
const [ minuteMode, setMinuteMode ] = useState(MODE_SKIP);
const [ minuteFrom, setMinuteFrom ] = useState(0);
const [ minuteTo, setMinuteTo ] = useState(0);
const [ secondMode, setSecondMode ] = useState(MODE_SKIP);
const [ secondFrom, setSecondFrom ] = useState(0);
const [ secondTo, setSecondTo ] = useState(0);
useEffect(() =>
{
if(!trigger) return;
setHourMode(MODE_OPTIONS.includes(trigger.intData[0]) ? trigger.intData[0] : MODE_SKIP);
setHourFrom(clampValue(trigger.intData[1] ?? 0, 0, 23));
setHourTo(clampValue(trigger.intData[2] ?? 0, 0, 23));
setMinuteMode(MODE_OPTIONS.includes(trigger.intData[3]) ? trigger.intData[3] : MODE_SKIP);
setMinuteFrom(clampValue(trigger.intData[4] ?? 0, 0, 59));
setMinuteTo(clampValue(trigger.intData[5] ?? 0, 0, 59));
setSecondMode(MODE_OPTIONS.includes(trigger.intData[6]) ? trigger.intData[6] : MODE_SKIP);
setSecondFrom(clampValue(trigger.intData[7] ?? 0, 0, 59));
setSecondTo(clampValue(trigger.intData[8] ?? 0, 0, 59));
}, [ trigger ]);
const save = () =>
{
setIntParams([
hourMode,
clampValue(hourFrom, 0, 23),
clampValue(hourTo, 0, 23),
minuteMode,
clampValue(minuteFrom, 0, 59),
clampValue(minuteTo, 0, 59),
secondMode,
clampValue(secondFrom, 0, 59),
clampValue(secondTo, 0, 59)
]);
};
return (
<WiredConditionBaseView hasSpecialInput={ true } requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_NONE } save={ save }>
<div className="flex flex-col gap-3">
<TimeFilterSection
fromValue={ hourFrom }
max={ 23 }
min={ 0 }
mode={ hourMode }
sectionId="matchTimeHour"
titleKey="wiredfurni.params.time.hour_selection"
toValue={ hourTo }
onFromChange={ value => setHourFrom(clampValue(value, 0, 23)) }
onModeChange={ setHourMode }
onToChange={ value => setHourTo(clampValue(value, 0, 23)) } />
<TimeFilterSection
fromValue={ minuteFrom }
max={ 59 }
min={ 0 }
mode={ minuteMode }
sectionId="matchTimeMinute"
titleKey="wiredfurni.params.time.minute_selection"
toValue={ minuteTo }
onFromChange={ value => setMinuteFrom(clampValue(value, 0, 59)) }
onModeChange={ setMinuteMode }
onToChange={ value => setMinuteTo(clampValue(value, 0, 59)) } />
<TimeFilterSection
fromValue={ secondFrom }
max={ 59 }
min={ 0 }
mode={ secondMode }
sectionId="matchTimeSecond"
titleKey="wiredfurni.params.time.second_selection"
toValue={ secondTo }
onFromChange={ value => setSecondFrom(clampValue(value, 0, 59)) }
onModeChange={ setSecondMode }
onToChange={ value => setSecondTo(clampValue(value, 0, 59)) } />
</div>
</WiredConditionBaseView>
);
};
@@ -0,0 +1,102 @@
import { FC, useEffect, useState } from 'react';
import { LocalizeText, WiredFurniType } from '../../../../api';
import { Text } from '../../../../common';
import { useWired } from '../../../../hooks';
import { WiredSourcesSelector } from '../WiredSourcesSelector';
import { WiredConditionBaseView } from './WiredConditionBaseView';
const TEAM_OPTIONS = [ 0, 1, 2, 3, 4 ];
const PLACEMENT_OPTIONS = [ 1, 2, 3, 4 ];
export const WiredConditionTeamHasRankView: FC<{}> = () =>
{
const { trigger = null, setIntParams = null } = useWired();
const [ team, setTeam ] = useState(1);
const [ placement, setPlacement ] = useState(1);
const [ userSource, setUserSource ] = useState(0);
const [ quantifier, setQuantifier ] = useState(0);
const [ showAdvanced, setShowAdvanced ] = useState(false);
useEffect(() =>
{
if(!trigger) return;
const nextTeam = (trigger.intData.length > 0) ? trigger.intData[0] : 1;
const nextPlacement = (trigger.intData.length > 1) ? trigger.intData[1] : 1;
const nextUserSource = (trigger.intData.length > 2) ? trigger.intData[2] : 0;
const nextQuantifier = (trigger.intData.length > 3) ? trigger.intData[3] : 0;
setTeam(TEAM_OPTIONS.includes(nextTeam) ? nextTeam : 1);
setPlacement(PLACEMENT_OPTIONS.includes(nextPlacement) ? nextPlacement : 1);
setUserSource(nextUserSource);
setQuantifier((nextQuantifier === 1) ? 1 : 0);
setShowAdvanced(nextUserSource !== 0 || nextQuantifier !== 0);
}, [ trigger ]);
const save = () =>
{
setIntParams([
team,
placement,
userSource,
quantifier
]);
};
return (
<WiredConditionBaseView
hasSpecialInput={ true }
requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_NONE }
save={ save }
footer={
<div className="flex flex-col gap-2">
<button className="btn btn-link p-0 align-self-start" type="button" onClick={ () => setShowAdvanced(value => !value) }>
{ LocalizeText(showAdvanced ? 'wiredfurni.params.sources.collapse' : 'wiredfurni.params.sources.expand') }
</button>
{ showAdvanced &&
<>
<div className="flex flex-col gap-1">
<Text bold>{ LocalizeText('wiredfurni.params.quantifier_selection') }</Text>
{ [ 0, 1 ].map(value =>
{
return (
<div key={ value } className="flex items-center gap-1">
<input checked={ (quantifier === value) } className="form-check-input" id={ `teamRankQuantifier${ value }` } name="teamRankQuantifier" type="radio" onChange={ () => setQuantifier(value) } />
<Text>{ LocalizeText(`wiredfurni.params.quantifier.users.${ value }`) }</Text>
</div>
);
}) }
</div>
<WiredSourcesSelector showUsers={ true } userSource={ userSource } onChangeUsers={ setUserSource } />
</> }
</div>
}>
<div className="flex flex-col gap-1">
<Text bold>{ LocalizeText('wiredfurni.params.team') }</Text>
{ TEAM_OPTIONS.map(value =>
{
const labelKey = (value === 0) ? 'wiredfurni.params.team.triggerer' : `wiredfurni.params.team.${ value }`;
return (
<div key={ value } className="flex items-center gap-1">
<input checked={ (team === value) } className="form-check-input" id={ `teamHasRank${ value }` } name="teamHasRank" type="radio" onChange={ () => setTeam(value) } />
<Text>{ LocalizeText(labelKey) }</Text>
</div>
);
}) }
</div>
<div className="flex flex-col gap-1">
<Text bold>{ LocalizeText('wiredfurni.params.placement_selection') }</Text>
{ PLACEMENT_OPTIONS.map(value =>
{
return (
<div key={ value } className="flex items-center gap-1">
<input checked={ (placement === value) } className="form-check-input" id={ `teamRankPlacement${ value }` } name="teamRankPlacement" type="radio" onChange={ () => setPlacement(value) } />
<Text>{ LocalizeText(`wiredfurni.params.placement.${ value }`) }</Text>
</div>
);
}) }
</div>
</WiredConditionBaseView>
);
};
@@ -0,0 +1,158 @@
import { FC, useEffect, useState } from 'react';
import { LocalizeText, WiredFurniType } from '../../../../api';
import { Slider, Text } from '../../../../common';
import { useWired } from '../../../../hooks';
import { WiredSourcesSelector } from '../WiredSourcesSelector';
import { WiredConditionBaseView } from './WiredConditionBaseView';
const TEAM_OPTIONS = [ 1, 2, 3, 4 ];
const COMPARISON_OPTIONS = [ 0, 1, 2 ];
const MIN_SCORE = 0;
const MAX_SCORE = 999;
const SCORE_PATTERN = /^\d*$/;
const clampScore = (value: number) =>
{
if(isNaN(value)) return MIN_SCORE;
return Math.max(MIN_SCORE, Math.min(MAX_SCORE, Math.floor(value)));
};
export const WiredConditionTeamHasScoreView: FC<{}> = () =>
{
const { trigger = null, setIntParams = null } = useWired();
const [ team, setTeam ] = useState(1);
const [ comparison, setComparison ] = useState(1);
const [ score, setScore ] = useState(0);
const [ scoreInput, setScoreInput ] = useState('0');
const [ userSource, setUserSource ] = useState(0);
const [ quantifier, setQuantifier ] = useState(0);
const [ showAdvanced, setShowAdvanced ] = useState(false);
useEffect(() =>
{
if(!trigger) return;
const nextTeam = (trigger.intData.length > 0) ? trigger.intData[0] : 1;
const nextComparison = (trigger.intData.length > 1) ? trigger.intData[1] : 1;
const nextScore = clampScore((trigger.intData.length > 2) ? trigger.intData[2] : 0);
const nextUserSource = (trigger.intData.length > 3) ? trigger.intData[3] : 0;
const nextQuantifier = (trigger.intData.length > 4) ? trigger.intData[4] : 0;
setTeam(TEAM_OPTIONS.includes(nextTeam) ? nextTeam : 1);
setComparison(COMPARISON_OPTIONS.includes(nextComparison) ? nextComparison : 1);
setScore(nextScore);
setScoreInput(nextScore.toString());
setUserSource(nextUserSource);
setQuantifier((nextQuantifier === 1) ? 1 : 0);
setShowAdvanced(nextUserSource !== 0 || nextQuantifier !== 0);
}, [ trigger ]);
const updateScore = (value: number) =>
{
const nextValue = clampScore(value);
setScore(nextValue);
setScoreInput(nextValue.toString());
};
const updateScoreInput = (value: string) =>
{
if(!SCORE_PATTERN.test(value)) return;
setScoreInput(value);
if(!value.length)
{
setScore(0);
return;
}
updateScore(parseInt(value));
};
const save = () =>
{
setIntParams([
team,
comparison,
clampScore(score),
userSource,
quantifier
]);
};
return (
<WiredConditionBaseView
hasSpecialInput={ true }
requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_NONE }
save={ save }
footer={
<div className="flex flex-col gap-2">
<button className="btn btn-link p-0 align-self-start" type="button" onClick={ () => setShowAdvanced(value => !value) }>
{ LocalizeText(showAdvanced ? 'wiredfurni.params.sources.collapse' : 'wiredfurni.params.sources.expand') }
</button>
{ showAdvanced &&
<>
<div className="flex flex-col gap-1">
<Text bold>{ LocalizeText('wiredfurni.params.quantifier_selection') }</Text>
{ [ 0, 1 ].map(value =>
{
return (
<div key={ value } className="flex items-center gap-1">
<input checked={ (quantifier === value) } className="form-check-input" id={ `teamScoreQuantifier${ value }` } name="teamScoreQuantifier" type="radio" onChange={ () => setQuantifier(value) } />
<Text>{ LocalizeText(`wiredfurni.params.quantifier.users.${ value }`) }</Text>
</div>
);
}) }
</div>
<WiredSourcesSelector showUsers={ true } userSource={ userSource } onChangeUsers={ setUserSource } />
</> }
</div>
}>
<div className="flex flex-col gap-1">
<Text bold>{ LocalizeText('wiredfurni.params.team') }</Text>
{ TEAM_OPTIONS.map(value =>
{
return (
<div key={ value } className="flex items-center gap-1">
<input checked={ (team === value) } className="form-check-input" id={ `teamHasScore${ value }` } name="teamHasScore" type="radio" onChange={ () => setTeam(value) } />
<Text>{ LocalizeText(`wiredfurni.params.team.${ value }`) }</Text>
</div>
);
}) }
</div>
<div className="flex flex-col gap-1">
<Text bold>{ LocalizeText('wiredfurni.params.comparison_selection') }</Text>
{ COMPARISON_OPTIONS.map(value =>
{
return (
<div key={ value } className="flex items-center gap-1">
<input checked={ (comparison === value) } className="form-check-input" id={ `teamScoreComparison${ value }` } name="teamScoreComparison" type="radio" onChange={ () => setComparison(value) } />
<Text>{ LocalizeText(`wiredfurni.params.comparison.${ value }`) }</Text>
</div>
);
}) }
</div>
<div className="flex flex-col gap-1">
<Text bold>{ LocalizeText('wiredfurni.params.setscore2') }</Text>
<input
className="form-control form-control-sm"
inputMode="numeric"
type="text"
value={ scoreInput }
onBlur={ () => setScoreInput(clampScore(score).toString()) }
onChange={ event => updateScoreInput(event.target.value) } />
</div>
<div className="flex flex-col gap-1">
<Slider
max={ MAX_SCORE }
min={ MIN_SCORE }
step={ 1 }
value={ score }
onChange={ event => updateScore(event as number) } />
<Text small>{ score }</Text>
</div>
</WiredConditionBaseView>
);
};
@@ -0,0 +1,130 @@
import { FC, useEffect, useState } from 'react';
import { LocalizeText, WiredFurniType } from '../../../../api';
import { Text } from '../../../../common';
import { useWired } from '../../../../hooks';
import { NitroInput } from '../../../../layout';
import { WiredSourceOption, WiredSourcesSelector } from '../WiredSourcesSelector';
import { WiredConditionBaseView } from './WiredConditionBaseView';
const ENTITY_HABBO = 1;
const ENTITY_PET = 2;
const ENTITY_BOT = 4;
const AVATAR_MODE_ANY = 0;
const AVATAR_MODE_CERTAIN = 1;
const SOURCE_SPECIFIED_USERNAME = 101;
const MATCH_USER_SOURCES: WiredSourceOption[] = [
{ value: 0, label: 'wiredfurni.params.sources.users.0' },
{ value: 200, label: 'wiredfurni.params.sources.users.200' },
{ value: 201, label: 'wiredfurni.params.sources.users.201' }
];
const COMPARE_USER_SOURCES: WiredSourceOption[] = [
...MATCH_USER_SOURCES,
{ value: SOURCE_SPECIFIED_USERNAME, label: 'wiredfurni.params.sources.users.101' }
];
export const WiredConditionTriggererMatchView: FC<{}> = () =>
{
const [ entityType, setEntityType ] = useState(ENTITY_HABBO);
const [ avatarMode, setAvatarMode ] = useState(AVATAR_MODE_ANY);
const [ username, setUsername ] = useState('');
const [ matchUserSource, setMatchUserSource ] = useState(0);
const [ compareUserSource, setCompareUserSource ] = useState(0);
const [ quantifier, setQuantifier ] = useState(0);
const [ showAdvanced, setShowAdvanced ] = useState(false);
const { trigger = null, setIntParams = null, setStringParam = null } = useWired();
const needsUsername = (avatarMode === AVATAR_MODE_CERTAIN) || (compareUserSource === SOURCE_SPECIFIED_USERNAME);
const save = () =>
{
setIntParams([
entityType,
avatarMode,
matchUserSource,
compareUserSource,
quantifier
]);
setStringParam(username);
};
useEffect(() =>
{
if(!trigger) return;
setEntityType((trigger.intData.length > 0) ? trigger.intData[0] : ENTITY_HABBO);
setAvatarMode((trigger.intData.length > 1) ? trigger.intData[1] : AVATAR_MODE_ANY);
setMatchUserSource((trigger.intData.length > 2) ? trigger.intData[2] : 0);
setCompareUserSource((trigger.intData.length > 3) ? trigger.intData[3] : 0);
setQuantifier((trigger.intData.length > 4) ? trigger.intData[4] : 0);
setUsername(trigger.stringData || '');
setShowAdvanced((trigger.intData.length > 2) ? (trigger.intData[2] !== 0 || trigger.intData[3] !== 0 || trigger.intData[4] !== 0) : false);
}, [ trigger ]);
return (
<WiredConditionBaseView
hasSpecialInput={ true }
requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_NONE }
save={ save }
footer={
<div className="flex flex-col gap-2">
<button className="btn btn-link p-0 align-self-start" type="button" onClick={ () => setShowAdvanced(value => !value) }>
{ LocalizeText(showAdvanced ? 'wiredfurni.params.sources.collapse' : 'wiredfurni.params.sources.expand') }
</button>
{ showAdvanced &&
<>
<div className="flex flex-col gap-1">
<Text bold>{ LocalizeText('wiredfurni.params.quantifier_selection') }</Text>
{ [ 0, 1 ].map(value =>
{
return (
<div key={ value } className="flex items-center gap-1">
<input checked={ (quantifier === value) } className="form-check-input" id={ `triggererMatchQuantifier${ value }` } name="triggererMatchQuantifier" type="radio" onChange={ () => setQuantifier(value) } />
<Text>{ LocalizeText(`wiredfurni.params.quantifier.users.${ value }`) }</Text>
</div>
);
}) }
</div>
<WiredSourcesSelector
showUsers={ true }
userSource={ matchUserSource }
userSources={ MATCH_USER_SOURCES }
usersTitle="wiredfurni.params.sources.users.title.match.0"
onChangeUsers={ setMatchUserSource } />
<WiredSourcesSelector
showUsers={ true }
userSource={ compareUserSource }
userSources={ COMPARE_USER_SOURCES }
usersTitle="wiredfurni.params.sources.users.title.match.1"
onChangeUsers={ setCompareUserSource } />
</> }
</div>
}>
<div className="flex flex-col gap-2">
{ [ ENTITY_HABBO, ENTITY_PET, ENTITY_BOT ].map(value =>
{
return (
<div key={ value } className="flex items-center gap-1">
<input checked={ (entityType === value) } className="form-check-input" id={ `triggererEntityType${ value }` } name="triggererEntityType" type="radio" onChange={ () => setEntityType(value) } />
<Text>{ LocalizeText(`wiredfurni.params.usertype.${ value }`) }</Text>
</div>
);
}) }
</div>
<div className="flex flex-col gap-1">
<Text bold>{ LocalizeText('wiredfurni.params.picktriggerer') }</Text>
<div className="flex items-center gap-1">
<input checked={ (avatarMode === AVATAR_MODE_ANY) } className="form-check-input" id="triggererAvatarMode0" name="triggererAvatarMode" type="radio" onChange={ () => setAvatarMode(AVATAR_MODE_ANY) } />
<Text>{ LocalizeText('wiredfurni.params.anyavatar') }</Text>
</div>
<div className="flex items-center gap-1">
<input checked={ (avatarMode === AVATAR_MODE_CERTAIN) } className="form-check-input" id="triggererAvatarMode1" name="triggererAvatarMode" type="radio" onChange={ () => setAvatarMode(AVATAR_MODE_CERTAIN) } />
<Text>{ LocalizeText('wiredfurni.params.certainavatar') }</Text>
</div>
{ needsUsername &&
<NitroInput type="text" value={ username } onChange={ event => setUsername(event.target.value) } /> }
</div>
</WiredConditionBaseView>
);
};
@@ -0,0 +1,151 @@
import { FC, useEffect, useState } from 'react';
import { LocalizeText, WiredFurniType } from '../../../../api';
import { Text } from '../../../../common';
import { useWired } from '../../../../hooks';
import { WiredSourceOption, WiredSourcesSelector } from '../WiredSourcesSelector';
import { WiredConditionBaseView } from './WiredConditionBaseView';
const ACTION_WAVE = 1;
const ACTION_BLOW_KISS = 2;
const ACTION_LAUGH = 3;
const ACTION_AWAKE = 4;
const ACTION_RELAX = 5;
const ACTION_SIT = 6;
const ACTION_STAND = 7;
const ACTION_LAY = 8;
const ACTION_SIGN = 9;
const ACTION_DANCE = 10;
const ACTION_THUMB_UP = 11;
const ACTION_OPTIONS = [
{ value: ACTION_WAVE, label: 'widget.memenu.wave' },
{ value: ACTION_BLOW_KISS, label: 'widget.memenu.blow' },
{ value: ACTION_LAUGH, label: 'widget.memenu.laugh' },
{ value: ACTION_THUMB_UP, label: 'widget.memenu.thumb' },
{ value: ACTION_AWAKE, label: 'wiredfurni.params.action.4' },
{ value: ACTION_RELAX, label: 'avatar.widget.random_walk' },
{ value: ACTION_SIT, label: 'widget.memenu.sit' },
{ value: ACTION_STAND, label: 'widget.memenu.stand' },
{ value: ACTION_LAY, label: 'wiredfurni.params.action.8' },
{ value: ACTION_SIGN, label: 'widget.memenu.sign' },
{ value: ACTION_DANCE, label: 'widget.memenu.dance' }
];
const SIGN_OPTIONS = Array.from({ length: 18 }, (_, value) => ({
value,
label: `wiredfurni.params.action.sign.${ value }`
}));
const DANCE_OPTIONS = [
{ value: 1, label: 'widget.memenu.dance1' },
{ value: 2, label: 'widget.memenu.dance2' },
{ value: 3, label: 'widget.memenu.dance3' },
{ value: 4, label: 'widget.memenu.dance4' }
];
const USER_ACTION_SOURCES: WiredSourceOption[] = [
{ value: 0, label: 'wiredfurni.params.sources.users.0' },
{ value: 200, label: 'wiredfurni.params.sources.users.200' },
{ value: 201, label: 'wiredfurni.params.sources.users.201' }
];
interface WiredConditionUserPerformsActionViewProps
{
negative?: boolean;
}
export const WiredConditionUserPerformsActionView: FC<WiredConditionUserPerformsActionViewProps> = props =>
{
const { negative = false } = props;
const [ selectedAction, setSelectedAction ] = useState(ACTION_WAVE);
const [ signFilterEnabled, setSignFilterEnabled ] = useState(false);
const [ signId, setSignId ] = useState(0);
const [ danceFilterEnabled, setDanceFilterEnabled ] = useState(false);
const [ danceId, setDanceId ] = useState(1);
const [ userSource, setUserSource ] = useState(0);
const [ quantifier, setQuantifier ] = useState(0);
const [ showAdvanced, setShowAdvanced ] = useState(false);
const { trigger = null, setIntParams = null } = useWired();
const quantifierKeyPrefix = negative ? 'wiredfurni.params.quantifier.users.neg' : 'wiredfurni.params.quantifier.users';
const save = () => setIntParams([
selectedAction,
signFilterEnabled ? 1 : 0,
signId,
danceFilterEnabled ? 1 : 0,
danceId,
userSource,
quantifier
]);
useEffect(() =>
{
setSelectedAction((trigger?.intData?.length > 0) ? trigger.intData[0] : ACTION_WAVE);
setSignFilterEnabled((trigger?.intData?.length > 1) ? (trigger.intData[1] === 1) : false);
setSignId((trigger?.intData?.length > 2) ? trigger.intData[2] : 0);
setDanceFilterEnabled((trigger?.intData?.length > 3) ? (trigger.intData[3] === 1) : false);
setDanceId((trigger?.intData?.length > 4) ? trigger.intData[4] : 1);
setUserSource((trigger?.intData?.length > 5) ? trigger.intData[5] : 0);
setQuantifier((trigger?.intData?.length > 6) ? trigger.intData[6] : 0);
setShowAdvanced((trigger?.intData?.length > 5) ? (trigger.intData[5] !== 0 || trigger.intData[6] !== 0) : false);
}, [ trigger ]);
return (
<WiredConditionBaseView
hasSpecialInput={ true }
requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_NONE }
save={ save }
footer={
<div className="flex flex-col gap-2">
<button className="btn btn-link p-0 align-self-start" type="button" onClick={ () => setShowAdvanced(value => !value) }>
{ LocalizeText(showAdvanced ? 'wiredfurni.params.sources.collapse' : 'wiredfurni.params.sources.expand') }
</button>
{ showAdvanced &&
<>
<div className="flex flex-col gap-1">
<Text bold>{ LocalizeText('wiredfurni.params.quantifier_selection') }</Text>
{ [ 0, 1 ].map(value =>
{
return (
<div key={ value } className="flex items-center gap-1">
<input checked={ (quantifier === value) } className="form-check-input" id={ `userActionQuantifier${ value }` } name="userActionQuantifier" type="radio" onChange={ () => setQuantifier(value) } />
<Text>{ LocalizeText(`${ quantifierKeyPrefix }.${ value }`) }</Text>
</div>
);
}) }
</div>
<WiredSourcesSelector showUsers={ true } userSource={ userSource } userSources={ USER_ACTION_SOURCES } onChangeUsers={ setUserSource } />
</> }
</div>
}>
<div className="flex flex-col gap-1">
<Text bold>Action</Text>
<select className="form-select form-select-sm" value={ selectedAction } onChange={ event => setSelectedAction(parseInt(event.target.value)) }>
{ ACTION_OPTIONS.map(option => <option key={ option.value } value={ option.value }>{ LocalizeText(option.label) }</option>) }
</select>
</div>
{ (selectedAction === ACTION_SIGN) &&
<div className="flex flex-col gap-1">
<div className="flex items-center gap-1">
<input checked={ signFilterEnabled } className="form-check-input" id="conditionSignFilterEnabled" type="checkbox" onChange={ event => setSignFilterEnabled(event.target.checked) } />
<Text>{ LocalizeText('wiredfurni.params.sign_filter') }</Text>
</div>
{ signFilterEnabled &&
<select className="form-select form-select-sm" value={ signId } onChange={ event => setSignId(parseInt(event.target.value)) }>
{ SIGN_OPTIONS.map(option => <option key={ option.value } value={ option.value }>{ LocalizeText(option.label) }</option>) }
</select> }
</div> }
{ (selectedAction === ACTION_DANCE) &&
<div className="flex flex-col gap-1">
<div className="flex items-center gap-1">
<input checked={ danceFilterEnabled } className="form-check-input" id="conditionDanceFilterEnabled" type="checkbox" onChange={ event => setDanceFilterEnabled(event.target.checked) } />
<Text>{ LocalizeText('wiredfurni.params.dance_filter') }</Text>
</div>
{ danceFilterEnabled &&
<select className="form-select form-select-sm" value={ danceId } onChange={ event => setDanceId(parseInt(event.target.value)) }>
{ DANCE_OPTIONS.map(option => <option key={ option.value } value={ option.value }>{ LocalizeText(option.label) }</option>) }
</select> }
</div> }
</WiredConditionBaseView>
);
};
@@ -0,0 +1,39 @@
import { FC, useEffect, useState } from 'react';
import { LocalizeText, WiredFurniType } from '../../../../api';
import { Text } from '../../../../common';
import { useWired } from '../../../../hooks';
import { NitroInput } from '../../../../layout';
import { WiredTriggerBaseView } from './WiredTriggerBaseView';
export const WiredTriggerAvatarLeaveRoomView: FC<{}> = props =>
{
const [ username, setUsername ] = useState('');
const [ avatarMode, setAvatarMode ] = useState(0);
const { trigger = null, setStringParam = null } = useWired();
const save = () => setStringParam((avatarMode === 1) ? username : '');
useEffect(() =>
{
setUsername(trigger.stringData);
setAvatarMode(trigger.stringData ? 1 : 0);
}, [ trigger ]);
return (
<WiredTriggerBaseView hasSpecialInput={ true } requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_NONE } save={ save }>
<div className="flex flex-col gap-1">
<Text bold>{ LocalizeText('wiredfurni.params.picktriggerer') }</Text>
<div className="flex items-center gap-1">
<input checked={ (avatarMode === 0) } className="form-check-input" id="avatarMode0" name="avatarMode" type="radio" onChange={ event => setAvatarMode(0) } />
<Text>{ LocalizeText('wiredfurni.params.anyavatar') }</Text>
</div>
<div className="flex items-center gap-1">
<input checked={ (avatarMode === 1) } className="form-check-input" id="avatarMode1" name="avatarMode" type="radio" onChange={ event => setAvatarMode(1) } />
<Text>{ LocalizeText('wiredfurni.params.certainavatar') }</Text>
</div>
{ (avatarMode === 1) &&
<NitroInput type="text" value={ username } onChange={ event => setUsername(event.target.value) } /> }
</div>
</WiredTriggerBaseView>
);
};
@@ -0,0 +1,8 @@
import { FC } from 'react';
import { WiredFurniType } from '../../../../api';
import { WiredTriggerBaseView } from './WiredTriggerBaseView';
export const WiredTriggerClickFurniView: FC<{}> = () =>
{
return <WiredTriggerBaseView hasSpecialInput={ false } requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_OR_BY_TYPE } save={ null } />;
};
@@ -0,0 +1,20 @@
import { FC, useEffect } from 'react';
import { WiredFurniType } from '../../../../api';
import { useWired } from '../../../../hooks';
import { WiredTriggerBaseView } from './WiredTriggerBaseView';
const CLICK_TILE_INTERACTION_TYPES = [ 'room_invisible_click_tile' ];
export const WiredTriggerClickTileView: FC<{}> = () =>
{
const { setAllowedInteractionTypes } = useWired();
useEffect(() =>
{
setAllowedInteractionTypes(CLICK_TILE_INTERACTION_TYPES);
return () => setAllowedInteractionTypes(null);
}, [ setAllowedInteractionTypes ]);
return <WiredTriggerBaseView hasSpecialInput={ false } requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_OR_BY_TYPE } save={ null } />;
};
@@ -0,0 +1,8 @@
import { FC } from 'react';
import { WiredFurniType } from '../../../../api';
import { WiredTriggerBaseView } from './WiredTriggerBaseView';
export const WiredTriggerClickUserView: FC<{}> = () =>
{
return <WiredTriggerBaseView hasSpecialInput={ false } requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_NONE } save={ null } />;
};
@@ -0,0 +1,32 @@
import { FC, useEffect, useState } from 'react';
import { LocalizeText, WiredFurniType } from '../../../../api';
import { Slider, Text } from '../../../../common';
import { useWired } from '../../../../hooks';
import { WiredTriggerBaseView } from './WiredTriggerBaseView';
export const WiredTriggeExecutePeriodicallyShortView: FC<{}> = () =>
{
const [ time, setTime ] = useState(10);
const { trigger = null, setIntParams = null } = useWired();
const save = () => setIntParams([ time ]);
useEffect(() =>
{
setTime((trigger.intData.length > 0) ? trigger.intData[0] : 10);
}, [ trigger ]);
return (
<WiredTriggerBaseView hasSpecialInput={ true } requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_NONE } save={ save }>
<div className="flex flex-col gap-1">
<Text bold>{ LocalizeText('wiredfurni.params.settime', [ 'seconds' ], [ ((time * 50) / 1000).toFixed(2) ]) }</Text>
<Text small>{ `${ time * 50 } ms` }</Text>
<Slider
max={ 10 }
min={ 1 }
value={ time }
onChange={ event => setTime(event) } />
</div>
</WiredTriggerBaseView>
);
};
@@ -1,14 +1,20 @@
import { WiredTriggerLayout } from '../../../../api';
import { WiredTriggerAvatarEnterRoomView } from './WiredTriggerAvatarEnterRoomView';
import { WiredTriggerAvatarLeaveRoomView } from './WiredTriggerAvatarLeaveRoomView';
import { WiredTriggerAvatarSaysSomethingView } from './WiredTriggerAvatarSaysSomethingView';
import { WiredTriggerAvatarWalksOffFurniView } from './WiredTriggerAvatarWalksOffFurniView';
import { WiredTriggerAvatarWalksOnFurniView } from './WiredTriggerAvatarWalksOnFurni';
import { WiredTriggerBotReachedAvatarView } from './WiredTriggerBotReachedAvatarView';
import { WiredTriggerBotReachedStuffView } from './WiredTriggerBotReachedStuffView';
import { WiredTriggerClickFurniView } from './WiredTriggerClickFurniView';
import { WiredTriggerClickTileView } from './WiredTriggerClickTileView';
import { WiredTriggerClickUserView } from './WiredTriggerClickUserView';
import { WiredTriggerCollisionView } from './WiredTriggerCollisionView';
import { WiredTriggerUserPerformsActionView } from './WiredTriggerUserPerformsActionView';
import { WiredTriggeExecuteOnceView } from './WiredTriggerExecuteOnceView';
import { WiredTriggeExecutePeriodicallyLongView } from './WiredTriggerExecutePeriodicallyLongView';
import { WiredTriggeExecutePeriodicallyView } from './WiredTriggerExecutePeriodicallyView';
import { WiredTriggeExecutePeriodicallyShortView } from './WiredTriggerExecutePeriodicallyShortView';
import { WiredTriggerGameEndsView } from './WiredTriggerGameEndsView';
import { WiredTriggerGameStartsView } from './WiredTriggerGameStartsView';
import { WiredTriggeScoreAchievedView } from './WiredTriggerScoreAchievedView';
@@ -21,6 +27,8 @@ export const WiredTriggerLayoutView = (code: number) =>
{
case WiredTriggerLayout.AVATAR_ENTERS_ROOM:
return <WiredTriggerAvatarEnterRoomView />;
case WiredTriggerLayout.AVATAR_LEAVES_ROOM:
return <WiredTriggerAvatarLeaveRoomView />;
case WiredTriggerLayout.AVATAR_SAYS_SOMETHING:
return <WiredTriggerAvatarSaysSomethingView />;
case WiredTriggerLayout.AVATAR_WALKS_OFF_FURNI:
@@ -31,12 +39,22 @@ export const WiredTriggerLayoutView = (code: number) =>
return <WiredTriggerBotReachedAvatarView />;
case WiredTriggerLayout.BOT_REACHED_STUFF:
return <WiredTriggerBotReachedStuffView />;
case WiredTriggerLayout.CLICK_FURNI:
return <WiredTriggerClickFurniView />;
case WiredTriggerLayout.CLICK_TILE:
return <WiredTriggerClickTileView />;
case WiredTriggerLayout.CLICK_USER:
return <WiredTriggerClickUserView />;
case WiredTriggerLayout.USER_PERFORMS_ACTION:
return <WiredTriggerUserPerformsActionView />;
case WiredTriggerLayout.COLLISION:
return <WiredTriggerCollisionView />;
case WiredTriggerLayout.EXECUTE_ONCE:
return <WiredTriggeExecuteOnceView />;
case WiredTriggerLayout.EXECUTE_PERIODICALLY:
return <WiredTriggeExecutePeriodicallyView />;
case WiredTriggerLayout.EXECUTE_PERIODICALLY_SHORT:
return <WiredTriggeExecutePeriodicallyShortView />;
case WiredTriggerLayout.EXECUTE_PERIODICALLY_LONG:
return <WiredTriggeExecutePeriodicallyLongView />;
case WiredTriggerLayout.GAME_ENDS:
@@ -1,8 +1,34 @@
import { FC } from 'react';
import { WiredFurniType } from '../../../../api';
import { FC, useEffect, useState } from 'react';
import { LocalizeText, WiredFurniType } from '../../../../api';
import { Text } from '../../../../common';
import { useWired } from '../../../../hooks';
import { WiredTriggerBaseView } from './WiredTriggerBaseView';
export const WiredTriggerToggleFurniView: FC<{}> = props =>
export const WiredTriggerToggleFurniView: FC<{}> = () =>
{
return <WiredTriggerBaseView hasSpecialInput={ false } requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_BY_ID_OR_BY_TYPE } save={ null } />;
const [ triggerMode, setTriggerMode ] = useState(0);
const { trigger = null, setIntParams = null } = useWired();
const save = () => setIntParams([ triggerMode ]);
useEffect(() =>
{
setTriggerMode((trigger?.intData?.length > 0) ? trigger.intData[0] : 0);
}, [ trigger ]);
return (
<WiredTriggerBaseView hasSpecialInput={ true } requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_BY_ID } save={ save }>
<div className="flex flex-col gap-1">
<Text bold>{ LocalizeText('wiredfurni.params.condition.state') }</Text>
<div className="flex items-center gap-1">
<input checked={ (triggerMode === 1) } className="form-check-input" id="stateTrigger1" name="stateTrigger" type="radio" onChange={ () => setTriggerMode(1) } />
<Text>{ LocalizeText('wiredfurni.params.state_trigger.1') }</Text>
</div>
<div className="flex items-center gap-1">
<input checked={ (triggerMode === 0) } className="form-check-input" id="stateTrigger0" name="stateTrigger" type="radio" onChange={ () => setTriggerMode(0) } />
<Text>{ LocalizeText('wiredfurni.params.state_trigger.0') }</Text>
</div>
</div>
</WiredTriggerBaseView>
);
};
@@ -0,0 +1,103 @@
import { FC, useEffect, useState } from 'react';
import { LocalizeText, WiredFurniType } from '../../../../api';
import { Text } from '../../../../common';
import { useWired } from '../../../../hooks';
import { WiredTriggerBaseView } from './WiredTriggerBaseView';
const ACTION_WAVE = 1;
const ACTION_BLOW_KISS = 2;
const ACTION_LAUGH = 3;
const ACTION_AWAKE = 4;
const ACTION_RELAX = 5;
const ACTION_SIT = 6;
const ACTION_STAND = 7;
const ACTION_LAY = 8;
const ACTION_SIGN = 9;
const ACTION_DANCE = 10;
const ACTION_THUMB_UP = 11;
const ACTION_OPTIONS = [
{ value: ACTION_WAVE, label: 'widget.memenu.wave' },
{ value: ACTION_BLOW_KISS, label: 'widget.memenu.blow' },
{ value: ACTION_LAUGH, label: 'widget.memenu.laugh' },
{ value: ACTION_THUMB_UP, label: 'widget.memenu.thumb' },
{ value: ACTION_AWAKE, label: 'wiredfurni.params.action.4' },
{ value: ACTION_RELAX, label: 'avatar.widget.random_walk' },
{ value: ACTION_SIT, label: 'widget.memenu.sit' },
{ value: ACTION_STAND, label: 'widget.memenu.stand' },
{ value: ACTION_LAY, label: 'wiredfurni.params.action.8' },
{ value: ACTION_SIGN, label: 'widget.memenu.sign' },
{ value: ACTION_DANCE, label: 'widget.memenu.dance' }
];
const SIGN_OPTIONS = Array.from({ length: 18 }, (_, value) => ({
value,
label: `wiredfurni.params.action.sign.${ value }`
}));
const DANCE_OPTIONS = [
{ value: 1, label: 'widget.memenu.dance1' },
{ value: 2, label: 'widget.memenu.dance2' },
{ value: 3, label: 'widget.memenu.dance3' },
{ value: 4, label: 'widget.memenu.dance4' }
];
export const WiredTriggerUserPerformsActionView: FC<{}> = () =>
{
const [ selectedAction, setSelectedAction ] = useState(ACTION_WAVE);
const [ signFilterEnabled, setSignFilterEnabled ] = useState(false);
const [ signId, setSignId ] = useState(0);
const [ danceFilterEnabled, setDanceFilterEnabled ] = useState(false);
const [ danceId, setDanceId ] = useState(1);
const { trigger = null, setIntParams = null } = useWired();
const save = () => setIntParams([
selectedAction,
signFilterEnabled ? 1 : 0,
signId,
danceFilterEnabled ? 1 : 0,
danceId
]);
useEffect(() =>
{
setSelectedAction((trigger?.intData?.length > 0) ? trigger.intData[0] : ACTION_WAVE);
setSignFilterEnabled((trigger?.intData?.length > 1) ? (trigger.intData[1] === 1) : false);
setSignId((trigger?.intData?.length > 2) ? trigger.intData[2] : 0);
setDanceFilterEnabled((trigger?.intData?.length > 3) ? (trigger.intData[3] === 1) : false);
setDanceId((trigger?.intData?.length > 4) ? trigger.intData[4] : 1);
}, [ trigger ]);
return (
<WiredTriggerBaseView hasSpecialInput={ true } requiresFurni={ WiredFurniType.STUFF_SELECTION_OPTION_NONE } save={ save }>
<div className="flex flex-col gap-1">
<Text bold>Action</Text>
<select className="form-select form-select-sm" value={ selectedAction } onChange={ event => setSelectedAction(parseInt(event.target.value)) }>
{ ACTION_OPTIONS.map(option => <option key={ option.value } value={ option.value }>{ LocalizeText(option.label) }</option>) }
</select>
</div>
{ (selectedAction === ACTION_SIGN) &&
<div className="flex flex-col gap-1">
<div className="flex items-center gap-1">
<input checked={ signFilterEnabled } className="form-check-input" id="signFilterEnabled" type="checkbox" onChange={ event => setSignFilterEnabled(event.target.checked) } />
<Text>{ LocalizeText('wiredfurni.params.sign_filter') }</Text>
</div>
{ signFilterEnabled &&
<select className="form-select form-select-sm" value={ signId } onChange={ event => setSignId(parseInt(event.target.value)) }>
{ SIGN_OPTIONS.map(option => <option key={ option.value } value={ option.value }>{ LocalizeText(option.label) }</option>) }
</select> }
</div> }
{ (selectedAction === ACTION_DANCE) &&
<div className="flex flex-col gap-1">
<div className="flex items-center gap-1">
<input checked={ danceFilterEnabled } className="form-check-input" id="danceFilterEnabled" type="checkbox" onChange={ event => setDanceFilterEnabled(event.target.checked) } />
<Text>{ LocalizeText('wiredfurni.params.dance_filter') }</Text>
</div>
{ danceFilterEnabled &&
<select className="form-select form-select-sm" value={ danceId } onChange={ event => setDanceId(parseInt(event.target.value)) }>
{ DANCE_OPTIONS.map(option => <option key={ option.value } value={ option.value }>{ LocalizeText(option.label) }</option>) }
</select> }
</div> }
</WiredTriggerBaseView>
);
};
+7 -7
View File
@@ -24,8 +24,8 @@ input[type=number] {
.btn-primary {
color: #fff;
background-color: var(--ui-btn-primary-bg, #3c6d82);
border: 2px solid var(--ui-btn-primary-border, #1a617f);
background-color: #3c6d82;
border: 2px solid #1a617f;
padding: 0.25rem 0.5rem;
font-size: .7875rem;
border-radius: 0.5rem;
@@ -33,7 +33,7 @@ input[type=number] {
}
.btn-primary:hover {
border: 2px solid var(--ui-btn-primary-border, #1a617f);
border: 2px solid #1a617f;
box-shadow: none!important;
}
@@ -81,16 +81,16 @@ input[type=number] {
.btn-dark {
color: #fff;
background-color: var(--ui-dark-bg, #212131);
border: 2px solid var(--ui-dark-border, #1c1c2a);
background-color: #212131;
border: 2px solid #1c1c2a;
box-shadow: none!important;
border-radius: 8px;
padding: 4px 11px 4px 11px;
}
.btn-dark:hover{
background-color: var(--ui-dark-bg, #212131);
border: 2px solid var(--ui-dark-border, #1c1c2a);
background-color: #212131;
border: 2px solid #1c1c2a;
box-shadow: none!important;
border-radius: 8px;
padding: 4px 11px 4px 11px;
+5
View File
@@ -13,11 +13,16 @@ body {
width: 100%;
height: 100%;
overflow: hidden;
background-color: #000;
-webkit-user-select: none;
user-select: none;
scrollbar-width: thin;
}
.image-rendering-pixelated {
image-rendering: pixelated;
}
*,
*:focus,
*:hover {
@@ -59,8 +59,8 @@
.alertView_nitro-coolui-logo {
width: 150px;
height: 78px;
height: 73px;
position: relative;
background-image: url("@/assets/images/notifications/coolui.png");
background-image: url("@/assets/images/notifications/nitro_v3.png");
background-repeat: no-repeat;
}
+2 -2
View File
@@ -22,7 +22,7 @@
pointer-events: all;
}
.borderhccontent{
background-color: var(--ui-dark-bg, #212131);
background-color: #212131;
border-radius: 0.5rem!important;
border: 2px solid #383853;
height: calc(100% - 3px);
@@ -46,7 +46,7 @@
}
.nitro-purse-seasonal-currency {
background-color: var(--ui-dark-bg, #212131);
background-color: #212131;
background: linear-gradient(to right, #5f5f8d, transparent);
height: 30px;
margin-bottom: 4px;
+1 -1
View File
@@ -27,7 +27,7 @@
width: clamp(160px, 20vw, 190px); /* Responsive width */
z-index: 30;
pointer-events: auto;
background: var(--ui-dark-bg, #212131);
background: #212131;
box-shadow: inset 0 5px rgba(38, 38, 57, 0.6), inset 0 -4px rgba(25, 25, 37, 0.6);
border-radius: 0.5rem;
padding: 10px;
+3 -3
View File
@@ -4,7 +4,7 @@
left: 15px;
.nitro-room-tools {
background: var(--ui-dark-bg, #212131);
background: #212131;
box-shadow: inset 0px 5px lighten(rgba(#000, .6), 2.5), inset 0 -4px darken(rgba(#000, .6), 4);
border-top-right-radius: .25rem;
border-bottom-right-radius: .25rem;
@@ -54,7 +54,7 @@
}
.nitro-room-history {
background: var(--ui-dark-bg, #212131);
background: #212131;
box-shadow: inset 0px 5px lighten(rgba(#000, .6), 2.5), inset 0 -4px darken(rgba(#000, .6), 4);
transition: all .2s ease;
width: 150px;
@@ -63,7 +63,7 @@
}
.nitro-room-tools-info {
background: var(--ui-dark-bg, #212131);
background: #212131;
box-shadow: inset 0px 5px lighten(rgba(#000, .6), 2.5), inset 0 -4px darken(rgba(#000, .6), 4);
transition: all .2s ease;
max-width: 250px;
+3 -1
View File
@@ -1,4 +1,4 @@
import { CanCreateRoomEventEvent, CantConnectMessageParser, CreateLinkEvent, DoorbellMessageEvent, FavouriteChangedEvent, FavouritesEvent, FlatAccessDeniedMessageEvent, FlatCreatedEvent, FollowFriendMessageComposer, GenericErrorEvent, GetGuestRoomMessageComposer, GetGuestRoomResultEvent, GetSessionDataManager, GetUserEventCatsMessageComposer, GetUserFlatCatsMessageComposer, HabboWebTools, LegacyExternalInterface, NavigatorCategoryDataParser, NavigatorEventCategoryDataParser, NavigatorHomeRoomEvent, NavigatorMetadataEvent, NavigatorOpenRoomCreatorEvent, NavigatorSavedSearch, NavigatorSearchesEvent, NavigatorSearchEvent, NavigatorSearchResultSet, NavigatorTopLevelContext, RoomDataParser, RoomDoorbellAcceptedEvent, RoomEnterErrorEvent, RoomEntryInfoMessageEvent, RoomForwardEvent, RoomScoreEvent, RoomSettingsUpdatedEvent, SecurityLevel, UserEventCatsEvent, UserFlatCatsEvent, UserInfoEvent, UserPermissionsEvent } from '@nitrots/nitro-renderer';
import { CanCreateRoomEventEvent, CantConnectMessageParser, CreateLinkEvent, DoorbellMessageEvent, FavouriteChangedEvent, FavouritesEvent, FlatAccessDeniedMessageEvent, FlatCreatedEvent, FollowFriendMessageComposer, GenericErrorEvent, GetGuestRoomMessageComposer, GetGuestRoomResultEvent, GetRoomSessionManager, GetSessionDataManager, GetUserEventCatsMessageComposer, GetUserFlatCatsMessageComposer, HabboWebTools, LegacyExternalInterface, NavigatorCategoryDataParser, NavigatorEventCategoryDataParser, NavigatorHomeRoomEvent, NavigatorMetadataEvent, NavigatorOpenRoomCreatorEvent, NavigatorSavedSearch, NavigatorSearchesEvent, NavigatorSearchEvent, NavigatorSearchResultSet, NavigatorTopLevelContext, RoomDataParser, RoomDoorbellAcceptedEvent, RoomEnterErrorEvent, RoomEntryInfoMessageEvent, RoomForwardEvent, RoomScoreEvent, RoomSettingsUpdatedEvent, SecurityLevel, UserEventCatsEvent, UserFlatCatsEvent, UserInfoEvent, UserPermissionsEvent } from '@nitrots/nitro-renderer';
import { useState } from 'react';
import { useBetween } from 'use-between';
import { CreateRoomSession, DoorStateType, GetConfigurationValue, INavigatorData, LocalizeText, NotificationAlertType, SendMessageComposer, TryVisitRoom, VisitDesktop } from '../../api';
@@ -397,6 +397,8 @@ const useNavigatorState = () =>
return;
}
if(GetRoomSessionManager().viewerSession) return;
let forwardType = -1;
let forwardId = -1;
+23 -1
View File
@@ -1,4 +1,4 @@
import { AchievementNotificationMessageEvent, ActivityPointNotificationMessageEvent, ClubGiftNotificationEvent, ClubGiftSelectedEvent, ConnectionErrorEvent, GetLocalizationManager, GetRoomEngine, GetSessionDataManager, HabboBroadcastMessageEvent, HotelClosedAndOpensEvent, HotelClosesAndWillOpenAtEvent, HotelWillCloseInMinutesEvent, InfoFeedEnableMessageEvent, MaintenanceStatusMessageEvent, ModeratorCautionEvent, ModeratorMessageEvent, MOTDNotificationEvent, NotificationDialogMessageEvent, PetLevelNotificationEvent, PetReceivedMessageEvent, RespectReceivedEvent, RoomEnterEffect, RoomEnterEvent, SimpleAlertMessageEvent, UserBannedMessageEvent, Vector3d } from '@nitrots/nitro-renderer';
import { AchievementNotificationMessageEvent, ActivityPointNotificationMessageEvent, ClubGiftNotificationEvent, ClubGiftSelectedEvent, ConnectionErrorEvent, GetLocalizationManager, GetRoomEngine, GetSessionDataManager, HabboBroadcastMessageEvent, HotelClosedAndOpensEvent, HotelClosesAndWillOpenAtEvent, HotelWillCloseInMinutesEvent, InfoFeedEnableMessageEvent, MaintenanceStatusMessageEvent, ModeratorCautionEvent, ModeratorMessageEvent, MOTDNotificationEvent, NotificationDialogMessageEvent, PetLevelNotificationEvent, PetReceivedMessageEvent, RespectReceivedEvent, RoomEnterEffect, RoomEnterEvent, SimpleAlertMessageEvent, UserBannedMessageEvent, Vector3d, WiredRewardResultMessageEvent } from '@nitrots/nitro-renderer';
import { useCallback, useState } from 'react';
import { useBetween } from 'use-between';
import { GetConfigurationValue, LocalizeBadgeName, LocalizeText, NotificationAlertItem, NotificationAlertType, NotificationBubbleItem, NotificationBubbleType, NotificationConfirmItem, PlaySound, ProductImageUtility, TradingNotificationType } from '../../api';
@@ -397,6 +397,28 @@ const useNotificationState = () =>
simpleAlert(LocalizeText(parser.alertMessage), NotificationAlertType.DEFAULT, null, null, LocalizeText(parser.titleMessage ? parser.titleMessage : 'notifications.broadcast.title'));
});
useMessageEvent<WiredRewardResultMessageEvent>(WiredRewardResultMessageEvent, event =>
{
const parser = event.getParser();
switch(parser.reason)
{
case WiredRewardResultMessageEvent.PRODUCT_DONATED_CODE:
case WiredRewardResultMessageEvent.BADGE_DONATED_CODE:
simpleAlert(LocalizeText('wiredfurni.rewardsuccess.body'), NotificationAlertType.DEFAULT, null, null, LocalizeText('wiredfurni.rewardsuccess.title'));
return;
case 0:
case 1:
case 2:
case 3:
case 4:
case 5:
case 8:
simpleAlert(LocalizeText(`wiredfurni.rewardfailed.reason.${ parser.reason }`), NotificationAlertType.DEFAULT, null, null, LocalizeText('wiredfurni.rewardfailed.title'));
return;
}
});
const onRoomEnterEvent = useCallback(() =>
{
if(modDisclaimerShown) return;
+12 -7
View File
@@ -1,7 +1,7 @@
import { ColorConverter, GetRenderer, GetRoomEngine, GetStage, IRoomSession, NitroAdjustmentFilter, NitroSprite, NitroTexture, RoomBackgroundColorEvent, RoomEngineEvent, RoomEngineObjectEvent, RoomGeometry, RoomId, RoomObjectCategory, RoomObjectHSLColorEnabledEvent, RoomObjectOperationType, RoomSessionEvent, RoomVariableEnum, Vector3d } from '@nitrots/nitro-renderer';
import { useEffect, useState } from 'react';
import { useBetween } from 'use-between';
import { CanManipulateFurniture, DispatchUiEvent, GetRoomSession, InitializeRoomInstanceRenderingCanvas, IsFurnitureSelectionDisabled, ProcessRoomObjectOperation, RoomWidgetUpdateBackgroundColorPreviewEvent, RoomWidgetUpdateRoomObjectEvent, SetActiveRoomId, StartRoomSession } from '../../api';
import { CanManipulateFurniture, DispatchUiEvent, GetRoomSession, IsFurnitureSelectionDisabled, ProcessRoomObjectOperation, RoomWidgetUpdateBackgroundColorPreviewEvent, RoomWidgetUpdateRoomObjectEvent, SetActiveRoomId, StartRoomSession } from '../../api';
import { useNitroEvent, useUiEvent } from '../events';
const useRoomState = () =>
@@ -253,15 +253,20 @@ const useRoomState = () =>
const resize = (event: UIEvent) =>
{
const width = Math.floor(window.innerWidth);
const height = Math.floor(window.innerHeight);
const newWidth = Math.floor(window.innerWidth);
const newHeight = Math.floor(window.innerHeight);
renderer.resize(width, height, window.devicePixelRatio);
const offsetX = canvas.screenOffsetX - (newWidth - canvas.width) / 2;
const offsetY = canvas.screenOffsetY - (newHeight - canvas.height) / 2;
background.width = width;
background.height = height;
renderer.resize(newWidth, newHeight, window.devicePixelRatio);
InitializeRoomInstanceRenderingCanvas(width, height, 1);
background.width = newWidth;
background.height = newHeight;
canvas.initialize(newWidth, newHeight);
canvas.screenOffsetX = ~~offsetX;
canvas.screenOffsetY = ~~offsetY;
};
window.addEventListener('resize', resize);
+4 -3
View File
@@ -123,9 +123,10 @@ const useChatWidgetState = () =>
text = LocalizeText('widget.chatbubble.handitem', ['username', 'handitem'], [username, LocalizeText(('handitem' + event.extraParam))]);
break;
case RoomSessionChatEvent.CHAT_TYPE_MUTE_REMAINING: {
const hours = ((event.extraParam > 0) ? Math.floor((event.extraParam / 3600)) : 0).toString();
const minutes = ((event.extraParam > 0) ? Math.floor((event.extraParam % 3600) / 60) : 0).toString();
const seconds = (event.extraParam % 60).toString();
const remainingSeconds = Math.max(0, event.extraParam);
const hours = Math.floor(remainingSeconds / 3600).toString();
const minutes = Math.floor((remainingSeconds % 3600) / 60).toString();
const seconds = (remainingSeconds % 60).toString();
text = LocalizeText('widget.chatbubble.mutetime', ['hours', 'minutes', 'seconds'], [hours, minutes, seconds]);
break;
+2
View File
@@ -235,6 +235,7 @@ const useWiredState = () =>
{
const parser = event.getParser();
WiredSelectionVisualizer.clearAllSelectionShaders();
setTrigger(null);
});
@@ -275,6 +276,7 @@ const useWiredState = () =>
return () =>
{
WiredSelectionVisualizer.clearAllSelectionShaders();
setIntParams([]);
setStringParam('');
setActionDelay(0);