From dc388d061cc28380ae8f1a0f44e59cf2b4cf8bc9 Mon Sep 17 00:00:00 2001 From: duckietm Date: Wed, 25 Mar 2026 17:28:33 +0100 Subject: [PATCH 1/7] =?UTF-8?q?=F0=9F=86=99=20add=20check=20for=20pets=20w?= =?UTF-8?q?hen=20sellable=20is=20enabled?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../avatar/IAvatarEditorCategoryPartItem.ts | 1 + .../AvatarEditorFigureSetItemView.tsx | 13 ++++++++---- src/css/index.css | 21 +++++++++++++++++++ src/hooks/avatar-editor/useAvatarEditor.ts | 8 +++++-- 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/api/avatar/IAvatarEditorCategoryPartItem.ts b/src/api/avatar/IAvatarEditorCategoryPartItem.ts index d1cbc0d..46f6991 100644 --- a/src/api/avatar/IAvatarEditorCategoryPartItem.ts +++ b/src/api/avatar/IAvatarEditorCategoryPartItem.ts @@ -7,4 +7,5 @@ export interface IAvatarEditorCategoryPartItem usesColor?: boolean; maxPaletteCount?: number; isClear?: boolean; + isSellableNotOwned?: boolean; } diff --git a/src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx b/src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx index fb33358..b021c5e 100644 --- a/src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx +++ b/src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx @@ -20,6 +20,7 @@ export const AvatarEditorFigureSetItemView: FC<{ const clubLevel = partItem.partSet?.clubLevel ?? 0; const isHC = !GetConfigurationValue('hc.disabled', false) && (clubLevel > 0); const isLocked = isHC && (GetClubMemberLevel() < clubLevel); + const isSellableNotOwned = partItem.isSellableNotOwned ?? false; useEffect(() => { @@ -39,22 +40,26 @@ export const AvatarEditorFigureSetItemView: FC<{ } else { - url = await AvatarEditorThumbnailsHelper.build(setType, partItem, partItem.usesColor, selectedColorParts[setType] ?? null, partIsLocked); + url = await AvatarEditorThumbnailsHelper.build(setType, partItem, partItem.usesColor, selectedColorParts[setType] ?? null, partIsLocked || isSellableNotOwned); } if(url && url.length) setAssetUrl(url); }; loadImage(); - }, [ setType, partItem, selectedColorParts, getFigureStringWithFace ]); + }, [ setType, partItem, selectedColorParts, getFigureStringWithFace, isSellableNotOwned ]); if(!partItem) return null; return ( - + { !partItem.isClear && isHC && } { partItem.isClear && } - { !partItem.isClear && partItem.partSet.isSellable && } + { !partItem.isClear && partItem.partSet.isSellable && !isSellableNotOwned && } + { isSellableNotOwned && +
+ +
}
); }; diff --git a/src/css/index.css b/src/css/index.css index 0e0dc01..4365664 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -1232,6 +1232,27 @@ body { overflow: hidden !important; } +.pet-sellable-locked { + opacity: 0.5; + cursor: not-allowed !important; + position: relative; +} + +.pet-sellable-badge { + position: absolute; + bottom: 2px; + right: 2px; + display: flex; + align-items: center; + gap: 1px; + background-color: rgba(0, 0, 0, 0.6); + border-radius: 3px; + padding: 1px 3px; + font-size: 9px; + color: #ffd700; + font-weight: bold; +} + .pet-remove-btn { background: none; border: none; diff --git a/src/hooks/avatar-editor/useAvatarEditor.ts b/src/hooks/avatar-editor/useAvatarEditor.ts index ae401de..420b9f9 100644 --- a/src/hooks/avatar-editor/useAvatarEditor.ts +++ b/src/hooks/avatar-editor/useAvatarEditor.ts @@ -65,6 +65,8 @@ const useAvatarEditorState = () => if(GetClubMemberLevel() < partItem.partSet.clubLevel) return; + if(partItem.isSellableNotOwned) return; + setMaxPaletteCount(partItem.maxPaletteCount || 1); selectPart(setType, partId); @@ -271,13 +273,15 @@ const useAvatarEditorState = () => if(!partSet || !partSet.isSelectable || ((partSet.gender !== gender) && (partSet.gender !== AvatarFigurePartType.UNISEX))) continue; - if(partSet.isSellable && figureSetIds.indexOf(partSet.id) === -1) continue; + const isSellableNotOwned = partSet.isSellable && figureSetIds.indexOf(partSet.id) === -1; + + if(isSellableNotOwned && setType !== AvatarFigurePartType.PET) continue; let maxPaletteCount = 0; for(const part of partSet.parts) maxPaletteCount = Math.max(maxPaletteCount, part.colorLayerIndex); - partItems.push({ id: partSet.id, partSet, usesColor, maxPaletteCount }); + partItems.push({ id: partSet.id, partSet, usesColor, maxPaletteCount, isSellableNotOwned }); } partItems.sort(AvatarEditorPartSorter(false)); From 3b3e91f6d99dc12361d7b6016878f82e22a11793 Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Thu, 26 Mar 2026 05:24:53 +0100 Subject: [PATCH 2/7] Add NFT avatar tab and wired extras UI --- public/UITexts.example | 19 ++- .../avatar/IAvatarEditorCategoryPartItem.ts | 1 + src/api/wired/WiredActionLayoutCode.ts | 2 + src/assets/images/wardrobe/nft.png | Bin 0 -> 166 bytes src/assets/images/wardrobe/pets.png | Bin 188 -> 222 bytes src/assets/images/wiredtools/furni.png | Bin 0 -> 331 bytes src/assets/images/wiredtools/global.png | Bin 0 -> 245 bytes src/assets/images/wiredtools/user.png | Bin 0 -> 201 bytes src/common/layout/LayoutAvatarImageView.tsx | 2 +- .../AvatarEditorFigurePreviewView.tsx | 4 +- .../avatar-editor/AvatarEditorNftView.tsx | 117 ++++++++++++++ .../avatar-editor/AvatarEditorView.tsx | 8 +- .../AvatarEditorFigureSetItemView.tsx | 21 ++- src/components/avatar-editor/index.ts | 1 + .../views/actions/WiredActionLayoutView.tsx | 6 + .../views/extras/WiredExtraOrEvalView.tsx | 143 ++++++++++++++++++ .../WiredExtraTextOutputUsernameView.tsx | 123 +++++++++++++++ src/css/index.css | 30 ++++ src/hooks/avatar-editor/useAvatarEditor.ts | 58 ++++++- src/hooks/wired/useWired.ts | 29 +++- 20 files changed, 542 insertions(+), 22 deletions(-) create mode 100644 src/assets/images/wardrobe/nft.png create mode 100644 src/assets/images/wiredtools/furni.png create mode 100644 src/assets/images/wiredtools/global.png create mode 100644 src/assets/images/wiredtools/user.png create mode 100644 src/components/avatar-editor/AvatarEditorNftView.tsx create mode 100644 src/components/wired/views/extras/WiredExtraOrEvalView.tsx create mode 100644 src/components/wired/views/extras/WiredExtraTextOutputUsernameView.tsx diff --git a/public/UITexts.example b/public/UITexts.example index 25c2cfa..b12e768 100644 --- a/public/UITexts.example +++ b/public/UITexts.example @@ -22,6 +22,21 @@ "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.users.11": "L'utente cliccato", - "wiredfurni.params.setexecutions": "Quantità di esecuzioni: %amount%", - "wiredfurni.params.settimewindow": "Tempo massimo consentito: %timewindow% secondi" + "wiredfurni.params.setexecutions": "Quantità di esecuzioni: %amount%", + "wiredfurni.params.settimewindow": "Tempo massimo consentito: %timewindow% secondi", + "wiredfurni.params.eval_mode": "Condizioni che devono corrispondere:", + "wiredfurni.params.eval_mode.0": "Tutte", + "wiredfurni.params.eval_mode.1": "Almeno una", + "wiredfurni.params.eval_mode.2": "Non tutte", + "wiredfurni.params.eval_mode.3": "Nessuna", + "wiredfurni.params.eval_mode.cmp.0": "Meno di:", + "wiredfurni.params.eval_mode.cmp.1": "Esattamente:", + "wiredfurni.params.eval_mode.cmp.2": "Più di:", + "wiredfurni.error.condition_evaluation_furni": "Puoi selezionare solo la Condizione e i componenti aggiuntivi!", + "wiredfurni.params.texts.placeholder_name": "Nome del segnaposto:", + "wiredfurni.params.texts.placeholder_preview": "Usalo digitando %placeholder% nei testi dei Wired.", + "wiredfurni.params.texts.placeholder_type": "Tipo di segnaposto:", + "wiredfurni.params.texts.placeholder_type.1": "Singolo", + "wiredfurni.params.texts.placeholder_type.2": "Multiplo", + "wiredfurni.params.texts.select_delimiter": "Seleziona il delimitatore:" } diff --git a/src/api/avatar/IAvatarEditorCategoryPartItem.ts b/src/api/avatar/IAvatarEditorCategoryPartItem.ts index d1cbc0d..46f6991 100644 --- a/src/api/avatar/IAvatarEditorCategoryPartItem.ts +++ b/src/api/avatar/IAvatarEditorCategoryPartItem.ts @@ -7,4 +7,5 @@ export interface IAvatarEditorCategoryPartItem usesColor?: boolean; maxPaletteCount?: number; isClear?: boolean; + isSellableNotOwned?: boolean; } diff --git a/src/api/wired/WiredActionLayoutCode.ts b/src/api/wired/WiredActionLayoutCode.ts index d3f1f5e..0839bb1 100644 --- a/src/api/wired/WiredActionLayoutCode.ts +++ b/src/api/wired/WiredActionLayoutCode.ts @@ -64,4 +64,6 @@ export class WiredActionLayoutCode public static RANDOM_EXTRA: number = 63; public static EXEC_IN_ORDER_EXTRA: number = 64; public static EXECUTION_LIMIT_EXTRA: number = 65; + public static OR_EVAL_EXTRA: number = 66; + public static TEXT_OUTPUT_USERNAME_EXTRA: number = 67; } diff --git a/src/assets/images/wardrobe/nft.png b/src/assets/images/wardrobe/nft.png new file mode 100644 index 0000000000000000000000000000000000000000..ad535a4c8d15fc2e60aea5d253c4a2d57dce17ea GIT binary patch literal 166 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9Y$ZW{!3_UF&^$uA7AO+!>Eakt z!FYBmBijK39){01YtrVQyJ7Yq;Ck1b1`F{8y;U6N;{!x9j#j*il8JA8$;ZjKN0*() zspUakxyJoQUftc!PKSRVN?aMl@t`s4ru44}{@&*|9@oFI-o$>7|LMTSC-XYD ziVdTR&AsxSpL=d^$ct}_>zO>|M*m*X|LNlI^4?1cI1aZC?WJ$uC7 zg{O51N}8W_kXU`Jm^t<%o6N3OhQ|>n6O`D3qeT;n7=mvz#JSnk%oOJO&&uN%mo5!- O0fVQjpUXO@geCyu<4cqP literal 188 zcmeAS@N?(olHy`uVBq!ia0vp^B0wz0!3HD`IPL*a&7LlfAr*6y6C_x-u>Id|vhy$d zMT;Lxq{}ihx$X#**m>$PU2HK>6i+S{+?7^h=O-@wl4+0Kfmco`hdVgsoL+tOOcGT# zy_VQD(R9+(9FAiqE7=a4HYEwlP8K+KWM^MUxKOHu=DH=jxC3Ku3g@>pdh$9iit+4v lZNQVZz{%=Js>NSMhJEv6gv`$V%m6xv!PC{xWt~$(69B7gK{EgV diff --git a/src/assets/images/wiredtools/furni.png b/src/assets/images/wiredtools/furni.png new file mode 100644 index 0000000000000000000000000000000000000000..4cae0aa2edfc8e2283b74b10eb33d53f3c5cb987 GIT binary patch literal 331 zcmV-R0kr;!P)X0{{R33)haO0001BP)t-s|Ns90 z008;<`OM7BGIy8o@bKW=*~`nzmdxP7z`er1yyN5JdAQz@#pKP+&BDOE;oaN9zq{h% z;#Z}`;oRDf!{g-S!|PqrN6B|T0>F_A0)}A#gjQrYXaxP!D8Ys{gd4|A#uHj2 zIM*}=&S5p8gpB~`VbqhN0nJ?^Oo9s2)F(Q(022{2q#Nz!0P=@*8|qQYT`bRY*W`JT dJg=>kI$zb&2Ni1ok5T{t002ovPDHLkV1mmNpXdMp literal 0 HcmV?d00001 diff --git a/src/assets/images/wiredtools/global.png b/src/assets/images/wiredtools/global.png new file mode 100644 index 0000000000000000000000000000000000000000..8126b1e280fb469b333b8269b7f31b27bab9baba GIT binary patch literal 245 zcmeAS@N?(olHy`uVBq!ia0vp^(jd&i3?z4Pv801I0X`wF|Ns97GJ)iwYY&Xdr-v^+ zdiKe?2OqvoJM*9}ta2Yvjzopr01Ay~L;wH) literal 0 HcmV?d00001 diff --git a/src/assets/images/wiredtools/user.png b/src/assets/images/wiredtools/user.png new file mode 100644 index 0000000000000000000000000000000000000000..eaed802c1693198ea6265005913cdc5ab12a890c GIT binary patch literal 201 zcmeAS@N?(olHy`uVBq!ia0vp^0zfRn!3-of@9}j7QYryHA+G=b{|7Sd?d`L&vaZaE z`*&*kubVr#ge8s2rw3RugfBfh?aYJibsBs?MT{jue!&b5&u*jvIl-PTjv*Dd)^<4x zH7M{L(b%%>)&Kvmg9@G=xX8n-vN^S1rTh9VmUh0(9Xnpe2Od4#DixsjfZZT export const LayoutAvatarImageView: FC = props => { - const { figure = '', gender = 'M', headOnly = false, direction = 0, scale = 1, classNames = [], style = {}, ...rest } = props; + const { figure = '', gender = '', headOnly = false, direction = 0, scale = 1, classNames = [], style = {}, ...rest } = props; const [ avatarUrl, setAvatarUrl ] = useState(null); const [ isReady, setIsReady ] = useState(false); const isDisposed = useRef(false); diff --git a/src/components/avatar-editor/AvatarEditorFigurePreviewView.tsx b/src/components/avatar-editor/AvatarEditorFigurePreviewView.tsx index 25bb609..857dcaa 100644 --- a/src/components/avatar-editor/AvatarEditorFigurePreviewView.tsx +++ b/src/components/avatar-editor/AvatarEditorFigurePreviewView.tsx @@ -9,7 +9,7 @@ const DEFAULT_DIRECTION: number = 4; export const AvatarEditorFigurePreviewView: FC<{}> = props => { const [ direction, setDirection ] = useState(DEFAULT_DIRECTION); - const { getFigureString = null } = useAvatarEditor(); + const { getFigureString = null, gender = 'M' } = useAvatarEditor(); const rotateFigure = (newDirection: number) => { @@ -28,7 +28,7 @@ export const AvatarEditorFigurePreviewView: FC<{}> = props => return (
- +
diff --git a/src/components/avatar-editor/AvatarEditorNftView.tsx b/src/components/avatar-editor/AvatarEditorNftView.tsx new file mode 100644 index 0000000..405f5ec --- /dev/null +++ b/src/components/avatar-editor/AvatarEditorNftView.tsx @@ -0,0 +1,117 @@ +import { AvatarFigurePartType, FigureDataContainer } from '@nitrots/nitro-renderer'; +import { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import { CreateLinkEvent, GetClubMemberLevel, IAvatarEditorCategory } from '../../api'; +import { LayoutAvatarImageView, LayoutCurrencyIcon } from '../../common'; +import { useAvatarEditor } from '../../hooks'; +import { AvatarEditorIcon } from './AvatarEditorIcon'; +import { AvatarEditorFigureSetView } from './figure-set'; +import { AvatarEditorAdvancedColorView, AvatarEditorPaletteSetView } from './palette-set'; + +export const AvatarEditorNftView: FC<{ + categories: IAvatarEditorCategory[]; +}> = props => +{ + const { categories = [] } = props; + const [ didChange, setDidChange ] = useState(false); + const [ activeSetType, setActiveSetType ] = useState(''); + const [ advancedColorMode, setAdvancedColorMode ] = useState(false); + const hasHC = GetClubMemberLevel() > 0; + const { maxPaletteCount = 1, selectedColorParts = null, getFirstSelectableColor = null, selectEditorColor = null, gender = null, setGender = null, getFigureString = '' } = useAvatarEditor(); + + const activeCategory = useMemo(() => + { + return categories.find(category => category.setType === activeSetType) ?? null; + }, [ categories, activeSetType ]); + + const selectSet = useCallback((setType: string) => + { + const selectedPalettes = selectedColorParts[setType]; + + if(!selectedPalettes || !selectedPalettes.length) selectEditorColor(setType, 0, getFirstSelectableColor(setType)); + + setActiveSetType(setType); + }, [ getFirstSelectableColor, selectEditorColor, selectedColorParts ]); + + useEffect(() => + { + if(!categories || !categories.length || !didChange) return; + + selectSet(categories[0]?.setType); + setDidChange(false); + }, [ categories, didChange, selectSet ]); + + useEffect(() => + { + setDidChange(true); + }, [ categories ]); + + if(!categories.length || !activeCategory) + { + return ( +
+
NFT
+
No NFT items available.
+
+ ); + } + + return ( +
+
+ { categories.map(category => +
selectSet(category.setType) }> + { (category.setType === AvatarFigurePartType.HEAD) + ? ( +
+ +
+ ) + : } +
+ ) } +
+ + { (activeSetType === AvatarFigurePartType.HEAD) && +
+
setGender(AvatarFigurePartType.MALE) }> + +
+
setGender(AvatarFigurePartType.FEMALE) }> + +
+
} + +
+ +
+ +
+ +
+ +
+ { (maxPaletteCount >= 1) && +
+ { advancedColorMode + ? + : } +
} + { (maxPaletteCount === 2) && +
+ { advancedColorMode + ? + : } +
} +
+
+ ); +}; diff --git a/src/components/avatar-editor/AvatarEditorView.tsx b/src/components/avatar-editor/AvatarEditorView.tsx index f080e21..6d23a3c 100644 --- a/src/components/avatar-editor/AvatarEditorView.tsx +++ b/src/components/avatar-editor/AvatarEditorView.tsx @@ -6,6 +6,7 @@ import { Button, ButtonGroup, NitroCardContentView, NitroCardHeaderView, NitroCa import { useAvatarEditor } from '../../hooks'; import { AvatarEditorFigurePreviewView } from './AvatarEditorFigurePreviewView'; import { AvatarEditorModelView } from './AvatarEditorModelView'; +import { AvatarEditorNftView } from './AvatarEditorNftView'; import { AvatarEditorPetView } from './AvatarEditorPetView'; import { AvatarEditorWardrobeView } from './AvatarEditorWardrobeView'; @@ -16,6 +17,7 @@ export const AvatarEditorView: FC<{}> = props => const isWardrobeOpen = (activeModelKey === AvatarEditorFigureCategory.WARDROBE); const isPetsOpen = (activeModelKey === AvatarEditorFigureCategory.PETS); + const isNftOpen = (activeModelKey === AvatarEditorFigureCategory.NFT); const processAction = (action: string) => { @@ -85,10 +87,12 @@ export const AvatarEditorView: FC<{}> = props => const isActive = (activeModelKey === modelKey); const isWardrobe = (modelKey === AvatarEditorFigureCategory.WARDROBE); const isPets = (modelKey === AvatarEditorFigureCategory.PETS); + const isNft = (modelKey === AvatarEditorFigureCategory.NFT); let tabClass = `tab ${ modelKey }`; if(isWardrobe) tabClass = 'tab-wardrobe'; else if(isPets) tabClass = 'tab-pets'; + else if(isNft) tabClass = 'tab-nft'; return ( setActiveModelKey(modelKey) }> @@ -101,12 +105,14 @@ export const AvatarEditorView: FC<{}> = props =>
{ /* left: model view or wardrobe */ }
- { (activeModelKey.length > 0 && !isWardrobeOpen && !isPetsOpen) && + { (activeModelKey.length > 0 && !isWardrobeOpen && !isPetsOpen && !isNftOpen) && } { isWardrobeOpen && } { isPetsOpen && } + { isNftOpen && + }
{ /* right: preview + actions */ }
diff --git a/src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx b/src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx index fb33358..9201ce2 100644 --- a/src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx +++ b/src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx @@ -20,6 +20,7 @@ export const AvatarEditorFigureSetItemView: FC<{ const clubLevel = partItem.partSet?.clubLevel ?? 0; const isHC = !GetConfigurationValue('hc.disabled', false) && (clubLevel > 0); const isLocked = isHC && (GetClubMemberLevel() < clubLevel); + const isSellableNotOwned = partItem.isSellableNotOwned ?? false; useEffect(() => { @@ -35,26 +36,36 @@ export const AvatarEditorFigureSetItemView: FC<{ if(setType === AvatarFigurePartType.HEAD) { - url = await AvatarEditorThumbnailsHelper.buildForFace(getFigureStringWithFace(partItem.id), partIsLocked); + url = await AvatarEditorThumbnailsHelper.buildForFace(getFigureStringWithFace(partItem.id), partIsLocked || isSellableNotOwned); } else { - url = await AvatarEditorThumbnailsHelper.build(setType, partItem, partItem.usesColor, selectedColorParts[setType] ?? null, partIsLocked); + url = await AvatarEditorThumbnailsHelper.build( + setType, + partItem, + partItem.usesColor, + selectedColorParts[setType] ?? null, + partIsLocked || isSellableNotOwned + ); } if(url && url.length) setAssetUrl(url); }; loadImage(); - }, [ setType, partItem, selectedColorParts, getFigureStringWithFace ]); + }, [ setType, partItem, selectedColorParts, getFigureStringWithFace, isSellableNotOwned ]); if(!partItem) return null; return ( - + { !partItem.isClear && isHC && } { partItem.isClear && } - { !partItem.isClear && partItem.partSet.isSellable && } + { !partItem.isClear && partItem.partSet.isSellable && !isSellableNotOwned && } + { isSellableNotOwned && +
+ +
}
); }; diff --git a/src/components/avatar-editor/index.ts b/src/components/avatar-editor/index.ts index 30769a7..5b21fd7 100644 --- a/src/components/avatar-editor/index.ts +++ b/src/components/avatar-editor/index.ts @@ -1,6 +1,7 @@ export * from './AvatarEditorFigurePreviewView'; export * from './AvatarEditorIcon'; export * from './AvatarEditorModelView'; +export * from './AvatarEditorNftView'; export * from './AvatarEditorPetView'; export * from './AvatarEditorView'; export * from './AvatarEditorWardrobeView'; diff --git a/src/components/wired/views/actions/WiredActionLayoutView.tsx b/src/components/wired/views/actions/WiredActionLayoutView.tsx index 051ae58..2ebefff 100644 --- a/src/components/wired/views/actions/WiredActionLayoutView.tsx +++ b/src/components/wired/views/actions/WiredActionLayoutView.tsx @@ -57,8 +57,10 @@ import { WiredExtraMoveCarryUsersView } from '../extras/WiredExtraMoveCarryUsers import { WiredExtraExecuteInOrderView } from '../extras/WiredExtraExecuteInOrderView'; import { WiredExtraExecutionLimitView } from '../extras/WiredExtraExecutionLimitView'; import { WiredExtraMoveNoAnimationView } from '../extras/WiredExtraMoveNoAnimationView'; +import { WiredExtraOrEvalView } from '../extras/WiredExtraOrEvalView'; import { WiredExtraMovePhysicsView } from '../extras/WiredExtraMovePhysicsView'; import { WiredExtraRandomView } from '../extras/WiredExtraRandomView'; +import { WiredExtraTextOutputUsernameView } from '../extras/WiredExtraTextOutputUsernameView'; import { WiredExtraUnseenView } from '../extras/WiredExtraUnseenView'; export const WiredActionLayoutView = (code: number) => @@ -189,6 +191,10 @@ export const WiredActionLayoutView = (code: number) => return ; case WiredActionLayoutCode.EXECUTION_LIMIT_EXTRA: return ; + case WiredActionLayoutCode.OR_EVAL_EXTRA: + return ; + case WiredActionLayoutCode.TEXT_OUTPUT_USERNAME_EXTRA: + return ; case WiredActionLayoutCode.SEND_SIGNAL: return ; } diff --git a/src/components/wired/views/extras/WiredExtraOrEvalView.tsx b/src/components/wired/views/extras/WiredExtraOrEvalView.tsx new file mode 100644 index 0000000..3e05bb8 --- /dev/null +++ b/src/components/wired/views/extras/WiredExtraOrEvalView.tsx @@ -0,0 +1,143 @@ +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 { WiredExtraBaseView } from './WiredExtraBaseView'; + +const MODE_ALL = 0; +const MODE_AT_LEAST_ONE = 1; +const MODE_NOT_ALL = 2; +const MODE_NONE = 3; +const MODE_LESS_THAN = 4; +const MODE_EXACTLY = 5; +const MODE_MORE_THAN = 6; +const MIN_COMPARE_VALUE = 0; +const MAX_COMPARE_VALUE = 100; +const DEFAULT_COMPARE_VALUE = 1; +const COMPARE_VALUE_PATTERN = /^\d*$/; +const CONDITION_EVALUATION_INTERACTION_TYPES = [ 'wf_cnd_*', 'wf_xtra_*' ]; +const CONDITION_EVALUATION_ERROR_KEY = 'wiredfurni.error.condition_evaluation_furni'; + +const FURNI_SOURCES: WiredSourceOption[] = [ + { value: 100, label: 'wiredfurni.params.sources.furni.100' }, + { value: 0, label: 'wiredfurni.params.sources.furni.0' }, + { value: 200, label: 'wiredfurni.params.sources.furni.200' }, + { value: 201, label: 'wiredfurni.params.sources.furni.201' } +]; + +const MODE_OPTIONS = [ MODE_ALL, MODE_AT_LEAST_ONE, MODE_NOT_ALL, MODE_NONE ]; +const COMPARISON_OPTIONS = [ MODE_LESS_THAN, MODE_EXACTLY, MODE_MORE_THAN ]; + +const normalizeEvaluationMode = (value: number) => ([ ...MODE_OPTIONS, ...COMPARISON_OPTIONS ].includes(value) ? value : MODE_ALL); +const normalizeFurniSource = (value: number) => (FURNI_SOURCES.some(option => option.value === value) ? value : 0); +const normalizeCompareValue = (value: number) => +{ + if(isNaN(value)) return DEFAULT_COMPARE_VALUE; + + return Math.max(MIN_COMPARE_VALUE, Math.min(MAX_COMPARE_VALUE, Math.floor(value))); +}; + +export const WiredExtraOrEvalView: FC<{}> = () => +{ + const { trigger = null, setIntParams = null, setStringParam = null, setAllowedInteractionTypes = null, setAllowedInteractionErrorKey = null } = useWired(); + const [ evaluationMode, setEvaluationMode ] = useState(MODE_ALL); + const [ furniSource, setFurniSource ] = useState(0); + const [ compareValue, setCompareValue ] = useState(DEFAULT_COMPARE_VALUE); + const [ compareValueInput, setCompareValueInput ] = useState(DEFAULT_COMPARE_VALUE.toString()); + + useEffect(() => + { + setAllowedInteractionTypes(CONDITION_EVALUATION_INTERACTION_TYPES); + setAllowedInteractionErrorKey(CONDITION_EVALUATION_ERROR_KEY); + + return () => + { + setAllowedInteractionTypes(null); + setAllowedInteractionErrorKey(null); + }; + }, [ setAllowedInteractionErrorKey, setAllowedInteractionTypes ]); + + useEffect(() => + { + if(!trigger) return; + + setEvaluationMode(normalizeEvaluationMode((trigger.intData.length > 0) ? trigger.intData[0] : MODE_ALL)); + setFurniSource(normalizeFurniSource((trigger.intData.length > 1) ? trigger.intData[1] : 0)); + const nextCompareValue = normalizeCompareValue((trigger.intData.length > 2) ? trigger.intData[2] : DEFAULT_COMPARE_VALUE); + setCompareValue(nextCompareValue); + setCompareValueInput(nextCompareValue.toString()); + }, [ trigger ]); + + const updateCompareValue = (value: number) => + { + const nextValue = normalizeCompareValue(value); + + setCompareValue(nextValue); + setCompareValueInput(nextValue.toString()); + }; + + const updateCompareValueInput = (value: string) => + { + if(!COMPARE_VALUE_PATTERN.test(value)) return; + + setCompareValueInput(value); + + if(!value.length) + { + setCompareValue(MIN_COMPARE_VALUE); + return; + } + + updateCompareValue(parseInt(value)); + }; + + const save = () => + { + setIntParams([ normalizeEvaluationMode(evaluationMode), normalizeFurniSource(furniSource), normalizeCompareValue(compareValue) ]); + setStringParam(''); + }; + + return ( + setFurniSource(normalizeFurniSource(value)) } /> }> +
+ { LocalizeText('wiredfurni.params.eval_mode') } + { MODE_OPTIONS.map(mode => + { + return ( + + ); + }) } + { COMPARISON_OPTIONS.map(mode => + { + const isSelected = (evaluationMode === mode); + + return ( + + ); + }) } +
+
+ ); +}; diff --git a/src/components/wired/views/extras/WiredExtraTextOutputUsernameView.tsx b/src/components/wired/views/extras/WiredExtraTextOutputUsernameView.tsx new file mode 100644 index 0000000..1aa3856 --- /dev/null +++ b/src/components/wired/views/extras/WiredExtraTextOutputUsernameView.tsx @@ -0,0 +1,123 @@ +import { FC, useEffect, useMemo, useState } from 'react'; +import { LocalizeText, WiredFurniType } from '../../../../api'; +import { Text } from '../../../../common'; +import { useWired } from '../../../../hooks'; +import { NitroInput } from '../../../../layout'; +import { WiredSourcesSelector, CLICKED_USER_SOURCE_VALUE } from '../WiredSourcesSelector'; +import { WiredExtraBaseView } from './WiredExtraBaseView'; + +const TYPE_SINGLE = 1; +const TYPE_MULTIPLE = 2; +const DEFAULT_PLACEHOLDER_NAME = ''; +const DEFAULT_DELIMITER = ', '; +const MAX_PLACEHOLDER_NAME_LENGTH = 32; +const MAX_DELIMITER_LENGTH = 16; +const PLACEHOLDER_WRAPPER_PATTERN = /^\$\((.*)\)$/; + +const normalizePlaceholderType = (value: number) => ((value === TYPE_MULTIPLE) ? TYPE_MULTIPLE : TYPE_SINGLE); +const normalizeUserSource = (value: number) => ((value === 0) || (value === 200) || (value === 201) || (value === CLICKED_USER_SOURCE_VALUE) ? value : 0); +const normalizePlaceholderName = (value: string) => +{ + let normalizedValue = (value ?? '').trim().replace(/[\t\r\n]/g, ''); + + if(PLACEHOLDER_WRAPPER_PATTERN.test(normalizedValue)) + { + normalizedValue = normalizedValue.substring(2, normalizedValue.length - 1).trim(); + } + + return normalizedValue.slice(0, MAX_PLACEHOLDER_NAME_LENGTH); +}; + +const normalizeDelimiter = (value: string) => +{ + if(value === undefined || value === null) return DEFAULT_DELIMITER; + + return value.replace(/[\t\r\n]/g, '').slice(0, MAX_DELIMITER_LENGTH); +}; + +const splitStringData = (value: string) => +{ + if(!value?.length) return [ DEFAULT_PLACEHOLDER_NAME, DEFAULT_DELIMITER ]; + + const parts = value.split('\t'); + + if(parts.length <= 1) return [ value, DEFAULT_DELIMITER ]; + + return [ parts[0], parts[1] ]; +}; + +const escapeHtml = (value: string) => value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + +export const WiredExtraTextOutputUsernameView: FC<{}> = () => +{ + const { trigger = null, setIntParams = null, setStringParam = null } = useWired(); + const [ placeholderName, setPlaceholderName ] = useState(DEFAULT_PLACEHOLDER_NAME); + const [ placeholderType, setPlaceholderType ] = useState(TYPE_SINGLE); + const [ delimiter, setDelimiter ] = useState(DEFAULT_DELIMITER); + const [ userSource, setUserSource ] = useState(0); + + useEffect(() => + { + if(!trigger) return; + + const [ nextPlaceholderName, nextDelimiter ] = splitStringData(trigger.stringData); + + setPlaceholderName(normalizePlaceholderName(nextPlaceholderName)); + setDelimiter(normalizeDelimiter(nextDelimiter)); + setPlaceholderType(normalizePlaceholderType((trigger.intData.length > 0) ? trigger.intData[0] : TYPE_SINGLE)); + setUserSource(normalizeUserSource((trigger.intData.length > 1) ? trigger.intData[1] : 0)); + }, [ trigger ]); + + const previewToken = useMemo(() => + { + const effectiveName = normalizePlaceholderName(placeholderName) || 'placeholder'; + + return `$(${ effectiveName })`; + }, [ placeholderName ]); + + const previewHtml = useMemo(() => LocalizeText('wiredfurni.params.texts.placeholder_preview', [ 'placeholder' ], [ escapeHtml(previewToken) ]), [ previewToken ]); + + const save = () => + { + setIntParams([ normalizePlaceholderType(placeholderType), normalizeUserSource(userSource) ]); + setStringParam(`${ normalizePlaceholderName(placeholderName) }\t${ normalizeDelimiter(delimiter) }`); + }; + + return ( + setUserSource(normalizeUserSource(value)) } /> }> +
+
+ { LocalizeText('wiredfurni.params.texts.placeholder_name') } + setPlaceholderName(normalizePlaceholderName(event.target.value)) } /> +
+ +
+ { LocalizeText('wiredfurni.params.texts.placeholder_type') } + + +
+ { placeholderType === TYPE_MULTIPLE && +
+ { LocalizeText('wiredfurni.params.texts.select_delimiter') } + setDelimiter(normalizeDelimiter(event.target.value)) } /> +
} +
+
+ ); +}; diff --git a/src/css/index.css b/src/css/index.css index 0e0dc01..6c62fc7 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -1180,6 +1180,15 @@ body { background-position: center; background-size: 22px 22px; } + + .tab-nft { + width: 34px; + height: 22px; + background-image: url('@/assets/images/wardrobe/nft.png'); + background-repeat: no-repeat; + background-position: center; + background-size: 22px 22px; + } } /* ── Avatar Editor misc ─────────────────────────────────────────────────── */ @@ -1232,6 +1241,27 @@ body { overflow: hidden !important; } +.pet-sellable-locked { + opacity: 0.5; + cursor: not-allowed !important; + position: relative; +} + +.pet-sellable-badge { + position: absolute; + bottom: 2px; + right: 2px; + display: flex; + align-items: center; + gap: 1px; + background-color: rgba(0, 0, 0, 0.6); + border-radius: 3px; + padding: 1px 3px; + font-size: 9px; + color: #ffd700; + font-weight: bold; +} + .pet-remove-btn { background: none; border: none; diff --git a/src/hooks/avatar-editor/useAvatarEditor.ts b/src/hooks/avatar-editor/useAvatarEditor.ts index ae401de..3046605 100644 --- a/src/hooks/avatar-editor/useAvatarEditor.ts +++ b/src/hooks/avatar-editor/useAvatarEditor.ts @@ -15,6 +15,7 @@ const useAvatarEditorState = () => const [ maxPaletteCount, setMaxPaletteCount ] = useState(1); const [ figureSetIds, setFigureSetIds ] = useState([]); const [ boundFurnitureNames, setBoundFurnitureNames ] = useState([]); + const [ figureSetNames, setFigureSetNames ] = useState>({}); const [ savedFigures, setSavedFigures ] = useState<[ IAvatarFigureContainer, string ][]>(null); const { selectedColors, gender, setGender, loadAvatarData, selectPart, selectColor, getFigureString, getFigureStringWithFace, selectedParts } = useFigureData(); @@ -65,6 +66,8 @@ const useAvatarEditorState = () => if(GetClubMemberLevel() < partItem.partSet.clubLevel) return; + if(partItem.isSellableNotOwned) return; + setMaxPaletteCount(partItem.maxPaletteCount || 1); selectPart(setType, partId); @@ -194,12 +197,27 @@ const useAvatarEditorState = () => loadAvatarData(figureContainer.getFigureString(), gender); }, [ figureSetIds, gender, loadAvatarData, selectedColors, selectedParts ]); + const nftFigureSetIds = useMemo(() => + { + const nftSetIds = new Set(); + + for(const [ setId, furnitureName ] of Object.entries(figureSetNames)) + { + if(!furnitureName?.toLowerCase().includes('nft')) continue; + + nftSetIds.add(Number(setId)); + } + + return nftSetIds; + }, [ figureSetNames ]); + useMessageEvent(FigureSetIdsMessageEvent, event => { const parser = event.getParser(); setFigureSetIds(parser.figureSetIds); setBoundFurnitureNames(parser.boundsFurnitureNames); + setFigureSetNames(parser.figureSetNameMap); }); useMessageEvent(UserWardrobePageEvent, event => @@ -236,8 +254,10 @@ const useAvatarEditorState = () => if(!isVisible) return; const newAvatarModels: { [index: string]: IAvatarEditorCategory[] } = {}; + const buildModeDefault = 'default'; + const buildModeNft = 'nft'; - const buildCategory = (setType: string) => + const buildCategory = (setType: string, buildMode: string = buildModeDefault) => { const partItems: IAvatarEditorCategoryPartItem[] = []; const colorItems: IPartColor[][] = []; @@ -271,13 +291,20 @@ const useAvatarEditorState = () => if(!partSet || !partSet.isSelectable || ((partSet.gender !== gender) && (partSet.gender !== AvatarFigurePartType.UNISEX))) continue; - if(partSet.isSellable && figureSetIds.indexOf(partSet.id) === -1) continue; + const isNftPartSet = nftFigureSetIds.has(partSet.id); + + if((buildMode === buildModeDefault) && isNftPartSet) continue; + if((buildMode === buildModeNft) && !isNftPartSet) continue; + + const isSellableNotOwned = partSet.isSellable && figureSetIds.indexOf(partSet.id) === -1; + + if(isSellableNotOwned && (buildMode !== buildModeNft) && setType !== AvatarFigurePartType.PET) continue; let maxPaletteCount = 0; for(const part of partSet.parts) maxPaletteCount = Math.max(maxPaletteCount, part.colorLayerIndex); - partItems.push({ id: partSet.id, partSet, usesColor, maxPaletteCount }); + partItems.push({ id: partSet.id, partSet, usesColor, maxPaletteCount, isSellableNotOwned }); } partItems.sort(AvatarEditorPartSorter(false)); @@ -287,16 +314,31 @@ const useAvatarEditorState = () => return { setType, partItems, colorItems }; }; - newAvatarModels[AvatarEditorFigureCategory.GENERIC] = [ AvatarFigurePartType.HEAD ].map(setType => buildCategory(setType)); - newAvatarModels[AvatarEditorFigureCategory.HEAD] = [ AvatarFigurePartType.HAIR, AvatarFigurePartType.HEAD_ACCESSORY, AvatarFigurePartType.HEAD_ACCESSORY_EXTRA, AvatarFigurePartType.EYE_ACCESSORY, AvatarFigurePartType.FACE_ACCESSORY ].map(setType => buildCategory(setType)); - newAvatarModels[AvatarEditorFigureCategory.TORSO] = [ AvatarFigurePartType.CHEST, AvatarFigurePartType.CHEST_PRINT, AvatarFigurePartType.COAT_CHEST, AvatarFigurePartType.CHEST_ACCESSORY ].map(setType => buildCategory(setType)); - newAvatarModels[AvatarEditorFigureCategory.LEGS] = [ AvatarFigurePartType.LEGS, AvatarFigurePartType.SHOES, AvatarFigurePartType.WAIST_ACCESSORY ].map(setType => buildCategory(setType)); + newAvatarModels[AvatarEditorFigureCategory.GENERIC] = [ AvatarFigurePartType.HEAD ].map(setType => buildCategory(setType, buildModeDefault)); + newAvatarModels[AvatarEditorFigureCategory.HEAD] = [ AvatarFigurePartType.HAIR, AvatarFigurePartType.HEAD_ACCESSORY, AvatarFigurePartType.HEAD_ACCESSORY_EXTRA, AvatarFigurePartType.EYE_ACCESSORY, AvatarFigurePartType.FACE_ACCESSORY ].map(setType => buildCategory(setType, buildModeDefault)); + newAvatarModels[AvatarEditorFigureCategory.TORSO] = [ AvatarFigurePartType.CHEST, AvatarFigurePartType.CHEST_PRINT, AvatarFigurePartType.COAT_CHEST, AvatarFigurePartType.CHEST_ACCESSORY ].map(setType => buildCategory(setType, buildModeDefault)); + newAvatarModels[AvatarEditorFigureCategory.LEGS] = [ AvatarFigurePartType.LEGS, AvatarFigurePartType.SHOES, AvatarFigurePartType.WAIST_ACCESSORY ].map(setType => buildCategory(setType, buildModeDefault)); newAvatarModels[AvatarEditorFigureCategory.PETS] = [ AvatarFigurePartType.PET ].map(setType => buildCategory(setType)).filter(Boolean); + newAvatarModels[AvatarEditorFigureCategory.NFT] = [ + AvatarFigurePartType.HEAD, + AvatarFigurePartType.HAIR, + AvatarFigurePartType.HEAD_ACCESSORY, + AvatarFigurePartType.HEAD_ACCESSORY_EXTRA, + AvatarFigurePartType.EYE_ACCESSORY, + AvatarFigurePartType.FACE_ACCESSORY, + AvatarFigurePartType.CHEST, + AvatarFigurePartType.CHEST_PRINT, + AvatarFigurePartType.COAT_CHEST, + AvatarFigurePartType.CHEST_ACCESSORY, + AvatarFigurePartType.LEGS, + AvatarFigurePartType.SHOES, + AvatarFigurePartType.WAIST_ACCESSORY + ].map(setType => buildCategory(setType, buildModeNft)).filter(Boolean); newAvatarModels[AvatarEditorFigureCategory.WARDROBE] = []; setAvatarModels(newAvatarModels); setActiveModelKey(AvatarEditorFigureCategory.GENERIC); - }, [ isVisible, gender, figureSetIds ]); + }, [ isVisible, gender, figureSetIds, nftFigureSetIds ]); useEffect(() => { diff --git a/src/hooks/wired/useWired.ts b/src/hooks/wired/useWired.ts index e6e3b53..e35e211 100644 --- a/src/hooks/wired/useWired.ts +++ b/src/hooks/wired/useWired.ts @@ -75,14 +75,37 @@ const useWiredState = () => return rawValue.toLowerCase(); }; + const getComparableInteractionNames = (furniData: any): string[] => + { + if(!furniData) return []; + + const values = [ + getInteractionTypeName(furniData), + (typeof (furniData as any).className === 'string') ? (furniData as any).className.toLowerCase() : null, + (typeof (furniData as any).fullName === 'string') ? (furniData as any).fullName.toLowerCase() : null, + (typeof (furniData as any).name === 'string') ? (furniData as any).name.toLowerCase() : null + ]; + + return values.filter((value, index, array): value is string => !!value && (array.indexOf(value) === index)); + }; + + const matchesAllowedPattern = (value: string, pattern: string) => + { + const normalizedPattern = pattern.toLowerCase(); + + if(normalizedPattern.endsWith('*')) return value.startsWith(normalizedPattern.slice(0, -1)); + + return (normalizedPattern === value); + }; + const isAllowedInteraction = (furniData: any): boolean => { if(!allowedInteractionTypes || !allowedInteractionTypes.length) return true; - const interactionType = getInteractionTypeName(furniData); - if(!interactionType) return true; + const comparableNames = getComparableInteractionNames(furniData); + if(!comparableNames.length) return true; - return allowedInteractionTypes.some(type => (type && type.toLowerCase() === interactionType)); + return comparableNames.some(value => allowedInteractionTypes.some(type => !!type && matchesAllowedPattern(value, type))); }; const handleDisallowedInteraction = () => From 0ee4455d8d9ef3fc6331a3fecc74e1d80239b5eb Mon Sep 17 00:00:00 2001 From: duckietm Date: Thu, 26 Mar 2026 13:31:12 +0100 Subject: [PATCH 3/7] =?UTF-8?q?=F0=9F=86=99=20SSO=20failure=20detection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 30 +++++++++++++++++++--- src/components/loading/LoadingView.tsx | 16 ++++++++---- src/components/reconnect/ReconnectView.tsx | 24 +++++++---------- 3 files changed, 46 insertions(+), 24 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index c97d6fa..9608aa8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,17 +1,30 @@ -import { GetAssetManager, GetAvatarRenderManager, GetCommunication, GetConfiguration, GetLocalizationManager, GetRoomEngine, GetRoomSessionManager, GetSessionDataManager, GetSoundManager, GetStage, GetTexturePool, GetTicker, HabboWebTools, LegacyExternalInterface, LoadGameUrlEvent, NitroLogger, NitroVersion, PrepareRenderer } from '@nitrots/nitro-renderer'; -import { FC, useEffect, useState } from 'react'; +import { GetAssetManager, GetAvatarRenderManager, GetCommunication, GetConfiguration, GetLocalizationManager, GetRoomEngine, GetRoomSessionManager, GetSessionDataManager, GetSoundManager, GetStage, GetTexturePool, GetTicker, HabboWebTools, LegacyExternalInterface, LoadGameUrlEvent, NitroEventType, NitroLogger, NitroVersion, PrepareRenderer } from '@nitrots/nitro-renderer'; +import { FC, useCallback, useEffect, useState } from 'react'; 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'; +import { useMessageEvent, useNitroEvent } from './hooks'; NitroVersion.UI_VERSION = GetUIVersion(); export const App: FC<{}> = props => { const [ isReady, setIsReady ] = useState(false); + const [ errorMessage, setErrorMessage ] = useState(''); + const [ homeUrl, setHomeUrl ] = useState(''); + + const showSessionExpired = useCallback(() => + { + const baseUrl = window.location.origin + '/'; + setHomeUrl(baseUrl); + setErrorMessage('Your session has expired.\nPlease log in again to enter the hotel.'); + setIsReady(false); + }, []); + + // Listen for socket closed events (code 1000 "Bye" - server rejected SSO) + useNitroEvent(NitroEventType.SOCKET_CLOSED, showSessionExpired); useMessageEvent(LoadGameUrlEvent, event => { @@ -30,6 +43,14 @@ export const App: FC<{}> = props => { if(!window.NitroConfig) throw new Error('NitroConfig is not defined!'); + const ssoTicket = window.NitroConfig['sso.ticket']; + + if(!ssoTicket || ssoTicket === '') + { + showSessionExpired(); + return; + } + const renderer = await PrepareRenderer({ width: Math.floor(width), height: Math.floor(height), @@ -83,6 +104,7 @@ export const App: FC<{}> = props => catch(err) { NitroLogger.error(err); + showSessionExpired(); } }; @@ -92,7 +114,7 @@ export const App: FC<{}> = props => return ( { !isReady && - } + 0 } message={ errorMessage } homeUrl={ homeUrl } /> } { isReady && } diff --git a/src/components/loading/LoadingView.tsx b/src/components/loading/LoadingView.tsx index a9dc9bc..c8bb131 100644 --- a/src/components/loading/LoadingView.tsx +++ b/src/components/loading/LoadingView.tsx @@ -4,10 +4,11 @@ import { Base, Column, Text } from '../../common'; interface LoadingViewProps { isError?: boolean; message?: string; + homeUrl?: string; } export const LoadingView: FC = props => { - const { isError = false, message = '' } = props; + const { isError = false, message = '', homeUrl = '' } = props; return ( @@ -19,11 +20,16 @@ export const LoadingView: FC = props => { { isError && (message && message.length) ? - Something went wrong while loading - - { message } - + + { homeUrl && + + Back to Hotel + + } : diff --git a/src/components/reconnect/ReconnectView.tsx b/src/components/reconnect/ReconnectView.tsx index 4c13094..af8e896 100644 --- a/src/components/reconnect/ReconnectView.tsx +++ b/src/components/reconnect/ReconnectView.tsx @@ -48,14 +48,14 @@ export const ReconnectView: FC<{}> = props => const handleReload = useCallback(() => { - window.location.reload(); + window.location.href = window.location.origin + '/'; }, []); const handleGoHome = useCallback(() => { sessionStorage.removeItem('nitro.session.lastRoomId'); sessionStorage.removeItem('nitro.session.lastRoomPassword'); - window.location.reload(); + window.location.href = window.location.origin + '/'; }, []); if(!isReconnecting && !hasFailed) return null; @@ -92,24 +92,18 @@ export const ReconnectView: FC<{}> = props => <> - Connection failed + Session expired - Unable to reconnect to the server after multiple attempts. + Your session has expired. Please log in again to enter the hotel. - - Reload Page - - - Go to Home - + Back to Hotel + ) } From 7c39dcb513bc3bf8ac472138b1b76fe19890914c Mon Sep 17 00:00:00 2001 From: duckietm Date: Thu, 26 Mar 2026 16:29:27 +0100 Subject: [PATCH 4/7] =?UTF-8?q?=F0=9F=86=99=20Small=20fixes=20NFT=20Clothi?= =?UTF-8?q?ng?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../figure-set/AvatarEditorFigureSetItemView.tsx | 12 ++++++------ src/hooks/avatar-editor/useAvatarEditor.ts | 4 +++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx b/src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx index 9201ce2..a08321d 100644 --- a/src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx +++ b/src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx @@ -1,4 +1,4 @@ -import { AvatarFigurePartType } from '@nitrots/nitro-renderer'; +import { AvatarEditorFigureCategory, AvatarFigurePartType } from '@nitrots/nitro-renderer'; import { FC, useEffect, useState } from 'react'; import { AvatarEditorThumbnailsHelper, GetClubMemberLevel, GetConfigurationValue, IAvatarEditorCategoryPartItem } from '../../../api'; import { LayoutCurrencyIcon, LayoutGridItemProps } from '../../../common'; @@ -15,7 +15,7 @@ export const AvatarEditorFigureSetItemView: FC<{ { const { setType = null, partItem = null, isSelected = false, width = '100%', ...rest } = props; const [ assetUrl, setAssetUrl ] = useState(''); - const { selectedColorParts = null, getFigureStringWithFace = null } = useAvatarEditor(); + const { activeModelKey = '', selectedColorParts = null, getFigureStringWithFace = null } = useAvatarEditor(); const clubLevel = partItem.partSet?.clubLevel ?? 0; const isHC = !GetConfigurationValue('hc.disabled', false) && (clubLevel > 0); @@ -34,7 +34,7 @@ export const AvatarEditorFigureSetItemView: FC<{ let url: string = null; - if(setType === AvatarFigurePartType.HEAD) + if(setType === AvatarFigurePartType.HEAD && activeModelKey !== AvatarEditorFigureCategory.NFT) { url = await AvatarEditorThumbnailsHelper.buildForFace(getFigureStringWithFace(partItem.id), partIsLocked || isSellableNotOwned); } @@ -53,16 +53,16 @@ export const AvatarEditorFigureSetItemView: FC<{ }; loadImage(); - }, [ setType, partItem, selectedColorParts, getFigureStringWithFace, isSellableNotOwned ]); + }, [ setType, partItem, selectedColorParts, getFigureStringWithFace, isSellableNotOwned, activeModelKey ]); if(!partItem) return null; return ( - + { !partItem.isClear && isHC && } { partItem.isClear && } { !partItem.isClear && partItem.partSet.isSellable && !isSellableNotOwned && } - { isSellableNotOwned && + { !partItem.isClear && isSellableNotOwned &&
} diff --git a/src/hooks/avatar-editor/useAvatarEditor.ts b/src/hooks/avatar-editor/useAvatarEditor.ts index 3046605..19a39bb 100644 --- a/src/hooks/avatar-editor/useAvatarEditor.ts +++ b/src/hooks/avatar-editor/useAvatarEditor.ts @@ -291,7 +291,9 @@ const useAvatarEditorState = () => if(!partSet || !partSet.isSelectable || ((partSet.gender !== gender) && (partSet.gender !== AvatarFigurePartType.UNISEX))) continue; - const isNftPartSet = nftFigureSetIds.has(partSet.id); + const isNftPartSet = nftFigureSetIds.size > 0 + ? nftFigureSetIds.has(partSet.id) + : GetAvatarRenderManager().downloadManager.isNftPartSet(partSet); if((buildMode === buildModeDefault) && isNftPartSet) continue; if((buildMode === buildModeNft) && !isNftPartSet) continue; From a095d818a36c4b43ead29d59ec9df7338de9b02d Mon Sep 17 00:00:00 2001 From: duckietm Date: Fri, 27 Mar 2026 09:53:55 +0100 Subject: [PATCH 5/7] =?UTF-8?q?=F0=9F=86=99=20Fix=20move=20avatar-editor?= =?UTF-8?q?=20to=20Tailwind?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../avatar-editor/AvatarEditorIcon.tsx | 90 +++++--- src/css/index.css | 216 +----------------- 2 files changed, 65 insertions(+), 241 deletions(-) diff --git a/src/components/avatar-editor/AvatarEditorIcon.tsx b/src/components/avatar-editor/AvatarEditorIcon.tsx index f5623ed..e992373 100644 --- a/src/components/avatar-editor/AvatarEditorIcon.tsx +++ b/src/components/avatar-editor/AvatarEditorIcon.tsx @@ -1,45 +1,81 @@ import { DetailedHTMLProps, HTMLAttributes, PropsWithChildren, forwardRef } from 'react'; import { classNames } from '../../layout'; -type AvatarIconType = 'male' | 'female' | 'clear' | 'sellable'; +import arrowLeftIcon from '../../assets/images/avatareditor/arrow-left-icon.png'; +import arrowRightIcon from '../../assets/images/avatareditor/arrow-right-icon.png'; +import caIcon from '../../assets/images/avatareditor/ca-icon.png'; +import caSelectedIcon from '../../assets/images/avatareditor/ca-selected-icon.png'; +import ccIcon from '../../assets/images/avatareditor/cc-icon.png'; +import ccSelectedIcon from '../../assets/images/avatareditor/cc-selected-icon.png'; +import chIcon from '../../assets/images/avatareditor/ch-icon.png'; +import chSelectedIcon from '../../assets/images/avatareditor/ch-selected-icon.png'; +import clearIcon from '../../assets/images/avatareditor/clear-icon.png'; +import cpIcon from '../../assets/images/avatareditor/cp-icon.png'; +import cpSelectedIcon from '../../assets/images/avatareditor/cp-selected-icon.png'; +import eaIcon from '../../assets/images/avatareditor/ea-icon.png'; +import eaSelectedIcon from '../../assets/images/avatareditor/ea-selected-icon.png'; +import faIcon from '../../assets/images/avatareditor/fa-icon.png'; +import faSelectedIcon from '../../assets/images/avatareditor/fa-selected-icon.png'; +import femaleIcon from '../../assets/images/avatareditor/female-icon.png'; +import femaleSelectedIcon from '../../assets/images/avatareditor/female-selected-icon.png'; +import haIcon from '../../assets/images/avatareditor/ha-icon.png'; +import haSelectedIcon from '../../assets/images/avatareditor/ha-selected-icon.png'; +import heIcon from '../../assets/images/avatareditor/he-icon.png'; +import heSelectedIcon from '../../assets/images/avatareditor/he-selected-icon.png'; +import hrIcon from '../../assets/images/avatareditor/hr-icon.png'; +import hrSelectedIcon from '../../assets/images/avatareditor/hr-selected-icon.png'; +import lgIcon from '../../assets/images/avatareditor/lg-icon.png'; +import lgSelectedIcon from '../../assets/images/avatareditor/lg-selected-icon.png'; +import maleIcon from '../../assets/images/avatareditor/male-icon.png'; +import maleSelectedIcon from '../../assets/images/avatareditor/male-selected-icon.png'; +import sellableIcon from '../../assets/images/avatareditor/sellable-icon.png'; +import shIcon from '../../assets/images/avatareditor/sh-icon.png'; +import shSelectedIcon from '../../assets/images/avatareditor/sh-selected-icon.png'; +import waIcon from '../../assets/images/avatareditor/wa-icon.png'; +import waSelectedIcon from '../../assets/images/avatareditor/wa-selected-icon.png'; + +const ICON_MAP: Record = { + 'arrow-left': { normal: arrowLeftIcon }, + 'arrow-right': { normal: arrowRightIcon }, + 'ca': { normal: caIcon, selected: caSelectedIcon }, + 'cc': { normal: ccIcon, selected: ccSelectedIcon }, + 'ch': { normal: chIcon, selected: chSelectedIcon }, + 'clear': { normal: clearIcon }, + 'cp': { normal: cpIcon, selected: cpSelectedIcon }, + 'ea': { normal: eaIcon, selected: eaSelectedIcon }, + 'fa': { normal: faIcon, selected: faSelectedIcon }, + 'female': { normal: femaleIcon, selected: femaleSelectedIcon }, + 'ha': { normal: haIcon, selected: haSelectedIcon }, + 'he': { normal: heIcon, selected: heSelectedIcon }, + 'hr': { normal: hrIcon, selected: hrSelectedIcon }, + 'lg': { normal: lgIcon, selected: lgSelectedIcon }, + 'male': { normal: maleIcon, selected: maleSelectedIcon }, + 'sellable': { normal: sellableIcon }, + 'sh': { normal: shIcon, selected: shSelectedIcon }, + 'wa': { normal: waIcon, selected: waSelectedIcon }, +}; export const AvatarEditorIcon = forwardRef & DetailedHTMLProps, HTMLDivElement>>((props, ref) => { - const { icon = null, selected = false, className = null, ...rest } = props; + const { icon = null, selected = false, className = null, children, ...rest } = props; - /* - switch (icon) - { - case 'male': + const iconEntry = icon ? ICON_MAP[icon] : null; + if(!iconEntry) return null; - break; + const src = (selected && iconEntry.selected) ? iconEntry.selected : iconEntry.normal; - case 'arrow-left': - - break; - - default: - //statements; - break; - - } -*/ return (
+ className={ classNames('flex items-center justify-center cursor-pointer', className) } + { ...rest }> + { + { children } +
); }); diff --git a/src/css/index.css b/src/css/index.css index 6c62fc7..fbfc8a4 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -447,219 +447,7 @@ body { } } -.nitro-avatar-editor-spritesheet { - background: url('@/assets/images/avatareditor/avatar-editor-spritesheet.png') transparent no-repeat; - - &.arrow-left-icon { - width: 28px; - height: 21px; - background-position: -226px -131px; - } - - &.arrow-right-icon { - width: 28px; - height: 21px; - background-position: -226px -162px; - } - - &.ca-icon { - width: 25px; - height: 25px; - background-position: -226px -61px; - - &.selected { - width: 25px; - height: 25px; - background-position: -226px -96px; - } - } - - &.cc-icon { - width: 31px; - height: 29px; - background-position: -145px -5px; - - &.selected { - width: 31px; - height: 29px; - background-position: -145px -44px; - } - } - - &.ch-icon { - width: 29px; - height: 24px; - background-position: -186px -39px; - - &.selected { - width: 29px; - height: 24px; - background-position: -186px -73px; - } - } - - &.clear-icon { - width: 27px; - height: 27px; - background-position: -145px -157px; - } - - &.cp-icon { - width: 30px; - height: 24px; - background-position: -145px -264px; - - &.selected { - width: 30px; - height: 24px; - background-position: -186px -5px; - } - } - - - &.ea-icon { - width: 35px; - height: 16px; - background-position: -226px -193px; - - &.selected { - width: 35px; - height: 16px; - background-position: -226px -219px; - } - } - - &.fa-icon { - width: 27px; - height: 20px; - background-position: -186px -137px; - - &.selected { - width: 27px; - height: 20px; - background-position: -186px -107px; - } - } - - &.female-icon { - width: 18px; - height: 27px; - background-position: -186px -202px; - - &.selected { - width: 18px; - height: 27px; - background-position: -186px -239px; - } - } - - &.ha-icon { - width: 25px; - height: 22px; - background-position: -226px -245px; - - &.selected { - width: 25px; - height: 22px; - background-position: -226px -277px; - } - } - - &.he-icon { - width: 31px; - height: 27px; - background-position: -145px -83px; - - &.selected { - width: 31px; - height: 27px; - background-position: -145px -120px; - } - } - - &.hr-icon { - width: 29px; - height: 25px; - background-position: -145px -194px; - - &.selected { - width: 29px; - height: 25px; - background-position: -145px -229px; - } - } - - &.lg-icon { - width: 19px; - height: 20px; - background-position: -303px -45px; - - &.selected { - width: 19px; - height: 20px; - background-position: -303px -75px; - } - } - - &.loading-icon { - width: 21px; - height: 25px; - background-position: -186px -167px; - } - - - &.male-icon { - width: 21px; - height: 21px; - background-position: -186px -276px; - - &.selected { - width: 21px; - height: 21px; - background-position: -272px -5px; - } - } - - - &.sellable-icon { - width: 17px; - height: 15px; - background-position: -303px -105px; - } - - - &.sh-icon { - width: 37px; - height: 10px; - background-position: -303px -5px; - - &.selected { - width: 37px; - height: 10px; - background-position: -303px -25px; - } - } - - - &.spotlight-icon { - width: 130px; - height: 305px; - background-position: -5px -5px; - } - - - &.wa-icon { - width: 36px; - height: 18px; - background-position: -226px -5px; - - &.selected { - width: 36px; - height: 18px; - background-position: -226px -33px; - } - } -} +/* Avatar editor icons are now rendered as tags via AvatarEditorIcon.tsx */ .nitro-avatar-editor-wardrobe-figure-preview { background-color: #677181; @@ -710,7 +498,7 @@ body { .category-item { - height: 40px; + height: 32px; } .figure-preview-container { From a4d476410545987f45fd951eaa732d4a9b182681 Mon Sep 17 00:00:00 2001 From: duckietm Date: Fri, 27 Mar 2026 10:08:02 +0100 Subject: [PATCH 6/7] =?UTF-8?q?=F0=9F=86=99=20Moved=20catalogue=20view=20t?= =?UTF-8?q?o=20Tailwind?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/catalog/CatalogModernView.tsx | 4 +- .../views/catalog-icon/CatalogIconView.tsx | 8 +-- .../catalog-rail/CatalogRailItemView.tsx | 4 +- .../views/favorites/CatalogFavoritesView.tsx | 2 +- .../navigation/CatalogNavigationItemView.tsx | 2 +- src/css/index.css | 50 ++++++++----------- 6 files changed, 32 insertions(+), 38 deletions(-) diff --git a/src/components/catalog/CatalogModernView.tsx b/src/components/catalog/CatalogModernView.tsx index ec19b70..95368b0 100644 --- a/src/components/catalog/CatalogModernView.tsx +++ b/src/components/catalog/CatalogModernView.tsx @@ -111,7 +111,7 @@ const CatalogModernViewInner: FC<{}> = () => className={ `flex items-center gap-2 mx-1 px-1.5 py-1.5 rounded cursor-pointer transition-all duration-150 ${ showFavorites ? 'bg-primary text-white' : 'hover:bg-card-grid-item-active' }` } onClick={ () => setShowFavorites(!showFavorites) } > -
+
0 ? 'text-danger' : 'text-muted' }` } /> { totalFavs > 0 && @@ -163,7 +163,7 @@ const CatalogModernViewInner: FC<{}> = () => activateNode(child); } } > -
+
{ isHidden && }
diff --git a/src/components/catalog/views/catalog-icon/CatalogIconView.tsx b/src/components/catalog/views/catalog-icon/CatalogIconView.tsx index 0178662..4e5ed5a 100644 --- a/src/components/catalog/views/catalog-icon/CatalogIconView.tsx +++ b/src/components/catalog/views/catalog-icon/CatalogIconView.tsx @@ -1,20 +1,20 @@ import { FC, useMemo } from 'react'; import { GetConfigurationValue } from '../../../../api'; -import { LayoutImage } from '../../../../common'; export interface CatalogIconViewProps { icon: number; + className?: string; } export const CatalogIconView: FC = props => { - const { icon = 0 } = props; + const { icon = 0, className = '' } = props; - const getIconUrl = useMemo(() => + const iconUrl = useMemo(() => { return ((GetConfigurationValue('catalog.asset.icon.url')).replace('%name%', icon.toString())); }, [ icon ]); - return ; + return ; }; diff --git a/src/components/catalog/views/catalog-rail/CatalogRailItemView.tsx b/src/components/catalog/views/catalog-rail/CatalogRailItemView.tsx index 3b1cd21..4d1e38e 100644 --- a/src/components/catalog/views/catalog-rail/CatalogRailItemView.tsx +++ b/src/components/catalog/views/catalog-rail/CatalogRailItemView.tsx @@ -19,8 +19,8 @@ export const CatalogRailItemView: FC = props => title={ node.localization } onClick={ onClick } > -
- +
+
{ node.localization } diff --git a/src/components/catalog/views/favorites/CatalogFavoritesView.tsx b/src/components/catalog/views/favorites/CatalogFavoritesView.tsx index 676f771..77b02e6 100644 --- a/src/components/catalog/views/favorites/CatalogFavoritesView.tsx +++ b/src/components/catalog/views/favorites/CatalogFavoritesView.tsx @@ -121,7 +121,7 @@ export const CatalogFavoritesView: FC = props => onClick={ () => { openPageByOfferId(fav.offerId); onClose(); } } > { /* Furni icon */ } -
+
{ fav.iconUrl ? : fav.nodeIconId !== null diff --git a/src/components/catalog/views/navigation/CatalogNavigationItemView.tsx b/src/components/catalog/views/navigation/CatalogNavigationItemView.tsx index d31d801..f897cad 100644 --- a/src/components/catalog/views/navigation/CatalogNavigationItemView.tsx +++ b/src/components/catalog/views/navigation/CatalogNavigationItemView.tsx @@ -86,7 +86,7 @@ export const CatalogNavigationItemView: FC = pro > { adminMode && } -
+
{ node.localization } diff --git a/src/css/index.css b/src/css/index.css index fbfc8a4..709acdb 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -30,51 +30,45 @@ body { } ::-webkit-scrollbar { - width: .5rem + width: .625rem; } ::-webkit-scrollbar:horizontal { - height: .5rem + height: .625rem; } ::-webkit-scrollbar:not(:horizontal) { - width: .5rem + width: .625rem; } -::-webkit-scrollbar-track:horizontal { - border-bottom: .25rem solid rgba(0, 0, 0, .1) +::-webkit-scrollbar-track { + background: rgba(0, 0, 0, .08); + border-radius: .5rem; } -::-webkit-scrollbar-track:not(:horizontal) { - border-right: .25rem solid rgba(0, 0, 0, .1) +::-webkit-scrollbar-thumb { + background: rgba(30, 114, 149, .35); + border-radius: .5rem; + border: 2px solid transparent; + background-clip: padding-box; } -::-webkit-scrollbar-thumb:horizontal { - border-bottom: .25rem solid rgba(30, 114, 149, .4) +::-webkit-scrollbar-thumb:hover { + background: rgba(30, 114, 149, .6); + border-radius: .5rem; + border: 2px solid transparent; + background-clip: padding-box; } -::-webkit-scrollbar-thumb:horizontal:hover { - border-bottom: .25rem solid rgba(30, 114, 149, .8) -} - -::-webkit-scrollbar-thumb:horizontal:active { - border-bottom: .25rem solid #185D79 -} - -::-webkit-scrollbar-thumb:not(:horizontal) { - border-right: .25rem solid rgba(30, 114, 149, .4) -} - -::-webkit-scrollbar-thumb:not(:horizontal):hover { - border-right: .25rem solid rgba(30, 114, 149, .8) -} - -::-webkit-scrollbar-thumb:not(:horizontal):active { - border-right: .25rem solid #185D79 +::-webkit-scrollbar-thumb:active { + background: #185D79; + border-radius: .5rem; + border: 2px solid transparent; + background-clip: padding-box; } ::-webkit-scrollbar-corner { - background: rgba(0, 0, 0, .1) + background: rgba(0, 0, 0, .08); } @layer components { From bbe71e9753ce00b68c57216f18a2cf9df5ccbb8c Mon Sep 17 00:00:00 2001 From: duckietm Date: Fri, 27 Mar 2026 13:38:03 +0100 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=86=99=20Cleanup=20Furni-Edit=20&=20F?= =?UTF-8?q?ix=20the=20avatar-editor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../avatar/AvatarEditorThumbnailsHelper.ts | 20 +- src/components/MainView.tsx | 2 - .../AvatarEditorFigureSetItemView.tsx | 28 +- .../figure-set/AvatarEditorFigureSetView.tsx | 5 +- .../infostand/InfoStandWidgetFurniView.tsx | 13 - src/components/toolbar/ToolbarView.tsx | 2 - src/hooks/furni-editor/index.ts | 1 - src/hooks/furni-editor/useFurniEditor.ts | 239 ------------------ 8 files changed, 27 insertions(+), 283 deletions(-) delete mode 100644 src/hooks/furni-editor/index.ts delete mode 100644 src/hooks/furni-editor/useFurniEditor.ts diff --git a/src/api/avatar/AvatarEditorThumbnailsHelper.ts b/src/api/avatar/AvatarEditorThumbnailsHelper.ts index f9ce18f..bffc418 100644 --- a/src/api/avatar/AvatarEditorThumbnailsHelper.ts +++ b/src/api/avatar/AvatarEditorThumbnailsHelper.ts @@ -109,22 +109,6 @@ export class AvatarEditorThumbnailsHelper container.addChild(sprite); } - if(container.children.length > 0) - { - const isPetPart = parts.some(p => p.type === 'pt' || p.type === 'ptl' || p.type === 'ptr'); - - if(isPetPart) - { - const bounds = container.getBounds(); - - for(const child of container.children) - { - (child as NitroSprite).position.x -= bounds.x; - (child as NitroSprite).position.y -= bounds.y; - } - } - } - return container; }; @@ -133,9 +117,9 @@ export class AvatarEditorThumbnailsHelper const resetFigure = async (figure: string) => { const container = buildContainer(part, useColors, partColors, isDisabled); - const imageUrl = await TextureUtils.generateImageUrl(container); + const imageUrl = await TextureUtils.generateImageUrl({ target: container, resolution: 1 }); - AvatarEditorThumbnailsHelper.THUMBNAIL_CACHE.set(thumbnailKey, imageUrl); + if(imageUrl) AvatarEditorThumbnailsHelper.THUMBNAIL_CACHE.set(thumbnailKey, imageUrl); resolve(imageUrl); }; diff --git a/src/components/MainView.tsx b/src/components/MainView.tsx index 4a39a59..b03d21d 100644 --- a/src/components/MainView.tsx +++ b/src/components/MainView.tsx @@ -9,7 +9,6 @@ import { CampaignView } from './campaign/CampaignView'; import { CatalogView } from './catalog/CatalogView'; import { ChatHistoryView } from './chat-history/ChatHistoryView'; import { FloorplanEditorView } from './floorplan-editor/FloorplanEditorView'; -import { FurniEditorView } from './furni-editor/FurniEditorView'; import { FriendsView } from './friends/FriendsView'; import { GameCenterView } from './game-center/GameCenterView'; import { GroupsView } from './groups/GroupsView'; @@ -121,7 +120,6 @@ export const MainView: FC<{}> = props => - diff --git a/src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx b/src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx index a08321d..789dc74 100644 --- a/src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx +++ b/src/components/avatar-editor/figure-set/AvatarEditorFigureSetItemView.tsx @@ -1,4 +1,3 @@ -import { AvatarEditorFigureCategory, AvatarFigurePartType } from '@nitrots/nitro-renderer'; import { FC, useEffect, useState } from 'react'; import { AvatarEditorThumbnailsHelper, GetClubMemberLevel, GetConfigurationValue, IAvatarEditorCategoryPartItem } from '../../../api'; import { LayoutCurrencyIcon, LayoutGridItemProps } from '../../../common'; @@ -15,7 +14,7 @@ export const AvatarEditorFigureSetItemView: FC<{ { const { setType = null, partItem = null, isSelected = false, width = '100%', ...rest } = props; const [ assetUrl, setAssetUrl ] = useState(''); - const { activeModelKey = '', selectedColorParts = null, getFigureStringWithFace = null } = useAvatarEditor(); + const { selectedColorParts = null, getFigureStringWithFace = null } = useAvatarEditor(); const clubLevel = partItem.partSet?.clubLevel ?? 0; const isHC = !GetConfigurationValue('hc.disabled', false) && (clubLevel > 0); @@ -24,7 +23,9 @@ export const AvatarEditorFigureSetItemView: FC<{ useEffect(() => { - if(!setType || !setType.length || !partItem) return; + setAssetUrl(''); + + if(!setType || !setType.length || !partItem || partItem.isClear) return; const loadImage = async () => { @@ -34,7 +35,7 @@ export const AvatarEditorFigureSetItemView: FC<{ let url: string = null; - if(setType === AvatarFigurePartType.HEAD && activeModelKey !== AvatarEditorFigureCategory.NFT) + if(setType === 'hd') { url = await AvatarEditorThumbnailsHelper.buildForFace(getFigureStringWithFace(partItem.id), partIsLocked || isSellableNotOwned); } @@ -53,12 +54,27 @@ export const AvatarEditorFigureSetItemView: FC<{ }; loadImage(); - }, [ setType, partItem, selectedColorParts, getFigureStringWithFace, isSellableNotOwned, activeModelKey ]); + }, [ setType, partItem, selectedColorParts, getFigureStringWithFace, isSellableNotOwned ]); if(!partItem) return null; + const isHead = (setType === 'hd'); + return ( - + + { !partItem.isClear && assetUrl && !isHead && + } { !partItem.isClear && isHC && } { partItem.isClear && } { !partItem.isClear && partItem.partSet.isSellable && !isSellableNotOwned && } diff --git a/src/components/avatar-editor/figure-set/AvatarEditorFigureSetView.tsx b/src/components/avatar-editor/figure-set/AvatarEditorFigureSetView.tsx index 7d7bd68..179f894 100644 --- a/src/components/avatar-editor/figure-set/AvatarEditorFigureSetView.tsx +++ b/src/components/avatar-editor/figure-set/AvatarEditorFigureSetView.tsx @@ -7,9 +7,10 @@ import { AvatarEditorFigureSetItemView } from './AvatarEditorFigureSetItemView'; export const AvatarEditorFigureSetView: FC<{ category: IAvatarEditorCategory; columnCount: number; + estimateSize?: number; }> = props => { - const { category = null, columnCount = 3 } = props; + const { category = null, columnCount = 3, estimateSize = 50 } = props; const { selectedParts = null, selectEditorPart } = useAvatarEditor(); const isPartItemSelected = (partItem: IAvatarEditorCategoryPartItem) => @@ -29,7 +30,7 @@ export const AvatarEditorFigureSetView: FC<{ }; return ( - columnCount={ columnCount } estimateSize={ 50 } itemRender={ (item: IAvatarEditorCategoryPartItem) => + columnCount={ columnCount } estimateSize={ estimateSize } itemRender={ (item: IAvatarEditorCategoryPartItem) => { if(!item) return null; diff --git a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx index 48e9b84..98604a6 100644 --- a/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx +++ b/src/components/room/widgets/avatar-info/infostand/InfoStandWidgetFurniView.tsx @@ -574,19 +574,6 @@ export const InfoStandWidgetFurniView: FC = props onClick={ () => setDropdownOpen(!dropdownOpen) }> { dropdownOpen ? `${LocalizeText('widget.furni.present.close')} Buildtools` : `${LocalizeText('navigator.roomsettings.doormode.open')} Buildtools` } - { dropdownOpen &&
{ /* Left panel: position + rotation */ } diff --git a/src/components/toolbar/ToolbarView.tsx b/src/components/toolbar/ToolbarView.tsx index fbb5ae8..ea9fb16 100644 --- a/src/components/toolbar/ToolbarView.tsx +++ b/src/components/toolbar/ToolbarView.tsx @@ -96,8 +96,6 @@ export const ToolbarView: FC<{ isInRoom: boolean }> = props => CreateLinkEvent('camera/toggle') } /> } { isMod && CreateLinkEvent('mod-tools/toggle') } /> } - { isMod && - CreateLinkEvent('furni-editor/toggle') } /> } diff --git a/src/hooks/furni-editor/index.ts b/src/hooks/furni-editor/index.ts deleted file mode 100644 index 47ce6ef..0000000 --- a/src/hooks/furni-editor/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './useFurniEditor'; diff --git a/src/hooks/furni-editor/useFurniEditor.ts b/src/hooks/furni-editor/useFurniEditor.ts deleted file mode 100644 index e4258e5..0000000 --- a/src/hooks/furni-editor/useFurniEditor.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { useCallback, useState } from 'react'; - -export interface FurniItem -{ - id: number; - spriteId: number; - itemName: string; - publicName: string; - type: string; - width: number; - length: number; - stackHeight: number; - allowStack: boolean; - allowWalk: boolean; - allowSit: boolean; - allowLay: boolean; - interactionType: string; - interactionModesCount: number; -} - -export interface FurniDetail extends FurniItem -{ - allowGift: boolean; - allowTrade: boolean; - allowRecycle: boolean; - allowMarketplaceSell: boolean; - allowInventoryStack: boolean; - vendingIds: string; - customparams: string; - effectIdMale: number; - effectIdFemale: number; - clothingOnWalk: string; - multiheight: string; - description: string; - usageCount: number; -} - -export interface CatalogRef -{ - id: number; - catalogName: string; - costCredits: number; - costPoints: number; - pointsType: number; - pageId: number; - pageName: string; -} - -const API_BASE = '/api/admin/furni-editor'; - -async function apiFetch(url: string, options?: RequestInit): Promise -{ - const res = await fetch(url, { credentials: 'include', ...options }); - const data = await res.json(); - - if(!res.ok || data.error) throw new Error(data.error || 'API error'); - - return data; -} - -export const useFurniEditor = () => -{ - const [ items, setItems ] = useState([]); - const [ total, setTotal ] = useState(0); - const [ page, setPage ] = useState(1); - const [ loading, setLoading ] = useState(false); - const [ error, setError ] = useState(null); - const [ selectedItem, setSelectedItem ] = useState(null); - const [ catalogItems, setCatalogItems ] = useState([]); - const [ interactions, setInteractions ] = useState([]); - const [ furniDataEntry, setFurniDataEntry ] = useState | null>(null); - - const clearError = useCallback(() => setError(null), []); - - const searchItems = useCallback(async (query: string, type: string, pg: number) => - { - setLoading(true); - setError(null); - - try - { - const params = new URLSearchParams({ q: query, limit: '20', page: String(pg) }); - - if(type) params.set('type', type); - - const data = await apiFetch<{ items: FurniItem[]; total: number; page: number }>(`${ API_BASE }?${ params }`); - - setItems(data.items); - setTotal(data.total); - setPage(data.page); - } - catch(e: any) - { - setError(e.message); - } - finally - { - setLoading(false); - } - }, []); - - const loadDetail = useCallback(async (id: number): Promise => - { - setLoading(true); - setError(null); - - try - { - const data = await apiFetch<{ item: FurniDetail; catalogItems: CatalogRef[]; furniDataEntry: Record | null }>(`${ API_BASE }/detail?id=${ id }`); - - setSelectedItem(data.item); - setCatalogItems(data.catalogItems); - setFurniDataEntry(data.furniDataEntry); - - return true; - } - catch(e: any) - { - setError(e.message); - - return false; - } - finally - { - setLoading(false); - } - }, []); - - const updateItem = useCallback(async (id: number, fields: Record) => - { - setLoading(true); - setError(null); - - try - { - await apiFetch(`${ API_BASE }/update?id=${ id }`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(fields) - }); - - return true; - } - catch(e: any) - { - setError(e.message); - - return false; - } - finally - { - setLoading(false); - } - }, []); - - const createItem = useCallback(async (fields: Record) => - { - setLoading(true); - setError(null); - - try - { - const data = await apiFetch<{ id: number }>(`${ API_BASE }`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(fields) - }); - - return data.id; - } - catch(e: any) - { - setError(e.message); - - return null; - } - finally - { - setLoading(false); - } - }, []); - - const deleteItem = useCallback(async (id: number) => - { - setLoading(true); - setError(null); - - try - { - await apiFetch(`${ API_BASE }/delete?id=${ id }`, { method: 'POST' }); - - return true; - } - catch(e: any) - { - setError(e.message); - - return false; - } - finally - { - setLoading(false); - } - }, []); - - const loadInteractions = useCallback(async () => - { - try - { - const data = await apiFetch<{ interactions: Array }>(`${ API_BASE }/interactions`); - - setInteractions(data.interactions.map(i => typeof i === 'string' ? i : i.name)); - } - catch {} - }, []); - - const loadBySpriteId = useCallback(async (spriteId: number): Promise => - { - try - { - const data = await apiFetch<{ id: number }>(`${ API_BASE }/by-sprite?spriteId=${ spriteId }`); - - return await loadDetail(data.id); - } - catch(e: any) - { - setError(e.message); - - return false; - } - }, [ loadDetail ]); - - return { - items, total, page, loading, error, clearError, - selectedItem, setSelectedItem, catalogItems, furniDataEntry, - interactions, - searchItems, loadDetail, loadBySpriteId, updateItem, createItem, deleteItem, loadInteractions - }; -};