From 31ea4af3471fcb50bc0ad261f7322f32bcff5eb1 Mon Sep 17 00:00:00 2001 From: DekinDev Date: Thu, 1 Jan 2026 13:12:38 +0100 Subject: [PATCH] License and Notice --- LICENSE | 215 ++++++++++++- NOTICE | 4 + .../smoothdoors/client/anim/SddAnimator.java | 294 ++++++++++++++---- 3 files changed, 435 insertions(+), 78 deletions(-) create mode 100644 NOTICE diff --git a/LICENSE b/LICENSE index aef5211..7ffde57 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,202 @@ -MIT License -Copyright (c) 2025-2026 DekinDev (Straice) + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. + 1. Definitions. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025-2026 DekinDev (Straice) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..836e138 --- /dev/null +++ b/NOTICE @@ -0,0 +1,4 @@ +Smooth Double Doors +Copyright 2025-2026 DekinDev (Straice) + +This product includes software developed by DekinDev, https://straice.com. \ No newline at end of file diff --git a/src/client/java/com/straice/smoothdoors/client/anim/SddAnimator.java b/src/client/java/com/straice/smoothdoors/client/anim/SddAnimator.java index f7d3b67..5caf4c7 100644 --- a/src/client/java/com/straice/smoothdoors/client/anim/SddAnimator.java +++ b/src/client/java/com/straice/smoothdoors/client/anim/SddAnimator.java @@ -7,6 +7,7 @@ import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; import net.minecraft.block.*; import net.minecraft.block.enums.BlockHalf; import net.minecraft.block.enums.DoorHinge; +import net.minecraft.block.enums.DoubleBlockHalf; import net.minecraft.client.MinecraftClient; import net.minecraft.client.render.OverlayTexture; import net.minecraft.client.render.VertexConsumerProvider; @@ -14,8 +15,8 @@ import net.minecraft.client.render.WorldRenderer; import net.minecraft.client.util.math.MatrixStack; import net.minecraft.util.math.*; import net.minecraft.util.math.RotationAxis; -import net.minecraft.util.math.Vec3d; import net.minecraft.world.BlockRenderView; +import net.minecraft.world.World; public final class SddAnimator { @@ -23,35 +24,46 @@ public final class SddAnimator { // Duración base (segundos) a speed=1.0x private static final float BASE_DURATION_S = 0.35f; + private static final float MIN_DURATION_S = 0.05f; private static final Long2ObjectOpenHashMap ANIMS = new Long2ObjectOpenHashMap<>(); - private static boolean hooksInit = false; - private static long lastNs = 0L; private SddAnimator() {} - // === Hook de render (NO mixin a WorldRenderer) === + /** + * Llamar desde SmoothDoubleDoorsClient.onInitializeClient() + * (evita mixins a WorldRenderer y firmas rotas). + */ public static void initClientHooks() { if (hooksInit) return; hooksInit = true; - WorldRenderEvents.LAST.register(ctx -> { - if (MC.world == null) return; + // Render cada frame + WorldRenderEvents.END.register(ctx -> { + if (MC.world == null || ANIMS.isEmpty()) return; + Vec3d camPos = ctx.camera().getPos(); - renderAll(camPos); + float tickDelta = MC.getRenderTickCounter().getTickProgress(true); + + renderAll(camPos, tickDelta); }); } - // === API usada por mixins === + // === API que usan tus mixins === - /** Se usa en BlockRenderManagerMixin para ocultar el bloque vanilla mientras animamos. */ + /** Se usa en BlockRenderManagerMixin para NO meter el modelo vanilla en el chunk mientras animamos. */ public static boolean shouldHideInChunk(BlockPos pos, BlockState state) { return ANIMS.containsKey(pos.asLong()); } - /** Se usa en ClientWorldMixin para arrancar/parar animaciones al cambiar OPEN. */ + /** + * Se llama desde tu WorldMixin (scheduleBlockRerenderIfNeeded). + * Arranca/actualiza animaciones. + */ public static void onBlockUpdate(BlockPos pos, BlockState oldState, BlockState newState) { + if (MC.world == null) return; + Kind kind = kindOf(oldState, newState); if (kind == null) { ANIMS.remove(pos.asLong()); @@ -64,64 +76,52 @@ public final class SddAnimator { SddConfig cfg = SddConfigManager.get(); if (!isAnimationEnabled(cfg, kind)) { - ANIMS.remove(pos.asLong()); + // por seguridad, limpia este pos y (si es puerta) también su otra mitad + removeWithPairIfDoor(pos, newState); return; } float speed = speedFor(cfg, kind); if (speed <= 0.001f) speed = 1.0f; - // Renderizamos SIEMPRE el modelo "cerrado" y aplicamos rotación nosotros. - BlockState base = forceClosedModel(newState, kind); + long startNs = System.nanoTime(); - Anim anim = new Anim(pos.toImmutable(), base, kind, oldOpen, newOpen, speed); - ANIMS.put(pos.asLong(), anim); + if (kind == Kind.DOOR) { + // IMPORTANTÍSIMO: + // 1) animar SIEMPRE LAS 2 MITADES (upper+lower) para que no quede “puerta vanilla” encima + // 2) si cfg.connectDoors está ON, arrancar también la puerta gemela con el MISMO startNs para que vayan a la vez + startDoorAnimationWithPairs(cfg, pos, oldOpen, newOpen, speed, startNs); + return; + } + + // Trapdoor / Fence gate (1 bloque) + BlockState base = forceClosedModel(newState, kind); + putOrReplaceAnim(pos, base, kind, oldOpen, newOpen, speed, startNs); + ensureRerender(pos); } // === Render === - /** Render por frame (se llama desde WorldRenderEvents). */ - public static void renderAll(Vec3d camPos) { - if (ANIMS.isEmpty() || MC.world == null) return; - - tickByTime(); + public static void renderAll(Vec3d camPos, float tickDelta) { + if (MC.world == null || ANIMS.isEmpty()) return; VertexConsumerProvider.Immediate consumers = MC.getBufferBuilders().getEntityVertexConsumers(); MatrixStack matrices = new MatrixStack(); + long nowNs = System.nanoTime(); + for (Anim a : ANIMS.values()) { - renderOne(a, camPos, matrices, consumers); + float t = a.progress01(nowNs); + renderOne(a, t, camPos, matrices, consumers); } - consumers.draw(); // flush - cleanupFinished(); + consumers.draw(); + cleanupFinished(nowNs); } // === Interno === - private static void tickByTime() { - long now = System.nanoTime(); - if (lastNs == 0L) lastNs = now; - float dt = (now - lastNs) / 1_000_000_000.0f; - lastNs = now; - - // cap por si hay alt-tab / lag - dt = MathHelper.clamp(dt, 0.0f, 0.10f); - - for (Anim a : ANIMS.values()) { - float duration = BASE_DURATION_S / a.speed; - if (duration < 0.05f) duration = 0.05f; - - float step = dt / duration; - a.t = MathHelper.clamp(a.t + step, 0.0f, 1.0f); - } - } - - private static void cleanupFinished() { - ANIMS.long2ObjectEntrySet().removeIf(e -> e.getValue().t >= 1.0f); - } - - private static void renderOne(Anim anim, Vec3d camPos, MatrixStack matrices, VertexConsumerProvider consumers) { + private static void renderOne(Anim anim, float t, Vec3d camPos, MatrixStack matrices, VertexConsumerProvider consumers) { if (MC.world == null) return; BlockPos pos = anim.pos; @@ -129,14 +129,14 @@ public final class SddAnimator { matrices.push(); - // Fijo al bloque (no “pegado” a la cámara): worldPos - camPos + // Fijado al bloque (NO a la cámara): world -> camera-relative matrices.translate( pos.getX() - camPos.x, pos.getY() - camPos.y, pos.getZ() - camPos.z ); - float eased = easeInOut(anim.t); + float eased = easeInOut(t); float angleDeg = lerpAngleDeg(anim.fromOpen, anim.toOpen, eased, anim.kind, state); applyTransform(anim.kind, state, angleDeg, matrices); @@ -146,6 +146,148 @@ public final class SddAnimator { matrices.pop(); } + private static void cleanupFinished(long nowNs) { + if (ANIMS.isEmpty() || MC.world == null) return; + + // Cuando termina una animación, hay que forzar rerender para que el chunk vuelva a incluir el bloque vanilla final. + ANIMS.long2ObjectEntrySet().removeIf(e -> { + Anim a = e.getValue(); + if (!a.isFinished(nowNs)) return false; + + ensureRerender(a.pos); + return true; + }); + } + + private static void putOrReplaceAnim(BlockPos pos, BlockState base, Kind kind, + boolean fromOpen, boolean toOpen, + float speed, long startNs) { + + Anim anim = new Anim(pos.toImmutable(), base, kind, fromOpen, toOpen, speed, startNs); + ANIMS.put(pos.asLong(), anim); + } + + private static void removeWithPairIfDoor(BlockPos pos, BlockState state) { + ANIMS.remove(pos.asLong()); + + if (!(state.getBlock() instanceof DoorBlock) || MC.world == null) return; + BlockPos other = otherDoorHalfPos(pos, state); + if (other != null) ANIMS.remove(other.asLong()); + } + + private static void startDoorAnimationWithPairs(SddConfig cfg, BlockPos anyHalfPos, + boolean fromOpen, boolean toOpen, + float speed, long startNs) { + if (MC.world == null) return; + + // Normaliza a LOWER para encontrar gemela de doble puerta de forma estable + BlockPos lowerPos = anyHalfPos; + BlockState anyStateNow = MC.world.getBlockState(anyHalfPos); + + if (!(anyStateNow.getBlock() instanceof DoorBlock)) return; + + if (anyStateNow.get(DoorBlock.HALF) == DoubleBlockHalf.UPPER) { + lowerPos = anyHalfPos.down(); + } + + BlockState lowerState = MC.world.getBlockState(lowerPos); + if (!(lowerState.getBlock() instanceof DoorBlock)) { + // fallback: al menos animar la mitad recibida + putOrReplaceAnim(anyHalfPos, forceClosedModel(anyStateNow, Kind.DOOR), Kind.DOOR, fromOpen, toOpen, speed, startNs); + ensureRerender(anyHalfPos); + return; + } + + // 1) Esta puerta: lower + upper + startDoorBothHalves(lowerPos, lowerState, fromOpen, toOpen, speed, startNs); + + // 2) Doble puerta (gemela), sincronizada + if (cfg.connectDoors) { + BlockPos otherLower = findDoubleDoorOtherLower(lowerPos, lowerState); + if (otherLower != null) { + BlockState otherLowerState = MC.world.getBlockState(otherLower); + // Solo si sigue siendo puerta + if (otherLowerState.getBlock() instanceof DoorBlock) { + startDoorBothHalves(otherLower, otherLowerState, fromOpen, toOpen, speed, startNs); + + // Forzamos rerender ya, aunque la gemela cambie un tick después: evita ver la puerta vanilla encima. + ensureRerender(otherLower); + BlockPos otherUpper = otherLower.up(); + ensureRerender(otherUpper); + } + } + } + + // Forzamos rerender de esta puerta ya + ensureRerender(lowerPos); + ensureRerender(lowerPos.up()); + } + + private static void startDoorBothHalves(BlockPos lowerPos, BlockState lowerState, + boolean fromOpen, boolean toOpen, + float speed, long startNs) { + if (MC.world == null) return; + + // LOWER + BlockState baseLower = forceClosedModel(lowerState, Kind.DOOR); + putOrReplaceAnim(lowerPos, baseLower, Kind.DOOR, fromOpen, toOpen, speed, startNs); + + // UPPER (si existe) + BlockPos upperPos = lowerPos.up(); + BlockState upperState = MC.world.getBlockState(upperPos); + if (upperState.getBlock() instanceof DoorBlock) { + BlockState baseUpper = forceClosedModel(upperState, Kind.DOOR); + putOrReplaceAnim(upperPos, baseUpper, Kind.DOOR, fromOpen, toOpen, speed, startNs); + } + } + + /** + * Encuentra la otra puerta (LOWER) de una doble puerta. + * Regla: + * - misma FACING + * - hinge opuesto + * - vecina en el lado “no bisagra” (donde se juntan) + */ + private static BlockPos findDoubleDoorOtherLower(BlockPos lowerPos, BlockState lowerState) { + if (MC.world == null) return null; + + if (!(lowerState.getBlock() instanceof DoorBlock)) return null; + if (lowerState.get(DoorBlock.HALF) != DoubleBlockHalf.LOWER) return null; + + Direction facing = lowerState.get(DoorBlock.FACING); + DoorHinge hinge = lowerState.get(DoorBlock.HINGE); + + // Si mi bisagra está a la IZQ, mi “lado de unión” es a la DCHA (clockwise), y viceversa. + Direction joinSide = (hinge == DoorHinge.LEFT) ? facing.rotateYClockwise() + : facing.rotateYCounterclockwise(); + + BlockPos otherLower = lowerPos.offset(joinSide); + BlockState other = MC.world.getBlockState(otherLower); + + if (!(other.getBlock() instanceof DoorBlock)) return null; + if (other.get(DoorBlock.HALF) != DoubleBlockHalf.LOWER) return null; + if (other.get(DoorBlock.FACING) != facing) return null; + + DoorHinge otherHinge = other.get(DoorBlock.HINGE); + if (otherHinge == hinge) return null; // deben ser opuestas + + return otherLower; + } + + private static BlockPos otherDoorHalfPos(BlockPos pos, BlockState state) { + if (!(state.getBlock() instanceof DoorBlock)) return null; + return (state.get(DoorBlock.HALF) == DoubleBlockHalf.UPPER) ? pos.down() : pos.up(); + } + + private static void ensureRerender(BlockPos pos) { + if (MC.world == null) return; + + // Esta existe (la estás mixineando). Esto fuerza al motor a reconstruir el chunk. + World w = MC.world; + BlockState s = w.getBlockState(pos); + w.scheduleBlockRerenderIfNeeded(pos, s, s); + } + private static float lerpAngleDeg(boolean fromOpen, boolean toOpen, float t, Kind kind, BlockState state) { float a = angleFor(kind, state, fromOpen); float b = angleFor(kind, state, toOpen); @@ -157,14 +299,24 @@ public final class SddAnimator { switch (kind) { case DOOR -> { + Direction facing = state.get(DoorBlock.FACING); DoorHinge hinge = state.get(DoorBlock.HINGE); - return (hinge == DoorHinge.RIGHT) ? -90.0f : 90.0f; + + // Igual que vanilla (en shapes): al abrir, la “dirección efectiva” rota ±90 según hinge. + Direction openDir = (hinge == DoorHinge.LEFT) ? facing.rotateYClockwise() + : facing.rotateYCounterclockwise(); + + float yawClosed = Direction.getHorizontalDegreesOrThrow(facing); + float yawOpen = Direction.getHorizontalDegreesOrThrow(openDir); + + return MathHelper.wrapDegrees(yawOpen - yawClosed); // +90 o -90 } case TRAPDOOR -> { - BlockHalf half = state.get(TrapdoorBlock.HALF); - return (half == BlockHalf.TOP) ? -90.0f : 90.0f; + // (lo afinamos luego) 90º + return 90.0f; } case FENCE_GATE -> { + // (lo afinamos luego) 90º return 90.0f; } } @@ -181,11 +333,14 @@ public final class SddAnimator { private static void transformDoor(BlockState state, float angleDeg, MatrixStack matrices) { Direction facing = state.get(DoorBlock.FACING); - DoorHinge hinge = state.get(DoorBlock.HINGE); + DoorHinge hinge = state.get(DoorBlock.HINGE); - Direction hingeSide = (hinge == DoorHinge.RIGHT) ? facing.rotateYClockwise() : facing.rotateYCounterclockwise(); - float pivotX = hingeSide.getOffsetX() == 1 ? 1.0f : (hingeSide.getOffsetX() == -1 ? 0.0f : 0.5f); - float pivotZ = hingeSide.getOffsetZ() == 1 ? 1.0f : (hingeSide.getOffsetZ() == -1 ? 0.0f : 0.5f); + // Bisagra en el borde IZQ/DCHA relativo a facing + Direction hingeSide = (hinge == DoorHinge.LEFT) ? facing.rotateYCounterclockwise() + : facing.rotateYClockwise(); + + float pivotX = (hingeSide == Direction.EAST) ? 1.0f : (hingeSide == Direction.WEST ? 0.0f : 0.5f); + float pivotZ = (hingeSide == Direction.SOUTH) ? 1.0f : (hingeSide == Direction.NORTH ? 0.0f : 0.5f); matrices.translate(pivotX, 0.0f, pivotZ); matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(angleDeg)); @@ -195,8 +350,8 @@ public final class SddAnimator { private static void transformTrapdoor(BlockState state, float angleDeg, MatrixStack matrices) { Direction facing = state.get(TrapdoorBlock.FACING); - float pivotX = facing.getOffsetX() == 1 ? 1.0f : (facing.getOffsetX() == -1 ? 0.0f : 0.5f); - float pivotZ = facing.getOffsetZ() == 1 ? 1.0f : (facing.getOffsetZ() == -1 ? 0.0f : 0.5f); + float pivotX = (facing == Direction.EAST) ? 1.0f : (facing == Direction.WEST ? 0.0f : 0.5f); + float pivotZ = (facing == Direction.SOUTH) ? 1.0f : (facing == Direction.NORTH ? 0.0f : 0.5f); matrices.translate(pivotX, 0.5f, pivotZ); @@ -262,7 +417,7 @@ public final class SddAnimator { } private static float easeInOut(float t) { - return t * t * (3.0f - 2.0f * t); + return t * t * (3.0f - 2.0f * t); // smoothstep } public enum Kind { DOOR, TRAPDOOR, FENCE_GATE } @@ -274,17 +429,34 @@ public final class SddAnimator { final boolean fromOpen; final boolean toOpen; final float speed; + final long startNs; + final long durationNs; - float t; // 0..1 + Anim(BlockPos pos, BlockState baseState, Kind kind, + boolean fromOpen, boolean toOpen, float speed, long startNs) { - Anim(BlockPos pos, BlockState baseState, Kind kind, boolean fromOpen, boolean toOpen, float speed) { this.pos = pos; this.baseState = baseState; this.kind = kind; this.fromOpen = fromOpen; this.toOpen = toOpen; this.speed = speed; - this.t = 0.0f; + this.startNs = startNs; + + float dur = BASE_DURATION_S / Math.max(0.001f, speed); + if (dur < MIN_DURATION_S) dur = MIN_DURATION_S; + this.durationNs = (long) (dur * 1_000_000_000L); + } + + float progress01(long nowNs) { + long dt = nowNs - startNs; + if (dt <= 0) return 0.0f; + if (dt >= durationNs) return 1.0f; + return (float) dt / (float) durationNs; + } + + boolean isFinished(long nowNs) { + return nowNs - startNs >= durationNs; } } }