diff --git a/api/build.gradle.kts b/api/build.gradle.kts new file mode 100644 index 0000000..66bda38 --- /dev/null +++ b/api/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + `java-library` +} + +dependencies{ + api(project(":nodes")) +} diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 61761b7..8195364 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -4,5 +4,6 @@ plugins{ dependencies{ api(project(":nodes")) + api(project(":api")) api(libs.bundles.eldoria.utilities) } diff --git a/core/src/main/java/de/eldoria/bloodnight/mob/CustomMob.java b/core/src/main/java/de/eldoria/bloodnight/mob/CustomMob.java index a3f26c1..bb73b49 100644 --- a/core/src/main/java/de/eldoria/bloodnight/mob/CustomMob.java +++ b/core/src/main/java/de/eldoria/bloodnight/mob/CustomMob.java @@ -4,8 +4,15 @@ import de.eldoria.bloodnight.mob.meta.MobDrops; import de.eldoria.bloodnight.mob.meta.Equipment; import de.eldoria.bloodnight.mob.meta.Extension; +import de.eldoria.bloodnight.nodes.container.ContainerMeta; import de.eldoria.bloodnight.nodes.container.NodeContainer; +import org.bukkit.entity.Entity; public record CustomMob(String id, Equipment equipment, Attributes attributes, NodeContainer nodes, MobDrops mobDrops, Extension extension) { + public CustomMob copy(Entity entity, Entity extension) { + NodeContainer copy = nodes.copy(); + copy.inject(new ContainerMeta(entity, extension)); + return new CustomMob(id, equipment, attributes, copy, mobDrops, this.extension); + } } diff --git a/core/src/main/java/de/eldoria/bloodnight/mob/meta/Attributes.java b/core/src/main/java/de/eldoria/bloodnight/mob/meta/Attributes.java index 7a093f2..d6d2678 100644 --- a/core/src/main/java/de/eldoria/bloodnight/mob/meta/Attributes.java +++ b/core/src/main/java/de/eldoria/bloodnight/mob/meta/Attributes.java @@ -1,5 +1,6 @@ package de.eldoria.bloodnight.mob.meta; +import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; public record Attributes(String displayName, @@ -9,6 +10,8 @@ public record Attributes(String displayName, ValueModifier damageModifier, double damage) { public boolean isAssignable(EntityType type) { if(exactType) return type == entityType; - return entityType.getEntityClass().isAssignableFrom(type.getEntityClass()); + Class thisClass = entityType.getEntityClass(); + Class thatClass = type.getEntityClass(); + return thisClass != null && thatClass != null && thisClass.isAssignableFrom(thatClass); } } diff --git a/core/src/main/java/de/eldoria/bloodnight/mobs/MobCoordinator.java b/core/src/main/java/de/eldoria/bloodnight/mobs/MobCoordinator.java index 85acc00..c571ade 100644 --- a/core/src/main/java/de/eldoria/bloodnight/mobs/MobCoordinator.java +++ b/core/src/main/java/de/eldoria/bloodnight/mobs/MobCoordinator.java @@ -7,7 +7,14 @@ import java.util.HashMap; import java.util.Map; +/** + * Class which holds all custom mobs. + * Responsible for dispatching event triggers to the correct custom mob. + */ public class MobCoordinator { + /** + * Maps which references the {@link Entity#getEntityId()} and holds the custom mob assigned to id. + */ private final Map mobs = new HashMap<>(); /** @@ -22,7 +29,16 @@ public void trigger(Entity entity, TriggerData trigger) { customMob.nodes().dispatch(trigger); } + /** + * Registers the custom mob on the provided entity. + * Will create a copy of the custom mob using {@link CustomMob#copy(Entity, Entity)} + * If this mob is already registered, it will be changed to the new custom mob. + * + * @param entity the entity to assign the mob to + * @param customMob the custom mob assigned to the entity. + */ public void register(Entity entity, CustomMob customMob) { - + // TODO spawn and register extension + mobs.put(entity.getEntityId(), customMob.copy(entity, null)); } } diff --git a/core/src/main/java/de/eldoria/bloodnight/mobs/MobRegistry.java b/core/src/main/java/de/eldoria/bloodnight/mobs/MobRegistry.java index e52cf3c..8c9d54a 100644 --- a/core/src/main/java/de/eldoria/bloodnight/mobs/MobRegistry.java +++ b/core/src/main/java/de/eldoria/bloodnight/mobs/MobRegistry.java @@ -3,9 +3,11 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import de.eldoria.bloodnight.mob.CustomMob; +import de.eldoria.bloodnight.mob.meta.Attributes; +import de.eldoria.bloodnight.mobs.exceptions.MobIdAlreadyTakenException; +import de.eldoria.bloodnight.util.Checks; import org.bukkit.entity.EntityType; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -24,11 +26,43 @@ public MobRegistry() { this.mobs = new HashMap<>(); } + /** + * Registers a new {@link CustomMob} at the registry. + * + * @param mob the mob to be registered. + * @throws MobIdAlreadyTakenException when the id is already taken. + */ public void add(CustomMob mob) { + check(mob); + if (mobs.containsKey(mob.id())) throw new MobIdAlreadyTakenException(mob, get(mob.id())); mobs.put(mob.id(), mob); } + + /** + * Returns the {@link CustomMob} associated with the given id. + * + * @param id the id of the mob to be retrieved. + * @return the {@link CustomMob} associated with the given id, or null if no mob is found. + */ + public CustomMob get(String id) { + return mobs.get(id); + } + + /** + * Gets all matching {@link CustomMob}s, depending on the result of calling {@link Attributes#isAssignable(EntityType)}. + * + * @param active a set containing the ids of active mobs that should be checked + * @param type the type of the entity + * @return an immutable list containing all mobs that are matching. May be empty. + */ public List getMatching(Set active, EntityType type) { return active.stream().map(mobs::get).filter(mob -> mob.attributes().isAssignable(type)).toList(); } + + private void check(CustomMob mob) { + Checks.notNull(mob, "Mob can not be null"); + Checks.notNull(mob.id(), "Mob id can not be null"); + Checks.lowerCase(mob.id(), "Mob id must be lower case."); + } } diff --git a/core/src/main/java/de/eldoria/bloodnight/mobs/TriggerDispatcher.java b/core/src/main/java/de/eldoria/bloodnight/mobs/TriggerDispatcher.java deleted file mode 100644 index ce05e37..0000000 --- a/core/src/main/java/de/eldoria/bloodnight/mobs/TriggerDispatcher.java +++ /dev/null @@ -1,7 +0,0 @@ -package de.eldoria.bloodnight.mobs; - -import org.bukkit.event.Listener; - -public class TriggerDispatcher implements Listener { - -} diff --git a/core/src/main/java/de/eldoria/bloodnight/mobs/dispatcher/DefaultTriggerDispatcher.java b/core/src/main/java/de/eldoria/bloodnight/mobs/dispatcher/DefaultTriggerDispatcher.java new file mode 100644 index 0000000..2265f3c --- /dev/null +++ b/core/src/main/java/de/eldoria/bloodnight/mobs/dispatcher/DefaultTriggerDispatcher.java @@ -0,0 +1,42 @@ +package de.eldoria.bloodnight.mobs.dispatcher; + +import de.eldoria.bloodnight.mobs.MobCoordinator; +import de.eldoria.bloodnight.nodes.dispatching.TriggerData; +import de.eldoria.bloodnight.nodes.trigger.TriggerNode; +import de.eldoria.bloodnight.nodes.trigger.impl.events.OnDeathNode; +import de.eldoria.bloodnight.nodes.trigger.impl.events.OnExplosionPrimeNode; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.entity.EntityDamageByBlockEvent; +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.entity.EntityDeathEvent; +import org.bukkit.event.entity.ExplosionPrimeEvent; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * This class listens to events, wraps their data into a {@link TriggerData} and trigger corresponding {@link TriggerNode}. + * This class only covers predefined trigger nodes. + * Custom nodes will need to create their own trigger dispatcher. + */ +public class DefaultTriggerDispatcher extends TriggerDispatcher { + + public DefaultTriggerDispatcher(MobCoordinator coordinator) { + super(coordinator); + } + + @EventHandler + public void onDeath(EntityDeathEvent event) { + trigger(event.getEntity(), TriggerData.forNode(OnDeathNode.class, event)); + } + + @EventHandler + public void onExplosionPrime(ExplosionPrimeEvent event) { + trigger(event.getEntity(), TriggerData.forNode(OnExplosionPrimeNode.class, event)); + } + +} diff --git a/core/src/main/java/de/eldoria/bloodnight/mobs/dispatcher/KillDispatcher.java b/core/src/main/java/de/eldoria/bloodnight/mobs/dispatcher/KillDispatcher.java new file mode 100644 index 0000000..b6ffb61 --- /dev/null +++ b/core/src/main/java/de/eldoria/bloodnight/mobs/dispatcher/KillDispatcher.java @@ -0,0 +1,42 @@ +package de.eldoria.bloodnight.mobs.dispatcher; + +import de.eldoria.bloodnight.mobs.MobCoordinator; +import de.eldoria.bloodnight.nodes.dispatching.TriggerData; +import de.eldoria.bloodnight.nodes.trigger.impl.events.OnDeathNode; +import de.eldoria.bloodnight.nodes.trigger.impl.events.OnEntityKillNode; +import de.eldoria.bloodnight.nodes.trigger.impl.events.OnPlayerKillNode; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.event.entity.EntityDeathEvent; + +public class KillDispatcher extends TriggerDispatcher { + public KillDispatcher(MobCoordinator coordinator) { + super(coordinator); + } + + /** + * This handler is purely for observing dealt damage to entities to notice who killed it. + * + * @param event damage event + */ + @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH) + public void onEntityDamageDelegate(EntityDamageByEntityEvent event) { + if (!(event.getEntity() instanceof LivingEntity living)) return; + // Check if entity will be killed. + if (event.getFinalDamage() >= living.getHealth()) { + if (event.getEntity() instanceof Player) { + trigger(event.getDamager(), TriggerData.forNode(OnPlayerKillNode.class, event)); + } else { + trigger(event.getDamager(), TriggerData.forNode(OnEntityKillNode.class, event)); + } + } + } + + @EventHandler + public void onEntityDeath(EntityDeathEvent event) { + trigger(event.getEntity(), TriggerData.forNode(OnDeathNode.class, event)); + } +} diff --git a/core/src/main/java/de/eldoria/bloodnight/mobs/dispatcher/TriggerDispatcher.java b/core/src/main/java/de/eldoria/bloodnight/mobs/dispatcher/TriggerDispatcher.java new file mode 100644 index 0000000..a249202 --- /dev/null +++ b/core/src/main/java/de/eldoria/bloodnight/mobs/dispatcher/TriggerDispatcher.java @@ -0,0 +1,22 @@ +package de.eldoria.bloodnight.mobs.dispatcher; + +import de.eldoria.bloodnight.mobs.MobCoordinator; +import de.eldoria.bloodnight.nodes.dispatching.TriggerData; +import org.bukkit.entity.Entity; +import org.bukkit.event.Listener; + +/** + * Basic Trigger dispatcher. + * Holds a {@link MobCoordinator} and provides delegates to the core functionality. + */ +public class TriggerDispatcher implements Listener { + private final MobCoordinator coordinator; + + public TriggerDispatcher(MobCoordinator coordinator) { + this.coordinator = coordinator; + } + + public void trigger(Entity entity, TriggerData trigger) { + coordinator.trigger(entity, trigger); + } +} diff --git a/core/src/main/java/de/eldoria/bloodnight/mobs/exceptions/MobIdAlreadyTakenException.java b/core/src/main/java/de/eldoria/bloodnight/mobs/exceptions/MobIdAlreadyTakenException.java new file mode 100644 index 0000000..0f896c5 --- /dev/null +++ b/core/src/main/java/de/eldoria/bloodnight/mobs/exceptions/MobIdAlreadyTakenException.java @@ -0,0 +1,25 @@ +package de.eldoria.bloodnight.mobs.exceptions; + +import de.eldoria.bloodnight.mob.CustomMob; + +/** + * Exception thrown when trying to register a CustomMob with an id that is already taken by another CustomMob. + */ +public class MobIdAlreadyTakenException extends RuntimeException { + private final CustomMob newMob; + private final CustomMob currentMob; + + public MobIdAlreadyTakenException(CustomMob newMob, CustomMob currentMob) { + super("Could not register %s. The id %s is already taken by %s".formatted(newMob.getClass().getName(), newMob.id(), currentMob.getClass().getName())); + this.newMob = newMob; + this.currentMob = currentMob; + } + + public CustomMob newMob() { + return newMob; + } + + public CustomMob currentMob() { + return currentMob; + } +} diff --git a/core/src/main/java/de/eldoria/bloodnight/util/MobTags.java b/core/src/main/java/de/eldoria/bloodnight/util/MobTags.java index e2a3833..9332e3f 100644 --- a/core/src/main/java/de/eldoria/bloodnight/util/MobTags.java +++ b/core/src/main/java/de/eldoria/bloodnight/util/MobTags.java @@ -1,7 +1,26 @@ package de.eldoria.bloodnight.util; +import de.eldoria.bloodnight.mob.CustomMob; import org.bukkit.NamespacedKey; +import org.bukkit.entity.Entity; +import org.bukkit.persistence.PersistentDataType; public class MobTags { + /** + * Marks that an {@link Entity} is a {@link CustomMob}. + * This tag is solely for existence checks and should use a {@link PersistentDataType#BYTE}. + * The value of the byte doesn't matter. + * If this tag is set on the mob with the BYTE type, it is always considered a custom mob. + */ public static final NamespacedKey CUSTOM_MOB = new NamespacedKey("bloodnight", "custom_mob"); + /** + * Marks that a mob is an extension of a custom mob. + * This tag should use a {@link PersistentDataType#INTEGER} containing the {@link Entity#getEntityId()} of the entity that the mob has, that is extended. + */ + public static final NamespacedKey EXTENDS = new NamespacedKey("bloodnight", "extends"); + /** + * Marks that a mob is extended and has another mob attached to it. + * This tag should use a {@link PersistentDataType#INTEGER} containing the {@link Entity#getEntityId()} of the entity that extends this mob. + */ + public static final NamespacedKey EXTENDED = new NamespacedKey("bloodnight", "extended"); } diff --git a/core/src/test/java/de/eldoria/bloodnight/editor/EditorPayloadTest.java b/core/src/test/java/de/eldoria/bloodnight/editor/EditorPayloadTest.java index 4d924b2..5e53e1a 100644 --- a/core/src/test/java/de/eldoria/bloodnight/editor/EditorPayloadTest.java +++ b/core/src/test/java/de/eldoria/bloodnight/editor/EditorPayloadTest.java @@ -32,6 +32,7 @@ import org.junit.jupiter.api.Test; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; diff --git a/nodes/src/main/java/de/eldoria/bloodnight/nodes/controlflow/impl/BranchNode.java b/nodes/src/main/java/de/eldoria/bloodnight/nodes/controlflow/impl/BranchNode.java index 25a6f01..dcd3e2c 100644 --- a/nodes/src/main/java/de/eldoria/bloodnight/nodes/controlflow/impl/BranchNode.java +++ b/nodes/src/main/java/de/eldoria/bloodnight/nodes/controlflow/impl/BranchNode.java @@ -20,7 +20,7 @@ @Input(name = Fields.VALUE, type = DataType.BOOLEAN) @Execution(Fields.TRUE) @Execution(Fields.FALSE) -@Meta(name = "Branch", description = "calling other node depending on the evaluated boolean value.") +@Meta(name = "Branch", description = "Calling other nodes depending on the evaluated boolean value.") public class BranchNode extends ControlFlowNode { public BranchNode() { diff --git a/nodes/src/main/java/de/eldoria/bloodnight/nodes/dispatching/TriggerData.java b/nodes/src/main/java/de/eldoria/bloodnight/nodes/dispatching/TriggerData.java index 86b165a..2e819d9 100644 --- a/nodes/src/main/java/de/eldoria/bloodnight/nodes/dispatching/TriggerData.java +++ b/nodes/src/main/java/de/eldoria/bloodnight/nodes/dispatching/TriggerData.java @@ -2,5 +2,24 @@ import de.eldoria.bloodnight.nodes.trigger.TriggerNode; +/** + * This class holds the trigger data for a {@link TriggerNode}. + * + * @param triggerNodeClass the class of the node to be triggered + * @param data the data required by the trigger node + * @param the type of the data required by the trigger node + */ public record TriggerData(Class> triggerNodeClass, V data) { + + /** + * Creates a new TriggerData instance for the specified TriggerNode class using the provided data. + * + * @param triggerNodeClass The class of the TriggerNode. + * @param data The data to be associated with the TriggerNode. + * @param The type of the data. + * @return A new TriggerData instance. + */ + public static TriggerData forNode(Class> triggerNodeClass, V data) { + return new TriggerData<>(triggerNodeClass, data); + } } diff --git a/nodes/src/main/java/de/eldoria/bloodnight/nodes/trigger/impl/events/OnKillNode.java b/nodes/src/main/java/de/eldoria/bloodnight/nodes/trigger/impl/events/OnEntityKillNode.java similarity index 76% rename from nodes/src/main/java/de/eldoria/bloodnight/nodes/trigger/impl/events/OnKillNode.java rename to nodes/src/main/java/de/eldoria/bloodnight/nodes/trigger/impl/events/OnEntityKillNode.java index b20da58..6ca08bb 100644 --- a/nodes/src/main/java/de/eldoria/bloodnight/nodes/trigger/impl/events/OnKillNode.java +++ b/nodes/src/main/java/de/eldoria/bloodnight/nodes/trigger/impl/events/OnEntityKillNode.java @@ -15,13 +15,13 @@ import java.util.Map; -@Output(name = OnKillNode.Outputs.KILLED_ENTITY, type = DataType.ENTITY) -@Output(name = OnKillNode.Outputs.CANCELABLE_EVENT, type = DataType.CANCELABLE_EVENT) +@Output(name = OnEntityKillNode.Outputs.KILLED_ENTITY, type = DataType.ENTITY) +@Output(name = OnEntityKillNode.Outputs.CANCELABLE_EVENT, type = DataType.CANCELABLE_EVENT) @Meta(name = "On entity kill", description = "A trigger, that's called when an entity got killed by this mob.", category = Categories.EVENT) -public class OnKillNode extends CancelableEventTriggerNode { +public class OnEntityKillNode extends CancelableEventTriggerNode { @JsonCreator - public OnKillNode(@JsonProperty("input") Map input, @JsonProperty("meta") EditorMeta meta) { + public OnEntityKillNode(@JsonProperty("input") Map input, @JsonProperty("meta") EditorMeta meta) { super(input, meta); } diff --git a/nodes/src/main/java/de/eldoria/bloodnight/nodes/trigger/impl/events/OnPlayerKillNode.java b/nodes/src/main/java/de/eldoria/bloodnight/nodes/trigger/impl/events/OnPlayerKillNode.java new file mode 100644 index 0000000..6c06832 --- /dev/null +++ b/nodes/src/main/java/de/eldoria/bloodnight/nodes/trigger/impl/events/OnPlayerKillNode.java @@ -0,0 +1,39 @@ +package de.eldoria.bloodnight.nodes.trigger.impl.events; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import de.eldoria.bloodnight.nodes.annotations.Meta; +import de.eldoria.bloodnight.nodes.annotations.Output; +import de.eldoria.bloodnight.nodes.base.io.Edge; +import de.eldoria.bloodnight.nodes.base.io.EditorMeta; +import de.eldoria.bloodnight.nodes.base.io.OutputContainer; +import de.eldoria.bloodnight.nodes.meta.Categories; +import de.eldoria.bloodnight.nodes.meta.DataType; +import de.eldoria.bloodnight.nodes.meta.Fields; +import de.eldoria.bloodnight.nodes.trigger.base.CancelableEventTriggerNode; +import org.bukkit.event.entity.EntityDamageByEntityEvent; + +import java.util.Map; + +@Output(name = OnPlayerKillNode.Outputs.KILLED_ENTITY, type = DataType.PLAYER) +@Output(name = OnPlayerKillNode.Outputs.CANCELABLE_EVENT, type = DataType.CANCELABLE_EVENT) +@Meta(name = "On entity kill", description = "A trigger, that's called when a player got killed by this mob.", category = Categories.EVENT) +public class OnPlayerKillNode extends CancelableEventTriggerNode { + + @JsonCreator + public OnPlayerKillNode(@JsonProperty("input") Map input, @JsonProperty("meta") EditorMeta meta) { + super(input, meta); + } + + @Override + protected OutputContainer output(OutputContainer output) { + return super.output() + .set(Outputs.KILLED_ENTITY, event.getEntity()); + } + + public static class Outputs { + public static final String KILLED_ENTITY = Fields.KILLED_ENTITY; + public static final String CANCELABLE_EVENT = Fields.CANCELABLE_EVENT; + } +} + diff --git a/nodes/src/main/java/de/eldoria/bloodnight/util/Checks.java b/nodes/src/main/java/de/eldoria/bloodnight/util/Checks.java index affcb6f..739fa23 100644 --- a/nodes/src/main/java/de/eldoria/bloodnight/util/Checks.java +++ b/nodes/src/main/java/de/eldoria/bloodnight/util/Checks.java @@ -3,6 +3,7 @@ import de.eldoria.bloodnight.nodes.meta.DataType; import org.jetbrains.annotations.Contract; +import java.util.Locale; import java.util.Objects; public class Checks { @@ -16,4 +17,10 @@ public static void isSubType(DataType type1, DataType type2) { throw new IllegalStateException("Data type is %s but %s was expected.".formatted(type1, type2)); } } + + @Contract("null, _ -> fail") + public static void lowerCase(String string, String message) { + if (string.toLowerCase(Locale.ROOT).equals(string)) return; + throw new IllegalStateException(message); + } } diff --git a/settings.gradle.kts b/settings.gradle.kts index ec42f3e..5a82454 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,6 +1,7 @@ rootProject.name = "bloodnight" include("nodes") include("core") +include("api") dependencyResolutionManagement { versionCatalogs {