Hytale Modding
Entity Component System

Example ECS Plugin

In this guide you will learn how to create a simple poison system utilizing all of the features you previously learned about Hytale's ECS system

Practical Example: Poison System

Putting together everything you learned so far, here's a complete poison effect. When applied to any entity, it deals damage at a set interval until it expires and removes itself.

package scot.oskar.hytaletemplate.components;

import com.hypixel.hytale.component.Component;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import javax.annotation.Nullable;

public class PoisonComponent implements Component<EntityStore> {

  private float damagePerTick;
  private float tickInterval;
  private int remainingTicks;
  private float elapsedTime;

  // Add static getter and setter to store componentType in the component instead of getting it from the plugin singleton.
  // - This allows anyone to call PoisonComponent.getComponentType() instead of ExamplePlugin.get().getPoisonComponentType();
  // - You could also update the setter to be a singleton if desired.
  // - Adding a throw condition if getComponentType() returns null could also be good.
  private static ComponentType<EntityStore, PoisonComponent> type;
  public static ComponentType<EntityStore, PoisonComponent> getComponentType(){
    return this.type;
  }
  public static void setComponentType(ComponentType<EntityStore, PoisonComponent> type){
    this.type = type;
  }

  // A builder codec is needed to allow components to be saved and loaded from disk
  public static final BuilderCodec<PoisonComponent> CODEC = BuilderCodec
        .builder(PoisonComponent.class, PoisonComponent::new)
        .append(
            new KeyedCodec<>("DamagePerTick", Codec.FLOAT),
            (component, value) -> component.damagePerTick = value,
            component -> component.damagePerTick
        ).add()
        .append(
            new KeyedCodec<>("TickInterval", Codec.FLOAT),
            (component, value) -> component.tickInterval = value,
            component -> component.tickInterval
        ).add()
        .append(
            new KeyedCodec<>("RemainingTicks", Codec.INTEGER),
            (component, value) -> component.remainingTicks = value,
            component -> component.remainingTicks
        )
        .add()
        .append(
            new KeyedCodec<>("ElapsedTime", Codec.FLOAT),
            (component, value) -> component.elapsedTime = value,
            component -> component.elapsedTime
        )
        .add()
        .build();

  public PoisonComponent() {
    this(5f, 1.0f, 10);
  }

  public PoisonComponent(float damagePerTick, float tickInterval, int totalTicks) {
    this.damagePerTick = damagePerTick;
    this.tickInterval = tickInterval;
    this.remainingTicks = totalTicks;
    this.elapsedTime = 0f;
  }

  public PoisonComponent(PoisonComponent other) {
    this.damagePerTick = other.damagePerTick;
    this.tickInterval = other.tickInterval;
    this.remainingTicks = other.remainingTicks;
    this.elapsedTime = other.elapsedTime;
  }

  @Nullable
  @Override
  public Component<EntityStore> clone() {
    return new PoisonComponent(this);
  }

  public float getDamagePerTick() {
    return damagePerTick;
  }

  public float getTickInterval() {
    return tickInterval;
  }

  public int getRemainingTicks() {
    return remainingTicks;
  }

  public float getElapsedTime() {
    return elapsedTime;
  }

  public void addElapsedTime(float dt) {
    this.elapsedTime += dt;
  }

  public void resetElapsedTime() {
    this.elapsedTime = 0f;
  }

  public void decrementRemainingTicks() {
    this.remainingTicks--;
  }

  public boolean isExpired() {
    return this.remainingTicks <= 0;
  }
}
package scot.oskar.hytaletemplate.systems;

import com.hypixel.hytale.component.ArchetypeChunk;
import com.hypixel.hytale.component.CommandBuffer;
import com.hypixel.hytale.component.ComponentType;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.component.SystemGroup;
import com.hypixel.hytale.component.query.Query;
import com.hypixel.hytale.component.system.tick.EntityTickingSystem;
import com.hypixel.hytale.server.core.modules.entity.damage.Damage;
import com.hypixel.hytale.server.core.modules.entity.damage.DamageCause;
import com.hypixel.hytale.server.core.modules.entity.damage.DamageModule;
import com.hypixel.hytale.server.core.modules.entity.damage.DamageSystems;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import scot.oskar.hytaletemplate.components.PoisonComponent;

