Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions src/main/java/net/goldenstack/loot/LootPredicate.java
Original file line number Diff line number Diff line change
Expand Up @@ -209,9 +209,7 @@ record EntityProperties(@Nullable EntityPredicate predicate, @NotNull RelevantEn
@Override
public boolean test(@NotNull LootContext context) {
Entity entity = context.get(this.entity.key());
Point origin = context.get(LootContext.ORIGIN);

return predicate == null || predicate.test(context.require(LootContext.WORLD), origin, entity);
return predicate == null || predicate.test(entity, context);
}

@Override
Expand Down
233 changes: 227 additions & 6 deletions src/main/java/net/goldenstack/loot/util/predicate/EntityPredicate.java
Original file line number Diff line number Diff line change
@@ -1,20 +1,241 @@
package net.goldenstack.loot.util.predicate;

import net.goldenstack.loot.LootContext;
import net.minestom.server.codec.Codec;
import net.minestom.server.codec.StructCodec;
import net.minestom.server.coordinate.Point;
import net.minestom.server.entity.Entity;
import net.minestom.server.instance.Instance;
import net.minestom.server.utils.Unit;
import net.minestom.server.entity.EntityCreature;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.EquipmentSlot;
import net.minestom.server.entity.LivingEntity;
import net.minestom.server.instance.block.predicate.DataComponentPredicates;
import net.minestom.server.potion.PotionEffect;
import net.minestom.server.registry.Registries;
import net.minestom.server.registry.RegistryKey;
import net.minestom.server.registry.RegistryTag;
import net.minestom.server.utils.Range;

import java.util.Map;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

// TODO: Incomplete
// TODO: nbt, slots, stepping_on, fall_distance in movement

@SuppressWarnings("UnstableApiUsage")
public interface EntityPredicate {
public record EntityPredicate(
@Nullable RegistryTag<EntityType> type,
@NotNull DataComponentPredicates predicate,
@Nullable DistancePredicate distance,
@NotNull Map<RegistryKey<PotionEffect>, EffectCondition> effects,
@NotNull Map<EquipmentSlot, ItemPredicate> equipment,
@Nullable FlagsPredicate flags,
@Nullable LocationPredicate location,
@Nullable EntityPredicate passanger,
@Nullable LocationPredicate movementAffectedBy,
@Nullable String team,
@Nullable EntityPredicate targetedEntity,
@Nullable EntityPredicate vehicle,
@Nullable MovementPredicate movement,
@Nullable Integer periodicTick
) {
// Distance
public static record DistancePredicate(
@Nullable Range.Double absolute,
@Nullable Range.Double horizontal,
@Nullable Range.Double x,
@Nullable Range.Double y,
@Nullable Range.Double z
) {
public static final @NotNull StructCodec<DistancePredicate> CODEC = StructCodec.struct(
"absolute", DataComponentPredicates.DOUBLE_RANGE_CODEC.optional(), DistancePredicate::absolute,
"horizontal", DataComponentPredicates.DOUBLE_RANGE_CODEC.optional(), DistancePredicate::horizontal,
"x", DataComponentPredicates.DOUBLE_RANGE_CODEC.optional(), DistancePredicate::x,
"y", DataComponentPredicates.DOUBLE_RANGE_CODEC.optional(), DistancePredicate::y,
"z", DataComponentPredicates.DOUBLE_RANGE_CODEC.optional(), DistancePredicate::z,
DistancePredicate::new
);

public boolean test(Point delta) {
if (absolute != null && !absolute.inRange(delta.distance(0.0, 0.0, 0.0))) return false;
if (horizontal != null && !horizontal.inRange(delta.mul(1.0, 0.0, 1.0).distance(0.0, 0.0, 0.0))) return false;
if (x != null && !x.inRange(Math.abs(delta.x()))) return false;
if (y != null && !y.inRange(Math.abs(delta.y()))) return false;
if (z != null && !z.inRange(Math.abs(delta.z()))) return false;
return true;
}
}

// Effects
public record EffectCondition(
@Nullable Range.Int amplifier,
@Nullable Range.Int duration,
@Nullable Boolean ambient,
@Nullable Boolean visible
) {
public static final @NotNull StructCodec<EffectCondition> CODEC = StructCodec.struct(
"amplifier", DataComponentPredicates.INT_RANGE_CODEC.optional(), EffectCondition::amplifier,
"duration", DataComponentPredicates.INT_RANGE_CODEC.optional(), EffectCondition::duration,
"ambient", StructCodec.BOOLEAN.optional(), EffectCondition::ambient,
"visible", StructCodec.BOOLEAN.optional(), EffectCondition::visible,
EffectCondition::new
);

public boolean test(@NotNull net.minestom.server.potion.TimedPotion effect, @NotNull Entity entity) {
final var remaining = effect.potion().duration() - (int)(entity.getAliveTicks() - effect.startingTicks());
if (remaining < 0) return false;
if (amplifier != null && !amplifier.inRange(effect.potion().amplifier())) return false;
if (duration != null && !duration.inRange(remaining)) return false;
if (ambient != null && ambient != effect.potion().isAmbient()) return false;
if (visible != null && visible != effect.potion().hasParticles()) return false;
return true;
}
}

// Flags
public record FlagsPredicate(
@Nullable Boolean isBaby,
@Nullable Boolean isOnFire,
@Nullable Boolean isSneaking,
@Nullable Boolean isSprinting,
@Nullable Boolean isSwimming,
@Nullable Boolean isOnGround,
@Nullable Boolean isFlying,
@Nullable Boolean isFallFlying
) {
public static final @NotNull StructCodec<FlagsPredicate> CODEC = StructCodec.struct(
"is_baby", StructCodec.BOOLEAN.optional(), FlagsPredicate::isBaby,
"is_on_fire", StructCodec.BOOLEAN.optional(), FlagsPredicate::isOnFire,
"is_sneaking", StructCodec.BOOLEAN.optional(), FlagsPredicate::isSneaking,
"is_sprinting", StructCodec.BOOLEAN.optional(), FlagsPredicate::isSprinting,
"is_swimming", StructCodec.BOOLEAN.optional(), FlagsPredicate::isSwimming,
"is_on_ground", StructCodec.BOOLEAN.optional(), FlagsPredicate::isOnGround,
"is_flying", StructCodec.BOOLEAN.optional(), FlagsPredicate::isFlying,
"is_fall_flying", StructCodec.BOOLEAN.optional(), FlagsPredicate::isFallFlying,
FlagsPredicate::new
);

public boolean test(@NotNull Entity entity) {
if (isBaby != null) {
if (!(entity.getEntityMeta() instanceof net.minestom.server.entity.metadata.AgeableMobMeta meta)) return false;
if (meta.isBaby() != isBaby) return false;
}
if (isOnFire != null && entity.isOnFire() != isOnFire) return false;
if (isSneaking != null && entity.isSneaking() != isSneaking) return false;
if (isSprinting != null && entity.isSprinting() != isSprinting) return false;
if (isSwimming != null && entity.getEntityMeta().isSwimming() != isSwimming) return false;
if (isOnGround != null && entity.isOnGround() != isOnGround) return false;
if (isFlying != null) {
var flying = entity.getEntityMeta().isFlyingWithElytra();
if (entity instanceof net.minestom.server.entity.Player player)
flying = flying || player.isFlying();
if (flying != isFlying) return false;
}
if (isFallFlying != null && entity.getEntityMeta().isFlyingWithElytra() != isFallFlying) return false;
return true;
}
}

// Movement
public record MovementPredicate(
@Nullable Range.Double x,
@Nullable Range.Double y,
@Nullable Range.Double z,
@Nullable Range.Double speed,
@Nullable Range.Double horizontalSpeed,
@Nullable Range.Double verticalSpeed,
@Nullable Range.Double fallDistance
) {
public static final @NotNull StructCodec<MovementPredicate> CODEC = StructCodec.struct(
"x", DataComponentPredicates.DOUBLE_RANGE_CODEC.optional(), MovementPredicate::x,
"y", DataComponentPredicates.DOUBLE_RANGE_CODEC.optional(), MovementPredicate::y,
"z", DataComponentPredicates.DOUBLE_RANGE_CODEC.optional(), MovementPredicate::z,
"speed", DataComponentPredicates.DOUBLE_RANGE_CODEC.optional(), MovementPredicate::speed,
"horizontal_speed", DataComponentPredicates.DOUBLE_RANGE_CODEC.optional(), MovementPredicate::horizontalSpeed,
"vertical_speed", DataComponentPredicates.DOUBLE_RANGE_CODEC.optional(), MovementPredicate::verticalSpeed,
"fall_distance", DataComponentPredicates.DOUBLE_RANGE_CODEC.optional(), MovementPredicate::fallDistance,
MovementPredicate::new
);

public boolean test(@NotNull Entity entity) {
final var velocity = entity.getVelocity();
if (x != null && !x.inRange(velocity.x())) return false;
if (y != null && !y.inRange(velocity.y())) return false;
if (z != null && !z.inRange(velocity.z())) return false;
if (speed != null && !speed.inRange(velocity.distance(0.0, 0.0, 0.0))) return false;
if (horizontalSpeed != null && !horizontalSpeed.inRange(velocity.mul(1.0, 0.0, 1.0).distance(0.0, 0.0, 0.0))) return false;
if (verticalSpeed != null && !verticalSpeed.inRange(velocity.y())) return false;
return true;
}
}

// Implementation
public static final @NotNull Codec<EntityPredicate> CODEC = Codec.Recursive(codec -> StructCodec.struct(
"type", RegistryTag.codec(Registries::entityType).optional(), EntityPredicate::type,
StructCodec.INLINE, DataComponentPredicates.CODEC, EntityPredicate::predicate,
"distance", DistancePredicate.CODEC.optional(), EntityPredicate::distance,
"effects", RegistryKey.codec(Registries::potionEffect).mapValue(EffectCondition.CODEC).optional(Map.of()), EntityPredicate::effects,
"equipment", EquipmentSlot.CODEC.mapValue(ItemPredicate.CODEC).optional(Map.of()), EntityPredicate::equipment,
"flags", FlagsPredicate.CODEC.optional(), EntityPredicate::flags,
"location", LocationPredicate.CODEC.optional(), EntityPredicate::location,
"passanger", codec.optional(), EntityPredicate::passanger,
"movement_affected_by", LocationPredicate.CODEC.optional(), EntityPredicate::movementAffectedBy,
"team", Codec.STRING.optional(), EntityPredicate::team,
"targeted_entity", codec.optional(), EntityPredicate::targetedEntity,
"vehicle", codec.optional(), EntityPredicate::vehicle,
"movement", MovementPredicate.CODEC.optional(), EntityPredicate::movement,
"periodic_tick", Codec.INT.optional(), EntityPredicate::periodicTick,
EntityPredicate::new
));

public boolean test(@Nullable Entity entity, @NotNull LootContext context) {
if (entity == null) return false; // Not sure about that
final var origin = context.get(LootContext.ORIGIN);
final var instance = context.require(LootContext.WORLD);

if (type != null && !type.contains(entity.getEntityType())) return false;
if (!predicate.test(entity)) return false;
if (distance != null && (origin == null || entity == null || !distance.test(entity.getPosition().sub(origin)))) return false;

for (final var entry : effects.entrySet()) {
final var effect = entity.getEffect(PotionEffect.staticRegistry().get(entry.getKey()));
if (!entry.getValue().test(effect, entity)) return false;
}

for (final var entry : equipment.entrySet()) {
if (!(entity instanceof LivingEntity living)) return false;
if (!entry.getValue().test(living.getEquipment(entry.getKey()), context)) return false;
}

if (flags != null && !flags.test(entity)) return false;
if (location != null && !location.test(instance, origin)) return false;
if (passanger != null) {
var passed = false;
for (final var candidate : entity.getPassengers()) {
if (passanger.test(candidate, context)) {
passed = true;
break;
}
}

if (!passed) return false;
}

@NotNull Codec<EntityPredicate> CODEC = Codec.UNIT.transform(a -> (instance, pos, entity) -> false, a -> Unit.INSTANCE);
if (movementAffectedBy != null && !movementAffectedBy.test(instance, origin.sub(0.0, 0.5, 0.0))) return false;
if (team != null) {
if (!(entity instanceof LivingEntity living)) return false;
if (!living.getTeam().getTeamName().equals(team)) return false;
}

boolean test(@NotNull Instance instance, @Nullable Point pos, @Nullable Entity entity);
if (targetedEntity != null) {
if (!(entity instanceof EntityCreature creature)) return false;
if (!targetedEntity.test(creature.getTarget(), context)) return false;
}

if (vehicle != null && !vehicle.test(entity.getVehicle(), context)) return false;
if (movement != null && !movement.test(entity)) return false;
if (periodicTick != null && entity.getAliveTicks() % periodicTick != 0) return false;
return true;
}
}
Loading