embed isometric
rooms with one
script tag.

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.

PixiJS v8 ~145 KB gzipped avatars + effects zero config

dev playgrounds

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.

feature.scene
loading…

avatars & effects

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.

avatar.playground
walk
loading avatar…

minimal bootstrap

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);

navigation (pathfinding)

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);

api reference

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

AvatarHandle

Returned 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

FurnitureHandle

Returned 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

Direction

Enum replacing raw 0/2/4/6. Direction.North, NorthEast, East, … plus helpers from(n), opposite(d), name(d).

tilemaps

Tilemaps are multiline strings. Each character is a tile:

// 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");
tilemap.live — raised platform, auto-stairs click a tile to log coords
loading…

furniture

batch placement (recommended)

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" },
]);

individual by direct URL

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);

FurnitureDescriptor

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.
furniture.live — batch placement + hover inspect hover furniture to inspect · click logs only
loading…

behaviours & logic

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(), …).

built-in logic types

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

generic use

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"
behavior.live — dice + multistate double-click a piece or use the buttons
dice · multistate
loading…

specialised use

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

registering your own

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.

events

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
events.live — live event stream click a tile to navigate · click the avatar to wave
hover a tile, avatar, or furniture to see events…