No more direct ModConfig.SERVER access outside SettingsAccessor. 32 new accessor methods, 21 consumer files rerouted.
458 lines
13 KiB
Java
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
|
|
}
|
|
}
|