Files
TiedUp-/src/main/java/com/tiedup/remake/entities/EntityKidnapperArcher.java
NotEvil 4e136cff96 centralize all ModConfig.SERVER reads through SettingsAccessor
No more direct ModConfig.SERVER access outside SettingsAccessor.
32 new accessor methods, 21 consumer files rerouted.
2026-04-16 13:16:05 +02:00

458 lines
13 KiB
Java

package com.tiedup.remake.entities;
import com.tiedup.remake.core.SettingsAccessor;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.dialogue.SpeakerType;
import com.tiedup.remake.entities.ai.kidnapper.KidnapperArcherRangedGoal;
import com.tiedup.remake.entities.skins.ArcherKidnapperSkinManager;
import com.tiedup.remake.entities.skins.Gender;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.Style;
import net.minecraft.network.syncher.EntityDataAccessor;
import net.minecraft.network.syncher.EntityDataSerializers;
import net.minecraft.network.syncher.SynchedEntityData;
import net.minecraft.world.InteractionHand;
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.monster.RangedAttackMob;
import net.minecraft.world.item.BowItem;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.UseAnim;
import net.minecraft.world.level.Level;
/**
* Archer Kidnapper - Ranged kidnapper that attacks with rope arrows.
*
*
* Differences from regular EntityKidnapper:
* - Attacks from range with rope arrows
* - Lower health (15 vs 20)
* - Faster movement for repositioning (0.30 vs 0.27)
* - Only approaches to capture once target is tied
* - Red name color (same as normal kidnapper)
* - Dedicated archer skins
* - Implements RangedAttackMob for bow animation
*
* Refactored: Uses parent's variant system via virtual methods.
*/
public class EntityKidnapperArcher
extends EntityKidnapper
implements RangedAttackMob
{
// CONSTANTS
public static final double ARCHER_MAX_HEALTH = 15.0D;
public static final double ARCHER_MOVEMENT_SPEED = 0.30D;
public static final double ARCHER_KNOCKBACK_RESISTANCE = 0.3D;
public static final double ARCHER_FOLLOW_RANGE = 40.0D;
public static final double ARCHER_ATTACK_DAMAGE = 4.0D;
public static final int ARCHER_CAPTURE_TIME_TICKS = 25;
public static final int ARCHER_NAME_COLOR = 0xFF5555;
public static final float ARROW_VELOCITY = 1.6F;
public static final float ARROW_INACCURACY = 2.0F;
// DATA SYNC (Archer-specific)
/**
* Whether the archer is currently aiming (for bow draw animation).
*/
private static final EntityDataAccessor<Boolean> DATA_AIMING =
SynchedEntityData.defineId(
EntityKidnapperArcher.class,
EntityDataSerializers.BOOLEAN
);
/**
* Whether the archer is in ranged attack mode (has active shooting target).
* Used for bow visibility and ready pose.
*/
private static final EntityDataAccessor<Boolean> DATA_IN_RANGED_MODE =
SynchedEntityData.defineId(
EntityKidnapperArcher.class,
EntityDataSerializers.BOOLEAN
);
// STATE
/** Ticks the archer has been charging the bow */
private int aimingTicks;
// HIT TRACKING (for cumulative bind chance)
/** Maximum bind chance (100%) */
public static final int ARCHER_MAX_BIND_CHANCE = 100;
/** Track hit counts per target UUID */
private final Map<UUID, Integer> targetHitCounts = new HashMap<>();
// CONSTRUCTOR
public EntityKidnapperArcher(
EntityType<? extends EntityKidnapperArcher> type,
Level level
) {
super(type, level);
// Force right-handed so bow is always in main hand (right)
this.setLeftHanded(false);
}
// ATTRIBUTES
/**
* Create archer kidnapper attributes.
* Fast but fragile - relies on range.
*/
public static AttributeSupplier.Builder createAttributes() {
return Mob.createMobAttributes()
.add(Attributes.MAX_HEALTH, ARCHER_MAX_HEALTH)
.add(Attributes.MOVEMENT_SPEED, ARCHER_MOVEMENT_SPEED)
.add(Attributes.KNOCKBACK_RESISTANCE, ARCHER_KNOCKBACK_RESISTANCE)
.add(Attributes.FOLLOW_RANGE, ARCHER_FOLLOW_RANGE)
.add(Attributes.ATTACK_DAMAGE, ARCHER_ATTACK_DAMAGE);
}
// DATA SYNC
@Override
protected void defineSynchedData() {
super.defineSynchedData();
this.entityData.define(DATA_AIMING, false);
this.entityData.define(DATA_IN_RANGED_MODE, false);
}
// VARIANT SYSTEM - Override virtual methods
@Override
public KidnapperVariant lookupVariantById(String variantId) {
return ArcherKidnapperSkinManager.CORE.getVariant(variantId);
}
@Override
public KidnapperVariant computeVariantForEntity(UUID entityUUID) {
Gender preferredGender = SettingsAccessor.getPreferredSpawnGender(
this.level() != null ? this.level().getGameRules() : null
);
return ArcherKidnapperSkinManager.CORE.getVariantForEntity(
entityUUID,
preferredGender
);
}
@Override
public String getVariantTextureFolder() {
return "textures/entity/kidnapper/archer/";
}
@Override
public String getDefaultVariantId() {
return "bowy";
}
@Override
public void applyVariantName(KidnapperVariant variant) {
// Archer variants always use their default name
this.setNpcName(variant.defaultName());
}
@Override
public String getVariantNBTKey() {
// Use different key for backward compatibility with existing saves
return "ArcherVariantName";
}
// AI GOALS
@Override
protected void registerGoals() {
// Call parent to register all standard kidnapper goals
super.registerGoals();
// Add ranged attack goal with priority 1 (same as FindTarget)
// This makes the archer shoot first, then approach to capture
this.goalSelector.addGoal(1, new KidnapperArcherRangedGoal(this));
TiedUpMod.LOGGER.debug(
"[EntityKidnapperArcher] Registered ranged attack goal"
);
}
// INITIALIZATION
@Override
public void onAddedToWorld() {
super.onAddedToWorld();
// Server-side: Give archer a bow if not holding one
if (!this.level().isClientSide && this.getMainHandItem().isEmpty()) {
this.setItemInHand(
InteractionHand.MAIN_HAND,
new ItemStack(Items.BOW)
);
TiedUpMod.LOGGER.debug(
"[EntityKidnapperArcher] Gave bow to {}",
this.getNpcName()
);
}
}
// RANGED ATTACK MOB INTERFACE
/**
* Called by the ranged goal to perform the actual attack.
* Required by RangedAttackMob interface.
*/
@Override
public void performRangedAttack(LivingEntity target, float velocity) {
// Create and shoot rope arrow
EntityRopeArrow arrow = new EntityRopeArrow(this.level(), this);
double dx = target.getX() - this.getX();
double dy = target.getY(0.33) - arrow.getY();
double dz = target.getZ() - this.getZ();
double horizontalDist = Math.sqrt(dx * dx + dz * dz);
arrow.shoot(
dx,
dy + horizontalDist * 0.2,
dz,
ARROW_VELOCITY,
ARROW_INACCURACY
);
// Play bow sound
this.level().playSound(
null,
this.getX(),
this.getY(),
this.getZ(),
net.minecraft.sounds.SoundEvents.ARROW_SHOOT,
this.getSoundSource(),
1.0F,
1.0F / (this.getRandom().nextFloat() * 0.4F + 0.8F)
);
this.level().addFreshEntity(arrow);
TiedUpMod.LOGGER.debug(
"[EntityKidnapperArcher] {} shot arrow at {}",
this.getNpcName(),
target.getName().getString()
);
}
// AIMING STATE (for animation)
/**
* Set whether the archer is currently aiming.
*/
public void setAiming(boolean aiming) {
if (aiming) {
this.aimingTicks = 0;
this.startUsingItem(InteractionHand.MAIN_HAND);
} else {
this.aimingTicks = 0;
this.stopUsingItem();
}
this.entityData.set(DATA_AIMING, aiming);
}
/**
* Check if the archer is currently aiming.
*/
public boolean isAiming() {
return this.entityData.get(DATA_AIMING);
}
/**
* Set whether the archer is in ranged attack mode.
*/
public void setInRangedMode(boolean inRangedMode) {
this.entityData.set(DATA_IN_RANGED_MODE, inRangedMode);
}
/**
* Check if the archer is in ranged attack mode.
*/
public boolean isInRangedMode() {
return this.entityData.get(DATA_IN_RANGED_MODE);
}
/**
* Get how long the archer has been aiming (for bow pull animation).
*/
public int getAimingTicks() {
return this.aimingTicks;
}
@Override
public void tick() {
super.tick();
if (this.isAiming()) {
this.aimingTicks++;
}
}
@Override
public boolean isUsingItem() {
return this.isAiming() || super.isUsingItem();
}
@Override
public InteractionHand getUsedItemHand() {
if (this.isAiming()) {
return InteractionHand.MAIN_HAND;
}
return super.getUsedItemHand();
}
/**
* Get the use animation type for the item.
*/
public UseAnim getItemUseAnimation() {
if (
this.isAiming() &&
this.getMainHandItem().getItem() instanceof BowItem
) {
return UseAnim.BOW;
}
return UseAnim.NONE;
}
// HIT TRACKING METHODS
/**
* Get the current bind chance for a target.
* Base 10% + 10% per previous hit.
*/
public int getBindChanceForTarget(UUID targetUUID) {
int hitCount = targetHitCounts.getOrDefault(targetUUID, 0);
int baseChance =
SettingsAccessor.getArcherArrowBindChanceBase();
int perHitChance =
SettingsAccessor.getArcherArrowBindChancePerHit();
int chance = baseChance + (hitCount * perHitChance);
return Math.min(chance, ARCHER_MAX_BIND_CHANCE);
}
/**
* Record a hit on a target (increases future bind chance).
*/
public void recordHitOnTarget(UUID targetUUID) {
int currentHits = targetHitCounts.getOrDefault(targetUUID, 0);
targetHitCounts.put(targetUUID, currentHits + 1);
TiedUpMod.LOGGER.debug(
"[EntityKidnapperArcher] {} hit target, new hit count: {}",
this.getNpcName(),
currentHits + 1
);
}
/**
* Clear hit counts for a specific target (called when target is captured).
*/
public void clearHitsForTarget(UUID targetUUID) {
targetHitCounts.remove(targetUUID);
}
/**
* Clear all hit counts (called when archer changes target).
*/
public void clearAllHitCounts() {
targetHitCounts.clear();
}
// DISPLAY
@Override
public Component getDisplayName() {
return Component.literal(this.getNpcName()).withStyle(
Style.EMPTY.withColor(ARCHER_NAME_COLOR)
);
}
// OVERRIDES
/**
* Archers take slightly longer to capture up close.
*/
@Override
public int getCaptureBindTime() {
return ARCHER_CAPTURE_TIME_TICKS;
}
/**
* Archers take slightly longer to gag.
*/
@Override
public int getCaptureGagTime() {
return ARCHER_CAPTURE_TIME_TICKS;
}
/**
* Archers use lower item probabilities - they rely on arrows.
*/
@Override
public KidnapperItemSelector.SelectionResult selectItemsForKidnapper() {
return KidnapperItemSelector.selectForArcherKidnapper();
}
/**
* Check if archer can use a bow (for animation layer).
*/
public boolean canUseBow() {
ItemStack mainHand = this.getMainHandItem();
return mainHand.getItem() instanceof BowItem;
}
// BOW RESTORATION
/**
* Override clearHeldItems to restore the bow.
* Archer should always have bow in hand when not actively capturing.
*/
@Override
public void clearHeldItems() {
// Restore bow in main hand instead of clearing
this.setItemSlot(
net.minecraft.world.entity.EquipmentSlot.MAINHAND,
new ItemStack(Items.BOW)
);
// Clear off hand
this.setItemSlot(
net.minecraft.world.entity.EquipmentSlot.OFFHAND,
ItemStack.EMPTY
);
TiedUpMod.LOGGER.debug(
"[EntityKidnapperArcher] {} restored bow after capture phase",
this.getNpcName()
);
}
// DIALOGUE SPEAKER (Archer-specific)
@Override
public SpeakerType getSpeakerType() {
return SpeakerType.KIDNAPPER_ARCHER;
}
@Override
public int getSpeakerMood() {
// Archers are calm and precise
if (this.isAiming()) {
return 60; // Focused
}
return 55; // Calm
}
}