Volver al índice

src/main/java/ar/com/companeros/horror/ParasiteUpgrades.java

package ar.com.companeros.horror;

import ar.com.companeros.decision.*;
import ar.com.companeros.identity.AgentRegistry;
import ar.com.companeros.runtime.AgentRuntime;
import ar.com.companeros.social.AgentCommunication;
import com.google.gson.JsonObject;
import java.nio.charset.StandardCharsets;
import java.util.*;
import net.minecraft.nbt.*;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.ai.attributes.*;
import net.minecraft.world.effect.*;
import net.minecraft.world.item.*;
import net.minecraft.world.level.saveddata.SavedData;
import net.minecraftforge.registries.ForgeRegistries;

/** Propuesta voluntaria → tasación neutral → pago físico → mejora acotada y persistente. */
public final class ParasiteUpgrades extends SavedData {
    public enum Stat { HEALTH, SPEED, DAMAGE, PERCEPTION }
    private final EnumMap<Stat, Integer> levels = new EnumMap<>(Stat.class);
    private final Map<String, Integer> adaptations = new TreeMap<>();
    private int visualUpgrades;
    private int appearance;
    private final ArrayDeque<String> history = new ArrayDeque<>();
    private UUID proposer, quoteId;
    private String requested;
    private String goal = "", explanation = "", status = "Sin propuestas";
    private Map<String, Integer> costs = Map.of();
    private long quotedAt, lastRequest = -24_000, sequence;
    private boolean pending;
    private long defeats, hitsTaken;
    private final Map<String, Long> damageKinds = new TreeMap<>();
    private static final Map<String, Double> VALUE = Map.of("minecraft:iron_ingot", 1D, "minecraft:copper_ingot", .25D,
        "minecraft:gold_ingot", 2D, "minecraft:diamond", 8D, "minecraft:redstone", .5D);

    // Datos del mundo y límites de efectos. No se asigna una alianza por abrir este servicio.
    public static ParasiteUpgrades get(MinecraftServer server) {
        if (!server.isSameThread()) throw new IllegalStateException("Las mejoras usan el hilo del servidor");
        return server.overworld().getDataStorage().computeIfAbsent(ParasiteUpgrades::load, ParasiteUpgrades::new, "raps_parasite_upgrades");
    }
    public int level(Stat stat) { return levels.getOrDefault(stat, 0); }
    public int level(String target) {
        try { return level(Stat.valueOf(target)); }
        catch (IllegalArgumentException failure) { return adaptations.getOrDefault(target, 0); }
    }
    public static String capacity(String name) {
        if (name == null || name.length() > 128) throw new IllegalArgumentException("Capacidad inválida");
        try { return Stat.valueOf(name.toUpperCase(Locale.ROOT)).name(); }
        catch (IllegalArgumentException ignored) { }
        if (Set.of("visual:iron", "visual:diamond", "visual:copper", "visual:obsidian").contains(name)) return name;
        if (name.startsWith("effect:")) {
            ResourceLocation id = ResourceLocation.tryParse(name.substring(7));
            if (id != null && ForgeRegistries.MOB_EFFECTS.containsKey(id)) return "effect:" + id;
        }
        if (name.startsWith("attribute:")) {
            ResourceLocation id = ResourceLocation.tryParse(name.substring(10));
            if (id != null && ForgeRegistries.ATTRIBUTES.containsKey(id)) return "attribute:" + id;
        }
        throw new IllegalArgumentException("Capacidad desconocida");
    }
    public String describe() {
        return status + (costs.isEmpty() ? "" : "; cotización=" + quoteId + " materiales=" + costs)
            + "; mejoras=" + levels + "; adaptaciones=" + adaptations.entrySet().stream().limit(6).toList()
            + " (total=" + adaptations.size() + "); derrotas=" + defeats + "; impactos recibidos=" + hitsTaken;
    }
    public void combatResult(boolean defeated) {
        if (defeated) { defeats++; append("Derrota del huésped; niveles=" + levels); } else hitsTaken++;
        setDirty();
    }
    public void damageObserved(String cause) {
        if (cause == null || cause.length() > 64) return;
        hitsTaken++;
        if (damageKinds.containsKey(cause) || damageKinds.size() < 16) damageKinds.merge(cause, 1L, Long::sum);
        setDirty();
    }

