Volver al índice

src/main/java/ar/com/companeros/horror/client/ParasiteModel.java

package ar.com.companeros.horror.client;

import ar.com.companeros.Companeros;
import ar.com.companeros.horror.ParasiteEntity;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.WeakHashMap;
import net.minecraft.client.model.HierarchicalModel;
import net.minecraft.client.model.geom.ModelLayerLocation;
import net.minecraft.client.model.geom.ModelPart;
import net.minecraft.client.model.geom.PartPose;
import net.minecraft.client.model.geom.builders.CubeListBuilder;
import net.minecraft.client.model.geom.builders.LayerDefinition;
import net.minecraft.client.model.geom.builders.MeshDefinition;
import net.minecraft.client.model.geom.builders.PartDefinition;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.util.Mth;
import net.minecraft.world.level.ClipContext;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
import net.minecraft.world.phys.Vec3;

/** Adapta el esqueleto compartido al renderizador nativo de Minecraft. */
public final class ParasiteModel extends HierarchicalModel<ParasiteEntity> {
    public static final ModelLayerLocation LAYER = new ModelLayerLocation(new ResourceLocation(Companeros.ID, "parasite"), "main");
    public static final ModelLayerLocation FRIENDLY_LAYER = new ModelLayerLocation(new ResourceLocation(Companeros.ID, "parasite_friendly"), "main");
    private final ModelPart root;
    private final Map<String, ModelPart> parts = new LinkedHashMap<>();
    private final boolean friendly;
    private record FootGround(int tick, float left, float right) {}
    private final Map<ParasiteEntity, FootGround> groundCache = new WeakHashMap<>();
    private record WallPlane(Vec3 point, Direction normal) { }
    private record PassageSpan(int tick, WallPlane leftWall, WallPlane rightWall) { }
    private final Map<ParasiteEntity, PassageSpan> passageCache = new WeakHashMap<>();
    private record PassageBlend(float age, float leftDistance, float rightDistance, float leftWeight, float rightWeight) { }
    private final Map<ParasiteEntity, PassageBlend> passageBlend = new WeakHashMap<>();
    private record ClimbingWall(int tick, WallPlane wall) { }
    private final Map<ParasiteEntity, ClimbingWall> climbingCache = new WeakHashMap<>();
    private final Map<ParasiteEntity, Map<String, ParasiteRig.Pose>> observedPoses = new WeakHashMap<>();

    public ParasiteModel(ModelPart root, boolean friendly) {
        this.root = root;
        this.friendly = friendly;
        for (ParasiteRig.Bone bone : ParasiteRig.bones(friendly)) {
            ModelPart parent = bone.parent().isEmpty() ? root : parts.get(bone.parent());
            parts.put(bone.name(), parent.getChild(bone.name()));
        }
    }

    @Override public ModelPart root() { return root; }
    public static LayerDefinition createBodyLayer() { return geometry(false); }
    public static LayerDefinition createFriendlyLayer() { return geometry(true); }

    // GEOMETRÍA: el mismo conjunto de cajas se exporta para la vista previa.
    private static LayerDefinition geometry(boolean friendly) {
        MeshDefinition mesh = new MeshDefinition();
        Map<String, PartDefinition> definitions = new LinkedHashMap<>();
        for (ParasiteRig.Bone bone : ParasiteRig.bones(friendly)) {
            CubeListBuilder cubes = CubeListBuilder.create();
            for (ParasiteRig.Box box : bone.boxes()) {
                cubes.texOffs(box.u(), box.v()).mirror(box.mirror())
                    .addBox(box.x(), box.y(), box.z(), box.w(), box.h(), box.d());
            }
            PartDefinition parent = bone.parent().isEmpty() ? mesh.getRoot() : definitions.get(bone.parent());
            definitions.put(bone.name(), parent.addOrReplaceChild(bone.name(), cubes,
                PartPose.offset(bone.x(), bone.y(), bone.z())));
        }
        return LayerDefinition.create(mesh, ParasiteRig.ATLAS_SIZE, ParasiteRig.ATLAS_SIZE);
    }

