多轮审查和修复

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,22 @@
{
"excludePlatforms": [],
"allowUnsafeCode": false,
"precompiledReferences": [],
"name": "BaseGames.Quest",
"defineConstraints": [],
"noEngineReferences": false,
"versionDefines": [],
"rootNamespace": "BaseGames.Quest",
"references": [
"BaseGames.Core.Events",
"BaseGames.Core.Save",
"BaseGames.Player",
"BaseGames.World",
"BaseGames.Enemies",
"BaseGames.Dialogue",
"Unity.Addressables"
],
"autoReferenced": true,
"overrideReferences": false,
"includePlatforms": []
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 4af005f1c262d81418344eb0fd372871
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,24 @@
using System;
using UnityEngine;
namespace BaseGames.Challenge
{
/// <summary>
/// Boss Rush 序列数据(架构 22_QuestChallengeModule §11
/// 资产路径: Assets/ScriptableObjects/Challenge/BossRush_{challengeId}.asset
/// </summary>
[CreateAssetMenu(menuName = "Challenge/BossRushSequence")]
public class BossRushSequenceSO : ScriptableObject
{
[Serializable]
public struct BossEntry
{
public string bossSceneName; // Boss 所在场景Additive 加载)
public string bossId;
[Range(0f, 1f)]
public float hpRestoreRatio; // 击败本 Boss 后玩家恢复 HP 比例(默认 0.3
}
public BossEntry[] bosses;
}
}

View File

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

View File

@@ -0,0 +1,25 @@
using System;
using UnityEngine;
namespace BaseGames.Challenge
{
/// <summary>
/// 单波敌人配置(架构 22_QuestChallengeModule §10
/// 资产路径: Assets/ScriptableObjects/Challenge/ENC_{challengeId}_{wave}.asset
/// </summary>
[CreateAssetMenu(menuName = "Challenge/Encounter")]
public class ChallengeEncounterSO : ScriptableObject
{
[Serializable]
public struct SpawnEntry
{
[Tooltip("Addressables key")]
public string enemyAddressKey;
public Transform spawnPoint;
public int count;
}
public SpawnEntry[] enemies;
public float waveDelay; // 上波清空后等待多少秒生成本波
}
}

View File

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

View File

@@ -0,0 +1,138 @@
using System.Collections;
using UnityEngine;
using UnityEngine.AddressableAssets;
using BaseGames.Core;
using BaseGames.Core.Events;
using BaseGames.Enemies;
using BaseGames.Player;
namespace BaseGames.Challenge
{
/// <summary>
/// 挑战房间流程管理器(架构 22_QuestChallengeModule §12
/// 挂在挑战房间场景的 [ChallengeManager] GameObject 上,场景加载时自动启动挑战。
/// </summary>
public class ChallengeRoomManager : MonoBehaviour
{
[SerializeField] private ChallengeRoomSO _challengeData;
[SerializeField] private PlayerStats _player; // 由场景 Inspector 绑定
[Header("Event Channels")]
[SerializeField] private StringEventChannelSO _onChallengeCompleted; // EVT_ChallengeCompleted
[SerializeField] private StringEventChannelSO _onChallengeFailed; // EVT_ChallengeFailed
private int _currentEncounterIndex;
private int _remainingEnemies;
private float _elapsedTime;
private bool _isRunning;
private bool _noHitViolated; // 架构 §12requireNoHit 挑战是否被破坏
private void OnEnable()
{
if (_player != null) _player.OnDamaged += OnPlayerDamaged;
}
private void OnDisable()
{
if (_player != null) _player.OnDamaged -= OnPlayerDamaged;
}
private void OnPlayerDamaged() => _noHitViolated = true;
private void Start() => StartChallenge();
private void Update()
{
if (!_isRunning) return;
_elapsedTime += Time.deltaTime;
if (_challengeData.timeLimit > 0 && _elapsedTime >= _challengeData.timeLimit)
FailChallenge();
}
private void StartChallenge()
{
// 自动快速存档(失败后读档返回挑战入口)
ServiceLocator.GetOrDefault<ISaveService>()?.QuickSave();
_isRunning = true;
_currentEncounterIndex = 0;
_elapsedTime = 0f;
_noHitViolated = false;
SpawnWave(0);
}
private void SpawnWave(int index)
{
if (_challengeData.encounters == null || index >= _challengeData.encounters.Length)
{
CompleteChallenge();
return;
}
var enc = _challengeData.encounters[index];
_remainingEnemies = 0;
foreach (var entry in enc.enemies)
{
for (int i = 0; i < entry.count; i++)
{
_remainingEnemies++;
Vector3 pos = entry.spawnPoint != null ? entry.spawnPoint.position : Vector3.zero;
Addressables.InstantiateAsync(entry.enemyAddressKey, pos, Quaternion.identity)
.Completed += handle =>
{
if (handle.Result != null &&
handle.Result.TryGetComponent<EnemyBase>(out var enemy))
{
enemy.OnDied += OnEnemyDefeated;
}
};
}
}
}
private void OnEnemyDefeated()
{
_remainingEnemies = Mathf.Max(0, _remainingEnemies - 1);
if (_remainingEnemies > 0) return;
_currentEncounterIndex++;
if (_currentEncounterIndex >= _challengeData.encounters.Length)
CompleteChallenge();
else
StartCoroutine(DelayedNextWave(_challengeData.encounters[_currentEncounterIndex].waveDelay));
}
private IEnumerator DelayedNextWave(float delay)
{
yield return new WaitForSeconds(delay);
SpawnWave(_currentEncounterIndex);
}
private void CompleteChallenge()
{
_isRunning = false;
// requireNoHit 挑战:受到伤害则判定失败(架构 §12
if (_challengeData.requireNoHit && _noHitViolated)
{
FailChallenge();
return;
}
var reward = ServiceLocator.GetOrDefault<ISaveService>() is { } sm && sm.IsFirstClear(_challengeData.challengeId)
? _challengeData.firstClearReward
: _challengeData.repeatedReward;
reward?.Apply(_player);
_onChallengeCompleted?.Raise(_challengeData.challengeId);
}
private void FailChallenge()
{
_isRunning = false;
_onChallengeFailed?.Raise(_challengeData.challengeId);
ServiceLocator.GetOrDefault<ISaveService>()?.QuickLoad();
}
}
}

View File

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

View File

@@ -0,0 +1,38 @@
using System;
using UnityEngine;
namespace BaseGames.Challenge
{
/// <summary>
/// 挑战房间定义 SO架构 22_QuestChallengeModule §9
/// 资产路径: Assets/ScriptableObjects/Challenge/CHR_{challengeId}.asset
/// </summary>
[CreateAssetMenu(menuName = "Challenge/ChallengeRoom")]
public class ChallengeRoomSO : ScriptableObject
{
[Header("标识")]
public string challengeId;
public string displayName;
public ChallengeType challengeType;
[Header("波次(非 BossRush")]
public ChallengeEncounterSO[] encounters;
[Header("Boss RushBossRush 类型专用)")]
public BossRushSequenceSO bossRushSequence;
[Header("限制条件")]
public float timeLimit; // 0 = 无时限
public bool requireNoHit;
public int minComboRequired; // 0 = 无要求
[Header("奖励")]
public BaseGames.Quest.RewardSO firstClearReward; // 首次通关奖励
public BaseGames.Quest.RewardSO repeatedReward; // 重复通关奖励
[Header("解锁条件")]
public string[] prerequisiteBossIds; // 需击败的 Boss ID
}
public enum ChallengeType { Survival, TimeTrial, BossRush, NoHit }
}

View File

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

View File

@@ -0,0 +1,53 @@
using UnityEngine;
using BaseGames.Core;
using BaseGames.Core.Events;
using BaseGames.World;
namespace BaseGames.Challenge
{
/// <summary>
/// 挑战房间入口触发物(架构 22_QuestChallengeModule §13
/// 实现 IInteractable玩家交互后通过事件频道触发挑战场景加载。
/// ⚠️ SceneLoader 无 Instance通过 EVT_SceneLoadRequest 频道触发加载(架构 03 §3
/// </summary>
[RequireComponent(typeof(Collider2D))]
public class ChallengeRoomTrigger : MonoBehaviour, IInteractable
{
[SerializeField] private ChallengeRoomSO _challengeData;
[SerializeField] private string _challengeSceneName;
[SerializeField] private SceneLoadRequestEventChannelSO _onSceneLoadRequest; // EVT_SceneLoadRequest
// ── IInteractable ──────────────────────────────────────────────────
public string InteractPrompt => _challengeData != null
? $"进入挑战:{_challengeData.displayName}"
: "进入挑战";
public bool CanInteract => IsUnlocked();
public void Interact(Transform player)
{
if (!IsUnlocked()) return;
_onSceneLoadRequest?.Raise(new SceneLoadRequest
{
SceneName = _challengeSceneName,
EntryTransitionId = string.Empty,
ShowLoadingScreen = false,
IsRespawn = false,
});
}
public void OnPlayerEnterRange(Transform player) { }
public void OnPlayerExitRange() { }
// ── 私有 ─────────────────────────────────────────────────────────
private bool IsUnlocked()
{
if (_challengeData?.prerequisiteBossIds == null) return true;
var sm = ServiceLocator.GetOrDefault<ISaveService>();
if (sm == null) return false;
foreach (var bossId in _challengeData.prerequisiteBossIds)
if (!sm.IsBossDefeated(bossId)) return false;
return true;
}
}
}

View File

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

View File

@@ -0,0 +1,24 @@
using BaseGames.Player;
using QuestStateEnum = BaseGames.Core.Events.QuestState;
namespace BaseGames.Quest
{
/// <summary>
/// 任务管理器的公开契约。ServiceLocator.Get&lt;IQuestManager&gt;() 获取实例,
/// 避免外部代码直接依赖 QuestManager 具体类型。
/// </summary>
public interface IQuestManager
{
/// <summary>接取任务(幂等)。</summary>
void AcceptQuest(string questId);
/// <summary>完成任务并发放奖励。</summary>
void CompleteQuest(string questId, PlayerStats player);
/// <summary>返回当前任务状态。未知 questId 返回 Unavailable。</summary>
QuestStateEnum GetState(string questId);
/// <summary>判断任务是否满足完成条件。</summary>
bool IsReadyToComplete(string questId);
}
}

View File

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

View File

@@ -0,0 +1,82 @@
using UnityEngine;
using BaseGames.Core;
using BaseGames.Dialogue;
using BaseGames.Player;
using QuestStateEnum = BaseGames.Core.Events.QuestState;
using SL = BaseGames.Core.ServiceLocator;
namespace BaseGames.Quest
{
/// <summary>
/// 可发布/完成任务的 NPC架构 22_QuestChallengeModule §6
/// 继承 InteractableNPC根据任务状态切换对话版本在交互时处理任务接收/完成。
/// </summary>
public class QuestGiver : InteractableNPC
{
[Header("任务")]
[SerializeField] private QuestSO[] _offeredQuests; // 该 NPC 可提供的所有任务(按优先级排列)
[Header("对话版本(根据任务状态切换)")]
[SerializeField] private DialogueSequenceSO _availableDialogue; // 任务可接时
[SerializeField] private DialogueSequenceSO _activeDialogue; // 任务进行中
[SerializeField] private DialogueSequenceSO _readyDialogue; // 完成条件满足时
[SerializeField] private DialogueSequenceSO _completedDialogue; // 任务已完成后
// ── InteractableNPC 覆盖 ──────────────────────────────────────────────
protected override void Interact_Internal(Transform player)
{
var qm = SL.GetOrDefault<IQuestManager>();
var quest = GetCurrentQuest(qm);
if (quest == null || qm == null) return;
var state = qm.GetState(quest.questId);
if (state == QuestStateEnum.Available)
{
qm.AcceptQuest(quest.questId);
}
else if (qm.IsReadyToComplete(quest.questId))
{
// 直接从 player 获取 PlayerStats避免对 PlayerController 的程序集依赖
var stats = player.GetComponentInParent<PlayerStats>();
qm.CompleteQuest(quest.questId, stats);
}
}
protected override DialogueSequenceSO GetCurrentDialogue()
{
var qm = SL.GetOrDefault<IQuestManager>();
var quest = GetCurrentQuest(qm);
if (quest == null || qm == null) return base.GetCurrentDialogue();
var state = qm.GetState(quest.questId);
return state switch
{
QuestStateEnum.Available => _availableDialogue,
QuestStateEnum.Active => qm.IsReadyToComplete(quest.questId)
? _readyDialogue : _activeDialogue,
QuestStateEnum.Completed => _completedDialogue,
_ => base.GetCurrentDialogue(),
};
}
// ── 私有辅助 ─────────────────────────────────────────────────────────
/// <summary>返回当前处于 Available 或 Active 状态的第一个任务。</summary>
private QuestSO GetCurrentQuest(IQuestManager qm = null)
{
if (_offeredQuests == null) return null;
qm ??= SL.GetOrDefault<IQuestManager>();
if (qm == null) return null;
foreach (var q in _offeredQuests)
{
if (q == null) continue;
var s = qm.GetState(q.questId);
if (s == QuestStateEnum.Available || s == QuestStateEnum.Active) return q;
}
return null;
}
}
}

View File

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

View File

@@ -0,0 +1,270 @@
using System.Collections.Generic;
using UnityEngine;
using BaseGames.Core.Events;
using BaseGames.Core.Save;
using BaseGames.Player;
using QuestStateEnum = BaseGames.Core.Events.QuestState;
namespace BaseGames.Quest
{
/// <summary>
/// 运行时任务管理器(架构 22_QuestChallengeModule §5
/// 挂在 Persistent 场景 [GameManagers] 下。
/// 事件驱动追踪目标进度,不主动轮询。
/// 实现 ISaveable通过 SaveManager 持久化任务状态。
/// </summary>
public class QuestManager : MonoBehaviour, ISaveable, IQuestManager
{
// ── Inspector ────────────────────────────────────────────────────────
[SerializeField] private QuestSO[] _allQuests;
[Header("Event Channels监听")]
[SerializeField] private StringEventChannelSO _onEnemyDied; // EVT_EnemyDiedenemyId
[SerializeField] private StringEventChannelSO _onCollectiblePickup; // EVT_CollectiblePickupitemId
[SerializeField] private StringEventChannelSO _onSceneLoaded; // EVT_SceneLoadedsceneName
[SerializeField] private StringEventChannelSO _onNpcDialogueCompleted; // EVT_NpcDialogueCompletednpcId
[Header("Event Channels广播")]
[SerializeField] private StringEventChannelSO _onQuestStarted; // questId
[SerializeField] private StringEventChannelSO _onQuestCompleted; // questId
[SerializeField] private StringEventChannelSO _onQuestFailed; // questId
[SerializeField] private QuestObjectiveEventChannelSO _onObjectiveUpdated;
// ── Runtime State ────────────────────────────────────────────────────
private readonly Dictionary<string, QuestStateEnum> _questStates = new();
private readonly Dictionary<string, QuestObjectiveState> _objectiveStates = new();
/// <summary>questId → QuestSO 快速查找表(由 Awake 构建,将 GetQuestSO O(n) 降为 O(1))。</summary>
private Dictionary<string, QuestSO> _questIndex;
private readonly CompositeDisposable _subs = new();
public StringEventChannelSO OnQuestStarted => _onQuestStarted;
public StringEventChannelSO OnQuestCompleted => _onQuestCompleted;
/// <summary>供 SaveManager 迭代的任务状态字典(只读视图)。</summary>
public IReadOnlyDictionary<string, QuestStateEnum> QuestStates => _questStates;
private void Awake()
{
if (BaseGames.Core.ServiceLocator.GetOrDefault<IQuestManager>() != null) { Destroy(gameObject); return; }
BaseGames.Core.ServiceLocator.Register<IQuestManager>(this);
// 构建任务字典索引,将 GetQuestSO 变为 O(1)
_questIndex = new Dictionary<string, QuestSO>(_allQuests?.Length ?? 0);
if (_allQuests != null)
foreach (var q in _allQuests)
if (q != null && !string.IsNullOrEmpty(q.questId))
_questIndex[q.questId] = q;
}
private void OnEnable()
{
_onEnemyDied?.Subscribe(HandleEnemyDefeated).AddTo(_subs);
_onCollectiblePickup?.Subscribe(HandleItemCollected).AddTo(_subs);
_onSceneLoaded?.Subscribe(HandleSceneLoaded).AddTo(_subs);
_onNpcDialogueCompleted?.Subscribe(HandleNpcDialogue).AddTo(_subs);
BaseGames.Core.ServiceLocator.GetOrDefault<BaseGames.Core.Save.ISaveableRegistry>()?.Register(this);
}
private void OnDisable()
{
_subs.Clear();
BaseGames.Core.ServiceLocator.GetOrDefault<BaseGames.Core.Save.ISaveableRegistry>()?.Unregister(this);
}
private void OnDestroy()
{
BaseGames.Core.ServiceLocator.Unregister<IQuestManager>(this);
}
// ── 公共 API ──────────────────────────────────────────────────────────
/// <summary>NPC 接受任务时调用。</summary>
public void AcceptQuest(string questId)
{
if (!CanAccept(questId)) return;
_questStates[questId] = QuestStateEnum.Active;
_onQuestStarted?.Raise(questId);
}
/// <summary>NPC 完成任务时调用。</summary>
public void CompleteQuest(string questId, PlayerStats player)
{
if (!IsReadyToComplete(questId)) return;
var quest = GetQuestSO(questId);
quest.reward?.Apply(player);
_questStates[questId] = QuestStateEnum.Completed;
_onQuestCompleted?.Raise(questId);
// 解锁后续任务(分支)
if (quest.branches != null)
{
foreach (var branch in quest.branches)
{
if (string.IsNullOrEmpty(branch.conditionQuestId) ||
GetState(branch.conditionQuestId) == QuestStateEnum.Completed)
{
if (branch.nextQuest != null)
_questStates[branch.nextQuest.questId] = QuestStateEnum.Available;
break;
}
}
}
}
public QuestStateEnum GetState(string questId)
=> _questStates.TryGetValue(questId, out var s) ? s : QuestStateEnum.Unavailable;
public bool IsReadyToComplete(string questId)
{
var quest = GetQuestSO(questId);
if (quest == null || GetState(questId) != QuestStateEnum.Active) return false;
if (quest.objectives == null) return true;
foreach (var obj in quest.objectives)
{
if (!obj.IsOptional && !IsObjectiveComplete(obj)) return false;
}
return true;
}
// ── ISaveable ─────────────────────────────────────────────────────────
public void OnSave(SaveData data)
{
data.Quests.QuestStates.Clear();
data.Quests.AvailableQuestIds.Clear();
foreach (var (id, state) in _questStates)
{
data.Quests.QuestStates[id] = new BaseGames.Core.Save.QuestState
{
Status = state.ToString(),
ObjectiveIndex = 0,
ProgressCounts = BuildProgressList(id),
};
if (state == QuestStateEnum.Available) data.Quests.AvailableQuestIds.Add(id);
}
}
public void OnLoad(SaveData data)
{
_questStates.Clear();
_objectiveStates.Clear();
foreach (var (id, saved) in data.Quests.QuestStates)
{
if (System.Enum.TryParse<QuestStateEnum>(saved.Status, out var parsedState))
_questStates[id] = parsedState;
// 恢复各目标进度
var quest = GetQuestSO(id);
if (quest?.objectives != null && saved.ProgressCounts != null)
{
for (int i = 0; i < quest.objectives.Length && i < saved.ProgressCounts.Count; i++)
{
var obj = quest.objectives[i];
if (obj == null) continue;
if (!_objectiveStates.TryGetValue(obj.objectiveId, out var os))
os = _objectiveStates[obj.objectiveId] = new QuestObjectiveState();
os.progressCount = saved.ProgressCounts[i];
}
}
}
}
// ── 私有辅助 ─────────────────────────────────────────────────────────
private bool CanAccept(string questId)
{
if (GetState(questId) != QuestStateEnum.Available) return false;
var quest = GetQuestSO(questId);
if (quest?.prerequisiteQuestIds == null) return true;
foreach (var pre in quest.prerequisiteQuestIds)
if (GetState(pre) != QuestStateEnum.Completed) return false;
return true;
}
private bool IsObjectiveComplete(QuestObjectiveSO obj)
{
_objectiveStates.TryGetValue(obj.objectiveId, out var s);
s ??= new QuestObjectiveState();
return obj.EvaluateCompletion(s);
}
private List<int> BuildProgressList(string questId)
{
var list = new List<int>();
var quest = GetQuestSO(questId);
if (quest?.objectives == null) return list;
foreach (var obj in quest.objectives)
{
_objectiveStates.TryGetValue(obj?.objectiveId ?? string.Empty, out var os);
list.Add(os?.progressCount ?? 0);
}
return list;
}
// ── 事件处理 ─────────────────────────────────────────────────────────
private void HandleEnemyDefeated(string enemyId)
{
ForEachActiveObjective<DefeatEnemyObjective>(obj =>
{
if (obj.targetEnemyId == enemyId)
IncrementProgress(obj.objectiveId);
});
}
private void HandleItemCollected(string itemId)
{
ForEachActiveObjective<CollectItemObjective>(obj =>
{
if (obj.itemId == itemId)
IncrementProgress(obj.objectiveId);
});
}
private void HandleNpcDialogue(string npcId)
{
ForEachActiveObjective<TalkToNPCObjective>(obj =>
{
if (obj.targetNpcId == npcId)
IncrementProgress(obj.objectiveId);
});
}
private void HandleSceneLoaded(string sceneName)
{
ForEachActiveObjective<ReachAreaObjective>(obj =>
{
if (obj.sceneName == sceneName)
IncrementProgress(obj.objectiveId);
});
}
private void ForEachActiveObjective<T>(System.Action<T> action) where T : QuestObjectiveSO
{
foreach (var (qid, state) in _questStates)
{
if (state != QuestStateEnum.Active) continue;
var quest = GetQuestSO(qid);
if (quest?.objectives == null) continue;
foreach (var obj in quest.objectives)
{
if (obj is T typed) action(typed);
}
}
}
private void IncrementProgress(string objectiveId)
{
if (!_objectiveStates.TryGetValue(objectiveId, out var s))
s = _objectiveStates[objectiveId] = new QuestObjectiveState();
s.progressCount++;
_onObjectiveUpdated?.Raise(new QuestObjectiveEvent
{
ObjectiveId = objectiveId,
Progress = s.progressCount,
});
}
private QuestSO GetQuestSO(string id)
=> _questIndex != null && _questIndex.TryGetValue(id, out var q) ? q : null;
}
}

View File

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

View File

@@ -0,0 +1,81 @@
using UnityEngine;
using BaseGames.Player;
namespace BaseGames.Quest
{
/// <summary>
/// 任务目标基类(抽象,架构 22_QuestChallengeModule §3
/// 所有具体目标类型均继承此类,通过多态实现零代码扩展。
/// 每种目标在事件驱动下由 QuestManager 调用 EvaluateCompletion()。
/// </summary>
public abstract class QuestObjectiveSO : ScriptableObject
{
[Header("标识")]
public string objectiveId;
[TextArea(1, 4)]
public string displayText; // 任务日志中显示的文本
public bool IsOptional; // 可选目标(完成加奖励但不阻塞任务)
/// <summary>根据当前进度判断目标是否完成。</summary>
public abstract bool EvaluateCompletion(QuestObjectiveState state);
}
// ── 运行时目标进度状态(由 QuestManager 管理,不继承 SO────────────────
public class QuestObjectiveState
{
public bool completed = false;
public int progressCount = 0;
}
// ── 具体目标类型 ────────────────────────────────────────────────────────
/// <summary>与指定 NPC 对话后完成。</summary>
[CreateAssetMenu(menuName = "Quest/Objective/TalkToNPC")]
public class TalkToNPCObjective : QuestObjectiveSO
{
public string targetNpcId;
public override bool EvaluateCompletion(QuestObjectiveState s) => s.progressCount >= 1;
}
/// <summary>击败指定 ID 的敌人若干次。</summary>
[CreateAssetMenu(menuName = "Quest/Objective/Defeat")]
public class DefeatEnemyObjective : QuestObjectiveSO
{
public string targetEnemyId;
[Min(1)] public int defeatCount = 1;
public override bool EvaluateCompletion(QuestObjectiveState s) => s.progressCount >= defeatCount;
}
/// <summary>收集指定 ID 的物品若干件。</summary>
[CreateAssetMenu(menuName = "Quest/Objective/Collect")]
public class CollectItemObjective : QuestObjectiveSO
{
public string itemId;
[Min(1)] public int collectCount = 1;
public override bool EvaluateCompletion(QuestObjectiveState s) => s.progressCount >= collectCount;
}
/// <summary>到达指定场景/区域标记点后完成。</summary>
[CreateAssetMenu(menuName = "Quest/Objective/Reach")]
public class ReachAreaObjective : QuestObjectiveSO
{
public string sceneName; // 需到达的场景
public string markerTag; // 场景内的目标标记 Tag预留
public override bool EvaluateCompletion(QuestObjectiveState s) => s.progressCount >= 1;
}
/// <summary>使用指定能力若干次后完成。</summary>
[CreateAssetMenu(menuName = "Quest/Objective/UseSkill")]
public class UseSkillObjective : QuestObjectiveSO
{
public AbilityType requiredAbility;
[Min(1)] public int useCount = 1;
public override bool EvaluateCompletion(QuestObjectiveState s) => s.progressCount >= useCount;
}
}

View File

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

View File

@@ -0,0 +1,46 @@
using System;
using UnityEngine;
using BaseGames.Dialogue;
namespace BaseGames.Quest
{
/// <summary>
/// 任务定义 SO架构 22_QuestChallengeModule §2
/// 资产路径: Assets/ScriptableObjects/Quest/Quest_{questId}.asset
/// </summary>
[CreateAssetMenu(menuName = "Quest/Quest")]
public class QuestSO : ScriptableObject
{
[Header("标识")]
public string questId; // 唯一 ID如 "Quest_FindMushroom"
public string displayName;
[TextArea(2, 6)]
public string description;
public Sprite icon;
[Header("目标链")]
public QuestObjectiveSO[] objectives; // 按顺序完成,全部完成 = 可交完
[Header("前置条件")]
public string[] prerequisiteQuestIds; // 所有前置任务 Completed 后才可接
public int minAffinityToAccept; // NPC 好感度门槛0 = 无限制)
[Header("奖励")]
public RewardSO reward;
[Header("失败条件(可选)")]
public bool canFail;
public QuestObjectiveSO failCondition;
[Header("完成后续任务(分支)")]
public QuestBranch[] branches;
}
[Serializable]
public class QuestBranch
{
public string conditionQuestId; // 若此任务已完成 → 走本分支(空 = 默认)
public QuestSO nextQuest;
public string npcDialogueKey; // 触发 NPC 对话 key
}
}

View File

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

View File

@@ -0,0 +1,46 @@
using UnityEngine;
using BaseGames.Player;
using BaseGames.Core.Events;
namespace BaseGames.Quest
{
/// <summary>
/// 任务奖励 SO架构 22_QuestChallengeModule §4
/// 由 QuestManager.CompleteQuest() 调用 Apply() 发放奖励。
/// 资产路径: Assets/ScriptableObjects/Quest/Reward_{questId}.asset
/// </summary>
[CreateAssetMenu(menuName = "Quest/Reward")]
public class RewardSO : ScriptableObject
{
public int geo; // Geo 货币奖励
public int soulBonus; // 灵魂槽扩展(+MaxSoulPower
public string[] itemIds; // 物品/护符 ID 列表(通过 InventoryManager 发放)
public int affinityBonus; // 对发布 NPC 的好感度增量(存入 SaveData.World.NpcRelations
public string unlockDialogueKey; // 解锁 NPC 新台词集合 key架构 §4
[Tooltip("是否解锁能力AbilityType 无 None 值,用 bool 标识)")]
public bool unlocksAbility; // ⚠️ AbilityType 无 None用 bool 标识
public AbilityType unlockedAbility; // 仅当 unlocksAbility == true 有效
[Header("物品发放事件")]
[Tooltip("EVT_CollectiblePickup向 QuestManager/EquipmentManager 广播 itemId")]
[SerializeField] private StringEventChannelSO _onCollectiblePickup;
/// <summary>将奖励应用到游戏状态(由 QuestManager.CompleteQuest 调用)。</summary>
public void Apply(PlayerStats player)
{
if (player == null) return;
if (geo > 0) player.AddGeo(geo);
if (soulBonus > 0) player.AddSoulPower(soulBonus);
if (unlocksAbility) player.UnlockAbility(unlockedAbility);
// 通过 EVT_CollectiblePickup 事件频道广播每个物品 ID
if (itemIds != null && _onCollectiblePickup != null)
{
foreach (var id in itemIds)
_onCollectiblePickup.Raise(id);
}
}
}
}

View File

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