change so clientside is independant of server

This commit is contained in:
CWX 2023-02-14 21:07:48 +00:00
parent 21e81a62d9
commit 9c4d79c715
26 changed files with 335 additions and 905 deletions

View File

@ -1,137 +0,0 @@
{
"en": {
"DAMAGE": "Damage",
"PENETRATION": "Armor penetration",
"ARMOR DAMAGE": "Damage to armor",
"FRAGMENTATION CHANCE": "Fragmentation chance",
"RICOCHET CHANCE": "Ricochet chance",
"ME_class": "Class",
"ME_noarmor": "Unarmored"
},
"cz": {
"DAMAGE": "Poškození",
"PENETRATION": "Průbojnost",
"ARMOR DAMAGE": "Poškození brnění",
"FRAGMENTATION CHANCE": "Šance na fragmentaci",
"RICOCHET CHANCE": "Šance na odraz",
"ME_class": "Třída",
"ME_noarmor": "Neobrněný"
},
"pl": {
"DAMAGE": "Szkoda",
"PENETRATION": "Penetracja pancerza",
"ARMOR DAMAGE": "Uszkodzenie zbroi",
"FRAGMENTATION CHANCE": "Szansa na fragmentację",
"RICOCHET CHANCE": "Szansa na rykoszet",
"ME_class": "Klasa",
"ME_noarmor": "Nieumiejętny"
},
"po": {
"DAMAGE": "Dano",
"PENETRATION": "Penetração de armadura",
"ARMOR DAMAGE": "Danos à armadura",
"FRAGMENTATION CHANCE": "Chance de fragmentação",
"RICOCHET CHANCE": "Chance de ricochete",
"ME_class": "Classe",
"ME_noarmor": "Sem armadura"
},
"ch": {
"DAMAGE": "损坏",
"PENETRATION": "护甲穿透",
"ARMOR DAMAGE": "对盔甲的伤害",
"FRAGMENTATION CHANCE": "碎片机会",
"RICOCHET CHANCE": "跳弹机会",
"ME_class": "类",
"ME_noarmor": "无所作为"
},
"ru": {
"DAMAGE": "Повреждать",
"PENETRATION": "Бронепробиваемость",
"ARMOR DAMAGE": "Повреждение брони",
"FRAGMENTATION CHANCE": "Вероятность фрагментации",
"RICOCHET CHANCE": "Шанс рикошета",
"ME_class": "Класс",
"ME_noarmor": "Без оружия"
},
"es": {
"DAMAGE": "Daño",
"PENETRATION": "Penetración de armadura",
"ARMOR DAMAGE": "Daño a la armadura",
"FRAGMENTATION CHANCE": "Posibilidad de fragmentación",
"RICOCHET CHANCE": "Oportunidad de rebote",
"ME_class": "Clase",
"ME_noarmor": "Sin armadura"
},
"es-mx": {
"DAMAGE": "Daño",
"PENETRATION": "Penetración de armadura",
"ARMOR DAMAGE": "Daño a la armadura",
"FRAGMENTATION CHANCE": "Posibilidad de fragmentación",
"RICOCHET CHANCE": "Oportunidad de rebote",
"ME_class": "Clase",
"ME_noarmor": "Sin armadura"
},
"ge": {
"DAMAGE": "Schaden",
"PENETRATION": "Rüstungsdurchdringung",
"ARMOR DAMAGE": "Beschädigung der Rüstung",
"FRAGMENTATION CHANCE": "Fragmentierung Chance",
"RICOCHET CHANCE": "Querschläger-Chance",
"ME_class": "Klasse",
"ME_noarmor": "Ungepanzert"
},
"sk": {
"DAMAGE": "Poškodenie",
"PENETRATION": "Prienik do brnenia",
"ARMOR DAMAGE": "Poškodenie brnenia",
"FRAGMENTATION CHANCE": "Šanca na fragmentáciu",
"RICOCHET CHANCE": "Šanca na odraz",
"ME_class": "Trieda",
"ME_noarmor": "Neozbrojený"
},
"tu": {
"DAMAGE": "Hasar",
"PENETRATION": "Zırh penetrasyon",
"ARMOR DAMAGE": "Zırhta hasar",
"FRAGMENTATION CHANCE": "Parçalanma şansı",
"RICOCHET CHANCE": "Sekme şansı",
"ME_class": "Sınıf",
"ME_noarmor": "zırhsız"
},
"it": {
"DAMAGE": "Danno",
"PENETRATION": "Penetrazione dell'armatura",
"ARMOR DAMAGE": "Danni all'armatura",
"FRAGMENTATION CHANCE": "Possibilità di frammentazione",
"RICOCHET CHANCE": "Possibilità di rimbalzo",
"ME_class": "Classe",
"ME_noarmor": "Disarmato"
},
"jp": {
"DAMAGE": "ダメージ",
"PENETRATION": "装甲貫通",
"ARMOR DAMAGE": "鎧の損傷",
"FRAGMENTATION CHANCE": "断片化の可能性",
"RICOCHET CHANCE": "跳ね返るチャンス",
"ME_class": "クラス",
"ME_noarmor": "無装甲"
},
"fr": {
"DAMAGE": "Dommage",
"PENETRATION": "Pénétration d'armure",
"ARMOR DAMAGE": "Dommages à l'armure",
"FRAGMENTATION CHANCE": "Chance de fragmentation",
"RICOCHET CHANCE": "Chance de ricochet",
"ME_class": "Classe",
"ME_noarmor": "Sans armure"
},
"hu": {
"DAMAGE": "Kár",
"PENETRATION": "Páncélátütő",
"ARMOR DAMAGE": "A páncél sérülése",
"FRAGMENTATION CHANCE": "Töredezettség esélye",
"RICOCHET CHANCE": "Ricochet esély",
"ME_class": "Osztály",
"ME_noarmor": "Fegyvertelen"
}
}

View File