    // Sólo el tasador puede entregar costs: este paso no existe en el catálogo de herramientas del bot.
    public String propose(ServerPlayer bot, String stat, String objective) {
        MinecraftServer server = bot.server;
        if (AgentRegistry.get(server).isHuman(bot) || !HorrorState.get(server).active()) return "No hay un huésped con quien colaborar";
        String wanted;
        try { wanted = capacity(stat); } catch (RuntimeException invalid) { return "Capacidad desconocida"; }
        long time = server.overworld().getGameTime();
        if (objective == null || objective.isBlank() || objective.length() > 220 || objective.contains("\n")) return "Objetivo inválido";
        if (pending || time - lastRequest < 1200) return "Hay una tasación pendiente o en espera";
        if (level(wanted) >= 3) return "La capacidad alcanzó su límite";
        if (!adaptations.containsKey(wanted) && adaptations.size() >= 32) return "El cuerpo ya tiene32 adaptaciones";
        proposer = bot.getUUID(); requested = wanted; goal = objective; quoteId = UUID.randomUUID();
        costs = Map.of(); pending = true; lastRequest = time; sequence++; status = "Tasación solicitada"; setDirty();
        UUID expected = quoteId;
        UUID actor = UUID.nameUUIDFromBytes(("raps-neutral-appraisal:" + AgentRegistry.get(server).worldId()).getBytes(StandardCharsets.UTF_8));
        String effect = switch (wanted) {
            case "HEALTH" -> "+8 de salud máxima, sin curar";
            case "SPEED" -> "+8% velocidad base, sin atravesar bloques";
            case "DAMAGE" -> "+1.5 daño de ataque";
            case "PERCEPTION" -> "+6 bloques de alcance de percepción";
            default -> wanted.startsWith("visual:") ? "Piezas articuladas visibles, sin atributos gratis. El precio debe incluir al menos cuatro unidades del material indicado por nivel: iron_ingot, diamond, copper_ingot u obsidian."
                : wanted.startsWith("effect:") ? "Efecto real del registro, potencia=nivel-1. Si es instantáneo sólo se activa al montar; otros son adaptaciones persistentes."
                : "Atributo real del registro: +2 por nivel, sujeto a límites del atributo. Para velocidad se usa+8% por nivel.";
        };
        List<String> observations = new ArrayList<>(List.of("Capacidad=" + wanted + "; nivel solicitado=" + (level(wanted) + 1) + "; efecto=" + effect,
            "materiales base registrados: minecraft:iron_ingot,minecraft:copper_ingot,minecraft:gold_ingot,minecraft:diamond,minecraft:redstone",
            "impactos recibidos=" + hitsTaken + "; derrotas=" + defeats,
            "Daños observados por causa: " + damageKinds.toString().substring(0, Math.min(400, damageKinds.toString().length()))));
        history.stream().skip(Math.max(0, history.size() - 4)).forEach(observations::add);
        ObservationSnapshot observation = new ObservationSnapshot(AgentRegistry.get(server).worldId(), bot.level().dimension().location().toString(),
            bot.getBlockX(), bot.getBlockY(), bot.getBlockZ(), bot.getHealth(), bot.getFoodData().getFoodLevel(), Map.of(),
            "Cotizar mejora " + wanted, observations, List.of(), List.of("Objetivo solicitado: " + objective));
        ToolCapability quote = new ToolCapability("quote_parasite_upgrade", 1, "Costos para la mejora limitada. Args costs:string JSON itemID:count, rationale:string.", Set.of("APPRAISE"));
        DecisionRequest request = new DecisionRequest(UUID.randomUUID(), actor, 1, sequence, "Tasación neutral de " + wanted,
            observation, Map.of(quote.id(), quote), Set.of("APPRAISE"), DecisionRequest.Kind.UPGRADE_APPRAISAL, null);
        try {
            AgentRuntime.appraiseUpgrade(server, request).whenComplete((answer, failure) -> server.execute(() -> {
                if (!expected.equals(quoteId)) return;
                pending = false; costs = Map.of();
                try {
                    if (failure != null || answer == null || !answer.accepted() || answer.decision().steps().size() != 1)
                        throw new IllegalArgumentException("Sin tasación válida");
                    var step = answer.decision().steps().get(0);
                    if (!step.toolId().equals("quote_parasite_upgrade") || !step.arguments().keySet().equals(Set.of("costs", "rationale")))
                        throw new IllegalArgumentException("Tasación inválida");
                    costs = validateCosts(step.arguments().get("costs"), level(requested) + 1);
                    validateVisualMaterial(requested, costs, level(requested) + 1);
                    explanation = step.arguments().get("rationale");
                    if (explanation.length() > 220) explanation = explanation.substring(0, 220);
                    quotedAt = server.overworld().getGameTime(); status = "Cotización disponible: " + requested;
                } catch (RuntimeException rejected) { status = "La mejora no obtuvo una cotización válida"; }
                append(status + (costs.isEmpty() ? "" : "; materiales=" + costs)); setDirty();
                AgentCommunication.sendInternal(server, proposer, proposer.toString(), describe());
            }));
        } catch (RuntimeException unavailable) { pending = false; status = "La tasación no está disponible"; setDirty(); }
        return status;
    }

