From 9b9b5ce88090dec35030fbe368d811f4379845bb Mon Sep 17 00:00:00 2001 From: Frank Yang Date: Sun, 20 Mar 2022 23:59:00 +0800 Subject: [PATCH] feat: first version --- TribeOfBattle/Imports.cs | 20 +++ TribeOfBattle/Menu.cs | 31 ++++ TribeOfBattle/Preloads.cs | 222 +++++++++++++++++++++++++++ TribeOfBattle/Resources/Lang/en.json | 9 ++ TribeOfBattle/Resources/Lang/zh.json | 9 ++ TribeOfBattle/SceneEdit.cs | 53 +++++++ TribeOfBattle/Settings.cs | 12 ++ TribeOfBattle/TribeOfBattle.cs | 53 +++++++ TribeOfBattle/TribeOfBattle.csproj | 2 +- TribeOfBattle/Util/EnumerableUtil.cs | 11 ++ TribeOfBattle/Util/L11nUtil.cs | 62 ++++++++ TribeOfBattle/Util/MiscUtil.cs | 19 +++ 12 files changed, 502 insertions(+), 1 deletion(-) create mode 100644 TribeOfBattle/Imports.cs create mode 100644 TribeOfBattle/Menu.cs create mode 100644 TribeOfBattle/Preloads.cs create mode 100644 TribeOfBattle/Resources/Lang/en.json create mode 100644 TribeOfBattle/Resources/Lang/zh.json create mode 100644 TribeOfBattle/SceneEdit.cs create mode 100644 TribeOfBattle/Settings.cs create mode 100644 TribeOfBattle/TribeOfBattle.cs create mode 100644 TribeOfBattle/Util/EnumerableUtil.cs create mode 100644 TribeOfBattle/Util/L11nUtil.cs create mode 100644 TribeOfBattle/Util/MiscUtil.cs diff --git a/TribeOfBattle/Imports.cs b/TribeOfBattle/Imports.cs new file mode 100644 index 0000000..52ad7b5 --- /dev/null +++ b/TribeOfBattle/Imports.cs @@ -0,0 +1,20 @@ +global using System; +global using System.Collections.Generic; +global using System.Linq; +global using System.Reflection; +global using System.Runtime.CompilerServices; + +global using HutongGames.PlayMaker; + +global using Modding; + +global using Satchel; + +global using TribeOfBattle.Util; + +global using UnityEngine; +global using UnityEngine.SceneManagement; + +global using Lang = Language.Language; +global using UObject = UnityEngine.Object; +global using USceneManager = UnityEngine.SceneManagement.SceneManager; diff --git a/TribeOfBattle/Menu.cs b/TribeOfBattle/Menu.cs new file mode 100644 index 0000000..69e09c1 --- /dev/null +++ b/TribeOfBattle/Menu.cs @@ -0,0 +1,31 @@ +using static Modding.IMenuMod; + +namespace TribeOfBattle; + +public sealed partial class TribeOfBattle : IMenuMod { + bool IMenuMod.ToggleButtonInsideMenu => true; + + List IMenuMod.GetMenuData(MenuEntry? toggleButtonEntry) => new() { + toggleButtonEntry!.Value, + new( + "Option/ModifyPantheons".Localize(), + new string[] { + Lang.Get("MOH_OFF", "MainMenu"), + Lang.Get("MOH_ON", "MainMenu") + }, + "", + i => GlobalSettings.modifyPantheons = i != 0, + () => GlobalSettings.modifyPantheons ? 1 : 0 + ), + new( + "Option/UnfixBugs".Localize(), + new string[] { + Lang.Get("MOH_OFF", "MainMenu"), + Lang.Get("MOH_ON", "MainMenu") + }, + "", + i => GlobalSettings.unfixBugs = i != 0, + () => GlobalSettings.unfixBugs ? 1 : 0 + ) + }; +} diff --git a/TribeOfBattle/Preloads.cs b/TribeOfBattle/Preloads.cs new file mode 100644 index 0000000..6f9f100 --- /dev/null +++ b/TribeOfBattle/Preloads.cs @@ -0,0 +1,222 @@ +using System.IO; + +using HutongGames.PlayMaker.Actions; + +namespace TribeOfBattle; + +public sealed partial class TribeOfBattle { + private static GameObject? mantisPreload = null; + + private static GameObject? traitorPrefab = null; + private static GameObject? traitorInstance = null; + + private static bool didGlobalChange = false; + + private static GameObject? grassPrefab = null; + + private static tk2dSprite? traitorSprite = null; + private static Texture2D? traitorTexOrig = null; + private static readonly Lazy traitorTex = new(() => { + Stream stream = typeof(TribeOfBattle).Assembly + .GetManifestResourceStream("TribeOfBattle.Resources.TraitorLord.png"); + MemoryStream ms = new((int) stream.Length); + + stream.CopyTo(ms); + stream.Close(); + + byte[] bytes = ms.ToArray(); + ms.Close(); + + Texture2D texture2D = new(2, 2); + texture2D.LoadImage(bytes, true); + + return texture2D; + }); + + public override List<(string, string)> GetPreloadNames() => new() { + ("GG_Mantis_Lords_V", "Mantis Battle/Battle Main/Mantis Lord"), + ("GG_Traitor_Lord", "Battle Scene/Wave 3/Mantis Traitor Lord") + }; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SavePreloads(Dictionary> preloads) { + if (preloads == null) { + return; + } + + mantisPreload = preloads["GG_Mantis_Lords_V"]["Mantis Battle/Battle Main/Mantis Lord"]; + traitorPrefab = CreateTraitorPrefab(preloads["GG_Traitor_Lord"]["Battle Scene/Wave 3/Mantis Traitor Lord"]); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static GameObject CreateTraitorPrefab(GameObject preload) { + var prefab = GameObject.Instantiate(preload); + GameObject.DontDestroyOnLoad(prefab); + + grassPrefab = prefab.GetComponent().startupPool + .Filter(pool => pool.prefab.name == "mega_mantis_tall_slash") + .Single().prefab.Child("Grass")!; + + traitorSprite = prefab.GetComponent(); + traitorTexOrig = traitorSprite.CurrentSprite.material.mainTexture as Texture2D; + + prefab.name = "TOB Traitor Lord"; + prefab.transform.SetPosition2D(35f, 22f); + UObject.DestroyImmediate(prefab.Child("Pt Mist")!); + UObject.DestroyImmediate(prefab.GetComponent()); + UObject.DestroyImmediate(prefab.GetComponent()); + prefab.AddComponent(); + prefab.AddComponent().active = false; + + #region Dream Convo + + EnemyDreamnailReaction dreamConvo = prefab.GetComponent(); + EnemyDreamnailReaction mantisDreamConvo = mantisPreload!.GetComponent(); + + ReflectionHelper.SetField( + dreamConvo, + "convoAmount", + ReflectionHelper.GetField(mantisDreamConvo, "convoAmount") + ); + dreamConvo.SetConvoTitle( + ReflectionHelper.GetField(mantisDreamConvo, "convoTitle") + ); + + #endregion + + #region Constrain Position + + ConstrainPosition constraint = prefab.AddComponent(); + constraint.constrainX = true; + constraint.xMin = 24.4f; + constraint.xMax = 36.7f; + + #endregion + + #region Hit Effect + + EnemyHitEffectsUninfected hitEffect = prefab.AddComponent(); + ReflectionHelper.SetField(prefab.GetComponent(), "hitEffectReceiver", hitEffect as IHitEffectReciever); + + EnemyHitEffectsUninfected mantisEffect = mantisPreload!.GetComponent(); + hitEffect.effectOrigin = mantisEffect.effectOrigin; + hitEffect.audioPlayerPrefab = mantisEffect.audioPlayerPrefab; + hitEffect.enemyDamage = mantisEffect.enemyDamage; + hitEffect.uninfectedHitPt = mantisEffect.uninfectedHitPt; + hitEffect.slashEffectGhost1 = mantisEffect.slashEffectGhost1; + hitEffect.slashEffectGhost2 = mantisEffect.slashEffectGhost2; + hitEffect.uninfectedHitPt = mantisEffect.uninfectedHitPt; + + #endregion + + return prefab; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InstantiateTraitor(GameObject parent) { + traitorInstance = GameObject.Instantiate(traitorPrefab)!; + traitorInstance!.transform.parent = parent.transform; + + HealthManager hm = traitorInstance.GetComponent(); + DamageHero dh = traitorInstance.GetComponent(); + NonBouncer nb = traitorInstance.GetComponent(); + PlayMakerFSM fsm = traitorInstance.LocateMyFSM("Mantis"); + + #region FSM Changes + + #region Intro + + // Sync with Mantis Lords roar + fsm.RemoveAction("Cloth?", 1); + fsm.RemoveAction("Cloth?", 0); + fsm.GetAction("Emerge Dust", 1).time = 1.6f; + fsm.RemoveAction("Emerge Dust", 2); + fsm.RemoveTransition("Intro Land", FsmEvent.Finished.Name); + fsm.AddTransition("Intro Land", "SUBS ROAR 2", "Roar"); + fsm.RemoveTransition("Roar", FsmEvent.Finished.Name); + fsm.AddTransition("Roar", "MLORD START SUB", "Roar End"); + fsm.AddCustomAction("Roar End", () => { + hm.IsInvincible = false; + dh.damageDealt = 1; + }); + + // Remove roar & title + fsm.RemoveAction("Roar", 9); + fsm.RemoveAction("Roar", 8); + fsm.RemoveAction("Roar", 7); + fsm.RemoveAction("Roar", 6); + + #endregion + + #region Death + + FsmState stateDefeated = fsm.AddState("Defeated"); + fsm.ChangeTransition("Idle", "DEATH SCENE", stateDefeated.Name); + fsm.AddAction(stateDefeated.Name, fsm.GetAction("Walk", 2)); + fsm.AddAction(stateDefeated.Name, fsm.GetAction("Idle", 1)); + fsm.AddCustomAction(stateDefeated.Name, () => { + hm.IsInvincible = true; + dh.damageDealt = 0; + nb.active = true; + traitorInstance.Child("Head Box")!.SetActive(false); + }); + + FsmState stateBowAntic = fsm.AddState("Bow Antic"); + fsm.AddTransition(stateDefeated.Name, "MANTIS DEFEAT", stateBowAntic.Name); + fsm.AddAction(stateBowAntic.Name, new Wait() { time = 1.6f, finishEvent = FsmEvent.Finished }); + + FsmState stateBow = fsm.AddState("Bow"); + fsm.AddTransition(stateBowAntic.Name, FsmEvent.Finished.Name, stateBow.Name); + fsm.AddAction(stateBow.Name, fsm.GetAction("Roar Recover", 0)); + + #endregion + + #region Change attacks + + // Fix Y + fsm.GetAction("Fall", 10).float2 = 9.6f; + fsm.GetAction("Intro Land", 0).y = 9.6f; + fsm.GetAction("DSlash", 13).float2 = 9.6f; + fsm.GetAction("Land", 0).y = 9.6f; + + // No feint + fsm.ChangeTransition("Attack Choice", "SLASH", "Attack Antic"); + + // No walk + fsm.RemoveAction("Idle", 5); + + // No back feint + fsm.RemoveAction("Too Close?", 1); + fsm.RemoveAction("Too Close?", 0); + + // Slam ignore HP + fsm.RemoveAction("Slam?", 2); + + #endregion + + #endregion + + if (!GlobalSettings.unfixBugs) { + // roar lock + fsm.RemoveAction("Roar", 3); + fsm.RemoveAction("Roar", 2); + + // Not interactable until roar finish + hm.IsInvincible = true; + dh.damageDealt = 0; + nb.active = true; + fsm.RemoveAction("Fall", 6); + fsm.AddCustomAction("Roar End", () => nb.active = false); + + // Fix slamming after death + fsm.InsertAction("Slam?", new BoolTest() { + boolVariable = fsm.FsmVariables.FindFsmBool("Zero HP"), + isTrue = FsmEvent.Finished + }, 0); + fsm.InsertAction("Repeat?", new BoolTest() { + boolVariable = fsm.FsmVariables.FindFsmBool("Zero HP"), + isTrue = FsmEvent.Finished + }, 0); + } + } +} diff --git a/TribeOfBattle/Resources/Lang/en.json b/TribeOfBattle/Resources/Lang/en.json new file mode 100644 index 0000000..53ee3c3 --- /dev/null +++ b/TribeOfBattle/Resources/Lang/en.json @@ -0,0 +1,9 @@ +{ + "Name": "Tribe of Battle", + "Desc": "Reunited gods of pride", + "Title/Super": "", + "Title/Main": "Tribe", + "Title/Sub": "Of Battle", + "Option/ModifyPantheons": "Modify Pantheons", + "Option/UnfixBugs": "Unfix Bugs" +} diff --git a/TribeOfBattle/Resources/Lang/zh.json b/TribeOfBattle/Resources/Lang/zh.json new file mode 100644 index 0000000..2606c01 --- /dev/null +++ b/TribeOfBattle/Resources/Lang/zh.json @@ -0,0 +1,9 @@ +{ + "Name": "战斗部落", + "Desc": "重聚的骄傲之神", + "Title/Super": "", + "Title/Main": "战斗部落", + "Title/Sub": "", + "Option/ModifyPantheons": "更改门内", + "Option/UnfixBugs": "还原 Bug" +} diff --git a/TribeOfBattle/SceneEdit.cs b/TribeOfBattle/SceneEdit.cs new file mode 100644 index 0000000..e078708 --- /dev/null +++ b/TribeOfBattle/SceneEdit.cs @@ -0,0 +1,53 @@ +using HealthShare; + +namespace TribeOfBattle; + +public sealed partial class TribeOfBattle { + private static void EditScene(Scene _, Scene next) { + if (Instance == null) { + goto Inactive; + } + + if (next.name != "GG_Mantis_Lords_V") { + goto Inactive; + } + + if (BossSequenceController.IsInSequence && !GlobalSettings.modifyPantheons) { + goto Inactive; + } + + if (!didGlobalChange) { + traitorSprite!.CurrentSprite.material.mainTexture = traitorTex.Value; + grassPrefab!.SetActive(false); + didGlobalChange = true; + } + + GameObject battle = next.GetRootGameObjects().First(go => go.name == "Mantis Battle"); + + InstantiateTraitor(battle); + + battle.Child("Mantis Lord Throne 2") + .LocateMyFSM("Mantis Throne Main") + .InsertCustomAction("I Stand", () => { + traitorInstance!.SetActive(true); + + new[] { 1, 2, 3 } + .Map(i => "Battle Sub/Mantis Lord S" + i) + .Map(path => battle.Child(path)!) + .Append(traitorInstance!) + .ShareHealth(name: "Tribe Of Battle").HP = + BossSceneController.Instance.BossLevel == 0 ? 3550 : 4750; + }, 4); + + return; + + Inactive: + if (didGlobalChange) { + traitorSprite!.CurrentSprite.material.mainTexture = traitorTexOrig; + grassPrefab!.SetActive(true); + didGlobalChange = false; + } + + return; + } +} diff --git a/TribeOfBattle/Settings.cs b/TribeOfBattle/Settings.cs new file mode 100644 index 0000000..cb92407 --- /dev/null +++ b/TribeOfBattle/Settings.cs @@ -0,0 +1,12 @@ +namespace TribeOfBattle; + +public sealed partial class TribeOfBattle : IGlobalSettings { + public static GlobalSettings GlobalSettings { get; private set; } = new(); + public void OnLoadGlobal(GlobalSettings s) => GlobalSettings = s; + public GlobalSettings OnSaveGlobal() => GlobalSettings; +} + +public sealed class GlobalSettings { + public bool modifyPantheons = false; + public bool unfixBugs = false; +} diff --git a/TribeOfBattle/TribeOfBattle.cs b/TribeOfBattle/TribeOfBattle.cs new file mode 100644 index 0000000..568db39 --- /dev/null +++ b/TribeOfBattle/TribeOfBattle.cs @@ -0,0 +1,53 @@ +namespace TribeOfBattle; + +public sealed partial class TribeOfBattle : Mod, ITogglableMod { + public static TribeOfBattle? Instance { get; private set; } + + private static readonly Lazy Version = new(() => Assembly + .GetExecutingAssembly() + .GetCustomAttribute() + .InformationalVersion +#if DEBUG + + "-dev" +#endif + ); + + public override string GetVersion() => Version.Value; + + public TribeOfBattle() => + USceneManager.activeSceneChanged += EditScene; + + public override void Initialize(Dictionary> preloads) { + if (Instance != null) { + LogWarn("Attempting to initialize multiple times, operation rejected"); + return; + } + + Instance = this; + SavePreloads(preloads); + HealthShare.HealthShare.GlobalSettings.modifyBosses = false; + + ModHooks.LanguageGetHook += Localize; + } + + public void Unload() { + ModHooks.LanguageGetHook -= Localize; + + Instance = null; + } + + private static string Localize(string key, string sheet, string orig) => sheet switch { + "CP3" => key switch { + "NAME_MANTIS_LORD_V" => "Name".Localize(), + "GG_S_MANTIS_LORD_V" => "Desc".Localize(), + _ => orig + }, + "Titles" => key switch { + "SISTERS_SUPER" => "Title/Super".Localize(), + "SISTERS_MAIN" => "Title/Main".Localize(), + "SISTERS_SUB" => "Title/Sub".Localize(), + _ => orig, + }, + _ => orig, + }; +} diff --git a/TribeOfBattle/TribeOfBattle.csproj b/TribeOfBattle/TribeOfBattle.csproj index c84c02e..173e4f4 100644 --- a/TribeOfBattle/TribeOfBattle.csproj +++ b/TribeOfBattle/TribeOfBattle.csproj @@ -1,7 +1,7 @@ TribeOfBattle - 0.1.0 + 1.0.0 Clazex MIT diff --git a/TribeOfBattle/Util/EnumerableUtil.cs b/TribeOfBattle/Util/EnumerableUtil.cs new file mode 100644 index 0000000..24afbd3 --- /dev/null +++ b/TribeOfBattle/Util/EnumerableUtil.cs @@ -0,0 +1,11 @@ +namespace TribeOfBattle.Util; + +internal static class EnumerableUtil { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static IEnumerable Map(this IEnumerable self, Func f) => + self.Select(f); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static IEnumerable Filter(this IEnumerable self, Func f) => + self.Where(f); +} diff --git a/TribeOfBattle/Util/L11nUtil.cs b/TribeOfBattle/Util/L11nUtil.cs new file mode 100644 index 0000000..f7efbf4 --- /dev/null +++ b/TribeOfBattle/Util/L11nUtil.cs @@ -0,0 +1,62 @@ +using System.IO; + +using Language; + +using Newtonsoft.Json; + +using Logger = Modding.Logger; + +namespace TribeOfBattle.Util; + +internal static class L11nUtil { + private const string resPrefix = "TribeOfBattle.Resources.Lang."; + private const string resPostfix = ".json"; + + private static readonly string[] langs = Lang + .GetLanguages() + .Map(str => str.ToLower().Replace('_', '-')) + .ToArray(); + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string ToIdentifier(this LanguageCode code) => + code.ToString().ToLower().Replace('_', '-'); + + private static string CurrentLang => + Lang.CurrentLanguage().ToIdentifier(); + + + internal static readonly Lazy>>> Dict = new(() => Assembly + .GetExecutingAssembly() + .GetManifestResourceNames() + .Filter(name => name.EnclosedWith(resPrefix, resPostfix)) + .Map(name => ( + lang: name.StripStart(resPrefix).StripEnd(resPostfix), + path: name + )) + .Filter(tuple => langs.Contains(tuple.lang)) + .ToDictionary( + tuple => tuple.lang, + tuple => new Lazy>(() => { + using Stream stream = Assembly + .GetExecutingAssembly() + .GetManifestResourceStream(tuple.path); + using StreamReader reader = new(stream); + + string content = reader.ReadToEnd(); + Dictionary table = + JsonConvert.DeserializeObject>(content)!; + + Logger.LogDebug($"[{nameof(TribeOfBattle)}] - Loaded localization for lang: {tuple.lang}"); + return table; + }) + ) + ); + + + internal static string Localize(this string key) { + Dict.Value.TryGetValue(CurrentLang, out Lazy>? table); + table ??= Dict.Value[LanguageCode.EN.ToIdentifier()]; + return table.Value.TryGetValue(key, out string value) ? value : key; + } +} diff --git a/TribeOfBattle/Util/MiscUtil.cs b/TribeOfBattle/Util/MiscUtil.cs new file mode 100644 index 0000000..8879e2a --- /dev/null +++ b/TribeOfBattle/Util/MiscUtil.cs @@ -0,0 +1,19 @@ +namespace TribeOfBattle.Util; + +internal static class MiscUtil { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool EnclosedWith(this string self, string start, string end) => + self.StartsWith(start) && self.EndsWith(end); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static string StripStart(this string self, string val) => + self.StartsWith(val) ? self.Substring(val.Length) : self; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static string StripEnd(this string self, string val) => + self.EndsWith(val) ? self.Substring(0, self.Length - val.Length) : self; + + + internal static GameObject? Child(this GameObject self, string name) => + self.transform.Find(name)?.gameObject; +}