Remove build artifacts, dev tool configs, unused dependencies, and third-party source dumps. Add proper README, update .gitignore, clean up Makefile.
710 lines
20 KiB
Java
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;
|
|
}
|
|
}
|