Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ GunsAndGadgets brings together weapon assembly and gun combat. Players build fir

- **Modular assembly** — select a weapon type and combine barrels, loaders, chambers, actions, and stocks where the design requires them.
- **Meaningful parts** — components contribute to accuracy, damage, range, capacity, reload speed, fire rate, spread, and piercing.
- **Ammunition compatibility** — weapons accept their supported calibres and consume ammunition as they are loaded and fired.
- **Ammunition compatibility** — weapons accept their supported calibres and consume ammunition as they are loaded and fired. Crouch and right-click to choose which carried calibre the next reload uses; the choice is saved on the gun.
- **Reloading and handling** — timed reloads, progress feedback, character attribute effects, and loaded or unloaded appearances communicate weapon state.
- **Projectile combat** — shot trajectories, impacts, sounds, and visual effects support firearm and launcher attacks.
- **Weapon appearances** — skin support keeps custom gun designs connected to their firing and reload states.
Expand Down
116 changes: 103 additions & 13 deletions src/main/java/net/tfminecraft/gunsandgadgets/manager/GunManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

Expand Down Expand Up @@ -52,6 +53,9 @@
import org.bukkit.scheduler.BukkitRunnable;

import io.lumine.mythic.lib.api.item.NBTItem;
import io.lumine.mythic.lib.api.player.MMOPlayerData;
import io.lumine.mythic.lib.message.actionbar.ActionBarPriority;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.tfminecraft.tlibs.TLibs;
import net.tfminecraft.tlibs.objects.api.subapi.ItemSkinPreserver;
import net.tfminecraft.tlibs.objects.api.subapi.StringFormatter;
Expand All @@ -72,10 +76,16 @@ public class GunManager implements Listener {
private final NamespacedKey reloadAmmoKey = new NamespacedKey(GunsAndGadgets.getInstance(), "reload_ammo");
private final NamespacedKey reloadAmountKey = new NamespacedKey(GunsAndGadgets.getInstance(), "reload_amount");
private final NamespacedKey loadedAmmoKey = new NamespacedKey(GunsAndGadgets.getInstance(), "ammo_loaded");
// Ammunition the player picked for the next reload; absent means the first carried caliber.
private final NamespacedKey selectedAmmoKey = new NamespacedKey(GunsAndGadgets.getInstance(), "ammo_selected");

private final Map<UUID, Boolean> reloading = new HashMap<>();
// Preserve the exact inputs through config reloads, including their item metadata.
private final Map<UUID, Collection<ItemStack>> reloadRefunds = new HashMap<>();
// One click can arrive as several interact events (each hand, or an entity then the air)
// spread over neighbouring ticks; clients allow a new click only every 4 ticks.
private static final int AMMO_SWITCH_BURST_TICKS = 2;
private final Map<UUID, Integer> lastAmmoSwitchTick = new HashMap<>();

@EventHandler
public void preventOldMuskets(UntargetedWeaponUseEvent e) {
Expand Down Expand Up @@ -157,6 +167,15 @@ public void handleGunUse(Player player, Cancellable event) {
if (reloading.getOrDefault(id, false)) {
return;
}
if (player.isSneaking()) {
int tick = Bukkit.getCurrentTick();
Integer last = lastAmmoSwitchTick.get(id);
if (last == null || tick - last > AMMO_SWITCH_BURST_TICKS) {
lastAmmoSwitchTick.put(id, tick);
cycleAmmo(player, item);
}
return;
}
reloading.put(id, true);

SkinData skin = SkinLoader.get().get(skinId);
Expand Down Expand Up @@ -227,10 +246,15 @@ public void handleGunUse(Player player, Cancellable event) {
.getOrDefault(capacityKey, PersistentDataType.INTEGER, 1);

// 🔫 Try to consume ammo before reload
Collection<Ammunition> calibers = Caliber.get(item); // your helper from before
String taken = takeAmmo(player, capacity, calibers);
List<Ammunition> calibers = Caliber.get(item); // your helper from before
Ammunition selected = getSelectedAmmo(meta, calibers);
String taken = takeAmmo(player, capacity, selected == null ? calibers : List.of(selected));

if (taken.equals("none")) {
if (selected != null) {
sendActionBar(player, "§cYou have no " + getAmmoName(selected)
+ "§c left. Crouch and right-click to choose another.");
}
reloading.remove(id);
return;
}
Expand All @@ -239,6 +263,9 @@ public void handleGunUse(Player player, Cancellable event) {
String[] split = taken.split("\\.");
String ammoId = split[0];
int takenAmount = Integer.parseInt(split[1]);
if (calibers.size() > 1) {
sendActionBar(player, "§7Loading " + getAmmoName(AmmunitionLoader.getByString(ammoId)));
}

// Save ammo info in PDC (for refund or finalize)
meta.getPersistentDataContainer().set(reloadAmmoKey, PersistentDataType.STRING, ammoId);
Expand Down Expand Up @@ -585,17 +612,8 @@ private String makeProgressBar(double progress, int bars, String filled, String
private String takeAmmo(Player p, int amount, Collection<Ammunition> calibers) {
if (amount <= 0) return "none";
for (Ammunition ammo : calibers) {
int totalFound = 0;
java.util.List<ItemStack> matchingStacks = new ArrayList<>();

// Count how many of this ammo we have
for (ItemStack item : p.getInventory().getContents()) {
if (item == null) continue;
if (TLibs.getItemAPI().getChecker().checkItemWithPath(item, ammo.getInput())) {
totalFound += item.getAmount();
matchingStacks.add(item);
}
}
java.util.List<ItemStack> matchingStacks = findAmmo(p, ammo);
int totalFound = countStacks(matchingStacks);

if (totalFound > 0) {
// Take min(capacity, found)
Expand All @@ -609,6 +627,78 @@ private String takeAmmo(Player p, int amount, Collection<Ammunition> calibers) {
return "none";
}

/** Stacks of this ammunition in the player's inventory, in slot order. */
private java.util.List<ItemStack> findAmmo(Player p, Ammunition ammo) {
java.util.List<ItemStack> matchingStacks = new ArrayList<>();
for (ItemStack item : p.getInventory().getContents()) {
if (item == null) continue;
if (TLibs.getItemAPI().getChecker().checkItemWithPath(item, ammo.getInput())) {
matchingStacks.add(item);
}
}
return matchingStacks;
}

private int countStacks(java.util.List<ItemStack> stacks) {
int total = 0;
for (ItemStack item : stacks) total += item.getAmount();
return total;
}

/** The player's pick for the next reload, or null when unset or no longer a caliber of this gun. */
private Ammunition getSelectedAmmo(ItemMeta meta, List<Ammunition> calibers) {
String selectedId = meta.getPersistentDataContainer().get(selectedAmmoKey, PersistentDataType.STRING);
for (Ammunition ammo : calibers) {
if (ammo.getKey().equals(selectedId)) return ammo;
}
return null;
}

/**
* Crouch + right-click: pick the next caliber the player carries for the next reload.
* Bullets already loaded stay loaded.
*/
private void cycleAmmo(Player player, ItemStack item) {
List<Ammunition> calibers = Caliber.get(item);
List<Ammunition> carried = new ArrayList<>();
for (Ammunition ammo : calibers) {
if (countStacks(findAmmo(player, ammo)) > 0) carried.add(ammo);
}
if (carried.isEmpty()) {
sendActionBar(player, "§cYou carry no shot this weapon can fire.");
return;
}

ItemMeta meta = item.getItemMeta();
Ammunition current = getSelectedAmmo(meta, calibers);
// Without a pick, reloads use the first carried caliber, so step past that one.
int index = calibers.indexOf(current != null ? current : carried.get(0));
Ammunition next;
do {
index = (index + 1) % calibers.size();
next = calibers.get(index);
} while (!carried.contains(next));

meta.getPersistentDataContainer().set(selectedAmmoKey, PersistentDataType.STRING, next.getKey());
item.setItemMeta(meta);
player.getInventory().setItemInMainHand(item);
sendActionBar(player, "§7Next load: " + getAmmoName(next)
+ " §8(" + countStacks(findAmmo(player, next)) + " carried)");
player.playSound(player.getLocation(), Sound.BLOCK_LEVER_CLICK, 1f, 1.5f);
}

// Keep the existing legacy text representation of ammunition item names.
@SuppressWarnings("deprecation")
private String getAmmoName(Ammunition ammo) {
return StringFormatter.getName(TLibs.getItemAPI().getCreator().getItemFromPath(ammo.getInput())).trim();
}

private void sendActionBar(Player player, String legacyText) {
// Hold MMOCore's stat bar back for two seconds so the message can be read; a null text only reserves it.
if (!MMOPlayerData.get(player).getActionBar().show(ActionBarPriority.NORMAL, 40L, (String) null)) return;
player.sendActionBar(LegacyComponentSerializer.legacySection().deserialize(legacyText));
}


/**
* Remove a certain number of items of a specific type from a player's inventory.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,9 @@ public ItemStack createOutputItem(GunType type, Collection<GunPart> parts, boole
lore.add("§7• " + ammoKey);
}
}
if (calibers.size() > 1) {
lore.add("§8Crouch and right-click to change shot");
}

// ✅ Save calibers in PDC
NamespacedKey calibersKey = new NamespacedKey(GunsAndGadgets.getInstance(), "calibers");
Expand Down Expand Up @@ -506,6 +509,7 @@ private void copyRuntimeAmmoPdc(ItemStack from, ItemStack to) {
NamespacedKey reloadAmmoKey = new NamespacedKey(GunsAndGadgets.getInstance(), "reload_ammo");
NamespacedKey reloadAmountKey = new NamespacedKey(GunsAndGadgets.getInstance(), "reload_amount");
NamespacedKey lastFireKey = new NamespacedKey(GunsAndGadgets.getInstance(), "last_fire");
NamespacedKey selectedAmmoKey = new NamespacedKey(GunsAndGadgets.getInstance(), "ammo_selected");

if (fromPdc.has(bulletsKey, PersistentDataType.INTEGER)) {
toPdc.set(bulletsKey, PersistentDataType.INTEGER,
Expand Down Expand Up @@ -542,6 +546,13 @@ private void copyRuntimeAmmoPdc(ItemStack from, ItemStack to) {
toPdc.remove(lastFireKey);
}

String selectedAmmo = fromPdc.get(selectedAmmoKey, PersistentDataType.STRING);
if (selectedAmmo != null) {
toPdc.set(selectedAmmoKey, PersistentDataType.STRING, selectedAmmo);
} else {
toPdc.remove(selectedAmmoKey);
}

to.setItemMeta(toMeta);
}

Expand Down Expand Up @@ -593,23 +604,23 @@ private void storeSounds(GunType gunType, Collection<GunPart> parts,

/**
* Finds the calibers for the selected parts.
* If an override is present, uses the last override only.
* If an override is present, the last part with one supplies every caliber.
*/
private List<String> resolveCalibers(Collection<GunPart> parts) {
List<String> overrides = new ArrayList<>();
List<String> override = null;
List<String> normal = new ArrayList<>();

for (GunPart part : parts) {
if (part.getCaliberOverrides() != null && !part.getCaliberOverrides().isEmpty()) {
overrides.addAll(part.getCaliberOverrides());
override = part.getCaliberOverrides();
} else if (!part.getCalibers().isEmpty()) {
normal.addAll(part.getCalibers());
}
}

if (!overrides.isEmpty()) {
// ✅ Only the last override matters
return List.of(overrides.get(overrides.size() - 1));
if (override != null) {
// ✅ Only the last override matters, with all of its calibers
return List.copyOf(override);
}
return normal;
}
Expand Down
Loading