650 lines
20 KiB
Java
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));
|
|
}
|
|
}
|