多轮审查和修复

This commit is contained in:
2026-05-12 15:34:08 +08:00
parent f55d2a57c3
commit ebbbb7332e
805 changed files with 838724 additions and 1905 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: daf4df072f7275047b2976edac77576e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7705d5b84ab1d90479faba8e44b35026
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8209dfbb9f9b3a8488930367b42d3ec9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fd55de95ca7807c4992067e47a5cd8b1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 37994d104d819474285eb6cee592425b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8bc9a9ed170dcd143bdd0fedd52eaf3f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1362775eb8b474f449a27210400b7e58
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a3ed424ac2b64bf48b89ca798a633fec
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ba114c279c716d44797693ddf74474d1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 97aec3097b5445b4ea79a691a0c068c1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3b6049404747c16438050523fac4a5e8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fee1576a0fc522045bf2986c1e266fd2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2aa2ebe291ae4ef4fbea3251af89f70f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 36a7aeb027b24524f986b350e22732e9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 81e573a1b9e4d964f9d595e14aee9928
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
}
}

View File

@@ -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,

View 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;
// 通过事件频道通知 SaveSystemSaveSystem 负责写入 data.World.DefeatedBossIds
_onBossDefeatedForSave?.Raise(bossId);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 181e29092571b1441b1f13f187e49f80
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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);
// 零耦合:通过事件频道通知 SaveSystemSaveSystem 负责 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();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3b45d419905072941ab5a54669b4ce48
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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>获取指定成就的进度01。</summary>
float GetProgress(string achievementId);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5885c6a493dfdf340897e63689e0bc7c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,70 @@
using UnityEngine;
using BaseGames.Core;
using BaseGames.Core.Events;
namespace BaseGames.Progression
{
/// <summary>
/// 进程锁(架构 09_ProgressionModule §12
/// 单向/永久性阻挡,需满足特定条件(击败 Boss才能解锁。
/// 通过 ServiceLocator.GetOrDefault&lt;SaveManager&gt;() 读取进度,订阅 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);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 489eda6d4db94f0418a0229a79d4e2e1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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; // 从外部进入该区域的第一个房间
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 73a7d9e5f9f70e34b9ec917b107a442c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: