diff --git a/src/main/java/fr/hugman/mubble/client/render/MubbleRenderLayers.java b/src/main/java/fr/hugman/mubble/client/render/MubbleRenderLayers.java index 3934d5cd..e0944072 100644 --- a/src/main/java/fr/hugman/mubble/client/render/MubbleRenderLayers.java +++ b/src/main/java/fr/hugman/mubble/client/render/MubbleRenderLayers.java @@ -2,14 +2,17 @@ import fr.hugman.mubble.Mubble; import fr.hugman.mubble.client.render.entity.model.GoombaModel; +import fr.hugman.mubble.client.render.entity.model.KoopaShellModel; import net.fabricmc.fabric.api.client.rendering.v1.EntityModelLayerRegistry; import net.minecraft.client.render.entity.model.EntityModelLayer; public class MubbleRenderLayers { public static final EntityModelLayer GOOMBA = createModelLayer("goomba"); + public static final EntityModelLayer KOOPA_SHELL = createModelLayer("koopa_shell"); public static void registerLayers() { EntityModelLayerRegistry.registerModelLayer(GOOMBA, GoombaModel::getTexturedModelData); + EntityModelLayerRegistry.registerModelLayer(KOOPA_SHELL, KoopaShellModel::getTexturedModelData); } private static EntityModelLayer createModelLayer(String name) { diff --git a/src/main/java/fr/hugman/mubble/client/render/MubbleRenderers.java b/src/main/java/fr/hugman/mubble/client/render/MubbleRenderers.java index dcb1f115..8fc89b19 100644 --- a/src/main/java/fr/hugman/mubble/client/render/MubbleRenderers.java +++ b/src/main/java/fr/hugman/mubble/client/render/MubbleRenderers.java @@ -2,6 +2,7 @@ import fr.hugman.mubble.block.MubbleBlockEntityTypes; import fr.hugman.mubble.client.render.entity.GoombaRenderer; +import fr.hugman.mubble.client.render.entity.KoopaShellRenderer; import fr.hugman.mubble.entity.MubbleEntityTypes; import net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry; import net.minecraft.client.render.block.entity.BlockEntityRendererFactories; @@ -9,6 +10,8 @@ public class MubbleRenderers { public static void registerEntities() { EntityRendererRegistry.register(MubbleEntityTypes.GOOMBA, GoombaRenderer::new); + EntityRendererRegistry.register(MubbleEntityTypes.GREEN_KOOPA_SHELL, KoopaShellRenderer::new); + EntityRendererRegistry.register(MubbleEntityTypes.RED_KOOPA_SHELL, KoopaShellRenderer::new); } public static void registerBlockEntities() { diff --git a/src/main/java/fr/hugman/mubble/client/render/entity/KoopaShellRenderer.java b/src/main/java/fr/hugman/mubble/client/render/entity/KoopaShellRenderer.java new file mode 100644 index 00000000..ed8c8ca4 --- /dev/null +++ b/src/main/java/fr/hugman/mubble/client/render/entity/KoopaShellRenderer.java @@ -0,0 +1,76 @@ +package fr.hugman.mubble.client.render.entity; + +import fr.hugman.mubble.client.render.MubbleRenderLayers; +import fr.hugman.mubble.client.render.entity.model.KoopaShellModel; +import fr.hugman.mubble.client.render.entity.state.KoopaShellEntityRenderState; +import fr.hugman.mubble.entity.KoopaShellEntity; +import net.minecraft.client.MinecraftClient; +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.EntityRenderer; +import net.minecraft.client.render.entity.EntityRendererFactory; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.util.math.MathHelper; +import org.jetbrains.annotations.Nullable; + +public class KoopaShellRenderer extends EntityRenderer { + protected KoopaShellModel model; + + public KoopaShellRenderer(EntityRendererFactory.Context context) { + super(context); + this.model = new KoopaShellModel(context.getPart(MubbleRenderLayers.KOOPA_SHELL)); + this.shadowRadius = 0.5f; + } + + @Override + public KoopaShellEntityRenderState createRenderState() { + return new KoopaShellEntityRenderState(); + } + + @Override + public void render(KoopaShellEntityRenderState state, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light) { + matrices.push(); + + matrices.scale(-1.0F, -1.0F, 1.0F); + matrices.translate(0.0F, -1.501F, 0.0F); + this.model.setAngles(state); + + boolean showBody = !state.invisible; + boolean translucent = !showBody && !state.invisibleToPlayer; + RenderLayer renderLayer = this.getRenderLayer(state, showBody, translucent, state.hasOutline); + if (renderLayer != null) { + VertexConsumer vertexConsumer = vertexConsumers.getBuffer(renderLayer); + int q = OverlayTexture.packUv(OverlayTexture.getU(0.0f), OverlayTexture.getV(false)); + this.model.render(matrices, vertexConsumer, light, q, translucent ? 654311423 : -1); + } + + matrices.pop(); + super.render(state, matrices, vertexConsumers, light); + } + + @Nullable + protected RenderLayer getRenderLayer(KoopaShellEntityRenderState state, boolean showBody, boolean translucent, boolean showOutline) { + if (translucent) { + return RenderLayer.getItemEntityTranslucentCull(state.texture); + } else if (showBody) { + return this.model.getLayer(state.texture); + } else { + return showOutline ? RenderLayer.getOutline(state.texture) : null; + } + } + + @Override + public void updateRenderState(K entity, KoopaShellEntityRenderState state, float tickDelta) { + super.updateRenderState(entity, state, tickDelta); + + state.texture = entity.getTexture(); + + MinecraftClient minecraftClient = MinecraftClient.getInstance(); + state.invisibleToPlayer = state.invisible && entity.isInvisibleTo(minecraftClient.player); + state.hasOutline = minecraftClient.hasOutline(entity); + + state.horizontalRotation = MathHelper.lerp(tickDelta, entity.getPreviousHorizontalRotation(), entity.getHorizontalRotation()); + } +} diff --git a/src/main/java/fr/hugman/mubble/client/render/entity/model/KoopaShellModel.java b/src/main/java/fr/hugman/mubble/client/render/entity/model/KoopaShellModel.java new file mode 100644 index 00000000..e48ab6b6 --- /dev/null +++ b/src/main/java/fr/hugman/mubble/client/render/entity/model/KoopaShellModel.java @@ -0,0 +1,32 @@ +package fr.hugman.mubble.client.render.entity.model; + +import fr.hugman.mubble.client.render.entity.state.KoopaShellEntityRenderState; +import net.minecraft.client.model.*; +import net.minecraft.client.render.entity.model.EntityModel; +import net.minecraft.client.render.entity.model.EntityModelPartNames; + +public class KoopaShellModel extends EntityModel { + public KoopaShellModel(ModelPart part) { + super(part.getChild(EntityModelPartNames.CUBE)); + } + + public static TexturedModelData getTexturedModelData() { + ModelData modelData = new ModelData(); + ModelPartData modelPartData = modelData.getRoot(); + modelPartData.addChild(EntityModelPartNames.CUBE, + ModelPartBuilder.create() + .uv(0, 0) + .cuboid(-5.0F, -3.25F, -5.0F, 10.0F, 7.0F, 10.0F) + .uv(0, 17) + .cuboid(-6.0F, -1.25F, -6.0F, 12.0F, 2.0F, 12.0F), + ModelTransform.pivot(0.0F, 20.25F, 0.0F) + ); + return TexturedModelData.of(modelData, 64, 32); + } + + @Override + public void setAngles(KoopaShellEntityRenderState state) { + super.setAngles(state); + this.root.yaw = state.horizontalRotation; + } +} \ No newline at end of file diff --git a/src/main/java/fr/hugman/mubble/client/render/entity/state/KoopaShellEntityRenderState.java b/src/main/java/fr/hugman/mubble/client/render/entity/state/KoopaShellEntityRenderState.java new file mode 100644 index 00000000..47538cc2 --- /dev/null +++ b/src/main/java/fr/hugman/mubble/client/render/entity/state/KoopaShellEntityRenderState.java @@ -0,0 +1,17 @@ +package fr.hugman.mubble.client.render.entity.state; + +import fr.hugman.mubble.Mubble; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.render.entity.state.EntityRenderState; +import net.minecraft.util.Identifier; + +@Environment(EnvType.CLIENT) +public class KoopaShellEntityRenderState extends EntityRenderState { + private static final Identifier DEFAULT_TEXTURE = Mubble.id("textures/entity/koopa_shell/green.png"); + + public Identifier texture = DEFAULT_TEXTURE; + public boolean invisibleToPlayer; + public boolean hasOutline; + public float horizontalRotation = 0.0f; +} diff --git a/src/main/java/fr/hugman/mubble/client/sound/MovingKoopaShellSoundInstance.java b/src/main/java/fr/hugman/mubble/client/sound/MovingKoopaShellSoundInstance.java new file mode 100644 index 00000000..7f590eb6 --- /dev/null +++ b/src/main/java/fr/hugman/mubble/client/sound/MovingKoopaShellSoundInstance.java @@ -0,0 +1,54 @@ +package fr.hugman.mubble.client.sound; + +import fr.hugman.mubble.entity.KoopaShellEntity; +import fr.hugman.mubble.sound.MubbleSounds; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.sound.MovingSoundInstance; +import net.minecraft.client.sound.SoundInstance; +import net.minecraft.sound.SoundCategory; +import net.minecraft.util.math.MathHelper; + +@Environment(EnvType.CLIENT) +public class MovingKoopaShellSoundInstance extends MovingSoundInstance { + private final KoopaShellEntity shell; + private float distance = 0.0F; + + public MovingKoopaShellSoundInstance(KoopaShellEntity shell) { + super(MubbleSounds.KOOPA_SHELL_SLIDE, SoundCategory.NEUTRAL, SoundInstance.createRandom()); + this.shell = shell; + this.repeat = true; + this.repeatDelay = 0; + this.volume = 0.3F; + } + + + @Override + public boolean canPlay() { + return !this.shell.isSilent(); + } + + @Override + public boolean shouldAlwaysPlay() { + return true; + } + + @Override + public void tick() { + if (this.shell.isRemoved()) { + this.setDone(); + } else { + this.x = ((float) this.shell.getX()); + this.y = ((float) this.shell.getY()); + this.z = ((float) this.shell.getZ()); + float f = (float) this.shell.getVelocity().horizontalLength(); + if (f >= 0.01F && this.shell.getWorld().getTickManager().shouldTick()) { + this.distance = MathHelper.clamp(this.distance + 0.0025F, 0.0F, 1.0F); + this.volume = MathHelper.lerp(MathHelper.clamp(f, 0.0F, 0.5F), 0.0F, 0.7F); + } else { + this.distance = 0.0F; + this.volume = 0.0F; + } + } + } +} diff --git a/src/main/java/fr/hugman/mubble/entity/GoombaEntity.java b/src/main/java/fr/hugman/mubble/entity/GoombaEntity.java index b39043f1..dc09a95f 100644 --- a/src/main/java/fr/hugman/mubble/entity/GoombaEntity.java +++ b/src/main/java/fr/hugman/mubble/entity/GoombaEntity.java @@ -4,10 +4,12 @@ import com.mojang.serialization.MapCodec; import fr.hugman.mubble.entity.ai.control.StunnableMoveControl; import fr.hugman.mubble.entity.ai.goal.SurprisedActiveTargetGoal; +import fr.hugman.mubble.entity.damage.MubbleDamageTypeTags; import fr.hugman.mubble.entity.data.MubbleTrackedData; import fr.hugman.mubble.registry.MubbleRegistryKeys; import fr.hugman.mubble.sound.MubbleSounds; import net.minecraft.block.BlockState; +import net.minecraft.command.argument.EntityAnchorArgumentType; import net.minecraft.entity.AnimationState; import net.minecraft.entity.EntityType; import net.minecraft.entity.VariantHolder; @@ -23,13 +25,14 @@ import net.minecraft.nbt.NbtCompound; import net.minecraft.nbt.NbtOps; import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.server.world.ServerWorld; import net.minecraft.sound.SoundEvent; import net.minecraft.text.Text; import net.minecraft.util.Identifier; import net.minecraft.util.math.BlockPos; import net.minecraft.world.World; -public class GoombaEntity extends StompableHostileEntity implements Surprisable, Stunnable, VariantHolder> { +public class GoombaEntity extends SuperMarioEnemyEntity implements Surprisable, Stunnable, VariantHolder> { public static final String VARIANT_KEY = "variant"; public static final MapCodec> VARIANT_MAP_CODEC = GoombaVariant.ENTRY_CODEC.fieldOf(VARIANT_KEY); @@ -100,13 +103,23 @@ public boolean isStunned() { } @Override - public void onDeath(DamageSource damageSource) { - super.onDeath(damageSource); - if (this.isStomped()) { - this.crushAnimationState.startIfNotRunning(this.age); + public void onSurprised() { + this.playSound(MubbleSounds.GOOMBA_FIND_TARGET, 1.0F, 1.0F); + if(null != this.getTarget()) { + this.lookAt(EntityAnchorArgumentType.EntityAnchor.EYES, this.getTarget().getPos()); } } + @Override + public AnimationState getStompDeathAnimationState() { + return this.crushAnimationState; + } + + @Override + public boolean damage(ServerWorld world, DamageSource source, float amount) { + return super.damage(world, source, source.isIn(MubbleDamageTypeTags.INSTANT_KILLS_GOOMBAS) ? Float.MAX_VALUE : amount); + } + // SOUNDS @Override @@ -147,12 +160,6 @@ public void onTrackedDataSet(TrackedData data) { this.surprisedAnimationState.start(this.age); } } - if (STOMPED.equals(data)) { - if (this.isStomped() && this.dead) { - this.crushAnimationState.startIfNotRunning(this.age); - } - } - super.onTrackedDataSet(data); } diff --git a/src/main/java/fr/hugman/mubble/entity/GreenKoopaShellEntity.java b/src/main/java/fr/hugman/mubble/entity/GreenKoopaShellEntity.java new file mode 100644 index 00000000..3a54a1f3 --- /dev/null +++ b/src/main/java/fr/hugman/mubble/entity/GreenKoopaShellEntity.java @@ -0,0 +1,35 @@ +package fr.hugman.mubble.entity; + +import fr.hugman.mubble.Mubble; +import fr.hugman.mubble.item.MubbleItems; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.LivingEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; +import net.minecraft.world.World; + +public class GreenKoopaShellEntity extends KoopaShellEntity { + private static final Identifier TEXTURE = Mubble.id("textures/entity/koopa_shell/green.png"); + + public GreenKoopaShellEntity(EntityType entityType, World world) { + super(entityType, world); + } + + public GreenKoopaShellEntity(World world, double x, double y, double z) { + super(MubbleEntityTypes.GREEN_KOOPA_SHELL, world, x, y, z); + } + + public GreenKoopaShellEntity(World world, LivingEntity owner) { + super(MubbleEntityTypes.GREEN_KOOPA_SHELL, world, owner); + } + + @Override + public ItemStack getPickBlockStack() { + return new ItemStack(MubbleItems.GREEN_KOOPA_SHELL); + } + + @Override + public Identifier getTexture() { + return TEXTURE; + } +} diff --git a/src/main/java/fr/hugman/mubble/entity/KoopaShellEntity.java b/src/main/java/fr/hugman/mubble/entity/KoopaShellEntity.java new file mode 100644 index 00000000..144fec67 --- /dev/null +++ b/src/main/java/fr/hugman/mubble/entity/KoopaShellEntity.java @@ -0,0 +1,197 @@ +package fr.hugman.mubble.entity; + +import fr.hugman.mubble.Mubble; +import fr.hugman.mubble.entity.damage.MubbleDamageTypeKeys; +import fr.hugman.mubble.sound.MubbleSounds; +import fr.hugman.mubble.util.BoxUtil; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.MovementType; +import net.minecraft.entity.data.DataTracker; +import net.minecraft.entity.projectile.ProjectileEntity; +import net.minecraft.entity.projectile.ProjectileUtil; +import net.minecraft.particle.ParticleTypes; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.Identifier; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.hit.EntityHitResult; +import net.minecraft.util.hit.HitResult; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; + +import java.util.List; + +// TODO: tweak with ProjectileUtil for better collision detection (square projection instead of center?) +// TODO: add a max amount of bounces +public abstract class KoopaShellEntity extends ProjectileEntity { + private static final float TARGET_SPEED = 0.5f; + private static final float TARGET_SPEED_ACCELERATION = 0.1f; + private float previousHorizontalRotation; + private float horizontalRotation; + + public KoopaShellEntity(EntityType entityType, World world) { + super(entityType, world); + } + + public KoopaShellEntity(EntityType entityType, World world, double x, double y, double z) { + this(entityType, world); + this.setPosition(x, y, z); + } + + public KoopaShellEntity(EntityType entityType, World world, LivingEntity owner) { + this(entityType, world, owner.getX(), owner.getEyeY() - 0.1F, owner.getZ()); + this.setOwner(owner); + } + + @Override + protected void initDataTracker(DataTracker.Builder builder) { + } + + public abstract Identifier getTexture(); + + public boolean isStopped() { + return this.getVelocity().horizontalLength() == 0.0; + } + + @Override + public void tick() { + super.tick(); + + boolean isStopped = this.isStopped(); + + HitResult hitResult = ProjectileUtil.getCollision(this, this::canHit); + this.hitOrDeflect(hitResult); + + this.applyGravity(); + + Box hitBox = this.getBoundingBox().offset(this.getVelocity().x > 0 ? 0.01d : -0.01d, 0.0d, this.getVelocity().z > 0 ? 0.01d : -0.01d); + + var prevVelocity = this.getVelocity(); + this.move(MovementType.SELF, prevVelocity); + var multiplier = BoxUtil.calculateHorizontalBouncingMultiplier(hitBox, BoxUtil.collectPotentialBlockCollisions(this.getWorld(), hitBox)); + if (!isStopped) { + if (multiplier != null) { + prevVelocity = prevVelocity.multiply(multiplier); + this.playBumpEffects(prevVelocity.negate()); + } + this.setVelocity(prevVelocity.getX(), this.getVelocity().getY(), prevVelocity.getZ()); + if (this.isOnGround()) { + //TODO: make this configurable + this.targetHorizontalSpeed(TARGET_SPEED, TARGET_SPEED_ACCELERATION); + } + this.velocityDirty = true; + } + + if (this.getWorld().isClient) { + this.tickRotation(); + } + } + + public void tickRotation() { + float velocityLength = (float) this.getVelocity().horizontalLength(); + this.previousHorizontalRotation = this.horizontalRotation; + this.horizontalRotation = this.previousHorizontalRotation + velocityLength * 0.35f; + } + + public void targetHorizontalSpeed(float targetSpeed, float acceleration) { + Vec3d velocity = this.getVelocity(); + if (velocity.x == 0 && velocity.z == 0) { + return; + } + double currentSpeed = velocity.horizontalLength(); + double scale; + if (currentSpeed > targetSpeed) { + scale = Math.min(currentSpeed + acceleration, targetSpeed) / currentSpeed; + } else { + scale = Math.max(currentSpeed - acceleration, targetSpeed) / currentSpeed; + } + this.setVelocity(velocity.getX() * scale, velocity.getY(), velocity.getZ() * scale); + } + + @Override + protected void onEntityHit(EntityHitResult result) { + super.onEntityHit(result); + result.getEntity().serverDamage(this.getDamageSources().create(MubbleDamageTypeKeys.KOOPA_SHELL, this, this.getOwner()), 2.0F); + + var bounce = true; + + if(result.getEntity() instanceof LivingEntity entity) { + bounce = entity.isAlive(); + } + + if (bounce) { + // TODO: make this behaviour configurable + Vec3d multiplier; + // TODO: this is ugly + if (Math.abs(this.getVelocity().x) > Math.abs(this.getVelocity().y)) { + multiplier = new Vec3d(-1.0, 1.0, 1.0); + } else if (Math.abs(this.getVelocity().x) < Math.abs(this.getVelocity().y)) { + multiplier = new Vec3d(1.0, 1.0, -1.0); + } else { + multiplier = new Vec3d(1.0, 1.0, 1.0); + } + + var vel = this.getVelocity().multiply(multiplier); + this.setVelocity(vel); + this.velocityDirty = true; + this.playBumpEffects(vel.negate()); + } + } + + @Override + protected void onBlockHit(BlockHitResult blockHitResult) { + if (blockHitResult.getSide().getAxis() != Direction.Axis.Y) { + super.onBlockHit(blockHitResult); + } + } + + @Override + public void onStompedBy(List entities) { + super.onStompedBy(entities); + if (this.getWorld() instanceof ServerWorld) { + Mubble.LOGGER.info(this.getVelocity().horizontalLength()); + + if (this.isStopped()) { + var vec3d = entities.getFirst().getVelocity(); + if (vec3d.horizontalLength() == 0.0D) { + vec3d = this.getPos().subtract(entities.getFirst().getPos()).normalize(); + } + //TODO: if still stopped, make it random + this.setVelocity(vec3d.x, 0.0d, vec3d.z); + this.targetHorizontalSpeed(TARGET_SPEED, Float.MAX_VALUE); + this.velocityDirty = true; + } else { + this.setVelocity(Vec3d.ZERO); + } + } + } + + @Override + protected double getGravity() { + return 0.08; + } + + @Override + public boolean shouldSpawnSprintingParticles() { + return !this.isSpectator() && !this.isInLava() && this.isAlive() && !this.isStopped(); + } + + protected void playBumpEffects(Vec3d direction) { + var center = this.getBoundingBox().getCenter(); + this.playSound(MubbleSounds.KOOPA_SHELL_HIT_BLOCK, 1.0F, 1.0F); + for (int l = 0; l < 8; l++) { + this.getWorld().addParticle(ParticleTypes.CRIT, center.x, center.y, center.z, direction.x + Math.random() - 0.5, direction.y + Math.random() - 0.5, direction.z + Math.random() - 0.5); + } + } + + public float getHorizontalRotation() { + return this.horizontalRotation; + } + + public float getPreviousHorizontalRotation() { + return this.previousHorizontalRotation; + } +} diff --git a/src/main/java/fr/hugman/mubble/entity/MubbleEntityTypeKeys.java b/src/main/java/fr/hugman/mubble/entity/MubbleEntityTypeKeys.java index 6abfa615..37340639 100644 --- a/src/main/java/fr/hugman/mubble/entity/MubbleEntityTypeKeys.java +++ b/src/main/java/fr/hugman/mubble/entity/MubbleEntityTypeKeys.java @@ -8,6 +8,8 @@ public class MubbleEntityTypeKeys { // SUPER MARIO public static final RegistryKey> GOOMBA = of("goomba"); + public static final RegistryKey> GREEN_KOOPA_SHELL = of("green_koopa_shell"); + public static final RegistryKey> RED_KOOPA_SHELL = of("red_koopa_shell"); private static RegistryKey> of(String path) { return RegistryKey.of(RegistryKeys.ENTITY_TYPE, Mubble.id(path)); diff --git a/src/main/java/fr/hugman/mubble/entity/MubbleEntityTypeTags.java b/src/main/java/fr/hugman/mubble/entity/MubbleEntityTypeTags.java index ea4527eb..c95db6d1 100644 --- a/src/main/java/fr/hugman/mubble/entity/MubbleEntityTypeTags.java +++ b/src/main/java/fr/hugman/mubble/entity/MubbleEntityTypeTags.java @@ -6,7 +6,8 @@ import net.minecraft.registry.tag.TagKey; public class MubbleEntityTypeTags { - public static final TagKey> CAN_JUMP_BUMP = of("can_jump_bump"); + public static final TagKey> CAN_STOMP = of("can_stomp"); + public static final TagKey> STOMPABLE = of("stompable"); private static TagKey> of(String path) { return TagKey.of(RegistryKeys.ENTITY_TYPE, Mubble.id(path)); diff --git a/src/main/java/fr/hugman/mubble/entity/MubbleEntityTypes.java b/src/main/java/fr/hugman/mubble/entity/MubbleEntityTypes.java index c562b9c2..0c9138f6 100644 --- a/src/main/java/fr/hugman/mubble/entity/MubbleEntityTypes.java +++ b/src/main/java/fr/hugman/mubble/entity/MubbleEntityTypes.java @@ -10,6 +10,8 @@ public final class MubbleEntityTypes { public static final EntityType GOOMBA = of(MubbleEntityTypeKeys.GOOMBA, EntityType.Builder.create(GoombaEntity::new, SpawnGroup.CREATURE).dimensions(0.6f, 0.755f).eyeHeight(0.53125f)); + public static final EntityType GREEN_KOOPA_SHELL = of(MubbleEntityTypeKeys.GREEN_KOOPA_SHELL, EntityType.Builder.create(GreenKoopaShellEntity::new, SpawnGroup.MISC).dimensions(10 / 16f, 7 / 16f)); + public static final EntityType RED_KOOPA_SHELL = of(MubbleEntityTypeKeys.RED_KOOPA_SHELL, EntityType.Builder.create(RedKoopaShellEntity::new, SpawnGroup.MISC).dimensions(10 / 16f, 7 / 16f)); private static EntityType of(RegistryKey> id, EntityType.Builder type) { return Registry.register(Registries.ENTITY_TYPE, id, type.build(id)); diff --git a/src/main/java/fr/hugman/mubble/entity/RedKoopaShellEntity.java b/src/main/java/fr/hugman/mubble/entity/RedKoopaShellEntity.java new file mode 100644 index 00000000..56b4c22e --- /dev/null +++ b/src/main/java/fr/hugman/mubble/entity/RedKoopaShellEntity.java @@ -0,0 +1,105 @@ +package fr.hugman.mubble.entity; + +import fr.hugman.mubble.Mubble; +import fr.hugman.mubble.item.MubbleItems; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.ai.TargetPredicate; +import net.minecraft.item.ItemStack; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; + +// TODO: break on impact +public class RedKoopaShellEntity extends KoopaShellEntity { + private static final Identifier TEXTURE = Mubble.id("textures/entity/koopa_shell/red.png"); + + private static final double MAX_TARGET_DISTANCE = 16.0; + private static final double MAX_TARGET_DISTANCE_SQUARE = MAX_TARGET_DISTANCE * MAX_TARGET_DISTANCE; + private static final TargetPredicate TARGET_PREDICATE = TargetPredicate.createAttackable() + .setBaseMaxDistance(MAX_TARGET_DISTANCE) + .ignoreVisibility() + .ignoreDistanceScalingFactor() + .setPredicate((target, w) -> target.isMobOrPlayer()); + + private LivingEntity target; + + public RedKoopaShellEntity(EntityType entityType, World world) { + super(entityType, world); + } + + public RedKoopaShellEntity(World world, double x, double y, double z) { + super(MubbleEntityTypes.RED_KOOPA_SHELL, world, x, y, z); + } + + public RedKoopaShellEntity(World world, LivingEntity owner) { + super(MubbleEntityTypes.RED_KOOPA_SHELL, world, owner); + } + + @Override + public ItemStack getPickBlockStack() { + return new ItemStack(MubbleItems.RED_KOOPA_SHELL); + } + + @Override + public Identifier getTexture() { + return TEXTURE; + } + + @Override + public void tick() { + if (this.age % 20 == 1) { + this.searchTarget(); + } + + if (this.target != null && (this.target.isSpectator() || this.target.isDead())) { + this.target = null; + } + + if (this.target != null && !this.getWorld().isClient) { + Vec3d currentPosition = this.getPos(); + Vec3d targetPosition = this.target.getPos(); + Vec3d desiredVelocity = targetPosition.subtract(currentPosition).withAxis(Direction.Axis.Y, 0).normalize().multiply(0.5); + + Vec3d currentVelocity = this.getVelocity(); + Vec3d velocityError = desiredVelocity.subtract(currentVelocity); + double pGain = 0.1; + double dGain = 0.05; + + Vec3d controlSignal = velocityError.multiply(pGain).add(velocityError.subtract(currentVelocity).multiply(dGain)); + this.setVelocity(currentVelocity.add(controlSignal).normalize().multiply(currentVelocity.length())); + this.velocityDirty = true; + } + + super.tick(); + } + + @Override + public void targetHorizontalSpeed(float targetSpeed, float acceleration) { + if (this.target == null) { + super.targetHorizontalSpeed(targetSpeed, acceleration); + } + } + + private void searchTarget() { + var world = this.getWorld(); + if (world instanceof ServerWorld serverWorld && this.getOwner() instanceof LivingEntity livingOwner) { + if (this.target == null || this.target.squaredDistanceTo(this) > MAX_TARGET_DISTANCE_SQUARE) { + this.target = serverWorld.getClosestEntity( + this.getWorld().getEntitiesByClass(LivingEntity.class, this.getSearchBox(MAX_TARGET_DISTANCE), livingEntity -> true), + TARGET_PREDICATE, + livingOwner, + this.getX(), + this.getEyeY(), + this.getZ()); + } + } + } + + protected Box getSearchBox(double distance) { + return this.getBoundingBox().expand(distance, distance, distance); + } +} diff --git a/src/main/java/fr/hugman/mubble/entity/Stompable.java b/src/main/java/fr/hugman/mubble/entity/Stompable.java new file mode 100644 index 00000000..06ac3863 --- /dev/null +++ b/src/main/java/fr/hugman/mubble/entity/Stompable.java @@ -0,0 +1,31 @@ +package fr.hugman.mubble.entity; + +import net.minecraft.entity.Entity; +import net.minecraft.predicate.entity.EntityPredicates; +import net.minecraft.util.math.Box; + +import java.util.List; +import java.util.function.Predicate; + +/** + * Represents an entity that can be stomped (be jumped on). + * + * @author Hugman + * @since v4.0.0 + */ +public interface Stompable { + default boolean canBeStomped() { + return false; + } + + default Box getStompBox() { + return null; + } + + default Predicate getStompableBy() { + return EntityPredicates.EXCEPT_CREATIVE_OR_SPECTATOR; + } + + default void onStompedBy(List entities) { + } +} diff --git a/src/main/java/fr/hugman/mubble/entity/StompableHostileEntity.java b/src/main/java/fr/hugman/mubble/entity/StompableHostileEntity.java deleted file mode 100644 index 832528fc..00000000 --- a/src/main/java/fr/hugman/mubble/entity/StompableHostileEntity.java +++ /dev/null @@ -1,109 +0,0 @@ -package fr.hugman.mubble.entity; - -import net.minecraft.entity.Entity; -import net.minecraft.entity.EntityType; -import net.minecraft.entity.data.DataTracker; -import net.minecraft.entity.data.TrackedData; -import net.minecraft.entity.data.TrackedDataHandlerRegistry; -import net.minecraft.entity.mob.HostileEntity; -import net.minecraft.entity.player.PlayerEntity; -import net.minecraft.network.packet.s2c.play.EntityVelocityUpdateS2CPacket; -import net.minecraft.predicate.entity.EntityPredicates; -import net.minecraft.server.network.ServerPlayerEntity; -import net.minecraft.server.world.ServerWorld; -import net.minecraft.util.math.Box; -import net.minecraft.world.World; - -import java.util.List; -import java.util.function.Predicate; - -/** - * Represents a hostile entity that can be stomped on. - * - * @author Hugman - * @since v4.0.0 - */ -abstract public class StompableHostileEntity extends HostileEntity { - protected static final TrackedData STOMPED = DataTracker.registerData(StompableHostileEntity.class, TrackedDataHandlerRegistry.BOOLEAN); - - protected StompableHostileEntity(EntityType entityType, World world) { - super(entityType, world); - } - - @Override - public void tickMovement() { - super.tickMovement(); - - if (this.canBeStomped()) { - Box hitBox = this.getStompBox(); - List list = this.getWorld().getOtherEntities(this, hitBox, this.getStompableBy()); - if (!list.isEmpty()) { - this.stomp(true); - if (this.getWorld() instanceof ServerWorld serverWorld) { - this.onStompedBy(serverWorld, list); - } - } - } - } - - @Override - protected void initDataTracker(DataTracker.Builder builder) { - super.initDataTracker(builder); - builder.add(STOMPED, false); - } - - public boolean isStomped() { - return this.dataTracker.get(STOMPED); - } - - public void stomp(boolean b) { - this.dataTracker.set(STOMPED, b); - } - - public boolean canBeStomped() { - return this.getHealth() > 0.0F && !this.isSpectator() && !this.hasPassengers(); - } - - /** - * Returns the box that is used to check if an entity can bump on top of this entity. - */ - public Box getStompBox() { - Box hitBox = this.getBoundingBox(); - hitBox = hitBox.withMinY(hitBox.maxY - (0.2D * (hitBox.maxY - hitBox.minY))); - hitBox = hitBox.withMaxY(hitBox.maxY + 0.5D); - - return hitBox; - } - - /** - * Returns a predicate that determines if an entity can bump on top of this entity. - */ - public Predicate getStompableBy() { - return EntityPredicates.EXCEPT_SPECTATOR.and(entity -> - entity.getType().isIn(MubbleEntityTypeTags.CAN_JUMP_BUMP) && - !entity.isOnGround() && - entity.getVelocity().getY() < 0.3D && - entity.isAlive()); - } - - /** - * Called when this entity is bumped on top by another entity. Fired on the server side only. - * - * @param entities the entities that bumped on top of this entity - */ - public void onStompedBy(ServerWorld serverWorld, List entities) { - if (!entities.isEmpty()) { - // TODO: custom damage source - this.damage(serverWorld, this.getDamageSources().fallingBlock(entities.getFirst()), Float.MAX_VALUE); - } else { - this.damage(serverWorld, this.getDamageSources().genericKill(), Float.MAX_VALUE); - } - for (Entity entity : entities) { - entity.setVelocity(entity.getVelocity().x, 0.5D, entity.getVelocity().z); - if (entity instanceof PlayerEntity player) { - ((ServerPlayerEntity) player).networkHandler.sendPacket(new EntityVelocityUpdateS2CPacket(player)); - } - entity.fallDistance = 0.0F; - } - } -} diff --git a/src/main/java/fr/hugman/mubble/entity/SuperMarioEnemyEntity.java b/src/main/java/fr/hugman/mubble/entity/SuperMarioEnemyEntity.java new file mode 100644 index 00000000..0b803b66 --- /dev/null +++ b/src/main/java/fr/hugman/mubble/entity/SuperMarioEnemyEntity.java @@ -0,0 +1,67 @@ +package fr.hugman.mubble.entity; + +import net.minecraft.entity.AnimationState; +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.entity.mob.HostileEntity; +import net.minecraft.world.World; + +import java.util.List; + +/** + * Represents an enemy from the Super Mario series. + * They can be killed by being stomped, and can display a custom death animation. + * + * @author Hugman + * @since v4.0.0 + */ +abstract public class SuperMarioEnemyEntity extends HostileEntity { + protected static final TrackedData STOMPED = DataTracker.registerData(SuperMarioEnemyEntity.class, TrackedDataHandlerRegistry.BOOLEAN); + + protected SuperMarioEnemyEntity(EntityType entityType, World world) { + super(entityType, world); + } + + @Override + protected void initDataTracker(DataTracker.Builder builder) { + super.initDataTracker(builder); + builder.add(STOMPED, false); + } + + @Override + public void onDeath(DamageSource damageSource) { + super.onDeath(damageSource); + if (this.isStomped()) { + this.getStompDeathAnimationState().startIfNotRunning(this.age); + } + } + + @Override + public void onTrackedDataSet(TrackedData data) { + if (STOMPED.equals(data)) { + if (this.isStomped() && this.dead) { + this.getStompDeathAnimationState().startIfNotRunning(this.age); + } + } + } + + public boolean isStomped() { + return this.dataTracker.get(STOMPED); + } + + public void setStomped(boolean b) { + this.dataTracker.set(STOMPED, b); + } + + @Override + public void onStompedBy(List entities) { + this.setStomped(true); + super.onStompedBy(entities); + } + + abstract public AnimationState getStompDeathAnimationState(); +} diff --git a/src/main/java/fr/hugman/mubble/entity/Surprisable.java b/src/main/java/fr/hugman/mubble/entity/Surprisable.java index 43f5a93d..b2269f2b 100644 --- a/src/main/java/fr/hugman/mubble/entity/Surprisable.java +++ b/src/main/java/fr/hugman/mubble/entity/Surprisable.java @@ -10,4 +10,9 @@ public interface Surprisable { boolean isSurprised(); void setSurprised(boolean b); + + default void onSurprised() { + } + + ; } diff --git a/src/main/java/fr/hugman/mubble/entity/ai/goal/SurprisedActiveTargetGoal.java b/src/main/java/fr/hugman/mubble/entity/ai/goal/SurprisedActiveTargetGoal.java index f45a7a37..617c1020 100644 --- a/src/main/java/fr/hugman/mubble/entity/ai/goal/SurprisedActiveTargetGoal.java +++ b/src/main/java/fr/hugman/mubble/entity/ai/goal/SurprisedActiveTargetGoal.java @@ -1,7 +1,6 @@ package fr.hugman.mubble.entity.ai.goal; import fr.hugman.mubble.entity.Surprisable; -import fr.hugman.mubble.sound.MubbleSounds; import net.minecraft.command.argument.EntityAnchorArgumentType; import net.minecraft.entity.LivingEntity; import net.minecraft.entity.ai.TargetPredicate; @@ -40,8 +39,7 @@ public void start() { if (this.mob instanceof Surprisable surprisable) { if (mob.getTarget() != null) { surprisable.setSurprised(true); - mob.lookAt(EntityAnchorArgumentType.EntityAnchor.EYES, mob.getTarget().getPos()); - mob.playSound(MubbleSounds.GOOMBA_FIND_TARGET, 1.0F, 1.0F); + surprisable.onSurprised(); } } } diff --git a/src/main/java/fr/hugman/mubble/entity/damage/MubbleDamageTypeKeys.java b/src/main/java/fr/hugman/mubble/entity/damage/MubbleDamageTypeKeys.java new file mode 100644 index 00000000..0e761bad --- /dev/null +++ b/src/main/java/fr/hugman/mubble/entity/damage/MubbleDamageTypeKeys.java @@ -0,0 +1,17 @@ +package fr.hugman.mubble.entity.damage; + +import fr.hugman.mubble.Mubble; +import net.minecraft.entity.damage.DamageType; +import net.minecraft.registry.RegistryKey; +import net.minecraft.registry.RegistryKeys; + +public class MubbleDamageTypeKeys { + // SUPER MARIO + public static final RegistryKey STOMP = of("stomp"); + public static final RegistryKey KOOPA_SHELL = of("koopa_shell"); + + private static RegistryKey of(String path) { + return RegistryKey.of(RegistryKeys.DAMAGE_TYPE, Mubble.id(path)); + } + +} diff --git a/src/main/java/fr/hugman/mubble/entity/damage/MubbleDamageTypeTags.java b/src/main/java/fr/hugman/mubble/entity/damage/MubbleDamageTypeTags.java new file mode 100644 index 00000000..33982bab --- /dev/null +++ b/src/main/java/fr/hugman/mubble/entity/damage/MubbleDamageTypeTags.java @@ -0,0 +1,16 @@ +package fr.hugman.mubble.entity.damage; + +import fr.hugman.mubble.Mubble; +import net.minecraft.entity.damage.DamageType; +import net.minecraft.registry.RegistryKeys; +import net.minecraft.registry.tag.TagKey; + +public class MubbleDamageTypeTags { + // SUPER MARIO + public static final TagKey INSTANT_KILLS_GOOMBAS = of("instant_kills_goombas"); + + private static TagKey of(String path) { + return TagKey.of(RegistryKeys.DAMAGE_TYPE, Mubble.id(path)); + } + +} diff --git a/src/main/java/fr/hugman/mubble/item/KoopaShellItem.java b/src/main/java/fr/hugman/mubble/item/KoopaShellItem.java new file mode 100644 index 00000000..77c63fad --- /dev/null +++ b/src/main/java/fr/hugman/mubble/item/KoopaShellItem.java @@ -0,0 +1,51 @@ +package fr.hugman.mubble.item; + +import fr.hugman.mubble.entity.GreenKoopaShellEntity; +import fr.hugman.mubble.entity.KoopaShellEntity; +import fr.hugman.mubble.entity.RedKoopaShellEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.projectile.ProjectileEntity; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.ProjectileItem; +import net.minecraft.sound.SoundCategory; +import net.minecraft.sound.SoundEvents; +import net.minecraft.stat.Stats; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Position; +import net.minecraft.world.World; + +public class KoopaShellItem extends Item implements ProjectileItem { + private final boolean isRed; //TODO: i dont like this impl + + public KoopaShellItem(Item.Settings settings, boolean isRed) { + super(settings); + this.isRed = isRed; + } + + @Override + public ActionResult use(World world, PlayerEntity user, Hand hand) { + ItemStack itemStack = user.getStackInHand(hand); + //TODO: sound from Mario Kart? + world.playSound( + null, user.getX(), user.getY(), user.getZ(), SoundEvents.ENTITY_EGG_THROW, SoundCategory.PLAYERS, 0.5F, 0.4F / (world.getRandom().nextFloat() * 0.4F + 0.8F) + ); + if (!world.isClient) { + KoopaShellEntity koopaShellEntity = isRed ? new RedKoopaShellEntity(world, user) : new GreenKoopaShellEntity(world, user); + koopaShellEntity.setVelocity(user, user.getPitch(), user.getYaw(), 0.0F, 0.5F, 1.0F); + world.spawnEntity(koopaShellEntity); + } + + user.incrementStat(Stats.USED.getOrCreateStat(this)); + itemStack.decrementUnlessCreative(1, user); + return ActionResult.SUCCESS; + } + + @Override + public ProjectileEntity createEntity(World world, Position pos, ItemStack stack, Direction direction) { + return isRed ? new RedKoopaShellEntity(world, pos.getX(), pos.getY(), pos.getZ()) : + new GreenKoopaShellEntity(world, pos.getX(), pos.getY(), pos.getZ()); + } +} diff --git a/src/main/java/fr/hugman/mubble/item/MubbleItemGroups.java b/src/main/java/fr/hugman/mubble/item/MubbleItemGroups.java index 20eb7c17..ae078e9b 100644 --- a/src/main/java/fr/hugman/mubble/item/MubbleItemGroups.java +++ b/src/main/java/fr/hugman/mubble/item/MubbleItemGroups.java @@ -41,6 +41,8 @@ public static void appendItemGroups() { ItemGroupEvents.modifyEntriesEvent(MubbleItemGroupKeys.SUPER_MARIO).register(entries -> { var context = entries.getContext(); + entries.add(MubbleItems.GREEN_KOOPA_SHELL); + entries.add(MubbleItems.RED_KOOPA_SHELL); entries.add(MubbleItems.MAKER_GLOVE); entries.add(MubbleBlocks.QUESTION_BLOCK); entries.add(MubbleBlocks.EMPTY_BLOCK); diff --git a/src/main/java/fr/hugman/mubble/item/MubbleItemKeys.java b/src/main/java/fr/hugman/mubble/item/MubbleItemKeys.java index e6ce9ff2..cbaa7911 100644 --- a/src/main/java/fr/hugman/mubble/item/MubbleItemKeys.java +++ b/src/main/java/fr/hugman/mubble/item/MubbleItemKeys.java @@ -8,6 +8,8 @@ public class MubbleItemKeys { // SUPER MARIO public static final RegistryKey MAKER_GLOVE = of("maker_glove"); + public static final RegistryKey GREEN_KOOPA_SHELL = of("green_koopa_shell"); + public static final RegistryKey RED_KOOPA_SHELL = of("red_koopa_shell"); public static final RegistryKey CAPE_FEATHER = of("cape_feather"); public static final RegistryKey SUPER_CAPE_FEATHER = of("super_cape_feather"); public static final RegistryKey GOOMBA_SPAWN_EGG = of("goomba_spawn_egg"); diff --git a/src/main/java/fr/hugman/mubble/item/MubbleItems.java b/src/main/java/fr/hugman/mubble/item/MubbleItems.java index 74c13411..4e4ee239 100644 --- a/src/main/java/fr/hugman/mubble/item/MubbleItems.java +++ b/src/main/java/fr/hugman/mubble/item/MubbleItems.java @@ -13,6 +13,8 @@ public class MubbleItems { // SUPER MARIO public static final Item MAKER_GLOVE = of(MubbleItemKeys.MAKER_GLOVE, new Item.Settings().maxCount(1)); + public static final KoopaShellItem GREEN_KOOPA_SHELL = of(MubbleItemKeys.GREEN_KOOPA_SHELL, s -> new KoopaShellItem(s, false), new Item.Settings().maxCount(3)); + public static final KoopaShellItem RED_KOOPA_SHELL = of(MubbleItemKeys.RED_KOOPA_SHELL, s -> new KoopaShellItem(s, true), new Item.Settings().maxCount(3)); public static final CapeFeatherItem CAPE_FEATHER = of(MubbleItemKeys.CAPE_FEATHER, s -> new CapeFeatherItem(s, false)); public static final CapeFeatherItem SUPER_CAPE_FEATHER = of(MubbleItemKeys.SUPER_CAPE_FEATHER, s -> new CapeFeatherItem(s.rarity(Rarity.EPIC), true)); diff --git a/src/main/java/fr/hugman/mubble/mixin/EntityMixin.java b/src/main/java/fr/hugman/mubble/mixin/EntityMixin.java index 1d227f21..c531a697 100644 --- a/src/main/java/fr/hugman/mubble/mixin/EntityMixin.java +++ b/src/main/java/fr/hugman/mubble/mixin/EntityMixin.java @@ -1,12 +1,21 @@ package fr.hugman.mubble.mixin; import fr.hugman.mubble.block.HittableBlock; +import fr.hugman.mubble.entity.MubbleEntityTypeTags; +import fr.hugman.mubble.entity.Stompable; +import fr.hugman.mubble.entity.damage.MubbleDamageTypeKeys; import net.minecraft.block.BlockState; import net.minecraft.entity.Entity; import net.minecraft.entity.MovementType; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.network.packet.s2c.play.EntityVelocityUpdateS2CPacket; +import net.minecraft.predicate.entity.EntityPredicates; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; import net.minecraft.util.hit.BlockHitResult; import net.minecraft.util.hit.HitResult; import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Box; import net.minecraft.util.math.Direction; import net.minecraft.util.math.Vec3d; import net.minecraft.world.RaycastContext; @@ -17,8 +26,11 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import java.util.List; +import java.util.function.Predicate; + @Mixin(Entity.class) -public class EntityMixin { +public class EntityMixin implements Stompable { // Inject right before the second call of setPosition() in the method move() @Inject(method = "move", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;setPosition(DDD)V", ordinal = 1)) private void mubble$move(MovementType type, Vec3d movement, CallbackInfo ci) { @@ -38,8 +50,64 @@ public class EntityMixin { } } + @Inject(method = "tick", at = @At("HEAD")) + private void mubble$tick(CallbackInfo ci) { + Entity thisEntity = (Entity) (Object) this; + if (this.canBeStomped()) { + Box hitBox = this.getStompBox(); + if (hitBox != null) { + List list = thisEntity.getWorld().getOtherEntities(thisEntity, hitBox, this.getStompableBy()); + if (!list.isEmpty()) { + this.onStompedBy(list); + } + } + } + } + @Shadow private Vec3d adjustMovementForCollisions(Vec3d movement) { return null; } + + @Override + public boolean canBeStomped() { + var thisEntity = ((Entity) (Object) this); + return thisEntity.getType().isIn(MubbleEntityTypeTags.STOMPABLE) && !thisEntity.isSpectator() && !thisEntity.hasPassengers(); + } + + @Override + public Box getStompBox() { + var thisEntity = ((Entity) (Object) this); + Box hitBox = thisEntity.getBoundingBox(); + hitBox = hitBox.withMinY(hitBox.maxY - (0.2D * (hitBox.maxY - hitBox.minY))); + hitBox = hitBox.withMaxY(hitBox.maxY + 0.5D); + + return hitBox; + } + + @Override + public Predicate getStompableBy() { + return EntityPredicates.EXCEPT_SPECTATOR.and(entity -> + entity.getType().isIn(MubbleEntityTypeTags.CAN_STOMP) && + !entity.isOnGround() && + entity.getVelocity().getY() < 0.3D && + entity.isAlive()); + } + + @Override + public void onStompedBy(List entities) { + var thisEntity = ((Entity) (Object) this); + //TODO: display particles! + if (thisEntity.getWorld() instanceof ServerWorld serverWorld) { + //TODO: calculate damage using boots? + thisEntity.damage(serverWorld, thisEntity.getDamageSources().create(MubbleDamageTypeKeys.STOMP, entities.getFirst()), 2.0F); + for (Entity entity : entities) { + entity.setVelocity(entity.getVelocity().x, 0.5D, entity.getVelocity().z); + if (entity instanceof PlayerEntity player) { + ((ServerPlayerEntity) player).networkHandler.sendPacket(new EntityVelocityUpdateS2CPacket(player)); + } + entity.fallDistance = 0.0F; + } + } + } } \ No newline at end of file diff --git a/src/main/java/fr/hugman/mubble/mixin/LivingEntityMixin.java b/src/main/java/fr/hugman/mubble/mixin/LivingEntityMixin.java new file mode 100644 index 00000000..14e08632 --- /dev/null +++ b/src/main/java/fr/hugman/mubble/mixin/LivingEntityMixin.java @@ -0,0 +1,12 @@ +package fr.hugman.mubble.mixin; + +import net.minecraft.entity.LivingEntity; +import org.spongepowered.asm.mixin.Mixin; + +@Mixin(LivingEntity.class) +public class LivingEntityMixin extends EntityMixin { + @Override + public boolean canBeStomped() { + return ((LivingEntity) (Object) this).getHealth() > 0.0f && super.canBeStomped(); + } +} diff --git a/src/main/java/fr/hugman/mubble/mixin/client/ClientCommonNetworkHandlerAccessor.java b/src/main/java/fr/hugman/mubble/mixin/client/ClientCommonNetworkHandlerAccessor.java new file mode 100644 index 00000000..c8d72935 --- /dev/null +++ b/src/main/java/fr/hugman/mubble/mixin/client/ClientCommonNetworkHandlerAccessor.java @@ -0,0 +1,12 @@ +package fr.hugman.mubble.mixin.client; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientCommonNetworkHandler; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(ClientCommonNetworkHandler.class) +public interface ClientCommonNetworkHandlerAccessor { + @Accessor("client") + MinecraftClient getClient(); +} \ No newline at end of file diff --git a/src/main/java/fr/hugman/mubble/mixin/client/ClientPlayNetworkHandlerMixin.java b/src/main/java/fr/hugman/mubble/mixin/client/ClientPlayNetworkHandlerMixin.java new file mode 100644 index 00000000..915f6616 --- /dev/null +++ b/src/main/java/fr/hugman/mubble/mixin/client/ClientPlayNetworkHandlerMixin.java @@ -0,0 +1,24 @@ +package fr.hugman.mubble.mixin.client; + +import fr.hugman.mubble.client.sound.MovingKoopaShellSoundInstance; +import fr.hugman.mubble.entity.KoopaShellEntity; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.network.ClientPlayNetworkHandler; +import net.minecraft.entity.Entity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ClientPlayNetworkHandler.class) +@Environment(EnvType.CLIENT) +public class ClientPlayNetworkHandlerMixin { + @Inject(method = "playSpawnSound", at = @At("HEAD")) + private void onPlaySpawnSound(Entity entity, CallbackInfo ci) { + ClientCommonNetworkHandlerAccessor accessor = (ClientCommonNetworkHandlerAccessor) this; + if (entity instanceof KoopaShellEntity koopaShell) { + accessor.getClient().getSoundManager().play(new MovingKoopaShellSoundInstance(koopaShell)); + } + } +} \ No newline at end of file diff --git a/src/main/java/fr/hugman/mubble/mixin/LivingEntityRendererMixin.java b/src/main/java/fr/hugman/mubble/mixin/client/LivingEntityRendererMixin.java similarity index 95% rename from src/main/java/fr/hugman/mubble/mixin/LivingEntityRendererMixin.java rename to src/main/java/fr/hugman/mubble/mixin/client/LivingEntityRendererMixin.java index 03999123..7b2f7c86 100644 --- a/src/main/java/fr/hugman/mubble/mixin/LivingEntityRendererMixin.java +++ b/src/main/java/fr/hugman/mubble/mixin/client/LivingEntityRendererMixin.java @@ -1,4 +1,4 @@ -package fr.hugman.mubble.mixin; +package fr.hugman.mubble.mixin.client; import fr.hugman.mubble.client.render.entity.state.GoombaEntityRenderState; import net.minecraft.client.render.entity.LivingEntityRenderer; diff --git a/src/main/java/fr/hugman/mubble/sound/MubbleSounds.java b/src/main/java/fr/hugman/mubble/sound/MubbleSounds.java index 069edd55..b5ebf6b1 100644 --- a/src/main/java/fr/hugman/mubble/sound/MubbleSounds.java +++ b/src/main/java/fr/hugman/mubble/sound/MubbleSounds.java @@ -24,6 +24,9 @@ public class MubbleSounds { public static final SoundEvent GOOMBA_DEATH = of("entity.goomba.death"); public static final SoundEvent GOOMBA_STOMP = of("entity.goomba.stomp"); + public static final SoundEvent KOOPA_SHELL_SLIDE = of("entity.koopa_shell.slide"); + public static final SoundEvent KOOPA_SHELL_HIT_BLOCK = of("entity.koopa_shell.hit_block"); + private static SoundEvent of(String path) { Identifier id = Mubble.id(path); return Registry.register(Registries.SOUND_EVENT, id, SoundEvent.of(id)); diff --git a/src/main/java/fr/hugman/mubble/util/BoxUtil.java b/src/main/java/fr/hugman/mubble/util/BoxUtil.java new file mode 100644 index 00000000..d6809151 --- /dev/null +++ b/src/main/java/fr/hugman/mubble/util/BoxUtil.java @@ -0,0 +1,108 @@ +package fr.hugman.mubble.util; + +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * Utility class for box collision calculations. + * + * @author Hugman + * @since v4.0.0 + */ +public class BoxUtil { + /** + * Calculates the horizontal bouncing multiplier vector based on the collision of + * the given origin box with a list of other boxes. + *

+ * This method checks for collisions in both the X and Z axes. If a + * collision is detected, it computes the minimum distance to the + * closest edge of the colliding boxes in both axes. The method returns + * a vector indicating the bounce direction. If there is no collision, + * it returns null. + * + * @param originBox the box for which the bouncing multiplier is calculated + * @param otherBoxes other boxes to check for collisions against + * @return a {@link Vec3d} representing the bouncing multiplier direction, + * or null if no collision is detected + */ + @Nullable + public static Vec3d calculateHorizontalBouncingMultiplier(Box originBox, Box... otherBoxes) { + double minDistanceX = Double.MAX_VALUE; + double minDistanceZ = Double.MAX_VALUE; + + for (Box box : otherBoxes) { + // Check for collision between the origin box and the other box + if (originBox.maxX > box.minX && originBox.minX < box.maxX && + originBox.maxZ > box.minZ && originBox.minZ < box.maxZ) { + + // Calculate the distances to the closest edges of the box + double distanceX = Math.min(originBox.maxX - box.minX, box.maxX - originBox.minX); + double distanceZ = Math.min(originBox.maxZ - box.minZ, box.maxZ - originBox.minZ); + + // Update the minimum distances for collision response + if (Math.abs(distanceX) < Math.abs(minDistanceX)) { + minDistanceX = distanceX; + } + if (Math.abs(distanceZ) < Math.abs(minDistanceZ)) { + minDistanceZ = distanceZ; + } + } + } + + // Check if a collision was detected + if (minDistanceX == Double.MAX_VALUE && minDistanceZ == Double.MAX_VALUE) { + return null; // No collision detected + } + + // Determine which axis the collision is on + return (minDistanceX < minDistanceZ) + ? new Vec3d(-1.0, 1.0, 1.0) // Bounce in the x-axis + : new Vec3d(1.0, 1.0, -1.0); // Bounce in the z-axis + } + + /** + * Calculates the horizontal bouncing multiplier vector based on the collision of + * the given origin box with a list of other boxes. + *

+ * This method checks for collisions in both the X and Z axes. If a + * collision is detected, it computes the minimum distance to the + * closest edge of the colliding boxes in both axes. The method returns + * a vector indicating the bounce direction. If there is no collision, + * it returns null. + * + * @param originBox the box for which the bouncing multiplier is calculated + * @param otherBoxes a list of other boxes to check for collisions against + * @return a {@link Vec3d} representing the bouncing multiplier direction, + * or null if no collision is detected + */ + public static Vec3d calculateHorizontalBouncingMultiplier(Box originBox, List otherBoxes) { + return calculateHorizontalBouncingMultiplier(originBox, otherBoxes.toArray(new Box[0])); + } + + /** + * Collects potential block collisions in the world for the given origin box. + *

+ * This method iterates over all block positions within the bounds of + * the origin box and retrieves the collision shapes for each block + * position. + * + * @param originBox the box to check for potential collisions + * @return a list of {@link Box} objects representing the potential collisions + */ + public static List collectPotentialBlockCollisions(World world, Box originBox) { + Iterable iterable = BlockPos.iterate(originBox); + List boundingBoxes = new ArrayList<>(); + for (BlockPos pos : iterable) { + // Collect bounding boxes from collision shapes directly + boundingBoxes.addAll(world.getBlockState(pos).getCollisionShape(world, pos) + .offset(Vec3d.of(pos)).getBoundingBoxes()); + } + return boundingBoxes; + } +} diff --git a/src/main/resources/assets/mubble/lang/en_us.json b/src/main/resources/assets/mubble/lang/en_us.json index c8093404..e265ed36 100644 --- a/src/main/resources/assets/mubble/lang/en_us.json +++ b/src/main/resources/assets/mubble/lang/en_us.json @@ -4,6 +4,8 @@ "item_group.mubble.super_mario": "Super Mario", "item_group.mubble.yoshi_island": "Yoshi's Island", + "item.mubble.green_koopa_shell": "Green Koopa Shell", + "item.mubble.red_koopa_shell": "Red Koopa Shell", "item.mubble.maker_glove": "Maker Glove", "block.mubble.question_block": "? Block", @@ -33,6 +35,8 @@ "entity.mubble.goomba": "Goomba", "entity.mubble.goomba.mini": "Mini Goomba", + "entity.mubble.green_koopa_shell": "Green Koopa Shell", + "entity.mubble.red_koopa_shell": "Red Koopa Shell", "block.mubble.blue_egg_block": "Blue Egg Block", "block.mubble.cyan_egg_block": "Cyan Egg Block", @@ -57,5 +61,8 @@ "block.mubble.bumpable.drop.all": "Drop all", "block.mubble.bumpable.drop.all.description": "The block will drop the entire item stack when bumped", "block.mubble.bumpable.drop.one": "Drop one", - "block.mubble.bumpable.drop.one.description": "The block will drop one item per bump" + "block.mubble.bumpable.drop.one.description": "The block will drop one item per bump", + + "death.attack.mubble.koopa_shell": "%1$s was shelled by %2$s", + "death.attack.mubble.koopa_shell.item": "%1$s was shelled by %2$s using %3$s" } \ No newline at end of file diff --git a/src/main/resources/assets/mubble/models/item/green_koopa_shell.json b/src/main/resources/assets/mubble/models/item/green_koopa_shell.json new file mode 100644 index 00000000..27b4aef9 --- /dev/null +++ b/src/main/resources/assets/mubble/models/item/green_koopa_shell.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "mubble:item/koopa_shell/green" + } +} diff --git a/src/main/resources/assets/mubble/models/item/red_koopa_shell.json b/src/main/resources/assets/mubble/models/item/red_koopa_shell.json new file mode 100644 index 00000000..fc255c96 --- /dev/null +++ b/src/main/resources/assets/mubble/models/item/red_koopa_shell.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "mubble:item/koopa_shell/red" + } +} diff --git a/src/main/resources/assets/mubble/sounds.json b/src/main/resources/assets/mubble/sounds.json index 5b3213ff..3598eb17 100644 --- a/src/main/resources/assets/mubble/sounds.json +++ b/src/main/resources/assets/mubble/sounds.json @@ -77,5 +77,17 @@ "sounds": [ "mubble:entity/goomba/stomp" ] + }, + "entity.koopa_shell.slide": { + "subtitle": "subtitles.mubble.entity.koopa_shell.slide", + "sounds": [ + "mubble:entity/koopa_shell/slide" + ] + }, + "entity.koopa_shell.hit_block": { + "subtitle": "subtitles.mubble.entity.koopa_shell.hit_block", + "sounds": [ + "mubble:entity/koopa_shell/hit_block" + ] } } \ No newline at end of file diff --git a/src/main/resources/assets/mubble/sounds/entity/koopa_shell/hit_block.ogg b/src/main/resources/assets/mubble/sounds/entity/koopa_shell/hit_block.ogg new file mode 100644 index 00000000..d368477a Binary files /dev/null and b/src/main/resources/assets/mubble/sounds/entity/koopa_shell/hit_block.ogg differ diff --git a/src/main/resources/assets/mubble/sounds/entity/koopa_shell/slide.ogg b/src/main/resources/assets/mubble/sounds/entity/koopa_shell/slide.ogg new file mode 100644 index 00000000..bad40911 Binary files /dev/null and b/src/main/resources/assets/mubble/sounds/entity/koopa_shell/slide.ogg differ diff --git a/src/main/resources/assets/mubble/textures/entity/koopa_shell/green.png b/src/main/resources/assets/mubble/textures/entity/koopa_shell/green.png new file mode 100644 index 00000000..3e1a3db9 Binary files /dev/null and b/src/main/resources/assets/mubble/textures/entity/koopa_shell/green.png differ diff --git a/src/main/resources/assets/mubble/textures/entity/koopa_shell/red.png b/src/main/resources/assets/mubble/textures/entity/koopa_shell/red.png new file mode 100644 index 00000000..72826ef1 Binary files /dev/null and b/src/main/resources/assets/mubble/textures/entity/koopa_shell/red.png differ diff --git a/src/main/resources/assets/mubble/textures/item/koopa_shell/green.png b/src/main/resources/assets/mubble/textures/item/koopa_shell/green.png new file mode 100644 index 00000000..6dbb2efb Binary files /dev/null and b/src/main/resources/assets/mubble/textures/item/koopa_shell/green.png differ diff --git a/src/main/resources/assets/mubble/textures/item/koopa_shell/red.png b/src/main/resources/assets/mubble/textures/item/koopa_shell/red.png new file mode 100644 index 00000000..7209f01b Binary files /dev/null and b/src/main/resources/assets/mubble/textures/item/koopa_shell/red.png differ diff --git a/src/main/resources/data/minecraft/tags/damage_type/is_projectile.json b/src/main/resources/data/minecraft/tags/damage_type/is_projectile.json new file mode 100644 index 00000000..103fd6c3 --- /dev/null +++ b/src/main/resources/data/minecraft/tags/damage_type/is_projectile.json @@ -0,0 +1,6 @@ +{ + "replace": false, + "values": [ + "mubble:koopa_shell" + ] +} diff --git a/src/main/resources/data/mubble/damage_type/koopa_shell.json b/src/main/resources/data/mubble/damage_type/koopa_shell.json new file mode 100644 index 00000000..207572e9 --- /dev/null +++ b/src/main/resources/data/mubble/damage_type/koopa_shell.json @@ -0,0 +1,5 @@ +{ + "message_id": "mubble.koopa_shell", + "exhaustion": 0.1, + "scaling": "when_caused_by_living_non_player" +} diff --git a/src/main/resources/data/mubble/damage_type/stomp.json b/src/main/resources/data/mubble/damage_type/stomp.json new file mode 100644 index 00000000..09ba0684 --- /dev/null +++ b/src/main/resources/data/mubble/damage_type/stomp.json @@ -0,0 +1,5 @@ +{ + "message_id": "mubble.stomp", + "exhaustion": 0.1, + "scaling": "when_caused_by_living_non_player" +} diff --git a/src/main/resources/data/mubble/tags/damage_type/instant_kills_goombas.json b/src/main/resources/data/mubble/tags/damage_type/instant_kills_goombas.json new file mode 100644 index 00000000..c9c9a88b --- /dev/null +++ b/src/main/resources/data/mubble/tags/damage_type/instant_kills_goombas.json @@ -0,0 +1,7 @@ +{ + "replace": false, + "values": [ + "mubble:stomp", + "mubble:koopa_shell" + ] +} diff --git a/src/main/resources/data/mubble/tags/entity_type/can_jump_bump.json b/src/main/resources/data/mubble/tags/entity_type/can_stomp.json similarity index 100% rename from src/main/resources/data/mubble/tags/entity_type/can_jump_bump.json rename to src/main/resources/data/mubble/tags/entity_type/can_stomp.json diff --git a/src/main/resources/data/mubble/tags/entity_type/koopa_shells.json b/src/main/resources/data/mubble/tags/entity_type/koopa_shells.json new file mode 100644 index 00000000..ceac6f00 --- /dev/null +++ b/src/main/resources/data/mubble/tags/entity_type/koopa_shells.json @@ -0,0 +1,7 @@ +{ + "replace": false, + "values": [ + "mubble:green_koopa_shell", + "mubble:red_koopa_shell" + ] +} diff --git a/src/main/resources/data/mubble/tags/entity_type/stompable.json b/src/main/resources/data/mubble/tags/entity_type/stompable.json new file mode 100644 index 00000000..68765b58 --- /dev/null +++ b/src/main/resources/data/mubble/tags/entity_type/stompable.json @@ -0,0 +1,7 @@ +{ + "replace": false, + "values": [ + "mubble:goomba", + "#mubble:koopa_shells" + ] +} diff --git a/src/main/resources/data/mubble/tags/item/koopa_shells.json b/src/main/resources/data/mubble/tags/item/koopa_shells.json new file mode 100644 index 00000000..ceac6f00 --- /dev/null +++ b/src/main/resources/data/mubble/tags/item/koopa_shells.json @@ -0,0 +1,7 @@ +{ + "replace": false, + "values": [ + "mubble:green_koopa_shell", + "mubble:red_koopa_shell" + ] +} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index cbdf6d75..8e2480d7 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -57,6 +57,9 @@ "fabric", "quilt" ] + }, + "loom:injected_interfaces": { + "net/minecraft/class_1297": ["fr/hugman/mubble/entity/Stompable"] } }, "license": "LGPL v3.0" diff --git a/src/main/resources/mubble.mixins.json b/src/main/resources/mubble.mixins.json index 21fc4eea..a2f8867c 100644 --- a/src/main/resources/mubble.mixins.json +++ b/src/main/resources/mubble.mixins.json @@ -3,12 +3,15 @@ "package": "fr.hugman.mubble.mixin", "compatibilityLevel": "JAVA_21", "mixins": [ - "EntityMixin" + "EntityMixin", + "LivingEntityMixin" ], "injectors": { "defaultRequire": 1 }, "client": [ - "LivingEntityRendererMixin" + "client.ClientCommonNetworkHandlerAccessor", + "client.ClientPlayNetworkHandlerMixin", + "client.LivingEntityRendererMixin" ] }