多轮审查和修复
This commit is contained in:
8
Assets/Scripts/Progression/Achievement.meta
Normal file
8
Assets/Scripts/Progression/Achievement.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: daf4df072f7275047b2976edac77576e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,22 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Save;
|
||||
|
||||
namespace BaseGames.Progression
|
||||
{
|
||||
/// <summary>收集指定数量魅饰(Charm)的成就条件。</summary>
|
||||
[CreateAssetMenu(menuName = "Achievement/Condition/CollectedAllCharms", fileName = "COND_CollectedAllCharms")]
|
||||
public class CollectedAllCharmsCondition : AchievementCondition
|
||||
{
|
||||
[Tooltip("需要收集的魅饰总数(游戏设计时确定)")]
|
||||
[Min(1)] public int totalCharmsCount = 45;
|
||||
|
||||
public override bool IsMet(SaveData save)
|
||||
=> save?.Equipment != null && save.Equipment.OwnedCharmIds.Count >= totalCharmsCount;
|
||||
|
||||
public override float GetProgress(SaveData save)
|
||||
{
|
||||
if (save?.Equipment == null || totalCharmsCount <= 0) return 0f;
|
||||
return UnityEngine.Mathf.Clamp01((float)save.Equipment.OwnedCharmIds.Count / totalCharmsCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7705d5b84ab1d90479faba8e44b35026
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Save;
|
||||
|
||||
namespace BaseGames.Progression
|
||||
{
|
||||
/// <summary>收集指定道具的成就条件(使用 World.CollectedIds)。</summary>
|
||||
[CreateAssetMenu(menuName = "Achievement/Condition/CollectedItem", fileName = "COND_CollectedItem_")]
|
||||
public class CollectedItemCondition : AchievementCondition
|
||||
{
|
||||
[Tooltip("道具唯一标识符(与 CollectibleItem.itemId 匹配)")]
|
||||
public string itemId;
|
||||
|
||||
public override bool IsMet(SaveData save)
|
||||
=> save?.World != null && save.World.CollectedIds.Contains(itemId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8209dfbb9f9b3a8488930367b42d3ec9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,30 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Save;
|
||||
|
||||
namespace BaseGames.Progression
|
||||
{
|
||||
/// <summary>击败全部指定 Boss 列表的成就条件。</summary>
|
||||
[CreateAssetMenu(menuName = "Achievement/Condition/DefeatedAllBosses", fileName = "COND_DefeatedAllBosses")]
|
||||
public class DefeatedAllBossesCondition : AchievementCondition
|
||||
{
|
||||
[Tooltip("需要全部击败的 Boss ID 列表")]
|
||||
public string[] requiredBossIds;
|
||||
|
||||
public override bool IsMet(SaveData save)
|
||||
{
|
||||
if (save?.World == null || requiredBossIds == null) return false;
|
||||
foreach (var id in requiredBossIds)
|
||||
if (!save.World.DefeatedBossIds.Contains(id)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public override float GetProgress(SaveData save)
|
||||
{
|
||||
if (save?.World == null || requiredBossIds == null || requiredBossIds.Length == 0) return 0f;
|
||||
int met = 0;
|
||||
foreach (var id in requiredBossIds)
|
||||
if (save.World.DefeatedBossIds.Contains(id)) met++;
|
||||
return (float)met / requiredBossIds.Length;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fd55de95ca7807c4992067e47a5cd8b1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Save;
|
||||
|
||||
namespace BaseGames.Progression
|
||||
{
|
||||
/// <summary>击败指定 Boss 的成就条件。</summary>
|
||||
[CreateAssetMenu(menuName = "Achievement/Condition/DefeatedBoss", fileName = "COND_DefeatedBoss_")]
|
||||
public class DefeatedBossCondition : AchievementCondition
|
||||
{
|
||||
[Tooltip("Boss 唯一标识符(与 BossRecord.bossId 匹配)")]
|
||||
public string bossId;
|
||||
|
||||
public override bool IsMet(SaveData save)
|
||||
=> save?.World != null && save.World.DefeatedBossIds.Contains(bossId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 37994d104d819474285eb6cee592425b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Save;
|
||||
|
||||
namespace BaseGames.Progression
|
||||
{
|
||||
/// <summary>进入指定场景(区域)的成就条件。</summary>
|
||||
[CreateAssetMenu(menuName = "Achievement/Condition/EnteredRegion", fileName = "COND_EnteredRegion_")]
|
||||
public class EnteredRegionCondition : AchievementCondition
|
||||
{
|
||||
[Tooltip("目标场景名称(Build Settings 中的 Scene name)")]
|
||||
public string sceneName;
|
||||
|
||||
public override bool IsMet(SaveData save)
|
||||
=> save?.World != null && save.World.VisitedScenes.Contains(sceneName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8bc9a9ed170dcd143bdd0fedd52eaf3f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,22 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Save;
|
||||
|
||||
namespace BaseGames.Progression
|
||||
{
|
||||
/// <summary>
|
||||
/// 由事件触发的成就条件(使用 World.Switches 中的布尔标志位)。
|
||||
/// 由代码(如剧情节点、NPC 交互)写入标志,AchievementManager 轮询检查。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Achievement/Condition/EventTriggered", fileName = "COND_EventTriggered_")]
|
||||
public class EventTriggeredCondition : AchievementCondition
|
||||
{
|
||||
[Tooltip("World.Switches 中用于标记此事件发生的布尔 Key")]
|
||||
public string flagKey;
|
||||
|
||||
public override bool IsMet(SaveData save)
|
||||
{
|
||||
if (save?.World?.Switches == null || string.IsNullOrEmpty(flagKey)) return false;
|
||||
return save.World.Switches.TryGetValue(flagKey, out bool val) && val;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1362775eb8b474f449a27210400b7e58
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,22 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Save;
|
||||
|
||||
namespace BaseGames.Progression
|
||||
{
|
||||
/// <summary>地图探索率(已探索房间数)的成就条件。</summary>
|
||||
[CreateAssetMenu(menuName = "Achievement/Condition/MapExploration", fileName = "COND_MapExploration_")]
|
||||
public class MapExplorationCondition : AchievementCondition
|
||||
{
|
||||
[Tooltip("需要探索的最少房间数量")]
|
||||
[Min(1)] public int requiredRoomCount = 1;
|
||||
|
||||
public override bool IsMet(SaveData save)
|
||||
=> save?.Map != null && save.Map.ExploredRooms.Count >= requiredRoomCount;
|
||||
|
||||
public override float GetProgress(SaveData save)
|
||||
{
|
||||
if (save?.Map == null || requiredRoomCount <= 0) return 0f;
|
||||
return UnityEngine.Mathf.Clamp01((float)save.Map.ExploredRooms.Count / requiredRoomCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a3ed424ac2b64bf48b89ca798a633fec
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,32 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Save;
|
||||
|
||||
namespace BaseGames.Progression
|
||||
{
|
||||
/// <summary>
|
||||
/// 累计钉击碰撞(NailClash)次数的成就条件。
|
||||
/// 使用 Stats.SkillUseCounts["NailClash"] 追踪(由 CombatSystem 写入)。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Achievement/Condition/NailClashCount", fileName = "COND_NailClashCount_")]
|
||||
public class NailClashCountCondition : AchievementCondition
|
||||
{
|
||||
public const string NailClashKey = "NailClash";
|
||||
|
||||
[Tooltip("需要累计触发钉击碰撞的次数")]
|
||||
[Min(1)] public int requiredCount = 5;
|
||||
|
||||
public override bool IsMet(SaveData save)
|
||||
{
|
||||
if (save?.Stats?.SkillUseCounts == null) return false;
|
||||
save.Stats.SkillUseCounts.TryGetValue(NailClashKey, out int count);
|
||||
return count >= requiredCount;
|
||||
}
|
||||
|
||||
public override float GetProgress(SaveData save)
|
||||
{
|
||||
if (save?.Stats?.SkillUseCounts == null || requiredCount <= 0) return 0f;
|
||||
save.Stats.SkillUseCounts.TryGetValue(NailClashKey, out int count);
|
||||
return UnityEngine.Mathf.Clamp01((float)count / requiredCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ba114c279c716d44797693ddf74474d1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
29
Assets/Scripts/Progression/Achievement/NoHealRunCondition.cs
Normal file
29
Assets/Scripts/Progression/Achievement/NoHealRunCondition.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Save;
|
||||
|
||||
namespace BaseGames.Progression
|
||||
{
|
||||
/// <summary>
|
||||
/// 无治疗通关成就条件。
|
||||
/// 依赖 World.Switches 中的标志位:AchievementManager 在玩家治疗时写入 noHeal_failed=true。
|
||||
/// 满足条件:Boss 已击败 且 无治疗标志未被置入失败状态。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Achievement/Condition/NoHealRun", fileName = "COND_NoHealRun_")]
|
||||
public class NoHealRunCondition : AchievementCondition
|
||||
{
|
||||
[Tooltip("追踪的 Boss ID(击败此 Boss 时检查是否治疗过)")]
|
||||
public string targetBossId;
|
||||
|
||||
[Tooltip("Switches 中记录「已治疗」失败状态的 Key(由 AchievementManager 设置)")]
|
||||
public string healFailFlagKey;
|
||||
|
||||
public override bool IsMet(SaveData save)
|
||||
{
|
||||
if (save?.World == null) return false;
|
||||
// Boss 已击败 且 未触发治疗失败标志
|
||||
bool bossDefeated = save.World.DefeatedBossIds.Contains(targetBossId);
|
||||
bool didHeal = save.World.Switches.TryGetValue(healFailFlagKey, out bool val) && val;
|
||||
return bossDefeated && !didHeal;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 97aec3097b5445b4ea79a691a0c068c1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,22 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Save;
|
||||
|
||||
namespace BaseGames.Progression
|
||||
{
|
||||
/// <summary>累计弹反成功次数的成就条件(使用 Stats.ParrySuccess)。</summary>
|
||||
[CreateAssetMenu(menuName = "Achievement/Condition/ParryCount", fileName = "COND_ParryCount_")]
|
||||
public class ParryCountCondition : AchievementCondition
|
||||
{
|
||||
[Tooltip("需要累计成功弹反的次数")]
|
||||
[Min(1)] public int requiredCount = 10;
|
||||
|
||||
public override bool IsMet(SaveData save)
|
||||
=> save?.Stats != null && save.Stats.ParrySuccess >= requiredCount;
|
||||
|
||||
public override float GetProgress(SaveData save)
|
||||
{
|
||||
if (save?.Stats == null || requiredCount <= 0) return 0f;
|
||||
return UnityEngine.Mathf.Clamp01((float)save.Stats.ParrySuccess / requiredCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3b6049404747c16438050523fac4a5e8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,23 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Save;
|
||||
|
||||
namespace BaseGames.Progression
|
||||
{
|
||||
/// <summary>在指定时间内击败 Boss 的成就条件(使用 ChallengeRooms.Records 的 BestTime)。</summary>
|
||||
[CreateAssetMenu(menuName = "Achievement/Condition/TimedBossKill", fileName = "COND_TimedBossKill_")]
|
||||
public class TimedBossKillCondition : AchievementCondition
|
||||
{
|
||||
[Tooltip("Boss 的 ChallengeRoom 记录 ID(与 ChallengeRoomRecord key 匹配)")]
|
||||
public string bossRoomId;
|
||||
|
||||
[Tooltip("需要在此秒数内完成击败(含)")]
|
||||
public float maxSeconds = 60f;
|
||||
|
||||
public override bool IsMet(SaveData save)
|
||||
{
|
||||
if (save?.ChallengeRooms?.Records == null) return false;
|
||||
if (!save.ChallengeRooms.Records.TryGetValue(bossRoomId, out var record)) return false;
|
||||
return record.BestTime > 0f && record.BestTime <= maxSeconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fee1576a0fc522045bf2986c1e266fd2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,38 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Save;
|
||||
using BaseGames.Player;
|
||||
|
||||
namespace BaseGames.Progression
|
||||
{
|
||||
/// <summary>解锁所有指定能力的成就条件(位掩码检查)。</summary>
|
||||
[CreateAssetMenu(menuName = "Achievement/Condition/UnlockedAllAbilities", fileName = "COND_UnlockedAllAbilities")]
|
||||
public class UnlockedAllAbilitiesCondition : AchievementCondition
|
||||
{
|
||||
[Tooltip("需要全部解锁的能力组合(位掩码)")]
|
||||
public AbilityType requiredAbilities = AbilityType.AllMovement;
|
||||
|
||||
public override bool IsMet(SaveData save)
|
||||
{
|
||||
if (save?.Player == null) return false;
|
||||
var flags = (AbilityType)save.Player.AbilityFlags;
|
||||
return (flags & requiredAbilities) == requiredAbilities;
|
||||
}
|
||||
|
||||
public override float GetProgress(SaveData save)
|
||||
{
|
||||
if (save?.Player == null) return 0f;
|
||||
var flags = (AbilityType)save.Player.AbilityFlags;
|
||||
int required = 0, met = 0;
|
||||
for (int i = 0; i < 32; i++)
|
||||
{
|
||||
var bit = (AbilityType)(1u << i);
|
||||
if ((requiredAbilities & bit) != 0)
|
||||
{
|
||||
required++;
|
||||
if ((flags & bit) != 0) met++;
|
||||
}
|
||||
}
|
||||
return required > 0 ? (float)met / required : 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2aa2ebe291ae4ef4fbea3251af89f70f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
18
Assets/Scripts/Progression/AchievementCondition.cs
Normal file
18
Assets/Scripts/Progression/AchievementCondition.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Save;
|
||||
|
||||
namespace BaseGames.Progression
|
||||
{
|
||||
/// <summary>
|
||||
/// 成就触发条件基类(架构 16_SupportingModules §2.3)。
|
||||
/// 每个子类对应一种具体触发逻辑,由 AchievementManager 驱动轮询或事件检查。
|
||||
/// </summary>
|
||||
public abstract class AchievementCondition : ScriptableObject
|
||||
{
|
||||
/// <summary>检查条件是否在当前存档数据中已满足。</summary>
|
||||
public abstract bool IsMet(SaveData save);
|
||||
|
||||
/// <summary>获取当前进度(0-1),用于成就 UI 进度条显示。返回 -1 表示不支持进度。</summary>
|
||||
public virtual float GetProgress(SaveData save) => IsMet(save) ? 1f : 0f;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Progression/AchievementCondition.cs.meta
Normal file
11
Assets/Scripts/Progression/AchievementCondition.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 36a7aeb027b24524f986b350e22732e9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
170
Assets/Scripts/Progression/AchievementManager.cs
Normal file
170
Assets/Scripts/Progression/AchievementManager.cs
Normal file
@@ -0,0 +1,170 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Save;
|
||||
using BaseGames.Platform;
|
||||
|
||||
namespace BaseGames.Progression
|
||||
{
|
||||
/// <summary>
|
||||
/// 成就运行时状态(不存档,仅运行时追踪)。
|
||||
/// </summary>
|
||||
public class AchievementRuntimeState
|
||||
{
|
||||
public AchievementSO Achievement;
|
||||
public bool IsUnlocked;
|
||||
public float Progress; // 0-1,用于 UI 进度条
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 成就管理器(架构 16_SupportingModules §2.3)。
|
||||
/// 负责加载所有 AchievementSO、轮询条件、解锁成就并同步到平台服务。
|
||||
/// </summary>
|
||||
public class AchievementManager : MonoBehaviour, ISaveable, IAchievementService
|
||||
{
|
||||
[Header("成就资产")]
|
||||
[Tooltip("将所有 AchievementSO 拖入此列表")]
|
||||
[SerializeField] private AchievementSO[] _achievements;
|
||||
|
||||
[Header("事件频道")]
|
||||
[SerializeField] private AchievementEventChannelSO _onAchievementUnlocked;
|
||||
|
||||
// ── 运行时状态 ─────────────────────────────────────────────────────────
|
||||
private readonly Dictionary<string, AchievementRuntimeState> _states = new();
|
||||
private SaveData _saveRef;
|
||||
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (ServiceLocator.GetOrDefault<IAchievementService>() != null) { Destroy(gameObject); return; }
|
||||
ServiceLocator.Register<IAchievementService>(this);
|
||||
InitStates();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
ServiceLocator.Unregister<IAchievementService>(this);
|
||||
}
|
||||
|
||||
private void InitStates()
|
||||
{
|
||||
_states.Clear();
|
||||
if (_achievements == null) return;
|
||||
foreach (var ach in _achievements)
|
||||
{
|
||||
if (ach == null || string.IsNullOrEmpty(ach.achievementId)) continue;
|
||||
_states[ach.achievementId] = new AchievementRuntimeState
|
||||
{
|
||||
Achievement = ach,
|
||||
IsUnlocked = false,
|
||||
Progress = 0f,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ── ISaveable ─────────────────────────────────────────────────────────
|
||||
public void OnSave(SaveData saveData)
|
||||
{
|
||||
if (saveData?.Achievements == null) return;
|
||||
saveData.Achievements.Unlocked.Clear();
|
||||
saveData.Achievements.Progress.Clear();
|
||||
|
||||
foreach (var kv in _states)
|
||||
{
|
||||
if (kv.Value.IsUnlocked)
|
||||
saveData.Achievements.Unlocked.Add(kv.Key);
|
||||
else if (kv.Value.Progress > 0f)
|
||||
saveData.Achievements.Progress[kv.Key] = new AchievementProgress { Percent = kv.Value.Progress };
|
||||
}
|
||||
}
|
||||
|
||||
public void OnLoad(SaveData saveData)
|
||||
{
|
||||
if (saveData?.Achievements == null) return;
|
||||
|
||||
foreach (var id in saveData.Achievements.Unlocked)
|
||||
{
|
||||
if (_states.TryGetValue(id, out var state))
|
||||
state.IsUnlocked = true;
|
||||
}
|
||||
foreach (var kv in saveData.Achievements.Progress)
|
||||
{
|
||||
if (_states.TryGetValue(kv.Key, out var state))
|
||||
state.Progress = kv.Value.Percent;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 轮询检查 ──────────────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// 使用最新存档数据检查所有未解锁成就的条件。
|
||||
/// 由 SaveManager.AfterLoad 或外部定期调用(例如每次进入房间)。
|
||||
/// </summary>
|
||||
public void EvaluateAll(SaveData save)
|
||||
{
|
||||
_saveRef = save;
|
||||
foreach (var state in _states.Values)
|
||||
{
|
||||
if (state.IsUnlocked) continue;
|
||||
EvaluateSingle(state, save);
|
||||
}
|
||||
}
|
||||
|
||||
private void EvaluateSingle(AchievementRuntimeState state, SaveData save)
|
||||
{
|
||||
if (state.Achievement.conditions == null || state.Achievement.conditions.Length == 0) return;
|
||||
|
||||
bool allMet = true;
|
||||
float totalProgress = 0f;
|
||||
|
||||
foreach (var cond in state.Achievement.conditions)
|
||||
{
|
||||
if (cond == null) continue;
|
||||
if (!cond.IsMet(save)) { allMet = false; }
|
||||
totalProgress += cond.GetProgress(save);
|
||||
}
|
||||
|
||||
int condCount = state.Achievement.conditions.Length;
|
||||
state.Progress = condCount > 0 ? totalProgress / condCount : 0f;
|
||||
|
||||
if (allMet)
|
||||
Unlock(state);
|
||||
}
|
||||
|
||||
// ── 主动解锁 ──────────────────────────────────────────────────────────
|
||||
/// <summary>直接解锁成就(用于剧情触发等无条件解锁场景)。</summary>
|
||||
public void UnlockById(string achievementId)
|
||||
{
|
||||
if (_states.TryGetValue(achievementId, out var state) && !state.IsUnlocked)
|
||||
Unlock(state);
|
||||
}
|
||||
|
||||
private void Unlock(AchievementRuntimeState state)
|
||||
{
|
||||
state.IsUnlocked = true;
|
||||
state.Progress = 1f;
|
||||
_onAchievementUnlocked?.Raise(state.Achievement);
|
||||
|
||||
#if STEAMWORKS_NET
|
||||
ServiceLocator.Get<IPlatformService>()?.UnlockAchievement(state.Achievement.achievementId);
|
||||
#endif
|
||||
|
||||
// 若成就授予凹槽,写入存档数值
|
||||
if (state.Achievement.grantsNotch && _saveRef != null)
|
||||
{
|
||||
_saveRef.Equipment.MaxNotches++;
|
||||
}
|
||||
|
||||
Debug.Log($"[Achievement] 解锁:{state.Achievement.displayName} ({state.Achievement.achievementId})");
|
||||
}
|
||||
|
||||
// ── 查询接口 ──────────────────────────────────────────────────────────
|
||||
public bool IsUnlocked(string id)
|
||||
=> _states.TryGetValue(id, out var s) && s.IsUnlocked;
|
||||
|
||||
public float GetProgress(string id)
|
||||
=> _states.TryGetValue(id, out var s) ? s.Progress : 0f;
|
||||
|
||||
public IEnumerable<AchievementRuntimeState> GetAllStates()
|
||||
=> _states.Values;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Progression/AchievementManager.cs.meta
Normal file
11
Assets/Scripts/Progression/AchievementManager.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 81e573a1b9e4d964f9d595e14aee9928
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,27 +1,49 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.Progression
|
||||
{
|
||||
public enum AchievementType { Story, Collection, Challenge, Hidden }
|
||||
public enum AchievementTier { Bronze, Silver, Gold }
|
||||
|
||||
/// <summary>
|
||||
/// 成就数据 ScriptableObject(架构 16_SupportingModules §2.3)。
|
||||
/// Phase 0 骨架:仅包含基础标识字段,Phase 4 扩充完整逻辑。
|
||||
/// 包含显示信息、类型/等级分类、触发条件列表及奖励配置。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Progression/Achievement", fileName = "ACH_")]
|
||||
[CreateAssetMenu(menuName = "Achievement/Achievement", fileName = "ACH_")]
|
||||
public class AchievementSO : ScriptableObject
|
||||
{
|
||||
[Header("标识")]
|
||||
[Tooltip("平台成就唯一标识符(与 Steam/平台后端同步)")]
|
||||
public string achievementId;
|
||||
|
||||
[Header("显示")]
|
||||
[Tooltip("成就显示名称")]
|
||||
public string displayName;
|
||||
|
||||
[TextArea(2, 4)]
|
||||
[Tooltip("成就描述")]
|
||||
[Tooltip("成就描述(解锁后展示)")]
|
||||
public string description;
|
||||
|
||||
[Tooltip("成就图标")]
|
||||
public Sprite icon;
|
||||
}
|
||||
[TextArea(2, 4)]
|
||||
[Tooltip("隐藏成就未解锁时展示的描述,留空则显示 ???")]
|
||||
public string hiddenDescription;
|
||||
|
||||
[Tooltip("成就图标(解锁后)")]
|
||||
public Sprite icon;
|
||||
|
||||
[Tooltip("隐藏成就未解锁时的占位图标")]
|
||||
public Sprite hiddenIcon;
|
||||
|
||||
[Header("分类")]
|
||||
public AchievementType type = AchievementType.Story;
|
||||
public AchievementTier tier = AchievementTier.Bronze;
|
||||
|
||||
[Header("触发条件")]
|
||||
[Tooltip("所有条件同时满足时触发解锁;留空则仅通过代码直接解锁")]
|
||||
public AchievementCondition[] conditions;
|
||||
|
||||
[Header("奖励")]
|
||||
[Tooltip("解锁此成就是否同时授予凹槽(Notch)")]
|
||||
public bool grantsNotch;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,10 @@
|
||||
"references": [
|
||||
"BaseGames.Core",
|
||||
"BaseGames.Core.Events",
|
||||
"BaseGames.Player"
|
||||
"BaseGames.Player",
|
||||
"BaseGames.Core.Save",
|
||||
"BaseGames.Input",
|
||||
"BaseGames.Platform"
|
||||
],
|
||||
"autoReferenced": true,
|
||||
"overrideReferences": false,
|
||||
|
||||
33
Assets/Scripts/Progression/BossProgressTracker.cs
Normal file
33
Assets/Scripts/Progression/BossProgressTracker.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.Progression
|
||||
{
|
||||
/// <summary>
|
||||
/// Boss 进程追踪器(架构 09_ProgressionModule §13)。
|
||||
/// 挂载在 Boss 房间的 BossTrigger 同一对象上。
|
||||
/// 监听 _onBossDefeated 事件,路由到 SaveSystem 专用频道(零耦合)。
|
||||
/// </summary>
|
||||
public class BossProgressTracker : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private string _bossId; // 如 "Boss_SpiderGuard"
|
||||
[SerializeField] private string[] _unlocksProgressLockIds; // 击败后应解锁的 ProgressLock ID(预留)
|
||||
|
||||
[Header("Event Channels")]
|
||||
[SerializeField] private StringEventChannelSO _onBossDefeated; // 监听(来自 BossCombat)
|
||||
[SerializeField] private StringEventChannelSO _onBossDefeatedForSave; // 广播 → SaveSystem
|
||||
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
private void OnEnable() => _onBossDefeated?.Subscribe(OnBossDefeated).AddTo(_subs);
|
||||
private void OnDisable() => _subs.Clear();
|
||||
|
||||
private void OnBossDefeated(string bossId)
|
||||
{
|
||||
if (bossId != _bossId) return;
|
||||
|
||||
// 通过事件频道通知 SaveSystem(SaveSystem 负责写入 data.World.DefeatedBossIds)
|
||||
_onBossDefeatedForSave?.Raise(bossId);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Progression/BossProgressTracker.cs.meta
Normal file
11
Assets/Scripts/Progression/BossProgressTracker.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 181e29092571b1441b1f13f187e49f80
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
67
Assets/Scripts/Progression/HPContainerPickup.cs
Normal file
67
Assets/Scripts/Progression/HPContainerPickup.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Input;
|
||||
|
||||
namespace BaseGames.Progression
|
||||
{
|
||||
/// <summary>
|
||||
/// HP 容器拾取物(架构 09_ProgressionModule §14)。
|
||||
/// 永久 MaxHP +2 的可拾取物件,拾取后通过事件频道通知 SaveSystem。
|
||||
/// 利用 SaveManager.Data.World.CollectedIds 防止重复拾取。
|
||||
/// </summary>
|
||||
public class HPContainerPickup : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private string _collectibleId; // 存档用唯一 ID
|
||||
[SerializeField] private InputReaderSO _inputReader;
|
||||
|
||||
[Header("Event Channels")]
|
||||
[SerializeField] private StringEventChannelSO _onMaxHPContainerPickedUp; // → SaveSystem
|
||||
[SerializeField] private IntEventChannelSO _onMaxHPChanged; // → HUDController
|
||||
|
||||
private bool _pickedUp;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// 若本局已拾取(存档),直接隐藏
|
||||
if (!string.IsNullOrEmpty(_collectibleId) &&
|
||||
ServiceLocator.GetOrDefault<ISaveService>()?.IsWorldCollected(_collectibleId) == true)
|
||||
{
|
||||
gameObject.SetActive(false);
|
||||
_pickedUp = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTriggerEnter2D(Collider2D other)
|
||||
{
|
||||
if (_pickedUp || !other.CompareTag("Player")) return;
|
||||
if (!string.IsNullOrEmpty(_collectibleId) &&
|
||||
ServiceLocator.GetOrDefault<ISaveService>()?.IsWorldCollected(_collectibleId) == true) return;
|
||||
StartCoroutine(PickupSequence());
|
||||
}
|
||||
|
||||
private IEnumerator PickupSequence()
|
||||
{
|
||||
_pickedUp = true;
|
||||
|
||||
// 禁用输入(切换到 UI 模式)
|
||||
_inputReader?.EnableUIInput();
|
||||
gameObject.SetActive(false);
|
||||
|
||||
// 等待特效播放(Feel MMF_Player 可通过外部引用补充)
|
||||
yield return new WaitForSeconds(0.8f);
|
||||
|
||||
// 零耦合:通过事件频道通知 SaveSystem(SaveSystem 负责 MaxHP+2 及写入 CollectedIds)
|
||||
_onMaxHPContainerPickedUp?.Raise(_collectibleId);
|
||||
|
||||
// 通知 HUD 更新(SaveSystem 写入后应重新查询 PlayerStats.MaxHP)
|
||||
_onMaxHPChanged?.Raise((ServiceLocator.GetOrDefault<ISaveService>()?.GetPlayerMaxHP() ?? 0) + 2);
|
||||
|
||||
yield return new WaitForSeconds(0.5f);
|
||||
|
||||
// 恢复输入
|
||||
_inputReader?.EnableGameplayInput();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Progression/HPContainerPickup.cs.meta
Normal file
11
Assets/Scripts/Progression/HPContainerPickup.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3b45d419905072941ab5a54669b4ce48
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
22
Assets/Scripts/Progression/IAchievementService.cs
Normal file
22
Assets/Scripts/Progression/IAchievementService.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using BaseGames.Core.Save;
|
||||
|
||||
namespace BaseGames.Progression
|
||||
{
|
||||
/// <summary>
|
||||
/// 成就服务接口。通过 ServiceLocator 注册,供外部系统查询和解锁成就。
|
||||
/// </summary>
|
||||
public interface IAchievementService
|
||||
{
|
||||
/// <summary>使用最新存档数据检查所有未解锁成就的条件。</summary>
|
||||
void EvaluateAll(SaveData save);
|
||||
|
||||
/// <summary>直接解锁成就(用于剧情触发等无条件解锁场景)。</summary>
|
||||
void UnlockById(string achievementId);
|
||||
|
||||
/// <summary>查询指定成就是否已解锁。</summary>
|
||||
bool IsUnlocked(string achievementId);
|
||||
|
||||
/// <summary>获取指定成就的进度(0–1)。</summary>
|
||||
float GetProgress(string achievementId);
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Progression/IAchievementService.cs.meta
Normal file
11
Assets/Scripts/Progression/IAchievementService.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5885c6a493dfdf340897e63689e0bc7c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
70
Assets/Scripts/Progression/ProgressLock.cs
Normal file
70
Assets/Scripts/Progression/ProgressLock.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.Progression
|
||||
{
|
||||
/// <summary>
|
||||
/// 进程锁(架构 09_ProgressionModule §12)。
|
||||
/// 单向/永久性阻挡,需满足特定条件(击败 Boss)才能解锁。
|
||||
/// 通过 ServiceLocator.GetOrDefault<SaveManager>() 读取进度,订阅 BossDefeated 事件实时响应。
|
||||
/// </summary>
|
||||
public class ProgressLock : MonoBehaviour
|
||||
{
|
||||
[Header("解锁条件")]
|
||||
[SerializeField] private string _requiredBossId; // 空 = 不检查 Boss
|
||||
[SerializeField] private string _requiredItemId; // 空 = 不检查道具(P1 预留)
|
||||
|
||||
[Header("物理表现")]
|
||||
[SerializeField] private GameObject _lockedVisuals; // 锁住状态视觉
|
||||
[SerializeField] private GameObject _unlockedVisuals; // 开启状态视觉(可 null)
|
||||
[SerializeField] private Collider2D _blockCollider;
|
||||
|
||||
[Header("存档")]
|
||||
[SerializeField] private string _lockId; // 唯一 ID,存档记录开启状态
|
||||
|
||||
[Header("Event Channels")]
|
||||
[SerializeField] private StringEventChannelSO _onBossDefeated; // EVT_BossDefeated
|
||||
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
private void Start()
|
||||
{
|
||||
ApplyState(CheckUnlocked());
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_onBossDefeated?.Subscribe(OnBossDefeated).AddTo(_subs);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
_subs.Clear();
|
||||
}
|
||||
|
||||
private void OnBossDefeated(string bossId)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_requiredBossId) && bossId != _requiredBossId) return;
|
||||
if (CheckUnlocked()) ApplyState(true);
|
||||
}
|
||||
|
||||
private bool CheckUnlocked()
|
||||
{
|
||||
var sm = ServiceLocator.GetOrDefault<ISaveService>();
|
||||
if (sm == null) return false;
|
||||
|
||||
if (!string.IsNullOrEmpty(_requiredBossId) && !sm.IsBossDefeated(_requiredBossId))
|
||||
return false;
|
||||
|
||||
return string.IsNullOrEmpty(_lockId) || sm.IsDoorOpened(_lockId);
|
||||
}
|
||||
|
||||
private void ApplyState(bool unlocked)
|
||||
{
|
||||
if (_blockCollider != null) _blockCollider.enabled = !unlocked;
|
||||
if (_lockedVisuals != null) _lockedVisuals.SetActive(!unlocked);
|
||||
if (_unlockedVisuals != null) _unlockedVisuals.SetActive(unlocked);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Progression/ProgressLock.cs.meta
Normal file
11
Assets/Scripts/Progression/ProgressLock.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 489eda6d4db94f0418a0229a79d4e2e1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
33
Assets/Scripts/Progression/RegionDefinitionSO.cs
Normal file
33
Assets/Scripts/Progression/RegionDefinitionSO.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Player;
|
||||
|
||||
namespace BaseGames.Progression
|
||||
{
|
||||
/// <summary>
|
||||
/// 区域定义 SO(架构 09_ProgressionModule §11)。
|
||||
/// 集中管理区域元数据:解锁条件、关联场景、地图展示数据。
|
||||
/// 资产路径: Assets/ScriptableObjects/Progression/Regions/Region_{RegionId}.asset
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Progression/RegionDefinition")]
|
||||
public class RegionDefinitionSO : ScriptableObject
|
||||
{
|
||||
[Header("Identity")]
|
||||
public string regionId; // 如 "Cave"(与 AudioZone.regionId 一致)
|
||||
public string displayName; // 如 "腐蚀洞穴"
|
||||
|
||||
[Header("Map")]
|
||||
public Color mapColor;
|
||||
public Sprite mapIconSprite; // P1:地图图标
|
||||
|
||||
[Header("解锁条件")]
|
||||
[Tooltip("击败指定 Boss 后解锁;留空 = 无条件")]
|
||||
public string requiredBossDefeated;
|
||||
[Tooltip("需持有指定能力;None = 无要求")]
|
||||
public AbilityType requiredAbility;
|
||||
|
||||
[Header("关联房间")]
|
||||
public string[] roomSceneNames; // 该区域包含的所有场景名
|
||||
public string bossSceneName; // Boss 房间场景名
|
||||
public string entrySceneName; // 从外部进入该区域的第一个房间
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Progression/RegionDefinitionSO.cs.meta
Normal file
11
Assets/Scripts/Progression/RegionDefinitionSO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 73a7d9e5f9f70e34b9ec917b107a442c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user