PerformanceStat's four fixed fields (HeatExponentMultiplier, DurabilityExponentMultiplier, QualityExponent, FromRole) tombstone into a declared List<StatTerm> (source + exponent + role), walked by the one evaluation path Cut 0 unified. A stat with no terms resolves to Max (the same identity a stat with all-zero exponents already produced), so the migration is exact: a zero multiplier becomes no term, not a term with exponent 0. R-heat (operator, docs/stats-and-power-cut.md): EquippableItemData's HeatPerformanceCurve (a BezierCurve, plus the 100-sample scan that inferred OptimalTemperature and its cache) is retired in favor of an authored minimum, maximum, optimum and plateau width, with linear falloff outside the plateau. StatValidation refuses an optimum outside its bounds or a negative plateau width, loudly, at catalog load. The plateau's consequence rides along for free: Wear's thermal term is pow(ThermalPerformance, ...), and Performance() now returns exactly 1 across the plateau, so a well-managed item takes wear only from deltaTemp there -- the operational-lifespan lever the operator asked for. ConsumableItemEffect's progress-through-duration substitution for "heat" becomes its own StatSource (ConsumableProgress) rather than an unconditional override applied regardless of what a stat declares. Tests: LoadoutTests' four PerformanceStat-shaped fixtures rebuilt against Terms/authored heat fields; HeatResponseTests.cs (new) pins load validation, the plateau shape, and the three wear-lever claims (held inside takes no thermal wear, held outside does, a fast swing across still wears) each under its own mutation in tests/mutation_tests_stats_power_cut1.py (new, 10 rules including a no-op control; all pass). 64 pre-Cut-1 tests still pass unmodified; 71 total with the 7 new ones. Unity batchmode: 0 error CS. ShieldPanelCut1Verify and Cut2Verify pass; Cut3Verify fails with an unrelated pre-existing "Kernel 'KInit' not found" compute-shader error, reproduced against Cut 0's own landing state with no shader/compute file in this diff -- flagged for Soul, not fixed here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
642 lines
23 KiB
C#
642 lines
23 KiB
C#
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
|
|
using GameCult.Caching;
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using TMPro;
|
|
using UniRx;
|
|
using UnityEngine;
|
|
using UnityEngine.EventSystems;
|
|
using UnityEngine.UI;
|
|
using CultMath;
|
|
using static CultMath.math;
|
|
|
|
public class PropertiesPanel : MonoBehaviour
|
|
{
|
|
public RectTransform Spacer;
|
|
public DropdownMenu Dropdown;
|
|
public TextMeshProUGUI Title;
|
|
public RectTransform Section;
|
|
public PropertiesList List;
|
|
public Property Property;
|
|
public PropertyLabel PropertyLabel;
|
|
public AttributeProperty Attribute;
|
|
public InputField InputField;
|
|
public RangedFloatField RangedFloatField;
|
|
public RangedFloatField ProgressField;
|
|
public EnumField EnumField;
|
|
public BoolField BoolField;
|
|
public PropertyButton PropertyButton;
|
|
public ButtonField ButtonField;
|
|
public IncrementField IncrementField;
|
|
public StatSheet StatSheet;
|
|
public CurveField CurveField;
|
|
public WeaponGroupAssignment WeaponGroupAssignment;
|
|
public RectTransform Content;
|
|
public RectTransform DragParent;
|
|
|
|
[HideInInspector] public ActionGameManager GameManager;
|
|
|
|
protected List<GameObject> Properties = new List<GameObject>();
|
|
protected event Action RefreshPropertyValues;
|
|
protected bool RadioSelection = false;
|
|
|
|
private RectTransform _dragObject;
|
|
|
|
|
|
protected event Action<GameObject> OnPropertyAdded;
|
|
protected Action OnPropertiesChanged;
|
|
|
|
public int Children => Properties.Count;
|
|
|
|
public void Awake()
|
|
{
|
|
OnPropertyAdded += go => go.SetActive(true);
|
|
}
|
|
|
|
public virtual void Update()
|
|
{
|
|
RefreshValues();
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
}
|
|
|
|
// private void OnDisable()
|
|
// {
|
|
// RemoveListener();
|
|
// }
|
|
|
|
public void Clear()
|
|
{
|
|
//RemoveListener();
|
|
foreach(var property in Properties)
|
|
Destroy(property);
|
|
Title.text = "Properties";
|
|
Properties.Clear();
|
|
RefreshPropertyValues = null;
|
|
RadioSelection = false;
|
|
}
|
|
|
|
public RectTransform AddSpacer()
|
|
{
|
|
var spacer = Instantiate(Spacer, Content ?? transform);
|
|
Properties.Add(spacer.gameObject);
|
|
OnPropertyAdded?.Invoke(spacer.gameObject);
|
|
return spacer;
|
|
}
|
|
|
|
public RectTransform AddSection(string name)
|
|
{
|
|
var section = Instantiate(Section, Content ?? transform);
|
|
section.GetComponentInChildren<TextMeshProUGUI>().text = name;
|
|
Properties.Add(section.gameObject);
|
|
OnPropertyAdded?.Invoke(section.gameObject);
|
|
return section;
|
|
}
|
|
|
|
public Property AddProperty(Func<string> read)
|
|
{
|
|
if (read == null) throw new ArgumentException("Attempted to add property with null read function!");
|
|
|
|
var property = Instantiate(Property, Content ?? transform);
|
|
property.Label.text = read();
|
|
|
|
RefreshPropertyValues += () => property.Label.text = read();
|
|
Properties.Add(property.gameObject);
|
|
OnPropertyAdded?.Invoke(property.gameObject);
|
|
return property;
|
|
}
|
|
|
|
public Property AddProperty(string name, Func<string> read = null)
|
|
{
|
|
Property property;
|
|
if(read != null)
|
|
property = Instantiate(PropertyLabel, Content ?? transform);
|
|
else
|
|
property = Instantiate(Property, Content ?? transform);
|
|
property.Label.text = name;
|
|
|
|
if (read != null)
|
|
RefreshPropertyValues += () => ((PropertyLabel) property).Value.text = read.Invoke();
|
|
Properties.Add(property.gameObject);
|
|
OnPropertyAdded?.Invoke(property.gameObject);
|
|
return property;
|
|
}
|
|
|
|
public StatSheet AddStatSheet()
|
|
{
|
|
var sheet = Instantiate(StatSheet, Content ?? transform);
|
|
Properties.Add(sheet.gameObject);
|
|
OnPropertyAdded?.Invoke(sheet.gameObject);
|
|
RefreshPropertyValues += () => sheet.RefreshValues();
|
|
return sheet;
|
|
}
|
|
|
|
public PropertiesList AddList(string name) //, IEnumerable<(string, Func<string>)> elements)
|
|
{
|
|
var list = Instantiate(List, Content ?? transform);
|
|
list.GameManager = GameManager;
|
|
list.Dropdown = Dropdown;
|
|
list.Title.text = name;
|
|
// foreach (var element in elements)
|
|
// {
|
|
// var item = Instantiate(PropertyPrefab, list);
|
|
// item.Name.text = element.Item1;
|
|
// item.Value.text = element.Item2();
|
|
// item.ValueFunction = element.Item2;
|
|
// Properties.Add(item.gameObject);
|
|
// }
|
|
//RefreshPropertyValues += () => list.RefreshValues();
|
|
Properties.Add(list.gameObject);
|
|
OnPropertyAdded?.Invoke(list.gameObject);
|
|
return list;
|
|
}
|
|
|
|
public AttributeProperty AddPersonalityProperty(PersonalityAttribute attribute, Func<float> read)
|
|
{
|
|
var attributeInstance = Instantiate(Attribute, Content ?? transform);
|
|
attributeInstance.Title.text = attribute.Name;
|
|
attributeInstance.HighLabel.text = attribute.HighName;
|
|
attributeInstance.LowLabel.text = attribute.LowName;
|
|
RefreshPropertyValues += () => attributeInstance.Slider.value = read();
|
|
Properties.Add(attributeInstance.gameObject);
|
|
OnPropertyAdded?.Invoke(attributeInstance.gameObject);
|
|
return attributeInstance;
|
|
}
|
|
|
|
public CurveField AddCurveField()
|
|
{
|
|
var curveInstance = Instantiate(CurveField, Content ?? transform);
|
|
Properties.Add(curveInstance.gameObject);
|
|
return curveInstance;
|
|
}
|
|
|
|
public virtual PropertyButton AddButton(string name, Action onClick)
|
|
{
|
|
var button = Instantiate(PropertyButton, Content ?? transform);
|
|
button.Label.text = name;
|
|
button.Button.interactable = onClick != null;
|
|
button.Button.onClick.AddListener(() => onClick());
|
|
Properties.Add(button.gameObject);
|
|
OnPropertyAdded?.Invoke(button.gameObject);
|
|
return button;
|
|
}
|
|
|
|
public void AddButton(string name, string label, Action onClick)
|
|
{
|
|
var button = Instantiate(ButtonField, Content ?? transform);
|
|
button.Label.text = name;
|
|
button.ButtonLabel.text = label;
|
|
button.Button.interactable = onClick != null;
|
|
button.Button.onClick.AddListener(() => onClick());
|
|
Properties.Add(button.gameObject);
|
|
OnPropertyAdded?.Invoke(button.gameObject);
|
|
}
|
|
|
|
public void AddField(string name, Func<string> read, Action<string> write)
|
|
{
|
|
var field = Instantiate(InputField, Content ?? transform);
|
|
field.Label.text = name;
|
|
field.Field.contentType = TMP_InputField.ContentType.Standard;
|
|
field.Field.onValueChanged.AddListener(val => write(val));
|
|
// field.Field.text = read();
|
|
RefreshPropertyValues += () =>
|
|
{
|
|
var s = read();
|
|
if (field.Field.text != s)
|
|
{
|
|
field.Field.text = s;
|
|
field.gameObject.SetActive(false);
|
|
Observable.NextFrame().Subscribe(_ =>
|
|
{
|
|
if(field != null)
|
|
OnPropertyAdded?.Invoke(field.gameObject);
|
|
});
|
|
}
|
|
};
|
|
Properties.Add(field.gameObject);
|
|
OnPropertyAdded?.Invoke(field.gameObject);
|
|
}
|
|
|
|
public void AddField(string name, Func<float> read, Action<float> write)
|
|
{
|
|
var field = Instantiate(InputField, Content ?? transform);
|
|
field.Label.text = name;
|
|
field.Field.contentType = TMP_InputField.ContentType.DecimalNumber;
|
|
field.Field.onValueChanged.AddListener(val => write(float.Parse(val)));
|
|
RefreshPropertyValues += () =>
|
|
{
|
|
var s = read().ToString(CultureInfo.InvariantCulture);
|
|
if (field.Field.text != s)
|
|
{
|
|
field.Field.text = s;
|
|
field.gameObject.SetActive(false);
|
|
Observable.NextFrame().Subscribe(_ => OnPropertyAdded?.Invoke(field.gameObject));
|
|
}
|
|
};
|
|
Properties.Add(field.gameObject);
|
|
OnPropertyAdded?.Invoke(field.gameObject);
|
|
}
|
|
|
|
public void AddField(string name, Func<int> read, Action<int> write)
|
|
{
|
|
var field = Instantiate(InputField, Content ?? transform);
|
|
field.Label.text = name;
|
|
field.Field.contentType = TMP_InputField.ContentType.IntegerNumber;
|
|
field.Field.onValueChanged.AddListener(val => write(int.Parse(val)));
|
|
RefreshPropertyValues += () =>
|
|
{
|
|
var s = read().ToString(CultureInfo.InvariantCulture);
|
|
if (field.Field.text != s)
|
|
{
|
|
field.Field.text = s;
|
|
field.gameObject.SetActive(false);
|
|
Observable.NextFrame().Subscribe(_ => OnPropertyAdded?.Invoke(field.gameObject));
|
|
}
|
|
};
|
|
Properties.Add(field.gameObject);
|
|
OnPropertyAdded?.Invoke(field.gameObject);
|
|
}
|
|
|
|
public void AddIncrementField(string name, Func<int> read, Action<int> write, Func<int> min, Func<int> max)
|
|
{
|
|
var field = Instantiate(IncrementField, Content ?? transform);
|
|
field.Label.text = name;
|
|
field.Increment.onClick.AddListener(() => write(read() + 1));
|
|
field.Decrement.onClick.AddListener(() => write(read() - 1));
|
|
RefreshPropertyValues += () =>
|
|
{
|
|
var val = read();
|
|
var minval = min();
|
|
var maxval = max();
|
|
if (val < minval || val > maxval)
|
|
write(val = clamp(val, minval, maxval));
|
|
field.Value.text = val.ToString(CultureInfo.InvariantCulture);
|
|
field.Increment.interactable = val == maxval;
|
|
field.Decrement.interactable = val == minval;
|
|
};
|
|
Properties.Add(field.gameObject);
|
|
OnPropertyAdded?.Invoke(field.gameObject);
|
|
}
|
|
|
|
public void AddField(string name, Func<float> read, Action<float> write, float min, float max)
|
|
{
|
|
var field = Instantiate(RangedFloatField, Content ?? transform);
|
|
field.Label.text = name;
|
|
field.Slider.wholeNumbers = false;
|
|
field.Slider.minValue = min;
|
|
field.Slider.maxValue = max;
|
|
field.Slider.onValueChanged.AddListener(val => write(val));
|
|
RefreshPropertyValues += () => field.Slider.value = read();
|
|
Properties.Add(field.gameObject);
|
|
OnPropertyAdded?.Invoke(field.gameObject);
|
|
}
|
|
|
|
public void AddProgressField(string name, Func<float> read)
|
|
{
|
|
var field = Instantiate(ProgressField, Content ?? transform);
|
|
field.Label.text = name;
|
|
field.Slider.wholeNumbers = false;
|
|
field.Slider.minValue = 0;
|
|
field.Slider.maxValue = 1;
|
|
//field.Slider.onValueChanged.AddListener(val => write(val));
|
|
RefreshPropertyValues += () => field.Slider.value = read();
|
|
Properties.Add(field.gameObject);
|
|
OnPropertyAdded?.Invoke(field.gameObject);
|
|
}
|
|
|
|
public void AddField(string name, Func<int> read, Action<int> write, int min, int max)
|
|
{
|
|
var field = Instantiate(RangedFloatField, Content ?? transform);
|
|
field.Label.text = name;
|
|
field.Slider.wholeNumbers = true;
|
|
field.Slider.minValue = min;
|
|
field.Slider.maxValue = max;
|
|
field.Slider.onValueChanged.AddListener(val => write(Mathf.RoundToInt(val)));
|
|
RefreshPropertyValues += () => field.Slider.value = read();
|
|
Properties.Add(field.gameObject);
|
|
OnPropertyAdded?.Invoke(field.gameObject);
|
|
}
|
|
|
|
public void AddField(string name, Func<bool> read, Action<bool> write)
|
|
{
|
|
var field = Instantiate(BoolField, Content ?? transform);
|
|
field.Label.text = name;
|
|
field.Toggle.onValueChanged.AddListener(val => write(val));
|
|
RefreshPropertyValues += () => field.Toggle.isOn = read();
|
|
Properties.Add(field.gameObject);
|
|
OnPropertyAdded?.Invoke(field.gameObject);
|
|
}
|
|
|
|
public void AddField(string name, Func<int> read, Action<int> write, string[] enumOptions)
|
|
{
|
|
var field = Instantiate(EnumField, Content ?? transform);
|
|
field.Label.text = name;
|
|
field.Dropdown.onClick.AddListener(() =>
|
|
{
|
|
var selected = read();
|
|
Dropdown.gameObject.SetActive(true);
|
|
Dropdown.Clear();
|
|
for (int i = 0; i < enumOptions.Length; i++)
|
|
{
|
|
var index = i;
|
|
Dropdown.AddOption(enumOptions[i], () => write(index), index == selected);
|
|
}
|
|
|
|
Dropdown.Show((RectTransform) field.Dropdown.transform);
|
|
});
|
|
RefreshPropertyValues += () => field.DropdownLabel.text = enumOptions[read()];
|
|
Properties.Add(field.gameObject);
|
|
OnPropertyAdded?.Invoke(field.gameObject);
|
|
}
|
|
|
|
private void AddItemProperties(ItemInstance item)
|
|
{
|
|
var data = GameManager.ItemManager.GetData(item);
|
|
|
|
AddProperty(data.Description);
|
|
|
|
if (item is SimpleCommodity simpleCommodity)
|
|
AddProperty("Quantity", () => simpleCommodity.Quantity.ToString());
|
|
|
|
var sheet = AddStatSheet();
|
|
// Branding is derived from provenance, not stored on the design; a SimpleCommodity carries no lot and shows none.
|
|
if (item is CraftedItemInstance crafted)
|
|
{
|
|
var (maker, product) = GameManager.ItemManager.Brand(crafted);
|
|
if (maker != null)
|
|
sheet.AddStat("Manufacturer", () => maker.Name);
|
|
if (product != null)
|
|
{
|
|
AddProperty(product.Name);
|
|
AddProperty(product.Description);
|
|
}
|
|
}
|
|
sheet.AddStat("Mass", () => ActionGameManager.PlayerSettings.Format(GameManager.ItemManager.GetMass(item)));
|
|
|
|
//AddProperty("Thermal Mass", () => Context.GetThermalMass(item).SignificantDigits(Context.GameplaySettings.SignificantDigits));
|
|
}
|
|
|
|
private void AddEquippableItemProperties(EquippableItem item, Func<PerformanceStat, float> statValueFunction)
|
|
{
|
|
if (item.Durability < .01f)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var gearData = GameManager.ItemManager.GetData(item);
|
|
|
|
var sheet = AddStatSheet();
|
|
foreach (var behavior in gearData.Behaviors)
|
|
{
|
|
if (behavior is StatModifierData statMod)
|
|
{
|
|
sheet.AddStat($"{statMod.Stat.Target.SplitCamelCase()}:{statMod.Stat.Stat.SplitCamelCase()}", () => $"{(statMod.Type == StatModifierType.Constant ? "+" : "x")}{ActionGameManager.PlayerSettings.Format(statValueFunction(statMod.Modifier))}");
|
|
}
|
|
else
|
|
{
|
|
var type = behavior.GetType();
|
|
if (type.GetCustomAttribute(typeof(RuntimeInspectable)) == null) continue;
|
|
foreach (var field in type.GetFields().Where(f => f.GetCustomAttribute<RuntimeInspectable>() != null))
|
|
{
|
|
var fieldType = field.FieldType;
|
|
if (fieldType == typeof(float))
|
|
sheet.AddStat(field.Name.SplitCamelCase(), () => $"{ActionGameManager.PlayerSettings.Format((float) field.GetValue(behavior))}");
|
|
else if (fieldType == typeof(int))
|
|
sheet.AddStat(field.Name.SplitCamelCase(), () => $"{(int) field.GetValue(behavior)}");
|
|
else if (fieldType == typeof(PerformanceStat))
|
|
{
|
|
var stat = (PerformanceStat) field.GetValue(behavior);
|
|
sheet.AddStat(field.Name.SplitCamelCase(), () => $"{ActionGameManager.PlayerSettings.Format(statValueFunction(stat))}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (gearData.Behaviors.FirstOrDefault(b => b is WeaponData) is WeaponData weapon)
|
|
{
|
|
var range = AddCurveField();
|
|
range.Show("Damage Range", weapon.DamageCurve, t => ActionGameManager.PlayerSettings.Format(lerp(statValueFunction(weapon.MinRange), statValueFunction(weapon.Range), t)));
|
|
}
|
|
}
|
|
|
|
private string GetTitle(EquippableItem item)
|
|
{
|
|
var data = GameManager.ItemManager.GetData(item);
|
|
var (tier, upgrades) = GameManager.ItemManager.GetTier(item);
|
|
return
|
|
$"<color=#{ColorUtility.ToHtmlStringRGB(tier.Color.ToColor())}>{data.Name}</color><smallcaps><size=60%> ({tier.Name}{new string('+', upgrades)})";
|
|
}
|
|
|
|
public void Inspect(EquippedItem item)
|
|
{
|
|
Clear();
|
|
if (item?.EquippableItem == null) return;
|
|
|
|
Title.text = GetTitle(item.EquippableItem);
|
|
|
|
AddItemProperties(item.EquippableItem);
|
|
AddSpacer();
|
|
|
|
if (item.GetBehavior<Weapon>() != null)
|
|
{
|
|
var weaponGroups = Instantiate(WeaponGroupAssignment, Content ?? transform);
|
|
weaponGroups.Inspect(item);
|
|
var dragOffset = Vector2.zero;
|
|
Transform dragObject = null;
|
|
weaponGroups.OnBeginDragAsObservable().Subscribe(x =>
|
|
{
|
|
//Debug.Log($"Began dragging weapon group {x.group}");
|
|
GameManager.BeginDrag(new WeaponGroupDragObject(x.group));
|
|
dragObject = Instantiate(weaponGroups.Groups[x.group], DragParent, true).transform;
|
|
dragOffset = (Vector2)dragObject.position - x.pointerEventData.position;
|
|
});
|
|
weaponGroups.OnDragAsObservable().Subscribe(x =>
|
|
{
|
|
dragObject.position = x.pointerEventData.position + dragOffset;
|
|
});
|
|
weaponGroups.OnEndDragAsObservable().Subscribe(x =>
|
|
{
|
|
//Debug.Log($"Ended dragging weapon group {x.group}");
|
|
GameManager.EndDrag();
|
|
Destroy(dragObject.gameObject);
|
|
});
|
|
Properties.Add(weaponGroups.gameObject);
|
|
OnPropertyAdded?.Invoke(weaponGroups.gameObject);
|
|
}
|
|
|
|
var gearData = GameManager.ItemManager.GetData(item.EquippableItem);
|
|
var statusSheet = AddStatSheet();
|
|
if (item.EquippableItem.Durability < .01f)
|
|
statusSheet.AddStat("Durability", () => "Item Destroyed!");
|
|
else statusSheet.AddStat("Durability", () => $"{(int)(item.EquippableItem.Durability / gearData.Durability * 100)}%");
|
|
statusSheet.AddStat("Temperature", () => ActionGameManager.PlayerSettings.FormatTemperature(item.Temperature));
|
|
// R-heat (docs/stats-and-power-cut.md): the authored shape is min/max/optimum/plateau width, not a
|
|
// curve, so the thermal performance panel reads those directly rather than sampling a BezierCurve.
|
|
// Cut 8 owns the real UI reconciliation pass; this keeps the panel legible and compiling in the interim.
|
|
statusSheet.AddStat("Optimal Temp", () => ActionGameManager.PlayerSettings.FormatTemperature(gearData.OptimalTemperature));
|
|
statusSheet.AddStat("Thermal Performance", () => $"{(int)(gearData.Performance(item.Temperature) * 100)}%");
|
|
AddEquippableItemProperties(item.EquippableItem, item.Evaluate);
|
|
AddSpacer();
|
|
|
|
AddField("Override Shutdown", () => item.EquippableItem.OverrideShutdown, b => item.EquippableItem.OverrideShutdown = b);
|
|
|
|
foreach (var behavior in item.Behaviors)
|
|
{
|
|
switch (behavior)
|
|
{
|
|
case Thermotoggle thermotoggle when thermotoggle.ThermotoggleData.Adjustable:
|
|
AddField("Target Temperature",
|
|
() => thermotoggle.TargetTemperature,
|
|
temp => thermotoggle.TargetTemperature = temp);
|
|
break;
|
|
}
|
|
}
|
|
|
|
RefreshValues();
|
|
}
|
|
|
|
public void Inspect(ItemInstance item)
|
|
{
|
|
Clear();
|
|
|
|
AddItemProperties(item);
|
|
|
|
if (item is EquippableItem gear)
|
|
{
|
|
Title.text = GetTitle(gear);
|
|
AddSpacer();
|
|
var gearData = GameManager.ItemManager.GetData(gear);
|
|
var statusSheet = AddStatSheet();
|
|
statusSheet.AddStat("Durability", () => $"{(int)(gear.Durability / gearData.Durability * 100)}%");
|
|
// R-heat: see the equipped Inspect() overload above for why this is stats, not a curve field.
|
|
statusSheet.AddStat("Optimal Temp", () => ActionGameManager.PlayerSettings.FormatTemperature(gearData.OptimalTemperature));
|
|
AddEquippableItemProperties(gear, stat => GameManager.ItemManager.Evaluate(stat, gear));
|
|
}
|
|
|
|
RefreshValues();
|
|
}
|
|
|
|
public void Inspect(ItemData data)
|
|
{
|
|
AddProperty("Type", () => data.Name);
|
|
AddProperty(data.Description).Label.fontStyle = FontStyles.Normal;
|
|
if (data is EquippableItemData gearData)
|
|
{
|
|
AddProperty("Durability", () => ActionGameManager.PlayerSettings.Format(gearData.Durability));
|
|
var sheet = AddStatSheet();
|
|
foreach (var behavior in gearData.Behaviors)
|
|
{
|
|
if (behavior is StatModifierData statMod)
|
|
{
|
|
if(Math.Abs(statMod.Modifier.Min - statMod.Modifier.Max) < .001f)
|
|
sheet.AddStat($"{statMod.Stat.Target}:{statMod.Stat.Stat}", () => $"{(statMod.Type == StatModifierType.Constant ? "+" : "x")}{ActionGameManager.PlayerSettings.Format(statMod.Modifier.Min)}");
|
|
else
|
|
sheet.AddStat($"{statMod.Stat.Target}:{statMod.Stat.Stat}", () => $"{(statMod.Type == StatModifierType.Constant ? "+" : "x")}{ActionGameManager.PlayerSettings.Format(statMod.Modifier.Min)}-{ActionGameManager.PlayerSettings.Format(statMod.Modifier.Max)}");
|
|
}
|
|
var type = behavior.GetType();
|
|
if (type.GetCustomAttribute(typeof(RuntimeInspectable)) != null)
|
|
{
|
|
foreach (var field in type.GetFields().Where(f => f.GetCustomAttribute<RuntimeInspectable>() != null))
|
|
{
|
|
var fieldType = field.FieldType;
|
|
if (fieldType == typeof(float))
|
|
sheet.AddStat(field.Name, () => $"{ActionGameManager.PlayerSettings.Format((float) field.GetValue(behavior))}");
|
|
else if (fieldType == typeof(int))
|
|
sheet.AddStat(field.Name, () => $"{(int) field.GetValue(behavior)}");
|
|
else if (fieldType == typeof(PerformanceStat))
|
|
{
|
|
var stat = (PerformanceStat) field.GetValue(behavior);
|
|
if(Math.Abs(stat.Min - stat.Max) < .001f)
|
|
sheet.AddStat(field.Name, () => $"{ActionGameManager.PlayerSettings.Format(stat.Min)}");
|
|
else
|
|
sheet.AddStat(field.Name, () => $"{ActionGameManager.PlayerSettings.Format(stat.Min)}-{ActionGameManager.PlayerSettings.Format(stat.Max)}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Inspect(object obj, bool inspectablesOnly = false, bool readWrite = false, bool topLevel = true)
|
|
{
|
|
var fields = obj.GetType().GetFields();
|
|
foreach (var field in fields)
|
|
Inspect(obj, field, inspectablesOnly, readWrite);
|
|
|
|
// if(topLevel)
|
|
// foreach (var field in _propertyFields)
|
|
// field.transform.SetSiblingIndex(_propertyFields.IndexOf(field));
|
|
|
|
RefreshValues();
|
|
}
|
|
|
|
public void Inspect(object obj, FieldInfo field, bool inspectablesOnly = false, bool readWrite = false)
|
|
{
|
|
var inspectable = field.GetCustomAttribute<InspectableAttribute>();
|
|
if (inspectable == null && inspectablesOnly) return;
|
|
var type = field.FieldType;
|
|
|
|
if (type == typeof(float))
|
|
{
|
|
if (readWrite)
|
|
{
|
|
if (field.GetCustomAttribute<CultInspectorRangeAttribute>() is { } ranged)
|
|
AddField(field.Name.SplitCamelCase(), () => (float) field.GetValue(obj), f => field.SetValue(obj, f),
|
|
ranged.Min, ranged.Max);
|
|
else
|
|
AddField(field.Name.SplitCamelCase(), () => (float) field.GetValue(obj), f => field.SetValue(obj, f));
|
|
} else AddProperty(field.Name.SplitCamelCase(), () => ((float) field.GetValue(obj)).ToString("0.##"));
|
|
}
|
|
else if (type == typeof(int))
|
|
{
|
|
if (readWrite)
|
|
{
|
|
if (field.GetCustomAttribute<CultInspectorRangeAttribute>() is { } ranged)
|
|
AddField(field.Name.SplitCamelCase(), () => (int) field.GetValue(obj), f => field.SetValue(obj, f),
|
|
(int) ranged.Min, (int) ranged.Max);
|
|
else
|
|
AddField(field.Name.SplitCamelCase(), () => (int) field.GetValue(obj), f => field.SetValue(obj, f));
|
|
} else AddProperty(field.Name.SplitCamelCase(), () => ((int) field.GetValue(obj)).ToString());
|
|
}
|
|
else if (type.IsEnum)
|
|
{
|
|
if (readWrite)
|
|
AddField(field.Name.SplitCamelCase(), () => (int) field.GetValue(obj), i => field.SetValue(obj, i),
|
|
Enum.GetNames(field.FieldType));
|
|
else
|
|
AddProperty(field.Name.SplitCamelCase(), () => Enum.GetName(type, field.GetValue(obj)).SplitCamelCase());
|
|
}
|
|
else if (type == typeof(string))
|
|
{
|
|
if (readWrite)
|
|
AddField(field.Name.SplitCamelCase(), () => (string) field.GetValue(obj), i => field.SetValue(obj, i));
|
|
else
|
|
AddProperty(field.Name.SplitCamelCase(), () => (string) field.GetValue(obj));
|
|
}
|
|
//else if (field.FieldType == typeof(Color)) Inspect(field.Name, () => (Color) field.GetValue(obj), c => field.SetValue(obj, c));
|
|
else if (field.FieldType == typeof(bool)) AddField(field.Name, () => (bool) field.GetValue(obj), b => field.SetValue(obj, b));
|
|
else if (type.GetCustomAttribute<InspectableAttribute>() != null)
|
|
{
|
|
// if(!_propertyFields.Any() || !_propertyFields.Last().gameObject.name.ToUpper().Contains("DIVIDER"))
|
|
// _propertyFields.Add(Divider.Instantiate<Prototype>());
|
|
var list = AddList(field.Name);
|
|
list.Inspect(field.GetValue(obj), inspectablesOnly, readWrite, false);
|
|
}
|
|
else
|
|
Debug.Log($"Field \"{field.Name}\" has unknown type {field.FieldType.Name}. No inspector was generated.");
|
|
}
|
|
|
|
public void RefreshValues()
|
|
{
|
|
RefreshPropertyValues?.Invoke();
|
|
}
|
|
}
|