Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.hrtk.frotty27.com/llms.txt

Use this file to discover all available pages before exploring further.

Hytale entities can have active effects managed by an EffectControllerComponent. HRTK’s EffectAssert class lets you verify which effects are present, count active effects, and check invulnerability state. Status effects are a critical part of game balance - poison, speed boosts, shields, and invulnerability all change how entities interact with the world. Testing effects ensures that your mod applies the right effects at the right time, that effects stack correctly, and that removal works cleanly.
Effect assertions require entities with an EffectControllerComponent. Not all entity types have this component. Use spawnNPC with a role name that includes effects, or check for the component before asserting.

Important: Empty Entities Lack EffectControllerComponent

A common pitfall when testing effects is using a bare entity created with ctx.createEntity(). Empty entities have no components at all, including no EffectControllerComponent. If you try to assert on effects for an empty entity, the assertion will fail because there is no controller to inspect. Always spawn a fully initialized NPC using ctx.spawnNPC(...) when testing effects. NPC entities come with the EffectControllerComponent pre-attached as part of their standard component set.

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_.EffectAssert;
import com.frotty27.hrtk.api.assert_.HytaleAssert;
import com.frotty27.hrtk.api.context.WorldTestContext;
import com.frotty27.hrtk.api.lifecycle.IsolationStrategy;

@HytaleSuite(value = "Effect Surface Tests", isolation = IsolationStrategy.DEDICATED_WORLD)
@Tag("effects")
public class EffectSurfaceTests {

    private static final int POISON_INDEX = 0;
    private static final int SPEED_BOOST_INDEX = 1;
    private static final int SHIELD_INDEX = 2;

    @WorldTest
    @Order(1)
    @DisplayName("Freshly spawned NPC has zero active effects")
    void newNPCHasNoEffects(WorldTestContext ctx) {
        // Spawn a real NPC so it has an EffectControllerComponent.
        Object entity = ctx.spawnNPC("Kweebec_Sapling", 0, 64, 0);
        ctx.waitTicks(1);

        Object store = ctx.getStore();

        // A freshly spawned NPC should have no active effects.
        EffectAssert.assertEffectCount(store, entity, 0);
    }

    @WorldTest
    @Order(2)
    @DisplayName("Apply an effect and verify it is present")
    void applyEffectAndVerify(WorldTestContext ctx) {
        Object entity = ctx.spawnNPC("Trork_Warrior", 10, 64, 10);
        ctx.waitTicks(1);

        // Apply a poison effect through the effect system.
        // Effect indices are integer identifiers used by Hytale's effect system.
        ctx.applyEffect(entity, POISON_INDEX);
        ctx.waitTicks(1);

        Object store = ctx.getStore();
        EffectAssert.assertHasEffect(store, entity, POISON_INDEX);
        EffectAssert.assertEffectCount(store, entity, 1);
    }

    @WorldTest
    @Order(3)
    @DisplayName("Remove an effect and verify it is gone")
    void removeEffectAndVerify(WorldTestContext ctx) {
        Object entity = ctx.spawnNPC("Trork_Warrior", 20, 64, 20);
        ctx.waitTicks(1);

        // Apply, then remove.
        ctx.applyEffect(entity, SPEED_BOOST_INDEX);
        ctx.waitTicks(1);
        EffectAssert.assertHasEffect(ctx.getStore(), entity, SPEED_BOOST_INDEX);

        ctx.removeEffect(entity, SPEED_BOOST_INDEX);
        ctx.waitTicks(1);
        EffectAssert.assertNoEffect(ctx.getStore(), entity, SPEED_BOOST_INDEX);
    }

    @WorldTest
    @Order(4)
    @DisplayName("Multiple effects can be active simultaneously")
    void multipleEffectsStack(WorldTestContext ctx) {
        Object entity = ctx.spawnNPC("Kweebec_Sapling", 30, 64, 30);
        ctx.waitTicks(1);

        // Apply three different effects.
        ctx.applyEffect(entity, POISON_INDEX);
        ctx.applyEffect(entity, SPEED_BOOST_INDEX);
        ctx.applyEffect(entity, SHIELD_INDEX);
        ctx.waitTicks(1);

        Object store = ctx.getStore();
        EffectAssert.assertEffectCount(store, entity, 3);
        EffectAssert.assertHasEffect(store, entity, POISON_INDEX);
        EffectAssert.assertHasEffect(store, entity, SPEED_BOOST_INDEX);
        EffectAssert.assertHasEffect(store, entity, SHIELD_INDEX);
    }

