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:
134
src/main/java/com/tiedup/remake/personality/CellQuality.java
Normal file
134
src/main/java/com/tiedup/remake/personality/CellQuality.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
40
src/main/java/com/tiedup/remake/personality/HomeType.java
Normal file
40
src/main/java/com/tiedup/remake/personality/HomeType.java
Normal 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;
|
||||
}
|
||||
}
|
||||
140
src/main/java/com/tiedup/remake/personality/JobExperience.java
Normal file
140
src/main/java/com/tiedup/remake/personality/JobExperience.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
242
src/main/java/com/tiedup/remake/personality/NpcCommand.java
Normal file
242
src/main/java/com/tiedup/remake/personality/NpcCommand.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
321
src/main/java/com/tiedup/remake/personality/NpcNeeds.java
Normal file
321
src/main/java/com/tiedup/remake/personality/NpcNeeds.java
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
121
src/main/java/com/tiedup/remake/personality/PersonalityType.java
Normal file
121
src/main/java/com/tiedup/remake/personality/PersonalityType.java
Normal 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;
|
||||
}
|
||||
}
|
||||
66
src/main/java/com/tiedup/remake/personality/ToolMode.java
Normal file
66
src/main/java/com/tiedup/remake/personality/ToolMode.java
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user