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