Volver al índice

src/main/java/ar/com/companeros/identity/HostSkinAtlas.java

package ar.com.companeros.identity;

import java.util.Arrays;

/** Compositor de píxeles compartido por el renderizador y las imágenes de presentación. */
public final class HostSkinAtlas {
    private HostSkinAtlas() { }

    // La piel de jugador conserva sus coordenadas originales dentro del atlas 128×128.
    public static int[] compose(int[] skinArgb, int height, int stage) {
        if (skinArgb == null || skinArgb.length != 64 * height || (height != 32 && height != 64) || stage < 0 || stage > 4)
            throw new IllegalArgumentException("Skin o etapa inválida");
        int[] skin = new int[64 * 64];
        System.arraycopy(skinArgb, 0, skin, 0, skinArgb.length);
        if (height == 32) {
            mirrorLimb(skin, 40, 16, 32, 48);
            mirrorLimb(skin, 0, 16, 16, 48);
            boolean opaqueHat = true;
            for (int y = 0; y < 16; y++) for (int x = 32; x < 64; x++) if ((skin[y * 64 + x] >>> 24) < 128) opaqueHat = false;
            if (opaqueHat) for (int y = 0; y < 16; y++) for (int x = 32; x < 64; x++) skin[y * 64 + x] &= 0xFFFFFF;
        } else if (slim(skin)) {
            widenArm(skin, 40, 16); widenArm(skin, 40, 32);
            widenArm(skin, 32, 48); widenArm(skin, 48, 48);
        }
        // La segunda capa se proyecta sobre el mismo cuerpo: conserva pelo, gorros y ropa.
        flatten(skin, 32, 0, 0, 0, 32, 16);
        if (height == 64) {
            flatten(skin, 16, 32, 16, 16, 24, 16);
            flatten(skin, 40, 32, 40, 16, 16, 16);
            flatten(skin, 0, 32, 0, 16, 16, 16);
            flatten(skin, 48, 48, 32, 48, 16, 16);
            flatten(skin, 0, 48, 16, 48, 16, 16);
        }
        int[] atlas = new int[128 * 128];
        int skinTone = averageFace(skin);
        Arrays.fill(atlas, skinTone);
        for (int y = 0; y < 64; y++) System.arraycopy(skin, y * 64, atlas, y * 128, 64);
        // La boca y los dientes ocupan UV independientes; nunca pintan cavidades sobre la cara.
        fill(atlas, 96, 96, 16, 16, 0xFF271A18);
        fill(atlas, 112, 96, 16, 16, 0xFFD6CDB7);
        metal(atlas, 64, 64, 0xFF26212B, 0xFF56314F);
        metal(atlas, 96, 64, 0xFF25AAB4, 0xFF98F2E8);
        metal(atlas, 64, 96, 0xFF737F88, 0xFFCFD7D8);
        metal(atlas, 64, 32, 0xFF94512C, 0xFFCE8653);
        if (stage > 0) {
            // Heridas propias sobre los UV originales: bordes oscuros, tejido rojo y pequeñas escurridas.
            scar(atlas, 14, 10, stage + 1, 1, stage); // mejilla, conserva ojos y pelo
            scar(atlas, 44, 23, 3 + stage, 1, stage);
            scar(atlas, 37, 54, 2 + stage, -1, stage);
            scar(atlas, 24, 23, stage + 1, 1, stage);
            if (stage >= 2) { scar(atlas, 10, 13, 2, 1, stage); scar(atlas, 5, 25, 3, 1, stage); }
            if (stage >= 3) { scar(atlas, 26, 21, 5, -1, stage); scar(atlas, 22, 55, 4, 1, stage); }
        }
        // PIEL EXPUESTA: net propio para antebrazos y cuello, sin alterar ropa ni skin original.
        // Una tapa de cubo intermedia no puede reutilizar el hombro vestido de vanilla.
        exposedArm(atlas, 40, 16, 64);
        exposedArm(atlas, 32, 48, 80);
        for (int y = 0; y < 8; y++) for (int x = 0; x < 16; x++)
            atlas[(12 + y) * 128 + 64 + x] = atlas[(28 + y % 4) * 128 + 44 + x % 4];
        return atlas;
    }

    // NET 4×6×4: paredes del antebrazo, tapa de codo y cap original de la mano en la muñeca.
    private static void exposedArm(int[] atlas, int sourceU, int sourceV, int targetU) {
        for (int side = 0; side < 4; side++)
            copyPatch(atlas, sourceU + side * 4, sourceV + 10, targetU + side * 4, 4, 4, 6);
        copyPatch(atlas, sourceU + 4, sourceV + 12, targetU + 4, 0, 4, 4);
        copyPatch(atlas, sourceU + 8, sourceV, targetU + 8, 0, 4, 4);
    }

