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:
929
src/main/java/com/tiedup/remake/entities/EntitySlaveTrader.java
Normal file
929
src/main/java/com/tiedup/remake/entities/EntitySlaveTrader.java
Normal file
@@ -0,0 +1,929 @@
|
||||
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));
|
||||
|
||||
// FUTURE: Trader command goals require personality system (EntityDamsel-only).
|
||||
// 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.literal(
|
||||
"You don't have a trader token. Leave now, or I'll make you leave."
|
||||
).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.literal(
|
||||
"A slave trader camp has been 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";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user