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.
This commit is contained in:
NotEvil
2026-04-12 00:51:22 +02:00
parent 2e7a1d403b
commit f6466360b6
1947 changed files with 238025 additions and 1 deletions

View File

@@ -0,0 +1,134 @@
package com.tiedup.remake.personality;
import com.tiedup.remake.cells.CellDataV2;
import com.tiedup.remake.v2.blocks.PetBedBlock;
import org.jetbrains.annotations.Nullable;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LightLayer;
import net.minecraft.world.level.block.BedBlock;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.CarpetBlock;
/**
* Quality of the cell/environment around an NPC's home.
*
* <p>Evaluated in a radius around the home position and applies
* a mood modifier based on living conditions.
*
* <ul>
* <li>LUXURY: Bed + Carpet + Light > 10 → +5 mood</li>
* <li>STANDARD: PetBed OR Bed → +0 mood</li>
* <li>DUNGEON: No bed + Light < 5 → -5 mood</li>
* </ul>
*/
public enum CellQuality {
/** Bed + Carpet + Light > 10: good living conditions */
LUXURY(5.0f),
/** PetBed OR Bed: normal conditions */
STANDARD(0.0f),
/** No bed + Light < 5: harsh conditions */
DUNGEON(-5.0f);
/** Mood modifier applied when NPC lives in this quality cell */
public final float moodModifier;
CellQuality(float moodModifier) {
this.moodModifier = moodModifier;
}
/** Detection radius around home (5 blocks) */
public static final int DETECTION_RADIUS = 5;
/** Light level threshold for LUXURY (> 10) */
public static final int LIGHT_LUXURY_THRESHOLD = 10;
/** Light level threshold for DUNGEON (< 5) */
public static final int LIGHT_DUNGEON_THRESHOLD = 5;
/**
* Evaluate cell quality around a home position, optionally using CellDataV2 features.
*
* <p>When a cell is provided, bed/pet bed detection uses pre-computed flood-fill data
* instead of scanning blocks. Carpets still require a block scan since they are not
* tracked by the flood-fill.
*
* @param homePos The home block position
* @param level The world level
* @param cell Optional CellDataV2 for optimized feature detection
* @return Evaluated cell quality
*/
public static CellQuality evaluate(
BlockPos homePos,
Level level,
@Nullable CellDataV2 cell
) {
if (homePos == null || level == null) return STANDARD;
boolean hasBed = false;
boolean hasPetBed = false;
boolean hasCarpet = false;
if (cell != null) {
// Use cell features (already detected by flood-fill)
hasBed = !cell.getBeds().isEmpty();
hasPetBed = !cell.getPetBeds().isEmpty();
// Carpets not tracked by flood-fill, still need scan
for (BlockPos pos : BlockPos.betweenClosed(
homePos.offset(-DETECTION_RADIUS, -1, -DETECTION_RADIUS),
homePos.offset(DETECTION_RADIUS, 0, DETECTION_RADIUS)
)) {
if (
level.getBlockState(pos).getBlock() instanceof CarpetBlock
) {
hasCarpet = true;
break;
}
}
} else {
// Fallback: full environment scan (no cell link)
for (BlockPos pos : BlockPos.betweenClosed(
homePos.offset(-DETECTION_RADIUS, -1, -DETECTION_RADIUS),
homePos.offset(DETECTION_RADIUS, 2, DETECTION_RADIUS)
)) {
Block block = level.getBlockState(pos).getBlock();
if (block instanceof BedBlock) hasBed = true;
if (block instanceof PetBedBlock) hasPetBed = true;
if (block instanceof CarpetBlock) hasCarpet = true;
// Early exit if we found all luxury requirements
if (hasBed && hasCarpet) break;
}
}
// Get light level at home position
int lightLevel = level.getBrightness(LightLayer.BLOCK, homePos);
// LUXURY: Bed + Carpet + Light > 10
if (hasBed && hasCarpet && lightLevel > LIGHT_LUXURY_THRESHOLD) {
return LUXURY;
}
// DUNGEON: No bed/pet bed + Light < 5
if (!hasBed && !hasPetBed && lightLevel < LIGHT_DUNGEON_THRESHOLD) {
return DUNGEON;
}
// STANDARD: Bed or PetBed (or medium light)
return STANDARD;
}
/**
* Evaluate cell quality around a home position.
*
* @param homePos The home block position
* @param level The world level
* @return Evaluated cell quality
*/
public static CellQuality evaluate(BlockPos homePos, Level level) {
return evaluate(homePos, level, null);
}
}

View File

