add wiring for offensive buffs
This commit is contained in:
parent
04ead32846
commit
3737ad517c
12 changed files with 338 additions and 3 deletions
|
|
@ -13,6 +13,7 @@ GameObject:
|
||||||
- component: {fileID: 2918837822014987993}
|
- component: {fileID: 2918837822014987993}
|
||||||
- component: {fileID: 7845089877743661692}
|
- component: {fileID: 7845089877743661692}
|
||||||
- component: {fileID: 4336209376377567030}
|
- component: {fileID: 4336209376377567030}
|
||||||
|
- component: {fileID: 2806524246861401760}
|
||||||
m_Layer: 0
|
m_Layer: 0
|
||||||
m_Name: Player
|
m_Name: Player
|
||||||
m_TagString: Untagged
|
m_TagString: Untagged
|
||||||
|
|
@ -101,3 +102,19 @@ MonoBehaviour:
|
||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.PlayerMatchState
|
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.PlayerMatchState
|
||||||
ShowTopMostFoldoutHeaderGroup: 1
|
ShowTopMostFoldoutHeaderGroup: 1
|
||||||
|
--- !u!114 &2806524246861401760
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 3493329038866903420}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 7775ee3a2f441b52480aabf54be6c1b6, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.PlayerBuffManager
|
||||||
|
ShowTopMostFoldoutHeaderGroup: 1
|
||||||
|
categories:
|
||||||
|
- {fileID: 0}
|
||||||
|
- {fileID: 0}
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,11 @@ namespace TD.Combat
|
||||||
// Cached on OnNetworkSpawn — avoids GetComponent every Update.
|
// Cached on OnNetworkSpawn — avoids GetComponent every Update.
|
||||||
private TowerInstance towerInstance;
|
private TowerInstance towerInstance;
|
||||||
|
|
||||||
|
// Lazily resolved on first attack — the owner's slot isn't guaranteed to be
|
||||||
|
// set at spawn time, so we defer the lookup until combat actually begins.
|
||||||
|
private PlayerBuffManager ownerBuffManager;
|
||||||
|
private bool buffManagerResolved;
|
||||||
|
|
||||||
// Shared OverlapSphere result buffer. 32 covers any realistic enemy
|
// Shared OverlapSphere result buffer. 32 covers any realistic enemy
|
||||||
// density; size up if profiling reveals overflow.
|
// density; size up if profiling reveals overflow.
|
||||||
private static readonly Collider[] s_overlapBuffer = new Collider[32];
|
private static readonly Collider[] s_overlapBuffer = new Collider[32];
|
||||||
|
|
@ -211,6 +216,25 @@ namespace TD.Combat
|
||||||
ClearTarget();
|
ClearTarget();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----- Buff multiplier lookup -------------------------------------
|
||||||
|
|
||||||
|
// Resolved lazily on the first attack because the owner slot NetworkVariable
|
||||||
|
// may not yet have replicated by the time OnNetworkSpawn runs.
|
||||||
|
private PlayerBuffManager GetOwnerBuffManager()
|
||||||
|
{
|
||||||
|
if (buffManagerResolved) return ownerBuffManager;
|
||||||
|
buffManagerResolved = true;
|
||||||
|
|
||||||
|
var slot = towerInstance.Owner;
|
||||||
|
if (slot == PlayerSlot.None) return null;
|
||||||
|
|
||||||
|
var matchState = PlayerMatchState.GetForSlot(slot);
|
||||||
|
if (matchState == null) return null;
|
||||||
|
|
||||||
|
ownerBuffManager = matchState.GetComponent<PlayerBuffManager>();
|
||||||
|
return ownerBuffManager;
|
||||||
|
}
|
||||||
|
|
||||||
// ----- Attack tick -------------------------------------------------
|
// ----- Attack tick -------------------------------------------------
|
||||||
|
|
||||||
private void TickAttack(TowerDefinition def)
|
private void TickAttack(TowerDefinition def)
|
||||||
|
|
@ -218,7 +242,9 @@ namespace TD.Combat
|
||||||
attackCooldown -= Time.deltaTime;
|
attackCooldown -= Time.deltaTime;
|
||||||
if (attackCooldown > 0f) return;
|
if (attackCooldown > 0f) return;
|
||||||
|
|
||||||
attackCooldown = 1f / def.FireRate;
|
float effectiveFireRate = def.FireRate
|
||||||
|
* (GetOwnerBuffManager()?.GetMultiplier(BuffStat.AttackSpeed) ?? 1f);
|
||||||
|
attackCooldown = 1f / effectiveFireRate;
|
||||||
Fire(def);
|
Fire(def);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -316,7 +342,9 @@ namespace TD.Combat
|
||||||
|
|
||||||
private void HitEnemy(TowerDefinition def, EnemyHealth target, PlayerSlot owner)
|
private void HitEnemy(TowerDefinition def, EnemyHealth target, PlayerSlot owner)
|
||||||
{
|
{
|
||||||
target.TakeDamage(def.Damage, def.DamageType, owner);
|
float effectiveDamage = def.Damage
|
||||||
|
* (GetOwnerBuffManager()?.GetMultiplier(BuffStat.Damage) ?? 1f);
|
||||||
|
target.TakeDamage(effectiveDamage, def.DamageType, owner);
|
||||||
ApplyStatusEffect(def, target, owner);
|
ApplyStatusEffect(def, target, owner);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -340,6 +368,8 @@ namespace TD.Combat
|
||||||
|
|
||||||
// ----- Projectile spawning -----------------------------------------
|
// ----- Projectile spawning -----------------------------------------
|
||||||
|
|
||||||
|
// TODO this seems wacky. Why do we calculate effective damage twice? Are they both used?
|
||||||
|
// Spawn projectile is void. Should it return a projectile?
|
||||||
private void SpawnProjectile(TowerDefinition def, EnemyHealth target)
|
private void SpawnProjectile(TowerDefinition def, EnemyHealth target)
|
||||||
{
|
{
|
||||||
var go = Instantiate(def.ProjectilePrefab, transform.position, Quaternion.identity);
|
var go = Instantiate(def.ProjectilePrefab, transform.position, Quaternion.identity);
|
||||||
|
|
@ -354,9 +384,11 @@ namespace TD.Combat
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float effectiveDamage = def.Damage
|
||||||
|
* (GetOwnerBuffManager()?.GetMultiplier(BuffStat.Damage) ?? 1f);
|
||||||
proj.InitializeServer(
|
proj.InitializeServer(
|
||||||
target,
|
target,
|
||||||
def.Damage,
|
effectiveDamage,
|
||||||
def.DamageType,
|
def.DamageType,
|
||||||
def.TargetType,
|
def.TargetType,
|
||||||
def.SplashRadius,
|
def.SplashRadius,
|
||||||
|
|
|
||||||
12
Assets/_Project/Scripts/Core/BuffStat.cs
Normal file
12
Assets/_Project/Scripts/Core/BuffStat.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Assets/_Project/Scripts/Core/BuffStat.cs
|
||||||
|
namespace TD.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Identifies which tower stat a <see cref="TD.Gameplay.BuffDefinition"/> modifies.
|
||||||
|
/// </summary>
|
||||||
|
public enum BuffStat : byte
|
||||||
|
{
|
||||||
|
Damage = 0,
|
||||||
|
AttackSpeed = 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/_Project/Scripts/Core/BuffStat.cs.meta
Normal file
2
Assets/_Project/Scripts/Core/BuffStat.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: fc84fec503f24bc7693ee29558f45d27
|
||||||
61
Assets/_Project/Scripts/Gameplay/ActiveBuff.cs
Normal file
61
Assets/_Project/Scripts/Gameplay/ActiveBuff.cs
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
// Assets/_Project/Scripts/Gameplay/ActiveBuff.cs
|
||||||
|
using System;
|
||||||
|
using Unity.Collections;
|
||||||
|
using Unity.Netcode;
|
||||||
|
using TD.Core;
|
||||||
|
|
||||||
|
namespace TD.Gameplay
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// One buff currently held by a player. Stored in
|
||||||
|
/// <see cref="PlayerBuffManager"/>'s NetworkList so all peers see the same set.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para><b>IEquatable.</b> NGO's NetworkList indexer setter short-circuits when
|
||||||
|
/// Equals returns true, silently dropping the write. Every mutable field
|
||||||
|
/// (only <see cref="IsActive"/>) is included in the comparison so toggling a
|
||||||
|
/// buff correctly propagates to clients.</para>
|
||||||
|
/// </remarks>
|
||||||
|
public struct ActiveBuff : INetworkSerializable, IEquatable<ActiveBuff>
|
||||||
|
{
|
||||||
|
/// <summary>Which tower stat this buff multiplies.</summary>
|
||||||
|
public BuffStat Stat;
|
||||||
|
|
||||||
|
/// <summary>Multiplicative factor, e.g. 1.25 for +25%.</summary>
|
||||||
|
public float Multiplier;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// False when disabled (e.g. by an enemy ability). Excluded from
|
||||||
|
/// <see cref="PlayerBuffManager.GetMultiplier"/> while false.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsActive;
|
||||||
|
|
||||||
|
/// <summary>Human-readable name for the HUD buff list.</summary>
|
||||||
|
public FixedString64Bytes DisplayName;
|
||||||
|
|
||||||
|
// ----- INetworkSerializable ---------------------------------------
|
||||||
|
|
||||||
|
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
|
||||||
|
{
|
||||||
|
byte statByte = (byte)Stat;
|
||||||
|
serializer.SerializeValue(ref statByte);
|
||||||
|
Stat = (BuffStat)statByte;
|
||||||
|
|
||||||
|
serializer.SerializeValue(ref Multiplier);
|
||||||
|
serializer.SerializeValue(ref IsActive);
|
||||||
|
serializer.SerializeValue(ref DisplayName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- IEquatable -------------------------------------------------
|
||||||
|
|
||||||
|
public bool Equals(ActiveBuff other)
|
||||||
|
=> Stat == other.Stat
|
||||||
|
&& Multiplier == other.Multiplier
|
||||||
|
&& IsActive == other.IsActive
|
||||||
|
&& DisplayName == other.DisplayName;
|
||||||
|
|
||||||
|
public override bool Equals(object obj) => obj is ActiveBuff other && Equals(other);
|
||||||
|
|
||||||
|
public override int GetHashCode() => HashCode.Combine((int)Stat, Multiplier, IsActive);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/_Project/Scripts/Gameplay/ActiveBuff.cs.meta
Normal file
2
Assets/_Project/Scripts/Gameplay/ActiveBuff.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b09203f8acc450bf3b3c4bd8ef3fe84e
|
||||||
23
Assets/_Project/Scripts/Gameplay/BuffCategory.cs
Normal file
23
Assets/_Project/Scripts/Gameplay/BuffCategory.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
// Assets/_Project/Scripts/Gameplay/BuffCategory.cs
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace TD.Gameplay
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A purchasable category of buffs. The player pays <see cref="Cost"/> gold
|
||||||
|
/// and receives a randomly chosen <see cref="BuffDefinition"/> from <see cref="Pool"/>.
|
||||||
|
/// </summary>
|
||||||
|
[CreateAssetMenu(fileName = "BuffCategory", menuName = "TD/Buffs/Buff Category")]
|
||||||
|
public class BuffCategory : ScriptableObject
|
||||||
|
{
|
||||||
|
[Tooltip("Name shown on the buff menu purchase button.")]
|
||||||
|
public string DisplayName;
|
||||||
|
|
||||||
|
[Tooltip("Gold cost to purchase one random buff from this category.")]
|
||||||
|
[Min(0)]
|
||||||
|
public int Cost;
|
||||||
|
|
||||||
|
[Tooltip("Set of buffs the player can receive. One is chosen uniformly at random.")]
|
||||||
|
public BuffDefinition[] Pool;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/_Project/Scripts/Gameplay/BuffCategory.cs.meta
Normal file
2
Assets/_Project/Scripts/Gameplay/BuffCategory.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 516e8faec948a4b3d977ec019376c438
|
||||||
24
Assets/_Project/Scripts/Gameplay/BuffDefinition.cs
Normal file
24
Assets/_Project/Scripts/Gameplay/BuffDefinition.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
// Assets/_Project/Scripts/Gameplay/BuffDefinition.cs
|
||||||
|
using UnityEngine;
|
||||||
|
using TD.Core;
|
||||||
|
|
||||||
|
namespace TD.Gameplay
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// One possible buff that can be awarded to a player. Lives in a
|
||||||
|
/// <see cref="BuffCategory"/>'s pool and is drawn randomly on purchase.
|
||||||
|
/// </summary>
|
||||||
|
[CreateAssetMenu(fileName = "BuffDefinition", menuName = "TD/Buffs/Buff Definition")]
|
||||||
|
public class BuffDefinition : ScriptableObject
|
||||||
|
{
|
||||||
|
[Tooltip("Name shown in the buff menu and tooltip.")]
|
||||||
|
public string DisplayName;
|
||||||
|
|
||||||
|
[Tooltip("Which tower stat this buff multiplies.")]
|
||||||
|
public BuffStat Stat;
|
||||||
|
|
||||||
|
[Tooltip("Multiplicative factor applied to the stat. 1.25 = +25%.")]
|
||||||
|
[Min(1f)]
|
||||||
|
public float Multiplier = 1.25f;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/_Project/Scripts/Gameplay/BuffDefinition.cs.meta
Normal file
2
Assets/_Project/Scripts/Gameplay/BuffDefinition.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b01f9aea62ee8a1c1991bb2a097de224
|
||||||
156
Assets/_Project/Scripts/Gameplay/PlayerBuffManager.cs
Normal file
156
Assets/_Project/Scripts/Gameplay/PlayerBuffManager.cs
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
// Assets/_Project/Scripts/Gameplay/PlayerBuffManager.cs
|
||||||
|
using Unity.Collections;
|
||||||
|
using Unity.Netcode;
|
||||||
|
using UnityEngine;
|
||||||
|
using TD.Core;
|
||||||
|
|
||||||
|
namespace TD.Gameplay
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Holds all buffs currently owned by one player. Lives on the Player prefab
|
||||||
|
/// alongside <see cref="PlayerGoldManager"/> and <see cref="PlayerMatchState"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para><b>Purchase flow.</b> The owning client calls
|
||||||
|
/// <see cref="RequestPurchaseBuffRpc"/> with a category index. The server
|
||||||
|
/// validates gold, deducts the cost, picks a random
|
||||||
|
/// <see cref="BuffDefinition"/> from the category's pool, and appends an
|
||||||
|
/// <see cref="ActiveBuff"/> to the replicated <see cref="buffs"/> list.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Multiplier query.</b> <see cref="GetMultiplier"/> is called by
|
||||||
|
/// <see cref="TD.Combat.TowerCombat"/> on the server each time a tower fires.
|
||||||
|
/// It multiplies all active buffs for the requested stat together.</para>
|
||||||
|
///
|
||||||
|
/// <para><b>Enable / disable.</b> Enemies can call <see cref="ServerDisableBuff"/>
|
||||||
|
/// and <see cref="ServerEnableBuff"/> by index to temporarily suppress buffs
|
||||||
|
/// without removing them from the list.</para>
|
||||||
|
/// </remarks>
|
||||||
|
public class PlayerBuffManager : NetworkBehaviour
|
||||||
|
{
|
||||||
|
[Tooltip("Purchasable buff categories shown in the buff menu. Index in this " +
|
||||||
|
"array is the categoryIndex passed to RequestPurchaseBuffRpc.")]
|
||||||
|
[SerializeField] private BuffCategory[] categories;
|
||||||
|
|
||||||
|
private NetworkList<ActiveBuff> buffs;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
buffs = new NetworkList<ActiveBuff>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Public API -------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the combined multiplier for <paramref name="stat"/> across all
|
||||||
|
/// active buffs owned by this player. Returns 1.0 if no matching buffs
|
||||||
|
/// are active. Safe to call from any peer.
|
||||||
|
/// </summary>
|
||||||
|
public float GetMultiplier(BuffStat stat)
|
||||||
|
{
|
||||||
|
float result = 1f;
|
||||||
|
for (int i = 0; i < buffs.Count; i++)
|
||||||
|
{
|
||||||
|
var buff = buffs[i];
|
||||||
|
if (buff.Stat == stat && buff.IsActive)
|
||||||
|
result *= buff.Multiplier;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the number of buff categories available, for building the UI.
|
||||||
|
/// </summary>
|
||||||
|
public int CategoryCount => categories?.Length ?? 0;
|
||||||
|
|
||||||
|
/// <summary>Returns the category at the given index, or null.</summary>
|
||||||
|
public BuffCategory GetCategory(int index)
|
||||||
|
=> (categories != null && index >= 0 && index < categories.Length)
|
||||||
|
? categories[index]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
/// <summary>Read-only access to the buff list for UI rendering.</summary>
|
||||||
|
public NetworkList<ActiveBuff> Buffs => buffs;
|
||||||
|
|
||||||
|
// ----- Server helpers ---------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Server-only: disables the buff at <paramref name="index"/>.
|
||||||
|
/// The buff remains in the list and can be re-enabled.
|
||||||
|
/// </summary>
|
||||||
|
public void ServerDisableBuff(int index)
|
||||||
|
{
|
||||||
|
if (!IsServer) return;
|
||||||
|
if (index < 0 || index >= buffs.Count) return;
|
||||||
|
|
||||||
|
var b = buffs[index];
|
||||||
|
if (!b.IsActive) return;
|
||||||
|
b.IsActive = false;
|
||||||
|
buffs[index] = b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Server-only: re-enables the buff at <paramref name="index"/>.</summary>
|
||||||
|
public void ServerEnableBuff(int index)
|
||||||
|
{
|
||||||
|
if (!IsServer) return;
|
||||||
|
if (index < 0 || index >= buffs.Count) return;
|
||||||
|
|
||||||
|
var b = buffs[index];
|
||||||
|
if (b.IsActive) return;
|
||||||
|
b.IsActive = true;
|
||||||
|
buffs[index] = b;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Purchase RPC -----------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Owning-client entry point. Sends a request to the server to purchase
|
||||||
|
/// a random buff from the category at <paramref name="categoryIndex"/>.
|
||||||
|
/// The server validates gold and adds the buff if the purchase succeeds.
|
||||||
|
/// </summary>
|
||||||
|
[Rpc(SendTo.Server, RequireOwnership = true)]
|
||||||
|
public void RequestPurchaseBuffRpc(int categoryIndex)
|
||||||
|
{
|
||||||
|
if (categories == null || categoryIndex < 0 || categoryIndex >= categories.Length)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[PlayerBuffManager] RequestPurchaseBuff: invalid category index.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var category = categories[categoryIndex];
|
||||||
|
if (category == null || category.Pool == null || category.Pool.Length == 0)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[PlayerBuffManager] RequestPurchaseBuff: category has no buffs.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var goldManager = PlayerGoldManager.GetForClient(OwnerClientId);
|
||||||
|
if (goldManager == null)
|
||||||
|
{
|
||||||
|
Debug.LogError("[PlayerBuffManager] RequestPurchaseBuff: no PlayerGoldManager found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (goldManager.CurrentGold < category.Cost)
|
||||||
|
{
|
||||||
|
Debug.Log($"[PlayerBuffManager] Client {OwnerClientId} cannot afford " +
|
||||||
|
$"'{category.DisplayName}' (cost {category.Cost}, " +
|
||||||
|
$"gold {goldManager.CurrentGold}).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
goldManager.DeductGold(category.Cost);
|
||||||
|
|
||||||
|
var def = category.Pool[Random.Range(0, category.Pool.Length)];
|
||||||
|
buffs.Add(new ActiveBuff
|
||||||
|
{
|
||||||
|
Stat = def.Stat,
|
||||||
|
Multiplier = def.Multiplier,
|
||||||
|
IsActive = true,
|
||||||
|
DisplayName = new FixedString64Bytes(def.DisplayName ?? string.Empty),
|
||||||
|
});
|
||||||
|
|
||||||
|
Debug.Log($"[PlayerBuffManager] Client {OwnerClientId} purchased buff " +
|
||||||
|
$"'{def.DisplayName}' ({def.Stat} ×{def.Multiplier}).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7775ee3a2f441b52480aabf54be6c1b6
|
||||||
Loading…
Add table
Add a link
Reference in a new issue