public class PoisonSystem extends EntityTickingSystem<EntityStore> {

  private final ComponentType<EntityStore, PoisonComponent> poisonComponentType;

  public PoisonSystem(ComponentType<EntityStore, PoisonComponent> poisonComponentType) {
    this.poisonComponentType = poisonComponentType;
  }

  @Override
  public void tick(float dt, int index, @Nonnull ArchetypeChunk<EntityStore> archetypeChunk,
      @Nonnull Store<EntityStore> store, @Nonnull CommandBuffer<EntityStore> commandBuffer) {

    // You could also just call PoisonComponent.getComponentType() instead of taking in the passed in variable here.
    PoisonComponent poison = archetypeChunk.getComponent(index, poisonComponentType);
    Ref<EntityStore> ref = archetypeChunk.getReferenceTo(index);

    poison.addElapsedTime(dt);

    if (poison.getElapsedTime() >= poison.getTickInterval()) {
      poison.resetElapsedTime();

      Damage damage = new Damage(Damage.NULL_SOURCE, DamageCause.OUT_OF_WORLD, poison.getDamagePerTick());
      DamageSystems.executeDamage(ref, commandBuffer, damage);

      poison.decrementRemainingTicks();
    }

    if (poison.isExpired()) {
      commandBuffer.removeComponent(ref, poisonComponentType);
    }
  }

  @Nullable
  @Override
  public SystemGroup<EntityStore> getGroup() {
    return DamageModule.get().getGatherDamageGroup();
  }

  @Nonnull
  @Override
  public Query<EntityStore> getQuery() {
    return Query.and(this.poisonComponentType);
  }
}
package scot.oskar.hytaletemplate.commands;

public class ExampleCommand extends AbstractPlayerCommand {

  public ExampleCommand() {
    super("test", "Super test command!");
  }

  @Override
  protected void execute(@Nonnull CommandContext commandContext, @Nonnull Store<EntityStore> store, 
      @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef playerRef, @Nonnull World world) {
    Player player = store.getComponent(ref, Player.getComponentType());
    PoisonComponent poison = new PoisonComponent(3f, 0.5f, 8);
    store.addComponent(ref, PoisonComponent.getComponentType(), poison);
    player.sendMessage(Message.raw("You have been poisoned!").color(Color.GREEN).bold(true));
  }
}
package scot.oskar.hytaletemplate;

public final class ExamplePlugin extends JavaPlugin {

  private static ExamplePlugin instance;

  public ExamplePlugin(@Nonnull JavaPluginInit init) {
    super(init);
    instance = this;
  }

  @Override
  protected void setup() {
    this.getCommandRegistry().registerCommand(new ExampleCommand());
    this.getEventRegistry().registerGlobal(PlayerReadyEvent.class, ExampleEvent::onPlayerReady);
    this.getEventRegistry().registerGlobal(PlayerChatEvent.class, ChatFormatter::onPlayerChat);

    // Register the component & set the type in the component class so its easier to retrieve.
    ComponentType<EntityStore, PoisonComponent> poisonComponentType = this.getEntityStoreRegistry()
        .registerComponent(PoisonComponent.class, PoisonComponent::new);
    PoisonComponent.setComponentType(poisonComponentType);

    this.getEntityStoreRegistry().registerSystem(new PoisonSystem(PoisonComponent.getComponentType()));
  }

  public static ExamplePlugin get() {
    return instance;
  }
}

The query uses only poisonComponentType which means the system will process any entity with a PoisonComponent, not just players. This makes it flexible for poisoning NPCs, mobs, or any living entity. The system places itself in the GatherDamageGroup so the damage it creates flows through the full damage pipeline including armor reduction and invulnerability checks.