@@ -0,0 +1,49 @@
package com.tiedup.remake.personality;
/**
* Types of discipline that can be applied to NPCs.
* Only affects mood — no relationship, fear, willpower, or resentment effects.
*/
public enum DisciplineType {
WHIP(-15),
PADDLE(-5),
PRAISE(10),
SHOCK(-20),
HAND(-3),
SCOLD(-5),
THREATEN(-8);
/** Mood modifier applied to personality state. */
public final int moodChange;
DisciplineType(int moodChange) {
this.moodChange = moodChange;
}
/**
* Check if this discipline type is punishment (causes pain/fear).
*
* @return True for all punishment types, false for PRAISE
*/
public boolean isPunishment() {
return this != PRAISE;
}
/**
* Check if this discipline is verbal (no physical contact).
*
* @return True for PRAISE, SCOLD, THREATEN
*/
public boolean isVerbal() {
return this == PRAISE || this == SCOLD || this == THREATEN;
}
/**
* Get the dialogue key for this discipline type.
*
* @return Key like "action.whip", "action.paddle", "action.praise"
*/
public String getDialogueKey() {
return "action." + name().toLowerCase();
}
}

View File

@@ -0,0 +1,40 @@
package com.tiedup.remake.personality;
/**
* Types of homes that can be assigned to NPCs.
*
* <p>Home types determine comfort level, which affects:
* <ul>
* <li>Hope regeneration (higher comfort = more hope)</li>
* <li>Cell quality evaluation (Phase 6)</li>
* <li>Mental state decay rates</li>
* </ul>
*/
public enum HomeType {
/** No home assigned - NPC sleeps anywhere */
NONE(0),
/** Cell with spawn point only - No comfort, but valid home */
CELL(0),
/** Pet bed block - Low comfort, for SUBJUGATED slaves */
PET_BED(1),
/** Standard Minecraft bed - High comfort, for trusted/DEVOTED */
BED(2);
/** Comfort level (0 = none, 1 = low, 2 = high) */
public final int comfortLevel;
HomeType(int comfortLevel) {
this.comfortLevel = comfortLevel;
}
/**
* Check if this home type provides any comfort.
* @return true if comfort level > 0
*/
public boolean hasComfort() {
return comfortLevel > 0;
}
}

View File

@@ -0,0 +1,140 @@
package com.tiedup.remake.personality;
import java.util.EnumMap;
import java.util.Map;
import net.minecraft.nbt.CompoundTag;
/**
* Tracks job experience per NpcCommand type.
* Experience accumulates as NPCs perform jobs and unlocks efficiency/yield bonuses.
* Experience accumulates per command type with proficiency tiers.
*/
public class JobExperience {
/**
* Job proficiency levels based on accumulated experience.
*/
public enum JobLevel {
NOVICE(0, 9, 1.0f, 1.0f, 0),
APPRENTICE(10, 24, 1.1f, 1.05f, 1),
SKILLED(25, 49, 1.25f, 1.15f, 2),
EXPERT(50, Integer.MAX_VALUE, 1.5f, 1.3f, 4);
public final int minExp;
public final int maxExp;
/** Speed multiplier for job actions */
public final float speedMultiplier;
/** Yield multiplier for job outputs */
public final float yieldMultiplier;
/** Extra work radius bonus (blocks) */
public final int rangeBonus;
JobLevel(
int minExp,
int maxExp,
float speedMultiplier,
float yieldMultiplier,
int rangeBonus
) {
this.minExp = minExp;
this.maxExp = maxExp;
this.speedMultiplier = speedMultiplier;
this.yieldMultiplier = yieldMultiplier;
this.rangeBonus = rangeBonus;
}
/**
* Get the job level for a given experience value.
*/
public static JobLevel fromExperience(int exp) {
if (exp >= EXPERT.minExp) return EXPERT;
if (exp >= SKILLED.minExp) return SKILLED;
if (exp >= APPRENTICE.minExp) return APPRENTICE;
return NOVICE;
}
}
/** Experience count per job command type. */
private final Map<NpcCommand, Integer> jobExperience = new EnumMap<>(
NpcCommand.class
);
/**
* Get experience for a specific job command.
*
* @param command The job command
* @return Experience count (0 if never performed)
*/
public int getExperience(NpcCommand command) {
return jobExperience.getOrDefault(command, 0);
}
/**
* Get the job level for a specific command.
*
* @param command The job command
* @return Current JobLevel based on experience
*/
public JobLevel getJobLevel(NpcCommand command) {
return JobLevel.fromExperience(getExperience(command));
}
/**
* Add experience for a job command (+1).
*
* @param command The job command
*/
public void addExperience(NpcCommand command) {
jobExperience.merge(command, 1, Integer::sum);
}
/**
* Get the speed multiplier for a job command based on experience.
*
* @param command The job command
* @return Speed multiplier (1.0 to 1.5)
*/
public float getSpeedMultiplier(NpcCommand command) {
return getJobLevel(command).speedMultiplier;
}
/**
* Get the yield multiplier for a job command based on experience.
*
* @param command The job command
* @return Yield multiplier (1.0 to 1.3)
*/
public float getYieldMultiplier(NpcCommand command) {
return getJobLevel(command).yieldMultiplier;
}
/**
* Get the range bonus for a job command based on experience.
*
* @param command The job command
* @return Extra radius in blocks (0 to 4)
*/
public int getRangeBonus(NpcCommand command) {
return getJobLevel(command).rangeBonus;
}
// --- NBT Persistence ---
public CompoundTag save() {
CompoundTag tag = new CompoundTag();
for (Map.Entry<NpcCommand, Integer> entry : jobExperience.entrySet()) {
tag.putInt(entry.getKey().name(), entry.getValue());
}
return tag;
}
public static JobExperience load(CompoundTag tag) {
JobExperience exp = new JobExperience();
for (NpcCommand cmd : NpcCommand.values()) {
if (tag.contains(cmd.name())) {
exp.jobExperience.put(cmd, tag.getInt(cmd.name()));
}
}
return exp;
}
}

