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.
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 |