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