任务系统一半多,商城筛选功能,一些细节和免费包
This commit is contained in:
@@ -0,0 +1,665 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
public sealed class DailyTaskService : MonoBehaviour
|
||||
{
|
||||
public static DailyTaskService Instance { get; private set; }
|
||||
|
||||
public event Action OnTasksChanged;
|
||||
|
||||
private readonly Queue<DailyTaskEventData> pendingEvents = new Queue<DailyTaskEventData>();
|
||||
private readonly Dictionary<string, userTasksPool.TaskDefinition> definitionById = new Dictionary<string, userTasksPool.TaskDefinition>(StringComparer.Ordinal);
|
||||
|
||||
private DailyTaskSaveData saveData;
|
||||
private userTasksPool configuredTaskPool;
|
||||
private int configuredTaskMaxSize = 5;
|
||||
private int configuredRefreshMaxTimes = 3;
|
||||
private bool initialized;
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
|
||||
private static void Bootstrap()
|
||||
{
|
||||
EnsureInstance();
|
||||
}
|
||||
|
||||
public static DailyTaskService EnsureInstance()
|
||||
{
|
||||
if (Instance != null)
|
||||
{
|
||||
return Instance;
|
||||
}
|
||||
|
||||
var host = new GameObject("__daily_task_service");
|
||||
host.hideFlags = HideFlags.HideInHierarchy | HideFlags.HideInInspector;
|
||||
DontDestroyOnLoad(host);
|
||||
Instance = host.AddComponent<DailyTaskService>();
|
||||
return Instance;
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
Instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
SceneManager.sceneLoaded -= OnSceneLoaded;
|
||||
SceneManager.sceneLoaded += OnSceneLoaded;
|
||||
InitializeStorageIfNeeded();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (Instance == this)
|
||||
{
|
||||
SceneManager.sceneLoaded -= OnSceneLoaded;
|
||||
}
|
||||
}
|
||||
|
||||
public void Configure(userTasksPool taskPool, int taskMaxSize, int refreshMaxTimes)
|
||||
{
|
||||
configuredTaskPool = taskPool;
|
||||
configuredTaskMaxSize = Mathf.Max(1, taskMaxSize);
|
||||
configuredRefreshMaxTimes = Mathf.Max(0, refreshMaxTimes);
|
||||
RebuildDefinitionIndex();
|
||||
EnsureTodayTasks();
|
||||
FlushPendingEvents();
|
||||
NotifyTasksChanged();
|
||||
}
|
||||
|
||||
public IReadOnlyList<DailyTaskViewData> GetActiveTaskViews()
|
||||
{
|
||||
EnsureTodayTasks();
|
||||
|
||||
var result = new List<DailyTaskViewData>();
|
||||
if (saveData == null || saveData.activeTasks == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
for (int i = 0; i < saveData.activeTasks.Count; i++)
|
||||
{
|
||||
var runtimeEntry = saveData.activeTasks[i];
|
||||
if (runtimeEntry == null || string.IsNullOrEmpty(runtimeEntry.taskID))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
userTasksPool.TaskDefinition definition;
|
||||
if (!definitionById.TryGetValue(runtimeEntry.taskID, out definition))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
result.Add(new DailyTaskViewData
|
||||
{
|
||||
definition = definition,
|
||||
runtimeEntry = runtimeEntry
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public void ReportEvent(DailyTaskEventData eventData)
|
||||
{
|
||||
InitializeStorageIfNeeded();
|
||||
if (!IsConfigured())
|
||||
{
|
||||
pendingEvents.Enqueue(eventData);
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureTodayTasks();
|
||||
float accumulatedIncrement = GetAccumulatedIncrement(eventData);
|
||||
if (accumulatedIncrement <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float accumulatedValue = AddAccumulatedProgress(eventData.taskType, accumulatedIncrement);
|
||||
|
||||
bool changed = false;
|
||||
for (int i = 0; i < saveData.activeTasks.Count; i++)
|
||||
{
|
||||
var runtimeEntry = saveData.activeTasks[i];
|
||||
if (runtimeEntry == null || runtimeEntry.isClaimed)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
userTasksPool.TaskDefinition definition;
|
||||
if (!definitionById.TryGetValue(runtimeEntry.taskID, out definition))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!IsEventMatch(definition, eventData))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float previousProgress = runtimeEntry.progress;
|
||||
bool previousCompleted = runtimeEntry.isCompleted;
|
||||
runtimeEntry.progress = GetInitialProgress(definition, accumulatedValue);
|
||||
runtimeEntry.isCompleted = runtimeEntry.progress >= Mathf.Max(0.0001f, definition.targetValue);
|
||||
|
||||
if (!Mathf.Approximately(previousProgress, runtimeEntry.progress) || previousCompleted != runtimeEntry.isCompleted)
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed)
|
||||
{
|
||||
Save();
|
||||
NotifyTasksChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryRefreshTasks()
|
||||
{
|
||||
if (!IsConfigured())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
EnsureTodayTasks();
|
||||
if (saveData.refreshUsedCount >= configuredRefreshMaxTimes)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
saveData.refreshUsedCount += 1;
|
||||
GenerateDailyTasks();
|
||||
Save();
|
||||
NotifyTasksChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
public int GetRemainingRefreshCount()
|
||||
{
|
||||
EnsureTodayTasks();
|
||||
return Mathf.Max(0, configuredRefreshMaxTimes - (saveData != null ? saveData.refreshUsedCount : 0));
|
||||
}
|
||||
|
||||
public bool TryClaimReward(string taskID, Player_SO playerData, out string failureMessage)
|
||||
{
|
||||
failureMessage = string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(taskID))
|
||||
{
|
||||
failureMessage = "任务参数无效";
|
||||
return false;
|
||||
}
|
||||
|
||||
EnsureTodayTasks();
|
||||
|
||||
DailyTaskRuntimeEntry runtimeEntry;
|
||||
userTasksPool.TaskDefinition definition;
|
||||
if (!TryGetTaskPair(taskID, out runtimeEntry, out definition))
|
||||
{
|
||||
failureMessage = "未找到任务";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!runtimeEntry.isCompleted)
|
||||
{
|
||||
failureMessage = "任务尚未完成";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (runtimeEntry.isClaimed)
|
||||
{
|
||||
failureMessage = "奖励已领取";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryGrantReward(definition, playerData, out failureMessage))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
runtimeEntry.isClaimed = true;
|
||||
Save();
|
||||
NotifyTasksChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
public int ClaimAllAvailableRewards(Player_SO playerData, out string failureMessage)
|
||||
{
|
||||
failureMessage = string.Empty;
|
||||
EnsureTodayTasks();
|
||||
|
||||
int claimedCount = 0;
|
||||
var taskIds = new List<string>();
|
||||
var views = GetActiveTaskViews();
|
||||
for (int i = 0; i < views.Count; i++)
|
||||
{
|
||||
var runtimeEntry = views[i].runtimeEntry;
|
||||
if (runtimeEntry == null || !runtimeEntry.isCompleted || runtimeEntry.isClaimed)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
taskIds.Add(runtimeEntry.taskID);
|
||||
}
|
||||
|
||||
for (int i = 0; i < taskIds.Count; i++)
|
||||
{
|
||||
string claimError;
|
||||
if (!TryClaimReward(taskIds[i], playerData, out claimError))
|
||||
{
|
||||
if (claimedCount <= 0)
|
||||
{
|
||||
failureMessage = claimError;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
claimedCount += 1;
|
||||
}
|
||||
|
||||
if (claimedCount <= 0 && string.IsNullOrEmpty(failureMessage))
|
||||
{
|
||||
failureMessage = "暂无可领取奖励";
|
||||
}
|
||||
|
||||
return claimedCount;
|
||||
}
|
||||
|
||||
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
|
||||
{
|
||||
if (string.Equals(scene.name, "UI_UI", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ReportEvent(new DailyTaskEventData
|
||||
{
|
||||
taskType = userTasksPool.TaskType.Login,
|
||||
amount = 1f
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeStorageIfNeeded()
|
||||
{
|
||||
if (initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
saveData = DailyTaskSaveService.Load() ?? new DailyTaskSaveData();
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
private void EnsureTodayTasks()
|
||||
{
|
||||
InitializeStorageIfNeeded();
|
||||
|
||||
if (!IsConfigured())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string todayKey = GetTodayKey();
|
||||
if (saveData == null)
|
||||
{
|
||||
saveData = new DailyTaskSaveData();
|
||||
}
|
||||
|
||||
bool needRegenerate = !string.Equals(saveData.dateKey, todayKey, StringComparison.Ordinal)
|
||||
|| saveData.activeTasks == null
|
||||
|| saveData.activeTasks.Count == 0;
|
||||
|
||||
if (!needRegenerate)
|
||||
{
|
||||
EnsureAccumulatedProgressCompatibility();
|
||||
return;
|
||||
}
|
||||
|
||||
saveData.dateKey = todayKey;
|
||||
saveData.refreshUsedCount = 0;
|
||||
saveData.accumulatedProgress = new List<DailyTaskAccumulatedProgress>();
|
||||
GenerateDailyTasks();
|
||||
Save();
|
||||
}
|
||||
|
||||
private void GenerateDailyTasks()
|
||||
{
|
||||
if (saveData == null)
|
||||
{
|
||||
saveData = new DailyTaskSaveData();
|
||||
}
|
||||
|
||||
saveData.activeTasks = new List<DailyTaskRuntimeEntry>();
|
||||
|
||||
var candidates = configuredTaskPool.tasks
|
||||
.Where(t => t != null && t.addToTaskPool && !string.IsNullOrWhiteSpace(t.taskID) && t.selectionWeight > 0)
|
||||
.ToList();
|
||||
|
||||
if (candidates.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int pickCount = Mathf.Min(configuredTaskMaxSize, candidates.Count);
|
||||
var random = new System.Random(unchecked(GetTodayKey().GetHashCode() + saveData.refreshUsedCount * 397));
|
||||
var selectedDefinitions = new List<userTasksPool.TaskDefinition>();
|
||||
|
||||
while (selectedDefinitions.Count < pickCount && candidates.Count > 0)
|
||||
{
|
||||
int totalWeight = candidates.Sum(t => Mathf.Max(1, t.selectionWeight));
|
||||
int roll = random.Next(0, totalWeight);
|
||||
int cumulative = 0;
|
||||
int selectedIndex = 0;
|
||||
for (int i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
cumulative += Mathf.Max(1, candidates[i].selectionWeight);
|
||||
if (roll < cumulative)
|
||||
{
|
||||
selectedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
selectedDefinitions.Add(candidates[selectedIndex]);
|
||||
candidates.RemoveAt(selectedIndex);
|
||||
}
|
||||
|
||||
for (int i = 0; i < selectedDefinitions.Count; i++)
|
||||
{
|
||||
var definition = selectedDefinitions[i];
|
||||
float currentProgress = GetInitialProgress(definition, GetAccumulatedProgress(definition.taskType));
|
||||
saveData.activeTasks.Add(new DailyTaskRuntimeEntry
|
||||
{
|
||||
taskID = definition.taskID,
|
||||
progress = currentProgress,
|
||||
isCompleted = currentProgress >= Mathf.Max(0.0001f, definition.targetValue),
|
||||
isClaimed = false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsConfigured()
|
||||
{
|
||||
return configuredTaskPool != null && definitionById.Count > 0;
|
||||
}
|
||||
|
||||
private void RebuildDefinitionIndex()
|
||||
{
|
||||
definitionById.Clear();
|
||||
if (configuredTaskPool == null || configuredTaskPool.tasks == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < configuredTaskPool.tasks.Count; i++)
|
||||
{
|
||||
var definition = configuredTaskPool.tasks[i];
|
||||
if (definition == null || string.IsNullOrWhiteSpace(definition.taskID))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!definitionById.ContainsKey(definition.taskID))
|
||||
{
|
||||
definitionById.Add(definition.taskID, definition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureAccumulatedProgressCompatibility()
|
||||
{
|
||||
if (saveData == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (saveData.accumulatedProgress == null)
|
||||
{
|
||||
saveData.accumulatedProgress = new List<DailyTaskAccumulatedProgress>();
|
||||
}
|
||||
|
||||
if (saveData.accumulatedProgress.Count > 0 || saveData.activeTasks == null || saveData.activeTasks.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
for (int i = 0; i < saveData.activeTasks.Count; i++)
|
||||
{
|
||||
var runtimeEntry = saveData.activeTasks[i];
|
||||
if (runtimeEntry == null || string.IsNullOrWhiteSpace(runtimeEntry.taskID))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
userTasksPool.TaskDefinition definition;
|
||||
if (!definitionById.TryGetValue(runtimeEntry.taskID, out definition))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
AddAccumulatedProgress(definition.taskType, Mathf.Max(0f, runtimeEntry.progress));
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed)
|
||||
{
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
private void FlushPendingEvents()
|
||||
{
|
||||
if (!IsConfigured())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
while (pendingEvents.Count > 0)
|
||||
{
|
||||
ReportEvent(pendingEvents.Dequeue());
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsEventMatch(userTasksPool.TaskDefinition definition, DailyTaskEventData eventData)
|
||||
{
|
||||
return definition != null && definition.taskType == eventData.taskType;
|
||||
}
|
||||
|
||||
private float AddAccumulatedProgress(userTasksPool.TaskType taskType, float amount)
|
||||
{
|
||||
if (saveData == null)
|
||||
{
|
||||
saveData = new DailyTaskSaveData();
|
||||
}
|
||||
|
||||
if (saveData.accumulatedProgress == null)
|
||||
{
|
||||
saveData.accumulatedProgress = new List<DailyTaskAccumulatedProgress>();
|
||||
}
|
||||
|
||||
int taskTypeValue = (int)taskType;
|
||||
for (int i = 0; i < saveData.accumulatedProgress.Count; i++)
|
||||
{
|
||||
var entry = saveData.accumulatedProgress[i];
|
||||
if (entry == null || entry.taskType != taskTypeValue)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.progress = Mathf.Max(0f, entry.progress + amount);
|
||||
return entry.progress;
|
||||
}
|
||||
|
||||
var newEntry = new DailyTaskAccumulatedProgress
|
||||
{
|
||||
taskType = taskTypeValue,
|
||||
progress = Mathf.Max(0f, amount)
|
||||
};
|
||||
saveData.accumulatedProgress.Add(newEntry);
|
||||
return newEntry.progress;
|
||||
}
|
||||
|
||||
private float GetAccumulatedProgress(userTasksPool.TaskType taskType)
|
||||
{
|
||||
if (saveData == null || saveData.accumulatedProgress == null)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
int taskTypeValue = (int)taskType;
|
||||
for (int i = 0; i < saveData.accumulatedProgress.Count; i++)
|
||||
{
|
||||
var entry = saveData.accumulatedProgress[i];
|
||||
if (entry == null || entry.taskType != taskTypeValue)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return Mathf.Max(0f, entry.progress);
|
||||
}
|
||||
|
||||
return 0f;
|
||||
}
|
||||
|
||||
private static float GetInitialProgress(userTasksPool.TaskDefinition definition, float accumulatedValue)
|
||||
{
|
||||
if (definition == null)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
return Mathf.Min(Mathf.Max(0f, accumulatedValue), Mathf.Max(0.0001f, definition.targetValue));
|
||||
}
|
||||
|
||||
private bool TryGetTaskPair(string taskID, out DailyTaskRuntimeEntry runtimeEntry, out userTasksPool.TaskDefinition definition)
|
||||
{
|
||||
runtimeEntry = null;
|
||||
definition = null;
|
||||
|
||||
if (saveData == null || saveData.activeTasks == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < saveData.activeTasks.Count; i++)
|
||||
{
|
||||
var candidate = saveData.activeTasks[i];
|
||||
if (candidate == null || !string.Equals(candidate.taskID, taskID, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
userTasksPool.TaskDefinition matchedDefinition;
|
||||
if (!definitionById.TryGetValue(candidate.taskID, out matchedDefinition))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
runtimeEntry = candidate;
|
||||
definition = matchedDefinition;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryGrantReward(userTasksPool.TaskDefinition definition, Player_SO playerData, out string failureMessage)
|
||||
{
|
||||
failureMessage = string.Empty;
|
||||
if (definition == null)
|
||||
{
|
||||
failureMessage = "任务定义无效";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (definition.rewardAmount <= 0)
|
||||
{
|
||||
failureMessage = "奖励数量无效";
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (definition.rewardType)
|
||||
{
|
||||
case userTasksPool.RewardType.coins:
|
||||
{
|
||||
var wallet = PlayerEconomyLedger.EnsureInstance();
|
||||
if (playerData != null)
|
||||
{
|
||||
wallet.AttachPlayerData(playerData);
|
||||
}
|
||||
|
||||
wallet.AddCoins(definition.rewardAmount);
|
||||
return true;
|
||||
}
|
||||
case userTasksPool.RewardType.expBottle:
|
||||
{
|
||||
var bottleLedger = ExpBottleLedger.EnsureInstance();
|
||||
if (playerData != null)
|
||||
{
|
||||
bottleLedger.AttachPlayerData(playerData);
|
||||
}
|
||||
|
||||
bottleLedger.Add(definition.rewardExpBottleKind, definition.rewardAmount);
|
||||
return true;
|
||||
}
|
||||
case userTasksPool.RewardType.dushMaterial:
|
||||
{
|
||||
var materialLedger = DushMaterialLedger.EnsureInstance();
|
||||
if (playerData != null)
|
||||
{
|
||||
materialLedger.AttachPlayerData(playerData);
|
||||
}
|
||||
|
||||
materialLedger.Add(definition.rewardDushMaterialKind, definition.rewardAmount);
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
failureMessage = "暂不支持该奖励类型";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static float GetAccumulatedIncrement(DailyTaskEventData eventData)
|
||||
{
|
||||
switch (eventData.taskType)
|
||||
{
|
||||
case userTasksPool.TaskType.TotalScore:
|
||||
case userTasksPool.TaskType.SpendCoins:
|
||||
case userTasksPool.TaskType.GameDuration:
|
||||
return Mathf.Max(0f, eventData.amount);
|
||||
case userTasksPool.TaskType.Login:
|
||||
case userTasksPool.TaskType.PlaySong:
|
||||
case userTasksPool.TaskType.FullCombo:
|
||||
case userTasksPool.TaskType.UseItem:
|
||||
return Mathf.Max(1f, eventData.amount);
|
||||
default:
|
||||
return Mathf.Max(0f, eventData.amount);
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetTodayKey()
|
||||
{
|
||||
return DateTime.Now.ToString("yyyy-MM-dd");
|
||||
}
|
||||
|
||||
private void Save()
|
||||
{
|
||||
DailyTaskSaveService.Save(saveData);
|
||||
}
|
||||
|
||||
private void NotifyTasksChanged()
|
||||
{
|
||||
if (OnTasksChanged != null)
|
||||
{
|
||||
OnTasksChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user