Skip to main content
The NPC surface provides helpers for spawning NPC entities, querying their role and variant data, and asserting on despawn state. It builds on WorldTestContext and adds NPC-specific operations through NPCTestAdapter and NPCAssert. NPCs are the living inhabitants of Hytale - Kweebecs, Trork Warriors, Outlander Hunters, and any custom roles your mod defines. Testing NPCs is about verifying that the right creatures spawn with the right properties and behave as expected.
NPC role names like Trork_Warrior and Kweebec_Sapling are examples from Hytale’s default content. Your mod’s NPCs will have their own role names. If a role name is not recognized, spawnEntity falls back to creating an empty entity with a TransformComponent. Use spawnNPC when you need to guarantee the NPC type is correct - it throws an error if the role is invalid.

spawnEntity vs spawnNPC

Understanding the difference between these two methods is important:
  • ctx.spawnEntity("Kweebec_Sapling", x, y, z) - Attempts to spawn via NPCPlugin first. If it fails, falls back to creating an empty entity with no components. The empty entity will have no health, no AI, and no model. This silent fallback can make tests pass when they should fail.
  • ctx.spawnNPC("Kweebec_Sapling", x, y, z) - Directly calls NPCPlugin to spawn a fully initialized NPC. If the role name is not recognized, it fails explicitly rather than silently creating an empty entity. This is the preferred method for NPC tests.
Always use spawnNPC when you need a real NPC with health, AI, and model components. Use spawnEntity only when you intentionally want the fallback behavior for generic entity testing.

Complete Example Suite

package com.example.tests;

import com.frotty27.hrtk.api.annotation.HytaleSuite;
import com.frotty27.hrtk.api.annotation.WorldTest;
import com.frotty27.hrtk.api.annotation.Tag;
import com.frotty27.hrtk.api.annotation.DisplayName;
import com.frotty27.hrtk.api.annotation.Order;
import com.frotty27.hrtk.api.assert_.HytaleAssert;
import com.frotty27.hrtk.api.assert_.NPCAssert;
import com.frotty27.hrtk.api.assert_.StatsAssert;
import com.frotty27.hrtk.api.context.WorldTestContext;
import com.frotty27.hrtk.api.lifecycle.IsolationStrategy;

@HytaleSuite(value = "NPC Surface Tests", isolation = IsolationStrategy.DEDICATED_WORLD)
@Tag("npc")
public class NPCSurfaceTests {

    @WorldTest
    @Order(1)
    @DisplayName("Spawn a Trork_Warrior and verify its role")
    void spawnTrorkAndVerifyRole(WorldTestContext ctx) {
        // spawnNPC takes the PascalCase role name with underscores.
        // The role name must match an entry in the NPC role registry.
        Object npc = ctx.spawnNPC("Trork_Warrior", 10, 64, 10);
        ctx.waitTicks(1);

        Object store = ctx.getStore();

        // assertNPCEntity verifies the entity has the NPC marker components.
        NPCAssert.assertNPCEntity(store, npc);

        // assertRoleName checks the role name stored in the NPC's role component.
        NPCAssert.assertRoleName(store, npc, "Trork_Warrior");
    }

    @WorldTest
    @Order(2)
    @DisplayName("Spawn a Kweebec_Sapling with a variant")
    void spawnKweebecWithVariant(WorldTestContext ctx) {
        // The variant parameter allows spawning a specific visual or behavioral variant.
        // Pass null for the default variant.
        Object npc = ctx.spawnNPC("Kweebec_Sapling", "elder", 20, 64, 20);
        ctx.waitTicks(1);

        Object store = ctx.getStore();
        NPCAssert.assertNPCEntity(store, npc);
        NPCAssert.assertRoleName(store, npc, "Kweebec_Sapling");
    }

    @WorldTest
    @Order(3)
    @DisplayName("Spawned NPC is alive and healthy")
    void spawnedNPCIsAlive(WorldTestContext ctx) {
        Object npc = ctx.spawnNPC("Trork_Warrior", 30, 64, 30);
        ctx.waitTicks(1);

        Object store = ctx.getStore();

        // Combine NPC assertions with StatsAssert to verify full initialization.
        // A properly spawned NPC should be both an NPC entity and alive.
        NPCAssert.assertNPCEntity(store, npc);
        StatsAssert.assertAlive(store, npc);
        StatsAssert.assertHealthAtMax(store, npc);
    }

    @WorldTest
    @Order(4)
    @DisplayName("Verify NPC is not in despawning state after spawn")
    void npcNotDespawningAfterSpawn(WorldTestContext ctx) {
        Object npc = ctx.spawnNPC("Outlander_Hunter", 40, 64, 40);
        ctx.waitTicks(1);

        Object store = ctx.getStore();

        // assertNotDespawning checks that the NPC has not entered the despawn process.
        // NPCs may despawn when they are too far from players or when the server
        // decides to cull them. A freshly spawned NPC should not be despawning.
        NPCAssert.assertNotDespawning(store, npc);
    }

    @WorldTest
    @Order(5)
    @DisplayName("Despawned NPC no longer exists in the world")
    void despawnedNPCIsRemoved(WorldTestContext ctx) {
        Object npc = ctx.spawnNPC("Trork_Warrior", 50, 64, 50);
        ctx.waitTicks(1);

        HytaleAssert.assertTrue("NPC should exist after spawning", ctx.entityExists(npc));

        // Explicitly despawn the NPC.
        ctx.despawn(npc);
        ctx.waitTicks(1);

        HytaleAssert.assertFalse(
            "NPC should not exist after despawning",
            ctx.entityExists(npc)
        );
    }

    @WorldTest
    @Order(6)
    @DisplayName("Verify that a known role exists in the registry")
    void roleExistsInRegistry(WorldTestContext ctx) {
        // assertRoleExists checks the NPC role registry without spawning anything.
        // This is useful for smoke tests that verify your mod's NPC roles are registered.
        NPCAssert.assertRoleExists("Kweebec_Sapling");
        NPCAssert.assertRoleExists("Trork_Warrior");
        NPCAssert.assertRoleExists("Trork_Warrior");
    }
}

Adapter Methods

MethodParametersReturnsDescription
spawnNPCString role, double x, double y, double zObjectSpawn a fully initialized NPC at the given position
spawnNPCString role, String variant, double x, double y, double zObjectSpawn an NPC with a specific variant
getNPCRoleObject store, Object refStringGet the role name of an NPC entity
getNPCVariantObject store, Object refStringGet the variant identifier of an NPC
isDespawningObject store, Object refbooleanCheck if the NPC is currently despawning
getRoleRegistrynoneObjectGet the NPC role registry for role lookups

Assertion Methods

MethodParametersFailure Message
assertNPCEntityObject store, Object ref”Expected entity to be an NPC”
assertRoleNameObject store, Object ref, String expected”Expected NPC role [expected] but was [actual]“
assertRoleExistsString roleName”NPC role [roleName] not found in registry”
assertNotDespawningObject store, Object ref”Expected NPC to not be despawning”

NPC Role Names

NPC role names in Hytale use PascalCase with underscores to separate words. Common built-in roles include:
  • Kweebec_Sapling - Friendly forest creature
  • Trork_Warrior - Hostile undead melee combatant
  • Outlander_Hunter - Ranged hostile NPC
Your mod can register custom roles that follow the same naming convention.
Call ctx.waitTicks(1) after spawning before asserting. The NPC needs one tick to initialize all its components (health, AI, model, role data). Asserting immediately after spawnNPC may find the entity in a partially initialized state.

Next Steps