    // ANIMACIÓN: estado sincronizado y movimiento local; no consulta al modelo de IA.
    @Override public void setupAnim(ParasiteEntity entity, float limbSwing, float limbAmount,
                                    float age, float yaw, float pitch) {
        float partialTick = Mth.clamp(age - entity.tickCount, 0, 1);
        // Trepar usa altura recorrida: el paso horizontal de vanilla puede quedar quieto.
        if (entity.isClimbing() && !friendly) {
            limbSwing = (float) Mth.lerp(partialTick, entity.yo, entity.getY()) * 16F;
            limbAmount = Mth.clamp((float) Math.abs(entity.getY() - entity.yo) * 5F, 0, 1);
        }
        ParasiteRig.State flat = new ParasiteRig.State(entity.getMutationStage(), limbSwing,
            limbAmount, age, yaw, pitch, entity.isPursuing(), entity.isClimbing(),
            entity.isAmbushing(), entity.isCrawling(), entity.getEncounterTicks(), entity.getAttackAnimationTicks(),
            entity.getCompactProgress(partialTick));
        FootGround ground = friendly || entity.isClimbing() ? new FootGround(entity.tickCount, 0, 0)
            : sampleGround(entity);
        ParasiteRig.State state = new ParasiteRig.State(flat.stage(), flat.limbSwing(), flat.limbAmount(),
            flat.age(), flat.yaw(), flat.pitch(), flat.pursuing(), flat.climbing(), flat.ambushing(),
            flat.crawling(), flat.encounterTicks(), flat.attackTicks(), flat.compactProgress(), ground.left(), ground.right());
        Map<String, ParasiteRig.Pose> poses = ParasiteRig.poses(state, friendly, entity.getVisualUpgrades());
        // Cada frame evalúa el rig una sola vez. Trepar y apoyar en corredor son excluyentes.
        if (!friendly && state.climbing()) alignClimbingWall(entity, partialTick, poses);
        else if (!friendly && state.compactProgress() >= .18F && state.compactProgress() <= .42F)
            bracePassage(entity, state, partialTick, poses);
        else passageBlend.remove(entity);
        poses.forEach((name, pose) -> {
            ModelPart part = parts.get(name);
            part.x = pose.x; part.y = pose.y; part.z = pose.z;
            part.xRot = pose.xRot; part.yRot = pose.yRot; part.zRot = pose.zRot;
            part.xScale = pose.xScale; part.yScale = pose.yScale; part.zScale = pose.zScale;
            part.visible = pose.visible;
        });
        if (!friendly) {
            if (observedPoses.size() > 16) observedPoses.clear();
            observedPoses.put(entity, poses);
        }
    }

    // TREPADA: acercar los apoyos al plano real de la pared, sin estirar el esqueleto.
    private void alignClimbingWall(ParasiteEntity entity, float partialTick, Map<String, ParasiteRig.Pose> poses) {
        ClimbingWall cached = climbingCache.get(entity);
        if (cached == null || entity.tickCount < cached.tick() || entity.tickCount - cached.tick() >= 4) {
            float yaw = entity.yBodyRot * Mth.DEG_TO_RAD;
            Vec3 origin = entity.position().add(0, Math.min(1, entity.getBbHeight() * .6), 0);
            Vec3 forward = new Vec3(-Math.sin(yaw), 0, Math.cos(yaw));
            BlockHitResult hit = entity.level().clip(new ClipContext(origin, origin.add(forward.scale(.8)),
                ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, entity));
            cached = new ClimbingWall(entity.tickCount, plane(hit));
            if (climbingCache.size() > 16) climbingCache.clear();
            climbingCache.put(entity, cached);
        }
        float yaw = Mth.rotLerp(partialTick, entity.yBodyRotO, entity.yBodyRot) * Mth.DEG_TO_RAD;
        float distance = planeDistance(cached.wall(), entity.getPosition(partialTick),
            new Vec3(-Math.sin(yaw), 0, Math.cos(yaw)));
        if (Float.isFinite(distance) && distance >= .15F && distance <= .75F) {
            // El rig mide apoyos en Z=-8; trasladar la figura conserva sus longitudes.
            poses.get("world").z += 8F - distance * 16F;
        }
    }