View File

@@ -0,0 +1,183 @@
package com.tiedup.remake.personality;
import java.util.EnumMap;
import java.util.Map;
/**
* Centralized lookup table for personality ↔ job interactions.
* Avoids scattered switch statements in each goal.
*/
public final class JobPersonalityModifiers {
private JobPersonalityModifiers() {}
/**
* Per-personality job modifier entry.
*/
private record JobModEntry(
float efficiencyMod,
NpcCommand preferredJob,
NpcCommand dislikedJob,
float moodModifier
) {}
/**
* Map of (PersonalityType, NpcCommand) → efficiency modifier.
* Only non-default entries are stored; default is 1.0.
*/
private static final Map<
PersonalityType,
Map<NpcCommand, Float>
> EFFICIENCY_TABLE = new EnumMap<>(PersonalityType.class);
/** Preferred jobs per personality */
private static final Map<PersonalityType, NpcCommand> PREFERRED_JOBS =
new EnumMap<>(PersonalityType.class);
/** Disliked jobs per personality */
private static final Map<PersonalityType, NpcCommand> DISLIKED_JOBS =
new EnumMap<>(PersonalityType.class);
static {
// GENTLE: Good at breeding/farming, bad at mining
putEfficiency(PersonalityType.GENTLE, NpcCommand.BREED, 1.3f);
putEfficiency(PersonalityType.GENTLE, NpcCommand.FARM, 1.2f);
putEfficiency(PersonalityType.GENTLE, NpcCommand.MINE, 0.7f);
PREFERRED_JOBS.put(PersonalityType.GENTLE, NpcCommand.BREED);
DISLIKED_JOBS.put(PersonalityType.GENTLE, NpcCommand.MINE);
// FIERCE: Good at guarding/mining, bad at cooking
putEfficiency(PersonalityType.FIERCE, NpcCommand.GUARD, 1.3f);
putEfficiency(PersonalityType.FIERCE, NpcCommand.MINE, 1.2f);
putEfficiency(PersonalityType.FIERCE, NpcCommand.COOK, 0.7f);
PREFERRED_JOBS.put(PersonalityType.FIERCE, NpcCommand.GUARD);
DISLIKED_JOBS.put(PersonalityType.FIERCE, NpcCommand.COOK);
// CURIOUS: Good at collecting, bad at patrol
putEfficiency(PersonalityType.CURIOUS, NpcCommand.COLLECT, 1.3f);
putEfficiency(PersonalityType.CURIOUS, NpcCommand.PATROL, 0.8f);
PREFERRED_JOBS.put(PersonalityType.CURIOUS, NpcCommand.COLLECT);
DISLIKED_JOBS.put(PersonalityType.CURIOUS, NpcCommand.PATROL);
// PLAYFUL: Good at breeding/collecting, bad at sorting
putEfficiency(PersonalityType.PLAYFUL, NpcCommand.BREED, 1.2f);
putEfficiency(PersonalityType.PLAYFUL, NpcCommand.COLLECT, 1.1f);
putEfficiency(PersonalityType.PLAYFUL, NpcCommand.SORT, 0.8f);
PREFERRED_JOBS.put(PersonalityType.PLAYFUL, NpcCommand.BREED);
DISLIKED_JOBS.put(PersonalityType.PLAYFUL, NpcCommand.SORT);
// PROUD: Good at guarding/patrolling, bad at collecting
putEfficiency(PersonalityType.PROUD, NpcCommand.GUARD, 1.2f);
putEfficiency(PersonalityType.PROUD, NpcCommand.PATROL, 1.1f);
putEfficiency(PersonalityType.PROUD, NpcCommand.COLLECT, 0.7f);
PREFERRED_JOBS.put(PersonalityType.PROUD, NpcCommand.GUARD);
DISLIKED_JOBS.put(PersonalityType.PROUD, NpcCommand.COLLECT);
// TIMID: Good at fishing (peaceful), bad at guarding
putEfficiency(PersonalityType.TIMID, NpcCommand.FISH, 1.2f);
putEfficiency(PersonalityType.TIMID, NpcCommand.FARM, 1.1f);
putEfficiency(PersonalityType.TIMID, NpcCommand.GUARD, 0.7f);
PREFERRED_JOBS.put(PersonalityType.TIMID, NpcCommand.FISH);
DISLIKED_JOBS.put(PersonalityType.TIMID, NpcCommand.GUARD);
// SUBMISSIVE: Good at all jobs (obedient), slight preference for sorting
putEfficiency(PersonalityType.SUBMISSIVE, NpcCommand.SORT, 1.2f);
putEfficiency(PersonalityType.SUBMISSIVE, NpcCommand.TRANSFER, 1.1f);
PREFERRED_JOBS.put(PersonalityType.SUBMISSIVE, NpcCommand.SORT);
DISLIKED_JOBS.put(PersonalityType.SUBMISSIVE, NpcCommand.MINE);
// CALM: Balanced, slight cooking bonus
putEfficiency(PersonalityType.CALM, NpcCommand.COOK, 1.1f);
putEfficiency(PersonalityType.CALM, NpcCommand.FARM, 1.1f);
PREFERRED_JOBS.put(PersonalityType.CALM, NpcCommand.COOK);
DISLIKED_JOBS.put(PersonalityType.CALM, NpcCommand.MINE);
// DEFIANT: Bad at most jobs (resistant), decent at mining (physical outlet)
putEfficiency(PersonalityType.DEFIANT, NpcCommand.MINE, 1.1f);
putEfficiency(PersonalityType.DEFIANT, NpcCommand.FARM, 0.7f);
putEfficiency(PersonalityType.DEFIANT, NpcCommand.SORT, 0.6f);
PREFERRED_JOBS.put(PersonalityType.DEFIANT, NpcCommand.MINE);
DISLIKED_JOBS.put(PersonalityType.DEFIANT, NpcCommand.SORT);
// MASOCHIST: Good at hard labor
putEfficiency(PersonalityType.MASOCHIST, NpcCommand.MINE, 1.2f);
putEfficiency(PersonalityType.MASOCHIST, NpcCommand.FARM, 1.1f);
PREFERRED_JOBS.put(PersonalityType.MASOCHIST, NpcCommand.MINE);
DISLIKED_JOBS.put(PersonalityType.MASOCHIST, NpcCommand.FISH);
// SADIST: Good at guarding (intimidation), bad at breeding (no patience)
putEfficiency(PersonalityType.SADIST, NpcCommand.GUARD, 1.3f);
putEfficiency(PersonalityType.SADIST, NpcCommand.BREED, 0.7f);
PREFERRED_JOBS.put(PersonalityType.SADIST, NpcCommand.GUARD);
DISLIKED_JOBS.put(PersonalityType.SADIST, NpcCommand.BREED);
}
private static void putEfficiency(
PersonalityType type,
NpcCommand cmd,
float mod
) {
EFFICIENCY_TABLE.computeIfAbsent(type, k ->
new EnumMap<>(NpcCommand.class)
).put(cmd, mod);
}
/**
* Get the efficiency modifier for a personality performing a specific job.
*
* @param personality NPC personality type
* @param command Job command being performed
* @return Efficiency multiplier (0.6 to 1.3, default 1.0)
*/
public static float getEfficiencyModifier(
PersonalityType personality,
NpcCommand command
) {
Map<NpcCommand, Float> personalityMods = EFFICIENCY_TABLE.get(
personality
);
if (personalityMods == null) return 1.0f;
return personalityMods.getOrDefault(command, 1.0f);
}
/**
* Get the preferred job for a personality type.
*
* @param personality NPC personality type
* @return Preferred NpcCommand, or FARM as default
*/
public static NpcCommand getPreferredJob(PersonalityType personality) {
return PREFERRED_JOBS.getOrDefault(personality, NpcCommand.FARM);
}
/**
* Get the disliked job for a personality type.
*
* @param personality NPC personality type
* @return Disliked NpcCommand, or NONE as default
*/
public static NpcCommand getDislikedJob(PersonalityType personality) {
return DISLIKED_JOBS.getOrDefault(personality, NpcCommand.NONE);
}
/**
* Get the mood modifier for a personality performing a specific job.
* Positive if preferred, negative if disliked, 0 otherwise.
*
* @param personality NPC personality type
* @param command Job command being performed
* @return Mood modifier (-0.1 to +0.1)
*/
public static float getJobMoodModifier(
PersonalityType personality,
NpcCommand command
) {
if (command == getPreferredJob(personality)) {
return 0.1f;
}
if (command == getDislikedJob(personality)) {
return -0.1f;
}
return 0.0f;
}
}

