superb-warfare/src/main/java/com/atsuishio/superbwarfare/data/gun/GunData.java
2025-07-02 02:02:09 +08:00

650 lines
20 KiB
Java

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<ItemStack, GunData> 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<ReloadType> 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<Item> 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<Perk> availablePerks() {
List<Perk> availablePerks = new ArrayList<>();
var perkNames = defaultGunData().availablePerks;
if (perkNames == null || perkNames.isEmpty()) return availablePerks;
List<String> 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<DeferredHolder<Perk, ? extends Perk>>();
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<FireMode> 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> 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));
}
}