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 { 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 parts = new LinkedHashMap<>(); private final boolean friendly; private record FootGround(int tick, float left, float right) {} private final Map groundCache = new WeakHashMap<>(); private record WallPlane(Vec3 point, Direction normal) { } private record PassageSpan(int tick, WallPlane leftWall, WallPlane rightWall) { } private final Map passageCache = new WeakHashMap<>(); private record PassageBlend(float age, float leftDistance, float rightDistance, float leftWeight, float rightWeight) { } private final Map passageBlend = new WeakHashMap<>(); private record ClimbingWall(int tick, WallPlane wall) { } private final Map climbingCache = new WeakHashMap<>(); private final Map> 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 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 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 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 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 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 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); } }