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);
}
}