diff --git a/gradle.properties b/gradle.properties index c5c25c0..fe5d434 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ minecraft_version=1.20.1 yarn_mappings=1.20.1+build.10 loader_version=0.18.3 # Mod Properties -mod_version=26.2.24.1 +mod_version=26.2.25 maven_group=dev.tggamesyt archives_base_name=szar # Dependencies diff --git a/src/client/java/dev/tggamesyt/szar/client/PlaneEntityRenderer.java b/src/client/java/dev/tggamesyt/szar/client/PlaneEntityRenderer.java index d7de19d..71a2391 100644 --- a/src/client/java/dev/tggamesyt/szar/client/PlaneEntityRenderer.java +++ b/src/client/java/dev/tggamesyt/szar/client/PlaneEntityRenderer.java @@ -6,7 +6,9 @@ import net.minecraft.client.render.*; import net.minecraft.client.render.entity.*; import net.minecraft.client.render.entity.model.EntityModelLayer; import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.entity.Entity; import net.minecraft.util.Identifier; +import net.minecraft.util.math.RotationAxis; public class PlaneEntityRenderer extends EntityRenderer { @@ -35,26 +37,48 @@ public class PlaneEntityRenderer extends EntityRenderer { int light ) { matrices.push(); + + // Smooth interpolation of rotation + float interpolatedYaw = entity.prevYaw + (entity.getYaw() - entity.prevYaw) * tickDelta; + float interpolatedPitch = entity.prevPitch + (entity.getPitch() - entity.prevPitch) * tickDelta; + + // Scale matrices.scale(4.0F, 4.0F, 4.0F); + + // Move model to correct pivot point matrices.translate(0.0, 1.5, 0.0); + + // Rotate to match hitbox exactly + matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(-interpolatedYaw)); + matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(interpolatedPitch)); + + // Rotate 180° to fix backwards-facing + matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(180.0F)); + + // Flip model upright (Minecraft model fix) matrices.scale(-1.0F, -1.0F, 1.0F); + // Set model angles model.setAngles( entity, 0, 0, entity.age + tickDelta, - 0, - 0 + interpolatedYaw, + interpolatedPitch ); VertexConsumer consumer = vertices.getBuffer(RenderLayer.getEntityCutout(getTexture(entity))); - model.render(matrices, consumer, light, OverlayTexture.DEFAULT_UV, - 1.0F, 1.0F, 1.0F, 1.0F); + model.render( + matrices, + consumer, + light, + OverlayTexture.DEFAULT_UV, + 1.0F, 1.0F, 1.0F, 1.0F + ); matrices.pop(); - super.render(entity, yaw, tickDelta, matrices, vertices, light); } } diff --git a/src/client/java/dev/tggamesyt/szar/client/SzarClient.java b/src/client/java/dev/tggamesyt/szar/client/SzarClient.java index 473871f..3991f6b 100644 --- a/src/client/java/dev/tggamesyt/szar/client/SzarClient.java +++ b/src/client/java/dev/tggamesyt/szar/client/SzarClient.java @@ -11,6 +11,7 @@ import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; import net.fabricmc.fabric.api.client.rendering.v1.EntityModelLayerRegistry; import net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry; import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback; +import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; import net.fabricmc.fabric.api.object.builder.v1.client.model.FabricModelPredicateProviderRegistry; import net.minecraft.client.MinecraftClient; import net.minecraft.client.option.KeyBinding; @@ -26,6 +27,7 @@ import net.minecraft.client.render.*; import net.minecraft.client.world.ClientWorld; import net.minecraft.entity.Entity; import net.minecraft.item.ItemStack; +import net.minecraft.network.PacketByteBuf; import net.minecraft.sound.SoundCategory; import net.minecraft.sound.SoundEvent; import net.minecraft.util.Identifier; @@ -73,6 +75,18 @@ public class SzarClient implements ClientModInitializer { int loopStart = startOffset + startLength; @Override public void onInitializeClient() { + ClientTickEvents.END_CLIENT_TICK.register(client -> { + if (client.player == null) return; + + boolean forward = client.options.attackKey.isPressed(); + boolean backward = client.options.useKey.isPressed(); + + PacketByteBuf buf = PacketByteBufs.create(); + buf.writeBoolean(forward); + buf.writeBoolean(backward); + + ClientPlayNetworking.send(PlayerMovementManager.PACKET_ID, buf); + }); ClientPlayNetworking.registerGlobalReceiver(SYNC_PACKET, (client, handler, buf, responseSender) -> { // First read the player UUID UUID playerUuid = buf.readUuid(); diff --git a/src/main/java/dev/tggamesyt/szar/PlaneEntity.java b/src/main/java/dev/tggamesyt/szar/PlaneEntity.java index 75ba786..04c6fa2 100644 --- a/src/main/java/dev/tggamesyt/szar/PlaneEntity.java +++ b/src/main/java/dev/tggamesyt/szar/PlaneEntity.java @@ -3,11 +3,13 @@ package dev.tggamesyt.szar; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.entity.*; +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.player.PlayerEntity; import net.minecraft.nbt.NbtCompound; +import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.util.ActionResult; import net.minecraft.util.Hand; import net.minecraft.util.math.MathHelper; @@ -22,6 +24,9 @@ public class PlaneEntity extends Entity { private PlaneAnimation currentServerAnimation = null; private float enginePower = 0f; private double lastY; + double stallSpeed = 1.0; + double explodeSpeed = 1.0; + private int brakeHoldTicks = 0; @Environment(EnvType.CLIENT) private PlaneAnimation currentAnimation; @@ -34,6 +39,7 @@ public class PlaneEntity extends Entity { public PlaneEntity(EntityType type, World world) { super(type, world); this.noClip = false; + this.setStepHeight(2.0f); this.setNoGravity(false); // FORCE gravity ON } @@ -66,128 +72,142 @@ public class PlaneEntity extends Entity { @Override public void tick() { super.tick(); - PlayerEntity player = getControllingPassenger(); + + // ----------------------------- + // No pilot: just apply basic gravity + // ----------------------------- if (player == null) { - Vec3d velocity = getVelocity(); - - // Apply gravity even without pilot - velocity = velocity.add(0, -0.04, 0); - - velocity = velocity.multiply(0.98); - + Vec3d velocity = getVelocity().add(0, -0.04, 0).multiply(0.95); setVelocity(velocity); move(MovementType.SELF, velocity); return; } - - /* -------------------------------- - CONTROLLER (AircraftEntity logic) - -------------------------------- */ - - // YAW + // ----------------------------- + // Yaw & pitch control + // ----------------------------- setYaw(getYaw() - player.sidewaysSpeed * 4.0f); + if (!isOnGround() || getVelocity().length() > 0.9) setPitch(getPitch() - player.forwardSpeed * 1.5f); + setPitch(getPitch() * 0.98f); // auto leveling + player.setInvisible(true); - // PITCH (only in air) - if (!isOnGround()) { - setPitch(getPitch() - player.forwardSpeed * 2.5f); + // ----------------------------- + // Engine target adjustments (server authoritative) + // ----------------------------- + boolean forward = !getWorld().isClient && PlayerMovementManager.isForwardPressed((ServerPlayerEntity) player); + boolean braking = !getWorld().isClient && PlayerMovementManager.isBackwardPressed((ServerPlayerEntity) player); + + if (forward) setEngineTarget(getEngineTarget() + 0.02f); + + if (braking) { + brakeHoldTicks++; + float baseBrake = isOnGround() ? 0.04f : 0.015f; + float progressive = Math.min(brakeHoldTicks * 0.0035f, 0.15f); + float brakeStrength = baseBrake + progressive; + setEngineTarget(getEngineTarget() - brakeStrength); + + // Apply actual braking locally + Vec3d vel = getVelocity(); + if (vel.lengthSquared() > 0.0001) { + Vec3d brakeDir = vel.normalize().multiply(-brakeStrength * 0.6); + setVelocity(vel.add(brakeDir)); + } + } else { + brakeHoldTicks = 0; } - // Stabilizer (small auto leveling) - setPitch(getPitch() * 0.98f); + // ----------------------------- + // Engine power smoothing + // ----------------------------- + float lerpSpeed = braking ? 0.25f : 0.05f; + enginePower += (getEngineTarget() - enginePower) * lerpSpeed; - /* -------------------------------- - THROTTLE (AirplaneEntity logic) - -------------------------------- */ - - if (player.jumping) { - setEngineTarget(getEngineTarget() + 0.02f); - } - - if (player.isSneaking()) { - setEngineTarget(getEngineTarget() - 0.02f); - } - - // Smooth engine reaction - enginePower += (getEngineTarget() - enginePower) * 0.05f; - -/* -------------------------------- - PHYSICS (STABLE VERSION) --------------------------------- */ - - Vec3d forward = getRotationVector().normalize(); + // ----------------------------- + // PHYSICS (runs on both client & server) + // ----------------------------- + Vec3d forwardVec = getRotationVector().normalize(); Vec3d velocity = getVelocity(); + double horizontalSpeed = Math.sqrt(velocity.x * velocity.x + velocity.z * velocity.z); - /* ---------- REALIGN VELOCITY ---------- */ - /* Prevents internal momentum stacking */ + // Stall gravity in air + if (!isOnGround() && horizontalSpeed < stallSpeed) { + velocity = velocity.add(0, -0.2, 0); + } + // Forward locking (only if not braking) double speed = velocity.length(); - - if (speed > 0.001) { - // Stronger forward locking - double alignment = 0.25; // was 0.08 - Vec3d newDir = velocity.normalize().lerp(forward, alignment).normalize(); + if (speed > 0.001 && enginePower > 0.01f && !braking) { + Vec3d newDir = velocity.normalize().lerp(forwardVec, 0.25).normalize(); velocity = newDir.multiply(speed); } - - /* ---------- THRUST ---------- */ - + // Apply thrust double thrust = Math.pow(enginePower, 2.0) * 0.08; - velocity = velocity.add(forward.multiply(thrust)); - - /* ---------- GLIDE ---------- */ + velocity = velocity.add(forwardVec.multiply(thrust)); + // Glide (air only) double diffY = lastY - getY(); - if (lastY != 0.0 && diffY != 0.0) { - velocity = velocity.add( - forward.multiply(diffY * 0.04 * (1.0 - Math.abs(forward.y))) - ); + if (!isOnGround() && lastY != 0.0 && diffY != 0.0) { + velocity = velocity.add(forwardVec.multiply(diffY * 0.04 * (1.0 - Math.abs(forwardVec.y)))); } lastY = getY(); - /* ---------- DYNAMIC GRAVITY ---------- */ - - double horizontalSpeed = velocity.length() * (1.0 - Math.abs(forward.y)); + // Dynamic gravity + horizontalSpeed = velocity.length() * (1.0 - Math.abs(forwardVec.y)); double gravityFactor = Math.max(0.0, 1.0 - horizontalSpeed * 1.5); - -// ALWAYS apply — do not check hasNoGravity() velocity = velocity.add(0, -0.04 * gravityFactor, 0); - /* ---------- DRAG ---------- */ + // Drag / friction + velocity = isOnGround() ? velocity.multiply(0.94) : velocity.multiply(0.98); - velocity = velocity.multiply(0.98); + // Max speed clamp + double maxSpeed = 2; + if (velocity.length() > maxSpeed) velocity = velocity.normalize().multiply(maxSpeed); - /* ---------- MAX SPEED CLAMP ---------- */ - - double maxSpeed = 1.5; - if (velocity.length() > maxSpeed) { - velocity = velocity.normalize().multiply(maxSpeed); - } + // Save vertical velocity before move for impact check + Vec3d preMoveVelocity = velocity; + // Move setVelocity(velocity); move(MovementType.SELF, velocity); - /* -------------------------------- - ANIMATION STATE SYNC --------------------------------- */ + // ----------------------------- + // Crash detection (server only) + // Explodes if hitting block with high horizontal or vertical velocity + // ----------------------------- + if (!getWorld().isClient) { + double horizontalImpact = Math.sqrt(preMoveVelocity.x * preMoveVelocity.x + preMoveVelocity.z * preMoveVelocity.z); + double verticalImpact = Math.abs(preMoveVelocity.y); - boolean hasPassenger = getControllingPassenger() != null; + boolean crash = (horizontalImpact > 1.5 && horizontalCollision) || (verticalImpact > explodeSpeed && verticalCollision); + if (crash) { + getWorld().createExplosion(this, getX(), getY(), getZ(), 7.0f, World.ExplosionSourceType.TNT); + remove(RemovalReason.KILLED); + return; + } + } - if (!hasPassenger) { - playServerAnimation(null); - } - else if (enginePower < 0.05f) { - playServerAnimation(PlaneAnimation.START_ENGINE); - } - else if (isOnGround()) { - playServerAnimation(PlaneAnimation.LAND_STARTED); - } - else { - playServerAnimation(PlaneAnimation.FLYING); - } + // Stop tiny movements + if (velocity.lengthSquared() < 0.005) velocity = Vec3d.ZERO; + setVelocity(velocity); + // ----------------------------- + // Animation sync + // ----------------------------- + boolean hasPassenger = getControllingPassenger() != null; + if (!hasPassenger) playServerAnimation(null); + else if (enginePower < 0.05f) playServerAnimation(PlaneAnimation.START_ENGINE); + else if (isOnGround()) playServerAnimation(PlaneAnimation.LAND_STARTED); + else playServerAnimation(PlaneAnimation.FLYING); + } + + @Override + protected void removePassenger(Entity passenger) { + super.removePassenger(passenger); + if (passenger instanceof PlayerEntity player) { + player.setInvisible(false); + } } @Override diff --git a/src/main/java/dev/tggamesyt/szar/PlayerMovementManager.java b/src/main/java/dev/tggamesyt/szar/PlayerMovementManager.java new file mode 100644 index 0000000..fcabe27 --- /dev/null +++ b/src/main/java/dev/tggamesyt/szar/PlayerMovementManager.java @@ -0,0 +1,53 @@ +package dev.tggamesyt.szar; + +import net.fabricmc.fabric.api.networking.v1.PacketSender; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.Identifier; + +import java.util.HashMap; +import java.util.Map; + +public class PlayerMovementManager { + + // Packet ID + public static final Identifier PACKET_ID = new Identifier("szar", "player_movement"); + + // Stores current key state per player + private static final Map playerStates = new HashMap<>(); + + // Represents pressed keys + public static class MovementState { + public boolean forwardPressed = false; + public boolean backwardPressed = false; + } + + // Server-side init (register packet receiver) + public static void init() { + ServerPlayNetworking.registerGlobalReceiver(PACKET_ID, (server, player, handler, buf, responseSender) -> { + boolean forward = buf.readBoolean(); + boolean backward = buf.readBoolean(); + + server.execute(() -> { + MovementState state = playerStates.computeIfAbsent(player, k -> new MovementState()); + state.forwardPressed = forward; + state.backwardPressed = backward; + }); + }); + } + + // Helper to get player state + public static boolean isForwardPressed(ServerPlayerEntity player) { + return playerStates.getOrDefault(player, new MovementState()).forwardPressed; + } + + public static boolean isBackwardPressed(ServerPlayerEntity player) { + return playerStates.getOrDefault(player, new MovementState()).backwardPressed; + } + + // Optional: clear state when player disconnects + public static void removePlayer(ServerPlayerEntity player) { + playerStates.remove(player); + } +} diff --git a/src/main/java/dev/tggamesyt/szar/Szar.java b/src/main/java/dev/tggamesyt/szar/Szar.java index 669d521..064b80d 100644 --- a/src/main/java/dev/tggamesyt/szar/Szar.java +++ b/src/main/java/dev/tggamesyt/szar/Szar.java @@ -293,6 +293,7 @@ public class Szar implements ModInitializer { private final Map sleepingPlayers = new HashMap<>(); @Override public void onInitialize() { + PlayerMovementManager.init(); ServerCosmetics.init(); ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { ServerPlayerEntity player = handler.getPlayer(); diff --git a/src/main/java/dev/tggamesyt/szar/mixin/LivingEntityFallDamageMixin.java b/src/main/java/dev/tggamesyt/szar/mixin/LivingEntityFallDamageMixin.java new file mode 100644 index 0000000..84f0e2d --- /dev/null +++ b/src/main/java/dev/tggamesyt/szar/mixin/LivingEntityFallDamageMixin.java @@ -0,0 +1,27 @@ +package dev.tggamesyt.szar.mixin; + +import dev.tggamesyt.szar.PlaneEntity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.entity.effect.StatusEffectInstance; +import net.minecraft.entity.effect.StatusEffects; +import net.minecraft.util.math.MathHelper; +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.CallbackInfoReturnable; + +@Mixin(LivingEntity.class) +public abstract class LivingEntityFallDamageMixin { + + // This injects at the start of computeFallDamage + @Inject(method = "computeFallDamage", at = @At("HEAD"), cancellable = true) + private void preventFallDamageIfOnPlane(float fallDistance, float damageMultiplier, CallbackInfoReturnable cir) { + LivingEntity self = (LivingEntity) (Object) this; + + // Check if the entity is a player riding a PlaneEntity + if (self.hasVehicle() && self.getVehicle() instanceof PlaneEntity) { + cir.setReturnValue(0); // Cancel fall damage + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/tggamesyt/szar/mixin/PlaneBlockInteractionMixin.java b/src/main/java/dev/tggamesyt/szar/mixin/PlaneBlockInteractionMixin.java new file mode 100644 index 0000000..785d2c8 --- /dev/null +++ b/src/main/java/dev/tggamesyt/szar/mixin/PlaneBlockInteractionMixin.java @@ -0,0 +1,34 @@ +package dev.tggamesyt.szar.mixin; + +import dev.tggamesyt.szar.PlaneEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.network.ServerPlayerInteractionManager; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +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.CallbackInfoReturnable; + +@Mixin(ServerPlayerInteractionManager.class) +public class PlaneBlockInteractionMixin { + + @Inject(method = "interactBlock", at = @At("HEAD"), cancellable = true) + private void interactBlock(ServerPlayerEntity player, World world, ItemStack stack, Hand hand, BlockHitResult hitResult, CallbackInfoReturnable cir) { + if (player.getVehicle() instanceof PlaneEntity) { + cir.setReturnValue(ActionResult.FAIL); + } + } + + @Inject(method = "tryBreakBlock", at = @At("HEAD"), cancellable = true) + private void preventBlockBreaking(BlockPos pos, CallbackInfoReturnable cir) { + ServerPlayerEntity player = ((ServerPlayerInteractionManager)(Object)this).player; + if (player.getVehicle() instanceof PlaneEntity) { + cir.setReturnValue(false); + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/tggamesyt/szar/mixin/PlayerInteractionMixin.java b/src/main/java/dev/tggamesyt/szar/mixin/PlayerInteractionMixin.java new file mode 100644 index 0000000..8ae7c62 --- /dev/null +++ b/src/main/java/dev/tggamesyt/szar/mixin/PlayerInteractionMixin.java @@ -0,0 +1,23 @@ +package dev.tggamesyt.szar.mixin; + +import dev.tggamesyt.szar.PlaneEntity; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +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.CallbackInfoReturnable; + +@Mixin(PlayerEntity.class) +public class PlayerInteractionMixin { + + @Inject(method = "interact", at = @At("HEAD"), cancellable = true) + private void preventPlaneInteraction(Entity entity, Hand hand, CallbackInfoReturnable cir) { + PlayerEntity player = (PlayerEntity) (Object) this; + if (player.getVehicle() instanceof PlaneEntity) { + cir.setReturnValue(ActionResult.FAIL); // cancel interaction + } + } +} \ No newline at end of file diff --git a/src/main/resources/data/szar/advancements/two_towers_explosion.json b/src/main/resources/data/szar/advancements/two_towers_explosion.json index 72523e3..1d1cca5 100644 --- a/src/main/resources/data/szar/advancements/two_towers_explosion.json +++ b/src/main/resources/data/szar/advancements/two_towers_explosion.json @@ -6,7 +6,7 @@ }, "display": { "icon": { "item": "minecraft:tnt" }, - "title": "Too Close", + "title": "Hmm, familiar...", "description": "You were there when the towers fell", "frame": "challenge", "show_toast": true, diff --git a/src/main/resources/szar.accesswidener b/src/main/resources/szar.accesswidener index 56dccd0..8a4a318 100644 --- a/src/main/resources/szar.accesswidener +++ b/src/main/resources/szar.accesswidener @@ -2,4 +2,5 @@ accessWidener v2 named accessible field net/minecraft/client/gui/hud/InGameHud spyglassScale F accessible field net/minecraft/entity/LivingEntity jumping Z -accessible class net/minecraft/client/gui/hud/InGameHud$HeartType \ No newline at end of file +accessible class net/minecraft/client/gui/hud/InGameHud$HeartType +accessible field net/minecraft/server/network/ServerPlayerInteractionManager player Lnet/minecraft/server/network/ServerPlayerEntity; \ No newline at end of file diff --git a/src/main/resources/szar.mixins.json b/src/main/resources/szar.mixins.json index 920619b..ab2704e 100644 --- a/src/main/resources/szar.mixins.json +++ b/src/main/resources/szar.mixins.json @@ -4,9 +4,12 @@ "package": "dev.tggamesyt.szar.mixin", "compatibilityLevel": "JAVA_17", "mixins": [ - "PlayerEntityMixin", "CraftingScreenHandlerMixin", "CraftingScreenHandlerMixin2", + "LivingEntityFallDamageMixin", + "PlaneBlockInteractionMixin", + "PlayerEntityMixin", + "PlayerInteractionMixin", "RadiatedItemMixin" ], "injectors": {