任务系统一半多,商城筛选功能,一些细节和免费包
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user