diff --git a/src/main/java/com/minelittlepony/unicopia/client/render/ModelPartHooks.java b/src/main/java/com/minelittlepony/unicopia/client/render/ModelPartHooks.java new file mode 100644 index 00000000..b892836b --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/client/render/ModelPartHooks.java @@ -0,0 +1,82 @@ +package com.minelittlepony.unicopia.client.render; + +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.Stack; +import net.minecraft.client.model.ModelPart; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.util.math.MathHelper; + +public class ModelPartHooks { + private static final float PIXEL_SCALE = 0.0625F; // 1/16; + private static Stack> renderCalls = new Stack<>(); + + public static void startCollecting() { + renderCalls.push(new LinkedHashSet<>()); + } + + public static Set stopCollecting() { + return renderCalls.isEmpty() ? Set.of() : renderCalls.pop(); + } + + public static void onHeadRendered(ModelPart part, MatrixStack matrices) { + if (part.hidden || !part.visible || renderCalls.isEmpty()) { + return; + } + + var head = renderCalls.peek(); + if (head.size() > 5) { + renderCalls.pop(); + return; + } + + final var bestCandidate = new EnqueudHeadRender(); + + matrices.push(); + part.forEachCuboid(matrices, (entry, name, index, cube) -> { + float x = cube.maxX - cube.minX; + float y = cube.maxY - cube.minY; + float z = cube.maxZ - cube.minZ; + + float volume = div(Math.abs(x * y * z), Math.abs(x + y + z)) * 3F; + + if (volume > 0 && volume > bestCandidate.volume) { + bestCandidate.cube = cube; + bestCandidate.transformation = entry; + bestCandidate.volume = volume; + bestCandidate.maxSideLength = Math.max(Math.max(x, z), y); + } + }); + matrices.pop(); + + if (bestCandidate.transformation != null) { + head.add(bestCandidate); + } + } + + static float div(float a, float b) { + return b == 0 ? 0 : a / b; + } + + public static final class EnqueudHeadRender { + private ModelPart.Cuboid cube; + private MatrixStack.Entry transformation; + private float volume; + private float maxSideLength; + + public void transform(MatrixStack matrices, float cubeSize) { + matrices.peek().getNormalMatrix().set(transformation.getNormalMatrix()); + matrices.peek().getPositionMatrix().set(transformation.getPositionMatrix()); + + float x = MathHelper.lerp(0.5F, cube.minX, cube.maxX); + float y = MathHelper.lerp(0.5F, cube.minY, cube.maxY); + float z = MathHelper.lerp(0.5F, cube.minZ, cube.maxZ); + float scale = (maxSideLength / cubeSize) * PIXEL_SCALE; + + matrices.translate(x * PIXEL_SCALE, y * PIXEL_SCALE, z * PIXEL_SCALE); + matrices.scale(scale, scale, scale); + //matrices.peek().getPositionMatrix().scaleAround(scale, x * PIXEL_SCALE, y * PIXEL_SCALE, z * PIXEL_SCALE); + //matrices.translate(cube.minX * PIXEL_SCALE, cube.minY * PIXEL_SCALE, cube.minZ * PIXEL_SCALE); + } + } +} diff --git a/src/main/java/com/minelittlepony/unicopia/client/render/SmittenEyesRenderer.java b/src/main/java/com/minelittlepony/unicopia/client/render/SmittenEyesRenderer.java new file mode 100644 index 00000000..4b5c40d5 --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/client/render/SmittenEyesRenderer.java @@ -0,0 +1,68 @@ +package com.minelittlepony.unicopia.client.render; + +import com.minelittlepony.unicopia.Unicopia; +import com.minelittlepony.unicopia.client.minelittlepony.MineLPDelegate; +import com.minelittlepony.unicopia.entity.Creature; +import com.minelittlepony.unicopia.item.enchantment.WantItNeedItEnchantment; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.model.Dilation; +import net.minecraft.client.model.ModelData; +import net.minecraft.client.model.ModelPart; +import net.minecraft.client.model.ModelPartBuilder; +import net.minecraft.client.model.ModelTransform; +import net.minecraft.client.model.TexturedModelData; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.VertexConsumer; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.WorldRenderer; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.RotationAxis; + +public class SmittenEyesRenderer { + public static final SmittenEyesRenderer INSTANCE = new SmittenEyesRenderer(); + + private static final Identifier TEXTURE = Unicopia.id("textures/entity/smitten_eyes.png"); + + private final MinecraftClient client = MinecraftClient.getInstance(); + private final ModelPart model; + + SmittenEyesRenderer() { + ModelData data = new ModelData(); + data.getRoot().addChild("hearts", ModelPartBuilder.create() + .uv(0, 0) + .cuboid(-4, -4, -4, 8.0f, 8.0f, 8.0f, Dilation.NONE), ModelTransform.NONE); + model = TexturedModelData.of(data, 32, 32).createModel(); + } + + public void render(Creature pony, MatrixStack matrices, VertexConsumerProvider vertices, int light, int overlay) { + VertexConsumer buffer = vertices.getBuffer(RenderLayer.getEntityCutout(TEXTURE)); + + ModelPartHooks.stopCollecting().forEach(head -> { + matrices.push(); + head.transform(matrices, 0.95F); + if (MineLPDelegate.getInstance().getRace(pony.asEntity()).isEquine()) { + matrices.multiply(RotationAxis.POSITIVE_Z.rotationDegrees(-90), 0, 1.2F, 0); + } + float scale = 1F + (1.3F + MathHelper.sin(pony.asEntity().age / 3F) * 0.06F); + matrices.scale(scale, scale, scale); + matrices.translate(0, 0.05F, 0); + model.render(matrices, buffer, light, overlay, 1, 1, 1, 1); + + if (client.getEntityRenderDispatcher().shouldRenderHitboxes()) { + VertexConsumer lines = vertices.getBuffer(RenderLayer.getLines()); + WorldRenderer.drawBox(matrices, lines, new Box(-0.25, 0, -0.25, 0.25, 0.25, 0.25), 1, 1, 0, 1); + WorldRenderer.drawBox(matrices, lines, new Box(-0.25, -0.25, -0.25, 0.25, 0, 0.25), 1, 0, 1, 1); + } + + matrices.pop(); + }); + } + + public boolean isSmitten(Creature pony) { + return pony.isSmitten() || WantItNeedItEnchantment.getLevel(pony.asEntity()) > 0; + } +} diff --git a/src/main/java/com/minelittlepony/unicopia/client/render/WorldRenderDelegate.java b/src/main/java/com/minelittlepony/unicopia/client/render/WorldRenderDelegate.java index 1789a462..3288ec9c 100644 --- a/src/main/java/com/minelittlepony/unicopia/client/render/WorldRenderDelegate.java +++ b/src/main/java/com/minelittlepony/unicopia/client/render/WorldRenderDelegate.java @@ -20,6 +20,7 @@ import net.minecraft.block.BlockState; import net.minecraft.block.entity.BlockEntity; import net.minecraft.client.MinecraftClient; import net.minecraft.client.render.*; +import net.minecraft.client.render.VertexConsumerProvider.Immediate; import net.minecraft.client.render.block.entity.BlockEntityRenderer; import net.minecraft.client.render.entity.EntityRenderDispatcher; import net.minecraft.client.render.entity.LivingEntityRenderer; @@ -146,6 +147,8 @@ public class WorldRenderDelegate { matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(diveAngle)); matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(yaw)); + } else if (pony instanceof Creature creature && SmittenEyesRenderer.INSTANCE.isSmitten(creature)) { + ModelPartHooks.startCollecting(); } matrices.translate(-x, -y - owner.getHeight() / 2, -z); @@ -178,7 +181,7 @@ public class WorldRenderDelegate { PehkUtil.clearScale(ee); }); - afterEntityRender(pony, matrices); + afterEntityRender(pony, matrices, light); PehkUtil.clearScale(e); return true; } @@ -189,11 +192,16 @@ public class WorldRenderDelegate { return false; } - public void afterEntityRender(Equine pony, MatrixStack matrices) { + public void afterEntityRender(Equine pony, MatrixStack matrices, int light) { if (recurseFrosting) { return; } + if (pony instanceof Creature creature && SmittenEyesRenderer.INSTANCE.isSmitten(creature)) { + Immediate immediate = MinecraftClient.getInstance().getBufferBuilders().getEntityVertexConsumers(); + SmittenEyesRenderer.INSTANCE.render(creature, matrices, immediate, light, 0); + } + if (pony instanceof ItemImpl || pony instanceof Living) { matrices.pop(); diff --git a/src/main/java/com/minelittlepony/unicopia/entity/Creature.java b/src/main/java/com/minelittlepony/unicopia/entity/Creature.java index 733d8cec..f82e1552 100644 --- a/src/main/java/com/minelittlepony/unicopia/entity/Creature.java +++ b/src/main/java/com/minelittlepony/unicopia/entity/Creature.java @@ -41,6 +41,7 @@ public class Creature extends Living implements WeaklyOwned.Mutabl public static final TrackedData GRAVITY = DataTracker.registerData(LivingEntity.class, TrackedDataHandlerRegistry.FLOAT); private static final TrackedData EATING = DataTracker.registerData(LivingEntity.class, TrackedDataHandlerRegistry.INTEGER); private static final TrackedData DISCORDED = DataTracker.registerData(LivingEntity.class, TrackedDataHandlerRegistry.BOOLEAN); + private static final TrackedData SMITTEN = DataTracker.registerData(LivingEntity.class, TrackedDataHandlerRegistry.BOOLEAN); public static void boostrap() {} @@ -56,6 +57,7 @@ public class Creature extends Living implements WeaklyOwned.Mutabl private EatMuffinGoal eatMuffinGoal; private boolean discordedChanged = true; + private int smittenTicks; private final Predicate targetPredicate = TargetSelecter.notOwnerOrFriend(() -> getOriginatingCaster().getAffinity(), this).and(e -> { return Equine.of(e) @@ -70,6 +72,7 @@ public class Creature extends Living implements WeaklyOwned.Mutabl entity.getDataTracker().startTracking(MASTER, owner.toNBT()); entity.getDataTracker().startTracking(EATING, 0); entity.getDataTracker().startTracking(DISCORDED, false); + entity.getDataTracker().startTracking(SMITTEN, false); addTicker(physics); addTicker(this::updateConsumption); @@ -92,6 +95,15 @@ public class Creature extends Living implements WeaklyOwned.Mutabl return entity.getDataTracker().get(DISCORDED); } + public boolean isSmitten() { + return entity.getDataTracker().get(SMITTEN); + } + + public void setSmitten(boolean smitten) { + smittenTicks = smitten ? 20 : 0; + entity.getDataTracker().set(SMITTEN, smitten); + } + public void setDiscorded(boolean discorded) { entity.getDataTracker().set(DISCORDED, discorded); discordedChanged = true; @@ -131,7 +143,7 @@ public class Creature extends Living implements WeaklyOwned.Mutabl DynamicTargetGoal targetter = new DynamicTargetGoal((MobEntity)entity); targets.add(1, targetter); if (!Unicopia.getConfig().wantItNeedItEntityExcludelist.get().contains(EntityType.getId(entity.getType()).toString())) { - goals.add(1, new WantItTakeItGoal((MobEntity)entity, targetter)); + goals.add(1, new WantItTakeItGoal(this, targetter)); } if (entity.getType().getSpawnGroup() == SpawnGroup.MONSTER) { goals.add(3, new BreakHeartGoal((MobEntity)entity, targetter)); @@ -199,6 +211,12 @@ public class Creature extends Living implements WeaklyOwned.Mutabl initDiscordedAi(); } + if (!isClient() && smittenTicks > 0) { + if (--smittenTicks <= 0) { + setSmitten(false); + } + } + return super.beforeUpdate(); } diff --git a/src/main/java/com/minelittlepony/unicopia/entity/ItemImpl.java b/src/main/java/com/minelittlepony/unicopia/entity/ItemImpl.java index 92fe5549..cdc8ff22 100644 --- a/src/main/java/com/minelittlepony/unicopia/entity/ItemImpl.java +++ b/src/main/java/com/minelittlepony/unicopia/entity/ItemImpl.java @@ -4,6 +4,10 @@ import java.util.List; import com.minelittlepony.unicopia.*; import com.minelittlepony.unicopia.item.enchantment.UEnchantments; +import com.minelittlepony.unicopia.item.enchantment.WantItNeedItEnchantment; +import com.minelittlepony.unicopia.particle.FollowingParticleEffect; +import com.minelittlepony.unicopia.particle.ParticleUtils; +import com.minelittlepony.unicopia.particle.UParticles; import com.minelittlepony.unicopia.util.VecHelper; import net.minecraft.enchantment.EnchantmentHelper; @@ -56,6 +60,16 @@ public class ItemImpl implements Equine { setSpecies(Race.HUMAN); setSpecies(race); } + + if (WantItNeedItEnchantment.getLevel(entity) > 0) { + var random = entity.getWorld().random; + + if (random.nextInt(15) == 0) { + ParticleUtils.spawnParticles(new FollowingParticleEffect(UParticles.HEALTH_DRAIN, entity.getPos().add( + VecHelper.sphere(random).get().add(0, 1, 0) + ), 0.2F), entity, 1); + } + } } ItemStack stack = entity.getStack(); diff --git a/src/main/java/com/minelittlepony/unicopia/entity/ai/WantItTakeItGoal.java b/src/main/java/com/minelittlepony/unicopia/entity/ai/WantItTakeItGoal.java index d2fad6d3..900b7632 100644 --- a/src/main/java/com/minelittlepony/unicopia/entity/ai/WantItTakeItGoal.java +++ b/src/main/java/com/minelittlepony/unicopia/entity/ai/WantItTakeItGoal.java @@ -2,11 +2,14 @@ package com.minelittlepony.unicopia.entity.ai; import com.minelittlepony.unicopia.AwaitTickQueue; import com.minelittlepony.unicopia.EquinePredicates; +import com.minelittlepony.unicopia.entity.Creature; import com.minelittlepony.unicopia.item.enchantment.UEnchantments; import com.minelittlepony.unicopia.item.enchantment.WantItNeedItEnchantment; import com.minelittlepony.unicopia.particle.FollowingParticleEffect; import com.minelittlepony.unicopia.particle.ParticleUtils; import com.minelittlepony.unicopia.particle.UParticles; +import com.minelittlepony.unicopia.util.VecHelper; + import net.minecraft.enchantment.EnchantmentHelper; import net.minecraft.entity.Entity; import net.minecraft.entity.EquipmentSlot; @@ -26,8 +29,11 @@ public class WantItTakeItGoal extends BreakHeartGoal { protected int cooldown; - public WantItTakeItGoal(MobEntity mob, DynamicTargetGoal targetter) { - super(mob, targetter); + private final Creature creature; + + public WantItTakeItGoal(Creature creature, DynamicTargetGoal targetter) { + super((MobEntity)creature.asEntity(), targetter); + this.creature = creature; } @Override @@ -41,7 +47,7 @@ public class WantItTakeItGoal extends BreakHeartGoal { @Override protected void attackTarget(Entity target, double reach, double distance) { - ParticleUtils.spawnParticles(new FollowingParticleEffect(UParticles.HEALTH_DRAIN, mob, 0.2F), mob, 1); + ParticleUtils.spawnParticles(new FollowingParticleEffect(UParticles.HEALTH_DRAIN, mob.getPos().add(VecHelper.sphere(mob.getWorld().random).get()), 0.2F), mob, 1); double speed = 0.8D; @@ -59,6 +65,7 @@ public class WantItTakeItGoal extends BreakHeartGoal { mob.getNavigation().startMovingTo(target, speed); cooldown = Math.max(cooldown - 1, 0); + creature.setSmitten(true); if (distance <= reach) { if (target instanceof LivingEntity) { diff --git a/src/main/java/com/minelittlepony/unicopia/entity/duck/Hookable.java b/src/main/java/com/minelittlepony/unicopia/entity/duck/Hookable.java new file mode 100644 index 00000000..831725ab --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/entity/duck/Hookable.java @@ -0,0 +1,5 @@ +package com.minelittlepony.unicopia.entity.duck; + +public interface Hookable { + void enableHooks(); +} diff --git a/src/main/java/com/minelittlepony/unicopia/mixin/client/MixinEntityRenderDispatcher.java b/src/main/java/com/minelittlepony/unicopia/mixin/client/MixinEntityRenderDispatcher.java index adf12aae..16289191 100644 --- a/src/main/java/com/minelittlepony/unicopia/mixin/client/MixinEntityRenderDispatcher.java +++ b/src/main/java/com/minelittlepony/unicopia/mixin/client/MixinEntityRenderDispatcher.java @@ -27,6 +27,6 @@ abstract class MixinEntityRenderDispatcher { @Inject(method = RENDER, at = @At("RETURN")) private void afterRender(E entity, double x, double y, double z, float yaw, float tickDelta, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, CallbackInfo info) { - Equine.of(entity).ifPresent(eq -> WorldRenderDelegate.INSTANCE.afterEntityRender(eq, matrices)); + Equine.of(entity).ifPresent(eq -> WorldRenderDelegate.INSTANCE.afterEntityRender(eq, matrices, light)); } } diff --git a/src/main/java/com/minelittlepony/unicopia/mixin/client/MixinModelPart.java b/src/main/java/com/minelittlepony/unicopia/mixin/client/MixinModelPart.java new file mode 100644 index 00000000..4fb5cd9a --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/mixin/client/MixinModelPart.java @@ -0,0 +1,48 @@ +package com.minelittlepony.unicopia.mixin.client; + +import java.util.List; +import java.util.Map; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import com.minelittlepony.unicopia.client.render.ModelPartHooks; +import com.minelittlepony.unicopia.entity.duck.Hookable; + +import net.minecraft.client.model.ModelPart; +import net.minecraft.client.model.ModelPart.Cuboid; +import net.minecraft.client.render.VertexConsumer; +import net.minecraft.client.render.entity.model.EntityModelPartNames; +import net.minecraft.client.util.math.MatrixStack; + +@Mixin(ModelPart.class) +abstract class MixinModelPart implements Hookable { + @Unique + private boolean isHeadPart; + + @Shadow + private boolean visible; + + @Inject(method = "", at = @At("RETURN")) + private void onModelPart(List cuboids, Map children, CallbackInfo info) { + if (((Object)children.getOrDefault(EntityModelPartNames.HEAD, null)) instanceof Hookable hook) { + hook.enableHooks(); + } + } + + @Override + public void enableHooks() { + isHeadPart = true; + } + + @Inject(method = "render(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumer;IIFFFF)V", at = @At("RETURN")) + public void render(MatrixStack matrices, VertexConsumer vertices, int light, int overlay, float red, float green, float blue, float alpha, CallbackInfo info) { + if (visible && isHeadPart) { + ModelPartHooks.onHeadRendered((ModelPart)(Object)this, matrices); + } + } +} diff --git a/src/main/resources/assets/unicopia/textures/entity/smitten_eyes.png b/src/main/resources/assets/unicopia/textures/entity/smitten_eyes.png new file mode 100644 index 00000000..65bb7f49 Binary files /dev/null and b/src/main/resources/assets/unicopia/textures/entity/smitten_eyes.png differ diff --git a/src/main/resources/unicopia.mixin.json b/src/main/resources/unicopia.mixin.json index 805fe03b..edb4eff6 100644 --- a/src/main/resources/unicopia.mixin.json +++ b/src/main/resources/unicopia.mixin.json @@ -65,6 +65,7 @@ "client.MixinItemModels", "client.MixinKeyboardInput", "client.MixinLivingEntityRenderer", + "client.MixinModelPart", "client.MixinMouse", "client.MixinPlayerEntityRenderer", "client.MixinTooltipComponent",