Volver al índice

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

package ar.com.companeros.horror;

import java.util.*;
import net.minecraft.nbt.*;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.saveddata.SavedData;

/** Ritmo persistente por jugador. El modelo elige oportunidades; estos límites se cumplen localmente. */
public final class HorrorPacing extends SavedData {
    private int intensity = 35;
    private final Map<UUID, Visit> players = new HashMap<>();
    private static final class Visit {
        float tension, burden;
        long observed = -1, began = -1, restUntil, lastSound = -12000, lastSleep = -12000, huntStarted = -1;
        int hits;
    }
    public static HorrorPacing get(MinecraftServer server) {
        return server.overworld().getDataStorage().computeIfAbsent(HorrorPacing::load, HorrorPacing::new, "raps_horror_pacing");
    }
    public int intensity() { return intensity; }
    public void intensity(int percentage) {
        if (percentage < 0 || percentage > 100) throw new IllegalArgumentException("La intensidad debe estar entre 0 y 100.");
        intensity = percentage; setDirty();
    }
    private Visit visit(UUID id) { return players.computeIfAbsent(id, ignored -> new Visit()); }

    // Una observación por segundo: sólo chunks presentes y jugadores humanos elegibles.
    public void observe(ServerPlayer player, long now) {
        boolean cave = !player.level().canSeeSky(player.blockPosition())
            && player.getY() < player.level().getSeaLevel();
        observe(player.getUUID(), now, cave, player.isSleeping());
    }
    void observe(UUID id, long now, boolean cave, boolean sleeping) {
        Visit v = visit(id);
        float elapsed = v.observed < 0 ? 0 : Math.max(0, now - v.observed) / 20F;
        v.tension = Math.max(0, v.tension - elapsed * .035F);
        v.burden = Math.max(0, v.burden - elapsed * .02F);
        boolean present = now >= v.restUntil && v.began >= 0 && now - v.began < windowTicks();
        if (present && intensity > 0) v.tension = Math.min(100, v.tension
            + Math.min(elapsed, 2) * (.35F + intensity * .015F + (cave ? .7F : 0)));
        if (present && sleeping && now - v.lastSleep >= 1200) {
            v.tension = Math.min(100, v.tension + 18); v.lastSleep = now;
        }
        v.observed = now; setDirty();
    }
    public boolean available(UUID id, long now) { return intensity > 0 && now >= visit(id).restUntil; }
    public long windowTicks() { return 20L * (60 + intensity / 2); }
    public long restTicks() { return 20L * (420 - intensity * 3); }
    public void begin(UUID id, long now) {
        Visit v = visit(id);
        if (v.began < 0) { v.began = now; v.hits = 0; v.huntStarted = -1; setDirty(); }
    }
    public boolean exhausted(UUID id, long now) {
        Visit v = visit(id);
        return !available(id, now) || v.burden >= 65
            || v.began >= 0 && now - v.began >= windowTicks()
            || v.huntStarted >= 0 && now - v.huntStarted >= 20L * (45 + intensity / 3);
    }
    public void release(UUID id, long now) {
        Visit v = visit(id); v.restUntil = Math.max(v.restUntil, now + restTicks());
        v.began = -1; v.huntStarted = -1; v.tension *= .45F; v.burden = Math.min(100, v.burden + 12); setDirty();
    }
    public float priority(UUID id, long now) {
        Visit v = visit(id); return available(id, now) ? v.tension - v.burden * 1.5F : -1000;
    }
    public ParasiteEntity.Intent baseline(UUID id) {
        float tension = visit(id).tension;
        return tension >= 70 ? ParasiteEntity.Intent.AMBUSH : tension >= 42 ? ParasiteEntity.Intent.SCARE : ParasiteEntity.Intent.STALK;
    }
    public boolean mayAttack(UUID id, long now, int stage) {
        return stage >= 2 && !exhausted(id, now) && visit(id).tension >= 70;
    }
    public void hunting(UUID id, long now) {
        Visit v = visit(id); if (v.huntStarted < 0) { v.huntStarted = now; setDirty(); }
    }
    public void hit(UUID id) { Visit v = visit(id); v.hits = Math.min(10000, v.hits + 1); setDirty(); }
    public void killed(UUID id, long now) {
        release(id, now); Visit v = visit(id);
        v.restUntil = Math.max(v.restUntil, now + 20 * 600); v.burden = Math.min(100, v.burden + 25); setDirty();
    }
    public void provoked(UUID id, long now) {
        if (!available(id, now)) return;
        Visit v = visit(id); v.tension = Math.min(100, v.tension + 25); setDirty();
    }
    public boolean soundReady(UUID id, long now) {
        Visit v = visit(id); return available(id, now) && now - v.lastSound >= 20L * (180 - intensity);
    }
    public boolean reserveSound(UUID id, long now) {
        if (!soundReady(id, now)) return false;
        Visit v = visit(id); v.lastSound = now; v.burden = Math.min(100, v.burden + 10); setDirty(); return true;
    }
    public String describe(UUID id, long now) {
        Visit v = visit(id);
        return "tension=" + (int)v.tension + " burden=" + (int)v.burden + " rest_seconds=" + Math.max(0, (v.restUntil - now) / 20)
            + " visit_seconds=" + (v.began < 0 ? 0 : Math.max(0, now - v.began) / 20) + " hits_this_visit=" + v.hits;
    }

    // Los descansos, visitas y avisos sobreviven al reinicio: reconectar no reinicia el acoso.
    @Override public CompoundTag save(CompoundTag tag) {
        tag.putInt("Intensity", intensity); ListTag list = new ListTag();
        players.forEach((id, v) -> {
            CompoundTag row = new CompoundTag(); row.putUUID("Id", id);
            row.putFloat("Tension", v.tension); row.putFloat("Burden", v.burden);
            row.putLong("Observed", v.observed); row.putLong("Began", v.began); row.putLong("RestUntil", v.restUntil);
            row.putLong("Sound", v.lastSound); row.putLong("Sleep", v.lastSleep); row.putLong("Hunt", v.huntStarted);
            row.putInt("Hits", v.hits); list.add(row);
        }); tag.put("Players", list); return tag;
    }
    public static HorrorPacing load(CompoundTag tag) {
        HorrorPacing pacing = new HorrorPacing();
        pacing.intensity = Math.max(0, Math.min(100, tag.contains("Intensity") ? tag.getInt("Intensity") : 35));
        for (Tag item : tag.getList("Players", Tag.TAG_COMPOUND)) {
            CompoundTag row = (CompoundTag)item; if (!row.hasUUID("Id") || pacing.players.size() >= 4096) continue;
            Visit v = new Visit(); v.tension = finite(row.getFloat("Tension")); v.burden = finite(row.getFloat("Burden"));
            v.observed = row.getLong("Observed"); v.began = row.getLong("Began"); v.restUntil = row.getLong("RestUntil");
            v.lastSound = row.getLong("Sound"); v.lastSleep = row.getLong("Sleep"); v.huntStarted = row.getLong("Hunt");
            v.hits = Math.max(0, Math.min(10000, row.getInt("Hits"))); pacing.players.put(row.getUUID("Id"), v);
        } return pacing;
    }
    private static float finite(float value) { return Float.isFinite(value) ? Math.max(0, Math.min(100, value)) : 0; }
}