1008 lines
29 KiB
C#
1008 lines
29 KiB
C#
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 enum ProgressCombineMode
|
|
{
|
|
Sum,
|
|
Max
|
|
}
|
|
|
|
private const float OnlineDurationFlushStepSeconds = 5f;
|
|
private const string GameplaySceneName = "gameplay_gameplay";
|
|
private const string MainUiSceneName = "UI_UI";
|
|
|
|
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;
|
|
private bool appFocused = true;
|
|
private float pendingOnlineDurationSeconds;
|
|
private bool isGameplayDurationTracking;
|
|
private float gameplayDurationRealtimeStart;
|
|
|
|
[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 Update()
|
|
{
|
|
if (!Application.isPlaying || !appFocused)
|
|
{
|
|
return;
|
|
}
|
|
|
|
pendingOnlineDurationSeconds += Time.unscaledDeltaTime;
|
|
if (pendingOnlineDurationSeconds >= OnlineDurationFlushStepSeconds)
|
|
{
|
|
float flushSeconds = Mathf.Floor(pendingOnlineDurationSeconds);
|
|
pendingOnlineDurationSeconds = Mathf.Max(0f, pendingOnlineDurationSeconds - flushSeconds);
|
|
ReportEvent(new DailyTaskEventData
|
|
{
|
|
taskType = userTasksPool.TaskType.OnlineDuration,
|
|
amount = flushSeconds
|
|
});
|
|
}
|
|
}
|
|
|
|
private void OnApplicationFocus(bool hasFocus)
|
|
{
|
|
appFocused = hasFocus;
|
|
if (!hasFocus)
|
|
{
|
|
FlushPendingOnlineDuration();
|
|
FlushGameplayDuration();
|
|
}
|
|
}
|
|
|
|
private void OnApplicationPause(bool pauseStatus)
|
|
{
|
|
if (pauseStatus)
|
|
{
|
|
FlushPendingOnlineDuration();
|
|
FlushGameplayDuration();
|
|
}
|
|
}
|
|
|
|
private void OnApplicationQuit()
|
|
{
|
|
FlushPendingOnlineDuration();
|
|
FlushGameplayDuration();
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
if (Instance == this)
|
|
{
|
|
FlushPendingOnlineDuration();
|
|
FlushGameplayDuration();
|
|
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();
|
|
|
|
bool changed = ApplyEventToTrackedProgress(eventData);
|
|
|
|
if (saveData != null && saveData.activeTasks != null)
|
|
{
|
|
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 nextProgress = CalculateProgressForDefinition(definition);
|
|
bool nextCompleted = nextProgress >= Mathf.Max(0.0001f, definition.targetValue);
|
|
|
|
if (!Mathf.Approximately(runtimeEntry.progress, nextProgress) || runtimeEntry.isCompleted != nextCompleted)
|
|
{
|
|
runtimeEntry.progress = nextProgress;
|
|
runtimeEntry.isCompleted = nextCompleted;
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (changed)
|
|
{
|
|
Save();
|
|
NotifyTasksChanged();
|
|
}
|
|
}
|
|
|
|
public bool TryRefreshTasks()
|
|
{
|
|
if (!IsConfigured())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
EnsureTodayTasks();
|
|
if (saveData.refreshUsedCount >= configuredRefreshMaxTimes)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
saveData.refreshUsedCount += 1;
|
|
StampRefreshUsage();
|
|
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 = LocalizationService.Get("daily.error.invalid_task_argument", "任务参数无效");
|
|
return false;
|
|
}
|
|
|
|
EnsureTodayTasks();
|
|
|
|
DailyTaskRuntimeEntry runtimeEntry;
|
|
userTasksPool.TaskDefinition definition;
|
|
if (!TryGetTaskPair(taskID, out runtimeEntry, out definition))
|
|
{
|
|
failureMessage = LocalizationService.Get("daily.error.task_not_found", "未找到任务");
|
|
return false;
|
|
}
|
|
|
|
if (!runtimeEntry.isCompleted)
|
|
{
|
|
failureMessage = LocalizationService.Get("daily.error.task_not_completed", "任务尚未完成");
|
|
return false;
|
|
}
|
|
|
|
if (runtimeEntry.isClaimed)
|
|
{
|
|
failureMessage = LocalizationService.Get("daily.error.reward_already_claimed", "奖励已领取");
|
|
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 = LocalizationService.Get("daily.no_claimable_reward", "暂无可领取奖励");
|
|
}
|
|
|
|
return claimedCount;
|
|
}
|
|
|
|
public void ClearPersistentState()
|
|
{
|
|
InitializeStorageIfNeeded();
|
|
|
|
pendingEvents.Clear();
|
|
pendingOnlineDurationSeconds = 0f;
|
|
isGameplayDurationTracking = false;
|
|
gameplayDurationRealtimeStart = 0f;
|
|
|
|
saveData = new DailyTaskSaveData();
|
|
EnsureRuntimeCollections();
|
|
DailyTaskSaveService.ClearPersistentState();
|
|
NotifyTasksChanged();
|
|
}
|
|
|
|
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
|
|
{
|
|
if (string.Equals(scene.name, MainUiSceneName, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
TryReportDailyLogin();
|
|
}
|
|
|
|
if (string.Equals(scene.name, GameplaySceneName, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
BeginGameplayDurationTracking();
|
|
}
|
|
else
|
|
{
|
|
FlushGameplayDuration();
|
|
}
|
|
}
|
|
|
|
private void InitializeStorageIfNeeded()
|
|
{
|
|
if (initialized)
|
|
{
|
|
return;
|
|
}
|
|
|
|
saveData = DailyTaskSaveService.Load() ?? new DailyTaskSaveData();
|
|
EnsureRuntimeCollections();
|
|
initialized = true;
|
|
}
|
|
|
|
private void EnsureTodayTasks()
|
|
{
|
|
InitializeStorageIfNeeded();
|
|
|
|
if (!IsConfigured())
|
|
{
|
|
return;
|
|
}
|
|
|
|
bool timeStateChanged;
|
|
int effectiveTodayStamp = GetEffectiveTodayStamp(out timeStateChanged);
|
|
string todayKey = FormatDateKey(effectiveTodayStamp);
|
|
if (saveData == null)
|
|
{
|
|
saveData = new DailyTaskSaveData();
|
|
}
|
|
|
|
EnsureRuntimeCollections();
|
|
|
|
bool needRegenerate = saveData.dateStamp != effectiveTodayStamp
|
|
|| !string.Equals(saveData.dateKey, todayKey, StringComparison.Ordinal)
|
|
|| saveData.activeTasks == null
|
|
|| saveData.activeTasks.Count == 0;
|
|
|
|
if (!needRegenerate)
|
|
{
|
|
EnsureAccumulatedProgressCompatibility();
|
|
if (timeStateChanged)
|
|
{
|
|
Save();
|
|
}
|
|
return;
|
|
}
|
|
|
|
saveData.dateKey = todayKey;
|
|
saveData.dateStamp = effectiveTodayStamp;
|
|
saveData.refreshUsedCount = 0;
|
|
saveData.loginReportedToday = false;
|
|
saveData.accumulatedProgress = new List<DailyTaskAccumulatedProgress>();
|
|
saveData.uniqueIntProgress = new List<DailyTaskUniqueIntProgress>();
|
|
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);
|
|
int dateSeed = saveData.dateStamp > 0 ? saveData.dateStamp : GetLocalDateStamp(DateTime.Now);
|
|
var random = new System.Random(unchecked(dateSeed + 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 = CalculateProgressForDefinition(definition);
|
|
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()
|
|
{
|
|
EnsureRuntimeCollections();
|
|
|
|
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;
|
|
}
|
|
|
|
if (definition.taskType == userTasksPool.TaskType.DifferentSongs)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
changed |= AddOrCombineProgress(definition.taskType, Mathf.Max(0f, runtimeEntry.progress), GetCombineMode(definition.taskType));
|
|
}
|
|
|
|
if (changed)
|
|
{
|
|
Save();
|
|
}
|
|
}
|
|
|
|
private void FlushPendingEvents()
|
|
{
|
|
if (!IsConfigured())
|
|
{
|
|
return;
|
|
}
|
|
|
|
while (pendingEvents.Count > 0)
|
|
{
|
|
ReportEvent(pendingEvents.Dequeue());
|
|
}
|
|
}
|
|
|
|
private void TryReportDailyLogin()
|
|
{
|
|
EnsureTodayTasks();
|
|
if (saveData == null || saveData.loginReportedToday)
|
|
{
|
|
return;
|
|
}
|
|
|
|
saveData.loginReportedToday = true;
|
|
ReportEvent(new DailyTaskEventData
|
|
{
|
|
taskType = userTasksPool.TaskType.Login,
|
|
amount = 1f
|
|
});
|
|
}
|
|
|
|
private void BeginGameplayDurationTracking()
|
|
{
|
|
if (isGameplayDurationTracking)
|
|
{
|
|
return;
|
|
}
|
|
|
|
isGameplayDurationTracking = true;
|
|
gameplayDurationRealtimeStart = Time.realtimeSinceStartup;
|
|
}
|
|
|
|
private void FlushGameplayDuration()
|
|
{
|
|
if (!isGameplayDurationTracking)
|
|
{
|
|
return;
|
|
}
|
|
|
|
float now = Time.realtimeSinceStartup;
|
|
float elapsed = Mathf.Max(0f, now - gameplayDurationRealtimeStart);
|
|
isGameplayDurationTracking = false;
|
|
gameplayDurationRealtimeStart = 0f;
|
|
|
|
if (elapsed <= 0f)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ReportEvent(new DailyTaskEventData
|
|
{
|
|
taskType = userTasksPool.TaskType.GameDuration,
|
|
amount = elapsed
|
|
});
|
|
}
|
|
|
|
private bool ApplyEventToTrackedProgress(DailyTaskEventData eventData)
|
|
{
|
|
switch (eventData.taskType)
|
|
{
|
|
case userTasksPool.TaskType.Login:
|
|
case userTasksPool.TaskType.PlaySong:
|
|
case userTasksPool.TaskType.FullCombo:
|
|
case userTasksPool.TaskType.WatchStory:
|
|
case userTasksPool.TaskType.UseItem:
|
|
case userTasksPool.TaskType.ShareGame:
|
|
{
|
|
bool changed = AddOrCombineProgress(eventData.taskType, Mathf.Max(1f, eventData.amount), ProgressCombineMode.Sum);
|
|
if (eventData.taskType == userTasksPool.TaskType.PlaySong && eventData.songID > 0)
|
|
{
|
|
changed |= AddUniqueIntValue(userTasksPool.TaskType.DifferentSongs, eventData.songID);
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
case userTasksPool.TaskType.TotalScore:
|
|
case userTasksPool.TaskType.SpendCoins:
|
|
case userTasksPool.TaskType.GameDuration:
|
|
case userTasksPool.TaskType.OnlineDuration:
|
|
return AddOrCombineProgress(eventData.taskType, Mathf.Max(0f, eventData.amount), ProgressCombineMode.Sum);
|
|
case userTasksPool.TaskType.SingleRunCombo:
|
|
case userTasksPool.TaskType.SingleRunScore:
|
|
return AddOrCombineProgress(eventData.taskType, Mathf.Max(0f, eventData.amount), ProgressCombineMode.Max);
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static bool IsEventMatch(userTasksPool.TaskDefinition definition, DailyTaskEventData eventData)
|
|
{
|
|
if (definition == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (definition.taskType == eventData.taskType)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return definition.taskType == userTasksPool.TaskType.DifferentSongs && eventData.taskType == userTasksPool.TaskType.PlaySong;
|
|
}
|
|
|
|
private float CalculateProgressForDefinition(userTasksPool.TaskDefinition definition)
|
|
{
|
|
if (definition == null)
|
|
{
|
|
return 0f;
|
|
}
|
|
|
|
float targetValue = Mathf.Max(0.0001f, definition.targetValue);
|
|
switch (definition.taskType)
|
|
{
|
|
case userTasksPool.TaskType.DifferentSongs:
|
|
return Mathf.Min(GetUniqueIntCount(userTasksPool.TaskType.DifferentSongs), targetValue);
|
|
default:
|
|
return Mathf.Min(Mathf.Max(0f, GetAccumulatedProgress(definition.taskType)), targetValue);
|
|
}
|
|
}
|
|
|
|
private bool AddOrCombineProgress(userTasksPool.TaskType taskType, float amount, ProgressCombineMode combineMode)
|
|
{
|
|
EnsureRuntimeCollections();
|
|
|
|
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;
|
|
}
|
|
|
|
float nextValue = combineMode == ProgressCombineMode.Max
|
|
? Mathf.Max(entry.progress, amount)
|
|
: Mathf.Max(0f, entry.progress + amount);
|
|
|
|
if (Mathf.Approximately(entry.progress, nextValue))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
entry.progress = nextValue;
|
|
return true;
|
|
}
|
|
|
|
saveData.accumulatedProgress.Add(new DailyTaskAccumulatedProgress
|
|
{
|
|
taskType = taskTypeValue,
|
|
progress = Mathf.Max(0f, amount)
|
|
});
|
|
return true;
|
|
}
|
|
|
|
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 bool AddUniqueIntValue(userTasksPool.TaskType taskType, int value)
|
|
{
|
|
if (value <= 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
EnsureRuntimeCollections();
|
|
int taskTypeValue = (int)taskType;
|
|
for (int i = 0; i < saveData.uniqueIntProgress.Count; i++)
|
|
{
|
|
var entry = saveData.uniqueIntProgress[i];
|
|
if (entry == null || entry.taskType != taskTypeValue)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (entry.values == null)
|
|
{
|
|
entry.values = new List<int>();
|
|
}
|
|
|
|
if (entry.values.Contains(value))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
entry.values.Add(value);
|
|
return true;
|
|
}
|
|
|
|
saveData.uniqueIntProgress.Add(new DailyTaskUniqueIntProgress
|
|
{
|
|
taskType = taskTypeValue,
|
|
values = new List<int> { value }
|
|
});
|
|
return true;
|
|
}
|
|
|
|
private int GetUniqueIntCount(userTasksPool.TaskType taskType)
|
|
{
|
|
if (saveData == null || saveData.uniqueIntProgress == null)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
int taskTypeValue = (int)taskType;
|
|
for (int i = 0; i < saveData.uniqueIntProgress.Count; i++)
|
|
{
|
|
var entry = saveData.uniqueIntProgress[i];
|
|
if (entry == null || entry.taskType != taskTypeValue || entry.values == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
return entry.values.Count;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
private static ProgressCombineMode GetCombineMode(userTasksPool.TaskType taskType)
|
|
{
|
|
switch (taskType)
|
|
{
|
|
case userTasksPool.TaskType.SingleRunCombo:
|
|
case userTasksPool.TaskType.SingleRunScore:
|
|
return ProgressCombineMode.Max;
|
|
default:
|
|
return ProgressCombineMode.Sum;
|
|
}
|
|
}
|
|
|
|
private void EnsureRuntimeCollections()
|
|
{
|
|
if (saveData == null)
|
|
{
|
|
saveData = new DailyTaskSaveData();
|
|
}
|
|
|
|
if (saveData.activeTasks == null)
|
|
{
|
|
saveData.activeTasks = new List<DailyTaskRuntimeEntry>();
|
|
}
|
|
|
|
if (saveData.accumulatedProgress == null)
|
|
{
|
|
saveData.accumulatedProgress = new List<DailyTaskAccumulatedProgress>();
|
|
}
|
|
|
|
if (saveData.uniqueIntProgress == null)
|
|
{
|
|
saveData.uniqueIntProgress = new List<DailyTaskUniqueIntProgress>();
|
|
}
|
|
}
|
|
|
|
private int GetEffectiveTodayStamp(out bool stateChanged)
|
|
{
|
|
stateChanged = false;
|
|
if (saveData == null)
|
|
{
|
|
saveData = new DailyTaskSaveData();
|
|
}
|
|
|
|
int currentStamp = GetLocalDateStamp(DateTime.Now);
|
|
long nowLocalTicks = DateTime.Now.Ticks;
|
|
long nowUtcTicks = DateTime.UtcNow.Ticks;
|
|
|
|
if (saveData.trustedDateStamp <= 0)
|
|
{
|
|
saveData.trustedDateStamp = currentStamp;
|
|
stateChanged = true;
|
|
}
|
|
else if (currentStamp < saveData.trustedDateStamp)
|
|
{
|
|
saveData.timeRollbackDetections += 1;
|
|
currentStamp = saveData.trustedDateStamp;
|
|
stateChanged = true;
|
|
}
|
|
else if (currentStamp > saveData.trustedDateStamp)
|
|
{
|
|
saveData.trustedDateStamp = currentStamp;
|
|
stateChanged = true;
|
|
}
|
|
|
|
if (saveData.lastSeenLocalTicks != nowLocalTicks)
|
|
{
|
|
saveData.lastSeenLocalTicks = nowLocalTicks;
|
|
stateChanged = true;
|
|
}
|
|
|
|
if (saveData.lastSeenUtcTicks != nowUtcTicks)
|
|
{
|
|
saveData.lastSeenUtcTicks = nowUtcTicks;
|
|
stateChanged = true;
|
|
}
|
|
|
|
return Mathf.Max(currentStamp, saveData.trustedDateStamp);
|
|
}
|
|
|
|
private void StampRefreshUsage()
|
|
{
|
|
saveData.lastRefreshDateStamp = saveData.dateStamp;
|
|
saveData.lastRefreshLocalTicks = DateTime.Now.Ticks;
|
|
saveData.lastRefreshUtcTicks = DateTime.UtcNow.Ticks;
|
|
}
|
|
|
|
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 = LocalizationService.Get("daily.error.invalid_definition", "任务定义无效");
|
|
return false;
|
|
}
|
|
|
|
if (definition.rewardAmount <= 0)
|
|
{
|
|
failureMessage = LocalizationService.Get("daily.error.invalid_reward_amount", "奖励数量无效");
|
|
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 = LocalizationService.Get("daily.error.unsupported_reward_type", "暂不支持该奖励类型");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private void FlushPendingOnlineDuration()
|
|
{
|
|
if (pendingOnlineDurationSeconds <= 0f)
|
|
{
|
|
return;
|
|
}
|
|
|
|
float flushSeconds = pendingOnlineDurationSeconds;
|
|
pendingOnlineDurationSeconds = 0f;
|
|
ReportEvent(new DailyTaskEventData
|
|
{
|
|
taskType = userTasksPool.TaskType.OnlineDuration,
|
|
amount = flushSeconds
|
|
});
|
|
}
|
|
|
|
private static int GetLocalDateStamp(DateTime dateTime)
|
|
{
|
|
return dateTime.Year * 10000 + dateTime.Month * 100 + dateTime.Day;
|
|
}
|
|
|
|
private static string FormatDateKey(int dateStamp)
|
|
{
|
|
int year = dateStamp / 10000;
|
|
int month = (dateStamp / 100) % 100;
|
|
int day = dateStamp % 100;
|
|
return year.ToString("D4") + "-" + month.ToString("D2") + "-" + day.ToString("D2");
|
|
}
|
|
|
|
private void Save()
|
|
{
|
|
DailyTaskSaveService.Save(saveData);
|
|
}
|
|
|
|
private void NotifyTasksChanged()
|
|
{
|
|
if (OnTasksChanged != null)
|
|
{
|
|
OnTasksChanged();
|
|
}
|
|
}
|
|
}
|