Full Example

A step-by-step walkthrough that shows off everything the HyCitizens API can do. We'll build two citizens from scratch: a friendly townsfolk NPC packed with animations and interaction logic, and a fully configured boss enemy ready to make players sweat.

📋

Before you start

All code below assumes you've already declared HyCitizens as a dependency and can access CitizensManager. If not, see the Developer API overview first.

CitizensManager manager = HyCitizensPlugin.get().getCitizensManager();

Part 1 — Amara the Merchant

Amara is a friendly shopkeeper who waves when players walk past, greets them when they interact, and hands them a starting item. She's passive, rotates to face nearby players, and plays idle and interact animations.

Step 1 — Core identity & position

Start by creating the CitizenData object and filling in the basics: name, world, position, and model.

CitizenData amara = new CitizenData();

// CitizenData creation with multi-line nametag — name on line 1, subtitle on line 2
CitizenData amara = new CitizenData(
    "amara_merchant", // Unique ID
    "Amara\nMerchant", // Name
    "Player",
    worldUuid,
    new Vector3d(128, 64, 256), // Position
    new Vector3f(0, 0, 0), // Rotation
    1.0f, // scale
    null,
    new ArrayList<>(),
    "",
    "",
    List.of(),
    true,
    false,
    "tidy", // Player to use skin
    null,
    0L,
    true // Rotate towards player
);

// Assign to a group for easy bulk queries later
amara.setGroup("merchants");

Step 2 — Equipped items

Give Amara something to hold so she looks the part.

// Merchant holds a potion in her hand
amara.setNpcHand("Potion_Regen_Health");

// Light traveling outfit
amara.setNpcHelmet("Armor_Leather_Light_Head");
amara.setNpcChest("Armor_Leather_Light_Chest");

Step 3 — Animations

We'll set up four animation rules: a continuous idle loop, a wave when a player walks nearby, and a jump when someone interacts, and a timed eat.

List<AnimationBehavior> animations = new ArrayList<>();

// 1. Looping idle base — always playing on slot 2
animations.add(new AnimationBehavior(
    "DEFAULT",
    "Idle",         // continuous base pose
    2,              // Movement slot
    0f, 0f
));

// 2. Wave when a player enters within 10 blocks — plays for 2.5 s then returns to Idle
animations.add(new AnimationBehavior(
    "ON_PROXIMITY_ENTER",
    "Wave",   // full-body wave
    2,              // Action slot
    0f,
    10.0f,          // proximity range: 10 blocks
    true,           // stop after time
    "Idle",         // return to Idle
    2.5f
));

// 3. Jump on interact — plays for 1.8 s then returns to Idle
animations.add(new AnimationBehavior(
    "ON_INTERACT",
    "Jump",
    2,
    0f, 0f,
    true,
    "Idle",
    1f
));

// 4. A periodic stretch every 25 seconds so she doesn't look frozen
animations.add(new AnimationBehavior(
    "TIMED",
    "Eat",
    2,
    25.0f,          // every 25 seconds
    0f,
    true,
    "Idle",
    3.0f
));

amara.setAnimationBehaviors(animations);

Step 4 — Messages

Set up three messages that cycle through sequentially each time the player interacts — so she says something different on each visit.

List<CitizenMessage> messages = new ArrayList<>();

// Each message has a short delay so they stagger when selection mode is ALL,
// but in SEQUENTIAL mode only one fires per interaction.
messages.add(new CitizenMessage(
    "{#FFD700}Hello, {PlayerName}! Looking for supplies?",
    "BOTH",   // fires on F key or left-click
    0.0f
));
messages.add(new CitizenMessage(
    "{WHITE}I just restocked — fresh goods from the eastern caravans.",
    "BOTH",
    0.0f
));
messages.add(new CitizenMessage(
    "{#90EE90}Come back any time, {PlayerName}. Safe travels!",
    "BOTH",
    0.0f
));

MessagesConfig messagesConfig = new MessagesConfig();
messagesConfig.setMessages(messages);
messagesConfig.setSelectionMode("SEQUENTIAL"); // cycle through on each visit
messagesConfig.setEnabled(true);

amara.setMessagesConfig(messagesConfig);

