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.

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…