diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4ff58a4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +################################################################################ +# This .gitignore file was automatically created by Microsoft(R) Visual Studio. +################################################################################ + +/.vs +ModUtil/** \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..c95a582 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,16 @@ +[submodule "RunStartControl"] + path = RunStartControl + url = https://github.com/Museus/RunStartControl.git +[submodule "sgg-mod-modutil"] + path = ModUtil + url = https://github.com/SGG-Modding/sgg-mod-modutil.git +[submodule "HellModeToggle"] + path = HellModeToggle + url = https://github.com/cgullsHadesMods/HellModeToggle +[submodule "DontGetVorimed"] + path = DontGetVorimed + url = https://github.com/cgullsHadesMods/DontGetVorimed +[submodule "hades-CharonSackControl"] + path = hades-CharonSackControl + url = https://github.com/cgullsHadesMods/hades-CharonSackControl.git + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f4f5b92 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,114 @@ + +# Change Log +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/), and will follow [Semantic Versioning](https://semver.org/). + +## [1.2.0] - 2022-05-30 + +### Added + + - Can now toggle guaranteed Charon Sack spawn for Loyalty Card + - QuickRestart now sets a flag for the Livesplit autosplitter to reset + +### Changed + - QuickRestart spawns you in the Courtyard instead of the blood pool + - QuickRestart re-equips your starting keepsake + - RtaTimer is now more efficient - should remove any lag issues that it was causing + - Default Hammer for Rama Aspect of the Bow is now Twin Shot + - Renamed sgg-mod-modutil to reduce duplicate ModUtil user installs + +### Bug fixes + - ShowChamberNumber now works with newer versions of modimporter + +## [1.1.1] - 2021-09-14 + +### Added + + - Can now toggle Hell Mode in mod settings + +### Removed + + - Starting Boon Selector was not legal on any rulesets + +### Changed + + - Clarified ruleset text + - Cleaned up menu and credits + +### Bug fixes + + - Remove Approval Process blocking on the DontGetVorimed boon + - Slight delay before QuickRestart to remove interaction bugs + +## [1.1.0] - 2021-08-01 + +### Added + + - Single-run ruleset + +### Changed + + - Quick Restart also resets RtaTimer + - Replaced FixedHammers with RunStartControl + - Asterius removed from Leaderboard ruleset + +### Bug fixes + + - Can no longer reset during the pause before Thanatos Encounter spawns to prevent broken GameState + + + +## [1.0.0] - 2021-06-04 + +No major bugs were found with v1.0.0-rc.2 so we are moving forward with leaderboard creation! + +## [1.0.0-rc.2] - 2021-05-30 + +### Changed + +- Quick Restart (insta-kill) can only be activated if + - Zagreus is not in the house + - Zagreus is not frozen (cutscenes, boon pickups, room endings) + - Combat UI is visible + + This is to prevent a variety of bugs, from duplicated boons to crashes. [#39](https://github.com/ellomenop/HadesSpeedrunningModPack/pull/39) + +- BoonSelector now has all filters enabled by default, and the rarity filter will be disabled automatically if "DontGetVorimed" is enabled. [#45](https://github.com/ellomenop/HadesSpeedrunningModPack/pull/45) + +- EmoteMod has been disabled due to an unknown issue with MacOS [#48](https://github.com/ellomenop/HadesSpeedrunningModPack/pull/48) + +### Fixed + +- MinibossControl now forces the correct amount of miniboss doors in Asphodel. [#38](https://github.com/ellomenop/HadesSpeedrunningModPack/pull/38) + +- MinibossControl now enforces changes on save reload. [#38](https://github.com/ellomenop/HadesSpeedrunningModPack/pull/38) + +## [1.0.0-rc.1] - 2021-05-27 + +Initial "Beta" release of the modpack. This includes: + +- Colorblind +- DontGetVorimed +- DoorVisualIndicators +- StartingBoonSelector +- EmoteMod +- ErumiUILib +- FixedHammers +- HadesSpeedrunningModpack +- InteractableChaos +- LootChoiceExt +- MinibossControl +- ModConfigMenu +- ModUtil +- ModdedWarning +- PrintUtil +- QuickRestart +- RemoveCutscenes +- RoomDeterminism +- RTATimer +- SatyrSackControl +- ShowChamberNumber +- ThanatosControl + +as well as a ruleset preset for "Multiweapon Leaderboard" runs. diff --git a/ChaosControl/ChaosControl.lua b/ChaosControl/ChaosControl.lua new file mode 100644 index 0000000..e87589c --- /dev/null +++ b/ChaosControl/ChaosControl.lua @@ -0,0 +1,118 @@ +--[[ + ChaosControl + Authors: + SleepSoul (Discord: SleepSoul#6006) + Museus (Discord: Museus#7777) + Dependencies: ModUtil, RCLib + Change the eligible offerings from Chaos, allowing certain curses or blessings to be removed. +]] +ModUtil.Mod.Register("ChaosControl") + +local config = { + ChaosSetting = "Vanilla" +} +ChaosControl.config = config --TODO add config option in menu +ChaosControl.EligibleBlessings = {} +ChaosControl.EligibleCurses = {} +ChaosControl.VanillaBlessings = {} +ChaosControl.VanillaCurses = {} + +ChaosControl.Presets = { --Define rulesets + Vanilla = {}, + Hypermodded = { + Blessings = { + Eclipse = false, + }, + Curses = { + Roiling = false, + }, + }, + Debug = { + Blessings = { + Shot = true, + }, + Curses = { + Abyssal = true, + }, + } +} + +ChaosControl.InheritVanilla = { -- Presets set to true will inherit the vanilla traits and only remove those set to false, Presets set to false will start from 0 and only add those set to true. + Hypermodded = { + Blessings = true, + Curses = true, + }, +} + +function ChaosControl.ReadPreset() -- Read current preset and create table of blessings and curses marked to eligible, including failsafe for presets with <3 traits + local Preset = { + Blessings = {}, + Curses = {}, + } + local InheritVanilla = {} + if ChaosControl.Presets[config.ChaosSetting] ~= nil then + Preset = ChaosControl.Presets[config.ChaosSetting] + end + if ChaosControl.InheritVanilla[config.ChaosSetting] ~= nil then + InheritVanilla = ChaosControl.InheritVanilla[config.ChaosSetting] + end + if Preset.Blessings ~= nil then + if InheritVanilla.Blessings then + RCLib.PopulateMinLength( + ChaosControl.EligibleBlessings, + RCLib.RemoveIneligibleStrings(Preset.Blessings,ChaosControl.VanillaBlessings,RCLib.NameToCode.ChaosBlessings), + 3 + ) + else + RCLib.PopulateMinLength( + ChaosControl.EligibleBlessings, + RCLib.GetEligible(Preset.Blessings,RCLib.NameToCode.ChaosBlessings), + 3 + ) + end + else + ChaosControl.EligibleBlessings = ChaosControl.VanillaBlessings + end + if Preset.Curses ~= nil then + if InheritVanilla.Curses then + RCLib.PopulateMinLength( + ChaosControl.EligibleCurses, + RCLib.RemoveIneligibleStrings(Preset.Curses,ChaosControl.VanillaCurses,RCLib.NameToCode.ChaosCurses), + 3 + ) + else + RCLib.PopulateMinLength( + ChaosControl.EligibleCurses, + RCLib.GetEligible(Preset.Curses,RCLib.NameToCode.ChaosCurses), + 3 + ) + end + else + ChaosControl.EligibleCurses = ChaosControl.VanillaCurses + end +end + +function ChaosControl.UpdateOfferings() --Inject eligible blessings/curses into table of Chaos' offerings in LootData + DebugPrint({Text = "Chaos preset: "..ChaosControl.config.ChaosSetting}) + ModUtil.Table.MergeKeyed(LootData, { + TrialUpgrade = { + PermanentTraits = ChaosControl.EligibleBlessings, + TemporaryTraits = ChaosControl.EligibleCurses, + } + }) + DebugPrint({Text = "Updated Chaos offerings"}) +end + +ModUtil.LoadOnce( function() + ChaosControl.VanillaBlessings = ModUtil.Table.Copy(LootData.TrialUpgrade.PermanentTraits) + ChaosControl.VanillaCurses = ModUtil.Table.Copy(LootData.TrialUpgrade.TemporaryTraits) + ChaosControl.ReadPreset() + ChaosControl.UpdateOfferings() +end) + +-- When a new run is started, make sure to apply the offering settings +ModUtil.Path.Wrap("StartNewRun", function ( baseFunc, currentRun ) + ChaosControl.ReadPreset() + ChaosControl.UpdateOfferings() + return baseFunc(currentRun) +end, ChaosControl) diff --git a/ChaosControl/modfile.txt b/ChaosControl/modfile.txt new file mode 100644 index 0000000..2e359a8 --- /dev/null +++ b/ChaosControl/modfile.txt @@ -0,0 +1,2 @@ +:: Miniboss Control +Import "ChaosControl.lua" diff --git a/Colorblind/colorblind.lua b/Colorblind/colorblind.lua index 8e0798d..5c31f33 100644 --- a/Colorblind/colorblind.lua +++ b/Colorblind/colorblind.lua @@ -1,4 +1,4 @@ -ModUtil.RegisterMod("ColorblindMod") +ModUtil.Mod.Register("ColorblindMod") local config = { @@ -98,7 +98,7 @@ function IsOutlineLegal(enemy) return true end -ModUtil.WrapBaseFunction("CreateLevelDisplay", function ( baseFunc, newEnemy, currentRun ) +ModUtil.Path.Wrap("CreateLevelDisplay", function ( baseFunc, newEnemy, currentRun ) local biome = CurrentRun.CurrentRoom.RoomSetName if ColorblindMod.config[(biome or "nil") .. "Enabled"] and IsOutlineLegal(newEnemy) then @@ -109,12 +109,15 @@ ModUtil.WrapBaseFunction("CreateLevelDisplay", function ( baseFunc, newEnemy, cu baseFunc(newEnemy, currentRun) end, ColorblindMod) -ModUtil.BaseOverride("DoEnemyHealthBufferDeplete", function ( enemy ) +ModUtil.Path.Override("DoEnemyHealthBufferDeplete", function ( enemy ) if enemy.OnHealthBufferDepleteFunctionName ~= nil then _G[enemy.OnHealthBufferDepleteFunctionName]( enemy ) end -- Mod start - -- RemoveOutline({ Id = enemy.ObjectId }) + local biome = CurrentRun.CurrentRoom.RoomSetName + if not ColorblindMod.config[(biome or "nil") .. "Enabled"] then + RemoveOutline({ Id = enemy.ObjectId }) + end -- Mod end if enemy.TetherIds ~= nil then for k, tetherId in ipairs( enemy.TetherIds ) do @@ -128,7 +131,7 @@ ModUtil.BaseOverride("DoEnemyHealthBufferDeplete", function ( enemy ) end, ColorblindMod) OnAnyLoad{function () - local biome = ModUtil.SafeGet(_G, ModUtil.PathArray("CurrentRun.CurrentRoom.RoomSetName")) + local biome = ModUtil.IndexArray.Get(_G, ModUtil.Path.IndexArray("CurrentRun.CurrentRoom.RoomSetName")) while ColorblindMod.config[(biome or "nil") .. "Enabled"] do local ammoIds = GetIdsByType({ Name = "AmmoPack" }) if ammoIds ~= nil then @@ -142,52 +145,52 @@ OnAnyLoad{function () end end} -ModUtil.WrapBaseFunction( "HarpyKillPresentation", function(baseFunc, unit, args) +ModUtil.Path.Wrap( "HarpyKillPresentation", function(baseFunc, unit, args) RemoveOutline({ Id = unit.ObjectId}) return baseFunc(unit, args) end) -ModUtil.WrapBaseFunction( "HydraKillPresentation", function(baseFunc, unit, args) +ModUtil.Path.Wrap( "HydraKillPresentation", function(baseFunc, unit, args) RemoveOutline({ Id = unit.ObjectId}) return baseFunc(unit, args) end) -ModUtil.WrapBaseFunction( "HydraKillPresentation", function(baseFunc, unit, args) +ModUtil.Path.Wrap( "HydraKillPresentation", function(baseFunc, unit, args) RemoveOutline({ Id = unit.ObjectId}) return baseFunc(unit, args) end) -ModUtil.WrapBaseFunction( "TheseusMinotaurKillPresentation", function(baseFunc, unit, args) +ModUtil.Path.Wrap( "TheseusMinotaurKillPresentation", function(baseFunc, unit, args) RemoveOutline({ Id = unit.ObjectId}) return baseFunc(unit, args) end) -ModUtil.WrapBaseFunction( "CrawlerMiniBossKillPresentation", function(baseFunc, unit, args) +ModUtil.Path.Wrap( "CrawlerMiniBossKillPresentation", function(baseFunc, unit, args) RemoveOutline({ Id = unit.ObjectId}) return baseFunc(unit, args) end) -ModUtil.WrapBaseFunction( "HadesKillPresentation", function(baseFunc, unit, args) +ModUtil.Path.Wrap( "HadesKillPresentation", function(baseFunc, unit, args) RemoveOutline({ Id = unit.ObjectId}) return baseFunc(unit, args) end) -ModUtil.WrapBaseFunction( "KillPresentation", function(baseFunc, victim, args) +ModUtil.Path.Wrap( "KillPresentation", function(baseFunc, victim, args) RemoveOutline({ Id = victim.ObjectId}) return baseFunc(victim, args) end) -ModUtil.WrapBaseFunction( "DeathPresentation", function(baseFunc, currentRun, killer, killingUnitWeapon ) +ModUtil.Path.Wrap( "DeathPresentation", function(baseFunc, currentRun, killer, killingUnitWeapon ) RemoveOutline({ Id = currentRun.Hero.ObjectId}) return baseFunc(currentRun, killer, killingUnitWeapon ) end) -ModUtil.WrapBaseFunction( "BoatToDeathAreaTransition", function(baseFunc, unit, args) +ModUtil.Path.Wrap( "BoatToDeathAreaTransition", function(baseFunc, unit, args) RemoveOutline({ Id = CurrentRun.Hero.ObjectId}) return baseFunc(unit, args) end) -ModUtil.WrapBaseFunction( "SurfaceDeathPresentation", function(baseFunc, unit, args) +ModUtil.Path.Wrap( "SurfaceDeathPresentation", function(baseFunc, unit, args) RemoveOutline({ Id = CurrentRun.Hero.ObjectId}) return baseFunc(unit, args) end) diff --git a/DontGetVorimed b/DontGetVorimed new file mode 160000 index 0000000..a3e9d1c --- /dev/null +++ b/DontGetVorimed @@ -0,0 +1 @@ +Subproject commit a3e9d1c5fafbaf1f681809321baa2deec190457b diff --git a/DontGetVorimed/DontGetVorimed.lua b/DontGetVorimed/DontGetVorimed.lua deleted file mode 100644 index 648b25a..0000000 --- a/DontGetVorimed/DontGetVorimed.lua +++ /dev/null @@ -1,39 +0,0 @@ ---[[ - DontGetVorimed v1.0 - Authors: - ellomenop (Discord: ellomenop#2254) - Makes the first boon reward offer all 4 core boons -]] -ModUtil.RegisterMod("DontGetVorimed") - -local config = { - ModName = "Dont Get Vorimed", - Enabled = true -} -DontGetVorimed.config = config - --- When the first room is created, set the number of loot choices to 4 --- This will persist until the first boon reward is received or rolled over -ModUtil.WrapBaseFunction("ChooseStartingRoom", function ( baseFunc, currentRun, roomSetName ) - if config.Enabled then - LootChoiceExt.Choices = 4 - end - return baseFunc(currentRun, roomSetName) -end, DontGetVorimed) - --- After first boon reward has been selected, return to normal number of choices -ModUtil.WrapBaseFunction("HandleUpgradeChoiceSelection", function ( baseFunc, screen, button ) - if config.Enabled and button.Data.God ~= nil then - LootChoiceExt.Choices = 3 - end - baseFunc(screen, button) -end, DontGetVorimed) - --- If the player ever rerolls, reduce to 3 options -ModUtil.WrapBaseFunction("DestroyBoonLootButtons", function ( baseFunc, lootData ) - baseFunc(lootData) - if config.Enabled and lootData.GodLoot then - LootChoiceExt.Choices = 3 - LootChoiceExt.LastLootChoices = 3 - end -end, DontGetVorimed) diff --git a/DontGetVorimed/modfile.txt b/DontGetVorimed/modfile.txt deleted file mode 100644 index dc14973..0000000 --- a/DontGetVorimed/modfile.txt +++ /dev/null @@ -1,6 +0,0 @@ --: - DontGetVorimed v1.0 - Authors: - ellomenop (Discord: ellomenop#2254) -:- -Import DontGetVorimed.lua diff --git a/DoorVisualIndicators/DoorVisualIndicators.lua b/DoorVisualIndicators/DoorVisualIndicators.lua index 021699a..4a49a5b 100644 --- a/DoorVisualIndicators/DoorVisualIndicators.lua +++ b/DoorVisualIndicators/DoorVisualIndicators.lua @@ -1,4 +1,4 @@ -ModUtil.RegisterMod("DoorVisualIndicators") +ModUtil.Mod.Register("DoorVisualIndicators") local config = { ModName = "Door Visual Indicators", @@ -42,10 +42,10 @@ DoorVisualIndicators.MiniBossAnimations = { D_MiniBoss04 = {"EnemyStyxThiefIdle"}, } -ModUtil.WrapBaseFunction("CreateDoorPreviewIcon", function ( baseFunc, exitDoor, args ) +ModUtil.Path.Wrap("CreateDoorPreviewIcon", function ( baseFunc, exitDoor, args ) baseFunc(exitDoor, args) - local room_name = ModUtil.PathGet('RoomData.Name', args) or "" + local room_name = ModUtil.Path.Get('RoomData.Name', args) or "" local is_miniboss = RoomData[room_name].IsMiniBossRoom or false -- If the room is a fountain, add a visual effect to the door icon diff --git a/EllosStartingBoonSelectorMod/Scripts/EllosBoonSelectorMod.lua b/EllosStartingBoonSelectorMod/Scripts/EllosBoonSelectorMod.lua deleted file mode 100644 index e406f79..0000000 --- a/EllosStartingBoonSelectorMod/Scripts/EllosBoonSelectorMod.lua +++ /dev/null @@ -1,941 +0,0 @@ -ModUtil.RegisterMod("EllosBoonSelectorMod") - -local config = { - ShowPreview = true -} -EllosBoonSelectorMod.config = config - -EllosBoonSelectorMod.RarityFilter = {} -EllosBoonSelectorMod.GodFilter = "" -EllosBoonSelectorMod.CurrentHammerIndex = 1 - -EllosBoonSelectorMod.RarityColors = {Color.BoonPatchCommon, Color.BoonPatchRare, Color.BoonPatchEpic, Color.BoonPatchLegendary} - -EllosBoonSelectorMod.BoonGods = {"Aphrodite", "Ares", "Artemis", "Athena", "Demeter", "Dionysus", "Poseidon", "Zeus"} -EllosBoonSelectorMod.PriorityBoons = {"WeaponTrait","SecondaryTrait","RangedTrait","RushTrait"} -EllosBoonSelectorMod.PriorityBoonsOrder = {WeaponTrait = 1, SecondaryTrait = 2, RangedTrait = 3, RushTrait = 4} -EllosBoonSelectorMod.PriorityBoonCannonicalNameToCodeName = { - Attack = "WeaponTrait", - Special = "SecondaryTrait", - Dash = "RushTrait", - Cast = "RangedTrait" -} - -EllosBoonSelectorMod.HammerOptions = { - SwordWeapon1 = {"SwordTwoComboTrait","SwordSecondaryAreaDamageTrait","SwordGoldDamageTrait","SwordBlinkTrait","SwordThrustWaveTrait","SwordHealthBufferDamageTrait","SwordSecondaryDoubleAttackTrait","SwordCriticalTrait","SwordBackstabTrait","SwordDoubleDashAttackTrait","SwordHeavySecondStrikeTrait","SwordCursedLifeStealTrait"}, - SwordWeapon2 = {"SwordTwoComboTrait","SwordSecondaryAreaDamageTrait","SwordGoldDamageTrait","SwordBlinkTrait","SwordThrustWaveTrait","SwordHealthBufferDamageTrait","SwordSecondaryDoubleAttackTrait","SwordCriticalTrait","SwordBackstabTrait","SwordDoubleDashAttackTrait","SwordHeavySecondStrikeTrait","SwordCursedLifeStealTrait"}, - SwordWeapon3 = {"SwordTwoComboTrait","SwordSecondaryAreaDamageTrait","SwordGoldDamageTrait","SwordBlinkTrait","SwordThrustWaveTrait","SwordHealthBufferDamageTrait","SwordSecondaryDoubleAttackTrait","SwordCriticalTrait","SwordBackstabTrait","SwordDoubleDashAttackTrait","SwordHeavySecondStrikeTrait","SwordCursedLifeStealTrait"}, - SwordWeapon4 = {"SwordConsecrationBoostTrait","SwordSecondaryAreaDamageTrait","SwordGoldDamageTrait","SwordBlinkTrait","SwordThrustWaveTrait","SwordHealthBufferDamageTrait","SwordSecondaryDoubleAttackTrait","SwordBackstabTrait","SwordDoubleDashAttackTrait","SwordCursedLifeStealTrait"}, - SpearWeapon1 = {"SpearReachAttack", "SpearAutoAttack", "SpearThrowExplode", "SpearThrowBounce", "SpearThrowPenetrate", "SpearThrowCritical", "SpearSpinDamageRadius", "SpearSpinChargeLevelTime", "SpearDashMultiStrike", "SpearThrowElectiveCharge", "SpearSpinChargeAreaDamageTrait", "SpearAttackPhalanxTrait"}, - SpearWeapon2 = {"SpearReachAttack", "SpearAutoAttack", "SpearThrowPenetrate", "SpearThrowCritical", "SpearSpinDamageRadius", "SpearSpinChargeLevelTime", "SpearDashMultiStrike", "SpearSpinChargeAreaDamageTrait", "SpearAttackPhalanxTrait"}, - SpearWeapon3 = {"SpearReachAttack", "SpearThrowExplode", "SpearThrowBounce", "SpearThrowPenetrate", "SpearThrowCritical", "SpearSpinDamageRadius", "SpearSpinChargeLevelTime", "SpearDashMultiStrike", "SpearThrowElectiveCharge", "SpearSpinChargeAreaDamageTrait", "SpearAttackPhalanxTrait"}, - SpearWeapon4 = {"SpearSpinTravelDurationTrait","SpearReachAttack", "SpearThrowPenetrate", "SpearSpinDamageRadius", "SpearSpinChargeLevelTime", "SpearDashMultiStrike", "SpearThrowElectiveCharge", "SpearSpinChargeAreaDamageTrait", "SpearAttackPhalanxTrait"}, - ShieldWeapon1 = {"ShieldDashAOETrait", "ShieldRushProjectileTrait", "ShieldThrowFastTrait", "ShieldThrowCatchExplode", "ShieldChargeHealthBufferTrait", "ShieldChargeSpeedTrait", "ShieldBashDamageTrait", "ShieldPerfectRushTrait", "ShieldThrowElectiveCharge", "ShieldThrowEmpowerTrait", "ShieldBlockEmpowerTrait", "ShieldThrowRushTrait"}, - ShieldWeapon2 = {"ShieldDashAOETrait", "ShieldRushProjectileTrait", "ShieldThrowFastTrait", "ShieldThrowCatchExplode", "ShieldChargeHealthBufferTrait", "ShieldChargeSpeedTrait", "ShieldBashDamageTrait", "ShieldPerfectRushTrait", "ShieldThrowEmpowerTrait", "ShieldBlockEmpowerTrait", "ShieldThrowRushTrait"}, - ShieldWeapon3 = {"ShieldDashAOETrait", "ShieldRushProjectileTrait", "ShieldThrowCatchExplode", "ShieldChargeHealthBufferTrait", "ShieldChargeSpeedTrait", "ShieldBashDamageTrait", "ShieldPerfectRushTrait", "ShieldThrowEmpowerTrait", "ShieldBlockEmpowerTrait"}, - ShieldWeapon4 = {"ShieldLoadAmmoBoostTrait", "ShieldDashAOETrait", "ShieldRushProjectileTrait", "ShieldThrowFastTrait", "ShieldThrowCatchExplode", "ShieldChargeHealthBufferTrait", "ShieldChargeSpeedTrait", "ShieldBashDamageTrait", "ShieldPerfectRushTrait", "ShieldThrowElectiveCharge", "ShieldThrowEmpowerTrait", "ShieldBlockEmpowerTrait", "ShieldThrowRushTrait"}, - BowWeapon1 = {"BowDoubleShotTrait", "BowLongRangeDamageTrait", "BowSlowChargeDamageTrait", "BowTapFireTrait", "BowPenetrationTrait", "BowPowerShotTrait", "BowSecondaryBarrageTrait", "BowTripleShotTrait", "BowSecondaryFocusedFireTrait", "BowChainShotTrait", "BowCloseAttackTrait", "BowConsecutiveBarrageTrait"}, - BowWeapon2 = {"BowDoubleShotTrait", "BowLongRangeDamageTrait", "BowSlowChargeDamageTrait", "BowTapFireTrait", "BowPenetrationTrait", "BowPowerShotTrait", "BowSecondaryBarrageTrait", "BowTripleShotTrait", "BowChainShotTrait", "BowCloseAttackTrait", "BowConsecutiveBarrageTrait"}, - BowWeapon3 = {"BowDoubleShotTrait", "BowLongRangeDamageTrait", "BowSlowChargeDamageTrait", "BowTapFireTrait", "BowPenetrationTrait", "BowPowerShotTrait", "BowSecondaryBarrageTrait", "BowTripleShotTrait", "BowSecondaryFocusedFireTrait", "BowChainShotTrait", "BowCloseAttackTrait", "BowConsecutiveBarrageTrait"}, - BowWeapon4 = {"BowBondBoostTrait", "BowDoubleShotTrait", "BowLongRangeDamageTrait", "BowSlowChargeDamageTrait", "BowPowerShotTrait", "BowSecondaryBarrageTrait", "BowTripleShotTrait", "BowChainShotTrait", "BowCloseAttackTrait"}, - FistWeapon1 = {"FistReachAttackTrait", "FistDashAttackHealthBufferTrait", "FistTeleportSpecialTrait", "FistDoubleDashSpecialTrait", "FistChargeSpecialTrait", "FistKillTrait", "FistSpecialLandTrait", "FistAttackFinisherTrait", "FistConsecutiveAttackTrait", "FistSpecialFireballTrait", "FistAttackDefenseTrait", "FistHeavyAttackTrait"}, - FistWeapon2 = {"FistReachAttackTrait", "FistDashAttackHealthBufferTrait", "FistKillTrait", "FistSpecialLandTrait", "FistAttackFinisherTrait", "FistConsecutiveAttackTrait", "FistAttackDefenseTrait", "FistHeavyAttackTrait", "FistDoubleDashSpecialTrait"}, - FistWeapon3 = {"FistReachAttackTrait", "FistDashAttackHealthBufferTrait", "FistTeleportSpecialTrait", "FistDoubleDashSpecialTrait", "FistChargeSpecialTrait", "FistKillTrait", "FistAttackFinisherTrait", "FistConsecutiveAttackTrait", "FistSpecialFireballTrait", "FistAttackDefenseTrait", "FistHeavyAttackTrait"}, - FistWeapon4 = {"FistDetonateBoostTrait", "FistSpecialLandTrait", "FistChargeSpecialTrait", "FistConsecutiveAttackTrait", "FistDashAttackHealthBufferTrait", "FistAttackDefenseTrait", "FistTeleportSpecialTrait", "FistDoubleDashSpecialTrait", "FistKillTrait"}, - GunWeapon1 = {"GunSlowGrenade", "GunMinigunTrait", "GunShotgunTrait", "GunExplodingSecondaryTrait", "GunGrenadeFastTrait", "GunArmorPenerationTrait", "GunInfiniteAmmoTrait", "GunGrenadeClusterTrait", "GunGrenadeDropTrait", "GunHeavyBulletTrait", "GunChainShotTrait", "GunHomingBulletTrait"}, - GunWeapon2 = {"GunSlowGrenade", "GunMinigunTrait", "GunShotgunTrait", "GunExplodingSecondaryTrait", "GunGrenadeFastTrait", "GunArmorPenerationTrait", "GunInfiniteAmmoTrait", "GunGrenadeClusterTrait", "GunGrenadeDropTrait", "GunHeavyBulletTrait", "GunChainShotTrait", "GunHomingBulletTrait"}, - GunWeapon3 = {"GunSlowGrenade", "GunMinigunTrait", "GunShotgunTrait", "GunExplodingSecondaryTrait", "GunGrenadeFastTrait", "GunArmorPenerationTrait", "GunInfiniteAmmoTrait", "GunGrenadeClusterTrait", "GunGrenadeDropTrait", "GunHeavyBulletTrait", "GunChainShotTrait", "GunHomingBulletTrait"}, - GunWeapon4 = {"GunLoadedGrenadeBoostTrait", "GunLoadedGrenadeLaserTrait", "GunLoadedGrenadeSpeedTrait", "GunLoadedGrenadeWideTrait", "GunLoadedGrenadeInfiniteAmmoTrait", "GunSlowGrenade", "GunGrenadeFastTrait", "GunArmorPenerationTrait"}, -} - ---[[ -Modified version of SeedControlScreen.lua -]] -ScreenData.SeedControl = -{ - ItemStartX = 1400, - ItemStartY = 560, - ItemSpacing = 100, - EntryYSpacer = 50, - ItemsPerPage = 12, - ScrollOffset = 0, - Digits = { 1, 2, 3, 4, 5, 6 }, -} - -function OpenRngSeedSelectorScreen(screen, button) - CloseAdvancedTooltipScreen() - UseSeedController(CurrentRun.Hero) -end - -function UseSeedController( usee, args ) - PlayInteractAnimation( usee.ObjectId ) - UseableOff({ Id = usee.ObjectId }) - StopStatusAnimation( usee ) - local screen = OpenSeedControlScreen() - UseableOn({ Id = usee.ObjectId }) -end - -function OpenSeedControlScreen( args ) - - local screen = DeepCopyTable( ScreenData.SeedControl ) - screen.Components = {} - local components = screen.Components - screen.CloseAnimation = "QuestLogBackground_Out" - - OnScreenOpened({ Flag = screen.Name, PersistCombatUI = true }) - FreezePlayerUnit() - EnableShopGamepadCursor() - SetConfigOption({ Name = "FreeFormSelectWrapY", Value = false }) - SetConfigOption({ Name = "FreeFormSelectStepDistance", Value = 8 }) - SetConfigOption({ Name = "FreeFormSelectSuccessDistanceStep", Value = 8 }) - - components.ShopBackgroundDim = CreateScreenComponent({ Name = "rectangle01", Group = "Combat_Menu" }) - components.ShopBackgroundSplatter = CreateScreenComponent({ Name = "LevelUpBackground", Group = "Combat_Menu" }) - components.ShopBackground = CreateScreenComponent({ Name = "rectangle01", Group = "Combat_Menu" }) - - SetAnimation({ DestinationId = components.ShopBackground.Id, Name = "QuestLogBackground_In", OffsetY = 30 }) - - SetScale({ Id = components.ShopBackgroundDim.Id, Fraction = 4 }) - SetColor({ Id = components.ShopBackgroundDim.Id, Color = {0.090, 0.055, 0.157, 0.8} }) - - PlaySound({ Name = "/SFX/Menu Sounds/FatedListOpen" }) - - wait(0.2) - - -- Title - CreateTextBox({ Id = components.ShopBackground.Id, Text = "Weave a New Thread?", FontSize = 34, OffsetX = 0, OffsetY = -460, Color = Color.White, Font = "SpectralSCLightTitling", ShadowBlur = 0, ShadowColor = {0,0,0,1}, ShadowOffset={0, 2}, Justification = "Center" }) - CreateTextBox({ Id = components.ShopBackground.Id, Text = "Beware, the Fates Watch On with a Single Judging Eye, your Thread in Hand", FontSize = 15, OffsetX = 0, OffsetY = -410, Width = 840, Color = {120, 120, 120, 255}, Font = "CrimsonTextItalic", ShadowBlur = 0, ShadowColor = {0,0,0,0}, ShadowOffset={0, 2}, Justification = "Center" }) - - -- Description Box - --components.DescriptionBox = CreateScreenComponent({ Name = "BlankObstacle", X = 815, Y = 290, Group = "Combat_Menu" }) - - local itemLocationX = screen.ItemStartX - local itemLocationY = screen.ItemStartY - - SeedControlScreenSyncDigits( screen ) - - for digit, digitValue in ipairs( screen.Digits ) do - components["DigitUp"..digit] = CreateScreenComponent({ Name = "ButtonCodexUp", X = itemLocationX, Y = itemLocationY - 100, Scale = 1.0, Sound = "/SFX/Menu Sounds/GeneralWhooshMENU", Group = "Combat_Menu" }) - components["DigitUp"..digit].OnPressedFunctionName = "SeedDigitDown" - components["DigitUp"..digit].ControlHotkey = "MenuLeft" - components["DigitUp"..digit].Digit = digit - - local digitKey = "DigitButton"..digit - components[digitKey] = CreateScreenComponent({ Name = "BlankObstacle", Scale = 1, X = itemLocationX, Y = itemLocationY, Group = "Combat_Menu" }) - components[digitKey].Digit = digit - AttachLua({ Id = components[digitKey].Id, Table = components[digitKey] }) - - CreateTextBox({ Id = components[digitKey].Id, - Text = digitValue, - Color = {245, 200, 47, 255}, - FontSize = 48, - OffsetX = 0, OffsetY = 0, - Font = "AlegreyaSansSCBold", - OutlineThickness = 0, - OutlineColor = {255, 205, 52, 255}, - ShadowBlur = 0, ShadowColor = {0,0,0,0}, ShadowOffset={0, 2}, - Justification = "Center", - DataProperties = - { - OpacityWithOwner = true, - }, - }) - - components["DigitDown"..digit] = CreateScreenComponent({ Name = "ButtonCodexDown", X = itemLocationX, Y = itemLocationY + 100, Scale = 1.0, Sound = "/SFX/Menu Sounds/GeneralWhooshMENU", Group = "Combat_Menu" }) - components["DigitDown"..digit].OnPressedFunctionName = "SeedDigitUp" - components["DigitDown"..digit].ControlHotkey = "MenuRight" - components["DigitDown"..digit].Digit = digit - - itemLocationX = itemLocationX - screen.ItemSpacing - end - - UpdateDigitDisplay( screen ) - - -- Randomize button - components.RandomizeButton = CreateScreenComponent({ Name = "ButtonDefault", Scale = 1.0, Group = "Combat_Menu", X = 1150, Y = 760 }) - components.RandomizeButton.OnPressedFunctionName = "SeedControlScreenRandomize" - CreateTextBox({ Id = components.RandomizeButton.Id, - Text = "Randomize Seed", - OffsetX = 0, OffsetY = 0, - FontSize = 22, - Color = Color.White, - Font = "AlegreyaSansSCRegular", - ShadowBlur = 0, ShadowColor = {0,0,0,1}, ShadowOffset={0, 2}, - Justification = "Center", - DataProperties = - { - OpacityWithOwner = true, - }, - }) - - -- Roll seed for filters button - components.ClearFiltersButton = CreateScreenComponent({ Name = "ButtonDefault", Scale = 1.0, Group = "Combat_Menu", X = 320, Y = 760 }) - components.ClearFiltersButton.OnPressedFunctionName = "ClearFilters" - CreateTextBox({ Id = components.ClearFiltersButton.Id, - Text = "Clear Filters", - OffsetX = 0, OffsetY = 0, - FontSize = 22, - Color = Color.White, - Font = "AlegreyaSansSCRegular", - ShadowBlur = 0, ShadowColor = {0,0,0,1}, ShadowOffset={0, 2}, - Justification = "Center", - DataProperties = - { - OpacityWithOwner = true, - }, - }) - - -- Roll seed for filters button - components.RollSeedForFiltersButton = CreateScreenComponent({ Name = "ButtonDefault", Scale = 1.0, Group = "Combat_Menu", X = 625, Y = 760 }) - components.RollSeedForFiltersButton.OnPressedFunctionName = "RollSeedForFilters" - CreateTextBox({ Id = components.RollSeedForFiltersButton.Id, - Text = "Roll Seed For Filters", - OffsetX = 0, OffsetY = 0, - FontSize = 22, - Color = Color.White, - Font = "AlegreyaSansSCRegular", - ShadowBlur = 0, ShadowColor = {0,0,0,1}, ShadowOffset={0, 2}, - Justification = "Center", - DataProperties = - { - OpacityWithOwner = true, - }, - }) - - components.FiltersTitle = CreateScreenComponent({ Name = "BlankObstacle", Scale = 1.0, Group = "Combat_Menu", X = 475, Y = 520 }) - CreateTextBox({ Id = components.FiltersTitle.Id, - Text = "Filter for a Specific God and Boon", - OffsetX = 0, OffsetY = -100, - FontSize = 28, - Color = Color.White, - Font = "AlegreyaSansSCRegular", - ShadowBlur = 0, ShadowColor = {0,0,0,1}, ShadowOffset={0, 2}, - Justification = "Center", - DataProperties = - { - OpacityWithOwner = true, - }, - }) - - -- God filter buttons - local x = -250 - local y = 30 - for _, god in pairs(EllosBoonSelectorMod.BoonGods) do - components[god .. "Filter"] = CreateScreenComponent({ Name = "ButtonDefault", Scale = 0.5, Group = "Combat_Menu", X = 500 + x, Y = 450 + y }) - components[god .. "Filter"].OnPressedFunctionName = "ToggleGodFilter" - components[god .. "Filter"].GodName = god - CreateTextBox({ Id = components[god .. "Filter"].Id, - Text = god, - OffsetX = 0, OffsetY = 0, - FontSize = 16, - Color = Color.BoonPatchCommon, - Font = "AlegreyaSansSCRegular", - ShadowBlur = 0, ShadowColor = {0,0,0,1}, ShadowOffset={0, 2}, - Justification = "Center", - DataProperties = - { - OpacityWithOwner = true, - }, - }) - - x = x + 150 - if x > 250 then - x = -250 - y = y + 50 - end - end - - -- Rarity filter buttons - x = -250 - y = 150 - for _, priorityBoon in pairs({"Attack", "Special", "Dash", "Cast"}) do - components[priorityBoon .. "Filter"] = CreateScreenComponent({ Name = "ButtonDefault", Scale = 0.5, Group = "Combat_Menu", X = 500 + x, Y = 450 + y }) - components[priorityBoon .. "Filter"].OnPressedFunctionName = "CycleRarityFilter" - components[priorityBoon .. "Filter"].PriorityBoon = EllosBoonSelectorMod.PriorityBoonCannonicalNameToCodeName[priorityBoon] - CreateTextBox({ Id = components[priorityBoon .. "Filter"].Id, - Text = priorityBoon, - OffsetX = 0, OffsetY = 0, - FontSize = 16, - Color = Color.White, - Font = "AlegreyaSansSCRegular", - ShadowBlur = 0, ShadowColor = {0,0,0,1}, ShadowOffset={0, 2}, - Justification = "Center", - DataProperties = - { - OpacityWithOwner = true, - }, - }) - x = x + 150 - if x > 250 then - x = -250 - y = y + 60 - end - end - - -- Hammer filter button - x = 480 - y = 675 - - components["HammerFilterLeft"] = CreateScreenComponent({ Name = "ButtonCodexDown", X = x - 200, Y = y, Scale = 1.0, Group = "Combat_Menu" }) - SetAngle({ Id = components["HammerFilterLeft"].Id, Angle = -90 }) - SetScale({ Id = components["HammerFilterLeft"].Id, Fraction = .8 }) - components["HammerFilterLeft"].OnPressedFunctionName = "HammerFilterLeft" - components["HammerFilterRight"] = CreateScreenComponent({ Name = "ButtonCodexDown", X = x + 200, Y = y, Scale = 1.0, Group = "Combat_Menu" }) - SetAngle({ Id = components["HammerFilterRight"].Id, Angle = 90 }) - SetScale({ Id = components["HammerFilterRight"].Id, Fraction = .8 }) - components["HammerFilterRight"].OnPressedFunctionName = "HammerFilterRight" - - components["HammerFilter"] = CreateScreenComponent({ Name = "BlankObstacle", Scale = 0.5, Group = "Combat_Menu", X = x, Y = y }) - EllosBoonSelectorMod.CurrentHammerIndex = 1 - - local weapon = GetEquippedWeapon() - local aspectIndex = GetEquippedWeaponTraitIndex( weapon ) - local hammerIndex = EllosBoonSelectorMod.CurrentHammerIndex - CreateTextBox({ Id = components["HammerFilter"].Id, - Text = EllosBoonSelectorMod.HammerOptions[weapon .. aspectIndex][hammerIndex], - OffsetX = 0, OffsetY = 0, - FontSize = 22, - Color = Color.White, - Font = "AlegreyaSansSCRegular", - ShadowBlur = 0, ShadowColor = {0,0,0,1}, ShadowOffset={0, 2}, - Justification = "Center", - DataProperties = - { - OpacityWithOwner = true, - }, - }) - - -- Icon Display - components.GodIcon = CreateScreenComponent({ Name = "BlankObstacle", Scale = 1.0, Group = "Combat_Menu", X = 550, Y = 665 }) - Attach({ Id = components.GodIcon.Id, DestinationId = components.ShopBackground.Id, OffsetX = 0, OffsetY = 0}) - components.Reward1 = CreateScreenComponent({ Name = "BlankObstacle", Scale = 1.0, Group = "Combat_Menu", X = 1200, Y = 650 }) - CreateTextBox({ Id = components.Reward1.Id, - Text = "", - OffsetX = 400, OffsetY = -50, - FontSize = 22, - Color = Color.White, - Font = "AlegreyaSansSCRegular", - ShadowBlur = 0, ShadowColor = {0,0,0,1}, ShadowOffset={0, 2}, - Justification = "Center", - DataProperties = - { - OpacityWithOwner = true, - }, - }) - components.Reward2 = CreateScreenComponent({ Name = "BlankObstacle", Scale = 1.0, Group = "Combat_Menu", X = 1200, Y = 650 }) - CreateTextBox({ Id = components.Reward2.Id, - Text = "", - OffsetX = 400, OffsetY = -20, - FontSize = 22, - Color = Color.White, - Font = "AlegreyaSansSCRegular", - ShadowBlur = 0, ShadowColor = {0,0,0,1}, ShadowOffset={0, 2}, - Justification = "Center", - DataProperties = - { - OpacityWithOwner = true, - }, - }) - components.Reward3 = CreateScreenComponent({ Name = "BlankObstacle", Scale = 1.0, Group = "Combat_Menu", X = 1200, Y = 650 }) - CreateTextBox({ Id = components.Reward3.Id, - Text = "", - OffsetX = 400, OffsetY = 10, - FontSize = 22, - Color = Color.White, - Font = "AlegreyaSansSCRegular", - ShadowBlur = 0, ShadowColor = {0,0,0,1}, ShadowOffset={0, 2}, - Justification = "Center", - DataProperties = - { - OpacityWithOwner = true, - }, - }) - components.ChaosRoomIndicator = CreateScreenComponent({ Name = "BlankObstacle", Scale = 1.0, Group = "Combat_Menu", X = 1200, Y = 650 }) - CreateTextBox({ Id = components.ChaosRoomIndicator.Id, - Text = "", - OffsetX = 400, OffsetY = 40, - FontSize = 22, - Color = Color.Purple, - Font = "AlegreyaSansSCRegular", - ShadowBlur = 0, ShadowColor = {0,0,0,1}, ShadowOffset={0, 2}, - Justification = "Center", - DataProperties = - { - OpacityWithOwner = true, - }, - }) - - local roomReward = PredictStartingRoomReward((NextSeeds[1] or 000000)) - UpdateRewardPreview( screen, roomReward ) - ClearFilters(screen) - - -- Close button - components.CloseButton = CreateScreenComponent({ Name = "ButtonClose", Scale = 0.7, Group = "Combat_Menu" }) - Attach({ Id = components.CloseButton.Id, DestinationId = components.ShopBackground.Id, OffsetX = -6, OffsetY = 456 }) - components.CloseButton.OnPressedFunctionName = "CloseSeedControlScreen" - components.CloseButton.ControlHotkey = "Cancel" - - wait(0.1) - --TeleportCursor({ OffsetX = screen.ItemStartX - 30, OffsetY = screen.ItemStartY, ForceUseCheck = true }) - - screen.KeepOpen = true - thread( HandleWASDInput, screen ) - HandleScreenInput( screen ) -end - -function DoesRewardMatchFilters(roomReward) - local targetReward = "Boon" - - if EllosBoonSelectorMod.GodFilter == "" and next(EllosBoonSelectorMod.RarityFilter) == nil then - targetReward = "Hammer" - end - - if roomReward.Type == "Boon" then - -- If looking for a hammer, return false - if targetReward == "Hammer" then - return false - end - - -- God must match filtered god if specified - if EllosBoonSelectorMod.GodFilter ~= "" and EllosBoonSelectorMod.GodFilter ~= roomReward.BoonData.God then - return false - end - - -- Rarities much match all active rarity filters - for priorityBoon, rarityFilter in pairs(EllosBoonSelectorMod.RarityFilter) do - local thisFilterPassed = false; - for index, boonOption in ipairs(roomReward.BoonData.Options) do - if boonOption.Blocked == nil then - if rarityFilter == nil or rarityFilter == 0 then - thisFilterPassed = true - elseif rarityFilter > 0 and priorityBoon == boonOption.Boon and rarityFilter <= boonOption.Rarity then - thisFilterPassed = true - end - end - end - -- If any rarity filter fails, return false - if not thisFilterPassed then - return false - end - end - - -- All filters passed - return true - elseif roomReward.Type == "Hammer" then - -- If looking for a boon, return false - if targetReward == "Boon" then - return false - end - - local weapon = GetEquippedWeapon() - local aspectIndex = GetEquippedWeaponTraitIndex( weapon ) - local hammerIndex = EllosBoonSelectorMod.CurrentHammerIndex - - for index, hammerOption in ipairs(roomReward.HammerData.Options) do - if hammerOption.Blocked == nil then - if hammerOption.Name == EllosBoonSelectorMod.HammerOptions[weapon .. aspectIndex][hammerIndex] then - return true - end - end - end - - return false - end -end - -function RollSeedForFilters( screen, button ) - local roomReward = nil - local counter = 0 - local seed = (NextSeeds[1] or 000000) - repeat - roomReward = PredictStartingRoomReward(seed + counter) - counter = counter + 1 - - -- Cool spinner visual, but searches much slower - --wait(.05) - --UpdateRewardPreview( screen, roomReward ) - --SeedControlScreenSyncDigits( screen ) - --UpdateDigitDisplay( screen ) - until DoesRewardMatchFilters(roomReward) or counter > 1000 - - if counter > 1000 then - -- TODO: tell the user we failed to find a seed - end - - UpdateRewardPreview( screen, roomReward ) - SeedControlScreenSyncDigits( screen ) - UpdateDigitDisplay( screen ) -end - -function ClearFilters ( screen, button ) - EllosBoonSelectorMod.GodFilter = "" - EllosBoonSelectorMod.RarityFilter = {} - - -- TODO: Use a constant for the priority boons so we don't hardcode them in multiple places - for _, god in pairs(EllosBoonSelectorMod.BoonGods) do - ModifyTextBox({ Id = screen.Components[god .. "Filter"].Id, Color = Color.BoonPatchCommon }) - end - for _, priorityBoon in pairs({"Attack", "Special", "Dash", "Cast"}) do - ModifyTextBox({ Id = screen.Components[priorityBoon .. "Filter"].Id, Color = Color.BoonPatchCommon }) - end -end - -function ToggleGodFilter ( screen, button ) - local godName = button.GodName - - EllosBoonSelectorMod.GodFilter = godName - - for _, god in pairs(EllosBoonSelectorMod.BoonGods) do - if god == godName and EllosBoonSelectorMod.GodFilter then - ModifyTextBox({ Id = screen.Components[god .. "Filter"].Id, Color = Color.BoonPatchCommon }) - else - ModifyTextBox({ Id = screen.Components[god .. "Filter"].Id, Color = Color.Gray }) - end - end -end - -function CycleRarityFilter ( screen, button ) - local priorityBoon = button.PriorityBoon - - local currentFilterLevel = (EllosBoonSelectorMod.RarityFilter[priorityBoon] or 0) - currentFilterLevel = (currentFilterLevel + 1) % 3 - EllosBoonSelectorMod.RarityFilter[priorityBoon] = currentFilterLevel - - -- CurrentFilterLevel goes from 0 to 2, add 1 to map to rarity values - ModifyTextBox({ Id = button.Id, Color = EllosBoonSelectorMod.RarityColors[currentFilterLevel + 1] }) -end - -function HammerFilterLeft ( screen, button ) - local weapon = GetEquippedWeapon() - local aspectIndex = GetEquippedWeaponTraitIndex( weapon ) - local hammerIndex = EllosBoonSelectorMod.CurrentHammerIndex - - hammerIndex = hammerIndex - 1 - if hammerIndex == 0 then - hammerIndex = TableLength(EllosBoonSelectorMod.HammerOptions[weapon .. aspectIndex]) - end - EllosBoonSelectorMod.CurrentHammerIndex = hammerIndex - ModifyTextBox({Id = screen.Components["HammerFilter"].Id, Text = EllosBoonSelectorMod.HammerOptions[weapon .. aspectIndex][hammerIndex]}) -end - -function HammerFilterRight ( screen, button ) - local weapon = GetEquippedWeapon() - local aspectIndex = GetEquippedWeaponTraitIndex( weapon ) - local hammerIndex = EllosBoonSelectorMod.CurrentHammerIndex - - hammerIndex = hammerIndex + 1 - if hammerIndex == TableLength(EllosBoonSelectorMod.HammerOptions[weapon .. aspectIndex]) + 1 then - hammerIndex = 1 - end - EllosBoonSelectorMod.CurrentHammerIndex = hammerIndex - ModifyTextBox({Id = screen.Components["HammerFilter"].Id, Text = EllosBoonSelectorMod.HammerOptions[weapon .. aspectIndex][hammerIndex]}) -end - -function SeedDigitUp( screen, button ) - local newDigitValue = screen.Digits[button.Digit] - newDigitValue = newDigitValue - 1 - if newDigitValue < 0 then - newDigitValue = 9 - end - screen.Digits[button.Digit] = newDigitValue - local newSeed = 0 - for digit, digitValue in ipairs( screen.Digits ) do - newSeed = newSeed + (digitValue * math.pow(10, digit - 1)) - end - local roomReward = PredictStartingRoomReward(newSeed) - UpdateRewardPreview( screen, roomReward ) - UpdateDigitDisplay( screen ) -end - -function SeedDigitDown( screen, button ) - local newDigitValue = screen.Digits[button.Digit] - newDigitValue = newDigitValue + 1 - if newDigitValue > 9 then - newDigitValue = 0 - end - screen.Digits[button.Digit] = newDigitValue - local newSeed = 0 - for digit, digitValue in ipairs( screen.Digits ) do - newSeed = newSeed + (digitValue * math.pow(10, digit - 1)) - end - local roomReward = PredictStartingRoomReward(newSeed) - UpdateRewardPreview( screen, roomReward ) - UpdateDigitDisplay( screen ) -end - -function SeedControlScreenSyncDigits( screen ) - local displayNumber = (NextSeeds[1] or 000000) - if displayNumber ~= nil then - for digit = 1, #screen.Digits do - local digitValue = displayNumber % 10 - screen.Digits[digit] = digitValue - displayNumber = math.floor( displayNumber / 10 ) - end - end -end - -function UpdateDigitDisplay( screen ) - for digit, digitValue in ipairs( screen.Digits ) do - local digitKey = "DigitButton"..digit - ModifyTextBox({ Id = screen.Components["DigitButton"..digit].Id, Text = digitValue }) - end -end - -function SeedControlScreenRandomize( screen, button ) - local newSeed = RandomInt(0, 999999) - local roomReward = PredictStartingRoomReward(newSeed) - UpdateRewardPreview( screen, roomReward ) - SeedControlScreenSyncDigits( screen ) - UpdateDigitDisplay( screen ) -end - -function CloseSeedControlScreen( screen, button ) - - local newSeed = 0 - local place = 1 - for digit, digitValue in ipairs( screen.Digits ) do - newSeed = newSeed + (digitValue * math.pow(10, digit - 1)) - end - - DisableShopGamepadCursor() - SetConfigOption({ Name = "FreeFormSelectWrapY", Value = false }) - SetConfigOption({ Name = "FreeFormSelectStepDistance", Value = 16 }) - SetConfigOption({ Name = "FreeFormSelectSuccessDistanceStep", Value = 8 }) - SetAnimation({ DestinationId = screen.Components.ShopBackground.Id, Name = screen.CloseAnimation }) - PlaySound({ Name = "/SFX/Menu Sounds/FatedListClose" }) - CloseScreen( GetAllIds( screen.Components ), 0.1 ) - UnfreezePlayerUnit() - screen.KeepOpen = false - OnScreenClosed({ Flag = screen.Name }) -end - ---- Takes in a seed and returns the predicted RoomReward object for the starting room. --- TODO: Update this to use a separate RNG entirely rather than using the current RNG and putting it back again --- --- @param int seedForPrediction RNG seed to use when predicting the rng calls --- @param int currentSeed RNG seed to reset to after making the predictions (Optional) --- @return RoomReward where RoomReward has a {Type} ("Hammer" or "Boon") and the corresponding Data e.g. {BoonData} or {HammerData} -function PredictStartingRoomReward( seedForPrediction, currentSeed ) - local roomReward = {} - - local roomRewardType = PredictRoomRewardType(seedForPrediction, currentSeed) - roomReward.Type = roomRewardType.Name - - if roomReward.Type == "Boon" then - roomReward.BoonData = {} - roomReward.BoonData.God = roomRewardType.God - - -- Get exact boon rewards and rarity and update the menu - roomReward.BoonData.Options = PredictStartingGodBoonOptions( roomReward.BoonData.God, seedForPrediction, currentSeed ) - roomReward.FirstRoomChaos = PredictChaos(6, seedForPrediction) - elseif roomReward.Type == "Hammer" then - roomReward.HammerData = {} - local currentWeapon = GetEquippedWeapon() - roomReward.HammerData.Options = PredictHammerOptionsForWeapon(currentWeapon, GetEquippedWeaponTraitIndex( currentWeapon ), seedForPrediction, currentSeed) - roomReward.FirstRoomChaos = PredictChaos(5, seedForPrediction) - end - - roomReward.SecondRoomChaos = PredictChaos(2, seedForPrediction) - - return roomReward -end - -function GetBlockedIndicesForAP() - -- Resync and calculate blocked indices from AP - RandomSynchronize(1) -- Sometimes 0 sometimes 1, maybe chaos makes 0? - local blockedIndices = {} - for i = 1, 3 do - table.insert( blockedIndices, i ) - end - for i = 1, CalcNumLootChoices() do - RemoveRandomValue( blockedIndices ) - end - return blockedIndices -end - -function GetChaosChance() - -- TODO: How to retrieve room date for a future room? Just need tile name? - --local secretChance = room.SecretSpawnChance or RoomData.BaseRoom.SecretSpawnChance - local secretChance = RoomData.BaseRoom.SecretSpawnChance - for k, mutator in pairs( GameState.ActiveMutators ) do - if mutator.SecretSpawnChanceMultiplier ~= nil then - secretChance = secretChance * mutator.SecretSpawnChanceMultiplier - end - end - return 0.15 -end - -function PredictFirstRoomChaos(seedForPrediction, currentSeed) - RandomSetNextInitSeed( {Seed = seedForPrediction} ) - RandomSynchronize(5) -- Known offset at which the RNG rolls reward type - local chaosPresent = RandomChance(GetChaosChance()) - - -- Reset RNG to the pre-call state - if currentSeed ~= nil then - RandomSetNextInitSeed( {Seed = currentSeed} ) - end - - return chaosPresent -end - -function PredictChaos(offset, seedForPrediction, currentSeed) - RandomSetNextInitSeed( {Seed = seedForPrediction} ) - RandomSynchronize(offset) -- Known offset at which the RNG rolls reward type - local chaosPresent = RandomChance(GetChaosChance()) - - -- Reset RNG to the pre-call state - if currentSeed ~= nil then - RandomSetNextInitSeed( {Seed = currentSeed} ) - end - - return chaosPresent -end - ---- Takes in a seed and returns the predicted RoomRewardType object. --- TODO: Update this to use a separate RNG entirely rather than using the current RNG and putting it back again --- --- @param int seedForPrediction RNG seed to use when predicting the rng calls --- @param int currentSeed RNG seed to reset to after making the predictions (Optional) --- @return RoomRewardType where RoomRewardType has a {Name} ("Hammer" or "Boon") and conditionally a {God} -function PredictRoomRewardType( seedForPrediction, currentSeed ) - local roomRewardType = {} - - -- Save off the current RNG the load the provided seed and offset - RandomSetNextInitSeed( {Seed = seedForPrediction} ) - RandomSynchronize(4) -- Known offset at which the RNG rolls reward type - roomRewardType.Name = GetRandomValue({"Boon", "Boon", "Boon", "Hammer"}) - - if roomRewardType.Name == "Boon" then - local godOptions = DeepCopyTable(EllosBoonSelectorMod.BoonGods) - local rarityTraits = GetHeroTraitValues("RarityBonus", { UnlimitedOnly = false }) - local god = "" - for i, rarityTraitData in pairs(rarityTraits) do - if rarityTraitData.RequiredGod ~= nil and rarityTraitData.RequiredGod ~= "TrialUpgrade" then - god = string.sub(rarityTraitData.RequiredGod, 1, -8) - end - end - if god == "" then - god = RemoveRandomValue(godOptions) - end - roomRewardType.God = god -- God RNG roll will happen at the next offset - end - - -- Reset RNG to the pre-call state - if currentSeed ~= nil then - RandomSetNextInitSeed( {Seed = currentSeed} ) - end - - return roomRewardType -end - ---- Takes in a weapon name and seed and returns a list of HammerOption objects. --- TODO: Update this to use a separate RNG entirely rather than using the current RNG and putting it back again --- --- @param string weapon The name of the weapon used to find the eligible hammers --- @param int aspectIndex Index corrseponding to the aspect of the weapon (e.g. 1-4 in order of unlock) --- @param int seedForPrediction RNG seed to use when predicting the rng calls --- @param int currentSeed RNG seed to reset to after making the predictions (Optional) --- @return list where HammerOption has a {Name} -function PredictHammerOptionsForWeapon( weapon, aspectIndex, seedForPrediction, currentSeed ) - local hammerOptions = {} - - -- Save off the current RNG the load the provided seed and offset - RandomSetNextInitSeed( {Seed = seedForPrediction} ) - local blockedIndices = GetBlockedIndicesForAP() - RandomSynchronize(1) -- Offset that when the hammer options start being rolled at - - local eligibleHammers = DeepCopyTable(EllosBoonSelectorMod.HammerOptions[weapon .. aspectIndex]) - - local selectedIndexes = TableLength( eligibleHammers ) - for index = 1, 3 do - hammerOptions[index] = {} - local selectedHammer = GetRandomValue(eligibleHammers) - hammerOptions[index].Name = selectedHammer - - -- Remove the picked hammer from the pool of eligible Hammers - for i, hammer in ipairs(eligibleHammers) do - if hammer == selectedHammer then - table.remove(eligibleHammers, i) - break - end - end - - for _ , value in pairs(blockedIndices) do - if value == index then - hammerOptions[index].Blocked = true - end - end - end - - -- Reset RNG to the pre-call state - if currentSeed ~= nil then - RandomSetNextInitSeed( {Seed = currentSeed} ) - end - - return hammerOptions -end - ---- Takes in a gods name and seed and returns a list of BoonOption objects for the starting room only. --- TODO: Update this to use a separate RNG entirely rather than using the current RNG and putting it back again --- --- @param string god The god for which the boon options should be predicted --- @param int seedForPrediction RNG seed to use when predicting the rng calls --- @param int currentSeed RNG seed to reset to after making the predictions (Optional) --- @return list where BoonOption has a {God}, {Name} and {Rarity} (numeric) -function PredictStartingGodBoonOptions( god, seedForPrediction, currentSeed ) - local startingBoons = {} - - -- Save off the current RNG the load the provided seed and offset - RandomSetNextInitSeed( {Seed = seedForPrediction} ) - local blockedIndices = GetBlockedIndicesForAP() - RandomSynchronize(1) -- Offset that when the boons start being rolled at - - -- First room always offers 3 priority boons (selected by excluding one of the 4 options) - local boonRewards = DeepCopyTable(EllosBoonSelectorMod.PriorityBoons) - local excluded = RemoveRandomValue(boonRewards) - boonRewards = CollapseTableOrdered(boonRewards) - - local rarities = ElloGetBoonRarityChances(god) - for index = 1, 3 do - startingBoons[index] = {} - startingBoons[index].Boon = boonRewards[index] - RandomChance(0) -- Legendary isn't possible for starting boons - if RandomChance(rarities.Epic) then -- Skip Heroic because starting boons can't naturally roll Heroic - startingBoons[index].Rarity = 2 - elseif RandomChance(rarities.Rare) then - startingBoons[index].Rarity = 1 - else - startingBoons[index].Rarity = 0 - end - - for key, value in pairs(blockedIndices) do - if value == index then - startingBoons[index].Blocked = true - end - end - end - - -- Reset RNG to the pre-call state - if currentSeed ~= nil then - RandomSetNextInitSeed( {Seed = currentSeed} ) - end - - table.sort( startingBoons, function(boon1, boon2) return EllosBoonSelectorMod.PriorityBoonsOrder[boon1.Boon] < EllosBoonSelectorMod.PriorityBoonsOrder[boon2.Boon] end) - return startingBoons -end - -function ElloGetBoonRarityChances( godName, roomRarityOverride ) - local name = godName - --local ignoreTempRarityBonus = args.IgnoreTempRarityBonus - local referencedTable = "BoonData" - -- "HermesUpgrade" then referencedTable = "HermesData" - - local legendaryRoll = CurrentRun.Hero[referencedTable].LegendaryChance or 0 - local heroicRoll = CurrentRun.Hero[referencedTable].HeroicChance or 0 - local epicRoll = CurrentRun.Hero[referencedTable].EpicChance or 0 - local rareRoll = CurrentRun.Hero[referencedTable].RareChance or 0 - - if roomRarityOverride ~= nil then - legendaryRoll = roomRarityOverride.Legendary or legendaryRoll - heroicRoll = roomRarityOverride.Heroic or heroicRoll - epicRoll = roomRarityOverride.EpicChance or epicRoll - rareRoll = roomRarityOverride.RareChance or rareRoll - end - - local metaupgradeRareBoost = GetNumMetaUpgrades( "RareBoonDropMetaUpgrade" ) * ( MetaUpgradeData.RareBoonDropMetaUpgrade.ChangeValue - 1 ) - local metaupgradeEpicBoost = GetNumMetaUpgrades( "EpicBoonDropMetaUpgrade" ) * ( MetaUpgradeData.EpicBoonDropMetaUpgrade.ChangeValue - 1 ) + GetNumMetaUpgrades( "EpicHeroicBoonMetaUpgrade" ) * ( MetaUpgradeData.EpicBoonDropMetaUpgrade.ChangeValue - 1 ) - local metaupgradeLegendaryBoost = GetNumMetaUpgrades( "DuoRarityBoonDropMetaUpgrade" ) * ( MetaUpgradeData.EpicBoonDropMetaUpgrade.ChangeValue - 1 ) - local metaupgradeHeroicBoost = GetNumMetaUpgrades( "EpicHeroicBoonMetaUpgrade" ) * ( MetaUpgradeData.EpicBoonDropMetaUpgrade.ChangeValue - 1 ) - legendaryRoll = legendaryRoll + metaupgradeLegendaryBoost - heroicRoll = heroicRoll + metaupgradeHeroicBoost - rareRoll = rareRoll + metaupgradeRareBoost - epicRoll = epicRoll + metaupgradeEpicBoost - - local rarityTraits = GetHeroTraitValues("RarityBonus", { UnlimitedOnly = false }) - for i, rarityTraitData in pairs(rarityTraits) do - if rarityTraitData.RequiredGod == nil or rarityTraitData.RequiredGod == name .. "Upgrade" then - if rarityTraitData.RareBonus then - rareRoll = rareRoll + rarityTraitData.RareBonus - end - if rarityTraitData.EpicBonus then - epicRoll = epicRoll + rarityTraitData.EpicBonus - end - if rarityTraitData.HeroicBonus then - heroicRoll = heroicRoll + rarityTraitData.HeroicBonus - end - if rarityTraitData.LegendaryBonus then - legendaryRoll = legendaryRoll + rarityTraitData.LegendaryBonus - end - end - end - return - { - Rare = rareRoll, - Epic = epicRoll, - Heroic = heroicRoll, - Legendary = legendaryRoll, - } -end - -function UpdateRewardPreview( screen, roomReward ) - if config.ShowPreview == false then - return - end - if roomReward.Type == "Boon" then - SetAnimation({ Name = "BoonSymbol" .. roomReward.BoonData.God .. "Isometric", DestinationId = screen.Components.GodIcon.Id, OffsetX = 640, OffsetY = -45}) - - local roomRewardOptions = roomReward.BoonData.Options - for index, boonOption in ipairs(roomRewardOptions) do - local color = EllosBoonSelectorMod.RarityColors[boonOption.Rarity + 1] - if boonOption.Blocked == true then - color = Color.Red - end - ModifyTextBox({ Id = screen.Components["Reward" .. index].Id, Text = roomReward.BoonData.God .. boonOption.Boon, Color = color}) - end - elseif roomReward.Type == "Hammer" then - SetAnimation({ Name = "WeaponUpgradePreview", DestinationId = screen.Components.GodIcon.Id, OffsetX = 640, OffsetY = -45}) - local hammerOptions = roomReward.HammerData.Options - - for index = 1, 3 do - local color = Color.BoonPatchCommon - if hammerOptions[index].Blocked == true then - color = Color.Red - end - ModifyTextBox({ Id = screen.Components["Reward" .. index].Id, Text = hammerOptions[index].Name, Color = color }) - end - end - - if roomReward.FirstRoomChaos then - ModifyTextBox({ Id = screen.Components.ChaosRoomIndicator.Id, Text = "Room 1 Chaos" }) - elseif roomReward.SecondRoomChaos then - ModifyTextBox({ Id = screen.Components.ChaosRoomIndicator.Id, Text = "Room 2 Chaos" }) - else - ModifyTextBox({ Id = screen.Components.ChaosRoomIndicator.Id, Text = "No Chaos" }) - end -end - --- Convenient place to add a button to the AdvancedTooltipScreen -ModUtil.WrapBaseFunction("CreatePrimaryBacking", function ( baseFunc ) - local components = ScreenAnchors.TraitTrayScreen.Components - - -- Add button for RNG seed select menu but only between runs - if ModUtil.PathGet("CurrentDeathAreaRoom") then - components.RngSeedButton = CreateScreenComponent({ Name = "ButtonDefault", Scale = 1.0, Group = "Combat_Menu_TraitTray", X = CombatUI.TraitUIStart + 105, Y = 930 }) - components.RngSeedButton.OnPressedFunctionName = "OpenRngSeedSelectorScreen" - CreateTextBox({ Id = components.RngSeedButton.Id, - Text = "Set Starting Boon", - OffsetX = 0, OffsetY = 0, - FontSize = 22, - Color = Color.White, - Font = "AlegreyaSansSCRegular", - ShadowBlur = 0, ShadowColor = {0,0,0,1}, ShadowOffset={0, 2}, - Justification = "Center", - DataProperties = - { - OpacityWithOwner = true, - }, - }) - Attach({ Id = components.RngSeedButton.Id, DestinationId = components.RngSeedButton, OffsetX = 500, OffsetY = 500 }) - end - baseFunc() -end, EllosBoonSelectorMod) diff --git a/EllosStartingBoonSelectorMod/modfile.txt b/EllosStartingBoonSelectorMod/modfile.txt deleted file mode 100644 index 1c61242..0000000 --- a/EllosStartingBoonSelectorMod/modfile.txt +++ /dev/null @@ -1,2 +0,0 @@ -:: Ello's Boon Selector Mod v1.1 -Import "Scripts" diff --git a/EmoteMod/EmoteMod.lua b/EmoteMod/EmoteMod.lua index 3c28e07..0e7bbd3 100644 --- a/EmoteMod/EmoteMod.lua +++ b/EmoteMod/EmoteMod.lua @@ -1,4 +1,4 @@ -ModUtil.RegisterMod("EmoteMod") +ModUtil.Mod.Register("EmoteMod") local config = { ModName = "EmoteMod", diff --git a/EmoteMod/modfile.txt b/EmoteMod/modfile.txt index 0a1e26a..003ff68 100644 --- a/EmoteMod/modfile.txt +++ b/EmoteMod/modfile.txt @@ -1 +1,3 @@ -Import "EmoteMod.lua" +:: Disabled until functional and/or the following issue is resolved +:: https://github.com/ellomenop/HadesSpeedrunningModPack/issues/37 +:: Import "EmoteMod.lua" diff --git a/EnemyControl/EnemyControl.lua b/EnemyControl/EnemyControl.lua new file mode 100644 index 0000000..5de31cc --- /dev/null +++ b/EnemyControl/EnemyControl.lua @@ -0,0 +1,193 @@ +--[[ + EnemyControl + Authors: + SleepSoul (Discord: SleepSoul#6006) + Museus (Discord: Museus#7777) + Dependencies: ModUtil, RCLib + Change the pool of enemies eligible in each room, allowing certain enemy types to be removed. +]] +ModUtil.Mod.Register("EnemyControl") + +local config = { + EnemySetting = "Vanilla" +} +EnemyControl.config = config --TODO add config option in menu +EnemyControl.EligibleEnemies = {} +EnemyControl.VanillaSets = {} + +EnemyControl.Presets = { -- Define rulesets + Vanilla = {}, + Hypermodded1 = { + StyxSmallRoom = { + TinyRat = false, + } + }, + Hypermodded2 = { + Tartarus = { + Numbskull = false, + Witch = false, + }, + Asphodel = { + Bloodless = false, + Gorgon = false, + }, + Elysium = { + Spearman = false, + Bowman = false, + Shieldsman = false, + Swordsman = false, + Flamewheel = false, + }, + StyxSmallRoom = { + TinyRat = false, + } + }, + RatsOClock = { + Tartarus = { + TinyRat = true, + }, + TartarusElite = { + TinyRat = true, + }, + TartarusSurvival = { + TinyRat = true, + }, + Asphodel = { + TinyRat = true, + }, + AsphodelElite = { + TinyRat = true, + }, + Elysium = { + TinyRat = true, + }, + ElysiumElite = { + TinyRat = true, + }, + StyxSmallRoom = { + TinyRat = true, + }, + StyxSmallRoomElite = { + TinyRat = true, + }, + StyxSmallRoomSingle = { + TinyRat = true, + }, + }, + Neuron = { + Tartarus = { + ArmoredSplitter = true, + }, + TartarusElite = { + ArmoredSplitter = true, + }, + TartarusSurvival = { + ArmoredSplitter = true, + }, + Asphodel = { + ArmoredSplitter = true, + }, + AsphodelElite = { + ArmoredSplitter = true, + }, + Elysium = { + ArmoredSplitter = true, + }, + ElysiumElite = { + ArmoredSplitter = true, + }, + StyxSmallRoom = { + ArmoredSplitter = true, + }, + StyxSmallRoomElite = { + ArmoredSplitter = true, + }, + StyxSmallRoomSingle = { + ArmoredSplitter = true, + }, + }, +} + +EnemyControl.InheritVanilla = { -- Biomes set to true will inherit the vanilla enemy set and only remove those set to false, biomes set to false will start from 0 and only add those set to true. + Hypermodded1 = { + StyxSmallRoom = true, + }, + Hypermodded2 = { + Tartarus = true, + Asphodel = true, + Elysium = true, + StyxSmallRoom = true, + } +} + +EnemyControl.RuleOverrides = { -- Any overrides to enemy eligibility are made here. Only option currently supported is HardForce, which will make the enemy always eligible to appear. TODO add overrides for minimum and maximum biome depth per biome + Neuron = { + ArmoredSplitter = { + HardForce = true, + }, + }, +} + +function EnemyControl.ReadPreset() --Read current preset and create table of enemies marked as eligible + local Preset = EnemyControl.Presets[config.EnemySetting] + local InheritVanilla = {} + if EnemyControl.InheritVanilla[config.EnemySetting] ~= nil then + InheritVanilla = EnemyControl.InheritVanilla[config.EnemySetting] + end + for biome, _ in pairs(Preset) do + EnemyControl.EligibleEnemies[biome] = {} + if InheritVanilla[biome] == true then + RCLib.PopulateMinLength( + EnemyControl.EligibleEnemies[biome], + RCLib.RemoveIneligibleStrings(Preset[biome],EnemyControl.VanillaSets[RCLib.EncodeEnemySet(biome)],RCLib.NameToCode.Enemies), + 1 + ) + else + RCLib.PopulateMinLength( + EnemyControl.EligibleEnemies[biome], + RCLib.GetEligible(Preset[biome],RCLib.NameToCode.Enemies), + 1 + ) + end + end +end + +function EnemyControl.UpdatePools() -- Inject every non-empty biome of the current preset into the relevant biomes in EnemySets.lua + DebugPrint({Text = "Enemy preset: "..EnemyControl.config.EnemySetting}) + if EnemyControl.InheritVanilla[config.EnemySetting] ~= nil then + InheritVanilla = EnemyControl.InheritVanilla[config.EnemySetting] + end + for biome, pool in pairs(EnemyControl.EligibleEnemies) do + EnemyControl.Target = RCLib.EncodeEnemySet(biome) + EnemyControl.Pool = pool + ModUtil.Table.Replace(EnemySets[EnemyControl.Target], EnemyControl.Pool) + DebugPrint({Text = "Updated enemy pool for "..biome}) + end +end + +ModUtil.LoadOnce( function() + EnemyControl.VanillaSets = ModUtil.Table.Copy(EnemySets) + EnemyControl.ReadPreset() + EnemyControl.UpdatePools() +end) + +-- When a new run is started, make sure to apply the pool settings +ModUtil.Path.Wrap("StartNewRun", function ( baseFunc, currentRun ) + EnemyControl.ReadPreset() + EnemyControl.UpdatePools() + return baseFunc(currentRun) +end, EnemyControl) + +ModUtil.Path.Wrap("IsEnemyEligible", function ( baseFunc, enemyName, encounter, wave ) + local Preset = EnemyControl.config.EnemySetting + local EnemyRef = RCLib.DecodeEnemy(enemyName) + local Overrides = {} + if EnemyControl.RuleOverrides[Preset] ~= nil and EnemyControl.RuleOverrides[Preset][EnemyRef] ~= nil then + Overrides = EnemyControl.RuleOverrides[Preset][EnemyRef] + end + if Overrides.HardForce then + return true + end + return baseFunc( enemyName, encounter, wave ) +end, EnemyControl) + diff --git a/EnemyControl/modfile.txt b/EnemyControl/modfile.txt new file mode 100644 index 0000000..8c0b6c6 --- /dev/null +++ b/EnemyControl/modfile.txt @@ -0,0 +1,2 @@ +:: Enemy Control +Import "EnemyControl.lua" diff --git a/ErumiUILib/ErumiUILib.lua b/ErumiUILib/ErumiUILib.lua index e70051e..bcb56b4 100644 --- a/ErumiUILib/ErumiUILib.lua +++ b/ErumiUILib/ErumiUILib.lua @@ -1,4 +1,4 @@ -ModUtil.RegisterMod("ErumiUILib") +ModUtil.Mod.Register("ErumiUILib") ErumiUILib = { Slider = {}, Dropdown = {}, diff --git a/FixedHammers/FixedHammers.lua b/FixedHammers/FixedHammers.lua deleted file mode 100644 index 5dce820..0000000 --- a/FixedHammers/FixedHammers.lua +++ /dev/null @@ -1,96 +0,0 @@ -ModUtil.RegisterMod("FixedHammers") - -local config = { - ModName = "Fixed Hammers", - Enabled = true, - AchillesRebalance = true, - NemesisRebalance = true, -} -FixedHammers.config = config - -local function GetEligibleHammers() - - local loot = DeepCopyTable( LootData["WeaponUpgrade"] ) - loot.RarityChances = {} - loot.ForceCommon = true - return GetEligibleUpgrades({}, loot, LootData["WeaponUpgrade"]) -end - -ModUtil.WrapBaseFunction("StartNewRun", function(baseFunc, ...) - -- initialize everything else first - local run = baseFunc(...) - - -- reset the mod - CurrentRun.Hammers = {} - - -- determine eligible hammers (for the current weapon) - local eligibleHammers = GetEligibleHammers() - - -- shuffle the hammers, creating two permutations - CurrentRun.Hammers[1] = FYShuffle(eligibleHammers) - CurrentRun.Hammers[2] = FYShuffle(eligibleHammers) - - if config.AchillesRebalance and HeroHasTrait( "SpearTeleportTrait" ) then - RemoveValueAndCollapse(CurrentRun.Hammers[1], {Type = "Trait", ItemName = "SpearAutoAttack"}) - table.insert(CurrentRun.Hammers[1], 1, {Type = "Trait", ItemName = "SpearAutoAttack"}) - end - - if config.NemesisRebalance and HeroHasTrait( "SwordCriticalParryTrait" ) then - RemoveValueAndCollapse(CurrentRun.Hammers[1], {Type = "Trait", ItemName = "SwordDoubleDashAttackTrait"}) - table.insert(CurrentRun.Hammers[1], 1, {Type = "Trait", ItemName = "SwordDoubleDashAttackTrait"}) - end - - return run -end, FixedHammers) - -ModUtil.WrapBaseFunction("SetTraitsOnLoot", function(baseFunc, lootData, args) - if lootData.Name == "WeaponUpgrade" and config.Enabled and CurrentRun.Hammers then - local previousHammers = CurrentRun.PreviousHammers or 0 - local hammersInOrder = CurrentRun.Hammers[previousHammers + 1] - - if hammersInOrder == nil then - DebugPrint({Text = "Something went wrong with Fixed Hammers and there are no preset hammers"}) - FixedHammers.Busted = lootData - return baseFunc(lootData, args) - end - - local eligibleHammers = GetEligibleHammers() - - for idx, hammer in pairs(hammersInOrder) do - DebugPrint({Text = idx .. ": " .. hammer.ItemName}) - end - for _, hammer in pairs(eligibleHammers) do - DebugPrint({Text = "-- : " .. hammer.ItemName}) - end - local upgradeOptions = {} - local upgradesSelected = 0 - - -- pick the first three hammers from the fixed permutation - for _, currentHammer in pairs(hammersInOrder) do - for _, eligibleHammer in pairs(eligibleHammers) do - if currentHammer.ItemName == eligibleHammer.ItemName then - eligibleHammer.Rarity = "Common" - table.insert(upgradeOptions, eligibleHammer) - upgradesSelected = upgradesSelected + 1 - break - end - end - if upgradesSelected == 3 then break end - end - baseFunc(lootData, args) - lootData.UpgradeOptions = upgradeOptions - FixedHammers.After = lootData - return - else - return baseFunc(lootData, args) - end -end, FixedHammers) - -ModUtil.WrapBaseFunction("AddTraitToHero", function(baseFunc, trait) - -- Track previous hammers manually because sometimes the SetTraitsOnLoot gets called multiple times - -- both before AND after the LootHistory gets incremented. First room hammer does not do this. - if ModUtil.SafeGet(trait, ModUtil.PathArray("TraitData.Frame")) == "Hammer" then - CurrentRun.PreviousHammers = (CurrentRun.PreviousHammers or 0) + 1 - end - baseFunc(trait) -end, FixedHammers) diff --git a/FixedHammers/LICENSE b/FixedHammers/LICENSE deleted file mode 100644 index 4909372..0000000 --- a/FixedHammers/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2021 paradigmsort - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/FixedHammers/modfile.txt b/FixedHammers/modfile.txt deleted file mode 100644 index d8a5f6e..0000000 --- a/FixedHammers/modfile.txt +++ /dev/null @@ -1 +0,0 @@ -Import "FixedHammers.lua" diff --git a/HadesSpeedrunningModpack/AttributionAndNotes.lua b/HadesSpeedrunningModpack/AttributionAndNotes.lua index 7abcd11..72ca843 100644 --- a/HadesSpeedrunningModpack/AttributionAndNotes.lua +++ b/HadesSpeedrunningModpack/AttributionAndNotes.lua @@ -17,15 +17,10 @@ function HSMConfigMenu.CreateAttributionMenu(screen) }) CreateTextBox({ Id = screen.Components["ThanksForAllTheFish"].Id, - Text = "Hey Everyone,\\n" - .. " Business first. The Hades Speedrunning ModPack (v0.1) will be ever evolving." - .. " There WILL be bugs. \\n It took over 8000 lines of code and 100s of hours of nerd sweat to get this far." - .. " So I know its difficult after 4 hours of Flurry Jab resets, but please be patient, it takes time and toil to make your ideas into reality." - - .. " \\n Also please lend your voice and opinions to making it better as we move forwards." - .. " Wriste is organizing a document and survey to get us started, but feel free to reach out directly or via #modification-station too." - - .. " \\n \\n Secondly, I just wanted to say thank you.\\n \\n Thank you to the Speedrunning Community:" + Text = "Modders: \\n" + .. " Ellomenop, Museus, cgull, paradigmsort, erumi321, Magic_Gonads, PonyWarrior" + .. "\\n Testers: The Entire Hades Speedrunning Commumity <3" + .. " \\n\\n\\n Note from Ello: \\nI just wanted to say thank you.\\n \\n Thank you to the Speedrunning Community:" .. " \\n This community is an immensely positive place to be and thats no accident." .. " \\n Its due to our shared love of this game (or perhaps our shared hatred of 5 sacks)." .. " \\n Its due to the hard work of everyone building each other up (even if its just so the Fish Flex hits that much harder)." diff --git a/HadesSpeedrunningModpack/HSMConfigMenu.lua b/HadesSpeedrunningModpack/HSMConfigMenu.lua index 1280efe..6b72a3c 100644 --- a/HadesSpeedrunningModpack/HSMConfigMenu.lua +++ b/HadesSpeedrunningModpack/HSMConfigMenu.lua @@ -30,9 +30,7 @@ ModUtil.LoadOnce(function() HSMConfigMenu_SavedNonRuleset = {HashInt = hashInt} end) -ModUtil.WrapBaseFunction("ModConfigMenu__Close", function ( baseFunc, screen, button ) - baseFunc(screen, button) - +function HSMConfigMenu.SaveSettingsToGlobal() -- Save Ruleset to global local hashInt = CalculateHash(HSMConfigMenu.RulesetSettings, _G) HSMConfigMenu_SavedRuleset = {HashInt = hashInt} @@ -47,10 +45,16 @@ ModUtil.WrapBaseFunction("ModConfigMenu__Close", function ( baseFunc, screen, bu HSMConfigMenu_SavedPersonalization.ModdedWarningWarningMessage = ModdedWarning.config.WarningMessage HSMConfigMenu_SavedPersonalization.ModdedWarningWarningColor = nil HSMConfigMenu_SavedPersonalization.ModdedWarningColor = ModdedWarning.config.Color +end + +ModUtil.Path.Wrap("UnfreezePlayerUnit", function ( baseFunc, flag ) + DebugPrint({Text="This is in the wrapper"}) + HSMConfigMenu.SaveSettingsToGlobal() + baseFunc(flag) end, HSMConfigMenu) OnAnyLoad{function() - if ModUtil.PathGet("CurrentDeathAreaRoom") then + if ModUtil.Path.Get("CurrentDeathAreaRoom") then HSMConfigMenu.updateRulesetHashDisplay() end end} diff --git a/HadesSpeedrunningModpack/HadesSpeedrunningModpack.lua b/HadesSpeedrunningModpack/HadesSpeedrunningModpack.lua index c9ba011..9653be1 100644 --- a/HadesSpeedrunningModpack/HadesSpeedrunningModpack.lua +++ b/HadesSpeedrunningModpack/HadesSpeedrunningModpack.lua @@ -1,7 +1,7 @@ -ModUtil.RegisterMod("HadesSpeedrunningModpack") +ModUtil.Mod.Register("HadesSpeedrunningModpack") config = { - Version = "v0.3" + Version = "v1.2.0" } HadesSpeedrunningModpack.config = config -ModUtil.RegisterMod("HSMConfigMenu") +ModUtil.Mod.Register("HSMConfigMenu") diff --git a/HadesSpeedrunningModpack/QualityOfLifeMenu.lua b/HadesSpeedrunningModpack/QualityOfLifeMenu.lua index b2da304..e61fd3c 100644 --- a/HadesSpeedrunningModpack/QualityOfLifeMenu.lua +++ b/HadesSpeedrunningModpack/QualityOfLifeMenu.lua @@ -67,8 +67,8 @@ function HSMConfigMenu.CreateQolMenu(screen) screen.Components["DisableIntroCheckBox"].Config = "RemoveCutscenes.config.RemoveIntro" screen.Components["DisableIntroCheckBox"].OnPressedFunctionName = "HSMConfigMenu__ToggleGenericConfigCheckBox" HSMConfigMenu__UpdateGenericConfigCheckbox(screen, screen.Components["DisableIntroCheckBox"]) - itemLocationY = itemLocationY + itemSpacingY + screen.Components["DisableOuttroTextBox"] = CreateScreenComponent({ Name = "BlankObstacle", Scale = 1, @@ -136,6 +136,100 @@ function HSMConfigMenu.CreateQolMenu(screen) screen.Components["RestartCheckBox"].Config = "QuickRestart.config.Enabled" screen.Components["RestartCheckBox"].OnPressedFunctionName = "HSMConfigMenu__ToggleGenericConfigCheckBox" HSMConfigMenu__UpdateGenericConfigCheckbox(screen, screen.Components["RestartCheckBox"]) + itemLocationY = itemLocationY + itemSpacingY + + + screen.Components["StartingKeepsakeTextBox"] = CreateScreenComponent({ + Name = "BlankObstacle", + Scale = 1, + X = itemLocationX, + Y = itemLocationY, + Group = "Combat_Menu" }) + CreateTextBox({ + Id = screen.Components["StartingKeepsakeTextBox"].Id, + Text = " - Re-equip Starting Keepsake: ", + Color = Color.BoonPatchCommon, + FontSize = 16, + OffsetX = 0, OffsetY = 0, + Font = "AlegrayaSansSCRegular", + ShadowBlur = 0, ShadowColor = { 0, 0, 0, 1 }, ShadowOffset = { 0, 2 }, + Justification = "Left", + }) + screen.Components["StartingKeepsakeCheckBox"] = CreateScreenComponent({ + Name = "RadioButton", + Scale = 1, + X = itemLocationX + itemSpacingX, + Y = itemLocationY, + Group = "CombatMenu" + }) + screen.Components["StartingKeepsakeCheckBox"].Config = "QuickRestart.config.KeepStartingKeepsake" + screen.Components["StartingKeepsakeCheckBox"].OnPressedFunctionName = "HSMConfigMenu__ToggleGenericConfigCheckBox" + HSMConfigMenu__UpdateGenericConfigCheckbox(screen, screen.Components["StartingKeepsakeCheckBox"]) + itemLocationY = itemLocationY + itemSpacingY + + ----------------------------- + -- Quick Restart on Death + ----------------------------- + + screen.Components["QuickDeathTextBox"] = CreateScreenComponent({ + Name = "BlankObstacle", + Scale = 1, + X = itemLocationX, + Y = itemLocationY, + Group = "Combat_Menu" }) + CreateTextBox({ + Id = screen.Components["QuickDeathTextBox"].Id, + Text = "Enable Quick Death:", + Color = Color.BoonPatchCommon, + FontSize = 16, + OffsetX = 0, OffsetY = 0, + Font = "AlegrayaSansSCRegular", + ShadowBlur = 0, ShadowColor = { 0, 0, 0, 1 }, ShadowOffset = { 0, 2 }, + Justification = "Left" + }) + screen.Components["QuickDeathCheckBox"] = CreateScreenComponent({ + Name = "RadioButton", + Scale = 1, + X = itemLocationX + itemSpacingX, + Y = itemLocationY, + Group = "CombatMenu" + }) + screen.Components["QuickDeathCheckBox"].Config = "QuickRestart.config.QuickDeathEnabled" + screen.Components["QuickDeathCheckBox"].OnPressedFunctionName = "HSMConfigMenu__ToggleGenericConfigCheckBox" + HSMConfigMenu__UpdateGenericConfigCheckbox(screen, screen.Components["QuickDeathCheckBox"]) + itemLocationY = itemLocationY + itemSpacingY + + ----------------------------- + -- Charon Sack Control + ----------------------------- + + screen.Components["CharonSackControlTextBox"] = CreateScreenComponent({ + Name = "BlankObstacle", + Scale = 1, + X = itemLocationX, + Y = itemLocationY, + Group = "Combat_Menu" }) + CreateTextBox({ + Id = screen.Components["CharonSackControlTextBox"].Id, + Text = "Force Charon Sack Spawn: ", + Color = Color.BoonPatchCommon, + FontSize = 16, + OffsetX = 0, OffsetY = 0, + Font = "AlegrayaSansSCRegular", + ShadowBlur = 0, ShadowColor = { 0, 0, 0, 1 }, ShadowOffset = { 0, 2 }, + Justification = "Left", + }) + screen.Components["CharonSackControlCheckBox"] = CreateScreenComponent({ + Name = "RadioButton", + Scale = 1, + X = itemLocationX + itemSpacingX, + Y = itemLocationY, + Group = "CombatMenu" + }) + screen.Components["CharonSackControlCheckBox"].Config = "CharonSackControl.config.SpawnSack" + screen.Components["CharonSackControlCheckBox"].OnPressedFunctionName = "HSMConfigMenu__ToggleGenericConfigCheckBox" + HSMConfigMenu__UpdateGenericConfigCheckbox(screen, screen.Components["CharonSackControlCheckBox"]) + itemLocationY = itemLocationY + itemSpacingY ----------------------------- -- Right Column @@ -202,7 +296,6 @@ function HSMConfigMenu.CreateQolMenu(screen) HSMConfigMenu__UpdateGenericConfigCheckbox(screen, screen.Components["RtaTimerMultiRunCheckBox"]) itemLocationY = itemLocationY + itemSpacingY - screen.Components["RtaTimerResetButton"] = CreateScreenComponent({ Name = "MarketSlot", Scale = 1, @@ -224,8 +317,53 @@ function HSMConfigMenu.CreateQolMenu(screen) }) screen.Components["RtaTimerResetButton"].OnPressedFunctionName = "RtaTimer__ResetRtaTimer" itemLocationY = itemLocationY + itemSpacingY + + ----------------------------- + -- Hell Mode Toggle + ----------------------------- + screen.Components["HellModeToggleTextBox"] = CreateScreenComponent({ + Name = "BlankObstacle", + Scale = 1, + X = itemLocationX, + Y = itemLocationY, + Group = "Combat_Menu" }) + CreateTextBox({ + Id = screen.Components["HellModeToggleTextBox"].Id, + Text = "Turn Hell Mode On/Off:", + Color = Color.BoonPatchCommon, + FontSize = 16, + OffsetX = 0, OffsetY = 0, + Font = "AlegrayaSansSCRegular", + ShadowBlur = 0, ShadowColor = { 0, 0, 0, 1 }, ShadowOffset = { 0, 2 }, + Justification = "Left" + }) + itemLocationY = itemLocationY + itemSpacingY + + screen.Components["HellModeToggleButton"] = CreateScreenComponent({ + Name = "MarketSlot", + Scale = 1, + X = itemLocationX + itemSpacingX / 2 - 50, + Y = itemLocationY, + Group = "Combat_Menu" }) + SetScaleX({ Id = screen.Components["HellModeToggleButton"].Id, Fraction = .375}) + SetScaleY({ Id = screen.Components["HellModeToggleButton"].Id, Fraction = .5 }) + + CreateTextBox({ + Id = screen.Components["HellModeToggleButton"].Id, + Text = "Toggle Hell Mode", + Color = Color.BoonPatchCommon, + FontSize = 16, + OffsetX = 0, OffsetY = 0, + Font = "AlegrayaSansSCRegular", + ShadowBlur = 0, ShadowColor = { 0, 0, 0, 1 }, ShadowOffset = { 0, 2 }, + Justification = "Center" + }) + screen.Components["HellModeToggleButton"].OnPressedFunctionName = "HellModeToggle.ToggleHellMode" + itemLocationY = itemLocationY + itemSpacingY + end + ModUtil.LoadOnce(function() ModConfigMenu.RegisterMenuOverride({ModName = "QoL"}, HSMConfigMenu.CreateQolMenu) end) diff --git a/HadesSpeedrunningModpack/RngBalanceMenu.lua b/HadesSpeedrunningModpack/RngBalanceMenu.lua index f746036..df36772 100644 --- a/HadesSpeedrunningModpack/RngBalanceMenu.lua +++ b/HadesSpeedrunningModpack/RngBalanceMenu.lua @@ -1,6 +1,6 @@ HSMConfigMenu__UpdateGenericConfigCheckbox = function(screen, button) local radioButtonValue = "RadioButton_Unselected" - if ModUtil.SafeGet(_G, ModUtil.PathArray(button.Config)) then + if ModUtil.IndexArray.Get(_G, ModUtil.Path.IndexArray(button.Config)) then radioButtonValue = "RadioButton_Selected" end SetThingProperty({ @@ -27,13 +27,13 @@ function HSMConfigMenu__ToggleGenericConfigCheckBox(screen, button, overrideValu if button.Enabled == false then return end - local configPathArray = ModUtil.PathArray(button.Config) - local configValue = ModUtil.SafeGet(_G, configPathArray) + local configPathArray = ModUtil.Path.IndexArray(button.Config) + local configValue = ModUtil.IndexArray.Get(_G, configPathArray) if configValue ~= nil then if overrideValue ~= nil then - ModUtil.SafeSet(_G, configPathArray, overrideValue) + ModUtil.IndexArray.Set(_G, configPathArray, overrideValue) else - ModUtil.SafeSet(_G, configPathArray, not configValue) + ModUtil.IndexArray.Set(_G, configPathArray, not configValue) end HSMConfigMenu__UpdateGenericConfigCheckbox(screen, button) end @@ -411,9 +411,9 @@ function HSMConfigMenu.CreateRNGMenu( screen ) local minibossDropdownOptions = {["Default"] = { event = function(dropdown) - MinibossControl.config.MinibossSetting = "HyperDelivery" + MinibossControl.config.MinibossSetting = "Leaderboard" end, - Text = "HyperDelivery", + Text = "Leaderboard", }} for k, v in pairs(MinibossControl.Presets) do table.insert(minibossDropdownOptions, @@ -616,7 +616,7 @@ function HSMConfigMenu.CreateRNGMenu( screen ) itemLocationY = itemLocationY + itemSpacingY -- Hammers - screen.Components["HammerDeterminismTextBox"] = CreateScreenComponent({ + screen.Components["HammerControlTextBox"] = CreateScreenComponent({ Name = "BlankObstacle", Scale = 1, X = itemLocationX, @@ -624,8 +624,8 @@ function HSMConfigMenu.CreateRNGMenu( screen ) Group = "Combat_Menu" }) CreateTextBox({ - Id = screen.Components["HammerDeterminismTextBox"].Id, - Text = "Deterministic Hammers by Seed: ", + Id = screen.Components["HammerControlTextBox"].Id, + Text = "Set Hammers by Aspect: ", Color = Color.BoonPatchCommon, FontSize = 16, OffsetX = 0, OffsetY = 0, @@ -633,95 +633,18 @@ function HSMConfigMenu.CreateRNGMenu( screen ) ShadowBlur = 0, ShadowColor = { 0, 0, 0, 1 }, ShadowOffset = { 0, 2 }, Justification = "Left" }) - screen.Components["HammerDeterminismCheckBox"] = CreateScreenComponent({ + screen.Components["HammerControlCheckBox"] = CreateScreenComponent({ Name = "RadioButton", Scale = 1, X = itemLocationX + itemSpacingX, Y = itemLocationY, Group = "CombatMenu" }) - screen.Components["HammerDeterminismCheckBox"].Config = "FixedHammers.config.Enabled" - screen.Components["HammerDeterminismCheckBox"].Children = {{Textbox = "AchillesRebalanceTextBox", Checkbox = "AchillesRebalanceCheckBox"}, {Textbox = "NemesisRebalanceTextBox", Checkbox = "NemesisRebalanceCheckBox"}} - screen.Components["HammerDeterminismCheckBox"].OnPressedFunctionName = "HSMConfigMenu__ToggleGenericConfigCheckBox" + screen.Components["HammerControlCheckBox"].Config = "RunStartControl.config.Enabled" + screen.Components["HammerControlCheckBox"].OnPressedFunctionName = "HSMConfigMenu__ToggleGenericConfigCheckBox" itemLocationY = itemLocationY + itemSpacingY - screen.Components["AchillesRebalanceTextBox"] = CreateScreenComponent({ - Name = "BlankObstacle", - Scale = 1, - X = itemLocationX, - Y = itemLocationY, - Group = "Combat_Menu" - }) - CreateTextBox({ - Id = screen.Components["AchillesRebalanceTextBox"].Id, - Text = " - Achilles Hammer Rebalance: ", - Color = Color.BoonPatchCommon, - FontSize = 16, - OffsetX = 0, OffsetY = 0, - Font = "AlegrayaSansSCRegular", - ShadowBlur = 0, ShadowColor = { 0, 0, 0, 1 }, ShadowOffset = { 0, 2 }, - Justification = "Left" - }) - screen.Components["AchillesRebalanceCheckBox"] = CreateScreenComponent({ - Name = "RadioButton", - Scale = 1, - X = itemLocationX + itemSpacingX, - Y = itemLocationY, - Group = "CombatMenu" - }) - screen.Components["AchillesRebalanceCheckBox"].Config = "FixedHammers.config.AchillesRebalance" - screen.Components["AchillesRebalanceCheckBox"].OnPressedFunctionName = "HSMConfigMenu__ToggleGenericConfigCheckBox" - CreateTextBox({ - Id = screen.Components["AchillesRebalanceTextBox"].Id, - Text = "(First Hammer always includes Flurry Jab) ", - Color = Color.BoonPatchCommon, - FontSize = 10, - OffsetX = 38, OffsetY = 22, - Font = "AlegrayaSansSCRegular", - ShadowBlur = 0, ShadowColor = { 0, 0, 0, 1 }, ShadowOffset = { 0, 2 }, - Justification = "Left", - }) - itemLocationY = itemLocationY + itemSpacingY - - screen.Components["NemesisRebalanceTextBox"] = CreateScreenComponent({ - Name = "BlankObstacle", - Scale = 1, - X = itemLocationX, - Y = itemLocationY, - Group = "Combat_Menu" - }) - CreateTextBox({ - Id = screen.Components["NemesisRebalanceTextBox"].Id, - Text = " - Nemesis Hammer Rebalance: ", - Color = Color.BoonPatchCommon, - FontSize = 16, - OffsetX = 0, OffsetY = 0, - Font = "AlegrayaSansSCRegular", - ShadowBlur = 0, ShadowColor = { 0, 0, 0, 1 }, ShadowOffset = { 0, 2 }, - Justification = "Left" - }) - screen.Components["NemesisRebalanceCheckBox"] = CreateScreenComponent({ - Name = "RadioButton", - Scale = 1, - X = itemLocationX + itemSpacingX, - Y = itemLocationY, - Group = "CombatMenu" - }) - screen.Components["NemesisRebalanceCheckBox"].Config = "FixedHammers.config.NemesisRebalance" - screen.Components["NemesisRebalanceCheckBox"].OnPressedFunctionName = "HSMConfigMenu__ToggleGenericConfigCheckBox" - CreateTextBox({ - Id = screen.Components["NemesisRebalanceTextBox"].Id, - Text = "(First Hammer always includes Double Edge) ", - Color = Color.BoonPatchCommon, - FontSize = 10, - OffsetX = 38, OffsetY = 22, - Font = "AlegrayaSansSCRegular", - ShadowBlur = 0, ShadowColor = { 0, 0, 0, 1 }, ShadowOffset = { 0, 2 }, - Justification = "Left", - }) - HSMConfigMenu__UpdateGenericConfigCheckbox(screen, screen.Components["HammerDeterminismCheckBox"]) - HSMConfigMenu__UpdateGenericConfigCheckbox(screen, screen.Components["AchillesRebalanceCheckBox"]) - HSMConfigMenu__UpdateGenericConfigCheckbox(screen, screen.Components["NemesisRebalanceCheckBox"]) + HSMConfigMenu__UpdateGenericConfigCheckbox(screen, screen.Components["HammerControlCheckBox"]) itemLocationY = itemLocationY + itemSpacingY end diff --git a/HadesSpeedrunningModpack/SettingsHashMenu.lua b/HadesSpeedrunningModpack/SettingsHashMenu.lua index 3c1980d..f0f18cc 100644 --- a/HadesSpeedrunningModpack/SettingsHashMenu.lua +++ b/HadesSpeedrunningModpack/SettingsHashMenu.lua @@ -6,15 +6,16 @@ HSMConfigMenu.RulesetSettings = { {Key = "DoorVisualIndicators.config.ShowMinibossDoorIndicator", Values = {false, true}, Default = false}, {Key = "DoorVisualIndicators.config.ShowFountainDoorIndictor", Values = {false, true}, Default = true}, - {Key = "EllosBoonSelectorMod.config.ShowPreview", Values = {false, true}, Default = true}, + {Key = "EllosBoonSelectorMod.config.ShowPreview", Values = {false, true}, Default = false}, - {Key = "FixedHammers.config.Enabled", Values = {false, true}, Default = true}, - {Key = "FixedHammers.config.AchillesRebalance", Values = {false, true}, Default = true}, - {Key = "FixedHammers.config.NemesisRebalance", Values = {false, true}, Default = true}, + {Key = "RunStartControl.config.Enabled", Values = {false, true}, Default = true}, {Key = "InteractableChaos.config.Enabled", Values = {false, true}, Default = false}, - {Key = "MinibossControl.config.MinibossSetting", Values = {"Vanilla", "HyperDelivery"}, Default = "HyperDelivery"}, + {Key = "MinibossControl.config.MinibossSetting", Values = {"Vanilla", "HyperDelivery1", "HyperDelivery", "Leaderboard"}, Default = "Leaderboard"}, + + {Key = "RemoveCutscenes.config.RemoveIntro", Values = {false, true}, Default = true}, + {Key = "RemoveCutscenes.config.RemoveOutro", Values = {false, true}, Default = true}, {Key = "RoomDeterminism.config.Enabled", Values = {false, true}, Default = false}, {Key = "RoomDeterminism.config.RoomGenerationAlgorithm", Values = {"Vanilla"}, Default = "Vanilla"}, @@ -23,33 +24,39 @@ HSMConfigMenu.RulesetSettings = { {Key = "SatyrSackControl.config.MinSack", Values = {1, 2, 3, 4, 5}, Default = 2}, {Key = "SatyrSackControl.config.MaxSack", Values = {1, 2, 3, 4, 5}, Default = 2}, - {Key = "ThanatosControl.config.ThanatosSetting", Values = {"Vanilla", "Rebalanced", "Removed"}, Default = "Removed"}, + {Key = "ShowChamberNumber.config.ShowDepth", Values = {false, true}, Default = true}, - {Key = "EllosBoonSelectorMod.config.ShowPreview", Values = {false, true}, Default = true}, + {Key = "ThanatosControl.config.ThanatosSetting", Values = {"Vanilla", "Removed"}, Default = "Removed"}, } +HSMConfigMenu.MultiRunSettings = DeepCopyTable(HSMConfigMenu.RulesetSettings) +HSMConfigMenu.SingleRunSettings = DeepCopyTable(HSMConfigMenu.RulesetSettings) +for i, setting in ipairs(HSMConfigMenu.SingleRunSettings) do + if setting.Key == "ThanatosControl.config.ThanatosSetting" then + setting.Default = "Vanilla" + break + end +end + HSMConfigMenu.NonRulesetSettings = { {Key = "QuickRestart.config.Enabled", Values = {false, true}, Default = false}, - {Key = "RemoveCutscenes.config.RemoveIntro", Values = {false, true}, Default = true}, - {Key = "RemoveCutscenes.config.RemoveOutro", Values = {false, true}, Default = true}, - {Key = "ColorblindMod.config.TartarusEnabled", Values = {false, true}, Default = false}, {Key = "ColorblindMod.config.AsphodelEnabled", Values = {false, true}, Default = false}, {Key = "ColorblindMod.config.ElysiumEnabled", Values = {false, true}, Default = false}, {Key = "ColorblindMod.config.StyxEnabled", Values = {false, true}, Default = false}, - {Key = "ShowChamberNumber.config.ShowDepth", Values = {false, true}, Default = true}, - - {Key = "RtaTimer.config.DisplayTimer", Values = {false, true}, Default = true}, - {Key = "RtaTimer.config.MultiWeapon", Values = {false, true}, Default = true}, + {Key = "RtaTimer.config.DisplayTimer", Values = {false, true}, Default = false}, + {Key = "RtaTimer.config.MultiWeapon", Values = {false, true}, Default = false}, {Key = "EmoteMod.config.Enabled", Values = {false, true}, Default = false}, } HSMConfigMenu.SettingsDefaults = { - RulesetSettings = 780877, - NonRulesetSettings = 782 + RulesetSettings = 769319, + MultiRunSettings = 769319, + SingleRunSettings = 769318, + NonRulesetSettings = 0 } -- Register Room Override @@ -149,15 +156,32 @@ function HSMConfigMenu.CreateSettingsHashMenu( screen ) Font = "AlegrayaSansSCRegular", Items = { ["Default"] = {Text = "Select a Preset", event = function() end}, - {Text = "Default Ruleset", event = function() - HSMConfigMenu.LoadSettings("RulesetSettings") - local rulesetHashInt = CalculateHash(HSMConfigMenu.RulesetSettings, _G) + {Text = "Real Time (RTA) Ruleset", event = function() + HSMConfigMenu.LoadSettings("MultiRunSettings") + local rulesetHashInt = CalculateHash(HSMConfigMenu.MultiRunSettings, _G) + local rulesetHash = HSMConfigMenu.ConvertIntToBase25(rulesetHashInt, 5) + HSMConfigMenu.CurrentRulesetHash = rulesetHash + + for i = 1, #rulesetHash do + SetAnimation({ Name = HSMConfigMenu.HashImages[rulesetHash[i]], DestinationId = screen.Components["RulesetHashImage" .. i].Id, OffsetX = 0, OffsetY = 0}) + end + + HSMConfigMenu.updateRulesetHashDisplay() + HSMConfigMenu.SaveSettingsToGlobal() + end}, + {Text = "In-Game Time (IGT) Ruleset", event = function() + HSMConfigMenu.LoadSettings("SingleRunSettings") + local rulesetHashInt = CalculateHash(HSMConfigMenu.SingleRunSettings, _G) local rulesetHash = HSMConfigMenu.ConvertIntToBase25(rulesetHashInt, 5) + HSMConfigMenu.CurrentRulesetHash = rulesetHash + for i = 1, #rulesetHash do SetAnimation({ Name = HSMConfigMenu.HashImages[rulesetHash[i]], DestinationId = screen.Components["RulesetHashImage" .. i].Id, OffsetX = 0, OffsetY = 0}) end + + HSMConfigMenu.updateRulesetHashDisplay() + HSMConfigMenu.SaveSettingsToGlobal() end}, - {Text = "Any Heat Speedrun Ruleset v1.0", IsEnabled = false, event = function() end} }, }) itemLocationY = itemLocationY + 100 @@ -213,7 +237,7 @@ end function GetConfigBits(parentTable, config) for i, val in ipairs(config.Values) do - local currentConfigVal = ModUtil.SafeGet(parentTable, ModUtil.PathArray(config.Key)) + local currentConfigVal = ModUtil.IndexArray.Get(parentTable, ModUtil.Path.IndexArray(config.Key)) if currentConfigVal == nil then currentConfigVal = config.Default -- One liner will not work for boolean values as it will be interpreted as a logical or end @@ -232,7 +256,7 @@ function SetConfigBits(parentTable, config, configBits) setValue = config.Default -- One liner will not work for boolean values as it will be interpreted as a logical or end - ModUtil.SafeSet(parentTable, ModUtil.PathArray(config.Key), setValue) + ModUtil.IndexArray.Set(parentTable, ModUtil.Path.IndexArray(config.Key), setValue) end function CalculateHash(settings, parentTable) diff --git a/HellModeToggle b/HellModeToggle new file mode 160000 index 0000000..8942977 --- /dev/null +++ b/HellModeToggle @@ -0,0 +1 @@ +Subproject commit 89429779afa365031b74cc9f79090c5ada2324a5 diff --git a/InteractableChaos/InteractableChaos.lua b/InteractableChaos/InteractableChaos.lua index 9018b9f..d7b7d62 100644 --- a/InteractableChaos/InteractableChaos.lua +++ b/InteractableChaos/InteractableChaos.lua @@ -1,4 +1,4 @@ -ModUtil.RegisterMod("InteractableChaos") +ModUtil.Mod.Register("InteractableChaos") local config = { ModName = "Interactable Chaos", @@ -7,7 +7,7 @@ local config = { InteractableChaos.config = config -- Spawn the chaos interactable and prevent interaction with the normal chaos gate -ModUtil.WrapBaseFunction("DoUnlockRoomExits", function ( baseFunc, run, room ) +ModUtil.Path.Wrap("DoUnlockRoomExits", function ( baseFunc, run, room ) baseFunc(run, room) if not config.Enabled then @@ -34,7 +34,7 @@ ModUtil.WrapBaseFunction("DoUnlockRoomExits", function ( baseFunc, run, room ) end, InteractableChaos) -- Prevent chaos gates from unlocking visually -ModUtil.WrapBaseFunction("ExitDoorUnlockedPresentation", function ( baseFunc, exitDoor ) +ModUtil.Path.Wrap("ExitDoorUnlockedPresentation", function ( baseFunc, exitDoor ) -- If this is a chaos gate, do not unlock if exitDoor.Name == "SecretDoor" and config.Enabled then return @@ -45,7 +45,7 @@ ModUtil.WrapBaseFunction("ExitDoorUnlockedPresentation", function ( baseFunc, ex end, InteractableChaos) -- Handling for picking up the special chaos interactable -ModUtil.WrapBaseFunction("HandleLootPickup", function ( baseFunc, currentRun, loot ) +ModUtil.Path.Wrap("HandleLootPickup", function ( baseFunc, currentRun, loot ) if loot.IsInteractableChaosLoot and config.Enabled then -- Calculate health cost and reduce it to 0 if chaos egg is equipped @@ -62,6 +62,9 @@ ModUtil.WrapBaseFunction("HandleLootPickup", function ( baseFunc, currentRun, lo Damage( CurrentRun.Hero, { triggeredById = CurrentRun.Hero.ObjectId, DamageAmount = healthCost, PureDamage = true } ) end baseFunc(currentRun, loot) + -- Removing Chaos visual artifacts + AdjustRadialBlurDistance({ Fraction = 0, Duration = 2 }) + AdjustRadialBlurStrength({ Fraction = 0, Duration = 2 }) return -- If Zag does not have enough hp, push him away and show a text box saying why he cannot pick up the boon diff --git a/LootChoiceExt/LootChoiceExt.lua b/LootChoiceExt/LootChoiceExt.lua index 89b0260..74fa32b 100644 --- a/LootChoiceExt/LootChoiceExt.lua +++ b/LootChoiceExt/LootChoiceExt.lua @@ -1,4 +1,4 @@ -ModUtil.RegisterMod("LootChoiceExt") +ModUtil.Mod.Register("LootChoiceExt") local config = { MinExtraLootChoices = 0, @@ -19,18 +19,18 @@ OnAnyLoad{ function() LootChoiceExt.LastLootChoices = LootChoiceExt.Choices + RandomInt( config.MinExtraLootChoices, config.MaxExtraLootChoices ) end} -ModUtil.BaseOverride("GetTotalLootChoices", function() +ModUtil.Path.Override("GetTotalLootChoices", function() return LootChoiceExt.LastLootChoices end, LootChoiceExt) -ModUtil.BaseOverride("CalcNumLootChoices", function() +ModUtil.Path.Override("CalcNumLootChoices", function() local numChoices = LootChoiceExt.LastLootChoices - GetNumMetaUpgrades("ReducedLootChoicesShrineUpgrade") return numChoices end, LootChoiceExt) -ModUtil.BaseOverride("CreateBoonLootButtons", function( lootData ) +ModUtil.Path.Override("CreateBoonLootButtons", function( lootData, reroll ) - -- BASE CODE ... + -- BASE CODE ... (From Live Steam version v1.37996, pulled on 2021.04.27) local components = ScreenAnchors.ChoiceScreen.Components local upgradeName = lootData.Name local upgradeChoiceData = LootData[upgradeName] @@ -43,7 +43,9 @@ ModUtil.BaseOverride("CreateBoonLootButtons", function( lootData ) if not lootData.StackNum then lootData.StackNum = 1 end - lootData.StackNum = lootData.StackNum + GetTotalHeroTraitValue("PomLevelBonus") + if not reroll then + lootData.StackNum = lootData.StackNum + GetTotalHeroTraitValue("PomLevelBonus") + end local tooltipData = {} local itemLocationY = 370 @@ -105,6 +107,13 @@ ModUtil.BaseOverride("CreateBoonLootButtons", function( lootData ) local squashY = 3/(3+excess) local fsquashT1 = 0 if excess < 3 then fsquashT1 = 1 end + local iconScaling + if excess == 0 then + iconScaling = 0.85 + else + iconScaling = 1.0 + end + for itemIndex, itemData in ipairs( upgradeOptions ) do local squashT1 = fsquashT1 @@ -112,7 +121,7 @@ ModUtil.BaseOverride("CreateBoonLootButtons", function( lootData ) components[itemBackingKey] = CreateScreenComponent({ Name = "TraitBacking", Group = "Combat_Menu", X = ScreenCenterX, Y = itemLocationY }) SetScaleY({ Id = components[itemBackingKey].Id, Fraction = 1.25*squashY }) - -- BASE CODE ... + -- BASE CODE ...(From Live Steam version v1.37996, pulled on 2021.04.27) local upgradeData = nil local upgradeTitle = nil @@ -124,11 +133,11 @@ ModUtil.BaseOverride("CreateBoonLootButtons", function( lootData ) upgradeTitle = "TraitLevel_Upgrade" upgradeData.Title = upgradeData.Name else - upgradeTitle = upgradeData.Name + upgradeTitle = GetTraitTooltipTitle( TraitData[itemData.ItemName] ) - upgradeData.Title = upgradeData.Name .."_Initial" + upgradeData.Title = GetTraitTooltipTitle( TraitData[itemData.ItemName] ) .."_Initial" if not HasDisplayName({ Text = upgradeData.Title }) then - upgradeData.Title = upgradeData.Name + upgradeData.Title = GetTraitTooltipTitle( TraitData[itemData.ItemName] ) end end @@ -139,15 +148,24 @@ ModUtil.BaseOverride("CreateBoonLootButtons", function( lootData ) tooltipData = GetProcessedTraitData({ Unit = CurrentRun.Hero, TraitName = itemData.ItemName, FakeStackNum = existingNum, RarityMultiplier = upgradeData.RarityMultiplier}) if existingNum > 1 then upgradeTitle = "TraitLevel_Exchange" - tooltipData.Title = upgradeData.Name + tooltipData.Title = GetTraitTooltipTitle( TraitData[upgradeData.Name]) tooltipData.Level = existingNum end elseif lootData.StackOnly then tooltipData = GetProcessedTraitData({ Unit = CurrentRun.Hero, TraitName = itemData.ItemName, FakeStackNum = lootData.StackNum, RarityMultiplier = upgradeData.RarityMultiplier}) tooltipData.OldLevel = traitNum; tooltipData.NewLevel = traitNum + lootData.StackNum; - tooltipData.Title = upgradeData.Name + tooltipData.Title = GetTraitTooltipTitle( TraitData[itemData.ItemName] ) + upgradeData.Title = tooltipData.Title else + if upgradeData.Rarity == "Legendary" then + if TraitData[upgradeData.Name].IsDuoBoon then + CreateAnimation({ Name = "BoonEntranceDuo", DestinationId = components[itemBackingKey].Id }) + else + CreateAnimation({ Name = "BoonEntranceLegendary", DestinationId = components[itemBackingKey].Id }) + end + end + tooltipData = upgradeData end SetTraitTextData( tooltipData ) @@ -158,7 +176,7 @@ ModUtil.BaseOverride("CreateBoonLootButtons", function( lootData ) upgradeData = GetRampedConsumableData(ConsumableData[itemData.ItemName], itemData.Rarity) upgradeTitle = upgradeData.Name - upgradeDescription = upgradeTitle + upgradeDescription = GetTraitTooltip(upgradeData) if upgradeData.UseFunctionArgs ~= nil then if upgradeData.UseFunctionName ~= nil and upgradeData.UseFunctionArgs.TraitName ~= nil then @@ -224,20 +242,20 @@ ModUtil.BaseOverride("CreateBoonLootButtons", function( lootData ) -- NEW CODE ... - local iconOffsetX = -323 + local iconOffsetX = -338 --(From Live Steam version v1.37996, pulled on 2021.04.27) local iconOffsetY = -2*squashY local exchangeIconPrefix = nil - local overlayLayer = "Combat_Menu_Overlay" + local overlayLayer = "Combat_Menu_Overlay_Backing" --(From Live Steam version v1.37996, pulled on 2021.04.27) components[purchaseButtonKey] = CreateScreenComponent({ Name = "BoonSlot"..RandomInt(1,3), Group = "Combat_Menu", Scale = 1, X = itemLocationX + buttonOffsetX, Y = itemLocationY }) SetScaleY({Id = components[purchaseButtonKey].Id, Fraction = squashY}) if upgradeData.CustomRarityColor then - components[purchaseButtonKey.."Patch"] = CreateScreenComponent({ Name = "BlankObstacle", Group = "Combat_Menu", X = iconOffsetX + itemLocationX + buttonOffsetX + 15, Y = iconOffsetY + itemLocationY }) + components[purchaseButtonKey.."Patch"] = CreateScreenComponent({ Name = "BlankObstacle", Group = "Combat_Menu", X = iconOffsetX + itemLocationX + buttonOffsetX + 38, Y = iconOffsetY + itemLocationY }) SetAnimation({ DestinationId = components[purchaseButtonKey.."Patch"].Id, Name = "BoonRarityPatch"}) SetColor({ Id = components[purchaseButtonKey.."Patch"].Id, Color = upgradeData.CustomRarityColor }) SetScaleY({Id = components[purchaseButtonKey.."Patch"].Id, Fraction = squashY}) elseif itemData.Rarity ~= "Common" then - components[purchaseButtonKey.."Patch"] = CreateScreenComponent({ Name = "BlankObstacle", Group = "Combat_Menu", X = iconOffsetX + itemLocationX + buttonOffsetX + 15, Y = iconOffsetY + itemLocationY }) + components[purchaseButtonKey.."Patch"] = CreateScreenComponent({ Name = "BlankObstacle", Group = "Combat_Menu", X = iconOffsetX + itemLocationX + buttonOffsetX + 38, Y = iconOffsetY + itemLocationY }) SetAnimation({ DestinationId = components[purchaseButtonKey.."Patch"].Id, Name = "BoonRarityPatch"}) SetColor({ Id = components[purchaseButtonKey.."Patch"].Id, Color = Color["BoonPatch" .. itemData.Rarity] }) SetScaleY({Id = components[purchaseButtonKey.."Patch"].Id, Fraction = squashY}) @@ -251,7 +269,7 @@ ModUtil.BaseOverride("CreateBoonLootButtons", function( lootData ) Text = "ReducedLootChoicesKeyword", OffsetX = textOffset, OffsetY = -30*squashY, Color = Color.Transparent, - Width = 615, + Width = 675, --(From Live Steam version v1.37996, pulled on 2021.04.27) }) thread( TraitLockedPresentation, { squashY = squashY, squashTI = squashTI, Components = components, Id = purchaseButtonKey, OffsetX = itemLocationX + buttonOffsetX, OffsetY = iconOffsetY + itemLocationY }) end @@ -259,8 +277,8 @@ ModUtil.BaseOverride("CreateBoonLootButtons", function( lootData ) if upgradeData.Icon ~= nil then components[purchaseButtonKey.."Icon"] = CreateScreenComponent({ Name = "BlankObstacle", Group = "Combat_Menu", X = iconOffsetX + itemLocationX + buttonOffsetX, Y = iconOffsetY + itemLocationY }) SetAnimation({ DestinationId = components[purchaseButtonKey.."Icon"].Id, Name = upgradeData.Icon .. "_Large" }) - SetScaleY({Id = components[purchaseButtonKey.."Icon"].Id, Fraction = squashY}) - SetScaleX({Id = components[purchaseButtonKey.."Icon"].Id, Fraction = squashY}) + SetScaleY({Id = components[purchaseButtonKey.."Icon"].Id, Fraction = iconScaling*squashY}) + SetScaleX({Id = components[purchaseButtonKey.."Icon"].Id, Fraction = iconScaling*squashY}) end local locScaleModifiers = @@ -304,7 +322,7 @@ ModUtil.BaseOverride("CreateBoonLootButtons", function( lootData ) OffsetX = textOffset, OffsetY = -12*squashY*squashT1 - blockedIconOffset, FontSize = 20, Color = {160, 160, 160, 255}, - Width = 615, + Width = 675, --(From Live Steam version v1.37996, pulled on 2021.04.27) Font = "AlegreyaSansSCRegular", ShadowBlur = 0, ShadowColor = {0,0,0,1}, ShadowOffset={0, 2}, Justification = "Left", @@ -317,7 +335,7 @@ ModUtil.BaseOverride("CreateBoonLootButtons", function( lootData ) OffsetX = textOffset + 150, OffsetY = -12*squashY*squashT1 - blockedIconOffset, FontSize = 20, Color = Color["BoonPatch" .. itemData.OldRarity], - Width = 615, + Width = 675, --(From Live Steam version v1.37996, pulled on 2021.04.27) Font = "AlegreyaSansSCRegular", ShadowBlur = 0, ShadowColor = {0,0,0,1}, ShadowOffset={0, 2}, Justification = "Left", @@ -327,16 +345,16 @@ ModUtil.BaseOverride("CreateBoonLootButtons", function( lootData ) end components[purchaseButtonKey.."Frame"] = CreateScreenComponent({ Name = "BlankObstacle", Group = "Combat_Menu", X = iconOffsetX + itemLocationX + buttonOffsetX, Y = iconOffsetY + itemLocationY }) - SetScaleY({Id = components[purchaseButtonKey.."Frame"].Id, Fraction = squashY}) - SetScaleX({Id = components[purchaseButtonKey.."Frame"].Id, Fraction = squashY}) + SetScaleY({Id = components[purchaseButtonKey.."Frame"].Id, Fraction = iconScaling*squashY}) + SetScaleX({Id = components[purchaseButtonKey.."Frame"].Id, Fraction = iconScaling*squashY}) if upgradeData.Frame then SetAnimation({ DestinationId = components[purchaseButtonKey.."Frame"].Id, Name = "Frame_Boon_Menu_".. upgradeData.Frame}) - SetScaleY({Id = components[purchaseButtonKey.."Frame"].Id, Fraction = squashY}) - SetScaleX({Id = components[purchaseButtonKey.."Frame"].Id, Fraction = squashY}) + SetScaleY({Id = components[purchaseButtonKey.."Frame"].Id, Fraction = iconScaling*squashY}) + SetScaleX({Id = components[purchaseButtonKey.."Frame"].Id, Fraction = iconScaling*squashY}) else SetAnimation({ DestinationId = components[purchaseButtonKey.."Frame"].Id, Name = "Frame_Boon_Menu_".. itemData.Rarity}) - SetScaleY({Id = components[purchaseButtonKey.."Frame"].Id, Fraction = squashY}) - SetScaleX({Id = components[purchaseButtonKey.."Frame"].Id, Fraction = squashY}) + SetScaleY({Id = components[purchaseButtonKey.."Frame"].Id, Fraction = iconScaling*squashY}) + SetScaleX({Id = components[purchaseButtonKey.."Frame"].Id, Fraction = iconScaling*squashY}) end -- Button data setup components[purchaseButtonKey].OnPressedFunctionName = "HandleUpgradeChoiceSelection" @@ -349,7 +367,7 @@ ModUtil.BaseOverride("CreateBoonLootButtons", function( lootData ) components[components[purchaseButtonKey].Id] = purchaseButtonKey -- Creates upgrade slot text - SetInteractProperty({ DestinationId = components[purchaseButtonKey].Id, Property = "TooltipOffsetX", Value = 640 }) + SetInteractProperty({ DestinationId = components[purchaseButtonKey].Id, Property = "TooltipOffsetX", Value = 675 }) --(From Live Steam version v1.37996, pulled on 2021.04.27) local selectionString = "UpgradeChoiceMenu_PermanentItem" local selectionStringColor = Color.Black @@ -362,7 +380,7 @@ ModUtil.BaseOverride("CreateBoonLootButtons", function( lootData ) selectionString = upgradeData.UpgradeChoiceText or "UpgradeChoiceMenu_PermanentItem" end - local textOffset = 135 - buttonOffsetX + local textOffset = 115 - buttonOffsetX --(From Live Steam version v1.37996, pulled on 2021.04.27) local exchangeIconOffset = 0 local lineSpacing = 8*squashY local text = "Boon_"..tostring(itemData.Rarity) @@ -376,8 +394,8 @@ ModUtil.BaseOverride("CreateBoonLootButtons", function( lootData ) end CreateTextBox({ Id = components[purchaseButtonKey].Id, Text = text , - FontSize = 25, - OffsetX = textOffset + 600, OffsetY = -60*squashY*squashT1, + FontSize = 27, --(From Live Steam version v1.37996, pulled on 2021.04.27) + OffsetX = textOffset + 630, OffsetY = -60*squashY*squashT1, --(From Live Steam version v1.37996, pulled on 2021.04.27), minus the squashes Width = 720, Color = color, Font = "AlegreyaSansSCLight", @@ -387,7 +405,7 @@ ModUtil.BaseOverride("CreateBoonLootButtons", function( lootData ) if exchangeIconPrefix then CreateTextBox({ Id = components[purchaseButtonKey].Id, Text = exchangeIconPrefix , - FontSize = 25, + FontSize = 27, --(From Live Steam version v1.37996, pulled on 2021.04.27) OffsetX = textOffset, OffsetY = -55*squashY*squashT1, Color = color, Font = "AlegreyaSansSCLight", @@ -402,7 +420,7 @@ ModUtil.BaseOverride("CreateBoonLootButtons", function( lootData ) end CreateTextBox({ Id = components[purchaseButtonKey].Id, Text = upgradeTitle, - FontSize = 25, + FontSize = 27, --(From Live Steam version v1.37996, pulled on 2021.04.27) OffsetX = textOffset + exchangeIconOffset, OffsetY = -55*squashY*squashT1, Color = color, Font = "AlegreyaSansSCLight", @@ -411,17 +429,24 @@ ModUtil.BaseOverride("CreateBoonLootButtons", function( lootData ) LuaKey = "TooltipData", LuaValue = tooltipData, }) + -- (From Live Steam version v1.37996, pulled on 2021.04.27) + -- Chaos curse/blessing traits need VariableAutoFormat disabled + local autoFormat = "BoldFormatGraft" + if upgradeDescription == "ChaosBlessingFormat" or itemData.Type == "TransformingTrait" then + autoFormat = nil + end + CreateTextBoxWithFormat(MergeTables({ Id = components[purchaseButtonKey].Id, Text = upgradeDescription, OffsetX = textOffset+2*ScreenCenterX*(1-fsquashT1), OffsetY = -30*squashY, - Width = 615, + Width = GetLocalizedValue(675, { { Code = "ja", Value = 670 }, }), -- (From Live Steam version v1.37996, pulled on 2021.04.27) Justification = "Left", VerticalJustification = "Top", LineSpacingBottom = lineSpacing, UseDescription = true, LuaKey = "TooltipData", LuaValue = tooltipData, Format = "BaseFormat", - VariableAutoFormat = "BoldFormatGraft", + VariableAutoFormat = autoFormat, -- (From Live Steam version v1.37996, pulled on 2021.04.27) TextSymbolScale = 0.8*squashY, }, locScaleModifiers)) @@ -433,7 +458,7 @@ ModUtil.BaseOverride("CreateBoonLootButtons", function( lootData ) end if needsQuestIcon then - components[purchaseButtonKey.."QuestIcon"] = CreateScreenComponent({ Name = "BlankObstacle", Group = "Combat_Menu", X = itemLocationX + 112, Y = itemLocationY - 55*squashY*squashT1 }) + components[purchaseButtonKey.."QuestIcon"] = CreateScreenComponent({ Name = "BlankObstacle", Group = "Combat_Menu", X = itemLocationX + 92, Y = itemLocationY - 55*squashY*squashT1 }) ---- (From Live Steam version v1.37996, pulled on 2021.04.27), except for squashing SetAnimation({ DestinationId = components[purchaseButtonKey.."QuestIcon"].Id, Name = "QuestItemFound" }) -- Silent toolip CreateTextBox({ Id = components[purchaseButtonKey].Id, TextSymbolScale = 0, Text = "TraitQuestItem", Color = Color.Transparent, LuaKey = "TooltipData", LuaValue = tooltipData, }) @@ -453,8 +478,8 @@ ModUtil.BaseOverride("CreateBoonLootButtons", function( lootData ) end -- BASE CODE ... - - if IsMetaUpgradeActive("RerollPanelMetaUpgrade") then + -- (From Live Steam version v1.37996, pulled on 2021.04.27) + if IsMetaUpgradeSelected( "RerollPanelMetaUpgrade" ) then local cost = -1 if lootData.BlockReroll then cost = -1 @@ -503,7 +528,7 @@ ModUtil.BaseOverride("CreateBoonLootButtons", function( lootData ) end, LootChoiceExt) -ModUtil.BaseOverride("DestroyBoonLootButtons", function( lootData ) +ModUtil.Path.Override("DestroyBoonLootButtons", function( lootData ) local components = ScreenAnchors.ChoiceScreen.Components local toDestroy = {} for index = 1, GetTotalLootChoices() do diff --git a/MinibossControl/MinibossControl.lua b/MinibossControl/MinibossControl.lua index 5c6e496..b8463d8 100644 --- a/MinibossControl/MinibossControl.lua +++ b/MinibossControl/MinibossControl.lua @@ -6,10 +6,10 @@ Change the proportions of miniboss chambers, allowing minibosses to be removed or replaced with others. ]] -ModUtil.RegisterMod("MinibossControl") +ModUtil.Mod.Register("MinibossControl") local config = { - MinibossSetting = "HyperDelivery" + MinibossSetting = "Leaderboard" } MinibossControl.config = config @@ -23,6 +23,13 @@ MinibossControl.Presets = { -- Tartarus_Sneak A_MiniBoss03 = 1, + --Megaera + A_Boss01 = true, + --Alecto + A_Boss02 = true, + --Tisiphone + A_Boss03 = true, + -- Asphodel_Barge B_Wrapping01 = 1, -- Asphodel_PowerCouple, @@ -38,12 +45,24 @@ MinibossControl.Presets = { -- If true, Tiny Vermin will not spawn RemoveTinyVermin = false, }, - HyperDelivery = { + HyperDelivery1 = { + -- Tartarus_Bombers A_MiniBoss01 = 1, + -- Tartarus_Doomstone TODO: Handle middle management? A_MiniBoss04 = 1, + -- Tartarus_Sneak A_MiniBoss03 = 1, + --Megaera + A_Boss01 = true, + --Alecto + A_Boss02 = true, + --Tisiphone + A_Boss03 = true, + + -- Asphodel_Barge B_Wrapping01 = 0, + -- Asphodel_PowerCouple, B_MiniBoss01 = 1, -- Asphodel_Witches B_MiniBoss02 = 2, @@ -53,6 +72,96 @@ MinibossControl.Presets = { -- Elysium_Asterius C_MiniBoss01 = 1, + -- If true, Tiny Vermin will not spawn + RemoveTinyVermin = true, + }, + HyperDelivery = { + -- Tartarus_Bombers + A_MiniBoss01 = 1, + -- Tartarus_Doomstone TODO: Handle middle management? + A_MiniBoss04 = 1, + -- Tartarus_Sneak + A_MiniBoss03 = 1, + + --Megaera + A_Boss01 = true, + --Alecto + A_Boss02 = true, + --Tisiphone + A_Boss03 = true, + + -- Asphodel_Barge + B_Wrapping01 = 0, + -- Asphodel_PowerCouple, + B_MiniBoss01 = 1, + -- Asphodel_Witches + B_MiniBoss02 = 2, + + -- Elysium_ButterflyBall + C_MiniBoss02 = 0, + -- Elysium_Asterius + C_MiniBoss01 = 2, + + -- If true, Tiny Vermin will not spawn + RemoveTinyVermin = true, + }, + Leaderboard = { + -- Tartarus_Bombers + A_MiniBoss01 = 1, + -- Tartarus_Doomstone TODO: Handle middle management? + A_MiniBoss04 = 1, + -- Tartarus_Sneak + A_MiniBoss03 = 1, + + --Megaera + A_Boss01 = true, + --Alecto + A_Boss02 = true, + --Tisiphone + A_Boss03 = true, + + -- Asphodel_Barge + B_Wrapping01 = 0, + -- Asphodel_PowerCouple, + B_MiniBoss01 = 1, + -- Asphodel_Witches + B_MiniBoss02 = 2, + + -- Elysium_ButterflyBall + C_MiniBoss02 = 2, + -- Elysium_Asterius + C_MiniBoss01 = 0, + + -- If true, Tiny Vermin will not spawn + RemoveTinyVermin = true, + }, + Hypermodded = { + -- Tartarus_Bombers + A_MiniBoss01 = 1, + -- Tartarus_Doomstone TODO: Handle middle management? + A_MiniBoss04 = 1, + -- Tartarus_Sneak + A_MiniBoss03 = 1, + + --Megaera + A_Boss01 = true, + --Alecto + A_Boss02 = true, + --Tisiphone + A_Boss03 = false, + + -- Asphodel_Barge + B_Wrapping01 = 0, + -- Asphodel_PowerCouple, + B_MiniBoss01 = 1, + -- Asphodel_Witches + B_MiniBoss02 = 2, + + -- Elysium_ButterflyBall + C_MiniBoss02 = 2, + -- Elysium_Asterius + C_MiniBoss01 = 0, + -- If true, Tiny Vermin will not spawn RemoveTinyVermin = true, } @@ -64,8 +173,20 @@ function MinibossControl.RegisterPreset(name, preset) end -- Apply the configured miniboss settings to the game data -function updateMinibossControl() - ModUtil.MapSetTable(RoomSetData, { +function MinibossControl.UpdateMaxCreations() + AllowedBossesTartarus = {} + + if MinibossControl.Presets[config.MinibossSetting].A_Boss01 then + table.insert(AllowedBossesTartarus,"A_Boss01") + end + if MinibossControl.Presets[config.MinibossSetting].A_Boss02 then + table.insert(AllowedBossesTartarus,"A_Boss02") + end + if MinibossControl.Presets[config.MinibossSetting].A_Boss03 then + table.insert(AllowedBossesTartarus,"A_Boss03") + end + + ModUtil.Table.MergeKeyed(RoomSetData, { --Table.Merge doesn't remove unwanted furies from LinkedRooms; this sets them to nil -- [[ Tartarus Miniboss Counts ]] Tartarus = { A_MiniBoss01 = { @@ -82,14 +203,17 @@ function updateMinibossControl() -- Vanilla Doomstone MaxCreationsThisRun = MinibossControl.Presets[config.MinibossSetting].A_MiniBoss04, }, + A_PreBoss01 = { + LinkedRooms = AllowedBossesTartarus, + } }, -- [[ Asphodel Miniboss Counts ]] Asphodel = { B_MiniBoss01 = { - MaxCreationsThisRun = MinibossControl.Presets[config.MinibossSetting].Asphodel_PowerCouple, + MaxCreationsThisRun = MinibossControl.Presets[config.MinibossSetting].B_MiniBoss01, }, B_MiniBoss02 = { - MaxCreationsThisRun = MinibossControl.Presets[config.MinibossSetting].Asphodel_Witches, + MaxCreationsThisRun = MinibossControl.Presets[config.MinibossSetting].B_MiniBoss02, }, B_Wrapping01 = { MaxCreationsThisRun = MinibossControl.Presets[config.MinibossSetting].B_Wrapping01, @@ -108,21 +232,21 @@ function updateMinibossControl() -- Remove Tiny Vermin if MinibossControl.Presets[config.MinibossSetting].RemoveTinyVermin then - ModUtil.MapSetTable(RoomSetData.Styx.D_MiniBoss03, { + ModUtil.Table.Merge(RoomSetData.Styx.D_MiniBoss03, { LegalEncounters = { "MiniBossHeavyRangedForked" }, }) else - ModUtil.MapSetTable(RoomSetData.Styx.D_MiniBoss03, { - LegalEncounters = { "MiniBossCrawler", "MiniBossHeavyRangedForked" }, + ModUtil.Table.Merge(RoomSetData.Styx.D_MiniBoss03, { + LegalEncounters = { "MiniBossCrawler", "MiniBossHeavyRangedForked" }, }) end end -- Scripts/RunManager.lua : 515 -ModUtil.WrapBaseFunction("ChooseNextRoomData", function( baseFunc, currentRun, args ) +ModUtil.Path.Wrap("ChooseNextRoomData", function( baseFunc, currentRun, args ) -- IsRoomEligible looks at RoomCreations and ignores it if it's nil. Make sure it's not nil -- Easier and cleaner than overriding the IsRoomEligible function to fix nil behavior - ModUtil.MapSetTable(CurrentRun.RoomCreations, { + ModUtil.Table.Merge(CurrentRun.RoomCreations, { A_MiniBoss01 = currentRun.RoomCreations.A_MiniBoss01 or 0, A_MiniBoss02 = currentRun.RoomCreations.A_MiniBoss02 or 0, A_MiniBoss03 = currentRun.RoomCreations.A_MiniBoss03 or 0, @@ -137,8 +261,12 @@ ModUtil.WrapBaseFunction("ChooseNextRoomData", function( baseFunc, currentRun, a return baseFunc(currentRun, args) end, MinibossControl) +ModUtil.LoadOnce( function() + MinibossControl.UpdateMaxCreations() +end) + -- When a new run is started, make sure to apply the miniboss modifications -ModUtil.WrapBaseFunction("StartNewRun", function ( baseFunc, currentRun ) - updateMinibossControl() +ModUtil.Path.Wrap("StartNewRun", function ( baseFunc, currentRun ) + MinibossControl.UpdateMaxCreations() return baseFunc(currentRun) end, MinibossControl) diff --git a/ModConfigMenu/ModConfigMenu.lua b/ModConfigMenu/ModConfigMenu.lua index 2db0e20..3e2ed61 100644 --- a/ModConfigMenu/ModConfigMenu.lua +++ b/ModConfigMenu/ModConfigMenu.lua @@ -1,4 +1,4 @@ -ModUtil.RegisterMod("ModConfigMenu") +ModUtil.Mod.Register("ModConfigMenu") ModConfigMenu.Menus = {} ModConfigMenu.CurrentMenuIdx = 1 @@ -279,9 +279,9 @@ function ModConfigMenu__Close( screen, button ) OnScreenClosed({ Flag = screen.Name }) end -ModUtil.WrapBaseFunction("CreatePrimaryBacking", function ( baseFunc ) +ModUtil.Path.Wrap("CreatePrimaryBacking", function ( baseFunc ) -- Only show menu between runs - if not ModUtil.PathGet("CurrentDeathAreaRoom") then + if not ModUtil.Path.Get("CurrentDeathAreaRoom") then return baseFunc() end diff --git a/ModUtil b/ModUtil new file mode 160000 index 0000000..7f97ae5 --- /dev/null +++ b/ModUtil @@ -0,0 +1 @@ +Subproject commit 7f97ae5330f4e7bf0ff398ecfbbd0d8bf6a375a3 diff --git a/ModUtil/ModUtil.lua b/ModUtil/ModUtil.lua deleted file mode 100644 index 9eb593e..0000000 --- a/ModUtil/ModUtil.lua +++ /dev/null @@ -1,1581 +0,0 @@ ---[[ -Mod: Mod Utility -Author: MagicGonads - - Library to allow mods to be more compatible with eachother and expand capabilities. - Use the mod importer to import this mod to ensure it is loaded in the right position. - -]] - --- BETA VERSION - -local Config = { - AutoCollapse = true, -} - -ModUtil = { - Config = Config, - ModName = "ModUtil", - WrapCallbacks = {}, - Mods = {}, - Overrides = {}, - PerFunctionEnv = {}, - Anchors = {Menu={},CloseFuncs={}}, - Context = {}, - Metatables = {}, - FuncsToLoad = {}, - MarkedForCollapse = {}, -} -SaveIgnores["ModUtil"]=true - --- Management - ---[[ - Create a namespace that can be used for the mod's functions - and data, and ensure that it doesn't end up in save files. - - modName - the name of the mod - parent - the parent mod, or nil if this mod stands alone -]] -function ModUtil.RegisterMod( modName, parent ) - if not parent then - parent = _G - SaveIgnores[modName]=true - end - if not parent[modName] then - parent[modName] = {} - table.insert( ModUtil.Mods, parent[modName] ) - end - parent[modName].ModName = modName - parent[modName].ModParent = parent - return parent[modName] -end - --- internal -function ModUtil.LoadFuncs( triggerArgs ) - for _,v in pairs(ModUtil.FuncsToLoad) do - v(triggerArgs) - end - ModUtil.FuncsToLoad = {} -end -OnAnyLoad{ModUtil.LoadFuncs} - ---[[ - Run the provided function once on the next in-game load. - - triggerFunction - the function to run -]] -function ModUtil.LoadOnce( triggerFunction ) - table.insert( ModUtil.FuncsToLoad, triggerFunction ) -end - ---[[ - Tell each screen anchor that they have been forced closed by the game -]] -function ModUtil.ForceClosed( triggerArgs ) - for _,v in pairs( ModUtil.Anchors.CloseFuncs ) do - v( nil, nil, triggerArgs ) - end - ModUtil.Anchors.CloseFuncs = {} - ModUtil.Anchors.Menu = {} -end -OnAnyLoad{ModUtil.ForceClosed} - --- Data Misc - -function ModUtil.ValueString( o ) - if type( o ) == 'string' then - return '"'..o..'"' - end - return tostring( o ) -end - -function ModUtil.KeyString( o ) - if type( o ) == 'number' then o = o..'.' end - return tostring( o ) -end - -function ModUtil.TableKeysString( o ) - if type( o ) == 'table' then - local first = true - local s = '' - for k,_ in pairs( o ) do - if not first then s = s .. ', ' else first = false end - s = s .. ModUtil.KeyString( k ) - end - return s - end -end - -function ModUtil.ToString( o ) - --https://stackoverflow.com/a/27028488 - if type( o ) == 'table' then - local first = true - local s = '{' - for k,v in pairs( o ) do - if not first then s = s .. ', ' else first = false end - s = s .. ModUtil.KeyString( k ) ..' = ' .. ModUtil.ToString( v ) - end - return s .. '}' - else - return ModUtil.ValueString( o ) - end -end - -function ModUtil.ToStringLimited( o, n, m, t, j) - if type( o ) == 'table' then - local first = true - local s = '' - local i = 0 - local go = true - if not j then j = 1 end - if not m then m = 0 end - if not t then t = {} end - for k,v in pairs( o ) do - if t[j] then go = type( v ) == t[j] or t[j] == true end - if go then - i = i + 1 - if n then if i > n+m then return s end end - if m < i then - if not first then s = s .. ', ' else first = false end - if type( v ) == "table" and t[j+1] then - s = s .. ModUtil.KeyString( k ) ..' = ('..ModUtil.ToStringLimited( v, n, m, t, j+1 )..')' - else - s = s .. ModUtil.KeyString( k ) ..' = '..ModUtil.ValueString( v ) - end - end - end - end - return s - else - return ModUtil.ValueString( o ) - end -end - -function ModUtil.ChunkText( text, chunkSize, maxChunks ) - local chunks = {""} - local cs = 0 - local ncs = 1 - for chr in text:gmatch( "." ) do - cs = cs + 1 - if cs > chunkSize or chr == "\n" then - ncs = ncs + 1 - if maxChunks then - if ncs > maxChunks then - return chunks - end - end - chunks[ncs] = "" - cs = 0 - end - if chr ~= "\n" then - chunks[ncs] = chunks[ncs] .. chr - end - end - return chunks -end - -function ModUtil.InvertTable( tableArg ) - local inverseTable = {} - for key,value in ipairs( tableArg ) do - inverseTable[value] = key - end - return inverseTable -end - -function ModUtil.IsUnKeyed( tableArg ) - local lk = 0 - for k, _ in pairs( tableArg ) do - if type( k ) ~= "number" then - return false - end - if lk+1 ~= k then - return false - end - lk = k - end - return true -end - -function ModUtil.AutoIsUnKeyed( tableArg ) - if ModUtil.Config.AutoCollapse then - if not ModUtil.MarkedForCollapse[ tableArg ] then - return ModUtil.IsUnKeyed( tableArg ) - else - return false - end - end - return false -end - --- Data Manipulation - ---[[ - Safely create a new empty table at Table.key. - - Table - the table to modify - key - the key at which to store the new empty table -]] -function ModUtil.NewTable( tableArg, key ) - if type( tableArg ) ~= "table" then return end - if tableArg[key] == nil then - tableArg[key] = {} - end -end - ---[[ - Safely retrieve the a value from deep inside a table, given - an array of indices into the table. - - For example, if indexArray is ["a", 1, "c"], then - Table["a"][1]["c"] is returned. If any of Table["a"], - Table["a"][1], or Table["a"][1]["c"] are nil, then nil - is returned instead. - - Table - the table to retrieve from - indexArray - the list of indices -]] -function ModUtil.SafeGet( baseTable, indexArray ) - local node = baseTable - for _, k in ipairs( indexArray ) do - if type( node ) ~= "table" then - return nil - end - node = node[k] - end - return node -end - ---[[ - Safely set a value deep inside a table, given an array of - indices into the table, and creating any necessary tables - along the way. - - For example, if indexArray is ["a", 1, "c"], then - Table["a"][1]["c"] = Value once this function returns. - If any of Table["a"] or Table["a"][1] does not exist, they - are created. - - baseTable - the table to set the value in - indexArray - the list of indices - value - the value to add -]] -function ModUtil.SafeSet( baseTable, indexArray, value ) - if next( indexArray ) == nil then - return false -- can't set the input argument - end - local n = #indexArray - local node = baseTable - for i = 1, n-1 do - local k = indexArray[i] - ModUtil.NewTable( node, k ) - node = node[k] - end - if ( node[indexArray[n]] == nil ) ~= ( value == nil ) then - if ModUtil.AutoIsUnKeyed( baseTable ) then - ModUtil.MarkForCollapse( node ) - end - end - node[indexArray[n]] = value - return true -end - ---[[ - Set all the values in inTable corresponding to keys - in nilTable to nil. - - For example, if inTable is - { - Foo = 5, - Bar = 6, - Baz = { - InnerFoo = 5, - InnerBar = 6 - } - } - - and nilTable is - { - Foo = true, - Baz = { - InnerBar = true - } - } - - then the result will be - { - Foo = nil - Bar = 6, - Baz = { - InnerFoo = 5, - InnerBar = nil - } - } -]] -function ModUtil.MapNilTable( inTable, nilTable ) - local unkeyed = ModUtil.AutoIsUnKeyed( inTable ) - for nilKey, nilVal in pairs( nilTable ) do - local inVal = inTable[nilKey] - if type(nilVal) == "table" and type( inVal ) == "table" then - ModUtil.MapNilTable( inVal, nilVal ) - else - inTable[nilKey] = nil - if unkeyed then - ModUtil.MarkForCollapse( inTable ) - end - end - end -end - ---[[ - Set all the the values in inTable corresponding to values - in setTable to their values in setTable. - - For example, if inTable is - { - Foo = 5, - Bar = 6 - } - - and setTable is - { - Foo = 7, - Baz = { - InnerBar = 8 - } - } - - then the result will be - { - Foo = 7, - Bar = 6, - Baz = { - InnerBar = 8 - } - } -]] -function ModUtil.MapSetTable( inTable, setTable ) - local unkeyed = ModUtil.AutoIsUnKeyed( inTable ) - for setKey, setVal in pairs( setTable ) do - local inVal = inTable[setKey] - if type( setVal ) == "table" and type( inVal ) == "table" then - ModUtil.MapSetTable( inVal, setVal ) - else - inTable[setKey] = setVal - if type( setKey ) ~= "number" and unkeyed then - ModUtil.MarkForCollapse( inTable ) - end - end - end -end - -local function CollapseTable( tableArg ) - -- from UtilityScripts.lua - if tableArg == nil then - return - end - - local collapsedTable = {} - local index = 1 - for _, v in pairs( tableArg ) do - collapsedTable[index] = v - index = index + 1 - end - - return collapsedTable - -end - -function ModUtil.CollapseMarked() - for tbl,_ in pairs( ModUtil.MarkedForCollapse ) do - local ctbl = CollapseTable( tbl ) - for k,_ in pairs(tbl) do - tbl[k]=nil - end - for k,v in pairs(ctbl) do - tbl[k] = v - end - end - ModUtil.MarkedForCollapse = {} -end -OnAnyLoad{ModUtil.CollapseMarked} - -function ModUtil.MarkForCollapse( table ) - ModUtil.MarkedForCollapse[table] = true -end - --- Path Manipulation - ---[[ - Concatenates two index arrays, in order. - - a, b - the index arrays -]] -function ModUtil.JoinIndexArrays( a, b ) - local c = {} - local j = 0 - for i,v in ipairs(a) do - c[i] = v - j = i - end - for i,v in ipairs(b) do - c[i+j] = v - end - return c -end - ---[[ - Create an index array from the provided Path. - - The returned array can be used as an argument to the safe table - manipulation functions, such as ModUtil.SafeSet and ModUtil.SafeGet. - - path - a dot-separated string that represents a path into a table -]] -function ModUtil.PathArray( path ) - local s = "" - local i = {} - for c in path:gmatch( "." ) do - if c ~= "." then - s = s .. c - else - table.insert( i, s ) - s = "" - end - end - if #s > 0 then - table.insert( i,s ) - end - return i -end - ---[[ - Safely get a value from a Path. - - For example, ModUtil.PathGet("a.b.c") returns a.b.c. - If either a or a.b is nil, nil is returned instead. - - path - the path to get the value - base - (optional) The table to retreive the value from. - If not provided, retreive a global. -]] -function ModUtil.PathGet( path, base ) - return ModUtil.SafeGet( base or _G, ModUtil.PathArray( path ) ) -end - ---[[ - Safely get set a value to a Path. - - For example, ModUtil.PathSet("a.b.c", 1) sets a.b.c = 1. - If either a or a.b is nil, they are created. - - path - the path to get the value - base - (optional) The table to retreive the value from. - If not provided, retreive a global. -]] -function ModUtil.PathSet( path, value, base ) - return ModUtil.SafeSet( base or _G, ModUtil.PathArray( path ), value ) -end - -function ModUtil.PathNilTable( path, nilTable, base ) - return ModUtil.MapNilTable( ModUtil.PathGet( path, base ), nilTable ) -end - -function ModUtil.PathSetTable( path, setTable, base ) - return ModUtil.MapSetTable( ModUtil.PathGet( path, base ), setTable ) -end - --- Metaprogramming Shenanigans - -function getfenv( fn ) - local i = 1 - while true do - local name, val = debug.getupvalue(fn, i) - if name == "_ENV" then - return val - elseif not name then - break - end - i = i + 1 - end -end - ---[[ - Replace a function's _ENV with a new environment table. - - Global variable lookups (including function calls) in that function - will use the new environment table rather than the normal one. - - This is useful for function-specific overrides. The new environment - table should generally have _G as its __index, so that any globals - other than those being overridden can still be read. -]] -function setfenv( fn, env ) - local i = 1 - while true do - local name = debug.getupvalue( fn, i ) - if name == "_ENV" then - debug.upvaluejoin( fn, i, (function() - return env - end), 1 ) - break - elseif not name then - break - end - i = i + 1 - end - return fn -end - -rawnext = next -function next(t,k) - local m = getmetatable(t) - local n = m and m.__next or rawnext - return n(t,k) -end - - - -ModUtil.Metatables.LocalLevel = { - __index = function( self, idx ) - return debug.getlocal( rawget(self,"level") + 1, idx ) - end, - __newindex = function( self, idx, value ) - local level = rawget(self,"level") + 1 - local name = debug.getlocal( level, idx) - if name ~= nil then - debug.setlocal( level, idx, value ) - end - end, - __next = function( self, idx ) - if idx == nil then - idx = 0 - end - idx = idx + 1 - local name, val = debug.getlocal( rawget(self,"level") + 1, idx ) - if val ~= nil then - return idx, name, val - end - end, - __pairs = function( self ) - return getmetatable( self ).__next, self, nil - end, - __ipairs = function( self ) - return getmetatable( self ).__next, self, 0 - end -} - ---[[ - Example Use: - for i, name, value in pairs(ModUtil.LocalLevel(level)) do - -- - end -]] -function ModUtil.LocalLevel(level) - local localLevel = { level = level } - setmetatable( localLevel, ModUtil.Metatables.LocalLevel ) - return localLevel -end - -ModUtil.Metatables.LocalLevels = { - __index = function( _, level ) - return level, ModUtil.LocalLevel( level ) - end, - __next = function( _, level ) - if level == nil then - level = 0 - end - level = level + 1 - if debug.getinfo(level + 1, "f") then - return level, ModUtil.LocalLevel( level ) - end - end, - __pairs = function( self ) - return getmetatable( self ).__next, self, nil - end, - __ipairs = function( self ) - return getmetatable( self ).__next, self, 0 - end -} ---[[ - Example Use: - for level,localLevel in pairs(ModUtil.LocalLevels) do - for i, name, value in pairs(localLevel) do - -- - end - end -]] -ModUtil.LocalLevels = {} -setmetatable(ModUtil.LocalLevels, ModUtil.Metatables.LocalLevels) - -ModUtil.Metatables.LocalsInterface = { - __index = function( self, name ) - local pair = rawget(self,"index")[name] - if pair ~= nil then - local _,value = debug.getlocal( pair.level + 1, pair.i ) - return value - end - end, - __newindex = function( self, name, value ) - local pair = rawget(self,"index")[name] - if pair ~= nil then - debug.setlocal( pair.level + 1, pair.i, value ) - end - end, - __next = function( self, name ) - local pair - name, pair = next( rawget(self,"index"), name ) - if pair ~= nil then - local _, value = debug.getlocal( pair.level + 1, pair.i ) - return name, value - end - end, - __pairs = function( self ) - return getmetatable( self ).__next, self, nil - end, - __ipairs = function( self ) - return function( self, i ) - local name - i, name = next( rawget(t,"order"), i ) - pair = rawget(self,"index")[name] - if pair ~= nil then - local _, value = debug.getlocal( pair.level + 1, pair.i ) - return i, value - end - end, self, 0 - end -} - ---[[ - Interface only valid within the scope it was constructed -]] -function ModUtil.GetLocalsInterface( names ) - local lookup = {} - if names then - -- assume names is strictly either a list or a lookup table - lookup = names - if #lookup ~= 0 then - lookup = ModUtil.InvertTable(names) - end - end - - local order = {} - local index = {} - - local level,localLevel = next(ModUtil.LocalLevels, 1) - while level do - for i, name, value in pairs(localLevel) do - if (not names or lookup[name]) and index[name] == nil then - index[name] = {level = level - 1, i = i} - table.insert( order, name ) - end - end - level,localLevel = next(ModUtil.LocalLevels, level) - end - local interface = {index = index, order = order} - setmetatable(interface, ModUtil.Metatables.LocalsInterface) - - return interface, index, order -end - -ModUtil.Metatables.Locals = { - __index = function( _, name) - local level = 2 - while debug.getinfo(level, "f") do - local idx = 1 - while true do - local n, v = debug.getlocal(level, idx) - if n == name then - return v - elseif not n then - break - end - idx = idx + 1 - end - level = level + 1 - end - end, - __newindex = function( _, name, value ) - local level = 2 - while debug.getinfo(level, "f") do - local idx = 1 - while true do - local n = debug.getlocal(level, idx) - if n == name then - debug.setlocal(level, idx, value) - return - elseif not n then - break - end - idx = idx + 1 - end - level = level + 1 - end - end, - __next = function( _, name ) - if name == nil then - return debug.getlocal( 2, 1 ) - end - local level = 2 - while debug.getinfo(level, "f") do - local idx = 1 - while true do - local n = debug.getlocal( level, idx ) - if n == name then - return debug.getlocal( level, idx + 1) - elseif not n then - break - end - idx = idx + 1 - end - level = level + 1 - end - end, - __pairs = function( self ) - return getmetatable(self).__next, self, nil - end, - __ipairs = function( self ) - return function( self, i ) - local level = 2 - local j = 0 - while debug.getinfo(level, "f") do - local idx = 1 - while true do - j = j + 1 - local n, v = debug.getlocal(level, idx) - if not n then - break - elseif j > i then - return j, v - end - idx = idx + 1 - end - level = level + 1 - end - end, self, 0 - end -} - ---[[ - Access to local variables, in the current function and callers. - The most recent definition with a given name on the call stack will - be used. - - For example, if your function is called from CreateTraitRequirements, - you could access its 'local screen' as ModUtil.Locals.screen - and its 'local hasRequirement' as ModUtil.Locals.hasRequirement. -]] -ModUtil.Locals = {} -setmetatable(ModUtil.Locals, ModUtil.Metatables.Locals) - -ModUtil.Metatables.UpValues = { - __index = function( self, name ) - local _, v = debug.getupvalue( rawget(self,"func"), rawget(self,"ind")[name] ) - return v - end, - __newindex = function( self, name, value ) - debug.setupvalue( rawget(self,"func"), rawget(self,"ind")[name], value ) - end, - __next = function ( self, name ) - name, i = next( rawget(self,"ind"), name ) - if i ~= nil then - return debug.getupvalue( rawget(self,"func"), i ) - end - end, - __pairs = function( self ) - return getmetatable(self).__next, self, nil - end, - __ipairs = function( self ) - return function( self, i ) - i = i + 1 - local name,v = debug.getupvalue( rawget(self,"func"), i ) - if name ~= nil then - return i, v - end - end, self, 0 - end -} - ---[[ - Return a table representing the upvalues of a function. - - Upvalues are those variables captured by a function from it's - creation context. For example, locals defined in the same file - as the function are accessible to the function as upvalues. - - func - the function to get upvalues from -]] -function ModUtil.GetUpValues( func ) - local ind = {} - local name - local i = 1 - while true do - name = debug.getupvalue( func, i ) - if name == nil then break end - ind[name] = i - i = i + 1 - end - local ups = {func = func, ind = ind} - setmetatable(ups, ModUtil.Metatables.UpValues) - return ups, ind -end - -function ModUtil.GetBottomUpValues( baseTable, indexArray ) - local baseValue = ModUtil.SafeGet( ModUtil.Overrides[baseTable], indexArray ) - if baseValue then - baseValue = baseValue[#baseValue].Base - else - baseValue = ModUtil.SafeGet( ModUtil.WrapCallbacks[baseTable], indexArray ) - if baseValue then - baseValue = baseValue[1].func - else - baseValue = ModUtil.SafeGet( baseTable, indexArray ) - end - end - return ModUtil.GetUpValues( baseValue ) -end - ---[[ - Return a table representing the upvalues of the base function identified - by basePath (ie. ignoring all wrappers that other mods may have placed - around the function). - - basePath - the path to the function, as a string -]] -function ModUtil.GetBaseBottomUpValues( basePath ) - return ModUtil.GetBottomUpValues( _G, ModUtil.PathArray( basePath ) ) -end - --- Globalisation - -ModUtil.Metatables.GlobalisedFunc = { - __call = function(self, ...) - return ModUtil.SafeGet( rawget(self,"table"), rawget(self,"array") )(...) - end -} - -function ModUtil.GlobaliseFunc( baseTable, indexArray, key ) - local funcTable = { table = baseTable, array = indexArray } - setmetatable( funcTable, ModUtil.Metatables.GlobalisedFunc ) - _G[key] = funcTable -end - ---[[ - Sets a unique global variable equal to the value stored at Path. - - For example, the OnPressedFunctionName of a button must refer to a single key - in the globals table (_G). If you have a function defined in your module's - table that you would like to use, ie. - - function YourModName.FunctionName(...) - - then ModUtil.GlobalizePath("YourModName.FunctionName") will create a global - variable for that function, and you can then set OnPressedFunctionName to - ModUtil.JoinPath("YourModName.FunctionName"). - - path - the path to be globalised - prefixPath - (optional) if present, add this path as a prefix to the - path from the root of the table. -]] -function ModUtil.GlobaliseFuncPath( path, prefixPath ) - if prefixPath == nil then - prefixPath = "" - else - prefixPath = prefixPath .. '.' - end - ModUtil.GlobaliseFunc( _G, ModUtil.PathArray( path ), prefixPath..path ) -end - ---[[ - Makes all the functions in Table available globally, as if - ModUtil.GlobalisePath had been called on each of them. - - If you have a lot of functions you need to export for UI or - other by-name callbacks, it will be eaiser to maintain a single - call to ModUtil.GlobaliseFuncs at the bottom of your mod file, - rather than having a bunch of GlobalisePath calls that need to - be maintained. - - For example, if you have a table at YourModName.UIFunctions with - - function YourModName.UIFunctions.OnButton1(...) - function YourModName.UIFunctiosn.OnButton2(...) - - then ModUtil.GlobaliseFuncs(YourModName.UIFunctions) will create global - functions called OnButton1 and OnButton2. If you are worried about - collisions with other global functions, consider using a prefix ie. - - ModUtil.GlobaliseFuncs(YourModName.UIFunctions, "YourModNameUI") - - So that the global functions are called YourModNameUI__OnButton1 etc. - - tableArg - The table containing the functions to globalise - prefixPath - (optional) if present, add this path as a prefix to the - path from the root of the table. -]] -function ModUtil.GlobaliseFuncs( tableArg, prefixPath ) - if prefixPath == nil then - prefixPath = "" - else - prefixPath = prefixPath .. '.' - end - for k,v in pairs( tableArg ) do - if type( k ) == "string" then - if type(v) == "function" then - ModUtil.GlobaliseFunc( v, {k}, prefixPath..'.'..k ) - elseif type(v) == "table" then - ModUtil.GlobaliseFuncs( v, prefixPath..'.'..k ) - end - end - end -end - ---[[ - Globalise all the functions in your mod object. - - modObject - The mod object created by ModUtil.RegisterMod -]] -function ModUtil.GlobaliseModFuncs( modObject ) - local parent = modObject - local prefix = modObject.ModName - while parent.ModParent do - parent = parent.ModParent - if parent == _G then break end - prefix = parent.ModName .. "." .. prefix - end - ModUtil.GlobaliseFuncs( modObject, prefix ) -end - --- Function Wrapping - ---[[ - Wrap a function, so that you can insert code that runs before/after that function - whenever it's called, and modify the return value if needed. - - Generally, you should use ModUtil.WrapBaseFunction instead for a more modder-friendly - interface. - - Multiple wrappers can be applied to the same function. - - As an example, for WrapFunction(_G, ["UIFunctions", "OnButton1Pushed"], wrapper, MyModObject) - - Wrappers are stored in a structure like this: - - ModUtil.WrapCallbacks[_G].UIFunctions.OnButton1Pushed = { - {id:1, mod=MyModObject, wrap=wrapper, func=} - } - - If a second wrapper is applied via - WrapFunction(_G, ["UIFunctions", "OnButton1Pushed"], wrapperFunction2, SomeOtherMod) - then the resulting structure will be like: - - ModUtil.WrapCallbacks[_G].UIFunctions.OnButton1Pushed = { - {id:1, mod=MyModObject, wrap=wrapper, func=} - {id:2, mod=SomeOtherMod, wrap=wrapper2, func=} - } - - This allows several mods to apply wrappers to the same base function, and then: - - unwrap again later - - reapply the same wrappers to a new base function when it's overridden - - This function also updates the entry in funcTable at indexArray to be the completely - wrapped function, ie. in our example with two wrappers it would do - - UIFunctions.OnButton1Pushed = - - funcTable - the table the function is stored in (usually _G) - indexArray - the array of path elements to the function in the table - wrapFunc - the wrapping function - modObject - (optional) the mod installing the wrapper, for informational purposes -]] -function ModUtil.WrapFunction( funcTable, indexArray, wrapFunc, modObject ) - if type( wrapFunc ) ~= "function" then return end - if not funcTable then return end - local func = ModUtil.SafeGet( funcTable, indexArray ) - if type( func ) ~= "function" then return end - - ModUtil.NewTable( ModUtil.WrapCallbacks, funcTable ) - local tempTable = ModUtil.SafeGet( ModUtil.WrapCallbacks[funcTable], indexArray ) - if tempTable == nil then - tempTable = {} - ModUtil.SafeSet( ModUtil.WrapCallbacks[funcTable], indexArray, tempTable ) - end - table.insert( tempTable, { Id = #tempTable + 1, Mod = modObject, Wrap = wrapFunc, Func = func } ) - - ModUtil.SafeSet( funcTable, indexArray, function( ... ) - return wrapFunc( func, ... ) - end ) -end - ---[[ - Internal utility that reapplies the list of wrappers when the base function changes. - - For example. if the list of wrappers looks like: - ModUtil.WrapCallbacks[_G].UIFunctions.OnButton1Pushed = { - {id:1, mod=MyModObject, wrap=wrapper, func=} - {id:2, mod=SomeOtherMod, wrap=wrapper2, func=} - {id:3, mod=ModNumber3, wrap=wrapper3, func=} - } - - and the base function is modified by setting [1].func to a new value, like so: - ModUtil.WrapCallbacks[_G].UIFunctions.OnButton1Pushed = { - {id:1, mod=MyModObject, wrap=wrapper, func=} - {id:2, mod=SomeOtherMod, wrap=wrapper2, func=} - {id:3, mod=ModNumber3, wrap=wrapper3, func=} - } - - Then rewrap function will fix up eg. [2].func, [3].func so that the correct wrappers are applied - ModUtil.WrapCallbacks[_G].UIFunctions.OnButton1Pushed = { - {id:1, mod=MyModObject, wrap=wrapper, func=} - {id:2, mod=SomeOtherMod, wrap=wrapper2, func=} - {id:3, mod=ModNumber3, wrap=wrapper3, func=} - } - and also update the entry in funcTable to be the completely wrapped function, ie. - - UIFunctions.OnButton1Pushed = - - funcTable - the table the function is stored in (usually _G) - indexArray - the array of path elements to the function in the table -]] -function ModUtil.RewrapFunction( funcTable, indexArray ) - local wrapCallbacks = ModUtil.SafeGet( ModUtil.WrapCallbacks[funcTable], indexArray ) - local preFunc = nil - - for i,t in ipairs( wrapCallbacks ) do - if preFunc then - t.Func = preFunc - end - preFunc = function( ... ) - return t.Wrap( t.Func, ... ) - end - ModUtil.SafeSet( funcTable, indexArray, preFunc ) - end - -end - ---[[ - Removes the most recent wrapper from a function, and restore it to its - previous value. - - Generally, you should use ModUtil.UnwrapBaseFunction instead for a more - modder-friendly interface. - - funcTable - the table the function is stored in (usually _G) - indexArray - the array of path elements to the function in the table -]] -function ModUtil.UnwrapFunction( funcTable, indexArray ) - if not funcTable then return end - local func = ModUtil.SafeGet( funcTable, indexArray ) - if type( func ) ~= "function" then return end - - local tempTable = ModUtil.SafeGet( ModUtil.WrapCallbacks[funcTable], indexArray ) - if not tempTable then return end - local funcData = table.remove( tempTable ) -- removes the last value - if not funcData then return end - - ModUtil.SafeSet( funcTable, indexArray, funcData.Func ) - return funcData -end - - ---[[ - Wraps the function with the path given by baseFuncPath, so that you - can execute code before or after the original function is called, - or modify the return value. - - For example: - - ModUtil.WrapBaseFunction("CreateNewHero", function(baseFunc, prevRun, args) - local hero = baseFunc(prevRun, args) - hero.Health = 1 - return hero - end, YourMod) - - will cause the function CreateNewHero to be wrapped so that the hero's - health is set to 1 before the hero is returned. - - This provides better compatibility with other mods that overriding the - function, since multiple mods can wrap the same function. - - baseFuncPath - the (global) path to the function, as a string - for most SGG-provided functions, this is just the function's name - eg. "CreateRoomReward" or "SetTraitsOnLoot" - wrapFunc - the function to wrap around the base function - this function receives the base function as its first parameter. - all subsequent parameters should be the same as the base function - modObject - (optional) the object for your mod, for debug purposes -]] -function ModUtil.WrapBaseFunction( baseFuncPath, wrapFunc, modObject ) - local pathArray = ModUtil.PathArray( baseFuncPath ) - ModUtil.WrapFunction( _G, pathArray, wrapFunc, modObject ) -end - ---[[ - Internal function that reapplies all the wrappers to a function. -]] -function ModUtil.RewrapBaseFunction( baseFuncPath ) - local pathArray = ModUtil.PathArray( baseFuncPath ) - ModUtil.RewrapFunction( _G, pathArray ) -end - ---[[ - Remove the most recent wrapper from the function at baseFuncPath, - restoring it to its previous state - - Note that this does _not_ remove overrides, and it removes the most - recent wrapper regardless of which mod added it, so be careful! - - baseFuncPath - the (global) path to the function, as a string - for most SGG-provided functions, this is just the function's name - eg. "CreateRoomReward" or "SetTraitsOnLoot" -]] -function ModUtil.UnwrapBaseFunction( baseFuncPath ) - local pathArray = ModUtil.PathArray( baseFuncPath ) - ModUtil.UnwrapFunction( _G, pathArray ) -end - --- Override Management - -local function getBaseValueForWraps( baseTable, indexArray ) - local baseValue = ModUtil.SafeGet( baseTable, indexArray ) - local wrapCallbacks = nil - wrapCallbacks = ModUtil.SafeGet( ModUtil.WrapCallbacks[baseTable], indexArray ) - if wrapCallbacks then if wrapCallbacks[1] then - baseValue = wrapCallbacks[1].Func - end end - - return baseValue -end - -local function setBaseValueForWraps( baseTable, indexArray, value) - local baseValue = ModUtil.SafeGet( baseTable, indexArray ) - - if type(baseValue) ~= "function" or type(value) ~= "function" then return false end - - local wrapCallbacks = nil - wrapCallbacks = ModUtil.SafeGet( ModUtil.WrapCallbacks[baseTable], indexArray ) - if wrapCallbacks then if wrapCallbacks[1] then - baseValue = wrapCallbacks[1].Func - wrapCallbacks[1].Func = value - ModUtil.RewrapFunction( baseTable, indexArray ) - return true - end end - - return false -end - ---[[ - Override a value in baseTable. - - Generally, you should use ModUtil.BaseOverride instead for a more modder-friendly - interface. - - If the value is a function, overrides only the base function, - preserving all the wraps added with ModUtil.WrapFunction. - - The previous value is stored so that it can be restored later if desired. - For example, ModUtil.Override(_G, ["UIFunctions", "OnButton1"], overrideFunc, MyMod) - will result in a data structure like: - - ModUtil.Overrides[_G].UIFunctions.OnButton1 = { - {id=1, mod=MyMod, value=overrideFunc, base=} - } - - and subsequent overides wil be added as subsequent entries in the same table. - - baseTable - the table in which to override, usually _G (globals) - indexArray - the list of indices - value - the new value - modObject - (optional) the mod that performed the override, for debugging purposes -]] -function ModUtil.Override( baseTable, indexArray, value, modObject ) - if not baseTable then return end - - local baseValue = getBaseValueForWraps( baseTable, indexArray ) - - ModUtil.NewTable( ModUtil.Overrides, baseTable ) - local tempTable = ModUtil.SafeGet( ModUtil.Overrides[baseTable], indexArray ) - if tempTable == nil then - tempTable = {} - ModUtil.SafeSet( ModUtil.Overrides[baseTable], indexArray, tempTable ) - end - table.insert( tempTable, { Id = #tempTable + 1, Mod = modObject, Value = value, Base = baseValue } ) - - if not setBaseValueForWraps( baseTable, indexArray, value) then - ModUtil.SafeSet( baseTable, indexArray, value ) - end -end - ---[[ - Undo the most recent override performed with ModUtil.Override, restoring - the previous value. - - Generally, you should use ModUtil.BaseRestore instead for a more modder-friendly - interface. - - If the previous value is a function, the current stack of wraps for that - IndexArray will be reapplied to it. - - baseTable = the table in which to undo the override, usually _G (globals) - indexArray - the list of indices -]] -function ModUtil.Restore( baseTable, indexArray ) - if not baseTable then return end - local tempTable = ModUtil.SafeGet( ModUtil.Overrides[baseTable], indexArray ) - if not tempTable then return end - local baseData = table.remove( tempTable ) -- remove the last entry - if not baseData then return end - - if not setBaseValueForWraps( baseTable, indexArray, baseData.Base ) then - ModUtil.SafeSet( baseTable, indexArray, baseData.Base ) - end - return baseData -end - ---[[ - Override the global value at the given basePath. - - If the Value is a function, preserves the wraps - applied with ModUtil.WrapBaseFunction et. al. - - basePath - the path to override, as a string - value - the new value to store at the path - modObject - (optional) the mod performing the override, - for debug purposes -]] -function ModUtil.BaseOverride( basePath, value, modObject ) - local pathArray = ModUtil.PathArray( basePath ) - ModUtil.Override( _G, pathArray, value, modObject ) -end - ---[[ - Undo the most recent override performed with ModUtil.Override, - or ModUtil.BaseOverride, restoring the previous value. - - Use this carefully - if you are not the most recent mod to - override the given path, the results may be unexpected. - - basePath - the path to restore, as a string -]] -function ModUtil.BaseRestore( basePath ) - local pathArray = ModUtil.PathArray( basePath ) - ModUtil.Restore( _G, pathArray ) -end - ---[[ - Create a new table whose getters and setters will default to the given - baseTable, except in cases where values are setOverride into the overrideTable. - - This allows pinpoint overriding of values in the override table, while allowing - all other accesses to continue to operate as if they were operating on baseTable. - - The overrides are stored in the _Overrides subtable. For example: - - _Overrides = { - CurrentRun = { - CurrentRoom = { - _IsModUtilOverride = true, - _Value = { - Name = "RoomSimple01", - ... - } - } - } - } - - would be the result of overriding "CurrentRun.CurrentRoom" with a table represnting a room. - - Any accesses that happen above the override point (ie. reads/writes to CurrentRun) are - redirected to the base table. Any accesses that happen at or below the override point (ie. - reads / writes to CurrentRun.CurrentRoom or CurrentRun.CurrentRoom.Name) are intercepted - and apply to the overridden value instead. - - baseTable - the table to access for entries not specifically overridden -]] -local function makeOverrideTable( baseTable, overrides ) - local overrideTable = { - _IsModUtilOverrideTable = true, - _Overrides = overrides or {}, - _BaseTable = baseTable - } - setmetatable(overrideTable, { - __index = function( self, name ) - local baseResult = self._BaseTable[name] - local overridesResult = self._Overrides[name] - if overridesResult == nil then - return baseResult - elseif overridesResult._IsModUtilOverride then - return overridesResult._Value - elseif type(baseResult) == "table" then - return makeOverrideTable( baseResult, overridesResult ) - else - return makeOverrideTable( {}, overridesResult ) - end - end, - __newindex = function( self, name, value ) - local currentOverride = self._Overrides[name] - if currentOverride == nil then - self._BaseTable[name] = value - elseif currentOverride._IsModUtilOverride then - currentOverride._Value = value - else - -- There is an override that is a child of this name, but the parent is being - -- overwritten with a new table. Assign directly to the baseTable. - -- The previous override will still remain in place, so eg. with - -- Overides = { Parent = { Child = {_IsModUtilOverride = true, _Value = 6 } } - -- Table.Parent = { Child = 5 }, Table.Parent.Child will still return 6. - -- I'm not sure what alternate behavior would be more correct. - self._BaseTable[name] = value - end - end - }) - return overrideTable -end - ---[[ - Override the entry at indexArray in table with value. - - table - the table whose entry should be overridden - must come from a previous call to makeOverrideTable() - indexArray - the list of indexes - value - the value to override -]] -local function setOverride( table, indexArray, value ) - if type(table) ~= "table" or not table._IsModUtilOverrideTable then return end - ModUtil.SafeSet( table._Overrides, indexArray, {_IsModUtilOverride = true, _Value = value} ) -end - ---[[ - Remove the override entry at indexArray in table. - - No effect if the indexArray does not identify an override point. - - table - the table whose override should be removed - indexArray - the list of indexes -]] -local function removeOverride( table, indexArray ) - if type(table) ~= "table" or not table._IsModUtilOverrideTable then return end - local currentOverride = ModUtil.SafeGet( table._Overrides, indexArray) - if currentOverride ~= nil and currentOverride._IsModUtilOverride then - ModUtil.SafeSet( table._Overrides, indexArray, nil ) - end -end - ---[[ - Check whether there is an override for indexArray in table. - - Only returns true for exact matches. For example, if you override CurrentRun in table T, - hasOverride( T, ["CurrentRun", "CurrentRoom"] ) will return false. - - table - the table to check for an override - indexArray - the list of indexes -]] -local function hasOverride( table, indexArray ) - if type(table) ~= "table" or not table._IsModUtilOverrideTable then return false end - local value = ModUtil.SafeGet( table._Overrides, indexArray ) - return value ~= nil and value._IsModUtilOverride -end - ---[[ - Gets the function's local (partially-overriden) environment, or - create a new one and attach it to the function if not yet present. - - baseTable - the base table, on which to base the function's environment - indexArray - the list of indexes -]] -local function getFunctionEnv( baseTable, indexArray ) - local func = getBaseValueForWraps( baseTable, indexArray ) - if type(func) ~= "function" then return nil end - - ModUtil.NewTable( ModUtil.PerFunctionEnv, baseTable ) - local env = ModUtil.SafeGet(ModUtil.PerFunctionEnv[baseTable], indexArray) - if not env then - env = makeOverrideTable( baseTable ) - ModUtil.SafeSet( ModUtil.PerFunctionEnv[baseTable], indexArray, env ) - setfenv( func, env ) - end - return env -end - -local function DeepCopyTable( orig ) - local orig_type = type(orig) - local copy - if orig_type == 'table' then - copy = {} - -- slightly more efficient to call next directly instead of using pairs - for k,v in next, orig, nil do - copy[k] = DeepCopyTable(v) - end - else - copy = orig - end - - return copy -end - ---[[ - Overrides the value at envIndexArray within the function referred to by - indexArray in baseTable, by replacing it with value. - - Accesses to the value at envIndexArray from within other functions are - unaffected. - - Generally, you should use ModUtil.BaseOverrideWithinFunction for a more - modder-friendly interface. - - For example, after you do - ModUtil.OverrideWithinFunction( _G, ["CreateRoom"], ["CurrentRun.CurrentRoom"], ) - - 1. Any reads to CurrentRun.CurrentRoom from CreateRoom will return - 2. Any writes to CurrentRun.CurrentRoom from CreateRoom will replace , which will - with the new value, which will be returned for subsequent accesses to CurrentRun.CurrentRoom. - 3. Any writes to CurrentRun.CurrentRoom from CreateRoom will not be visible from other functions. - 4. If CreateRoom performs writes within (eg. CurrentRun.CurrentRoom.Name = "foo") - these will be visible to from any function that has access to it. If you want - full isolation, make sure you pass in an object that nobody else has a reference to, eg. by - creating one fresh or making a copy). - - baseTable - the base table for function and environment lookups (usually _G) - indexArray - the list of indices identifying the function whose environment is to be overridden - envIndexArray - the list of indices identifying the value to be overridden in the function's environment - value - the value with which to override -]] -function ModUtil.OverrideWithinFunction( baseTable, indexArray, envIndexArray, value ) - if not baseTable then return end - local env = getFunctionEnv( baseTable, indexArray ) - - if not env then return end - if hasOverride( env, envIndexArray ) then - -- we might have wraps to reapply - local overrideEnvIndexArray = DeepCopyTable( envIndexArray ) - table.insert(overrideEnvIndexArray, "_Value") - if not setBaseValueForWraps(env._Overrides, overrideEnvIndexArray, value) then - setOverride( env, envIndexArray, value ) - end - else - setOverride( env, envIndexArray, value ) - end -end - ---[[ - Remove the override at envIndexArray for the function at indexArray in baseTable, - so that reads and writes to envIndexArray have their usual effects on the base environment. - - baseTable - the base table for function and environment lookups (usually _G) - indexArray - the list of indices identifying the function whose environment is to be overridden - envIndexArray - the list of indices identifying the value to be overridden in the function's environment -]] -function ModUtil.RestoreWithinFunction( baseTable, indexArray, envIndexArray ) - if not baseTable then return end - - local env = getFunctionEnv( baseTable, indexArray ) - if not env then return end - - removeOverride( env, envIndexArray ) -end - - ---[[ - Wrap a function, so that you can insert code that runs before/after that function whenever - it's called from within a particular other function, and modify the return value if needed. - - Generally, you should use ModUtil.WrapBaseWithinFunction for a more modder-friendly interface. - - baseTable - the base table for function and environment lookups (usually _G) - indexArray - the list of indices identifying the function within with the wrap will apply - envIndexArray - the list of indices identifying the function whose calls will be wrapped - wrapFunc - the wrapping function - modObject - (optional) the mod performing the wrapping, for debug purposes -]] -function ModUtil.WrapWithinFunction( baseTable, indexArray, envIndexArray, wrapFunc, modObject ) - if type(wrapFunc) ~= "function" then return end - if not baseTable then return end - local env = getFunctionEnv( baseTable, indexArray ) - if not env then return end - - if not hasOverride( env, envIndexArray ) then - -- Resolve the entry in baseTable at call time (not now), - -- in case further wraps or overrides are applied to it. - setOverride( - env, - envIndexArray, - function(...) - local resolvedFunc = ModUtil.SafeGet( baseTable, envIndexArray ) - return resolvedFunc(...) - end) - end - - ModUtil.WrapFunction( env, envIndexArray, wrapFunc, modObject ) -end - ---[[ - Override the global value at the given path, when accessed from - within the global function at funcPath. - - If the Value is a function, preserves the wraps - applied with ModUtil.WrapBaseWithinFunction et. al. - - basePath - the path to override, as a string - envPath - the path to override, as a string - value - the new value to store at the path -]] -function ModUtil.BaseOverrideWithinFunction( funcPath, basePath, value ) - local indexArray = ModUtil.PathArray( funcPath ) - local envIndexArray = ModUtil.PathArray( basePath ) - ModUtil.OverrideWithinFunction( _G, indexArray, envIndexArray, value ) -end - ---[[ - Wraps the function with the path given by baseFuncPath, when it is called from - the function given by funcPath. - - This lets you insert code within a function, without affecting other functions - that might make similar calls. - - For example, to insert code to display a "cancel" at the point in "CreateBoonLootButtons" - where it calls IsMetaUpgradeSelected( "RerollPanelMetaUpgrade" ), do: - - ModUtil.WrapBaseWithinFunction("CreateBoonLootButtons", "IsMetaUpgradeSelected", function(baseFunc, name) - if name == "RerollPanelMetaUpgrade" and CalcNumLootChoices() == 0 then - < code to display the cancel button > - return false - else - return baseFunc(name) - end - end, YourMod) - - This provides better compatibility between mods that just wrapping IsMetaUpgradeSelected, because - you new code only executes when within CreateBoonLootButtons, so there are less chances for collisions - and side effects. - - It also provides better compatibility than using BaseOverride on "CreateBoonLootButtons", since only one - override for a function can be active at a time, but multiple wraps can be active. - - funcPath - the (global) path to the function within which the wrap will apply, as a string - for most SGG-provided functions, this is just the function's name - eg. "CreateRoomReward" or "SetTraitsOnLoot" - baseFuncPath - the (global) path to the function to wrap, as a string - for most SGG-provided functions, this is just the function's name - eg. "CreateRoomReward" or "SetTraitsOnLoot" - wrapFunc - the function to wrap around the base function - this function receives the base function as its first parameter. - all subsequent parameters should be the same as the base function - modObject - (optional) the object for your mod, for debug purposes -]] -function ModUtil.WrapBaseWithinFunction( funcPath, baseFuncPath, wrapFunc, modObject ) - local indexArray = ModUtil.PathArray( funcPath ) - local envIndexArray = ModUtil.PathArray( baseFuncPath ) - ModUtil.WrapWithinFunction( _G, indexArray, envIndexArray, wrapFunc, modObject ) -end - --- Context Managers (High WIP) - -function ModUtil.Context.Within(path, func) - local parentEnv = ModUtil.Locals.env or _G - local env = getFunctionEnv(parentEnv, path) - return env -end - -function ModUtil.Context.Meta(path, func) - --- -end diff --git a/ModUtil/ModUtilHades.lua b/ModUtil/ModUtilHades.lua deleted file mode 100644 index 9bf4679..0000000 --- a/ModUtil/ModUtilHades.lua +++ /dev/null @@ -1,402 +0,0 @@ - -ModUtil.RegisterMod("Hades",ModUtil) -ModUtil.Hades={ - PrintStackHeight = 10, - PrintStackCapacity = 80 -} - -ModUtil.Anchors.PrintOverhead = {} - --- Screen Handling - -OnAnyLoad{ function() - if ModUtil.Hades.UnfreezeLoop then return end - ModUtil.Hades.UnfreezeLoop = true - thread( function() - while ModUtil.Hades.UnfreezeLoop do - wait(15) - if ModUtil.SafeGet(CurrentRun,{'Hero','FreezeInputKeys'}) then - if (not AreScreensActive()) and (not IsInputAllowed({})) then - UnfreezePlayerUnit() - DisableShopGamepadCursor() - end - end - end - end) -end} - --- Menu Handling - -function ModUtil.Hades.CloseMenu( screen, button ) - CloseScreen(GetAllIds(screen.Components), 0.1) - ModUtil.Anchors.Menu[screen.Name] = nil - screen.KeepOpen = false - OnScreenClosed({ Flag = screen.Name }) - SetConfigOption({ Name = "FreeFormSelectWrapY", Value = false }) - SetConfigOption({ Name = "UseOcclusion", Value = true }) - if TableLength(ModUtil.Anchors.Menu) == 0 then - UnfreezePlayerUnit() - DisableShopGamepadCursor() - end - if ModUtil.Anchors.CloseFuncs[screen.Name] then - ModUtil.Anchors.CloseFuncs[screen.Name]( screen, button ) - ModUtil.Anchors.CloseFuncs[screen.Name]=nil - end -end - -function ModUtil.Hades.OpenMenu( group, closeFunc, openFunc ) - if ModUtil.Anchors.Menu[group] then - ModUtil.Hades.CloseMenu(ModUtil.Anchors.Menu[group]) - end - if closeFunc then ModUtil.Anchors.CloseFuncs[group]=closeFunc end - - local screen = { Name = group, Components = {} } - local components = screen.Components - ModUtil.Anchors.Menu[group] = screen - - OnScreenOpened({ Flag = screen.Name, PersistCombatUI = true }) - SetConfigOption({ Name = "UseOcclusion", Value = false }) - - components.Background = CreateScreenComponent({ Name = "BlankObstacle", Group = group }) - - if openFunc then openFunc(screen) end - - return screen -end - -function ModUtil.Hades.DimMenu( screen ) - if not screen then return end - if not screen.Components.BackgroundDim then - screen.Components.BackgroundDim = CreateScreenComponent({ Name = "rectangle01", Group = screen.Name }) - SetScale({ Id = screen.Components.BackgroundDim.Id, Fraction = 4 }) - end - SetColor({ Id = screen.Components.BackgroundDim.Id, Color = {0.090, 0.090, 0.090, 0.8} }) -end - -function ModUtil.Hades.UndimMenu( screen ) - if not screen then return end - if not screen.Components.BackgroundDim then return end - SetColor({ Id = screen.Components.BackgroundDim.Id, Color = {0.090, 0.090, 0.090, 0} }) -end - -function ModUtil.Hades.PostOpenMenu( screen ) - FreezePlayerUnit() - EnableShopGamepadCursor() - thread(HandleWASDInput, screen) - HandleScreenInput(screen) - return screen -end - -function ModUtil.Hades.GetMenuScreen( group ) - return ModUtil.Anchors.Menu[group] -end - --- Debug Printing - -function ModUtil.Hades.PrintDisplay( text , delay, color ) - if type(text) ~= "string" then - text = tostring(text) - end - text = " "..text.." " - if color == nil then - color = Color.Yellow - end - if delay == nil then - delay = 5 - end - if ModUtil.Anchors.PrintDisplay then - Destroy({Ids = {ModUtil.Anchors.PrintDisplay.Id}}) - end - ModUtil.Anchors.PrintDisplay = CreateScreenComponent({Name = "BlankObstacle", Group = "PrintDisplay", X = ScreenCenterX, Y = 40 }) - CreateTextBox({ Id = ModUtil.Anchors.PrintDisplay.Id, Text = text, FontSize = 22, Color = color, Font = "UbuntuMonoBold"}) - - if delay > 0 then - thread(function() - wait(delay) - Destroy({Ids = {ModUtil.Anchors.PrintDisplay.Id}}) - ModUtil.Anchors.PrintDisplay = nil - end) - end -end - -function ModUtil.Hades.PrintOverhead(text, delay, color, dest) - if type(text) ~= "string" then - text = tostring(text) - end - text = " "..text.." " - if dest == nil then - dest = CurrentRun.Hero.ObjectId - end - if color == nil then - color = Color.Yellow - end - if delay == nil then - delay = 5 - end - Destroy({Ids = {ModUtil.Anchors.PrintOverhead[dest]}}) - local id = SpawnObstacle({ Name = "BlankObstacle", Group = "PrintOverhead", DestinationId = dest }) - ModUtil.Anchors.PrintOverhead[dest] = id - Attach({ Id = id, DestinationId = dest }) - CreateTextBox({ Id = id, Text = text, FontSize = 32, OffsetX = 0, OffsetY = -150, Color = color, Font = "AlegreyaSansSCBold", Justification = "Center" }) - if delay > 0 then - thread(function() - wait(delay) - if ModUtil.Anchors.PrintOverhead[dest] then - Destroy({Ids = {id}}) - ModUtil.Anchors.PrintOverhead[dest] = nil - end - end) - end -end - -local function ClosePrintStack() - if ModUtil.Anchors.PrintStack then - ModUtil.Anchors.PrintStack.CullEnabled = false - PlaySound({ Name = "/SFX/Menu Sounds/GeneralWhooshMENU" }) - ModUtil.Anchors.PrintStack.KeepOpen = false - - CloseScreen(GetAllIds(ModUtil.Anchors.PrintStack.Components),0) - ModUtil.Anchors.PrintStack = nil - end -end - -OnAnyLoad{ ClosePrintStack } - -local function OrderPrintStack(screen,components) - - if screen.CullPrintStack then - local v = screen.TextStack[1] - if v.obj then - Destroy({Ids = {v.obj.Id}}) - components["TextStack_" .. v.tid] = nil - v.obj = nil - screen.TextStack[v.tid]=nil - end - thread( function() - local v = screen.TextStack[2] - if v then - wait(v.data.Delay) - if v.obj then - screen.CullPrintStack = true - end - end - end) - else - thread( function() - local v = screen.TextStack[1] - if v then - wait(v.data.Delay) - if v.obj then - screen.CullPrintStack = true - end - end - end) - end - screen.CullPrintStack = false - - for k,v in pairs(screen.TextStack) do - components["TextStack_" .. k] = nil - Destroy({Ids = {v.obj.Id}}) - end - - screen.TextStack = CollapseTable(screen.TextStack) - for i,v in pairs(screen.TextStack) do - v.tid = i - end - if #screen.TextStack == 0 then - return ClosePrintStack() - end - - local Ymul = screen.StackHeight+1 - local Ygap = 30 - local Yoff = 26*screen.StackHeight+22 - local n =#screen.TextStack - - if n then - for k=1,math.min(n,Ymul) do - v = screen.TextStack[k] - if v then - local data = v.data - screen.TextStack[k].obj = CreateScreenComponent({ Name = "rectangle01", Group = "PrintStack", X = -1000, Y = -1000}) - local textStack = screen.TextStack[k].obj - components["TextStack_" .. k] = textStack - SetScaleX({Id = textStack.Id, Fraction = 10/6}) - SetScaleY({Id = textStack.Id, Fraction = 0.1}) - SetColor({ Id = textStack.Id, Color = data.Bgcol }) - CreateTextBox({ Id = textStack.Id, Text = data.Text, FontSize = data.FontSize, OffsetX = 0, OffsetY = 0, Color = data.Color, Font = data.Font, Justification = "Center" }) - Attach({ Id = textStack.Id, DestinationId = components.Background.Id, OffsetX = 220, OffsetY = -Yoff }) - Yoff = Yoff - Ygap - end - end - end - -end - -function ModUtil.Hades.PrintStack( text, delay, color, bgcol, fontsize, font, sound ) - if color == nil then color = {1,1,1,1} end - if bgcol == nil then bgcol = {0.590, 0.555, 0.657,0.125} end - if fontsize == nil then fontsize = 13 end - if font == nil then font = "UbuntuMonoBold" end - if sound == nil then sound = "/Leftovers/SFX/AuraOff" end - if delay == nil then delay = 3 end - - if type(text) ~= "string" then - text = tostring(text) - end - text = " "..text.." " - - local first = false - if not ModUtil.Anchors.PrintStack then - first = true - ModUtil.Anchors.PrintStack = { Components = {} } - end - local screen = ModUtil.Anchors.PrintStack - local components = screen.Components - - if first then - - screen.KeepOpen = true - screen.TextStack = {} - screen.CullPrintStack = false - screen.MaxStacks = ModUtil.Hades.PrintStackCapacity - screen.StackHeight = ModUtil.Hades.PrintStackHeight - PlaySound({ Name = "/SFX/Menu Sounds/DialoguePanelOutMenu" }) - components.Background = CreateScreenComponent({ Name = "BlankObstacle", Group = "PrintStack", X = ScreenCenterX, Y = 2*ScreenCenterY}) - components.Backing = CreateScreenComponent({ Name = "TraitTray_Center", Group = "PrintStack"}) - Attach({ Id = components.Backing.Id, DestinationId = components.Background.Id, OffsetX = -180, OffsetY = 0 }) - SetColor({ Id = components.Backing.Id, Color = {0.590, 0.555, 0.657, 0.8} }) - SetScaleX({Id = components.Backing.Id, Fraction = 6.25}) - SetScaleY({Id = components.Backing.Id, Fraction = 6/55*(2+screen.StackHeight)}) - - thread( function() - while screen do - wait(0.5) - if screen.CullEnabled then - if screen.CullPrintStack then - OrderPrintStack(screen,components) - end - end - end - end) - - end - - if #screen.TextStack >= screen.MaxStacks then return end - - screen.CullEnabled = false - - local newText = {} - newText.obj = CreateScreenComponent({ Name = "rectangle01", Group = "PrintStack"}) - newText.data = {Delay = delay, Text = text, Color = color, Bgcol = bgcol, Font = font, FontSize = fontsize} - SetColor({ Id = newText.obj.Id, Color = {0,0,0,0}}) - table.insert(screen.TextStack, newText) - - PlaySound({ Name = sound }) - - OrderPrintStack(screen,components) - - screen.CullEnabled = true - -end - -function ModUtil.Hades.PrintStackChunks( text, linespan, ... ) - if not linespan then linespan = 90 end - for _,s in ipairs(ModUtil.ChunkText(text,linespan,ModUtil.Hades.PrintStackCapacity)) do - ModUtil.Hades.PrintStack(s,...) - end -end - --- Custom Menus - -function ModUtil.Hades.NewMenuYesNo( group, closeFunc, openFunc, yesFunc, noFunc, title, body, yesText, noText, icon, iconScale) - - if not group or group == "" then group = "MenuYesNo" end - if not yesFunc or not noFunc then return end - if not icon then icon = "AmmoPack" end - if not iconScale then iconScale = 1 end - if not yesText then yesText = "Yes" end - if not noText then noText = "No" end - if not body then body = "Make a choice..." end - if not title then title = group end - - local screen = ModUtil.Hades.OpenMenu( group, closeFunc, openFunc ) - local components = screen.Components - - PlaySound({ Name = "/SFX/Menu Sounds/GodBoonInteract" }) - - components.LeftPart = CreateScreenComponent({ Name = "TraitTrayBackground", Group = group, X = 1030, Y = 424}) - components.MiddlePart = CreateScreenComponent({ Name = "TraitTray_Center", Group = group, X = 660, Y = 464 }) - components.RightPart = CreateScreenComponent({ Name = "TraitTray_Right", Group = group, X = 1270, Y = 438 }) - SetScaleY({Id = components.LeftPart.Id, Fraction = 0.8}) - SetScaleY({Id = components.MiddlePart.Id, Fraction = 0.8}) - SetScaleY({Id = components.RightPart.Id, Fraction = 0.8}) - SetScaleX({Id = components.MiddlePart.Id, Fraction = 5}) - - - CreateTextBox({ Id = components.Background.Id, Text = " "..title.." ", FontSize = 34, - OffsetX = 0, OffsetY = -225, Color = Color.White, Font = "SpectralSCLight", - ShadowBlur = 0, ShadowColor = {0,0,0,1}, ShadowOffset={0, 1}, Justification = "Center" }) - CreateTextBox({ Id = components.Background.Id, Text = " "..body.." ", FontSize = 19, - OffsetX = 0, OffsetY = -175, Width = 840, Color = Color.SubTitle, Font = "CrimsonTextItalic", - ShadowBlur = 0, ShadowColor = {0,0,0,1}, ShadowOffset={0, 1}, Justification = "Center" }) - - components.Icon = CreateScreenComponent({ Name = "BlankObstacle", Group = group }) - Attach({ Id = components.Icon.Id, DestinationId = components.Background.Id, OffsetX = 0, OffsetY = -50}) - SetAnimation({ Name = icon, DestinationId = components.Icon.Id, Scale = iconScale }) - - ModUtil.NewTable(ModUtil.Anchors.Menu[group], "Funcs") - ModUtil.Anchors.Menu[group].Funcs={ - Yes = function(screen, button) - local ret = yesFunc(screen,button) - ModUtil.Hades.CloseMenuYesNo(screen,button) - return ret - end, - No = function(screen, button) - local ret = noFunc(screen,button) - ModUtil.Hades.CloseMenuYesNo(screen,button) - return ret - end, - } - ModUtil.GlobaliseFuncs( ModUtil.Anchors.Menu[group].Funcs, "ModUtil.Anchors.Menu."..group..".Funcs" ) - - components.CloseButton = CreateScreenComponent({ Name = "ButtonClose", Scale = 0.7, Group = group }) - Attach({ Id = components.CloseButton.Id, DestinationId = components.Background.Id, OffsetX = 0, OffsetY = ScreenCenterY - 315 }) - components.CloseButton.OnPressedFunctionName = "ModUtil.Hades.CloseMenuYesNo" - components.CloseButton.ControlHotkey = "Cancel" - - components.YesButton = CreateScreenComponent({ Name = "BoonSlot1", Group = group, Scale = 0.35, }) - components.YesButton.OnPressedFunctionName = "ModUtil.Anchors.Menu."..group..".Funcs.Yes" - SetScaleX({Id = components.YesButton.Id, Fraction = 0.75}) - SetScaleY({Id = components.YesButton.Id, Fraction = 1.15}) - Attach({ Id = components.YesButton.Id, DestinationId = components.Background.Id, OffsetX = -150, OffsetY = 75 }) - CreateTextBox({ Id = components.YesButton.Id, Text = " "..yesText.." ", - FontSize = 28, OffsetX = 0, OffsetY = 0, Width = 720, Color = Color.LimeGreen, Font = "AlegreyaSansSCLight", - ShadowBlur = 0, ShadowColor = {0,0,0,1}, ShadowOffset={0, 2}, Justification = "Center" - }) - - components.NoButton = CreateScreenComponent({ Name = "BoonSlot1", Group = group, Scale = 0.35, }) - components.NoButton.OnPressedFunctionName = "ModUtil.Anchors.Menu."..group..".Funcs.No" - SetScaleX({Id = components.NoButton.Id, Fraction = 0.75}) - SetScaleY({Id = components.NoButton.Id, Fraction = 1.15}) - Attach({ Id = components.NoButton.Id, DestinationId = components.Background.Id, OffsetX = 150, OffsetY = 75 }) - CreateTextBox({ Id = components.NoButton.Id, Text = noText, - FontSize = 26, OffsetX = 0, OffsetY = 0, Width = 720, Color = Color.Red, Font = "AlegreyaSansSCLight", - ShadowBlur = 0, ShadowColor = {0,0,0,1}, ShadowOffset={0, 2}, Justification = "Center" - }) - - return ModUtil.Hades.PostOpenMenu( screen ) -end - -function ModUtil.Hades.CloseMenuYesNo( screen, button ) - PlaySound({ Name = "/SFX/Menu Sounds/GeneralWhooshMENU" }) - _G["ModUtil.Anchors.Menu."..screen.Name..".Funcs.Yes"]=nil - _G["ModUtil.Anchors.Menu."..screen.Name..".Funcs.No"]=nil - ModUtil.Hades.CloseMenu( screen, button ) -end - --- Misc - -function ModUtil.Hades.RandomElement( tableArg, rng ) - local Collapsed = CollapseTable( tableArg ) - return Collapsed[RandomInt( 1, #Collapsed, rng )] -end \ No newline at end of file diff --git a/ModUtil/modfile.txt b/ModUtil/modfile.txt deleted file mode 100644 index 087f24d..0000000 --- a/ModUtil/modfile.txt +++ /dev/null @@ -1,5 +0,0 @@ -:: Mod Utility - -Load Priority 0 -Top Import "ModUtil.lua" -Import "ModUtilHades.lua" \ No newline at end of file diff --git a/ModdedWarning/ModdedWarning.lua b/ModdedWarning/ModdedWarning.lua index 83c1e1a..75ca2fb 100644 --- a/ModdedWarning/ModdedWarning.lua +++ b/ModdedWarning/ModdedWarning.lua @@ -1,4 +1,4 @@ -ModUtil.RegisterMod("ModdedWarning") +ModUtil.Mod.Register("ModdedWarning") local config = { WarningMessage = "MODDED GAME", diff --git a/PrintUtil/PrintUtil.lua b/PrintUtil/PrintUtil.lua index 1a90d64..db9b688 100644 --- a/PrintUtil/PrintUtil.lua +++ b/PrintUtil/PrintUtil.lua @@ -8,7 +8,7 @@ manner, and to get the current stack trace, to determine who is calling a function and from where. ]] -ModUtil.RegisterMod("PrintUtil") +ModUtil.Mod.Register("PrintUtil") --- Util method to draw text to the screen -- @param obstacleName Name of textbox diff --git a/QuickRestart/QuickRestart.lua b/QuickRestart/QuickRestart.lua index beb1806..3156832 100644 --- a/QuickRestart/QuickRestart.lua +++ b/QuickRestart/QuickRestart.lua @@ -1,54 +1,148 @@ -ModUtil.RegisterMod("QuickRestart") +ModUtil.Mod.Register("QuickRestart") local config = { - Enabled = false + Enabled = true, + KeepStartingKeepsake = true, + QuickDeathEnabled = true, } + QuickRestart.config = config -OnControlPressed{ "Assist", - function(triggerArgs) - if config.Enabled and not ModUtil.PathGet("CurrentDeathAreaRoom") then - if IsControlDown({ Name = "Assist" }) - and IsControlDown({ Name = "Use" }) - and IsControlDown({ Name = "Shout" }) - and IsControlDown({ Name = "Reload" }) then - KillHero(CurrentRun.Hero, triggerArgs) - end +function QuickRestart.CanReset() + -- QuickRestart must be Enabled + if not config.Enabled then return false end + + -- Short delay to handle edge cases + wait(0.1) + + -- Zag must not be in the House + if ModUtil.Path.Get("CurrentDeathAreaRoom") then return false end + + -- Zag must not be frozen + if not IsEmpty( CurrentRun.Hero.FreezeInputKeys ) then return false end + + -- Combat UI must be visible + if not (ShowingCombatUI or false) then return false end + + -- We can't be LastStand-ing + if QuickRestart.LastStanding then return false end + + -- We can't be QuickRestart-ing + if QuickRestart.UsedQuickRestart then return false end + + -- We can't be mid-trial god selection + if QuickRestart.MidDevotion then return false end + + -- If we're in a Thanatos Room, enemies must have already spawned + if (CurrentRun ~= nil and CurrentRun.CurrentRoom ~= nil and + CurrentRun.CurrentRoom.Encounter ~= nil and + CurrentRun.CurrentRoom.Encounter.ThanatosId ~= nil + and GetActiveEnemyCount() == 0) then + return false end -end} - -OnControlPressed{ "Use", - function(triggerArgs) - if config.Enabled and not ModUtil.PathGet("CurrentDeathAreaRoom") then - if IsControlDown({ Name = "Assist" }) - and IsControlDown({ Name = "Use" }) - and IsControlDown({ Name = "Shout" }) - and IsControlDown({ Name = "Reload" }) then - KillHero(CurrentRun.Hero, triggerArgs) - end + + return true +end + +function QuickRestart.ResetRun(triggerArgs) + if not QuickRestart.CanReset() then + ModUtil.Hades.PrintOverhead("Can't reset now!", 2) + return end -end} - -OnControlPressed{ "Shout", - function(triggerArgs) - if config.Enabled and not ModUtil.PathGet("CurrentDeathAreaRoom") then - if IsControlDown({ Name = "Assist" }) - and IsControlDown({ Name = "Use" }) - and IsControlDown({ Name = "Shout" }) - and IsControlDown({ Name = "Reload" }) then - KillHero(CurrentRun.Hero, triggerArgs) - end + + if RtaTimer then + RtaTimer__ResetRtaTimer() end -end} - -OnControlPressed{ "Reload", - function(triggerArgs) - if config.Enabled and not ModUtil.PathGet("CurrentDeathAreaRoom") then - if IsControlDown({ Name = "Assist" }) - and IsControlDown({ Name = "Use" }) - and IsControlDown({ Name = "Shout" }) - and IsControlDown({ Name = "Reload" }) then - KillHero(CurrentRun.Hero, triggerArgs) - end + + QuickRestart.UsedQuickRestart = true + AddInputBlock({ Name = "QuickRestart" }) + + wait(0.1) + + Kill( CurrentRun.Hero, triggerArgs ) +end + +OnControlPressed{ "Assist Use Shout Reload", + function(triggerArgs) + if config.Enabled then + if IsControlDown({ Name = "Assist" }) + and IsControlDown({ Name = "Use" }) + and IsControlDown({ Name = "Shout" }) + and IsControlDown({ Name = "Reload" }) then + QuickRestart.ResetRun(triggerArgs) + end + end end -end} +} + +OnAnyLoad{ "RoomPreRun", + function ( triggerArgs ) + if QuickRestart.UsedQuickRestart then + thread( PlayVoiceLines, GlobalVoiceLines.EnteredDeathAreaVoiceLines ) + QuickRestart.UsedQuickRestart = false + + RemoveLastAwardTrait() + UnequipWeaponUpgrade() + RemoveLastAssistTrait() + + -- Reset Starting Keepsake + if QuickRestart.config.KeepStartingKeepsake then + DebugPrint({ Text="Setting keepsake trigger" }) + GameState.LastAwardTrait = GameState.QuickRestartStartingKeepsake or GameState.LastAwardTrait + end + end + end +} + +ModUtil.Path.Wrap("HandleDeath", function(baseFunc, currentRun, killer, killingUnitWeapon) + if QuickRestart.UsedQuickRestart then + RemoveInputBlock({ Name = "QuickRestart" }) + end + QuickRestart.QuickDeathApplicable = config.QuickDeathEnabled and not currentRun.Cleared + return baseFunc(currentRun, killer, killingUnitWeapon) +end, QuickRestart) + +ModUtil.Path.Context.Wrap("HandleDeath", function () + ModUtil.Path.Wrap("LoadMap", function(baseFunc, argTable) + if QuickRestart.UsedQuickRestart or QuickRestart.QuickDeathApplicable then + argTable.Name = "RoomPreRun" + + if QuickRestart.KeepStartingKeepsake and GameState.QuickRestartStartingKeepsake then + GameState.LastAwardTrait = GameState.QuickRestartStartingKeepsake + end + end + + -- Set UsedQuickRestart Flag so Keepsake and InputBlock are appropriately set. + local quickDeathApplicable = config.QuickDeathEnabled and not currentRun.Cleared + if quickDeathApplicable then + QuickRestart.UsedQuickRestart = true + end + + baseFunc(argTable) + end, QuickRestart) +end, QuickRestart) + +ModUtil.Path.Wrap("WindowDropEntrance", function( baseFunc, ... ) + local val = baseFunc(...) + -- Get starting keepsake + GameState.QuickRestartStartingKeepsake = GameState.LastAwardTrait + return val +end, QuickRestart) + +ModUtil.Path.Wrap("PlayerLastStandPresentationStart", function( baseFunc, ... ) + QuickRestart.LastStanding = true + return baseFunc( ... ) +end, QuickRestart) + +ModUtil.Path.Wrap("PlayerLastStandPresentationEnd", function( baseFunc, ... ) + local val = baseFunc( ... ) + QuickRestart.LastStanding = false + return val +end, QuickRestart) + +ModUtil.Path.Wrap("StartDevotionTestPresentation", function( baseFunc, ... ) + QuickRestart.MidDevotion = true + baseFunc(...) + QuickRestart.MidDevotion = false +end, QuickRestart) + diff --git a/QuickRestart/modfile.txt b/QuickRestart/modfile.txt index 19ff908..027d9da 100644 --- a/QuickRestart/modfile.txt +++ b/QuickRestart/modfile.txt @@ -1 +1 @@ -Import "QuickRestart.lua" +Import "QuickRestart.lua" \ No newline at end of file diff --git a/RCLib/Map.lua b/RCLib/Map.lua new file mode 100644 index 0000000..f5349c8 --- /dev/null +++ b/RCLib/Map.lua @@ -0,0 +1,260 @@ +RCLib.NameToCode = { + ChaosBlessings = { + -- Uses in-game names for blessings and curses. + -- Several curses and blessings are unused- these are not currently included. + Strike = "ChaosBlessingMeleeTrait", + Shot = "ChaosBlessingRangedTrait", + Grasp = "ChaosBlessingAmmoTrait", + Soul = "ChaosBlessingMaxHealthTrait", + Favor = "ChaosBlessingBoonRarityTrait", + Affluence = "ChaosBlessingMoneyTrait", + Eclipse = "ChaosBlessingMetapointTrait", + Flourish = "ChaosBlessingSecondaryTrait", + Lunge = "ChaosBlessingDashAttackTrait", + Defiance = "ChaosBlessingExtraChanceTrait", + Ambush = "ChaosBlessingBackstabTrait", + Assault = "ChaosBlessingAlphaStrikeTrait", + }, + ChaosCurses = { + Paupers = "ChaosCurseNoMoneyTrait", + Slippery = "ChaosCurseAmmoUseDelayTrait", + Maimed = "ChaosCursePrimaryAttackTrait", + Flayed = "ChaosCurseSecondaryAttackTrait", + Addled = "ChaosCurseCastAttackTrait", + Caustic = "ChaosCurseDeathWeaponTrait", + Enshrouded = "ChaosCurseHiddenRoomReward", + Excruciating = "ChaosCurseDamageTrait", + Abyssal = "ChaosCurseTrapDamageTrait", + Atrophic = "ChaosCurseHealthTrait", + Slothful = "ChaosCurseMoveSpeedTrait", + Roiling = "ChaosCurseSpawnTrait", + Halting = "ChaosCurseDashRangeTrait", + }, + Enemies = { + -- Uses in-game names for enemies except when a fan name is much more popular. + -- Elite is referred to as Armored due to this being the more common term. + -- Unused enemies, bosses, and enemies not in EnemyData are currently not included. + -- Note that some enemies do not have Super Elite variations, some Elite variations only appear as midbosses, etc. + + -- Tartarus Enemies + Brimstone = "HeavyRanged", + Lout = "PunchingBag", + Numbskull = "Swarmer", + Pest = "ThiefMineLayer", + Skullomat = "LightSpawner", + Thug = "HeavyMelee", + Witch = "LightRanged", + Wringer = "DisembodiedHand", + + -- Tartarus Armored + ArmoredBrimstone = "HeavyRangedElite", + ArmoredLout = "PunchingBagElite", + ArmoredNumbskull = "SwarmerElite", + ArmoredPest = "ThiefMineLayerElite", + ArmoredSkullomat = "LightSpawnerElite", + ArmoredThug = "HeavyMeleeElite", + ArmoredWitch = "LightRangedElite", + ArmoredWringer = "DisembodiedHandElite", + + -- Tartarus Super Elite + SuperEliteLout = "PunchingBagSuperElite", + SuperEliteNumbskull = "SwarmerSuperElite", + SuperEliteSkullomat = "LightSpawnerSuperElite", + SuperEliteThug = "HeavyMeleeSuperElite", + SuperEliteWitch = "LightRangedSuperElite", + SuperEliteWringer = "DisembodiedHandSuperElite", + + SuperEliteDoomstone = "HeavyRangedSplitterSuperElite", + + -- Tartarus Minibosses + Doomstone = "HeavyRangedSplitterMiniboss", + DoomstoneFragment = "HeavyRangedSplitterFragment", + Sneak = "WretchAssassinMiniboss", + + -- Asphodel + Bloodless = "BloodlessNaked", + BoneRaker = "BloodlessNakedBerserker", + WaveMaker = "BloodlessWaveFist", + + BurnFlinger = "BloodlessPitcher", + InfernoBomber = "BloodlessGrenadier", + SlamDancer = "BloodlessSelfDestruct", + + Dracon = "RangedBurrower", + Gorgon = "FreezeShotUnit", + SkullCrusher = "CrusherUnit", + Spreader = "SpreadShotUnit", + + -- Asphodel Armored + ArmoredBloodless = "BloodlessNakedElite", + ArmoredBoneRaker = "BloodlessNakedBerserkerElite", + ArmoredWaveMaker = "BloodlessWaveFistElite", + + ArmoredBurnFlinger = "BloodlessPitcherElite", + ArmoredInfernoBomber = "BloodlessGrenadierElite", + ArmoredSlamDancer = "BloodlessSelfDestructElite", + + ArmoredDracon = "RangedBurrowerElite", + ArmoredGorgon = "FreezeShotUnitElite", + ArmoredSkullomat = "LightSpawnerElite", + ArmoredSpreader = "SpreadShotUnitElite", + + -- Asphodel Super Elite + SuperEliteBloodless = "BloodlessNakedSuperElite", + SuperEliteBurnFlinger = "BloodlessPitcherSuperElite", + SuperEliteDracon = "RangedBurrowerSuperElite", + SuperEliteSkullomat = "LightSpawnerSuperElite", + SuperEliteSpreader = "SpreadShotUnitSuperElite", + + SuperEliteMegaGorgon = "HitAndRunUnitSuperElite", + + -- Asphodel Minibosses + ArmoredSkullCrusher = "CrusherElite", + MegaGorgon = "HitAndRunUnitElite", --HitAndRunUnit exists but is unused + MinibossSpreader = "SpreadShotUnitMiniboss", + BargeVoidstone = "ShieldRangedMiniboss", + + -- Elysium Enemies + Eyeball = "ShadeNaked", + Bowman = "ShadeBowUnit", + Shieldsman = "ShadeShieldUnit", + Spearman = "ShadeSpearUnit", + Swordsman = "ShadeSwordUnit", + + Chariot = "Chariot", + Flamewheel = "ChariotSuicide", + GhostShield = "SupportShields", + SoulCatcher = "FlurrySpawner", + Splitter = "SplitShotUnit", + Voidstone = "ShieldRanged", + + -- Elysium Armored + ArmoredEyeball = "ShadeNakedElite", + ArmoredBowman = "ShadeBowUnitElite", + ArmoredShieldsman = "ShadeShieldUnitElite", + ArmoredSpearman = "ShadeSpearUnitElite", + ArmoredSwordsman = "ShadeSwordUnitElite", + + ArmoredChariot = "ChariotElite", + ArmoredFlamewheel = "ChariotSuicideElite", + ArmoredSplitter = "SplitShotUnitElite", + ArmoredVoidstone = "ShieldRangedElite", + + -- Elysium Super Elite + SuperEliteEyeball = "ShadeNakedSuperElite", + SuperEliteBowman = "ShadeBowUnitSuperElite", + SuperEliteShieldsman = "ShadeShieldUnitSuperElite", + SuperEliteSpearman = "ShadeSpearUnitSuperElite", + SuperEliteSwordsman = "ShadeSwordUnitSuperElite", + + SuperEliteChariot = "ChariotSuperElite", + SuperEliteSoulCatcher = "FlurrySpawnerSuperElite", + SuperEliteVoidstone = "ShieldRangedSuperElite", + + SuperEliteiteButterflyBall = "FlurrySpawnerSuperElite", + + -- Elysium Minibosses + ButterflyBall = "FlurrySpawnerElite", + + -- Styx Enemies + Rat = "RatThug", + TinyRat = "Crawler", + Satyr = "SatyrRanged", + Snakestone = "HeavyRangedForked", + Bother = "ThiefImpulseMineLayer", + + -- Styx Armored + ArmoredRat = "RatThugElite", + ArmoredSatyr = "SatyrRangedElite", + ArmoredSnakestone = "HeavyRangedForkedElite", + + -- Styx Super Elite + SuperEliteSnakestone = "HeavyRangedForkedSuperElite", + + -- Styx Minibosses + GiantVermin = "RatThugElite", -- Anthony + SnakestoneMiniboss = "HeavyRangedForkedMiniboss", + MegaSatyr = "SatyrRangedMiniboss", + TinyVermin = "CrawlerMiniboss", -- Tony + }, + EnemySets = { + -- There are several unused enemy sets, such as survival rooms for the other two biomes- these are not included. + Tartarus = "EnemiesBiome1", + TartarusTrial = "EnemiesBiome1Devotion", + TartarusSurvival = "EnemiesBiome1Survival", + TartarusElite = "EnemiesBiome1Hard", + TartarusThanatos = "EnemiesBiome1Thanatos", + TartarusErebus = "ShrineChallengeTartarus", + + Asphodel = "EnemiesBiome2", + AsphodelTrial = "EnemiesBiome2Devotion", + AsphodelElite = "EnemiesBiome2Hard", + AsphodelBarge = "EnemiesBiome2Wrapping", + AsphodelTrove = "EnemiesBiome2Challenge", + AsphodelThanatos = "EnemiesBiome2Thanatos", + AsphodelErebus = "ShrineChallengeAsphodel", + + HydraHeads = "HydraHeads", + + Elysium = "EnemiesBiome3", + ElysiumTrial = "EnemiesBiome3Devotion", + ElysiumElite = "EnemiesBiome3Hard", + ElysiumErebus = "ShrineChallengeElysium", + + StyxBigRoom = "EnemiesBiome4", + StyxSmallRoom = "EnemiesBiome4Mini", + StyxSmallRoomElite = "EnemiesBiome4MiniHard", + StyxSmallRoomSingle = "EnemiesBiome4MiniSingle", + StyxMinibossAdds = "EnemiesBiome4MiniBossFodder", + + HadesSmallAdds = "EnemiesHadesSmall", + HadesLargeAdds = "EnemiesHadesLarge", + EM4HadesSmallAdds = "EnemiesHadesSmall", + EM4HadesLargeAdds = "EnemiesHadesLarge", + + ErebusSuperElite = "ShrineChallengeSuperElite", + } +} +RCLib.CodeToName = { + ChaosBlessings = {}, + ChaosCurses = {}, + Enemies = {}, + EnemySets = {}, +} + +RCLib.CodeToName.ChaosBlessings = ModUtil.Table.Transpose(RCLib.NameToCode.ChaosBlessings) +RCLib.CodeToName.ChaosCurses = ModUtil.Table.Transpose(RCLib.NameToCode.ChaosCurses) +RCLib.CodeToName.Enemies = ModUtil.Table.Transpose(RCLib.NameToCode.Enemies) +RCLib.CodeToName.EnemySets = ModUtil.Table.Transpose(RCLib.NameToCode.EnemySets) + +function RCLib.EncodeChaosBlessing(name) + return RCLib.NameToCode.ChaosBlessings[name] +end + +function RCLib.DecodeChaosBlessing(name) + return RCLib.CodeToName.ChaosBlessings[name] +end + +function RCLib.EncodeChaosCurse(name) + return RCLib.NameToCode.ChaosCurses[name] +end + +function RCLib.DecodeChaosCurse(name) + return RCLib.CodeToName.ChaosCurses[name] +end + +function RCLib.EncodeEnemy(name) + return RCLib.NameToCode.Enemies[name] +end + +function RCLib.DecodeEnemy(name) + return RCLib.CodeToName.Enemies[name] +end + +function RCLib.EncodeEnemySet(name) + return RCLib.NameToCode.EnemySets[name] +end + +function RCLib.DecodeEnemySet(name) + return RCLib.CodeToName.EnemySets[name] +end diff --git a/RCLib/RCLib.lua b/RCLib/RCLib.lua new file mode 100644 index 0000000..cd10c1e --- /dev/null +++ b/RCLib/RCLib.lua @@ -0,0 +1,80 @@ +--[[ + RCLib/RunControlLib + Author: + SleepSoul (Discord: SleepSoul#6006) + + A set of common utility functions used to modify run events and game content in mods like ChaosControl, EnemyControl, etc. +]] +ModUtil.Mod.Register("RCLib") + +function RCLib.GetEligible(inputTable,lookupTable) -- Read a table of bools, returning a table of the names of all that are true. Optionally use a lookup table to convert the names in inputTable. + local eligible = {} + for name, bool in pairs(inputTable) do + if bool then + if lookupTable ~= nil then + table.insert(eligible, lookupTable[name]) + else + table.insert(eligible, name) + end + end + end + return eligible +end + +function RCLib.RemoveIneligibleBools(inputTable, baseTable, lookupTable) -- Read two tables of bools, returning a table with all the values set to true in baseTable minus all the values set to false in inputTable. Optionally use a lookup table to convert the names in inputTable. + local eligible = {} + local match = false + for name, bool in pairs(baseTable) do + match = false + if bool then + if next(inputTable) == nil then + table.insert(eligible,name) + else + for name2, bool2 in pairs(inputTable) do + if lookupTable ~= nil and lookupTable[name2] == name and not bool2 then + match = true + elseif name2 == name and not bool2 then + match = true + end + end + if match == false then + table.insert(eligible, name) + end + end + end + end + return eligible +end + +function RCLib.RemoveIneligibleStrings(inputTable, baseTable, lookupTable) -- Read a table of bools and a table of strings, returning a table with all the strings in baseTable minus all the values set to false in inputTable. Optionally use a lookup table to convert the names in inputTable. + local eligible = {} + local match = false + for _, name in ipairs(baseTable) do + match = false + if next(inputTable) == nil then + table.insert(eligible,name) + else + for name2, bool in pairs(inputTable) do + if lookupTable ~= nil and lookupTable[name2] == name and not bool then + match = true + elseif name2 == name and not bool then + match = true + end + end + if match == false then + table.insert(eligible, name) + end + end + end + return eligible +end + +function RCLib.PopulateMinLength(targetTable, inputTable, minLength) -- Populates a target table with the contents of an input table, repeatedly inserting until a minimum length is reached. + local i = 0 + while i < minLength do + for _, name in pairs(inputTable) do + table.insert(targetTable, name) + i = i + 1 + end + end +end diff --git a/RCLib/modfile.txt b/RCLib/modfile.txt new file mode 100644 index 0000000..0b0a809 --- /dev/null +++ b/RCLib/modfile.txt @@ -0,0 +1,3 @@ +::Run Control Lib +Import "RCLib.lua" +Import "Map.lua" diff --git a/RemoveCutscenes/RemoveCutscenes.lua b/RemoveCutscenes/RemoveCutscenes.lua index 5c4364a..cfb46b2 100644 --- a/RemoveCutscenes/RemoveCutscenes.lua +++ b/RemoveCutscenes/RemoveCutscenes.lua @@ -5,7 +5,7 @@ Optionally removes intro and outro cutscenes to runs ]] -ModUtil.RegisterMod("RemoveCutscenes") +ModUtil.Mod.Register("RemoveCutscenes") local config = { ModName = "RemoveCutscenes", @@ -15,7 +15,7 @@ local config = { RemoveCutscenes.config = config -- Remove starting cutscene -ModUtil.WrapBaseFunction("ShowRunIntro", function( baseFunc ) +ModUtil.Path.Wrap("ShowRunIntro", function( baseFunc ) if config.RemoveIntro then return end @@ -24,7 +24,7 @@ ModUtil.WrapBaseFunction("ShowRunIntro", function( baseFunc ) end, RemoveCutscenes) -ModUtil.WrapBaseFunction("EndEarlyAccessPresentation", function ( baseFunc ) +ModUtil.Path.Wrap("EndEarlyAccessPresentation", function ( baseFunc ) if config.RemoveOutro then CurrentRun.ActiveBiomeTimer = false diff --git a/RoomDeterminism/RoomDeterminism.lua b/RoomDeterminism/RoomDeterminism.lua index eafdd6f..84d2752 100644 --- a/RoomDeterminism/RoomDeterminism.lua +++ b/RoomDeterminism/RoomDeterminism.lua @@ -1,4 +1,4 @@ -ModUtil.RegisterMod("RoomDeterminism") +ModUtil.Mod.Register("RoomDeterminism") local config = { -- TODO: these configs can yield infinite loops. Needs addressing before exposing to player via a UI @@ -330,11 +330,11 @@ function GetRoomBiome(room_name_to_find) error("Invalid room name passed to GetRoomBiome: " .. (room_name_to_find or "nil")) end -ModUtil.WrapBaseFunction("ChooseNextRoomData", function ( baseFunc, run, args ) +ModUtil.Path.Wrap("ChooseNextRoomData", function ( baseFunc, run, args ) local next_room_data = baseFunc( run, args ) -- If the mod isn't enabled or the next room is a vanilla chaos, then don't override the next room - if (not config.Enabled) or ModUtil.SafeGet(args, {"RoomDataSet"}) ~= nil then + if (not config.Enabled) or ModUtil.IndexArray.Get(args, {"RoomDataSet"}) ~= nil then return next_room_data end @@ -372,14 +372,14 @@ ModUtil.WrapBaseFunction("ChooseNextRoomData", function ( baseFunc, run, args ) return next_room_data end, RoomDeterminism) -ModUtil.WrapBaseFunction("LeaveRoom", function ( baseFunc, currentRun, door ) +ModUtil.Path.Wrap("LeaveRoom", function ( baseFunc, currentRun, door ) -- Reset our naive exit door counter RoomDeterminism.CurrentExitForDepth = 1 baseFunc(currentRun, door) end, RoomDeterminism) -ModUtil.WrapBaseFunction("StartNewRun", function ( baseFunc, currentRun ) +ModUtil.Path.Wrap("StartNewRun", function ( baseFunc, currentRun ) if config.Enabled and config.RoomGenerationAlgorithm ~= nil then RandomSynchronize(3110) RoomDeterminism.RoomGenerationAlgorithms[config.RoomGenerationAlgorithm]() diff --git a/RtaTimer/RtaTimer.lua b/RtaTimer/RtaTimer.lua index 293c22b..25fa729 100644 --- a/RtaTimer/RtaTimer.lua +++ b/RtaTimer/RtaTimer.lua @@ -5,7 +5,7 @@ Track RTA time and display it below the IGT timer ]] -ModUtil.RegisterMod("RtaTimer") +ModUtil.Mod.Register("RtaTimer") local config = { ModName = "RtaTimer", @@ -54,7 +54,16 @@ function RtaTimer.UpdateRtaTimer() local current_time = "00:00.00" -- If the timer has been reset, it should stay at 00:00.00 until it "starts" again if not RtaTimer.TimerWasReset then - current_time = RtaTimer.FormatElapsedTime(RtaTimer.StartTime, GetTime({ })) + -- Use _worldTime to prevent overusing system calls + current_time = RtaTimer.FormatElapsedTime(RtaTimer.StartWorldTime, _worldTime + RtaTimer.Offset) + RtaTimer.Cycle = RtaTimer.Cycle + 1 + + -- Update offset every 15 cycles to prevent drift during pauses + if RtaTimer.Cycle == 15 then + RtaTimer.Offset = (GetTime({ }) - RtaTimer.StartTime) - (_worldTime - RtaTimer.StartWorldTime) + current_time = RtaTimer.FormatElapsedTime(RtaTimer.StartTime, GetTime({ })) + RtaTimer.Cycle = 0 + end end PrintUtil.createOverlayLine( @@ -73,6 +82,9 @@ end function RtaTimer.StartRtaTimer() RtaTimer.Running = true + RtaTimer.Cycle = 0 + RtaTimer.Offset = 0.0 + while RtaTimer.Running do RtaTimer.UpdateRtaTimer() -- Update once per frame @@ -85,13 +97,14 @@ function RtaTimer__ResetRtaTimer() RtaTimer.UpdateRtaTimer() end -ModUtil.WrapBaseFunction("WindowDropEntrance", function( baseFunc, ... ) +ModUtil.Path.Wrap("WindowDropEntrance", function( baseFunc, ... ) local val = baseFunc(...) -- If single run, timer should always restart -- If multiweapon, only restart if timer was reset if not config.MultiWeapon or RtaTimer.TimerWasReset then RtaTimer.StartTime = GetTime({ }) + RtaTimer.StartWorldTime = _worldTime RtaTimer.TimerWasReset = false end @@ -101,7 +114,7 @@ ModUtil.WrapBaseFunction("WindowDropEntrance", function( baseFunc, ... ) end, RtaTimer) -- Stop timer when Hades dies (but leave it on screen) -ModUtil.WrapBaseFunction("HadesKillPresentation", function( baseFunc, ...) +ModUtil.Path.Wrap("HadesKillPresentation", function( baseFunc, ...) RtaTimer.Running = false baseFunc(...) end, RtaTimer) @@ -109,13 +122,14 @@ end, RtaTimer) ModUtil.LoadOnce( function() -- If not in a run, reset timer and prepare for run start - if ModUtil.PathGet("CurrentDeathAreaRoom") then + if ModUtil.Path.Get("CurrentDeathAreaRoom") then RtaTimer.TimerWasReset = true -- If in a run, just start the timer from the time the mod was loaded else RtaTimer.TimerWasReset = false RtaTimer.StartTime = GetTime({ }) + RtaTimer.StartWorldTime = _worldTime thread(RtaTimer.StartRtaTimer) end end diff --git a/RunStartControl b/RunStartControl new file mode 160000 index 0000000..cceb981 --- /dev/null +++ b/RunStartControl @@ -0,0 +1 @@ +Subproject commit cceb981438f9a21ba51b826bb5097f7b986f005d diff --git a/SatyrSackControl/SatyrSackControl.lua b/SatyrSackControl/SatyrSackControl.lua index b1b1181..36dbe0c 100644 --- a/SatyrSackControl/SatyrSackControl.lua +++ b/SatyrSackControl/SatyrSackControl.lua @@ -2,106 +2,52 @@ SatyrSackControl v1.0 Author: Museus (Discord: Museus#7777) - + SleepSoul (Discord: SleepSoul#6006) + Zyruvias (Discord: Zyruvias#3283) Forces the Styx satyr sack to appear in a specified range. ]] -ModUtil.RegisterMod("SatyrSackControl") +ModUtil.Mod.Register("SatyrSackControl") local config = { Enabled = true, -- If true, sack wil fall between MinSack and MaxSack + ForceShortTunnels = true, -- If true, all tunnels will be short MinSack = 2, -- Lowest tunnel count to see Sack MaxSack = 2 -- Highest tunnel count to see Sack } SatyrSackControl.config = config -- Scripts/RoomManager.lua : 1874 -ModUtil.WrapBaseFunction("StartRoom", function ( baseFunc, currentRun, currentRoom ) +ModUtil.Path.Wrap("StartRoom", function ( baseFunc, currentRun, currentRoom ) baseFunc(currentRun, currentRoom) end, SatyrSackControl) -- Scripts/UIScripts.lua : 145 -ModUtil.WrapBaseFunction("ShowCombatUI", function ( baseFunc, flag ) +ModUtil.Path.Wrap("ShowCombatUI", function ( baseFunc, flag ) baseFunc(flag) end, SatyrSackControl) -- Scripts/RunManager.lua : 591 -ModUtil.BaseOverride("IsRoomForced", function(currentRun, currentRoom, nextRoomData, args) - if nextRoomData.AlwaysForce then - return true - end - - if - nextRoomData.ForceIfEncounterNotCompleted ~= nil and - not HasEncounterBeenCompleted(nextRoomData.ForceIfEncounterNotCompleted) - then - return true - end - - if - nextRoomData.ForceIfUnseenForRuns ~= nil and - not HasSeenRoomInNumRuns(nextRoomData.Name, nextRoomData.ForceIfUnseenForRuns) - then - DebugPrint({Text = "Forcing = " .. nextRoomData.Name}) - return true - end - - args = args or {} - - local depthSkip = args.RoomsSkipped or 0 - local currentRunDepth = currentRun.RunDepthCache + depthSkip - if nextRoomData.ForceAtRunDepth ~= nil and currentRunDepth == nextRoomData.ForceAtRunDepth then - return true - end - if nextRoomData.ForceAtRunDepthMin ~= nil and currentRunDepth >= nextRoomData.ForceAtRunDepthMin then - if currentRunDepth >= nextRoomData.ForceAtRunDepthMax then - return true - else - local forcedChance = 1 / (nextRoomData.ForceAtRunDepthMax - currentRunDepth) - if RandomChance(forcedChance) then - return true - end - end - end - local currentBiomeDepth = currentRun.BiomeDepthCache + depthSkip - if nextRoomData.ForceAtBiomeDepth ~= nil and currentBiomeDepth == nextRoomData.ForceAtBiomeDepth then - return true - end - if nextRoomData.ForceAtBiomeDepthMin ~= nil and currentBiomeDepth >= nextRoomData.ForceAtBiomeDepthMin then - if currentBiomeDepth >= nextRoomData.ForceAtBiomeDepthMax then - return true - else - local forcedChance = 1 / (nextRoomData.ForceAtBiomeDepthMax - currentBiomeDepth) - if RandomChance(forcedChance) then - return true - end - end - end - - if - currentRoom ~= nil and currentRoom.ForceWingEndMiniBoss and nextRoomData.WingEndMiniBoss and - (currentRun.CompletedStyxWings < 4 or HasSeenRoomInRun(currentRun, "D_Reprieve01")) - then - return true - end - +ModUtil.Path.Wrap("IsRoomForced", function ( baseFunction, currentRun, currentRoom, nextRoomData, args ) if nextRoomData.ForceChanceByRemainingWings then - -- [[ CHANGES MADE HERE ]] - if SatyrSackControl.config.Enabled then - if (currentRun.CompletedStyxWings + 1) < SatyrSackControl.config.MinSack then - return false - end - - if (currentRun.CompletedStyxWings + 1) >= SatyrSackControl.config.MaxSack then - return true + if SatyrSackControl.config.Enabled then + if (currentRun.CompletedStyxWings + 1) < SatyrSackControl.config.MinSack then + return false + end + + if (currentRun.CompletedStyxWings + 1) >= SatyrSackControl.config.MaxSack then + return true + end end end - -- [[ END OF CHANGES ]] - - local chance = 1 / (5 - currentRun.CompletedStyxWings) - if RandomChance(chance) then - return true + return baseFunction( currentRun, currentRoom, nextRoomData, args ) + end) + +-- Scripts/RunManager.lua : 652 +ModUtil.Path.Wrap("IsRoomEligible", function ( baseFunction, currentRun, currentRoom, nextRoomData, args ) + if currentRoom ~= nil then + if SatyrSackControl.config.ForceShortTunnels and currentRun.WingDepth >= 3 and not nextRoomData.WingEndRoom then + return false end end - - return false -end) + return baseFunction( currentRun, currentRoom, nextRoomData, args ) +end) \ No newline at end of file diff --git a/ShowChamberNumber/ShowChamberNumber.lua b/ShowChamberNumber/ShowChamberNumber.lua index 855ee4a..8fd2a2e 100644 --- a/ShowChamberNumber/ShowChamberNumber.lua +++ b/ShowChamberNumber/ShowChamberNumber.lua @@ -7,7 +7,7 @@ Shows the current Chamber Number immediately upon starting a room. If that fails for some reason, fall back to showing the Depth during ShowCombatUI. ]] -ModUtil.RegisterMod("ShowChamberNumber") +ModUtil.Mod.Register("ShowChamberNumber") local config = { ShowDepth = true, @@ -15,7 +15,7 @@ local config = { ShowChamberNumber.config = config -- Scripts/RoomManager.lua : 1874 -ModUtil.WrapBaseFunction("StartRoom", function ( baseFunc, currentRun, currentRoom ) +ModUtil.Path.Wrap("StartRoom", function ( baseFunc, currentRun, currentRoom ) if config.ShowDepth then ShowDepthCounter() end @@ -24,7 +24,7 @@ ModUtil.WrapBaseFunction("StartRoom", function ( baseFunc, currentRun, currentRo end, ShowChamberNumber) -- Scripts/UIScripts.lua : 145 -ModUtil.WrapBaseFunction("ShowCombatUI", function ( baseFunc, flag ) +ModUtil.Path.Wrap("ShowCombatUI", function ( baseFunc, flag ) if config.ShowDepth then ShowDepthCounter() end @@ -33,7 +33,7 @@ ModUtil.WrapBaseFunction("ShowCombatUI", function ( baseFunc, flag ) end, ShowChamberNumber) -- Hiding Depth Counter doesn't actually do anything -ModUtil.WrapBaseFunction("HideDepthCounter", function ( baseFunc ) +ModUtil.Path.Wrap("HideDepthCounter", function ( baseFunc ) if config.ShowDepth then return end diff --git a/ShowChamberNumber/modfile.txt b/ShowChamberNumber/modfile.txt index 8636f76..2f79db0 100644 --- a/ShowChamberNumber/modfile.txt +++ b/ShowChamberNumber/modfile.txt @@ -1,3 +1,4 @@ +-: ShowChamberNumber Authors: Museus (Discord: Museus#7777) diff --git a/ThanatosControl/ThanatosControl.lua b/ThanatosControl/ThanatosControl.lua index 5561376..2cae21b 100644 --- a/ThanatosControl/ThanatosControl.lua +++ b/ThanatosControl/ThanatosControl.lua @@ -7,7 +7,7 @@ Gives options to modify or remove Thanatos ]] -ModUtil.RegisterMod("ThanatosControl") +ModUtil.Mod.Register("ThanatosControl") local config = { ThanatosSetting = "Removed" @@ -69,7 +69,7 @@ function updateNumberOfThanatos() if config.ThanatosSetting == "Removed" then maxThans = 0 end - ModUtil.MapSetTable(EncounterData, { + ModUtil.Table.Merge(EncounterData, { ThanatosTartarus = { MaxThanatosSpawnsThisRun = maxThans, }, @@ -92,7 +92,7 @@ function updateThanatosValues(data) data.MaxWaves = ThanatosControl.Presets[config.ThanatosSetting][biome].MaxWaves end -ModUtil.WrapBaseFunction("SetupEncounter", function( baseFunc, encounterData, room ) +ModUtil.Path.Wrap("SetupEncounter", function( baseFunc, encounterData, room ) -- TODO: Make this actually work if false and config.ThanatosSetting ~= "Removed" then local moddedEncounter = DeepCopyTable( encounterData ) @@ -106,7 +106,7 @@ ModUtil.WrapBaseFunction("SetupEncounter", function( baseFunc, encounterData, ro end, ThanatosControl) -- When a new run is started, make sure to apply the than modifications -ModUtil.WrapBaseFunction("StartNewRun", function ( baseFunc, currentRun ) +ModUtil.Path.Wrap("StartNewRun", function ( baseFunc, currentRun ) updateNumberOfThanatos() return baseFunc(currentRun) end, ThanatosControl) diff --git a/hades-CharonSackControl b/hades-CharonSackControl new file mode 160000 index 0000000..0daf7d9 --- /dev/null +++ b/hades-CharonSackControl @@ -0,0 +1 @@ +Subproject commit 0daf7d92c571988a627650676557e2b589d88a1c