Step 5 — Command actions

When a player interacts using the F key, Amara sends a warm follow-up message and then gives them a starter supply pack using a server command. The command runs 0.5 s after the message so the delivery feels natural.

List<CommandAction> actions = new ArrayList<>();

// A formatted chat message — no slash command needed, just the {SendMessage} prefix
// {SendMessage} is legacy since message actions were added, but it can still be used
actions.add(new CommandAction(
    "{SendMessage}{GRAY}Here, take this — everyone needs a head start.",
    false,   // run as player (ignored for SendMessage)
    0.3f,    // 0.3 s delay
    "F_KEY"  // only on F key, not left-click
));

// Give the player a item via server command
actions.add(new CommandAction(
    "give {PlayerName} Weapon_Sword_Steel",
    true,    // run as server
    0.8f,    // 0.8 s delay so it arrives after the message
    "F_KEY"
));

amara.setCommandActions(actions);

Step 6 — Movement

Amara stays at her market stall — she uses IDLE movement so she won't wander off. The rotateTowardsPlayer flag we set earlier handles the head-turning.

MovementBehavior movement = new MovementBehavior();
movement.setType("IDLE");
amara.setMovementBehavior(movement);

Step 7 — Register & spawn

// addCitizen registers, spawns, and saves in one call
manager.addCitizen(amara, true);

Amara is live!

She'll idle at her stall, wave at players who come within 10 blocks, cycle through her greetings, and hand out a starter item on F key interaction.

Listening for Amara's interactions with an event

Let's also log every time a player talks to Amara, and block the interaction for players who have a "merchant-banned" flag — without touching the citizen config at all.

manager.addCitizenInteractListener(event -> {
    CitizenData citizen = event.getCitizen();

    // Only care about our merchant group
    if (!"merchants".equals(citizen.getGroup())) return;

    PlayerRef player = event.getPlayerRef();

    // Example: block banned players
    if (isMerchantBanned(player.getUuid())) {
        event.setCancelled(true); // stops messages, commands, animations
        player.sendMessage(Message.raw(
            CitizenInteraction.parseColoredMessage(
                "{RED}The merchants of this town will not deal with you."
            )
        ));
        return;
    }

    System.out.println("[HyCitizens] " + player.getUsername()
        + " spoke with " + citizen.getName());
});

Part 2 — Malgrath, the Ashen King (Boss)

Malgrath is a scaled-up boss enemy with full armor, an Adamantite sword, massive health and damage, wide detection, aggressive combat behavior, a patrol route through his lair, rich death drops, and a server-wide death broadcast hooked through an event listener.

Step 1 — Core identity

CitizenData malgrath = new CitizenData();

// Create CitizenData
CitizenData malgrath = new CitizenData(
    "malgrath",                 // Unique ID
    "Malgrath\nThe Ashen King", // Name
    "Skeleton_Fighter",         // Skeleton Figher model
    worldUuid,
    new Vector3d(512, 70, 512), // Spawn position
    new Vector3f(0, 0, 0),
    2.5f,                       // Scale
    null,
    new ArrayList<>(),
    "",
    "",
    List.of(),
    false,
    false,
    null,
    null,
    0L,
    false
);

// Group tag for event filtering and bulk operations
malgrath.setGroup("world-bosses");

Step 2 — Equipped items

Full heavy armor set with an Adamantite sword. Item ID strings must match valid Hytale item IDs.

// Adamantite sword in main hand
malgrath.setNpcHand("Weapon_Longsword_Adamantite");

// Full Adamantite armor set
malgrath.setNpcHelmet("Armor_Adamantite_Head");
malgrath.setNpcChest("Armor_Adamantite_Chest");
malgrath.setNpcGloves("Armor_Adamantite_Hands");
malgrath.setNpcLeggings("Armor_Adamantite_Legs");

Step 3 — Attitude, health & damage

// Aggressive — attacks any player on sight
malgrath.setAttitude("AGGRESSIVE");
malgrath.setTakesDamage(true);

// 8,000 HP
malgrath.setOverrideHealth(true);
malgrath.setHealthAmount(8000f);

