package net.minecraft.world.item; import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; import java.util.List; import java.util.Optional; import java.util.function.Predicate; import net.minecraft.advancements.CriteriaTriggers; import net.minecraft.core.Holder; import net.minecraft.core.component.DataComponents; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.sounds.SoundEvent; import net.minecraft.sounds.SoundEvents; import net.minecraft.sounds.SoundSource; import net.minecraft.stats.Stats; import net.minecraft.util.Mth; import net.minecraft.util.RandomSource; import net.minecraft.util.StringRepresentable; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.projectile.AbstractArrow; import net.minecraft.world.entity.projectile.FireworkRocketEntity; import net.minecraft.world.entity.projectile.Projectile; import net.minecraft.world.item.component.ChargedProjectiles; import net.minecraft.world.item.enchantment.EnchantmentEffectComponents; import net.minecraft.world.item.enchantment.EnchantmentHelper; import net.minecraft.world.level.Level; import net.minecraft.world.phys.Vec3; import org.jetbrains.annotations.Nullable; import org.joml.Quaternionf; import org.joml.Vector3f; public class CrossbowItem extends ProjectileWeaponItem { private static final float MAX_CHARGE_DURATION = 1.25F; public static final int DEFAULT_RANGE = 8; /** * Set to {@code true} when the crossbow is 20% charged. */ private boolean startSoundPlayed = false; /** * Set to {@code true} when the crossbow is 50% charged. */ private boolean midLoadSoundPlayed = false; private static final float START_SOUND_PERCENT = 0.2F; private static final float MID_SOUND_PERCENT = 0.5F; private static final float ARROW_POWER = 3.15F; private static final float FIREWORK_POWER = 1.6F; public static final float MOB_ARROW_POWER = 1.6F; private static final CrossbowItem.ChargingSounds DEFAULT_SOUNDS = new CrossbowItem.ChargingSounds( Optional.of(SoundEvents.CROSSBOW_LOADING_START), Optional.of(SoundEvents.CROSSBOW_LOADING_MIDDLE), Optional.of(SoundEvents.CROSSBOW_LOADING_END) ); public CrossbowItem(Item.Properties properties) { super(properties); } @Override public Predicate getSupportedHeldProjectiles() { return ARROW_OR_FIREWORK; } @Override public Predicate getAllSupportedProjectiles() { return ARROW_ONLY; } @Override public InteractionResult use(Level level, Player player, InteractionHand hand) { ItemStack itemStack = player.getItemInHand(hand); ChargedProjectiles chargedProjectiles = itemStack.get(DataComponents.CHARGED_PROJECTILES); if (chargedProjectiles != null && !chargedProjectiles.isEmpty()) { this.performShooting(level, player, hand, itemStack, getShootingPower(chargedProjectiles), 1.0F, null); return InteractionResult.CONSUME; } else if (!player.getProjectile(itemStack).isEmpty()) { this.startSoundPlayed = false; this.midLoadSoundPlayed = false; player.startUsingItem(hand); return InteractionResult.CONSUME; } else { return InteractionResult.FAIL; } } private static float getShootingPower(ChargedProjectiles projectile) { return projectile.contains(Items.FIREWORK_ROCKET) ? 1.6F : 3.15F; } @Override public boolean releaseUsing(ItemStack stack, Level level, LivingEntity entity, int timeLeft) { int i = this.getUseDuration(stack, entity) - timeLeft; return getPowerForTime(i, stack, entity) >= 1.0F && isCharged(stack); } private static boolean tryLoadProjectiles(LivingEntity shooter, ItemStack crossbowStack) { List list = draw(crossbowStack, shooter.getProjectile(crossbowStack), shooter); if (!list.isEmpty()) { crossbowStack.set(DataComponents.CHARGED_PROJECTILES, ChargedProjectiles.of(list)); return true; } else { return false; } } public static boolean isCharged(ItemStack crossbowStack) { ChargedProjectiles chargedProjectiles = crossbowStack.getOrDefault(DataComponents.CHARGED_PROJECTILES, ChargedProjectiles.EMPTY); return !chargedProjectiles.isEmpty(); } @Override protected void shootProjectile( LivingEntity shooter, Projectile projectile, int index, float velocity, float inaccuracy, float angle, @Nullable LivingEntity target ) { Vector3f vector3f; if (target != null) { double d = target.getX() - shooter.getX(); double e = target.getZ() - shooter.getZ(); double f = Math.sqrt(d * d + e * e); double g = target.getY(0.3333333333333333) - projectile.getY() + f * 0.2F; vector3f = getProjectileShotVector(shooter, new Vec3(d, g, e), angle); } else { Vec3 vec3 = shooter.getUpVector(1.0F); Quaternionf quaternionf = new Quaternionf().setAngleAxis((double)(angle * (float) (Math.PI / 180.0)), vec3.x, vec3.y, vec3.z); Vec3 vec32 = shooter.getViewVector(1.0F); vector3f = vec32.toVector3f().rotate(quaternionf); } projectile.shoot(vector3f.x(), vector3f.y(), vector3f.z(), velocity, inaccuracy); float h = getShotPitch(shooter.getRandom(), index); shooter.level().playSound(null, shooter.getX(), shooter.getY(), shooter.getZ(), SoundEvents.CROSSBOW_SHOOT, shooter.getSoundSource(), 1.0F, h); } private static Vector3f getProjectileShotVector(LivingEntity shooter, Vec3 distance, float angle) { Vector3f vector3f = distance.toVector3f().normalize(); Vector3f vector3f2 = new Vector3f(vector3f).cross(new Vector3f(0.0F, 1.0F, 0.0F)); if (vector3f2.lengthSquared() <= 1.0E-7) { Vec3 vec3 = shooter.getUpVector(1.0F); vector3f2 = new Vector3f(vector3f).cross(vec3.toVector3f()); } Vector3f vector3f3 = new Vector3f(vector3f).rotateAxis((float) (Math.PI / 2), vector3f2.x, vector3f2.y, vector3f2.z); return new Vector3f(vector3f).rotateAxis(angle * (float) (Math.PI / 180.0), vector3f3.x, vector3f3.y, vector3f3.z); } @Override protected Projectile createProjectile(Level level, LivingEntity shooter, ItemStack weapon, ItemStack ammo, boolean isCrit) { if (ammo.is(Items.FIREWORK_ROCKET)) { return new FireworkRocketEntity(level, ammo, shooter, shooter.getX(), shooter.getEyeY() - 0.15F, shooter.getZ(), true); } else { Projectile projectile = super.createProjectile(level, shooter, weapon, ammo, isCrit); if (projectile instanceof AbstractArrow abstractArrow) { abstractArrow.setSoundEvent(SoundEvents.CROSSBOW_HIT); } return projectile; } } @Override protected int getDurabilityUse(ItemStack stack) { return stack.is(Items.FIREWORK_ROCKET) ? 3 : 1; } public void performShooting( Level level, LivingEntity shooter, InteractionHand hand, ItemStack weapon, float velocity, float inaccuracy, @Nullable LivingEntity target ) { if (level instanceof ServerLevel serverLevel) { ChargedProjectiles chargedProjectiles = weapon.set(DataComponents.CHARGED_PROJECTILES, ChargedProjectiles.EMPTY); if (chargedProjectiles != null && !chargedProjectiles.isEmpty()) { this.shoot(serverLevel, shooter, hand, weapon, chargedProjectiles.getItems(), velocity, inaccuracy, shooter instanceof Player, target); if (shooter instanceof ServerPlayer serverPlayer) { CriteriaTriggers.SHOT_CROSSBOW.trigger(serverPlayer, weapon); serverPlayer.awardStat(Stats.ITEM_USED.get(weapon.getItem())); } } } } private static float getShotPitch(RandomSource random, int index) { return index == 0 ? 1.0F : getRandomShotPitch((index & 1) == 1, random); } private static float getRandomShotPitch(boolean isHighPitched, RandomSource random) { float f = isHighPitched ? 0.63F : 0.43F; return 1.0F / (random.nextFloat() * 0.5F + 1.8F) + f; } @Override public void onUseTick(Level level, LivingEntity livingEntity, ItemStack stack, int remainingUseDuration) { if (!level.isClientSide) { CrossbowItem.ChargingSounds chargingSounds = this.getChargingSounds(stack); float f = (float)(stack.getUseDuration(livingEntity) - remainingUseDuration) / getChargeDuration(stack, livingEntity); if (f < 0.2F) { this.startSoundPlayed = false; this.midLoadSoundPlayed = false; } if (f >= 0.2F && !this.startSoundPlayed) { this.startSoundPlayed = true; chargingSounds.start() .ifPresent( holder -> level.playSound( null, livingEntity.getX(), livingEntity.getY(), livingEntity.getZ(), (SoundEvent)holder.value(), SoundSource.PLAYERS, 0.5F, 1.0F ) ); } if (f >= 0.5F && !this.midLoadSoundPlayed) { this.midLoadSoundPlayed = true; chargingSounds.mid() .ifPresent( holder -> level.playSound( null, livingEntity.getX(), livingEntity.getY(), livingEntity.getZ(), (SoundEvent)holder.value(), SoundSource.PLAYERS, 0.5F, 1.0F ) ); } if (f >= 1.0F && !isCharged(stack) && tryLoadProjectiles(livingEntity, stack)) { chargingSounds.end() .ifPresent( holder -> level.playSound( null, livingEntity.getX(), livingEntity.getY(), livingEntity.getZ(), (SoundEvent)holder.value(), livingEntity.getSoundSource(), 1.0F, 1.0F / (level.getRandom().nextFloat() * 0.5F + 1.0F) + 0.2F ) ); } } } @Override public int getUseDuration(ItemStack stack, LivingEntity entity) { return 72000; } public static int getChargeDuration(ItemStack stack, LivingEntity shooter) { float f = EnchantmentHelper.modifyCrossbowChargingTime(stack, shooter, 1.25F); return Mth.floor(f * 20.0F); } @Override public ItemUseAnimation getUseAnimation(ItemStack stack) { return ItemUseAnimation.CROSSBOW; } CrossbowItem.ChargingSounds getChargingSounds(ItemStack stack) { return (CrossbowItem.ChargingSounds)EnchantmentHelper.pickHighestLevel(stack, EnchantmentEffectComponents.CROSSBOW_CHARGING_SOUNDS).orElse(DEFAULT_SOUNDS); } private static float getPowerForTime(int timeLeft, ItemStack stack, LivingEntity shooter) { float f = (float)timeLeft / getChargeDuration(stack, shooter); if (f > 1.0F) { f = 1.0F; } return f; } @Override public boolean useOnRelease(ItemStack stack) { return stack.is(this); } @Override public int getDefaultProjectileRange() { return 8; } public static enum ChargeType implements StringRepresentable { NONE("none"), ARROW("arrow"), ROCKET("rocket"); public static final Codec CODEC = StringRepresentable.fromEnum(CrossbowItem.ChargeType::values); private final String name; private ChargeType(final String name) { this.name = name; } @Override public String getSerializedName() { return this.name; } } public record ChargingSounds(Optional> start, Optional> mid, Optional> end) { public static final Codec CODEC = RecordCodecBuilder.create( instance -> instance.group( SoundEvent.CODEC.optionalFieldOf("start").forGetter(CrossbowItem.ChargingSounds::start), SoundEvent.CODEC.optionalFieldOf("mid").forGetter(CrossbowItem.ChargingSounds::mid), SoundEvent.CODEC.optionalFieldOf("end").forGetter(CrossbowItem.ChargingSounds::end) ) .apply(instance, CrossbowItem.ChargingSounds::new) ); } }