View File

@@ -0,0 +1,242 @@
package com.tiedup.remake.personality;
import org.jetbrains.annotations.Nullable;
/**
* Commands that can be given to NPCs with collar (collar required).
* All commands are always available - no training tier restrictions.
*/
public enum NpcCommand {
/** No active command */
NONE(CommandType.INSTANT, 1.0f),
// --- Basic Commands ---
/** Follow the master */
FOLLOW(CommandType.CONTINUOUS, 0.75f),
/** Stay in current position */
STAY(CommandType.CONTINUOUS, 0.80f),
/** Come to the master */
COME(CommandType.INSTANT, 0.75f),
/** Rest in place, speak idle dialogue */
IDLE(CommandType.CONTINUOUS, 0.90f),
/** Go to assigned home position */
GO_HOME(CommandType.INSTANT, 0.85f),
// --- Pose Commands ---
/** Sit down */
SIT(CommandType.INSTANT, 0.70f),
// HEEL removed - now a FollowDistance mode for FOLLOW command
/** Kneel position */
KNEEL(CommandType.INSTANT, 0.60f),
// --- Job Commands ---
/** Patrol a defined zone */
PATROL(CommandType.JOB, 0.65f),
/** Guard a zone, alert on intruders */
GUARD(CommandType.JOB, 0.60f),
/** Pick up an item */
FETCH(CommandType.INSTANT, 0.55f),
/** Collect all items in a zone */
COLLECT(CommandType.JOB, 0.50f),
// --- Work Commands ---
/** Farm crops in zone around chest hub */
FARM(CommandType.JOB, 0.55f),
/** Cook items using furnace, resources from chest hub */
COOK(CommandType.JOB, 0.50f),
/** Transfer items from chest A to chest B */
TRANSFER(CommandType.JOB, 0.60f),
/** Shear sheep in zone (requires shears in hand) */
SHEAR(CommandType.JOB, 0.55f),
/** Mine blocks in zone (requires pickaxe in hand) */
MINE(CommandType.JOB, 0.50f),
/** Breed animals in zone using food from chest hub */
BREED(CommandType.JOB, 0.60f),
/** Fish near water using simulated fishing */
FISH(CommandType.JOB, 0.65f),
/** Sort items between source and destination chests by category */
SORT(CommandType.JOB, 0.60f);
// NOTE: Combat commands (ATTACK, DEFEND, CAPTURE) have been removed.
// Combat behavior is now determined by the item in the NPC's main hand
// during FOLLOW command. See ToolMode enum and NpcFollowCommandGoal.
/** Type of command execution */
public enum CommandType {
/** Executes once and completes */
INSTANT,
/** Continues until cancelled */
CONTINUOUS,
/** Has progress and completion state */
JOB,
}
/**
* Distance modes for FOLLOW command.
* Allows configuring how closely the NPC follows.
*/
public enum FollowDistance {
/** Far following (6-10 blocks) - original FOLLOW behavior */
FAR(2.0, 6.0, 10, 1200, 2, false),
/** Close following (2-4 blocks) - moderate distance */
CLOSE(1.5, 4.0, 8, 1000, 2, false),
/** Heel position (1-2 blocks) - stays behind master */
HEEL(0.8, 2.0, 5, 800, 3, true);
/** Minimum distance to keep from master */
public final double minDistance;
/** Distance at which to start following */
public final double startDistance;
/** Ticks between path recalculations */
public final int pathRecalcInterval;
/** Ticks between XP awards */
public final int xpInterval;
/** XP awarded per interval */
public final int xpAmount;
/** Whether to follow behind master (heel behavior) */
public final boolean followBehind;
FollowDistance(
double minDistance,
double startDistance,
int pathRecalcInterval,
int xpInterval,
int xpAmount,
boolean followBehind
) {
this.minDistance = minDistance;
this.startDistance = startDistance;
this.pathRecalcInterval = pathRecalcInterval;
this.xpInterval = xpInterval;
this.xpAmount = xpAmount;
this.followBehind = followBehind;
}
/**
* Get translation key for this distance mode.
*/
public String getTranslationKey() {
return "command.follow.distance." + this.name().toLowerCase();
}
}
/** Command execution type */
public final CommandType type;
/** Base success chance (before modifiers) */
public final float baseSuccessChance;
NpcCommand(CommandType type, float baseSuccessChance) {
this.type = type;
this.baseSuccessChance = baseSuccessChance;
}
/**
* Check if this command requires a target position.
*
* @return true if command needs BlockPos
*/
public boolean requiresPosition() {
return (
this == PATROL ||
this == GUARD ||
this == COLLECT ||
this == FARM ||
this == COOK ||
this == TRANSFER ||
this == SHEAR ||
this == BREED ||
this == FISH ||
this == SORT
);
}
/**
* Check if this command is a work command (uses chest-hub system).
*
* @return true if this is a work command
*/
public boolean isWorkCommand() {
return (
this == FARM ||
this == COOK ||
this == TRANSFER ||
this == SHEAR ||
this == BREED ||
this == FISH ||
this == SORT
);
}
/**
* Check if this command is an active job that consumes rest.
* Jobs that involve physical labor drain rest over time.
*
* @return true if this command drains rest
*/
public boolean isActiveJob() {
return this.type == CommandType.JOB;
}
/**
* Check if this command requires a target entity.
*
* @return true if command needs entity target
*/
public boolean requiresEntityTarget() {
return this == FETCH;
}
/**
* Get XP reward for successful completion.
*
* @return XP amount (1-10)
*/
public int getCompletionXP() {
return switch (this.type) {
case INSTANT -> 2;
case CONTINUOUS -> 2;
case JOB -> 5;
};
}
/**
* Get localization key for this command.
*
* @return Translation key
*/
public String getTranslationKey() {
return "command." + this.name().toLowerCase();
}
/**
* Parse command from string (case-insensitive).
*
* @param name Command name
* @return NpcCommand or NONE if not found
*/
public static NpcCommand fromString(@Nullable String name) {
if (name == null || name.isEmpty()) return NONE;
try {
return valueOf(name.toUpperCase());
} catch (IllegalArgumentException e) {
return NONE;
}
}
}