// 60 damage per hit
malgrath.setOverrideDamage(true);
malgrath.setDamageAmount(60f);

// Respawn 10 minutes after being killed
malgrath.setRespawnOnDeath(true);
malgrath.setRespawnDelaySeconds(600f);

Step 4 — Detection

Malgrath has an enormous detection radius — players can't sneak up on him.

DetectionConfig detection = new DetectionConfig(true); // start from hostile defaults

detection.setViewRange(50f);          // sees players from 50 blocks away
detection.setViewSector(270f);        // nearly 270° field of view
detection.setHearingRange(30f);       // hears movement from 30 blocks
detection.setAbsoluteDetectionRange(5f); // always detects within 5 blocks, no line-of-sight needed
detection.setAlertedRange(60f);       // alerts all nearby citizens within 60 blocks

// How long he stays on high alert and searches after losing a target
detection.setAlertedTimeMin(2.0f);
detection.setAlertedTimeMax(3.0f);
detection.setSearchTimeMin(20.0f);
detection.setSearchTimeMax(30.0f);

// 90% chance to respond to a call for help from other NPCs
detection.setChanceToBeAlertedWhenReceivingCallForHelp(90);

malgrath.setDetectionConfig(detection);

Step 5 — Combat

Aggressive pursuit, wide attack range, tight strafing, fast chasing, and a high block chance to make him feel dangerous.

CombatConfig combat = new CombatConfig();

// Attack
combat.setAttackType("Root_NPC_Attack_Melee");
combat.setAttackDistance(4.5f);           // Increase attack distance
combat.setDesiredAttackDistanceMin(3.0f);
combat.setDesiredAttackDistanceMax(4.0f);
combat.setTargetRange(8.0f);              // acquires targets within 8 blocks

// Attack timing — slow but devastating
combat.setAttackPauseMin(2.5f);
combat.setAttackPauseMax(3.5f);
combat.setCombatAttackPreDelayMin(0.4f);  // longer wind-up telegraph
combat.setCombatAttackPreDelayMax(0.6f);
combat.setCombatAttackPostDelayMin(0.3f); // recovery after swing
combat.setCombatAttackPostDelayMax(0.5f);

// Chase — relentless
combat.setChaseSpeed(0.85f);
combat.setCombatBehaviorDistance(6.0f);

// Movement in combat — heavy strafing to make him feel weighty
combat.setCombatStrafeWeight(15);
combat.setCombatDirectWeight(8);
combat.setCombatStrafingDurationMin(1.5f);
combat.setCombatStrafingDurationMax(2.5f);
combat.setCombatStrafingFrequencyMin(1.0f);
combat.setCombatStrafingFrequencyMax(2.0f);
combat.setCombatRelativeTurnSpeed(1.2f);
combat.setCombatMovingRelativeSpeed(0.75f);
combat.setCombatBackwardsRelativeSpeed(0.35f);

// Back-off after each swing
combat.setBackOffAfterAttack(true);
combat.setBackOffDistance(5.0f);
combat.setBackOffDurationMin(1.5f);
combat.setBackOffDurationMax(2.5f);

// Blocking — 40% chance to block incoming player attacks
combat.setBlockAbility("Shield_Block_Heavy");
combat.setBlockProbability(40);

// Target switching — holds focus for 8–10 seconds before switching
combat.setTargetSwitchTimerMin(8.0f);
combat.setTargetSwitchTimerMax(10.0f);

// Enable advanced action evaluation for more dynamic AI
combat.setUseCombatActionEvaluator(true);

malgrath.setCombatConfig(combat);

Step 6 — Patrol path

Malgrath patrols a slow circuit around his throne room between fights. We build the path from code, register it, then assign it to the citizen via PathConfig so the assignment persists across restarts.

// Build the patrol route
PatrolPath throneCircuit = new PatrolPath("MalgrathThrone", PatrolPath.LoopMode.PING_PONG);
throneCircuit.addWaypoint(new PatrolWaypoint(512, 70, 512, 4.0f)); // throne — 4 s pause
throneCircuit.addWaypoint(new PatrolWaypoint(525, 70, 512, 1.0f));
throneCircuit.addWaypoint(new PatrolWaypoint(525, 70, 525, 1.0f));
throneCircuit.addWaypoint(new PatrolWaypoint(512, 70, 525, 1.0f));
throneCircuit.addWaypoint(new PatrolWaypoint(499, 70, 525, 1.0f));
throneCircuit.addWaypoint(new PatrolWaypoint(499, 70, 512, 1.0f));

