mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
Merge branch 'main' into improve-mod-tools-ui
This commit is contained in:
+68
-8
@@ -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:"
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ 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();
|
||||
@@ -93,6 +94,7 @@ export const App: FC<{}> = props =>
|
||||
{ !isReady &&
|
||||
<LoadingView /> }
|
||||
{ isReady && <MainView /> }
|
||||
<ReconnectView />
|
||||
<Base id="draggable-windows-container" />
|
||||
</Base>
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface IRoomData
|
||||
tags: string[];
|
||||
tradeState: number;
|
||||
allowWalkthrough: boolean;
|
||||
allowUnderpass: boolean;
|
||||
lockState: number;
|
||||
password: string;
|
||||
allowPets: boolean;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GetRoomSessionManager } from '@nitrots/nitro-renderer';
|
||||
import { GetRoomSessionManager, NitroLogger } from '@nitrots/nitro-renderer';
|
||||
import { GetRoomSession } from './GetRoomSession';
|
||||
import { GoToDesktop } from './GoToDesktop';
|
||||
|
||||
@@ -6,6 +6,8 @@ export const VisitDesktop = () =>
|
||||
{
|
||||
if(!GetRoomSession()) return;
|
||||
|
||||
NitroLogger.log('[VisitDesktop] Called (isReconnecting=' + GetRoomSessionManager().isReconnecting + ')');
|
||||
|
||||
GoToDesktop();
|
||||
GetRoomSessionManager().removeSession(-1);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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 |
+18
-4
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}, []);
|
||||
|
||||
@@ -67,11 +67,20 @@ export const LayoutBadgeImageView: FC<LayoutBadgeImageViewProps> = props =>
|
||||
{
|
||||
if(event.badgeId !== badgeCode) return;
|
||||
|
||||
const element = await TextureUtils.generateImage(new NitroSprite(event.image));
|
||||
if(isGroup)
|
||||
{
|
||||
const element = await TextureUtils.generateImage(new NitroSprite(event.image));
|
||||
|
||||
console.log ('boe');
|
||||
element.onload = () => setImageElement(element);
|
||||
}
|
||||
else
|
||||
{
|
||||
const badgeUrl = GetConfigurationValue<string>('badge.asset.url').replace('%badgename%', badgeCode.toString());
|
||||
const img = new Image();
|
||||
|
||||
element.onload = () => setImageElement(element);
|
||||
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);
|
||||
})();
|
||||
}
|
||||
else
|
||||
{
|
||||
const badgeUrl = GetConfigurationValue<string>('badge.asset.url').replace('%badgename%', badgeCode.toString());
|
||||
const img = new Image();
|
||||
|
||||
element.onload = () => setImageElement(element);
|
||||
})();
|
||||
img.onload = () => setImageElement(img);
|
||||
img.src = badgeUrl;
|
||||
}
|
||||
}
|
||||
|
||||
return () => GetEventDispatcher().removeEventListener(BadgeImageReadyEvent.IMAGE_READY, onBadgeImageReadyEvent);
|
||||
|
||||
@@ -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';
|
||||
@@ -29,6 +29,7 @@ import { ToolbarView } from './toolbar/ToolbarView';
|
||||
import { UserProfileView } from './user-profile/UserProfileView';
|
||||
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 =>
|
||||
@@ -43,6 +44,8 @@ export const MainView: FC<{}> = props =>
|
||||
{
|
||||
setIsReady(true);
|
||||
|
||||
GetRoomSessionManager().tryRestoreSession();
|
||||
|
||||
GetCommunication().connection.ready();
|
||||
}, []);
|
||||
|
||||
@@ -95,6 +98,7 @@ export const MainView: FC<{}> = props =>
|
||||
</AnimatePresence>
|
||||
<ToolbarView isInRoom={ !landingViewVisible } />
|
||||
<ModToolsView />
|
||||
<WiredCreatorToolsView />
|
||||
<RoomView />
|
||||
<ChatHistoryView />
|
||||
<WiredView />
|
||||
|
||||
@@ -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 ]);
|
||||
|
||||
+15
-1
@@ -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(() =>
|
||||
|
||||
+7
-1
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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>) =>
|
||||
|
||||
@@ -69,12 +69,12 @@ export const InventoryBadgeView: FC<{ filteredBadgeCodes?: string[] }> = props =
|
||||
{
|
||||
const { filteredBadgeCodes = null } = props;
|
||||
const [ isVisible, setIsVisible ] = useState(false);
|
||||
const { badgeCodes = [], activeBadgeCodes = [], selectedBadgeCode = null, isWearingBadge = null, canWearBadges = null, toggleBadge = null, getBadgeId = null, setBadgeAtSlot = null, removeBadge = null, reorderBadges = null, setSelectedBadgeCode = null, maxBadgeCount = 5, activate = null, deactivate = null } = useInventoryBadges();
|
||||
const { badgeCodes = [], activeBadgeCodes = [], selectedBadgeCode = null, isWearingBadge = null, canWearBadges = null, toggleBadge = null, getBadgeId = null, setBadgeAtSlot = null, removeBadge = null, reorderBadges = null, setSelectedBadgeCode = null, activate = null, deactivate = null } = useInventoryBadges();
|
||||
const { isUnseen = null, removeUnseen = null } = useInventoryUnseenTracker();
|
||||
const { showConfirm = null } = useNotification();
|
||||
const [ isDragOverInventory, setIsDragOverInventory ] = useState(false);
|
||||
|
||||
const maxSlots = maxBadgeCount;
|
||||
const maxSlots = 5;
|
||||
const displayCodes = (filteredBadgeCodes !== null ? filteredBadgeCodes : badgeCodes);
|
||||
|
||||
const attemptDeleteBadge = () =>
|
||||
|
||||
@@ -2,8 +2,8 @@ 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 => {
|
||||
@@ -13,10 +13,18 @@ export const LoadingView: FC<LoadingViewProps> = props => {
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -162,6 +162,11 @@ export const NavigatorRoomSettingsBasicTabView: FC<NavigatorRoomSettingsTabViewP
|
||||
<input className="form-check-input" type="checkbox" checked={ roomData.allowWalkthrough } onChange={ event => handleChange('allow_walkthrough', event.target.checked) } />
|
||||
<Text>{ LocalizeText('navigator.roomsettings.allow_walk_through') }</Text>
|
||||
</Flex>
|
||||
<Flex alignItems="center" gap={ 1 }>
|
||||
<Base className="col-3" />
|
||||
<input className="form-check-input" type="checkbox" checked={ roomData.allowUnderpass } onChange={ event => handleChange('allow_underpass', event.target.checked) } />
|
||||
<Text>{ LocalizeText('navigator.roomsettings.allow_underpass') }</Text>
|
||||
</Flex>
|
||||
<Text variant="danger" underline bold pointer className="d-flex justify-content-center align-items-center gap-1" onClick={ deleteRoom }>
|
||||
<FaTimes className="fa-icon" /> { LocalizeText('navigator.roomsettings.delete') }
|
||||
</Text>
|
||||
|
||||
+15
-4
@@ -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>
|
||||
|
||||
@@ -39,6 +39,7 @@ export const NavigatorRoomSettingsView: FC<{}> = props =>
|
||||
tags: data.tags,
|
||||
tradeState: data.tradeMode,
|
||||
allowWalkthrough: data.allowWalkThrough,
|
||||
allowUnderpass: data.allowUnderpass,
|
||||
lockState: data.doorMode,
|
||||
password: null,
|
||||
allowPets: data.allowPets,
|
||||
@@ -98,6 +99,9 @@ export const NavigatorRoomSettingsView: FC<{}> = props =>
|
||||
case 'allow_walkthrough':
|
||||
newValue.allowWalkthrough = Boolean(value);
|
||||
break;
|
||||
case 'allow_underpass':
|
||||
newValue.allowUnderpass = Boolean(value);
|
||||
break;
|
||||
case 'allow_pets':
|
||||
newValue.allowPets = Boolean(value);
|
||||
break;
|
||||
@@ -171,7 +175,8 @@ export const NavigatorRoomSettingsView: FC<{}> = props =>
|
||||
newValue.chatSettings.weight,
|
||||
newValue.chatSettings.speed,
|
||||
newValue.chatSettings.distance,
|
||||
newValue.chatSettings.protection
|
||||
newValue.chatSettings.protection,
|
||||
newValue.allowUnderpass
|
||||
));
|
||||
|
||||
return newValue;
|
||||
|
||||
@@ -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 } />;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
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(() =>
|
||||
{
|
||||
// Socket is open but not yet re-authenticated.
|
||||
// Update attempt display but keep the overlay visible until
|
||||
// re-authentication completes (SOCKET_REAUTHENTICATED).
|
||||
setHasFailed(false);
|
||||
}, []);
|
||||
|
||||
const onReauthenticated = useCallback(() =>
|
||||
{
|
||||
// Fully re-authenticated — dismiss the overlay so the room view
|
||||
// (which stayed alive behind the overlay) is visible again.
|
||||
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_REAUTHENTICATED, onReauthenticated);
|
||||
useNitroEvent(NitroEventType.SOCKET_RECONNECT_FAILED, onReconnectFailed);
|
||||
|
||||
const handleReload = useCallback(() =>
|
||||
{
|
||||
window.location.reload();
|
||||
}, []);
|
||||
|
||||
const handleGoHome = useCallback(() =>
|
||||
{
|
||||
sessionStorage.removeItem('nitro.session.lastRoomId');
|
||||
sessionStorage.removeItem('nitro.session.lastRoomPassword');
|
||||
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">⚠</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>
|
||||
);
|
||||
};
|
||||
@@ -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 />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { FaPlus } from 'react-icons/fa';
|
||||
import { LocalizeText } from '../../../../../api';
|
||||
import { LayoutBadgeImageView } from '../../../../../common';
|
||||
import { useInventoryBadges } from '../../../../../hooks';
|
||||
|
||||
@@ -49,13 +50,13 @@ const BadgeMiniPicker: FC<{
|
||||
<input
|
||||
autoFocus
|
||||
className="w-full text-xs text-white bg-white/10 border border-white/20 rounded px-2 py-1 mb-2 outline-none focus:border-white/40"
|
||||
placeholder="Cerca badge..."
|
||||
placeholder={ LocalizeText('catalog.search') }
|
||||
type="text"
|
||||
value={ search }
|
||||
onChange={ e => setSearch(e.target.value) }
|
||||
/>
|
||||
{ badgeCodes.length === 0
|
||||
? <span className="text-xs text-white/40 text-center py-2 block">Caricamento...</span>
|
||||
? <span className="text-xs text-white/40 text-center py-2 block">{ LocalizeText('generic.loading') }</span>
|
||||
: (
|
||||
<div className="grid grid-cols-4 gap-1 max-h-[160px] overflow-y-auto">
|
||||
{ filtered.slice(0, 40).map(code => (
|
||||
@@ -67,7 +68,7 @@ const BadgeMiniPicker: FC<{
|
||||
</div>
|
||||
)) }
|
||||
{ filtered.length === 0 && (
|
||||
<span className="text-xs text-white/40 col-span-4 text-center py-2">Nessun badge</span>
|
||||
<span className="text-xs text-white/40 col-span-4 text-center py-2">{ LocalizeText('generic.no_results_found') }</span>
|
||||
) }
|
||||
</div>
|
||||
) }
|
||||
@@ -81,8 +82,6 @@ export const InfoStandBadgeSlotView: FC<InfoStandBadgeSlotProps> = ({ slotIndex,
|
||||
const [ isDragOver, setIsDragOver ] = useState(false);
|
||||
const [ showPicker, setShowPicker ] = useState(false);
|
||||
|
||||
// For own user: use hook data if loaded, otherwise fall back to props (avatarInfo)
|
||||
// For other users: always use props
|
||||
const hookBadge = activeBadgeCodes.length > 0 ? (activeBadgeCodes[slotIndex] ?? null) : null;
|
||||
const badgeCode = isOwnUser ? (hookBadge ?? badgeCodeFromProps ?? null) : (badgeCodeFromProps ?? null);
|
||||
|
||||
@@ -117,14 +116,12 @@ export const InfoStandBadgeSlotView: FC<InfoStandBadgeSlotProps> = ({ slotIndex,
|
||||
|
||||
if(sourceSlotStr !== '')
|
||||
{
|
||||
// Dragged from another infostand slot -> always swap (works with empty slots too)
|
||||
const sourceSlot = parseInt(sourceSlotStr);
|
||||
|
||||
if(sourceSlot !== slotIndex) swapBadges(sourceSlot, slotIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Dragged from inventory or external -> place at this slot
|
||||
setBadgeAtSlot(droppedBadgeCode, slotIndex);
|
||||
}
|
||||
}, [ isOwnUser, slotIndex, swapBadges, setBadgeAtSlot ]);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -231,7 +231,7 @@ const useInventoryBadgesState = () =>
|
||||
SendMessageComposer(new RequestBadgesComposer());
|
||||
};
|
||||
|
||||
return { badgeCodes, activeBadgeCodes, selectedBadgeCode, setSelectedBadgeCode, isWearingBadge, canWearBadges, toggleBadge, getBadgeId, setBadgeAtSlot, removeBadge, reorderBadges, swapBadges, requestBadges, maxBadgeCount, activate, deactivate };
|
||||
return { badgeCodes, activeBadgeCodes, selectedBadgeCode, setSelectedBadgeCode, isWearingBadge, canWearBadges, toggleBadge, getBadgeId, setBadgeAtSlot, removeBadge, reorderBadges, swapBadges, requestBadges, activate, deactivate };
|
||||
};
|
||||
|
||||
export const useInventoryBadges = () => useBetween(useInventoryBadgesState);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
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, NitroEventType, 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';
|
||||
import { useMessageEvent } from '../events';
|
||||
import { useMessageEvent, useNitroEvent } from '../events';
|
||||
import { useNotification } from '../notification';
|
||||
|
||||
const useNavigatorState = () =>
|
||||
@@ -373,6 +373,15 @@ const useNavigatorState = () =>
|
||||
CreateRoomSession(parser.roomId);
|
||||
});
|
||||
|
||||
// When reconnection starts, reset settingsReceived so the login sequence's
|
||||
// NavigatorHomeRoomEvent is treated as a fresh login. Without this, the
|
||||
// prevSettingsReceived check blocks home room navigation after reconnection,
|
||||
// leaving the user stuck on hotel view.
|
||||
useNitroEvent(NitroEventType.SOCKET_RECONNECTING, () =>
|
||||
{
|
||||
setNavigatorData(prevValue => ({ ...prevValue, settingsReceived: false }));
|
||||
});
|
||||
|
||||
useMessageEvent<NavigatorHomeRoomEvent>(NavigatorHomeRoomEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
@@ -397,6 +406,10 @@ const useNavigatorState = () =>
|
||||
return;
|
||||
}
|
||||
|
||||
// If a room session was already restored (from a network disconnect reload),
|
||||
// skip the normal home room navigation to avoid overriding it.
|
||||
if(GetRoomSessionManager().viewerSession) return;
|
||||
|
||||
let forwardType = -1;
|
||||
let forwardId = -1;
|
||||
|
||||
@@ -456,6 +469,11 @@ const useNavigatorState = () =>
|
||||
break;
|
||||
}
|
||||
|
||||
// During reconnection, don't navigate to desktop — the reconnection guard
|
||||
// will handle retrying or cleaning up. Calling VisitDesktop here would
|
||||
// remove the session from the map and send the user to hotel view.
|
||||
if(GetRoomSessionManager().isReconnecting) return;
|
||||
|
||||
VisitDesktop();
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ export default defineConfig({
|
||||
chunkSizeWarningLimit: 200000,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
assetFileNames: 'src/assets/[name].[ext]',
|
||||
assetFileNames: 'src/assets/[name]-[hash].[ext]',
|
||||
manualChunks: id =>
|
||||
{
|
||||
if(id.includes('node_modules'))
|
||||
|
||||
Reference in New Issue
Block a user