View File

@@ -0,0 +1,321 @@
package com.tiedup.remake.personality;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.entity.LivingEntity;
/**
* Manages the 2 basic needs of an NPC: HUNGER and REST.
* Needs decay over time and affect mood and behavior.
*
* REST replaces the old DIGNITY system:
* - Decreases during active work (jobs)
* - Increases when idle, sitting, or sleeping
* - Affects job efficiency, movement speed, and attack damage
*/
public class NpcNeeds {
/** Maximum value for any need */
public static final float MAX_VALUE = 100.0f;
/** Threshold for "low" state (tired/hungry) */
public static final float LOW_THRESHOLD = 30.0f;
/** Threshold for "critical" state (exhausted/starving) */
public static final float CRITICAL_THRESHOLD = 10.0f;
/** Threshold for "well rested" bonus */
public static final float RESTED_THRESHOLD = 70.0f;
// Current need values (0-100)
private float hunger = MAX_VALUE;
private float rest = MAX_VALUE;
// Previous tick states for transition detection
private boolean wasHungry = false;
private boolean wasStarving = false;
private boolean wasTired = false;
private boolean wasExhausted = false;
// Decay/recovery rates per tick (20 ticks = 1 second)
private static final float HUNGER_DECAY = 0.001f; // ~8.3 min to empty
// Rest drain during work
private static final float REST_DRAIN_WORK = 0.002f; // ~8.3 min of work to empty
// Rest recovery rates (per tick, 20 ticks/sec)
private static final float REST_RECOVERY_SLEEPING = 0.028f; // ~3 min from 0 to 100 (in bed)
private static final float REST_RECOVERY_SITTING = 0.01f; // ~8 min from 0 to 100 (in pet bed/home)
private static final float REST_RECOVERY_IDLE = 0.0025f; // ~33 min from 0 to 100 (standing idle)
// --- Getters ---
public float getHunger() {
return hunger;
}
public float getRest() {
return rest;
}
// --- State checks ---
public boolean isHungry() {
return hunger < LOW_THRESHOLD;
}
public boolean isStarving() {
return hunger < CRITICAL_THRESHOLD;
}
public boolean isTired() {
return rest < LOW_THRESHOLD;
}
public boolean isExhausted() {
return rest < CRITICAL_THRESHOLD;
}
public boolean isRested() {
return rest >= RESTED_THRESHOLD;
}
// --- Modification ---
public void setHunger(float value) {
this.hunger = clamp(value);
}
public void setRest(float value) {
this.rest = clamp(value);
}
public void modifyHunger(float delta) {
this.hunger = clamp(this.hunger + delta);
}
public void modifyRest(float delta) {
this.rest = clamp(this.rest + delta);
}
/**
* Drain rest during active work.
* @param workIntensity Multiplier for drain rate (1.0 = normal)
*/
public void drainFromWork(float workIntensity) {
this.rest = clamp(this.rest - REST_DRAIN_WORK * workIntensity);
}
/**
* Recover rest while resting.
* @param restType Type of rest (SLEEPING, SITTING, or IDLE)
*/
public void recoverRest(RestType restType) {
float recovery = switch (restType) {
case SLEEPING -> REST_RECOVERY_SLEEPING;
case SITTING -> REST_RECOVERY_SITTING;
case IDLE -> REST_RECOVERY_IDLE;
};
this.rest = clamp(this.rest + recovery);
}
/**
* Feed the NPC.
*
* @param nutritionValue How much hunger to restore (1-20 typically)
*/
public void feed(float nutritionValue) {
modifyHunger(nutritionValue * 5); // Food items restore 5x their value
}
// --- Efficiency Modifiers ---
/**
* Get job efficiency modifier based on rest level.
* @return 0.4 (exhausted) to 1.0 (normal/rested)
*/
public float getEfficiencyModifier() {
if (isExhausted()) return 0.4f;
if (isTired()) return 0.7f;
return 1.0f;
}
/**
* Get movement speed modifier based on rest level.
* @return 0.6 (exhausted) to 1.0 (normal/rested)
*/
public float getSpeedModifier() {
if (isExhausted()) return 0.6f;
if (isTired()) return 0.8f;
return 1.0f;
}
/**
* Get attack damage modifier based on rest level.
* @return 0.7 (exhausted) to 1.0 (normal/rested)
*/
public float getDamageModifier() {
if (isExhausted()) return 0.7f;
if (isTired()) return 0.9f;
return 1.0f;
}
// --- Tick update ---
/** Shared empty transitions instance to avoid allocation */
private static final NeedTransitions EMPTY_TRANSITIONS =
new NeedTransitions();
/**
* Update needs based on current state. Called every tick.
*
* @param entity The entity to check state from
* @param personality The personality (unused, kept for API compat)
* @param isWorking Whether the NPC is currently doing an active job
* @return NeedTransitions containing any threshold crossings this tick
*/
public NeedTransitions tick(
LivingEntity entity,
PersonalityType personality,
boolean isWorking,
RestType restType
) {
// Hunger always decays
hunger = clamp(hunger - HUNGER_DECAY);
// Rest logic: drain when working, recover based on rest type
if (isWorking) {
rest = clamp(rest - REST_DRAIN_WORK);
} else {
// Recovery rate depends on rest type (BED=SLEEPING, PET_BED=SITTING, else=IDLE)
recoverRest(restType);
}
// Detect transitions (threshold crossings)
boolean transitionOccurred = false;
NeedTransitions transitions = null;
// Hunger transitions
if (isHungry() && !wasHungry) {
if (transitions == null) transitions = new NeedTransitions();
transitions.becameHungry = true;
transitionOccurred = true;
}
if (isStarving() && !wasStarving) {
if (transitions == null) transitions = new NeedTransitions();
transitions.becameStarving = true;
transitionOccurred = true;
}
// Rest transitions
if (isTired() && !wasTired) {
if (transitions == null) transitions = new NeedTransitions();
transitions.becameTired = true;
transitionOccurred = true;
}
if (isExhausted() && !wasExhausted) {
if (transitions == null) transitions = new NeedTransitions();
transitions.becameExhausted = true;
transitionOccurred = true;
}
// Update previous states for next tick
wasHungry = isHungry();
wasStarving = isStarving();
wasTired = isTired();
wasExhausted = isExhausted();
return transitionOccurred ? transitions : EMPTY_TRANSITIONS;
}
/**
* Calculate overall mood impact from needs.
*
* @return Mood modifier (-40 to +5)
*/
public float getMoodImpact() {
float impact = 0;
// Hunger impact
if (isStarving()) impact -= 20;
else if (isHungry()) impact -= 10;
else if (hunger > 80) impact += 5;
// Rest impact
if (isExhausted()) impact -= 20;
else if (isTired()) impact -= 10;
else if (isRested()) impact += 5;
return impact;
}
/**
* Get the most critical need (lowest value).
*
* @return The need type that's lowest
*/
public NeedType getMostCriticalNeed() {
if (rest < hunger) {
return NeedType.REST;
}
return NeedType.HUNGER;
}
public enum NeedType {
HUNGER,
REST,
}
public enum RestType {
SLEEPING, // In bed - fastest recovery
SITTING, // In pet bed/home - medium recovery
IDLE, // Standing idle - slow recovery
}
/**
* Holds information about need state transitions that occurred this tick.
* Used to trigger dialogues when needs cross thresholds.
*/
public static class NeedTransitions {
public boolean becameHungry = false;
public boolean becameStarving = false;
public boolean becameTired = false;
public boolean becameExhausted = false;
/** Returns true if any transition occurred */
public boolean hasAny() {
return (
becameHungry || becameStarving || becameTired || becameExhausted
);
}
}
// --- NBT Persistence ---
public CompoundTag save() {
CompoundTag tag = new CompoundTag();
tag.putFloat("Hunger", hunger);
tag.putFloat("Rest", rest);
return tag;
}
public static NpcNeeds load(CompoundTag tag) {
NpcNeeds needs = new NpcNeeds();
if (tag.contains("Hunger")) needs.hunger = clamp(
tag.getFloat("Hunger")
);
// Support both new "Rest" and legacy "Dignity" keys
if (tag.contains("Rest")) {
needs.rest = clamp(tag.getFloat("Rest"));
} else if (tag.contains("Dignity")) {
needs.rest = clamp(tag.getFloat("Dignity"));
}
return needs;
}
// --- Utility ---
private static float clamp(float value) {
return Math.max(0, Math.min(MAX_VALUE, value));
}
}

