Skip to content

Commit

Permalink
Implement multishot
Browse files Browse the repository at this point in the history
Multishot works on both bows and crossbows. On bows, the arrows get vertical angles, while crossbows get horizontal arrows (once implemented)
May get tweaked later, went with this for simplicty so far
  • Loading branch information
KnightMiner committed Dec 23, 2022
1 parent f93fa5d commit ffd8932
Show file tree
Hide file tree
Showing 23 changed files with 306 additions and 101 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"type": "tconstruct:modifier",
"inputs": [
{
"item": "minecraft:piston"
},
{
"tag": "forge:ingots/amethyst_bronze"
},
{
"item": "minecraft:piston"
},
{
"tag": "forge:slimeball/ichor"
},
{
"tag": "forge:slimeball/ichor"
}
],
"tools": {
"tag": "tconstruct:modifiable/ranged/bows"
},
"slots": {
"abilities": 1
},
"result": {
"name": "tconstruct:multishot",
"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:multishot",
"min_level": 1
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
package slimeknights.tconstruct.library.modifiers.hook;

import net.minecraft.world.InteractionHand;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.common.ForgeHooks;
import net.minecraftforge.items.ItemHandlerHelper;
import slimeknights.tconstruct.library.modifiers.ModifierEntry;
import slimeknights.tconstruct.library.modifiers.TinkerHooks;
import slimeknights.tconstruct.library.tools.nbt.IToolStackView;
import slimeknights.tconstruct.tools.TinkerModifiers;

import java.util.function.Predicate;

Expand Down Expand Up @@ -31,9 +38,159 @@ public interface BowAmmoModifierHook {
* @param tool Tool instance
* @param modifier Modifier instance
* @param shooter Entity shooting the ammo
* @param ammo Ammo that was found
* @param ammo Ammo that was found by {@link #findAmmo(IToolStackView, ModifierEntry, LivingEntity, ItemStack, Predicate)}
* @param needed Desired size, should always be less than the size of {@code ammo}
*/
default void shrinkAmmo(IToolStackView tool, ModifierEntry modifier, LivingEntity shooter, ItemStack ammo) {
ammo.shrink(1);
default void shrinkAmmo(IToolStackView tool, ModifierEntry modifier, LivingEntity shooter, ItemStack ammo, int needed) {
ammo.shrink(needed);
}

/**
* Checks if the player has ammo for the given tool
* @param tool Tool instance, for running modifier hooks
* @param bowStack Bow stack instance, for standard ammo lookup
* @param player Player instance, for standard ammo lookup
* @param predicate Predicate for finding ammo in modifiers
* @return True if there is ammo either on the player or on the modifiers
*/
static boolean hasAmmo(IToolStackView tool, ItemStack bowStack, Player player, Predicate<ItemStack> predicate) {
// 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
if (!player.getProjectile(bowStack).isEmpty()) {
return true;
}
for (ModifierEntry entry : tool.getModifierList()) {
if (!entry.getHook(TinkerHooks.BOW_AMMO).findAmmo(tool, entry, player, ItemStack.EMPTY, predicate).isEmpty()) {
return true;
}
}
return false;
}

/**
* Looks for a matching item stack in the player inventory
* @param bow Bow stack
* @param living Entity to search
* @param predicate Predicate for finding ammo in modifiers
* @return Matching stack in the player inventory
*/
private static ItemStack findMatchingAmmo(ItemStack bow, LivingEntity living, Predicate<ItemStack> predicate) {
// start with hands, find one that matches but is not the bow
for (InteractionHand hand : InteractionHand.values()) {
ItemStack stack = living.getItemInHand(hand);
if (stack != bow && predicate.test(stack)) {
return ForgeHooks.getProjectile(living, bow, stack);
}
}

// was not in hand, search the rest of the inventory
if (living instanceof Player player) {
Inventory inventory = player.getInventory();
for (int i = 0; i < inventory.getContainerSize(); i++) {
ItemStack stack = inventory.getItem(i);
if (!stack.isEmpty() && predicate.test(stack)) {
return ForgeHooks.getProjectile(player, bow, stack);
}
}
}
return ItemStack.EMPTY;
}

/**
* Finds ammo in the inventory, and consume it if not creative
* @param tool Tool instance
* @param bow Bow stack instance
* @param predicate Predicate for valid ammo
* @param player Player to search
* @return Found ammo
*/
static ItemStack findAmmo(IToolStackView tool, ItemStack bow, Player player, Predicate<ItemStack> predicate) {
int projectilesDesired = 1 + (2 * tool.getModifierLevel(TinkerModifiers.multishot.getId()));
// treat client side as creative, no need to shrink the stacks clientside
boolean creative = player.getAbilities().instabuild || player.level.isClientSide;

// first search, find what ammo type we want
ItemStack standardAmmo = player.getProjectile(bow);
ItemStack resultStack = ItemStack.EMPTY;
for (ModifierEntry entry : tool.getModifierList()) {
BowAmmoModifierHook hook = entry.getHook(TinkerHooks.BOW_AMMO);
ItemStack ammo = hook.findAmmo(tool, entry, player, standardAmmo, predicate);
if (!ammo.isEmpty()) {
// if creative, we are done, just return the ammo with the given size
if (creative) {
return ItemHandlerHelper.copyStackWithSize(ammo, projectilesDesired);
}

// 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());
break;
}
}

// result stack being empty means no modifier found it, so we use standard ammo
if (resultStack.isEmpty()) {
// if standard ammo is empty as well, nothing else to do but give up
if (standardAmmo.isEmpty()) {
return ItemStack.EMPTY;
}
// with standard ammo, in creative we can just return that
if (creative) {
return ItemHandlerHelper.copyStackWithSize(standardAmmo, projectilesDesired);
}
// make a copy of the result, up to the desired size
resultStack = standardAmmo.split(projectilesDesired);
if (standardAmmo.isEmpty()) {
player.getInventory().removeItem(standardAmmo);
}
}

// if we made it this far, we found ammo and are not in creative
// we may be done already, saves making a predicate
// can also return if on client side, they don't need the full stack
if (resultStack.getCount() >= projectilesDesired || player.level.isClientSide) {
return resultStack;
}

// not enough? keep searching until we fill the stack
ItemStack match = resultStack;
predicate = stack -> ItemStack.isSameItemSameTags(stack, match);
hasEnough:
do {
// if standard ammo is empty, try finding a matching stack again
if (standardAmmo.isEmpty()) {
standardAmmo = findMatchingAmmo(bow, player, predicate);
}
// next, try asking modifiers if they have anything new again
for (ModifierEntry entry : tool.getModifierList()) {
BowAmmoModifierHook hook = entry.getHook(TinkerHooks.BOW_AMMO);
ItemStack ammo = hook.findAmmo(tool, entry, player, standardAmmo, predicate);
if (!ammo.isEmpty()) {
// consume as much of the stack as we need then restart the loop
hook.shrinkAmmo(tool, entry, player, ammo, Math.min(projectilesDesired - resultStack.getCount(), ammo.getCount()));
continue hasEnough;
}
}
// no standard and no modifier found means we give up
if (standardAmmo.isEmpty()) {
break;
}

// if we have standard ammo, take what we can then loop again
int needed = projectilesDesired - resultStack.getCount();
if (needed <= standardAmmo.getCount()) {
// consume the whole stack
resultStack.grow(standardAmmo.getCount());
player.getInventory().removeItem(standardAmmo);
} else {
// found what we need, we are done
standardAmmo.shrink(needed);
resultStack.grow(needed);
break;
}
} while (resultStack.getCount() < projectilesDesired);

// TODO: diyo would prefer enforcing an odd number, so if we do not find more we may want to grow the ammo stack back a bit
return resultStack;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
*/
public interface ProjectileLaunchModifierHook {
/** Default instance */
ProjectileLaunchModifierHook EMPTY = (tool, modifier, shooter, projectile, arrow, persistentData) -> {};
ProjectileLaunchModifierHook EMPTY = (tool, modifier, shooter, projectile, arrow, persistentData, primary) -> {};
/** Merger instance */
Function<Collection<ProjectileLaunchModifierHook>,ProjectileLaunchModifierHook> ALL_MERGER = AllMerger::new;

Expand All @@ -28,15 +28,16 @@ public interface ProjectileLaunchModifierHook {
* @param projectile Projectile to modify
* @param arrow Arrow to modify as most modifiers wish to change that, will be null for non-arrow projectiles
* @param persistentData Persistent data instance stored on the arrow to write arbitrary data. Note the modifier list was already written
* @param primary If true, this is the primary projectile. Multishot may launch multiple
*/
void onProjectileLaunch(IToolStackView tool, ModifierEntry modifier, LivingEntity shooter, Projectile projectile, @Nullable AbstractArrow arrow, NamespacedNBT persistentData);
void onProjectileLaunch(IToolStackView tool, ModifierEntry modifier, LivingEntity shooter, Projectile projectile, @Nullable AbstractArrow arrow, NamespacedNBT persistentData, boolean primary);

/** Logic to merge multiple hooks into one */
record AllMerger(Collection<ProjectileLaunchModifierHook> modules) implements ProjectileLaunchModifierHook {
@Override
public void onProjectileLaunch(IToolStackView tool, ModifierEntry modifier, LivingEntity shooter, Projectile projectile, @Nullable AbstractArrow arrow, NamespacedNBT persistentData) {
public void onProjectileLaunch(IToolStackView tool, ModifierEntry modifier, LivingEntity shooter, Projectile projectile, @Nullable AbstractArrow arrow, NamespacedNBT persistentData, boolean primary) {
for (ProjectileLaunchModifierHook module : modules) {
module.onProjectileLaunch(tool, modifier, shooter, projectile, arrow, persistentData);
module.onProjectileLaunch(tool, modifier, shooter, projectile, arrow, persistentData, primary);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@

import javax.annotation.Nullable;
import java.util.List;
import java.util.Random;
import java.util.function.Consumer;

/** Base class for any items that launch projectiles */
Expand Down Expand Up @@ -322,4 +323,20 @@ public boolean shouldCauseBlockBreakReset(ItemStack oldStack, ItemStack newStack
public boolean shouldCauseReequipAnimation(ItemStack oldStack, ItemStack newStack, boolean slotChanged) {
return ModifiableItemUtil.shouldCauseReequip(oldStack, newStack, slotChanged);
}


/* Multishot helper */

/** Gets the arrow pitch */
protected static float getRandomShotPitch(int index, Random pRandom) {
if (index == 0) {
return 1.0f;
}
return 1.0F / (pRandom.nextFloat() * 0.5F + 1.8F) + (index == 1 ? 0.63F : 0.43F);
}

/** Gets the angle to fire the first arrow, each additional arrow offsets an additional 10 degrees */
protected static float getAngleStart(int count) {
return -5 * (count - 1);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ public TinkerModifiers() {
public static final StaticModifier<BulkQuiverModifier> bulkQuiver = MODIFIERS.register("bulk_quiver", BulkQuiverModifier::new);
public static final StaticModifier<TrickQuiverModifier> trickQuiver = MODIFIERS.register("trick_quiver", TrickQuiverModifier::new);
public static final StaticModifier<CrystalshotModifier> crystalshot = MODIFIERS.register("crystalshot", CrystalshotModifier::new);
public static final StaticModifier<Modifier> multishot = MODIFIERS.register("multishot", Modifier::new);

// armor
// protection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,16 @@ private void addModifierRecipes(Consumer<FinishedRecipe> consumer) {
.setSlots(SlotType.ABILITY, 1)
.setTools(TinkerTags.Items.BOWS)
.saveSalvage(consumer, prefix(TinkerModifiers.crystalshot, abilitySalvage));
ModifierRecipeBuilder.modifier(TinkerModifiers.multishot)
.addInput(Items.PISTON)
.addInput(TinkerMaterials.amethystBronze.getIngotTag())
.addInput(Items.PISTON)
.addInput(SlimeType.ICHOR.getSlimeballTag())
.addInput(SlimeType.ICHOR.getSlimeballTag())
.setSlots(SlotType.ABILITY, 1)
.setTools(TinkerTags.Items.BOWS)
.saveSalvage(consumer, prefix(TinkerModifiers.multishot, abilitySalvage))
.save(consumer, prefix(TinkerModifiers.multishot, abilityFolder));

/*
* armor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ public boolean isInfinite(ItemStack stack, ItemStack bow, Player player) {
public void fillItemCategory(CreativeModeTab pCategory, NonNullList<ItemStack> pItems) {}

/** Creates a crystal shot with the given variant */
public static ItemStack withVariant(String variant) {
public static ItemStack withVariant(String variant, int size) {
ItemStack stack = new ItemStack(TinkerTools.crystalshotItem);
stack.setCount(size);
stack.getOrCreateTag().putString(TAG_VARIANT, variant);
return stack;
}
Expand Down Expand Up @@ -83,7 +84,7 @@ public void setVariant(String variant) {

@Override
public ItemStack getPickupItem() {
return withVariant(getVariant());
return withVariant(getVariant(), 1);
}

@Override
Expand Down

0 comments on commit ffd8932

Please sign in to comment.