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 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 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 targetHitCounts = new HashMap<>(); // CONSTRUCTOR public EntityKidnapperArcher( EntityType 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 } }