View File

@@ -0,0 +1,709 @@
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;
}
}

View File

@@ -0,0 +1,121 @@
package com.tiedup.remake.personality;
import java.util.Random;
/**
* Defines the 11 base personality types for NPCs.
* Each personality affects behavior:
* - struggleModifier: Multiplier for struggle success chance
* - flightModifier: Multiplier for flee behavior distance/urgency
* - baseCompliance: Base compliance rate (kept for dialogue variety)
* - spawnWeight: Weight for random generation
*/
public enum PersonalityType {
TIMID(0.7f, 1.5f, 0.75f, 0.15f),
GENTLE(0.5f, 1.2f, 0.80f, 0.12f),
SUBMISSIVE(0.3f, 1.0f, 0.90f, 0.05f),
CALM(1.0f, 1.0f, 0.60f, 0.20f),
CURIOUS(0.8f, 0.9f, 0.70f, 0.10f),
PROUD(1.3f, 0.7f, 0.30f, 0.10f),
FIERCE(1.5f, 0.3f, 0.20f, 0.08f),
DEFIANT(1.4f, 0.5f, 0.10f, 0.07f),
PLAYFUL(0.9f, 1.1f, 0.60f, 0.08f),
MASOCHIST(0.4f, 0.8f, 0.85f, 0.03f),
SADIST(1.2f, 0.6f, 0.40f, 0.02f);
/** Multiplier for struggle escape chance */
public final float struggleModifier;
/** Multiplier for flight behavior */
public final float flightModifier;
/** Base compliance rate (0.0 to 1.0) — for dialogue/RP variety */
public final float baseCompliance;
/** Spawn weight for random generation */
public final float spawnWeight;
PersonalityType(
float struggleModifier,
float flightModifier,
float baseCompliance,
float spawnWeight
) {
this.struggleModifier = struggleModifier;
this.flightModifier = flightModifier;
this.baseCompliance = baseCompliance;
this.spawnWeight = spawnWeight;
}
/**
* Calculate struggle timer multiplier.
* Lower = struggles more often.
*
* @return Timer multiplier (0.3 for FIERCE to 4.0 for MASOCHIST)
*/
public float getStruggleTimerMultiplier() {
return switch (this) {
case FIERCE -> 0.3f;
case DEFIANT -> 0.5f;
case PROUD -> 0.6f;
case CALM, CURIOUS, PLAYFUL -> 1.0f;
case TIMID -> 1.5f;
case GENTLE -> 2.0f;
case SUBMISSIVE -> 3.0f;
case MASOCHIST -> 4.0f;
case SADIST -> 0.8f;
};
}
// --- Static utility methods ---
private static final Random RANDOM = new Random();
private static float totalWeight = -1;
/**
* Generate a random personality based on spawn weights.
*
* @return Random PersonalityType
*/
public static PersonalityType randomForDamsel() {
if (totalWeight < 0) {
totalWeight = 0;
for (PersonalityType type : values()) {
if (type != SADIST) {
totalWeight += type.spawnWeight;
}
}
}
float roll = RANDOM.nextFloat() * totalWeight;
float cumulative = 0;
for (PersonalityType type : values()) {
if (type == SADIST) continue;
cumulative += type.spawnWeight;
if (roll < cumulative) {
return type;
}
}
return CALM; // Fallback
}
/**
* Generate a random personality for kidnappers.
* Includes SADIST and excludes SUBMISSIVE.
*
* @return Random PersonalityType for kidnapper
*/
public static PersonalityType randomForKidnapper() {
float roll = RANDOM.nextFloat();
if (roll < 0.25f) return FIERCE;
if (roll < 0.45f) return SADIST;
if (roll < 0.60f) return PROUD;
if (roll < 0.75f) return DEFIANT;
if (roll < 0.85f) return CALM;
if (roll < 0.92f) return PLAYFUL;
return CURIOUS;
}
}

