Merge pull request #201 from duckietm/Dev

🆙 Catalog & Inventory preview now to original
This commit is contained in:
DuckieTM
2026-06-06 08:11:29 +02:00
committed by GitHub
8 changed files with 186 additions and 253 deletions
@@ -8,6 +8,14 @@ export const LayoutRoomPreviewerView: FC<{
{ {
const { roomPreviewer = null, height = 0 } = props; const { roomPreviewer = null, height = 0 } = props;
const elementRef = useRef<HTMLDivElement>(null); const elementRef = useRef<HTMLDivElement>(null);
// Counter that disables further renders once Pixi throws in this
// previewer too many times in a row. The Pixi v8 null-texture bug
// (see src/pixiPatch.ts) is mostly absorbed at the prototype level,
// but any stray throw still cascades every animation frame. Allow
// a small number of consecutive failures so a transient bad frame
// self-recovers; permanently disable only if the previewer is truly
// wedged, which is what produces the "disabling further renders"
// log the user sees.
const renderFailuresRef = useRef(0); const renderFailuresRef = useRef(0);
const MAX_RENDER_FAILURES = 6; const MAX_RENDER_FAILURES = 6;
@@ -62,6 +70,8 @@ export const LayoutRoomPreviewerView: FC<{
canvas.height = 0; canvas.height = 0;
elementRef.current.style.backgroundImage = `url(${ base64 })`; elementRef.current.style.backgroundImage = `url(${ base64 })`;
// A successful paint is the signal we've recovered from
// a transient bad frame; reset the failure counter.
renderFailuresRef.current = 0; renderFailuresRef.current = 0;
} }
catch(error) catch(error)
@@ -1,4 +1,4 @@
import { GetAvatarRenderManager, GetSessionDataManager, Vector3d } from '@nitrots/nitro-renderer'; import { GetAvatarRenderManager, GetRoomEngine, GetSessionDataManager, RoomObjectVariable, Vector3d } from '@nitrots/nitro-renderer';
import { FC, useEffect } from 'react'; import { FC, useEffect } from 'react';
import { BuildPurchasableClothingFigure, FurniCategory, Offer, ProductTypeEnum } from '../../../../../api'; import { BuildPurchasableClothingFigure, FurniCategory, Offer, ProductTypeEnum } from '../../../../../api';
import { AutoGrid, Column, LayoutGridItem, LayoutRoomPreviewerView } from '../../../../../common'; import { AutoGrid, Column, LayoutGridItem, LayoutRoomPreviewerView } from '../../../../../common';
@@ -19,7 +19,25 @@ export const CatalogViewProductWidgetView: FC<{}> = props =>
if(!product) return; if(!product) return;
roomPreviewer.reset(false); roomPreviewer.reset(false);
roomPreviewer.updateObjectRoom('default', 'default', 'default');
// Mirror the user's current room so the catalog preview shows
// the item against the wallpaper / floor / landscape they
// actually have decorated. Same approach as
// InventoryFurnitureView - read the active room's pattern ids
// off the room engine, fall back to '101' / '101' / '1.1' if
// the user isn't in a room yet (those are real Habbo pattern
// ids, the literal 'default' we used before is not and made
// the previewer fall back to blank white surfaces).
const roomEngine = GetRoomEngine();
let floorType = roomEngine.getRoomInstanceVariable<string>(roomEngine.activeRoomId, RoomObjectVariable.ROOM_FLOOR_TYPE);
let wallType = roomEngine.getRoomInstanceVariable<string>(roomEngine.activeRoomId, RoomObjectVariable.ROOM_WALL_TYPE);
let landscapeType = roomEngine.getRoomInstanceVariable<string>(roomEngine.activeRoomId, RoomObjectVariable.ROOM_LANDSCAPE_TYPE);
floorType = (floorType && floorType.length) ? floorType : '3002';
wallType = (wallType && wallType.length) ? wallType : '3001';
landscapeType = (landscapeType && landscapeType.length) ? landscapeType : '1.1';
roomPreviewer.updateObjectRoom(floorType, wallType, landscapeType);
roomPreviewer.updateRoomWallsAndFloorVisibility(true, true); roomPreviewer.updateRoomWallsAndFloorVisibility(true, true);
const populate = () => const populate = () =>
@@ -91,7 +109,7 @@ export const CatalogViewProductWidgetView: FC<{}> = props =>
return; return;
} }
default: default:
roomPreviewer.updateObjectRoom('default', 'default', 'default'); roomPreviewer.updateObjectRoom('101', '101', '1.1');
roomPreviewer.addWallItemIntoRoom(product.productClassId, new Vector3d(90), product.extraParam); roomPreviewer.addWallItemIntoRoom(product.productClassId, new Vector3d(90), product.extraParam);
return; return;
} }
@@ -29,8 +29,8 @@ export const InventoryBotView: FC<{
let floorType = roomEngine.getRoomInstanceVariable<string>(roomEngine.activeRoomId, RoomObjectVariable.ROOM_FLOOR_TYPE); let floorType = roomEngine.getRoomInstanceVariable<string>(roomEngine.activeRoomId, RoomObjectVariable.ROOM_FLOOR_TYPE);
let landscapeType = roomEngine.getRoomInstanceVariable<string>(roomEngine.activeRoomId, RoomObjectVariable.ROOM_LANDSCAPE_TYPE); let landscapeType = roomEngine.getRoomInstanceVariable<string>(roomEngine.activeRoomId, RoomObjectVariable.ROOM_LANDSCAPE_TYPE);
wallType = (wallType && wallType.length) ? wallType : '101'; wallType = (wallType && wallType.length) ? wallType : '3001';
floorType = (floorType && floorType.length) ? floorType : '101'; floorType = (floorType && floorType.length) ? floorType : '3002';
landscapeType = (landscapeType && landscapeType.length) ? landscapeType : '1.1'; landscapeType = (landscapeType && landscapeType.length) ? landscapeType : '1.1';
roomPreviewer.reset(false); roomPreviewer.reset(false);
@@ -61,8 +61,8 @@ export const InventoryFurnitureView: FC<{
let floorType = roomEngine.getRoomInstanceVariable<string>(roomEngine.activeRoomId, RoomObjectVariable.ROOM_FLOOR_TYPE); let floorType = roomEngine.getRoomInstanceVariable<string>(roomEngine.activeRoomId, RoomObjectVariable.ROOM_FLOOR_TYPE);
let landscapeType = roomEngine.getRoomInstanceVariable<string>(roomEngine.activeRoomId, RoomObjectVariable.ROOM_LANDSCAPE_TYPE); let landscapeType = roomEngine.getRoomInstanceVariable<string>(roomEngine.activeRoomId, RoomObjectVariable.ROOM_LANDSCAPE_TYPE);
wallType = (wallType && wallType.length) ? wallType : '101'; wallType = (wallType && wallType.length) ? wallType : '3001';
floorType = (floorType && floorType.length) ? floorType : '101'; floorType = (floorType && floorType.length) ? floorType : '3002';
landscapeType = (landscapeType && landscapeType.length) ? landscapeType : '1.1'; landscapeType = (landscapeType && landscapeType.length) ? landscapeType : '1.1';
roomPreviewer.updateRoomWallsAndFloorVisibility(true, true); roomPreviewer.updateRoomWallsAndFloorVisibility(true, true);
@@ -83,7 +83,24 @@ export const InventoryFurnitureView: FC<{
return; return;
} }
roomPreviewer.updateObjectRoom('default', 'default', 'default'); // Mirror the active room's pattern ids so the furniture
// preview lands on the same wallpaper / floor / landscape the
// user is decorated with. Same fallback as the
// isRoomDecoration branch above; 'default' isn't a real
// pattern id, so passing it made the previewer fall back to
// blank white surfaces.
{
const roomEngine = GetRoomEngine();
let wallType = roomEngine.getRoomInstanceVariable<string>(roomEngine.activeRoomId, RoomObjectVariable.ROOM_WALL_TYPE);
let floorType = roomEngine.getRoomInstanceVariable<string>(roomEngine.activeRoomId, RoomObjectVariable.ROOM_FLOOR_TYPE);
let landscapeType = roomEngine.getRoomInstanceVariable<string>(roomEngine.activeRoomId, RoomObjectVariable.ROOM_LANDSCAPE_TYPE);
wallType = (wallType && wallType.length) ? wallType : '3001';
floorType = (floorType && floorType.length) ? floorType : '3002';
landscapeType = (landscapeType && landscapeType.length) ? landscapeType : '1.1';
roomPreviewer.updateObjectRoom(floorType, wallType, landscapeType);
}
roomPreviewer.updateRoomWallsAndFloorVisibility(true, true); roomPreviewer.updateRoomWallsAndFloorVisibility(true, true);
if(selectedItem.isWallItem) if(selectedItem.isWallItem)
@@ -44,8 +44,8 @@ export const InventoryPetView: FC<{
let floorType = roomEngine.getRoomInstanceVariable<string>(roomEngine.activeRoomId, RoomObjectVariable.ROOM_FLOOR_TYPE); let floorType = roomEngine.getRoomInstanceVariable<string>(roomEngine.activeRoomId, RoomObjectVariable.ROOM_FLOOR_TYPE);
let landscapeType = roomEngine.getRoomInstanceVariable<string>(roomEngine.activeRoomId, RoomObjectVariable.ROOM_LANDSCAPE_TYPE); let landscapeType = roomEngine.getRoomInstanceVariable<string>(roomEngine.activeRoomId, RoomObjectVariable.ROOM_LANDSCAPE_TYPE);
wallType = (wallType && wallType.length) ? wallType : '101'; wallType = (wallType && wallType.length) ? wallType : '3001';
floorType = (floorType && floorType.length) ? floorType : '101'; floorType = (floorType && floorType.length) ? floorType : '3002';
landscapeType = (landscapeType && landscapeType.length) ? landscapeType : '1.1'; landscapeType = (landscapeType && landscapeType.length) ? landscapeType : '1.1';
roomPreviewer.reset(false); roomPreviewer.reset(false);
+98
View File
@@ -12,10 +12,19 @@
--catalog-swf-select-outer: #82d1ed; --catalog-swf-select-outer: #82d1ed;
--catalog-swf-bc: #ff8d00; --catalog-swf-bc: #ff8d00;
--catalog-swf-bc-outer: #ffb53c; --catalog-swf-bc-outer: #ffb53c;
/* Light gray secondary button - cropped from catalog_skin1.png
at (10, 190, 25x22). Drives the gift button "Cadeau", the
preview-room control button and the generic .nitro-catalog-swf-
button via border-image-slice 3 3 3 3 fill. */
--habbo-slice-button-default: url("../../assets/images/catalog/buttons/btn_secondary.png"); --habbo-slice-button-default: url("../../assets/images/catalog/buttons/btn_secondary.png");
--habbo-slice-button-hover: url("../../assets/images/catalog/buttons/btn_secondary_hover.png"); --habbo-slice-button-hover: url("../../assets/images/catalog/buttons/btn_secondary_hover.png");
--habbo-slice-button-pressed: url("../../assets/images/catalog/buttons/btn_secondary_pressed.png"); --habbo-slice-button-pressed: url("../../assets/images/catalog/buttons/btn_secondary_pressed.png");
--habbo-slice-button-disabled: url("../../assets/images/catalog/buttons/btn_secondary_disabled.png"); --habbo-slice-button-disabled: url("../../assets/images/catalog/buttons/btn_secondary_disabled.png");
/* Classic Habbo "Osta!" Buy button - cropped from catalog_skin3.png
yellow band. The historical name says "green" but the user's
skin sheet ships yellow for the action colour, so that's what
we paint. The 27x34 sprite border-image-slices nicely at 6px
since the rounded corner is ~5px. */
--habbo-slice-button-buy: url("../../assets/images/catalog/buttons/buy.png"); --habbo-slice-button-buy: url("../../assets/images/catalog/buttons/buy.png");
--habbo-slice-button-large: url("../../assets/images/catalog/buttons/buy.png"); --habbo-slice-button-large: url("../../assets/images/catalog/buttons/buy.png");
--habbo-slice-button-large-hover: url("../../assets/images/catalog/buttons/buy_hover.png"); --habbo-slice-button-large-hover: url("../../assets/images/catalog/buttons/buy_hover.png");
@@ -39,6 +48,9 @@
--habbo-stepper-minus-hover: url("../../assets/images/catalog/buttons/minus_hover.png"); --habbo-stepper-minus-hover: url("../../assets/images/catalog/buttons/minus_hover.png");
--habbo-stepper-minus-pressed: url("../../assets/images/catalog/buttons/minus_pressed.png"); --habbo-stepper-minus-pressed: url("../../assets/images/catalog/buttons/minus_pressed.png");
--habbo-stepper-minus-disabled: url("../../assets/images/catalog/buttons/minus_disabled.png"); --habbo-stepper-minus-disabled: url("../../assets/images/catalog/buttons/minus_disabled.png");
/* Scrollbar sprites cropped from catalog_skin1.png. The single-piece
thumb has caps + grip baked into one 17x34 image - stretch it
full-height with background-size: 17px 100%. */
--habbo-scrollbar-up: url("../../assets/images/catalog/scrollbar/scroll_v_up.png"); --habbo-scrollbar-up: url("../../assets/images/catalog/scrollbar/scroll_v_up.png");
--habbo-scrollbar-up-pressed: url("../../assets/images/catalog/scrollbar/scroll_v_up_pressed.png"); --habbo-scrollbar-up-pressed: url("../../assets/images/catalog/scrollbar/scroll_v_up_pressed.png");
--habbo-scrollbar-down: url("../../assets/images/catalog/scrollbar/scroll_v_down.png"); --habbo-scrollbar-down: url("../../assets/images/catalog/scrollbar/scroll_v_down.png");
@@ -877,6 +889,11 @@
} }
.nitro-catalog-classic-window .layout-grid-item { .nitro-catalog-classic-window .layout-grid-item {
/* Let the tile flex to whatever min/max width the AutoGrid sets
via repeat(auto-fill, minmax(N, 1fr)) - hard-pinning 53x74 was
overriding the layout's columnMinWidth prop, so the row count
never changed when we reduced it. Width is now 100% of the
column cell, height tracks --nitro-grid-column-min-height. */
width: 100% !important; width: 100% !important;
height: var(--nitro-grid-column-min-height, 70px) !important; height: var(--nitro-grid-column-min-height, 70px) !important;
min-width: 0 !important; min-width: 0 !important;
@@ -888,10 +905,19 @@
overflow: visible !important; overflow: visible !important;
} }
/* Furni tiles drive their look from the icon image and need a clear
background. Color-grouping swatches use itemHighlight (.has-highlight)
to ask LayoutGridItem for a solid colour via inline backgroundColor -
keep the transparent override off those so the swatch is visible. */
.nitro-catalog-classic-window .layout-grid-item:not(.has-highlight) { .nitro-catalog-classic-window .layout-grid-item:not(.has-highlight) {
background-color: transparent !important; background-color: transparent !important;
} }
/* Pets breed picker: the pet-head image renders at its native ~81x69px
regardless of the grid cell size. The old grid-cols-6 stretched
each .layout-grid-item to ~150-200px so the head looked huge in
acres of empty tile. Pin each tile just big enough to host the
head and let the container flex-wrap. */
.nitro-catalog-classic-window .nitro-catalog-classic-pet-breeds .layout-grid-item { .nitro-catalog-classic-window .nitro-catalog-classic-pet-breeds .layout-grid-item {
width: 84px !important; width: 84px !important;
min-width: 84px !important; min-width: 84px !important;
@@ -912,6 +938,12 @@
inset -2px -2px 0 #ecece4 !important; inset -2px -2px 0 #ecece4 !important;
} }
/* Habbo-classic colour swatches: small chip with a 1px dark border
and a subtle inner highlight so light tones still read as buttons.
Hover lifts the border; the selected swatch is "pressed" with a
sunken inner shadow and a bright cyan ring matching the catalog
selection accent. The cream inset from the generic .is-active rule
above would wash out the swatch colour, so we replace it here. */
.nitro-catalog-classic-window .layout-grid-item.has-highlight { .nitro-catalog-classic-window .layout-grid-item.has-highlight {
width: 26px !important; width: 26px !important;
height: 26px !important; height: 26px !important;
@@ -975,10 +1007,18 @@
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.75); text-shadow: 0 1px 0 rgba(255, 255, 255, 0.75);
} }
/* When the tile shows a full-tile bot/pet avatar (instead of a small
icon), pin the price strip to the bottom of the tile and give it a
translucent backdrop so it doesn't overlap with the avatar body. */
.nitro-catalog-classic-grid .layout-grid-item:has(.avatar-image) .nitro-catalog-classic-grid-price, .nitro-catalog-classic-grid .layout-grid-item:has(.avatar-image) .nitro-catalog-classic-grid-price,
.nitro-catalog-classic-grid .layout-grid-item:has(> .avatar-image) > .nitro-catalog-classic-grid-price, .nitro-catalog-classic-grid .layout-grid-item:has(> .avatar-image) > .nitro-catalog-classic-grid-price,
.nitro-catalog-classic-grid .avatar-image ~ .nitro-catalog-classic-grid-price { .nitro-catalog-classic-grid .avatar-image ~ .nitro-catalog-classic-grid-price {
top: auto !important; top: auto !important;
/* Re-anchor horizontally too: the parent rule's left: 2px /
right: 2px combined with content-sized inner flex was visually
parking the pill at the left side of the tile. Center it via
explicit left/right + transform so it lands smack in the
middle regardless of inner content width. */
left: 50% !important; left: 50% !important;
right: auto !important; right: auto !important;
bottom: 4px !important; bottom: 4px !important;
@@ -996,6 +1036,10 @@
z-index: 5 !important; z-index: 5 !important;
} }
/* Tighten the price entry inside the avatar-tile pill so the number
and currency icon center on the same baseline (the global
.grid-price-entry height: 13px clipped the 15px wallet icon and
pushed it visually below the number). */
.nitro-catalog-classic-grid .layout-grid-item:has(.avatar-image) .nitro-catalog-classic-grid-price-entry, .nitro-catalog-classic-grid .layout-grid-item:has(.avatar-image) .nitro-catalog-classic-grid-price-entry,
.nitro-catalog-classic-grid .avatar-image ~ .nitro-catalog-classic-grid-price .nitro-catalog-classic-grid-price-entry { .nitro-catalog-classic-grid .avatar-image ~ .nitro-catalog-classic-grid-price .nitro-catalog-classic-grid-price-entry {
height: auto !important; height: auto !important;
@@ -1048,6 +1092,8 @@
.nitro-catalog-classic-price-row { .nitro-catalog-classic-price-row {
position: absolute; position: absolute;
left: 0; left: 0;
/* Anchored from the bottom so the Aantal/Prezzo row sits just
above the Cadeau/Koop buttons regardless of layout height. */
bottom: 38px; bottom: 38px;
width: 360px; width: 360px;
height: 25px; height: 25px;
@@ -1065,6 +1111,8 @@
.nitro-catalog-classic-total-price-slot { .nitro-catalog-classic-total-price-slot {
position: absolute; position: absolute;
/* Anchored to the right of the now-100% wide price row so the
Prezzo + amount stays flush with the right edge of the panel. */
right: 2px; right: 2px;
top: 0; top: 0;
width: auto; width: auto;
@@ -1087,6 +1135,9 @@
.nitro-catalog-classic-purchase-row { .nitro-catalog-classic-purchase-row {
position: absolute; position: absolute;
left: 0; left: 0;
/* Anchored to the bottom of the panel with a 4px breathing strip
so the Cadeau / Koop buttons stay flush at the bottom of the
window no matter how tall the catalog is. */
bottom: 4px; bottom: 4px;
width: 360px; width: 360px;
height: 30px; height: 30px;
@@ -1100,6 +1151,9 @@
align-items: flex-start; align-items: flex-start;
justify-content: space-between; justify-content: space-between;
gap: 10px !important; gap: 10px !important;
/* Fill the now-100% wide purchase row instead of staying pinned at
330px (which used to match the old 360px column - 15px each
side). */
width: auto; width: auto;
height: 24px; height: 24px;
margin-left: 15px; margin-left: 15px;
@@ -1159,6 +1213,13 @@
opacity: 1 !important; opacity: 1 !important;
} }
/* Buy / Gift buttons - pure CSS. border-image-slicing the bitmap
sprites produced thin highlight/shadow stripes at the top and
bottom because the source rounded corners are ~5-6px tall but the
buttons render at 22-24px, so the slice rows stretched into a
visible band. CSS gradients give a crisp pixel-art classic-habbo
look without those artefacts. */
.nitro-catalog-classic-window .nitro-catalog-swf-buy-button { .nitro-catalog-classic-window .nitro-catalog-swf-buy-button {
width: 160px !important; width: 160px !important;
min-width: 160px !important; min-width: 160px !important;
@@ -1168,6 +1229,8 @@
border-radius: 4px !important; border-radius: 4px !important;
border-image: none !important; border-image: none !important;
border-image-source: none !important; border-image-source: none !important;
/* Yellow body with the same #f0a318 / #ffd54d tones as the
skin3-yellow Buy sprite. */
background: background:
linear-gradient(180deg, #ffe66b 0%, #ffc828 45%, #f0a318 100%) !important; linear-gradient(180deg, #ffe66b 0%, #ffc828 45%, #f0a318 100%) !important;
box-shadow: box-shadow:
@@ -1194,6 +1257,9 @@
.nitro-catalog-classic-window .nitro-catalog-swf-buy-button.pointer-events-none, .nitro-catalog-classic-window .nitro-catalog-swf-buy-button.pointer-events-none,
.nitro-catalog-classic-window .nitro-catalog-swf-buy-button:disabled { .nitro-catalog-classic-window .nitro-catalog-swf-buy-button:disabled {
/* Stay yellow when disabled - the user wants the action colour
to be recognisable regardless of state. Drop opacity + flip
the cursor so it still reads as non-interactive. */
background: background:
linear-gradient(180deg, #ffe66b 0%, #ffc828 45%, #f0a318 100%) !important; linear-gradient(180deg, #ffe66b 0%, #ffc828 45%, #f0a318 100%) !important;
color: #4a2b00 !important; color: #4a2b00 !important;
@@ -1211,6 +1277,7 @@
border-radius: 4px !important; border-radius: 4px !important;
border-image: none !important; border-image: none !important;
border-image-source: none !important; border-image-source: none !important;
/* Cream / light-gray body matching the catalog cardstock. */
background: background:
linear-gradient(180deg, #ececec 0%, #cfcfc4 100%) !important; linear-gradient(180deg, #ececec 0%, #cfcfc4 100%) !important;
box-shadow: box-shadow:
@@ -1243,12 +1310,19 @@
text-shadow: none !important; text-shadow: none !important;
} }
/* Pet purchase card lives in a tight flex row alongside the price,
so the main 160px Buy button doesn't fit. Shrink it down here. */
.nitro-catalog-classic-pet-card .nitro-catalog-swf-buy-button { .nitro-catalog-classic-pet-card .nitro-catalog-swf-buy-button {
width: auto !important; width: auto !important;
min-width: 0 !important; min-width: 0 !important;
padding: 0 14px !important; padding: 0 14px !important;
} }
/* All catalog grids must scroll vertically only - horizontal overflow
produces a stray horizontal scrollbar at the bottom of the items
strip on narrow columns (e.g. guild_furni). minmax(N, 1fr) usually
contains content but the safety net stops any odd item from
triggering a horizontal bar. */
.nitro-catalog-classic-window .layout-grid, .nitro-catalog-classic-window .layout-grid,
.nitro-catalog-classic-window [class*="grid-cols-["] { .nitro-catalog-classic-window [class*="grid-cols-["] {
overflow-x: hidden !important; overflow-x: hidden !important;
@@ -1295,6 +1369,8 @@
image-rendering: pixelated !important; image-rendering: pixelated !important;
} }
/* react-icons FaMinus/FaPlus glyphs ride inside these buttons; hide
them - the sprite already contains the +/- mark. */
.nitro-catalog-classic-window button.nitro-catalog-swf-spinner-button svg { .nitro-catalog-classic-window button.nitro-catalog-swf-spinner-button svg {
display: none !important; display: none !important;
} }
@@ -1390,6 +1466,9 @@
min-width: 25px; min-width: 25px;
padding: 0 !important; padding: 0 !important;
overflow: hidden; overflow: hidden;
/* font-size: 0 was killing the SVG: react-icons emits
<svg width="1em" height="1em">, so 0em -> 0x0. Use a real
font-size and pin the SVG to explicit pixels below. */
font-size: 14px !important; font-size: 14px !important;
line-height: 1 !important; line-height: 1 !important;
display: inline-flex !important; display: inline-flex !important;
@@ -1416,6 +1495,13 @@
right: 6px; right: 6px;
} }
/* Bulletproof override for the rotate/state buttons. The shared SWF
button rule above lays a transparent body + border-image skin on
top, which works only when the catalog/buttons/btn_secondary*.png
sprites resolve - if they're missing the button renders 0x0
invisible. Pin the box and paint a visible gradient + outline so
the controls are always discoverable, and force z-index above the
room-previewer DIV so they sit on top of the rendered scene. */
.nitro-catalog-classic-window button.nitro-catalog-classic-preview-btn { .nitro-catalog-classic-window button.nitro-catalog-classic-preview-btn {
width: 28px !important; width: 28px !important;
height: 26px !important; height: 26px !important;
@@ -1456,6 +1542,12 @@
height: 17px; height: 17px;
} }
/* ===== Classic catalog scrollbar (pure CSS, no sprites) =====
Drew this with CSS gradients instead of stretching the 17x34
skin1 thumb sprite. The sprite version pixelated into visible
horizontal bands on tall scroll areas because every source row
stretched 5-10x. CSS gradients stay crisp at any height. */
.nitro-catalog-classic-window * { .nitro-catalog-classic-window * {
scrollbar-color: auto !important; scrollbar-color: auto !important;
scrollbar-width: auto; scrollbar-width: auto;
@@ -1474,6 +1566,10 @@
border: 0 !important; border: 0 !important;
} }
/* Habbo thumb: symmetric light-edges -> darker-middle gradient (the
"pinched in the middle" look of the classic Ubuntu scrollbar),
1px near-black outline, three central grip lines via SVG centered
no-repeat. */
.nitro-catalog-classic-window *::-webkit-scrollbar-thumb { .nitro-catalog-classic-window *::-webkit-scrollbar-thumb {
min-height: 28px !important; min-height: 28px !important;
border: 1px solid #2a2a26 !important; border: 1px solid #2a2a26 !important;
@@ -1503,6 +1599,8 @@
inset 0 -1px 0 rgba(255, 255, 255, 0.25) !important; inset 0 -1px 0 rgba(255, 255, 255, 0.25) !important;
} }
/* Arrow buttons: cream cap with a 1px black outline + dark inset
chevron. SVG glyphs so they stay crisp at any zoom. */
.nitro-catalog-classic-window *::-webkit-scrollbar-button:single-button:vertical:decrement { .nitro-catalog-classic-window *::-webkit-scrollbar-button:single-button:vertical:decrement {
display: block !important; display: block !important;
width: 17px !important; width: 17px !important;
-242
View File
@@ -1,242 +0,0 @@
.habbo-navigator-desktop {
border: 1px solid #000;
border-radius: 7px;
background: #e9e9e1;
box-shadow: 0 4px 4px rgba(0, 0, 0, 0.35);
color: #111;
font-family: Ubuntu, Arial, sans-serif;
}
.habbo-navigator-desktop .nitro-card-header-shell {
min-height: 32px;
max-height: 32px;
background: #418db0;
border-bottom: 1px solid #000;
border-radius: 6px 6px 0 0;
}
.habbo-navigator-desktop .nitro-card-title {
color: #fff;
font-family: UbuntuCondensed, Ubuntu, Arial, sans-serif;
font-size: 18px;
font-weight: 700;
line-height: 1;
text-shadow: 1px 1px 0 #000;
}
.habbo-navigator-desktop .nitro-card-close-button {
width: 19px;
height: 20px;
right: 6px;
border: 2px solid #000;
border-radius: 4px;
background: #c73a32;
box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.35);
}
.habbo-navigator-desktop .nitro-card-close-button::before,
.habbo-navigator-desktop .nitro-card-close-button::after {
content: "";
position: absolute;
width: 10px;
height: 2px;
background: #fff;
box-shadow: 1px 1px 0 #64120f;
}
.habbo-navigator-desktop .nitro-card-close-button::before {
transform: rotate(45deg);
}
.habbo-navigator-desktop .nitro-card-close-button::after {
transform: rotate(-45deg);
}
.habbo-navigator-desktop .nitro-card-tabs-shell {
justify-content: flex-start;
gap: 0;
min-height: 27px;
max-height: 27px;
padding: 4px 8px 0;
background: #e9e9e1;
border-bottom: 1px solid #b8b8ad;
}
.habbo-navigator-desktop .nitro-card-tab-item {
min-height: 23px;
margin-right: -1px;
padding: 4px 12px 3px;
border: 1px solid #555;
border-bottom: 0;
border-radius: 6px 6px 0 0;
background-color: #d5d8cf;
background-image: url("../../assets/images/navigator/swf/tab_bg_unsel.png");
background-repeat: repeat-x;
background-size: auto 100%;
color: #111;
font-size: 12px;
font-weight: 400;
line-height: 1;
box-shadow: inset 1px 1px 0 #fff;
}
.habbo-navigator-desktop .nitro-card-tab-item:hover {
background-color: #e7e8df;
background-image: url("../../assets/images/navigator/swf/tab_bg_hilite.png");
}
.habbo-navigator-desktop .nitro-card-tab-item-active {
z-index: 2;
margin-bottom: -1px;
background-color: #f4f4ed;
background-image: url("../../assets/images/navigator/swf/tab_bg_sel.png");
border-bottom: 1px solid #f4f4ed;
font-weight: 700;
}
.habbo-navigator-desktop .habbo-navigator-desktop-content {
padding: 8px 9px 9px;
overflow: hidden;
background: #f4f4ed;
border-radius: 0 0 6px 6px;
color: #111;
}
.habbo-navigator-desktop .habbo-navigator-desktop-content input,
.habbo-navigator-desktop .habbo-navigator-desktop-content select,
.habbo-navigator-desktop .habbo-navigator-desktop-content textarea {
height: 22px;
border: 1px solid #a0a49c;
border-radius: 3px;
background-color: #fff;
background-image: url("../../assets/images/navigator/swf/hdr_search.png");
background-repeat: repeat-x;
color: #333;
font-size: 12px;
box-shadow: inset 1px 1px 0 rgba(0, 0, 0, 0.08);
}
.habbo-navigator-desktop .habbo-navigator-desktop-content button,
.habbo-navigator-desktop .habbo-navigator-desktop-content .btn {
min-height: 22px;
border: 1px solid #3a3a3a;
border-radius: 4px;
background-color: #d6d6d1;
background-image: url("../../assets/images/navigator/swf/button.png");
background-repeat: repeat-x;
background-size: auto 100%;
color: #111;
font-size: 12px;
font-weight: 700;
box-shadow: inset 1px 1px 0 #fff;
}
.habbo-navigator-desktop .nitro-card-panel {
border: 1px solid #babdb4;
border-radius: 6px;
background: #efefe8;
box-shadow: inset 1px 1px 0 #fff;
overflow: hidden;
}
.habbo-navigator-desktop .nitro-card-panel > .flex:first-child {
min-height: 28px;
padding: 5px 8px;
border-bottom: 1px solid #d3d5cd;
background: #efefe8;
}
.habbo-navigator-desktop .navigator-grid {
padding: 0 4px 5px;
background: #fff;
}
.habbo-navigator-desktop .navigator-item {
min-height: 28px;
margin: 2px 0;
border: 1px solid transparent;
border-radius: 5px;
background: #f5f5ef;
color: #111;
}
.habbo-navigator-desktop .navigator-item:nth-child(even) {
background: #e7e8e0;
}
.habbo-navigator-desktop .navigator-item:hover {
border-color: #777;
background: #fff;
}
.habbo-navigator-desktop .nitro-navigator-search-saves-result {
width: 155px;
min-width: 155px;
height: 100%;
border: 1px solid #babdb4;
border-radius: 6px;
background: #efefe8;
padding: 4px;
}
.habbo-navigator-desktop .nitro-navigator-search-saves-result > .flex:first-child {
min-height: 24px;
border: 1px solid #d58e00;
border-radius: 4px;
background: #f8a900;
box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.45);
}
.habbo-navigator-desktop .nitro-navigator-search-saves-result span {
color: #111;
font-weight: 700;
}
.habbo-navigator-desktop .nitro-icon.icon-navigator-info {
width: 16px;
height: 16px;
}
.habbo-navigator-desktop ::-webkit-scrollbar {
width: 17px !important;
height: 17px !important;
}
.habbo-navigator-desktop ::-webkit-scrollbar-track {
border: 0 !important;
background-color: transparent !important;
background-image: url("../../assets/images/catalog/swf/ubuntu_scrollbar_track_v_17x2.png") !important;
background-repeat: repeat-y !important;
background-position: center top !important;
}
.habbo-navigator-desktop ::-webkit-scrollbar-thumb {
min-height: 24px !important;
border: 0 !important;
border-radius: 0 !important;
background-color: transparent !important;
background-image:
url("../../assets/images/catalog/swf/ubuntu_scrollbar_thumb_v_grip_7x10.png"),
url("../../assets/images/catalog/swf/ubuntu_scrollbar_thumb_v_top_17x2.png"),
url("../../assets/images/catalog/swf/ubuntu_scrollbar_thumb_v_bottom_17x2.png"),
url("../../assets/images/catalog/swf/ubuntu_scrollbar_thumb_v_mid_17x1.png") !important;
background-repeat: repeat-y, no-repeat, no-repeat, repeat-y !important;
background-position: center center, center top, center bottom, center top !important;
background-size: 7px 10px, 17px 2px, 17px 2px, 17px 1px !important;
box-shadow: none !important;
}
.habbo-navigator-desktop ::-webkit-scrollbar-button {
width: 17px !important;
height: 16px !important;
background-color: transparent !important;
border: 0 !important;
}
.habbo-navigator-desktop ::-webkit-scrollbar-button:single-button:vertical:decrement {
background-image: url("../../assets/images/catalog/swf/ubuntu_scrollbar_up_17x16.png") !important;
}
.habbo-navigator-desktop ::-webkit-scrollbar-button:single-button:vertical:increment {
background-image: url("../../assets/images/catalog/swf/ubuntu_scrollbar_down_17x16.png") !important;
}
+33 -1
View File
@@ -1,3 +1,34 @@
/**
* Runtime patches for pixi.js v8 batcher edge cases.
*
* Pixi v8 (through 8.19 at least) has a long-running family of crashes
* where `getAdjustedBlendModeBlend(blendMode, textureSource)` is invoked
* with a `null` textureSource and throws on `null.alphaMode` or
* `null.uid`. We've seen it from at least four call sites:
* - Batcher.break() (FilterPipe, StencilMaskPipe, AlphaMaskPipe)
* - Batcher.checkAndUpdateTexture() (SpritePipe.validateRenderable)
*
* The trigger varies, but the symptom is always the same: a single bad
* frame inside the catalog room previewer (or anywhere RoomSpriteCanvas
* drives Pixi) tanks the whole render loop with an endless cascade of
* requestAnimationFrame errors.
*
* We removed the two custom filters we owned (BlackToAlphaFilter,
* PlaneMaskFilter) earlier, but several call sites are inside Pixi
* itself or inside renderer-side mask setup that we can't sensibly
* delete (RoomSpriteCanvas pins a Sprite mask on the master display to
* clip the room to the canvas).
*
* Patch every known throwing entry point so that when it throws because
* of a null textureSource we treat the frame as a no-op instead of
* propagating the exception. The visible cost is a missed batch this
* tick - the next tick re-renders cleanly. Without this patch the
* LayoutRoomPreviewerView safety latch fires permanently on the first
* affected offer.
*
* Importing this module has the side effect of installing the patch
* exactly once, idempotent across HMR reloads.
*/
import * as PIXI from 'pixi.js'; import * as PIXI from 'pixi.js';
type AnyFn = (...args: unknown[]) => unknown; type AnyFn = (...args: unknown[]) => unknown;
@@ -64,8 +95,9 @@ const installPatch = (): void =>
const proto = (ctor as { prototype?: MethodHost } | undefined)?.prototype; const proto = (ctor as { prototype?: MethodHost } | undefined)?.prototype;
if(!proto) continue; if(!proto) continue;
// break() is called during FilterPipe / StencilMaskPipe / AlphaMaskPipe.pop
if(guardMethod(proto, 'break', name)) patched = true; if(guardMethod(proto, 'break', name)) patched = true;
// checkAndUpdateTexture() is called during SpritePipe.validateRenderable
if(guardMethod(proto, 'checkAndUpdateTexture', name)) patched = true; if(guardMethod(proto, 'checkAndUpdateTexture', name)) patched = true;
} }