Skip to content

Commit

Permalink
Implement ammo hook and bulk quiver
Browse files Browse the repository at this point in the history
Bulk quiver is a quiver that supplies arrows after the main inventory empies of arrows. Lets you save some slots for arrows
In the future two alternatives will be added, one for trick arrows to use before the inventory, and one infinity alternative
  • Loading branch information
KnightMiner committed Dec 18, 2022
1 parent 3aee0c0 commit 2aa8e67
Show file tree
Hide file tree
Showing 14 changed files with 254 additions and 24 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"type": "tconstruct:modifier",
"inputs": [
{
"item": "minecraft:leather"
},
{
"item": "tconstruct:sky_slime_vine"
},
{
"item": "minecraft:leather"
},
{
"item": "tconstruct:sky_slime_vine"
},
{
"item": "tconstruct:sky_slime_vine"
}
],
"tools": {
"tag": "tconstruct:modifiable/ranged/bows"
},
"slots": {
"abilities": 1
},
"result": {
"name": "tconstruct:bulk_quiver",
"level": 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"type": "tconstruct:modifier_salvage",
"tools": {
"tag": "tconstruct:modifiable/ranged/bows"
},
"slots": {
"abilities": 1
},
"modifier": "tconstruct:bulk_quiver",
"min_level": 1
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import slimeknights.tconstruct.TConstruct;
import slimeknights.tconstruct.library.modifiers.hook.ArmorWalkModifierHook;
import slimeknights.tconstruct.library.modifiers.hook.ArrowLaunchModifierHook;
import slimeknights.tconstruct.library.modifiers.hook.BowAmmoModifierHook;
import slimeknights.tconstruct.library.modifiers.hook.ConditionalStatModifierHook;
import slimeknights.tconstruct.library.modifiers.hook.ElytraFlightModifierHook;
import slimeknights.tconstruct.library.modifiers.hook.HarvestEnchantmentsModifierHook;
Expand Down Expand Up @@ -75,6 +76,8 @@ private TinkerHooks() {}
public static final ModifierHook<ArrowLaunchModifierHook> ARROW_LAUNCH = register("arrow_launch", ArrowLaunchModifierHook.class, ArrowLaunchModifierHook.ALL_MERGER, ArrowLaunchModifierHook.EMPTY);
/** Hook called when an arrow hits an entity or block */
public static final ModifierHook<ProjectileHitModifierHook> PROJECTILE_HIT = register("projectile_hit", ProjectileHitModifierHook.class, ProjectileHitModifierHook.FIRST_MERGER, ProjectileHitModifierHook.EMPTY);
/** Hook called when a bow is looking for ammo. Does not support merging multiple hooks on one modifier */
public static final ModifierHook<BowAmmoModifierHook> BOW_AMMO = register("bow_ammo", BowAmmoModifierHook.class, BowAmmoModifierHook.EMPTY);

/* Misc Armor */

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package slimeknights.tconstruct.library.modifiers.hook;

import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemStack;
import slimeknights.tconstruct.library.modifiers.ModifierEntry;
import slimeknights.tconstruct.library.tools.nbt.IToolStackView;

import java.util.function.Predicate;

/**
* Hook to find ammo on a bow
*/
public interface BowAmmoModifierHook {
/** Default instance */
BowAmmoModifierHook EMPTY = (tool, modifier, shooter, standardAmmo, ammoPredicate) -> ItemStack.EMPTY;

/**
* Finds the ammo. Does *not* modify the tool, this method may be called without loosing an arrow
* @param tool Tool instance
* @param modifier Modifier being called
* @param shooter Entity using the bow
* @param standardAmmo Arrows found in the player inventory. Will be empty if not found
* @param ammoPredicate Predicate from the bow of types of ammo it accepts
* @return Item stack of ammo found. If empty, will continue searching for ammo elsewhere until falling back to standard ammo
*/
ItemStack findAmmo(IToolStackView tool, ModifierEntry modifier, LivingEntity shooter, ItemStack standardAmmo, Predicate<ItemStack> ammoPredicate);

/**
* Callback to shrink the ammo returned by {@link #findAmmo(IToolStackView, ModifierEntry, LivingEntity, ItemStack, Predicate)}.
* Will only be called on the modifier that returned non-empty in the previous method
* @param tool Tool instance
* @param modifier Modifier instance
* @param shooter Entity shooting the ammo
* @param ammo Ammo that was found
*/
default void shrinkAmmo(IToolStackView tool, ModifierEntry modifier, LivingEntity shooter, ItemStack ammo) {
ammo.shrink(1);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
import slimeknights.tconstruct.tools.modifiers.ability.interaction.PathingModifier;
import slimeknights.tconstruct.tools.modifiers.ability.interaction.ShearsAbilityModifier;
import slimeknights.tconstruct.tools.modifiers.ability.interaction.SilkyShearsAbilityModifier;
import slimeknights.tconstruct.tools.modifiers.ability.ranged.BulkQuiverModifier;
import slimeknights.tconstruct.tools.modifiers.ability.tool.AutosmeltModifier;
import slimeknights.tconstruct.tools.modifiers.ability.tool.BucketingModifier;
import slimeknights.tconstruct.tools.modifiers.ability.tool.DuelWieldingModifier;
Expand Down Expand Up @@ -281,6 +282,7 @@ public TinkerModifiers() {
public static final StaticModifier<PunchModifier> punch = MODIFIERS.register("punch", PunchModifier::new);
public static final StaticModifier<ImpalingModifier> impaling = MODIFIERS.register("impaling", ImpalingModifier::new);
public static final StaticModifier<FreezingModifier> freezing = MODIFIERS.register("freezing", FreezingModifier::new);
public static final StaticModifier<BulkQuiverModifier> bulkQuiver = MODIFIERS.register("bulk_quiver", BulkQuiverModifier::new);

// armor
// protection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +570,16 @@ private void addModifierRecipes(Consumer<FinishedRecipe> consumer) {
.setTools(TinkerTags.Items.BOWS) // no elementals on fluid cannon
.saveSalvage(consumer, prefix(TinkerModifiers.freezing, upgradeSalvage))
.save(consumer, prefix(TinkerModifiers.freezing, upgradeFolder));

ModifierRecipeBuilder.modifier(TinkerModifiers.bulkQuiver)
.addInput(Items.LEATHER)
.addInput(TinkerWorld.skySlimeVine)
.addInput(Items.LEATHER)
.addInput(TinkerWorld.skySlimeVine)
.addInput(TinkerWorld.skySlimeVine)
.setSlots(SlotType.ABILITY, 1)
.setTools(TinkerTags.Items.BOWS)
.saveSalvage(consumer, prefix(TinkerModifiers.bulkQuiver, abilitySalvage))
.save(consumer, prefix(TinkerModifiers.bulkQuiver, abilityFolder));

/*
* armor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import net.minecraft.sounds.SoundSource;
import net.minecraft.stats.Stats;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
Expand All @@ -15,22 +16,25 @@
import net.minecraft.world.item.Items;
import net.minecraft.world.item.ProjectileWeaponItem;
import net.minecraft.world.item.UseAnim;
import net.minecraft.world.item.enchantment.EnchantmentHelper;
import net.minecraft.world.item.enchantment.Enchantments;
import net.minecraft.world.level.Level;
import net.minecraftforge.event.ForgeEventFactory;
import slimeknights.tconstruct.library.modifiers.ModifierEntry;
import slimeknights.tconstruct.library.modifiers.TinkerHooks;
import slimeknights.tconstruct.library.modifiers.hook.ConditionalStatModifierHook;
import slimeknights.tconstruct.library.tools.capability.EntityModifierCapability;
import slimeknights.tconstruct.library.tools.capability.PersistentDataCapability;
import slimeknights.tconstruct.library.tools.capability.ToolInventoryCapability;
import slimeknights.tconstruct.library.tools.definition.ToolDefinition;
import slimeknights.tconstruct.library.tools.helper.ToolBuildHandler;
import slimeknights.tconstruct.library.tools.helper.ToolDamageUtil;
import slimeknights.tconstruct.library.tools.item.ModifiableLauncherItem;
import slimeknights.tconstruct.library.tools.nbt.ModifierNBT;
import slimeknights.tconstruct.library.tools.nbt.NamespacedNBT;
import slimeknights.tconstruct.library.tools.nbt.StatsNBT;
import slimeknights.tconstruct.library.tools.nbt.ToolStack;
import slimeknights.tconstruct.library.tools.stat.ToolStats;
import slimeknights.tconstruct.library.utils.Util;
import slimeknights.tconstruct.tools.data.material.MaterialIds;

import java.util.function.Predicate;
Expand Down Expand Up @@ -63,50 +67,88 @@ public UseAnim getUseAnimation(ItemStack pStack) {

@Override
public InteractionResultHolder<ItemStack> use(Level level, Player player, InteractionHand hand) {
ItemStack itemstack = player.getItemInHand(hand);
boolean noAmmo = player.getProjectile(itemstack).isEmpty();
InteractionResultHolder<ItemStack> override = ForgeEventFactory.onArrowNock(itemstack, level, player, hand, !noAmmo);
ItemStack bow = player.getItemInHand(hand);

// TODO: move this to left click later once left click hooks are implemented
ToolStack tool = ToolStack.from(bow);
if (player.isCrouching()) {
InteractionResult result = ToolInventoryCapability.tryOpenContainer(bow, tool, player, Util.getSlotType(hand));
if (result.consumesAction()) {
return new InteractionResultHolder<>(result, bow);
}
}

// no need to ask the modifiers for ammo if we have it in the inventory, as there is no way for a modifier to say not to use ammo if its present
// inventory search is probably a bit faster on average than modifier search as its already parsed
boolean hasAmmo = !player.getProjectile(bow).isEmpty();
if (!hasAmmo) {
Predicate<ItemStack> ammoPredicate = getAllSupportedProjectiles();
for (ModifierEntry entry : tool.getModifierList()) {
if (!entry.getHook(TinkerHooks.BOW_AMMO).findAmmo(tool, entry, player, ItemStack.EMPTY, ammoPredicate).isEmpty()) {
hasAmmo = true;
break;
}
}
}

// ask forge if it has any different opinions
InteractionResultHolder<ItemStack> override = ForgeEventFactory.onArrowNock(bow, level, player, hand, hasAmmo);
if (override != null) {
return override;
}
if (!player.getAbilities().instabuild && noAmmo) {
return InteractionResultHolder.fail(itemstack);
if (!player.getAbilities().instabuild && !hasAmmo) {
return InteractionResultHolder.fail(bow);
}
player.startUsingItem(hand);
return InteractionResultHolder.consume(itemstack);
return InteractionResultHolder.consume(bow);
}

@Override
public void releaseUsing(ItemStack stack, Level level, LivingEntity living, int timeLeft) {
public void releaseUsing(ItemStack bow, Level level, LivingEntity living, int timeLeft) {
// need player
if (!(living instanceof Player player)) {
return;
}
// no broken
ToolStack tool = ToolStack.from(stack);
ToolStack tool = ToolStack.from(bow);
if (tool.isBroken()) {
return;
}

// TODO: modifier hook for inifinity/chance base arrow use
boolean infinity = player.getAbilities().instabuild; // || tool.getPersistentData().getBoolean();
// TODO: hook for custom ammo?
ItemStack ammo = player.getProjectile(stack); // TODO: we could make this stack sensitive instead
// for the sake of compat with custom arrows, we cannot do an infinity hook, as its up to each arrow to decide if it supports infinity or not and the only way to decide that is enchants
// we may in the future we may toss a conditional enchantment hook here though if we end up needing other bow enchants
boolean hasInfinity = player.getAbilities().instabuild || EnchantmentHelper.getItemEnchantmentLevel(Enchantments.INFINITY_ARROWS, bow) > 0;

// find ammo
ItemStack standardAmmo = player.getProjectile(bow);
ItemStack ammo = ItemStack.EMPTY;
ModifierEntry ammoFindingModifier = null;
Predicate<ItemStack> ammoPredicate = getAllSupportedProjectiles();
for (ModifierEntry entry : tool.getModifierList()) {
ammo = entry.getHook(TinkerHooks.BOW_AMMO).findAmmo(tool, entry, living, standardAmmo, ammoPredicate);
if (!ammo.isEmpty()) {
ammoFindingModifier = entry;
break;
}
}
if (ammo.isEmpty()) {
ammo = standardAmmo;
}

int chargeTime = this.getUseDuration(stack) - timeLeft;
chargeTime = ForgeEventFactory.onArrowLoose(stack, level, player, chargeTime, !ammo.isEmpty() || infinity);
// ask forge its thoughts on shooting
int chargeTime = this.getUseDuration(bow) - timeLeft;
chargeTime = ForgeEventFactory.onArrowLoose(bow, level, player, chargeTime, !ammo.isEmpty() || hasInfinity);

// no ammo? no charge? nothing to do
if (chargeTime < 0 || (ammo.isEmpty() && !infinity)) {
if (chargeTime < 0 || (ammo.isEmpty() && !hasInfinity)) {
return;
}
// no ammo? sub in vanilla arrows
// could only be empty at this point if we had infinity
if (ammo.isEmpty()) {
ammo = new ItemStack(Items.ARROW);
}

// calculate arrow power
StatsNBT stats = tool.getStats();
float charge = chargeTime * ConditionalStatModifierHook.getModifiedStat(tool, living, ToolStats.DRAW_SPEED) / 20f;
charge = (charge * charge + charge * 2) / 3;
if (charge > 1) {
Expand All @@ -119,13 +161,12 @@ public void releaseUsing(ItemStack stack, Level level, LivingEntity living, int
}

// launch the arrow
boolean arrowInfinite = player.getAbilities().instabuild || (ammo.getItem() instanceof ArrowItem arrow && arrow.isInfinite(ammo, stack, player));
boolean arrowInfinite = player.getAbilities().instabuild || (ammo.getItem() instanceof ArrowItem arrow && arrow.isInfinite(ammo, bow, player));
if (!level.isClientSide) {
ArrowItem arrowItem = ammo.getItem() instanceof ArrowItem arrow ? arrow : (ArrowItem)Items.ARROW;
AbstractArrow arrowEntity = arrowItem.createArrow(level, ammo, player);
float accuracy = ConditionalStatModifierHook.getModifiedStat(tool, living, ToolStats.ACCURACY);
arrowEntity.shootFromRotation(player, player.getXRot(), player.getYRot(), 0.0F, power * 3.0F, 3*(1/accuracy-1) * velocity);
// TODO: modifier hook to add arrow properties
if (charge == 1.0F) {
arrowEntity.setCritArrow(true);
}
Expand All @@ -147,16 +188,21 @@ public void releaseUsing(ItemStack stack, Level level, LivingEntity living, int

ToolDamageUtil.damageAnimated(tool, 1, player, player.getUsedItemHand());
// if infinite, skip pickup
if (arrowInfinite || player.getAbilities().instabuild && !ammo.is(Items.ARROW)) {
if (arrowInfinite) {
arrowEntity.pickup = AbstractArrow.Pickup.CREATIVE_ONLY;
}
level.addFreshEntity(arrowEntity);
}

// consume items
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);
if (!arrowInfinite && !player.getAbilities().instabuild) {
ammo.shrink(1);
if (!arrowInfinite) {
if (ammoFindingModifier != null) {
ammoFindingModifier.getHook(TinkerHooks.BOW_AMMO).shrinkAmmo(tool, ammoFindingModifier, living, ammo);
} else {
ammo.shrink(1);
}
// there is a chance the ammo came from the inventory either way, does not hurt to call this if it did not
if (ammo.isEmpty()) {
player.getInventory().removeItem(ammo);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package slimeknights.tconstruct.tools.modifiers.ability.ranged;

import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ArrowItem;
import net.minecraft.world.item.ItemStack;
import slimeknights.tconstruct.TConstruct;
import slimeknights.tconstruct.library.modifiers.ModifierEntry;
import slimeknights.tconstruct.library.modifiers.TinkerHooks;
import slimeknights.tconstruct.library.modifiers.hook.BowAmmoModifierHook;
import slimeknights.tconstruct.library.modifiers.impl.InventoryModifier;
import slimeknights.tconstruct.library.modifiers.util.ModifierHookMap.Builder;
import slimeknights.tconstruct.library.recipe.partbuilder.Pattern;
import slimeknights.tconstruct.library.tools.nbt.IToolStackView;
import slimeknights.tconstruct.library.tools.nbt.ModDataNBT;

import javax.annotation.Nullable;
import java.util.function.Predicate;

public class BulkQuiverModifier extends InventoryModifier implements BowAmmoModifierHook {
private static final ResourceLocation INVENTORY_KEY = TConstruct.getResource("bulk_quiver");
private static final ResourceLocation LAST_SLOT = TConstruct.getResource("quiver_last_selected");
private static final Pattern ARROW = new Pattern(TConstruct.getResource("arrow"));
public BulkQuiverModifier() {
super(INVENTORY_KEY, 2);
}

@Override
protected void registerHooks(Builder hookBuilder) {
super.registerHooks(hookBuilder);
hookBuilder.addHook(this, TinkerHooks.BOW_AMMO);
}

@Override
public boolean isItemValid(IToolStackView tool, ModifierEntry modifier, int slot, ItemStack stack) {
return stack.getItem() instanceof ArrowItem;
}

@Nullable
@Override
public Pattern getPattern(IToolStackView tool, ModifierEntry modifier, int slot, boolean hasStack) {
return hasStack ? null : ARROW;
}

@Override
public ItemStack findAmmo(IToolStackView tool, ModifierEntry modifier, LivingEntity shooter, ItemStack standardAmmo, Predicate<ItemStack> ammoPredicate) {
// skip if we have standard ammo, this quiver holds backup arrows
if (!standardAmmo.isEmpty()) {
return ItemStack.EMPTY;
}
ModDataNBT persistentData = tool.getPersistentData();
ResourceLocation key = getInventoryKey();
ListTag slots = persistentData.get(key, GET_COMPOUND_LIST);
if (!slots.isEmpty()) {
// search all slots for the first match
for (int i = 0; i < slots.size(); i++) {
CompoundTag compound = slots.getCompound(i);
ItemStack stack = ItemStack.of(compound);
if (!stack.isEmpty() && ammoPredicate.test(stack)) {
persistentData.putInt(LAST_SLOT, compound.getInt(TAG_SLOT));
return stack;
}
}
}
return ItemStack.EMPTY;
}

@Override
public void shrinkAmmo(IToolStackView tool, ModifierEntry modifier, LivingEntity shooter, ItemStack ammo) {
// we assume no one else touched the quiver inventory, this is a good assumption, do not make it a bad assumption by modifying the quiver in other modifiers
ammo.shrink(1);
setStack(tool, modifier, tool.getPersistentData().getInt((LAST_SLOT)), ammo);
}
}

0 comments on commit 2aa8e67

Please sign in to comment.