View File

@@ -0,0 +1,66 @@
package com.tiedup.remake.personality;
import com.tiedup.remake.items.base.ItemBind;
import net.minecraft.world.item.AxeItem;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.PickaxeItem;
import net.minecraft.world.item.SwordItem;
/**
* Tool modes that determine NPC behavior during FOLLOW command.
* The item in the NPC's main hand determines their active behavior.
*/
public enum ToolMode {
/** No tool or unrecognized item - passive following */
PASSIVE,
/** SwordItem - attack hostile mobs near master */
ATTACK,
/** PickaxeItem - mine stone/ores near master */
MINING,
/** AxeItem - chop wood near master */
WOODCUTTING,
/** IBondageItem (BIND type) - capture untied damsels */
CAPTURE;
/**
* Detect tool mode from main hand item.
*
* @param mainHand Item in main hand
* @return Detected tool mode
*/
public static ToolMode fromItem(ItemStack mainHand) {
if (mainHand.isEmpty()) {
return PASSIVE;
}
var item = mainHand.getItem();
if (item instanceof SwordItem) {
return ATTACK;
}
if (item instanceof PickaxeItem) {
return MINING;
}
if (item instanceof AxeItem) {
return WOODCUTTING;
}
if (item instanceof ItemBind) {
return CAPTURE;
}
return PASSIVE;
}
/**
* Get translation key for this mode.
*
* @return Translation key
*/
public String getTranslationKey() {
return "toolmode." + this.name().toLowerCase();
}
}