    // Validación local del precio: cantidades enteras, registro real, costo mínimo y sin contenedores.
    static Map<String, Integer> validateCosts(String json, int nextLevel) {
        JsonObject data = DecisionCodec.strictObject(json, 2048);
        if (nextLevel < 1 || nextLevel > 3 || data.size() < 1 || data.size() > 8) throw new IllegalArgumentException("Costo fuera de límites");
        Map<String, Integer> result = new TreeMap<>(); int total = 0; double points = 0;
        for (var entry : data.entrySet()) {
            ResourceLocation id = ResourceLocation.tryParse(entry.getKey());
            if (id == null || !ForgeRegistries.ITEMS.containsKey(id) || !entry.getValue().isJsonPrimitive()
                    || !entry.getValue().getAsJsonPrimitive().isNumber()) throw new IllegalArgumentException("Material desconocido");
            Item item = ForgeRegistries.ITEMS.getValue(id);
            if (item == Items.AIR || item.canFitInsideContainerItems() == false || item instanceof BucketItem
                    || item instanceof net.minecraft.world.item.BlockItem block && block.getBlock() instanceof net.minecraft.world.level.block.EntityBlock)
                throw new IllegalArgumentException("Material no consumible");
            int count = entry.getValue().getAsBigDecimal().intValueExact();
            if (count < 1 || count > 512 || (total += count) > 1024) throw new IllegalArgumentException("Cantidad fuera de límite");
            result.put(id.toString(), count); points += VALUE.getOrDefault(id.toString(), 0D) * count;
        }
        if (points < 16 * nextLevel) throw new IllegalArgumentException("Costo insuficiente");
        return Map.copyOf(result);
    }
    static boolean consume(net.minecraft.world.entity.player.Inventory inventory, Map<String, Integer> price) {
        for (var entry : price.entrySet()) {
            Item item = ForgeRegistries.ITEMS.getValue(new ResourceLocation(entry.getKey())); int found = 0;
            for (ItemStack stack : inventory.items) if (stack.is(item) && !stack.hasTag()) found += stack.getCount();
            if (found < entry.getValue()) return false;
        }
        for (var entry : price.entrySet()) {
            Item item = ForgeRegistries.ITEMS.getValue(new ResourceLocation(entry.getKey())); int left = entry.getValue();
            for (ItemStack stack : inventory.items) if (stack.is(item) && !stack.hasTag() && left > 0) {
                int count = Math.min(left, stack.getCount()); stack.shrink(count); left -= count;
            }
        }
        inventory.setChanged(); return true;
    }