@ -1,108 +0,0 @@
import type { DependencyContainer } from "tsyringe";
import { IPreAkiLoadMod } from "@spt-aki/models/external/IPreAkiLoadMod";
import { IPostAkiLoadMod } from "@spt-aki/models/external/IPostAkiLoadMod";
import { DynamicRouterModService } from "@spt-aki/services/mod/dynamicRouter/DynamicRouterModService"
import { DatabaseServer } from "@spt-aki/servers/DatabaseServer"
import { JsonUtil } from "@spt-aki/utils/JsonUtil"
import { IDatabaseTables } from "@spt-aki/models/spt/server/IDatabaseTables";
import { PreAkiModLoader } from "@spt-aki/loaders/PreAkiModLoader";
class MunitionsExpert implements IPreAkiLoadMod, IPostAkiLoadMod
{
private database: DatabaseServer;
private router: DynamicRouterModService;
private json: JsonUtil;
private modLoader: PreAkiModLoader;
private table: IDatabaseTables;
private globalLocale: { [x: string]: { interface: { [x: string]: any; }; }; };
private translations: { [x: string]: any; };
private items: { [x: string]: any; };
private path: { resolve: (arg0: string) => any; };
private cfg: { BulletBackgroundColours: boolean; };
public preAkiLoad(container: DependencyContainer)
{
this.router = container.resolve<DynamicRouterModService>("DynamicRouterModService");
this.json = container.resolve<JsonUtil>("JsonUtil");
this.translations = require("../res/translations.json");
this.path = require("path");
this.cfg = require("./config.json");
this.hookRoutes();
}
public postAkiLoad(container: DependencyContainer)
{
this.modLoader = container.resolve<PreAkiModLoader>("PreAkiModLoader");
this.database = container.resolve<DatabaseServer>("DatabaseServer");
this.table = this.database.getTables();
this.globalLocale = this.table.locales.global;
this.items = this.table.templates.items;
this.updateLocalization();
this.changeBulletColour();
}
private updateLocalization()
{
for (const language in this.translations)
{
if (!(language in this.globalLocale))
{
continue;
}
const attrKvPair = this.translations[language];
for (const attrKey in attrKvPair)
{
const attrValue = attrKvPair[attrKey];
this.globalLocale[language][attrKey] = attrValue;
}
}
}
private hookRoutes()
{
this.router.registerDynamicRouter(
"MunitionsExpert",
[
{
url: "/MunitionsExpert/GetInfo",
action: (url, info, sessionId, output) =>
{
return this.json.serialize(this.path.resolve(this.modLoader.getModPath("Faupi-MunitionsExpert 1.6.9")));
}
}
],
"MunitionsExpert"
)
}
changeBulletColour()
{
if (this.cfg.BulletBackgroundColours === true)
{
for (const i in this.items)
{
const item = this.items[i]
//set baground colour of ammo depending on pen
if (item._parent === "5485a8684bdc2da71d8b4567")
{
const pen = item._props.PenetrationPower
let colour = ""
pen > 60 ? colour = "red" : //SuperHighPen
pen > 50 ? colour = "yellow" : //HighPen
pen > 40 ? colour = "violet" : //MedHighPen
pen > 30 ? colour = "blue" : //MedPen
pen > 20 ? colour = "green" : //LowMedPen
colour = "grey" //LowPen
item._props.BackgroundColor = colour
}
}
}
}
}
module.exports = { mod: new MunitionsExpert() };

View File

