Implement sombra boss battle

This commit is contained in:
Sollace 2023-08-25 22:06:08 +01:00
parent 65614e927c
commit 97c3d1a250
No known key found for this signature in database
GPG key ID: E52FACE7B5C773DB
36 changed files with 1240 additions and 97 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

BIN
assets/models/clouds.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

File diff suppressed because one or more lines are too long

View file

@ -53,6 +53,10 @@ public interface USounds {
SoundEvent ENTITY_HOT_AIR_BALLOON_EQUIP_CANOPY = ITEM_ARMOR_EQUIP_LEATHER;
SoundEvent ENTITY_HOT_AIR_BALLOON_EQUIP_BURNER = ENTITY_IRON_GOLEM_DAMAGE;
SoundEvent ENTITY_SOMBRA_AMBIENT = register("entity.sombra.ambient");
SoundEvent ENTITY_SOMBRA_LAUGH = register("entity.sombra.laugh");
SoundEvent ENTITY_SOMBRA_SNICKER = register("entity.sombra.snicker");
SoundEvent ITEM_AMULET_CHARGING = register("item.amulet.charging");
SoundEvent ITEM_AMULET_RECHARGE = register("item.amulet.recharge");

View file

@ -79,6 +79,8 @@ public interface URenderers {
EntityRendererRegistry.register(UEntities.TWITTERMITE, FairyEntityRenderer::new);
EntityRendererRegistry.register(UEntities.SPELLBOOK, SpellbookEntityRenderer::new);
EntityRendererRegistry.register(UEntities.SOMBRA, SombraEntityRenderer::new);
EntityRendererRegistry.register(UEntities.CRYSTAL_SHARDS, CrystalShardsEntityRenderer::new);
EntityRendererRegistry.register(UEntities.STORM_CLOUD, StormCloudEntityRenderer::new);
EntityRendererRegistry.register(UEntities.AIR_BALLOON, AirBalloonEntityRenderer::new);
BlockEntityRendererFactories.register(UBlockEntities.WEATHER_VANE, WeatherVaneBlockEntityRenderer::new);

View file

@ -2,6 +2,8 @@ package com.minelittlepony.unicopia.client;
import java.util.Optional;
import org.jetbrains.annotations.Nullable;
import com.minelittlepony.common.client.gui.element.Button;
import com.minelittlepony.common.event.ScreenInitCallback;
import com.minelittlepony.common.event.ScreenInitCallback.ButtonList;
@ -17,6 +19,7 @@ import com.minelittlepony.unicopia.container.*;
import com.minelittlepony.unicopia.entity.player.PlayerCamera;
import com.minelittlepony.unicopia.entity.player.Pony;
import com.minelittlepony.unicopia.network.handler.ClientNetworkHandlerImpl;
import com.minelittlepony.unicopia.server.world.WeatherConditions;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
@ -26,9 +29,11 @@ import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.OpenToLanScreen;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.screen.ingame.HandledScreens;
import net.minecraft.client.world.ClientWorld;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.resource.ResourceType;
import net.minecraft.text.Text;
import net.minecraft.util.math.BlockPos;
public class UnicopiaClient implements ClientModInitializer {
@ -59,6 +64,9 @@ public class UnicopiaClient implements ClientModInitializer {
return 0.6F;
}
@Nullable
private Float originalRainGradient;
@Override
public void onInitializeClient() {
InteractionManager.INSTANCE = new ClientInteractionManager();
@ -70,6 +78,7 @@ public class UnicopiaClient implements ClientModInitializer {
HandledScreens.register(UScreenHandlers.SPELL_BOOK, SpellbookScreen::new);
ClientTickEvents.END_CLIENT_TICK.register(this::onTick);
ClientTickEvents.END_WORLD_TICK.register(this::onWorldTick);
ScreenInitCallback.EVENT.register(this::onScreenInit);
ItemTooltipCallback.EVENT.register(new ModifierTooltipRenderer());
@ -83,6 +92,25 @@ public class UnicopiaClient implements ClientModInitializer {
UHud.INSTANCE.tick();
}
private void onWorldTick(ClientWorld world) {
BlockPos pos = MinecraftClient.getInstance().getCameraEntity().getBlockPos();
float tickDelta = MinecraftClient.getInstance().getTickDelta();
if (WeatherConditions.get(world).isInRangeOfStorm(pos)) {
if (originalRainGradient == null) {
originalRainGradient = world.getRainGradient(tickDelta);
}
world.setRainGradient(1);
world.setThunderGradient(1);
} else {
if (originalRainGradient != null) {
world.setRainGradient(originalRainGradient);
world.setThunderGradient(0);
originalRainGradient = null;
}
}
}
private void onScreenInit(Screen screen, ButtonList buttons) {
if (screen instanceof OpenToLanScreen) {
buttons.addButton(new Button(screen.width / 2 - 155, 130, 150, 20))

View file

@ -6,8 +6,8 @@ import java.util.Optional;
import org.joml.Vector3f;
import com.minelittlepony.unicopia.EntityConvertable;
import com.minelittlepony.unicopia.Unicopia;
import com.minelittlepony.unicopia.ability.magic.Caster;
import com.minelittlepony.unicopia.entity.player.Pony;
import com.minelittlepony.unicopia.particle.ParticleHandle.Attachment;
import com.minelittlepony.unicopia.particle.ParticleHandle.Link;
@ -79,8 +79,8 @@ public class RainbowTrailParticle extends AbstractBillboardParticle implements A
}
}
private void follow(Caster<?> caster) {
Vec3d next = caster.getOriginVector();
private void follow(EntityConvertable<?> caster) {
Vec3d next = caster.asEntity().getPos();
if (segments.isEmpty()) {
segments.add(new Segment(next));

View file

@ -6,8 +6,8 @@ import org.joml.Quaternionf;
import org.joml.Vector3f;
import com.minelittlepony.common.util.Color;
import com.minelittlepony.unicopia.EntityConvertable;
import com.minelittlepony.unicopia.Unicopia;
import com.minelittlepony.unicopia.ability.magic.Caster;
import com.minelittlepony.unicopia.particle.OrientedBillboardParticleEffect;
import com.minelittlepony.unicopia.particle.ParticleHandle.Attachment;
import com.minelittlepony.unicopia.particle.ParticleHandle.Link;
@ -16,6 +16,7 @@ import com.mojang.blaze3d.systems.RenderSystem;
import net.minecraft.client.render.BufferBuilder;
import net.minecraft.client.render.Tessellator;
import net.minecraft.client.world.ClientWorld;
import net.minecraft.entity.Entity;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.*;
@ -62,7 +63,7 @@ public class RunesParticle extends OrientedBillboardParticle implements Attachme
velocityX = 0;
velocityY = 0;
velocityZ = 0;
Vec3d pos = link.get().map(Caster::getOriginVector).orElse(Vec3d.ZERO);
Vec3d pos = link.get().map(EntityConvertable::asEntity).map(Entity::getPos).orElse(Vec3d.ZERO);
setPos(pos.x, pos.y + 0.25, pos.z);
}
@ -164,7 +165,7 @@ public class RunesParticle extends OrientedBillboardParticle implements Attachme
public void tick() {
super.tick();
link.flatMap(Link::get).map(Caster::asEntity).ifPresentOrElse(e -> {
link.flatMap(Link::get).map(EntityConvertable::asEntity).ifPresentOrElse(e -> {
if (getAlphaScale() >= 0.9F) {
if (stasisAge < 0) {
stasisAge = age;

View file

@ -12,7 +12,7 @@ import net.minecraft.client.world.ClientWorld;
import net.minecraft.util.math.MathHelper;
import net.minecraft.util.math.Vec3d;
import com.minelittlepony.unicopia.ability.magic.Caster;
import com.minelittlepony.unicopia.EntityConvertable;
import com.minelittlepony.unicopia.client.render.RenderLayers;
import com.minelittlepony.unicopia.client.render.model.SphereModel;
import com.minelittlepony.unicopia.particle.SphereParticleEffect;
@ -38,6 +38,8 @@ public class SphereParticle extends Particle implements Attachment {
private final SphereParticleEffect parameters;
private boolean bound;
public SphereParticle(SphereParticleEffect parameters, ClientWorld w, double x, double y, double z, double vX, double vY, double vZ) {
this(parameters, w, x, y, z);
@ -90,6 +92,9 @@ public class SphereParticle extends Particle implements Attachment {
if (key == ATTR_OPACITY) {
alpha = value.floatValue();
}
if (key == ATTR_BOUND) {
bound = value.intValue() == 1;
}
}
@Override
@ -102,13 +107,15 @@ public class SphereParticle extends Particle implements Attachment {
super.tick();
if (link.isPresent()) {
link.flatMap(Link::get).map(Caster::asEntity).ifPresentOrElse(e -> {
Vec3d offset = parameters.getOffset();
setPos(e.getX() + offset.getX(), e.getY() + offset.getY(), e.getZ() + offset.getZ());
link.flatMap(Link::get).map(EntityConvertable::asEntity).ifPresentOrElse(e -> {
if (!bound) {
Vec3d offset = parameters.getOffset();
setPos(e.getX() + offset.getX(), e.getY() + offset.getY(), e.getZ() + offset.getZ());
prevPosX = e.lastRenderX + offset.getX();
prevPosY = e.lastRenderY + offset.getY();
prevPosZ = e.lastRenderZ + offset.getZ();
prevPosX = e.lastRenderX + offset.getX();
prevPosY = e.lastRenderY + offset.getY();
prevPosZ = e.lastRenderZ + offset.getZ();
}
}, this::detach);
if (steps-- > 0) {

View file

@ -0,0 +1,42 @@
package com.minelittlepony.unicopia.client.render.entity;
import com.minelittlepony.unicopia.entity.CrystalShardsEntity;
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.ModelPartData;
import net.minecraft.client.model.ModelTransform;
import net.minecraft.client.model.TexturedModelData;
import net.minecraft.client.render.VertexConsumer;
import net.minecraft.client.render.entity.model.EntityModel;
import net.minecraft.client.util.math.MatrixStack;
public class CrystalShardsEntityModel extends EntityModel<CrystalShardsEntity> {
private final ModelPart part;
public CrystalShardsEntityModel(ModelPart root) {
this.part = root;
}
public static TexturedModelData getTexturedModelData() {
ModelData data = new ModelData();
ModelPartData root = data.getRoot();
root.addChild("west", ModelPartBuilder.create().uv(12, 0).cuboid(-1, -10, -2, 3, 10, 3, Dilation.NONE), ModelTransform.rotation(0.4075F, 0.0378F, 1.0216F));
root.addChild("north", ModelPartBuilder.create().uv(12, 0).cuboid(-1, -10, -2, 3, 10, 3, Dilation.NONE), ModelTransform.rotation(1.817F, 0.6399F, 2.0582F));
root.addChild("south", ModelPartBuilder.create().uv(12, 0).cuboid(-2, -9, -2, 3, 10, 3, Dilation.NONE), ModelTransform.rotation(-1.8254F, 0.6819F, -1.3949F));
root.addChild("east", ModelPartBuilder.create().uv(12, 13).cuboid(-2, -8, -2, 3, 8, 3, Dilation.NONE), ModelTransform.rotation(-0.1151F, 0.3935F, -0.7545F));
root.addChild("primary", ModelPartBuilder.create().uv(0, 0).cuboid(-2, -20, -2, 3, 20, 3, Dilation.NONE), ModelTransform.rotation(0.2411F, 0.3339F, 0.1819F));
return TexturedModelData.of(data, 32, 32);
}
@Override
public void setAngles(CrystalShardsEntity entity, float limbSwing, float limbSwingAmount, float ageInTicks, float netHeadYaw, float headPitch) {
}
@Override
public void render(MatrixStack matrices, VertexConsumer vertexConsumer, int light, int overlay, float red, float green, float blue, float alpha) {
part.render(matrices, vertexConsumer, light, overlay, red, green, blue, alpha);
}
}

View file

@ -0,0 +1,51 @@
package com.minelittlepony.unicopia.client.render.entity;
import java.util.List;
import com.minelittlepony.unicopia.Unicopia;
import com.minelittlepony.unicopia.entity.CrystalShardsEntity;
import net.minecraft.client.render.OverlayTexture;
import net.minecraft.client.render.VertexConsumerProvider;
import net.minecraft.client.render.entity.EntityRenderer;
import net.minecraft.client.render.entity.EntityRendererFactory;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.RotationAxis;
public class CrystalShardsEntityRenderer extends EntityRenderer<CrystalShardsEntity> {
private static final Identifier TEXTURE = Unicopia.id("textures/entity/crystal_shards/normal.png");
private static final Identifier[] CORRUPTED = List.of("corrupt", "dark", "darker").stream()
.map(name -> Unicopia.id("textures/entity/crystal_shards/" + name + ".png"))
.toArray(Identifier[]::new);
private final CrystalShardsEntityModel model;
public CrystalShardsEntityRenderer(EntityRendererFactory.Context context) {
super(context);
model = new CrystalShardsEntityModel(CrystalShardsEntityModel.getTexturedModelData().createModel());
}
@Override
public void render(CrystalShardsEntity entity, float yaw, float tickDelta, MatrixStack matrices, VertexConsumerProvider vertices, int light) {
matrices.push();
matrices.multiply(entity.getAttachmentFace().getRotationQuaternion());
matrices.scale(-1, -1, 1);
float scale = entity.getGrowth(tickDelta);
matrices.scale(scale, scale, scale);
matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(yaw));
model.setAngles(entity, 0, 0, 0, 0, 0);
model.render(matrices, vertices.getBuffer(model.getLayer(getTexture(entity))), light, OverlayTexture.DEFAULT_UV, 1, 1, 1, 1);
matrices.pop();
super.render(entity, yaw, tickDelta, matrices, vertices, light);
}
@Override
public Identifier getTexture(CrystalShardsEntity entity) {
if (entity.isCorrupt()) {
return CORRUPTED[(int)(Math.abs(entity.getUuid().getMostSignificantBits()) % CORRUPTED.length)];
}
return TEXTURE;
}
}

View file

@ -19,7 +19,6 @@ public class SombraEntityModel extends EntityModel<SombraEntity> {
private final ModelPart part;
private final ModelPart head;
private final ModelPart mane;
private final ModelPart upperJaw;
private final ModelPart lowerJaw;
@ -28,7 +27,6 @@ public class SombraEntityModel extends EntityModel<SombraEntity> {
public SombraEntityModel(ModelPart root) {
this.part = root;
this.head = root.getChild("head");
this.mane = head.getChild("mane");
this.upperJaw = head.getChild("upper_jaw");
this.lowerJaw = head.getChild("lower_jaw");
this.body = root.getChild("body");
@ -84,27 +82,34 @@ public class SombraEntityModel extends EntityModel<SombraEntity> {
}
@Override
public void setAngles(SombraEntity entity, float limbSwing, float limbSwingAmount, float ageInTicks, float netHeadYaw, float headPitch) {
public void animateModel(SombraEntity entity, float limbAngle, float limbDistance, float tickDelta) {
float jawsOpenAmount = entity.getBiteAmount(tickDelta); //(1 - Math.max(0, MathHelper.sin(ageInTicks * 0.1F)));
lowerJaw.resetTransform();
lowerJaw.pivotY -= jawsOpenAmount * 3;
lowerJaw.pivotX -= jawsOpenAmount * 3;
lowerJaw.roll += jawsOpenAmount - 0.9F;
upperJaw.resetTransform();
upperJaw.roll -= jawsOpenAmount * 0.2F;
}
@Override
public void setAngles(SombraEntity entity, float limbAngle, float limbDistance, float animationProgress, float netHeadYaw, float headPitch) {
float scale = 1.6F;
part.xScale = scale;
part.yScale = scale;
part.zScale = scale;
part.yaw = -MathHelper.HALF_PI;
part.pivotY = MathHelper.sin(ageInTicks * 0.05F) - 3;
part.pivotZ = MathHelper.cos(ageInTicks * 0.045F);
//part.yaw = (float)entity.getVelocity().getX();
part.pivotY = MathHelper.sin(animationProgress * 0.05F) - 12;
part.pivotZ = MathHelper.cos(animationProgress * 0.045F);
head.pitch = headPitch * MathHelper.RADIANS_PER_DEGREE;
head.yaw = netHeadYaw * MathHelper.RADIANS_PER_DEGREE;
lowerJaw.resetTransform();
float jawsOpenAmount = (1 - Math.max(0, MathHelper.sin(ageInTicks * 0.1F)));
lowerJaw.pivotY -= jawsOpenAmount * 3;
lowerJaw.pivotX -= jawsOpenAmount * 3;
lowerJaw.roll += jawsOpenAmount - 0.9F;
upperJaw.resetTransform();
upperJaw.roll -= jawsOpenAmount * 0.2F;
body.roll = limbSwingAmount * 0.3F;
body.roll = limbDistance * 0.3F;
}
@Override

View file

@ -0,0 +1,138 @@
package com.minelittlepony.unicopia.client.render.entity;
import java.util.List;
import org.joml.Vector3f;
import com.minelittlepony.unicopia.entity.StormCloudEntity;
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.ModelPartData;
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.entity.model.EntityModel;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.util.math.MathHelper;
import net.minecraft.util.math.random.Random;
public class StormCloudEntityModel extends EntityModel<StormCloudEntity> {
private final ModelPart part;
private final ModelPart smallPuffs;
private final List<ModelPart> smallPuffCubes;
private final ModelPart anvilHeads;
private final List<ModelPart> anvilHeadCubes;
private final ModelPart puff;
private final Random rng = Random.create(0);
private final Vector3f puffLocation = new Vector3f();
public StormCloudEntityModel(ModelPart root) {
super(RenderLayer::getEntityTranslucent);
this.part = root;
this.smallPuffs = part.getChild("small_puffs");
this.smallPuffCubes = smallPuffs.traverse().toList();
this.anvilHeads = part.getChild("anvil_heads");
this.anvilHeadCubes = anvilHeads.traverse().toList();
this.puff = part.getChild("puff");
}
public static TexturedModelData getTexturedModelData() {
ModelData data = new ModelData();
ModelPartData root = data.getRoot();
ModelPartData small_puffs = root.addChild("small_puffs", ModelPartBuilder.create(), ModelTransform.pivot(0, 24, 0));
small_puffs.addChild("cube_r1", ModelPartBuilder.create()
.uv(0, 37).cuboid(-2, -20, -20, 7, 17, 20, Dilation.NONE)
.uv(0, 37).cuboid(-2, -13, -40, 7, 17, 20, Dilation.NONE), ModelTransform.of(0, -6, 0, 0, 0, 1.5708F));
small_puffs.addChild("cube_r2", ModelPartBuilder.create().uv(0, 37).cuboid(-2, -7, -25, 7, 17, 20, Dilation.NONE), ModelTransform.of(0, -6, 0, 0.1745F, 0, 1.5708F));
small_puffs.addChild("cube_r3", ModelPartBuilder.create().uv(0, 37).cuboid(-7, -7, -7, 7, 17, 20, Dilation.NONE), ModelTransform.of(-15, -1, 15, 0.2182F, 0, 1.5708F));
small_puffs.addChild("cube_r4", ModelPartBuilder.create().cuboid(-13, -14, -31, 7, 17, 20, Dilation.NONE), ModelTransform.of(2, -2, 19, 0, 0, 1.6581F));
small_puffs.addChild("cube_r5", ModelPartBuilder.create().uv(0, 37).cuboid(-6, -17, -6, 7, 17, 20, Dilation.NONE), ModelTransform.of(2, -2, 19, -0.3491F, 0, 1.5708F));
small_puffs.addChild("cube_r6", ModelPartBuilder.create().cuboid(-3.5F, -4.5F, -23, 7, 17, 20, Dilation.NONE), ModelTransform.of(0.4471F, -12.1925F, 36, 0, 0, -3.1416F));
small_puffs.addChild("cube_r7", ModelPartBuilder.create().uv(0, 37).cuboid(-6, -17, -12, 7, 17, 20, Dilation.NONE), ModelTransform.of(-15, -2, 26, -0.829F, 0, 1.5708F));
small_puffs.addChild("cube_r8", ModelPartBuilder.create()
.uv(0, 37).cuboid(-6, -20, -21, 7, 17, 20, Dilation.NONE)
.cuboid(-13, -20, -40, 7, 17, 20, Dilation.NONE), ModelTransform.of(-15, -2, 26, 0, 0, 1.5708F));
small_puffs.addChild("cube_r9", ModelPartBuilder.create().cuboid(-15, -13, -18, 7, 17, 20, Dilation.NONE), ModelTransform.of(-15, -1, 15, 0, 0, 1.4835F));
small_puffs.addChild("cube_r10", ModelPartBuilder.create().uv(0, 37).cuboid(-7, -3, -25, 7, 17, 20, Dilation.NONE), ModelTransform.of(-15, -1, 15, 0, 0, 1.5708F));
small_puffs.addChild("cube_r11", ModelPartBuilder.create().uv(0, 37).cuboid(-2, -4, -7, 7, 17, 20, Dilation.NONE), ModelTransform.of(0, -6, 0, -0.0873F, 0, 1.5708F));
small_puffs.addChild("cube_r12", ModelPartBuilder.create().cuboid(-9.5F, -8.5F, -10, 7, 17, 20, Dilation.NONE), ModelTransform.of(8.842F, -13.6476F, -20.7083F, 0.1739F, 0.0151F, -0.0423F));
small_puffs.addChild("cube_r13", ModelPartBuilder.create().uv(0, 37).cuboid(-2, -19, -6, 7, 17, 20, Dilation.NONE), ModelTransform.of(0, -6, 0, -0.3043F, 0, 1.5708F));
ModelPartData anvil_heads = root.addChild("anvil_heads", ModelPartBuilder.create(), ModelTransform.pivot(-2, 1, 0));
anvil_heads.addChild("cube_r14", ModelPartBuilder.create().cuboid(-3.5F, -1.5F, -4, 7, 17, 20, new Dilation(2)), ModelTransform.of(-5.5F, -4.5F, 16, -0.3927F, 0, 0));
anvil_heads.addChild("cube_r15", ModelPartBuilder.create().cuboid(-2, -18, 4, 7, 17, 20, new Dilation(5)), ModelTransform.of(0, -6, 0, -0.1309F, 0, 0));
anvil_heads.addChild("cube_r16", ModelPartBuilder.create().cuboid(0, -1.5F, -12.5F, 7, 17, 20, new Dilation(5)), ModelTransform.of(0, -6, 0, 0.3491F, 0, 0));
anvil_heads.addChild("cube_r17", ModelPartBuilder.create().cuboid(7, 9, 0, 7, 17, 20, new Dilation(2)), ModelTransform.of(0, -6, 0, 0.2618F, 0, 0));
anvil_heads.addChild("cube_r18", ModelPartBuilder.create().cuboid(-12, 3, -9, 7, 17, 20, new Dilation(2)), ModelTransform.of(0, -6, 0, 0.3927F, 0, 0));
root.addChild("puff", ModelPartBuilder.create()
.uv(0, 77).cuboid(-6, -11, -5, 18, 5, 9, Dilation.NONE)
.uv(0, 80).cuboid(-8, -10, 4, 14, 4, 6, Dilation.NONE)
.uv(0, 74).cuboid(-2, -10, -11, 12, 5, 11, Dilation.NONE)
.uv(7, 77).cuboid(-14, -11, -10, 12, 5, 9, Dilation.NONE)
.uv(0, 79).cuboid(-2, -13, 0, 8, 3, 6, Dilation.NONE), ModelTransform.of(-5, 14, -20, 0, 1.5272F, 0));
return TexturedModelData.of(data, 64, 128);
}
@Override
public void setAngles(StormCloudEntity entity, float limbSwing, float limbSwingAmount, float ageInTicks, float netHeadYaw, float headPitch) {
float dir = 1;
float globalScale = 0.012F;
float roll = MathHelper.cos(entity.age / 10F) * globalScale;
float flop = MathHelper.sin(entity.age / 30F) * 0.02F * globalScale;
for (ModelPart swirl : smallPuffCubes) {
swirl.resetTransform();
dir = -dir;
swirl.yaw += roll * 0.01F * dir;
}
for (ModelPart swirl : anvilHeadCubes) {
swirl.resetTransform();
dir = -dir;
swirl.pitch += flop + roll * -0.01F * dir;
swirl.yaw += roll * 0.01F * dir;
swirl.roll += -flop + roll * 0.01F * dir;
}
part.pivotY = MathHelper.sin(entity.age * 0.25F) * 0.03F * globalScale;
part.pivotX = MathHelper.cos(entity.age * 0.05125F) * 0.7F * globalScale;
part.pivotZ = MathHelper.sin(entity.age * 0.05125F) * 0.7F * globalScale;
rng.setSeed(entity.getId());
}
@Override
public void render(MatrixStack matrices, VertexConsumer vertexConsumer, int light, int overlay, float red, float green, float blue, float alpha) {
matrices.push();
part.rotate(matrices);
smallPuffs.render(matrices, vertexConsumer, light, overlay, red, green, blue, alpha);
anvilHeads.render(matrices, vertexConsumer, light, overlay, red, green, blue, alpha);
int puffCount = rng.nextInt(7);
for (int i = 0; i < puffCount; i++) {
puff.resetTransform();
puff.translate(puffLocation.set(
rng.nextGaussian(),
rng.nextGaussian(),
rng.nextGaussian()
).mul(16));
puff.render(matrices, vertexConsumer, light, overlay, red, green, blue, alpha);
}
matrices.pop();
}
}

View file

@ -0,0 +1,53 @@
package com.minelittlepony.unicopia.client.render.entity;
import com.minelittlepony.unicopia.Unicopia;
import com.minelittlepony.unicopia.entity.StormCloudEntity;
import net.minecraft.client.render.Frustum;
import net.minecraft.client.render.OverlayTexture;
import net.minecraft.client.render.VertexConsumerProvider;
import net.minecraft.client.render.entity.EntityRenderer;
import net.minecraft.client.render.entity.EntityRendererFactory;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.RotationAxis;
public class StormCloudEntityRenderer extends EntityRenderer<StormCloudEntity> {
private static final Identifier TEXTURE = Unicopia.id("textures/entity/storm_cloud.png");
private final StormCloudEntityModel model;
public StormCloudEntityRenderer(EntityRendererFactory.Context context) {
super(context);
model = new StormCloudEntityModel(StormCloudEntityModel.getTexturedModelData().createModel());
}
@Override
public void render(StormCloudEntity entity, float yaw, float tickDelta, MatrixStack matrices, VertexConsumerProvider vertices, int light) {
matrices.push();
matrices.scale(-1, -1, 1);
matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(yaw));
float scale = entity.getSize(tickDelta);
matrices.scale(scale, scale, scale);
matrices.translate(0, -1.45F, 0);
model.setAngles(entity, 0, 0, 0, 0, 0);
model.render(matrices, vertices.getBuffer(model.getLayer(getTexture(entity))),
entity.isStormy() ? 0 : light,
OverlayTexture.DEFAULT_UV, 1, 1, 1, 0.9F);
matrices.pop();
super.render(entity, yaw, tickDelta, matrices, vertices, light);
}
@Override
public Identifier getTexture(StormCloudEntity entity) {
return TEXTURE;
}
@Override
public boolean shouldRender(StormCloudEntity entity, Frustum frustum, double x, double y, double z) {
return true;
}
}

View file

@ -0,0 +1,16 @@
package com.minelittlepony.unicopia.entity;
import java.util.Optional;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Vec3d;
public interface ArenaCombatant {
Optional<BlockPos> getHomePos();
default float getAreaRadius() {
return 30;
}
boolean teleportTo(Vec3d destination);
}

View file

@ -0,0 +1,208 @@
package com.minelittlepony.unicopia.entity;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import com.minelittlepony.unicopia.USounds;
import com.minelittlepony.unicopia.item.UItems;
import net.minecraft.block.SideShapeType;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityType;
import net.minecraft.entity.damage.DamageSource;
import net.minecraft.entity.data.DataTracker;
import net.minecraft.entity.data.TrackedData;
import net.minecraft.entity.data.TrackedDataHandlerRegistry;
import net.minecraft.fluid.Fluids;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.predicate.entity.EntityPredicates;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Box;
import net.minecraft.util.math.Direction;
import net.minecraft.util.math.MathHelper;
import net.minecraft.world.World;
public class CrystalShardsEntity extends Entity {
static final byte SHAKE = 1;
static final int FULL_GROWTH_AGE = 25;
private static final Set<Direction> ALL_DIRECTIONS = Set.of(Direction.values());
private static final TrackedData<Direction> ATTACHMENT_FACE = DataTracker.registerData(SombraEntity.class, TrackedDataHandlerRegistry.FACING);
private static final TrackedData<Integer> GROWTH = DataTracker.registerData(SombraEntity.class, TrackedDataHandlerRegistry.INTEGER);
private static final TrackedData<Boolean> DECAYING = DataTracker.registerData(SombraEntity.class, TrackedDataHandlerRegistry.BOOLEAN);
private static final TrackedData<Boolean> CORRUPT = DataTracker.registerData(SombraEntity.class, TrackedDataHandlerRegistry.BOOLEAN);
public static boolean infestBlock(ServerWorld world, BlockPos pos) {
if (world.isAir(pos) || !world.getFluidState(pos).isOf(Fluids.EMPTY)) {
return false;
}
boolean success = false;
for (Direction face : getFreeFaces(world, pos)) {
CrystalShardsEntity shards = UEntities.CRYSTAL_SHARDS.create(world);
shards.setPosition(pos.offset(face).toCenterPos());
shards.setAttachmentFace(face);
shards.setYaw(world.getRandom().nextFloat() * 360);
shards.setCorrupt(true);
world.spawnEntity(shards);
success = true;
}
return success;
}
public static Set<Direction> getFreeFaces(World world, BlockPos pos) {
Set<Direction> freeFaces = new HashSet<>(ALL_DIRECTIONS);
freeFaces.removeAll(getOccupiedFaces(world, pos));
freeFaces.removeIf(face -> isInvalid(world, pos.offset(face), face));
return freeFaces;
}
public static Set<Direction> getOccupiedFaces(World world, BlockPos pos) {
return world.getEntitiesByClass(CrystalShardsEntity.class, new Box(pos).expand(1), EntityPredicates.VALID_ENTITY)
.stream()
.map(e -> e.getAttachmentFace())
.distinct()
.collect(Collectors.toSet());
}
static boolean isInvalid(World world, BlockPos crystalPos, Direction attachmentFace) {
if (!world.isAir(crystalPos)) {
return true;
}
BlockPos attachmentPos = crystalPos.offset(attachmentFace.getOpposite());
return !world.getBlockState(attachmentPos).isSideSolid(world, attachmentPos, attachmentFace, SideShapeType.RIGID);
}
private int prevAge;
private int ticksShaking;
public CrystalShardsEntity(EntityType<CrystalShardsEntity> type, World world) {
super(type, world);
}
@Override
protected void initDataTracker() {
dataTracker.startTracking(ATTACHMENT_FACE, Direction.UP);
dataTracker.startTracking(GROWTH, 0);
dataTracker.startTracking(DECAYING, false);
dataTracker.startTracking(CORRUPT, false);
}
public float getGrowth(float tickDelta) {
int age = getGrowth();
float lerped = MathHelper.clamp(MathHelper.lerp(tickDelta, prevAge, age), 0, FULL_GROWTH_AGE) / (float)FULL_GROWTH_AGE;
return lerped * 2 * (1 + Math.abs(getUuid().getLeastSignificantBits() % 20) / 20F);
}
public int getGrowth() {
return dataTracker.get(GROWTH);
}
public void setGrowth(int growth) {
dataTracker.set(GROWTH, Math.max(0, growth));
}
public void setDecaying(boolean decaying) {
dataTracker.set(DECAYING, decaying);
}
public boolean isDecaying() {
return dataTracker.get(DECAYING);
}
public void setCorrupt(boolean corrupted) {
dataTracker.set(CORRUPT, corrupted);
}
public boolean isCorrupt() {
return dataTracker.get(CORRUPT);
}
public boolean isShaking() {
return ticksShaking > 0;
}
public Direction getAttachmentFace() {
return Objects.requireNonNullElse(dataTracker.get(ATTACHMENT_FACE), Direction.UP);
}
public void setAttachmentFace(Direction face) {
dataTracker.set(ATTACHMENT_FACE, face);
}
@Override
public void tick() {
prevAge = getGrowth();
if (!getWorld().isClient) {
int growAmount = 1 + Math.abs((int)getUuid().getLeastSignificantBits() % 5);
setGrowth(prevAge + (isDecaying() ? -growAmount : growAmount));
}
setFireTicks(0);
if (ticksShaking > 0) {
ticksShaking--;
}
setPosition(getBlockPos().toCenterPos());
super.tick();
if (getGrowth() < FULL_GROWTH_AGE) {
playSound(USounds.Vanilla.BLOCK_AMETHYST_BLOCK_CHIME, 1, 1);
}
if (isInvalid(getWorld(), getBlockPos(), getAttachmentFace())) {
kill();
}
}
@Override
public boolean damage(DamageSource source, float amount) {
getWorld().sendEntityStatus(this, SHAKE);
return super.damage(source, amount);
}
@Override
public void remove(RemovalReason reason) {
if (reason == RemovalReason.KILLED) {
playSound(USounds.Vanilla.BLOCK_AMETHYST_BLOCK_BREAK, 1, 1);
dropStack(new ItemStack(UItems.CRYSTAL_SHARD, 6));
}
super.remove(reason);
}
@Override
public void handleStatus(byte status) {
switch (status) {
case SHAKE:
ticksShaking = 30;
break;
default:
super.handleStatus(status);
}
}
@Override
public void writeCustomDataToNbt(NbtCompound nbt) {
nbt.putFloat("yaw", this.getYaw());
nbt.putInt("growth", getGrowth());
nbt.putString("face", getAttachmentFace().getName());
nbt.putBoolean("decaying", isDecaying());
nbt.putBoolean("corrupt", isCorrupt());
}
@Override
public void readCustomDataFromNbt(NbtCompound nbt) {
setYaw(nbt.getFloat("yaw"));
setGrowth(nbt.getInt("growth"));
setAttachmentFace(Direction.byName(nbt.getString("face")));
setDecaying(nbt.getBoolean("decaying"));
setCorrupt(nbt.getBoolean("corrupt"));
prevAge = getGrowth();
}
}

View file

@ -1,28 +1,41 @@
package com.minelittlepony.unicopia.entity;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Stream;
import org.jetbrains.annotations.Nullable;
import com.minelittlepony.unicopia.USounds;
import com.minelittlepony.unicopia.entity.ai.ArenaAttackGoal;
import com.minelittlepony.unicopia.item.AmuletItem;
import com.minelittlepony.unicopia.item.UItems;
import com.minelittlepony.unicopia.particle.ParticleHandle;
import com.minelittlepony.unicopia.particle.ParticleHandle.Attachment;
import com.minelittlepony.unicopia.particle.ParticleSource;
import com.minelittlepony.unicopia.particle.ParticleUtils;
import com.minelittlepony.unicopia.particle.SphereParticleEffect;
import com.minelittlepony.unicopia.particle.UParticles;
import com.minelittlepony.unicopia.util.VecHelper;
import com.minelittlepony.unicopia.util.shape.Sphere;
import net.minecraft.block.BlockState;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityType;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.ai.goal.ActiveTargetGoal;
import net.minecraft.entity.ai.goal.AttackGoal;
import net.minecraft.entity.ai.goal.FlyGoal;
import net.minecraft.entity.ai.goal.LongDoorInteractGoal;
import net.minecraft.entity.ai.goal.LookAroundGoal;
import net.minecraft.entity.ai.goal.LookAtEntityGoal;
import net.minecraft.entity.ai.goal.PounceAtTargetGoal;
import net.minecraft.entity.ai.goal.RevengeGoal;
import net.minecraft.entity.ai.goal.WanderAroundGoal;
import net.minecraft.entity.ai.pathing.BirdPathNodeMaker;
import net.minecraft.entity.ai.pathing.EntityNavigation;
import net.minecraft.entity.ai.pathing.MobNavigation;
import net.minecraft.entity.ai.pathing.PathNodeNavigator;
import net.minecraft.entity.ai.pathing.PathNodeType;
import net.minecraft.entity.attribute.DefaultAttributeContainer;
import net.minecraft.entity.attribute.EntityAttributes;
import net.minecraft.entity.boss.BossBar;
@ -41,20 +54,26 @@ import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.nbt.NbtElement;
import net.minecraft.nbt.NbtHelper;
import net.minecraft.particle.ParticleEffect;
import net.minecraft.particle.ParticleTypes;
import net.minecraft.predicate.entity.EntityPredicates;
import net.minecraft.registry.tag.*;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.sound.SoundEvent;
import net.minecraft.sound.SoundEvents;
import net.minecraft.text.Text;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.util.math.MathHelper;
import net.minecraft.util.math.Vec3d;
import net.minecraft.world.World;
import net.minecraft.world.event.GameEvent;
public class SombraEntity extends HostileEntity {
public class SombraEntity extends HostileEntity implements ArenaCombatant, ParticleSource<SombraEntity> {
static final byte BITE = 70;
static final int MAX_BITE_TIME = 20;
static final Predicate<Entity> EFFECT_TARGET_PREDICATE = EntityPredicates.VALID_LIVING_ENTITY.and(EntityPredicates.EXCEPT_CREATIVE_OR_SPECTATOR);
private static final TrackedData<Optional<BlockPos>> HOME_POS = DataTracker.registerData(SombraEntity.class, TrackedDataHandlerRegistry.OPTIONAL_BLOCK_POS);
@ -62,6 +81,21 @@ public class SombraEntity extends HostileEntity {
.setDarkenSky(true)
.setThickenFog(true);
private final ParticleHandle shroud = new ParticleHandle();
final EntityReference<StormCloudEntity> stormCloud = new EntityReference<>();
private int prevBiteTime;
private int biteTime;
public static void startEncounter(World world, BlockPos pos) {
StormCloudEntity cloud = UEntities.STORM_CLOUD.create(world);
cloud.setPosition(pos.up(10).toCenterPos());
cloud.setSize(1);
cloud.cursed = true;
world.spawnEntity(cloud);
}
public SombraEntity(EntityType<SombraEntity> type, World world) {
super(type, world);
bossBar.setStyle(BossBar.Style.NOTCHED_10);
@ -70,7 +104,13 @@ public class SombraEntity extends HostileEntity {
public static DefaultAttributeContainer.Builder createMobAttributes() {
return HostileEntity.createMobAttributes()
.add(EntityAttributes.GENERIC_MAX_HEALTH, 2000)
.add(EntityAttributes.GENERIC_ATTACK_DAMAGE, 102);
.add(EntityAttributes.GENERIC_ATTACK_KNOCKBACK, 1.5)
.add(EntityAttributes.GENERIC_ATTACK_DAMAGE, 22);
}
@Override
public SombraEntity asEntity() {
return this;
}
@Override
@ -101,27 +141,37 @@ public class SombraEntity extends HostileEntity {
@Override
protected void initGoals() {
goalSelector.add(5, new WanderAroundGoal(this, 1));
goalSelector.add(6, new LookAtEntityGoal(this, PlayerEntity.class, 8F));
goalSelector.add(2, new LongDoorInteractGoal(this, true));
goalSelector.add(5, new FlyGoal(this, 1));
goalSelector.add(6, new LookAtEntityGoal(this, PlayerEntity.class, 18F));
goalSelector.add(7, new LookAroundGoal(this));
goalSelector.add(8, new PounceAtTargetGoal(this, 0.3f));
goalSelector.add(8, new AttackGoal(this));
goalSelector.add(7, new WanderAroundGoal(this, 1));
goalSelector.add(8, new PounceAtTargetGoal(this, 1.3F));
goalSelector.add(8, new ArenaAttackGoal<>(this));
targetSelector.add(1, new RevengeGoal(this));
targetSelector.add(2, new ActiveTargetGoal<>(this, PlayerEntity.class, false));
targetSelector.add(3, new ActiveTargetGoal<>(this, MerchantEntity.class, false));
targetSelector.add(3, new ActiveTargetGoal<>(this, IronGolemEntity.class, true));
}
@Override
protected EntityNavigation createNavigation(World world) {
MobNavigation nav = new MobNavigation(this, world);
MobNavigation nav = new MobNavigation(this, world) {
@Override
protected PathNodeNavigator createPathNodeNavigator(int range) {
nodeMaker = new BirdPathNodeMaker();
nodeMaker.setCanEnterOpenDoors(true);
return new PathNodeNavigator(nodeMaker, range);
}
};
nav.setCanPathThroughDoors(true);
nav.setCanSwim(true);
nav.setCanEnterOpenDoors(true);
nav.canJumpToNext(PathNodeType.UNPASSABLE_RAIL);
return nav;
}
@Override
public Optional<BlockPos> getHomePos() {
return dataTracker.get(HOME_POS);
}
@ -130,9 +180,14 @@ public class SombraEntity extends HostileEntity {
dataTracker.set(HOME_POS, Optional.of(pos));
}
public float getBiteAmount(float tickDelta) {
float progress = (MathHelper.lerp(tickDelta, prevBiteTime, biteTime) / (float)MAX_BITE_TIME);
return 1 - Math.abs(MathHelper.sin(progress * MathHelper.PI));
}
@Override
public void tick() {
setPersistent();
Optional<BlockPos> homePos = getHomePos();
if (homePos.isEmpty() && !isRemoved()) {
@ -140,59 +195,128 @@ public class SombraEntity extends HostileEntity {
return;
}
if (this.getBlockPos().getSquaredDistance(homePos.get()) > 16) {
if (getBlockPos().getSquaredDistance(homePos.get()) > MathHelper.square(getAreaRadius())) {
teleportTo(Vec3d.ofCenter(homePos.get()));
setTarget(null);
getNavigation().stop();
}
prevBiteTime = biteTime;
if (biteTime > 0) {
biteTime--;
}
super.tick();
addVelocity(0, 0.002F, 0);
if (getTarget() == null && getVelocity().y < 0) {
setVelocity(getVelocity().multiply(1, 0.4, 1));
}
addVelocity(0, 0.0242F, 0);
if (isSubmergedInWater()) {
jump();
}
if (age % 50 == 0) {
playSound(SoundEvents.ENTITY_POLAR_BEAR_AMBIENT, 3, 0.3F);
if (random.nextInt(1200) == 0) {
playSound(USounds.ENTITY_SOMBRA_LAUGH, 1, 1);
getWorld().sendEntityStatus(this, BITE);
}
if (age % 125 == 0) {
if (random.nextInt(340) == 0) {
playSound(SoundEvents.AMBIENT_CAVE.value(), 1, 0.3F);
} else if (random.nextInt(1340) == 0) {
playSound(USounds.ENTITY_SOMBRA_AMBIENT, 1, 1);
}
if (getWorld().isClient) {
float range = 9;
for (int i = 0; i < 3; i++) {
var particle = new SphereParticleEffect(UParticles.SPHERE,
0x222222,
0.7F,
(float)getWorld().getRandom().nextTriangular(2.5F, 0.7F)
);
getWorld().addParticle(particle,
getWorld().getRandom().nextTriangular(getX(), range),
getWorld().getRandom().nextTriangular(getY(), range),
getWorld().getRandom().nextTriangular(getZ(), range),
getWorld().getRandom().nextGaussian() / 6F,
getWorld().getRandom().nextGaussian() / 6F,
getWorld().getRandom().nextGaussian() / 6F
);
generateBodyParticles();
} else {
for (BlockPos p : BlockPos.iterateOutwards(getBlockPos(), 2, 1, 2)) {
if (getWorld().getBlockState(p).getLuminance() > 3) {
destroyLightSource(p);
}
}
for (int i = 0; i < 13; i++) {
getWorld().addParticle(ParticleTypes.LARGE_SMOKE,
getWorld().getRandom().nextTriangular(getX(), 1),
getWorld().getRandom().nextTriangular(getY(), 1),
getWorld().getRandom().nextTriangular(getZ(), 1),
0,
0,
0
);
for (BlockPos p : BlockPos.iterateRandomly(random, 3, getBlockPos(), 20)) {
CrystalShardsEntity.infestBlock((ServerWorld)getWorld(), p);
}
if (getTarget() == null && getNavigation().isIdle()) {
getNavigation().startMovingTo(homePos.get().getX(), homePos.get().getY() + 10, homePos.get().getZ(), 2);
}
}
getWorld().getOtherEntities(this, this.getBoundingBox().expand(5), EntityPredicates.VALID_LIVING_ENTITY).forEach(target -> {
((LivingEntity)target).addStatusEffect(new StatusEffectInstance(StatusEffects.BLINDNESS, 100, 1));
});
getHomePos().ifPresent(this::generateArenaEffects);
}
protected void applyAreaEffects(Entity target) {
if (this.age % 150 == 0) {
target.playSound(
random.nextInt(30) == 0 ? USounds.ENTITY_SOMBRA_AMBIENT
: random.nextInt(10) == 0 ? USounds.Vanilla.ENTITY_GHAST_AMBIENT
: USounds.Vanilla.AMBIENT_CAVE.value(),
(float)random.nextTriangular(1, 0.2F),
(float)random.nextTriangular(0.3F, 0.2F)
);
}
((LivingEntity)target).addStatusEffect(new StatusEffectInstance(StatusEffects.BLINDNESS, 26, 0, true, false));
((LivingEntity)target).addStatusEffect(new StatusEffectInstance(StatusEffects.DARKNESS, 26, 0, true, false));
if (getTarget() == null && target instanceof PlayerEntity player && player.distanceTo(this) < getAreaRadius() / 2F) {
setTarget(player);
if (teleportTo(target.getPos())) {
setPosition(getPos().add(0, 4, 0));
}
}
}
protected void destroyLightSource(BlockPos pos) {
getWorld().breakBlock(pos, true);
playSound(USounds.ENTITY_SOMBRA_SNICKER, 1, 1);
}
protected void generateBodyParticles() {
for (int i = 0; i < 23; i++) {
getWorld().addParticle(ParticleTypes.LARGE_SMOKE,
random.nextTriangular(getX(), 8),
random.nextTriangular(getY(), 1),
random.nextTriangular(getZ(), 8),
0,
0,
0
);
}
}
private void generateArenaEffects(BlockPos home) {
if (getWorld().isClient()) {
Stream.concat(
new Sphere(false, getAreaRadius()).translate(home).randomPoints(random).filter(this::isSurfaceBlock).limit(80),
new Sphere(true, getAreaRadius()).translate(home).randomPoints(random).filter(this::isSurfaceBlock).limit(30))
.forEach(pos -> {
ParticleEffect type = random.nextInt(3) < 1 ? ParticleTypes.LARGE_SMOKE : ParticleTypes.SOUL_FIRE_FLAME;
ParticleUtils.spawnParticle(getWorld(), type, pos, Vec3d.ZERO);
ParticleUtils.spawnParticle(getWorld(), type, pos, pos.subtract(getPos()).add(0, 0.1, 0).multiply(-0.013));
});
shroud.update(getUuid(), this, spawner -> {
var radius = getAreaRadius();
var center = home.toCenterPos();
spawner.addParticle(new SphereParticleEffect(UParticles.SPHERE, 0xFF000000, 1, radius - 0.2F), center, Vec3d.ZERO);
}).ifPresent(attachment -> {
attachment.setAttribute(Attachment.ATTR_BOUND, 1);
attachment.setAttribute(Attachment.ATTR_COLOR, 0xFF000000);
attachment.setAttribute(Attachment.ATTR_OPACITY, 2.5F);
});
} else {
for (Entity target : VecHelper.findInRange(this, getWorld(), home.toCenterPos(), getAreaRadius() - 0.2F, EFFECT_TARGET_PREDICATE)) {
applyAreaEffects(target);
}
}
}
private boolean isSurfaceBlock(Vec3d pos) {
BlockPos bPos = BlockPos.ofFloored(pos);
return getWorld().isAir(bPos) && !getWorld().isAir(bPos.down());
}
@Override
@ -207,21 +331,6 @@ public class SombraEntity extends HostileEntity {
return distance < d * d;
}
@Override
public boolean isPushable() {
return false;
}
@Override
protected void pushAway(Entity entity) {
}
@Override
protected void tickCramming() {
}
@Override
public boolean handleFallDamage(float distance, float damageMultiplier, DamageSource cause) {
return false;
@ -232,6 +341,7 @@ public class SombraEntity extends HostileEntity {
if (source.getAttacker() instanceof PlayerEntity player) {
if (AmuletSelectors.ALICORN_AMULET.test(player)) {
if (!getWorld().isClient) {
playSound(USounds.ENTITY_SOMBRA_SNICKER, 1, 1);
player.sendMessage(Text.translatable("entity.unicopia.sombra.taunt"));
}
}
@ -249,10 +359,47 @@ public class SombraEntity extends HostileEntity {
return damaged;
}
@Override
public void onDeath(DamageSource damageSource) {
if (!dead) {
stormCloud.ifPresent(getWorld(), cloud -> {
cloud.setStormTicks(0);
cloud.setDissipating(true);
});
stormCloud.set(null);
getHomePos().ifPresent(home -> {
VecHelper.findInRange(this, getWorld(), home.toCenterPos(), getAreaRadius() - 0.2F, e -> e instanceof CrystalShardsEntity).forEach(e -> {
((CrystalShardsEntity)e).setDecaying(true);
});
});
}
super.onDeath(damageSource);
}
@Override
protected void fall(double y, boolean onGroundIn, BlockState state, BlockPos pos) {
}
@Override
public boolean canTarget(LivingEntity target) {
return super.canTarget(target) && getHomePos().filter(home -> target.getPos().isInRange(home.toCenterPos(), getAreaRadius())).isPresent();
}
@Override
public boolean tryAttack(Entity target) {
getWorld().sendEntityStatus(this, BITE);
return super.tryAttack(target);
}
@Override
public void handleStatus(byte status) {
if (status == BITE) {
biteTime = MAX_BITE_TIME;
} else {
super.handleStatus(status);
}
}
protected boolean teleportRandomly(int maxDistance) {
if (getWorld().isClient() || !isAlive()) {
return false;
@ -260,7 +407,8 @@ public class SombraEntity extends HostileEntity {
return teleportTo(getPos().add(VecHelper.supply(() -> random.nextTriangular(0, maxDistance))));
}
private boolean teleportTo(Vec3d destination) {
@Override
public boolean teleportTo(Vec3d destination) {
Vec3d oldPos = getPos();
if (canTeleportTo(destination) && teleport(destination.x, destination.y, destination.z, true)) {
getWorld().emitGameEvent(GameEvent.TELEPORT, oldPos, GameEvent.Emitter.of(this));
@ -309,6 +457,7 @@ public class SombraEntity extends HostileEntity {
getHomePos().map(NbtHelper::fromBlockPos).ifPresent(pos -> {
nbt.put("homePos", pos);
});
nbt.put("cloud", stormCloud.toNBT());
}
@Override
@ -320,5 +469,6 @@ public class SombraEntity extends HostileEntity {
if (hasCustomName()) {
bossBar.setName(getDisplayName());
}
stormCloud.fromNBT(nbt.getCompound("cloud"));
}
}

View file

@ -0,0 +1,317 @@
package com.minelittlepony.unicopia.entity;
import java.util.function.Consumer;
import com.minelittlepony.unicopia.USounds;
import com.minelittlepony.unicopia.particle.UParticles;
import com.minelittlepony.unicopia.server.world.WeatherConditions;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityType;
import net.minecraft.entity.LightningEntity;
import net.minecraft.entity.damage.DamageSource;
import net.minecraft.entity.data.DataTracker;
import net.minecraft.entity.data.TrackedData;
import net.minecraft.entity.data.TrackedDataHandlerRegistry;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.predicate.entity.EntityPredicates;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.util.math.MathHelper;
import net.minecraft.util.math.Vec3d;
import net.minecraft.world.Heightmap.Type;
import net.minecraft.world.World;
public class StormCloudEntity extends Entity {
private static final TrackedData<Integer> CLEAR_TICKS = DataTracker.registerData(StormCloudEntity.class, TrackedDataHandlerRegistry.INTEGER);
private static final TrackedData<Integer> STORM_TICKS = DataTracker.registerData(StormCloudEntity.class, TrackedDataHandlerRegistry.INTEGER);
private static final TrackedData<Float> TARGET_SIZE = DataTracker.registerData(StormCloudEntity.class, TrackedDataHandlerRegistry.FLOAT);
private static final TrackedData<Boolean> DISSIPATING = DataTracker.registerData(StormCloudEntity.class, TrackedDataHandlerRegistry.BOOLEAN);
private float prevSize;
private float currentSize;
public boolean cursed;
private int phase;
private int nextPhase;
public StormCloudEntity(EntityType<StormCloudEntity> type, World world) {
super(type, world);
setSize(1 + random.nextInt(4));
}
@Override
protected void initDataTracker() {
dataTracker.startTracking(STORM_TICKS, 0);
dataTracker.startTracking(CLEAR_TICKS, 0);
dataTracker.startTracking(TARGET_SIZE, 1F);
dataTracker.startTracking(DISSIPATING, false);
}
public boolean isStormy() {
return getStormTicks() != 0;
}
public int getStormTicks() {
return dataTracker.get(STORM_TICKS);
}
public void setStormTicks(int stormTicks) {
dataTracker.set(STORM_TICKS, stormTicks);
setClearTicks(stormTicks);
}
public void setDissipating(boolean dissipating) {
dataTracker.set(DISSIPATING, dissipating);
}
public boolean isDissipating() {
return dataTracker.get(DISSIPATING);
}
private void setClearTicks(int clearTicks) {
dataTracker.set(CLEAR_TICKS, clearTicks);
}
private int getClearTicks() {
return dataTracker.get(CLEAR_TICKS);
}
public boolean tickClear() {
int clearTicks = getClearTicks();
if (clearTicks > 0) {
setClearTicks(--clearTicks);
}
return clearTicks > 0;
}
public float getSize(float tickDelta) {
return MathHelper.clamp(MathHelper.lerp(tickDelta, prevSize, currentSize), 1, 30);
}
public void setSize(float size) {
dataTracker.set(TARGET_SIZE, size);
}
public int getSizeInBlocks() {
return (int)(getWidth() * (getSize(1) / 15F));
}
@Override
public void tick() {
setFireTicks(1);
prevSize = currentSize;
float targetSize = dataTracker.get(TARGET_SIZE);
if (currentSize != targetSize) {
float sizeDifference = (dataTracker.get(TARGET_SIZE) - currentSize);
currentSize = Math.abs(sizeDifference) < 0.01F ? targetSize : currentSize + (sizeDifference * 0.2F);
}
if (isStormy()) {
int stormTicks = getStormTicks();
if (stormTicks > 0) {
setStormTicks(stormTicks - 1);
}
}
if (isLogicalSideForUpdatingMovement()) {
float groundY = getWorld().getTopY(Type.MOTION_BLOCKING_NO_LEAVES, (int)getX(), (int)getZ());
float cloudY = (float)getY() - (isStormy() ? 20 : 90);
addVelocity(0, 0.03F * (groundY - cloudY), 0);
if (!cursed && !isStormy()) {
Vec3d wind = WeatherConditions.get(getWorld()).getWindDirection();
addVelocity(wind.x * 0.001F, 0, wind.z * 0.001F);
}
Vec3d velocity = getVelocity();
setPosition(getPos().add(velocity.multiply(0.7F)));
setVelocity(velocity.multiply(0.9F));
float randomYaw = random.nextFloat() * 360;
Vec3d randomVelocity = Vec3d.fromPolar(0, randomYaw).multiply(0.002);
for (var cloud : getWorld().getEntitiesByClass(StormCloudEntity.class, getBoundingBox(), EntityPredicates.VALID_ENTITY)) {
if (cloud != this) {
cloud.addVelocity(randomVelocity);
}
}
}
super.tick();
float size = getSize(1);
if (getWorld().isClient()) {
if (isStormy()) {
WeatherConditions.get(getWorld()).addStorm(this);
float sizeInBlocks = getSizeInBlocks();
int area = (int)MathHelper.square(sizeInBlocks);
sizeInBlocks /= getWidth();
for (int i = 0; i < area; i++) {
float x = (float)getParticleX(sizeInBlocks);
float z = (float)getParticleZ(sizeInBlocks);
getWorld().addParticle(UParticles.RAIN_DROPS, x, getY(), z, 0, -0.2F, 0);
}
}
} else {
if (!isStormy() && !cursed && size > 10) {
if (random.nextInt(1 + (int)(4000 / size)) == 0) {
split(random.nextBetween(1, 3));
return;
}
}
if (currentSize == targetSize) {
if (isDissipating()) {
if (size < 2) {
kill();
} else {
if (random.nextInt(4) == 0) {
split(2);
} else {
setSize(size - 1);
}
}
} else {
if (size < 30) {
if (cursed) {
setSize(size + 1);
setStormTicks(1);
spawnLightningStrike(getBlockPos(), false, false);
playSound(USounds.AMBIENT_WIND_GUST, 1, 1);
}
}
}
}
if (size >= 30) {
if (cursed) {
setStormTicks(-1);
if (nextPhase-- == 0) {
phase++;
nextPhase = random.nextBetween(17, 120);
if (phase == 1) {
playSound(USounds.ENTITY_SOMBRA_SNICKER, 10, 1);
}
if (++phase >= 7) {
pickRandomPoints(13, pos -> spawnLightningStrike(pos, true, false));
cursed = false;
SombraEntity sombra = UEntities.SOMBRA.create(getWorld());
sombra.setPosition(getPos());
sombra.setHomePos(getWorld().getTopPosition(Type.MOTION_BLOCKING_NO_LEAVES, getBlockPos()));
sombra.stormCloud.set(this);
getWorld().spawnEntity(sombra);
playSound(USounds.ENTITY_SOMBRA_LAUGH, 10, 1);
} else {
pickRandomPoints(3, pos -> spawnLightningStrike(pos, true, false));
}
}
} else {
if (!isStormy() && !tickClear()) {
setStormTicks(random.nextBetween(30, 120));
}
}
}
if (isStormy() && age % 170 == 0) {
pickRandomPoints(3, pos -> spawnLightningStrike(pos, false, random.nextInt(30) == 0));
}
}
}
private void pickRandomPoints(int count, Consumer<BlockPos> action) {
BlockPos.iterateRandomly(random, 3, getBlockPos(), getSizeInBlocks()).forEach(pos -> {
BlockPos.Mutable mutable = new BlockPos.Mutable();
mutable.set(pos);
while (getWorld().isInBuildLimit(mutable) && getWorld().isAir(mutable)) {
mutable.move(Direction.DOWN);
}
while (getWorld().isInBuildLimit(mutable) && !getWorld().isAir(mutable)) {
mutable.move(Direction.UP);
}
mutable.move(Direction.DOWN);
action.accept(pos);
});
}
private void spawnLightningStrike(BlockPos pos, boolean cosmetic, boolean infect) {
if (infect) {
if (!CrystalShardsEntity.infestBlock((ServerWorld)getWorld(), pos)) {
return;
}
}
LightningEntity lightning = EntityType.LIGHTNING_BOLT.create(getWorld());
lightning.refreshPositionAfterTeleport(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5);
lightning.setCosmetic(cosmetic);
getWorld().spawnEntity(lightning);
}
@Override
public void onDamaged(DamageSource damageSource) {
if (random.nextInt(35) == 0) {
split(2 + random.nextInt(4));
}
}
public void split(int splitCount) {
Vec3d center = getPos();
float totalAngle = (360F / splitCount) + ((float)random.nextGaussian() * 360F);
float size = getSize(1) / splitCount;
int stormTicks = getStormTicks() / splitCount;
if (size < 1) {
return;
}
discard();
for (int i = 0; i < splitCount; i++) {
StormCloudEntity lump = (StormCloudEntity)getType().create(getWorld());
lump.setSize(size);
lump.setStormTicks(stormTicks);
lump.setPosition(center);
lump.setDissipating(isDissipating());
lump.setVelocity(Vec3d.fromPolar(0, totalAngle * i).normalize().multiply(1.8F));
getWorld().spawnEntity(lump);
}
}
@Override
public void handleStatus(byte status) {
super.handleStatus(status);
}
@Override
public void writeCustomDataToNbt(NbtCompound nbt) {
nbt.putInt("stormTicks", getStormTicks());
nbt.putInt("clearTicks", getClearTicks());
nbt.putFloat("size", getSize(1));
nbt.putBoolean("cursed", cursed);
}
@Override
public void readCustomDataFromNbt(NbtCompound nbt) {
setStormTicks(nbt.getInt("stormTicks"));
setClearTicks(nbt.getInt("clearTicks"));
setSize(currentSize = nbt.getFloat("size"));
cursed = nbt.getBoolean("cursed");
}
}

View file

@ -48,7 +48,13 @@ public interface UEntities {
.dimensions(EntityDimensions.fixed(0.9F, 0.5F)));
EntityType<SombraEntity> SOMBRA = register("sombra", FabricEntityTypeBuilder.create(SpawnGroup.MONSTER, SombraEntity::new)
.trackRangeBlocks(200)
.dimensions(EntityDimensions.fixed(1F, 2F)));
EntityType<CrystalShardsEntity> CRYSTAL_SHARDS = register("crystal_shards", FabricEntityTypeBuilder.create(SpawnGroup.MISC, CrystalShardsEntity::new)
.trackRangeBlocks(100)
.dimensions(EntityDimensions.fixed(1F, 1F)));
EntityType<StormCloudEntity> STORM_CLOUD = register("storm_cloud", FabricEntityTypeBuilder.create(SpawnGroup.MISC, StormCloudEntity::new)
.trackRangeBlocks(200)
.dimensions(EntityDimensions.fixed(20F, 20F)));
EntityType<AirBalloonEntity> AIR_BALLOON = register("air_balloon", FabricEntityTypeBuilder.create(SpawnGroup.MISC, AirBalloonEntity::new)
.trackRangeBlocks(1000)
.dimensions(EntityDimensions.changing(2.5F, 0.1F)));

View file

@ -0,0 +1,47 @@
package com.minelittlepony.unicopia.entity.ai;
import com.minelittlepony.unicopia.entity.ArenaCombatant;
import net.minecraft.entity.ai.goal.AttackGoal;
import net.minecraft.entity.ai.pathing.Path;
import net.minecraft.entity.mob.MobEntity;
import net.minecraft.util.math.BlockPos;
public class ArenaAttackGoal<E extends MobEntity & ArenaCombatant> extends AttackGoal {
private final E combatant;
public ArenaAttackGoal(E mob) {
super(mob);
this.combatant = mob;
}
@Override
public boolean canStart() {
return combatant.getHomePos().isPresent() && super.canStart();
}
@Override
public boolean shouldContinue() {
return combatant.getHomePos().filter(this::isInArena).isPresent() && super.shouldContinue();
}
private boolean isInArena(BlockPos homePos) {
return combatant.getBlockPos().isWithinDistance(homePos, combatant.getAreaRadius());
}
@Override
public void stop() {
super.stop();
combatant.setTarget(null);
combatant.getHomePos().ifPresent(home -> {
Path path = combatant.getNavigation().findPathTo(home, 2, (int)combatant.getAreaRadius() * 2);
if (path != null) {
combatant.getNavigation().startMovingAlong(path, combatant.getMovementSpeed() * 2F);
} else {
combatant.teleportTo(home.toCenterPos());
}
});
}
}

View file

@ -21,7 +21,6 @@ public interface MeteorlogicalUtil {
return false;
}
final float skyAngle = getSkyAngle(entity.getWorld());
float playerYaw = MathHelper.wrapDegrees(entity.getHeadYaw());
float playerAngle = (-entity.getPitch(1) / 90F) / 2F;

View file

@ -586,6 +586,10 @@ public class Pony extends Living<PlayerEntity> implements Copyable<Pony>, Update
}
sendCapabilities();
//if (!isClient()) {
// CrystalShardsEntity.infestBlock((ServerWorld)asWorld(), entity.getBlockPos().down());
//}
}
@Override

View file

@ -257,6 +257,11 @@ public class AlicornAmuletItem extends AmuletItem implements ItemTracker.Trackab
player.getHungerManager().addExhaustion(90F);
float healthDrop = MathHelper.clamp(player.getMaxHealth() - player.getHealth(), 2, 5);
player.damage(pony.damageOf(UDamageTypes.ALICORN_AMULET), healthDrop);
if (player.getHealth() < 2) {
stack.decrement(1);
SombraEntity.startEncounter(player.getWorld(), player.getBlockPos());
}
}
return;

View file

@ -8,6 +8,7 @@ import java.util.UUID;
import java.util.WeakHashMap;
import java.util.function.Consumer;
import com.minelittlepony.unicopia.EntityConvertable;
import com.minelittlepony.unicopia.ability.magic.Caster;
import net.fabricmc.api.EnvType;
@ -59,8 +60,8 @@ public class ParticleHandle {
Entry p = SPAWNED_PARTICLES.computeIfAbsent(id, i -> new WeakHashMap<>()).computeIfAbsent(partName, i -> {
constructor.accept((effect, pos, vel) -> {
pp = MinecraftClient.getInstance().particleManager.addParticle(effect, pos.x, pos.y, pos.z, vel.x, vel.y, vel.z);
if (pp instanceof Attachment && source instanceof Caster<?>) {
((Attachment) pp).attach(new Link(id, (Caster<?>)source));
if (pp instanceof Attachment) {
((Attachment) pp).attach(new Link(id, source));
}
});
return new Entry(new WeakReference<>(MinecraftClient.getInstance().world), new WeakReference<>(pp));
@ -102,16 +103,16 @@ public class ParticleHandle {
}
public static final class Link {
private Optional<WeakReference<Caster<?>>> caster = Optional.empty();
private Optional<WeakReference<EntityConvertable<?>>> caster = Optional.empty();
private UUID effect;
private Link(UUID effect, Caster<?> caster) {
private Link(UUID effect, EntityConvertable<?> caster) {
this.caster = Optional.of(new WeakReference<>(caster));
this.effect = effect;
}
public Optional<Caster<?>> get() {
caster = caster.filter(r -> r.get() != null && r.get().getSpellSlot().contains(effect) && r.get().asEntity().isAlive());
public Optional<EntityConvertable<?>> get() {
caster = caster.filter(r -> r.get() != null && (!(r.get() instanceof Caster<?> c) || c.getSpellSlot().contains(effect)) && r.get().asEntity().isAlive());
return caster.map(WeakReference::get);
}
}

View file

@ -1,6 +1,12 @@
package com.minelittlepony.unicopia.server.world;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import com.minelittlepony.unicopia.Unicopia;
import com.minelittlepony.unicopia.entity.StormCloudEntity;
import com.minelittlepony.unicopia.entity.player.MeteorlogicalUtil;
import com.minelittlepony.unicopia.util.Tickable;
@ -55,6 +61,8 @@ public class WeatherConditions extends PersistentState implements Tickable {
private boolean prevDayState;
private Map<UUID, Storm> storms = new HashMap<>();
private WeatherConditions(World world, NbtCompound compound) {
this(world);
windYaw = compound.getFloat("windYaw");
@ -68,6 +76,19 @@ public class WeatherConditions extends PersistentState implements Tickable {
this.world = world;
}
public void addStorm(StormCloudEntity cloud) {
synchronized (storms) {
storms.computeIfAbsent(cloud.getUuid(), id -> new Storm(cloud));
}
}
public boolean isInRangeOfStorm(BlockPos pos) {
synchronized (storms) {
storms.values().removeIf(Storm::shouldRemove);
return storms.values().stream().anyMatch(storm -> storm.inRange(pos));
}
}
@Override
public void tick() {
if (interpolation < maxInterpolation) {
@ -107,6 +128,24 @@ public class WeatherConditions extends PersistentState implements Tickable {
return compound;
}
private class Storm {
private final WeakReference<StormCloudEntity> cloud;
public Storm(StormCloudEntity cloud) {
this.cloud = new WeakReference<>(cloud);
}
public boolean inRange(BlockPos pos) {
final StormCloudEntity cloud = this.cloud.get();
return cloud != null && cloud.getBlockPos().isWithinDistance(pos, cloud.getSizeInBlocks());
}
public boolean shouldRemove() {
final StormCloudEntity cloud = this.cloud.get();
return cloud == null || cloud.isRemoved() || !cloud.isStormy();
}
}
public static Vec3d getAirflow(BlockPos pos, World world) {
BlockPos.Mutable probedPosition = new BlockPos.Mutable();

View file

@ -228,6 +228,24 @@
],
"subtitle": "unicopia.subtitle.entity.twittermite.hurt"
},
"entity.sombra.ambient": {
"subtitle": "unicopia.subtitle.sombra.ambient",
"sounds": [
"unicopia:entity/sombra/sombra_idle"
]
},
"entity.sombra.laugh": {
"subtitle": "unicopia.subtitle.sombra.laugh",
"sounds": [
"unicopia:entity/sombra/sombra_laugh"
]
},
"entity.sombra.snicker": {
"subtitle": "unicopia.subtitle.sombra.snicker",
"sounds": [
"unicopia:entity/sombra/sombra_snicker"
]
},
"entity.jar.throw": {
"sounds": [
"random/bow"

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 996 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
tom.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B