package com.atsuishio.superbwarfare.data.gun; import com.atsuishio.superbwarfare.Mod; import com.atsuishio.superbwarfare.data.gun.subdata.*; import com.atsuishio.superbwarfare.data.gun.value.*; import com.atsuishio.superbwarfare.init.ModPerks; import com.atsuishio.superbwarfare.item.gun.GunItem; import com.atsuishio.superbwarfare.perk.AmmoPerk; import com.atsuishio.superbwarfare.perk.Perk; import com.atsuishio.superbwarfare.tools.Ammo; import com.atsuishio.superbwarfare.tools.GunsTool; import com.atsuishio.superbwarfare.tools.InventoryTool; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import net.minecraft.core.component.DataComponents; import net.minecraft.nbt.CompoundTag; import net.minecraft.resources.ResourceLocation; import net.minecraft.tags.ItemTags; import net.minecraft.tags.TagKey; import net.minecraft.util.Mth; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.component.CustomData; import net.neoforged.neoforge.registries.DeferredHolder; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; public class GunData { public final ItemStack stack; public final GunItem item; public final CompoundTag tag; public final CompoundTag data; public final CompoundTag perkTag; public final CompoundTag attachmentTag; public final String id; public static final LoadingCache dataCache = CacheBuilder.newBuilder() .weakKeys() .build(new CacheLoader<>() { public @NotNull GunData load(@NotNull ItemStack stack) { return new GunData(stack); } }); private GunData(ItemStack stack) { if (!(stack.getItem() instanceof GunItem gunItem)) { throw new IllegalArgumentException("stack is not GunItem!"); } this.item = gunItem; this.stack = stack; var id = stack.getDescriptionId(); this.id = id.substring(id.indexOf(".") + 1).replace('.', ':'); var customData = stack.get(DataComponents.CUSTOM_DATA); this.tag = customData != null ? customData.copyTag() : new CompoundTag(); data = getOrPut("GunData"); perkTag = getOrPut("Perks"); attachmentTag = getOrPut("Attachments"); reload = new Reload(this); charge = new Charge(this); bolt = new Bolt(this); attachment = new Attachment(this); perk = new Perks(this); ammo = new IntValue(data, "Ammo"); var defaultFireMode = defaultGunData().defaultFireMode; if (defaultFireMode == null) { defaultFireMode = FireMode.SEMI; } fireMode = new StringEnumValue<>(data, "FireMode", defaultFireMode, FireMode::fromValue); level = new IntValue(data, "Level"); exp = new DoubleValue(data, "Exp"); upgradePoint = new DoubleValue(data, "UpgradePoint"); isEmpty = new BooleanValue(data, "IsEmpty"); closeHammer = new BooleanValue(data, "CloseHammer"); stopped = new BooleanValue(data, "Stopped"); forceStop = new BooleanValue(data, "ForceStop"); loadIndex = new IntValue(data, "LoadIndex"); holdOpen = new BooleanValue(data, "HoldOpen"); hideBulletChain = new BooleanValue(data, "HideBulletChain"); draw = new BooleanValue(data, "Draw"); sensitivity = new IntValue(data, "Sensitivity"); heat = new DoubleValue(data, "Heat"); overHeat = new BooleanValue(data, "OverHeat"); } private CompoundTag getOrPut(String name) { CompoundTag tag; if (!this.tag.contains(name)) { tag = new CompoundTag(); this.tag.put(name, tag); } else { tag = this.tag.getCompound(name); } return tag; } public boolean initialized() { return data.hasUUID("UUID"); } public void initialize() { if (initialized()) return; data.putUUID("UUID", UUID.randomUUID()); save(); } public static GunData from(ItemStack stack) { return dataCache.getUnchecked(stack); } public GunItem item() { return item; } public ItemStack stack() { return stack; } public CompoundTag tag() { return tag; } public CompoundTag data() { return data; } public CompoundTag perk() { return perkTag; } public CompoundTag attachment() { return attachmentTag; } DefaultGunData defaultGunData() { return GunsTool.gunsData.getOrDefault(id, new DefaultGunData()); } // 枪械本体属性开始 public double rawDamage() { return defaultGunData().damage; } public double perkDamageRate() { var perk = this.perk.get(Perk.Type.AMMO); if (perk instanceof AmmoPerk ammoPerk) { if (ammoPerk.slug) { return ammoPerk.damageRate * rawProjectileAmount(); } return ammoPerk.damageRate; } return 1; } public double damage() { return (rawDamage() + item.getCustomDamage(stack)) * perkDamageRate(); } public double meleeDamage() { return defaultGunData().meleeDamage; } public int meleeDuration() { return Math.max(0, defaultGunData().meleeDuration); } public int meleeDamageTime() { return Math.min(meleeDuration(), defaultGunData().meleeDamageTime); } public double explosionDamage() { return defaultGunData().explosionDamage; } public double explosionRadius() { return defaultGunData().explosionRadius; } public double velocity() { return defaultGunData().velocity + item.getCustomVelocity(stack); } public double spread() { return defaultGunData().spread; } public int magazine() { return defaultGunData().magazine + item.getCustomMagazine(stack); } /** * 武器是否直接使用背包内弹药 */ public boolean useBackpackAmmo() { return magazine() <= 0; } public ProjectileInfo projectileInfo() { var info = defaultGunData().projectile; if (info == null) return new ProjectileInfo(); return info; } public String projectileType() { return projectileInfo().type; } public int rawProjectileAmount() { return defaultGunData().projectileAmount; } public int projectileAmount() { var perk = this.perk.get(Perk.Type.AMMO); if (perk instanceof AmmoPerk ammoPerk && ammoPerk.slug) { return 1; } return defaultGunData().projectileAmount; } public double headshot() { return defaultGunData().headshot + item.getCustomHeadshot(stack); } public Set reloadTypes() { if (defaultGunData().reloadTypes == null) return Set.of(); return defaultGunData().reloadTypes; } public int defaultNormalReloadTime() { return defaultGunData().normalReloadTime; } public int defaultEmptyReloadTime() { return defaultGunData().emptyReloadTime; } public int defaultIterativeTime() { return defaultGunData().iterativeTime; } public int iterativeAmmoLoadTime() { return defaultGunData().iterativeAmmoLoadTime; } public int iterativeLoadAmount() { return defaultGunData().iterativeLoadAmount; } public int defaultPrepareTime() { return defaultGunData().prepareTime; } public int defaultPrepareLoadTime() { return defaultGunData().prepareLoadTime; } public int prepareAmmoLoadTime() { return defaultGunData().prepareAmmoLoadTime; } public int defaultPrepareEmptyTime() { return defaultGunData().prepareEmptyTime; } public int defaultFinishTime() { return defaultGunData().finishTime; } public int defaultActionTime() { return defaultGunData().boltActionTime + item.getCustomBoltActionTime(stack()); } public double soundRadius() { return defaultGunData().soundRadius + item.getCustomSoundRadius(stack); } public double bypassArmor() { return defaultGunData().bypassArmor + item.getCustomBypassArmor(stack); } public double recoilX() { return defaultGunData().recoilX; } public double recoilY() { return defaultGunData().recoilY; } public double recoil() { return defaultGunData().recoil; } public double weight() { return defaultGunData().weight + customWeight(); } public double customWeight() { return item.getCustomWeight(stack); } public double defaultZoom() { return defaultGunData().defaultZoom; } public double minZoom() { int scopeType = this.attachment.get(AttachmentType.SCOPE); return scopeType == 3 ? defaultGunData().minZoom : 1.25; } public double maxZoom() { int scopeType = this.attachment.get(AttachmentType.SCOPE); return scopeType == 3 ? defaultGunData().maxZoom : 114514; } public double zoom() { if (minZoom() == maxZoom()) return defaultZoom(); return Mth.clamp(defaultZoom() + item.getCustomZoom(stack), minZoom(), maxZoom()); } public int rpm() { return (defaultGunData().rpm + item.getCustomRPM(stack)); } public int burstAmount() { return defaultGunData().burstAmount; } public int shootDelay() { return defaultGunData().shootDelay; } public double heatPerShoot() { return defaultGunData().heatPerShoot; } public enum AmmoConsumeType { PLAYER_AMMO, ITEM, TAG, INVALID, } public record AmmoTypeInfo(AmmoConsumeType type, String value) { /** * 尝试返回Ammo类型 */ public @Nullable Ammo playerAmmoType() { if (type != AmmoConsumeType.PLAYER_AMMO) return null; return toPlayerAmmoType(); } public @NotNull Ammo toPlayerAmmoType() { if (type != AmmoConsumeType.PLAYER_AMMO) throw new IllegalArgumentException("not PLAYER_AMMO type!"); return Objects.requireNonNull(Ammo.getType(value)); } public TagKey toTag() { if (type != AmmoConsumeType.TAG) throw new IllegalArgumentException("not TAG type!"); return ItemTags.create(ResourceLocation.parse(this.value())); } } public AmmoTypeInfo ammoTypeInfo() { var ammoType = defaultGunData().ammoType; if (ammoType == null || ammoType.isEmpty()) { return new AmmoTypeInfo(AmmoConsumeType.INVALID, ""); } // 玩家弹药 if (ammoType.startsWith("@")) { if (Ammo.getType(ammoType.substring(1)) == null) { return new AmmoTypeInfo(AmmoConsumeType.INVALID, ammoType.substring(1)); } return new AmmoTypeInfo(AmmoConsumeType.PLAYER_AMMO, ammoType.substring(1)); } // 物品Tag if (ammoType.startsWith("#")) { if (ResourceLocation.tryParse(ammoType.substring(1)) == null) { return new AmmoTypeInfo(AmmoConsumeType.INVALID, ammoType.substring(1)); } return new AmmoTypeInfo(AmmoConsumeType.TAG, ammoType.substring(1)); } // 普通物品 if (ResourceLocation.tryParse(ammoType) == null) { return new AmmoTypeInfo(AmmoConsumeType.INVALID, ammoType); } return new AmmoTypeInfo(AmmoConsumeType.ITEM, ammoType); } /** * 是否还有剩余弹药(不考虑枪内弹药) */ public boolean hasBackupAmmo(Player player) { return countBackupAmmo(player) > 0; } /** * 计算剩余弹药数量(不考虑枪内弹药) */ public int countBackupAmmo(Player player) { if (player.isCreative() || InventoryTool.hasCreativeAmmoBox(player)) return Integer.MAX_VALUE; var info = ammoTypeInfo(); return switch (info.type()) { case PLAYER_AMMO -> { var type = Ammo.getType(info.value()); assert type != null; yield type.get(player); } case ITEM -> player.getInventory().clearOrCountMatchingItems( p -> p.getItem().toString().equals(info.value()), 0, player.inventoryMenu.getCraftSlots() ); case TAG -> player.getInventory().clearOrCountMatchingItems( p -> p.is(info.toTag()), 0, player.inventoryMenu.getCraftSlots() ); case INVALID -> 0; }; } /** * 消耗额外弹药(不影响枪内弹药) */ public void consumeBackupAmmo(Player player, int count) { if (player.isCreative() || InventoryTool.hasCreativeAmmoBox(player) || count <= 0) return; var info = ammoTypeInfo(); switch (info.type()) { case PLAYER_AMMO -> info.toPlayerAmmoType().set(player, info.toPlayerAmmoType().get(player) - count); case ITEM -> player.getInventory().clearOrCountMatchingItems( p -> p.getItem().toString().equals(info.value()), count, player.inventoryMenu.getCraftSlots() ); case TAG -> player.getInventory().clearOrCountMatchingItems( p -> p.is(info.toTag()), count, player.inventoryMenu.getCraftSlots() ); } } /** * 是否拥有足够的弹药进行开火 */ public boolean hasEnoughAmmoToShoot(Player player) { return useBackpackAmmo() ? hasBackupAmmo(player) : this.ammo.get() > 0; } public void reload(Player player) { reload(player, false); } public void reload(Player player, boolean extraOne) { if (useBackpackAmmo()) return; int mag = magazine(); int ammo = this.ammo.get(); int ammoNeeded = mag - ammo + (extraOne ? 1 : 0); // 空仓换弹的栓动武器应该在换弹后取消待上膛标记 if (ammo == 0 && defaultActionTime() > 0) { bolt.needed.set(false); } var available = countBackupAmmo(player); var ammoToAdd = Math.min(ammoNeeded, available); consumeBackupAmmo(player, ammoToAdd); this.ammo.set(ammo + ammoToAdd); reload.setState(ReloadState.NOT_RELOADING); } private static int getPriority(String s) { if (s == null || s.isEmpty()) return 2; if (s.startsWith("@")) return 0; else if (s.startsWith("!")) return 2; else return 1; } public List availablePerks() { List availablePerks = new ArrayList<>(); var perkNames = defaultGunData().availablePerks; if (perkNames == null || perkNames.isEmpty()) return availablePerks; List sortedNames = new ArrayList<>(perkNames); sortedNames.sort((s1, s2) -> { int p1 = getPriority(s1); int p2 = getPriority(s2); if (p1 != p2) { return Integer.compare(p1, p2); } else { return s1.compareTo(s2); } }); // TODO 正确实现注册项读取 var perks = new ArrayList>(); perks.addAll(ModPerks.AMMO_PERKS.getEntries()); perks.addAll(ModPerks.DAMAGE_PERKS.getEntries()); perks.addAll(ModPerks.FUNC_PERKS.getEntries()); var perkValues = perks.stream().map(DeferredHolder::get).toList(); var perkKeys = perks.stream().map(perk -> perk.getKey().location().toString()).toList(); for (String name : sortedNames) { if (name.startsWith("@")) { String type = name.substring(1); switch (type) { case "Ammo" -> availablePerks.addAll(perkValues.stream().filter(perk -> perk.type == Perk.Type.AMMO).toList()); case "Functional" -> availablePerks.addAll(perkValues.stream().filter(perk -> perk.type == Perk.Type.FUNCTIONAL).toList()); case "Damage" -> availablePerks.addAll(perkValues.stream().filter(perk -> perk.type == Perk.Type.DAMAGE).toList()); } } else if (name.startsWith("!")) { String n = name.substring(1); var index = perkKeys.indexOf(n); if (index != -1) { availablePerks.remove(perkValues.get(index)); } else { Mod.LOGGER.info("Perk {} not found", n); } } else { var index = perkKeys.indexOf(name); if (index != -1) { availablePerks.add(perkValues.get(index)); } else { Mod.LOGGER.info("Perk {} not found", name); } } } return availablePerks; } public boolean canApplyPerk(Perk perk) { return availablePerks().contains(perk); } public Set getAvailableFireModes() { if (defaultGunData().availableFireModes == null) return Set.of(); return defaultGunData().availableFireModes; } public DamageReduce getRawDamageReduce() { return defaultGunData().damageReduce; } public double getRawDamageReduceRate() { return getRawDamageReduce().getRate(); } public double getDamageReduceRate() { for (Perk.Type type : Perk.Type.values()) { var instance = this.perk.getInstance(type); if (instance != null) { return instance.perk().getModifiedDamageReduceRate(getRawDamageReduce()); } } return getRawDamageReduce().getRate(); } public double getRawDamageReduceMinDistance() { return getRawDamageReduce().getMinDistance(); } public double getDamageReduceMinDistance() { for (Perk.Type type : Perk.Type.values()) { var instance = this.perk.getInstance(type); if (instance != null) { return instance.perk().getModifiedDamageReduceMinDistance(getRawDamageReduce()); } } return getRawDamageReduce().getMinDistance(); } // 可持久化属性开始 public final IntValue ammo; public final StringEnumValue fireMode; public final IntValue level; public final DoubleValue exp; public final DoubleValue upgradePoint; public final DoubleValue heat; public final BooleanValue overHeat; public boolean canAdjustZoom() { return item.canAdjustZoom(stack); } public boolean canSwitchScope() { return item.canSwitchScope(stack); } public final Reload reload; public boolean reloading() { return reload.state() != ReloadState.NOT_RELOADING; } public final Charge charge; public boolean charging() { return charge.time() > 0; } public final BooleanValue isEmpty; public final BooleanValue closeHammer; public final BooleanValue stopped; public final BooleanValue forceStop; public final IntValue loadIndex; public final BooleanValue holdOpen; public final BooleanValue hideBulletChain; public final BooleanValue draw; public final IntValue sensitivity; // 其他子级属性 public final Bolt bolt; public final Attachment attachment; public final Perks perk; public void save() { stack.set(DataComponents.CUSTOM_DATA, CustomData.of(tag)); } }