    // PASO 1×3: apoya las palmas sobre superficies reales. Dos rayos cada cuatro ticks, sin cargar chunks.
    // Ninguna pared puede convertir el esqueleto en una figura aplastada mediante escala global.
    private void bracePassage(ParasiteEntity entity, ParasiteRig.State state, float partialTick,
                              Map<String, ParasiteRig.Pose> poses) {
        PassageSpan cached = passageCache.get(entity);
        if (cached == null || entity.tickCount < cached.tick() || entity.tickCount - cached.tick() >= 4) {
            float yaw = entity.yBodyRot * Mth.DEG_TO_RAD;
            cached = new PassageSpan(entity.tickCount, wallPlane(entity, yaw, 1, poses), wallPlane(entity, yaw, -1, poses));
            if (passageCache.size() > 16) passageCache.clear();
            passageCache.put(entity, cached);
        }
        float yaw = Mth.rotLerp(partialTick, entity.yBodyRotO, entity.yBodyRot) * Mth.DEG_TO_RAD;
        Vec3 right = new Vec3(Mth.cos(yaw), 0, Mth.sin(yaw));
        Vec3 position = entity.getPosition(partialTick);
        float left = planeDistance(cached.leftWall(), position, right);
        float other = planeDistance(cached.rightWall(), position, right.scale(-1));
        PassageBlend before = passageBlend.get(entity);
        float elapsed = before == null ? 0 : Mth.clamp(state.age() - before.age(), 0, 1);
        float leftWeight = contactWeight(before == null ? 0 : before.leftWeight(), left, elapsed);
        float rightWeight = contactWeight(before == null ? 0 : before.rightWeight(), other, elapsed);
        // Al soltar una superficie, la mano vuelve por el mismo arco durante
        // seis ticks; no cambia de pose de golpe porque terminó el raycast.
        float leftDistance = reachable(left) ? left : before == null ? 0 : before.leftDistance();
        float rightDistance = reachable(other) ? other : before == null ? 0 : before.rightDistance();
        if (passageBlend.size() > 16) passageBlend.clear();
        passageBlend.put(entity, new PassageBlend(state.age(), leftDistance, rightDistance, leftWeight, rightWeight));
        ParasiteRig.braceCorridor(poses, state, leftDistance, rightDistance, leftWeight, rightWeight);
    }
    private static boolean reachable(float distance) { return Float.isFinite(distance) && distance >= .34F && distance <= .75F; }
    private static float contactWeight(float before, float distance, float elapsed) {
        return reachable(distance) ? Math.min(1, before + elapsed / 6) : Math.max(0, before - elapsed / 6);
    }
    private static WallPlane wallPlane(ParasiteEntity entity, float yaw, int side, Map<String, ParasiteRig.Pose> poses) {
        String arm = side > 0 ? "arm_l" : "arm_r";
        ParasiteRig.Pose shoulder = poses.get(arm);
        float[] modelPoint = ParasiteRig.transform(poses, "spine_upper", shoulder.x, shoulder.y, shoulder.z);
        // Consulta a la altura y profundidad donde estará la palma. Un bloque
        // situado sólo a la altura de la cabeza no sirve como apoyo de muñeca.
        float z = modelPoint[2] - 5;
        Vec3 start = new Vec3(entity.getX() + z * Mth.sin(yaw) / 16,
            entity.getY() + (24 - modelPoint[1] - 7.5F) / 16,
            entity.getZ() - z * Mth.cos(yaw) / 16);
        Vec3 end = start.add(side * Mth.cos(yaw) * .9, 0, side * Mth.sin(yaw) * .9);
        if (!entity.level().hasChunkAt(BlockPos.containing(end))) return null;
        BlockHitResult hit = entity.level().clip(new ClipContext(start, end, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, entity));
        return plane(hit);
    }

    // Se conserva el plano del bloque, no una distancia que envejece cuatro ticks.
    // Recalcular desde la posición interpolada mantiene el apoyo fijo mientras camina.
    private static WallPlane plane(BlockHitResult hit) {
        return hit.getType() == HitResult.Type.BLOCK && hit.getDirection().getAxis() != Direction.Axis.Y
            ? new WallPlane(hit.getLocation(), hit.getDirection()) : null;
    }
    private static float planeDistance(WallPlane wall, Vec3 origin, Vec3 direction) {
        if (wall == null) return 0;
        Direction normal = wall.normal();
        double along = direction.x * normal.getStepX() + direction.z * normal.getStepZ();
        if (Math.abs(along) < .05) return 0;
        double separation = (wall.point().x - origin.x) * normal.getStepX()
            + (wall.point().z - origin.z) * normal.getStepZ();
        return (float) (separation / along);
    }

    // TERRENO: dos rayos por entidad cada cuatro ticks; nunca inspecciona bloques por frame.
    private FootGround sampleGround(ParasiteEntity entity) {
        FootGround cached = groundCache.get(entity);
        if (cached != null && entity.tickCount >= cached.tick() && entity.tickCount - cached.tick() < 4) return cached;
        // Observar el último frame evita generar y descartar un segundo esqueleto.
        Map<String, ParasiteRig.Pose> pose = observedPoses.get(entity);
        float[] leftPoint = pose == null ? new float[]{1.7F, 24, .9F}
            : ParasiteRig.transform(pose, "leg_l_hand", 0, 2, 0);
        float[] rightPoint = pose == null ? new float[]{-1.7F, 24, .9F}
            : ParasiteRig.transform(pose, "leg_r_hand", 0, 2, 0);
        float left = footGround(entity, leftPoint);
        float right = footGround(entity, rightPoint);
        FootGround ground = new FootGround(entity.tickCount, left, right);
        if (groundCache.size() > 16) groundCache.clear();
        groundCache.put(entity, ground);
        return ground;
    }

    private static float footGround(ParasiteEntity entity, float[] modelPoint) {
        float yaw = entity.yBodyRot * Mth.DEG_TO_RAD;
        double x = entity.getX() + (modelPoint[0] * Mth.cos(yaw) + modelPoint[2] * Mth.sin(yaw)) / 16;
        double z = entity.getZ() + (modelPoint[0] * Mth.sin(yaw) - modelPoint[2] * Mth.cos(yaw)) / 16;
        if (!entity.level().hasChunkAt(BlockPos.containing(x, entity.getY(), z))) return 0;
        Vec3 start = new Vec3(x, entity.getY() + .55, z);
        Vec3 end = new Vec3(x, entity.getY() - .65, z);
        BlockHitResult hit = entity.level().clip(new ClipContext(start, end,
            ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, entity));
        if (hit.getType() != HitResult.Type.BLOCK || hit.getDirection() != Direction.UP) return 0;
        return Mth.clamp((float)(hit.getLocation().y - entity.getY()), -.4F, .5F);
    }
}