diff --git a/src/main/java/slimeknights/tconstruct/library/modifiers/hook/ranged/BowAmmoModifierHook.java b/src/main/java/slimeknights/tconstruct/library/modifiers/hook/ranged/BowAmmoModifierHook.java index 5d6eb8f74e5..bd184d1cc0b 100644 --- a/src/main/java/slimeknights/tconstruct/library/modifiers/hook/ranged/BowAmmoModifierHook.java +++ b/src/main/java/slimeknights/tconstruct/library/modifiers/hook/ranged/BowAmmoModifierHook.java @@ -100,21 +100,21 @@ private static ItemStack findMatchingAmmo(ItemStack bow, LivingEntity living, Pr * @param tool Tool instance * @param bow Bow stack instance * @param predicate Predicate for valid ammo - * @param player Player to search + * @param holder Weapon holder to search ammo from * @return Found ammo */ - static ItemStack findAmmo(IToolStackView tool, ItemStack bow, Player player, Predicate predicate) { + static ItemStack findAmmo(IToolStackView tool, ItemStack bow, LivingEntity holder, Predicate predicate) { int projectilesDesired = 1 + (2 * tool.getModifierLevel(TinkerModifiers.multishot.getId())); // treat client side as creative, no need to shrink the stacks clientside - Level level = player.level(); - boolean creative = player.getAbilities().instabuild || level.isClientSide; + Level level = holder.level(); + boolean creative = holder instanceof Player player && player.getAbilities().instabuild || level.isClientSide; // first search, find what ammo type we want - ItemStack standardAmmo = player.getProjectile(bow); + ItemStack standardAmmo = holder.getProjectile(bow); ItemStack resultStack = ItemStack.EMPTY; for (ModifierEntry entry : tool.getModifierList()) { BowAmmoModifierHook hook = entry.getHook(ModifierHooks.BOW_AMMO); - ItemStack ammo = hook.findAmmo(tool, entry, player, standardAmmo, predicate); + ItemStack ammo = hook.findAmmo(tool, entry, holder, standardAmmo, predicate); if (!ammo.isEmpty()) { // if creative, we are done, just return the ammo with the given size if (creative) { @@ -123,7 +123,7 @@ static ItemStack findAmmo(IToolStackView tool, ItemStack bow, Player player, Pre // not creative, split out the desired amount. We may have to do more work if it is too small resultStack = ItemHandlerHelper.copyStackWithSize(ammo, Math.min(projectilesDesired, ammo.getCount())); - hook.shrinkAmmo(tool, entry, player, ammo, resultStack.getCount()); + hook.shrinkAmmo(tool, entry, holder, ammo, resultStack.getCount()); break; } } @@ -140,7 +140,7 @@ static ItemStack findAmmo(IToolStackView tool, ItemStack bow, Player player, Pre } // make a copy of the result, up to the desired size resultStack = standardAmmo.split(projectilesDesired); - if (standardAmmo.isEmpty()) { + if (standardAmmo.isEmpty() && holder instanceof Player player) { player.getInventory().removeItem(standardAmmo); } } @@ -159,17 +159,17 @@ static ItemStack findAmmo(IToolStackView tool, ItemStack bow, Player player, Pre do { // if standard ammo is empty, try finding a matching stack again if (standardAmmo.isEmpty()) { - standardAmmo = findMatchingAmmo(bow, player, predicate); + standardAmmo = findMatchingAmmo(bow, holder, predicate); } // next, try asking modifiers if they have anything new again int needed = projectilesDesired - resultStack.getCount(); for (ModifierEntry entry : tool.getModifierList()) { BowAmmoModifierHook hook = entry.getHook(ModifierHooks.BOW_AMMO); - ItemStack ammo = hook.findAmmo(tool, entry, player, standardAmmo, predicate); + ItemStack ammo = hook.findAmmo(tool, entry, holder, standardAmmo, predicate); if (!ammo.isEmpty()) { // consume as much of the stack as we need then continue, loop condition will stop if we are now done int gained = Math.min(needed, ammo.getCount()); - hook.shrinkAmmo(tool, entry, player, ammo, gained); + hook.shrinkAmmo(tool, entry, holder, ammo, gained); resultStack.grow(gained); continue hasEnough; } @@ -183,7 +183,10 @@ static ItemStack findAmmo(IToolStackView tool, ItemStack bow, Player player, Pre if (needed > standardAmmo.getCount()) { // consume the whole stack resultStack.grow(standardAmmo.getCount()); - player.getInventory().removeItem(standardAmmo); + standardAmmo.setCount(0); + if (holder instanceof Player player) { + player.getInventory().removeItem(standardAmmo); + } standardAmmo = ItemStack.EMPTY; } else { // found what we need, we are done diff --git a/src/main/java/slimeknights/tconstruct/library/tools/item/ranged/ModifiableBowItem.java b/src/main/java/slimeknights/tconstruct/library/tools/item/ranged/ModifiableBowItem.java index b2f3805ccc9..f464900d508 100644 --- a/src/main/java/slimeknights/tconstruct/library/tools/item/ranged/ModifiableBowItem.java +++ b/src/main/java/slimeknights/tconstruct/library/tools/item/ranged/ModifiableBowItem.java @@ -3,32 +3,28 @@ 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.world.InteractionHand; import net.minecraft.world.InteractionResultHolder; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.player.Player; -import net.minecraft.world.entity.projectile.AbstractArrow; -import net.minecraft.world.item.ArrowItem; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.item.ProjectileWeaponItem; import net.minecraft.world.item.UseAnim; import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; import net.minecraftforge.common.ToolActions; import net.minecraftforge.event.ForgeEventFactory; +import org.joml.Vector3f; import slimeknights.tconstruct.common.Sounds; -import slimeknights.tconstruct.library.modifiers.ModifierEntry; -import slimeknights.tconstruct.library.modifiers.ModifierHooks; import slimeknights.tconstruct.library.modifiers.hook.build.ConditionalStatModifierHook; import slimeknights.tconstruct.library.modifiers.hook.interaction.GeneralInteractionModifierHook; import slimeknights.tconstruct.library.modifiers.hook.ranged.BowAmmoModifierHook; -import slimeknights.tconstruct.library.tools.capability.EntityModifierCapability; -import slimeknights.tconstruct.library.tools.capability.PersistentDataCapability; import slimeknights.tconstruct.library.tools.definition.ToolDefinition; import slimeknights.tconstruct.library.tools.helper.ModifierUtil; import slimeknights.tconstruct.library.tools.helper.ToolDamageUtil; -import slimeknights.tconstruct.library.tools.nbt.ModifierNBT; -import slimeknights.tconstruct.library.tools.nbt.ModDataNBT; import slimeknights.tconstruct.library.tools.nbt.ToolStack; import slimeknights.tconstruct.library.tools.stat.ToolStats; import slimeknights.tconstruct.tools.modifiers.ability.interaction.BlockingModifier; @@ -95,6 +91,17 @@ public InteractionResultHolder use(Level level, Player player, Intera return InteractionResultHolder.consume(bow); } + @Override + public void playShotSound(LivingEntity user, float charge, float angle, RandomSource random) { + user.level().playSound(null, user.getX(), user.getY(), user.getZ(), SoundEvents.ARROW_SHOOT, SoundSource.PLAYERS, 1.0F, + 1.0F / (random.nextFloat() * 0.4F + 1.2F) + charge * 0.5F + (angle / 10f)); + } + + @Override + public Vector3f modifyShootAngle(LivingEntity user, Vec3 target, float angle) { + return target.xRot(angle * Mth.DEG_TO_RAD).toVector3f(); + } + @Override public void releaseUsing(ItemStack bow, Level level, LivingEntity living, int timeLeft) { // clear zoom regardless, does not matter if the tool broke, we should not be zooming @@ -130,8 +137,9 @@ public void releaseUsing(ItemStack bow, Level level, LivingEntity living, int ti // calculate arrow power float charge = GeneralInteractionModifierHook.getToolCharge(tool, chargeTime); tool.getPersistentData().remove(KEY_DRAWTIME); - float velocity = ConditionalStatModifierHook.getModifiedStat(tool, living, ToolStats.VELOCITY); - float power = charge * velocity; + + // may we just use charge to test if we can fire the arrows? + float power = charge * ConditionalStatModifierHook.getModifiedStat(tool, living, ToolStats.VELOCITY); if (power < 0.1f) { return; } @@ -144,45 +152,11 @@ public void releaseUsing(ItemStack bow, Level level, LivingEntity living, int ti if (ammo.isEmpty()) { ammo = new ItemStack(Items.ARROW); } + int damage = shootProjectiles(player, tool, ammo, player.getViewVector(1.0f), charge, 3, 1, creative); + if (!creative) { + ToolDamageUtil.damageAnimated(tool, damage, player, player.getUsedItemHand()); - // prepare the arrows - ArrowItem arrowItem = ammo.getItem() instanceof ArrowItem arrow ? arrow : (ArrowItem)Items.ARROW; - float inaccuracy = ModifierUtil.getInaccuracy(tool, living); - float startAngle = getAngleStart(ammo.getCount()); - int primaryIndex = ammo.getCount() / 2; - for (int arrowIndex = 0; arrowIndex < ammo.getCount(); arrowIndex++) { - AbstractArrow arrow = arrowItem.createArrow(level, ammo, player); - float angle = startAngle + (10 * arrowIndex); - arrow.shootFromRotation(player, player.getXRot() + angle, player.getYRot(), 0, power * 3.0F, inaccuracy); - if (charge == 1.0F) { - arrow.setCritArrow(true); - } - - // vanilla arrows have a base damage of 2, cancel that out then add in our base damage to account for custom arrows with higher base damage - // calculate it just once as all four arrows are the same item, they should have the same damage - float baseArrowDamage = (float)(arrow.getBaseDamage() - 2 + tool.getStats().get(ToolStats.PROJECTILE_DAMAGE)); - arrow.setBaseDamage(ConditionalStatModifierHook.getModifiedStat(tool, player, ToolStats.PROJECTILE_DAMAGE, baseArrowDamage)); - - // just store all modifiers on the tool for simplicity - ModifierNBT modifiers = tool.getModifiers(); - arrow.getCapability(EntityModifierCapability.CAPABILITY).ifPresent(cap -> cap.setModifiers(modifiers)); - - // fetch the persistent data for the arrow as modifiers may want to store data - ModDataNBT arrowData = PersistentDataCapability.getOrWarn(arrow); - - // if infinite, skip pickup - if (creative) { - arrow.pickup = AbstractArrow.Pickup.CREATIVE_ONLY; - } - - // let modifiers such as fiery and punch set properties - for (ModifierEntry entry : modifiers.getModifiers()) { - entry.getHook(ModifierHooks.PROJECTILE_LAUNCH).onProjectileLaunch(tool, entry, living, arrow, arrow, arrowData, arrowIndex == primaryIndex); - } - level.addFreshEntity(arrow); - level.playSound(null, player.getX(), player.getY(), player.getZ(), SoundEvents.ARROW_SHOOT, SoundSource.PLAYERS, 1.0F, 1.0F / (level.getRandom().nextFloat() * 0.4F + 1.2F) + charge * 0.5F + (angle / 10f)); } - ToolDamageUtil.damageAnimated(tool, ammo.getCount(), player, player.getUsedItemHand()); } // stats and sounds diff --git a/src/main/java/slimeknights/tconstruct/library/tools/item/ranged/ModifiableCrossbowItem.java b/src/main/java/slimeknights/tconstruct/library/tools/item/ranged/ModifiableCrossbowItem.java index c5590e5a652..650d473b9d5 100644 --- a/src/main/java/slimeknights/tconstruct/library/tools/item/ranged/ModifiableCrossbowItem.java +++ b/src/main/java/slimeknights/tconstruct/library/tools/item/ranged/ModifiableCrossbowItem.java @@ -18,10 +18,8 @@ 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.AbstractArrow.Pickup; import net.minecraft.world.entity.projectile.FireworkRocketEntity; import net.minecraft.world.entity.projectile.Projectile; -import net.minecraft.world.item.ArrowItem; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.item.TooltipFlag; @@ -33,21 +31,14 @@ import org.joml.Vector3f; import slimeknights.mantle.client.TooltipKey; import slimeknights.tconstruct.TConstruct; -import slimeknights.tconstruct.library.modifiers.ModifierEntry; -import slimeknights.tconstruct.library.modifiers.ModifierHooks; -import slimeknights.tconstruct.library.modifiers.hook.build.ConditionalStatModifierHook; import slimeknights.tconstruct.library.modifiers.hook.interaction.GeneralInteractionModifierHook; import slimeknights.tconstruct.library.modifiers.hook.ranged.BowAmmoModifierHook; -import slimeknights.tconstruct.library.tools.capability.EntityModifierCapability; -import slimeknights.tconstruct.library.tools.capability.PersistentDataCapability; import slimeknights.tconstruct.library.tools.definition.ToolDefinition; import slimeknights.tconstruct.library.tools.helper.ModifierUtil; import slimeknights.tconstruct.library.tools.helper.ToolDamageUtil; import slimeknights.tconstruct.library.tools.nbt.IToolStackView; import slimeknights.tconstruct.library.tools.nbt.ModDataNBT; -import slimeknights.tconstruct.library.tools.nbt.ModifierNBT; import slimeknights.tconstruct.library.tools.nbt.ToolStack; -import slimeknights.tconstruct.library.tools.stat.ToolStats; import slimeknights.tconstruct.tools.TinkerModifiers; import slimeknights.tconstruct.tools.modifiers.ability.interaction.BlockingModifier; import slimeknights.tconstruct.tools.modifiers.upgrades.ranged.ScopeModifier; @@ -101,12 +92,33 @@ public boolean useOnRelease(ItemStack stack) { /* Arrow launching */ - /** Gets the arrow pitch */ - private static float getRandomShotPitch(float angle, RandomSource pRandom) { - if (angle == 0) { - return 1.0f; + @Override + public void playShotSound(LivingEntity user, float charge, float angle, RandomSource pRandom) { + float pitch = angle == 0 ? 1 : 1.0F / (pRandom.nextFloat() * 0.5F + 1.8F) + 0.53f + (angle / 10f); + user.level().playSound(null, user.getX(), user.getY(), user.getZ(), + SoundEvents.CROSSBOW_SHOOT, SoundSource.PLAYERS, 1.0F, pitch); + } + + @Override + public ProjectileData createProjectile(Level level, LivingEntity user, ItemStack ammo) { + if (ammo.is(Items.FIREWORK_ROCKET)) { + // TODO: don't hardcode fireworks, perhaps use a map or a JSON behavior list + Projectile projectile = new FireworkRocketEntity(level, ammo, user, user.getX(), user.getEyeY() - 0.15f, user.getZ(), true); + return new ProjectileData(projectile, 0.5f, 3); + } else { + ProjectileData info = super.createProjectile(level, user, ammo); + if (info.projectile() instanceof AbstractArrow arrow) { + arrow.setSoundEvent(SoundEvents.CROSSBOW_HIT); + arrow.setShotFromCrossbow(true); + } + return info; } - return 1.0F / (pRandom.nextFloat() * 0.5F + 1.8F) + 0.53f + (angle / 10f); + } + + @Override + public Vector3f modifyShootAngle(LivingEntity user, Vec3 target, float angle) { + Vec3 upVector = user.getUpVector(1.0f); + return target.toVector3f().rotate((new Quaternionf()).setAngleAxis(angle * Math.PI / 180F, upVector.x, upVector.y, upVector.z)); } @Override @@ -171,82 +183,20 @@ public InteractionResultHolder use(Level level, Player player, Intera * @param hand Hand fired from * @param heldAmmo Ammo used to fire, should be non-empty */ - public static void fireCrossbow(IToolStackView tool, Player player, InteractionHand hand, CompoundTag heldAmmo) { - // ammo already loaded? time to fire + public void fireCrossbow(IToolStackView tool, Player player, InteractionHand hand, CompoundTag heldAmmo) { Level level = player.level(); + boolean creative = player.getAbilities().instabuild; if (!level.isClientSide) { - // shoot the projectile - int damage = 0; - - // don't need to calculate these multiple times - float velocity = ConditionalStatModifierHook.getModifiedStat(tool, player, ToolStats.VELOCITY); - float inaccuracy = ModifierUtil.getInaccuracy(tool, player); - boolean creative = player.getAbilities().instabuild; - // the ammo has a stack size that may be greater than 1 (meaning multishot) // when creating the ammo stacks, we use split, so its getting smaller each time ItemStack ammo = ItemStack.of(heldAmmo); - float startAngle = getAngleStart(ammo.getCount()); - int primaryIndex = ammo.getCount() / 2; - for (int arrowIndex = 0; arrowIndex < ammo.getCount(); arrowIndex++) { - // setup projectile - AbstractArrow arrow = null; - Projectile projectile; - float speed; - if (ammo.is(Items.FIREWORK_ROCKET)) { - // TODO: don't hardcode fireworks, perhaps use a map or a JSON behavior list - projectile = new FireworkRocketEntity(level, ammo, player, player.getX(), player.getEyeY() - 0.15f, player.getZ(), true); - speed = 1.5f; - damage += 3; - } else { - ArrowItem arrowItem = ammo.getItem() instanceof ArrowItem a ? a : (ArrowItem)Items.ARROW; - arrow = arrowItem.createArrow(level, ammo, player); - projectile = arrow; - arrow.setCritArrow(true); - arrow.setSoundEvent(SoundEvents.CROSSBOW_HIT); - arrow.setShotFromCrossbow(true); - speed = 3f; - damage += 1; - - // vanilla arrows have a base damage of 2, cancel that out then add in our base damage to account for custom arrows with higher base damage - float baseArrowDamage = (float)(arrow.getBaseDamage() - 2 + tool.getStats().get(ToolStats.PROJECTILE_DAMAGE)); - arrow.setBaseDamage(ConditionalStatModifierHook.getModifiedStat(tool, player, ToolStats.PROJECTILE_DAMAGE, baseArrowDamage)); - - // fortunately, don't need to deal with vanilla infinity here, our infinity was dealt with during loading - if (creative) { - arrow.pickup = Pickup.CREATIVE_ONLY; - } - } - - // TODO: can we get piglins/illagers to use our crossbow? - - // setup projectile - Vec3 upVector = player.getUpVector(1.0f); - float angle = startAngle + (10 * arrowIndex); - Vector3f targetVector = player.getViewVector(1.0f).toVector3f().rotate((new Quaternionf()).setAngleAxis(angle * Math.PI / 180F, upVector.x, upVector.y, upVector.z)); - projectile.shoot(targetVector.x(), targetVector.y(), targetVector.z(), velocity * speed, inaccuracy); - - // add modifiers to the projectile, will let us use them on impact - ModifierNBT modifiers = tool.getModifiers(); - projectile.getCapability(EntityModifierCapability.CAPABILITY).ifPresent(cap -> cap.setModifiers(modifiers)); - - // fetch the persistent data for the arrow as modifiers may want to store data - ModDataNBT projectileData = PersistentDataCapability.getOrWarn(projectile); - - // let modifiers set properties - for (ModifierEntry entry : modifiers.getModifiers()) { - entry.getHook(ModifierHooks.PROJECTILE_LAUNCH).onProjectileLaunch(tool, entry, player, projectile, arrow, projectileData, arrowIndex == primaryIndex); - } - - // finally, fire the projectile - level.addFreshEntity(projectile); - level.playSound(null, player.getX(), player.getY(), player.getZ(), SoundEvents.CROSSBOW_SHOOT, SoundSource.PLAYERS, 1.0F, getRandomShotPitch(angle, player.getRandom())); + // fire arrows, damage bow, play sound + int damage = shootProjectiles(player, tool, ammo, player.getViewVector(1.0f), 1, 3.15f, 1, creative); + if (!creative) { + ToolDamageUtil.damageAnimated(tool, damage, player, hand); } - - // clear the ammo, damage the bow + // clear the ammo tool.getPersistentData().remove(KEY_CROSSBOW_AMMO); - ToolDamageUtil.damageAnimated(tool, damage, player, hand); - // stats if (player instanceof ServerPlayer serverPlayer) { CriteriaTriggers.SHOT_CROSSBOW.trigger(serverPlayer, player.getItemInHand(hand)); diff --git a/src/main/java/slimeknights/tconstruct/library/tools/item/ranged/ModifiableLauncherItem.java b/src/main/java/slimeknights/tconstruct/library/tools/item/ranged/ModifiableLauncherItem.java index 10ac149ba5d..90ea35e03ad 100644 --- a/src/main/java/slimeknights/tconstruct/library/tools/item/ranged/ModifiableLauncherItem.java +++ b/src/main/java/slimeknights/tconstruct/library/tools/item/ranged/ModifiableLauncherItem.java @@ -9,6 +9,8 @@ import net.minecraft.sounds.SoundEvents; import net.minecraft.sounds.SoundSource; import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.InteractionHand; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.EquipmentSlot.Type; @@ -17,9 +19,13 @@ import net.minecraft.world.entity.ai.attributes.Attribute; import net.minecraft.world.entity.ai.attributes.AttributeModifier; import net.minecraft.world.entity.player.Player; +import net.minecraft.world.entity.projectile.AbstractArrow; +import net.minecraft.world.entity.projectile.Projectile; import net.minecraft.world.inventory.ClickAction; import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ArrowItem; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; import net.minecraft.world.item.ProjectileWeaponItem; import net.minecraft.world.item.Rarity; import net.minecraft.world.item.TooltipFlag; @@ -27,16 +33,23 @@ import net.minecraft.world.item.enchantment.Enchantment; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.Vec3; import net.minecraftforge.common.ToolAction; import net.minecraftforge.common.capabilities.ICapabilityProvider; +import org.joml.Vector3f; import slimeknights.mantle.client.SafeClientAccess; +import slimeknights.tconstruct.library.modifiers.ModifierEntry; +import slimeknights.tconstruct.library.modifiers.ModifierHooks; import slimeknights.tconstruct.library.modifiers.hook.behavior.AttributesModifierHook; import slimeknights.tconstruct.library.modifiers.hook.behavior.EnchantmentModifierHook; +import slimeknights.tconstruct.library.modifiers.hook.build.ConditionalStatModifierHook; import slimeknights.tconstruct.library.modifiers.hook.display.DurabilityDisplayModifierHook; import slimeknights.tconstruct.library.modifiers.hook.interaction.EntityInteractionModifierHook; import slimeknights.tconstruct.library.modifiers.hook.interaction.InventoryTickModifierHook; import slimeknights.tconstruct.library.modifiers.hook.interaction.SlotStackModifierHook; import slimeknights.tconstruct.library.tools.IndestructibleItemEntity; +import slimeknights.tconstruct.library.tools.capability.EntityModifierCapability; +import slimeknights.tconstruct.library.tools.capability.PersistentDataCapability; import slimeknights.tconstruct.library.tools.capability.TinkerDataCapability; import slimeknights.tconstruct.library.tools.capability.TinkerDataKeys; import slimeknights.tconstruct.library.tools.capability.ToolCapabilityProvider; @@ -51,7 +64,10 @@ import slimeknights.tconstruct.library.tools.item.IModifiableDisplay; import slimeknights.tconstruct.library.tools.item.ModifiableItem; import slimeknights.tconstruct.library.tools.nbt.IToolStackView; +import slimeknights.tconstruct.library.tools.nbt.ModDataNBT; +import slimeknights.tconstruct.library.tools.nbt.ModifierNBT; import slimeknights.tconstruct.library.tools.nbt.ToolStack; +import slimeknights.tconstruct.library.tools.stat.ToolStats; import slimeknights.tconstruct.tools.TinkerModifiers; import slimeknights.tconstruct.tools.TinkerToolActions; import slimeknights.tconstruct.tools.modifiers.upgrades.ranged.ScopeModifier; @@ -386,8 +402,99 @@ public boolean onBlockStartBreak(ItemStack stack, BlockPos pos, Player player) { /* Multishot helper */ + /** Play shooting sound based on charge and angle spread */ + public abstract void playShotSound(LivingEntity user, float charge, float angle, RandomSource pRandom); + /** Gets the angle to fire the first arrow, each additional arrow offsets an additional 10 degrees */ public static float getAngleStart(int count) { return -5 * (count - 1); } + + public static void assignArrowProperties(Projectile projectile, LivingEntity user, IToolStackView tool, boolean crit, boolean infinite, boolean primary) { + if (projectile instanceof AbstractArrow arrow) { + if (crit) { + arrow.setCritArrow(true); + } + + // if infinite, skip pickup + if (infinite) { + arrow.pickup = AbstractArrow.Pickup.CREATIVE_ONLY; + } + + // vanilla arrows have a base damage of 2, cancel that out then add in our base damage to account for custom arrows with higher base damage + // calculate it just once as all four arrows are the same item, they should have the same damage + float baseArrowDamage = (float) (arrow.getBaseDamage() - 2 + tool.getStats().get(ToolStats.PROJECTILE_DAMAGE)); + arrow.setBaseDamage(ConditionalStatModifierHook.getModifiedStat(tool, user, ToolStats.PROJECTILE_DAMAGE, baseArrowDamage)); + } + + // just store all modifiers on the tool for simplicity + ModifierNBT modifiers = tool.getModifiers(); + projectile.getCapability(EntityModifierCapability.CAPABILITY).ifPresent(cap -> cap.setModifiers(modifiers)); + + // fetch the persistent data for the arrow as modifiers may want to store data + ModDataNBT arrowData = PersistentDataCapability.getOrWarn(projectile); + + // let modifiers such as fiery and punch set properties + for (ModifierEntry entry : modifiers.getModifiers()) { + entry.getHook(ModifierHooks.PROJECTILE_LAUNCH).onProjectileLaunch(tool, entry, user, projectile, + projectile instanceof AbstractArrow arrow ? arrow : null, arrowData, primary); + } + } + + /** Create projectile from ammunition stack. Also provide speed and durability cost information */ + public ProjectileData createProjectile(Level level, LivingEntity user, ItemStack ammo) { + ArrowItem arrowItem = ammo.getItem() instanceof ArrowItem a ? a : (ArrowItem) Items.ARROW; + AbstractArrow arrow = arrowItem.createArrow(level, ammo, user); + return new ProjectileData(arrow, 1f, 1); + } + + /** Modify projectile launch angle for specific angle offset. Bow use vertical spread and crossbow use horizontal spread. */ + public abstract Vector3f modifyShootAngle(LivingEntity user, Vec3 target, float angle); + + /** + * Launches projectile, plays sound, but does not consume durability nor handle ammunition consumption. + * Call {@code ToolDamageUtil.damageAnimated} to consume durability. + * + * @param holder user + * @param tool projectile weapon + * @param ammo ammunition. Will not be modified inside this method. + * @param direction direction to shoot at + * @param charge charging percentage + * @param speedFactor regular arrow speed at full charge. 3 for player with bow. 1.6 for skeletons. 3.15 for crossbows. + * @param inaccuracyFactor shoot inaccuracy factor. 1 for player, and hostile mobs use a complex formula involving difficulty. + * @param infinite Whether to make the arrow marked as no-pickup. True for infinite arrows or hostile mob arrows. + * @return Amount of durability to consume + */ + public int shootProjectiles( + LivingEntity holder, IToolStackView tool, ItemStack ammo, Vec3 direction, float charge, + float speedFactor, float inaccuracyFactor, boolean infinite + ) { + float velocity = ConditionalStatModifierHook.getModifiedStat(tool, holder, ToolStats.VELOCITY); + float inaccuracy = ModifierUtil.getInaccuracy(tool, holder); + float startAngle = getAngleStart(ammo.getCount()); + int primaryIndex = ammo.getCount() / 2; + int damage = 0; + for (int arrowIndex = 0; arrowIndex < ammo.getCount(); arrowIndex++) { + // setup projectile + ProjectileData data = createProjectile(holder.level(), holder, ammo); + Projectile projectile = data.projectile(); + damage += data.damage(); + assignArrowProperties(projectile, holder, tool, charge >= 1, infinite, arrowIndex == primaryIndex); + // setup projectile + float angle = startAngle + (10 * arrowIndex); + Vector3f targetVector = modifyShootAngle(holder, direction, angle); + projectile.shoot(targetVector.x(), targetVector.y(), targetVector.z(), + charge * velocity * data.speed() * speedFactor, + inaccuracy * inaccuracyFactor); + // finally, fire the projectile + holder.level().addFreshEntity(projectile); + playShotSound(holder, charge, angle, holder.getRandom()); + } + return damage; + } + + public record ProjectileData(Projectile projectile, float speed, int damage) { + + } + } diff --git a/src/main/java/slimeknights/tconstruct/tools/modifiers/upgrades/ranged/SinistralModifier.java b/src/main/java/slimeknights/tconstruct/tools/modifiers/upgrades/ranged/SinistralModifier.java index e0809a0cb06..c64dea2a08e 100644 --- a/src/main/java/slimeknights/tconstruct/tools/modifiers/upgrades/ranged/SinistralModifier.java +++ b/src/main/java/slimeknights/tconstruct/tools/modifiers/upgrades/ranged/SinistralModifier.java @@ -31,8 +31,8 @@ public InteractionResult afterEntityUse(IToolStackView tool, ModifierEntry modif public InteractionResult onToolUse(IToolStackView tool, ModifierEntry modifier, Player player, InteractionHand hand, InteractionSource source) { if (source == InteractionSource.LEFT_CLICK && hand == InteractionHand.MAIN_HAND && !tool.isBroken()) { CompoundTag heldAmmo = tool.getPersistentData().getCompound(ModifiableCrossbowItem.KEY_CROSSBOW_AMMO); - if (!heldAmmo.isEmpty()) { - ModifiableCrossbowItem.fireCrossbow(tool, player, hand, heldAmmo); + if (!heldAmmo.isEmpty() && tool.getItem() instanceof ModifiableCrossbowItem item) { + item.fireCrossbow(tool, player, hand, heldAmmo); return InteractionResult.CONSUME; } }