├── Images ├── README.md ├── ModImporter.png ├── ModsFolder.png └── ContentFolder.png ├── Examples ├── Hades │ ├── MagicEdits │ │ ├── modfile.txt │ │ └── MagicEdits.lua │ ├── BottleFountain │ │ ├── modfile.txt │ │ └── BottleFountain.lua │ ├── GodAmongFish │ │ ├── modfile.txt │ │ └── GodAmongFish.lua │ ├── ClimbOfSisyphus │ │ ├── modfile.txt │ │ └── ClimbOfSisyphus.lua │ ├── GreedyRandomiser │ │ ├── modfile.txt │ │ └── GreedyRandomiser.lua │ ├── SequentialGifts │ │ ├── modfile.txt │ │ └── SequentialGifts.lua │ ├── AdjustKeepsakeProgress │ │ ├── modfile.txt │ │ └── AdjustKeepsakeProgress.lua │ ├── LootChoiceExt │ │ ├── modfile.txt │ │ └── LootChoiceExt.lua │ └── OLD │ │ └── modMagicEdits.lua └── Pyre │ └── VersusExtra │ ├── modfile.txt │ └── VersusExtra.lua ├── .gitmodules ├── Misc └── sjson_search.py ├── LICENSE ├── README.md └── Format Syntaxes └── modfile.xml /Images/README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Examples/Hades/MagicEdits/modfile.txt: -------------------------------------------------------------------------------- 1 | Import "MagicEdits.lua" -------------------------------------------------------------------------------- /Examples/Hades/BottleFountain/modfile.txt: -------------------------------------------------------------------------------- 1 | Import "BottleFountain.lua" -------------------------------------------------------------------------------- /Examples/Hades/GodAmongFish/modfile.txt: -------------------------------------------------------------------------------- 1 | Import "GodAmongFish.lua" -------------------------------------------------------------------------------- /Examples/Hades/ClimbOfSisyphus/modfile.txt: -------------------------------------------------------------------------------- 1 | Import "ClimbOfSisyphus.lua" -------------------------------------------------------------------------------- /Examples/Hades/GreedyRandomiser/modfile.txt: -------------------------------------------------------------------------------- 1 | Import "GreedyRandomiser.lua" -------------------------------------------------------------------------------- /Examples/Hades/SequentialGifts/modfile.txt: -------------------------------------------------------------------------------- 1 | Import "SequentialGifts.lua" -------------------------------------------------------------------------------- /Examples/Hades/AdjustKeepsakeProgress/modfile.txt: -------------------------------------------------------------------------------- 1 | Import "AdjustKeepsakeProgress.lua" -------------------------------------------------------------------------------- /Examples/Hades/LootChoiceExt/modfile.txt: -------------------------------------------------------------------------------- 1 | Load Priority 50 2 | Import "LootChoiceExt.lua" -------------------------------------------------------------------------------- /Examples/Pyre/VersusExtra/modfile.txt: -------------------------------------------------------------------------------- 1 | To "Scripts/MPScripts.lua" 2 | Import "VersusExtra.lua" 3 | -------------------------------------------------------------------------------- /Images/ModImporter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SGG-Modding/SGG-Mod-Format/HEAD/Images/ModImporter.png -------------------------------------------------------------------------------- /Images/ModsFolder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SGG-Modding/SGG-Mod-Format/HEAD/Images/ModsFolder.png -------------------------------------------------------------------------------- /Images/ContentFolder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SGG-Modding/SGG-Mod-Format/HEAD/Images/ContentFolder.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "ModImporter"] 2 | path = ModImporter 3 | url = https://github.com/SGG-Modding/ModImporter.git 4 | [submodule "SGGMI"] 5 | path = SGGMI 6 | url = https://github.com/SGG-Modding/SGGMI.git 7 | [submodule "ModUtil"] 8 | path = ModUtil 9 | url = https://github.com/SGG-Modding/ModUtil.git 10 | -------------------------------------------------------------------------------- /Examples/Hades/SequentialGifts/SequentialGifts.lua: -------------------------------------------------------------------------------- 1 | ModUtil.Mod.Register( "SequentialGifts" ) 2 | 3 | ModUtil.Path.Wrap( "CanReceiveGift", function( base, npcData, ... ) 4 | local name = GetGenusName( npcData ) 5 | local record = CurrentRun.GiftRecord[ name ] 6 | CurrentRun.GiftRecord[ name ] = false 7 | local ret = table.pack( base( npcData, ... ) ) 8 | CurrentRun.GiftRecord[ name ] = CurrentRun.GiftRecord[ name ] or record 9 | return table.unpack( ret ) 10 | end, SequentialGifts ) -------------------------------------------------------------------------------- /Misc/sjson_search.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import io 3 | import sjson 4 | import sys 5 | 6 | 7 | def traverse(tree, key, value): 8 | paths = [] 9 | iter = enumerate(tree) if isinstance(tree, list) else tree.items() 10 | for k, v in iter: 11 | if key == k and value == v: 12 | paths.append([k]) 13 | if isinstance(v, collections.OrderedDict) or isinstance(v, list): 14 | for p in traverse(v, key, value): 15 | t = [k] + p 16 | paths.append(t) 17 | return paths 18 | 19 | 20 | file = io.open(sys.argv[1], "r",encoding='utf-8-sig') 21 | obj = sjson.loads(file.read()) 22 | for path in traverse(obj, sys.argv[2], sys.argv[3]): 23 | out = "" 24 | for node in path: 25 | out = f"{out}::{str(node)}" 26 | print(out[2:]) 27 | -------------------------------------------------------------------------------- /Examples/Hades/GreedyRandomiser/GreedyRandomiser.lua: -------------------------------------------------------------------------------- 1 | ModUtil.Mod.Register( "GreedyRandomiser" ) 2 | -- Boon choice randomiser implemented a greedy way, likely to have bugs and incompatibilities 3 | 4 | local loot, gift = { }, { } 5 | for k, v in pairs( LootData ) do 6 | if v.GodLoot and not v.DebugOnly then 7 | ModUtil.Table.Merge( loot, v ) 8 | ModUtil.Table.Merge( gift, GiftData[ k ] ) 9 | end 10 | end 11 | LootData.RandomUpgrade = loot 12 | GiftData.RandomUpgrade = gift 13 | 14 | ModUtil.Path.Wrap( "CreateBoonLootButtons", function( base, lootData, ... ) 15 | if LootData[ lootData.Name ].GodLoot and not LootData[ lootData.Name ].DebugOnly then 16 | ModUtil.Table.Merge( LootData.RandomUpgrade, LootData[ lootData.Name ] ) 17 | ModUtil.Table.Merge( GiftData.RandomUpgrade, GiftData[ lootData.Name ] ) 18 | lootData.Name = "RandomUpgrade" 19 | end 20 | return base( lootData, ... ) 21 | end, GreedyRandomiser ) 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Andre Issa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Examples/Hades/AdjustKeepsakeProgress/AdjustKeepsakeProgress.lua: -------------------------------------------------------------------------------- 1 | ModUtil.Mod.Register( "AdjustKeepsakeProgress" ) 2 | 3 | local config = { 4 | -- Write any expression you want for the new keepsake progress to increase by 5 | Adjustment = " ... + 1 " 6 | -- "..." is the amount the progress increases by normally 7 | } 8 | 9 | AdjustKeepsakeProgress.Config = setmetatable( { }, { 10 | __index = config, 11 | __newindex = function( _, key, value ) 12 | config[ key ] = value 13 | if key == "Adjustment" then 14 | AdjustKeepsakeProgress.GenerateAdjustmentFunction( ) 15 | end 16 | end 17 | } ) 18 | 19 | function AdjustKeepsakeProgress.GenerateAdjustmentFunction( ) 20 | AdjustKeepsakeProgress.AdjustmentFunction = load( "local _ENV = { }; return " .. AdjustKeepsakeProgress.Config.Adjustment ) 21 | end 22 | 23 | AdjustKeepsakeProgress.Config.Adjustment = config.Adjustment 24 | 25 | ModUtil.Path.Wrap( "IncrementTableValue", function( base, tbl, key, amount, ... ) 26 | if tbl and tbl == ModUtil.PathGet( "GameState.KeepsakeChambers" ) then 27 | DebugPrint{ Text = "(INFO) AdjustKeepsakeProgress: Adjusted keepsake progress for " .. key .. " as: " .. AdjustKeepsakeProgress.Config.Adjustment } 28 | return base( tbl, key, AdjustKeepsakeProgress.AdjustmentFunction( amount or 1 ) ) 29 | end 30 | return base( tbl, key, amount, ... ) 31 | end, AdjustKeepsakeProgress ) 32 | -------------------------------------------------------------------------------- /Examples/Hades/GodAmongFish/GodAmongFish.lua: -------------------------------------------------------------------------------- 1 | ModUtil.Mod.Register( "GodAmongFish" ) 2 | 3 | local config = { 4 | PerfectInterval = 4, 5 | GoodInterval = 6, 6 | WayLateInterval = 8, 7 | MaxFakeDunks = 0, 8 | FishingPointChance = 1, 9 | RequiredMinRoomsSinceFishingPoint = 1, 10 | ClearFishingPointRequirements = true, 11 | GiveUnlimitedSkeletalLure = false, 12 | GiveHugeCatch = false 13 | } 14 | 15 | ModUtil.LoadOnce( function( ) 16 | ModUtil.Table.Merge( FishingData, { 17 | NumFakeDunks = { Max = config.MaxFakeDunks }, 18 | PerfectInterval = config.PerfectInterval, 19 | GoodInterval = config.GoodInterval, 20 | WayLateInterval = config.WayLateInterval, 21 | }) 22 | for k, v in pairs( RoomData ) do 23 | if v[ "FishingPointChance" ] then 24 | if config.ClearFishingPointRequirements then 25 | v["FishingPointRequirements"] = { } 26 | end 27 | ModUtil.Table.Merge( v, { 28 | FishingPointChance = config.FishingPointChance, 29 | FishingPointRequirements = { 30 | RequiredMinRoomsSinceFishingPoint = config.RequiredMinRoomsSinceFishingPoint, 31 | }, 32 | } ) 33 | end 34 | end 35 | end) 36 | 37 | ModUtil.Path.Wrap( "StartNewRun", function( StartNewRun, ... ) 38 | local currentRun = StartNewRun( ... ) 39 | if config.GiveUnlimitedSkeletalLure then 40 | local traitData = GetProcessedTraitData{ Unit = currentRun.Hero, TraitName = "TemporaryForcedFishingPointTrait", Rarity = "Common" } 41 | traitData.RemainingUses = math.inf 42 | TraitData[traitData.Name].RemainingUses = traitData.RemainingUses 43 | AddTraitToHero{ TraitData = traitData } 44 | end 45 | if config.GiveHugeCatch then 46 | AddTraitToHero{ TraitData = GetProcessedTraitData{ Unit = currentRun.Hero, TraitName = "FishingTrait", Rarity = "Legendary" } } 47 | end 48 | return currentRun 49 | end, "GodAmongFish" ) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Supergiant Games Mod Format 2 | Format for making and loading lua/sjson/xml mods for SuperGiantGames' games (Bastion, Transistor, Pyre, Hades) 3 | 4 | 5 | 6 | To use `modimporter.py` put it in the game's Content folder. 7 | For full functionality it also requires the [python SJSON module](https://github.com/SGG-Modding/sjson). 8 | 9 | When run with python it will read the mods in the folders in `Content/Mods` and implement the changes to the base files. 10 | The unedited base files will be stored in `Content/Backups`. 11 | 12 | **Check the [Wiki](https://github.com/MagicGonads/ssg-mod-format/wiki) for the important information!** 13 | 14 | [Format Specification](https://github.com/MagicGonads/sgg-mod-format/wiki/Format-Specification) 15 | [List of Mods in the Format](https://github.com/MagicGonads/sgg-mod-format/wiki/Importable-Mods-List) 16 | 17 | ### Tutorials 18 | *tells you how to do stuff* 19 | 20 | - [Installing Mods | The Mods Folder](https://github.com/MagicGonads/sgg-mod-format/wiki/Installing-Mods-%7C-The-Mods-Folder) 21 | 22 | ### Guides 23 | *tells you both how to do stuff and what you can do* 24 | 25 | - [The Mod Importer](https://github.com/MagicGonads/sgg-mod-format/wiki/The-Mod-Importer) 26 | - [Import Type: LUA](https://github.com/MagicGonads/sgg-mod-format/wiki/Import-Type:-LUA) 27 | - [Import Type: SJSON](https://github.com/MagicGonads/sgg-mod-format/wiki/Import-Type:-SJSON) 28 | - [Import Type: XML](https://github.com/MagicGonads/sgg-mod-format/wiki/Import-Type:-XML) 29 | 30 | ### Documentation 31 | *tells you what you can do* 32 | 33 | - [Modfiles | modfile.txt](https://github.com/MagicGonads/sgg-mod-format/wiki/Modfiles-%7C-modfile.txt) 34 | - [ModUtil Documentation](https://github.com/MagicGonads/sgg-mod-modutil/wiki/Documentation) 35 | 36 | ### Articles 37 | *tells you about stuff* 38 | 39 | - [On Importing Mods](https://github.com/MagicGonads/sgg-mod-format/wiki/On-Importing-Mods) 40 | -------------------------------------------------------------------------------- /Examples/Hades/BottleFountain/BottleFountain.lua: -------------------------------------------------------------------------------- 1 | ModUtil.Mod.Register("BottleFountain") 2 | 3 | local config = { 4 | debug = false, 5 | StartBottles = {}, 6 | NectarCost = 1, 7 | } 8 | BottleFountain.config = config 9 | 10 | function BottleFountain.SetupBottles() 11 | if CurrentRun.FountainBottles == nil then CurrentRun.FountainBottles = config.StartBottles end 12 | end 13 | 14 | function BottleFountain.HasBottles( amount ) 15 | BottleFountain.SetupBottles() 16 | return #CurrentRun.FountainBottles >= amount 17 | end 18 | 19 | function BottleFountain.ConsumeBottle( amount ) 20 | BottleFountain.SetupBottles() 21 | for i = 1, amount do 22 | BottleFountain.DoHeal(table.remove(CurrentRun.FountainBottles)) 23 | end 24 | if config.debug then ModUtil.Hades.PrintStack("Bottles "..#CurrentRun.FountainBottles) end 25 | end 26 | 27 | function BottleFountain.CollectBottle( amount, healFraction ) 28 | BottleFountain.SetupBottles() 29 | for i = 1, amount do 30 | table.insert(CurrentRun.FountainBottles,healFraction) 31 | end 32 | if config.debug then ModUtil.Hades.PrintStack("Bottles "..#CurrentRun.FountainBottles) end 33 | end 34 | 35 | function BottleFountain.DoHeal( healFraction ) 36 | healFraction = healFraction * CalculateHealingMultiplier() 37 | Heal( CurrentRun.Hero, { HealFraction = healFraction, SourceName = "BottleFountain" } ) 38 | thread(UpdateHealthUI) 39 | end 40 | 41 | ModUtil.Path.Wrap( "BeginOpeningCodex", function(base, ... ) 42 | if not CanOpenCodex() and not AreScreensActive() and IsInputAllowed({}) then 43 | if CurrentRun.Hero.MaxHealth > CurrentRun.Hero.Health then 44 | wait(0.2) 45 | BottleFountain.SetupBottles() 46 | if BottleFountain.HasBottles( 1 ) then 47 | BottleFountain.ConsumeBottle( 1 ) 48 | end 49 | wait(0.2) 50 | end 51 | end 52 | return base(...) 53 | end, BottleFountain) 54 | 55 | ModUtil.Path.Wrap( "Heal", function(base, victim, triggerArgs) 56 | if victim == CurrentRun.Hero then 57 | if triggerArgs.SourceName == "HealthFountain" and triggerArgs.HealFraction ~= 0 and HasResource( "GiftPoints", config.NectarCost ) then 58 | local bottleChose = false 59 | local screen = ModUtil.Hades.NewMenuYesNo( 60 | "BottleFountainGet", 61 | function() 62 | if bottleChose then return end 63 | base(victim, triggerArgs) 64 | thread(UpdateHealthUI) 65 | end, 66 | ModUtil.Hades.DimMenu, 67 | function() 68 | SpendResource( "GiftPoints", config.NectarCost, "BottleFountain" ) 69 | BottleFountain.CollectBottle( 1, triggerArgs.HealFraction / CalculateHealingMultiplier() ) 70 | bottleChose = true 71 | end, 72 | function() end, 73 | "Bottled Fountain Vigor", 74 | "Bottle up at the cost of nectar or drink now?", 75 | "Bottle Up", 76 | "Drink", 77 | "GiftIcon",1.25 78 | ) 79 | return 80 | end 81 | end 82 | return base(victim, triggerArgs) 83 | end, BottleFountain) 84 | -------------------------------------------------------------------------------- /Examples/Pyre/VersusExtra/VersusExtra.lua: -------------------------------------------------------------------------------- 1 | ModUtil.Mod.Register( "VersusExtra" ) 2 | 3 | local config = { 4 | TeamA = { 5 | BaseCount = 21, 6 | { 7 | Base = 2, 8 | Bench = 11, 9 | NilTable = {}, 10 | SetTable = {}, 11 | }, 12 | { 13 | Base = 3, 14 | Bench = 11, 15 | NilTable = {}, 16 | SetTable = {}, 17 | }, 18 | { 19 | Base = 2, 20 | Bench = 10, 21 | NilTable = {}, 22 | SetTable = {}, 23 | }, 24 | { 25 | Base = 3, 26 | Bench = 10, 27 | NilTable = {}, 28 | SetTable = {}, 29 | }, 30 | }, 31 | } 32 | config.TeamB = config.TeamA 33 | VersusExtra.config = config 34 | 35 | function VersusExtra.CopyCharacterTeamData( character, copyteam ) 36 | character.MaskHue = copyteam.MaskHue 37 | character.MaskSaturationAddition = copyteam.MaskSaturationAddition 38 | character.MaskValueAddition = copyteam.MaskValueAddition 39 | character.MaskHue2 = copyteam.MaskHue2 40 | character.MaskSaturationAddition2 = copyteam.MaskSaturationAddition2 41 | character.MaskValueAddition2 = copyteam.MaskValueAddition2 42 | character.UsePhantomShader = copyteam.UsePhantomShader 43 | end 44 | 45 | function VersusExtra.AddCharacter( addteam, data, index ) 46 | local copyteam = League[data.Bench] 47 | local character = DeepCopyTable(copyteam.TeamBench[data.Base]) 48 | 49 | VersusExtra.CopyCharacterTeamData( character, copyteam ) 50 | 51 | ModUtil.MapNilTable(character,data.NilTable) 52 | ModUtil.Table.Merge(character,data.SetTable) 53 | 54 | if index == nil then 55 | index = #addteam.TeamBench + 1 56 | else 57 | for i = #addteam.TeamBench, index, -1 do 58 | addteam.TeamBench[i].CharacterIndex = i+1 59 | addteam.TeamBench[i+1] = addteam.TeamBench[i] 60 | end 61 | end 62 | 63 | character.TeamIndex = addteam.LeagueIndex 64 | character.CharacterIndex = index 65 | 66 | addteam.TeamBench[index] = character 67 | end 68 | 69 | ModUtil.Path.Wrap( "PrepareLocalMPDraft", function(base, TeamAid, TeamBid ) 70 | local TeamA = League[TeamAid] 71 | local TeamB = League[TeamBid] 72 | if #TeamA.TeamBench == config.TeamA.BaseCount and #TeamB.TeamBench == config.TeamB.BaseCount then 73 | for i = 1, #config.TeamA, 1 do 74 | local data = config.TeamA[i] 75 | if not data.Bench then data.Bench = TeamAid end 76 | VersusExtra.AddCharacter( TeamA, data ) 77 | end 78 | for i = 1, #config.TeamB, 1 do 79 | local data = config.TeamB[i] 80 | if not data.Bench then data.Bench = TeamBid end 81 | VersusExtra.AddCharacter( TeamB, data ) 82 | end 83 | end 84 | return base( TeamAid, TeamBid ) 85 | end, VersusExtra) 86 | 87 | ModUtil.Path.Wrap( "DisplayDraftScreen", function(base, ...) 88 | local ret = base(...) 89 | if IsMultiplayerMatch() then 90 | SetMenuOptions({ Name = "RosterScreen", Item = "YButton", Properties = {OffsetY = 400} }) 91 | end 92 | return ret 93 | end, VersusExtra) 94 | 95 | ModUtil.Path.Wrap( "ViewTeam", function(base, ...) 96 | local ret = base(...) 97 | if IsMultiplayerMatch() then 98 | SetMenuOptions({ Name = "RosterScreen", Item = "YButton", Properties = {OffsetY = 400} }) 99 | end 100 | return ret 101 | end, VersusExtra) -------------------------------------------------------------------------------- /Examples/Hades/LootChoiceExt/LootChoiceExt.lua: -------------------------------------------------------------------------------- 1 | ModUtil.Mod.Register("LootChoiceExt") 2 | 3 | local config = { 4 | MinExtraLootChoices = 1, 5 | MaxExtraLootChoices = 2 6 | } 7 | LootChoiceExt.config = config 8 | 9 | local baseChoices = GetTotalLootChoices() 10 | 11 | -- Other mods should override this value 12 | LootChoiceExt.Choices = baseChoices 13 | 14 | LootChoiceExt.GetBaseChoices = function( ) 15 | return baseChoices 16 | end 17 | 18 | OnAnyLoad{ function() 19 | CurrentRun.LastLootChoices = LootChoiceExt.Choices + RandomInt( config.MinExtraLootChoices, config.MaxExtraLootChoices ) 20 | end} 21 | 22 | ModUtil.Path.Override("GetTotalLootChoices", function( ) 23 | return CurrentRun.LastLootChoices or LootChoiceExt.Choices 24 | end, LootChoiceExt) 25 | 26 | ModUtil.Path.Override("CalcNumLootChoices", function( ) 27 | local numChoices = CurrentRun.LastLootChoices - GetNumMetaUpgrades("ReducedLootChoicesShrineUpgrade") 28 | return numChoices 29 | end, LootChoiceExt) 30 | 31 | ModUtil.Path.Context.Wrap("CreateBoonLootButtons", function( ) 32 | local data = { } 33 | local active = false 34 | ModUtil.Path.Wrap( "CreateScreenComponent", function( base, args ) 35 | if not active and args.Name == "TraitBacking" then 36 | active = true 37 | local locals = ModUtil.Locals.Stacked( ) 38 | data.upgrade = locals.upgradeData 39 | local excess = math.max( 3,#locals.upgradeOptions )-3 40 | locals.itemLocationY = locals.itemLocationY-12.5*excess 41 | data.squashY = 3/(3+excess) 42 | data.iconScaling = (excess == 0) and 0.85 or 1 43 | elseif active and args.Group == "Combat_Menu" then 44 | local locals = ModUtil.Locals.Stacked( ) 45 | if args.Name == "TraitBacking" then 46 | locals.itemLocationY = locals.itemLocationY + 220*(data.squashY-1) 47 | args.Y = locals.itemLocationY 48 | end 49 | if args.Name == "BoonSlot"..locals.itemIndex then 50 | locals.iconOffsetY = -2*data.squashY 51 | args.Name = "BoonSlot"..RandomInt( 1, 3 ) 52 | end 53 | end 54 | local component = base( args ) 55 | SetScaleY({Id = component.Id, Fraction = 1}) 56 | return component 57 | end, LootChoiceExt ) 58 | ModUtil.Path.Wrap( "CreateTextBox", function( base, args ) 59 | if active then 60 | if args.OffsetY then args.OffsetY = args.OffsetY*data.squashY end 61 | if args.FontSize then args.FontSize = args.FontSize*math.pow(data.squashY,1/3) end 62 | if data.upgrade and args.Text == data.upgrade.CustomRarityName then 63 | ModUtil.Locals.Stacked( ).lineSpacing = 8*data.squashY 64 | end 65 | end 66 | return base( args ) 67 | end, LootChoiceExt ) 68 | ModUtil.Path.Wrap( "SetScale", function( base, args ) 69 | if active then 70 | args.Fraction = data.iconScaling 71 | SetScaleY(args) 72 | args.Fraction = data.iconScaling*data.squashY 73 | return SetScaleX(args) 74 | end 75 | return base( args ) 76 | end, LootChoiceExt) 77 | ModUtil.Path.Wrap( "SetScaleY", function( base, args ) 78 | if active then args.Fraction = args.Fraction*data.squashY end 79 | return base( args ) 80 | end, LootChoiceExt) 81 | ModUtil.Path.Wrap( "IsMetaUpgradeSelected", function( base, arg ) 82 | if active and arg == "RerollPanelMetaUpgrade" then active = false end 83 | return base( arg ) 84 | end, LootChoiceExt ) 85 | end, LootChoiceExt) 86 | 87 | ModUtil.Path.Override("DestroyBoonLootButtons", function( lootData ) 88 | local components = ScreenAnchors.ChoiceScreen.Components 89 | local toDestroy = {} 90 | for index = 1, GetTotalLootChoices() do 91 | local destroyIndexes = { 92 | "PurchaseButton"..index, 93 | "PurchaseButton"..index.. "Lock", 94 | "PurchaseButton"..index.. "Icon", 95 | "PurchaseButton"..index.. "ExchangeIcon", 96 | "PurchaseButton"..index.. "ExchangeIconFrame", 97 | "PurchaseButton"..index.. "QuestIcon", 98 | "Backing"..index, 99 | "PurchaseButton"..index.. "Frame", 100 | "PurchaseButton"..index.. "Patch" 101 | } 102 | for i, indexName in pairs( destroyIndexes ) do 103 | if components[indexName] then 104 | table.insert(toDestroy, components[indexName].Id) 105 | components[indexName] = nil 106 | end 107 | end 108 | end 109 | if components["RerollPanel"] then 110 | table.insert(toDestroy, components["RerollPanel"].Id) 111 | components["RerollPanel"] = nil 112 | end 113 | if ScreenAnchors.ChoiceScreen.SacrificedTraitId then 114 | table.insert(toDestroy, ScreenAnchors.ChoiceScreen.SacrificedTraitId ) 115 | ScreenAnchors.ChoiceScreen.SacrificedTraitId = nil 116 | end 117 | if ScreenAnchors.ChoiceScreen.SacrificedFrameId then 118 | table.insert(toDestroy, ScreenAnchors.ChoiceScreen.SacrificedFrameId ) 119 | ScreenAnchors.ChoiceScreen.SacrificedFrameId = nil 120 | end 121 | if ScreenAnchors.ChoiceScreen.ActivateSwapId then 122 | table.insert(toDestroy, ScreenAnchors.ChoiceScreen.ActivateSwapId ) 123 | ScreenAnchors.ChoiceScreen.ActivateSwapId = nil 124 | end 125 | Destroy({ Ids = toDestroy }) 126 | end, LootChoiceExt) 127 | -------------------------------------------------------------------------------- /Format Syntaxes/modfile.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 00:: 01 02\n 03-: 04:- 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | , ; 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | Include To Hookify 28 | Import SJSON XML 29 | Load Priority Before After Require 30 | Anchor 31 | 32 | 33 | 34 | 35 | 00" 01 02" 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /Examples/Hades/OLD/modMagicEdits.lua: -------------------------------------------------------------------------------- 1 | -- IMPORT @ DEFAULT 2 | -- PRIORITY 150 3 | 4 | --[[ Installation Instructions: 5 | Place this file in /Content/Mods/modMagicEdits/Scripts 6 | Add 'Import "../Mods/modMagicEdits/Scripts/modMagicEdits.lua"' to the bottom of RoomManager.lua 7 | Configure by changing values in the config table below 8 | Load/reload a save 9 | --]] 10 | 11 | --[[ 12 | Mod: Magic Edits 13 | Author: MagicGonads 14 | Collection of small configurable edits with no particular theme 15 | Proof of concept for this mod format 16 | -]] 17 | 18 | --[[ Future Ideas 19 | - Increase upgrade density (replace resources?) 20 | - No health loss on chaos chamber entrance 21 | - Make chaos debuff chamber numbers really small or zero / instantly remove them 22 | - Make Charon "chambers" numbers really large / make them permanent in a proper way 23 | - Keys instead of heat for heat shortcuts 24 | ]] 25 | 26 | modMagicEdits = {} 27 | SaveIgnores["modMagicEdits"] = true 28 | 29 | local config = -- (set to nil or false to disable any of them) 30 | { 31 | AlwaysSeeChamberCount = true, -- Always see chamber depth in the top right during runs 32 | AlwaysUseSpecialDoors = true, -- Can always open special chamber doors (will still cost whatever it costs) 33 | CanAlwaysExitRoom = true, -- Exit chambers early by interacting with the exit doors 34 | UnlockEveryCodexEntry = false, -- Unlock every codex entry ... (will affect your save permanently!) 35 | AllowRespecMirror = false, -- Always able to respec at the mirror and swap upgrades (will affect your save permanently!) 36 | ChooseMultipleUpgrades = 1, -- Choose multiple upgrades at any given choice (multiplier of available number) 37 | PlayerDamageMult = 0.5, -- Multiply damage the player recieves 38 | EnemyDamageMult = 1.5, -- Multiply damage enemies recieve 39 | ExtraRarity = 0.65, -- Global boost to rarity (100% best rarity if 1 or more, 0 unchanged) 40 | ExtraMoney = 5, -- Global multiplier of charon coins gained 41 | PurchaseCost = 0, -- Global multiplier of all purchase costs (except using darkness on mirror) 42 | GatherBonus = { -- Multipliers on resources gained 43 | Global = 3, 44 | MetaPoints = 50, -- Darkness 45 | GiftPoints = 5, -- Nectar 46 | LockKeys = 5, -- Chthonic Keys 47 | Gems = 20, -- Gemstones 48 | } 49 | } 50 | 51 | modMagicEdits.config = config 52 | 53 | if config.UnlockEveryCodexEntry then 54 | local function doUnlockEveryCodexEntry() 55 | if CodexStatus then 56 | CodexStatus.Enabled = true 57 | for chapterName, chapterData in pairs(Codex) do 58 | for entryName, entryData in pairs(Codex[chapterName].Entries) do 59 | UnlockCodexEntry(chapterName, entryName, 1, true ) 60 | end 61 | end 62 | end 63 | doUnlockEveryCodexEntry = function() end 64 | end 65 | OnAnyLoad{ doUnlockEveryCodexEntry } 66 | end 67 | 68 | if config.AllowRespecMirror then 69 | OnAnyLoad{ 70 | function() 71 | GameState.Flags.SwapMetaupgradesEnabled = true 72 | end 73 | } 74 | end 75 | 76 | if config.GatherBonus then 77 | local baseAddResource = AddResource 78 | function AddResource( name, amount, source, args ) 79 | if source ~= "MetaPointCapRefund" then 80 | if amount then 81 | if config.GatherBonus[name] then 82 | return baseAddResource( name, config.GatherBonus[name]*amount, source, args ) 83 | elseif config.GatherBonus.Global then 84 | return baseAddResource( name, config.GatherBonus.Global*amount, source, args ) 85 | end 86 | end 87 | end 88 | baseAddResource( name, amount, source, args ) 89 | end 90 | end 91 | 92 | if config.PurchaseCost then 93 | local baseHasResource = HasResource 94 | function HasResource( name, amount ) 95 | if name ~= "MetaPoints" then 96 | if amount then 97 | return baseHasResource( name, config.PurchaseCost*amount ) 98 | end 99 | end 100 | return baseHasResource( name, amount ) 101 | end 102 | 103 | local baseSpendResource = SpendResource 104 | function SpendResource( name, amount, source, args ) 105 | if amount then 106 | return baseSpendResource( name, config.PurchaseCost*amount, source, args ) 107 | end 108 | return baseSpendResource( name, amount, source, args ) 109 | end 110 | 111 | local baseSpendMoney = SpendMoney 112 | function SpendMoney( amount, source ) 113 | if amount then 114 | return baseSpendMoney( config.PurchaseCost*amount, source ) 115 | end 116 | return baseSpendMoney( amount, source ) 117 | end 118 | end 119 | 120 | if config.ChooseMultipleUpgrades then 121 | if config.ChooseMultipleUpgrades >= 0 then 122 | 123 | local baseCloseUpgradeChoiceScreen = CloseUpgradeChoiceScreen 124 | function CloseUpgradeChoiceScreen( screen, button ) 125 | CurrentRun.Hero.UpgradeChoicesSinceMenuOpened = CurrentRun.Hero.UpgradeChoicesSinceMenuOpened - 1 126 | if CurrentRun.Hero.UpgradeChoicesSinceMenuOpened < 1 then 127 | baseCloseUpgradeChoiceScreen( screen, button ) 128 | end 129 | end 130 | 131 | local baseCreateBoonLootButtons = CreateBoonLootButtons 132 | function CreateBoonLootButtons( lootData ) 133 | if lootData.UpgradeOptions == nil then 134 | SetTraitsOnLoot( lootData ) 135 | end 136 | if IsEmpty( lootData.UpgradeOptions ) then 137 | table.insert(lootData.UpgradeOptions, { ItemName = "FallbackMoneyDrop", Type = "Consumable", Rarity = "Common" }) 138 | end 139 | CurrentRun.Hero.UpgradeChoicesSinceMenuOpened = TableLength(lootData.UpgradeOptions) 140 | if CurrentRun.Hero.UpgradeChoicesSinceMenuOpened then 141 | CurrentRun.Hero.UpgradeChoicesSinceMenuOpened=CurrentRun.Hero.UpgradeChoicesSinceMenuOpened*config.ChooseMultipleUpgrades 142 | else 143 | CurrentRun.Hero.UpgradeChoicesSinceMenuOpened = 1 144 | end 145 | return baseCreateBoonLootButtons( lootData ) 146 | end 147 | end 148 | end 149 | 150 | if config.ExtraRarity then 151 | 152 | local baseSetTraitsOnLoot = SetTraitsOnLoot 153 | function SetTraitsOnLoot( lootData ) 154 | if lootData.RarityChances.Legendary and config.ExtraRarity < 1 then 155 | lootData.RarityChances.Legendary = lootData.RarityChances.Legendary*(1-config.ExtraRarity) + config.ExtraRarity 156 | else 157 | lootData.RarityChances.Legendary = config.ExtraRarity 158 | end 159 | if lootData.RarityChances.Heroic and config.ExtraRarity < 1 then 160 | lootData.RarityChances.Heroic = lootData.RarityChances.Heroic*(1-config.ExtraRarity) + config.ExtraRarity 161 | else 162 | lootData.RarityChances.Heroic = config.ExtraRarity 163 | end 164 | if lootData.RarityChances.Epic and config.ExtraRarity < 1 then 165 | lootData.RarityChances.Epic = lootData.RarityChances.Epic*(1-config.ExtraRarity) + config.ExtraRarity 166 | else 167 | lootData.RarityChances.Epic = config.ExtraRarity 168 | end 169 | if lootData.RarityChances.Rare and config.ExtraRarity < 1 then 170 | lootData.RarityChances.Rare = lootData.RarityChances.Rare*(1-config.ExtraRarity) + config.ExtraRarity 171 | else 172 | lootData.RarityChances.Rare = config.ExtraRarity 173 | end 174 | if lootData.RarityChances.Common and config.ExtraRarity < 1 then 175 | lootData.RarityChances.Common = lootData.RarityChances.Common*(1-config.ExtraRarity) + config.ExtraRarity 176 | else 177 | lootData.RarityChances.Common = config.ExtraRarity 178 | end 179 | baseSetTraitsOnLoot( lootData ) 180 | end 181 | end 182 | 183 | if config.ExtraMoney then 184 | local baseAddMoney = AddMoney 185 | function AddMoney( amount, source ) 186 | if amount then 187 | return baseAddMoney( config.ExtraMoney*amount, source ) 188 | end 189 | return baseAddMoney( amount, source ) 190 | end 191 | end 192 | 193 | if config.AlwaysSeeChamberCount then 194 | local baseShowHealthUI = ShowHealthUI 195 | function ShowHealthUI() 196 | ShowDepthCounter() 197 | baseShowHealthUI() 198 | end 199 | end 200 | 201 | if config.AlwaysUseSpecialDoors then 202 | local baseCheckSpecialDoorRequirement = CheckSpecialDoorRequirement 203 | function CheckSpecialDoorRequirement( door ) 204 | baseCheckSpecialDoorRequirement( door ) 205 | return nil 206 | end 207 | end 208 | 209 | if config.PlayerDamageMult then 210 | local baseDamage = Damage 211 | function Damage( victim, triggerArgs ) 212 | if triggerArgs.DamageAmount and victim == CurrentRun.Hero then 213 | triggerArgs.DamageAmount = triggerArgs.DamageAmount * config.PlayerDamageMult 214 | end 215 | baseDamage( victim, triggerArgs ) 216 | end 217 | end 218 | if config.EnemyDamageMult then 219 | local baseDamageEnemy = DamageEnemy 220 | function DamageEnemy( victim, triggerArgs ) 221 | if triggerArgs.DamageAmount then 222 | triggerArgs.DamageAmount = triggerArgs.DamageAmount * config.EnemyDamageMult 223 | end 224 | baseDamageEnemy( victim, triggerArgs ) 225 | end 226 | end 227 | 228 | if config.CanAlwaysExitRoom then 229 | local baseCheckRoomExitsReady = CheckRoomExitsReady 230 | function CheckRoomExitsReady( currentRoom ) 231 | baseCheckRoomExitsReady( currentRoom ) 232 | return true 233 | end 234 | 235 | local baseAttemptUseDoor = AttemptUseDoor 236 | function AttemptUseDoor( door ) 237 | if not door.Room then 238 | DoUnlockRoomExits( CurrentRun, CurrentRoom ) 239 | else 240 | door.ReadyToUse = true 241 | baseAttemptUseDoor( door ) 242 | end 243 | end 244 | end 245 | -------------------------------------------------------------------------------- /Examples/Hades/MagicEdits/MagicEdits.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Mod: Magic Edits 3 | Author: MagicGonads 4 | Collection of small configurable edits with no particular theme 5 | Proof of concept for this mod format 6 | -]] 7 | 8 | ModUtil.Mod.Register("MagicEdits") 9 | 10 | local config = -- (set to nil or false to disable any of them) 11 | { 12 | AlwaysSeeChamberCount = true, -- Always see chamber depth in the top right during runs 13 | AlwaysUseSpecialDoors = true, -- Can always open special chamber doors (will still cost whatever it costs) 14 | CanAlwaysExitRoom = true, -- Exit chambers early by interacting with the exit doors 15 | UnlockEveryCodexEntry = false, -- Unlock every codex entry ... (will affect your save permanently!) 16 | AllowRespecMirror = false, -- Always able to respec at the mirror and swap upgrades (will affect your save permanently!) 17 | UnlimitedGodsPerRun = true, -- Any unlocked god can appear in the same run 18 | ChooseMultipleUpgrades = 1, -- Choose multiple upgrades at any given choice (to always choose all options once put '...' instead (quotes included)) 19 | PlayerDamageMult = 0.5, -- Multiply damage the player recieves 20 | EnemyDamageMult = 1.5, -- Multiply damage enemies recieve 21 | ExtraMoney = 5, -- Global multiplier of charon coins gained 22 | MoneyCost = 0, -- Global Multiplier of coin prices 23 | PurchaseCost = { -- Multiplier of purchase costs (except using darkness on mirror) 24 | Global = 0, 25 | GiftPoints = 1, -- Nectar 26 | LockKeys = 0, -- Chthonic Keys 27 | Gems = 0, -- Gemstones 28 | }, 29 | GatherBonus = { -- Multipliers on resources gained 30 | Global = 3, 31 | MetaPoints = 50, -- Darkness 32 | GiftPoints = 5, -- Nectar 33 | LockKeys = 5, -- Chthonic Keys 34 | Gems = 20, -- Gemstones 35 | }, 36 | ExtraRarity = { -- boosts to rarity (100% best rarity if 1 or more, 0 unchanged) 37 | Legendary = 0.65, 38 | Heroic = 0.65, 39 | Epic = 0.65, 40 | Rare = 0.65, 41 | Common = 0.65, 42 | } 43 | } 44 | 45 | local function generateFunction( value ) 46 | if type(value) == "string" then 47 | return load( "local _ENV = { }; return " .. value ) 48 | end 49 | return function() return value end 50 | end 51 | 52 | MagicEdits.Config = setmetatable( { }, { 53 | __index = config, 54 | __newindex = function( _, key, value ) 55 | config[ key ] = value 56 | if key == "ChooseMultipleUpgrades" then 57 | config.ChooseMultipleUpgradesFunction = generateFunction( value ) 58 | end 59 | end 60 | } ) 61 | 62 | MagicEdits.Config.ChooseMultipleUpgrades = config.ChooseMultipleUpgrades 63 | 64 | ModUtil.Path.Wrap( "IncrementTableValue", function( base, tbl, key, amount, ... ) 65 | if tbl and tbl == ModUtil.PathGet( "GameState.KeepsakeChambers" ) then 66 | DebugPrint{ Text = "(INFO) AdjustKeepsakeProgress: Adjusted keepsake progress for " .. key .. " as: " .. AdjustKeepsakeProgress.Config.Adjustment } 67 | return base( tbl, key, AdjustKeepsakeProgress.AdjustmentFunction( amount or 1 ) ) 68 | end 69 | return base( tbl, key, amount, ... ) 70 | end, AdjustKeepsakeProgress ) 71 | 72 | if config.UnlockEveryCodexEntry then 73 | ModUtil.LoadOnce( function() 74 | if CodexStatus then 75 | CodexStatus.Enabled = true 76 | for chapterName, chapterData in pairs(Codex) do 77 | for entryName, entryData in pairs(Codex[chapterName].Entries) do 78 | UnlockCodexEntry(chapterName, entryName, 1, true ) 79 | end 80 | end 81 | end 82 | end) 83 | end 84 | 85 | if config.AllowRespecMirror then 86 | OnAnyLoad{ 87 | function() 88 | GameState.Flags.SwapMetaupgradesEnabled = true 89 | end 90 | } 91 | end 92 | 93 | if config.GatherBonus then 94 | ModUtil.Path.Wrap("AddResource", function( base, name, amount, source, args ) 95 | if source ~= "MetaPointCapRefund" then 96 | if amount then 97 | if config.GatherBonus[name] then 98 | return base( name, config.GatherBonus[name]*amount, source, args ) 99 | elseif config.GatherBonus.Global then 100 | return base( name, config.GatherBonus.Global*amount, source, args ) 101 | end 102 | end 103 | end 104 | base( name, amount, source, args ) 105 | end, MagicEdits) 106 | end 107 | 108 | if config.PurchaseCost then 109 | ModUtil.Path.Wrap( "HasResource", function( base, name, amount ) 110 | if name ~= "MetaPoints" then 111 | if amount then 112 | if config.PurchaseCost[name] then 113 | return base( name, config.PurchaseCost[name]*amount) 114 | elseif config.PurchaseCost.Global then 115 | return base( name, config.PurchaseCost.Global*amount) 116 | end 117 | end 118 | end 119 | return base( name, amount ) 120 | end, MagicEdits) 121 | 122 | ModUtil.Path.Wrap( "SpendResource", function( base, name, amount, source, args ) 123 | if amount then 124 | if config.PurchaseCost[name] then 125 | return base( name, config.PurchaseCost[name]*amount, source, args) 126 | elseif config.PurchaseCost.Global then 127 | return base( name, config.PurchaseCost.Global*amount, source, args) 128 | end 129 | end 130 | return base( name, amount ) 131 | end, MagicEdits) 132 | end 133 | 134 | if config.MoneyCost then 135 | ModUtil.Path.Wrap( "SpendMoney", function(base, amount, source ) 136 | if amount then 137 | return base( config.MoneyCost*amount, source ) 138 | end 139 | return base( amount, source ) 140 | end, MagicEdits) 141 | end 142 | 143 | if config.ChooseMultipleUpgrades then 144 | ModUtil.Path.Wrap( "CloseUpgradeChoiceScreen", function( base, screen, button ) 145 | CurrentRun.Hero.UpgradeChoicesSinceMenuOpened = CurrentRun.Hero.UpgradeChoicesSinceMenuOpened - 1 146 | if CurrentRun.Hero.UpgradeChoicesSinceMenuOpened < 1 then 147 | base( screen, button ) 148 | end 149 | end, MagicEdits) 150 | 151 | ModUtil.Path.Wrap( "CreateBoonLootButtons", function( base, lootData) 152 | if lootData.UpgradeOptions == nil then 153 | SetTraitsOnLoot( lootData ) 154 | end 155 | if IsEmpty( lootData.UpgradeOptions ) then 156 | table.insert(lootData.UpgradeOptions, { ItemName = "FallbackMoneyDrop", Type = "Consumable", Rarity = "Common" }) 157 | end 158 | CurrentRun.Hero.UpgradeChoicesSinceMenuOpened = TableLength(lootData.UpgradeOptions) 159 | if CurrentRun.Hero.UpgradeChoicesSinceMenuOpened then 160 | CurrentRun.Hero.UpgradeChoicesSinceMenuOpened = config.ChooseMultipleUpgradesFunction( CurrentRun.Hero.UpgradeChoicesSinceMenuOpened ) 161 | else 162 | CurrentRun.Hero.UpgradeChoicesSinceMenuOpened = 1 163 | end 164 | return base( lootData ) 165 | end, MagicEdits) 166 | end 167 | 168 | if config.ExtraRarity then 169 | 170 | ModUtil.Path.Wrap( "SetTraitsOnLoot", function( base, lootData ) 171 | if lootData.RarityChances.Legendary and config.ExtraRarity.Legendary < 1 then 172 | lootData.RarityChances.Legendary = lootData.RarityChances.Legendary*(1-config.ExtraRarity.Legendary) + config.ExtraRarity.Legendary 173 | else 174 | lootData.RarityChances.Legendary = config.ExtraRarity.Legendary 175 | end 176 | if lootData.RarityChances.Heroic and config.ExtraRarity.Heroic < 1 then 177 | lootData.RarityChances.Heroic = lootData.RarityChances.Heroic*(1-config.ExtraRarity.Heroic) + config.ExtraRarity.Heroic 178 | else 179 | lootData.RarityChances.Heroic = config.ExtraRarity.Heroic 180 | end 181 | if lootData.RarityChances.Epic and config.ExtraRarity.Epic < 1 then 182 | lootData.RarityChances.Epic = lootData.RarityChances.Epic*(1-config.ExtraRarity.Epic) + config.ExtraRarity.Epic 183 | else 184 | lootData.RarityChances.Epic = config.ExtraRarity.Epic 185 | end 186 | if lootData.RarityChances.Rare and config.ExtraRarity.Rare < 1 then 187 | lootData.RarityChances.Rare = lootData.RarityChances.Rare*(1-config.ExtraRarity.Rare) + config.ExtraRarity.Rare 188 | else 189 | lootData.RarityChances.Rare = config.ExtraRarity.Rare 190 | end 191 | if lootData.RarityChances.Common and config.ExtraRarity.Common < 1 then 192 | lootData.RarityChances.Common = lootData.RarityChances.Common*(1-config.ExtraRarity.Common) + config.ExtraRarity.Common 193 | else 194 | lootData.RarityChances.Common = config.ExtraRarity.Common 195 | end 196 | base( lootData ) 197 | end, MagicEdits) 198 | end 199 | 200 | if config.ExtraMoney then 201 | ModUtil.Path.Wrap( "AddMoney", function( base, amount, source ) 202 | if amount then 203 | return base( config.ExtraMoney*amount, source ) 204 | end 205 | return base( amount, source ) 206 | end, MagicEdits) 207 | end 208 | 209 | if config.AlwaysSeeChamberCount then 210 | ModUtil.Path.Wrap( "ShowHealthUI", function( base ) 211 | ShowDepthCounter() 212 | base() 213 | end, MagicEdits) 214 | end 215 | 216 | if config.AlwaysUseSpecialDoors then 217 | ModUtil.Path.Wrap( "CheckSpecialDoorRequirement", function( base, door ) 218 | base( door ) 219 | return nil 220 | end, MagicEdits) 221 | end 222 | 223 | if config.PlayerDamageMult then 224 | ModUtil.Path.Wrap( "Damage", function( base, victim, triggerArgs ) 225 | if triggerArgs.DamageAmount and victim == CurrentRun.Hero then 226 | triggerArgs.DamageAmount = triggerArgs.DamageAmount * config.PlayerDamageMult 227 | end 228 | base( victim, triggerArgs ) 229 | end, MagicEdits) 230 | end 231 | if config.EnemyDamageMult then 232 | ModUtil.Path.Wrap( "DamageEnemy", function( base, victim, triggerArgs ) 233 | if triggerArgs.DamageAmount then 234 | triggerArgs.DamageAmount = triggerArgs.DamageAmount * config.EnemyDamageMult 235 | end 236 | base( victim, triggerArgs ) 237 | end, MagicEdits) 238 | end 239 | 240 | if config.CanAlwaysExitRoom then 241 | ModUtil.Path.Wrap( "CheckRoomExitsReady", function( base, currentRoom ) 242 | base( currentRoom ) 243 | return true 244 | end, MagicEdits) 245 | 246 | ModUtil.Path.Wrap( "AttemptUseDoor", function( base, door ) 247 | if not door.Room then 248 | DoUnlockRoomExits( CurrentRun, CurrentRoom ) 249 | else 250 | door.ReadyToUse = true 251 | base( door ) 252 | end 253 | end, MagicEdits) 254 | end 255 | 256 | if config.UnlimitedGodsPerRun then 257 | ModUtil.BaseOverride("ReachedMaxGods",function( excludedGods ) 258 | return false 259 | end, MagicEdits) 260 | end 261 | -------------------------------------------------------------------------------- /Examples/Hades/ClimbOfSisyphus/ClimbOfSisyphus.lua: -------------------------------------------------------------------------------- 1 | ModUtil.Mod.Register("ClimbOfSisyphus") 2 | 3 | local config = { 4 | TestMode = false, 5 | BaseFalls = 0, 6 | BaseGods = 4, 7 | MaxGodRate = 1, 8 | PlayerDamageRate = 0.05, 9 | PlayerDamageLimit = 10, 10 | PlayerDamageBase = 1, 11 | EnemyDamageRate = 0.2, 12 | EnemyDamageLimit = 0.02, 13 | EnemyDamageBase = 1, 14 | RarityRate = 0.1, 15 | ExchangeRate = 0.15, 16 | EncounterModificationEnabled = true, 17 | EncounterDifficultyRate = 2.35, 18 | EncounterMinWaveRate = 0.65, 19 | EncounterMaxWaveRate = 1.25, 20 | EncounterEnemyCapRate = 0.65, 21 | EncounterTypesRate = 0.20 22 | } 23 | ClimbOfSisyphus.Config = config 24 | 25 | local function falloff( x ) 26 | return x / math.sqrt( 3 + x * x ) 27 | end 28 | 29 | local function sfalloff( x, r, f, t ) 30 | return f - ( f - t ) * falloff( x * r ) 31 | end 32 | 33 | local function lerp( x, y, t, d ) 34 | if x and y then 35 | return x * ( 1 - t ) + t * y 36 | end 37 | return d 38 | end 39 | 40 | local function maxInterpolate( x, t ) 41 | if x and t < 1 then 42 | return x * ( 1 - t ) + t 43 | end 44 | return t 45 | end 46 | 47 | OnAnyLoad{ function( ) 48 | if not CurrentRun then return end 49 | if not CurrentRun.TotalFalls then 50 | CurrentRun.TotalFalls = config.BaseFalls 51 | CurrentRun.MetaDepth = GetBiomeDepth( CurrentRun ) 52 | end 53 | end } 54 | 55 | function ClimbOfSisyphus.SkipToEnd( skipFight ) 56 | if skipFight then 57 | ForceNextEncounter = "Empty" 58 | end 59 | return LeaveRoomWithNoDoor( nil, { NextMap = "D_Boss01" } ) 60 | end 61 | 62 | function ClimbOfSisyphus.ShowLevelIndicator( ) 63 | if ClimbOfSisyphus.LevelIndicator then 64 | Destroy{ Ids = ClimbOfSisyphus.LevelIndicator.Id } 65 | end 66 | CurrentRun.TotalFalls = CurrentRun.TotalFalls or config.BaseFalls 67 | if CurrentRun.TotalFalls > 0 then 68 | ClimbOfSisyphus.LevelIndicator = CreateScreenComponent{ Name = "BlankObstacle", Group = "LevelIndicator", X = 2*ScreenCenterX-55, Y = 110 } 69 | CreateTextBox{ Id = ClimbOfSisyphus.LevelIndicator.Id, Text = tostring( CurrentRun.TotalFalls ), OffsetX = -40, FontSize = 22, Color = color, Font = "AlegreyaSansSCExtraBold" } 70 | SetAnimation{ Name = "EasyModeIcon", DestinationId = ClimbOfSisyphus.LevelIndicator.Id, Scale = 1 } 71 | end 72 | end 73 | 74 | function ClimbOfSisyphus.EndFallFunc( currentRun, exitDoor ) 75 | AddInputBlock{ Name = "LeaveRoomPresentation" } 76 | ToggleControl{ Names = { "AdvancedTooltip" }, Enabled = false } 77 | 78 | HideCombatUI( ) 79 | LeaveRoomAudio( currentRun, exitDoor ) 80 | wait( 0.1 ) 81 | 82 | AllowShout = false 83 | 84 | RemoveInputBlock{ Name = "LeaveRoomPresentation" } 85 | ToggleControl{ Names = { "AdvancedTooltip" }, Enabled = true } 86 | StartNewRunPresentation( currentRun ) 87 | end 88 | 89 | function ClimbOfSisyphus.RunFall( currentRun, door ) 90 | 91 | currentRun.TotalFalls = currentRun.TotalFalls + 1 92 | currentRun.MetaDepth = GetBiomeDepth( currentRun ) 93 | currentRun.NumRerolls = currentRun.NumRerolls + GetNumMetaUpgrades( "RerollMetaUpgrade" ) + GetNumMetaUpgrades( "RerollPanelMetaUpgrade" ) 94 | 95 | currentRun.BiomeRoomCountCache = { } 96 | currentRun.RoomCountCache = { } 97 | currentRun.RoomHistory = { } 98 | currentRun.EncountersCompletedCache = { } 99 | currentRun.EncountersOccuredCache = { } 100 | currentRun.EncountersOccuredBiomedCache = { } 101 | currentRun.EncountersDepthCache = { } 102 | 103 | currentRun.DamageRecord = {} 104 | currentRun.HealthRecord = {} 105 | currentRun.ConsumableRecord = {} 106 | currentRun.ActualHealthRecord = {} 107 | currentRun.BlockTimerFlags = {} 108 | currentRun.WeaponsFiredRecord = {} 109 | 110 | currentRun.RoomCreations = { } 111 | --currentRun.LootTypeHistory = {} 112 | currentRun.NPCInteractions = {} 113 | currentRun.AnimationState = {} 114 | currentRun.EventState = {} 115 | currentRun.ActivationRecord = {} 116 | currentRun.SpeechRecord = {} 117 | currentRun.TextLinesRecord = {} 118 | currentRun.TriggerRecord = {} 119 | currentRun.UseRecord = {} 120 | currentRun.GiftRecord = {} 121 | currentRun.HintRecord = {} 122 | currentRun.EnemyUpgrades = {} 123 | currentRun.BlockedEncounters = {} 124 | currentRun.InvulnerableFlags = {} 125 | currentRun.PhasingFlags = {} 126 | currentRun.MoneySpent = 0 127 | currentRun.MoneyRecord = {} 128 | currentRun.ActiveObjectives = {} 129 | --currentRun.RunDepthCache = 1 130 | --currentRun.GameplayTime = 0 131 | currentRun.BiomeTime = 0 132 | currentRun.ThanatosSpawns = 0 133 | currentRun.SupportAINames = {} 134 | currentRun.ClosedDoors = {} 135 | currentRun.CompletedStyxWings = 0 136 | 137 | UpdateRunHistoryCache( currentRun ) 138 | 139 | door.Room = CreateRoom( RoomData["RoomOpening"] ) 140 | door.ExitFunctionName = ClimbOfSisyphus.EndFallFunc 141 | door.Room.EntranceDirection = nil 142 | currentRun.CurrentRoom.ExitFunctionName = nil 143 | currentRun.CurrentRoom.ExitDirection = nil 144 | currentRun.CurrentRoom.SkipLoadNextMap = false 145 | end 146 | 147 | ModUtil.Path.Wrap( "IsEncounterEligible", function( base, currentRun, room, encounterData ) 148 | if room.ForcedReward == "Story" and encounterData.EncounterType == "NonCombat" then return true end 149 | return base( currentRun, room, encounterData ) 150 | end, ClimbOfSisyphus ) 151 | 152 | ModUtil.Path.Wrap( "RunShopGeneration", function( base, currentRoom, ... ) 153 | if currentRoom.Name == "RoomOpening" or currentRoom.Name == "D_Boss01" then 154 | currentRoom.Flipped = false 155 | end 156 | return base( currentRoom, ... ) 157 | end, ClimbOfSisyphus ) 158 | 159 | ModUtil.Path.Wrap( "LeaveRoom", function( base, currentRun, door ) 160 | if currentRun.CurrentRoom.EntranceFunctionName == "RoomEntranceHades" then 161 | local screen = ModUtil.Hades.NewMenuYesNo( 162 | "ClimbOfSisyphusExitMenu", 163 | function( ) 164 | base( currentRun, door ) 165 | end, 166 | function( ) end, 167 | function( ) 168 | ClimbOfSisyphus.RunFall( currentRun, door ) 169 | end, 170 | function( ) end, 171 | "Endless Calling", 172 | "Go back to Tartarus to climb once more?", 173 | " Fall ", 174 | " Escape ", 175 | "EasyModeIcon", 2.25 176 | ) 177 | else 178 | base( currentRun, door ) 179 | end 180 | end, ClimbOfSisyphus ) 181 | 182 | ModUtil.Path.Override( "ReachedMaxGods", function( excludedGods ) 183 | if not CurrentRun then return end 184 | if not CurrentRun.TotalFalls then 185 | CurrentRun.TotalFalls = config.BaseFalls 186 | CurrentRun.MetaDepth = GetBiomeDepth( CurrentRun ) 187 | end 188 | 189 | excludedGods = excludedGods or { } 190 | local maxLootTypes = config.BaseGods + config.MaxGodRate * CurrentRun.TotalFalls 191 | local gods = ShallowCopyTable( excludedGods ) 192 | for i, godName in pairs( GetInteractedGodsThisRun( ) ) do 193 | if not Contains( gods, godName ) then 194 | table.insert( gods, godName ) 195 | end 196 | end 197 | return TableLength( gods ) >= maxLootTypes 198 | end, ClimbOfSisyphus ) 199 | 200 | ModUtil.Path.Wrap( "Damage", function( base, victim, triggerArgs ) 201 | if victim == CurrentRun.Hero then 202 | victim.CannotDieFromDamage = config.TestMode 203 | triggerArgs.DamageAmount = triggerArgs.DamageAmount * sfalloff( CurrentRun.TotalFalls, config.PlayerDamageRate, config.PlayerDamageBase, config.PlayerDamageLimit ) 204 | end 205 | return base( victim, triggerArgs ) 206 | end, ClimbOfSisyphus ) 207 | 208 | ModUtil.Path.Wrap( "DamageEnemy", function( base, victim, triggerArgs ) 209 | if config.TestMode then 210 | victim.Health = 0 211 | end 212 | triggerArgs.DamageAmount = triggerArgs.DamageAmount * sfalloff( CurrentRun.TotalFalls, config.EnemyDamageRate, config.PlayerDamageBase, config.EnemyDamageLimit ) 213 | return base( victim, triggerArgs ) 214 | end, ClimbOfSisyphus ) 215 | 216 | ModUtil.Path.Wrap( "GetBiomeDepth", function( base, currentRun, ... ) 217 | if currentRun.MetaDepth then 218 | return currentRun.MetaDepth + base( currentRun, ...) 219 | end 220 | return base( currentRun ) 221 | end, ClimbOfSisyphus ) 222 | 223 | ModUtil.Path.Wrap( "ShowHealthUI", function( base ) 224 | if not CurrentRun.EndingMoney then 225 | ClimbOfSisyphus.ShowLevelIndicator( ) 226 | end 227 | return base( ) 228 | end, ClimbOfSisyphus ) 229 | 230 | if config.EncounterModificationEnabled then 231 | ModUtil.Path.Wrap( "GenerateEncounter", function( base, currentRun, room, encounter ) 232 | if encounter.EncounterType ~= "Spawned" then 233 | if not CurrentRun.TotalFalls then 234 | CurrentRun.TotalFalls = config.BaseFalls 235 | CurrentRun.MetaDepth = GetBiomeDepth( CurrentRun ) 236 | end 237 | 238 | encounter.DifficultyModifier = ( encounter.DifficultyModifier or 0 ) + config.EncounterDifficultyRate * CurrentRun.TotalFalls 239 | if encounter.ActiveEnemyCapDepthRamp then 240 | encounter.ActiveEnemyCapDepthRamp = encounter.ActiveEnemyCapDepthRamp + config.EncounterDifficultyRate * CurrentRun.TotalFalls 241 | end 242 | if encounter.ActiveEnemyCapBase then 243 | encounter.ActiveEnemyCapBase = encounter.ActiveEnemyCapBase + config.EncounterEnemyCapRate * CurrentRun.TotalFalls 244 | end 245 | if encounter.ActiveEnemyCapMax then 246 | encounter.ActiveEnemyCapMax = encounter.ActiveEnemyCapMax + config.EncounterEnemyCapRate * CurrentRun.TotalFalls 247 | end 248 | 249 | local waveCap = #WaveDifficultyPatterns 250 | encounter.MinWaves = lerp( ( encounter.MinWaves or 1 ), waveCap, falloff( config.EncounterMinWaveRate * CurrentRun.TotalFalls ) ) 251 | encounter.MaxWaves = lerp( ( encounter.MaxWaves or 1 ), waveCap, falloff( config.EncounterMaxWaveRate * CurrentRun.TotalFalls ) ) 252 | if encounter.MinWaves > encounter.MaxWaves then encounter.MinWaves = encounter.MaxWaves end 253 | 254 | if encounter.MaxTypesCap then 255 | encounter.MaxTypes = lerp( ( encounter.MaxTypes or 1 ), encounter.MaxTypesCap, falloff( config.EncounterTypesRate * CurrentRun.TotalFalls ) ) 256 | else 257 | encounter.MaxTypes = ( encounter.MaxTypes or 1 ) + config.EncounterTypesRate * CurrentRun.TotalFalls 258 | end 259 | if encounter.MaxEliteTypes then 260 | encounter.MaxEliteTypes = encounter.MaxEliteTypes + config.EncounterTypesRate * CurrentRun.TotalFalls 261 | end 262 | end 263 | 264 | return base( currentRun, room, encounter ) 265 | end, ClimbOfSisyphus ) 266 | end 267 | 268 | ModUtil.Path.Wrap( "SetTraitsOnLoot", function( base, lootData, args ) 269 | local extraRarity = falloff( config.RarityRate * CurrentRun.TotalFalls ) 270 | local extraReplace = falloff( config.ExchangeRate * CurrentRun.TotalFalls ) 271 | local oldRarityChances = lootData.RarityChances 272 | local oldReplaceChance = CurrentRun.Hero.BoonData.ReplaceChance 273 | lootData.RarityChances = ShallowCopyTable( oldRarityChances ) 274 | lootData.RarityChances.Legendary = maxInterpolate( lootData.RarityChances.Legendary, extraRarity ) 275 | lootData.RarityChances.Heroic = maxInterpolate( lootData.RarityChances.Heroic, extraRarity ) 276 | lootData.RarityChances.Epic = maxInterpolate( lootData.RarityChances.Epic, extraRarity ) 277 | lootData.RarityChances.Rare = maxInterpolate( lootData.RarityChances.Rare, extraRarity ) 278 | lootData.RarityChances.Common = maxInterpolate( lootData.RarityChances.Common, extraRarity ) 279 | CurrentRun.Hero.BoonData.ReplaceChance = maxInterpolate( oldReplaceChance, extraReplace ) 280 | base( lootData, args ) 281 | lootData.RarityChances = oldRarityChances 282 | CurrentRun.Hero.BoonData.ReplaceChance = oldReplaceChance 283 | end, ClimbOfSisyphus ) --------------------------------------------------------------------------------