    // Montaje físico: mismo mundo, cerca del cuerpo canónico, pago completo y una sola aplicación.
    public String apply(ServerPlayer bot, String quotedId) {
        MinecraftServer server = bot.server;
        if (AgentRegistry.get(server).isHuman(bot) || pending || costs.isEmpty() || quoteId == null
                || !quoteId.toString().equals(quotedId)) return "Cotización no disponible";
        if (server.overworld().getGameTime() - quotedAt > 24_000 || level(requested) >= 3) return "Cotización vencida";
        ParasiteEntity body = HorrorModule.findParasite(server).filter(ParasiteEntity::isAlive).orElse(null);
        if (body == null || body.level() != bot.level() || body.distanceToSqr(bot) > 36) return "Acercate al huésped para montar la mejora";
        if (requested.startsWith("attribute:") && body.getAttribute(ForgeRegistries.ATTRIBUTES.getValue(new ResourceLocation(requested.substring(10)))) == null)
            return "El cuerpo no admite ese atributo";
        validateVisualMaterial(requested, costs, level(requested) + 1);
        if (!consume(bot.getInventory(), costs)) return "Faltan materiales para completar la mejora";
        Map<String, Integer> paid = costs;
        visualUpgrades |= appearanceFor(requested, paid);
        int style = requested.equals("visual:diamond") || (appearanceFor(requested, paid) & 4) != 0 ? 3
            : requested.equals("visual:copper") ? 4 : (appearanceFor(requested, paid) & 1) != 0 ? 1 : 2;
        if (appearanceFor(requested, paid) != 0) appearance = style | style << 3 | style << 6;
        try { levels.put(Stat.valueOf(requested), level(requested) + 1); }
        catch (IllegalArgumentException dynamic) { adaptations.put(requested, level(requested) + 1); }
        costs = Map.of();
        applyAttributes(body); status = "Mejora montada: " + requested + " nivel " + level(requested);
        append(status + "; bot=" + bot.getUUID() + "; pagó=" + paid); setDirty();
        if (requested.startsWith("effect:")) {
            MobEffect effect = ForgeRegistries.MOB_EFFECTS.getValue(new ResourceLocation(requested.substring(7)));
            if (effect != null && effect.isInstantenous()) effect.applyInstantenousEffect(bot, bot, body, level(requested) - 1, 1);
        }
        server.getPlayerList().saveAll();
        server.overworld().getDataStorage().save(); return status;
    }
    public void applyAttributes(ParasiteEntity body) {
        body.setVisualUpgrades(appearance);
        for (Stat stat : Stat.values()) {
            Attribute attribute = switch (stat) { case HEALTH -> Attributes.MAX_HEALTH; case SPEED -> Attributes.MOVEMENT_SPEED;
                case DAMAGE -> Attributes.ATTACK_DAMAGE; case PERCEPTION -> Attributes.FOLLOW_RANGE; };
            AttributeInstance instance = body.getAttribute(attribute); if (instance == null) continue;
            UUID id = UUID.nameUUIDFromBytes(("raps-upgrade-" + stat).getBytes(StandardCharsets.UTF_8));
            double amount = level(stat) * switch (stat) { case HEALTH -> 8D; case SPEED -> .08D; case DAMAGE -> 1.5D; case PERCEPTION -> 6D; };
            AttributeModifier old = instance.getModifier(id);
            if (old != null && old.getAmount() == amount) continue;
            instance.removeModifier(id);
            if (amount > 0) instance.addTransientModifier(new AttributeModifier(id, "raps_upgrade_" + stat, amount,
                stat == Stat.SPEED ? AttributeModifier.Operation.MULTIPLY_BASE : AttributeModifier.Operation.ADDITION));
        }
        for (var adaptation : adaptations.entrySet()) {
            if (adaptation.getKey().startsWith("effect:")) {
                MobEffect effect = ForgeRegistries.MOB_EFFECTS.getValue(new ResourceLocation(adaptation.getKey().substring(7)));
                if (effect != null && !effect.isInstantenous()) {
                    MobEffectInstance current = body.getEffect(effect);
                    if (current == null || current.getDuration() < 60 || current.getAmplifier() < adaptation.getValue() - 1)
                        body.addEffect(new MobEffectInstance(effect, 160, adaptation.getValue() - 1, true, false, false));
                }
            } else if (adaptation.getKey().startsWith("attribute:")) {
                Attribute attribute = ForgeRegistries.ATTRIBUTES.getValue(new ResourceLocation(adaptation.getKey().substring(10)));
                AttributeInstance instance = attribute == null ? null : body.getAttribute(attribute);
                if (instance == null) continue;
                UUID id = UUID.nameUUIDFromBytes(("raps-adaptation-" + adaptation.getKey()).getBytes(StandardCharsets.UTF_8));
                double amount = adaptation.getValue() * (attribute == Attributes.MOVEMENT_SPEED ? .08D : 2D);
                AttributeModifier old = instance.getModifier(id);
                if (old != null && old.getAmount() == amount) continue;
                instance.removeModifier(id); instance.addTransientModifier(new AttributeModifier(id, "raps_adaptation", amount,
                    attribute == Attributes.MOVEMENT_SPEED ? AttributeModifier.Operation.MULTIPLY_BASE : AttributeModifier.Operation.ADDITION));
            }
        }
    }
    public static List<String> catalog(String query) {
        String match = query.toLowerCase(Locale.ROOT);
        List<String> result = new ArrayList<>();
        for (String visual : List.of("iron", "diamond", "copper", "obsidian")) if (("visual:" + visual).contains(match)) result.add("visual:" + visual);
        ForgeRegistries.MOB_EFFECTS.getKeys().stream().sorted().map(id -> "effect:" + id)
            .filter(id -> id.contains(match)).limit(24).forEach(result::add);
        ForgeRegistries.ATTRIBUTES.getKeys().stream().sorted().map(id -> "attribute:" + id)
            .filter(id -> id.contains(match)).limit(16).forEach(result::add);
        return result;
    }
    private void append(String message) { while (history.size() >= 12) history.removeFirst(); history.addLast(message.length() > 512 ? message.substring(0, 512) : message); }

