Files
bansonic_beta_main/Assets/_eqpmtSys/BansonicEquipmentGenerator.cs
2026-07-16 23:04:59 +08:00

856 lines
41 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace Bansonic
{
public static class equipmentGenerator
{
private const string EditorOutputFolder = "Assets/Resources/so/uEquip";
private const string RuntimeOutputFolder = "so/uEquip";
private const string NextGeneratedIdPrefsKey = "bansonic_equipment_next_id_v1";
private const string RuntimeSaveCategory = "runtime_equipment";
private const string RuntimeSaveKey = "generated_list";
private const string RuntimeRecoverySlotKey = "runtime_generated_equipment_list";
private static readonly List<equipmentSO> RuntimeGeneratedEquipments = new List<equipmentSO>();
private static bool runtimeGeneratedLoaded;
private static bool runtimeGeneratedLoadedFromSave;
private static bool runtimeGeneratedHasRecoverableState;
private enum QualityRollMode { Default, High }
private class GeneratedEffectValue
{
public equipmentSO.EquipmentSpecialEffectType effectType;
public float value;
public int skillId;
}
[Serializable]
private class RuntimeGeneratedEquipmentListPayload
{
public List<RuntimeGeneratedEquipmentPayload> items = new List<RuntimeGeneratedEquipmentPayload>();
}
[Serializable]
private class RuntimeGeneratedEquipmentPayload
{
public string name;
public int skillType;
public int level;
public string tierNameConfigName;
public bool enableTypeSameEffects;
public int saSkillId;
public int iaSkillId;
public StatPayload maxHp;
public StatPayload attack;
public StatPayload maxMana;
public StatPayload damageResistance;
public StatPayload scoreEfficiency;
public List<EffectPayload> specialEffects = new List<EffectPayload>();
public List<EffectPayload> typeSameEffects = new List<EffectPayload>();
public List<EffectPayload> illusionEffects = new List<EffectPayload>();
public List<EffectPayload> maxLevelEffects = new List<EffectPayload>();
}
[Serializable]
private class StatPayload
{
public float basicGain;
public float cultivationInterval;
}
[Serializable]
private class EffectPayload
{
public int effectType;
public float value;
}
public static IReadOnlyList<equipmentSO> GetRuntimeGeneratedEquipments()
{
EnsureRuntimeGeneratedLoaded();
return RuntimeGeneratedEquipments;
}
public static equipmentSO RegisterGeneratedEquipment(equipmentSO generated)
{
return Persist(generated);
}
public static string GenerateUniqueEquipmentName(equipmentSO.EquipmentSkillType type)
{
return BuildGeneratedName(type);
}
public static void RemoveRuntimeGeneratedEquipment(equipmentSO equipment)
{
EnsureRuntimeGeneratedLoaded();
if (equipment == null)
{
return;
}
RuntimeGeneratedEquipments.RemoveAll(item => item == null || item == equipment || item.name == equipment.name);
SaveRuntimeGeneratedState();
}
public static void SaveRuntimeEquipmentState(equipmentSO equipment)
{
EnsureRuntimeGeneratedLoaded();
if (equipment == null)
{
return;
}
RuntimeGeneratedEquipments.RemoveAll(item => item == null || item == equipment || item.name == equipment.name);
RuntimeGeneratedEquipments.Add(equipment);
SaveRuntimeGeneratedState();
}
public static void ClearRuntimeGeneratedPersistence()
{
RuntimeGeneratedEquipments.Clear();
runtimeGeneratedLoaded = true;
runtimeGeneratedLoadedFromSave = false;
runtimeGeneratedHasRecoverableState = false;
SecureSaveVault.Delete(RuntimeSaveCategory, RuntimeSaveKey);
LocalRecoveryMirror.DeleteSlot(RuntimeRecoverySlotKey);
}
public static List<equipmentSO> generateEquipment(
int quantity,
int level,
equipmentSO.EquipmentSkillType type,
float maxHp,
float attack,
float maxMana,
float damageResistance,
float scoreEfficiency,
equipmentSO.EquipmentSpecialEffect[] specialEffects,
equipmentSO.EquipmentSpecialEffect[] typeSameEffects,
equipmentSO.EquipmentSpecialEffect[] illusionEffects,
equipmentSO.EquipmentSpecialEffect[] maxLevelEffects,
int carriedSkillId,
equipmentSO template = null)
{
var results = new List<equipmentSO>(Mathf.Max(0, quantity));
for (int i = 0; i < Mathf.Max(0, quantity); i++)
{
equipmentSO generated = CreateBaseEquipment(template);
generated.name = BuildGeneratedName(type);
generated.skillType = type;
generated.level = Mathf.Max(0, level);
generated.maxHp.basicGain = maxHp;
generated.attack.basicGain = attack;
generated.maxMana.basicGain = maxMana;
generated.damageResistance.basicGain = damageResistance;
generated.scoreEfficiency.basicGain = scoreEfficiency;
generated.specialEffects = CloneEffects(specialEffects);
generated.typeSameEffects = CloneEffects(typeSameEffects);
generated.illusionEffects = CloneEffects(illusionEffects);
generated.maxLevelEffects = CloneEffects(maxLevelEffects);
generated.sa_skillID = Mathf.Max(0, carriedSkillId);
generated.ia_skillID = 0;
results.Add(Persist(generated));
}
return results;
}
public static equipmentSO GenerateFromSmeltReward(
smeltStageRewardSO rewardSo,
smeltStageRewardSO.SmeltStageRewardRequirement requirement,
equipmentSO.EquipmentSkillType? forcedType = null,
int forcedSkillGroupId = 0)
{
if (rewardSo == null || requirement == null || rewardSo.rewardRandomConfig == null)
{
return null;
}
equipmentRandomConfigSO config = rewardSo.rewardRandomConfig;
config.NormalizeRuntimeData();
equipmentSO generated = CreateBaseEquipment(rewardSo.rewardTemplate);
generated.skillType = forcedType ?? (requirement.equipmentType == smeltStageRewardSO.equipType.selfChosenType
? requirement.chosenSkillType
: RollRandomSkillType());
generated.name = BuildGeneratedName(generated.skillType);
generated.level = 0;
generated.sa_skillID = 0;
generated.ia_skillID = 0;
QualityRollMode mode = requirement.qualityType == smeltStageRewardSO.attributeQuality.highQuality
? QualityRollMode.High
: QualityRollMode.Default;
ResetBasicGains(generated);
GenerateBasicAttributes(generated, config, mode);
GenerateTypeSameEffects(generated, config, mode);
GenerateSpecialEffects(generated, config, mode, requirement.skillRequirement == smeltStageRewardSO.skillOwned.mustHave, forcedSkillGroupId);
GenerateIllusionEffects(generated, config, mode);
return Persist(generated);
}
private static equipmentSO CreateBaseEquipment(equipmentSO template)
{
equipmentSO generated = template != null ? UnityEngine.Object.Instantiate(template) : ScriptableObject.CreateInstance<equipmentSO>();
generated.hideFlags = HideFlags.None;
generated.maxHp ??= new equipmentSO.EquipmentStatTuning();
generated.attack ??= new equipmentSO.EquipmentStatTuning();
generated.maxMana ??= new equipmentSO.EquipmentStatTuning();
generated.damageResistance ??= new equipmentSO.EquipmentStatTuning();
generated.scoreEfficiency ??= new equipmentSO.EquipmentStatTuning();
generated.specialEffects ??= Array.Empty<equipmentSO.EquipmentSpecialEffect>();
generated.typeSameEffects ??= Array.Empty<equipmentSO.EquipmentSpecialEffect>();
generated.illusionEffects ??= Array.Empty<equipmentSO.EquipmentSpecialEffect>();
generated.maxLevelEffects ??= Array.Empty<equipmentSO.EquipmentSpecialEffect>();
return generated;
}
private static equipmentSO.EquipmentSkillType RollRandomSkillType()
{
Array values = Enum.GetValues(typeof(equipmentSO.EquipmentSkillType));
return (equipmentSO.EquipmentSkillType)values.GetValue(UnityEngine.Random.Range(0, values.Length));
}
private static void GenerateBasicAttributes(equipmentSO generated, equipmentRandomConfigSO config, QualityRollMode mode)
{
var candidates = new List<(equipmentSO.EquipmentStatTuning tuning, equipmentRandomConfigSO.RandomBasicAttributeType rangeType)>
{
(generated.maxHp, equipmentRandomConfigSO.RandomBasicAttributeType.MaxHp),
(generated.attack, equipmentRandomConfigSO.RandomBasicAttributeType.Attack),
(generated.maxMana, equipmentRandomConfigSO.RandomBasicAttributeType.MaxMana),
(generated.damageResistance, equipmentRandomConfigSO.RandomBasicAttributeType.DamageResistance),
(generated.scoreEfficiency, equipmentRandomConfigSO.RandomBasicAttributeType.ScoreEfficiency)
};
candidates.RemoveAll(e => e.tuning == null || !config.TryGetBasicRange(e.rangeType, out _, out _));
int count = Mathf.Clamp(config.randomBasicAttributeCount, 0, candidates.Count);
for (int i = 0; i < count; i++)
{
int pick = UnityEngine.Random.Range(0, candidates.Count);
var item = candidates[pick];
candidates.RemoveAt(pick);
config.TryGetBasicRange(item.rangeType, out float min, out float max);
item.tuning.basicGain = RollConfiguredValue(min, max, mode);
}
}
private static void GenerateTypeSameEffects(equipmentSO generated, equipmentRandomConfigSO config, QualityRollMode mode)
{
var selected = GetSelectedBasicAttributeTypes(generated);
var results = new List<equipmentSO.EquipmentSpecialEffect>();
foreach (var effectType in selected)
{
if (!TryMapToSpecialRange(effectType, out equipmentRandomConfigSO.RandomSpecialEffectType rangeType)) continue;
if (!config.TryGetTypeSameEffectRange(rangeType, out float min, out float max)) continue;
results.Add(new equipmentSO.EquipmentSpecialEffect { effectType = effectType, value = RollConfiguredValue(min, max, mode) });
}
generated.typeSameEffects = results.ToArray();
}
private static void GenerateSpecialEffects(equipmentSO generated, equipmentRandomConfigSO config, QualityRollMode mode, bool mustHaveSkill, int forcedSkillGroupId = 0)
{
int count = RollSpecialEffectCount(config);
var pool = BuildSpecialEffectPool(generated, config);
var generatedEffects = new List<GeneratedEffectValue>();
if (pool.Count == 0 || count <= 0)
{
generated.specialEffects = Array.Empty<equipmentSO.EquipmentSpecialEffect>();
generated.sa_skillID = 0;
return;
}
for (int i = 0; i < count; i++)
{
var effectType = pool[UnityEngine.Random.Range(0, pool.Count)];
if (!TryMapToSpecialRange(effectType, out equipmentRandomConfigSO.RandomSpecialEffectType rangeType)) continue;
if (!config.TryGetSpecialRange(rangeType, out float min, out float max)) continue;
generatedEffects.Add(new GeneratedEffectValue { effectType = effectType, value = RollConfiguredValue(min, max, mode) });
}
ResolveDuplicateEffects(generatedEffects);
bool hasForcedSkill = forcedSkillGroupId > 0;
bool shouldGenerateSkill = mustHaveSkill || UnityEngine.Random.value <= config.sesp;
if ((hasForcedSkill || shouldGenerateSkill) && generatedEffects.Count > 0)
{
int skillId = 0;
if (hasForcedSkill)
{
skillId = forcedSkillGroupId;
}
else if (!config.TryGetSpecialSkillId(generated.skillType, out skillId) || skillId <= 0)
{
generated.sa_skillID = 0;
generated.specialEffects = BuildGeneratedEffects(generatedEffects);
return;
}
int replace = UnityEngine.Random.Range(0, generatedEffects.Count);
generatedEffects[replace] = new GeneratedEffectValue { effectType = equipmentSO.EquipmentSpecialEffectType.Skill, value = 0f, skillId = skillId };
generated.sa_skillID = skillId;
}
else
{
generated.sa_skillID = 0;
}
generated.specialEffects = BuildGeneratedEffects(generatedEffects);
}
private static void GenerateIllusionEffects(equipmentSO generated, equipmentRandomConfigSO config, QualityRollMode mode)
{
if (generated.illusionEffects == null || generated.illusionEffects.Length == 0)
{
generated.ia_skillID = 0;
return;
}
int count = Mathf.Min(RollSpecialEffectCount(config), generated.illusionEffects.Length);
bool hasTemplateSkill = false;
var valuePool = new List<equipmentSO.EquipmentSpecialEffect>();
foreach (var effect in generated.illusionEffects)
{
if (effect == null) continue;
if (effect.effectType == equipmentSO.EquipmentSpecialEffectType.Skill) { hasTemplateSkill = true; continue; }
if (TryMapToIllusionRange(effect.effectType, out _)) valuePool.Add(effect);
}
var results = new List<equipmentSO.EquipmentSpecialEffect>();
generated.ia_skillID = 0;
for (int i = 0; i < count; i++)
{
bool shouldSkill = hasTemplateSkill && UnityEngine.Random.value <= GetIllusionSkillChance(config, i);
if (shouldSkill && config.TryGetIllusionSkillId(out int illusionSkillId) && illusionSkillId > 0)
{
results.Add(new equipmentSO.EquipmentSpecialEffect { effectType = equipmentSO.EquipmentSpecialEffectType.Skill, value = 0f });
generated.ia_skillID = illusionSkillId;
hasTemplateSkill = false;
continue;
}
if (valuePool.Count == 0) break;
int pick = UnityEngine.Random.Range(0, valuePool.Count);
var effect = valuePool[pick];
valuePool.RemoveAt(pick);
if (!TryMapToIllusionRange(effect.effectType, out equipmentRandomConfigSO.RandomIllusionEffectType rangeType)) { i--; continue; }
if (!config.TryGetIllusionRange(rangeType, out float min, out float max)) { i--; continue; }
results.Add(new equipmentSO.EquipmentSpecialEffect { effectType = effect.effectType, value = RollConfiguredValue(min, max, mode) });
}
generated.illusionEffects = results.ToArray();
}
private static List<equipmentSO.EquipmentSpecialEffectType> BuildSpecialEffectPool(equipmentSO generated, equipmentRandomConfigSO config)
{
var pool = new List<equipmentSO.EquipmentSpecialEffectType>();
var selected = GetSelectedBasicAttributeTypes(generated);
equipmentSO.EquipmentSpecialEffectType[] allTypes =
{
equipmentSO.EquipmentSpecialEffectType.MaxHp,
equipmentSO.EquipmentSpecialEffectType.Attack,
equipmentSO.EquipmentSpecialEffectType.MaxMana,
equipmentSO.EquipmentSpecialEffectType.DamageResistance,
equipmentSO.EquipmentSpecialEffectType.ScoreEfficiency
};
if (generated.enableTypeSameEffects && selected.Count > 0)
{
foreach (var effectType in selected)
{
if (TryMapToSpecialRange(effectType, out equipmentRandomConfigSO.RandomSpecialEffectType rangeType) && config.TryGetSpecialRange(rangeType, out _, out _))
pool.Add(effectType);
}
return pool;
}
for (int i = 0; i < allTypes.Length; i++)
{
if (TryMapToSpecialRange(allTypes[i], out equipmentRandomConfigSO.RandomSpecialEffectType rangeType) && config.TryGetSpecialRange(rangeType, out _, out _))
pool.Add(allTypes[i]);
}
return pool;
}
private static HashSet<equipmentSO.EquipmentSpecialEffectType> GetSelectedBasicAttributeTypes(equipmentSO target)
{
var selected = new HashSet<equipmentSO.EquipmentSpecialEffectType>();
if (target.maxHp != null && !Mathf.Approximately(target.maxHp.basicGain, 0f)) selected.Add(equipmentSO.EquipmentSpecialEffectType.MaxHp);
if (target.attack != null && !Mathf.Approximately(target.attack.basicGain, 0f)) selected.Add(equipmentSO.EquipmentSpecialEffectType.Attack);
if (target.maxMana != null && !Mathf.Approximately(target.maxMana.basicGain, 0f)) selected.Add(equipmentSO.EquipmentSpecialEffectType.MaxMana);
if (target.damageResistance != null && !Mathf.Approximately(target.damageResistance.basicGain, 0f)) selected.Add(equipmentSO.EquipmentSpecialEffectType.DamageResistance);
if (target.scoreEfficiency != null && !Mathf.Approximately(target.scoreEfficiency.basicGain, 0f)) selected.Add(equipmentSO.EquipmentSpecialEffectType.ScoreEfficiency);
return selected;
}
private static int RollSpecialEffectCount(equipmentRandomConfigSO config)
{
float roll = UnityEngine.Random.value;
if (roll <= config.se01p) return 1;
if (roll <= config.se01p + config.se02p) return 2;
return 3;
}
private static float GetIllusionSkillChance(equipmentRandomConfigSO config, int slotIndex) => slotIndex switch
{
0 => config.ies01,
1 => config.ies02,
2 => config.ies03,
_ => config.ies04,
};
private static equipmentSO Persist(equipmentSO generated)
{
if (generated == null) return null;
#if UNITY_EDITOR
if (!Application.isPlaying)
{
EnsureEditorOutputFolderExists();
string assetPath = Path.Combine(EditorOutputFolder, generated.name + ".asset").Replace("\\", "/");
AssetDatabase.CreateAsset(generated, assetPath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
equipmentSO persisted = AssetDatabase.LoadAssetAtPath<equipmentSO>(assetPath);
if (persisted != null)
{
RuntimeGeneratedEquipments.RemoveAll(item => item == null || item.name == persisted.name);
RuntimeGeneratedEquipments.Add(persisted);
return persisted;
}
}
#endif
EnsureRuntimeGeneratedLoaded();
RuntimeGeneratedEquipments.RemoveAll(item => item == null || item.name == generated.name);
RuntimeGeneratedEquipments.Add(generated);
SaveRuntimeGeneratedState();
return generated;
}
private static string BuildGeneratedName(equipmentSO.EquipmentSkillType type)
{
int typeIndex = (int)type;
string dateStamp = DateTime.Now.ToString("yyyyMMdd");
int nextId = GetNextGeneratedId();
return $"type{typeIndex}_{dateStamp}_{nextId:D8}";
}
private static int GetNextGeneratedId()
{
EnsureRuntimeGeneratedLoaded();
int maxId = Mathf.Max(0, PlayerPrefs.GetInt(NextGeneratedIdPrefsKey, 0));
for (int i = 0; i < RuntimeGeneratedEquipments.Count; i++)
{
if (RuntimeGeneratedEquipments[i] != null)
{
TryUpdateMaxId(RuntimeGeneratedEquipments[i].name, ref maxId);
}
}
#if UNITY_EDITOR
string absoluteFolder = Path.GetFullPath(EditorOutputFolder);
if (Directory.Exists(absoluteFolder))
{
string[] files = Directory.GetFiles(absoluteFolder, "*.asset", SearchOption.TopDirectoryOnly);
for (int i = 0; i < files.Length; i++)
{
TryUpdateMaxId(Path.GetFileNameWithoutExtension(files[i]), ref maxId);
}
}
#else
equipmentSO[] resources = Resources.LoadAll<equipmentSO>(RuntimeOutputFolder);
for (int i = 0; i < resources.Length; i++)
{
if (resources[i] != null)
{
TryUpdateMaxId(resources[i].name, ref maxId);
}
}
#endif
int nextId = maxId + 1;
PlayerPrefs.SetInt(NextGeneratedIdPrefsKey, nextId);
PlayerPrefs.Save();
return nextId;
}
private static void TryUpdateMaxId(string fileName, ref int maxId)
{
if (string.IsNullOrWhiteSpace(fileName)) return;
int lastUnderscore = fileName.LastIndexOf('_');
if (lastUnderscore < 0 || lastUnderscore >= fileName.Length - 1) return;
string idPart = fileName.Substring(lastUnderscore + 1);
if (int.TryParse(idPart, out int parsedId) && parsedId > maxId)
{
maxId = parsedId;
}
}
private static equipmentSO.EquipmentSpecialEffect[] CloneEffects(equipmentSO.EquipmentSpecialEffect[] source)
{
if (source == null || source.Length == 0) return Array.Empty<equipmentSO.EquipmentSpecialEffect>();
var clone = new equipmentSO.EquipmentSpecialEffect[source.Length];
for (int i = 0; i < source.Length; i++)
{
equipmentSO.EquipmentSpecialEffect effect = source[i];
clone[i] = effect == null ? null : new equipmentSO.EquipmentSpecialEffect { effectType = effect.effectType, value = effect.value };
}
return clone;
}
private static void ResetBasicGains(equipmentSO generated)
{
if (generated.maxHp != null) generated.maxHp.basicGain = 0f;
if (generated.attack != null) generated.attack.basicGain = 0f;
if (generated.maxMana != null) generated.maxMana.basicGain = 0f;
if (generated.damageResistance != null) generated.damageResistance.basicGain = 0f;
if (generated.scoreEfficiency != null) generated.scoreEfficiency.basicGain = 0f;
}
private static bool TryMapToSpecialRange(equipmentSO.EquipmentSpecialEffectType effectType, out equipmentRandomConfigSO.RandomSpecialEffectType mappedType)
{
switch (effectType)
{
case equipmentSO.EquipmentSpecialEffectType.MaxHp: mappedType = equipmentRandomConfigSO.RandomSpecialEffectType.MaxHp; return true;
case equipmentSO.EquipmentSpecialEffectType.Attack: mappedType = equipmentRandomConfigSO.RandomSpecialEffectType.Attack; return true;
case equipmentSO.EquipmentSpecialEffectType.MaxMana: mappedType = equipmentRandomConfigSO.RandomSpecialEffectType.MaxMana; return true;
case equipmentSO.EquipmentSpecialEffectType.DamageResistance: mappedType = equipmentRandomConfigSO.RandomSpecialEffectType.DamageResistance; return true;
case equipmentSO.EquipmentSpecialEffectType.ScoreEfficiency: mappedType = equipmentRandomConfigSO.RandomSpecialEffectType.ScoreEfficiency; return true;
default: mappedType = default; return false;
}
}
private static bool TryMapToIllusionRange(equipmentSO.EquipmentSpecialEffectType effectType, out equipmentRandomConfigSO.RandomIllusionEffectType mappedType)
{
switch (effectType)
{
case equipmentSO.EquipmentSpecialEffectType.MaxHp: mappedType = equipmentRandomConfigSO.RandomIllusionEffectType.MaxHp; return true;
case equipmentSO.EquipmentSpecialEffectType.Attack: mappedType = equipmentRandomConfigSO.RandomIllusionEffectType.Attack; return true;
case equipmentSO.EquipmentSpecialEffectType.MaxMana: mappedType = equipmentRandomConfigSO.RandomIllusionEffectType.MaxMana; return true;
case equipmentSO.EquipmentSpecialEffectType.DamageResistance: mappedType = equipmentRandomConfigSO.RandomIllusionEffectType.DamageResistance; return true;
case equipmentSO.EquipmentSpecialEffectType.ScoreEfficiency: mappedType = equipmentRandomConfigSO.RandomIllusionEffectType.ScoreEfficiency; return true;
case equipmentSO.EquipmentSpecialEffectType.HitManaRecovery: mappedType = equipmentRandomConfigSO.RandomIllusionEffectType.HitManaRecovery; return true;
case equipmentSO.EquipmentSpecialEffectType.HitDamageMultiplier: mappedType = equipmentRandomConfigSO.RandomIllusionEffectType.HitDamageMultiplier; return true;
case equipmentSO.EquipmentSpecialEffectType.HpLoseBase: mappedType = equipmentRandomConfigSO.RandomIllusionEffectType.HpLoseBase; return true;
default: mappedType = default; return false;
}
}
private static equipmentSO.EquipmentSpecialEffect[] BuildGeneratedEffects(List<GeneratedEffectValue> generatedEffects)
{
if (generatedEffects == null || generatedEffects.Count == 0) return Array.Empty<equipmentSO.EquipmentSpecialEffect>();
var results = new List<equipmentSO.EquipmentSpecialEffect>(generatedEffects.Count);
for (int i = 0; i < generatedEffects.Count; i++)
{
GeneratedEffectValue value = generatedEffects[i];
results.Add(new equipmentSO.EquipmentSpecialEffect
{
effectType = value.effectType,
value = value.effectType == equipmentSO.EquipmentSpecialEffectType.Skill ? 0f : value.value
});
}
return results.ToArray();
}
private static float RollConfiguredValue(float minValue, float maxValue, QualityRollMode mode)
{
return mode == QualityRollMode.High ? RollHighQualityValue(minValue, maxValue) : RollBiasedValue(minValue, maxValue);
}
private static float RollHighQualityValue(float minValue, float maxValue)
{
if (Mathf.Approximately(minValue, maxValue)) return AvoidZero(minValue, maxValue, minValue);
if (maxValue < minValue) { float cached = minValue; minValue = maxValue; maxValue = cached; }
float span = maxValue - minValue;
float tail = Mathf.Max(span * 0.2f, 0.0001f);
return AvoidZero(minValue, maxValue, UnityEngine.Random.Range(Mathf.Max(minValue, maxValue - tail), maxValue));
}
private static float RollBiasedValue(float minValue, float maxValue)
{
if (Mathf.Approximately(minValue, maxValue)) return AvoidZero(minValue, maxValue, minValue);
if (maxValue < minValue) { float cached = minValue; minValue = maxValue; maxValue = cached; }
float span = maxValue - minValue;
float roll = UnityEngine.Random.value;
if (roll <= 0.05f) return AvoidZero(minValue, maxValue, UnityEngine.Random.Range(minValue, minValue + span * 0.1f));
if (roll >= 0.95f) return AvoidZero(minValue, maxValue, UnityEngine.Random.Range(maxValue - span * 0.1f, maxValue));
return AvoidZero(minValue, maxValue, Mathf.Lerp(minValue, maxValue, SampleClampedGaussian01()));
}
private static float SampleClampedGaussian01()
{
float u1 = Mathf.Max(Mathf.Clamp01(UnityEngine.Random.value), 1e-6f);
float u2 = Mathf.Clamp01(UnityEngine.Random.value);
float standardNormal = Mathf.Sqrt(-2f * Mathf.Log(u1)) * Mathf.Cos(2f * Mathf.PI * u2);
return Mathf.Clamp01(0.5f + standardNormal * 0.18f);
}
private static void ResolveDuplicateEffects(List<GeneratedEffectValue> effects)
{
if (effects == null || effects.Count <= 1) return;
var bestByType = new Dictionary<equipmentSO.EquipmentSpecialEffectType, GeneratedEffectValue>();
for (int i = 0; i < effects.Count; i++)
{
GeneratedEffectValue candidate = effects[i];
if (!bestByType.TryGetValue(candidate.effectType, out GeneratedEffectValue existing))
{
bestByType[candidate.effectType] = candidate;
continue;
}
bestByType[candidate.effectType] = ChoosePreferred(existing, candidate);
}
effects.Clear();
foreach (GeneratedEffectValue value in bestByType.Values) effects.Add(value);
}
private static GeneratedEffectValue ChoosePreferred(GeneratedEffectValue left, GeneratedEffectValue right)
{
bool leftNegative = left.value < 0f;
bool rightNegative = right.value < 0f;
if (leftNegative && rightNegative) return left.value <= right.value ? left : right;
if (!leftNegative && !rightNegative) return left.value >= right.value ? left : right;
return Mathf.Abs(left.value) >= Mathf.Abs(right.value) ? left : right;
}
private static float AvoidZero(float minValue, float maxValue, float value)
{
if (!Mathf.Approximately(value, 0f)) return value;
if (minValue > 0f || maxValue < 0f) return value;
float epsilon = Mathf.Min(0.0001f, Mathf.Max(Mathf.Abs(minValue), Mathf.Abs(maxValue), 0.0001f));
return UnityEngine.Random.value < 0.5f ? -epsilon : epsilon;
}
#if UNITY_EDITOR
private static void EnsureEditorOutputFolderExists()
{
if (AssetDatabase.IsValidFolder(EditorOutputFolder)) return;
if (!AssetDatabase.IsValidFolder("Assets/Resources")) AssetDatabase.CreateFolder("Assets", "Resources");
if (!AssetDatabase.IsValidFolder("Assets/Resources/so")) AssetDatabase.CreateFolder("Assets/Resources", "so");
if (!AssetDatabase.IsValidFolder(EditorOutputFolder)) AssetDatabase.CreateFolder("Assets/Resources/so", "uEquip");
}
#endif
private static void EnsureRuntimeGeneratedLoaded()
{
if (runtimeGeneratedLoaded)
{
return;
}
runtimeGeneratedLoaded = true;
runtimeGeneratedLoadedFromSave = false;
runtimeGeneratedHasRecoverableState =
SecureSaveVault.HasAnyRecoverableState(RuntimeSaveCategory, RuntimeSaveKey)
|| LocalRecoveryMirror.HasSlotData(RuntimeRecoverySlotKey);
RuntimeGeneratedEquipments.Clear();
RuntimeGeneratedEquipmentListPayload payload;
if (!SecureSaveVault.TryLoadJson(RuntimeSaveCategory, RuntimeSaveKey, out payload) || payload == null || payload.items == null)
{
if (!LocalRecoveryMirror.TryLoadJson(RuntimeRecoverySlotKey, out payload) || payload == null || payload.items == null)
{
return;
}
SecureSaveVault.SaveJson(RuntimeSaveCategory, RuntimeSaveKey, payload);
}
runtimeGeneratedLoadedFromSave = true;
for (int i = 0; i < payload.items.Count; i++)
{
RuntimeGeneratedEquipmentPayload itemPayload = payload.items[i];
equipmentSO restored = BuildEquipmentFromPayload(itemPayload);
if (restored != null)
{
RuntimeGeneratedEquipments.Add(restored);
}
}
}
private static void SaveRuntimeGeneratedState()
{
if (!runtimeGeneratedLoadedFromSave && runtimeGeneratedHasRecoverableState)
{
Debug.LogWarning("[EquipmentGenerator] Recoverable runtime equipment state exists but could not be loaded; skip saving to avoid overwriting older equipment data.");
return;
}
RuntimeGeneratedEquipmentListPayload payload = new RuntimeGeneratedEquipmentListPayload();
for (int i = 0; i < RuntimeGeneratedEquipments.Count; i++)
{
equipmentSO equipment = RuntimeGeneratedEquipments[i];
if (equipment == null)
{
continue;
}
payload.items.Add(BuildPayload(equipment));
}
SecureSaveVault.SaveJson(RuntimeSaveCategory, RuntimeSaveKey, payload);
LocalRecoveryMirror.SaveJson(RuntimeRecoverySlotKey, payload);
}
private static RuntimeGeneratedEquipmentPayload BuildPayload(equipmentSO equipment)
{
return new RuntimeGeneratedEquipmentPayload
{
name = equipment.name ?? string.Empty,
skillType = (int)equipment.skillType,
level = equipment.level,
tierNameConfigName = equipment.tierNameConfig != null ? equipment.tierNameConfig.name : string.Empty,
enableTypeSameEffects = equipment.enableTypeSameEffects,
saSkillId = equipment.sa_skillID,
iaSkillId = equipment.ia_skillID,
maxHp = BuildStatPayload(equipment.maxHp),
attack = BuildStatPayload(equipment.attack),
maxMana = BuildStatPayload(equipment.maxMana),
damageResistance = BuildStatPayload(equipment.damageResistance),
scoreEfficiency = BuildStatPayload(equipment.scoreEfficiency),
specialEffects = BuildEffectPayloads(equipment.specialEffects),
typeSameEffects = BuildEffectPayloads(equipment.typeSameEffects),
illusionEffects = BuildEffectPayloads(equipment.illusionEffects),
maxLevelEffects = BuildEffectPayloads(equipment.maxLevelEffects)
};
}
private static equipmentSO BuildEquipmentFromPayload(RuntimeGeneratedEquipmentPayload payload)
{
if (payload == null || string.IsNullOrWhiteSpace(payload.name))
{
return null;
}
equipmentSO equipment = ScriptableObject.CreateInstance<equipmentSO>();
equipment.name = payload.name;
equipment.skillType = (equipmentSO.EquipmentSkillType)Mathf.Clamp(payload.skillType, 0, Enum.GetValues(typeof(equipmentSO.EquipmentSkillType)).Length - 1);
equipment.level = Mathf.Max(0, payload.level);
equipment.tierNameConfig = ResolveTierNameConfig(payload.tierNameConfigName);
equipment.enableTypeSameEffects = payload.enableTypeSameEffects;
equipment.sa_skillID = Mathf.Max(0, payload.saSkillId);
equipment.ia_skillID = Mathf.Max(0, payload.iaSkillId);
ApplyStatPayload(equipment.maxHp, payload.maxHp);
ApplyStatPayload(equipment.attack, payload.attack);
ApplyStatPayload(equipment.maxMana, payload.maxMana);
ApplyStatPayload(equipment.damageResistance, payload.damageResistance);
ApplyStatPayload(equipment.scoreEfficiency, payload.scoreEfficiency);
equipment.specialEffects = BuildEffectsFromPayload(payload.specialEffects);
equipment.typeSameEffects = BuildEffectsFromPayload(payload.typeSameEffects);
equipment.illusionEffects = BuildEffectsFromPayload(payload.illusionEffects);
equipment.maxLevelEffects = BuildEffectsFromPayload(payload.maxLevelEffects);
equipment.NormalizeIllusionEffectsInPlace();
equipment.hideFlags = HideFlags.None;
return equipment;
}
private static StatPayload BuildStatPayload(equipmentSO.EquipmentStatTuning tuning)
{
return new StatPayload
{
basicGain = tuning != null ? tuning.basicGain : 0f,
cultivationInterval = tuning != null ? tuning.cultivationInterval : 0f
};
}
private static void ApplyStatPayload(equipmentSO.EquipmentStatTuning tuning, StatPayload payload)
{
if (tuning == null || payload == null)
{
return;
}
tuning.basicGain = payload.basicGain;
tuning.cultivationInterval = payload.cultivationInterval;
}
private static List<EffectPayload> BuildEffectPayloads(equipmentSO.EquipmentSpecialEffect[] effects)
{
List<EffectPayload> payloads = new List<EffectPayload>();
if (effects == null)
{
return payloads;
}
for (int i = 0; i < effects.Length; i++)
{
equipmentSO.EquipmentSpecialEffect effect = effects[i];
if (effect == null)
{
continue;
}
payloads.Add(new EffectPayload
{
effectType = (int)effect.effectType,
value = effect.value
});
}
return payloads;
}
private static equipmentSO.EquipmentSpecialEffect[] BuildEffectsFromPayload(List<EffectPayload> payloads)
{
if (payloads == null || payloads.Count == 0)
{
return Array.Empty<equipmentSO.EquipmentSpecialEffect>();
}
equipmentSO.EquipmentSpecialEffect[] effects = new equipmentSO.EquipmentSpecialEffect[payloads.Count];
for (int i = 0; i < payloads.Count; i++)
{
EffectPayload payload = payloads[i];
effects[i] = new equipmentSO.EquipmentSpecialEffect
{
effectType = (equipmentSO.EquipmentSpecialEffectType)Mathf.Clamp(payload.effectType, 0, Enum.GetValues(typeof(equipmentSO.EquipmentSpecialEffectType)).Length - 1),
value = payload.value
};
}
return effects;
}
private static equipmentTierNameConfigSO ResolveTierNameConfig(string configName)
{
#if UNITY_EDITOR
if (!Application.isPlaying)
{
if (!string.IsNullOrWhiteSpace(configName))
{
string[] guids = AssetDatabase.FindAssets($"t:equipmentTierNameConfigSO {configName}");
for (int i = 0; i < guids.Length; i++)
{
string path = AssetDatabase.GUIDToAssetPath(guids[i]);
equipmentTierNameConfigSO config = AssetDatabase.LoadAssetAtPath<equipmentTierNameConfigSO>(path);
if (config != null && string.Equals(config.name, configName, StringComparison.Ordinal))
{
return config;
}
}
}
equipmentTierNameConfigSO directEditorConfig = AssetDatabase.LoadAssetAtPath<equipmentTierNameConfigSO>("Assets/_eqpmtSys/EquipmentTierNameConfig.asset");
if (directEditorConfig != null)
{
return directEditorConfig;
}
}
#endif
if (string.IsNullOrWhiteSpace(configName))
{
return Resources.Load<equipmentTierNameConfigSO>("EquipmentTierNameConfig");
}
equipmentTierNameConfigSO[] configs = Resources.LoadAll<equipmentTierNameConfigSO>(string.Empty);
for (int i = 0; i < configs.Length; i++)
{
if (configs[i] != null && string.Equals(configs[i].name, configName, StringComparison.Ordinal))
{
return configs[i];
}
}
return Resources.Load<equipmentTierNameConfigSO>("EquipmentTierNameConfig");
}
}
}