diff --git a/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/crafting/AltarRecipeMatch.java b/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/crafting/AltarRecipeMatch.java new file mode 100644 index 00000000..820f7667 --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/crafting/AltarRecipeMatch.java @@ -0,0 +1,42 @@ +package com.minelittlepony.unicopia.ability.magic.spell.crafting; + +import java.util.List; + +import org.jetbrains.annotations.Nullable; + +import com.minelittlepony.unicopia.item.UItems; + +import net.minecraft.entity.Entity; +import net.minecraft.entity.ItemEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; + +public record AltarRecipeMatch( + ItemEntity target, + List ingredients, + ItemStack result + ) { + + @Nullable + public static AltarRecipeMatch of(List inputs) { + ItemEntity clock = inputs.stream().filter(item -> item.getStack().isOf(Items.CLOCK)).findFirst().orElse(null); + + if (clock != null) { + return new AltarRecipeMatch(clock, List.of(), UItems.SPECTRAL_CLOCK.getDefaultStack()); + } + + return null; + } + + public boolean isRemoved() { + return target.isRemoved() || ingredients.stream().anyMatch(ItemEntity::isRemoved); + } + + public void craft() { + ItemStack clockStack = result.copyWithCount(target.getStack().getCount()); + clockStack.setNbt(target.getStack().getNbt()); + target.setStack(clockStack); + target.setInvulnerable(true); + ingredients.forEach(Entity::discard); + } +} diff --git a/src/main/java/com/minelittlepony/unicopia/client/render/entity/SpellbookEntityRenderer.java b/src/main/java/com/minelittlepony/unicopia/client/render/entity/SpellbookEntityRenderer.java index f04a5494..ceac4e95 100644 --- a/src/main/java/com/minelittlepony/unicopia/client/render/entity/SpellbookEntityRenderer.java +++ b/src/main/java/com/minelittlepony/unicopia/client/render/entity/SpellbookEntityRenderer.java @@ -1,19 +1,31 @@ package com.minelittlepony.unicopia.client.render.entity; +import org.joml.Matrix3f; +import org.joml.Matrix4f; + import com.minelittlepony.unicopia.Unicopia; import com.minelittlepony.unicopia.entity.mob.SpellbookEntity; +import com.minelittlepony.unicopia.server.world.Altar; +import net.minecraft.client.render.OverlayTexture; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.VertexConsumer; +import net.minecraft.client.render.VertexConsumerProvider; import net.minecraft.client.render.entity.EntityRendererFactory; import net.minecraft.client.render.entity.LivingEntityRenderer; +import net.minecraft.client.render.entity.feature.FeatureRenderer; +import net.minecraft.client.render.entity.feature.FeatureRendererContext; import net.minecraft.client.util.math.MatrixStack; import net.minecraft.util.Identifier; import net.minecraft.util.math.*; public class SpellbookEntityRenderer extends LivingEntityRenderer { private static final Identifier TEXTURE = Unicopia.id("textures/entity/spellbook/normal.png"); + private static final Identifier ALTAR_BEAM_TEXTURE = new Identifier("textures/entity/end_crystal/end_crystal_beam.png"); public SpellbookEntityRenderer(EntityRendererFactory.Context context) { super(context, new SpellbookModel(SpellbookModel.getTexturedModelData().createModel()), 0); + addFeature(new AltarBeamFeature(this)); } @Override @@ -51,4 +63,79 @@ public class SpellbookEntityRenderer extends LivingEntityRenderer { + public AltarBeamFeature(FeatureRendererContext context) { + super(context); + } + + @Override + public void render(MatrixStack matrices, VertexConsumerProvider vertices, int light, SpellbookEntity entity, float limbPos, float limbSpeed, float tickDelta, float animationProgress, float yaw, float pitch) { + if (!entity.hasBeams()) { + return; + } + + matrices.peek(); + matrices.pop(); + matrices.push(); + + + Altar altar = entity.getAltar().get(); + Vec3d center = altar.origin().toCenterPos(); + + float x = (float)MathHelper.lerp(tickDelta, entity.prevX, entity.getX()); + float y = (float)MathHelper.lerp(tickDelta, entity.prevY, entity.getY()); + float z = (float)MathHelper.lerp(tickDelta, entity.prevZ, entity.getZ()); + Vec3d bookPos = new Vec3d(x, y, z); + Vec3d shift = bookPos.subtract(center); + + matrices.push(); + matrices.multiply(RotationAxis.POSITIVE_Z.rotationDegrees(180)); + matrices.translate(shift.x, shift.y - 1, shift.z); + + for (BlockPos pillar : altar.pillars()) { + renderBeam(center.subtract(pillar.toCenterPos()), -tickDelta, -entity.age, matrices, vertices, light, 1, 0, 1); + } + + matrices.pop(); + } + + public static float getYOffset(float animationProgress) { + animationProgress = MathHelper.sin(animationProgress * 0.2F) * 0.5F + 0.5F; + return ((animationProgress * animationProgress + animationProgress) * 0.4F) - 1.4F; + } + } + + public static void renderBeam(Vec3d offset, float tickDelta, int age, MatrixStack matrices, VertexConsumerProvider buffers, int light, float r, float g, float b) { + final float horizontalDistance = (float)offset.horizontalLength(); + final float distance = (float)offset.length(); + matrices.push(); + matrices.multiply(RotationAxis.POSITIVE_Y.rotation((float)(-Math.atan2(offset.z, offset.x)) - 1.5707964f)); + matrices.multiply(RotationAxis.POSITIVE_X.rotation((float)(-Math.atan2(horizontalDistance, offset.y)) - 1.5707964f)); + VertexConsumer buffer = buffers.getBuffer(RenderLayer.getEntityTranslucent(ALTAR_BEAM_TEXTURE)); + final float minV = -(age + tickDelta) * 0.01f; + final float maxV = minV + (distance / 32F); + final int sides = 8; + final float diameter = 0.35F; + float segmentX = 0; + float segmentY = diameter; + float minU = 0; + MatrixStack.Entry entry = matrices.peek(); + Matrix4f positionMat = entry.getPositionMatrix(); + Matrix3f normalMat = entry.getNormalMatrix(); + + for (int i = 1; i <= sides; i++) { + float o = MathHelper.sin(i * MathHelper.TAU / sides) * diameter; + float p = MathHelper.cos(i * MathHelper.TAU / sides) * diameter; + float maxU = i / (float)sides; + buffer.vertex(positionMat, segmentX * 0.2F, segmentY * 0.2F, 0).color(0, 0, 0, 255).texture(minU, minV).overlay(OverlayTexture.DEFAULT_UV).light(light).normal(normalMat, 0, -1, 0).next(); + buffer.vertex(positionMat, segmentX, segmentY, distance).color(r, g, b, 1).texture(minU, maxV).overlay(OverlayTexture.DEFAULT_UV).light(light).normal(normalMat, 0, -1, 0).next(); + buffer.vertex(positionMat, o, p, distance).color(r, g, b, 1).texture(maxU, maxV).overlay(OverlayTexture.DEFAULT_UV).light(light).normal(normalMat, 0, -1, 0).next(); + buffer.vertex(positionMat, o * 0.2F, p * 0.2F, 0).color(0, 0, 0, 255).texture(maxU, minV).overlay(OverlayTexture.DEFAULT_UV).light(light).normal(normalMat, 0, -1, 0).next(); + segmentX = o; + segmentY = p; + minU = maxU; + } + matrices.pop(); + } } \ No newline at end of file diff --git a/src/main/java/com/minelittlepony/unicopia/entity/mob/SpellbookEntity.java b/src/main/java/com/minelittlepony/unicopia/entity/mob/SpellbookEntity.java index eb97f159..a09abfc8 100644 --- a/src/main/java/com/minelittlepony/unicopia/entity/mob/SpellbookEntity.java +++ b/src/main/java/com/minelittlepony/unicopia/entity/mob/SpellbookEntity.java @@ -2,9 +2,12 @@ package com.minelittlepony.unicopia.entity.mob; import java.util.Optional; +import org.jetbrains.annotations.Nullable; + import com.minelittlepony.unicopia.EquinePredicates; import com.minelittlepony.unicopia.USounds; import com.minelittlepony.unicopia.UTags; +import com.minelittlepony.unicopia.ability.magic.spell.crafting.AltarRecipeMatch; import com.minelittlepony.unicopia.container.SpellbookScreenHandler; import com.minelittlepony.unicopia.container.SpellbookState; import com.minelittlepony.unicopia.entity.MagicImmune; @@ -19,6 +22,7 @@ import net.fabricmc.fabric.api.util.TriState; import net.minecraft.block.Blocks; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityType; +import net.minecraft.entity.ItemEntity; import net.minecraft.entity.damage.DamageSource; import net.minecraft.entity.data.DataTracker; import net.minecraft.entity.data.TrackedData; @@ -30,6 +34,7 @@ import net.minecraft.item.ItemStack; import net.minecraft.nbt.NbtCompound; import net.minecraft.network.PacketByteBuf; import net.minecraft.particle.ParticleTypes; +import net.minecraft.predicate.entity.EntityPredicates; import net.minecraft.screen.*; import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.server.world.ServerWorld; @@ -38,13 +43,18 @@ import net.minecraft.sound.SoundCategory; import net.minecraft.text.Text; import net.minecraft.util.ActionResult; import net.minecraft.util.Hand; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Box; import net.minecraft.util.math.Vec3d; import net.minecraft.world.GameRules; import net.minecraft.world.World; +import net.minecraft.world.World.ExplosionSourceType; public class SpellbookEntity extends MobEntity implements MagicImmune { private static final TrackedData LOCKED = DataTracker.registerData(SpellbookEntity.class, TrackedDataHandlerRegistry.BYTE); private static final TrackedData ALTERED = DataTracker.registerData(SpellbookEntity.class, TrackedDataHandlerRegistry.BOOLEAN); + private static final byte ALTAR_BEAMS_START = 61; + private static final byte ALTAR_BEAMS_END = 62; private static final int TICKS_TO_SLEEP = 600; @@ -55,6 +65,12 @@ public class SpellbookEntity extends MobEntity implements MagicImmune { private Optional altar = Optional.empty(); + private boolean hasBeams; + private int beamsActive; + + @Nullable + private AltarRecipeMatch activeRecipe; + public SpellbookEntity(EntityType type, World world) { super(type, world); setPersistent(); @@ -104,6 +120,14 @@ public class SpellbookEntity extends MobEntity implements MagicImmune { this.altar = Optional.of(altar); } + public Optional getAltar() { + return altar; + } + + public boolean hasBeams() { + return hasBeams && altar.isPresent(); + } + public boolean isAltered() { return dataTracker.get(ALTERED); } @@ -205,44 +229,83 @@ public class SpellbookEntity extends MobEntity implements MagicImmune { return false; } - altar.pillars().forEach(pillar -> { - Vec3d center = pillar.toCenterPos().add( - random.nextTriangular(0.5, 0.2), - random.nextTriangular(0.5, 0.2), - random.nextTriangular(0.5, 0.2) - ); + tickAltarCrafting(altar); - ((ServerWorld)getWorld()).spawnParticles( - ParticleTypes.SOUL_FIRE_FLAME, - center.x - 0.5, center.y + 0.5, center.z - 0.5, - 0, - 0.5, 0.5, 0.5, 0); - - if (random.nextInt(12) != 0) { - return; - } - - Vec3d vel = center.subtract(this.altar.get().origin().toCenterPos()).normalize(); - - ((ServerWorld)getWorld()).spawnParticles( - ParticleTypes.SOUL_FIRE_FLAME, - center.x - 0.5, center.y + 0.5, center.z - 0.5, - 0, - vel.x, vel.y, vel.z, -0.2); - - if (random.nextInt(2000) == 0) { - if (getWorld().getBlockState(pillar).isOf(Blocks.CRYING_OBSIDIAN)) { - pillar = pillar.down(); - } - getWorld().setBlockState(pillar, Blocks.CRYING_OBSIDIAN.getDefaultState()); - } - }); + Vec3d origin = altar.origin().toCenterPos(); + altar.pillars().forEach(pillar -> tickAltarPillar(origin, pillar)); return true; }); } } + public void setBeamTicks(int ticks) { + getWorld().sendEntityStatus(this, ticks > 0 ? ALTAR_BEAMS_START : ALTAR_BEAMS_END); + beamsActive = ticks; + } + + private void tickAltarCrafting(Altar altar) { + if (activeRecipe == null || activeRecipe.isRemoved()) { + activeRecipe = AltarRecipeMatch.of(getWorld().getEntitiesByClass(ItemEntity.class, Box.of(altar.origin().toCenterPos(), 2, 2, 2), EntityPredicates.VALID_ENTITY)); + + if (activeRecipe != null) { + setBeamTicks(5); + } + } + + if (beamsActive <= 0) { + return; + } + + if (--beamsActive > 0) { + playSound(USounds.Vanilla.ENTITY_GUARDIAN_ATTACK, 1.5F, 0.5F); + return; + } + + //setBeamTicks(0); + + if (activeRecipe == null) { + return; + } + + activeRecipe.craft(); + activeRecipe = null; + getWorld().createExplosion(this, altar.origin().getX(), altar.origin().getY(), altar.origin().getZ(), 0, ExplosionSourceType.NONE); + } + + private void tickAltarPillar(Vec3d origin, BlockPos pillar) { + Vec3d center = pillar.toCenterPos().add( + random.nextTriangular(0.5, 0.2), + random.nextTriangular(0.5, 0.2), + random.nextTriangular(0.5, 0.2) + ); + + ((ServerWorld)getWorld()).spawnParticles( + ParticleTypes.SOUL_FIRE_FLAME, + center.x - 0.5, center.y + 0.5, center.z - 0.5, + 0, + 0.5, 0.5, 0.5, 0); + + if (random.nextInt(12) != 0) { + return; + } + + Vec3d vel = center.subtract(origin).normalize(); + + ((ServerWorld)getWorld()).spawnParticles( + ParticleTypes.SOUL_FIRE_FLAME, + center.x - 0.5, center.y + 0.5, center.z - 0.5, + 0, + vel.x, vel.y, vel.z, -0.2); + + if (random.nextInt(2000) == 0) { + if (getWorld().getBlockState(pillar).isOf(Blocks.CRYING_OBSIDIAN)) { + pillar = pillar.down(); + } + getWorld().setBlockState(pillar, Blocks.CRYING_OBSIDIAN.getDefaultState()); + } + } + private boolean shouldBeSleeping() { return MeteorlogicalUtil.getSkyAngle(getWorld()) > 1 && activeTicks <= 0; } @@ -337,4 +400,19 @@ public class SpellbookEntity extends MobEntity implements MagicImmune { }); Altar.SERIALIZER.writeOptional("altar", compound, altar); } + + @Override + public void handleStatus(byte status) { + switch (status) { + case ALTAR_BEAMS_START: + altar = Altar.locateAltar(getWorld(), getBlockPos()); + hasBeams = altar.isPresent(); + break; + case ALTAR_BEAMS_END: + altar = Optional.empty(); + hasBeams = false; + default: + super.handleStatus(status); + } + } }