    // Persistencia: una cotización del modelo se revalida al cargar; nunca guarda una llamada pendiente.
    public static ParasiteUpgrades load(CompoundTag tag) {
        ParasiteUpgrades state = new ParasiteUpgrades();
        state.visualUpgrades = tag.getInt("VisualUpgrades") & 15;
        state.appearance = tag.getInt("Appearance") & 511;
        for (Stat stat : Stat.values()) state.levels.put(stat, Math.max(0, Math.min(3, tag.getInt(stat.name()))));
        CompoundTag adaptations = tag.getCompound("Adaptations");
        for (String target : adaptations.getAllKeys().stream().sorted().limit(32).toList()) {
            try { String valid = capacity(target); int level = Math.max(0, Math.min(3, adaptations.getInt(target)));
                if (level > 0 && (valid.startsWith("effect:") || valid.startsWith("attribute:") || valid.startsWith("visual:"))) state.adaptations.put(valid, level);
            } catch (RuntimeException invalid) { }
        }
        state.defeats = Math.max(0, tag.getLong("Defeats")); state.hitsTaken = Math.max(0, tag.getLong("Hits"));
        CompoundTag damage = tag.getCompound("DamageKinds");
        for (String cause : damage.getAllKeys().stream().sorted().limit(16).toList())
            if (cause.length() <= 64) state.damageKinds.put(cause, Math.max(0, damage.getLong(cause)));
        state.lastRequest = tag.getLong("LastRequest"); state.sequence = Math.max(0, tag.getLong("Sequence"));
        ListTag history = tag.getList("History", Tag.TAG_STRING);
        for (int i = 0; i < Math.min(12, history.size()); i++) state.append(history.getString(i));
        try {
            if (tag.hasUUID("Proposer") && tag.hasUUID("Quote")) {
                state.proposer = tag.getUUID("Proposer"); state.quoteId = tag.getUUID("Quote"); state.requested = capacity(tag.getString("Stat"));
                state.costs = validateCosts(tag.getString("Costs"), state.level(state.requested) + 1);
                state.quotedAt = tag.getLong("QuotedAt"); state.status = "Cotización disponible: " + state.requested;
            }
        } catch (RuntimeException rejected) { state.costs = Map.of(); }
        return state;
    }
    @Override public CompoundTag save(CompoundTag tag) {
        tag.putInt("VisualUpgrades", visualUpgrades);
        tag.putInt("Appearance", appearance);
        for (Stat stat : Stat.values()) tag.putInt(stat.name(), level(stat));
        CompoundTag adaptationData = new CompoundTag(); adaptations.forEach(adaptationData::putInt); tag.put("Adaptations", adaptationData);
        tag.putLong("Defeats", defeats); tag.putLong("Hits", hitsTaken); tag.putLong("LastRequest", lastRequest); tag.putLong("Sequence", sequence);
        CompoundTag damage = new CompoundTag(); damageKinds.forEach(damage::putLong); tag.put("DamageKinds", damage);
        ListTag records = new ListTag(); history.forEach(message -> records.add(StringTag.valueOf(message))); tag.put("History", records);
        if (!costs.isEmpty() && proposer != null) {
            tag.putUUID("Proposer", proposer); tag.putUUID("Quote", quoteId); tag.putString("Stat", requested);
            JsonObject price = new JsonObject(); costs.forEach(price::addProperty); tag.putString("Costs", price.toString()); tag.putLong("QuotedAt", quotedAt);
        }
        return tag;
    }
    static void validateVisualMaterial(String requested, Map<String, Integer> cost, int nextLevel) {
        if (!requested.startsWith("visual:")) return;
        String material = switch (requested) {
            case "visual:iron" -> "minecraft:iron_ingot";
            case "visual:diamond" -> "minecraft:diamond";
            case "visual:copper" -> "minecraft:copper_ingot";
            case "visual:obsidian" -> "minecraft:obsidian";
            default -> throw new IllegalArgumentException("Aspecto desconocido");
        };
        if (cost.getOrDefault(material, 0) < nextLevel * 4) throw new IllegalArgumentException("Falta material para fabricar las piezas visibles");
    }
    public String appearance(ServerPlayer bot, String prefab, String slot) {
        ParasiteEntity body = HorrorModule.findParasite(bot.server).orElse(null);
        if (AgentRegistry.get(bot.server).isHuman(bot) || body == null || bot.level() != body.level() || bot.distanceToSqr(body) > 36)
            return "Acercate al huésped para montar la pieza";
        int style = switch (prefab) { case "obsidian" -> 1; case "iron" -> 2; case "diamond" -> 3; case "copper" -> 4; case "none" -> 0; default -> -1; };
        int index = switch (slot) { case "chest" -> 0; case "arms" -> 1; case "legs" -> 2; default -> -1; };
        if (style < 0 || index < 0) return "Prefab o articulación desconocido";
        int flag = style == 1 ? 1 : style == 2 ? 2 : style == 3 ? 4 : style == 4 ? 8 : 0;
        if (flag != 0 && (visualUpgrades & flag) == 0) return "Primero hay que fabricar y pagar ese prefab";
        appearance = appearance & ~(7 << (index * 3)) | style << (index * 3);
        setDirty(); applyAttributes(body); bot.server.overworld().getDataStorage().save();
        return "Aspecto montado: " + prefab + " en " + slot;
    }
    static int appearanceFor(String requested, Map<String, Integer> paid) {
        if (requested.equals("effect:minecraft:fire_resistance") || requested.equals("visual:obsidian")) return 1;
        if (requested.equals("visual:diamond")) return 4;
        if (requested.equals("visual:copper")) return 8;
        if (requested.equals("visual:iron")) return 2;
        if (requested.equals("attribute:minecraft:generic.armor") || requested.equals("attribute:minecraft:generic.armor_toughness"))
            return paid.getOrDefault("minecraft:diamond", 0) > 0 ? 4 : 2;
        return 0;
    }
}