Ducket Suites is a lightweight renderer for isometric rooms, animated furniture, and Habbo-style avatars — figure looks, poses, walks, and named effects (DKFE bundles from the CDN). Drop it on any page, no build step required.
Mobile-first, in-memory testers on this site — no Nakama, no account. Use them while building the client: preview every avatar effect, edit looks in a full wardrobe, and inspect furni from the CDN catalog.
Render guest avatars outside a room or inside one: look strings,
eight-way facing, action bits (Sit, Wave,
Dance, …), smooth tile walks, and optional
effect ids resolved through effectmap.xml
with .ducket DKFE sprites from the CDN.
Need a still image instead of a live canvas? The
avatar imager renders any figure to a PNG from a URL —
a drop-in for habbo-imaging/avatarimage, no script tag required.
One createWorld call boots Pixi, mounts a canvas, and readies the avatar manager.
spawn() returns an AvatarHandle with first-class navigateTo
(BFS pathfinding through the room grid), walkTo (scripted straight-line walks),
setLook, face, and typed pointer events — no position-function wiring, no iso
math.
var world = await DucketSuites.createWorld({ mount: "#stage", tilemap: null, // standalone-avatar mode }); var alice = await world.avatars.spawn({ look: "hd-180-1.ch-255-66.lg-280-110.sh-305-62.hr-828-61", at: { x: 2, y: 2 }, facing: DucketSuites.Direction.South, }); // optional named effect (resolved via effectmap + DKFE on CDN) alice.setEffect("dance.1"); alice.do(DucketSuites.AvatarAction.Dance);
navigateTo uses BFS over the room tile grid to find the shortest walkable path,
then walks tile-by-tile at a consistent per-tile speed. Calling it again while the avatar
is walking supersedes the old path — the avatar finishes its current tile and re-routes.
Use walkTo when you supply the path yourself (scripted sequences, cutscenes).
// click-to-move: pathfinds through walls + stairs automatically world.events.on("tile:click", (tile) => { void alice.navigateTo(tile); }); // abandon the current path (finishes current tile, then stops) alice.cancelNavigation(); // scripted sequence: supply your own path with walkTo await alice.walkTo({ x: 3, y: 2 }); await alice.walkTo({ x: 3, y: 4 }); alice.face(DucketSuites.Direction.South);
createWorld(options)The only entry point. Returns a World bound to a mount element, with lazy avatar + furniture
managers and a typed event bus.
| option | type | description |
|---|---|---|
mount |
string | Element | null |
Selector, element, or null for headless |
tilemap |
string | string[] | null |
Room grid, or null for standalone-avatar mode |
theme |
{ wall, floor, background } |
Hex strings or numbers; defaults are sensible |
centre |
boolean | undefined |
true by default — recentre the room on boot and canvas resize. Pass false if you drive world.stage position yourself. |
assets |
{ cdn, resources } |
Override CDN + resource hosts |
World| property | type | notes |
|---|---|---|
app |
pixi.Application |
Escape hatch for advanced rendering |
events |
TypedEventBus |
Unified tile:* / wall:* / avatar:* / furni:* stream |
avatars |
AvatarManager |
spawn(), get(), list(), remove() |
furniture |
FurnitureManager |
place(), search(), remove() |
tileToScreen(tile) |
{ x, y } |
Room → canvas-local pixels (useful for overlays) |
recentre() |
() => void |
Re-centre after changing walls/floor visibility |
destroy() |
() => void |
Tear down the canvas, listeners, handles |
AvatarHandleReturned by world.avatars.spawn({ look, at, facing, … }). Every mutator that triggers async
work returns a Promise so you can chain.
| method / field | notes |
|---|---|
navigateTo({x, y}) |
BFS pathfind through the room grid and walk tile-by-tile at consistent speed.
Supersedes any prior navigateTo at the next tile boundary.
No-op when there is no room or no walkable path. Prefer this over walkTo
for interactive click-to-move. |
cancelNavigation() |
Abandon the current navigateTo path without interrupting the current tile animation |
walkTo({x, y}, { facing? }) |
Low-level: walk in a straight line to a tile (no pathfinding). Returns Promise<void> resolving on arrival. Use for scripted sequences where you supply the path yourself. |
stopWalking() |
Cancel any navigateTo and clear the movement queue |
teleport({x, y}) |
Instant move, no animation |
setLook(str) |
Returns Promise<void> resolving once the new sprites render |
face(dir), lookAt(dir) |
Body / head rotation, Direction values |
do(action), stop(action), toggle(action) |
Action bits (AvatarAction enum) |
wave(on?) |
Sugar for Wave action toggle |
setEffect(id), setItem(id) |
Named effect / held item, undefined to clear |
on(event, handler) |
Typed pointer events — returns an unsubscribe fn |
tile, facing, look, actions,
waving |
Read-only state getters |
FurnitureHandleReturned by world.furniture.place({ type, at, facing, … }) (pass an array for batch).
| method / field | notes |
|---|---|
moveTo({x, y, z?}) |
Floor pieces only |
face(dir) |
Change direction (uses Direction enum) |
setState(id), playScene(...), setColor(id) |
Park on a state (auto-plays transitions) · run a named scene · swap colour |
logic, visualization |
Manifest's lo/vz strings (e.g. "furniture_dice", "furniture_animated"). The registry (see behaviours) keys on logic; pre-v1.4 bundles without these fields stay non-interactive until re-migrated. |
behaviors, getBehavior(Ctor) |
Attached behaviour list + typed lookup. Use for specialised calls (e.g. getBehavior(FurnitureDiceBehavior)?.roll(6)). |
on(event, handler) |
Typed pointer events |
remove() |
Destroy the piece |
DirectionEnum replacing raw 0/2/4/6. Direction.North, NorthEast,
East, … plus helpers from(n), opposite(d), name(d).
Tilemaps are multiline strings. Each character is a tile:
x — void (no tile)0–9 — floor at that height levelx is the door row// simple flat room with door var flat = [ "xxxxx", "x0000", "00000", // ← door row "x0000", "x0000", ].join("\n"); // raised platform — stairs auto-generated var raised = [ "xxxxxxxxx", "x00000000", "000000000", "x00111100", // height 1 platform "x00111100", "x00000000", ].join("\n");
Pass an array of descriptors — bundles are deduped, fetched in parallel, and auto-stacked when
at.z is omitted.
var Dir = DucketSuites.Direction; await world.furniture.place([ { type: "throne", at: { x: 3, y: 3 }, facing: Dir.South }, { type: "nft_h23_trippy_aloe", at: { x: 1, y: 1 }, facing: Dir.East, state: "0" }, { type: "hc23_11", at: { x: 5, y: 2 }, facing: Dir.West, state: "0" }, ]);
var chair = await world.furniture.place({ url: "https://cdn.ducket.net/hof_furni/65456/throne.ducket", at: { x: 4, y: 3 }, facing: DucketSuites.Direction.East, }); // move / rotate / animate via the handle chair.moveTo({ x: 6, y: 3 }); chair.face(DucketSuites.Direction.South);
| field | type | description |
|---|---|---|
type |
string |
Classname from furnidata (lazy catalog) |
url |
string |
Direct .ducket URL (skip the catalog) |
at |
{ x, y, z? } |
Tile; z auto-stacks if omitted |
facing |
Direction |
Defaults to Direction.South |
state |
string? |
Resting state id — call setState(id) on the handle to switch at runtime (transitions auto-play); playScene(name) runs a named scene |
color |
string | number? |
Palette colour variant |
placement |
"floor" | "wall" |
Wall placements also need offsetX/offsetY |
behaviors |
IFurnitureBehavior[] |
Optional. Auto-attached for known logic types; pass explicit ones to stack or override. |
Habbo's authoring pipeline tags every furni with a logic type
— a short string like furniture_dice,
furniture_multistate, or furniture_random_state.
The ducket runtime mirrors that: each logic type maps to a
behaviour (a state machine that drives the piece), and
the right behaviour auto-attaches when a bundle loads.
Most of the time you don't touch any of this. A dice rolls on
double-click. A multistate door cycles. A random-state tree
picks its look on spawn. This port follows reference
IFurnitureBehavior: behaviours attach via
setParent and wire pointer handlers themselves.
For programmatic control, use FurnitureHandle.getBehavior(Ctor)
and call behaviour methods (roll(),
next(), …).
logic (manifest.lo) |
behaviour | double-click (built-in wiring) |
|---|---|---|
furniture_dice |
FurnitureDiceBehavior |
Roll → land on a random 1-6 face; second click closes |
furniture_multistate |
FurnitureMultiStateBehavior |
Cycle to the next resting state, wrap at the end |
furniture_one_way_door |
FurnitureMultiStateBehavior (mode: "one-way") |
Advance once, latch on the last state |
furniture_random_state |
FurnitureMultiStateBehavior (pickRandomOnAttach) |
Random state on spawn; non-interactive |
Branch on getBehavior for the logic you care about,
or subscribe to world.events and dispatch yourself.
var { FurnitureDiceBehavior, FurnitureMultiStateBehavior } = DucketSuites; var piece = await world.furniture.place({ type: "edicehc", // or a multistate / one-way classname at: { x: 2, y: 2 }, }); var dice = piece.getBehavior(FurnitureDiceBehavior); if (dice) { await dice.roll(); // programmatic roll (double-click also wired in-engine) } else { piece.getBehavior(FurnitureMultiStateBehavior)?.next(); } console.log(piece.logic); // "furniture_dice" console.log(piece.visualization); // "furniture_animated"
When you need behaviour-specific methods (roll(value)
on a dice, setIndex(i) on a multistate), look them
up by constructor.
var { FurnitureDiceBehavior } = DucketSuites; var dice = await world.furniture.place({ type: "edicehc", at: { x: 2, y: 2 }, }); var behaviour = dice.getBehavior(FurnitureDiceBehavior); await behaviour?.roll(6); // force land on six behaviour?.close(); // no face shown
Host apps can plug in custom behaviours at app init. Key on
the manifest's Habbo logic tag — the same string
index.bin ships upstream.
import { registerLogicBehavior } from "@suites-public/index"; // Every furni whose manifest.lo === "furniture_jukebox" gets one: registerLogicBehavior(["furniture_jukebox"], () => new JukeboxBehavior());
Writing your own behaviour? Implement setParent(furni)
for attachment (reference parity). Register pointer UX with
furni.addPointerInteractionListener(kind, fn)
and keep the returned unsubscribe; call it from
destroy() alongside any timers. Do not assign
onClick / onDoubleClick on furniture —
that bypasses the merged pipeline.
Porting a pre-v1.4 bundle? Re-run the migration
pipeline (tsx ducket/bin/migrate-shroom.ts)
and the fresh encode will populate manifest.lo /
manifest.vz from the source index.bin.
The ducket corpus is the runtime's single source of truth for
interactivity — no classname lookup tables, no external
metadata, no .shroom dependency after migration.
One typed bus for everything — subscribe with world.events.on(...), returns an unsubscribe
function.
world.events.on("tile:click", (tile) => { console.log("clicked", tile.x, tile.y, tile.z); }); world.events.on("avatar:click", ({ avatar }) => { avatar.toggle(DucketSuites.AvatarAction.Wave); }); world.events.on("furni:hover", ({ furniture }) => { console.log("over", furniture.type, furniture.tile); });
| event | payload | description |
|---|---|---|
tile:click |
{ x, y, z } |
User clicks a floor tile |
tile:hover, tile:leave |
{ x, y, z } / void |
Pointer enters / leaves tiles |
wall:hover, wall:leave |
{ x, y, offsetX, offsetY, wall } |
Pointer over a wall |
avatar:click, avatar:hover, avatar:leave |
{ avatar, native } |
Same shape for every avatar event |
avatar:doubleclick |
{ avatar, native } |
Second tap on an avatar (Pixi double-click) |
furni:click, furni:hover, furni:leave |
{ furniture, native } |
Same shape for every furniture event |
ready, resize, destroy |
void / { width, height } / void |
Lifecycle |