// Register and save the path to disk
PatrolManager patrolManager = manager.getPatrolManager();
patrolManager.savePath(throneCircuit);

// Assign via PathConfig so the patrol persists after server restarts
PathConfig pathConfig = new PathConfig();
pathConfig.setFollowPath(true);
pathConfig.setPathName("MalgrathThrone");
pathConfig.setPatrol(true);
pathConfig.setLoopMode("PING_PONG");

malgrath.setPathConfig(pathConfig);
malgrath.setMovementBehavior(new MovementBehavior("PATROL", 6.0f, 0f, 0f, 0f));

Step 7 — Death configuration

On death, Malgrath drops legendary loot, sends a kill message to the player, and runs a broadcast command announcing the kill to the whole server.

DeathConfig deathConfig = new DeathConfig();

// Loot drops
deathConfig.setDropItems(List.of(
        new DeathDropItem("Weapon_Longsword_Adamantite", 1),
        new DeathDropItem("Ingredient_Bar_Adamantite", 25),
        new DeathDropItem("Ingredient_Bone_Fragment", 50)
));

// Personal kill message — sent only to the killing player
deathConfig.setDeathMessages(List.of(
    new CitizenMessage(
        "{#FF8C00}You have slain {CitizenName}! The Ashen King falls.",
        "BOTH",
        0.0f
    ),
    new CitizenMessage(
        "{GRAY}Legendary loot has dropped at the kill site.",
        "BOTH",
        1.5f   // stagger: second message arrives 1.5 s after the first
    )
));
deathConfig.setMessageSelectionMode("ALL"); // send both messages

// Server-wide broadcast command
deathConfig.setDeathCommands(List.of(
    new CommandAction(
        "say {#FF2222}[World Boss] {PlayerName} has defeated {CitizenName} — claim the loot!",
        true,   // run as server
        0.5f    // slight delay so drops appear first
    ),
));
deathConfig.setCommandSelectionMode("ALL");

malgrath.setDeathConfig(deathConfig);

Step 8 — Register & spawn

manager.addCitizen(malgrath, true);

Malgrath is live!

He'll patrol his throne room, detect players from 50 blocks away, chase them down, and drop legendary loot on death — broadcasting the kill to the entire server.

Part 3 — Wiring Up Boss Events

Death configuration handles the standard drops and messages, but events let you go further — custom plugin logic, phase changes, or global game state changes — without touching the citizen's config at all.

Death event — awarding a title and triggering a server event

manager.addCitizenDeathListener(event -> {
    CitizenData citizen = event.getCitizen();

    // Only care about world bosses
    if (!"world-bosses".equals(citizen.getGroup())) return;

    PlayerRef killer = event.getKillerRef();
    if (killer == null) return; // ignore non-player kills

    // Award a persistent "Boss Slayer" achievement in your own plugin
    YourPlugin.get().getAchievementManager()
        .award(killer.getUuid(), "boss_slayer_" + citizen.getId());

    // Trigger a server-wide event: open the boss chest room for 5 minutes
    YourPlugin.get().getEventManager().startBossLootEvent(
        citizen.getWorldUUID(),
        citizen.getPosition(),
        300  // seconds
    );

    System.out.println("[BossEvent] " + killer.getUsername()
        + " killed " + citizen.getName() + " — loot room opened for 5 minutes.");
});

Preventing the boss from dying below 20% health (phase trigger)

A classic boss mechanic: when Malgrath's health drops low, cancel the death, heal him partially, and trigger a rage phase instead.

// Track which bosses have already triggered their phase change
Set<String> phaseTriggered = new HashSet<>();

