Files
TiedUp-/src/main/java/com/tiedup/remake/entities/EntitySlaveTrader.java
NotEvil fd60086322 feat(i18n): complete migration — items, entities, AI goals, GUI screens
119 new translation keys across 3 domains:
- Items/Blocks/Misc (46 keys): tooltips, action messages, trap states
- Entities/AI Goals (55 keys): NPC speech, maid/master/guard messages
- Client GUI (18 keys): widget labels, screen buttons, merchant display

Remaining 119 Component.literal() are all intentional:
- Debug/Admin/Command wands (47) — dev tools, not player-facing
- Entity display names (~25) — dynamic getNpcName() calls
- Empty string roots (~15) — .append() chain bases
- User-typed text (~10) — /me, /pm, /norp chat content
- Runtime data (~12) — StringBuilder, gag muffling, MCA compat
2026-04-16 12:33:13 +02:00

894 lines
29 KiB
Java

package com.tiedup.remake.entities;
import com.tiedup.remake.cells.CampLifecycleManager;
import com.tiedup.remake.cells.CampOwnership;
import com.tiedup.remake.cells.CellDataV2;
import com.tiedup.remake.cells.CellRegistryV2;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.dialogue.SpeakerType;
// Prison system v2 goals
import com.tiedup.remake.entities.ai.trader.goals.TraderIdleGoal;
import com.tiedup.remake.entities.ai.trader.goals.TraderSellGoal;
import com.tiedup.remake.entities.skins.Gender;
import com.tiedup.remake.entities.skins.TraderSkinManager;
import com.tiedup.remake.items.ModItems;
import com.tiedup.remake.items.base.IHasResistance;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.trader.PacketOpenTraderScreen;
import com.tiedup.remake.network.trader.PacketOpenTraderScreen.CaptiveOfferData;
import com.tiedup.remake.personality.PersonalityType;
import com.tiedup.remake.prison.PrisonerManager;
import com.tiedup.remake.prison.PrisonerRecord;
import com.tiedup.remake.state.IRestrainable;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.util.tasks.ItemTask;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import javax.annotation.Nullable;
import net.minecraft.ChatFormatting;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.Style;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.ai.attributes.AttributeSupplier;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.entity.ai.goal.*;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
/**
* EntitySlaveTrader - Boss of a camp who sells captives.
*
* Slave Trader & Maid System
*
* Characteristics:
* - Stats like Elite (high HP, fast, resistant)
* - Does NOT capture (stays at camp)
* - Manages sale of captives in camp cells
* - Has a Maid who follows orders
* - Token required to interact peacefully
*
* When killed:
* - Camp becomes permanently dead
* - Maid becomes neutral
*/
public class EntitySlaveTrader extends EntityKidnapperElite {
// CONSTANTS
public static final double TRADER_MAX_HEALTH = 50.0D;
public static final double TRADER_MOVEMENT_SPEED = 0.30D;
public static final double TRADER_KNOCKBACK_RESISTANCE = 0.95D;
public static final double TRADER_FOLLOW_RANGE = 40.0D;
public static final double TRADER_ATTACK_DAMAGE = 10.0D;
public static final int TRADER_NAME_COLOR = 0xFFD700; // Gold
// STATE
/** UUID of the maid that serves this trader */
@Nullable
private UUID maidUUID;
/** UUID of the camp this trader manages */
@Nullable
private UUID campUUID;
/** Spawn position (marker location) */
@Nullable
private net.minecraft.core.BlockPos spawnPos;
/** Grace period: warned player UUID and tick */
@Nullable
private UUID warnedPlayerUUID;
private long warningTick;
private static final int GRACE_PERIOD_TICKS = 100; // 5 seconds before warning expires
/** Chase limit: tick when target was set, and max chase duration */
private long chaseStartTick;
private static final int MAX_CHASE_TICKS = 200; // 10 seconds max chase
private static final double MAX_CHASE_DISTANCE_FROM_SPAWN = 30.0;
/** Hostility cooldown (ticks remaining before auto-reset) */
private int hostileCooldownTicks = 0;
private static final int HOSTILE_DURATION_TICKS = 600; // 30 seconds max hostility
// CONSTRUCTOR
public EntitySlaveTrader(
EntityType<? extends EntitySlaveTrader> type,
Level level
) {
super(type, level);
}
// ATTRIBUTES
public static AttributeSupplier.Builder createAttributes() {
return Mob.createMobAttributes()
.add(Attributes.MAX_HEALTH, TRADER_MAX_HEALTH)
.add(Attributes.MOVEMENT_SPEED, TRADER_MOVEMENT_SPEED)
.add(Attributes.KNOCKBACK_RESISTANCE, TRADER_KNOCKBACK_RESISTANCE)
.add(Attributes.FOLLOW_RANGE, TRADER_FOLLOW_RANGE)
.add(Attributes.ATTACK_DAMAGE, TRADER_ATTACK_DAMAGE);
}
// AI GOALS
@Override
protected void registerGoals() {
// Trader has different AI - doesn't hunt, stays at camp
// Priority 0: Always swim
this.goalSelector.addGoal(0, new FloatGoal(this));
// Priority 1: Melee attack if hostile (trader fights but doesn't carry captives)
this.goalSelector.addGoal(1, new MeleeAttackGoal(this, 1.2D, false));
// Priority 4: Sell captives (interact with buyers)
this.goalSelector.addGoal(4, new TraderSellGoal(this));
// Priority 5: Idle/patrol behavior
this.goalSelector.addGoal(5, new TraderIdleGoal(this));
// These were non-functional before (Traders have no PersonalityState).
// Proper fix: Create TraderCommandGoals that use collar owner instead.
// DamselAIController.registerCommandGoals(this.goalSelector, this, 6);
// Priority 10: Look at players
this.goalSelector.addGoal(
10,
new LookAtPlayerGoal(this, Player.class, 8.0F)
);
// Priority 11: Random look around
this.goalSelector.addGoal(11, new RandomLookAroundGoal(this));
// Priority 12: Wander occasionally
this.goalSelector.addGoal(
12,
new WaterAvoidingRandomStrollGoal(this, 0.6D)
);
}
// INTERACTION
@Override
public InteractionResult mobInteract(Player player, InteractionHand hand) {
// Enslaved: use base NPC behavior (feeding, conversation via EntityDamsel)
if (this.isTiedUp()) {
return super.mobInteract(player, hand);
}
if (this.level().isClientSide) {
return InteractionResult.SUCCESS;
}
// Only process main hand to avoid double-triggering
if (hand != InteractionHand.MAIN_HAND) {
return InteractionResult.PASS;
}
// Check if player has token
if (hasTokenInInventory(player)) {
// Open trading screen
if (player instanceof ServerPlayer serverPlayer) {
openTradingScreen(serverPlayer);
}
return InteractionResult.SUCCESS;
} else {
// No token - grace period before becoming hostile
if (player instanceof ServerPlayer serverPlayer) {
if (
warnedPlayerUUID == null ||
!warnedPlayerUUID.equals(player.getUUID())
) {
// First warning — do NOT attack yet
warnedPlayerUUID = player.getUUID();
warningTick = this.tickCount;
serverPlayer.sendSystemMessage(
Component.literal("[" + this.getNpcName() + "] ")
.withStyle(Style.EMPTY.withColor(TRADER_NAME_COLOR))
.append(
Component.translatable(
"entity.tiedup.trader.no_token_warning"
).withStyle(ChatFormatting.RED)
)
);
return InteractionResult.SUCCESS;
}
// Second click → attack directly
this.hostileCooldownTicks = HOSTILE_DURATION_TICKS;
this.setTarget(player);
}
return InteractionResult.SUCCESS;
}
}
/**
* Open the trading screen for a player.
* Gathers available captives and sends packet to client.
*/
private void openTradingScreen(ServerPlayer player) {
if (!(this.level() instanceof ServerLevel serverLevel)) {
return;
}
// Gather captive offers from camp cells
List<CaptiveOfferData> offers = gatherCaptiveOffers(serverLevel);
TiedUpMod.LOGGER.info(
"[EntitySlaveTrader] {} opening trading screen for {} with {} offers",
this.getNpcName(),
player.getName().getString(),
offers.size()
);
// Send packet to open screen
ModNetwork.sendToPlayer(
new PacketOpenTraderScreen(this.getId(), this.getNpcName(), offers),
player
);
}
/**
* Gather captive offers from camp cells.
* Supports both Players and NPCs (Damsels) as captives.
*/
private List<CaptiveOfferData> gatherCaptiveOffers(ServerLevel level) {
List<CaptiveOfferData> offers = new ArrayList<>();
if (campUUID == null) {
return offers;
}
CampOwnership ownership = CampOwnership.get(level);
CampOwnership.CampData campData = ownership.getCamp(campUUID);
if (campData == null || campData.getCenter() == null) {
return offers;
}
CellRegistryV2 cellRegistry = CellRegistryV2.get(level);
List<CellDataV2> cells = cellRegistry.findCellsNear(
campData.getCenter(),
50.0
);
for (CellDataV2 cell : cells) {
if (!cell.isOccupied()) {
continue;
}
// Iterate over ALL prisoners in the cell (not just first)
for (UUID captiveId : cell.getPrisonerIds()) {
// Try to find the captive - first as Player, then as Entity
net.minecraft.world.entity.LivingEntity captiveEntity = null;
String captiveName = null;
// Try player first
ServerPlayer captivePlayer = level
.getServer()
.getPlayerList()
.getPlayer(captiveId);
if (captivePlayer != null) {
captiveEntity = captivePlayer;
captiveName = captivePlayer.getName().getString();
} else {
// Try as entity (e.g., Damsel)
net.minecraft.world.entity.Entity entity = level.getEntity(
captiveId
);
if (
entity instanceof
net.minecraft.world.entity.LivingEntity living
) {
captiveEntity = living;
captiveName = living.getName().getString();
}
}
if (captiveEntity == null) {
continue;
}
IRestrainable kidnappedState =
KidnappedHelper.getKidnappedState(captiveEntity);
if (kidnappedState == null || !kidnappedState.isForSell()) {
continue;
}
ItemTask price = kidnappedState.getSalePrice();
if (price == null) {
continue;
}
String priceDescription =
price.getAmount() +
"x " +
(price.getItem() != null
? price.getItem().getDescription().getString()
: "???");
offers.add(
new CaptiveOfferData(
captiveId,
captiveName,
priceDescription,
price.getAmount(),
price.getItemId()
)
);
}
}
return offers;
}
// Uses EntityKidnapper.hasTokenInInventory(player) - inherited from parent
// TARGETING - Trader doesn't capture
@Override
public void setTarget(@Nullable LivingEntity target) {
super.setTarget(target);
if (target != null) {
this.chaseStartTick = this.tickCount;
}
}
@Override
public boolean isSuitableTarget(
net.minecraft.world.entity.LivingEntity entity
) {
// Trader doesn't actively hunt - only attacks if:
// 1. Entity attacked us (getLastAttacker)
// 2. Entity has no token
if (entity instanceof Player player) {
// Don't target players with tokens
if (hasTokenInInventory(player)) {
return false;
}
}
// Only target if they attacked us
return entity == this.getLastAttacker();
}
// MAID & CAMP MANAGEMENT
@Nullable
public UUID getMaidUUID() {
return maidUUID;
}
public void setMaidUUID(@Nullable UUID maidUUID) {
this.maidUUID = maidUUID;
}
@Nullable
public UUID getCampUUID() {
return campUUID;
}
public void setCampUUID(@Nullable UUID campUUID) {
this.campUUID = campUUID;
}
@Nullable
public net.minecraft.core.BlockPos getSpawnPos() {
return spawnPos;
}
public void setSpawnPos(@Nullable net.minecraft.core.BlockPos spawnPos) {
this.spawnPos = spawnPos;
}
/**
* Clear all hostility state and return to neutral.
*/
private void clearHostility() {
this.setTarget(null);
this.hostileCooldownTicks = 0;
this.warnedPlayerUUID = null;
}
/**
* Get the maid entity if loaded.
*/
@Nullable
public EntityMaid getMaid() {
if (
maidUUID == null ||
!(this.level() instanceof ServerLevel serverLevel)
) {
return null;
}
var entity = serverLevel.getEntity(maidUUID);
return entity instanceof EntityMaid maid ? maid : null;
}
/**
* Order the maid to deliver a captive to a buyer.
*/
public void orderMaidDeliverCaptive(IRestrainable captive, Player buyer) {
EntityMaid maid = getMaid();
if (maid != null) {
maid.startDeliverCaptive(captive, buyer);
}
}
/**
* Order the maid to collect ransom/items.
*/
public void orderMaidCollectItems(BlockPos target) {
EntityMaid maid = getMaid();
if (maid != null) {
maid.startCollectItems(target);
}
}
// DEATH HANDLING
/** Guard against double-cleanup (die() triggers remove(KILLED)) */
private boolean cleanedUp = false;
private void performCleanup() {
if (cleanedUp) return;
cleanedUp = true;
if (
!this.level().isClientSide &&
this.level() instanceof ServerLevel serverLevel
) {
// Mark camp as dead and perform full cleanup
// This will: cancel ransoms, free prisoners, unlock collars, clear labor states
if (campUUID != null) {
CampLifecycleManager.markCampDead(campUUID, serverLevel);
TiedUpMod.LOGGER.info(
"[EntitySlaveTrader] {} removed, camp {} marked as dead and prisoners freed",
this.getNpcName(),
campUUID.toString().substring(0, 8)
);
}
// Free the maid (becomes neutral/capturable)
EntityMaid maid = getMaid();
if (maid != null) {
maid.onTraderDeath();
}
}
}
@Override
public void die(
net.minecraft.world.damagesource.DamageSource damageSource
) {
performCleanup();
super.die(damageSource);
}
@Override
public void remove(Entity.RemovalReason reason) {
performCleanup();
super.remove(reason);
}
// VARIANT SYSTEM
@Override
public KidnapperVariant lookupVariantById(String variantId) {
return TraderSkinManager.CORE.getVariant(variantId);
}
@Override
public KidnapperVariant computeVariantForEntity(UUID entityUUID) {
return TraderSkinManager.CORE.getVariantForEntity(
entityUUID,
Gender.FEMALE
);
}
@Override
public String getVariantTextureFolder() {
return "textures/entity/kidnapper/trader/";
}
@Override
public String getDefaultVariantId() {
return "trader_default";
}
@Override
public String getVariantNBTKey() {
return "TraderVariantId";
}
@Override
public void applyVariantName(KidnapperVariant variant) {
// Numbered variants (trader_mob_1, trader_mob_2, etc.) get random names
// Named variants (trader_default, trader_noble, etc.) use their default name
if (variant.id().startsWith("trader_mob_")) {
this.setNpcName(
com.tiedup.remake.util.NameGenerator.getRandomTraderName()
);
} else {
this.setNpcName(variant.defaultName());
}
}
// DISPLAY
@Override
public Component getDisplayName() {
return Component.literal(this.getNpcName()).withStyle(
Style.EMPTY.withColor(TRADER_NAME_COLOR)
);
}
// NBT PERSISTENCE
@Override
public void addAdditionalSaveData(CompoundTag tag) {
super.addAdditionalSaveData(tag);
if (maidUUID != null) {
tag.putUUID("MaidUUID", maidUUID);
}
if (campUUID != null) {
tag.putUUID("CampUUID", campUUID);
}
if (spawnPos != null) {
tag.putInt("SpawnX", spawnPos.getX());
tag.putInt("SpawnY", spawnPos.getY());
tag.putInt("SpawnZ", spawnPos.getZ());
}
if (hostileCooldownTicks > 0) {
tag.putInt("HostileCooldown", hostileCooldownTicks);
}
}
@Override
public void readAdditionalSaveData(CompoundTag tag) {
super.readAdditionalSaveData(tag);
if (tag.contains("MaidUUID")) {
maidUUID = tag.getUUID("MaidUUID");
}
if (tag.contains("CampUUID")) {
campUUID = tag.getUUID("CampUUID");
}
if (
tag.contains("SpawnX") &&
tag.contains("SpawnY") &&
tag.contains("SpawnZ")
) {
spawnPos = new net.minecraft.core.BlockPos(
tag.getInt("SpawnX"),
tag.getInt("SpawnY"),
tag.getInt("SpawnZ")
);
}
if (tag.contains("HostileCooldown")) {
hostileCooldownTicks = tag.getInt("HostileCooldown");
}
}
// CAPTURE DETECTION
/**
* Check if trader has been captured and removed from camp.
* If captured and taken far from camp (>100 blocks), the camp dies.
*/
@Override
public void tick() {
super.tick();
// Server-side only
if (
!this.level().isClientSide &&
this.level() instanceof ServerLevel serverLevel
) {
// Grace period expiry — just reset warning (player gets a fresh warning next click)
if (
warnedPlayerUUID != null &&
this.tickCount - warningTick > GRACE_PERIOD_TICKS
) {
warnedPlayerUUID = null;
}
// Chase limit — give up if too far from spawn or chasing too long
if (this.getTarget() != null) {
boolean shouldClearTarget = false;
// Timer-based limit (always works, even if spawnPos is null)
boolean tooLong =
this.tickCount - chaseStartTick > MAX_CHASE_TICKS;
if (tooLong) {
shouldClearTarget = true;
}
// Distance-from-spawn limit (when spawnPos is available)
if (!shouldClearTarget && spawnPos != null) {
double distFromSpawn = this.distanceToSqr(
spawnPos.getX() + 0.5,
spawnPos.getY(),
spawnPos.getZ() + 0.5
);
if (
distFromSpawn >
MAX_CHASE_DISTANCE_FROM_SPAWN *
MAX_CHASE_DISTANCE_FROM_SPAWN
) {
shouldClearTarget = true;
}
}
// Token forgiveness — stop chasing if target acquired a token
if (
!shouldClearTarget &&
this.getTarget() instanceof Player targetPlayer &&
hasTokenInInventory(targetPlayer)
) {
shouldClearTarget = true;
}
if (shouldClearTarget) {
clearHostility();
}
}
// Hostility cooldown — auto-reset after duration expires
if (hostileCooldownTicks > 0) {
hostileCooldownTicks--;
if (hostileCooldownTicks <= 0 && this.getTarget() != null) {
clearHostility();
}
}
// Check every second (20 ticks)
if (this.tickCount % 20 == 0) {
checkIfCapturedAndRemoved(serverLevel);
}
}
}
private void checkIfCapturedAndRemoved(ServerLevel level) {
// Must be tied up
if (!isTiedUp()) return;
// Must have a camp
UUID campId = getCampUUID();
if (campId == null) return;
CampOwnership ownership = CampOwnership.get(level);
CampOwnership.CampData camp = ownership.getCamp(campId);
if (camp == null || camp.getCenter() == null) return;
// Check distance from camp center
BlockPos campCenter = camp.getCenter();
double distanceSq = this.distanceToSqr(
campCenter.getX(),
campCenter.getY(),
campCenter.getZ()
);
// If > 100 blocks away → camp destroyed
double CAPTURE_DISTANCE_THRESHOLD = 100.0;
if (
distanceSq > CAPTURE_DISTANCE_THRESHOLD * CAPTURE_DISTANCE_THRESHOLD
) {
TiedUpMod.LOGGER.warn(
"[EntitySlaveTrader] {} captured and removed {} blocks from camp {} - marking camp dead",
this.getNpcName(),
(int) Math.sqrt(distanceSq),
campId.toString().substring(0, 8)
);
// Mark camp as dead (frees all prisoners, etc.)
CampLifecycleManager.markCampDead(campId, level);
// Clear our camp reference
this.setCampUUID(null);
// Broadcast destruction message
broadcastCampDestruction(level, campCenter);
}
}
private void broadcastCampDestruction(
ServerLevel level,
BlockPos campCenter
) {
Component message = Component.translatable(
"entity.tiedup.trader.camp_destroyed"
).withStyle(ChatFormatting.GOLD, ChatFormatting.BOLD);
// Send to all players within 200 blocks
for (ServerPlayer player : level.players()) {
double distSq = player.distanceToSqr(
campCenter.getX(),
campCenter.getY(),
campCenter.getZ()
);
if (distSq < 200 * 200) {
player.sendSystemMessage(message);
}
}
}
// STRUGGLE DETECTION
/** Target prisoner to approach after catching them struggling */
@Nullable
private ServerPlayer strugglePunishmentTarget;
/** Maximum distance to HEAR struggle (through bars, no line of sight needed) */
private static final double STRUGGLE_HEARING_RANGE = 6.0;
/** Maximum distance to SEE struggle (requires line of sight) */
private static final double STRUGGLE_VISION_RANGE = 15.0;
/**
* Called when a prisoner is detected struggling nearby.
* Uses HYBRID detection: HEARING (close range) + VISION (far range).
*
* Detection modes:
* - Within 6 blocks: Can HEAR through bars/fences (no line of sight needed)
* - Within 15 blocks: Can SEE if line of sight is clear
*
* If detected:
* 1. Shock the prisoner (punishment - harder than maid!)
* 2. Approach to tighten their binds (reset resistance)
*
* @param prisoner The player who is struggling
*/
public void onStruggleDetected(ServerPlayer prisoner) {
// HYBRID DETECTION: Hearing (close) + Vision (far)
double distance = this.distanceTo(prisoner);
// HEARING: Close range - can hear through bars/fences (no LOS needed)
boolean canHear = distance <= STRUGGLE_HEARING_RANGE;
// VISION: Longer range - requires clear line of sight
boolean canSee =
distance <= STRUGGLE_VISION_RANGE &&
this.getSensing().hasLineOfSight(prisoner);
if (!canHear && !canSee) {
return; // Can't detect the struggle
}
// Check if this player is a prisoner of our camp
if (campUUID == null) {
return;
}
if (!(this.level() instanceof ServerLevel serverLevel)) {
return;
}
PrisonerManager manager = PrisonerManager.get(serverLevel);
PrisonerRecord record = manager.getPrisoner(prisoner.getUUID());
if (record == null || !campUUID.equals(record.getCampId())) {
return; // Not our prisoner
}
IRestrainable state = KidnappedHelper.getKidnappedState(prisoner);
if (state == null || !state.isTiedUp()) {
return;
}
String detectionMethod = canHear ? "heard" : "saw";
TiedUpMod.LOGGER.info(
"[EntitySlaveTrader] {} {} {} struggling! Punishing... (distance: {})",
this.getNpcName(),
detectionMethod,
prisoner.getName().getString(),
distance
);
// PUNISHMENT: Shock the prisoner
state.shockKidnapped(" Don't even think about it.", 3.0f); // Trader shocks harder
// TIGHTEN BINDS: Reset resistance to maximum
tightenBinds(state, prisoner);
// Look at the prisoner menacingly
this.getLookControl().setLookAt(prisoner, 30.0f, 30.0f);
// Set as target to approach
this.strugglePunishmentTarget = prisoner;
}
/**
* Tighten the prisoner's binds by resetting their resistance to maximum.
* This happens when a guard catches someone struggling.
*
* @param state The prisoner's kidnapped state
* @param prisoner The prisoner entity
*/
private void tightenBinds(IRestrainable state, LivingEntity prisoner) {
com.tiedup.remake.util.RestraintApplicator.tightenBind(state, prisoner);
}
/**
* Get the current struggle punishment target.
* @return The prisoner to approach, or null if none
*/
@Nullable
public ServerPlayer getStrugglePunishmentTarget() {
return this.strugglePunishmentTarget;
}
/**
* Clear the struggle punishment target (after approaching or timeout).
*/
public void clearStrugglePunishmentTarget() {
this.strugglePunishmentTarget = null;
}
// DIALOGUE SPEAKER (Trader-specific)
@Override
public SpeakerType getSpeakerType() {
return SpeakerType.TRADER;
}
@Override
public PersonalityType getSpeakerPersonality() {
// Traders are greedy business-oriented
return PersonalityType.PROUD;
}
@Override
public int getSpeakerMood() {
// Mood based on business (captives to sell)
// Count captives in camp cells
int captiveCount = 0;
if (
campUUID != null && this.level() instanceof ServerLevel serverLevel
) {
CampOwnership ownership = CampOwnership.get(serverLevel);
CampOwnership.CampData campData = ownership.getCamp(campUUID);
if (campData != null && campData.getCenter() != null) {
CellRegistryV2 cellRegistry = CellRegistryV2.get(serverLevel);
List<CellDataV2> cells = cellRegistry.findCellsNear(
campData.getCenter(),
50.0
);
for (CellDataV2 cell : cells) {
if (cell.isOccupied()) {
captiveCount += cell.getPrisonerIds().size();
}
}
}
}
if (captiveCount > 2) {
return 85; // Excellent business!
} else if (captiveCount > 0) {
return 70; // Good business
}
return 50; // Waiting for captives
}
@Override
public String getTargetRelation(Player player) {
// Check if player has trader token (friendly)
// For now, just return customer if interacting
return "customer";
}
}