using System; using UnityEngine; using System.Collections.Generic; #if UNITY_EDITOR using UnityEditor; #endif [CreateAssetMenu(fileName = "NewAllyHero", menuName = "SO_Data/AllyHero")] public class AllyHero_SO : ScriptableObject { public static readonly string[] BehaviourAxisNames = { "题海战术", "诛锄异己", "逸乐共谋", "完美至上", "涵养心境", "众志成城", "过激行为", "二次形变", "自有主见", "迷茫或凡想" }; [Header("Inspector")] public string ally_heroName; public string ally_heroDesignation; public int ally_heroID; public bool isUnlocked; public equipmentSO.EquipmentSkillType allyType; [Tooltip("Optional obsession tag used by memory skills such as 30011012. Leave empty to ignore mismatch checks.")] public string obsessionTag; [Header("Inspector")] public Sprite ally_heroImage; [Header("Inspector")] public Sprite ally_heroProfile; [Header("Inspector")] public Sprite ally_heroSelectIcon; [Header("Inspector")] public Sprite ally_heroIcon; [Header("Inspector")] public Sprite ally_heroPoster; [Header("Inspector")] public Sprite ally_hero_HD_image; [Header("Inspector")] public Sprite ally_hero_squareProfile; [Header("Inspector")] public Color ally_heroThemeColor; [Header("Inspector")] [TextArea(3, 10)] [Tooltip("Documentation text normalized.")] public string ally_heroDescription; [Header("Inspector")] public List levelStats = new List(); [System.Serializable] public class AllyLevelInfo { [Tooltip("Documentation text normalized.")] public string levelName; public int levelID; [Tooltip("Documentation text normalized.")] public int attack = 0; [Tooltip("Documentation text normalized.")] public int maxHP = 100; [Tooltip("Documentation text normalized.")] public float damageResistance = 0f; [Tooltip("Documentation text normalized.")] public int maxMana = 100; [Tooltip("Documentation text normalized.")] public float scoreEfficiency = 1f; [Header("Inspector")] [Tooltip("Documentation text normalized.")] public int requiredEXP; [Header("Inspector")] [Tooltip("Documentation text normalized.")] public int skill_slot_limited; [Header("Note-hit shared parameters")] [Tooltip("Documentation text normalized.")] public int manaGainGood = 1; [Tooltip("Documentation text normalized.")] public int manaGainGreat = 2; [Tooltip("Documentation text normalized.")] public int manaGainPerfect = 3; [Tooltip("Documentation text normalized.")] public int manaGainOnMiss = 5; [Tooltip("Documentation text normalized.")] public float damageMultiplierGood = 0.5f; [Tooltip("Documentation text normalized.")] public float damageMultiplierGreat = 0.75f; [Tooltip("Documentation text normalized.")] public float damageMultiplierPerfect = 1f; [Tooltip("Documentation text normalized.")] public float missHpLossBase = 10f; } [System.Serializable] public class AllyBehaviourAxis { public string axisName; [Range(0, 100)] public int value; } [Header("Inspector")] public int ally_currentEXP; public int ally_growthUnlockedTierIndex; public bool ally_autoBreakthroughEnabled; public int ally_battleDeployCount; public int ally_finishCount; public int ally_mvpCount; public long ally_joinDateUtcTicks; [Header("Behaviour Radar")] public List behaviourAxes = new List(); [Header("Skills")] public SkillDefinition[] availableSkills; [Tooltip("Indexes into availableSkills for preselected skills; empty means none selected")] public int[] selectedSkillIndices = new int[0]; [Tooltip("Primary selected skill index into availableSkills (-1 = none)")] public int primarySkillIndex = -1; [Tooltip("Skill groups defined on this hero. Each SkillGroup has a numeric skillGroupID which can be equipped into slots to allow activation of the entire group via that ID.")] public SkillGroup[] skillGroups = new SkillGroup[0]; [Tooltip("Equipped skill group IDs for this ally. Each int references a skillGroupID defined in skillGroups. Use 0 to indicate empty.")] public int[] equippedSkillGroupIDs = new int[0]; [Header("Equipments")] public equipmentSO equippedEquipment; public string equippedEquipmentId; public SkillDefinition GetPrimarySkill() { if (availableSkills == null || primarySkillIndex < 0 || primarySkillIndex >= availableSkills.Length) return null; return availableSkills[primarySkillIndex]; } public SkillGroup GetPrimarySkillGroup() { int[] runtimeGroupIds = GetEffectiveEquippedSkillGroupIDs(); if (runtimeGroupIds != null && runtimeGroupIds.Length > 0) { foreach (int gid in runtimeGroupIds) { if (gid == 0) continue; SkillGroup group = GetSkillGroupByID(gid); if (group != null) return group; } } if (skillGroups != null) { foreach (SkillGroup group in skillGroups) { if (group != null) return group; } } return null; } public SkillGroup GetSkillGroupByID(int groupID) { if (skillGroups != null && skillGroups.Length > 0) { foreach (SkillGroup group in skillGroups) { if (group != null && group.skillGroupID == groupID) return group; } } return EquipmentSkillGroupLibrary.ResolveSkillGroup(groupID); } public int[] GetEffectiveEquippedSkillGroupIDs() { var result = new List(); var dedupe = new HashSet(); int heroSkillSlotsRemaining = GetEffectiveSkillSlotLimit(); if (equippedSkillGroupIDs != null) { for (int i = 0; i < equippedSkillGroupIDs.Length; i++) { if (heroSkillSlotsRemaining <= 0) { break; } int groupId = equippedSkillGroupIDs[i]; if (groupId <= 0 || !dedupe.Add(groupId)) { continue; } result.Add(groupId); heroSkillSlotsRemaining--; } } equipmentSO equipment = GetEquippedEquipmentResolved(); if (equipment != null) { TryAddEquipmentSkillGroupId(result, dedupe, equipment.sa_skillID); TryAddEquipmentSkillGroupId(result, dedupe, equipment.ia_skillID); } return result.ToArray(); } public int GetEffectiveSkillSlotLimit() { AllyLevelInfo currentLevelInfo = GetEffectiveLevelForCurrentEXP(); int slotLimit = currentLevelInfo != null ? Mathf.Max(0, currentLevelInfo.skill_slot_limited) : 0; if (HasExclusiveIllusionExtraSlotBonus()) { slotLimit += 1; } return slotLimit; } public AllyLevelInfo GetEffectiveLevelInfoWithEquipment(AllyLevelInfo baseInfo) { if (baseInfo == null) { return null; } AllyLevelInfo resolved = CloneLevelInfo(baseInfo); resolved.skill_slot_limited = GetEffectiveSkillSlotLimit(); equipmentSO equipment = GetEquippedEquipmentResolved(); if (equipment == null) { return resolved; } resolved.maxHP = ApplyIntEquipmentBonus( resolved.maxHP, equipment.maxHp, equipment, equipmentSO.EquipmentSpecialEffectType.MaxHp); resolved.attack = ApplyIntEquipmentBonus( resolved.attack, equipment.attack, equipment, equipmentSO.EquipmentSpecialEffectType.Attack); resolved.maxMana = ApplyIntEquipmentBonus( resolved.maxMana, equipment.maxMana, equipment, equipmentSO.EquipmentSpecialEffectType.MaxMana); resolved.damageResistance = ApplyFloatEquipmentBonus( resolved.damageResistance, equipment.damageResistance, equipment, equipmentSO.EquipmentSpecialEffectType.DamageResistance); resolved.scoreEfficiency = ApplyFloatEquipmentBonus( resolved.scoreEfficiency, equipment.scoreEfficiency, equipment, equipmentSO.EquipmentSpecialEffectType.ScoreEfficiency); return resolved; } private bool HasExclusiveIllusionExtraSlotBonus() { equipmentSO equipment = GetEquippedEquipmentResolved(); if (equipment == null || equipment.ia_skillID != 30021007) { return false; } AllyHero_SO winner = ResolveExclusiveIllusionExtraSlotOwner(); return winner != null && winner.ally_heroID == ally_heroID; } private static AllyHero_SO ResolveExclusiveIllusionExtraSlotOwner() { AllyHero_SO[] heroes = LoadAllHeroAssetsForEquipmentChecks(); if (heroes == null || heroes.Length == 0) { return null; } AllyHero_SO selected = null; for (int i = 0; i < heroes.Length; i++) { AllyHero_SO hero = heroes[i]; if (hero == null) { continue; } hero.LoadEquippedEquipmentFromLocal(); equipmentSO equipped = hero.GetEquippedEquipmentResolved(); if (equipped == null || equipped.ia_skillID != 30021007) { continue; } if (selected == null || hero.ally_heroID < selected.ally_heroID || (hero.ally_heroID == selected.ally_heroID && string.CompareOrdinal(hero.ally_heroName ?? string.Empty, selected.ally_heroName ?? string.Empty) < 0)) { selected = hero; } } return selected; } private static AllyHero_SO[] LoadAllHeroAssetsForEquipmentChecks() { #if UNITY_EDITOR if (!Application.isPlaying) { string[] guids = AssetDatabase.FindAssets("t:AllyHero_SO", new[] { "Assets/Resources/so/ally" }); var heroes = new List(guids.Length); for (int i = 0; i < guids.Length; i++) { string path = AssetDatabase.GUIDToAssetPath(guids[i]); AllyHero_SO hero = AssetDatabase.LoadAssetAtPath(path); if (hero != null) { heroes.Add(hero); } } return heroes.ToArray(); } #endif return Resources.LoadAll("so/ally"); } private static void TryAddEquipmentSkillGroupId(List result, HashSet dedupe, int groupId) { if (groupId <= 0 || result == null || dedupe == null || !dedupe.Add(groupId)) { return; } result.Add(groupId); } public AllyLevelInfo GetEffectiveLevelForCurrentEXP() { if (levelStats == null || levelStats.Count == 0) return null; List sorted = new List(); for (int i = 0; i < levelStats.Count; i++) { if (levelStats[i] != null) { sorted.Add(levelStats[i]); } } if (sorted.Count == 0) { return null; } sorted.Sort((left, right) => left.requiredEXP.CompareTo(right.requiredEXP)); int unlockedTierIndex = Mathf.Clamp(ally_growthUnlockedTierIndex, 0, sorted.Count - 1); return sorted[unlockedTierIndex]; } private AllyLevelInfo CloneLevelInfo(AllyLevelInfo source) { return new AllyLevelInfo { levelName = source.levelName, levelID = source.levelID, attack = source.attack, maxHP = source.maxHP, damageResistance = source.damageResistance, maxMana = source.maxMana, scoreEfficiency = source.scoreEfficiency, requiredEXP = source.requiredEXP, skill_slot_limited = source.skill_slot_limited, manaGainGood = source.manaGainGood, manaGainGreat = source.manaGainGreat, manaGainPerfect = source.manaGainPerfect, manaGainOnMiss = source.manaGainOnMiss, damageMultiplierGood = source.damageMultiplierGood, damageMultiplierGreat = source.damageMultiplierGreat, damageMultiplierPerfect = source.damageMultiplierPerfect, missHpLossBase = source.missHpLossBase }; } private int ApplyIntEquipmentBonus(int baseValue, equipmentSO.EquipmentStatTuning tuning, equipmentSO equipment, equipmentSO.EquipmentSpecialEffectType effectType) { float percent = GetEquipmentPercentBonus(tuning, equipment, effectType); float flat = GetEquipmentFlatBonus(equipment, effectType); return Mathf.Max(0, Mathf.FloorToInt(baseValue * (1f + percent) + flat)); } private float ApplyFloatEquipmentBonus(float baseValue, equipmentSO.EquipmentStatTuning tuning, equipmentSO equipment, equipmentSO.EquipmentSpecialEffectType effectType) { float percent = GetEquipmentPercentBonus(tuning, equipment, effectType); float flat = GetEquipmentFlatBonus(equipment, effectType); return Mathf.Max(0f, (baseValue * (1f + percent)) + flat); } private float GetEquipmentPercentBonus(equipmentSO.EquipmentStatTuning tuning, equipmentSO equipment, equipmentSO.EquipmentSpecialEffectType effectType) { float total = 0f; if (tuning != null) { if (!Mathf.Approximately(tuning.basicGain, 0f)) { total += tuning.basicGain; total += Mathf.Max(0, equipment.level) * tuning.cultivationInterval; } } if (equipment.enableTypeSameEffects && equipment.skillType == allyType) { total += SumEffectValues(equipment.typeSameEffects, effectType); } total += SumEffectValues(equipment.maxLevelEffects, effectType); return total; } private float GetEquipmentFlatBonus(equipmentSO equipment, equipmentSO.EquipmentSpecialEffectType effectType) { float total = 0f; total += SumEffectValues(equipment.specialEffects, effectType); equipmentSO.EquipmentSpecialEffect[] normalizedIllusionEffects = equipment.GetNormalizedIllusionEffects(); total += SumEffectValues(normalizedIllusionEffects, effectType); return total; } private static float SumEffectValues(equipmentSO.EquipmentSpecialEffect[] effects, equipmentSO.EquipmentSpecialEffectType effectType) { if (effects == null || effects.Length == 0) { return 0f; } float total = 0f; for (int i = 0; i < effects.Length; i++) { equipmentSO.EquipmentSpecialEffect effect = effects[i]; if (effect == null || effect.effectType != effectType) { continue; } total += effect.value; } return total; } [System.Serializable] private class EquippedSkillGroupIDsPayload { public int[] equippedSkillGroupIDs; } [System.Serializable] private class EquippedEquipmentPayload { public string equippedEquipmentId; } private string GetEquippedSkillsPrefsKey() { return "ally_equippedSkillGroupIDs_" + ally_heroID; } private string GetEquippedEquipmentPrefsKey() { return "ally_equippedEquipment_" + ally_heroID; } public void SaveEquippedSkillsToLocal() { var payload = new EquippedSkillGroupIDsPayload { equippedSkillGroupIDs = equippedSkillGroupIDs ?? new int[0] }; string json = JsonUtility.ToJson(payload); PlayerPrefs.SetString(GetEquippedSkillsPrefsKey(), json); PlayerPrefs.Save(); } public void LoadEquippedSkillsFromLocal() { string key = GetEquippedSkillsPrefsKey(); if (!PlayerPrefs.HasKey(key)) return; string json = PlayerPrefs.GetString(key, ""); if (string.IsNullOrEmpty(json)) return; var payload = JsonUtility.FromJson(json); if (payload == null) return; equippedSkillGroupIDs = payload.equippedSkillGroupIDs ?? new int[0]; } public void SaveEquippedEquipmentToLocal() { equippedEquipmentId = equippedEquipment != null ? equippedEquipment.name : string.Empty; var payload = new EquippedEquipmentPayload { equippedEquipmentId = equippedEquipmentId ?? string.Empty }; string json = JsonUtility.ToJson(payload); PlayerPrefs.SetString(GetEquippedEquipmentPrefsKey(), json); PlayerPrefs.Save(); } public void LoadEquippedEquipmentFromLocal() { string key = GetEquippedEquipmentPrefsKey(); if (!PlayerPrefs.HasKey(key)) { return; } string json = PlayerPrefs.GetString(key, string.Empty); if (string.IsNullOrEmpty(json)) { return; } EquippedEquipmentPayload payload = JsonUtility.FromJson(json); if (payload == null) { return; } equippedEquipmentId = payload.equippedEquipmentId ?? string.Empty; equippedEquipment = ResolveEquippedEquipmentById(equippedEquipmentId); } public void SetEquippedEquipment(equipmentSO equipment, bool persist = true) { equippedEquipment = equipment; equippedEquipmentId = equipment != null ? equipment.name : string.Empty; #if UNITY_EDITOR if (!Application.isPlaying) { UnityEditor.EditorUtility.SetDirty(this); if (persist) { UnityEditor.AssetDatabase.SaveAssets(); } } #endif if (persist) { SaveEquippedEquipmentToLocal(); } } public equipmentSO GetEquippedEquipmentResolved() { if (equippedEquipment != null) { return equippedEquipment; } if (string.IsNullOrWhiteSpace(equippedEquipmentId)) { return null; } equippedEquipment = ResolveEquippedEquipmentById(equippedEquipmentId); return equippedEquipment; } public void ClearEquippedEquipment() { equippedEquipment = null; equippedEquipmentId = string.Empty; PlayerPrefs.DeleteKey(GetEquippedEquipmentPrefsKey()); PlayerPrefs.Save(); #if UNITY_EDITOR if (!Application.isPlaying) { UnityEditor.EditorUtility.SetDirty(this); UnityEditor.AssetDatabase.SaveAssets(); } #endif } private equipmentSO ResolveEquippedEquipmentById(string equipmentId) { if (string.IsNullOrWhiteSpace(equipmentId)) { return null; } #if UNITY_EDITOR if (!Application.isPlaying) { string[] guids = UnityEditor.AssetDatabase.FindAssets("t:equipmentSO", new[] { "Assets/Resources/so/uEquip" }); for (int i = 0; i < guids.Length; i++) { string path = UnityEditor.AssetDatabase.GUIDToAssetPath(guids[i]); equipmentSO equipment = UnityEditor.AssetDatabase.LoadAssetAtPath(path); if (equipment != null && equipment.name == equipmentId) { return equipment; } } } #endif equipmentSO[] runtimeEquipments = Resources.LoadAll("so/uEquip"); for (int i = 0; i < runtimeEquipments.Length; i++) { if (runtimeEquipments[i] != null && runtimeEquipments[i].name == equipmentId) { return runtimeEquipments[i]; } } var generatedEquipments = Bansonic.equipmentGenerator.GetRuntimeGeneratedEquipments(); for (int i = 0; i < generatedEquipments.Count; i++) { if (generatedEquipments[i] != null && generatedEquipments[i].name == equipmentId) { return generatedEquipments[i]; } } return null; } public void ClearEquippedSkills() { equippedSkillGroupIDs = Array.Empty(); PlayerPrefs.DeleteKey(GetEquippedSkillsPrefsKey()); PlayerPrefs.Save(); #if UNITY_EDITOR if (!Application.isPlaying) { UnityEditor.EditorUtility.SetDirty(this); } #endif } public static void ClearAllEquippedSkills() { HashSet clearedHeroIds = new HashSet(); #if UNITY_EDITOR string[] guids = UnityEditor.AssetDatabase.FindAssets("t:AllyHero_SO", new[] { "Assets/Resources/so/ally" }); for (int i = 0; i < guids.Length; i++) { string path = UnityEditor.AssetDatabase.GUIDToAssetPath(guids[i]); AllyHero_SO hero = UnityEditor.AssetDatabase.LoadAssetAtPath(path); if (hero == null) { continue; } hero.ClearEquippedSkills(); clearedHeroIds.Add(hero.ally_heroID); } UnityEditor.AssetDatabase.SaveAssets(); #endif AllyHero_SO[] runtimeHeroes = Resources.LoadAll("so/ally"); for (int i = 0; i < runtimeHeroes.Length; i++) { AllyHero_SO hero = runtimeHeroes[i]; if (hero == null || clearedHeroIds.Contains(hero.ally_heroID)) { continue; } hero.ClearEquippedSkills(); } PlayerPrefs.Save(); } public void SetUnlocked(bool value) { if (isUnlocked == value) return; isUnlocked = value; #if UNITY_EDITOR if (!Application.isPlaying) { UnityEditor.EditorUtility.SetDirty(this); } #endif } private void OnValidate() { EnsureBehaviourAxes(); } public void EnsureBehaviourAxes() { if (behaviourAxes == null) { behaviourAxes = new List(); } while (behaviourAxes.Count < BehaviourAxisNames.Length) { behaviourAxes.Add(new AllyBehaviourAxis()); } if (behaviourAxes.Count > BehaviourAxisNames.Length) { behaviourAxes.RemoveRange(BehaviourAxisNames.Length, behaviourAxes.Count - BehaviourAxisNames.Length); } for (int i = 0; i < BehaviourAxisNames.Length; i++) { if (behaviourAxes[i] == null) { behaviourAxes[i] = new AllyBehaviourAxis(); } behaviourAxes[i].axisName = BehaviourAxisNames[i]; behaviourAxes[i].value = Mathf.Clamp(behaviourAxes[i].value, 0, 100); } } public void IncrementBattleDeployCount(int amount = 1) { if (amount <= 0) return; AllyHeroDeployLedger.EnsureInstance().IncrementDeployCount(this, amount); #if UNITY_EDITOR if (!Application.isPlaying) { UnityEditor.EditorUtility.SetDirty(this); } #endif } public void LoadBattleDeployCountFromLocal() { ally_battleDeployCount = AllyHeroDeployLedger.EnsureInstance().GetDeployCount(ally_heroID); } public void SaveBattleDeployCountToLocal() { AllyHeroDeployLedger.EnsureInstance().SaveNow(); } public void IncrementFinishCount(int amount = 1) { if (amount <= 0) return; AllyHeroDeployLedger.EnsureInstance().IncrementFinishCount(this, amount); #if UNITY_EDITOR if (!Application.isPlaying) { UnityEditor.EditorUtility.SetDirty(this); } #endif } public void IncrementMvpCount(int amount = 1) { if (amount <= 0) return; AllyHeroDeployLedger.EnsureInstance().IncrementMvpCount(this, amount); #if UNITY_EDITOR if (!Application.isPlaying) { UnityEditor.EditorUtility.SetDirty(this); } #endif } public void SetJoinDateIfMissing(System.DateTime? utcTime = null) { AllyHeroDeployLedger.EnsureInstance().SetJoinDateIfMissing(this, utcTime); #if UNITY_EDITOR if (!Application.isPlaying) { UnityEditor.EditorUtility.SetDirty(this); } #endif } }