1280 lines
42 KiB
C#
1280 lines
42 KiB
C#
using System;
|
||
using UnityEngine;
|
||
using System.Collections.Generic;
|
||
using Spine.Unity;
|
||
#if UNITY_EDITOR
|
||
using UnityEditor;
|
||
#endif
|
||
|
||
[CreateAssetMenu(fileName = "NewAllyHero", menuName = "SO_Data/AllyHero")]
|
||
public class AllyHero_SO : ScriptableObject
|
||
{
|
||
public enum AllyRolePosition
|
||
{
|
||
攻击,
|
||
回血,
|
||
回蓝,
|
||
加分,
|
||
综合
|
||
}
|
||
|
||
private const string EquippedEquipmentSaveCategory = "ally_equipped_equipment";
|
||
private const string EquippedEquipmentRecoverySlotPrefix = "ally_equipped_equipment_";
|
||
|
||
public static readonly string[] BehaviourAxisNames =
|
||
{
|
||
"题海战术",
|
||
"诛锄异己",
|
||
"逸乐共谋",
|
||
"完美至上",
|
||
"涵养心境",
|
||
"众志成城",
|
||
"过激行为",
|
||
"二次形变",
|
||
"自有主见",
|
||
"迷茫或凡想"
|
||
};
|
||
|
||
[Header("Inspector")]
|
||
public string ally_heroName;
|
||
public string ally_heroDesignation;
|
||
public int ally_heroID;
|
||
public bool isUnlocked;
|
||
[Header("Role Setup")]
|
||
[InspectorName("所属阵营")]
|
||
[Tooltip("Uses the same category list as equipment skill types.")]
|
||
public equipmentSO.EquipmentSkillType allyType;
|
||
[InspectorName("角色定位")]
|
||
[Tooltip("Primary combat role used for configuration and display.")]
|
||
public AllyRolePosition allyRolePosition = AllyRolePosition.综合;
|
||
[Tooltip("Optional obsession tag used by memory skills such as 30011012. Leave empty to ignore mismatch checks.")]
|
||
public string obsessionTag;
|
||
|
||
[Header("DLC")]
|
||
[Tooltip("DLC key that owns this hero. Leave empty for base-game heroes that are always available. When set, the hero is only accessible when the matching DLC is owned.")]
|
||
public string sourceDlcId;
|
||
|
||
[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 Sprite ally_hero_settleDisplay;
|
||
|
||
[Header("spine")]
|
||
public SkeletonDataAsset ally_heroSpineData;
|
||
|
||
[Header("Skins")]
|
||
public string selectedSkinId;
|
||
|
||
[Header("Inspector")]
|
||
public Color ally_heroThemeColor;
|
||
|
||
[Header("Inspector")]
|
||
[TextArea(3, 10)]
|
||
[Tooltip("Documentation text normalized.")]
|
||
public string ally_heroDescription;
|
||
|
||
[Header("Inspector")]
|
||
public List<AllyLevelInfo> levelStats = new List<AllyLevelInfo>();
|
||
|
||
[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;
|
||
[Tooltip("When enabled, level display/effective level resolution is clamped to the previous tier while this hero is breakthrough-locked, even if EXP has already reached the next tier threshold.")]
|
||
public bool level_lock;
|
||
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<AllyBehaviourAxis> behaviourAxes = new List<AllyBehaviourAxis>();
|
||
|
||
[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;
|
||
|
||
// 【性能】装备存档已从磁盘水合过一次的标记。水合后内存即权威来源:
|
||
// SetEquippedEquipment / ApplyEquippedEquipmentPayload / ClearEquippedEquipment 都会同步更新内存与磁盘,
|
||
// 因此无需反复解密读盘。idols 界面每次切换角色时 RebuildBag 会对"每件装备 × 每个英雄"调用
|
||
// LoadEquippedEquipmentFromLocal 做占用检查,若每次都真正解密 SecureSaveVault 会造成严重卡顿。
|
||
// 该标记确保每个英雄整个会话只读盘一次,之后走内存,行为完全不变。
|
||
[System.NonSerialized] private bool equippedEquipmentHydrated;
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
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 bool ValidateEquippedSkillSelections(bool persistIfChanged = true)
|
||
{
|
||
LoadEquippedSkillsFromLocal();
|
||
LoadEquippedEquipmentFromLocal();
|
||
|
||
if (equippedSkillGroupIDs == null)
|
||
{
|
||
equippedSkillGroupIDs = Array.Empty<int>();
|
||
}
|
||
|
||
int currentLevelId = AllyHeroLevelRuntimeResolver.GetCurrentLevelId(this);
|
||
int maxSkillSlots = Mathf.Max(0, AllyHeroLevelRuntimeResolver.GetEffectiveSkillSlotLimit(this));
|
||
List<int> validatedGroupIds = new List<int>(Mathf.Min(equippedSkillGroupIDs.Length, maxSkillSlots));
|
||
HashSet<int> seenGroupIds = new HashSet<int>();
|
||
|
||
for (int i = 0; i < equippedSkillGroupIDs.Length; i++)
|
||
{
|
||
int groupId = equippedSkillGroupIDs[i];
|
||
if (groupId <= 0 || !seenGroupIds.Add(groupId))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (validatedGroupIds.Count >= maxSkillSlots)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
SkillGroup group = GetOwnedSkillGroupByID(groupId);
|
||
if (group == null)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
int requiredLevelId = Mathf.Clamp(group.thisSkill_levelLimit, 1, 4);
|
||
if (currentLevelId < requiredLevelId)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
validatedGroupIds.Add(groupId);
|
||
}
|
||
|
||
int[] validatedArray = validatedGroupIds.ToArray();
|
||
bool changed = !AreSkillGroupIdArraysEqual(equippedSkillGroupIDs, validatedArray);
|
||
if (!changed)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
equippedSkillGroupIDs = validatedArray;
|
||
|
||
if (persistIfChanged)
|
||
{
|
||
SaveEquippedSkillsToLocal();
|
||
}
|
||
|
||
#if UNITY_EDITOR
|
||
if (!Application.isPlaying)
|
||
{
|
||
EditorUtility.SetDirty(this);
|
||
if (persistIfChanged)
|
||
{
|
||
AssetDatabase.SaveAssets();
|
||
}
|
||
}
|
||
#endif
|
||
|
||
return true;
|
||
}
|
||
|
||
// 性能:GetEffectiveEquippedSkillGroupIDs 在单次掉血/事件中会被多入口重复调用(每次都 new List+HashSet+ToArray)。
|
||
// 用"输入快照比对"缓存返回数组:仅当影响返回值的可变输入变化时才重建。所有调用点均只读遍历返回数组,
|
||
// 故返回共享缓存安全。EXP/tier 由外部 AllyHeroDeployLedger 直接写公有字段(无法用脏标记拦截),
|
||
// 故此处按值/引用快照比对——这些比较都不分配,命中缓存时零 GC,且结果与每次重算完全一致(等效)。
|
||
// 全局装备数据版本号。装备的换装/卸下(影响跨英雄幻象额外槽)以及对已装备装备
|
||
// sa_skillID/ia_skillID 的“原地修改”(引用不变,单靠 _snapEquipment 引用比对无法察觉)
|
||
// 都会自增此计数,从而让所有英雄的 GetEffectiveEquippedSkillGroupIDs 缓存失效重建。
|
||
// 这些操作都发生在菜单/养成界面,战斗中不变,故战斗热路径仍是零 GC 命中缓存。
|
||
private static int s_equipmentDataRevision;
|
||
|
||
// 装备数据发生“引用不变的原地变更”或“可能影响跨英雄派生结果”的变更时调用,
|
||
// 使所有英雄的装备派生缓存(有效技能组 ID)在下次读取时重算。
|
||
public static void InvalidateEquipmentDerivedCaches()
|
||
{
|
||
s_equipmentDataRevision++;
|
||
}
|
||
|
||
[System.NonSerialized] private int[] _cachedEffectiveGroupIds;
|
||
[System.NonSerialized] private bool _cachedGroupIdsValid;
|
||
[System.NonSerialized] private int[] _snapEquippedGroupIdsRef; // equippedSkillGroupIDs 引用(整体替换才变)
|
||
[System.NonSerialized] private int _snapCurrentEXP;
|
||
[System.NonSerialized] private int _snapUnlockedTierIndex;
|
||
[System.NonSerialized] private equipmentSO _snapEquipment;
|
||
[System.NonSerialized] private int _snapEquipmentDataRevision = -1;
|
||
|
||
public int[] GetEffectiveEquippedSkillGroupIDs()
|
||
{
|
||
// 快照比对:任一影响返回值的输入变化则失效重建。
|
||
equipmentSO currentEquipment = GetEquippedEquipmentResolved();
|
||
if (_cachedGroupIdsValid
|
||
&& _snapEquipmentDataRevision == s_equipmentDataRevision
|
||
&& ReferenceEquals(_snapEquippedGroupIdsRef, equippedSkillGroupIDs)
|
||
&& _snapCurrentEXP == ally_currentEXP
|
||
&& _snapUnlockedTierIndex == ally_growthUnlockedTierIndex
|
||
&& ReferenceEquals(_snapEquipment, currentEquipment))
|
||
{
|
||
return _cachedEffectiveGroupIds;
|
||
}
|
||
|
||
var result = new List<int>();
|
||
var dedupe = new HashSet<int>();
|
||
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--;
|
||
}
|
||
}
|
||
|
||
if (currentEquipment != null)
|
||
{
|
||
TryAddEquipmentSkillGroupId(result, dedupe, currentEquipment.sa_skillID);
|
||
TryAddEquipmentSkillGroupId(result, dedupe, currentEquipment.ia_skillID);
|
||
}
|
||
|
||
// 写入缓存并记录本次输入快照(与上面失效判断的字段一一对应)。
|
||
_cachedEffectiveGroupIds = result.ToArray();
|
||
_snapEquippedGroupIdsRef = equippedSkillGroupIDs;
|
||
_snapCurrentEXP = ally_currentEXP;
|
||
_snapUnlockedTierIndex = ally_growthUnlockedTierIndex;
|
||
_snapEquipment = currentEquipment;
|
||
_snapEquipmentDataRevision = s_equipmentDataRevision;
|
||
_cachedGroupIdsValid = true;
|
||
return _cachedEffectiveGroupIds;
|
||
}
|
||
|
||
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<AllyHero_SO>(guids.Length);
|
||
for (int i = 0; i < guids.Length; i++)
|
||
{
|
||
string path = AssetDatabase.GUIDToAssetPath(guids[i]);
|
||
AllyHero_SO hero = AssetDatabase.LoadAssetAtPath<AllyHero_SO>(path);
|
||
if (hero != null)
|
||
{
|
||
heroes.Add(hero);
|
||
}
|
||
}
|
||
|
||
return heroes.ToArray();
|
||
}
|
||
#endif
|
||
|
||
return RuntimeResourcesCache.LoadAll<AllyHero_SO>("so/ally");
|
||
}
|
||
|
||
private static void TryAddEquipmentSkillGroupId(List<int> result, HashSet<int> dedupe, int groupId)
|
||
{
|
||
if (groupId <= 0 || result == null || dedupe == null || !dedupe.Add(groupId))
|
||
{
|
||
return;
|
||
}
|
||
|
||
result.Add(groupId);
|
||
}
|
||
|
||
private static bool AreSkillGroupIdArraysEqual(int[] left, int[] right)
|
||
{
|
||
int leftLength = left != null ? left.Length : 0;
|
||
int rightLength = right != null ? right.Length : 0;
|
||
if (leftLength != rightLength)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
for (int i = 0; i < leftLength; i++)
|
||
{
|
||
if (left[i] != right[i])
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
private SkillGroup GetOwnedSkillGroupByID(int groupId)
|
||
{
|
||
if (skillGroups == null || skillGroups.Length == 0)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
for (int i = 0; i < skillGroups.Length; i++)
|
||
{
|
||
SkillGroup group = skillGroups[i];
|
||
if (group != null && group.skillGroupID == groupId)
|
||
{
|
||
return group;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
public int GetUnlockedLevelIndex()
|
||
{
|
||
List<AllyLevelInfo> sorted = BuildSortedLevelStats();
|
||
return ResolveUnlockedLevelIndex(sorted);
|
||
}
|
||
|
||
public int GetExpQualifiedLevelIndex()
|
||
{
|
||
List<AllyLevelInfo> sorted = BuildSortedLevelStats();
|
||
return ResolveExpQualifiedLevelIndex(sorted);
|
||
}
|
||
|
||
public int GetDisplayLevelIndex()
|
||
{
|
||
List<AllyLevelInfo> sorted = BuildSortedLevelStats();
|
||
return ResolveDisplayLevelIndex(sorted);
|
||
}
|
||
|
||
public string GetDisplayLevelRatingKey()
|
||
{
|
||
int displayIndex = GetDisplayLevelIndex();
|
||
return DisplayLevelIndexToRatingKey(displayIndex);
|
||
}
|
||
|
||
// 用外部传入的成长值(来自持久化账本 AllyHeroDeployLedger)计算等级评级,
|
||
// 而不是读取 SO 上被打包烘焙的镜像字段。用于 teamSelector 等直接依赖账本真相的界面,
|
||
// 避免"账本已加载但尚未回写 SO 镜像"窗口内读到过期的烘焙值。判级规则与 GetDisplayLevelRatingKey 完全一致。
|
||
public string GetDisplayLevelRatingKeyFromGrowth(int currentExp, int unlockedTierIndex, bool levelLocked)
|
||
{
|
||
int displayIndex = ResolveDisplayLevelIndexFromGrowth(currentExp, unlockedTierIndex, levelLocked);
|
||
return DisplayLevelIndexToRatingKey(displayIndex);
|
||
}
|
||
|
||
private static string DisplayLevelIndexToRatingKey(int displayIndex)
|
||
{
|
||
if (displayIndex <= 0) return "C";
|
||
if (displayIndex == 1) return "B";
|
||
if (displayIndex == 2) return "A";
|
||
return "S";
|
||
}
|
||
|
||
private int ResolveDisplayLevelIndexFromGrowth(int currentExp, int unlockedTierIndex, bool levelLocked)
|
||
{
|
||
List<AllyLevelInfo> sorted = BuildSortedLevelStats();
|
||
if (sorted == null || sorted.Count == 0)
|
||
{
|
||
return -1;
|
||
}
|
||
|
||
int safeExp = Mathf.Max(0, currentExp);
|
||
int expQualifiedIndex = 0;
|
||
for (int i = 0; i < sorted.Count; i++)
|
||
{
|
||
if (safeExp >= sorted[i].requiredEXP)
|
||
{
|
||
expQualifiedIndex = i;
|
||
}
|
||
else
|
||
{
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!levelLocked)
|
||
{
|
||
return expQualifiedIndex;
|
||
}
|
||
|
||
int unlockedIndex = Mathf.Clamp(unlockedTierIndex, 0, sorted.Count - 1);
|
||
return Mathf.Clamp(Mathf.Min(expQualifiedIndex, unlockedIndex), 0, sorted.Count - 1);
|
||
}
|
||
|
||
public AllyLevelInfo GetEffectiveLevelForCurrentEXP()
|
||
{
|
||
List<AllyLevelInfo> sorted = BuildSortedLevelStats();
|
||
int unlockedIndex = ResolveUnlockedLevelIndex(sorted);
|
||
int expQualifiedIndex = ResolveExpQualifiedLevelIndex(sorted);
|
||
if (unlockedIndex < 0 || expQualifiedIndex < 0 || sorted == null || sorted.Count == 0)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
int effectiveIndex = Mathf.Clamp(Mathf.Min(expQualifiedIndex, unlockedIndex), 0, sorted.Count - 1);
|
||
return sorted[effectiveIndex];
|
||
}
|
||
|
||
// 性能:levelStats 是序列化配置,运行时不变,故排序结果可缓存,避免每次事件 new List + Sort(闭包+排序)。
|
||
// 用 (引用, Count) 快照判失效,防止极少数运行时改配置的情况漏更新。返回缓存列表,调用方只读(仅索引读取)。
|
||
[System.NonSerialized] private List<AllyLevelInfo> _cachedSortedLevelStats;
|
||
[System.NonSerialized] private bool _cachedSortedLevelStatsIsNull;
|
||
[System.NonSerialized] private object _snapLevelStatsRef;
|
||
[System.NonSerialized] private int _snapLevelStatsCount = -1;
|
||
|
||
private List<AllyLevelInfo> BuildSortedLevelStats()
|
||
{
|
||
if (_cachedSortedLevelStats != null
|
||
&& ReferenceEquals(_snapLevelStatsRef, levelStats)
|
||
&& _snapLevelStatsCount == (levelStats != null ? levelStats.Count : 0))
|
||
{
|
||
return _cachedSortedLevelStatsIsNull ? null : _cachedSortedLevelStats;
|
||
}
|
||
|
||
var sorted = new List<AllyLevelInfo>();
|
||
_snapLevelStatsRef = levelStats;
|
||
_snapLevelStatsCount = levelStats != null ? levelStats.Count : 0;
|
||
|
||
if (levelStats == null || levelStats.Count == 0)
|
||
{
|
||
// 空配置:原逻辑返回空列表(非 null)。缓存该空列表。
|
||
_cachedSortedLevelStats = sorted;
|
||
_cachedSortedLevelStatsIsNull = false;
|
||
return sorted;
|
||
}
|
||
|
||
for (int i = 0; i < levelStats.Count; i++)
|
||
{
|
||
if (levelStats[i] != null)
|
||
{
|
||
sorted.Add(levelStats[i]);
|
||
}
|
||
}
|
||
|
||
if (sorted.Count == 0)
|
||
{
|
||
// 原逻辑:全为 null 时返回 null。缓存该 null 语义(仍记录快照避免每次重建)。
|
||
_cachedSortedLevelStats = sorted; // 存空列表占位,使缓存有效
|
||
_cachedSortedLevelStatsIsNull = true;
|
||
return null;
|
||
}
|
||
|
||
sorted.Sort((left, right) => left.requiredEXP.CompareTo(right.requiredEXP));
|
||
_cachedSortedLevelStats = sorted;
|
||
_cachedSortedLevelStatsIsNull = false;
|
||
return sorted;
|
||
}
|
||
|
||
private int ResolveUnlockedLevelIndex(List<AllyLevelInfo> sorted)
|
||
{
|
||
if (sorted == null || sorted.Count == 0)
|
||
{
|
||
return -1;
|
||
}
|
||
|
||
return Mathf.Clamp(ally_growthUnlockedTierIndex, 0, sorted.Count - 1);
|
||
}
|
||
|
||
private int ResolveExpQualifiedLevelIndex(List<AllyLevelInfo> sorted)
|
||
{
|
||
if (sorted == null || sorted.Count == 0)
|
||
{
|
||
return -1;
|
||
}
|
||
|
||
int expQualifiedIndex = 0;
|
||
for (int i = 0; i < sorted.Count; i++)
|
||
{
|
||
if (ally_currentEXP >= sorted[i].requiredEXP)
|
||
{
|
||
expQualifiedIndex = i;
|
||
}
|
||
else
|
||
{
|
||
break;
|
||
}
|
||
}
|
||
|
||
return Mathf.Clamp(expQualifiedIndex, 0, sorted.Count - 1);
|
||
}
|
||
|
||
private int ResolveDisplayLevelIndex(List<AllyLevelInfo> sorted)
|
||
{
|
||
if (sorted == null || sorted.Count == 0)
|
||
{
|
||
return -1;
|
||
}
|
||
|
||
int expQualifiedIndex = ResolveExpQualifiedLevelIndex(sorted);
|
||
if (!level_lock)
|
||
{
|
||
return expQualifiedIndex;
|
||
}
|
||
|
||
int unlockedIndex = ResolveUnlockedLevelIndex(sorted);
|
||
return Mathf.Clamp(Mathf.Min(expQualifiedIndex, unlockedIndex), 0, sorted.Count - 1);
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
if (equipment.IsMaxLevelEffectActive())
|
||
{
|
||
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;
|
||
}
|
||
|
||
private string GetEquippedEquipmentSaveKey()
|
||
{
|
||
return ally_heroID.ToString();
|
||
}
|
||
|
||
private string GetEquippedEquipmentRecoverySlotKey()
|
||
{
|
||
return EquippedEquipmentRecoverySlotPrefix + ally_heroID;
|
||
}
|
||
|
||
private string GetSelectedSkinPrefsKey()
|
||
{
|
||
return "ally_selectedSkin_" + 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<EquippedSkillGroupIDsPayload>(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);
|
||
SecureSaveVault.SaveJson(EquippedEquipmentSaveCategory, GetEquippedEquipmentSaveKey(), payload);
|
||
LocalRecoveryMirror.SaveJson(GetEquippedEquipmentRecoverySlotKey(), payload);
|
||
PlayerPrefs.SetString(GetEquippedEquipmentPrefsKey(), json);
|
||
PlayerPrefs.Save();
|
||
}
|
||
|
||
public void LoadEquippedEquipmentFromLocal(bool forceReload = false)
|
||
{
|
||
// 已水合且非强制刷新时直接返回,避免重复解密读盘(见 equippedEquipmentHydrated 说明)。
|
||
if (equippedEquipmentHydrated && !forceReload)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// 无论后续走哪条分支,本次调用都视为已完成水合:
|
||
// 命中存档→内存已填充;无存档→内存保持当前值,也不必反复重试读盘。
|
||
equippedEquipmentHydrated = true;
|
||
|
||
string key = GetEquippedEquipmentPrefsKey();
|
||
EquippedEquipmentPayload payload;
|
||
if (SecureSaveVault.TryLoadJson(EquippedEquipmentSaveCategory, GetEquippedEquipmentSaveKey(), out payload) && payload != null)
|
||
{
|
||
ApplyEquippedEquipmentPayload(payload);
|
||
return;
|
||
}
|
||
|
||
if (LocalRecoveryMirror.TryLoadJson(GetEquippedEquipmentRecoverySlotKey(), out payload) && payload != null)
|
||
{
|
||
ApplyEquippedEquipmentPayload(payload);
|
||
SecureSaveVault.SaveJson(EquippedEquipmentSaveCategory, GetEquippedEquipmentSaveKey(), payload);
|
||
return;
|
||
}
|
||
|
||
if (!PlayerPrefs.HasKey(key))
|
||
{
|
||
return;
|
||
}
|
||
|
||
string json = PlayerPrefs.GetString(key, string.Empty);
|
||
if (string.IsNullOrEmpty(json))
|
||
{
|
||
return;
|
||
}
|
||
|
||
payload = JsonUtility.FromJson<EquippedEquipmentPayload>(json);
|
||
if (payload == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
ApplyEquippedEquipmentPayload(payload);
|
||
SecureSaveVault.SaveJson(EquippedEquipmentSaveCategory, GetEquippedEquipmentSaveKey(), payload);
|
||
LocalRecoveryMirror.SaveJson(GetEquippedEquipmentRecoverySlotKey(), payload);
|
||
}
|
||
|
||
private void ApplyEquippedEquipmentPayload(EquippedEquipmentPayload payload)
|
||
{
|
||
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;
|
||
equippedEquipmentHydrated = true;
|
||
// 换装可能改变本英雄及其它英雄的跨英雄幻象额外槽归属,使全体派生缓存失效。
|
||
InvalidateEquipmentDerivedCaches();
|
||
|
||
#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;
|
||
equippedEquipmentHydrated = true;
|
||
// 卸下装备同样可能改变跨英雄幻象额外槽归属,使全体派生缓存失效。
|
||
InvalidateEquipmentDerivedCaches();
|
||
SecureSaveVault.Delete(EquippedEquipmentSaveCategory, GetEquippedEquipmentSaveKey());
|
||
LocalRecoveryMirror.DeleteSlot(GetEquippedEquipmentRecoverySlotKey());
|
||
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<equipmentSO>(path);
|
||
if (equipment != null && equipment.name == equipmentId)
|
||
{
|
||
return equipment;
|
||
}
|
||
}
|
||
}
|
||
#endif
|
||
|
||
var generatedEquipments = Bansonic.equipmentGenerator.GetRuntimeGeneratedEquipments();
|
||
for (int i = 0; i < generatedEquipments.Count; i++)
|
||
{
|
||
if (generatedEquipments[i] != null && generatedEquipments[i].name == equipmentId)
|
||
{
|
||
return generatedEquipments[i];
|
||
}
|
||
}
|
||
|
||
equipmentSO[] runtimeEquipments = Resources.LoadAll<equipmentSO>("so/uEquip");
|
||
for (int i = 0; i < runtimeEquipments.Length; i++)
|
||
{
|
||
if (runtimeEquipments[i] != null && runtimeEquipments[i].name == equipmentId)
|
||
{
|
||
return runtimeEquipments[i];
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
public void ClearEquippedSkills()
|
||
{
|
||
equippedSkillGroupIDs = Array.Empty<int>();
|
||
PlayerPrefs.DeleteKey(GetEquippedSkillsPrefsKey());
|
||
PlayerPrefs.Save();
|
||
#if UNITY_EDITOR
|
||
if (!Application.isPlaying)
|
||
{
|
||
UnityEditor.EditorUtility.SetDirty(this);
|
||
}
|
||
#endif
|
||
}
|
||
|
||
public static void ClearAllEquippedSkills()
|
||
{
|
||
HashSet<int> clearedHeroIds = new HashSet<int>();
|
||
|
||
#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<AllyHero_SO>(path);
|
||
if (hero == null)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
hero.ClearEquippedSkills();
|
||
clearedHeroIds.Add(hero.ally_heroID);
|
||
}
|
||
|
||
UnityEditor.AssetDatabase.SaveAssets();
|
||
#endif
|
||
|
||
AllyHero_SO[] runtimeHeroes = RuntimeResourcesCache.LoadAll<AllyHero_SO>("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 List<HeroSkinResolvedData> GetResolvedSkinSet(bool includeBaseSkin = true)
|
||
{
|
||
return HeroSkinResolver.GetSkinSet(this, includeBaseSkin);
|
||
}
|
||
|
||
public HeroSkinResolvedData GetResolvedSelectedSkin(bool fallbackToBase = true)
|
||
{
|
||
return HeroSkinResolver.GetSkinById(this, selectedSkinId, fallbackToBase);
|
||
}
|
||
|
||
public void SetSelectedSkin(string skinId, bool persist = true)
|
||
{
|
||
selectedSkinId = string.IsNullOrWhiteSpace(skinId) ? HeroSkinResolver.BuildBaseSkinId(ally_heroID) : skinId.Trim();
|
||
|
||
#if UNITY_EDITOR
|
||
if (!Application.isPlaying)
|
||
{
|
||
UnityEditor.EditorUtility.SetDirty(this);
|
||
if (persist)
|
||
{
|
||
UnityEditor.AssetDatabase.SaveAssets();
|
||
}
|
||
}
|
||
#endif
|
||
|
||
if (persist)
|
||
{
|
||
SaveSelectedSkinToLocal();
|
||
}
|
||
}
|
||
|
||
public void SaveSelectedSkinToLocal()
|
||
{
|
||
string safeSkinId = string.IsNullOrWhiteSpace(selectedSkinId) ? HeroSkinResolver.BuildBaseSkinId(ally_heroID) : selectedSkinId.Trim();
|
||
PlayerPrefs.SetString(GetSelectedSkinPrefsKey(), safeSkinId);
|
||
PlayerPrefs.Save();
|
||
}
|
||
|
||
public void LoadSelectedSkinFromLocal()
|
||
{
|
||
string key = GetSelectedSkinPrefsKey();
|
||
if (!PlayerPrefs.HasKey(key))
|
||
{
|
||
return;
|
||
}
|
||
|
||
selectedSkinId = PlayerPrefs.GetString(key, HeroSkinResolver.BuildBaseSkinId(ally_heroID));
|
||
if (string.IsNullOrWhiteSpace(selectedSkinId))
|
||
{
|
||
selectedSkinId = HeroSkinResolver.BuildBaseSkinId(ally_heroID);
|
||
}
|
||
}
|
||
|
||
public void ClearSelectedSkin()
|
||
{
|
||
selectedSkinId = HeroSkinResolver.BuildBaseSkinId(ally_heroID);
|
||
PlayerPrefs.DeleteKey(GetSelectedSkinPrefsKey());
|
||
PlayerPrefs.Save();
|
||
#if UNITY_EDITOR
|
||
if (!Application.isPlaying)
|
||
{
|
||
UnityEditor.EditorUtility.SetDirty(this);
|
||
UnityEditor.AssetDatabase.SaveAssets();
|
||
}
|
||
#endif
|
||
}
|
||
|
||
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<AllyBehaviourAxis>();
|
||
}
|
||
|
||
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
|
||
}
|
||
}
|