@ -1,11 +1,11 @@
{ {
"name": "MunitionsExpert", "name": "MunitionsExpert",
"author": "Faupi", "author": "Faupi",
"version": "1.6.9", "updatedBy": "CWX",
"version": "1.7.0",
"license": "NCSA Open Source", "license": "NCSA Open Source",
"main": "./src/MunitionsExpert.js", "main": "./src/MunitionsExpert.js",
"akiVersion": "3.5.0", "akiVersion": "3.5.0",
"updatedBy": "CWX",
"scripts": { "scripts": {
"setup": "npm i", "setup": "npm i",
"build": "node ./packageBuild.ts" "build": "node ./packageBuild.ts"

View File

@ -0,0 +1,58 @@
import type { DependencyContainer } from "tsyringe";
import { IPostAkiLoadMod } from "@spt-aki/models/external/IPostAkiLoadMod";
import { DatabaseServer } from "@spt-aki/servers/DatabaseServer"
import { ITemplateItem } from "@spt-aki/models/eft/common/tables/ITemplateItem";
class MunitionsExpert implements IPostAkiLoadMod
{
private database: DatabaseServer;
private items: Record<string, ITemplateItem>;
private cfg: { BulletBackgroundColours: boolean; } = require("./config.json");
public postAkiLoad(container: DependencyContainer)
{
this.database = container.resolve<DatabaseServer>("DatabaseServer");
this.items = this.database.getTables().templates.items;
this.changeBulletColour();
}
private changeBulletColour()
{
if (this.cfg.BulletBackgroundColours)
{
for (const i in this.items)
{
//set background colour of ammo depending on pen
if (this.items[i]._parent === "5485a8684bdc2da71d8b4567")
{
const pen = this.items[i]._props.PenetrationPower;
switch (true)
{
case (pen > 60):
this.items[i]._props.BackgroundColor = "red";
break;
case (pen > 50):
this.items[i]._props.BackgroundColor = "yellow";
break;
case (pen > 40):
this.items[i]._props.BackgroundColor = "violet";
break;
case (pen > 30):
this.items[i]._props.BackgroundColor = "blue";
break;
case (pen > 20):
this.items[i]._props.BackgroundColor = "green";
break;
default:
this.items[i]._props.BackgroundColor = "grey";
break;
}
}
}
}
}
}
module.exports = { mod: new MunitionsExpert() };

View File

@ -1,26 +0,0 @@
using Aki.Reflection.Patching;
using EFT.InventoryLogic;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace MunitionsExpert
{
internal class CachedAttributesPatch : ModulePatch
{
protected override MethodBase GetTargetMethod()
{
return typeof(AmmoTemplate).GetMethod("GetCachedReadonlyQualities", BindingFlags.Instance | BindingFlags.Public);
}
[PatchPostfix]
private static void PatchPostfix(ref AmmoTemplate __instance, ref List<ItemAttributeClass> __result)
{
if (!__result.Any((ItemAttributeClass a) => (Attributes.ENewItemAttributeId)a.Id == Attributes.ENewItemAttributeId.Damage))
{
//MunitionsExpert.FormatExistingAttributes(ref __result, __instance);
Plugin.AddNewAttributes(ref __result, __instance);
}
}
}
}

View File

@ -0,0 +1,147 @@
using BepInEx;
using Comfort.Common;
using EFT.InventoryLogic;
using MunitionsExpert.Patches;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
namespace MunitionsExpert
{
[BepInPlugin("com.Faupi.MunitionsExpert", "Faupi-MunitionsExpert", "1.7.0")]
public class MunitionsExpertPlugin : BaseUnityPlugin
{
public static Dictionary<Enum, Sprite> iconCache = new Dictionary<Enum, Sprite>();
private void Awake()
{
CacheIcons();
new CachedAttributesPatch().Enable();
new StaticIconsPatch().Enable();
}
public static void CacheIcons()
{
iconCache.Add(Attributes.ENewItemAttributeId.Damage, Resources.Load<Sprite>("characteristics/icons/icon_info_damage"));
iconCache.Add(Attributes.ENewItemAttributeId.FragmentationChance, Resources.Load<Sprite>("characteristics/icons/icon_info_shrapnelcount"));
iconCache.Add(EItemAttributeId.LightBleedingDelta, Resources.Load<Sprite>("characteristics/icons/icon_info_bloodloss"));
iconCache.Add(EItemAttributeId.HeavyBleedingDelta, Resources.Load<Sprite>("characteristics/icon_info_hydration"));
iconCache.Add(Attributes.ENewItemAttributeId.Penetration, Resources.Load<Sprite>("characteristics/icon_info_penetration"));
_ = LoadTexture(Attributes.ENewItemAttributeId.ArmorDamage, Path.Combine(Directory.GetCurrentDirectory(), "BepInEx/plugins/Faupi-MunitionsExpert/armorDamage.png"));
_ = LoadTexture(Attributes.ENewItemAttributeId.RicochetChance, Path.Combine(Directory.GetCurrentDirectory(), "BepInEx/plugins/Faupi-MunitionsExpert/ricochet.png"));
}
public static async Task LoadTexture(Enum id, string path)
{
var unityWebRequest = UnityWebRequestTexture.GetTexture(path);
if (unityWebRequest != null)
{
unityWebRequest.SendWebRequest();
while (!unityWebRequest.isDone)
{
await Task.Delay(5);
}
if (unityWebRequest.responseCode != 200)
{
Debug.LogError($"[MunitionsExpert]: Request Error - {unityWebRequest.responseCode}: {unityWebRequest.error}");
}
else
{
Debug.LogError($"[MunitionsExpert]: Request Success - {id} from {path}");
var cachedTexture = DownloadHandlerTexture.GetContent(unityWebRequest);
iconCache.Add(id, Sprite.Create(cachedTexture, new Rect(0, 0, cachedTexture.width, cachedTexture.height), new Vector2(0, 0)));
}
}
}
public static void AddNewAttributes(ref List<ItemAttributeClass> attributes, AmmoTemplate template)
{
var totalDamage = template.Damage * template.ProjectileCount;
var damageString = totalDamage.ToString();
if (template.ProjectileCount > 1)
{
damageString += $" ({template.Damage} x {template.ProjectileCount})";
}
var damageAttribute = new ItemAttributeClass(Attributes.ENewItemAttributeId.Damage)
{
Name = Attributes.ENewItemAttributeId.Damage.GetName(),
Base = () => totalDamage,
StringValue = () => damageString,
DisplayType = () => EItemAttributeDisplayType.Compact
};
attributes.Add(damageAttribute);
if (template.ArmorDamage > 0)
{
var armorDamageAttribute = new ItemAttributeClass(Attributes.ENewItemAttributeId.ArmorDamage)
{
Name = Attributes.ENewItemAttributeId.ArmorDamage.GetName(),
Base = () => template.ArmorDamage,
StringValue = () => $"{template.ArmorDamage}%",
DisplayType = () => EItemAttributeDisplayType.Compact
};
attributes.Add(armorDamageAttribute);
}
if (template.PenetrationPower > 0)
{
string GetStringValue()
{
int ratedClass = 0;
if (!Singleton<BackendConfigSettingsClass>.Instantiated) { return $"[MunitionsExpert]: CLASS_DATA_MISSING {template.PenetrationPower}"; }
var armorClasses = Singleton<BackendConfigSettingsClass>.Instance.Armor.ArmorClass;
for (var i = 0; i < armorClasses.Length; i++)
{
if (armorClasses[i].Resistance > template.PenetrationPower) continue;
ratedClass = Math.Max(ratedClass, i);
}
return $"{(ratedClass > 0 ? $"Class {ratedClass}" : "Unarmored")} ({template.PenetrationPower})";
}
ItemAttributeClass penetrationAttribute = new ItemAttributeClass(Attributes.ENewItemAttributeId.Penetration)
{
Name = Attributes.ENewItemAttributeId.Penetration.GetName(),
Base = () => template.PenetrationPower,
StringValue = GetStringValue,
DisplayType = () => EItemAttributeDisplayType.Compact
};
attributes.Add(penetrationAttribute);
}
if (template.FragmentationChance > 0)
{
ItemAttributeClass fragmentationAttribute = new ItemAttributeClass(Attributes.ENewItemAttributeId.FragmentationChance)
{
Name = Attributes.ENewItemAttributeId.FragmentationChance.GetName(),
Base = () => template.FragmentationChance,
StringValue = () => $"{template.FragmentationChance * 100}%",
DisplayType = () => EItemAttributeDisplayType.Compact
};
attributes.Add(fragmentationAttribute);
}
if (template.RicochetChance > 0)
{
ItemAttributeClass ricochetAttribute = new ItemAttributeClass(Attributes.ENewItemAttributeId.RicochetChance)
{
Name = Attributes.ENewItemAttributeId.RicochetChance.GetName(),
Base = () => template.RicochetChance,
StringValue = () => $"{template.RicochetChance * 100}%",
DisplayType = () => EItemAttributeDisplayType.Compact
};
attributes.Add(ricochetAttribute);
}
}
}
}

View File

@ -0,0 +1,25 @@
using Aki.Reflection.Patching;
using EFT.InventoryLogic;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace MunitionsExpert.Patches
{
internal class CachedAttributesPatch : ModulePatch
{
protected override MethodBase GetTargetMethod()
{
return typeof(AmmoTemplate).GetMethod("GetCachedReadonlyQualities", BindingFlags.Instance | BindingFlags.Public);
}
[PatchPostfix]
private static void PatchPostfix(ref AmmoTemplate __instance, ref List<ItemAttributeClass> __result)
{
if (__result.All(a => (Attributes.ENewItemAttributeId)a.Id != Attributes.ENewItemAttributeId.Damage))
{
MunitionsExpertPlugin.AddNewAttributes(ref __result, __instance);
}
}
}
}

View File

@ -0,0 +1,35 @@
using System;
using System.Reflection;
using Aki.Reflection.Patching;
using EFT.UI;
using UnityEngine;
namespace MunitionsExpert.Patches
{
internal class StaticIconsPatch : ModulePatch
{
protected override MethodBase GetTargetMethod()
{
return typeof(StaticIcons).GetMethod("GetAttributeIcon", BindingFlags.Instance | BindingFlags.Public);
}
[PatchPrefix]
private static bool PatchPrefix(ref Sprite __result, Enum id)
{
if (id == null || !MunitionsExpertPlugin.iconCache.ContainsKey(id))
{
return true;
}
var sprite = MunitionsExpertPlugin.iconCache[id];
if (sprite != null)
{
__result = sprite;
return false;
}
return true;
}
}
}

View File

@ -1,155 +0,0 @@
using Aki.Common.Http;
using Aki.Common.Utils;
using BepInEx;
using Comfort.Common;
using EFT.InventoryLogic;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
using static MunitionsExpert.Attributes;
namespace MunitionsExpert
{
[BepInPlugin("com.Faupi.MunitionsExpert", "Faupi-MunitionsExpert", "1.6.9")]
public class Plugin : BaseUnityPlugin
{
public static Dictionary<Enum, Sprite> iconCache = new Dictionary<Enum, Sprite>();
public static List<ItemAttributeClass> penAttributes = new List<ItemAttributeClass>();
public static string modPath;
private void Awake()
{
GetPath();
new CachedAttributesPatch().Enable();
new StaticIconsPatch().Enable();
CacheIcons();
}
private void GetPath()
{
var mod = RequestHandler.GetJson($"/MunitionsExpert/GetInfo");
modPath = Json.Deserialize<string>(mod);
}
public static void CacheIcons()
{
iconCache.Add(ENewItemAttributeId.Damage, Resources.Load<Sprite>("characteristics/icons/icon_info_damage"));
iconCache.Add(ENewItemAttributeId.FragmentationChance, Resources.Load<Sprite>("characteristics/icons/icon_info_shrapnelcount"));
iconCache.Add(EItemAttributeId.LightBleedingDelta, Resources.Load<Sprite>("characteristics/icons/icon_info_bloodloss"));
iconCache.Add(EItemAttributeId.HeavyBleedingDelta, Resources.Load<Sprite>("characteristics/icon_info_hydration"));
iconCache.Add(ENewItemAttributeId.Penetration, Resources.Load<Sprite>("characteristics/icon_info_penetration"));
_ = LoadTexture(ENewItemAttributeId.ArmorDamage, Path.Combine(modPath, "res\\armorDamage.png"));
_ = LoadTexture(ENewItemAttributeId.RicochetChance, Path.Combine(modPath, "res\\armorDamage.png"));
}
public static async Task LoadTexture(Enum id, string path)
{
using (UnityWebRequest uwr = UnityWebRequestTexture.GetTexture(path))
{
uwr.SendWebRequest();
while (!uwr.isDone)
await Task.Delay(5);
if (uwr.responseCode != 200)
{
//Log.Error($"[{modName}] Request error {uwr.responseCode}: {uwr.error}");
}
else
{
// Get downloaded asset bundle
//Log.Info($"[{modName}] Retrieved texture! {id.ToString()} from {path}");
Texture2D cachedTexture = DownloadHandlerTexture.GetContent(uwr);
iconCache.Add(id, Sprite.Create(cachedTexture, new Rect(0, 0, cachedTexture.width, cachedTexture.height), new Vector2(0, 0)));
}
}
}
public static void AddNewAttributes(ref List<ItemAttributeClass> attributes, AmmoTemplate template)
{
int projCount = template.ProjectileCount;
int totalDamage = template.Damage * template.ProjectileCount;
string damageStr = totalDamage.ToString(); // Total damage
if (template.ProjectileCount > 1)
{
damageStr += $" ({template.Damage} x {template.ProjectileCount})"; // Add the "damage calculation" after total damage (damage per pellet * pellet count)
}
ItemAttributeClass at_damage = new ItemAttributeClass(ENewItemAttributeId.Damage)
{
Name = ENewItemAttributeId.Damage.GetName(),
Base = () => totalDamage,
StringValue = () => damageStr,
DisplayType = () => EItemAttributeDisplayType.Compact
};
attributes.Add(at_damage);
if (template.ArmorDamage > 0)
{
ItemAttributeClass at_armordmg = new ItemAttributeClass(ENewItemAttributeId.ArmorDamage)
{
Name = ENewItemAttributeId.ArmorDamage.GetName(),
Base = () => template.ArmorDamage,
StringValue = () => $"{(template.ArmorDamage).ToString()}%",
DisplayType = () => EItemAttributeDisplayType.Compact
};
attributes.Add(at_armordmg);
}
if (template.PenetrationPower > 0)
{
string getStringValue()
{
int ratedClass = 0;
if (!Singleton<BackendConfigSettingsClass>.Instantiated) { return $"CLASS_DATA_MISSING {template.PenetrationPower.ToString()}"; }
var classes = Singleton<BackendConfigSettingsClass>.Instance.Armor.ArmorClass;
for (int i = 0; i < classes.Length; i++)
{
if (classes[i].Resistance > template.PenetrationPower) continue;
ratedClass = Math.Max(ratedClass, i);
}
return $"{(ratedClass > 0 ? $"{"ME_class".Localized()} {ratedClass}" : "ME_noarmor".Localized())} ({template.PenetrationPower.ToString()})";
}
ItemAttributeClass at_pen = new ItemAttributeClass(ENewItemAttributeId.Penetration)
{
Name = ENewItemAttributeId.Penetration.GetName(),
Base = () => template.PenetrationPower,
StringValue = getStringValue,
DisplayType = () => EItemAttributeDisplayType.Compact
};
attributes.Add(at_pen);
}
if (template.FragmentationChance > 0)
{
ItemAttributeClass at_frag = new ItemAttributeClass(ENewItemAttributeId.FragmentationChance)
{
Name = ENewItemAttributeId.FragmentationChance.GetName(),
Base = () => template.FragmentationChance,
StringValue = () => $"{(template.FragmentationChance * 100).ToString()}%",
DisplayType = () => EItemAttributeDisplayType.Compact
};
attributes.Add(at_frag);
}
if (template.RicochetChance > 0)
{
ItemAttributeClass at_ricochet = new ItemAttributeClass(ENewItemAttributeId.RicochetChance)
{
Name = ENewItemAttributeId.RicochetChance.GetName(),
Base = () => template.RicochetChance,
StringValue = () => $"{(template.RicochetChance * 100).ToString()}%",
DisplayType = () => EItemAttributeDisplayType.Compact
};
attributes.Add(at_ricochet);
}
}
}
}

View File

@ -1,35 +0,0 @@
using System;
using System.Reflection;
using Aki.Reflection.Patching;
using EFT.UI;
using UnityEngine;
namespace MunitionsExpert
{
internal class StaticIconsPatch : ModulePatch
{
protected override MethodBase GetTargetMethod()
{
return typeof(StaticIcons).GetMethod("GetAttributeIcon", BindingFlags.Instance | BindingFlags.Public);
}
[PatchPrefix]
private static bool PatchPrefix(ref Sprite __result, Enum id)
{
if (id == null || !Plugin.iconCache.ContainsKey(id))
{
return true;
}
Sprite sprite = Plugin.iconCache[id];
if (sprite != null)
{
__result = sprite;
return false;
}
return true;
}
}
}

View File

@ -1,11 +1,11 @@
{ {
"name": "MunitionsExpert", "name": "MunitionsExpert",
"author": "Faupi", "author": "Faupi",
"version": "1.6.9", "updatedBy": "CWX",
"version": "1.7.0",
"license": "NCSA Open Source", "license": "NCSA Open Source",
"main": "./src/MunitionsExpert.js", "main": "./src/MunitionsExpert.js",
"akiVersion": "3.5.0", "akiVersion": "3.5.0",
"updatedBy": "CWX",
"scripts": { "scripts": {
"setup": "npm i", "setup": "npm i",
"build": "node ./packageBuild.ts" "build": "node ./packageBuild.ts"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 701 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 732 B

View File

@ -1,137 +0,0 @@
{
"en": {
"DAMAGE": "Damage",
"PENETRATION": "Armor penetration",
"ARMOR DAMAGE": "Damage to armor",
"FRAGMENTATION CHANCE": "Fragmentation chance",
"RICOCHET CHANCE": "Ricochet chance",
"ME_class": "Class",
"ME_noarmor": "Unarmored"
},
"cz": {
"DAMAGE": "Poškození",
"PENETRATION": "Průbojnost",
"ARMOR DAMAGE": "Poškození brnění",
"FRAGMENTATION CHANCE": "Šance na fragmentaci",
"RICOCHET CHANCE": "Šance na odraz",
"ME_class": "Třída",
"ME_noarmor": "Neobrněný"
},
"pl": {
"DAMAGE": "Szkoda",
"PENETRATION": "Penetracja pancerza",
"ARMOR DAMAGE": "Uszkodzenie zbroi",
"FRAGMENTATION CHANCE": "Szansa na fragmentację",
"RICOCHET CHANCE": "Szansa na rykoszet",
"ME_class": "Klasa",
"ME_noarmor": "Nieumiejętny"
},
"po": {
"DAMAGE": "Dano",
"PENETRATION": "Penetração de armadura",
"ARMOR DAMAGE": "Danos à armadura",
"FRAGMENTATION CHANCE": "Chance de fragmentação",
"RICOCHET CHANCE": "Chance de ricochete",
"ME_class": "Classe",
"ME_noarmor": "Sem armadura"
},
"ch": {
"DAMAGE": "损坏",
"PENETRATION": "护甲穿透",
"ARMOR DAMAGE": "对盔甲的伤害",
"FRAGMENTATION CHANCE": "碎片机会",
"RICOCHET CHANCE": "跳弹机会",
"ME_class": "类",
"ME_noarmor": "无所作为"
},
"ru": {
"DAMAGE": "Повреждать",
"PENETRATION": "Бронепробиваемость",
"ARMOR DAMAGE": "Повреждение брони",
"FRAGMENTATION CHANCE": "Вероятность фрагментации",
"RICOCHET CHANCE": "Шанс рикошета",
"ME_class": "Класс",
"ME_noarmor": "Без оружия"
},
"es": {
"DAMAGE": "Daño",
"PENETRATION": "Penetración de armadura",
"ARMOR DAMAGE": "Daño a la armadura",
"FRAGMENTATION CHANCE": "Posibilidad de fragmentación",
"RICOCHET CHANCE": "Oportunidad de rebote",
"ME_class": "Clase",
"ME_noarmor": "Sin armadura"
},
"es-mx": {
"DAMAGE": "Daño",
"PENETRATION": "Penetración de armadura",
"ARMOR DAMAGE": "Daño a la armadura",
"FRAGMENTATION CHANCE": "Posibilidad de fragmentación",
"RICOCHET CHANCE": "Oportunidad de rebote",
"ME_class": "Clase",
"ME_noarmor": "Sin armadura"
},
"ge": {
"DAMAGE": "Schaden",
"PENETRATION": "Rüstungsdurchdringung",
"ARMOR DAMAGE": "Beschädigung der Rüstung",
"FRAGMENTATION CHANCE": "Fragmentierung Chance",
"RICOCHET CHANCE": "Querschläger-Chance",
"ME_class": "Klasse",
"ME_noarmor": "Ungepanzert"
},
"sk": {
"DAMAGE": "Poškodenie",
"PENETRATION": "Prienik do brnenia",
"ARMOR DAMAGE": "Poškodenie brnenia",
"FRAGMENTATION CHANCE": "Šanca na fragmentáciu",
"RICOCHET CHANCE": "Šanca na odraz",
"ME_class": "Trieda",
"ME_noarmor": "Neozbrojený"
},
"tu": {
"DAMAGE": "Hasar",
"PENETRATION": "Zırh penetrasyon",
"ARMOR DAMAGE": "Zırhta hasar",
"FRAGMENTATION CHANCE": "Parçalanma şansı",
"RICOCHET CHANCE": "Sekme şansı",
"ME_class": "Sınıf",
"ME_noarmor": "zırhsız"
},
"it": {
"DAMAGE": "Danno",
"PENETRATION": "Penetrazione dell'armatura",
"ARMOR DAMAGE": "Danni all'armatura",
"FRAGMENTATION CHANCE": "Possibilità di frammentazione",
"RICOCHET CHANCE": "Possibilità di rimbalzo",
"ME_class": "Classe",
"ME_noarmor": "Disarmato"
},
"jp": {
"DAMAGE": "ダメージ",
"PENETRATION": "装甲貫通",
"ARMOR DAMAGE": "鎧の損傷",
"FRAGMENTATION CHANCE": "断片化の可能性",
"RICOCHET CHANCE": "跳ね返るチャンス",
"ME_class": "クラス",
"ME_noarmor": "無装甲"
},
"fr": {
"DAMAGE": "Dommage",
"PENETRATION": "Pénétration d'armure",
"ARMOR DAMAGE": "Dommages à l'armure",
"FRAGMENTATION CHANCE": "Chance de fragmentation",
"RICOCHET CHANCE": "Chance de ricochet",
"ME_class": "Classe",
"ME_noarmor": "Sans armure"
},
"hu": {
"DAMAGE": "Kár",
"PENETRATION": "Páncélátütő",
"ARMOR DAMAGE": "A páncél sérülése",
"FRAGMENTATION CHANCE": "Töredezettség esélye",
"RICOCHET CHANCE": "Ricochet esély",
"ME_class": "Osztály",
"ME_noarmor": "Fegyvertelen"
}
}

View File

@ -1,103 +1,53 @@
import type { DependencyContainer } from "tsyringe"; import type { DependencyContainer } from "tsyringe";
import { IPreAkiLoadMod } from "@spt-aki/models/external/IPreAkiLoadMod";
import { IPostAkiLoadMod } from "@spt-aki/models/external/IPostAkiLoadMod"; import { IPostAkiLoadMod } from "@spt-aki/models/external/IPostAkiLoadMod";
import { DynamicRouterModService } from "@spt-aki/services/mod/dynamicRouter/DynamicRouterModService"
import { DatabaseServer } from "@spt-aki/servers/DatabaseServer" import { DatabaseServer } from "@spt-aki/servers/DatabaseServer"
import { JsonUtil } from "@spt-aki/utils/JsonUtil" import { ITemplateItem } from "@spt-aki/models/eft/common/tables/ITemplateItem";
import { IDatabaseTables } from "@spt-aki/models/spt/server/IDatabaseTables";
import { PreAkiModLoader } from "@spt-aki/loaders/PreAkiModLoader";
class MunitionsExpert implements IPreAkiLoadMod, IPostAkiLoadMod class MunitionsExpert implements IPostAkiLoadMod
{ {
private database: DatabaseServer; private database: DatabaseServer;
private router: DynamicRouterModService; private items: Record<string, ITemplateItem>;
private json: JsonUtil; private cfg: { BulletBackgroundColours: boolean; } = require("./config.json");
private modLoader: PreAkiModLoader;
private table: IDatabaseTables;
private globalLocale: { [x: string]: { interface: { [x: string]: any; }; }; };
private translations: { [x: string]: any; };
private items: { [x: string]: any; };
private path: { resolve: (arg0: string) => any; };
private cfg: { BulletBackgroundColours: boolean; };
public preAkiLoad(container: DependencyContainer)
{
this.router = container.resolve<DynamicRouterModService>("DynamicRouterModService");
this.json = container.resolve<JsonUtil>("JsonUtil");
this.translations = require("../res/translations.json");
this.path = require("path");
this.cfg = require("./config.json");
this.hookRoutes();
}
public postAkiLoad(container: DependencyContainer) public postAkiLoad(container: DependencyContainer)
{ {
this.modLoader = container.resolve<PreAkiModLoader>("PreAkiModLoader");
this.database = container.resolve<DatabaseServer>("DatabaseServer"); this.database = container.resolve<DatabaseServer>("DatabaseServer");
this.table = this.database.getTables(); this.items = this.database.getTables().templates.items;
this.globalLocale = this.table.locales.global;
this.items = this.table.templates.items;
this.updateLocalization();
this.changeBulletColour(); this.changeBulletColour();
} }
private updateLocalization() private changeBulletColour()
{ {
for (const language in this.translations) if (this.cfg.BulletBackgroundColours)
{
if (!(language in this.globalLocale))
{
continue;
}
const attrKvPair = this.translations[language];
for (const attrKey in attrKvPair)
{
const attrValue = attrKvPair[attrKey];
this.globalLocale[language][attrKey] = attrValue;
}
}
}
private hookRoutes()
{
this.router.registerDynamicRouter(
"MunitionsExpert",
[
{
url: "/MunitionsExpert/GetInfo",
action: (url, info, sessionId, output) =>
{
return this.json.serialize(this.path.resolve(this.modLoader.getModPath("Faupi-MunitionsExpert 1.6.9")));
}
}
],
"MunitionsExpert"
)
}
changeBulletColour()
{
if (this.cfg.BulletBackgroundColours === true)
{ {
for (const i in this.items) for (const i in this.items)
{ {
const item = this.items[i] //set background colour of ammo depending on pen
if (this.items[i]._parent === "5485a8684bdc2da71d8b4567")
//set baground colour of ammo depending on pen
if (item._parent === "5485a8684bdc2da71d8b4567")
{ {
const pen = item._props.PenetrationPower const pen = this.items[i]._props.PenetrationPower;
let colour = ""
switch (true)
pen > 60 ? colour = "red" : //SuperHighPen {
pen > 50 ? colour = "yellow" : //HighPen case (pen > 60):
pen > 40 ? colour = "violet" : //MedHighPen this.items[i]._props.BackgroundColor = "red";
pen > 30 ? colour = "blue" : //MedPen break;
pen > 20 ? colour = "green" : //LowMedPen case (pen > 50):
colour = "grey" //LowPen this.items[i]._props.BackgroundColor = "yellow";
item._props.BackgroundColor = colour break;
case (pen > 40):
this.items[i]._props.BackgroundColor = "violet";
break;
case (pen > 30):
this.items[i]._props.BackgroundColor = "blue";
break;
case (pen > 20):
this.items[i]._props.BackgroundColor = "green";
break;
default:
this.items[i]._props.BackgroundColor = "grey";
break;
}
} }
} }
} }

View File

@ -1,11 +1,11 @@
{ {
"name": "MunitionsExpert", "name": "MunitionsExpert",
"author": "Faupi", "author": "Faupi",
"version": "1.6.9", "updatedBy": "CWX",
"version": "1.7.0",
"license": "NCSA Open Source", "license": "NCSA Open Source",
"main": "./src/MunitionsExpert.js", "main": "./src/MunitionsExpert.js",
"akiVersion": "3.5.0", "akiVersion": "3.5.0",
"updatedBy": "CWX",
"scripts": { "scripts": {
"setup": "npm i", "setup": "npm i",
"build": "node ./packageBuild.ts" "build": "node ./packageBuild.ts"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 701 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 732 B

View File

@ -1,137 +0,0 @@
{
"en": {
"DAMAGE": "Damage",
"PENETRATION": "Armor penetration",
"ARMOR DAMAGE": "Damage to armor",
"FRAGMENTATION CHANCE": "Fragmentation chance",
"RICOCHET CHANCE": "Ricochet chance",
"ME_class": "Class",
"ME_noarmor": "Unarmored"
},
"cz": {
"DAMAGE": "Poškození",
"PENETRATION": "Průbojnost",
"ARMOR DAMAGE": "Poškození brnění",
"FRAGMENTATION CHANCE": "Šance na fragmentaci",
"RICOCHET CHANCE": "Šance na odraz",
"ME_class": "Třída",
"ME_noarmor": "Neobrněný"
},
"pl": {
"DAMAGE": "Szkoda",
"PENETRATION": "Penetracja pancerza",
"ARMOR DAMAGE": "Uszkodzenie zbroi",
"FRAGMENTATION CHANCE": "Szansa na fragmentację",
"RICOCHET CHANCE": "Szansa na rykoszet",
"ME_class": "Klasa",
"ME_noarmor": "Nieumiejętny"
},
"po": {
"DAMAGE": "Dano",
"PENETRATION": "Penetração de armadura",
"ARMOR DAMAGE": "Danos à armadura",
"FRAGMENTATION CHANCE": "Chance de fragmentação",
"RICOCHET CHANCE": "Chance de ricochete",
"ME_class": "Classe",
"ME_noarmor": "Sem armadura"
},
"ch": {
"DAMAGE": "损坏",
"PENETRATION": "护甲穿透",
"ARMOR DAMAGE": "对盔甲的伤害",
"FRAGMENTATION CHANCE": "碎片机会",
"RICOCHET CHANCE": "跳弹机会",
"ME_class": "类",
"ME_noarmor": "无所作为"
},
"ru": {
"DAMAGE": "Повреждать",
"PENETRATION": "Бронепробиваемость",
"ARMOR DAMAGE": "Повреждение брони",
"FRAGMENTATION CHANCE": "Вероятность фрагментации",
"RICOCHET CHANCE": "Шанс рикошета",
"ME_class": "Класс",
"ME_noarmor": "Без оружия"
},
"es": {
"DAMAGE": "Daño",
"PENETRATION": "Penetración de armadura",
"ARMOR DAMAGE": "Daño a la armadura",
"FRAGMENTATION CHANCE": "Posibilidad de fragmentación",
"RICOCHET CHANCE": "Oportunidad de rebote",
"ME_class": "Clase",
"ME_noarmor": "Sin armadura"
},
"es-mx": {
"DAMAGE": "Daño",
"PENETRATION": "Penetración de armadura",
"ARMOR DAMAGE": "Daño a la armadura",
"FRAGMENTATION CHANCE": "Posibilidad de fragmentación",
"RICOCHET CHANCE": "Oportunidad de rebote",
"ME_class": "Clase",
"ME_noarmor": "Sin armadura"
},
"ge": {
"DAMAGE": "Schaden",
"PENETRATION": "Rüstungsdurchdringung",
"ARMOR DAMAGE": "Beschädigung der Rüstung",
"FRAGMENTATION CHANCE": "Fragmentierung Chance",
"RICOCHET CHANCE": "Querschläger-Chance",
"ME_class": "Klasse",
"ME_noarmor": "Ungepanzert"
},
"sk": {
"DAMAGE": "Poškodenie",
"PENETRATION": "Prienik do brnenia",
"ARMOR DAMAGE": "Poškodenie brnenia",
"FRAGMENTATION CHANCE": "Šanca na fragmentáciu",
"RICOCHET CHANCE": "Šanca na odraz",
"ME_class": "Trieda",
"ME_noarmor": "Neozbrojený"
},
"tu": {
"DAMAGE": "Hasar",
"PENETRATION": "Zırh penetrasyon",
"ARMOR DAMAGE": "Zırhta hasar",
"FRAGMENTATION CHANCE": "Parçalanma şansı",
"RICOCHET CHANCE": "Sekme şansı",
"ME_class": "Sınıf",
"ME_noarmor": "zırhsız"
},
"it": {
"DAMAGE": "Danno",
"PENETRATION": "Penetrazione dell'armatura",
"ARMOR DAMAGE": "Danni all'armatura",
"FRAGMENTATION CHANCE": "Possibilità di frammentazione",
"RICOCHET CHANCE": "Possibilità di rimbalzo",
"ME_class": "Classe",
"ME_noarmor": "Disarmato"
},
"jp": {
"DAMAGE": "ダメージ",
"PENETRATION": "装甲貫通",
"ARMOR DAMAGE": "鎧の損傷",
"FRAGMENTATION CHANCE": "断片化の可能性",
"RICOCHET CHANCE": "跳ね返るチャンス",
"ME_class": "クラス",
"ME_noarmor": "無装甲"
},
"fr": {
"DAMAGE": "Dommage",
"PENETRATION": "Pénétration d'armure",
"ARMOR DAMAGE": "Dommages à l'armure",
"FRAGMENTATION CHANCE": "Chance de fragmentation",
"RICOCHET CHANCE": "Chance de ricochet",
"ME_class": "Classe",
"ME_noarmor": "Sans armure"
},
"hu": {
"DAMAGE": "Kár",
"PENETRATION": "Páncélátütő",
"ARMOR DAMAGE": "A páncél sérülése",
"FRAGMENTATION CHANCE": "Töredezettség esélye",
"RICOCHET CHANCE": "Ricochet esély",
"ME_class": "Osztály",
"ME_noarmor": "Fegyvertelen"
}
}

View File

@ -1,103 +1,53 @@
import type { DependencyContainer } from "tsyringe"; import type { DependencyContainer } from "tsyringe";
import { IPreAkiLoadMod } from "@spt-aki/models/external/IPreAkiLoadMod";
import { IPostAkiLoadMod } from "@spt-aki/models/external/IPostAkiLoadMod"; import { IPostAkiLoadMod } from "@spt-aki/models/external/IPostAkiLoadMod";
import { DynamicRouterModService } from "@spt-aki/services/mod/dynamicRouter/DynamicRouterModService"
import { DatabaseServer } from "@spt-aki/servers/DatabaseServer" import { DatabaseServer } from "@spt-aki/servers/DatabaseServer"
import { JsonUtil } from "@spt-aki/utils/JsonUtil" import { ITemplateItem } from "@spt-aki/models/eft/common/tables/ITemplateItem";
import { IDatabaseTables } from "@spt-aki/models/spt/server/IDatabaseTables";
import { PreAkiModLoader } from "@spt-aki/loaders/PreAkiModLoader";
class MunitionsExpert implements IPreAkiLoadMod, IPostAkiLoadMod class MunitionsExpert implements IPostAkiLoadMod
{ {
private database: DatabaseServer; private database: DatabaseServer;
private router: DynamicRouterModService; private items: Record<string, ITemplateItem>;
private json: JsonUtil; private cfg: { BulletBackgroundColours: boolean; } = require("./config.json");
private modLoader: PreAkiModLoader;
private table: IDatabaseTables;
private globalLocale: { [x: string]: { interface: { [x: string]: any; }; }; };
private translations: { [x: string]: any; };
private items: { [x: string]: any; };
private path: { resolve: (arg0: string) => any; };
private cfg: { BulletBackgroundColours: boolean; };
public preAkiLoad(container: DependencyContainer)
{
this.router = container.resolve<DynamicRouterModService>("DynamicRouterModService");
this.json = container.resolve<JsonUtil>("JsonUtil");
this.translations = require("../res/translations.json");
this.path = require("path");
this.cfg = require("./config.json");
this.hookRoutes();
}
public postAkiLoad(container: DependencyContainer) public postAkiLoad(container: DependencyContainer)
{ {
this.modLoader = container.resolve<PreAkiModLoader>("PreAkiModLoader");
this.database = container.resolve<DatabaseServer>("DatabaseServer"); this.database = container.resolve<DatabaseServer>("DatabaseServer");
this.table = this.database.getTables(); this.items = this.database.getTables().templates.items;
this.globalLocale = this.table.locales.global;
this.items = this.table.templates.items;
this.updateLocalization();
this.changeBulletColour(); this.changeBulletColour();
} }
private updateLocalization() private changeBulletColour()
{ {
for (const language in this.translations) if (this.cfg.BulletBackgroundColours)
{
if (!(language in this.globalLocale))
{
continue;
}
const attrKvPair = this.translations[language];
for (const attrKey in attrKvPair)
{
const attrValue = attrKvPair[attrKey];
this.globalLocale[language][attrKey] = attrValue;
}
}
}
private hookRoutes()
{
this.router.registerDynamicRouter(
"MunitionsExpert",
[
{
url: "/MunitionsExpert/GetInfo",
action: (url, info, sessionId, output) =>
{
return this.json.serialize(this.path.resolve(this.modLoader.getModPath("Faupi-MunitionsExpert 1.6.9")));
}
}
],
"MunitionsExpert"
)
}
changeBulletColour()
{
if (this.cfg.BulletBackgroundColours === true)
{ {
for (const i in this.items) for (const i in this.items)
{ {
const item = this.items[i] //set background colour of ammo depending on pen
if (this.items[i]._parent === "5485a8684bdc2da71d8b4567")
//set baground colour of ammo depending on pen
if (item._parent === "5485a8684bdc2da71d8b4567")
{ {
const pen = item._props.PenetrationPower const pen = this.items[i]._props.PenetrationPower;
let colour = ""
switch (true)
pen > 60 ? colour = "red" : //SuperHighPen {
pen > 50 ? colour = "yellow" : //HighPen case (pen > 60):
pen > 40 ? colour = "violet" : //MedHighPen this.items[i]._props.BackgroundColor = "red";
pen > 30 ? colour = "blue" : //MedPen break;
pen > 20 ? colour = "green" : //LowMedPen case (pen > 50):
colour = "grey" //LowPen this.items[i]._props.BackgroundColor = "yellow";
item._props.BackgroundColor = colour break;
case (pen > 40):
this.items[i]._props.BackgroundColor = "violet";
break;
case (pen > 30):
this.items[i]._props.BackgroundColor = "blue";
break;
case (pen > 20):
this.items[i]._props.BackgroundColor = "green";
break;
default:
this.items[i]._props.BackgroundColor = "grey";
break;
}
} }
} }
} }