任务系统一半多,商城筛选功能,一些细节和免费包

This commit is contained in:
FloatGaming
2026-03-15 04:27:21 +08:00
parent 416190b23e
commit ec42f2e34b
344 changed files with 32225 additions and 6481 deletions
+173 -7
View File
@@ -1,5 +1,4 @@
using System.Runtime.ExceptionServices;
using UnityEngine;
using UnityEngine;
[CreateAssetMenu(fileName = "PlayerDefaultSO", menuName = "SO_Data/PlayerSO")]
public class Player_SO : ScriptableObject
@@ -7,17 +6,184 @@ public class Player_SO : ScriptableObject
[Header("Inspector")]
public int player_currentEXP;
public int player_currentLevel;
[Header("economics")]
[SerializeField] private int player_coins;
[SerializeField] private int player_material;
[Header("exp bottles")]
[SerializeField] private int commonExpBottle; // 凡品经验瓶
[SerializeField] private int mediumExpBottle; // 中品经验瓶
[SerializeField] private int superiorExpBottle; // 上品经验瓶
[SerializeField] private int supremeExpBottle; // 极品经验瓶
[SerializeField] private int emptyExpBottle78000;
[SerializeField] private int commonExpBottle78001;
[SerializeField] private int mediumExpBottle78002;
[SerializeField] private int superiorExpBottle78003;
[SerializeField] private int supremeExpBottle78004;
[SerializeField] private int extraordinaryExpBottle78005;
[SerializeField] private int celestialExpBottle78006;
[SerializeField] private int dushMaterial78021;
[SerializeField] private int dushMaterial78022;
[SerializeField] private int dushMaterial78023;
[SerializeField] private int dushMaterial78024;
[Header("Device Records")]
public string firstLaunchDate;
public int Coins
{
get { return player_coins; }
}
public int Material
{
get { return player_material; }
}
public void SetCoins(int value)
{
player_coins = Mathf.Max(0, value);
PersistEditorChanges();
}
public void AddCoins(int amount)
{
if (amount == 0)
{
return;
}
SetCoins(player_coins + amount);
}
public void AddMaterial(int amount)
{
if (amount == 0)
{
return;
}
SetMaterial(player_material + amount);
}
public void AddPlayerExperience(int amount)
{
if (amount == 0)
{
return;
}
player_currentEXP = Mathf.Max(0, player_currentEXP + amount);
PersistEditorChanges();
}
public bool TrySpendCoins(int amount)
{
if (amount <= 0)
{
return true;
}
if (player_coins < amount)
{
return false;
}
SetCoins(player_coins - amount);
return true;
}
public void SetMaterial(int value)
{
player_material = Mathf.Max(0, value);
PersistEditorChanges();
}
public int GetLegacyExpBottleCount(string fieldName)
{
switch (fieldName)
{
case "emptyExpBottle78000": return emptyExpBottle78000;
case "commonExpBottle78001": return commonExpBottle78001;
case "mediumExpBottle78002": return mediumExpBottle78002;
case "superiorExpBottle78003": return superiorExpBottle78003;
case "supremeExpBottle78004": return supremeExpBottle78004;
case "extraordinaryExpBottle78005": return extraordinaryExpBottle78005;
case "celestialExpBottle78006": return celestialExpBottle78006;
default: return 0;
}
}
public void SetLegacyExpBottleCount(string fieldName, int value)
{
int safeValue = Mathf.Max(0, value);
switch (fieldName)
{
case "emptyExpBottle78000":
emptyExpBottle78000 = safeValue;
break;
case "commonExpBottle78001":
commonExpBottle78001 = safeValue;
break;
case "mediumExpBottle78002":
mediumExpBottle78002 = safeValue;
break;
case "superiorExpBottle78003":
superiorExpBottle78003 = safeValue;
break;
case "supremeExpBottle78004":
supremeExpBottle78004 = safeValue;
break;
case "extraordinaryExpBottle78005":
extraordinaryExpBottle78005 = safeValue;
break;
case "celestialExpBottle78006":
celestialExpBottle78006 = safeValue;
break;
default:
return;
}
PersistEditorChanges();
}
public int GetLegacyDushMaterialCount(string fieldName)
{
switch (fieldName)
{
case "dushMaterial78021": return dushMaterial78021;
case "dushMaterial78022": return dushMaterial78022;
case "dushMaterial78023": return dushMaterial78023;
case "dushMaterial78024": return dushMaterial78024;
default: return 0;
}
}
public void SetLegacyDushMaterialCount(string fieldName, int value)
{
int safeValue = Mathf.Max(0, value);
switch (fieldName)
{
case "dushMaterial78021":
dushMaterial78021 = safeValue;
break;
case "dushMaterial78022":
dushMaterial78022 = safeValue;
break;
case "dushMaterial78023":
dushMaterial78023 = safeValue;
break;
case "dushMaterial78024":
dushMaterial78024 = safeValue;
break;
default:
return;
}
PersistEditorChanges();
}
private void PersistEditorChanges()
{
#if UNITY_EDITOR
UnityEditor.EditorUtility.SetDirty(this);
#endif
}
}
@@ -2485,9 +2485,9 @@ RectTransform:
m_Children: []
m_Father: {fileID: 9209391586188457348}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 0, y: 3.5}
m_SizeDelta: {x: 84, y: 107}
m_Pivot: {x: 0, y: 1}
--- !u!222 &4953888595604622927
@@ -2562,9 +2562,9 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 0, y: -45.5}
m_AnchoredPosition: {x: 118.5, y: -117.50053}
m_SizeDelta: {x: 237, y: 144}
m_Pivot: {x: 0, y: 1}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &11903364479572129
CanvasRenderer:
m_ObjectHideFlags: 0
@@ -3028,8 +3028,8 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300000, guid: 7f3b1220317607e44bbb8f0e1998044a, type: 3}
m_Type: 1
m_Sprite: {fileID: -6726115822594207860, guid: 4ff010a6a12e2c1459b8693607095e21, type: 3}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
@@ -3115,11 +3115,11 @@ RectTransform:
- {fileID: 3689223780715248708}
m_Father: {fileID: 8079540942653106974}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 6.0999985, y: -11.7}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 3.4}
m_SizeDelta: {x: 237, y: 86}
m_Pivot: {x: 0, y: 1}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &9066257676768141630
CanvasRenderer:
m_ObjectHideFlags: 0
@@ -3421,8 +3421,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -5.339966, y: 0.000019073486}
m_SizeDelta: {x: 16.775, y: 25.1625}
m_AnchoredPosition: {x: 1.02, y: 0.000019073486}
m_SizeDelta: {x: 20, y: 20}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &5184938500494465621
CanvasRenderer:
@@ -3445,14 +3445,14 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.24705882, g: 0.2901961, b: 0.3372549, a: 1}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: -7770052838922911145, guid: cfb305efbe7c8c4408085867d5209768, type: 3}
m_Sprite: {fileID: 21300000, guid: 81c13879952e72d408a0cfa362ebfd02, type: 3}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
@@ -4034,8 +4034,8 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300000, guid: 7f3b1220317607e44bbb8f0e1998044a, type: 3}
m_Type: 1
m_Sprite: {fileID: -6726115822594207860, guid: 4ff010a6a12e2c1459b8693607095e21, type: 3}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
@@ -4334,7 +4334,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
m_IsActive: 0
--- !u!224 &4386699895607034587
RectTransform:
m_ObjectHideFlags: 0
@@ -4432,7 +4432,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 451, y: -52}
m_AnchoredPosition: {x: 451, y: -43}
m_SizeDelta: {x: 100, y: 100}
m_Pivot: {x: 0, y: 0}
--- !u!114 &786756074167547563
@@ -4455,7 +4455,7 @@ MonoBehaviour:
m_ChildAlignment: 0
m_StartCorner: 0
m_StartAxis: 0
m_CellSize: {x: 188, y: 36.7}
m_CellSize: {x: 50, y: 50}
m_Spacing: {x: 20, y: 18}
m_Constraint: 1
m_ConstraintCount: 3
@@ -4493,7 +4493,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 6.100006, y: -9.2}
m_AnchoredPosition: {x: 15.5, y: -16.5}
m_SizeDelta: {x: 19, y: 17}
m_Pivot: {x: 0, y: 1}
--- !u!222 &8302545596642335859
@@ -4825,6 +4825,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
player_SO: {fileID: 11400000, guid: a59c019c71199384eaac0703299047c8, type: 2}
playerCoins_legacy: {fileID: 7376147910533281946}
putPrefabsHere: {fileID: 6773918330215714056}
settings_launch: {fileID: 865676738076373517}
userInfo_launch: {fileID: 1502937460034745443}
@@ -4861,7 +4862,7 @@ MonoBehaviour:
uiSceneName: UI_UI
musicPicFadeTime: 0.25
showLevel_prefab: {fileID: 775714823646948286, guid: 4c26aec0fc471c24d94487eff6821e14, type: 3}
store_prefab: {fileID: 0}
store_prefab: {fileID: 3878406062860244594, guid: d94caf56bd6dff64fa606eeeb6e06fa9, type: 3}
settings_prefab: {fileID: 4801108313180107556, guid: 8db6dd820b152984980f7fd2124138c3, type: 3}
userInfo_prefab: {fileID: 0}
email_prefab: {fileID: 3756075665884320709, guid: 41f4ea31b64f18d48aeef21b79930a6c, type: 3}
@@ -4996,81 +4997,6 @@ MonoBehaviour:
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!1 &5252340762442355171
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1406487017777146792}
- component: {fileID: 5423049842164886235}
- component: {fileID: 7245051247299556331}
m_Layer: 5
m_Name: Image (2)
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &1406487017777146792
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5252340762442355171}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1858607040280804733}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 0.000049591064}
m_SizeDelta: {x: 12.5919, y: 14.2488}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &5423049842164886235
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5252340762442355171}
m_CullTransparentMesh: 1
--- !u!114 &7245051247299556331
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5252340762442355171}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.24705882, g: 0.2901961, b: 0.3372549, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 8147908939353441651, guid: cfb305efbe7c8c4408085867d5209768, type: 3}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!1 &5343413001911633717
GameObject:
m_ObjectHideFlags: 0
@@ -5722,7 +5648,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
m_IsActive: 0
--- !u!224 &9209391586188457348
RectTransform:
m_ObjectHideFlags: 0
@@ -5839,8 +5765,8 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300000, guid: 7f3b1220317607e44bbb8f0e1998044a, type: 3}
m_Type: 1
m_Sprite: {fileID: -6726115822594207860, guid: 4ff010a6a12e2c1459b8693607095e21, type: 3}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
@@ -6462,9 +6388,9 @@ RectTransform:
m_Children: []
m_Father: {fileID: 9209391586188457348}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 20.100006, y: 3.5}
m_SizeDelta: {x: 82, y: 107}
m_Pivot: {x: 0, y: 1}
--- !u!222 &3945772172327520090
@@ -6522,7 +6448,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
m_IsActive: 0
--- !u!224 &7514224236584078014
RectTransform:
m_ObjectHideFlags: 0
@@ -7280,7 +7206,6 @@ RectTransform:
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 5311400505464405760}
- {fileID: 1406487017777146792}
m_Father: {fileID: 1216734273723155318}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
@@ -7678,9 +7603,9 @@ RectTransform:
m_Children: []
m_Father: {fileID: 9209391586188457348}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 10.050003, y: 3.5}
m_SizeDelta: {x: 84, y: 107}
m_Pivot: {x: 0, y: 1}
--- !u!222 &3586857079069254382
@@ -1,4 +1,4 @@
using UnityEngine;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;
using UnityEngine.SceneManagement;
@@ -12,6 +12,7 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
{
[Header("player unique so")]
public Player_SO player_SO;
public Text playerCoins_legacy;
[Header("put prefabs here")]
public GameObject putPrefabsHere;
[Header("son buttons")]
@@ -97,6 +98,7 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
// cached reference to the settings instance to ensure only one exists
private GameObject settingsInstance;
private GameObject storeInstance;
private GameObject showLevelInstance;
private GameObject emailInstance;
private GameObject noticeInstance;
@@ -119,6 +121,10 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
void Start()
{
PlayerEconomyLedger.EnsureInstance().AttachPlayerData(player_SO);
PlayerEconomyLedger.EnsureInstance().OnCoinsChanged += HandleCoinsChanged;
UpdatePlayerCoinsLegacyText(PlayerEconomyLedger.EnsureInstance().GetCoins());
if (button_Music == null)
{
var btn = transform.Find("Button_Music") as RectTransform;
@@ -132,15 +138,14 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
}
instantiateSettings = () => ToggleSettingsPrefab();
instantiateUserInfo = () => { gNotice.recommendation.display("功能即将上线,敬请期待"); };
instantiateStore = () => InstantiatePrefab(store_prefab);
instantiateStore = () => ShowStorePrefab();
instantiateShowLevel = () => { gNotice.error.display("功能即将下线,禁止访问"); };
// instantiateShowLevel = () => ShowLevelPrefab();
instantiateEmail = () => { gNotice.warning.display("此版本该功能暂不可用"); };
// instantiateEmail = () => ShowEmailPrefab();
instantiateNotice = () => { gNotice.warning.display("此版本该功能暂不可用"); };
instantiateEmail = () => ShowEmailPrefab();
instantiateNotice = () => { gNotice.warning.display("姝ょ増鏈鍔熻兘鏆備笉鍙敤"); };
// instantiateNotice = () => ShowNoticePrefab();
navGuideAction = () => ToggleGuideDisplay();
instantiateMarket = () => { gNotice.alarm.display("商店未开放"); };
instantiateMarket = () => ShowStorePrefab();
if (settings_launch != null)
settings_launch.onClick.AddListener(instantiateSettings);
@@ -412,6 +417,22 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
home_navButton.onClick.RemoveListener(navHomeAction);
if (settings_navButton != null && navSettingsAction != null)
settings_navButton.onClick.RemoveListener(navSettingsAction);
if (PlayerEconomyLedger.Instance != null)
PlayerEconomyLedger.Instance.OnCoinsChanged -= HandleCoinsChanged;
}
private void HandleCoinsChanged(int coinAmount)
{
UpdatePlayerCoinsLegacyText(coinAmount);
}
private void UpdatePlayerCoinsLegacyText(int coinAmount)
{
if (playerCoins_legacy != null)
{
playerCoins_legacy.text = coinAmount.ToString();
}
}
private void ToggleGuideDisplay()
@@ -595,6 +616,12 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
CloseInfoPanels(showLevelInstance);
}
private void ShowStorePrefab()
{
ShowPrefab(store_prefab, ref storeInstance);
CloseInfoPanels(storeInstance);
}
private void ShowEmailPrefab()
{
ShowPrefab(email_prefab, ref emailInstance);
@@ -631,6 +658,7 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
private void CloseInfoPanels(GameObject keep)
{
CloseInstance(ref storeInstance, keep);
CloseInstance(ref showLevelInstance, keep);
CloseInstance(ref emailInstance, keep);
CloseInstance(ref noticeInstance, keep);
@@ -950,3 +978,4 @@ public class btmandtopController : MonoBehaviour, ICancelHandler
}
}
}
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9553bd2f6e6680948adc5ebcd8052fed
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
public static class DushMaterialCatalog
{
private static readonly DushMaterialDescriptor[] Descriptors =
{
new DushMaterialDescriptor(DushMaterialKind.Material78021, "dush_78021", "突破材料78021", "dushMaterial78021"),
new DushMaterialDescriptor(DushMaterialKind.Material78022, "dush_78022", "突破材料78022", "dushMaterial78022"),
new DushMaterialDescriptor(DushMaterialKind.Material78023, "dush_78023", "突破材料78023", "dushMaterial78023"),
new DushMaterialDescriptor(DushMaterialKind.Material78024, "dush_78024", "突破材料78024", "dushMaterial78024")
};
private static readonly Dictionary<DushMaterialKind, DushMaterialDescriptor> ByKind = BuildByKind();
private static readonly Dictionary<string, DushMaterialDescriptor> ByKey = BuildByKey();
public static IReadOnlyList<DushMaterialDescriptor> All
{
get { return Descriptors; }
}
public static string GetKey(DushMaterialKind kind)
{
return ByKind[kind].Key;
}
public static string GetDisplayName(DushMaterialKind kind)
{
return ByKind[kind].DisplayName;
}
public static bool TryGetKind(string key, out DushMaterialKind kind)
{
kind = default(DushMaterialKind);
if (string.IsNullOrEmpty(key))
{
return false;
}
DushMaterialDescriptor descriptor;
if (!ByKey.TryGetValue(key, out descriptor))
{
return false;
}
kind = descriptor.Kind;
return true;
}
public static bool TryGetLegacyFieldName(DushMaterialKind kind, out string fieldName)
{
fieldName = null;
DushMaterialDescriptor descriptor;
if (!ByKind.TryGetValue(kind, out descriptor) || string.IsNullOrEmpty(descriptor.LegacyPlayerFieldName))
{
return false;
}
fieldName = descriptor.LegacyPlayerFieldName;
return true;
}
private static Dictionary<DushMaterialKind, DushMaterialDescriptor> BuildByKind()
{
var map = new Dictionary<DushMaterialKind, DushMaterialDescriptor>();
for (int i = 0; i < Descriptors.Length; i++)
{
map[Descriptors[i].Kind] = Descriptors[i];
}
return map;
}
private static Dictionary<string, DushMaterialDescriptor> BuildByKey()
{
var map = new Dictionary<string, DushMaterialDescriptor>(StringComparer.Ordinal);
for (int i = 0; i < Descriptors.Length; i++)
{
map[Descriptors[i].Key] = Descriptors[i];
}
return map;
}
}
public sealed class DushMaterialDescriptor
{
public DushMaterialDescriptor(DushMaterialKind kind, string key, string displayName, string legacyPlayerFieldName)
{
Kind = kind;
Key = key;
DisplayName = displayName;
LegacyPlayerFieldName = legacyPlayerFieldName;
}
public DushMaterialKind Kind { get; private set; }
public string Key { get; private set; }
public string DisplayName { get; private set; }
public string LegacyPlayerFieldName { get; private set; }
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6ddef3f5e3ba2bb459b87b89cbc204c1
@@ -0,0 +1,7 @@
public enum DushMaterialKind
{
Material78021,
Material78022,
Material78023,
Material78024
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e5e9ca0167d7b814d8fe2d7c462606df
@@ -0,0 +1,291 @@
using System;
using System.Collections.Generic;
using UnityEngine;
public sealed class DushMaterialLedger : MonoBehaviour
{
public static DushMaterialLedger Instance { get; private set; }
public event Action<DushMaterialKind, int> OnMaterialCountChanged;
private readonly Dictionary<string, int> countsByKey = new Dictionary<string, int>(StringComparer.Ordinal);
private bool initialized;
private bool loadedFromSave;
private Player_SO boundPlayerData;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Bootstrap()
{
EnsureInstance();
}
public static DushMaterialLedger EnsureInstance()
{
if (Instance != null)
{
return Instance;
}
var host = new GameObject("__runtime_dush_material_bridge");
host.hideFlags = HideFlags.HideInHierarchy | HideFlags.HideInInspector;
DontDestroyOnLoad(host);
Instance = host.AddComponent<DushMaterialLedger>();
return Instance;
}
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
InitializeIfNeeded();
}
private void OnApplicationPause(bool pauseStatus)
{
if (pauseStatus)
{
SaveNow();
}
}
private void OnApplicationQuit()
{
SaveNow();
}
public void InitializeIfNeeded()
{
if (initialized)
{
return;
}
DushMaterialLedgerPayload payload;
loadedFromSave = DushMaterialLedgerStorage.TryLoad(out payload);
RebuildFromPayload(payload);
initialized = true;
SaveNow();
}
public void AttachPlayerData(Player_SO playerData)
{
if (playerData == null)
{
return;
}
InitializeIfNeeded();
boundPlayerData = playerData;
if (!loadedFromSave)
{
SeedFromPlayerSo(playerData, false);
loadedFromSave = true;
}
else
{
SyncToPlayerData();
}
}
public int GetCount(DushMaterialKind kind)
{
return GetCountByKey(DushMaterialCatalog.GetKey(kind));
}
public void Add(DushMaterialKind kind, int amount)
{
if (amount <= 0)
{
return;
}
ChangeCount(DushMaterialCatalog.GetKey(kind), amount);
}
public bool SeedFromPlayerSo(Player_SO playerData, bool overwriteExistingCounts)
{
if (playerData == null)
{
return false;
}
InitializeIfNeeded();
bool changed = false;
for (int i = 0; i < DushMaterialCatalog.All.Count; i++)
{
var descriptor = DushMaterialCatalog.All[i];
if (string.IsNullOrEmpty(descriptor.LegacyPlayerFieldName))
{
continue;
}
int legacyValue = Mathf.Max(0, playerData.GetLegacyDushMaterialCount(descriptor.LegacyPlayerFieldName));
int currentValue = GetCountByKey(descriptor.Key);
if (!overwriteExistingCounts && currentValue > 0)
{
continue;
}
if (currentValue == legacyValue)
{
continue;
}
countsByKey[descriptor.Key] = legacyValue;
changed = true;
}
if (!changed)
{
return false;
}
SaveNow();
NotifyAllCountsChanged();
return true;
}
public void SaveNow()
{
if (!initialized)
{
InitializeIfNeeded();
}
DushMaterialLedgerStorage.TrySave(BuildPayload());
SyncToPlayerData();
}
private int GetCountByKey(string key)
{
InitializeIfNeeded();
int count;
if (!countsByKey.TryGetValue(key, out count))
{
return 0;
}
return count;
}
private void ChangeCount(string key, int delta)
{
InitializeIfNeeded();
int current = GetCountByKey(key);
long next = (long)current + delta;
if (next < 0)
{
next = 0;
}
else if (next > int.MaxValue)
{
next = int.MaxValue;
}
SetCountByKey(key, (int)next);
}
private void SetCountByKey(string key, int value)
{
int safeValue = Mathf.Max(0, value);
int current = GetCountByKey(key);
if (current == safeValue)
{
return;
}
countsByKey[key] = safeValue;
SaveNow();
DushMaterialKind kind;
if (DushMaterialCatalog.TryGetKind(key, out kind) && OnMaterialCountChanged != null)
{
OnMaterialCountChanged(kind, safeValue);
}
}
private void RebuildFromPayload(DushMaterialLedgerPayload payload)
{
countsByKey.Clear();
for (int i = 0; i < DushMaterialCatalog.All.Count; i++)
{
countsByKey[DushMaterialCatalog.All[i].Key] = 0;
}
if (payload == null || payload.entries == null)
{
return;
}
for (int i = 0; i < payload.entries.Count; i++)
{
var entry = payload.entries[i];
if (entry == null || string.IsNullOrEmpty(entry.key))
{
continue;
}
countsByKey[entry.key] = Mathf.Max(0, entry.count);
}
}
private DushMaterialLedgerPayload BuildPayload()
{
var payload = DushMaterialLedgerStorage.CreateDefaultPayload();
payload.entries.Clear();
for (int i = 0; i < DushMaterialCatalog.All.Count; i++)
{
var descriptor = DushMaterialCatalog.All[i];
payload.entries.Add(new DushMaterialEntry
{
key = descriptor.Key,
count = GetCountByKey(descriptor.Key)
});
}
return payload;
}
private void SyncToPlayerData()
{
if (boundPlayerData == null)
{
return;
}
for (int i = 0; i < DushMaterialCatalog.All.Count; i++)
{
var descriptor = DushMaterialCatalog.All[i];
if (string.IsNullOrEmpty(descriptor.LegacyPlayerFieldName))
{
continue;
}
boundPlayerData.SetLegacyDushMaterialCount(descriptor.LegacyPlayerFieldName, GetCountByKey(descriptor.Key));
}
}
private void NotifyAllCountsChanged()
{
if (OnMaterialCountChanged == null)
{
return;
}
for (int i = 0; i < DushMaterialCatalog.All.Count; i++)
{
var descriptor = DushMaterialCatalog.All[i];
OnMaterialCountChanged(descriptor.Kind, GetCountByKey(descriptor.Key));
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ada06cb892a78874e9bf37d4a8944f3a
@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
[Serializable]
public class DushMaterialEntry
{
public string key;
public int count;
}
[Serializable]
public class DushMaterialLedgerPayload
{
public int version = 1;
public long lastUpdatedUtcTicks;
public List<DushMaterialEntry> entries = new List<DushMaterialEntry>();
}
[Serializable]
public class DushMaterialLedgerEnvelope
{
public string payload;
public string signature;
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5723f5a049e0ef244b877816827a2678
@@ -0,0 +1,202 @@
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using UnityEngine;
public static class DushMaterialLedgerStorage
{
private const string SecretSeed = "ban_total.dush_material_ledger.v1";
private const string VaultDirectoryName = ".cache_bridge";
private const string MainFileName = ".dsm.dat";
private const string BackupFileName = ".dsm.bak";
private const string TempFileName = ".dsm.tmp";
private static string VaultDirectoryPath
{
get { return Path.Combine(Application.persistentDataPath, VaultDirectoryName); }
}
private static string MainFilePath
{
get { return Path.Combine(VaultDirectoryPath, MainFileName); }
}
private static string BackupFilePath
{
get { return Path.Combine(VaultDirectoryPath, BackupFileName); }
}
private static string TempFilePath
{
get { return Path.Combine(VaultDirectoryPath, TempFileName); }
}
public static bool TryLoad(out DushMaterialLedgerPayload payload)
{
payload = CreateDefaultPayload();
if (TryReadPayload(MainFilePath, out payload))
{
return true;
}
if (TryReadPayload(BackupFilePath, out payload))
{
TrySave(payload);
return true;
}
return false;
}
public static bool TrySave(DushMaterialLedgerPayload payload)
{
try
{
Directory.CreateDirectory(VaultDirectoryPath);
TryHidePath(VaultDirectoryPath);
var envelopeJson = BuildEnvelopeJson(payload);
File.WriteAllText(TempFilePath, envelopeJson, Encoding.UTF8);
TryHidePath(TempFilePath);
if (File.Exists(MainFilePath))
{
File.Copy(MainFilePath, BackupFilePath, true);
TryHidePath(BackupFilePath);
}
File.Copy(TempFilePath, MainFilePath, true);
TryHidePath(MainFilePath);
File.Delete(TempFilePath);
return true;
}
catch (Exception ex)
{
Debug.LogWarning($"[DushMaterialLedgerStorage] Save failed: {ex.Message}");
return false;
}
}
public static DushMaterialLedgerPayload CreateDefaultPayload()
{
return new DushMaterialLedgerPayload
{
version = 1,
lastUpdatedUtcTicks = DateTime.UtcNow.Ticks,
entries = new System.Collections.Generic.List<DushMaterialEntry>()
};
}
private static bool TryReadPayload(string path, out DushMaterialLedgerPayload payload)
{
payload = CreateDefaultPayload();
if (!File.Exists(path))
{
return false;
}
try
{
var envelopeJson = File.ReadAllText(path, Encoding.UTF8);
var envelope = JsonUtility.FromJson<DushMaterialLedgerEnvelope>(envelopeJson);
if (envelope == null || string.IsNullOrEmpty(envelope.payload) || string.IsNullOrEmpty(envelope.signature))
{
return false;
}
var expectedSignature = ComputeSignature(envelope.payload);
if (!string.Equals(expectedSignature, envelope.signature, StringComparison.Ordinal))
{
Debug.LogWarning("[DushMaterialLedgerStorage] Save signature mismatch. Possible tampering detected.");
return false;
}
var encryptedBytes = Convert.FromBase64String(envelope.payload);
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes());
var payloadJson = Encoding.UTF8.GetString(plainBytes);
var loadedPayload = JsonUtility.FromJson<DushMaterialLedgerPayload>(payloadJson);
if (loadedPayload == null)
{
return false;
}
payload = loadedPayload;
return true;
}
catch (Exception ex)
{
Debug.LogWarning($"[DushMaterialLedgerStorage] Load failed from '{path}': {ex.Message}");
return false;
}
}
private static string BuildEnvelopeJson(DushMaterialLedgerPayload payload)
{
payload.lastUpdatedUtcTicks = DateTime.UtcNow.Ticks;
var payloadJson = JsonUtility.ToJson(payload, false);
var plainBytes = Encoding.UTF8.GetBytes(payloadJson);
var encryptedBytes = XorTransform(plainBytes, BuildKeyBytes());
var payloadBase64 = Convert.ToBase64String(encryptedBytes);
var envelope = new DushMaterialLedgerEnvelope
{
payload = payloadBase64,
signature = ComputeSignature(payloadBase64)
};
return JsonUtility.ToJson(envelope, false);
}
private static string ComputeSignature(string payloadBase64)
{
var signText = payloadBase64 + "|" + Application.identifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
using (var sha = SHA256.Create())
{
var bytes = Encoding.UTF8.GetBytes(signText);
var hash = sha.ComputeHash(bytes);
return Convert.ToBase64String(hash);
}
}
private static byte[] BuildKeyBytes()
{
using (var sha = SHA256.Create())
{
var seed = Application.identifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
}
}
private static byte[] XorTransform(byte[] source, byte[] key)
{
var result = new byte[source.Length];
for (int i = 0; i < source.Length; i++)
{
result[i] = (byte)(source[i] ^ key[i % key.Length]);
}
return result;
}
private static void TryHidePath(string path)
{
try
{
if (Application.platform != RuntimePlatform.WindowsEditor && Application.platform != RuntimePlatform.WindowsPlayer)
{
return;
}
var attributes = File.GetAttributes(path);
if ((attributes & FileAttributes.Hidden) == 0)
{
File.SetAttributes(path, attributes | FileAttributes.Hidden);
}
}
catch
{
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 86b3e9466d0c05649a03301d13dd870c
@@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
public static class ExpBottleCatalog
{
private static readonly ExpBottleDescriptor[] Descriptors =
{
new ExpBottleDescriptor(ExpBottleKind.Common, "exp_common", "凡品经验瓶", "commonExpBottle78001"),
new ExpBottleDescriptor(ExpBottleKind.Medium, "exp_medium", "中品经验瓶", "mediumExpBottle78002"),
new ExpBottleDescriptor(ExpBottleKind.Superior, "exp_superior", "上品经验瓶", "superiorExpBottle78003"),
new ExpBottleDescriptor(ExpBottleKind.Supreme, "exp_supreme", "极品经验瓶", "supremeExpBottle78004"),
new ExpBottleDescriptor(ExpBottleKind.Extraordinary, "exp_extraordinary", "绝品经验瓶", "extraordinaryExpBottle78005"),
new ExpBottleDescriptor(ExpBottleKind.Celestial, "exp_celestial", "仙品经验瓶", "celestialExpBottle78006"),
new ExpBottleDescriptor(ExpBottleKind.RainAll, "exp_rain_all", "雨露均沾", null),
new ExpBottleDescriptor(ExpBottleKind.AdvancedRainAll, "exp_rain_all_advanced", "高级雨露均沾", null),
new ExpBottleDescriptor(ExpBottleKind.SuperRainAll, "exp_rain_all_super", "超级雨露均沾", null)
};
private static readonly Dictionary<ExpBottleKind, ExpBottleDescriptor> ByKind = BuildByKind();
private static readonly Dictionary<string, ExpBottleDescriptor> ByKey = BuildByKey();
public static IReadOnlyList<ExpBottleDescriptor> All
{
get { return Descriptors; }
}
public static string GetKey(ExpBottleKind kind)
{
return ByKind[kind].Key;
}
public static string GetDisplayName(ExpBottleKind kind)
{
return ByKind[kind].DisplayName;
}
public static bool TryGetKind(string key, out ExpBottleKind kind)
{
kind = default(ExpBottleKind);
if (string.IsNullOrEmpty(key))
{
return false;
}
ExpBottleDescriptor descriptor;
if (!ByKey.TryGetValue(key, out descriptor))
{
return false;
}
kind = descriptor.Kind;
return true;
}
public static bool TryGetLegacyFieldName(ExpBottleKind kind, out string fieldName)
{
fieldName = null;
ExpBottleDescriptor descriptor;
if (!ByKind.TryGetValue(kind, out descriptor) || string.IsNullOrEmpty(descriptor.LegacyPlayerFieldName))
{
return false;
}
fieldName = descriptor.LegacyPlayerFieldName;
return true;
}
private static Dictionary<ExpBottleKind, ExpBottleDescriptor> BuildByKind()
{
var map = new Dictionary<ExpBottleKind, ExpBottleDescriptor>();
for (int i = 0; i < Descriptors.Length; i++)
{
map[Descriptors[i].Kind] = Descriptors[i];
}
return map;
}
private static Dictionary<string, ExpBottleDescriptor> BuildByKey()
{
var map = new Dictionary<string, ExpBottleDescriptor>(StringComparer.Ordinal);
for (int i = 0; i < Descriptors.Length; i++)
{
map[Descriptors[i].Key] = Descriptors[i];
}
return map;
}
}
public sealed class ExpBottleDescriptor
{
public ExpBottleDescriptor(ExpBottleKind kind, string key, string displayName, string legacyPlayerFieldName)
{
Kind = kind;
Key = key;
DisplayName = displayName;
LegacyPlayerFieldName = legacyPlayerFieldName;
}
public ExpBottleKind Kind { get; private set; }
public string Key { get; private set; }
public string DisplayName { get; private set; }
public string LegacyPlayerFieldName { get; private set; }
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f77f44577d9ae334bba50305822e2a54
@@ -0,0 +1,12 @@
public enum ExpBottleKind
{
Common,
Medium,
Superior,
Supreme,
Extraordinary,
Celestial,
RainAll,
AdvancedRainAll,
SuperRainAll
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c5a8cc0e8ff06254f891cfd7aebd7c81
@@ -0,0 +1,444 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
public sealed class ExpBottleLedger : MonoBehaviour
{
public static ExpBottleLedger Instance { get; private set; }
public event Action<ExpBottleKind, int> OnBottleCountChanged;
public event Action OnLedgerReloaded;
private readonly Dictionary<string, int> countsByKey = new Dictionary<string, int>(StringComparer.Ordinal);
private bool initialized;
private bool loadedFromSave;
private Player_SO boundPlayerData;
public bool IsReady
{
get { return initialized; }
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Bootstrap()
{
EnsureInstance();
}
public static ExpBottleLedger EnsureInstance()
{
if (Instance != null)
{
return Instance;
}
var host = new GameObject("__runtime_cache_bridge");
host.hideFlags = HideFlags.HideInHierarchy | HideFlags.HideInInspector;
DontDestroyOnLoad(host);
Instance = host.AddComponent<ExpBottleLedger>();
return Instance;
}
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
InitializeIfNeeded();
}
private void OnApplicationPause(bool pauseStatus)
{
if (pauseStatus)
{
SaveNow();
}
}
private void OnApplicationQuit()
{
SaveNow();
}
public void InitializeIfNeeded()
{
if (initialized)
{
return;
}
ExpBottleLedgerPayload payload;
loadedFromSave = ExpBottleLedgerStorage.TryLoad(out payload);
RebuildFromPayload(payload);
initialized = true;
SaveNow();
if (OnLedgerReloaded != null)
{
OnLedgerReloaded();
}
}
public void AttachPlayerData(Player_SO playerData)
{
if (playerData == null)
{
return;
}
InitializeIfNeeded();
boundPlayerData = playerData;
if (!loadedFromSave)
{
SeedFromPlayerSo(playerData, false);
loadedFromSave = true;
}
else
{
SyncToPlayerData();
}
}
public int GetCount(ExpBottleKind kind)
{
return GetCountByKey(ExpBottleCatalog.GetKey(kind));
}
public int GetCountByKey(string key)
{
InitializeIfNeeded();
int count;
if (!countsByKey.TryGetValue(key, out count))
{
return 0;
}
return count;
}
public IReadOnlyList<ExpBottleSnapshotEntry> GetSnapshot()
{
InitializeIfNeeded();
var result = new List<ExpBottleSnapshotEntry>(ExpBottleCatalog.All.Count);
for (int i = 0; i < ExpBottleCatalog.All.Count; i++)
{
var descriptor = ExpBottleCatalog.All[i];
result.Add(new ExpBottleSnapshotEntry
{
key = descriptor.Key,
count = GetCountByKey(descriptor.Key)
});
}
return result;
}
public void Add(ExpBottleKind kind, int amount)
{
if (amount <= 0)
{
return;
}
ChangeCount(ExpBottleCatalog.GetKey(kind), amount);
}
public void AddByKey(string key, int amount)
{
if (string.IsNullOrWhiteSpace(key) || amount <= 0)
{
return;
}
ChangeCount(key.Trim(), amount);
}
public bool TryConsume(ExpBottleKind kind, int amount)
{
if (amount <= 0)
{
return false;
}
var key = ExpBottleCatalog.GetKey(kind);
var current = GetCountByKey(key);
if (current < amount)
{
return false;
}
ChangeCount(key, -amount);
DailyTaskEventHub.ReportUseItem(amount);
return true;
}
public bool TryConsumeByKey(string key, int amount)
{
if (string.IsNullOrWhiteSpace(key) || amount <= 0)
{
return false;
}
var safeKey = key.Trim();
var current = GetCountByKey(safeKey);
if (current < amount)
{
return false;
}
ChangeCount(safeKey, -amount);
DailyTaskEventHub.ReportUseItem(amount);
return true;
}
public void SetCount(ExpBottleKind kind, int value)
{
var safeValue = Mathf.Max(0, value);
SetCountByKey(ExpBottleCatalog.GetKey(kind), safeValue);
}
public void ResetAllToZero()
{
InitializeIfNeeded();
for (int i = 0; i < ExpBottleCatalog.All.Count; i++)
{
countsByKey[ExpBottleCatalog.All[i].Key] = 0;
}
SaveNow();
NotifyAllCountsChanged();
}
public bool SeedFromPlayerSo(Player_SO playerData, bool overwriteExistingCounts)
{
if (playerData == null)
{
return false;
}
InitializeIfNeeded();
bool changed = false;
var flags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public;
var type = typeof(Player_SO);
for (int i = 0; i < ExpBottleCatalog.All.Count; i++)
{
var descriptor = ExpBottleCatalog.All[i];
if (string.IsNullOrEmpty(descriptor.LegacyPlayerFieldName))
{
continue;
}
var field = type.GetField(descriptor.LegacyPlayerFieldName, flags);
if (field == null || field.FieldType != typeof(int))
{
continue;
}
var legacyValue = Mathf.Max(0, (int)field.GetValue(playerData));
var currentValue = GetCountByKey(descriptor.Key);
if (!overwriteExistingCounts && currentValue > 0)
{
continue;
}
if (currentValue == legacyValue)
{
continue;
}
countsByKey[descriptor.Key] = legacyValue;
changed = true;
}
if (!changed)
{
return false;
}
SaveNow();
NotifyAllCountsChanged();
return true;
}
public bool HasEnough(ExpBottleKind kind, int amount)
{
if (amount <= 0)
{
return true;
}
return GetCount(kind) >= amount;
}
public bool HasEnoughByKey(string key, int amount)
{
if (amount <= 0)
{
return true;
}
if (string.IsNullOrWhiteSpace(key))
{
return false;
}
return GetCountByKey(key.Trim()) >= amount;
}
public void SaveNow()
{
InitializeIfNeededForSave();
ExpBottleLedgerStorage.TrySave(BuildPayload());
SyncToPlayerData();
}
private void InitializeIfNeededForSave()
{
if (!initialized)
{
InitializeIfNeeded();
}
}
private void ChangeCount(string key, int delta)
{
InitializeIfNeeded();
var current = GetCountByKey(key);
long next = (long)current + delta;
if (next < 0)
{
next = 0;
}
else if (next > int.MaxValue)
{
next = int.MaxValue;
}
SetCountByKey(key, (int)next);
}
private void SetCountByKey(string key, int value)
{
InitializeIfNeeded();
int safeValue = Mathf.Max(0, value);
int current = GetCountByKey(key);
if (current == safeValue)
{
return;
}
countsByKey[key] = safeValue;
SaveNow();
ExpBottleKind kind;
if (ExpBottleCatalog.TryGetKind(key, out kind) && OnBottleCountChanged != null)
{
OnBottleCountChanged(kind, safeValue);
}
}
private void RebuildFromPayload(ExpBottleLedgerPayload payload)
{
countsByKey.Clear();
for (int i = 0; i < ExpBottleCatalog.All.Count; i++)
{
countsByKey[ExpBottleCatalog.All[i].Key] = 0;
}
if (payload == null || payload.entries == null)
{
return;
}
for (int i = 0; i < payload.entries.Count; i++)
{
var entry = payload.entries[i];
if (entry == null || string.IsNullOrEmpty(entry.key))
{
continue;
}
countsByKey[entry.key] = Mathf.Max(0, entry.count);
}
}
private ExpBottleLedgerPayload BuildPayload()
{
var payload = ExpBottleLedgerStorage.CreateDefaultPayload();
payload.entries.Clear();
var serializedKeys = new HashSet<string>(StringComparer.Ordinal);
for (int i = 0; i < ExpBottleCatalog.All.Count; i++)
{
var descriptor = ExpBottleCatalog.All[i];
payload.entries.Add(new ExpBottleEntry
{
key = descriptor.Key,
count = GetCountByKey(descriptor.Key)
});
serializedKeys.Add(descriptor.Key);
}
foreach (var pair in countsByKey)
{
if (serializedKeys.Contains(pair.Key))
{
continue;
}
payload.entries.Add(new ExpBottleEntry
{
key = pair.Key,
count = Mathf.Max(0, pair.Value)
});
}
return payload;
}
private void NotifyAllCountsChanged()
{
if (OnBottleCountChanged == null)
{
return;
}
for (int i = 0; i < ExpBottleCatalog.All.Count; i++)
{
var descriptor = ExpBottleCatalog.All[i];
OnBottleCountChanged(descriptor.Kind, GetCountByKey(descriptor.Key));
}
}
private void SyncToPlayerData()
{
if (boundPlayerData == null)
{
return;
}
for (int i = 0; i < ExpBottleCatalog.All.Count; i++)
{
var descriptor = ExpBottleCatalog.All[i];
if (string.IsNullOrEmpty(descriptor.LegacyPlayerFieldName))
{
continue;
}
boundPlayerData.SetLegacyExpBottleCount(descriptor.LegacyPlayerFieldName, GetCountByKey(descriptor.Key));
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: afddc8319b116d444af5bd7bb307bb53
@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
[Serializable]
public class ExpBottleEntry
{
public string key;
public int count;
}
[Serializable]
public class ExpBottleLedgerPayload
{
public int version = 1;
public long lastUpdatedUtcTicks;
public List<ExpBottleEntry> entries = new List<ExpBottleEntry>();
}
[Serializable]
public class ExpBottleLedgerEnvelope
{
public string payload;
public string signature;
}
[Serializable]
public class ExpBottleSnapshotEntry
{
public string key;
public int count;
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 657e3733be92d094ab912ce247c6e9e6
@@ -0,0 +1,202 @@
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using UnityEngine;
public static class ExpBottleLedgerStorage
{
private const string SecretSeed = "ban_total.exp_bottle_ledger.v1";
private const string VaultDirectoryName = ".cache_bridge";
private const string MainFileName = ".xpv.dat";
private const string BackupFileName = ".xpv.bak";
private const string TempFileName = ".xpv.tmp";
private static string VaultDirectoryPath
{
get { return Path.Combine(Application.persistentDataPath, VaultDirectoryName); }
}
private static string MainFilePath
{
get { return Path.Combine(VaultDirectoryPath, MainFileName); }
}
private static string BackupFilePath
{
get { return Path.Combine(VaultDirectoryPath, BackupFileName); }
}
private static string TempFilePath
{
get { return Path.Combine(VaultDirectoryPath, TempFileName); }
}
public static bool TryLoad(out ExpBottleLedgerPayload payload)
{
payload = CreateDefaultPayload();
if (TryReadPayload(MainFilePath, out payload))
{
return true;
}
if (TryReadPayload(BackupFilePath, out payload))
{
TrySave(payload);
return true;
}
return false;
}
public static bool TrySave(ExpBottleLedgerPayload payload)
{
try
{
Directory.CreateDirectory(VaultDirectoryPath);
TryHidePath(VaultDirectoryPath);
var envelopeJson = BuildEnvelopeJson(payload);
File.WriteAllText(TempFilePath, envelopeJson, Encoding.UTF8);
TryHidePath(TempFilePath);
if (File.Exists(MainFilePath))
{
File.Copy(MainFilePath, BackupFilePath, true);
TryHidePath(BackupFilePath);
}
File.Copy(TempFilePath, MainFilePath, true);
TryHidePath(MainFilePath);
File.Delete(TempFilePath);
return true;
}
catch (Exception ex)
{
Debug.LogWarning($"[ExpBottleLedgerStorage] Save failed: {ex.Message}");
return false;
}
}
public static ExpBottleLedgerPayload CreateDefaultPayload()
{
return new ExpBottleLedgerPayload
{
version = 1,
lastUpdatedUtcTicks = DateTime.UtcNow.Ticks,
entries = new System.Collections.Generic.List<ExpBottleEntry>()
};
}
private static bool TryReadPayload(string path, out ExpBottleLedgerPayload payload)
{
payload = CreateDefaultPayload();
if (!File.Exists(path))
{
return false;
}
try
{
var envelopeJson = File.ReadAllText(path, Encoding.UTF8);
var envelope = JsonUtility.FromJson<ExpBottleLedgerEnvelope>(envelopeJson);
if (envelope == null || string.IsNullOrEmpty(envelope.payload) || string.IsNullOrEmpty(envelope.signature))
{
return false;
}
var expectedSignature = ComputeSignature(envelope.payload);
if (!string.Equals(expectedSignature, envelope.signature, StringComparison.Ordinal))
{
Debug.LogWarning("[ExpBottleLedgerStorage] Save signature mismatch. Possible tampering detected.");
return false;
}
var encryptedBytes = Convert.FromBase64String(envelope.payload);
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes());
var payloadJson = Encoding.UTF8.GetString(plainBytes);
var loadedPayload = JsonUtility.FromJson<ExpBottleLedgerPayload>(payloadJson);
if (loadedPayload == null)
{
return false;
}
payload = loadedPayload;
return true;
}
catch (Exception ex)
{
Debug.LogWarning($"[ExpBottleLedgerStorage] Load failed from '{path}': {ex.Message}");
return false;
}
}
private static string BuildEnvelopeJson(ExpBottleLedgerPayload payload)
{
payload.lastUpdatedUtcTicks = DateTime.UtcNow.Ticks;
var payloadJson = JsonUtility.ToJson(payload, false);
var plainBytes = Encoding.UTF8.GetBytes(payloadJson);
var encryptedBytes = XorTransform(plainBytes, BuildKeyBytes());
var payloadBase64 = Convert.ToBase64String(encryptedBytes);
var envelope = new ExpBottleLedgerEnvelope
{
payload = payloadBase64,
signature = ComputeSignature(payloadBase64)
};
return JsonUtility.ToJson(envelope, false);
}
private static string ComputeSignature(string payloadBase64)
{
var signText = payloadBase64 + "|" + Application.identifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
using (var sha = SHA256.Create())
{
var bytes = Encoding.UTF8.GetBytes(signText);
var hash = sha.ComputeHash(bytes);
return Convert.ToBase64String(hash);
}
}
private static byte[] BuildKeyBytes()
{
using (var sha = SHA256.Create())
{
var seed = Application.identifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
}
}
private static byte[] XorTransform(byte[] source, byte[] key)
{
var result = new byte[source.Length];
for (int i = 0; i < source.Length; i++)
{
result[i] = (byte)(source[i] ^ key[i % key.Length]);
}
return result;
}
private static void TryHidePath(string path)
{
try
{
if (Application.platform != RuntimePlatform.WindowsEditor && Application.platform != RuntimePlatform.WindowsPlayer)
{
return;
}
var attributes = File.GetAttributes(path);
if ((attributes & FileAttributes.Hidden) == 0)
{
File.SetAttributes(path, attributes | FileAttributes.Hidden);
}
}
catch
{
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 33984b0353b28904d8e022ea7f2355e5
@@ -0,0 +1,222 @@
using System;
using UnityEngine;
public sealed class PlayerEconomyLedger : MonoBehaviour
{
public static PlayerEconomyLedger Instance { get; private set; }
public event Action<int> OnCoinsChanged;
public event Action<int> OnMaterialChanged;
private PlayerEconomyPayload payload;
private bool initialized;
private bool loadedFromSave;
private Player_SO boundPlayerData;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Bootstrap()
{
EnsureInstance();
}
public static PlayerEconomyLedger EnsureInstance()
{
if (Instance != null)
{
return Instance;
}
var host = new GameObject("__runtime_wallet_bridge");
host.hideFlags = HideFlags.HideInHierarchy | HideFlags.HideInInspector;
DontDestroyOnLoad(host);
Instance = host.AddComponent<PlayerEconomyLedger>();
return Instance;
}
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
InitializeIfNeeded();
}
private void OnApplicationPause(bool pauseStatus)
{
if (pauseStatus)
{
SaveNow();
}
}
private void OnApplicationQuit()
{
SaveNow();
}
public void InitializeIfNeeded()
{
if (initialized)
{
return;
}
PlayerEconomyPayload loadedPayload;
loadedFromSave = PlayerEconomyStorage.TryLoad(out loadedPayload);
payload = loadedPayload;
initialized = true;
SaveNow();
}
public void AttachPlayerData(Player_SO playerData)
{
if (playerData == null)
{
return;
}
InitializeIfNeeded();
boundPlayerData = playerData;
if (!loadedFromSave)
{
payload.coins = Mathf.Max(0, playerData.Coins);
payload.material = Mathf.Max(0, playerData.Material);
SaveNow();
loadedFromSave = true;
}
SyncToPlayerData();
NotifyEconomyChanged();
}
public int GetCoins()
{
InitializeIfNeeded();
return payload.coins;
}
public int GetMaterial()
{
InitializeIfNeeded();
return payload.material;
}
public bool HasEnoughCoins(long amount)
{
if (amount <= 0)
{
return true;
}
InitializeIfNeeded();
return payload.coins >= amount;
}
public void AddCoins(int amount)
{
if (amount <= 0)
{
return;
}
InitializeIfNeeded();
long next = (long)payload.coins + amount;
payload.coins = next > int.MaxValue ? int.MaxValue : (int)next;
SaveNow();
}
public void AddMaterial(int amount)
{
if (amount <= 0)
{
return;
}
InitializeIfNeeded();
long next = (long)payload.material + amount;
payload.material = next > int.MaxValue ? int.MaxValue : (int)next;
SaveNow();
}
public bool TrySpendCoins(int amount)
{
if (amount <= 0)
{
return true;
}
InitializeIfNeeded();
if (payload.coins < amount)
{
return false;
}
payload.coins -= amount;
SaveNow();
DailyTaskEventHub.ReportSpendCoins(amount);
return true;
}
public void SetCoins(int value)
{
InitializeIfNeeded();
payload.coins = Mathf.Max(0, value);
SaveNow();
}
public void SaveNow()
{
InitializeIfNeededForSave();
PlayerEconomyStorage.TrySave(payload);
SyncToPlayerData();
NotifyEconomyChanged();
}
private void InitializeIfNeededForSave()
{
if (!initialized)
{
InitializeIfNeeded();
}
if (payload == null)
{
payload = PlayerEconomyStorage.CreateDefaultPayload();
}
}
private void SyncToPlayerData()
{
if (boundPlayerData == null || payload == null)
{
return;
}
boundPlayerData.SetCoins(payload.coins);
boundPlayerData.SetMaterial(payload.material);
}
private void NotifyEconomyChanged()
{
if (payload == null)
{
return;
}
if (OnCoinsChanged != null)
{
OnCoinsChanged(payload.coins);
}
if (OnMaterialChanged != null)
{
OnMaterialChanged(payload.material);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1f7dcb3a6dc02924a9c4ac11aada44c5
@@ -0,0 +1,17 @@
using System;
[Serializable]
public class PlayerEconomyPayload
{
public int version = 1;
public long lastUpdatedUtcTicks;
public int coins;
public int material;
}
[Serializable]
public class PlayerEconomyEnvelope
{
public string payload;
public string signature;
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c23bb09c2ddc89b44b2dc1e4fdea6b31
@@ -0,0 +1,203 @@
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using UnityEngine;
public static class PlayerEconomyStorage
{
private const string SecretSeed = "ban_total.player_economy.v1";
private const string VaultDirectoryName = ".cache_bridge";
private const string MainFileName = ".eco.dat";
private const string BackupFileName = ".eco.bak";
private const string TempFileName = ".eco.tmp";
private static string VaultDirectoryPath
{
get { return Path.Combine(Application.persistentDataPath, VaultDirectoryName); }
}
private static string MainFilePath
{
get { return Path.Combine(VaultDirectoryPath, MainFileName); }
}
private static string BackupFilePath
{
get { return Path.Combine(VaultDirectoryPath, BackupFileName); }
}
private static string TempFilePath
{
get { return Path.Combine(VaultDirectoryPath, TempFileName); }
}
public static bool TryLoad(out PlayerEconomyPayload payload)
{
payload = CreateDefaultPayload();
if (TryReadPayload(MainFilePath, out payload))
{
return true;
}
if (TryReadPayload(BackupFilePath, out payload))
{
TrySave(payload);
return true;
}
return false;
}
public static bool TrySave(PlayerEconomyPayload payload)
{
try
{
Directory.CreateDirectory(VaultDirectoryPath);
TryHidePath(VaultDirectoryPath);
var envelopeJson = BuildEnvelopeJson(payload);
File.WriteAllText(TempFilePath, envelopeJson, Encoding.UTF8);
TryHidePath(TempFilePath);
if (File.Exists(MainFilePath))
{
File.Copy(MainFilePath, BackupFilePath, true);
TryHidePath(BackupFilePath);
}
File.Copy(TempFilePath, MainFilePath, true);
TryHidePath(MainFilePath);
File.Delete(TempFilePath);
return true;
}
catch (Exception ex)
{
Debug.LogWarning($"[PlayerEconomyStorage] Save failed: {ex.Message}");
return false;
}
}
public static PlayerEconomyPayload CreateDefaultPayload()
{
return new PlayerEconomyPayload
{
version = 1,
lastUpdatedUtcTicks = DateTime.UtcNow.Ticks,
coins = 0,
material = 0
};
}
private static bool TryReadPayload(string path, out PlayerEconomyPayload payload)
{
payload = CreateDefaultPayload();
if (!File.Exists(path))
{
return false;
}
try
{
var envelopeJson = File.ReadAllText(path, Encoding.UTF8);
var envelope = JsonUtility.FromJson<PlayerEconomyEnvelope>(envelopeJson);
if (envelope == null || string.IsNullOrEmpty(envelope.payload) || string.IsNullOrEmpty(envelope.signature))
{
return false;
}
var expectedSignature = ComputeSignature(envelope.payload);
if (!string.Equals(expectedSignature, envelope.signature, StringComparison.Ordinal))
{
Debug.LogWarning("[PlayerEconomyStorage] Save signature mismatch. Possible tampering detected.");
return false;
}
var encryptedBytes = Convert.FromBase64String(envelope.payload);
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes());
var payloadJson = Encoding.UTF8.GetString(plainBytes);
var loadedPayload = JsonUtility.FromJson<PlayerEconomyPayload>(payloadJson);
if (loadedPayload == null)
{
return false;
}
payload = loadedPayload;
return true;
}
catch (Exception ex)
{
Debug.LogWarning($"[PlayerEconomyStorage] Load failed from '{path}': {ex.Message}");
return false;
}
}
private static string BuildEnvelopeJson(PlayerEconomyPayload payload)
{
payload.lastUpdatedUtcTicks = DateTime.UtcNow.Ticks;
var payloadJson = JsonUtility.ToJson(payload, false);
var plainBytes = Encoding.UTF8.GetBytes(payloadJson);
var encryptedBytes = XorTransform(plainBytes, BuildKeyBytes());
var payloadBase64 = Convert.ToBase64String(encryptedBytes);
var envelope = new PlayerEconomyEnvelope
{
payload = payloadBase64,
signature = ComputeSignature(payloadBase64)
};
return JsonUtility.ToJson(envelope, false);
}
private static string ComputeSignature(string payloadBase64)
{
var signText = payloadBase64 + "|" + Application.identifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
using (var sha = SHA256.Create())
{
var bytes = Encoding.UTF8.GetBytes(signText);
var hash = sha.ComputeHash(bytes);
return Convert.ToBase64String(hash);
}
}
private static byte[] BuildKeyBytes()
{
using (var sha = SHA256.Create())
{
var seed = Application.identifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
}
}
private static byte[] XorTransform(byte[] source, byte[] key)
{
var result = new byte[source.Length];
for (int i = 0; i < source.Length; i++)
{
result[i] = (byte)(source[i] ^ key[i % key.Length]);
}
return result;
}
private static void TryHidePath(string path)
{
try
{
if (Application.platform != RuntimePlatform.WindowsEditor && Application.platform != RuntimePlatform.WindowsPlayer)
{
return;
}
var attributes = File.GetAttributes(path);
if ((attributes & FileAttributes.Hidden) == 0)
{
File.SetAttributes(path, attributes | FileAttributes.Hidden);
}
}
catch
{
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 10031f858527cb54e9670306e650d834
@@ -0,0 +1,101 @@
using System;
public static class StoreExpBottleGrantResolver
{
public static bool TryResolve(storeItemSO itemSO, out ExpBottleKind kind)
{
kind = default(ExpBottleKind);
if (itemSO == null)
{
return false;
}
switch (itemSO.itemID)
{
case 78001:
kind = ExpBottleKind.Common;
return true;
case 78002:
kind = ExpBottleKind.Medium;
return true;
case 78003:
kind = ExpBottleKind.Superior;
return true;
case 78004:
kind = ExpBottleKind.Supreme;
return true;
case 78005:
kind = ExpBottleKind.Extraordinary;
return true;
case 78006:
kind = ExpBottleKind.Celestial;
return true;
case 78011:
kind = ExpBottleKind.RainAll;
return true;
case 78012:
kind = ExpBottleKind.AdvancedRainAll;
return true;
case 78013:
kind = ExpBottleKind.SuperRainAll;
return true;
}
var name = (itemSO.itemName ?? string.Empty).Trim();
if (name.IndexOf("超级雨露均沾", StringComparison.Ordinal) >= 0)
{
kind = ExpBottleKind.SuperRainAll;
return true;
}
if (name.IndexOf("高级雨露均沾", StringComparison.Ordinal) >= 0)
{
kind = ExpBottleKind.AdvancedRainAll;
return true;
}
if (name.IndexOf("雨露均沾", StringComparison.Ordinal) >= 0)
{
kind = ExpBottleKind.RainAll;
return true;
}
if (name.IndexOf("凡品", StringComparison.Ordinal) >= 0)
{
kind = ExpBottleKind.Common;
return true;
}
if (name.IndexOf("中品", StringComparison.Ordinal) >= 0)
{
kind = ExpBottleKind.Medium;
return true;
}
if (name.IndexOf("上品", StringComparison.Ordinal) >= 0)
{
kind = ExpBottleKind.Superior;
return true;
}
if (name.IndexOf("极品", StringComparison.Ordinal) >= 0)
{
kind = ExpBottleKind.Supreme;
return true;
}
if (name.IndexOf("绝品", StringComparison.Ordinal) >= 0)
{
kind = ExpBottleKind.Extraordinary;
return true;
}
if (name.IndexOf("仙品", StringComparison.Ordinal) >= 0)
{
kind = ExpBottleKind.Celestial;
return true;
}
return false;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6d3976e57689138418c34c6437f16798
@@ -0,0 +1,133 @@
using System.Text;
using UnityEngine;
public static class StoreExpBottlePurchaseService
{
public static bool TryPurchase(Player_SO playerData, storeItemSO itemSO, int packageCount, out string failureMessage, out int grantedCount)
{
failureMessage = string.Empty;
grantedCount = 0;
if (playerData == null)
{
failureMessage = "玩家数据丢失";
return false;
}
if (itemSO == null)
{
failureMessage = "商品数据丢失";
return false;
}
if (!itemSO.isOnShelf || !itemSO.canbepurchased)
{
failureMessage = "物品未上架";
return false;
}
if (packageCount <= 0)
{
failureMessage = "本店禁止无实物交易";
return false;
}
if (itemSO.costRequirements == null || itemSO.costRequirements.Count == 0)
{
failureMessage = "不可购买";
return false;
}
var primaryCost = itemSO.costRequirements[0];
if (primaryCost.currencyType != storeItemSO.CurrencyType.coins)
{
failureMessage = "不可购买";
return false;
}
ExpBottleKind kind;
if (!StoreExpBottleGrantResolver.TryResolve(itemSO, out kind))
{
failureMessage = "当前仅支持经验瓶发放";
return false;
}
long longGrantedCount = (long)Mathf.Max(1, itemSO.itemSinglePurchaseQty) * packageCount;
if (longGrantedCount > int.MaxValue)
{
failureMessage = "达到单次购买限额";
return false;
}
grantedCount = (int)longGrantedCount;
if (itemSO.itemPurchaseQuota >= 0 && itemSO.purchasedCount + grantedCount > itemSO.itemPurchaseQuota)
{
failureMessage = "超过限购额度";
return false;
}
long totalCostLong = (long)primaryCost.amount * packageCount;
if (totalCostLong > int.MaxValue)
{
failureMessage = "达到单次购买限额";
return false;
}
int totalCost = (int)totalCostLong;
PlayerEconomyLedger.EnsureInstance().AttachPlayerData(playerData);
ExpBottleLedger.EnsureInstance().AttachPlayerData(playerData);
if (!PlayerEconomyLedger.EnsureInstance().HasEnoughCoins(totalCost))
{
failureMessage = "货币不足";
return false;
}
if (!PlayerEconomyLedger.EnsureInstance().TrySpendCoins(totalCost))
{
failureMessage = "货币不足";
return false;
}
ExpBottleLedger.EnsureInstance().Add(kind, grantedCount);
DebugPurchaseSuccess(itemSO, totalCost, grantedCount);
return true;
}
private static void DebugPurchaseSuccess(storeItemSO itemSO, int totalCost, int grantedCount)
{
var snapshot = ExpBottleLedger.EnsureInstance().GetSnapshot();
var builder = new StringBuilder();
builder.Append("[StorePurchase] 已成功购买:");
builder.Append(itemSO != null ? itemSO.itemName : "未知物品");
builder.Append(" | 花费Coins=");
builder.Append(totalCost);
builder.Append(" | 发放数量=");
builder.Append(grantedCount);
builder.Append(" | 经验瓶库存:");
bool appendedAny = false;
for (int i = 0; i < snapshot.Count; i++)
{
var entry = snapshot[i];
ExpBottleKind kind;
if (!ExpBottleCatalog.TryGetKind(entry.key, out kind))
{
continue;
}
if (appendedAny)
{
builder.Append(" ; ");
}
builder.Append(ExpBottleCatalog.GetDisplayName(kind));
builder.Append("=");
builder.Append(entry.count);
appendedAny = true;
}
Debug.Log(builder.ToString());
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: dabd40a1c2ebdaa4b87f6ce4d3a18a3f
@@ -0,0 +1,65 @@
public static class DailyTaskEventHub
{
public static void ReportLogin()
{
DailyTaskService.EnsureInstance().ReportEvent(new DailyTaskEventData
{
taskType = userTasksPool.TaskType.Login,
amount = 1f
});
}
public static void ReportPlaySong()
{
DailyTaskService.EnsureInstance().ReportEvent(new DailyTaskEventData
{
taskType = userTasksPool.TaskType.PlaySong,
amount = 1f
});
}
public static void ReportFullCombo()
{
DailyTaskService.EnsureInstance().ReportEvent(new DailyTaskEventData
{
taskType = userTasksPool.TaskType.FullCombo,
amount = 1f
});
}
public static void ReportTotalScore(float totalScore)
{
DailyTaskService.EnsureInstance().ReportEvent(new DailyTaskEventData
{
taskType = userTasksPool.TaskType.TotalScore,
amount = totalScore
});
}
public static void ReportUseItem(int amount)
{
DailyTaskService.EnsureInstance().ReportEvent(new DailyTaskEventData
{
taskType = userTasksPool.TaskType.UseItem,
amount = amount
});
}
public static void ReportSpendCoins(int amount)
{
DailyTaskService.EnsureInstance().ReportEvent(new DailyTaskEventData
{
taskType = userTasksPool.TaskType.SpendCoins,
amount = amount
});
}
public static void ReportGameDuration(float durationSeconds)
{
DailyTaskService.EnsureInstance().ReportEvent(new DailyTaskEventData
{
taskType = userTasksPool.TaskType.GameDuration,
amount = durationSeconds
});
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 14861f485792abe459d3724786b183f8
@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
[Serializable]
public class DailyTaskRuntimeEntry
{
public string taskID;
public float progress;
public bool isCompleted;
public bool isClaimed;
}
[Serializable]
public class DailyTaskSaveData
{
public string dateKey;
public int refreshUsedCount;
public List<DailyTaskRuntimeEntry> activeTasks = new List<DailyTaskRuntimeEntry>();
public List<DailyTaskAccumulatedProgress> accumulatedProgress = new List<DailyTaskAccumulatedProgress>();
}
public struct DailyTaskEventData
{
public userTasksPool.TaskType taskType;
public float amount;
}
[Serializable]
public class DailyTaskAccumulatedProgress
{
public int taskType;
public float progress;
}
public sealed class DailyTaskViewData
{
public userTasksPool.TaskDefinition definition;
public DailyTaskRuntimeEntry runtimeEntry;
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 03c6abfd01940964aa67c37ecf92af16
@@ -0,0 +1,49 @@
using System;
using System.IO;
using UnityEngine;
public static class DailyTaskSaveService
{
private static string SaveFilePath
{
get { return Path.Combine(Application.persistentDataPath, "daily_tasks.json"); }
}
public static DailyTaskSaveData Load()
{
if (!File.Exists(SaveFilePath))
{
return new DailyTaskSaveData();
}
try
{
var json = File.ReadAllText(SaveFilePath);
var data = JsonUtility.FromJson<DailyTaskSaveData>(json);
return data ?? new DailyTaskSaveData();
}
catch (Exception ex)
{
Debug.LogWarning($"[DailyTaskSaveService] Load failed: {ex.Message}");
return new DailyTaskSaveData();
}
}
public static void Save(DailyTaskSaveData data)
{
if (data == null)
{
return;
}
try
{
var json = JsonUtility.ToJson(data, true);
File.WriteAllText(SaveFilePath, json);
}
catch (Exception ex)
{
Debug.LogWarning($"[DailyTaskSaveService] Save failed: {ex.Message}");
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: abd7cb33db74f5f4096c58a5416f9b53
@@ -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();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 41ce491ff1399f340bd82a554499a880
+303 -4
View File
@@ -1,7 +1,6 @@
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Generic;
using Bansonic;
using UnityEngine;
using UnityEngine.UI;
public class dailyTaskManager : MonoBehaviour
@@ -9,16 +8,21 @@ public class dailyTaskManager : MonoBehaviour
[Header("任务池so")]
public userTasksPool taskPool;
[Header("player data")]
public Player_SO playerData;
[Header("今日进度")]
public Slider todayProgressSlider;
public Text todayProgressPercent;
public Text todayProgressText;
public Text notasksanymore;
[Header("一键领取")]
public Button oneKeyRewardBtn;
[Header("刷新任务")]
public Button refreshTaskBtn;
[Header("任务配置")]
[Tooltip("任务列表最大容量")]
public int taskMaxSize = 5;
@@ -28,4 +32,299 @@ public class dailyTaskManager : MonoBehaviour
[Header("prefabs")]
public GameObject dailyTaskItemPrefab;
public Transform dailyTaskItemParent;
[Header("reward sprites")]
public Sprite coinRewardSprite;
private readonly List<GameObject> spawnedTaskItems = new List<GameObject>();
private readonly Dictionary<int, Sprite> rewardSpriteByItemId = new Dictionary<int, Sprite>();
private void Start()
{
CacheRewardSpritesFromStoreItems();
var taskService = DailyTaskService.EnsureInstance();
taskService.Configure(taskPool, taskMaxSize, refreshMaxTimes);
taskService.OnTasksChanged += RefreshTaskUi;
if (oneKeyRewardBtn != null)
{
oneKeyRewardBtn.onClick.RemoveAllListeners();
oneKeyRewardBtn.onClick.AddListener(OnOneKeyRewardClicked);
}
if (refreshTaskBtn != null)
{
refreshTaskBtn.onClick.RemoveAllListeners();
refreshTaskBtn.onClick.AddListener(OnRefreshTaskClicked);
}
RefreshTaskUi();
}
private void OnDestroy()
{
if (DailyTaskService.Instance != null)
{
DailyTaskService.Instance.OnTasksChanged -= RefreshTaskUi;
}
}
private void RefreshTaskUi()
{
ClearSpawnedItems();
var tasks = DailyTaskService.EnsureInstance().GetActiveTaskViews();
if (notasksanymore != null)
{
notasksanymore.gameObject.SetActive(tasks.Count == 0);
notasksanymore.text = tasks.Count == 0 ? "今日暂无任务" : string.Empty;
}
int completedCount = 0;
for (int i = 0; i < tasks.Count; i++)
{
var taskView = tasks[i];
if (taskView == null || taskView.definition == null || taskView.runtimeEntry == null)
{
continue;
}
if (taskView.runtimeEntry.isCompleted)
{
completedCount += 1;
}
SpawnTaskItem(i, taskView);
}
UpdateTodayProgress(tasks.Count, completedCount);
RefreshOneKeyRewardButtonState(tasks);
RefreshTaskButtonState();
}
private void SpawnTaskItem(int index, DailyTaskViewData taskView)
{
if (dailyTaskItemPrefab == null || dailyTaskItemParent == null)
{
return;
}
var instance = Instantiate(dailyTaskItemPrefab, dailyTaskItemParent);
spawnedTaskItems.Add(instance);
var controller = instance.GetComponent<taskPrefabController>();
if (controller == null)
{
return;
}
controller.Setup(taskView, index + 1, ResolveRewardSprite(taskView.definition), () => OnSingleRewardClicked(taskView.runtimeEntry.taskID));
}
private void UpdateTodayProgress(int totalTaskCount, int completedCount)
{
float progress = totalTaskCount <= 0 ? 0f : (float)completedCount / totalTaskCount;
if (todayProgressSlider != null)
{
todayProgressSlider.value = progress;
}
if (todayProgressPercent != null)
{
todayProgressPercent.text = (progress * 100f).ToString("F0") + "%";
}
if (todayProgressText != null)
{
todayProgressText.text = completedCount + "/" + totalTaskCount;
}
}
private void ClearSpawnedItems()
{
for (int i = spawnedTaskItems.Count - 1; i >= 0; i--)
{
if (spawnedTaskItems[i] != null)
{
Destroy(spawnedTaskItems[i]);
}
}
spawnedTaskItems.Clear();
}
private void OnSingleRewardClicked(string taskID)
{
string failureMessage;
if (!DailyTaskService.EnsureInstance().TryClaimReward(taskID, playerData, out failureMessage))
{
if (!string.IsNullOrEmpty(failureMessage))
{
gNotice.warning.display(failureMessage);
}
return;
}
RefreshTaskUi();
}
private void OnOneKeyRewardClicked()
{
string failureMessage;
int claimedCount = DailyTaskService.EnsureInstance().ClaimAllAvailableRewards(playerData, out failureMessage);
if (claimedCount <= 0)
{
if (!string.IsNullOrEmpty(failureMessage))
{
gNotice.warning.display(failureMessage);
}
return;
}
RefreshTaskUi();
}
private void RefreshOneKeyRewardButtonState(IReadOnlyList<DailyTaskViewData> tasks)
{
if (oneKeyRewardBtn == null)
{
return;
}
bool hasClaimableReward = false;
for (int i = 0; i < tasks.Count; i++)
{
var taskView = tasks[i];
if (taskView == null || taskView.runtimeEntry == null)
{
continue;
}
if (taskView.runtimeEntry.isCompleted && !taskView.runtimeEntry.isClaimed)
{
hasClaimableReward = true;
break;
}
}
oneKeyRewardBtn.interactable = hasClaimableReward;
}
private void OnRefreshTaskClicked()
{
if (taskPool == null)
{
gNotice.warning.display("未配置任务池");
return;
}
var taskService = DailyTaskService.EnsureInstance();
if (!taskService.TryRefreshTasks())
{
if (taskService.GetRemainingRefreshCount() <= 0)
{
gNotice.warning.display("已达到今日刷新上限");
}
else
{
gNotice.warning.display("刷新失败");
}
RefreshTaskButtonState();
return;
}
RefreshTaskUi();
}
private void RefreshTaskButtonState()
{
if (refreshTaskBtn == null)
{
return;
}
refreshTaskBtn.interactable = taskPool != null && DailyTaskService.EnsureInstance().GetRemainingRefreshCount() > 0;
}
private Sprite ResolveRewardSprite(userTasksPool.TaskDefinition definition)
{
if (definition == null)
{
return null;
}
switch (definition.rewardType)
{
case userTasksPool.RewardType.coins:
return coinRewardSprite;
case userTasksPool.RewardType.expBottle:
return GetRewardSpriteByItemId(GetExpBottleRewardItemId(definition.rewardExpBottleKind));
case userTasksPool.RewardType.dushMaterial:
return GetRewardSpriteByItemId(GetDushMaterialRewardItemId(definition.rewardDushMaterialKind));
default:
return null;
}
}
private void CacheRewardSpritesFromStoreItems()
{
rewardSpriteByItemId.Clear();
var rewardStoreItems = Resources.LoadAll<storeItemSO>("so/storeSO");
for (int i = 0; i < rewardStoreItems.Length; i++)
{
var itemSO = rewardStoreItems[i];
if (itemSO == null || itemSO.itemIcon == null)
{
continue;
}
rewardSpriteByItemId[itemSO.itemID] = itemSO.itemIcon;
}
}
private Sprite GetRewardSpriteByItemId(int itemId)
{
if (itemId <= 0)
{
return null;
}
Sprite sprite;
return rewardSpriteByItemId.TryGetValue(itemId, out sprite) ? sprite : null;
}
private static int GetExpBottleRewardItemId(ExpBottleKind kind)
{
switch (kind)
{
case ExpBottleKind.Common: return 78001;
case ExpBottleKind.Medium: return 78002;
case ExpBottleKind.Superior: return 78003;
case ExpBottleKind.Supreme: return 78004;
case ExpBottleKind.Extraordinary: return 78005;
case ExpBottleKind.Celestial: return 78006;
case ExpBottleKind.RainAll: return 78011;
case ExpBottleKind.AdvancedRainAll: return 78012;
case ExpBottleKind.SuperRainAll: return 78013;
default: return 0;
}
}
private static int GetDushMaterialRewardItemId(DushMaterialKind kind)
{
switch (kind)
{
case DushMaterialKind.Material78021: return 78021;
case DushMaterialKind.Material78022: return 78022;
case DushMaterialKind.Material78023: return 78023;
case DushMaterialKind.Material78024: return 78024;
default: return 0;
}
}
}
@@ -14,13 +14,16 @@ MonoBehaviour:
m_EditorClassIdentifier:
tasks:
- taskID: 10101
description: "\u767B\u5F55\u6E38\u620F"
description: "\u767B\u5F55\u6E38\u620Fv50"
taskType: 0
refreshFrequency: 1
targetValue: 1
rewardType: 0
rewardAmount: 0
rewardAmount: 50
rewardExpBottleKind: 0
rewardDushMaterialKind: 0
addToTaskPool: 1
selectionWeight: 1
- taskID: 10102
description: "\u5B8C\u62103\u9996\u6B4C\u66F2"
taskType: 1
@@ -28,28 +31,62 @@ MonoBehaviour:
targetValue: 3
rewardType: 0
rewardAmount: 0
rewardExpBottleKind: 0
rewardDushMaterialKind: 0
addToTaskPool: 1
selectionWeight: 1
- taskID: 10103
description: "\u5B8C\u6210\u4E00\u6B2199\u8FDE\u51FB"
description: "\u8FBE\u62101\u6B21Full Combo"
taskType: 2
refreshFrequency: 1
targetValue: 99
targetValue: 1
rewardType: 0
rewardAmount: 0
rewardExpBottleKind: 0
rewardDushMaterialKind: 0
addToTaskPool: 1
selectionWeight: 1
- taskID: 10104
description: "\u8FBE\u5230\u4E00\u6B21\u603B\u52061000000"
description: "\u7D2F\u8BA1\u83B7\u5F971000000\u5206"
taskType: 3
refreshFrequency: 1
targetValue: 1000000
rewardType: 0
rewardAmount: 0
rewardType: 1
rewardAmount: 4
rewardExpBottleKind: 0
rewardDushMaterialKind: 0
addToTaskPool: 1
selectionWeight: 1
- taskID: 10105
description: "\u4F7F\u7528\u4E00\u6B21\u9644\u9B54\u4E4B\u74F6"
description: "\u4F7F\u7528\u4EFB\u610F\u7C7B\u578B\u7ECF\u9A8C\u74F6\u4E00\u6B21"
taskType: 5
refreshFrequency: 1
targetValue: 1
rewardType: 0
rewardAmount: 0
addToTaskPool: 0
rewardExpBottleKind: 0
rewardDushMaterialKind: 0
addToTaskPool: 1
selectionWeight: 1
- taskID: 10106
description: "\u82B1\u8D391000\u91D1\u5E01"
taskType: 7
refreshFrequency: 1
targetValue: 1000
rewardType: 0
rewardAmount: 0
rewardExpBottleKind: 0
rewardDushMaterialKind: 0
addToTaskPool: 1
selectionWeight: 1
- taskID: 10107
description: "\u6E38\u620F\u65F6\u957F\u8FBE\u523030\u5206\u949F"
taskType: 8
refreshFrequency: 1
targetValue: 1800
rewardType: 0
rewardAmount: 0
rewardExpBottleKind: 0
rewardDushMaterialKind: 0
addToTaskPool: 1
selectionWeight: 1
@@ -1,5 +1,5 @@
using System.Collections.Generic;
using UnityEngine;
using System.Collections.Generic;
[CreateAssetMenu(fileName = "NewUserTasksPool", menuName = "DailyTask/UserTasksPool")]
public class userTasksPool : ScriptableObject
@@ -10,7 +10,6 @@ public class userTasksPool : ScriptableObject
[System.Serializable]
public class TaskDefinition
{
[Tooltip("Unique identifier for the task")]
public string taskID;
@@ -22,20 +21,28 @@ public class userTasksPool : ScriptableObject
public TaskType taskType;
[Tooltip("How often the task refreshes")]
public RefreshFrequency refreshFrequency;
public RefreshFrequency refreshFrequency = RefreshFrequency.DailyReset;
[Tooltip("Target value to complete the task (e.g., 100 combo, 3 songs)")]
public float targetValue;
[Tooltip("Target value to complete the task")]
public float targetValue = 1f;
[Tooltip("Type of reward given upon completion")]
public RewardType rewardType;
[Tooltip("Quantity of the reward")]
public int rewardAmount;
public int rewardAmount = 1;
[Tooltip("是否加入任务池")]
[Tooltip("Used when rewardType is expBottle")]
public ExpBottleKind rewardExpBottleKind = ExpBottleKind.Common;
[Tooltip("Used when rewardType is dushMaterial")]
public DushMaterialKind rewardDushMaterialKind = DushMaterialKind.Material78021;
[Tooltip("Whether this task can be included in the random daily task pool")]
public bool addToTaskPool = true;
[Tooltip("Weighted random selection weight")]
public int selectionWeight = 1;
}
public enum TaskType
@@ -46,21 +53,22 @@ public class userTasksPool : ScriptableObject
TotalScore,
WatchStory,
UseItem,
ShareGame
ShareGame,
SpendCoins,
GameDuration
}
public enum RewardType
{
player_money,
player_material,
bottleOfEXP,
playerEXP
coins,
expBottle,
dushMaterial
}
public enum RefreshFrequency
{
OnLogin,
DailyReset,
Never
OnLogin,
DailyReset,
Never
}
}
+117 -27
View File
@@ -245,8 +245,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 14.409, y: -0.56745}
m_SizeDelta: {x: 29.618, y: 15.665}
m_AnchoredPosition: {x: 12.13, y: 0}
m_SizeDelta: {x: 34.1858, y: 20}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &602272132256342015
CanvasRenderer:
@@ -278,12 +278,12 @@ MonoBehaviour:
m_Calls: []
m_FontData:
m_Font: {fileID: 12800000, guid: dfa10de3e2846524089b5073175cd151, type: 3}
m_FontSize: 14
m_FontSize: 18
m_FontStyle: 0
m_BestFit: 0
m_MinSize: 10
m_MinSize: 1
m_MaxSize: 40
m_Alignment: 0
m_Alignment: 3
m_AlignByGeometry: 0
m_RichText: 1
m_HorizontalOverflow: 0
@@ -325,7 +325,7 @@ RectTransform:
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -19.4, y: 0.6}
m_SizeDelta: {x: 14, y: 18}
m_SizeDelta: {x: 18, y: 18}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &5147910286390164955
CanvasRenderer:
@@ -355,7 +355,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 4453427214848945594, guid: be41ed26869bfcf46b4c6b3acf9ec09e, type: 3}
m_Sprite: {fileID: 21300000, guid: 30f736af053d2094e9406aa272c0725b, type: 3}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
@@ -402,8 +402,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 171.90002, y: 1.1999512}
m_SizeDelta: {x: 110, y: 47}
m_AnchoredPosition: {x: 171.90002, y: -0.3146}
m_SizeDelta: {x: 110, y: 50.0289}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &5599503168244631389
CanvasRenderer:
@@ -468,7 +468,7 @@ MonoBehaviour:
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_DisabledColor: {r: 0.6509434, g: 0.6509434, b: 0.6509434, a: 1}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
@@ -498,6 +498,7 @@ GameObject:
- component: {fileID: 1888209457113168138}
- component: {fileID: 4060125231613820467}
- component: {fileID: 5034613503719026902}
- component: {fileID: 3223954077874542836}
m_Layer: 5
m_Name: progress
m_TagString: Untagged
@@ -516,13 +517,14 @@ RectTransform:
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Children:
- {fileID: 8144451642442379881}
m_Father: {fileID: 5109224055821985532}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 348, y: 55}
m_AnchoredPosition: {x: 6.5, y: 0}
m_SizeDelta: {x: 317, y: 30}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &4060125231613820467
CanvasRenderer:
@@ -545,23 +547,111 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 0.74294424, g: 1, b: 0.6556604, a: 1}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300000, guid: 4dcbef834d5273c41aee29d861169fd9, type: 3}
m_Sprite: {fileID: 21300000, guid: ad4148593b05d0f47980774815c325fe, type: 3}
m_Type: 3
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 0
m_FillAmount: 0
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!114 &3223954077874542836
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4811890131364998009}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 31a19414c41e5ae4aae2af33fee712f6, type: 3}
m_Name:
m_EditorClassIdentifier:
m_ShowMaskGraphic: 1
--- !u!1 &4969152878334586126
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 8144451642442379881}
- component: {fileID: 8489452282027704886}
- component: {fileID: 5843470283112251827}
m_Layer: 5
m_Name: Image
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &8144451642442379881
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4969152878334586126}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1888209457113168138}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 13.8749, y: 0.000059128}
m_SizeDelta: {x: 371.4091, y: 30}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &8489452282027704886
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4969152878334586126}
m_CullTransparentMesh: 1
--- !u!114 &5843470283112251827
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4969152878334586126}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300000, guid: f47d8a39555c75d4aa3ca75114e70bc4, type: 3}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1.83
--- !u!1 &6006229956337094046
GameObject:
m_ObjectHideFlags: 0
@@ -746,8 +836,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -4.121, y: 1.7968}
m_SizeDelta: {x: 255.338, y: 28.4246}
m_AnchoredPosition: {x: -9.9906, y: 0}
m_SizeDelta: {x: 267.0772, y: 30}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &3518498349447220834
CanvasRenderer:
@@ -779,7 +869,7 @@ MonoBehaviour:
m_Calls: []
m_FontData:
m_Font: {fileID: 12800000, guid: bc54bd51ea8b84448ba1b65311872862, type: 3}
m_FontSize: 14
m_FontSize: 15
m_FontStyle: 0
m_BestFit: 0
m_MinSize: 1
@@ -863,7 +953,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0.375, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -74.6, y: 17}
m_AnchoredPosition: {x: -76.8, y: 12.584}
m_SizeDelta: {x: 200, y: 50}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &2318309606455167046
@@ -921,8 +1011,8 @@ MonoBehaviour:
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 28
m_fontSizeBase: 28
m_fontSize: 20
m_fontSizeBase: 20
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
@@ -999,8 +1089,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 348, y: 55}
m_AnchoredPosition: {x: 6.5, y: 0}
m_SizeDelta: {x: 317, y: 30}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &8024881274196398679
CanvasRenderer:
@@ -1030,8 +1120,8 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300000, guid: 4dcbef834d5273c41aee29d861169fd9, type: 3}
m_Type: 0
m_Sprite: {fileID: 0}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
@@ -1039,4 +1129,4 @@ MonoBehaviour:
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
m_PixelsPerUnitMultiplier: 0.7
@@ -1,6 +1,7 @@
using UnityEngine;
using TMPro;
using UnityEngine.UI;
using UnityEngine.Events;
public class taskPrefabController : MonoBehaviour
{
@@ -13,4 +14,53 @@ public class taskPrefabController : MonoBehaviour
public Button rewardButton;
public Image rewardImg;
public Text rewardAmmount;
public void Setup(DailyTaskViewData taskView, int displayIndex, Sprite rewardSprite, UnityAction onRewardClicked)
{
if (taskView == null || taskView.definition == null || taskView.runtimeEntry == null)
{
return;
}
if (taskNumber != null)
{
taskNumber.text = displayIndex.ToString("00");
}
if (taskText != null)
{
taskText.text = taskView.definition.description;
}
if (taskProgress_fillAmount_img != null)
{
float targetValue = Mathf.Max(0.0001f, taskView.definition.targetValue);
float clampedProgress = Mathf.Clamp(taskView.runtimeEntry.progress, 0f, targetValue);
taskProgress_fillAmount_img.fillAmount = clampedProgress / targetValue;
}
if (rewardImg != null)
{
rewardImg.sprite = rewardSprite;
rewardImg.enabled = rewardSprite != null;
}
if (rewardAmmount != null)
{
rewardAmmount.text = taskView.definition.rewardAmount.ToString();
}
if (rewardButton == null)
{
return;
}
rewardButton.onClick.RemoveAllListeners();
if (onRewardClicked != null)
{
rewardButton.onClick.AddListener(onRewardClicked);
}
rewardButton.interactable = taskView.runtimeEntry.isCompleted && !taskView.runtimeEntry.isClaimed;
}
}
@@ -1636,6 +1636,16 @@ public class GameManager : MonoBehaviour
/// Records the elapsed play time since the session started and adds it to the song's total play time.
/// Can be called multiple times, but only records once per session.
/// </summary>
public float GetPendingSessionDurationSeconds()
{
if (timeRecorded || currentSong == null || GameConfig.autoPlayEnabled)
{
return 0f;
}
return Mathf.Max(0f, Time.realtimeSinceStartup - sessionStartTime);
}
public void RecordTotalPlayTime()
{
if (timeRecorded || currentSong == null || GameConfig.autoPlayEnabled) return;
@@ -265,8 +265,9 @@ public class settlementController : MonoBehaviour
getThisSong_info();
// --- Statistics: Record total play time at settlement ---
if (gm != null) gm.RecordTotalPlayTime();
else { var activeGM = FindAnyObjectByType<GameManager>(); if (activeGM != null) activeGM.RecordTotalPlayTime(); }
GameManager runtimeGameManager = gm != null ? gm : FindAnyObjectByType<GameManager>();
float sessionDurationForTasks = runtimeGameManager != null ? runtimeGameManager.GetPendingSessionDurationSeconds() : 0f;
if (runtimeGameManager != null) runtimeGameManager.RecordTotalPlayTime();
// Documentation text normalized.
PrepareSettlementMusic();
@@ -314,6 +315,21 @@ public class settlementController : MonoBehaviour
targetGoodCount * good_weight +
targetMissCount * miss_weight) / noteCountSum * 100f);
if (!GameConfig.autoPlayEnabled)
{
DailyTaskEventHub.ReportPlaySong();
DailyTaskEventHub.ReportTotalScore(targetTotalScore);
if (targetMissCount <= 0 && noteCountRaw > 0)
{
DailyTaskEventHub.ReportFullCombo();
}
if (sessionDurationForTasks > 0f)
{
DailyTaskEventHub.ReportGameDuration(sessionDurationForTasks);
}
}
perfectHitCount_Text.text = targetPerfectCount.ToString();
greatHitCount_Text.text = targetGreatCount.ToString();
goodHitCount_Text.text = targetGoodCount.ToString();