manager.addCitizenDeathListener(event -> {
    // This must be ran in a world thread. For this example, it's not

    CitizenData citizen = event.getCitizen();

    if (!"world-bosses".equals(citizen.getGroup())) return;
    if (phaseTriggered.contains(citizen.getId())) return; // phase already used

    // Suppress the death — Malgrath doesn't go down that easily
    event.setCancelled(true);
    phaseTriggered.add(citizen.getId());

    // Heal him to 20%
    NPCEntity npcEntity = citizen.getNpcRef().getStore().getComponent(citizen.getNpcRef(), NPCEntity.getComponentType());

    if (npcEntity != null) {
        EntityStatMap statMap = npcEntity.getReference().getStore().getComponent(npcEntity.getReference(), EntityStatsModule.get().getEntityStatMapComponentType());
        statMap.setStatValue(DefaultEntityStatTypes.getHealth(), 1600);
    }

    // Trigger a rage animation
    manager.playAnimationForCitizen(citizen, "Emote_Rage", 2);

    // Broadcast the phase change
    CitizensManager mgr = HyCitizensPlugin.get().getCitizensManager();
    // Update name to reflect the new phase
    citizen.setName("{#FF2222}Malgrath \n{#FF0000}ENRAGED");
    mgr.updateCitizenHologram(citizen, true);

    System.out.println("[BossEvent] " + citizen.getName() + " entered rage phase!");
});

Gating Amara behind a quest requirement

Block interaction with the merchant until a player has completed a prerequisite quest — without changing her citizen config at all.

manager.addCitizenInteractListener(event -> {
    if (!"merchants".equals(event.getCitizen().getGroup())) return;

    PlayerRef player = event.getPlayerRef();

    if (!YourPlugin.get().getQuestManager().hasCompleted(player.getUuid(), "prologue")) {
        event.setCancelled(true);
        player.sendMessage(CitizenInteraction.parseColoredMessage(
            "{GRAY}She looks busy. Come back once you've spoken with the Village Elder."
        ));
    }
});

Everything Combined

Here is everything from both citizens and all event listeners in a single ready-to-paste block, suitable for dropping into your plugin. Note: Spawn positions will need to be changed and Emotale from CurseForge is needed.

