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