This commit is contained in:
2026-04-30 19:33:16 +08:00
commit fb2252f688
556 changed files with 69721 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
//------------------------------------------------------------
// Game Framework
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
// Homepage: https://gameframework.cn/
// Feedback: mailto:ellan@gameframework.cn
//------------------------------------------------------------
namespace GeometryTD.Procedure
{
public abstract class ProcedureBase : GameFramework.Procedure.ProcedureBase
{
// 获取流程是否使用原生对话框
// 在一些特殊的流程(如游戏逻辑对话框资源更新完成前的流程)中,可以考虑调用原生对话框进行消息提示行为
public abstract bool UseNativeDialog
{
get;
}
}
}
@@ -0,0 +1,149 @@
using GameFramework.DataTable;
using GameFramework.Event;
using GeometryTD.CustomUtility;
using GeometryTD.DataTable;
using GeometryTD.Definition;
using UnityGameFramework.Runtime;
using ProcedureOwner = GameFramework.Fsm.IFsm<GameFramework.Procedure.IProcedureManager>;
namespace GeometryTD.Procedure
{
public class ProcedureChangeScene : ProcedureBase
{
private int _nextSceneId = 0;
private bool _isChangeSceneComplete = false;
private int _backgroundMusicId = 0;
public override bool UseNativeDialog => false;
protected override void OnEnter(ProcedureOwner procedureOwner)
{
base.OnEnter(procedureOwner);
_isChangeSceneComplete = false;
GameEntry.Event.Subscribe(LoadSceneSuccessEventArgs.EventId, OnLoadSceneSuccess);
GameEntry.Event.Subscribe(LoadSceneFailureEventArgs.EventId, OnLoadSceneFailure);
GameEntry.Event.Subscribe(LoadSceneUpdateEventArgs.EventId, OnLoadSceneUpdate);
GameEntry.Event.Subscribe(LoadSceneDependencyAssetEventArgs.EventId, OnLoadSceneDependencyAsset);
// 停止所有声音
GameEntry.Sound.StopAllLoadingSounds();
GameEntry.Sound.StopAllLoadedSounds();
// 隐藏所有实体
GameEntry.Entity.HideAllLoadingEntities();
GameEntry.Entity.HideAllLoadedEntities();
// 卸载所有场景
string[] loadedSceneAssetNames = GameEntry.Scene.GetLoadedSceneAssetNames();
for (int i = 0; i < loadedSceneAssetNames.Length; i++)
{
GameEntry.Scene.UnloadScene(loadedSceneAssetNames[i]);
}
// 还原游戏速度
GameEntry.Base.ResetNormalGameSpeed();
_nextSceneId = procedureOwner.GetData<VarInt32>("NextSceneId");
IDataTable<DRScene> dtScene = GameEntry.DataTable.GetDataTable<DRScene>();
DRScene drScene = dtScene.GetDataRow(_nextSceneId);
if (drScene == null)
{
Log.Warning("Can not load scene '{0}' from data table.", _nextSceneId.ToString());
return;
}
GameEntry.Scene.LoadScene(AssetUtility.GetSceneAsset(drScene.AssetName), Constant.AssetPriority.SceneAsset,
this);
_backgroundMusicId = drScene.BackgroundMusicId;
}
protected override void OnLeave(ProcedureOwner procedureOwner, bool isShutdown)
{
GameEntry.Event.Unsubscribe(LoadSceneSuccessEventArgs.EventId, OnLoadSceneSuccess);
GameEntry.Event.Unsubscribe(LoadSceneFailureEventArgs.EventId, OnLoadSceneFailure);
GameEntry.Event.Unsubscribe(LoadSceneUpdateEventArgs.EventId, OnLoadSceneUpdate);
GameEntry.Event.Unsubscribe(LoadSceneDependencyAssetEventArgs.EventId, OnLoadSceneDependencyAsset);
base.OnLeave(procedureOwner, isShutdown);
}
protected override void OnUpdate(ProcedureOwner procedureOwner, float elapseSeconds, float realElapseSeconds)
{
base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds);
if (!_isChangeSceneComplete)
{
return;
}
SceneType sceneType = (SceneType)_nextSceneId;
switch (sceneType)
{
case SceneType.Menu:
ChangeState<ProcedureMenu>(procedureOwner);
break;
case SceneType.Main:
ChangeState<ProcedureMain>(procedureOwner);
break;
default:
Log.Debug($"Scene {sceneType.ToString()} don't configure a procedure");
break;
}
}
private void OnLoadSceneSuccess(object sender, GameEventArgs e)
{
LoadSceneSuccessEventArgs ne = (LoadSceneSuccessEventArgs)e;
if (ne.UserData != this)
{
return;
}
Log.Info("Load scene '{0}' OK.", ne.SceneAssetName);
if (_backgroundMusicId > 0)
{
GameEntry.Sound.PlayMusic(_backgroundMusicId);
}
_isChangeSceneComplete = true;
}
private void OnLoadSceneFailure(object sender, GameEventArgs e)
{
LoadSceneFailureEventArgs ne = (LoadSceneFailureEventArgs)e;
if (ne.UserData != this)
{
return;
}
Log.Error("Load scene '{0}' failure, error message '{1}'.", ne.SceneAssetName, ne.ErrorMessage);
}
private void OnLoadSceneUpdate(object sender, GameEventArgs e)
{
LoadSceneUpdateEventArgs ne = (LoadSceneUpdateEventArgs)e;
if (ne.UserData != this)
{
return;
}
Log.Info("Load scene '{0}' update, progress '{1}'.", ne.SceneAssetName, ne.Progress.ToString("P2"));
}
private void OnLoadSceneDependencyAsset(object sender, GameEventArgs e)
{
LoadSceneDependencyAssetEventArgs ne = (LoadSceneDependencyAssetEventArgs)e;
if (ne.UserData != this)
{
return;
}
Log.Info("Load scene '{0}' dependency asset '{1}', count '{2}/{3}'.", ne.SceneAssetName,
ne.DependencyAssetName, ne.LoadedCount.ToString(), ne.TotalCount.ToString());
}
}
}
@@ -0,0 +1,57 @@
using UnityGameFramework.Runtime;
using ProcedureOwner = GameFramework.Fsm.IFsm<GameFramework.Procedure.IProcedureManager>;
namespace GeometryTD.Procedure
{
public class ProcedureCheckResources : ProcedureBase
{
private bool m_CheckResourcesComplete = false;
private bool m_NeedUpdateResources = false;
private int m_UpdateResourceCount = 0;
private long m_UpdateResourceTotalCompressedLength = 0L;
public override bool UseNativeDialog => true;
protected override void OnEnter(ProcedureOwner procedureOwner)
{
base.OnEnter(procedureOwner);
m_CheckResourcesComplete = false;
m_NeedUpdateResources = false;
m_UpdateResourceCount = 0;
m_UpdateResourceTotalCompressedLength = 0L;
GameEntry.Resource.CheckResources(OnCheckResourcesComplete);
}
protected override void OnUpdate(ProcedureOwner procedureOwner, float elapseSeconds, float realElapseSeconds)
{
base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds);
if (!m_CheckResourcesComplete)
{
return;
}
if (m_NeedUpdateResources)
{
procedureOwner.SetData<VarInt32>("UpdateResourceCount", m_UpdateResourceCount);
procedureOwner.SetData<VarInt64>("UpdateResourceTotalCompressedLength", m_UpdateResourceTotalCompressedLength);
ChangeState<ProcedureUpdateResources>(procedureOwner);
}
else
{
ChangeState<ProcedurePreload>(procedureOwner);
}
}
private void OnCheckResourcesComplete(int movedCount, int removedCount, int updateCount, long updateTotalLength, long updateTotalCompressedLength)
{
m_CheckResourcesComplete = true;
m_NeedUpdateResources = updateCount > 0;
m_UpdateResourceCount = updateCount;
m_UpdateResourceTotalCompressedLength = updateTotalCompressedLength;
Log.Info("Check resources complete, '{0}' resources need to update, compressed length is '{1}', uncompressed length is '{2}'.", updateCount.ToString(), updateTotalCompressedLength.ToString(), updateTotalLength.ToString());
}
}
}
@@ -0,0 +1,162 @@
using GameFramework;
using GameFramework.Event;
using GameFramework.Resource;
using GeometryTD.Definition;
using GeometryTD.UI;
using UnityEngine;
using UnityGameFramework.Runtime;
using ProcedureOwner = GameFramework.Fsm.IFsm<GameFramework.Procedure.IProcedureManager>;
namespace GeometryTD.Procedure
{
public class ProcedureCheckVersion : ProcedureBase
{
private bool m_CheckVersionComplete = false;
private bool m_NeedUpdateVersion = false;
private VersionInfo m_VersionInfo = null;
public override bool UseNativeDialog => true;
protected override void OnEnter(ProcedureOwner procedureOwner)
{
base.OnEnter(procedureOwner);
m_CheckVersionComplete = false;
m_NeedUpdateVersion = false;
m_VersionInfo = null;
GameEntry.Event.Subscribe(WebRequestSuccessEventArgs.EventId, OnWebRequestSuccess);
GameEntry.Event.Subscribe(WebRequestFailureEventArgs.EventId, OnWebRequestFailure);
// 向服务器请求版本信息
GameEntry.WebRequest.AddWebRequest(Utility.Text.Format(GameEntry.BuiltinData.BuildInfo.CheckVersionUrl, GetPlatformPath()), this);
}
protected override void OnLeave(ProcedureOwner procedureOwner, bool isShutdown)
{
GameEntry.Event.Unsubscribe(WebRequestSuccessEventArgs.EventId, OnWebRequestSuccess);
GameEntry.Event.Unsubscribe(WebRequestFailureEventArgs.EventId, OnWebRequestFailure);
base.OnLeave(procedureOwner, isShutdown);
}
protected override void OnUpdate(ProcedureOwner procedureOwner, float elapseSeconds, float realElapseSeconds)
{
base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds);
if (!m_CheckVersionComplete)
{
return;
}
if (m_NeedUpdateVersion)
{
procedureOwner.SetData<VarInt32>("VersionListLength", m_VersionInfo.VersionListLength);
procedureOwner.SetData<VarInt32>("VersionListHashCode", m_VersionInfo.VersionListHashCode);
procedureOwner.SetData<VarInt32>("VersionListCompressedLength", m_VersionInfo.VersionListCompressedLength);
procedureOwner.SetData<VarInt32>("VersionListCompressedHashCode", m_VersionInfo.VersionListCompressedHashCode);
ChangeState<ProcedureUpdateVersion>(procedureOwner);
}
else
{
ChangeState<ProcedureVerifyResources>(procedureOwner);
}
}
private void GotoUpdateApp(object userData)
{
string url = null;
#if UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN
url = GameEntry.BuiltinData.BuildInfo.WindowsAppUrl;
#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX
url = GameEntry.BuiltinData.BuildInfo.MacOSAppUrl;
#elif UNITY_IOS
url = GameEntry.BuiltinData.BuildInfo.IOSAppUrl;
#elif UNITY_ANDROID
url = GameEntry.BuiltinData.BuildInfo.AndroidAppUrl;
#endif
if (!string.IsNullOrEmpty(url))
{
Application.OpenURL(url);
}
}
private void OnWebRequestSuccess(object sender, GameEventArgs e)
{
WebRequestSuccessEventArgs ne = (WebRequestSuccessEventArgs)e;
if (ne.UserData != this)
{
return;
}
// 解析版本信息
byte[] versionInfoBytes = ne.GetWebResponseBytes();
string versionInfoString = Utility.Converter.GetString(versionInfoBytes);
m_VersionInfo = Utility.Json.ToObject<VersionInfo>(versionInfoString);
if (m_VersionInfo == null)
{
Log.Error("Parse VersionInfo failure.");
return;
}
Log.Info("Latest game version is '{0} ({1})', local game version is '{2} ({3})'.", m_VersionInfo.LatestGameVersion, m_VersionInfo.InternalGameVersion.ToString(), Version.GameVersion, Version.InternalGameVersion.ToString());
if (m_VersionInfo.ForceUpdateGame)
{
// 需要强制更新游戏应用
GameEntry.UI.OpenDialog(new DialogParams
{
Mode = 2,
Title = GameEntry.Localization.GetString("ForceUpdate.Title"),
Message = GameEntry.Localization.GetString("ForceUpdate.Message"),
ConfirmText = GameEntry.Localization.GetString("ForceUpdate.UpdateButton"),
OnClickConfirm = GotoUpdateApp,
CancelText = GameEntry.Localization.GetString("ForceUpdate.QuitButton"),
OnClickCancel = delegate (object userData) { UnityGameFramework.Runtime.GameEntry.Shutdown(ShutdownType.Quit); },
});
return;
}
// 设置资源更新下载地址
GameEntry.Resource.UpdatePrefixUri = Utility.Path.GetRegularPath(m_VersionInfo.UpdatePrefixUri);
m_CheckVersionComplete = true;
m_NeedUpdateVersion = GameEntry.Resource.CheckVersionList(m_VersionInfo.InternalResourceVersion) == CheckVersionListResult.NeedUpdate;
}
private void OnWebRequestFailure(object sender, GameEventArgs e)
{
WebRequestFailureEventArgs ne = (WebRequestFailureEventArgs)e;
if (ne.UserData != this)
{
return;
}
Log.Warning("Check version failure, error message is '{0}'.", ne.ErrorMessage);
}
private string GetPlatformPath()
{
switch (Application.platform)
{
case RuntimePlatform.WindowsEditor:
case RuntimePlatform.WindowsPlayer:
return "Windows";
case RuntimePlatform.OSXEditor:
case RuntimePlatform.OSXPlayer:
return "MacOS";
case RuntimePlatform.IPhonePlayer:
return "IOS";
case RuntimePlatform.Android:
return "Android";
default:
throw new System.NotSupportedException(Utility.Text.Format("Platform '{0}' is not supported.", Application.platform));
}
}
}
}
@@ -0,0 +1,48 @@
//------------------------------------------------------------
// Game Framework
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
// Homepage: https://gameframework.cn/
// Feedback: mailto:ellan@gameframework.cn
//------------------------------------------------------------
using UnityGameFramework.Runtime;
using ProcedureOwner = GameFramework.Fsm.IFsm<GameFramework.Procedure.IProcedureManager>;
namespace GeometryTD.Procedure
{
public class ProcedureInitResources : ProcedureBase
{
private bool m_InitResourcesComplete = false;
public override bool UseNativeDialog => true;
protected override void OnEnter(ProcedureOwner procedureOwner)
{
base.OnEnter(procedureOwner);
m_InitResourcesComplete = false;
// 注意:使用单机模式并初始化资源前,需要先构建 AssetBundle 并复制到 StreamingAssets 中,否则会产生 HTTP 404 错误
GameEntry.Resource.InitResources(OnInitResourcesComplete);
}
protected override void OnUpdate(ProcedureOwner procedureOwner, float elapseSeconds, float realElapseSeconds)
{
base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds);
if (!m_InitResourcesComplete)
{
// 初始化资源未完成则继续等待
return;
}
ChangeState<ProcedurePreload>(procedureOwner);
}
private void OnInitResourcesComplete()
{
m_InitResourcesComplete = true;
Log.Info("Init resources complete.");
}
}
}
+129
View File
@@ -0,0 +1,129 @@
//------------------------------------------------------------
// Game Framework
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
// Homepage: https://gameframework.cn/
// Feedback: mailto:ellan@gameframework.cn
//------------------------------------------------------------
using GameFramework.Localization;
using System;
using GeometryTD;
using UnityGameFramework.Runtime;
using ProcedureOwner = GameFramework.Fsm.IFsm<GameFramework.Procedure.IProcedureManager>;
namespace GeometryTD.Procedure
{
public class ProcedureLaunch : ProcedureBase
{
public override bool UseNativeDialog => true;
protected override void OnEnter(ProcedureOwner procedureOwner)
{
base.OnEnter(procedureOwner);
// 构建信息:发布版本时,把一些数据以 Json 的格式写入 Assets/GameMain/Configs/BuildInfo.txt,供游戏逻辑读取
GameEntry.BuiltinData.InitBuildInfo();
// 语言配置:设置当前使用的语言,如果不设置,则默认使用操作系统语言
InitLanguageSettings();
// 变体配置:根据使用的语言,通知底层加载对应的资源变体
InitCurrentVariant();
// 声音配置:根据用户配置数据,设置即将使用的声音选项
InitSoundSettings();
}
protected override void OnUpdate(ProcedureOwner procedureOwner, float elapseSeconds, float realElapseSeconds)
{
base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds);
// 运行一帧即切换到 Splash 展示流程
ChangeState<ProcedureSplash>(procedureOwner);
}
private void InitLanguageSettings()
{
if (GameEntry.Base.EditorResourceMode && GameEntry.Base.EditorLanguage != Language.Unspecified)
{
// 编辑器资源模式直接使用 Inspector 上设置的语言
return;
}
Language language = GameEntry.Localization.Language;
if (GameEntry.Setting.HasSetting(Constant.Setting.Language))
{
try
{
string languageString = GameEntry.Setting.GetString(Constant.Setting.Language);
language = (Language)Enum.Parse(typeof(Language), languageString);
}
catch
{
}
}
if (language != Language.English
&& language != Language.ChineseSimplified
&& language != Language.ChineseTraditional
&& language != Language.Korean)
{
// 若是暂不支持的语言,则使用英语
language = Language.English;
GameEntry.Setting.SetString(Constant.Setting.Language, language.ToString());
GameEntry.Setting.Save();
}
GameEntry.Localization.Language = language;
Log.Info("Init language settings complete, current language is '{0}'.", language.ToString());
}
private void InitCurrentVariant()
{
if (GameEntry.Base.EditorResourceMode)
{
// 编辑器资源模式不使用 AssetBundle,也就没有变体了
return;
}
string currentVariant = null;
switch (GameEntry.Localization.Language)
{
case Language.English:
currentVariant = "en-us";
break;
case Language.ChineseSimplified:
currentVariant = "zh-cn";
break;
case Language.ChineseTraditional:
currentVariant = "zh-tw";
break;
case Language.Korean:
currentVariant = "ko-kr";
break;
default:
currentVariant = "zh-cn";
break;
}
GameEntry.Resource.SetCurrentVariant(currentVariant);
Log.Info("Init current variant complete.");
}
private void InitSoundSettings()
{
GameEntry.Sound.Mute("Music", GameEntry.Setting.GetBool(Constant.Setting.MusicMuted, false));
GameEntry.Sound.SetVolume("Music", GameEntry.Setting.GetFloat(Constant.Setting.MusicVolume, 0.3f));
GameEntry.Sound.Mute("Sound", GameEntry.Setting.GetBool(Constant.Setting.SoundMuted, false));
GameEntry.Sound.SetVolume("Sound", GameEntry.Setting.GetFloat(Constant.Setting.SoundVolume, 1f));
GameEntry.Sound.Mute("UISound", GameEntry.Setting.GetBool(Constant.Setting.UISoundMuted, false));
GameEntry.Sound.SetVolume("UISound", GameEntry.Setting.GetFloat(Constant.Setting.UISoundVolume, 1f));
Log.Info("Init sound settings complete.");
}
}
}
+237
View File
@@ -0,0 +1,237 @@
using GameFramework;
using GameFramework.Event;
using GameFramework.Resource;
using System.Collections.Generic;
using GeometryTD.CustomUtility;
using GeometryTD.DataTable;
using GeometryTD.Definition;
using TMPro;
using GeometryTD.UI;
using UnityEngine;
using UnityGameFramework.Runtime;
using ProcedureOwner = GameFramework.Fsm.IFsm<GameFramework.Procedure.IProcedureManager>;
namespace GeometryTD.Procedure
{
public class ProcedurePreload : ProcedureBase
{
public static readonly string[] DataTableNames = new string[]
{
"BaseComp",
"BearingComp",
"Enemy",
"Entity",
"Event",
"Level",
"LevelPhase",
"LevelSpawnEntry",
"Music",
"MuzzleComp",
"Scene",
"ShopPrice",
"Sound",
"Tag",
"RarityTagBudget",
"TagConfig",
"OutGameDropPool",
"UIForm",
"UISound",
};
private readonly Dictionary<string, bool> _loadedFlag = new();
public override bool UseNativeDialog => true;
protected override void OnEnter(ProcedureOwner procedureOwner)
{
base.OnEnter(procedureOwner);
GameEntry.Event.Subscribe(LoadConfigSuccessEventArgs.EventId, OnLoadConfigSuccess);
GameEntry.Event.Subscribe(LoadConfigFailureEventArgs.EventId, OnLoadConfigFailure);
GameEntry.Event.Subscribe(LoadDataTableSuccessEventArgs.EventId, OnLoadDataTableSuccess);
GameEntry.Event.Subscribe(LoadDataTableFailureEventArgs.EventId, OnLoadDataTableFailure);
GameEntry.Event.Subscribe(LoadDictionarySuccessEventArgs.EventId, OnLoadDictionarySuccess);
GameEntry.Event.Subscribe(LoadDictionaryFailureEventArgs.EventId, OnLoadDictionaryFailure);
_loadedFlag.Clear();
PreloadResources();
}
protected override void OnLeave(ProcedureOwner procedureOwner, bool isShutdown)
{
GameEntry.Event.Unsubscribe(LoadConfigSuccessEventArgs.EventId, OnLoadConfigSuccess);
GameEntry.Event.Unsubscribe(LoadConfigFailureEventArgs.EventId, OnLoadConfigFailure);
GameEntry.Event.Unsubscribe(LoadDataTableSuccessEventArgs.EventId, OnLoadDataTableSuccess);
GameEntry.Event.Unsubscribe(LoadDataTableFailureEventArgs.EventId, OnLoadDataTableFailure);
GameEntry.Event.Unsubscribe(LoadDictionarySuccessEventArgs.EventId, OnLoadDictionarySuccess);
GameEntry.Event.Unsubscribe(LoadDictionaryFailureEventArgs.EventId, OnLoadDictionaryFailure);
base.OnLeave(procedureOwner, isShutdown);
}
protected override void OnUpdate(ProcedureOwner procedureOwner, float elapseSeconds, float realElapseSeconds)
{
base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds);
foreach (KeyValuePair<string, bool> loadedFlag in _loadedFlag)
{
if (!loadedFlag.Value)
{
return;
}
}
procedureOwner.SetData<VarInt32>("NextSceneId", (int)SceneType.Menu);
ChangeState<ProcedureChangeScene>(procedureOwner);
}
private void PreloadResources()
{
// Preload configs
LoadConfig("DefaultConfig");
// Preload data tables
foreach (string dataTableName in DataTableNames)
{
LoadDataTable(dataTableName);
}
// Preload dictionaries
//LoadDictionary("Default");
// Preload fonts
LoadFont("MainFont");
LoadTMPFont("MainTMPFont");
}
private void LoadConfig(string configName)
{
string configAssetName = AssetUtility.GetConfigAsset(configName, false);
_loadedFlag.Add(configAssetName, false);
GameEntry.Config.ReadData(configAssetName, this);
}
private void LoadDataTable(string dataTableName)
{
string dataTableAssetName = AssetUtility.GetDataTableAsset(dataTableName, false);
_loadedFlag.Add(dataTableAssetName, false);
GameEntry.DataTable.LoadDataTable(dataTableName, dataTableAssetName, this);
}
private void LoadDictionary(string dictionaryName)
{
string dictionaryAssetName = AssetUtility.GetDictionaryAsset(dictionaryName, false);
_loadedFlag.Add(dictionaryAssetName, false);
GameEntry.Localization.ReadData(dictionaryAssetName, this);
}
private void LoadFont(string fontName)
{
_loadedFlag.Add(Utility.Text.Format("Font.{0}", fontName), false);
GameEntry.Resource.LoadAsset(AssetUtility.GetFontAsset(fontName), Constant.AssetPriority.FontAsset,
new LoadAssetCallbacks(
(assetName, asset, duration, userData) =>
{
_loadedFlag[Utility.Text.Format("Font.{0}", fontName)] = true;
UGuiForm.SetMainFont((Font)asset);
Log.Info("Load font '{0}' OK.", fontName);
},
(assetName, status, errorMessage, userData) =>
{
Log.Error("Can not load font '{0}' from '{1}' with error message '{2}'.", fontName, assetName,
errorMessage);
}));
}
private void LoadTMPFont(string fontName)
{
_loadedFlag.Add(Utility.Text.Format("Font.{0}", fontName), false);
GameEntry.Resource.LoadAsset(AssetUtility.GetTMPFontAsset(fontName), Constant.AssetPriority.FontAsset,
new LoadAssetCallbacks(
(assetName, asset, duration, userData) =>
{
_loadedFlag[Utility.Text.Format("Font.{0}", fontName)] = true;
UGuiForm.SetMainTMPFont((TMP_FontAsset)asset);
Log.Info("Load font '{0}' OK.", fontName);
},
(assetName, status, errorMessage, userData) =>
{
Log.Error("Can not load font '{0}' from '{1}' with error message '{2}'.", fontName, assetName,
errorMessage);
}));
}
private void OnLoadConfigSuccess(object sender, GameEventArgs e)
{
LoadConfigSuccessEventArgs ne = (LoadConfigSuccessEventArgs)e;
if (ne.UserData != this)
{
return;
}
_loadedFlag[ne.ConfigAssetName] = true;
Log.Info("Load config '{0}' OK.", ne.ConfigAssetName);
}
private void OnLoadConfigFailure(object sender, GameEventArgs e)
{
LoadConfigFailureEventArgs ne = (LoadConfigFailureEventArgs)e;
if (ne.UserData != this)
{
return;
}
Log.Error("Can not load config '{0}' from '{1}' with error message '{2}'.", ne.ConfigAssetName,
ne.ConfigAssetName, ne.ErrorMessage);
}
private void OnLoadDataTableSuccess(object sender, GameEventArgs e)
{
LoadDataTableSuccessEventArgs ne = (LoadDataTableSuccessEventArgs)e;
if (ne.UserData != this)
{
return;
}
_loadedFlag[ne.DataTableAssetName] = true;
Log.Info("Load data table '{0}' OK.", ne.DataTableAssetName);
}
private void OnLoadDataTableFailure(object sender, GameEventArgs e)
{
LoadDataTableFailureEventArgs ne = (LoadDataTableFailureEventArgs)e;
if (ne.UserData != this)
{
return;
}
Log.Error("Can not load data table '{0}' from '{1}' with error message '{2}'.", ne.DataTableAssetName,
ne.DataTableAssetName, ne.ErrorMessage);
}
private void OnLoadDictionarySuccess(object sender, GameEventArgs e)
{
LoadDictionarySuccessEventArgs ne = (LoadDictionarySuccessEventArgs)e;
if (ne.UserData != this)
{
return;
}
_loadedFlag[ne.DictionaryAssetName] = true;
Log.Info("Load dictionary '{0}' OK.", ne.DictionaryAssetName);
}
private void OnLoadDictionaryFailure(object sender, GameEventArgs e)
{
LoadDictionaryFailureEventArgs ne = (LoadDictionaryFailureEventArgs)e;
if (ne.UserData != this)
{
return;
}
Log.Error("Can not load dictionary '{0}' from '{1}' with error message '{2}'.", ne.DictionaryAssetName,
ne.DictionaryAssetName, ne.ErrorMessage);
}
}
}
+45
View File
@@ -0,0 +1,45 @@
//------------------------------------------------------------
// Game Framework
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
// Homepage: https://gameframework.cn/
// Feedback: mailto:ellan@gameframework.cn
//------------------------------------------------------------
using GameFramework.Resource;
using UnityGameFramework.Runtime;
using ProcedureOwner = GameFramework.Fsm.IFsm<GameFramework.Procedure.IProcedureManager>;
namespace GeometryTD.Procedure
{
public class ProcedureSplash : ProcedureBase
{
public override bool UseNativeDialog => true;
protected override void OnUpdate(ProcedureOwner procedureOwner, float elapseSeconds, float realElapseSeconds)
{
base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds);
// TODO: 这里可以播放一个 Splash 动画
// ...
if (GameEntry.Base.EditorResourceMode)
{
// 编辑器模式
Log.Info("Editor resource mode detected.");
ChangeState<ProcedurePreload>(procedureOwner);
}
else if (GameEntry.Resource.ResourceMode == ResourceMode.Package)
{
// 单机模式
Log.Info("Package resource mode detected.");
ChangeState<ProcedureInitResources>(procedureOwner);
}
else
{
// 可更新模式
Log.Info("Updatable resource mode detected.");
ChangeState<ProcedureCheckVersion>(procedureOwner);
}
}
}
}
@@ -0,0 +1,270 @@
using GameFramework;
using GameFramework.Event;
using System.Collections.Generic;
using GeometryTD;
using GeometryTD.UI;
using UnityEngine;
using UnityGameFramework.Runtime;
using ProcedureOwner = GameFramework.Fsm.IFsm<GameFramework.Procedure.IProcedureManager>;
namespace GeometryTD.Procedure
{
public class ProcedureUpdateResources : ProcedureBase
{
private bool m_UpdateResourcesComplete = false;
private int m_UpdateCount = 0;
private long m_UpdateTotalCompressedLength = 0L;
private int m_UpdateSuccessCount = 0;
private List<UpdateLengthData> m_UpdateLengthData = new List<UpdateLengthData>();
private UpdateResourceForm m_UpdateResourceForm = null;
public override bool UseNativeDialog
{
get
{
return true;
}
}
protected override void OnEnter(ProcedureOwner procedureOwner)
{
base.OnEnter(procedureOwner);
m_UpdateResourcesComplete = false;
m_UpdateCount = procedureOwner.GetData<VarInt32>("UpdateResourceCount");
procedureOwner.RemoveData("UpdateResourceCount");
m_UpdateTotalCompressedLength = procedureOwner.GetData<VarInt64>("UpdateResourceTotalCompressedLength");
procedureOwner.RemoveData("UpdateResourceTotalCompressedLength");
m_UpdateSuccessCount = 0;
m_UpdateLengthData.Clear();
m_UpdateResourceForm = null;
GameEntry.Event.Subscribe(ResourceUpdateStartEventArgs.EventId, OnResourceUpdateStart);
GameEntry.Event.Subscribe(ResourceUpdateChangedEventArgs.EventId, OnResourceUpdateChanged);
GameEntry.Event.Subscribe(ResourceUpdateSuccessEventArgs.EventId, OnResourceUpdateSuccess);
GameEntry.Event.Subscribe(ResourceUpdateFailureEventArgs.EventId, OnResourceUpdateFailure);
if (Application.internetReachability == NetworkReachability.ReachableViaCarrierDataNetwork)
{
GameEntry.UI.OpenDialog(new DialogParams
{
Mode = 2,
Title = GameEntry.Localization.GetString("UpdateResourceViaCarrierDataNetwork.Title"),
Message = GameEntry.Localization.GetString("UpdateResourceViaCarrierDataNetwork.Message"),
ConfirmText = GameEntry.Localization.GetString("UpdateResourceViaCarrierDataNetwork.UpdateButton"),
OnClickConfirm = StartUpdateResources,
CancelText = GameEntry.Localization.GetString("UpdateResourceViaCarrierDataNetwork.QuitButton"),
OnClickCancel = delegate (object userData) { UnityGameFramework.Runtime.GameEntry.Shutdown(ShutdownType.Quit); },
});
return;
}
StartUpdateResources(null);
}
protected override void OnLeave(ProcedureOwner procedureOwner, bool isShutdown)
{
if (m_UpdateResourceForm != null)
{
Object.Destroy(m_UpdateResourceForm.gameObject);
m_UpdateResourceForm = null;
}
GameEntry.Event.Unsubscribe(ResourceUpdateStartEventArgs.EventId, OnResourceUpdateStart);
GameEntry.Event.Unsubscribe(ResourceUpdateChangedEventArgs.EventId, OnResourceUpdateChanged);
GameEntry.Event.Unsubscribe(ResourceUpdateSuccessEventArgs.EventId, OnResourceUpdateSuccess);
GameEntry.Event.Unsubscribe(ResourceUpdateFailureEventArgs.EventId, OnResourceUpdateFailure);
base.OnLeave(procedureOwner, isShutdown);
}
protected override void OnUpdate(ProcedureOwner procedureOwner, float elapseSeconds, float realElapseSeconds)
{
base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds);
if (!m_UpdateResourcesComplete)
{
return;
}
ChangeState<ProcedurePreload>(procedureOwner);
}
private void StartUpdateResources(object userData)
{
if (m_UpdateResourceForm == null)
{
m_UpdateResourceForm = Object.Instantiate(GameEntry.BuiltinData.UpdateResourceFormTemplate);
}
Log.Info("Start update resources...");
GameEntry.Resource.UpdateResources(OnUpdateResourcesComplete);
}
private void RefreshProgress()
{
long currentTotalUpdateLength = 0L;
for (int i = 0; i < m_UpdateLengthData.Count; i++)
{
currentTotalUpdateLength += m_UpdateLengthData[i].Length;
}
float progressTotal = (float)currentTotalUpdateLength / m_UpdateTotalCompressedLength;
string descriptionText = GameEntry.Localization.GetString("UpdateResource.Tips", m_UpdateSuccessCount.ToString(), m_UpdateCount.ToString(), GetByteLengthString(currentTotalUpdateLength), GetByteLengthString(m_UpdateTotalCompressedLength), progressTotal, GetByteLengthString((int)GameEntry.Download.CurrentSpeed));
m_UpdateResourceForm.SetProgress(progressTotal, descriptionText);
}
private string GetByteLengthString(long byteLength)
{
if (byteLength < 1024L) // 2 ^ 10
{
return Utility.Text.Format("{0} Bytes", byteLength);
}
if (byteLength < 1048576L) // 2 ^ 20
{
return Utility.Text.Format("{0:F2} KB", byteLength / 1024f);
}
if (byteLength < 1073741824L) // 2 ^ 30
{
return Utility.Text.Format("{0:F2} MB", byteLength / 1048576f);
}
if (byteLength < 1099511627776L) // 2 ^ 40
{
return Utility.Text.Format("{0:F2} GB", byteLength / 1073741824f);
}
if (byteLength < 1125899906842624L) // 2 ^ 50
{
return Utility.Text.Format("{0:F2} TB", byteLength / 1099511627776f);
}
if (byteLength < 1152921504606846976L) // 2 ^ 60
{
return Utility.Text.Format("{0:F2} PB", byteLength / 1125899906842624f);
}
return Utility.Text.Format("{0:F2} EB", byteLength / 1152921504606846976f);
}
private void OnUpdateResourcesComplete(GameFramework.Resource.IResourceGroup resourceGroup, bool result)
{
if (result)
{
m_UpdateResourcesComplete = true;
Log.Info("Update resources complete with no errors.");
}
else
{
Log.Error("Update resources complete with errors.");
}
}
private void OnResourceUpdateStart(object sender, GameEventArgs e)
{
ResourceUpdateStartEventArgs ne = (ResourceUpdateStartEventArgs)e;
for (int i = 0; i < m_UpdateLengthData.Count; i++)
{
if (m_UpdateLengthData[i].Name == ne.Name)
{
Log.Warning("Update resource '{0}' is invalid.", ne.Name);
m_UpdateLengthData[i].Length = 0;
RefreshProgress();
return;
}
}
m_UpdateLengthData.Add(new UpdateLengthData(ne.Name));
}
private void OnResourceUpdateChanged(object sender, GameEventArgs e)
{
ResourceUpdateChangedEventArgs ne = (ResourceUpdateChangedEventArgs)e;
for (int i = 0; i < m_UpdateLengthData.Count; i++)
{
if (m_UpdateLengthData[i].Name == ne.Name)
{
m_UpdateLengthData[i].Length = ne.CurrentLength;
RefreshProgress();
return;
}
}
Log.Warning("Update resource '{0}' is invalid.", ne.Name);
}
private void OnResourceUpdateSuccess(object sender, GameEventArgs e)
{
ResourceUpdateSuccessEventArgs ne = (ResourceUpdateSuccessEventArgs)e;
Log.Info("Update resource '{0}' success.", ne.Name);
for (int i = 0; i < m_UpdateLengthData.Count; i++)
{
if (m_UpdateLengthData[i].Name == ne.Name)
{
m_UpdateLengthData[i].Length = ne.CompressedLength;
m_UpdateSuccessCount++;
RefreshProgress();
return;
}
}
Log.Warning("Update resource '{0}' is invalid.", ne.Name);
}
private void OnResourceUpdateFailure(object sender, GameEventArgs e)
{
ResourceUpdateFailureEventArgs ne = (ResourceUpdateFailureEventArgs)e;
if (ne.RetryCount >= ne.TotalRetryCount)
{
Log.Error("Update resource '{0}' failure from '{1}' with error message '{2}', retry count '{3}'.", ne.Name, ne.DownloadUri, ne.ErrorMessage, ne.RetryCount.ToString());
return;
}
else
{
Log.Info("Update resource '{0}' failure from '{1}' with error message '{2}', retry count '{3}'.", ne.Name, ne.DownloadUri, ne.ErrorMessage, ne.RetryCount.ToString());
}
for (int i = 0; i < m_UpdateLengthData.Count; i++)
{
if (m_UpdateLengthData[i].Name == ne.Name)
{
m_UpdateLengthData.Remove(m_UpdateLengthData[i]);
RefreshProgress();
return;
}
}
Log.Warning("Update resource '{0}' is invalid.", ne.Name);
}
private class UpdateLengthData
{
private readonly string m_Name;
public UpdateLengthData(string name)
{
m_Name = name;
}
public string Name
{
get
{
return m_Name;
}
}
public int Length
{
get;
set;
}
}
}
}
@@ -0,0 +1,70 @@
//------------------------------------------------------------
// Game Framework
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
// Homepage: https://gameframework.cn/
// Feedback: mailto:ellan@gameframework.cn
//------------------------------------------------------------
using GameFramework.Resource;
using UnityGameFramework.Runtime;
using ProcedureOwner = GameFramework.Fsm.IFsm<GameFramework.Procedure.IProcedureManager>;
namespace GeometryTD.Procedure
{
public class ProcedureUpdateVersion : ProcedureBase
{
private bool m_UpdateVersionComplete = false;
private UpdateVersionListCallbacks m_UpdateVersionListCallbacks = null;
public override bool UseNativeDialog
{
get
{
return true;
}
}
protected override void OnInit(ProcedureOwner procedureOwner)
{
base.OnInit(procedureOwner);
m_UpdateVersionListCallbacks = new UpdateVersionListCallbacks(OnUpdateVersionListSuccess, OnUpdateVersionListFailure);
}
protected override void OnEnter(ProcedureOwner procedureOwner)
{
base.OnEnter(procedureOwner);
m_UpdateVersionComplete = false;
GameEntry.Resource.UpdateVersionList(procedureOwner.GetData<VarInt32>("VersionListLength"), procedureOwner.GetData<VarInt32>("VersionListHashCode"), procedureOwner.GetData<VarInt32>("VersionListCompressedLength"), procedureOwner.GetData<VarInt32>("VersionListCompressedHashCode"), m_UpdateVersionListCallbacks);
procedureOwner.RemoveData("VersionListLength");
procedureOwner.RemoveData("VersionListHashCode");
procedureOwner.RemoveData("VersionListCompressedLength");
procedureOwner.RemoveData("VersionListCompressedHashCode");
}
protected override void OnUpdate(ProcedureOwner procedureOwner, float elapseSeconds, float realElapseSeconds)
{
base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds);
if (!m_UpdateVersionComplete)
{
return;
}
ChangeState<ProcedureVerifyResources>(procedureOwner);
}
private void OnUpdateVersionListSuccess(string downloadPath, string downloadUri)
{
m_UpdateVersionComplete = true;
Log.Info("Update version list from '{0}' success.", downloadUri);
}
private void OnUpdateVersionListFailure(string downloadUri, string errorMessage)
{
Log.Warning("Update version list from '{0}' failure, error message is '{1}'.", downloadUri, errorMessage);
}
}
}
@@ -0,0 +1,77 @@
using GameFramework.Event;
using GeometryTD;
using UnityGameFramework.Runtime;
using ProcedureOwner = GameFramework.Fsm.IFsm<GameFramework.Procedure.IProcedureManager>;
namespace GeometryTD.Procedure
{
public class ProcedureVerifyResources : ProcedureBase
{
private bool m_VerifyResourcesComplete = false;
public override bool UseNativeDialog
{
get
{
return true;
}
}
protected override void OnEnter(ProcedureOwner procedureOwner)
{
base.OnEnter(procedureOwner);
GameEntry.Event.Subscribe(ResourceVerifyStartEventArgs.EventId, OnResourceVerifyStart);
GameEntry.Event.Subscribe(ResourceVerifySuccessEventArgs.EventId, OnResourceVerifySuccess);
GameEntry.Event.Subscribe(ResourceVerifyFailureEventArgs.EventId, OnResourceVerifyFailure);
m_VerifyResourcesComplete = false;
GameEntry.Resource.VerifyResources(OnVerifyResourcesComplete);
}
protected override void OnLeave(ProcedureOwner procedureOwner, bool isShutdown)
{
base.OnLeave(procedureOwner, isShutdown);
GameEntry.Event.Unsubscribe(ResourceVerifyStartEventArgs.EventId, OnResourceVerifyStart);
GameEntry.Event.Unsubscribe(ResourceVerifySuccessEventArgs.EventId, OnResourceVerifySuccess);
GameEntry.Event.Unsubscribe(ResourceVerifyFailureEventArgs.EventId, OnResourceVerifyFailure);
}
protected override void OnUpdate(ProcedureOwner procedureOwner, float elapseSeconds, float realElapseSeconds)
{
base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds);
if (!m_VerifyResourcesComplete)
{
return;
}
ChangeState<ProcedureCheckResources>(procedureOwner);
}
private void OnVerifyResourcesComplete(bool result)
{
m_VerifyResourcesComplete = true;
Log.Info("Verify resources complete, result is '{0}'.", result);
}
private void OnResourceVerifyStart(object sender, GameEventArgs e)
{
ResourceVerifyStartEventArgs ne = (ResourceVerifyStartEventArgs)e;
Log.Info("Start verify resources, verify resource count '{0}', verify resource total length '{1}'.", ne.Count, ne.TotalLength);
}
private void OnResourceVerifySuccess(object sender, GameEventArgs e)
{
ResourceVerifySuccessEventArgs ne = (ResourceVerifySuccessEventArgs)e;
Log.Info("Verify resource '{0}' success.", ne.Name);
}
private void OnResourceVerifyFailure(object sender, GameEventArgs e)
{
ResourceVerifyFailureEventArgs ne = (ResourceVerifyFailureEventArgs)e;
Log.Warning("Verify resource '{0}' failure.", ne.Name);
}
}
}
@@ -0,0 +1,52 @@
using System.Collections.Generic;
using GeometryTD.Definition;
namespace GeometryTD.Procedure
{
public static class FixedRunNodeSequenceBuilder
{
private const int FixedNodeCount = 10;
private static readonly int[] PlainCombatLevelCycle = { 1, 2, 3, 1 };
public static IReadOnlyList<RunNodeSeed> Build(LevelThemeType themeType)
{
if (themeType != LevelThemeType.Plain)
{
return new List<RunNodeSeed>();
}
List<RunNodeSeed> nodeSeeds = new List<RunNodeSeed>(FixedNodeCount)
{
CreateNodeSeed(1, 0, RunNodeType.Combat, themeType, PlainCombatLevelCycle[0]),
CreateNodeSeed(2, 1, RunNodeType.Event, themeType, 0),
CreateNodeSeed(3, 2, RunNodeType.Combat, themeType, PlainCombatLevelCycle[1]),
CreateNodeSeed(4, 3, RunNodeType.Shop, themeType, 0),
CreateNodeSeed(5, 4, RunNodeType.Combat, themeType, PlainCombatLevelCycle[2]),
CreateNodeSeed(6, 5, RunNodeType.Event, themeType, 0),
CreateNodeSeed(7, 6, RunNodeType.Combat, themeType, PlainCombatLevelCycle[3]),
CreateNodeSeed(8, 7, RunNodeType.Shop, themeType, 0),
CreateNodeSeed(9, 8, RunNodeType.Event, themeType, 0),
CreateNodeSeed(10, 9, RunNodeType.BossCombat, themeType, 4)
};
return nodeSeeds;
}
private static RunNodeSeed CreateNodeSeed(
int nodeId,
int sequenceIndex,
RunNodeType nodeType,
LevelThemeType themeType,
int linkedLevelId)
{
return new RunNodeSeed
{
NodeId = nodeId,
SequenceIndex = sequenceIndex,
NodeType = nodeType,
ThemeType = themeType,
LinkedLevelId = linkedLevelId
};
}
}
}
@@ -0,0 +1,468 @@
using GameFramework.Event;
using GameFramework.Fsm;
using GameFramework.Procedure;
using System;
using GeometryTD.CustomEvent;
using GeometryTD.CustomUtility;
using GeometryTD.Definition;
using GeometryTD.Factory;
using GeometryTD.UI;
using UnityGameFramework.Runtime;
namespace GeometryTD.Procedure
{
public class ProcedureMain : ProcedureBase
{
private const int MaxParticipantTowerCount = 4;
public override bool UseNativeDialog => false;
private RepoFormUseCase _repoFormUseCase;
private NodeMapFormUseCase _nodeMapFormUseCase;
private RunState _currentRunState;
private ProcedureMainFlowPhase _flowPhase = ProcedureMainFlowPhase.Hub;
private bool _isRunCompleteDialogShown;
private bool _isReturnToMenuPending;
#region FSM
protected override void OnEnter(IFsm<IProcedureManager> procedureOwner)
{
base.OnEnter(procedureOwner);
GameEntry.Event.Subscribe(NodeCompleteEventArgs.EventId, OnNodeComplete);
GameEntry.Event.Subscribe(NodeEnterEventArgs.EventId, OnNodeEnter);
GameEntry.Event.Subscribe(NodeMapNodeEnterRequestedEventArgs.EventId, OnNodeMapNodeEnterRequested);
GameEntry.TagRegistry.OnInit();
GameEntry.EventNode.OnInit();
GameEntry.CombatNode.OnInit(LevelThemeType.Plain);
GameEntry.ShopNode.OnInit();
string runId = Guid.NewGuid().ToString("N");
int runSeed = RunStateFactory.CreateRunSeed();
BackpackInventoryData initialInventory = InventorySeedUtility.CreateSampleInventory(runSeed);
GameEntry.PlayerInventory?.OnInit(initialInventory);
_currentRunState = RunStateFactory.CreateFixedRun(
LevelThemeType.Plain,
GameEntry.PlayerInventory != null
? GameEntry.PlayerInventory.GetInventorySnapshot()
: initialInventory,
runId,
runSeed);
_repoFormUseCase = new RepoFormUseCase();
GameEntry.UIRouter.BindUIUseCase(UIFormType.RepoForm, _repoFormUseCase);
_nodeMapFormUseCase = new NodeMapFormUseCase();
_nodeMapFormUseCase.SetRunState(_currentRunState);
GameEntry.UIRouter.BindUIUseCase(UIFormType.NodeMapForm, _nodeMapFormUseCase);
_isRunCompleteDialogShown = false;
_isReturnToMenuPending = false;
EnterHubFlow();
}
protected override void OnUpdate(IFsm<IProcedureManager> procedureOwner, float elapseSeconds,
float realElapseSeconds)
{
base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds);
if (_isReturnToMenuPending)
{
_isReturnToMenuPending = false;
procedureOwner.SetData<VarInt32>("NextSceneId", (int)SceneType.Menu);
ChangeState<ProcedureChangeScene>(procedureOwner);
return;
}
GameEntry.CombatNode.OnUpdate(elapseSeconds, realElapseSeconds);
}
protected override void OnLeave(IFsm<IProcedureManager> procedureOwner, bool isShutdown)
{
GameEntry.CombatNode.OnShutdown();
GameEntry.Event.Unsubscribe(NodeMapNodeEnterRequestedEventArgs.EventId, OnNodeMapNodeEnterRequested);
GameEntry.Event.Unsubscribe(NodeEnterEventArgs.EventId, OnNodeEnter);
GameEntry.Event.Unsubscribe(NodeCompleteEventArgs.EventId, OnNodeComplete);
GameEntry.UIRouter.CloseUI(UIFormType.RepoForm);
GameEntry.UIRouter.CloseUI(UIFormType.NodeMapForm);
GameEntry.UIRouter.CloseUI(UIFormType.MainForm);
GameEntry.UIRouter.CloseUI(UIFormType.DialogForm);
_repoFormUseCase = null;
_nodeMapFormUseCase = null;
_currentRunState = null;
_flowPhase = ProcedureMainFlowPhase.Hub;
_isRunCompleteDialogShown = false;
_isReturnToMenuPending = false;
base.OnLeave(procedureOwner, isShutdown);
}
#endregion
private void OnNodeEnter(object sender, GameEventArgs e)
{
if (!(e is NodeEnterEventArgs args))
{
return;
}
if (!string.IsNullOrWhiteSpace(args.RunId) &&
_currentRunState != null &&
!string.Equals(args.RunId, _currentRunState.RunId))
{
Log.Warning(
"ProcedureMain.OnNodeEnter() ignored. EventRunId={0}, CurrentRunId={1}.",
args.RunId,
_currentRunState.RunId);
return;
}
RunNodeState currentNode = _currentRunState?.CurrentNode;
if (!ProcedureMainNodeEventGuardService.MatchesCurrentNode(
_currentRunState,
args.NodeId,
args.NodeType,
args.SequenceIndex))
{
Log.Warning(
"ProcedureMain.OnNodeEnter() node mismatch. EventNodeId={0}, EventNodeType={1}, EventSequenceIndex={2}; CurrentNodeId={3}, CurrentNodeType={4}, CurrentSequenceIndex={5}.",
args.NodeId,
args.NodeType,
args.SequenceIndex,
currentNode?.NodeId ?? 0,
currentNode?.NodeType ?? RunNodeType.None,
currentNode?.SequenceIndex ?? -1);
return;
}
Log.Info(
"ProcedureMain.OnNodeEnter() accepted. RunId={0}, NodeId={1}, NodeType={2}, SequenceIndex={3}.",
string.IsNullOrWhiteSpace(args.RunId) ? _currentRunState?.RunId : args.RunId,
args.NodeId,
args.NodeType,
args.SequenceIndex);
EnterNodeFlow();
}
private void OnNodeComplete(object sender, GameEventArgs e)
{
if (!(e is NodeCompleteEventArgs args))
{
return;
}
if (!string.IsNullOrWhiteSpace(args.RunId) &&
_currentRunState != null &&
!string.Equals(args.RunId, _currentRunState.RunId))
{
Log.Warning(
"ProcedureMain.OnNodeComplete() ignored. EventRunId={0}, CurrentRunId={1}.",
args.RunId,
_currentRunState.RunId);
return;
}
RunNodeState currentNode = _currentRunState?.CurrentNode;
if (!ProcedureMainNodeEventGuardService.MatchesCurrentNode(
_currentRunState,
args.NodeId,
args.NodeType,
args.SequenceIndex))
{
Log.Warning(
"ProcedureMain.OnNodeComplete() node mismatch. EventNodeId={0}, EventNodeType={1}, EventSequenceIndex={2}; CurrentNodeId={3}, CurrentNodeType={4}, CurrentSequenceIndex={5}.",
args.NodeId,
args.NodeType,
args.SequenceIndex,
currentNode?.NodeId ?? 0,
currentNode?.NodeType ?? RunNodeType.None,
currentNode?.SequenceIndex ?? -1);
return;
}
Log.Info(
"ProcedureMain.OnNodeComplete() accepted. RunId={0}, NodeId={1}, NodeType={2}, SequenceIndex={3}, CompletionStatus={4}, CombatWon={5}.",
string.IsNullOrWhiteSpace(args.RunId) ? _currentRunState?.RunId : args.RunId,
args.NodeId,
args.NodeType,
args.SequenceIndex,
args.CompletionStatus,
args.CombatWon);
RunNodeCompletionSnapshot completionSnapshot = args.CompletionSnapshot != null
? args.CompletionSnapshot.Clone()
: _currentRunState?.CreateCurrentNodeContext()?.CreateCompletionSnapshot(null);
BackpackInventoryData inventorySnapshot = completionSnapshot?.InventorySnapshot;
if (inventorySnapshot == null && GameEntry.PlayerInventory != null)
{
inventorySnapshot = GameEntry.PlayerInventory.GetInventorySnapshot();
}
if (completionSnapshot != null)
{
completionSnapshot.InventorySnapshot = inventorySnapshot;
}
ProcedureMainParticipantTowerCleanupResult cleanupResult =
ProcedureMainParticipantTowerCleanupService.RemoveBrokenParticipantTowers(
inventorySnapshot,
MaxParticipantTowerCount);
if (cleanupResult.HasAnyRemovedTower && GameEntry.PlayerInventory != null)
{
GameEntry.PlayerInventory.ReplaceInventorySnapshot(inventorySnapshot);
}
if (completionSnapshot != null)
{
completionSnapshot.InventorySnapshot = inventorySnapshot;
}
ProcedureMainRunAdvanceResult advanceResult =
ProcedureMainRunFlowService.TryAdvanceRun(_currentRunState, args.CompletionStatus, completionSnapshot);
HandleRunAdvanceResult(advanceResult);
if (cleanupResult.HasAnyRemovedTower &&
advanceResult != ProcedureMainRunAdvanceResult.RunCompleted)
{
OpenRemovedParticipantTowerDialog(cleanupResult);
}
}
private void OnNodeMapNodeEnterRequested(object sender, GameEventArgs e)
{
if (!(sender is NodeMapForm) || !(e is NodeMapNodeEnterRequestedEventArgs args))
{
return;
}
RunNodeState currentNode = _currentRunState?.CurrentNode;
if (_currentRunState == null ||
_flowPhase != ProcedureMainFlowPhase.Hub ||
!_currentRunState.CanEnterCurrentNode ||
currentNode == null)
{
Log.Warning(
"ProcedureMain.OnNodeMapNodeEnterRequested() ignored. FlowPhase={0}, HasEnterableNode={1}.",
_flowPhase,
_currentRunState != null && _currentRunState.CanEnterCurrentNode);
return;
}
if (args.SequenceIndex != currentNode.SequenceIndex || args.NodeType != currentNode.NodeType)
{
Log.Warning(
"ProcedureMain.OnNodeMapNodeEnterRequested() ignored. Requested={0}#{1}, CurrentNode={2}#{3}.",
args.NodeType,
args.SequenceIndex,
currentNode.NodeType,
currentNode.SequenceIndex);
return;
}
switch (currentNode.NodeType)
{
case RunNodeType.Combat:
case RunNodeType.BossCombat:
ProcedureMainCombatEntryValidationResult validationResult =
ProcedureMainCombatEntryValidationService.Validate(
GameEntry.PlayerInventory != null
? GameEntry.PlayerInventory.GetInventorySnapshot()
: null);
if (!validationResult.CanEnterCombat)
{
LogCombatEntryBlocked(currentNode, validationResult);
OpenBlockedCombatDialog(validationResult);
return;
}
if (GameEntry.CombatNode.CurrentThemeType != currentNode.ThemeType)
{
GameEntry.CombatNode.OnInit(currentNode.ThemeType);
}
GameEntry.CombatNode.StartCombat(
currentNode.LinkedLevelId,
_currentRunState.CreateCurrentNodeContext());
return;
case RunNodeType.Event:
GameEntry.EventNode.StartEvent(_currentRunState.CreateCurrentNodeContext());
return;
case RunNodeType.Shop:
GameEntry.ShopNode.StartShop(_currentRunState.CreateCurrentNodeContext());
return;
default:
Log.Warning("ProcedureMain.OnNodeMapNodeEnterRequested() encountered unsupported node type: {0}.",
currentNode.NodeType);
return;
}
}
private void OpenBlockedCombatDialog(ProcedureMainCombatEntryValidationResult validationResult)
{
GameEntry.UIRouter.CloseUI(UIFormType.DialogForm);
GameEntry.UIRouter.OpenUI(
UIFormType.DialogForm,
ProcedureMainCombatEntryValidationService.BuildBlockedCombatDialogRawData(validationResult));
}
private void LogCombatEntryBlocked(
RunNodeState currentNode,
ProcedureMainCombatEntryValidationResult validationResult)
{
switch (validationResult?.BlockReason)
{
case ProcedureMainCombatEntryBlockReason.InventoryUnavailable:
Log.Warning(
"ProcedureMain blocked combat start. RunId={0}, NodeId={1}, NodeType={2}, SequenceIndex={3}, Reason={4}.",
_currentRunState?.RunId,
currentNode?.NodeId ?? 0,
currentNode?.NodeType ?? RunNodeType.None,
currentNode?.SequenceIndex ?? -1,
ProcedureMainCombatEntryBlockReason.InventoryUnavailable);
return;
case ProcedureMainCombatEntryBlockReason.NoValidParticipantTower:
Log.Warning(
"ProcedureMain blocked combat start. RunId={0}, NodeId={1}, NodeType={2}, SequenceIndex={3}, Reason={4}, InvalidParticipantTowers={5}.",
_currentRunState?.RunId,
currentNode?.NodeId ?? 0,
currentNode?.NodeType ?? RunNodeType.None,
currentNode?.SequenceIndex ?? -1,
ProcedureMainCombatEntryBlockReason.NoValidParticipantTower,
ProcedureMainCombatEntryValidationService.BuildInvalidParticipantTowerLog(
validationResult.ValidationSummary));
return;
default:
Log.Warning(
"ProcedureMain blocked combat start. RunId={0}, NodeId={1}, NodeType={2}, SequenceIndex={3}, Reason=Unknown.",
_currentRunState?.RunId,
currentNode?.NodeId ?? 0,
currentNode?.NodeType ?? RunNodeType.None,
currentNode?.SequenceIndex ?? -1);
return;
}
}
private void HandleRunAdvanceResult(ProcedureMainRunAdvanceResult result)
{
switch (result)
{
case ProcedureMainRunAdvanceResult.NoChange:
return;
case ProcedureMainRunAdvanceResult.NodeException:
Log.Info(
"ProcedureMain current node entered exception state. RunId={0}, NodeId={1}, NodeType={2}, SequenceIndex={3}.",
_currentRunState?.RunId,
_currentRunState?.CurrentNode?.NodeId ?? 0,
_currentRunState?.CurrentNode?.NodeType ?? RunNodeType.None,
_currentRunState?.CurrentNode?.SequenceIndex ?? -1);
EnterHubFlow();
return;
case ProcedureMainRunAdvanceResult.AdvancedToNextNode:
LogNextNode();
EnterHubFlow();
return;
case ProcedureMainRunAdvanceResult.RunCompleted:
EnterRunCompletedPendingFinish();
return;
default:
return;
}
}
private void EnterHubFlow()
{
_flowPhase = ProcedureMainFlowPhase.Hub;
_nodeMapFormUseCase?.SetRunState(_currentRunState);
GameEntry.UIRouter.OpenUI(UIFormType.NodeMapForm);
GameEntry.UIRouter.OpenUI(UIFormType.MainForm);
}
private void EnterNodeFlow()
{
_flowPhase = ProcedureMainFlowPhase.NodeActive;
CloseHubUI();
}
private void EnterRunCompletedPendingFinish()
{
_flowPhase = ProcedureMainFlowPhase.RunCompletedPendingFinish;
_nodeMapFormUseCase?.SetRunState(_currentRunState);
CloseHubUI();
Log.Info("ProcedureMain run completed. RunId={0}", _currentRunState?.RunId);
ProcedureMainRunCompletionResult result =
ProcedureMainRunCompletionService.TryEnterCompletedPendingFinish(_isRunCompleteDialogShown);
if (result == ProcedureMainRunCompletionResult.ShowCompletionDialog)
{
_isRunCompleteDialogShown = true;
OpenRunCompleteDialog();
}
}
private void CloseHubUI()
{
GameEntry.UIRouter.CloseUI(UIFormType.NodeMapForm);
GameEntry.UIRouter.CloseUI(UIFormType.MainForm);
}
private void LogNextNode()
{
RunNodeState nextNode = _currentRunState?.CurrentNode;
if (nextNode == null)
{
return;
}
Log.Info(
"ProcedureMain advanced run. RunId={0}, NextNodeType={1}, SequenceIndex={2}, LevelId={3}.",
_currentRunState.RunId,
nextNode.NodeType,
nextNode.SequenceIndex,
nextNode.LinkedLevelId);
}
private void OpenRunCompleteDialog()
{
GameEntry.UIRouter.OpenUI(UIFormType.DialogForm, new DialogFormRawData
{
Mode = 1,
Title = "Run Complete",
Message = "Boss node completed. This run is finished and will return to the main menu.",
PauseGame = false,
ConfirmText = "Return to Menu",
OnClickConfirm = OnRunCompleteDialogConfirmed
});
}
private void OpenRemovedParticipantTowerDialog(ProcedureMainParticipantTowerCleanupResult cleanupResult)
{
GameEntry.UIRouter.CloseUI(UIFormType.DialogForm);
GameEntry.UIRouter.OpenUI(
UIFormType.DialogForm,
ProcedureMainParticipantTowerCleanupService.BuildRemovedTowerDialogRawData(cleanupResult));
}
private void OnRunCompleteDialogConfirmed(object userData)
{
_ = userData;
ProcedureMainRunCompletionResult result = ProcedureMainRunCompletionService.TryConfirmReturnToMenu(
_flowPhase,
_isReturnToMenuPending);
if (result != ProcedureMainRunCompletionResult.ReturnToMenu)
{
Log.Warning(
"ProcedureMain ignored run completion confirm. FlowPhase={0}, ReturnPending={1}.",
_flowPhase,
_isReturnToMenuPending);
return;
}
_isReturnToMenuPending = true;
}
}
}
@@ -0,0 +1,13 @@
using GeometryTD.Definition;
namespace GeometryTD.Procedure
{
public sealed class ProcedureMainCombatEntryValidationResult
{
public bool CanEnterCombat => BlockReason == ProcedureMainCombatEntryBlockReason.None;
public ProcedureMainCombatEntryBlockReason BlockReason { get; set; }
public CombatParticipantTowerValidationSummary ValidationSummary { get; set; }
}
}
@@ -0,0 +1,123 @@
using System.Text;
using GeometryTD.Definition;
using GeometryTD.UI;
namespace GeometryTD.Procedure
{
public static class ProcedureMainCombatEntryValidationService
{
public static ProcedureMainCombatEntryValidationResult Validate(BackpackInventoryData inventory)
{
if (inventory == null)
{
return new ProcedureMainCombatEntryValidationResult
{
BlockReason = ProcedureMainCombatEntryBlockReason.InventoryUnavailable,
ValidationSummary = new CombatParticipantTowerValidationSummary()
};
}
CombatParticipantTowerValidationSummary summary =
CombatParticipantTowerValidationService.ValidateParticipantTowers(inventory);
return new ProcedureMainCombatEntryValidationResult
{
BlockReason = summary.HasAnyValidParticipantTower
? ProcedureMainCombatEntryBlockReason.None
: ProcedureMainCombatEntryBlockReason.NoValidParticipantTower,
ValidationSummary = summary
};
}
public static string BuildInvalidParticipantTowerLog(
CombatParticipantTowerValidationSummary summary)
{
if (summary?.InvalidResults == null || summary.InvalidResults.Count <= 0)
{
return "none";
}
StringBuilder builder = new StringBuilder();
for (int i = 0; i < summary.InvalidResults.Count; i++)
{
CombatParticipantTowerValidationResult result = summary.InvalidResults[i];
if (result == null)
{
continue;
}
if (builder.Length > 0)
{
builder.Append(", ");
}
builder.Append('#');
builder.Append(result.TowerInstanceId);
builder.Append(':');
builder.Append(result.FailureReason);
}
return builder.Length > 0 ? builder.ToString() : "none";
}
public static DialogFormRawData BuildBlockedCombatDialogRawData(
ProcedureMainCombatEntryValidationResult validationResult)
{
return new DialogFormRawData
{
Mode = 1,
Title = "无法进入战斗",
Message = BuildBlockedCombatDialogMessage(validationResult),
PauseGame = false,
ConfirmText = "知道了"
};
}
private static string BuildBlockedCombatDialogMessage(
ProcedureMainCombatEntryValidationResult validationResult)
{
if (validationResult == null)
{
return "当前无法确认出战信息,请稍后重试。";
}
switch (validationResult.BlockReason)
{
case ProcedureMainCombatEntryBlockReason.InventoryUnavailable:
return "当前无法读取库存快照,暂时不能进入战斗。请重新进入本轮流程后再试。";
case ProcedureMainCombatEntryBlockReason.NoValidParticipantTower:
return BuildNoValidParticipantTowerMessage(validationResult.ValidationSummary);
default:
return "当前出战校验未通过,暂时不能进入战斗。";
}
}
private static string BuildNoValidParticipantTowerMessage(
CombatParticipantTowerValidationSummary summary)
{
if (summary?.InvalidResults == null || summary.InvalidResults.Count <= 0)
{
return "参战区至少需要 1 座完整装配了枪口、轴承、底座的塔,才能进入战斗。";
}
StringBuilder builder = new StringBuilder();
builder.Append("参战区没有可出战的完整塔。");
for (int i = 0; i < summary.InvalidResults.Count; i++)
{
CombatParticipantTowerValidationResult result = summary.InvalidResults[i];
if (result == null)
{
continue;
}
builder.Append('\n');
builder.Append("塔 #");
builder.Append(result.TowerInstanceId);
builder.Append(' ');
builder.Append(CombatParticipantTowerValidationText.GetFailureReasonMessage(result.FailureReason));
}
return builder.ToString();
}
}
}
@@ -0,0 +1,22 @@
namespace GeometryTD.Procedure
{
public static class ProcedureMainNodeEventGuardService
{
public static bool MatchesCurrentNode(
RunState runState,
int nodeId,
RunNodeType nodeType,
int sequenceIndex)
{
RunNodeState currentNode = runState?.CurrentNode;
if (currentNode == null)
{
return false;
}
return (nodeId <= 0 || nodeId == currentNode.NodeId) &&
(nodeType == RunNodeType.None || nodeType == currentNode.NodeType) &&
(sequenceIndex < 0 || sequenceIndex == currentNode.SequenceIndex);
}
}
}
@@ -0,0 +1,106 @@
using System.Collections.Generic;
using GeometryTD.CustomUtility;
using GeometryTD.Definition;
using GeometryTD.UI;
namespace GeometryTD.Procedure
{
public sealed class ProcedureMainParticipantTowerCleanupResult
{
public List<CombatParticipantTowerValidationResult> RemovedResults { get; } = new();
public bool HasAnyRemovedTower => RemovedResults.Count > 0;
}
public static class ProcedureMainParticipantTowerCleanupService
{
public static ProcedureMainParticipantTowerCleanupResult RemoveBrokenParticipantTowers(
BackpackInventoryData inventory,
int maxParticipantCount)
{
ProcedureMainParticipantTowerCleanupResult result = new ProcedureMainParticipantTowerCleanupResult();
if (inventory == null)
{
return result;
}
CombatParticipantTowerValidationSummary summary =
CombatParticipantTowerValidationService.ValidateParticipantTowers(inventory);
if (summary.InvalidResults == null || summary.InvalidResults.Count <= 0)
{
return result;
}
HashSet<long> removedTowerIds = new HashSet<long>();
for (int i = 0; i < summary.InvalidResults.Count; i++)
{
CombatParticipantTowerValidationResult invalidResult = summary.InvalidResults[i];
if (invalidResult == null ||
invalidResult.TowerInstanceId <= 0 ||
!IsBrokenFailureReason(invalidResult.FailureReason) ||
!removedTowerIds.Add(invalidResult.TowerInstanceId))
{
continue;
}
if (InventoryParticipantUtility.TryRemoveParticipantTower(
inventory,
invalidResult.TowerInstanceId,
maxParticipantCount))
{
result.RemovedResults.Add(invalidResult);
}
}
return result;
}
public static DialogFormRawData BuildRemovedTowerDialogRawData(
ProcedureMainParticipantTowerCleanupResult cleanupResult)
{
return new DialogFormRawData
{
Mode = 1,
Title = "出战塔已损坏",
Message = BuildRemovedTowerDialogMessage(cleanupResult),
PauseGame = false,
ConfirmText = "知道了"
};
}
private static string BuildRemovedTowerDialogMessage(
ProcedureMainParticipantTowerCleanupResult cleanupResult)
{
if (cleanupResult == null || !cleanupResult.HasAnyRemovedTower)
{
return "当前没有需要移出参战区的损坏防御塔。";
}
System.Text.StringBuilder builder = new System.Text.StringBuilder();
builder.Append("以下防御塔已损坏,已自动移出参战区:");
for (int i = 0; i < cleanupResult.RemovedResults.Count; i++)
{
CombatParticipantTowerValidationResult removedResult = cleanupResult.RemovedResults[i];
if (removedResult == null)
{
continue;
}
builder.Append('\n');
builder.Append("塔 #");
builder.Append(removedResult.TowerInstanceId);
builder.Append(' ');
builder.Append(CombatParticipantTowerValidationText.GetFailureReasonMessage(removedResult.FailureReason));
}
return builder.ToString();
}
private static bool IsBrokenFailureReason(CombatParticipantTowerValidationFailureReason failureReason)
{
return failureReason == CombatParticipantTowerValidationFailureReason.BrokenMuzzleComponent ||
failureReason == CombatParticipantTowerValidationFailureReason.BrokenBearingComponent ||
failureReason == CombatParticipantTowerValidationFailureReason.BrokenBaseComponent;
}
}
}
@@ -0,0 +1,26 @@
using GeometryTD.Definition;
namespace GeometryTD.Procedure
{
public static class ProcedureMainRunCompletionService
{
public static ProcedureMainRunCompletionResult TryEnterCompletedPendingFinish(bool isCompletionDialogShown)
{
return isCompletionDialogShown
? ProcedureMainRunCompletionResult.NoChange
: ProcedureMainRunCompletionResult.ShowCompletionDialog;
}
public static ProcedureMainRunCompletionResult TryConfirmReturnToMenu(
ProcedureMainFlowPhase flowPhase,
bool isReturnToMenuPending)
{
if (flowPhase != ProcedureMainFlowPhase.RunCompletedPendingFinish || isReturnToMenuPending)
{
return ProcedureMainRunCompletionResult.NoChange;
}
return ProcedureMainRunCompletionResult.ReturnToMenu;
}
}
}
@@ -0,0 +1,27 @@
using GeometryTD.Definition;
namespace GeometryTD.Procedure
{
public static class ProcedureMainRunFlowService
{
public static ProcedureMainRunAdvanceResult TryAdvanceRun(
RunState runState,
RunNodeCompletionStatus completionStatus,
RunNodeCompletionSnapshot completionSnapshot)
{
if (!RunStateAdvanceService.TryCompleteCurrentNode(runState, completionStatus, completionSnapshot))
{
return ProcedureMainRunAdvanceResult.NoChange;
}
if (runState != null && runState.IsCompleted)
{
return ProcedureMainRunAdvanceResult.RunCompleted;
}
return completionStatus == RunNodeCompletionStatus.Exception
? ProcedureMainRunAdvanceResult.NodeException
: ProcedureMainRunAdvanceResult.AdvancedToNextNode;
}
}
}
+355
View File
@@ -0,0 +1,355 @@
using System;
using System.Collections.Generic;
using GeometryTD.CustomUtility;
using GeometryTD.Definition;
using GeometryTD.Factory;
namespace GeometryTD.Procedure
{
public enum RunNodeType
{
None = 0,
Combat = 1,
Event = 2,
Shop = 3,
BossCombat = 4
}
public enum RunNodeStatus
{
Locked = 0,
Available = 1,
Completed = 2,
Exception = 3,
Skipped = 4
}
public enum RunNodeCompletionStatus
{
None = 0,
Completed = 1,
Exception = 2
}
[Serializable]
public sealed class RunNodeSeed
{
public int NodeId { get; set; }
public RunNodeType NodeType { get; set; }
public LevelThemeType ThemeType { get; set; }
public int LinkedLevelId { get; set; }
public int SequenceIndex { get; set; } = -1;
}
[Serializable]
public sealed class RunNodeState
{
public int NodeId { get; internal set; }
public RunNodeType NodeType { get; internal set; }
public LevelThemeType ThemeType { get; internal set; }
public int LinkedLevelId { get; internal set; }
public RunNodeStatus Status { get; internal set; }
public int SequenceIndex { get; internal set; }
internal RunNodeState Clone()
{
return new RunNodeState
{
NodeId = NodeId,
NodeType = NodeType,
ThemeType = ThemeType,
LinkedLevelId = LinkedLevelId,
Status = Status,
SequenceIndex = SequenceIndex
};
}
}
[Serializable]
public sealed class RunItemState
{
public int ItemId { get; set; }
public int StackCount { get; set; }
internal RunItemState Clone()
{
return new RunItemState
{
ItemId = ItemId,
StackCount = StackCount
};
}
}
[Serializable]
public sealed class RunNodeExecutionContext
{
public string RunId { get; set; }
public int RunSeed { get; set; }
public int NodeId { get; set; }
public RunNodeType NodeType { get; set; }
public int SequenceIndex { get; set; }
public LevelThemeType ThemeType { get; set; }
public int ThemeStageIndex { get; set; }
public List<LevelThemeType> CurrentThemePool { get; set; } = new List<LevelThemeType>();
public List<LevelThemeType> ThemeHistory { get; set; } = new List<LevelThemeType>();
public int CurrentNodeContinueChallengeLayer { get; set; }
public List<RunItemState> RunItems { get; set; } = new List<RunItemState>();
internal RunNodeExecutionContext Clone()
{
return new RunNodeExecutionContext
{
RunId = RunId,
RunSeed = RunSeed,
NodeId = NodeId,
NodeType = NodeType,
SequenceIndex = SequenceIndex,
ThemeType = ThemeType,
ThemeStageIndex = ThemeStageIndex,
CurrentThemePool = RunStateCloneUtility.CloneThemeList(CurrentThemePool),
ThemeHistory = RunStateCloneUtility.CloneThemeList(ThemeHistory),
CurrentNodeContinueChallengeLayer = CurrentNodeContinueChallengeLayer,
RunItems = RunStateCloneUtility.CloneRunItems(RunItems)
};
}
public RunNodeCompletionSnapshot CreateCompletionSnapshot(BackpackInventoryData inventorySnapshot)
{
return new RunNodeCompletionSnapshot
{
InventorySnapshot = InventoryCloneUtility.CloneInventory(inventorySnapshot),
ThemeType = ThemeType,
ThemeStageIndex = ThemeStageIndex,
CurrentThemePool = RunStateCloneUtility.CloneThemeList(CurrentThemePool),
ThemeHistory = RunStateCloneUtility.CloneThemeList(ThemeHistory),
CurrentNodeContinueChallengeLayer = CurrentNodeContinueChallengeLayer,
RunItems = RunStateCloneUtility.CloneRunItems(RunItems)
};
}
}
[Serializable]
public sealed class RunNodeCompletionSnapshot
{
public BackpackInventoryData InventorySnapshot { get; set; }
public LevelThemeType ThemeType { get; set; }
public int ThemeStageIndex { get; set; }
public List<LevelThemeType> CurrentThemePool { get; set; } = new List<LevelThemeType>();
public List<LevelThemeType> ThemeHistory { get; set; } = new List<LevelThemeType>();
public int CurrentNodeContinueChallengeLayer { get; set; }
public List<RunItemState> RunItems { get; set; } = new List<RunItemState>();
internal RunNodeCompletionSnapshot Clone()
{
return new RunNodeCompletionSnapshot
{
InventorySnapshot = InventoryCloneUtility.CloneInventory(InventorySnapshot),
ThemeType = ThemeType,
ThemeStageIndex = ThemeStageIndex,
CurrentThemePool = RunStateCloneUtility.CloneThemeList(CurrentThemePool),
ThemeHistory = RunStateCloneUtility.CloneThemeList(ThemeHistory),
CurrentNodeContinueChallengeLayer = CurrentNodeContinueChallengeLayer,
RunItems = RunStateCloneUtility.CloneRunItems(RunItems)
};
}
}
[Serializable]
public sealed class RunState
{
private readonly List<RunNodeState> _nodes;
internal RunState(
string runId,
int runSeed,
LevelThemeType themeType,
List<RunNodeState> nodes,
BackpackInventoryData runInventorySnapshot)
{
RunId = string.IsNullOrWhiteSpace(runId) ? Guid.NewGuid().ToString("N") : runId;
RunSeed = runSeed == 0 ? RunStateFactory.CreateRunSeed() : runSeed;
ThemeType = themeType;
_nodes = nodes ?? new List<RunNodeState>();
RunInventorySnapshot = InventoryCloneUtility.CloneInventory(runInventorySnapshot);
ThemeStageIndex = 0;
CurrentThemePool = CreateDefaultThemeList(themeType);
ThemeHistory = CreateDefaultThemeList(themeType);
CurrentNodeContinueChallengeLayer = 0;
RunItems = new List<RunItemState>();
CurrentNodeIndex = _nodes.Count > 0 ? 0 : -1;
IsCompleted = _nodes.Count <= 0;
}
public string RunId { get; internal set; }
public int RunSeed { get; internal set; }
public LevelThemeType ThemeType { get; internal set; }
public int CurrentNodeIndex { get; internal set; }
public bool IsCompleted { get; internal set; }
public BackpackInventoryData RunInventorySnapshot { get; internal set; }
public int ThemeStageIndex { get; internal set; }
public List<LevelThemeType> CurrentThemePool { get; internal set; }
public List<LevelThemeType> ThemeHistory { get; internal set; }
public int CurrentNodeContinueChallengeLayer { get; internal set; }
public List<RunItemState> RunItems { get; internal set; }
public IReadOnlyList<RunNodeState> Nodes => _nodes;
public int NodeCount => _nodes.Count;
public RunNodeState CurrentNode
{
get
{
if (CurrentNodeIndex < 0 || CurrentNodeIndex >= _nodes.Count)
{
return null;
}
return _nodes[CurrentNodeIndex];
}
}
public bool CanEnterCurrentNode => !IsCompleted && CurrentNode != null && CurrentNode.Status == RunNodeStatus.Available;
public RunNodeState GetNodeBySequenceIndex(int sequenceIndex)
{
if (sequenceIndex < 0)
{
return null;
}
foreach (RunNodeState nodeState in _nodes)
{
if (nodeState.SequenceIndex == sequenceIndex)
{
return nodeState;
}
}
return null;
}
public static bool IsBossNode(RunNodeState nodeState)
{
return nodeState != null && nodeState.NodeType == RunNodeType.BossCombat;
}
public int CompletedNodeCount
{
get
{
int count = 0;
foreach (var nodeState in _nodes)
{
if (nodeState.Status == RunNodeStatus.Completed)
{
count++;
}
}
return count;
}
}
internal void ReplaceInventorySnapshot(BackpackInventoryData inventorySnapshot)
{
RunInventorySnapshot = InventoryCloneUtility.CloneInventory(inventorySnapshot);
}
public RunNodeExecutionContext CreateCurrentNodeContext()
{
RunNodeState currentNode = CurrentNode;
if (currentNode == null)
{
return null;
}
return new RunNodeExecutionContext
{
RunId = RunId,
RunSeed = RunSeed,
NodeId = currentNode.NodeId,
NodeType = currentNode.NodeType,
SequenceIndex = currentNode.SequenceIndex,
ThemeType = ThemeType,
ThemeStageIndex = ThemeStageIndex,
CurrentThemePool = RunStateCloneUtility.CloneThemeList(CurrentThemePool),
ThemeHistory = RunStateCloneUtility.CloneThemeList(ThemeHistory),
CurrentNodeContinueChallengeLayer = CurrentNodeContinueChallengeLayer,
RunItems = RunStateCloneUtility.CloneRunItems(RunItems)
};
}
public void ApplyCompletionSnapshot(RunNodeCompletionSnapshot completionSnapshot)
{
RunInventorySnapshot = InventoryCloneUtility.CloneInventory(completionSnapshot.InventorySnapshot);
ThemeType = completionSnapshot.ThemeType;
ThemeStageIndex = completionSnapshot.ThemeStageIndex;
CurrentThemePool = RunStateCloneUtility.CloneThemeList(completionSnapshot.CurrentThemePool);
ThemeHistory = RunStateCloneUtility.CloneThemeList(completionSnapshot.ThemeHistory);
CurrentNodeContinueChallengeLayer = completionSnapshot.CurrentNodeContinueChallengeLayer;
RunItems = RunStateCloneUtility.CloneRunItems(completionSnapshot.RunItems);
}
private static List<LevelThemeType> CreateDefaultThemeList(LevelThemeType themeType)
{
List<LevelThemeType> themes = new List<LevelThemeType>();
if (themeType != LevelThemeType.None)
{
themes.Add(themeType);
}
return themes;
}
}
internal static class RunStateCloneUtility
{
internal static List<LevelThemeType> CloneThemeList(IReadOnlyList<LevelThemeType> source)
{
List<LevelThemeType> cloned = new List<LevelThemeType>();
if (source == null)
{
return cloned;
}
for (int i = 0; i < source.Count; i++)
{
cloned.Add(source[i]);
}
return cloned;
}
internal static List<RunItemState> CloneRunItems(IReadOnlyList<RunItemState> source)
{
List<RunItemState> cloned = new List<RunItemState>();
if (source == null)
{
return cloned;
}
for (int i = 0; i < source.Count; i++)
{
RunItemState item = source[i];
if (item != null)
{
cloned.Add(item.Clone());
}
}
return cloned;
}
}
}
@@ -0,0 +1,56 @@
namespace GeometryTD.Procedure
{
public static class RunStateAdvanceService
{
public static bool TryCompleteCurrentNode(
RunState runState,
RunNodeCompletionStatus completionStatus,
RunNodeCompletionSnapshot completionSnapshot)
{
if (runState == null || runState.IsCompleted)
{
return false;
}
RunNodeState currentNode = runState.CurrentNode;
if (currentNode == null || currentNode.Status != RunNodeStatus.Available)
{
return false;
}
runState.ApplyCompletionSnapshot(completionSnapshot);
if (completionStatus == RunNodeCompletionStatus.Exception)
{
currentNode.Status = RunNodeStatus.Exception;
return true;
}
if (completionStatus != RunNodeCompletionStatus.Completed)
{
return false;
}
currentNode.Status = RunNodeStatus.Completed;
int nextIndex = runState.CurrentNodeIndex + 1;
if (nextIndex >= runState.Nodes.Count)
{
runState.CurrentNodeContinueChallengeLayer = 0;
runState.CurrentNodeIndex = runState.Nodes.Count;
runState.IsCompleted = true;
return true;
}
runState.CurrentNodeIndex = nextIndex;
runState.CurrentNodeContinueChallengeLayer = 0;
RunNodeState nextNode = runState.CurrentNode;
if (nextNode != null && nextNode.Status == RunNodeStatus.Locked)
{
nextNode.Status = RunNodeStatus.Available;
}
return true;
}
}
}
+59
View File
@@ -0,0 +1,59 @@
using GameFramework.Fsm;
using GameFramework.Procedure;
using GeometryTD.Definition;
using GeometryTD.UI;
using UnityGameFramework.Runtime;
namespace GeometryTD.Procedure
{
public class ProcedureMenu : ProcedureBase
{
public override bool UseNativeDialog => false;
public bool GameStart { get; set; }
private MenuFormUseCase _menuFormUseCase;
#region FSM
protected override void OnInit(IFsm<IProcedureManager> procedureOwner)
{
base.OnInit(procedureOwner);
}
protected override void OnEnter(IFsm<IProcedureManager> procedureOwner)
{
base.OnEnter(procedureOwner);
GameStart = false;
_menuFormUseCase = new MenuFormUseCase();
GameEntry.UIRouter.BindUIUseCase(UIFormType.MenuForm, _menuFormUseCase);
GameEntry.UIRouter.OpenUI(UIFormType.MenuForm);
}
protected override void OnUpdate(IFsm<IProcedureManager> procedureOwner, float elapseSeconds,
float realElapseSeconds)
{
base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds);
if (GameStart)
{
procedureOwner.SetData<VarInt32>("NextSceneId", (int)SceneType.Main);
ChangeState<ProcedureChangeScene>(procedureOwner);
return;
}
}
protected override void OnLeave(IFsm<IProcedureManager> procedureOwner, bool isShutdown)
{
GameEntry.UIRouter.CloseUI(UIFormType.MenuForm);
_menuFormUseCase = null;
base.OnLeave(procedureOwner, isShutdown);
}
#endregion
}
}
+30
View File
@@ -0,0 +1,30 @@
using GameFramework.Fsm;
using GameFramework.Procedure;
namespace GeometryTD.Procedure
{
public class ProcedureTest : ProcedureBase
{
public override bool UseNativeDialog => false;
#region FSM
protected override void OnEnter(IFsm<IProcedureManager> procedureOwner)
{
base.OnEnter(procedureOwner);
}
protected override void OnUpdate(IFsm<IProcedureManager> procedureOwner, float elapseSeconds,
float realElapseSeconds)
{
base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds);
}
protected override void OnLeave(IFsm<IProcedureManager> procedureOwner, bool isShutdown)
{
base.OnLeave(procedureOwner, isShutdown);
}
#endregion
}
}