public void setupCitizens(UUID worldUUID) {
    CitizensManager manager = HyCitizensPlugin.get().getCitizensManager();

    // ──────────────────────────────────────────
    //  AMARA THE MERCHANT
    // ──────────────────────────────────────────

    CitizenData amara = new CitizenData(
        "amara_merchant",
        "Amara\nMerchant",
        "Player",
        worldUuid,
        new Vector3d(128, 64, 256),
        new Vector3f(0, 0, 0),
        1.0f,
        null,
        new ArrayList<>(),
        "",
        "",
        List.of(),
        true,
        false,
        "tidy",
        null,
        0L,
        true
    );

    amara.setGroup("merchants");

    amara.setNpcHand("Potion_Regen_Health");
    amara.setNpcHelmet("Armor_Leather_Light_Head");
    amara.setNpcChest("Armor_Leather_Light_Chest");

    amara.setAnimationBehaviors(List.of(
        new AnimationBehavior("DEFAULT",             "Idle",          0, 0f,    0f),
        new AnimationBehavior("ON_PROXIMITY_ENTER",  "Wave",    4, 0f,   10.0f, true, "Idle", 2.5f),
        new AnimationBehavior("ON_INTERACT",         "Jump",    4, 0f,    0f,   true, "Idle", 1.8f),
        new AnimationBehavior("TIMED",               "Eat", 4, 25.0f, 0f,   true, "Idle", 3.0f)
    ));

    MessagesConfig amaraMessages = new MessagesConfig(List.of(
        new CitizenMessage("{#FFD700}Hello, {PlayerName}! Looking for supplies?",              "BOTH", 0f),
        new CitizenMessage("{WHITE}I just restocked — fresh goods from the eastern caravans.", "BOTH", 0f),
        new CitizenMessage("{#90EE90}Come back any time, {PlayerName}. Safe travels!",         "BOTH", 0f)
    ), "SEQUENTIAL", true);
    amara.setMessagesConfig(amaraMessages);

    amara.setCommandActions(List.of(
        new CommandAction("{SendMessage}{GRAY}Here, take this — everyone needs a head start.", false, 0.3f, "F_KEY"),
        new CommandAction("give {PlayerName} Weapon_Sword_Steel",                              true,  0.8f, "F_KEY")
    ));

    amara.setMovementBehavior(new MovementBehavior("IDLE", 0f, 0f, 0f, 0f));
    manager.addCitizen(amara, true);


    // ──────────────────────────────────────────
    //  MALGRATH THE ASHEN KING
    // ──────────────────────────────────────────

    CitizenData malgrath = new CitizenData(
        "malgrath",
        "Malgrath\nThe Ashen King",
        "Skeleton_Fighter",
        worldUuid,
        new Vector3d(512, 70, 512),
        new Vector3f(0, 0, 0),
        2.5f,
        null,
        new ArrayList<>(),
        "",
        "",
        List.of(),
        false,
        false,
        null,
        null,
        0L,
        false
    );

    malgrath.setGroup("world-bosses");

    malgrath.setNpcHand("Weapon_Longsword_Adamantite");
    malgrath.setNpcHelmet("Armor_Adamantite_Head");
    malgrath.setNpcChest("Armor_Adamantite_Chest");
    malgrath.setNpcGloves("Armor_Adamantite_Hands");
    malgrath.setNpcLeggings("Armor_Adamantite_Legs");

    malgrath.setAttitude("AGGRESSIVE");
    malgrath.setTakesDamage(true);
    malgrath.setOverrideHealth(true);
    malgrath.setHealthAmount(8000f);
    malgrath.setOverrideDamage(true);
    malgrath.setDamageAmount(60f);
    malgrath.setRespawnOnDeath(true);
    malgrath.setRespawnDelaySeconds(600f);

    DetectionConfig detection = new DetectionConfig(true);
    detection.setViewRange(50f);
    detection.setViewSector(270f);
    detection.setHearingRange(30f);
    detection.setAbsoluteDetectionRange(5f);
    detection.setAlertedRange(60f);
    detection.setAlertedTimeMin(2.0f);   detection.setAlertedTimeMax(3.0f);
    detection.setSearchTimeMin(20.0f);   detection.setSearchTimeMax(30.0f);
    detection.setChanceToBeAlertedWhenReceivingCallForHelp(90);
    malgrath.setDetectionConfig(detection);

    CombatConfig combat = new CombatConfig();
    combat.setAttackType("Root_NPC_Attack_Melee");
    combat.setAttackDistance(4.5f);
    combat.setDesiredAttackDistanceMin(3.0f);
    combat.setDesiredAttackDistanceMax(4.0f);
    combat.setTargetRange(8.0f);
    combat.setAttackPauseMin(2.5f);
    ombat.setAttackPauseMax(3.5f);
    combat.setCombatAttackPreDelayMin(0.4f); 
    combat.setCombatAttackPreDelayMax(0.6f);
    combat.setCombatAttackPostDelayMin(0.3f);
    combat.setCombatAttackPostDelayMax(0.5f);
    combat.setChaseSpeed(0.85f);
    combat.setCombatBehaviorDistance(6.0f);
    combat.setCombatStrafeWeight(15);
    combat.setCombatDirectWeight(8);
    combat.setCombatStrafingDurationMin(1.5f); 
    combat.setCombatStrafingDurationMax(2.5f);
    combat.setCombatStrafingFrequencyMin(1.0f);
    combat.setCombatStrafingFrequencyMax(2.0f);
    combat.setCombatRelativeTurnSpeed(1.2f);
    combat.setCombatMovingRelativeSpeed(0.75f);
    combat.setCombatBackwardsRelativeSpeed(0.35f);
    combat.setBackOffAfterAttack(true);
    combat.setBackOffDistance(5.0f);
    combat.setBackOffDurationMin(1.5f);
    combat.setBackOffDurationMax(2.5f);
    combat.setBlockAbility("Shield_Block_Heavy");
    combat.setBlockProbability(40);
    combat.setTargetSwitchTimerMin(8.0f);
    combat.setTargetSwitchTimerMax(10.0f);
    combat.setUseCombatActionEvaluator(true);
    malgrath.setCombatConfig(combat);

    PatrolPath throneCircuit = new PatrolPath("MalgrathThrone", PatrolPath.LoopMode.PING_PONG);
    throneCircuit.addWaypoint(new PatrolWaypoint(512, 70, 512, 4.0f));
    throneCircuit.addWaypoint(new PatrolWaypoint(525, 70, 512, 1.0f));
    throneCircuit.addWaypoint(new PatrolWaypoint(525, 70, 525, 1.0f));
    throneCircuit.addWaypoint(new PatrolWaypoint(512, 70, 525, 1.0f));
    throneCircuit.addWaypoint(new PatrolWaypoint(499, 70, 525, 1.0f));
    throneCircuit.addWaypoint(new PatrolWaypoint(499, 70, 512, 1.0f));
    manager.getPatrolManager().savePath(throneCircuit);

    PathConfig pathConfig = new PathConfig();
    pathConfig.setFollowPath(true);
    pathConfig.setPathName("MalgrathThrone");
    pathConfig.setPatrol(true);
    pathConfig.setLoopMode("PING_PONG");
    malgrath.setPathConfig(pathConfig);
    malgrath.setMovementBehavior(new MovementBehavior("PATROL", 6.0f, 0f, 0f, 0f));

    DeathConfig deathConfig = new DeathConfig();
    deathConfig.setDropItems(List.of(
        new DeathDropItem("Weapon_Longsword_Adamantite", 1),
        new DeathDropItem("Ingredient_Bar_Adamantite", 25),
        new DeathDropItem("Ingredient_Bone_Fragment", 50)
    ));
    deathConfig.setDeathMessages(List.of(
        new CitizenMessage("{#FF8C00}You have slain {CitizenName}! The Ashen King falls.", "BOTH", 0.0f),
        new CitizenMessage("{GRAY}Legendary loot has dropped at the kill site.", "BOTH", 1.5f)
    ));
    deathConfig.setMessageSelectionMode("ALL");
    deathConfig.setDeathCommands(List.of(
        new CommandAction("say {#FF2222}[World Boss] {PlayerName} has defeated {CitizenName}!", true, 0.5f)
    ));
    deathConfig.setCommandSelectionMode("ALL");
    malgrath.setDeathConfig(deathConfig);

    manager.addCitizen(malgrath, true);


    // ──────────────────────────────────────────
    //  EVENT LISTENERS
    // ──────────────────────────────────────────

    // Gate Amara behind prologue quest completion
    manager.addCitizenInteractListener(event -> {
        if (!"merchants".equals(event.getCitizen().getGroup())) return;
        PlayerRef player = event.getPlayerRef();
        if (!YourPlugin.get().getQuestManager().hasCompleted(player.getUuid(), "prologue")) {
            event.setCancelled(true);
            player.sendMessage(CitizenInteraction.parseColoredMessage(
                "{GRAY}She looks busy. Come back once you've spoken with the Village Elder."
            ));
        }
    });

    // Boss death: award achievement, open loot room, phase-change logic
    Set<String> phaseTriggered = new HashSet<>();

    manager.addCitizenDeathListener(event -> {
        CitizenData citizen = event.getCitizen();
        if (!"world-bosses".equals(citizen.getGroup())) return;

        PlayerRef killer = event.getKillerRef();

        // Phase change: cancel first death and trigger rage
        if (!phaseTriggered.contains(citizen.getId())) {
            event.setCancelled(true);
            phaseTriggered.add(citizen.getId());
            manager.playAnimationForCitizen(citizen, "Emote_Rage", 2);
            citizen.setName("{#FF2222}☠ Malgrath ☠\n{#FF0000}ENRAGED\n{GRAY}World Boss");
            manager.updateCitizenHologram(citizen, true);
            return;
        }

        // Second death — the real kill
        phaseTriggered.remove(citizen.getId()); // reset for next spawn
        if (killer == null) return;

        YourPlugin.get().getAchievementManager()
            .award(killer.getUuid(), "boss_slayer_" + citizen.getId());

        YourPlugin.get().getEventManager().startBossLootEvent(
            citizen.getWorldUUID(), citizen.getPosition(), 300
        );
    });
}
Next steps — explore the full model reference to see every field available on each config class. Good pages to visit next: CitizenData, CitizensManager, and Events.