Commit 1a9ba655 by Jonathan Thomas

New animated lead particle (arrows pointing where they are going). New animated…

New animated lead particle (arrows pointing where they are going). New animated attack particles with random # of particles.
parent b57fd513
Pipeline #12875 failed with stages
in 16 seconds
...@@ -10,6 +10,8 @@ All notable changes to **CreatureChat** are documented in this file. The format ...@@ -10,6 +10,8 @@ All notable changes to **CreatureChat** are documented in this file. The format
- New friendship particles (hearts + fire) to indicate when friendship changes - New friendship particles (hearts + fire) to indicate when friendship changes
- Added sound effects for max friendship and max enemy - Added sound effects for max friendship and max enemy
- New follow, flee, attack, and protect particles & sound effects - New follow, flee, attack, and protect particles & sound effects
- New animated lead particle (arrows pointing where they are going)
- New animated attack particles (with random # of particles)
### Changed ### Changed
- Entity chat data now separates messages and friendship by player - Entity chat data now separates messages and friendship by player
......
...@@ -3,6 +3,7 @@ package com.owlmaddie; ...@@ -3,6 +3,7 @@ package com.owlmaddie;
import com.owlmaddie.chat.ChatDataManager; import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.network.ClientPackets; import com.owlmaddie.network.ClientPackets;
import com.owlmaddie.particle.CreatureParticleFactory; import com.owlmaddie.particle.CreatureParticleFactory;
import com.owlmaddie.particle.LeadParticleFactory;
import com.owlmaddie.ui.BubbleRenderer; import com.owlmaddie.ui.BubbleRenderer;
import com.owlmaddie.ui.ClickHandler; import com.owlmaddie.ui.ClickHandler;
import com.owlmaddie.ui.PlayerMessageManager; import com.owlmaddie.ui.PlayerMessageManager;
...@@ -33,6 +34,7 @@ public class ClientInit implements ClientModInitializer { ...@@ -33,6 +34,7 @@ public class ClientInit implements ClientModInitializer {
ParticleFactoryRegistry.getInstance().register(FOLLOW_ENEMY_PARTICLE, CreatureParticleFactory::new); ParticleFactoryRegistry.getInstance().register(FOLLOW_ENEMY_PARTICLE, CreatureParticleFactory::new);
ParticleFactoryRegistry.getInstance().register(FOLLOW_FRIEND_PARTICLE, CreatureParticleFactory::new); ParticleFactoryRegistry.getInstance().register(FOLLOW_FRIEND_PARTICLE, CreatureParticleFactory::new);
ParticleFactoryRegistry.getInstance().register(PROTECT_PARTICLE, CreatureParticleFactory::new); ParticleFactoryRegistry.getInstance().register(PROTECT_PARTICLE, CreatureParticleFactory::new);
ParticleFactoryRegistry.getInstance().register(LEAD_PARTICLE, LeadParticleFactory::new);
ClientTickEvents.END_CLIENT_TICK.register(client -> { ClientTickEvents.END_CLIENT_TICK.register(client -> {
tickCounter++; tickCounter++;
......
package com.owlmaddie.particle;
import net.minecraft.client.particle.ParticleTextureSheet;
import net.minecraft.client.particle.SpriteBillboardParticle;
import net.minecraft.client.particle.SpriteProvider;
import net.minecraft.client.render.Camera;
import net.minecraft.client.render.VertexConsumer;
import net.minecraft.client.world.ClientWorld;
import net.minecraft.util.math.MathHelper;
import org.joml.Vector3f;
import net.minecraft.util.math.Vec3d;
/**
* The {@code LeadParticle} class renders a static LEAD behavior particle (i.e. animated arrow pointing in the direction of lead). It
* uses a SpriteProvider for animation.
*/
public class LeadParticle extends SpriteBillboardParticle {
private final SpriteProvider spriteProvider;
public LeadParticle(ClientWorld world, double x, double y, double z, double velocityX, double velocityY, double velocityZ, SpriteProvider spriteProvider, double angle) {
super(world, x, y, z, velocityX, velocityY, velocityZ);
this.velocityX = 0f;
this.velocityY = 0f;
this.velocityZ = 0f;
this.spriteProvider = spriteProvider;
this.angle = (float) angle;
this.scale(6F);
this.setMaxAge(40);
this.setSpriteForAge(spriteProvider);
}
@Override
public void tick() {
super.tick();
this.setSpriteForAge(spriteProvider);
}
@Override
public int getBrightness(float tint) {
return 0xF000F0;
}
@Override
public Rotator getRotator() {
// Return null to avoid camera billboarding (fixed rotation based on angle)
return null;
}
@Override
public ParticleTextureSheet getType() {
return ParticleTextureSheet.PARTICLE_SHEET_TRANSLUCENT;
}
@Override
public void buildGeometry(VertexConsumer vertexConsumer, Camera camera, float tickDelta) {
// Get the current position of the particle relative to the camera
Vec3d cameraPos = camera.getPos();
float particleX = (float)(MathHelper.lerp((double)tickDelta, this.prevPosX, this.x) - cameraPos.getX());
float particleY = (float)(MathHelper.lerp((double)tickDelta, this.prevPosY, this.y) - cameraPos.getY());
float particleZ = (float)(MathHelper.lerp((double)tickDelta, this.prevPosZ, this.z) - cameraPos.getZ());
// Define the four vertices of the particle (keeping it flat on the XY plane)
Vector3f[] vertices = new Vector3f[]{
new Vector3f(-1.0F, 0.0F, -1.0F), // Bottom-left
new Vector3f(-1.0F, 0.0F, 1.0F), // Top-left
new Vector3f(1.0F, 0.0F, 1.0F), // Top-right
new Vector3f(1.0F, 0.0F, -1.0F) // Bottom-right
};
// Apply scaling and rotation using the particle's angle (in world space)
float size = this.getSize(tickDelta); // Get the size of the particle at the current tick
for (Vector3f vertex : vertices) {
vertex.mul(size); // Scale the vertices
vertex.rotateY(angle);
vertex.add(particleX, particleY, particleZ); // Translate to particle position
}
// Get the UV coordinates from the sprite (used for texture mapping)
float minU = this.getMinU();
float maxU = this.getMaxU();
float minV = this.getMinV();
float maxV = this.getMaxV();
int light = this.getBrightness(tickDelta);
// Render each vertex of the particle (flat on the XY plane)
vertexConsumer.vertex(vertices[0].x(), vertices[0].y(), vertices[0].z()).texture(maxU, maxV).color(this.red, this.green, this.blue, this.alpha).light(light).next();
vertexConsumer.vertex(vertices[1].x(), vertices[1].y(), vertices[1].z()).texture(maxU, minV).color(this.red, this.green, this.blue, this.alpha).light(light).next();
vertexConsumer.vertex(vertices[2].x(), vertices[2].y(), vertices[2].z()).texture(minU, minV).color(this.red, this.green, this.blue, this.alpha).light(light).next();
vertexConsumer.vertex(vertices[3].x(), vertices[3].y(), vertices[3].z()).texture(minU, maxV).color(this.red, this.green, this.blue, this.alpha).light(light).next();
}
}
\ No newline at end of file
package com.owlmaddie.particle;
import net.minecraft.client.particle.ParticleFactory;
import net.minecraft.client.particle.SpriteProvider;
import net.minecraft.client.world.ClientWorld;
/**
* The {@code LeadParticleFactory} class generates new arrow particles for LEAD behavior. It passes along the 'angle' to rotate the particle. It also
* sets the motion/acceleration to 0.
*/
public class LeadParticleFactory implements ParticleFactory<LeadParticleEffect> {
private final SpriteProvider spriteProvider;
public LeadParticleFactory(SpriteProvider spriteProvider) {
this.spriteProvider = spriteProvider;
}
@Override
public LeadParticle createParticle(LeadParticleEffect effect, ClientWorld world, double x, double y, double z, double velocityX, double velocityY, double velocityZ) {
double angle = effect.getAngle();
return new LeadParticle(world, x, y, z, 0, 0, 0, this.spriteProvider, angle);
}
}
...@@ -3,6 +3,7 @@ package com.owlmaddie.goals; ...@@ -3,6 +3,7 @@ package com.owlmaddie.goals;
import net.minecraft.entity.LivingEntity; import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.ai.RangedAttackMob; import net.minecraft.entity.ai.RangedAttackMob;
import net.minecraft.entity.mob.Angerable; import net.minecraft.entity.mob.Angerable;
import java.util.concurrent.ThreadLocalRandom;
import net.minecraft.entity.mob.HostileEntity; import net.minecraft.entity.mob.HostileEntity;
import net.minecraft.entity.mob.MobEntity; import net.minecraft.entity.mob.MobEntity;
import net.minecraft.entity.passive.GolemEntity; import net.minecraft.entity.passive.GolemEntity;
...@@ -95,12 +96,10 @@ public class AttackPlayerGoal extends PlayerBaseGoal { ...@@ -95,12 +96,10 @@ public class AttackPlayerGoal extends PlayerBaseGoal {
this.attackerEntity.playSound(SoundEvents.ENTITY_PLAYER_HURT, 1F, 1F); this.attackerEntity.playSound(SoundEvents.ENTITY_PLAYER_HURT, 1F, 1F);
// Spawn red particles to simulate 'injury' // Spawn red particles to simulate 'injury'
int numParticles = ThreadLocalRandom.current().nextInt(2, 7); // Random number between 2 (inclusive) and 7 (exclusive)
((ServerWorld) this.attackerEntity.getWorld()).spawnParticles(ATTACK_PARTICLE, ((ServerWorld) this.attackerEntity.getWorld()).spawnParticles(ATTACK_PARTICLE,
this.targetEntity.getX(), this.targetEntity.getX(), this.targetEntity.getBodyY(0.5D), this.targetEntity.getZ(),
this.targetEntity.getBodyY(0.5D), numParticles, 0.5, 0.5, 0.1, 0.4);
this.targetEntity.getZ(),
4, // number of particles
0.1, 0.1, 0.1, 0.2); // speed and randomness
} }
@Override @Override
......
...@@ -4,12 +4,11 @@ import com.owlmaddie.chat.ChatDataManager; ...@@ -4,12 +4,11 @@ import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.chat.EntityChatData; import com.owlmaddie.chat.EntityChatData;
import com.owlmaddie.controls.LookControls; import com.owlmaddie.controls.LookControls;
import com.owlmaddie.network.ServerPackets; import com.owlmaddie.network.ServerPackets;
import com.owlmaddie.particle.LeadParticleEffect;
import com.owlmaddie.utils.RandomTargetFinder; import com.owlmaddie.utils.RandomTargetFinder;
import net.minecraft.entity.ai.pathing.Path; import net.minecraft.entity.ai.pathing.Path;
import net.minecraft.entity.mob.MobEntity; import net.minecraft.entity.mob.MobEntity;
import net.minecraft.entity.mob.PathAwareEntity; import net.minecraft.entity.mob.PathAwareEntity;
import net.minecraft.particle.ParticleEffect;
import net.minecraft.particle.ParticleTypes;
import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld; import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.math.MathHelper; import net.minecraft.util.math.MathHelper;
...@@ -131,27 +130,50 @@ public class LeadPlayerGoal extends PlayerBaseGoal { ...@@ -131,27 +130,50 @@ public class LeadPlayerGoal extends PlayerBaseGoal {
LOGGER.info("Waypoint " + currentWaypoint + " / " + this.totalWaypoints); LOGGER.info("Waypoint " + currentWaypoint + " / " + this.totalWaypoints);
this.currentTarget = RandomTargetFinder.findRandomTarget(this.entity, 30, 24, 36); this.currentTarget = RandomTargetFinder.findRandomTarget(this.entity, 30, 24, 36);
if (this.currentTarget != null) { if (this.currentTarget != null) {
emitParticleAt(this.currentTarget, ParticleTypes.FLAME); emitParticlesAlongRaycast(this.entity.getPos(), this.currentTarget);
emitParticlesAlongRaycast(this.entity.getPos(), this.currentTarget, ParticleTypes.CLOUD, 0.5);
} }
// Stop following current path (if any) // Stop following current path (if any)
this.entity.getNavigation().stop(); this.entity.getNavigation().stop();
} }
private void emitParticleAt(Vec3d position, ParticleEffect particleType) { private void emitParticleAt(Vec3d position, double angle) {
if (this.entity.getWorld() instanceof ServerWorld) { if (this.entity.getWorld() instanceof ServerWorld) {
ServerWorld serverWorld = (ServerWorld) this.entity.getWorld(); ServerWorld serverWorld = (ServerWorld) this.entity.getWorld();
serverWorld.spawnParticles(particleType, position.x, position.y, position.z, 5, 0, 0, 0, 0);
// Pass the angle using the "speed" argument, with deltaX, deltaY, deltaZ set to 0
LeadParticleEffect effect = new LeadParticleEffect(angle);
serverWorld.spawnParticles(effect, position.x, position.y + 0.3, position.z, 1, 0, 0, 0, 0);
}
} }
private void emitParticlesAlongRaycast(Vec3d start, Vec3d end) {
// Calculate the direction vector from the entity (start) to the target (end)
Vec3d direction = end.subtract(start);
// Calculate the angle in the XZ-plane using atan2 (this is in radians)
double angleRadians = Math.atan2(direction.z, direction.x);
// Convert from radians to degrees
double angleDegrees = Math.toDegrees(angleRadians);
// Convert the calculated angle to Minecraft's yaw system:
// Shift by 90 degrees, and invert to match Minecraft's clockwise yaw system
double minecraftYaw = (360 - (angleDegrees + 90)) % 360;
// Correct the 180-degree flip
minecraftYaw = (minecraftYaw + 180) % 360;
// Ensure the yaw is positive
if (minecraftYaw < 0) {
minecraftYaw += 360;
} }
private void emitParticlesAlongRaycast(Vec3d start, Vec3d end, ParticleEffect particleType, double step) { // Emit particles along the ray using the corrected angle in radians
Vec3d direction = end.subtract(start).normalize();
double distance = start.distanceTo(end); double distance = start.distanceTo(end);
for (double d = 0; d <= distance; d += step) { for (double d = 0; d <= distance; d += 4) {
Vec3d pos = start.add(direction.multiply(d)); Vec3d pos = start.add(direction.normalize().multiply(d));
emitParticleAt(pos, particleType); emitParticleAt(pos, Math.toRadians(minecraftYaw)); // Convert back to radians for rendering
} }
} }
} }
\ No newline at end of file
...@@ -8,6 +8,7 @@ import com.owlmaddie.commands.ConfigurationHandler; ...@@ -8,6 +8,7 @@ import com.owlmaddie.commands.ConfigurationHandler;
import com.owlmaddie.goals.EntityBehaviorManager; import com.owlmaddie.goals.EntityBehaviorManager;
import com.owlmaddie.goals.GoalPriority; import com.owlmaddie.goals.GoalPriority;
import com.owlmaddie.goals.TalkPlayerGoal; import com.owlmaddie.goals.TalkPlayerGoal;
import com.owlmaddie.particle.LeadParticleEffect;
import com.owlmaddie.utils.Compression; import com.owlmaddie.utils.Compression;
import com.owlmaddie.utils.Randomizer; import com.owlmaddie.utils.Randomizer;
import com.owlmaddie.utils.ServerEntityFinder; import com.owlmaddie.utils.ServerEntityFinder;
...@@ -22,6 +23,7 @@ import net.minecraft.entity.mob.MobEntity; ...@@ -22,6 +23,7 @@ import net.minecraft.entity.mob.MobEntity;
import net.minecraft.entity.player.PlayerEntity; import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.network.PacketByteBuf; import net.minecraft.network.PacketByteBuf;
import net.minecraft.particle.DefaultParticleType; import net.minecraft.particle.DefaultParticleType;
import net.minecraft.particle.ParticleType;
import net.minecraft.registry.Registries; import net.minecraft.registry.Registries;
import net.minecraft.registry.Registry; import net.minecraft.registry.Registry;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
...@@ -67,6 +69,7 @@ public class ServerPackets { ...@@ -67,6 +69,7 @@ public class ServerPackets {
public static final DefaultParticleType FOLLOW_FRIEND_PARTICLE = FabricParticleTypes.simple(); public static final DefaultParticleType FOLLOW_FRIEND_PARTICLE = FabricParticleTypes.simple();
public static final DefaultParticleType FOLLOW_ENEMY_PARTICLE = FabricParticleTypes.simple(); public static final DefaultParticleType FOLLOW_ENEMY_PARTICLE = FabricParticleTypes.simple();
public static final DefaultParticleType PROTECT_PARTICLE = FabricParticleTypes.simple(); public static final DefaultParticleType PROTECT_PARTICLE = FabricParticleTypes.simple();
public static final ParticleType<LeadParticleEffect> LEAD_PARTICLE = FabricParticleTypes.complex(LeadParticleEffect.DESERIALIZER);
public static void register() { public static void register() {
// Register custom particles // Register custom particles
...@@ -79,6 +82,7 @@ public class ServerPackets { ...@@ -79,6 +82,7 @@ public class ServerPackets {
Registry.register(Registries.PARTICLE_TYPE, new Identifier("creaturechat", "follow_enemy"), FOLLOW_ENEMY_PARTICLE); Registry.register(Registries.PARTICLE_TYPE, new Identifier("creaturechat", "follow_enemy"), FOLLOW_ENEMY_PARTICLE);
Registry.register(Registries.PARTICLE_TYPE, new Identifier("creaturechat", "follow_friend"), FOLLOW_FRIEND_PARTICLE); Registry.register(Registries.PARTICLE_TYPE, new Identifier("creaturechat", "follow_friend"), FOLLOW_FRIEND_PARTICLE);
Registry.register(Registries.PARTICLE_TYPE, new Identifier("creaturechat", "protect"), PROTECT_PARTICLE); Registry.register(Registries.PARTICLE_TYPE, new Identifier("creaturechat", "protect"), PROTECT_PARTICLE);
Registry.register(Registries.PARTICLE_TYPE, new Identifier("creaturechat", "lead"), LEAD_PARTICLE);
// Handle packet for Greeting // Handle packet for Greeting
ServerPlayNetworking.registerGlobalReceiver(PACKET_C2S_GREETING, (server, player, handler, buf, responseSender) -> { ServerPlayNetworking.registerGlobalReceiver(PACKET_C2S_GREETING, (server, player, handler, buf, responseSender) -> {
......
package com.owlmaddie.particle;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.minecraft.network.PacketByteBuf;
import net.minecraft.particle.ParticleEffect;
import net.minecraft.particle.ParticleType;
import static com.owlmaddie.network.ServerPackets.LEAD_PARTICLE;
/**
* The {@code LeadParticleEffect} class allows for an 'angle' to be passed along with the Particle, to rotate it in the direction of LEAD behavior.
*/
public class LeadParticleEffect implements ParticleEffect {
public static final ParticleEffect.Factory<LeadParticleEffect> DESERIALIZER = new Factory<>() {
@Override
public LeadParticleEffect read(ParticleType<LeadParticleEffect> particleType, PacketByteBuf buf) {
// Read the angle (or any other data) from the packet
double angle = buf.readDouble();
return new LeadParticleEffect(angle);
}
@Override
public LeadParticleEffect read(ParticleType<LeadParticleEffect> particleType, StringReader reader) throws CommandSyntaxException {
// Read the angle from a string
double angle = reader.readDouble();
return new LeadParticleEffect(angle);
}
};
private final double angle;
public LeadParticleEffect(double angle) {
this.angle = angle;
}
@Override
public ParticleType<?> getType() {
return LEAD_PARTICLE;
}
public double getAngle() {
return angle;
}
@Override
public void write(PacketByteBuf buf) {
// Write the angle to the packet
buf.writeDouble(angle);
}
@Override
public String asString() {
return Double.toString(angle);
}
}
{ {
"textures": [ "textures": [
"creaturechat:attack" "creaturechat:attack",
"creaturechat:attack1",
"creaturechat:attack2",
"creaturechat:attack3"
] ]
} }
\ No newline at end of file
{
"textures": [
"creaturechat:lead",
"creaturechat:lead1",
"creaturechat:lead2",
"creaturechat:lead3",
"creaturechat:lead4"
]
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment