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 RuntimeGeneratedEquipments = new List(); 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 items = new List(); } [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 specialEffects = new List(); public List typeSameEffects = new List(); public List illusionEffects = new List(); public List maxLevelEffects = new List(); } [Serializable] private class StatPayload { public float basicGain; public float cultivationInterval; } [Serializable] private class EffectPayload { public int effectType; public float value; } public static IReadOnlyList 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 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(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(); 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(); generated.typeSameEffects ??= Array.Empty(); generated.illusionEffects ??= Array.Empty(); generated.maxLevelEffects ??= Array.Empty(); 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(); 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(); if (pool.Count == 0 || count <= 0) { generated.specialEffects = Array.Empty(); 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(); 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(); 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 BuildSpecialEffectPool(equipmentSO generated, equipmentRandomConfigSO config) { var pool = new List(); 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 GetSelectedBasicAttributeTypes(equipmentSO target) { var selected = new HashSet(); 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(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(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(); 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 generatedEffects) { if (generatedEffects == null || generatedEffects.Count == 0) return Array.Empty(); var results = new List(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 effects) { if (effects == null || effects.Count <= 1) return; var bestByType = new Dictionary(); 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(); 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 BuildEffectPayloads(equipmentSO.EquipmentSpecialEffect[] effects) { List payloads = new List(); 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 payloads) { if (payloads == null || payloads.Count == 0) { return Array.Empty(); } 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(path); if (config != null && string.Equals(config.name, configName, StringComparison.Ordinal)) { return config; } } } equipmentTierNameConfigSO directEditorConfig = AssetDatabase.LoadAssetAtPath("Assets/_eqpmtSys/EquipmentTierNameConfig.asset"); if (directEditorConfig != null) { return directEditorConfig; } } #endif if (string.IsNullOrWhiteSpace(configName)) { return Resources.Load("EquipmentTierNameConfig"); } equipmentTierNameConfigSO[] configs = Resources.LoadAll(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("EquipmentTierNameConfig"); } } }