Files
TiedUp-/src/main/java/com/tiedup/remake/personality/PersonalityState.java
NotEvil f6466360b6 Clean repo for open source release
Remove build artifacts, dev tool configs, unused dependencies,
and third-party source dumps. Add proper README, update .gitignore,
clean up Makefile.
2026-04-12 00:51:22 +02:00

710 lines
20 KiB
Java

package com.tiedup.remake.personality;
import com.tiedup.remake.cells.CellDataV2;
import com.tiedup.remake.cells.CellRegistryV2;
import com.tiedup.remake.entities.EntityDamsel;
import java.util.List;
import java.util.UUID;
import org.jetbrains.annotations.Nullable;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.level.ServerLevel;
/**
* Main personality state manager for an NPC.
* Contains personality type, needs, mood, command state, and job experience.
*
* NPCs always obey commands when captured — no refusal, no relationship checks.
*/
public class PersonalityState {
// --- Core personality (immutable after generation) ---
private final PersonalityType personality;
// --- Needs ---
private final NpcNeeds needs = new NpcNeeds();
// --- Current command state ---
private NpcCommand activeCommand = NpcCommand.NONE;
/** Current follow distance (for FOLLOW command) */
private NpcCommand.FollowDistance followDistance =
NpcCommand.FollowDistance.FAR;
@Nullable
private UUID commandingPlayer = null;
@Nullable
private BlockPos commandTarget = null;
/** Secondary command target (for TRANSFER: destination chest) */
@Nullable
private BlockPos commandTarget2 = null;
private int commandProgress = 0;
// --- Struggle state ---
private int struggleTimer = 0;
private int consecutiveStruggleFails = 0;
// --- Mood (calculated from needs) ---
private float mood = 50.0f; // 0-100, 50 is neutral
// --- Name state ---
private boolean hasBeenNamed = false;
// --- Home State ---
@Nullable
private BlockPos homePos = null;
private HomeType homeType = HomeType.NONE;
@Nullable
private UUID cellId = null;
// --- Cell Quality ---
private CellQuality cellQuality = CellQuality.STANDARD;
// --- Cell Navigation ---
@Nullable
private BlockPos cellDeliveryPoint = null;
// --- Auto-Rest Setting ---
/** Whether the NPC will automatically go rest when tired */
private boolean autoRestEnabled = true;
// --- Job Experience ---
/** Persistent job experience tracking */
private JobExperience jobExperience = new JobExperience();
// --- Constructor ---
public PersonalityState(PersonalityType personality) {
this.personality = personality;
resetStruggleTimer();
}
// --- Factory methods ---
/**
* Generate a random personality state for a Damsel.
*
* @param entityUUID Entity UUID for seeding
* @return New PersonalityState
*/
public static PersonalityState generateForDamsel(UUID entityUUID) {
PersonalityType type = PersonalityType.randomForDamsel();
return new PersonalityState(type);
}
/**
* Generate a random personality state for a Kidnapper.
*
* @param entityUUID Entity UUID for seeding
* @return New PersonalityState
*/
public static PersonalityState generateForKidnapper(UUID entityUUID) {
PersonalityType type = PersonalityType.randomForKidnapper();
return new PersonalityState(type);
}
// --- Getters ---
public PersonalityType getPersonality() {
return personality;
}
public NpcNeeds getNeeds() {
return needs;
}
public NpcCommand getActiveCommand() {
return activeCommand;
}
public NpcCommand.FollowDistance getFollowDistance() {
return followDistance;
}
public void setFollowDistance(NpcCommand.FollowDistance distance) {
this.followDistance = distance;
}
@Nullable
public UUID getCommandingPlayer() {
return commandingPlayer;
}
@Nullable
public BlockPos getCommandTarget() {
return commandTarget;
}
/**
* Get secondary command target (for TRANSFER: destination chest B).
*/
@Nullable
public BlockPos getCommandTarget2() {
return commandTarget2;
}
/**
* Set secondary command target.
*/
public void setCommandTarget2(@Nullable BlockPos pos) {
this.commandTarget2 = pos;
}
public int getCommandProgress() {
return commandProgress;
}
public float getMood() {
return mood;
}
public boolean hasBeenNamed() {
return hasBeenNamed;
}
/**
* Modify mood directly.
*
* @param amount Amount to add (can be negative)
*/
public void modifyMood(float amount) {
this.mood = Math.max(0, Math.min(100, this.mood + amount));
}
// --- Home Management ---
@Nullable
public BlockPos getHomePos() {
return homePos;
}
public HomeType getHomeType() {
return homeType;
}
@Nullable
public BlockPos getCellDeliveryPoint() {
return cellDeliveryPoint;
}
@Nullable
public UUID getCellId() {
return cellId;
}
public boolean hasHome() {
return homePos != null && homeType != HomeType.NONE;
}
public void clearHome() {
this.homePos = null;
this.homeType = HomeType.NONE;
this.cellId = null;
this.cellDeliveryPoint = null;
}
/**
* Assign a cell to this NPC. Derives homePos/homeType from cell content.
* Uses NPC's index in the prisoner list to distribute beds among multiple NPCs.
*/
public void assignCell(UUID cellId, CellDataV2 cell, UUID npcId) {
this.cellId = cellId;
deriveHomeFromCell(cell, npcId);
}
/**
* Derive homePos and homeType from cell content.
* Priority: PetBed > Bed > SpawnPoint.
*/
private void deriveHomeFromCell(CellDataV2 cell, @Nullable UUID npcId) {
int npcIndex = 0;
if (npcId != null) {
List<UUID> prisoners = cell.getPrisonerIds();
int idx = prisoners.indexOf(npcId);
if (idx >= 0) npcIndex = idx;
}
if (!cell.getPetBeds().isEmpty()) {
int bedIdx = npcIndex % cell.getPetBeds().size();
this.homePos = cell.getPetBeds().get(bedIdx);
this.homeType = HomeType.PET_BED;
} else if (!cell.getBeds().isEmpty()) {
int bedIdx = npcIndex % cell.getBeds().size();
this.homePos = cell.getBeds().get(bedIdx);
this.homeType = HomeType.BED;
} else {
this.homePos = cell.getSpawnPoint();
this.homeType = HomeType.CELL;
}
this.cellDeliveryPoint =
cell.getDeliveryPoint() != null
? cell.getDeliveryPoint()
: cell.getSpawnPoint();
}
public void unassignCell() {
clearHome();
}
public boolean isNearHome(BlockPos currentPos, int radius) {
if (homePos == null) return false;
return homePos.closerThan(currentPos, radius);
}
/**
* Derive the rest type based on home type and proximity.
*/
public NpcNeeds.RestType deriveRestType(EntityDamsel entity) {
if (!isNearHome(entity.blockPosition(), 3)) {
return NpcNeeds.RestType.IDLE;
}
return switch (homeType) {
case BED -> NpcNeeds.RestType.SLEEPING;
case PET_BED -> NpcNeeds.RestType.SITTING;
default -> NpcNeeds.RestType.IDLE;
};
}
/**
* Called when home block is destroyed.
*/
public void onHomeDestroyed() {
if (homePos != null) {
modifyMood(-5);
clearHome();
}
}
// --- Cell Quality ---
public CellQuality getCellQuality() {
return cellQuality;
}
// --- Auto-Rest Setting ---
public boolean isAutoRestEnabled() {
return autoRestEnabled;
}
public void setAutoRestEnabled(boolean enabled) {
this.autoRestEnabled = enabled;
}
public boolean toggleAutoRest() {
this.autoRestEnabled = !this.autoRestEnabled;
return this.autoRestEnabled;
}
// --- Job Experience ---
public JobExperience getJobExperience() {
return jobExperience;
}
// --- Discipline System ---
/**
* Apply discipline to the NPC. Only affects mood.
*
* @param type Discipline type
* @param worldTime Current world time (unused, kept for API compat)
* @return false (no brutality tracking)
*/
public boolean applyDiscipline(DisciplineType type, long worldTime) {
// MASOCHIST: Inverted mood for punishment
float moodMult = 1.0f;
if (personality == PersonalityType.MASOCHIST && type.isPunishment()) {
moodMult = -1.0f;
}
modifyMood(type.moodChange * moodMult);
return false;
}
// --- Command System ---
/**
* Check if the NPC will obey a command from a player.
* Always returns true — NPCs obey when captured.
*/
public boolean willObeyCommand(
net.minecraft.world.entity.player.Player commander,
NpcCommand command
) {
return true;
}
/**
* Set the active command.
*/
public void setActiveCommand(
NpcCommand command,
UUID playerUUID,
@Nullable BlockPos target
) {
this.activeCommand = command;
this.commandingPlayer = playerUUID;
this.commandTarget = target;
this.commandProgress = 0;
}
/**
* Clear the active command.
*/
public void clearCommand() {
this.activeCommand = NpcCommand.NONE;
this.commandingPlayer = null;
this.commandTarget = null;
this.commandTarget2 = null;
this.commandProgress = 0;
}
/**
* Increment command progress.
*/
public void addCommandProgress(int amount) {
this.commandProgress += amount;
}
// --- Struggle System ---
public void resetStruggleTimer() {
resetStruggleTimer(6000);
}
public void resetStruggleTimer(int baseInterval) {
float personalityMod = personality.getStruggleTimerMultiplier();
float randomFactor = 0.8f + (float) Math.random() * 0.4f;
this.struggleTimer = Math.round(
baseInterval * personalityMod * randomFactor
);
}
public int getStruggleTimer() {
return struggleTimer;
}
public boolean tickStruggleTimer() {
if (struggleTimer > 0) {
struggleTimer--;
return false;
}
return true;
}
/**
* Calculate struggle success chance.
*
* @param bindResistance Resistance of current bind item
* @param captorNearby Whether the captor is nearby
* @param allyNearby Whether an ally NPC is nearby
* @return Success chance (0.0 to 1.0)
*/
public float calculateStruggleChance(
float bindResistance,
boolean captorNearby,
boolean allyNearby
) {
float baseChance = 0.15f;
// Personality modifier
baseChance *= personality.struggleModifier;
// Bind resistance
baseChance *= (1.0f / Math.max(1.0f, bindResistance));
// Mood affects struggle (miserable = more likely)
if (mood < 30) baseChance *= 1.5f;
else if (mood > 70) baseChance *= 0.7f;
// Environmental factors
if (captorNearby) baseChance *= 0.7f;
if (allyNearby) baseChance *= 1.3f;
return Math.min(0.8f, Math.max(0.01f, baseChance));
}
/**
* Record struggle result.
*/
public void recordStruggleResult(boolean success, int baseInterval) {
if (success) {
consecutiveStruggleFails = 0;
modifyMood(5);
} else {
consecutiveStruggleFails++;
modifyMood(-2);
}
resetStruggleTimer(baseInterval);
}
// --- Mood ---
/**
* Get mood modifier for command compliance (kept for dialogue variety).
*
* @return Modifier (0.7 to 1.3)
*/
public float getMoodModifier() {
if (mood < 20) return 0.7f;
if (mood < 40) return 0.85f;
if (mood > 80) return 1.2f;
if (mood > 60) return 1.1f;
return 1.0f;
}
/**
* Recalculate mood based on needs and cell quality.
*/
public void recalculateMood() {
float newMood = 50.0f;
// Needs impact
newMood += needs.getMoodImpact();
// Job personality preference impact
if (activeCommand != null && activeCommand.isActiveJob()) {
float jobMoodMod = JobPersonalityModifiers.getJobMoodModifier(
personality,
activeCommand
);
newMood += jobMoodMod * 10;
}
// Cell quality impact
newMood += cellQuality.moodModifier;
// Clamp
this.mood = Math.max(0, Math.min(100, newMood));
}
// --- Naming ---
public void markAsNamed() {
this.hasBeenNamed = true;
}
// --- Tick ---
/**
* Tick the personality state. Called every game tick.
*
* @param entity The entity
* @param isNight Whether it's night (unused, kept for API compat)
* @param masterUUID Current collar owner UUID (unused, kept for API compat)
* @param isLeashed Whether the entity is currently leashed (unused)
* @return NeedTransitions containing any threshold crossings this tick
*/
public NpcNeeds.NeedTransitions tick(
EntityDamsel entity,
boolean isNight,
@Nullable UUID masterUUID,
boolean isLeashed
) {
// Determine if NPC is doing active work (jobs that consume rest)
boolean isWorking =
activeCommand != null && activeCommand.isActiveJob();
// Derive rest type based on home proximity and type
NpcNeeds.RestType restType = deriveRestType(entity);
// Update needs and get transitions
NpcNeeds.NeedTransitions transitions = needs.tick(
entity,
personality,
isWorking,
restType
);
// Cell quality evaluation (every 60 seconds = 1200 ticks)
if (entity.tickCount % 1200 == 0 && hasHome()) {
// Validate cell link
CellDataV2 cell = null;
if (
cellId != null &&
entity.level() instanceof ServerLevel serverLevel
) {
CellRegistryV2 registry = CellRegistryV2.get(serverLevel);
cell = registry.getCell(cellId);
if (cell == null) {
// Cell destroyed
onHomeDestroyed();
return transitions;
}
// Re-derive homePos in case cell content changed
deriveHomeFromCell(cell, entity.getUUID());
}
cellQuality = CellQuality.evaluate(homePos, entity.level(), cell);
}
// Recalculate mood every 100 ticks (5 seconds)
if (entity.tickCount % 100 == 0) {
recalculateMood();
}
return transitions;
}
// --- NBT Persistence ---
public CompoundTag save() {
CompoundTag tag = new CompoundTag();
// Core
tag.putString("Personality", personality.name());
// Needs
tag.put("Needs", needs.save());
// Command state
tag.putString("ActiveCommand", activeCommand.name());
tag.putString("FollowDistance", followDistance.name());
if (commandingPlayer != null) {
tag.putUUID("CommandingPlayer", commandingPlayer);
}
if (commandTarget != null) {
tag.putLong("CommandTarget", commandTarget.asLong());
}
if (commandTarget2 != null) {
tag.putLong("CommandTarget2", commandTarget2.asLong());
}
tag.putInt("CommandProgress", commandProgress);
// Struggle state
tag.putInt("StruggleTimer", struggleTimer);
tag.putInt("ConsecutiveFails", consecutiveStruggleFails);
// Other
tag.putFloat("Mood", mood);
tag.putBoolean("HasBeenNamed", hasBeenNamed);
// Home state
if (homePos != null) {
tag.putLong("HomePos", homePos.asLong());
tag.putString("HomeType", homeType.name());
}
if (cellId != null) {
tag.putUUID("HomeCellId", cellId);
}
if (cellDeliveryPoint != null) {
tag.putLong("CellDeliveryPoint", cellDeliveryPoint.asLong());
}
// Cell quality
tag.putString("CellQuality", cellQuality.name());
// Auto-rest setting
tag.putBoolean("AutoRestEnabled", autoRestEnabled);
// Job experience
tag.put("JobExperience", jobExperience.save());
return tag;
}
public static PersonalityState load(CompoundTag tag) {
// Core
PersonalityType type;
try {
type = PersonalityType.valueOf(tag.getString("Personality"));
} catch (IllegalArgumentException e) {
type = PersonalityType.CALM;
}
PersonalityState state = new PersonalityState(type);
// Needs
if (tag.contains("Needs")) {
NpcNeeds loadedNeeds = NpcNeeds.load(tag.getCompound("Needs"));
state.needs.setHunger(loadedNeeds.getHunger());
state.needs.setRest(loadedNeeds.getRest());
}
// Command state (gracefully handles removed commands)
try {
state.activeCommand = NpcCommand.valueOf(
tag.getString("ActiveCommand")
);
} catch (IllegalArgumentException e) {
state.activeCommand = NpcCommand.NONE;
}
try {
state.followDistance = NpcCommand.FollowDistance.valueOf(
tag.getString("FollowDistance")
);
} catch (IllegalArgumentException e) {
state.followDistance = NpcCommand.FollowDistance.FAR;
}
if (tag.contains("CommandingPlayer")) {
state.commandingPlayer = tag.getUUID("CommandingPlayer");
}
if (tag.contains("CommandTarget")) {
state.commandTarget = BlockPos.of(tag.getLong("CommandTarget"));
}
if (tag.contains("CommandTarget2")) {
state.commandTarget2 = BlockPos.of(tag.getLong("CommandTarget2"));
}
state.commandProgress = tag.getInt("CommandProgress");
// Struggle state
state.struggleTimer = tag.getInt("StruggleTimer");
state.consecutiveStruggleFails = tag.getInt("ConsecutiveFails");
// Other
state.mood = tag.getFloat("Mood");
state.hasBeenNamed = tag.getBoolean("HasBeenNamed");
// Home state
if (tag.contains("HomePos")) {
state.homePos = BlockPos.of(tag.getLong("HomePos"));
try {
String homeTypeName = tag.getString("HomeType");
if ("BASKET".equals(homeTypeName)) homeTypeName = "PET_BED"; // migration
state.homeType = HomeType.valueOf(homeTypeName);
} catch (IllegalArgumentException e) {
state.homeType = HomeType.NONE;
}
}
if (tag.hasUUID("HomeCellId")) {
state.cellId = tag.getUUID("HomeCellId");
}
if (tag.contains("CellDeliveryPoint")) {
state.cellDeliveryPoint = BlockPos.of(
tag.getLong("CellDeliveryPoint")
);
}
// Cell quality
if (tag.contains("CellQuality")) {
try {
state.cellQuality = CellQuality.valueOf(
tag.getString("CellQuality")
);
} catch (IllegalArgumentException e) {
state.cellQuality = CellQuality.STANDARD;
}
}
// Auto-rest setting (default true if not present)
if (tag.contains("AutoRestEnabled")) {
state.autoRestEnabled = tag.getBoolean("AutoRestEnabled");
}
// Job experience
if (tag.contains("JobExperience")) {
state.jobExperience = JobExperience.load(
tag.getCompound("JobExperience")
);
}
return state;
}
}