    @WorldTest
    @Order(5)
    @DisplayName("Invulnerability effect makes entity invulnerable")
    void invulnerabilityWorks(WorldTestContext ctx) {
        Object entity = ctx.spawnNPC("Outlander_Hunter", 40, 64, 40);
        ctx.waitTicks(1);

        // Apply invulnerability through the effect system.
        // assertInvulnerable checks a separate flag on the EffectControllerComponent.
        ctx.applyInvulnerability(entity);
        ctx.waitTicks(1);

        EffectAssert.assertInvulnerable(ctx.getStore(), entity);
    }

    @WorldTest
    @Order(6)
    @DisplayName("Effect is absent when it was never applied")
    void effectAbsentWhenNeverApplied(WorldTestContext ctx) {
        Object entity = ctx.spawnNPC("Kweebec_Sapling", 50, 64, 50);
        ctx.waitTicks(1);

        // assertNoEffect passes when the entity does not have the specified effect.
        // This is a negative test - verify that effects are not present spuriously.
        EffectAssert.assertNoEffect(ctx.getStore(), entity, POISON_INDEX);
        EffectAssert.assertNoEffect(ctx.getStore(), entity, SPEED_BOOST_INDEX);
    }
}

EffectAssert Methods

MethodDescription
assertHasEffect(store, ref, effectIndex)Assert entity has the effect at the given index
assertNoEffect(store, ref, effectIndex)Assert entity does NOT have the effect
assertEffectCount(store, ref, expected)Assert the number of active effects
assertInvulnerable(store, ref)Assert entity is invulnerable

How It Works

EffectAssert locates the EffectControllerComponent on the entity via reflection, then calls:
  • hasEffect(int index) to check for specific effects
  • getActiveEffects() to count active effects
  • isInvulnerable() for invulnerability checks
Effect indices are integer identifiers used by Hytale’s effect system. The specific index values depend on your game content and mod configuration. Define constants in your test suite for readability, as shown in the example above.

Hytale Effect System API

Beyond the HRTK wrappers, the Hytale server exposes these key effect classes directly:
  • EffectControllerComponent - The ECS component on entities that manages all active effects. Has addEffect(), addInfiniteEffect(), removeEffect(), clearEffects(), isInvulnerable(), setInvulnerable(), getAllActiveEntityEffects().
  • ActiveEntityEffect - Represents a running effect instance. Has getRemainingDuration(), isInfinite(), isDebuff(), isInvulnerable().
  • EntityEffect (asset) - The static effect definition loaded from content. Has getDuration(), getOverlapBehavior(), getStatModifiers(), isDebuff(), isInfinite().
  • OverlapBehavior (enum) - EXTEND, OVERWRITE, IGNORE. Controls what happens when the same effect is applied twice.
For advanced effect testing, access these classes directly instead of going through the HRTK adapter:
var controller = (EffectControllerComponent) ctx.getComponent(
    entity, EffectControllerComponent.getComponentType()
);
controller.addEffect(effectIndex, duration, isDebuff, isInvulnerable);

Duration and Debuff Considerations

Effects in Hytale typically have a duration (measured in ticks). When testing timed effects:
  • Apply the effect, then call ctx.waitTicks(n) to advance time
  • If the effect duration is shorter than your wait, the effect will have expired
  • Use assertNoEffect after waiting to verify that timed effects expire correctly
  • Debuffs (like poison) deal damage over time, so combine with StatsAssert.assertAlive() to verify the entity survives the debuff duration
Effect tests modify entity state. Use IsolationStrategy.DEDICATED_WORLD:
@HytaleSuite(value = "Effect Tests", isolation = IsolationStrategy.DEDICATED_WORLD)
@Tag("effects")
public class EffectTests { }

Next Steps