    private static void copyPatch(int[] atlas, int sx, int sy, int dx, int dy, int width, int height) {
        for (int y = 0; y < height; y++) for (int x = 0; x < width; x++)
            atlas[(dy + y) * 128 + dx + x] = atlas[(sy + y) * 128 + sx + x];
    }
    private static void scar(int[] atlas, int x, int y, int length, int slope, int stage) {
        for (int i = 0; i < length; i++) {
            int px = x + (i / 3) * slope, py = y + i;
            int center = py * 128 + px;
            atlas[center] = (i & 1) == 0 ? 0xFF9B2524 : 0xFF651717;
            if (stage >= 2 && px > 0) atlas[center - 1] = blend(atlas[center - 1], 0xFF4F2020, .65);
            if (stage >= 3) atlas[center + 1] = blend(atlas[center + 1], 0xFFCA5544, .65);
        }
    }
    private static void metal(int[] atlas, int u, int v, int base, int edge) {
        for (int y = 0; y < 32; y++) for (int x = 0; x < 32; x++) {
            double highlight = x % 8 == 0 || y % 8 == 0 ? .65 : (x + y) % 11 == 0 ? .22 : .06;
            atlas[(v + y) * 128 + u + x] = blend(base, edge, highlight);
        }
    }
    private static boolean slim(int[] skin) {
        for (int y = 20; y < 32; y++) if ((skin[y * 64 + 54] >>> 24) != 0 || (skin[y * 64 + 55] >>> 24) != 0) return false;
        return (skin[20 * 64 + 44] >>> 24) != 0;
    }
    private static void widenArm(int[] skin, int u, int v) {
        int[] before = skin.clone();
        copyFace(before, skin, u + 4, v, 3, 4, u + 4, v, 4, 4, false);
        copyFace(before, skin, u + 7, v, 3, 4, u + 8, v, 4, 4, false);
        copyFace(before, skin, u, v + 4, 4, 12, u, v + 4, 4, 12, false);
        copyFace(before, skin, u + 4, v + 4, 3, 12, u + 4, v + 4, 4, 12, false);
        copyFace(before, skin, u + 7, v + 4, 4, 12, u + 8, v + 4, 4, 12, false);
        copyFace(before, skin, u + 11, v + 4, 3, 12, u + 12, v + 4, 4, 12, false);
    }
    private static void mirrorLimb(int[] skin, int fromU, int fromV, int toU, int toV) {
        int[] before = skin.clone();
        for (int side = 0; side < 4; side++) copyFace(before, skin, fromU + side * 4, fromV + 4, 4, 12, toU + side * 4, toV + 4, 4, 12, true);
        copyFace(before, skin, fromU + 4, fromV, 4, 4, toU + 4, toV, 4, 4, true);
        copyFace(before, skin, fromU + 8, fromV, 4, 4, toU + 8, toV, 4, 4, true);
    }
    private static void copyFace(int[] source, int[] destination, int sx, int sy, int sw, int sh, int dx, int dy, int dw, int dh, boolean mirror) {
        for (int y = 0; y < dh; y++) for (int x = 0; x < dw; x++) {
            int sampleX = x * sw / dw; if (mirror) sampleX = sw - sampleX - 1;
            destination[(dy + y) * 64 + dx + x] = source[(sy + y * sh / dh) * 64 + sx + sampleX];
        }
    }
    private static void flatten(int[] skin, int sx, int sy, int dx, int dy, int width, int height) {
        int[] before = skin.clone();
        for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) {
            int top = before[(sy + y) * 64 + sx + x];
            int bottom = before[(dy + y) * 64 + dx + x];
            if ((top >>> 24) != 0) skin[(dy + y) * 64 + dx + x] = blend(bottom, top, (top >>> 24) / 255.0);
        }
    }
    private static int averageFace(int[] skin) {
        long red = 0, green = 0, blue = 0; int count = 0;
        for (int y = 12; y < 15; y++) for (int x = 9; x < 15; x++) {
            int pixel = skin[y * 64 + x]; if ((pixel >>> 24) == 0) continue;
            red += (pixel >>> 16) & 255; green += (pixel >>> 8) & 255; blue += pixel & 255; count++;
        }
        return count == 0 ? 0xFFC58E73 : 0xFF000000 | (int)(red / count) << 16 | (int)(green / count) << 8 | (int)(blue / count);
    }
    private static int blend(int bottom, int top, double amount) {
        int r = (int)(((bottom >>> 16) & 255) * (1 - amount) + ((top >>> 16) & 255) * amount);
        int g = (int)(((bottom >>> 8) & 255) * (1 - amount) + ((top >>> 8) & 255) * amount);
        int b = (int)((bottom & 255) * (1 - amount) + (top & 255) * amount);
        return 0xFF000000 | r << 16 | g << 8 | b;
    }
    private static void fill(int[] image, int x, int y, int width, int height, int color) {
        for (int row = y; row < y + height; row++) Arrays.fill(image, row * 128 + x, row * 128 + x + width, color);
    }
}