Skip to content

Instantly share code, notes, and snippets.

@cwonrails
Created May 29, 2021 15:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cwonrails/f6fe2ae61c793373c902b324a77d7e63 to your computer and use it in GitHub Desktop.
Save cwonrails/f6fe2ae61c793373c902b324a77d7e63 to your computer and use it in GitHub Desktop.
Import "../Mods/ModUtil/ModUtil.lua"
Import "Color.lua"
Import "UIPresentation.lua"
Import "UIScripts.lua"
Import "CreditsScripts.lua"
Import "EventPresentation.lua"
Import "AudioScripts.lua"
Import "Narrative.lua"
Import "CodexData.lua"
Import "GiftData.lua"
Import "SellTraitData.lua"
Import "StoreData.lua"
Import "WeaponUpgradeData.lua"
Import "BoonInfoScreenData.lua"
Import "GhostData.lua"
Import "ObstacleData.lua"
Import "CodexScripts.lua"
Import "UtilityScripts.lua"
Import "DebugScripts.lua"
Import "PlayerAIData.lua"
Import "Localization.lua"
Import "Art.lua"
Import "EnemyAI.lua"
Import "UpgradeManager.lua"
Import "FishingScripts.lua"
Import "FishingData.lua"
Import "AwardMenuScripts.lua"
Import "KeepsakeScripts.lua"
Import "StoreScripts.lua"
Import "SellTraitScripts.lua"
Import "MarketScreen.lua"
Import "GhostAdminScreen.lua"
Import "MusicPlayerScreen.lua"
Import "QuestLogScreen.lua"
Import "AchievementLogic.lua"
Import "BadgePresentation.lua"
Import "BadgeLogic.lua"
Import "RunClearScreen.lua"
Import "RunHistoryScreen.lua"
Import "GameStatsScreen.lua"
Import "RoomPresentation.lua"
Import "RunData.lua"
Import "RunManager.lua"
Import "Combat.lua"
Import "CombatPresentation.lua"
Import "GhostScripts.lua"
Import "TraitTrayScripts.lua"
Import "WeaponUpgradeScripts.lua"
Import "BoonInfoScreenScripts.lua"
-- Spawned Objects
Using "BlackScreen"
Using "BaseLoot"
Using "BaseBoon"
Using "BloodFrame"
Using "ChallengeSwitchBase"
Using "ChallengeSwitchLocked"
Using "ChallengeSwitchOpen"
Using "SecretDoor_Closed"
Using "SecretDoor_Revealed"
Using "ShrinePointDoor_Closed"
Using "ShrinePointDoor_Revealed"
Using "InteractBacking"
Using "ConsolationPrize"
Using "ShadeShieldBunker"
Using "EnemySwordIdle"
Using "EnemySwordPickup"
Using "EnemySpearIdle"
Using "EnemySpearPickup"
Using "EnemyBowIdle"
Using "EnemyBowPickup"
Using "EnemyShieldIdle"
Using "EnemyShieldPickup"
Using "StyxGibletsRat01a"
Using "StyxGibletsRat01b"
Using "Critter_MouseBounce"
Using "Critter_MouseScurry"
-- Hot-swapped Art
Using "BreakableIdle1"
Using "BreakableIdle2"
Using "BreakableIdle3"
Using "BreakableHighValueIdle"
Using "BreakableHighValueIdleSuper"
Using "TartarusWallPillar01"
Using "TartarusWallMural01"
Using "HouseStatueSkelly01"
Using "HouseStatueSkelly02"
Using "HouseStatueSkelly03"
Using "HouseStatueSkelly04"
-- Non-Binked Traps
Using "SpikeTrapIdle"
Using "SpikeTrapPressed"
Using "SpikeTrapPreFire"
Using "SpikeTrapDeactivated"
Using "DartTrapPressed"
Using "DartTrapPreFire"
Using "DartTrapDeactivated"
Using "ReflectionBlob"
Using "TrapFissureDisabled"
Using "GasTrapIdle"
Using "GasTrapActivated"
Using "ReflectiveMirrorIdle"
Using "ReflectiveMirrorTriggered"
Using "SawTrapProjectile"
Using "SawTrapIdle"
Using "SawTrapDeactivated"
Using "GasTrapPoisonIdle"
Using "GasTrapPoisonActivated"
Using "HadesTombstone"
-- Non-Binked Enemies
Using "Swarmer"
Using "SwarmerHelmeted"
Using "LightSpawner"
Using "HeavyRanged"
Using "HeavyRangedForked"
Using "HeavyRangedSplitterMiniboss"
Using "ShieldRanged"
Using "EnemyWretchThiefIdle"
Using "Projectile_BloodlessGrenadier"
Using "Projectile_BloodlessGrenadierActivate"
Using "Projectile_BloodlessGrenadierToss"
Using "CrusherUnit"
Using "CrusherUnitOnGround"
Using "BaseMedusaHead"
Using "EnemyMedusaHeadFire"
Using "EnemyMedusaHeadFireHitAndRun"
Using "ShadeNaked"
Using "ShadeNakedDeathVFX"
Using "Crawler"
Using "RatThug" -- Binked but need its fx textures, especially the scales since it shares many with Crawler
Using "FlurrySpawner"
Using "SoulSpawnerButterfly"
Using "SoulSpawnerButterflyDeath"
RoomThreadName = "RoomThread"
OnPreThingCreation
{
function( triggerArgs )
--ValidateThreadLeaks()
UpdateConfigOptionCache()
RandomInit()
DoPatches()
SetupMap()
AudioInit()
NarrativeInit()
CodexInit()
LifeOnKillRecord = {}
DamageRecord = {}
HealthRecord = {}
SpawnRecord = {}
OfferedExitDoors = {}
local mapName = GetMapName({ })
if RoomData[mapName] == nil then
return
end
if GameState == nil then
StartNewGame()
end
ResetUI()
if RandomChance(0.5) then
local thingsToSwap = GetMapThings({ Name = "RandomSwaps" })
for swapId, currentName in pairs( thingsToSwap ) do
if ObstacleData[currentName] ~= nil and ObstacleData[currentName].RandomSwaps ~= nil then
local randomSwapName = GetRandomValue( ObstacleData[currentName].RandomSwaps )
SwapName({ DestinationId = swapId, Name = randomSwapName })
end
end
end
if CurrentRun.CurrentRoom ~= nil then
RunUnthreadedEvents( CurrentRun.CurrentRoom.PreThingCreationUnthreadedEvents, CurrentRun.CurrentRoom )
if GetConfigOptionValue({ Name = "ResetRoomData" }) then
CurrentRun.CurrentRoom.Flipped = false
for key, value in pairs( RoomData[CurrentRun.CurrentRoom.Name] ) do
CurrentRun.CurrentRoom[key] = value
end
if CurrentRun.CurrentRoom.Encounter ~= nil then
local encounterData = EncounterData[CurrentRun.CurrentRoom.Encounter.Name]
if encounterData ~= nil then
for key, value in pairs( encounterData ) do
CurrentRun.CurrentRoom.Encounter[key] = value
end
if CurrentRun.CurrentRoom.Encounter.Generated then
GenerateEncounter( CurrentRun, CurrentRun.CurrentRoom, CurrentRun.CurrentRoom.Encounter )
end
else
CurrentRun.CurrentRoom.Encounter = ChooseEncounter( CurrentRun, CurrentRun.CurrentRoom )
end
end
end
if GetConfigOptionValue({ Name = "ForceFlipMapThings" }) then
CurrentRun.CurrentRoom.Flipped = true
end
end
if CurrentRun ~= nil and CurrentRun.CurrentRoom.Flipped ~= nil and CurrentRun.CurrentRoom.Name == GetMapName({ }) then
SetConfigOption({ Name = "FlipMapThings", Value = CurrentRun.CurrentRoom.Flipped })
end
if GetConfigOptionValue({ Name = "DumpRunStatsOnLoad" }) then
DumpGameStateToFile()
end
end
}
OnAnyLoad
{
function( triggerArgs )
CheckQuestStatus({ Silent = true })
thread( CheckProgressAchievements )
RemoveInputBlock({ Name = "MapLoad" })
local mapName = triggerArgs.name
if RoomData[mapName] ~= nil then
if CurrentRun.CurrentRoom.ExitsUnlocked then
if CurrentRun.CurrentRoom.Encounter ~= nil then
-- Hack to correct bad save state
CurrentRun.CurrentRoom.Encounter.Completed = true
end
RestoreUnlockRoomExits( CurrentRun, CurrentRun.CurrentRoom )
else
StartRoom( CurrentRun, CurrentRun.CurrentRoom )
end
end
end
}
Import "DeathLoop.lua"
function ValidateIdLeaks( trace, tableToCheck )
if tableToCheck.ObjectId ~= nil or tableToCheck.Id ~= nil then
DebugAssert({ Condition = false, Text = "Id leak found in "..trace })
end
local idLeakIgnores = ToLookup({ "InspectPoints", "RunHistory", "RoomHistory", "Hero", "PostLineFunctionArgs", "AmbientMusicSource", "ChallengeSwitch", })
for key, value in pairs( tableToCheck ) do
if not SaveIgnores[key] and not idLeakIgnores[key] and type(value) == "table" then
local recursiveTrace = trace.."."..key
ValidateIdLeaks( recursiveTrace, value )
end
end
end
function ValidateLoops( trace, tableToCheck )
if string.len( trace ) > 150 then
DebugAssert({ Condition = false, Text = "Loop found in "..trace })
end
local extraIgnores = ToLookup({ })
for key, value in pairs( tableToCheck ) do
if not SaveIgnores[key] and not extraIgnores[key] and type(value) == "table" then
local recursiveTrace = trace.."."..key
ValidateLoops( recursiveTrace, value )
end
end
end
function ValidateThreadLeaks()
local threadIgnores = ToLookup({ "MapLoad", "CombatUIHide" })
for k, thread in pairs( _threads ) do
if not threadIgnores[thread.tag] then
DebugAssert({ false, Text = "thread still active: "..tostring(thread.tag) })
end
end
end
function DoPatches()
TextRecord = nil
-- Cleanup leaked globals
hero = nil
itemData = nil
roomForDoorData = nil
enemyName = nil
tooltipData = nil
upgradeData = nil
healValue = nil
hasIdentical = nil
curseData = nil
alternateLootId = nil
rumbleParams = nil
notifyName = nil
enemyVulnerability = nil
collisionVulnerability = nil
blessingData = nil
giftOffsetZ = nil
upgradeTitlePrefix = nil
upgradeTitleSuffix = nil
weaponSetNames = nil
weaponData = nil
SkellyWeaponEquipReactionVoiceLines = nil
FurthestRunProgress = nil
attributeRequirements = nil
ViewedTooltips = nil
CombatSlow = nil
LastStandVignette = nil
HeroWeapons = nil
EnemiesBiome3Champions = nil
RunRecordData = nil
DamageTextRecord = nil
DialogueBackground = nil
LeapPointsOccupied = nil
HadesBloodstoneVignette = nil
PersistentShoutEffectSoundId = nil
DevotionSoundId = nil
TheseusWrathActivationVoiceLines = nil
GiftPointRecord = nil
LockKeyRecord = nil
bothBossesDead = nil
ammoVulnerability = nil
for key, value in pairs( GameData ) do
_G[key] = nil
end
for key, value in pairs( ConstantsData ) do
_G[key] = nil
end
for setName, set in pairs( EnemySets ) do
_G[setName] = nil
end
for setName, set in pairs( WeaponSets ) do
_G[setName] = nil
end
for setName, set in pairs( EncounterSets ) do
_G[setName] = nil
end
ShowingCodexUpdateAnimation = false
if ActiveScreens then
if ActiveScreens.TraitTrayScreen then
ActiveScreens.TraitTrayScreen = nil
end
if ActiveScreens.Codex then
ActiveScreens.Codex = nil
end
end
if GameState ~= nil then
GameState.Flags = GameState.Flags or {}
GameState.Flags.Overlook = false
GameState.ReturnedRandomEligibleSourceNames = GameState.ReturnedRandomEligibleSourceNames or {}
GameState.PlayedRandomRunIntroData = GameState.PlayedRandomRunIntroData or {}
GameState.PlayedRandomRunOutroData = GameState.PlayedRandomRunOutroData or {}
GameState.HeardGhostLines = GameState.HeardGhostLines or {}
GameState.LastUnlockedMetaUpgrades = GameState.LastUnlockedMetaUpgrades or {}
GameState.MetaUpgradesUnlocked = GameState.MetaUpgradesUnlocked or {}
GameState.MetaUpgradesSelected = GameState.MetaUpgradesSelected or {}
GameState.MetaUpgradeState = GameState.MetaUpgradeState or {}
GameState.KeepsakeChambers = GameState.KeepsakeChambers or {}
GameState.Resources = GameState.Resources or {}
GameState.LifetimeResourcesGained = GameState.LifetimeResourcesGained or {}
GameState.LifetimeResourcesSpent = GameState.LifetimeResourcesSpent or {}
GameState.EnemyEliteAttributeKills = GameState.EnemyEliteAttributeKills or {}
if GameState.LockKeys ~= nil then
GameState.Resources.LockKeys = GameState.LockKeys
GameState.LockKeys = nil
end
if GameState.LockKeysGained ~= nil then
GameState.LifetimeResourcesGained.LockKeys = GameState.LockKeysGained
GameState.LockKeysGained = nil
end
if GameState.GiftPoints ~= nil then
GameState.Resources.GiftPoints = GameState.GiftPoints
GameState.GiftPoints = nil
end
if GameState.MetaPoints ~= nil then
GameState.Resources.MetaPoints = GameState.MetaPoints
GameState.MetaPoints = nil
end
if GameState.ShrinePoints ~= nil then
GameState.Resources.MetaPoints = (GameState.Resources.MetaPoints or 0) + (GameState.ShrinePoints * 5)
GameState.ShrinePoints = nil
end
if (Revision == nil or Revision < 25100 ) and GameState.Flags.HardMode then
GameState.MetaUpgrades.MetaPointCapShrineUpgrade = 0
if GameState.MetaUpgrades.HealingReductionShrineUpgrade == nil or GameState.MetaUpgrades.HealingReductionShrineUpgrade == 0 then
GameState.MetaUpgrades.HealingReductionShrineUpgrade = 1
end
end
GameState.ShrinePointClearsComplete = GameState.ShrinePointClearsComplete or {}
GameState.ActiveMutators = GameState.ActiveMutators or {}
GameState.Onslaughts = GameState.Onslaughts or {}
GameState.WeaponUnlocks = GameState.WeaponUnlocks or {}
GameState.SeenWeaponUnlocks = GameState.SeenWeaponUnlocks or {}
GameState.AssistUnlocks = GameState.AssistUnlocks or {}
GameState.LastWeaponUpgradeData = GameState.LastWeaponUpgradeData or {}
GameState.TimesClearedWeapon = GameState.TimesClearedWeapon or {}
GameState.WeaponRecordsClearTime = GameState.WeaponRecordsClearTime or {}
GameState.WeaponRecordsShrinePoints = GameState.WeaponRecordsShrinePoints or {}
GameState.RecordClearedShrineThreshold = GameState.RecordClearedShrineThreshold or {}
GameState.TraitsTaken = GameState.TraitsTaken or {}
GameState.ClearedWithMetaUpgrades = GameState.ClearedWithMetaUpgrades or {}
GameState.QuestsViewed = GameState.QuestsViewed or {}
GameState.QuestStatus = GameState.QuestStatus or {}
GameState.SpeechRecordContexts = GameState.SpeechRecordContexts or {}
GameState.EasyModeLevel = GameState.EasyModeLevel or 0
if GameState.SuspendedRun == nil then
if not GameState.MetaUpgradeStagesUnlocked then
local highestUnlockIndex = 0
for metaUpgradeName in pairs( GameState.MetaUpgradesUnlocked ) do
for i, rowData in pairs(MetaUpgradeOrder) do
if Contains( rowData, metaUpgradeName ) and highestUnlockIndex < i then
highestUnlockIndex = i
end
end
end
local expectedUnlockStage = math.ceil(( highestUnlockIndex - MetaUpgradeLockOrder.BaseUnlocked ) / MetaUpgradeLockOrder.LockedSetsCount)
GameState.MetaUpgradeStagesUnlocked = expectedUnlockStage
end
for metaUpgradeName, metaUpgradeData in pairs( MetaUpgradeData ) do
if metaUpgradeData.Starting then
GameState.MetaUpgradesUnlocked[metaUpgradeName] = true
end
if not GameState.MetaUpgradesUnlocked[metaUpgradeName] or MetaUpgradeData[metaUpgradeName].Skip then
local metaPointRefund = GetMetaUpgradeTotalInvestment( metaUpgradeData )
if metaPointRefund > 0 then
DebugPrint({ Text = metaUpgradeData.Name.." Refunded: "..metaPointRefund })
GameState.Resources.MetaPoints = GameState.Resources.MetaPoints + metaPointRefund
end
GameState.MetaUpgrades[metaUpgradeName] = nil
end
local condemnedKeys = {}
for key, value in pairs(GameState.MetaUpgrades) do
if type(value) == "string" then
IncrementTableValue( GameState.MetaUpgrades, value )
table.insert( condemnedKeys, key )
end
end
for i, key in pairs(condemnedKeys) do
GameState.MetaUpgrades[key] = nil
end
if (GameState.MetaUpgrades[metaUpgradeName] or 0) < 0 then
DebugPrint({ Text = metaUpgradeName.." below 0. Fixing." })
GameState.MetaUpgrades[metaUpgradeName] = 0
end
if GameState.MetaUpgrades[metaUpgradeName] ~= nil and metaUpgradeData.MaxInvestment ~= nil and metaUpgradeData.Cost ~= nil and not Contains( ShrineUpgradeOrder, metaUpgradeName ) then
-- Refund MetaUpgrades that have had their cap lowered
local currentRank = GameState.MetaUpgradeState[metaUpgradeName] or 0
local newCap = metaUpgradeData.MaxInvestment
if currentRank > newCap then
DebugPrint({ Text = metaUpgradeData.Name.." PrevRank = "..currentRank })
DebugPrint({ Text = metaUpgradeData.Name.." NewRank = "..newCap })
local metaPointRefund = (currentRank - newCap) * metaUpgradeData.Cost
DebugPrint({ Text = metaUpgradeData.Name.." Refunded = "..metaPointRefund })
GameState.Resources.MetaPoints = GameState.Resources.MetaPoints + metaPointRefund
GameState.MetaUpgradeState[metaUpgradeName] = newCap
if Contains(GameState.MetaUpgradesSelected, metaUpgradeName ) then
GameState.MetaUpgrades[metaUpgradeName] = newCap
end
end
end
if metaUpgradeData.OldCostTable ~= nil and (Revision == nil or Revision < metaUpgradeData.OldRevision) and not GameState.Flags.InFlashback and CurrentRun ~= nil and CurrentRun.CurrentRoom.Name ~= "RoomOpening" then
local currentRank = GameState.MetaUpgradeState[metaUpgradeName] or 0
for rank, oldValue in ipairs( metaUpgradeData.OldCostTable ) do
if rank <= currentRank then
local newValue = metaUpgradeData.CostTable[rank] or 0
local metaPointRefund = oldValue - newValue
if metaPointRefund > 0 then
DebugPrint({ Text = metaUpgradeData.Name.." Rank = "..rank })
DebugPrint({ Text = metaUpgradeData.Name.." Refunded = "..metaPointRefund })
GameState.Resources.MetaPoints = GameState.Resources.MetaPoints + metaPointRefund
end
if metaUpgradeData.CostTable[rank] and not metaUpgradeData.CostTable[rank + 1] and rank < GameState.MetaUpgradeState[metaUpgradeName] then
GameState.MetaUpgradeState[metaUpgradeName] = rank
if Contains(GameState.MetaUpgradesSelected, metaUpgradeName ) then
GameState.MetaUpgrades[metaUpgradeName] = rank
end
end
end
end
end
--[[
if GameState.MetaUpgrades[metaUpgradeName] ~= nil and metaUpgradeData.CostTable ~= nil then
-- Refund MetaUpgrades that have had their cap lowered
local currentRank = GameState.MetaUpgrades[metaUpgradeName]
local newCap = TableLength( metaUpgradeData.CostTable )
if GameState.MetaUpgrades[metaUpgradeName] > newCap then
DebugPrint({ Text = metaUpgradeData.Name.." CurrentRank: "..currentRank })
for i = currentRank - 1, newCap, -1 do
local metaPointRefund = metaUpgradeData.CostTable[newCap]
if metaPointRefund > 0 then
DebugPrint({ Text = metaUpgradeData.Name.." Refunded: "..metaPointRefund })
if Contains( ShrineUpgradeOrder, metaUpgradeName ) then
-- Do nothing
else
GameState.Resources.MetaPoints = GameState.Resources.MetaPoints + metaPointRefund
end
end
end
GameState.MetaUpgrades[metaUpgradeName] = newCap
DebugPrint({ Text = metaUpgradeData.Name.." NewRank: "..GameState.MetaUpgrades[metaUpgradeName] })
end
end
]]
end
if IsEmpty( GameState.MetaUpgradesSelected ) then
for k, metaUpgradeChoices in pairs( MetaUpgradeOrder ) do
GameState.MetaUpgradesSelected[k] = metaUpgradeChoices[1]
end
else
for k, metaUpgradeName in pairs( GameState.MetaUpgradesSelected ) do
if not MetaUpgradeData[metaUpgradeName] and MetaUpgradeOrder[k] then
GameState.MetaUpgradesSelected[k] = MetaUpgradeOrder[k][1]
end
end
end
if (Revision == nil or Revision <= 31053) and ( Contains( GameState.MetaUpgradesSelected, "DuoRarityBoonDropMetaUpgrade" ) or Contains( GameState.MetaUpgradesSelected, "RunProgressRewardMetaUpgrade" ) ) then
-- Swap out DuoRarityBoonDropMetaUpgrade and in RareBoonDropMetaUpgrade
GameState.MetaUpgradesSelected[10] = "RareBoonDropMetaUpgrade"
GameState.MetaUpgrades.RareBoonDropMetaUpgrade = GameState.MetaUpgradeState.RareBoonDropMetaUpgrade
GameState.MetaUpgrades.DuoRarityBoonDropMetaUpgrade = 0
-- Swap out RunProgressRewardMetaUpgrade and in EpicBoonDropMetaUpgrade
GameState.MetaUpgradesSelected[11] = "EpicBoonDropMetaUpgrade"
GameState.MetaUpgrades.EpicBoonDropMetaUpgrade = GameState.MetaUpgradeState.EpicBoonDropMetaUpgrade
GameState.MetaUpgrades.RunProgressRewardMetaUpgrade = 0
end
if not CurrentRun.MetaUpgradeCache then
BuildMetaupgradeCache()
end
end
GameState.WeaponsTouched = GameState.WeaponsTouched or {}
GameState.WeaponsUnlocked = GameState.WeaponsUnlocked or {}
if IsWeaponUpgradeUnlocked( "SpearWeapon", 4 ) then
GameState.SeenWeaponUnlocks.SpearWeapon4 = true
end
GameState.Cosmetics = GameState.Cosmetics or {}
GameState.CosmeticsViewed = GameState.CosmeticsViewed or {}
GameState.CosmeticsAdded = GameState.CosmeticsAdded or {}
GameState.MusicTracksViewed = GameState.MusicTracksViewed or {}
if not GameState.Cosmetics.Cosmetic_DrapesRed and not GameState.Cosmetics.Cosmetic_DrapesGreen and not GameState.Cosmetics.Cosmetic_DrapesBlue and not GameState.Cosmetics.Cosmetic_DrapesGrey then
AddCosmetic( "Cosmetic_DrapesRed" )
end
if not GameState.Cosmetics.Cosmetic_LaurelsRed and not GameState.Cosmetics.Cosmetic_LaurelsBlue and not GameState.Cosmetics.Cosmetic_LaurelsSkulls then
AddCosmetic( "Cosmetic_LaurelsRed" )
end
if not GameState.Cosmetics.Cosmetic_WallWeaponBident and not GameState.Cosmetics.Cosmetic_WallWeaponSword and not GameState.Cosmetics.Cosmetic_WallWeaponAxe then
AddCosmetic( "Cosmetic_WallWeaponBident" )
end
if not GameState.CosmeticsAdded.Cosmetic_SouthHallTrimBrown then
AddCosmetic( "Cosmetic_SouthHallTrimBrown" )
end
if not GameState.CosmeticsAdded.Cosmetic_HouseCandles01 then
AddCosmetic( "Cosmetic_HouseCandles01" )
end
if not GameState.CosmeticsAdded.Cosmetic_UISkinDefault then
AddCosmetic( "Cosmetic_UISkinDefault" )
end
AddCosmetic( "/Music/MusicPlayer/MainThemeMusicPlayer" )
AddCosmetic( "/Music/MusicPlayer/MusicExploration4MusicPlayer" )
GameState.Cosmetics.Cosmetic_NorthHallPedestalHammer = nil
GameState.Cosmetics.Cosmetic_NorthHallPedestalArtifact = nil
GameState.Cosmetics.Cosmetic_NorthHallPedestalSphere = nil
if GameState.Cosmetics.Cosmetic_NorthHallPaintingTartarus and GameState.Cosmetics.Cosmetic_NorthHallPaintingAsphodel then
-- Mutually exclusive now
GameState.Cosmetics.Cosmetic_NorthHallPaintingTartarus = nil
end
local activeCharacterPaintings = {}
for k, characterPainting in pairs( { "Cosmetic_NorthHallPaintingHades", "Cosmetic_NorthHallPaintingZagreus", "Cosmetic_NorthHallPaintingMysteryWoman" } ) do
if GameState.Cosmetics[characterPainting] then
activeCharacterPaintings[characterPainting] = true
end
end
while TableLength( activeCharacterPaintings ) >= 2 do
local characterPainting = RemoveRandomKey( activeCharacterPaintings )
GameState.Cosmetics[characterPainting] = nil
DebugPrint({ Text = "Disabled mutually exclusive cosmetic: "..characterPainting })
end
local activeMiscPaintings = {}
for k, miscPainting in pairs( { "Cosmetic_NorthHallPaintingMysteryGirl", "Cosmetic_NorthHallPaintingTots", "Cosmetic_NorthHallPaintingTheseus" } ) do
if GameState.Cosmetics[miscPainting] then
activeMiscPaintings[miscPainting] = true
end
end
while TableLength( activeMiscPaintings ) >= 2 do
local miscPainting = RemoveRandomKey( activeMiscPaintings )
GameState.Cosmetics[miscPainting] = nil
DebugPrint({ Text = "Disabled mutually exclusive cosmetic: "..miscPainting })
end
if TextLinesRecord.OrpheusFirstMeeting then
AddCosmetic( "OrpheusUnlockItem" )
end
if TextLinesRecord.OrpheusWithEurydice01 then
GameState.Flags.OrpheusReunionInProgress = false
end
if TextLinesRecord.AchillesAboutHadesBedroom01 ~= nil and TextLinesRecord.Inspect_DeathAreaBedroomHades_Portrait_01 == nil then
thread(RestoreActivateHadesBedroomDoor)
end
for cosmeticName, v in pairs( GameState.Cosmetics ) do
GameState.CosmeticsAdded[cosmeticName] = true
end
if (Revision == nil or Revision < 30100 ) and GameState.CosmeticsAdded.Cosmetic_HousePillarsA then
DebugPrint({ Text = "Refunding Cosmetic_HousePillarsA" })
AddResource( "Gems", 800 )
end
if (Revision == nil or Revision < 32686 ) and GameState.CosmeticsAdded.NyxQuestItem then
DebugPrint({ Text = "Refunding NyxQuestItem" })
AddResource( "MetaPoints", 5746 )
end
GameState.ScreensViewed = GameState.ScreensViewed or {}
GameState.ItemInteractions = GameState.ItemInteractions or {}
GameState.EnemySpawns = GameState.EnemySpawns or {}
GameState.EnemyDamage = GameState.EnemyDamage or {}
if TableLength(GameState.WeaponsUnlocked) > 1 then
GameState.CompletedObjectiveSets["UnlockWeapon"] = true
end
GameState.ObjectivesCompleted = GameState.ObjectivesCompleted or {}
GameState.ObjectivesFailed = GameState.ObjectivesFailed or {}
GameState.LastObjectiveCompletedRun = GameState.LastObjectiveCompletedRun or {}
GameState.LastObjectiveFailedRun = GameState.LastObjectiveFailedRun or {}
if GameState.CaughtFish and not GameState.TotalCaughtFish then
GameState.TotalCaughtFish = ShallowCopyTable( GameState.CaughtFish )
end
GameState.TotalCaughtFish = GameState.TotalCaughtFish or {}
if not IsEmpty( GameState.RunHistory ) then
local prevRun = GameState.RunHistory[#GameState.RunHistory]
if prevRun ~= nil then
UpdateRunHistoryCache( prevRun )
end
end
if GameState.EncountersOccuredCache ~= nil then
GameState.EncountersOccurredCache = GameState.EncountersOccuredCache
GameState.EncountersOccuredCache = nil
end
GameState.EncountersOccurredCache = GameState.EncountersOccurredCache or {}
GameState.EncountersCompletedCache = GameState.EncountersCompletedCache or {}
GameState.RoomCountCache = GameState.RoomCountCache or {}
for cosmeticName, data in pairs( GameState.Cosmetics ) do
if type(data) ~= "string" then
GameState.Cosmetics[cosmeticName] = UIData.Constants.VISIBLE
end
end
GameState.CompletedRunsCache = GetCompletedRuns()
GameState.AccumulatedMetaPointsCache = GameState.LifetimeResourcesGained.MetaPoints or 0
for npcName in pairs( GiftData ) do
if GameState.Gift and not GameState.Gift[npcName] then
GameState.Gift[npcName] = {}
GameState.Gift[npcName].Value = GiftData[npcName].Value
end
end
-- gifting overrides
if GameState.Gift.TrainingMelee ~= nil and GameState.Gift.NPC_Skelly_01 == nil then
GameState.Gift.NPC_Skelly_01 = DeepCopyTable( GameState.Gift.TrainingMelee )
elseif GameState.Gift.TrainingMelee ~= nil and GameState.Gift.NPC_Skelly_01 ~= nil and GameState.Gift.TrainingMelee.Value > GameState.Gift.NPC_Skelly_01.Value then
GameState.Gift.NPC_Skelly_01.Value = GameState.Gift.TrainingMelee.Value
end
if GameState.Gift.NPC_Cerberus_01 ~= nil and GameState.Gift.NPC_Cerberus_01.Value then
local targetGiftValue = GameState.Gift.NPC_Cerberus_01.Value
if GameState.Gift.NPC_Cerberus_01.Value > 0 and not TextLinesRecord.CerberusGift01 then
GameState.Gift.NPC_Cerberus_01.Value = 0
elseif GameState.Gift.NPC_Cerberus_01.Value > 1 and not TextLinesRecord.CerberusGift02 then
GameState.Gift.NPC_Cerberus_01.Value = 1
elseif GameState.Gift.NPC_Cerberus_01.Value > 2 and not TextLinesRecord.CerberusGift03 then
GameState.Gift.NPC_Cerberus_01.Value = 2
elseif GameState.Gift.NPC_Cerberus_01.Value > 3 and not TextLinesRecord.CerberusGift04 then
GameState.Gift.NPC_Cerberus_01.Value = 3
elseif GameState.Gift.NPC_Cerberus_01.Value > 4 and not TextLinesRecord.CerberusGift05 then
GameState.Gift.NPC_Cerberus_01.Value = 4
end
end
if GameState.Gift.ArtemisUpgrade ~= nil and GameState.Gift.ArtemisUpgrade.Value then
if GameState.Gift.ArtemisUpgrade.Value > 4 and not TextLinesRecord.ArtemisGift03 then
GameState.Gift.ArtemisUpgrade.Value = 3
end
end
if GameState.Gift.NPC_Achilles_01 ~= nil and GameState.Gift.NPC_Achilles_01.Value then
local targetGiftValue = GameState.Gift.NPC_Achilles_01.Value
if targetGiftValue >= 4 and not TextLinesRecord.AchillesGift04_A then
GameState.Gift.NPC_Achilles_01.Value = 3
elseif targetGiftValue >= 5 and not TextLinesRecord.AchillesGift05_A and not TextLinesRecord.AchillesGift05_B then
GameState.Gift.NPC_Achilles_01.Value = 4
elseif targetGiftValue >= 6 and not TextLinesRecord.AchillesGift06_A then
GameState.Gift.NPC_Achilles_01.Value = 5
elseif targetGiftValue >= 7 and not TextLinesRecord.AchillesGift07_A then
GameState.Gift.NPC_Achilles_01.Value = 6
end
if targetGiftValue < 4 and TextLinesRecord.AchillesGift04_A then
GameState.Gift.NPC_Achilles_01.Value = 4
end
if targetGiftValue < 5 and ( TextLinesRecord.AchillesGift05_A or TextLinesRecord.AchillesGift05_B ) then
GameState.Gift.NPC_Achilles_01.Value = 5
end
if targetGiftValue < 6 and TextLinesRecord.AchillesGift06_A then
GameState.Gift.NPC_Achilles_01.Value = 6
end
if targetGiftValue < 7 and TextLinesRecord.AchillesGift07_A then
GameState.Gift.NPC_Achilles_01.Value = 7
end
if targetGiftValue < 8 and TextLinesRecord.AchillesGift08_A then
GameState.Gift.NPC_Achilles_01.Value = 8
end
if targetGiftValue < 9 and TextLinesRecord.AchillesGift09_A then
GameState.Gift.NPC_Achilles_01.Value = 9
end
end
if GameState.Gift.NPC_Sisyphus_01 ~= nil and GameState.Gift.NPC_Sisyphus_01.Value > 6 and not TextLinesRecord.SisyphusGift07_A then
GameState.Gift.NPC_Sisyphus_01.Value = 6
end
if GameState.Gift.NPC_Thanatos_01 ~= nil and GameState.Gift.NPC_Thanatos_01.Value then
if GameState.Gift.NPC_Thanatos_01.Value > 5 and not TextLinesRecord.ThanatosGift06 then
GameState.Gift.NPC_Thanatos_01.Value = 5
end
if GameState.Gift.NPC_Thanatos_01.Value > 6 and not TextLinesRecord.ThanatosGift07_A then
GameState.Gift.NPC_Thanatos_01.Value = 6
end
if GameState.Gift.NPC_Thanatos_01.Value > 9 and not TextLinesRecord.ThanatosGift10 then
GameState.Gift.NPC_Thanatos_01.Value = 9
end
end
if GameState.Gift.NPC_Patroclus_01 ~= nil and GameState.Gift.NPC_Patroclus_01.Value > 6 and not TextLinesRecord.PatroclusGift07_A then
GameState.Gift.NPC_Patroclus_01.Value = 6
end
if GameState.Gift.NPC_Dusa_01 ~= nil then
local decrementGiftMeter = false
if GameState.Gift.NPC_Dusa_01.Value == 6 and not TextLinesRecord.DusaGift06 then
decrementGiftMeter = true
elseif GameState.Gift.NPC_Dusa_01.Value == 7 and not TextLinesRecord.DusaGift07 then
decrementGiftMeter = true
elseif GameState.Gift.NPC_Dusa_01.Value == 8 and not TextLinesRecord.DusaGift08 then
decrementGiftMeter = true
elseif GameState.Gift.NPC_Dusa_01.Value == 9 and not TextLinesRecord.DusaGift09 then
decrementGiftMeter = true
elseif GameState.Gift.NPC_Dusa_01.Value == 10 and not TextLinesRecord.DusaGift10 then
decrementGiftMeter = true
end
if decrementGiftMeter then
GameState.Gift.NPC_Dusa_01.Value = GameState.Gift.NPC_Dusa_01.Value - 1
end
end
if GameState.Gift.NPC_Eurydice_01 ~= nil then
if GameState.Gift.NPC_Eurydice_01.Value > 3 and not TextLinesRecord.EurydiceGift04 then
GameState.Gift.NPC_Eurydice_01.Value = 3
elseif GameState.Gift.NPC_Eurydice_01.Value == 4 and not TextLinesRecord.EurydiceGift05 then
GameState.Gift.NPC_Eurydice_01.Value = 4
end
end
if GameState.Gift.HermesUpgrade ~= nil then
if GameState.Gift.HermesUpgrade.Value > 5 and not ( TextLinesRecord.HermesGift06 or TextLinesRecord.HermesGift06B ) then
GameState.Gift.HermesUpgrade.Value = 5
end
if GameState.Gift.HermesUpgrade.Value > 7 and not ( TextLinesRecord.HermesGift08 or TextLinesRecord.HermesGift08B ) then
GameState.Gift.HermesUpgrade.Value = 7
end
end
if GameState.Gift.AphroditeUpgrade ~= nil then
if GameState.Gift.AphroditeUpgrade.Value > 5 and not TextLinesRecord.AphroditeGift06 then
GameState.Gift.AphroditeUpgrade.Value = 5
end
end
end
if CodexStatus ~= nil then
if CodexStatus.OtherDenizens ~= nil then
local patchCodexNames =
{
TrainingMelee = "NPC_Skelly_01",
NPC_MaleGhost_01 = "NPC_Achilles_01",
NPC_FemaleGhost_01 = "NPC_Nyx_01",
NPC_Musician_01 = "NPC_Orpheus_01",
}
for oldName, newName in pairs(patchCodexNames) do
if CodexStatus.OtherDenizens[oldName] and not CodexStatus.OtherDenizens[newName] then
CodexStatus.OtherDenizens[newName] = DeepCopyTable( CodexStatus.OtherDenizens[oldName] )
end
if GameState.Gift[oldName] and not GameState.Gift[newName] then
GameState.Gift[newName] = DeepCopyTable( GameState.Gift[oldName] )
end
end
end
if CodexStatus.ChthonicGods ~= nil then
local patchCodexNames =
{
NPC_BoatMan_01 = "NPC_Charon_01",
NPC_ChildGhost_01 = "NPC_Hypnos_01",
NPC_FemaleGhost_01 = "NPC_Nyx_01",
}
for oldName, newName in pairs(patchCodexNames) do
if CodexStatus.ChthonicGods[oldName] and not CodexStatus.ChthonicGods[newName] then
CodexStatus.ChthonicGods[newName] = DeepCopyTable( CodexStatus.ChthonicGods[oldName] )
end
if GameState.Gift[oldName] and not GameState.Gift[newName] then
GameState.Gift[newName] = DeepCopyTable( GameState.Gift[oldName] )
end
end
end
if Codex[CodexStatus.SelectedChapterName] == nil then
CodexStatus.SelectedChapterName = GetFirstKey(Codex)
end
end
if CurrentRun ~= nil then
UpdateRunHistoryCache( CurrentRun )
CurrentRun.AnimationState = CurrentRun.AnimationState or {}
CurrentRun.EventState = CurrentRun.EventState or {}
CurrentRun.NPCInteractions = CurrentRun.NPCInteractions or {}
CurrentRun.ActivationRecord = CurrentRun.ActivationRecord or {}
CurrentRun.TriggerRecord = CurrentRun.TriggerRecord or {}
CurrentRun.MoneySpent = CurrentRun.MoneySpent or 0
CurrentRun.RoomCreations = CurrentRun.RoomCreations or {}
CurrentRun.InvulnerableFlags = CurrentRun.InvulnerableFlags or {}
CurrentRun.PhasingFlags = CurrentRun.PhasingFlags or {}
CurrentRun.HealthRecord = CurrentRun.HealthRecord or {}
CurrentRun.ActualHealthRecord = CurrentRun.ActualHealthRecord or {}
CurrentRun.DamageRecord = CurrentRun.DamageRecord or {}
CurrentRun.ConsumableRecord = CurrentRun.ConsumableRecord or {}
CurrentRun.WeaponsFiredRecord = CurrentRun.WeaponsFiredRecord or {}
CurrentRun.GameplayTime = CurrentRun.GameplayTime or 99999
CurrentRun.BiomeTime = CurrentRun.BiomeTime or 99999
CurrentRun.BlockTimerFlags = CurrentRun.BlockTimerFlags or {}
if CurrentRun.BlockTimerFlags.Codex then
DebugPrint({Text = " Removing Erroneous Codex Timer Block"})
CurrentRun.BlockTimerFlags.Codex = nil
end
CurrentRun.NumRerolls = CurrentRun.NumRerolls or 0
CurrentRun.ThanatosSpawns = CurrentRun.ThanatosSpawns or 0
CurrentRun.ClosedDoors = CurrentRun.ClosedDoors or {}
CurrentRun.SupportAINames = CurrentRun.SupportAINames or {}
CurrentRun.BannedEliteAttributes = CurrentRun.BannedEliteAttributes or {}
if CurrentRun.CompletedStyxWings == nil then
CurrentRun.CompletedStyxWings = 0
for i, roomData in pairs( CurrentRun.RoomHistory ) do
if roomData.LinkedRoom == "D_Hub" and roomData.Name ~= "D_Intro" then
CurrentRun.CompletedStyxWings = CurrentRun.CompletedStyxWings + 1
end
end
end
if CurrentRun.WingDepth ~= nil and CurrentRun.WingDepth >= 3 and CurrentRun.BiomeRoomCountCache["D_Combat02"] ~= nil and CurrentRun.BiomeRoomCountCache["D_Combat03"] ~= nil and CurrentRun.BiomeRoomCountCache["D_Combat06"] ~= nil then
DebugPrint({ Text="Manually forcing a MiniBoss instead of Combat room" })
CurrentRun.CurrentRoom.ForceWingEndMiniBoss = true
end
for i, roomData in pairs( CurrentRun.RoomHistory ) do
if roomData.Store and roomData.Store.SpawnedStoreItems then
for s, spawnedItemData in pairs (roomData.Store.SpawnedStoreItems) do
CurrentRun.RoomHistory[i].Store.SpawnedStoreItems[s] = { ObjectId = spawnedItemData.ObjectId, Cost = spawnedItemData.Cost }
end
end
end
if CurrentRun.CurrentRoom ~= nil then
CurrentRun.CurrentRoom.EliteAttributes = CurrentRun.CurrentRoom.EliteAttributes or {}
CurrentRun.CurrentRoom.VoiceLinesPlayed = CurrentRun.CurrentRoom.VoiceLinesPlayed or {}
CurrentRun.CurrentRoom.TextLinesRecord = CurrentRun.CurrentRoom.TextLinesRecord or {}
if CurrentRun.CurrentRoom.ChallengeSwitch ~= nil and CurrentRun.CurrentRoom.ChallengeSwitch.RewardType == nil then
CurrentRun.CurrentRoom.ChallengeSwitch = DeepCopyTable( ObstacleData.ChallengeSwitch )
end
if CurrentRun.CurrentRoom.SellOptions then
local hasIndex = false
for i, data in pairs( CurrentRun.CurrentRoom.SellOptions ) do
if data.Index then
hasIndex = true
end
if data.Value == 0 and data.Index and CurrentRun.Hero.Traits[data.Index] then
data.Value = GetTraitValue(CurrentRun.Hero.Traits[data.Index])
end
end
if hasIndex then
CurrentRun.CurrentRoom.SellOptions = nil
end
end
if CurrentRun.CurrentRoom.Store ~= nil and CurrentRun.CurrentRoom.Store.StoreOptions ~= nil then
local giftDropIndex = nil
for i, storeOption in pairs( CurrentRun.CurrentRoom.Store.StoreOptions ) do
if storeOption.Type == "Cosmetic" then
storeOption.Name = "HealDropRange"
storeOption.Type = "Consumable"
elseif not storeOption.Name then
storeOption.Name = "HealDropRange"
storeOption.Type = "Consumable"
elseif storeOption.Name == "StoreGiftDrop" then
giftDropIndex = i
end
end
if giftDropIndex ~= nil then
CurrentRun.CurrentRoom.Store.StoreOptions[giftDropIndex] = GetRampedConsumableData( ConsumableData.GiftDrop )
CurrentRun.CurrentRoom.Store.StoreOptions[giftDropIndex].Type = "Consumable"
end
end
DebugAssert({ Condition = RoomData[CurrentRun.CurrentRoom.Name] ~= nil, Text = "Missing Room: "..tostring(CurrentRun.CurrentRoom.Name) })
CurrentRun.CurrentRoom.ShrineMetaUpgradeName = RoomData[CurrentRun.CurrentRoom.Name].ShrineMetaUpgradeName
CurrentRun.CurrentRoom.SpawnRewardOnId = RoomData[CurrentRun.CurrentRoom.Name].SpawnRewardOnId
CurrentRun.CurrentRoom.DisableRewardMagnetisim = RoomData[CurrentRun.CurrentRoom.Name].DisableRewardMagnetisim
CurrentRun.CurrentRoom.HideEncounterText = RoomData[CurrentRun.CurrentRoom.Name].HideEncounterText
CurrentRun.CurrentRoom.Binks = RoomData[CurrentRun.CurrentRoom.Name].Binks
CurrentRun.CurrentRoom.FadeOutAnimation = RoomData[CurrentRun.CurrentRoom.Name].FadeOutAnimation
CurrentRun.CurrentRoom.UnthreadedEvents = DeepCopyTable(RoomData[CurrentRun.CurrentRoom.Name].UnthreadedEvents)
CurrentRun.CurrentRoom.ObstacleData = DeepCopyTable(RoomData[CurrentRun.CurrentRoom.Name].ObstacleData)
if CurrentRun.CurrentRoom.Name ~= nil and RoomData[CurrentRun.CurrentRoom.Name] ~= nil then
CurrentRun.CurrentRoom.StackNumOverride = CurrentRun.CurrentRoom.StackNumOverride or RoomData[CurrentRun.CurrentRoom.Name].StackNumOverride
end
if CurrentRun.CurrentRoom.Name == "A_Boss03" then
CurrentRun.CurrentRoom.RestorePresentationFunction = RoomData.A_Boss03.RestorePresentationFunction
end
if CurrentRun.CurrentRoom.Name == "B_Boss01" then
CurrentRun.CurrentRoom.LinkedRoom = RoomData.B_Boss01.LinkedRoom
CurrentRun.CurrentRoom.ExitFunctionName = RoomData.BaseAsphodel.ExitFunctionName
CurrentRun.CurrentRoom.SkipLoadNextMap = RoomData.BaseAsphodel.SkipLoadNextMap
CurrentRun.CurrentRoom.HydraStartingPosition = RoomData.B_Boss01.HydraStartingPosition
end
if CurrentRun.CurrentRoom.Name == "B_Boss02" then
CurrentRun.CurrentRoom.HydraStartingPosition = RoomData.B_Boss02.HydraStartingPosition
end
if CurrentRun.CurrentRoom.Name == "C_Boss01" then
CurrentRun.CurrentRoom.ShowRunClearScreen = RoomData.C_Boss01.ShowRunClearScreen
CurrentRun.CurrentRoom.LinkedRoom = RoomData.C_Boss01.LinkedRoom
CurrentRun.CurrentRoom.ExitFunctionName = RoomData.BaseElysium.ExitFunctionName
CurrentRun.CurrentRoom.SkipLoadNextMap = false
CurrentRun.Cleared = false
RemoveTimerBlock( CurrentRun, "TheseusMinotaurKillPresentation" )
end
if CurrentRun.CurrentRoom.Name == "D_Boss01" then
CurrentRun.CurrentRoom.LinkedRoom = RoomData.D_Boss01.LinkedRoom
CurrentRun.CurrentRoom.ExitFunctionName = RoomData.D_Boss01.ExitFunctionName
CurrentRun.CurrentRoom.SkipLoadNextMap = RoomData.D_Boss01.SkipLoadNextMap
CurrentRun.CurrentRoom.NextRoomSet = RoomData.D_Boss01.NextRoomSet
CurrentRun.CurrentRoom.FishBiome = RoomData.D_Boss01.FishBiome
CurrentRun.CurrentRoom.SwapAnimations = RoomData.D_Boss01.SwapAnimations
CurrentRun.CurrentRoom.ZoomFraction = RoomData.D_Boss01.ZoomFraction
end
if CurrentRun.CurrentRoom.Name == "A_MiniBoss02" then
CurrentRun.CurrentRoom.SpawnOnIds = RoomData.A_MiniBoss02.SpawnOnIds
end
if CurrentRun.CurrentRoom.Name == "C_MiniBoss01" then
if CurrentRun.CurrentRoom.Encounter ~= nil and CurrentRun.CurrentRoom.Encounter.UnthreadedEvents ~= nil and CurrentRun.CurrentRoom.Encounter.UnthreadedEvents[3].FunctionName == "WaitForMinotaurEarlyExit" then
CurrentRun.CurrentRoom.Encounter = SetupEncounter( EncounterData.MiniBossMinotaur )
end
end
if CurrentRun.CurrentRoom.Name == "C_MiniBoss02" and CurrentRun.CurrentRoom.Encounter.Name ~= "MiniBossNakedSpawners" then
CurrentRun.CurrentRoom.Encounter = SetupEncounter( EncounterData.MiniBossNakedSpawners )
end
if CurrentRun.CurrentRoom.Encounter ~= nil and EncounterData[CurrentRun.CurrentRoom.Encounter.Name] ~= nil then
CurrentRun.CurrentRoom.Encounter.RequiredKillFunctionName = CurrentRun.CurrentRoom.Encounter.RequiredKillFunctionName or EncounterData[CurrentRun.CurrentRoom.Encounter.Name].RequiredKillFunctionName
end
if CurrentRun.CurrentRoom.Name == "D_Reprieve01" then
CurrentRun.CurrentRoom.StartUnthreadedEvents = DeepCopyTable(RoomData.D_Reprieve01.StartUnthreadedEvents)
end
if CurrentRun.CurrentRoom.Name == "D_Hub" then
CurrentRun.CurrentRoom.IntroSequenceDuration = RoomData[CurrentRun.CurrentRoom.Name].IntroSequenceDuration
CurrentRun.CurrentRoom.BlockCameraReattach = RoomData[CurrentRun.CurrentRoom.Name].BlockCameraReattach
CurrentRun.CurrentRoom.EntranceFunctionName = RoomData[CurrentRun.CurrentRoom.Name].EntranceFunctionName
end
if CurrentRun.CurrentRoom.Name == "E_Story01" then
CurrentRun.CurrentRoom.ExitFunctionName = RoomData.E_Story01.ExitFunctionName
CurrentRun.CurrentRoom.RunOverrides = RoomData.E_Story01.RunOverrides
end
end
if CurrentRun.RewardStores == nil then
InitializeRewardStores( CurrentRun )
else
for storeName, storeData in pairs( RewardStoreData ) do
if CurrentRun.RewardStores[storeName] == nil then
CurrentRun.RewardStores[storeName] = DeepCopyTable( storeData )
end
end
if CurrentRun.CurrentRoom ~= nil and CurrentRun.CurrentRoom.RoomSetName == "Styx" and CurrentRun.RewardStores.RunProgress ~= nil then
for k, reward in pairs( CurrentRun.RewardStores.RunProgress ) do
if reward.Name == "Devotion" then
CurrentRun.RewardStores.RunProgress[k] = nil
DebugPrint({ Text = "Removed bad reward" })
end
end
end
end
if CurrentRun.Hero ~= nil then
CurrentRun.Hero.OnTouchdown = nil
CurrentRun.Hero.CanBeStyxPoisoned = true
CurrentRun.Hero.CanStoreAmmo = true
CurrentRun.Hero.PersistentInvulnerableFlags = CurrentRun.Hero.PersistentInvulnerableFlags or {}
if not CurrentRun.Hero.PersistentInvulnerableFlags.AutoPlay then
CurrentRun.Hero.PersistentInvulnerableFlags.AutoPlay = nil
end
CurrentRun.Hero.DisableCombatControlsKeys = CurrentRun.Hero.DisableCombatControlsKeys or {}
CurrentRun.Hero.InvulnerableFlags = CurrentRun.Hero.InvulnerableFlags or {}
CurrentRun.Hero.LastStands = CurrentRun.Hero.LastStands or {}
CurrentRun.Hero.StoredAmmo = {}
CurrentRun.Hero.MaxLastStands = CurrentRun.Hero.MaxLastStands or TableLength( CurrentRun.Hero.LastStands )
CurrentRun.Hero.MaxHealth = CurrentRun.Hero.MaxHealth or HeroData.DefaultHero.MaxHealth
CurrentRun.Hero.Health = CurrentRun.Hero.Health or CurrentRun.Hero.MaxHealth
CurrentRun.Hero.Health = math.ceil(CurrentRun.Hero.Health)
CurrentRun.Hero.SuperActive = false
CurrentRun.Hero.EnemyMoneyDropBaseValue = CurrentRun.Hero.EnemyMoneyDropBaseValue or 1.0
CurrentRun.Hero.AnimOffsetZ = CurrentRun.Hero.AnimOffsetZ or HeroData.DefaultHero.AnimOffsetZ
CurrentRun.Hero.ShrinePointMetaPointBonusMultiplier = CurrentRun.Hero.ShrinePointMetaPointBonusMultiplier or HeroData.DefaultHero.ShrinePointMetaPointBonusMultiplier
CurrentRun.Hero.BoonData.ReplaceChance = CurrentRun.Hero.BoonData.ReplaceChance or HeroData.DefaultHero.BoonData.ReplaceChance
CurrentRun.Hero.BoonData.GameStateRequirements = CurrentRun.Hero.BoonData.GameStateRequirements or ShallowCopyTable(HeroData.DefaultHero.BoonData.GameStateRequirements)
CurrentRun.Hero.DamagedAnimation = CurrentRun.Hero.DamagedAnimation or HeroData.DefaultHero.DamagedAnimation
CurrentRun.Hero.DamagedFxStyles = CurrentRun.Hero.DamagedFxStyles or HeroData.DefaultHero.DamagedFxStyles
CurrentRun.Hero.InvulnerableHitFx = CurrentRun.Hero.InvulnerableHitFx or HeroData.DefaultHero.InvulnerableHitFx
CurrentRun.Hero.ComboThreshold = CurrentRun.Hero.ComboThreshold or HeroData.DefaultHero.ComboThreshold
CurrentRun.Hero.PerfectDashHitDisableDuration = CurrentRun.Hero.PerfectDashHitDisableDuration or HeroData.DefaultHero.PerfectDashHitDisableDuration
CurrentRun.Hero.GuaranteedCrit = {}
if CurrentRun.Hero.GuaranteedCritWeapons == nil then
CurrentRun.Hero.GuaranteedCritWeapons = {}
end
CurrentRun.Hero.Binks = ShallowCopyTable(HeroData.DefaultHero.Binks)
if CurrentRun.Hero.WeaponBinks == nil then
CurrentRun.Hero.WeaponBinks = ShallowCopyTable(HeroData.DefaultHero.WeaponBinks)
end
if not Contains(CurrentRun.Hero.WeaponBinks, "ZagreusWrath_Bink") then
table.insert(CurrentRun.Hero.WeaponBinks, "ZagreusWrath_Bink")
end
if CurrentRun.Hero.SuperTempHealthGain ~= nil then
CurrentRun.Hero.SuperTempHealthGain = nil
end
if GameState.LastAwardTrait == "RetainMoneyOnDeathTrait" then
GameState.LastAwardTrait = "BonusMoneyTrait"
end
for weaponName in pairs( CurrentRun.Hero.Weapons ) do
if WeaponData[weaponName] ~= nil and WeaponData[weaponName].SecondaryWeapon and not CurrentRun.Hero.Weapons[WeaponData[weaponName].SecondaryWeapon] then
CurrentRun.Hero.Weapons[WeaponData[weaponName].SecondaryWeapon] = true
end
end
CurrentRun.Hero.Weapons.ShoutWeapon = nil
CurrentRun.Hero.DefaultWeapon = CurrentRun.Hero.DefaultWeapon or HeroData.DefaultHero.DefaultWeapon
CurrentRun.Hero.LeaveRoomAmmoMangetismSpeed = CurrentRun.Hero.LeaveRoomAmmoMangetismSpeed or HeroData.DefaultHero.LeaveRoomAmmoMangetismSpeed
CurrentRun.Hero.DashManeuverTimeThreshold = CurrentRun.Hero.DashManeuverTimeThreshold or HeroData.DefaultHero.DashManeuverTimeThreshold
CurrentRun.Hero.FinalHitSlowParameters = CurrentRun.Hero.FinalHitSlowParameters or HeroData.DefaultHero.FinalHitSlowParameters
CurrentRun.Hero.ShoutSlowParameters = CurrentRun.Hero.ShoutSlowParameters or HeroData.DefaultHero.ShoutSlowParameters
CurrentRun.Hero.InvulnerableFrameMinDamage = CurrentRun.Hero.InvulnerableFrameMinDamage or HeroData.DefaultHero.InvulnerableFrameMinDamage
if CurrentRun.Hero.Super == nil then
CurrentRun.Hero.Super = ShallowCopyTable(HeroData.DefaultHero.Super)
end
if CurrentRun.Hero.RallyHealth == nil then
CurrentRun.Hero.RallyHealth = ShallowCopyTable(HeroData.DefaultHero.RallyHealth)
else
if CurrentRun.Hero.RallyHealth.RallyDecayRateSeconds == nil then
CurrentRun.Hero.RallyHealth.RallyDecayRateSeconds = HeroData.DefaultHero.RallyHealth.RallyDecayRateSeconds
end
if CurrentRun.Hero.RallyHealth.RallyDecayHold == nil then
CurrentRun.Hero.RallyHealth.RallyDecayHold = HeroData.DefaultHero.RallyHealth.RallyDecayHold
end
if CurrentRun.Hero.RallyHealth.State == nil then
CurrentRun.Hero.RallyHealth.State = HeroData.DefaultHero.RallyHealth.State
end
if CurrentRun.Hero.RallyHealth.MaxRallyHealthPerHit == nil then
CurrentRun.Hero.RallyHealth.MaxRallyHealthPerHit = HeroData.DefaultHero.RallyHealth.MaxRallyHealthPerHit
end
end
CurrentRun.Hero.HandlingDeath = false
if CurrentRun.Hero.IsDead and CurrentRun.CurrentRoom and RoomSetData.Secrets[CurrentRun.CurrentRoom.Name] and CurrentDeathAreaRoom == nil then
CurrentRun.Hero.IsDead = false
end
local mapName = GetMapName({ })
if mapName == "DeathArea" then
CurrentRun.Hero.IsDead = true
end
if CurrentRun.Hero.StackData == nil then
CurrentRun.Hero.StackData = ShallowCopyTable(HeroData.DefaultHero.StackData)
else
CurrentRun.Hero.StackData.AllowRarityOverride = HeroData.DefaultHero.StackData.AllowRarityOverride
end
if CurrentRun.Hero.HermesData == nil then
CurrentRun.Hero.HermesData = ShallowCopyTable(HeroData.DefaultHero.HermesData)
end
if CurrentRun.Hero.IsDead and CurrentRun.ActiveBiomeTimer then
CurrentRun.ActiveBiomeTimer = false
end
if CurrentRun.Hero.OutgoingDamageModifiers then
local condemnedIds = {}
for i, modifierData in pairs(CurrentRun.Hero.OutgoingDamageModifiers) do
if modifierData.ValidWeapons and not modifierData.ValidWeaponsLookup then
modifierData.ValidWeaponsLookup = ToLookup( modifierData.ValidWeapons )
end
if modifierData.Temporary then
table.insert(condemnedIds, i)
end
end
if not IsEmpty(condemnedIds) then
for i, index in pairs(condemnedIds) do
CurrentRun.Hero.OutgoingDamageModifiers[index] = nil
end
CurrentRun.Hero.OutgoingDamageModifiers = CollapseTable(CurrentRun.Hero.OutgoingDamageModifiers)
end
end
if CurrentRun.Hero.IncomingDamageModifiers then
local condemnedIds = {}
for i, modifierData in pairs(CurrentRun.Hero.IncomingDamageModifiers) do
if modifierData.ValidWeapons and not modifierData.ValidWeaponsLookup then
modifierData.ValidWeaponsLookup = ToLookup( modifierData.ValidWeapons )
end
if modifierData.Temporary then
table.insert(condemnedIds, i)
end
end
if not IsEmpty(condemnedIds) then
for i, index in pairs(condemnedIds) do
CurrentRun.Hero.IncomingDamageModifiers[index] = nil
end
CurrentRun.Hero.IncomingDamageModifiers = CollapseTable(CurrentRun.Hero.IncomingDamageModifiers)
end
end
if CurrentRun.Hero.RecentTraits ~= nil then
if type(CurrentRun.Hero.RecentTraits[1]) ~= "table" then
CurrentRun.Hero.RecentTraits = {}
end
if IsEmpty(CurrentRun.Hero.RecentTraits) or not CurrentRun.Hero.RecentTraits[1].Id then
for i, traitData in pairs( CurrentRun.Hero.Traits ) do
if IsPrioritizedDisplayTrait( traitData ) then
-- force no overlaps with any existing traits
traitData.Id = -1 * i
PriorityTrayTraitAdd( traitData, { DeferSort = true })
end
end
end
else
CurrentRun.Hero.RecentTraits = {}
end
local traitsToAdd = {}
local traitsToRemove = {}
for i, trait in pairs(CurrentRun.Hero.Traits) do
ExtractValues( CurrentRun.Hero, trait, trait )
trait.AnchorId = nil
trait.AdditionalDataAnchorId = nil
trait.AdvancedTooltipFrame = nil
trait.AdvancedTooltipIcon = nil
trait.Id = trait.Id or GetTraitUniqueId()
local addTraitToUpdate = function ( trait )
if trait.OnExpire then
trait.OnExpire = nil
IncrementTableValue( traitsToRemove, trait.Name )
else
IncrementTableValue( traitsToAdd, trait.Name )
end
end
if not TraitData[trait.Name] then
IncrementTableValue(traitsToRemove, trait.Name)
elseif TableLength(trait.PropertyChanges) ~= TableLength(TraitData[trait.Name].PropertyChanges) or
TableLength(trait.EnemyPropertyChanges) ~= TableLength(TraitData[trait.Name].EnemyPropertyChanges) or
TableLength(trait.AddShout) ~= TableLength(TraitData[trait.Name].AddShout) or
TableLength( trait.LoadBinks ) ~= TableLength( TraitData[trait.Name].LoadBinks ) then
addTraitToUpdate( trait )
elseif trait.Slot == "Shout" and not trait.TooltipWrathStocks then
addTraitToUpdate( trait )
elseif trait.Slot == "Shout" and TraitData[trait.Name].AddShout and TraitData[trait.Name].AddShout.MaxDurationMultiplier then
local hasExtractValues = true
if TraitData[trait.Name].AddShout.ExtractValues then
for i, extractData in pairs( TraitData[trait.Name].AddShout.ExtractValues ) do
if not trait[extractData.ExtractAs] then
hasExtractValues = false
end
end
end
if not hasExtractValues then
addTraitToUpdate( trait )
end
elseif trait.CustomNameWithMetaUpgrade and trait.CustomNameWithMetaUpgrade.MetaUpgradeName == "HealingReductionShrineUpgrade" and not trait.HealingReduction then
addTraitToUpdate( trait )
else
for key, data in pairs (TraitData[trait.Name]) do
if not trait[key] then
addTraitToUpdate( trait )
break
end
end
end
if trait.PropertyChanges ~= nil then
for k, propertyChange in pairs( trait.PropertyChanges ) do
if propertyChange.ProjectileProperty == "DamagePerConescutiveHit" then
addTraitToUpdate( trait )
elseif propertyChange.WeaponProperty == "AdditionalProjectileDamageMultiplier" then
addTraitToUpdate( trait )
elseif propertyChange.LifeProperty == "MaxHealth" then
addTraitToUpdate( trait )
elseif propertyChange.LifeProperty == "Health" then
addTraitToUpdate( trait )
end
end
end
if trait.AddOutgoingDamageModifiers and trait.AddOutgoingDamageModifiers.ValidWeapons and not trait.AddOutgoingDamageModifiers.ValidWeaponsLookup then
addTraitToUpdate( trait )
end
if trait.Name == "NoCollisionTrait" then
IncrementTableValue(traitsToRemove, trait.Name)
IncrementTableValue(traitsToAdd, "MetaPointHealTrait")
end
if trait.Name == "LimitGainTrait" then
IncrementTableValue(traitsToRemove, trait.Name)
IncrementTableValue(traitsToAdd, "SuperGenerationTrait")
end
if trait.Name == "RetainMoneyOnDeathTrait" then
IncrementTableValue(traitsToRemove, trait.Name)
IncrementTableValue(traitsToAdd, "BonusMoneyTrait")
end
if trait.Name == "ChaosCurseMoveSpeedTrait" or trait.Name == "TemporaryMoveSpeedTrait" then
if trait.PropertyChanges[1].ChangeType == "Add" then
IncrementTableValue(traitsToRemove, trait.Name)
end
end
if trait.Name == "MetaUpgradeLastStandTrait" or trait.Name == "AthenaLastStandTrait" then
IncrementTableValue(traitsToRemove, trait.Name)
AddLastStand({
Icon = trait.LifeIcon or "ExtraLifeZag",
WeaponName = "LastStandMetaUpgradeShield",
HealFraction = 0.5
})
end
if trait.Name == "ReincarnationTrait" and trait.KeepsakeLastStandHealAmount == nil then
IncrementTableValue(traitsToRemove, trait.Name)
AddLastStand({
InsertAtEnd = true,
Icon = "ExtraLifeSkelly",
WeaponName = "LastStandReincarnateShield",
HealAmount = GetTotalHeroTraitValue( "KeepsakeLastStandHealAmount" ),
})
end
end
local orderedTraitsToAdd = CollapseTableAsOrderedKeyValuePairs(traitsToAdd)
for index, kvp in ipairs(orderedTraitsToAdd) do
local traitName = kvp.Key
local traitNumber = kvp.Value
for i=1, traitNumber do
DebugPrint({Text = " Removing " .. traitName })
RemoveTrait( CurrentRun.Hero, traitName )
end
for i=1, traitNumber do
DebugPrint({Text = " Readding " .. traitName })
AddTraitToHero({ TraitName = traitName, Rarity = "Common" })
end
end
local orderedTraitsToRemove = CollapseTableAsOrderedKeyValuePairs(traitsToRemove)
for index, kvp in ipairs(orderedTraitsToRemove) do
local traitName = kvp.Key
local traitNumber = kvp.Value
for i=1, traitNumber do
DebugPrint({Text = " Removing " .. traitName })
RemoveTrait( CurrentRun.Hero, traitName )
end
end
if CurrentRun.Hero.OnFireWeapons and HeroHasTrait("ShieldRushBonusProjectileTrait") then
if CurrentRun.Hero.OnFireWeapons.ShieldThrow and CurrentRun.Hero.OnFireWeapons.ShieldThrow.ChaosShieldThrow then
RemoveOnFireWeapons( CurrentRun.Hero, { LegalOnFireWeapons = {"ShieldThrow" }, AddOnFireWeapons = { "ChaosShieldThrow" } } )
end
if CurrentRun.Hero.OnFireWeapons.ShieldThrowDash and CurrentRun.Hero.OnFireWeapons.ShieldThrowDash.ChaosShieldThrow then
RemoveOnFireWeapons( CurrentRun.Hero, { LegalOnFireWeapons = {"ShieldThrowDash" }, AddOnFireWeapons = { "ChaosShieldThrow" } } )
end
end
ValidateMaxHealth()
CleanRecentTraitsRecord()
SortPriorityTraits()
CurrentRun.Hero.TargetMetaRewardsRatio = CurrentRun.Hero.TargetMetaRewardsRatio or HeroData.DefaultHero.TargetMetaRewardsRatio
CurrentRun.Hero.TargetMetaRewardsAdjustSpeed = CurrentRun.Hero.TargetMetaRewardsAdjustSpeed or HeroData.DefaultHero.TargetMetaRewardsAdjustSpeed
CurrentRun.Hero.CanBeFrozen = CurrentRun.Hero.CanBeFrozen or HeroData.DefaultHero.CanBeFrozen
if GetNumMetaUpgrades( "EnemyDamageShrineUpgrade" ) > 0 then
if not HasIncomingDamageModifier(CurrentRun.Hero, "EnemyDamageShrineUpgrade") then
local damageIncrease = GetNumMetaUpgrades( "EnemyDamageShrineUpgrade" ) * ( MetaUpgradeData.EnemyDamageShrineUpgrade.ChangeValue - 1 )
AddIncomingDamageModifier( CurrentRun.Hero,
{
Name = "EnemyDamageShrineUpgrade",
NonTrapDamageTakenMultiplier = damageIncrease
})
end
end
if HasIncomingDamageModifier( CurrentRun.Hero, "MagicShield" ) then
RemoveIncomingDamageModifier( CurrentRun.Hero, "MagicShield" )
end
end
if CurrentRun.CurrentRoom ~= nil then
if CurrentRun.CurrentRoom.ChosenRewardType == "Health" then
CurrentRun.CurrentRoom.ChosenRewardType = "RoomRewardMaxHealthDrop"
elseif CurrentRun.CurrentRoom.ChosenRewardType == "Money" then
CurrentRun.CurrentRoom.ChosenRewardType = "RoomRewardMoneyDrop"
elseif CurrentRun.CurrentRoom.ChosenRewardType == "MetaPoints" then
CurrentRun.CurrentRoom.ChosenRewardType = "RoomRewardMetaPointDrop"
elseif CurrentRun.CurrentRoom.ChosenRewardType == "Gift" then
CurrentRun.CurrentRoom.ChosenRewardType = "GiftDrop"
elseif CurrentRun.CurrentRoom.ChosenRewardType == "LockKey" then
CurrentRun.CurrentRoom.ChosenRewardType = "LockKeyDrop"
end
if CurrentRun.CurrentRoom.ChallengeEncounterName == "TimeChallengeEncounterTartarus" then
CurrentRun.CurrentRoom.ChallengeEncounterName = "TimeChallengeTartarus"
end
if CurrentRun.CurrentRoom.ChallengeEncounterName == "TimeChallengeEncounterAsphodel" then
CurrentRun.CurrentRoom.ChallengeEncounterName = "TimeChallengeAsphodel"
end
if CurrentRun.CurrentRoom.Name == "B_Boss01" then
CurrentRun.CurrentRoom.ZoomFraction = RoomSetData.Asphodel.B_Boss01.ZoomFraction
CurrentRun.CurrentRoom.CameraZoomWeights = RoomSetData.Asphodel.B_Boss01.CameraZoomWeights
end
if CurrentRun.CurrentRoom.Encounter ~= nil and CurrentRun.CurrentRoom.Encounter.Name == "Boss01" then
CurrentRun.CurrentRoom.Encounter.Name = "BossHarpy1"
end
end
if CurrentRun.CurrentRoom.Encounter ~= nil and EncounterData[CurrentRun.CurrentRoom.Encounter.Name] ~= nil then
CurrentRun.CurrentRoom.Encounter.StartRoomUnthreadedEvents = EncounterData[CurrentRun.CurrentRoom.Encounter.Name].StartRoomUnthreadedEvents
CurrentRun.CurrentRoom.Encounter.WipeEnemiesOnKill = EncounterData[CurrentRun.CurrentRoom.Encounter.Name].WipeEnemiesOnKill
CurrentRun.CurrentRoom.Encounter.CancelSpawnsOnKill = EncounterData[CurrentRun.CurrentRoom.Encounter.Name].CancelSpawnsOnKill
CurrentRun.CurrentRoom.Encounter.CancelSpawnsOnKillAll = EncounterData[CurrentRun.CurrentRoom.Encounter.Name].CancelSpawnsOnKillAll
CurrentRun.CurrentRoom.Encounter.CancelSpawnsOnKillAllTypes = EncounterData[CurrentRun.CurrentRoom.Encounter.Name].CancelSpawnsOnKillAllTypes
CurrentRun.CurrentRoom.Encounter.PreSpawnEnemies = EncounterData[CurrentRun.CurrentRoom.Encounter.Name].PreSpawnEnemies
CurrentRun.CurrentRoom.Encounter.EnemyCountShineModifierAmount = EncounterData[CurrentRun.CurrentRoom.Encounter.Name].EnemyCountShineModifierAmount
CurrentRun.CurrentRoom.Encounter.BlockEliteAttributes = EncounterData[CurrentRun.CurrentRoom.Encounter.Name].BlockEliteAttributes
CurrentRun.CurrentRoom.Encounter.SpawnIntervalMin = EncounterData[CurrentRun.CurrentRoom.Encounter.Name].SpawnIntervalMin
CurrentRun.CurrentRoom.Encounter.SpawnIntervalMax = EncounterData[CurrentRun.CurrentRoom.Encounter.Name].SpawnIntervalMax
CurrentRun.CurrentRoom.Encounter.SpawnCapHitReleaseDuration = EncounterData[CurrentRun.CurrentRoom.Encounter.Name].SpawnCapHitReleaseDuration
CurrentRun.CurrentRoom.Encounter.SpawnThreadName = EncounterData[CurrentRun.CurrentRoom.Encounter.Name].SpawnThreadName
CurrentRun.CurrentRoom.Encounter.LoadBinksFromEnemySet = EncounterData[CurrentRun.CurrentRoom.Encounter.Name].LoadBinksFromEnemySet
CurrentRun.CurrentRoom.Encounter.Binks = EncounterData[CurrentRun.CurrentRoom.Encounter.Name].Binks
if CurrentRun.CurrentRoom.Encounter.EnemyCountShrineModifierName ~= nil and CurrentRun.CurrentRoom.Encounter.EnemyCountShineModifierAmount == nil then
CurrentRun.CurrentRoom.Encounter.EnemyCountShineModifierAmount = 1
end
if CurrentRun.CurrentRoom.Encounter.WrappingData ~= nil and CurrentRun.CurrentRoom.Encounter.WrappingData.ObstacleOptions ~= nil then
--if type(GetFirstValue(CurrentRun.CurrentRoom.Encounter.WrappingData.ObstacleOptions)) == "string" then
CurrentRun.CurrentRoom.Encounter.WrappingData = EncounterData.WrappingAsphodel.WrappingData
--end
end
if CurrentRun.CurrentRoom.Encounter.DistanceTriggers ~= nil then
for k, trigger in pairs( CurrentRun.CurrentRoom.Encounter.DistanceTriggers ) do
if trigger.TriggerGroup ~= nil and type( trigger.TriggerGroup ) == "table" then
trigger.TriggerGroup = nil
DebugPrint({ Text = "removed back-compat wrong type" })
end
end
end
if CurrentRun.CurrentRoom.Encounter.Name == "WrappingAsphodel" then
CurrentRun.CurrentRoom.Encounter.UnthreadedEvents = EncounterSets.EncounterEventsWrapping
end
if CurrentRun.CurrentRoom.Encounter.Name == "MiniBossCrawler" then
CurrentRun.CurrentRoom.Encounter.UnthreadedEvents = EncounterData.MiniBossCrawler.UnthreadedEvents
end
if CurrentRun.CurrentRoom.Encounter.Name == "A_Boss01" then
CurrentRun.CurrentRoom.Encounter.SpawnWaves = EncounterData.A_Boss01.SpawnWaves
end
if CurrentRun.CurrentRoom.Encounter.Name == "A_Boss02" then
CurrentRun.CurrentRoom.Encounter.SpawnWaves = EncounterData.A_Boss01.SpawnWaves
end
if CurrentRun.CurrentRoom.Encounter.Name == "BossHydra" then
CurrentRun.CurrentRoom.Encounter.BlockHeadsByHydraVariant = EncounterData.BossHydra.BlockHeadsByHydraVariant
end
if CurrentRun.CurrentRoom.Encounter.Name == "MiniBossMinotaur" then
OverwriteTableKeys(CurrentRun.CurrentRoom.Encounter, EncounterData.MiniBossMinotaur)
CurrentRun.CurrentRoom.Encounter.UnthreadedEvents = EncounterData.MiniBossMinotaur.UnthreadedEvents
CurrentRun.CurrentRoom.Encounter.SpawnWaves = EncounterData.MiniBossMinotaur.SpawnWaves
end
if CurrentRun.CurrentRoom.Encounter.Name == "SurvivalElysium" then
CurrentRun.CurrentRoom.Encounter.ActiveEnemyCapBase = EncounterData.SurvivalElysium.ActiveEnemyCapBase
end
if CurrentRun.CurrentRoom.Encounter.Name == "BossTheseusAndMinotaur" then
CurrentRun.CurrentRoom.Encounter.SpawnThreadName = EncounterData.BossTheseusAndMinotaur.SpawnThreadName
end
if CurrentRun.CurrentRoom.Encounter.Name == "BossCharon" then
CurrentRun.CurrentRoom.Encounter.SpawnWaves = EncounterData.BossCharon.SpawnWaves
end
if CurrentRun.CurrentRoom.Encounter.Name == "BossHadesPeaceful" then
CurrentRun.CurrentRoom.Encounter.PostUnthreadedEvents = EncounterData.BossHadesPeaceful.PostUnthreadedEvents
end
if CurrentRun.CurrentRoom.Encounter.InheritFrom ~= nil and Contains(CurrentRun.CurrentRoom.Encounter.InheritFrom, "BaseIntroEncounter") then
CurrentRun.CurrentRoom.Encounter.DistanceTriggers = EncounterData[CurrentRun.CurrentRoom.Encounter.Name].DistanceTriggers
end
if CurrentRun.CurrentRoom.Encounter.RequiredKillFunctionName == "TrackThanatosChallengeProgress" then
CurrentRun.CurrentRoom.Encounter.ObjectiveSets = EncounterData.BaseThanatos.ObjectiveSets
end
if CurrentRun.CurrentRoom.Encounter.CancelSpawnsOnKill ~= nil and type(CurrentRun.CurrentRoom.Encounter.CancelSpawnsOnKill) ~= "table" then
CurrentRun.CurrentRoom.Encounter.CancelSpawnsOnKill = { CurrentRun.CurrentRoom.Encounter.CancelSpawnsOnKill }
end
if CurrentRun.CurrentRoom.Encounter.Spawns ~= nil then
for k, spawnData in pairs( CurrentRun.CurrentRoom.Encounter.Spawns ) do
if spawnData.Name == "Turret" then
spawnData.Name = "SplitShotUnit"
end
if spawnData.Name == "TurretElite" then
spawnData.Name = "SplitShotUnitElite"
end
end
end
if CurrentRun.CurrentRoom.Encounter.SpawnWaves ~= nil then
for k, wave in pairs( CurrentRun.CurrentRoom.Encounter.SpawnWaves ) do
for k, spawnData in pairs( wave.Spawns ) do
if spawnData.Name == "Turret" then
spawnData.Name = "SplitShotUnit"
end
if spawnData.Name == "TurretElite" then
spawnData.Name = "SplitShotUnitElite"
end
end
end
end
end
CurrentRun.MoneyRecord = CurrentRun.MoneyRecord or {}
if TextLinesRecord.NyxMiscMeeting01 or TextLinesRecord.NyxGrantsRespec then
GameState.Flags.SwapMetaupgradesEnabled = true
end
if IsWeaponUnlocked( "FistWeapon" ) then
GameState.Flags.FistUnlocked = true
end
if IsWeaponUnlocked( "GunWeapon" ) then
GameState.Flags.GunUnlocked = true
end
if GetNumUnlockedWeaponUpgrades() > 0 then
GameState.Flags.AspectsUnlocked = true
end
-- Super old, not needed anymore
--if CalcTotalNumEntries( UseRecord ) > 9999 then
--UseRecord = {}
--end
if CurrentRun.MarketItems ~= nil then
for k, item in pairs( CurrentRun.MarketItems ) do
if item.Buys ~= nil or item.CostLow ~= nil then
-- Remove old data structure
CurrentRun.MarketItems = nil
break
end
end
end
if CurrentRun.LootTypeHistory ~= nil then
for lootName, i in pairs(CurrentRun.LootTypeHistory) do
if not GameData.MissingPackages[lootName] then
LoadPackages({ Name = lootName })
end
end
end
end
DebugPrint({Text = " Done patching."})
end
function GetMaxHealthUpgradeIncrement( value )
local expectedMaxHealth = HeroData.DefaultHero.MaxHealth
for i, trait in pairs(CurrentRun.Hero.Traits) do
if trait.PropertyChanges ~= nil then
for k, propertyChange in pairs( trait.PropertyChanges ) do
if propertyChange.LuaProperty == "MaxHealth" then
expectedMaxHealth = expectedMaxHealth + propertyChange.ChangeValue
end
end
end
end
expectedMaxHealth = expectedMaxHealth + GetNumMetaUpgrades("HealthMetaUpgrade") * MetaUpgradeData.HealthMetaUpgrade.ChangeValue + value
expectedMaxHealth = round(expectedMaxHealth * GetTotalHeroTraitValue("MaxHealthMultiplier", { IsMultiplier = true }))
return expectedMaxHealth - CurrentRun.Hero.MaxHealth
end
function ValidateMaxHealth( blockHealing )
local expectedMaxHealth = HeroData.DefaultHero.MaxHealth
for i, trait in pairs(CurrentRun.Hero.Traits) do
if trait.PropertyChanges ~= nil then
for k, propertyChange in pairs( trait.PropertyChanges ) do
if propertyChange.LuaProperty == "MaxHealth" then
expectedMaxHealth = expectedMaxHealth + propertyChange.ChangeValue
end
end
end
end
expectedMaxHealth = expectedMaxHealth + GetNumMetaUpgrades("HealthMetaUpgrade") * MetaUpgradeData.HealthMetaUpgrade.ChangeValue
expectedMaxHealth = round(expectedMaxHealth * GetTotalHeroTraitValue("MaxHealthMultiplier", { IsMultiplier = true }))
if expectedMaxHealth ~= CurrentRun.Hero.MaxHealth then
local delta = expectedMaxHealth - CurrentRun.Hero.MaxHealth
CurrentRun.Hero.MaxHealth = round( CurrentRun.Hero.MaxHealth + delta )
if not blockHealing then
CurrentRun.Hero.Health = round( CurrentRun.Hero.Health + delta )
end
end
end
function SetupMap()
CreateGroup({ Name = "Standing" })
CreateGroup({ Name = "Standing_Back" })
InsertGroupBehind({ Name = "Standing_Back", DestinationName = "Standing" })
CreateGroup({ Name = "Standing_Back_Add", BlendMode = "Additive" })
InsertGroupInFront({ Name = "Standing_Back_Add", DestinationName = "Standing_Back" })
CreateGroup({ Name = "FX_Terrain", BlendMode = "Normal" })
InsertGroupBehind({ Name = "FX_Terrain", DestinationName = "Standing_Back" })
CreateGroup({ Name = "FX_Terrain_Dark", BlendMode = "Normal" })
InsertGroupBehind({ Name = "FX_Terrain_Dark", DestinationName = "FX_Terrain" })
CreateGroup({ Name = "FX_Terrain_Liquid", BlendMode = "Normal" })
InsertGroupBehind({ Name = "FX_Terrain_Liquid", DestinationName = "FX_Terrain" })
CreateGroup({ Name = "FX_Terrain_Add", BlendMode = "Additive" })
InsertGroupInFront({ Name = "FX_Terrain_Add", DestinationName = "FX_Terrain" })
CreateGroup({ Name = "FX_Displacement", BlendMode = "DisplacementMerge" })
InsertGroupInFront({ Name = "FX_Displacement", DestinationName = "Standing" })
CreateGroup({ Name = "FX_Standing_Add", BlendMode = "AdditiveMerge" })
InsertGroupInFront({ Name = "FX_Standing_Add", DestinationName = "FX_Displacement" })
CreateGroup({ Name = "FX_Dark", BlendMode = "Normal" })
InsertGroupBehind({ Name = "FX_Dark", DestinationName = "Standing_Back" })
CreateGroup({ Name = "FX_Terrain_Top", BlendMode = "Normal" })
InsertGroupInFront({ Name = "FX_Terrain_Top", DestinationName = "FX_Terrain" })
CreateGroup({ Name = "Shadows" })
InsertGroupBehind({ Name = "Shadows", DestinationName = "FX_Terrain_Add" })
CreateGroup({ Name = "FX_Standing_Top", BlendMode = "Normal" })
InsertGroupInFront({ Name = "FX_Standing_Top", DestinationName = "FX_Standing_Add" })
CreateGroup({ Name = "FX_Add_Top", BlendMode = "Additive" })
InsertGroupInFront({ Name = "FX_Add_Top", DestinationName = "FX_Standing_Top" })
local vignetteBlendMode = "Overlay"
if not GetConfigOptionValue({ Name = "DrawBloom" }) then
vignetteBlendMode = "Additive"
end
CreateGroup({ Name = "Vignette", BlendMode = vignetteBlendMode })
InsertGroupInFront({ Name = "Vignette", DestinationName = "MapArt" })
CreateGroup({ Name = "Combat_UI_World_Backing" })
CreateGroup({ Name = "Combat_UI_World" })
CreateGroup({ Name = "Combat_UI_World_Add", BlendMode = "Additive" })
CreateGroup({ Name = "Combat_UI" })
CreateGroup({ Name = "Combat_UI_Backing" })
CreateGroup({ Name = "Combat_UI_Additive", BlendMode = "Additive" })
CreateGroup({ Name = "Combat_Menu" })
CreateGroup({ Name = "Combat_Menu_Additive", BlendMode = "Additive" })
CreateGroup({ Name = "Combat_Menu_Overlay_Backing" })
CreateGroup({ Name = "Combat_Menu_Overlay" })
CreateGroup({ Name = "Combat_Menu_TraitTray_Backing" })
CreateGroup({ Name = "Combat_Menu_TraitTray" })
CreateGroup({ Name = "Combat_Menu_TraitTray_Additive", BlendMode = "Additive" })
CreateGroup({ Name = "Combat_Menu_TraitTray_Overlay" })
CreateGroup({ Name = "Combat_Menu_TraitTray_Overlay_Additive", BlendMode = "Additive" })
CreateGroup({ Name = "Portrait_FX_Behind" })
CreateGroup({ Name = "Portrait_FX_Behind_Add", BlendMode = "Additive" })
InsertGroupInFront({ Name = "Combat_UI_World", DestinationName = "Combat_UI_World_Backing" })
InsertGroupInFront({ Name = "Combat_UI_Backing", DestinationName = "Combat_UI_World" })
InsertGroupInFront({ Name = "Combat_UI_World_Add", DestinationName = "Combat_UI_World_Backing" })
InsertGroupInFront({ Name = "Combat_UI", DestinationName = "Combat_UI_Backing" })
InsertGroupInFront({ Name = "Combat_UI_Additive", DestinationName = "Combat_UI" })
InsertGroupInFront({ Name = "Combat_Menu", DestinationName = "Combat_UI_Additive" })
InsertGroupBehind({ Name = "Portrait_FX_Behind", DestinationName = "Combat_Menu" })
InsertGroupInFront({ Name = "Portrait_FX_Behind_Add", DestinationName = "Portrait_FX_Behind" })
InsertGroupInFront({ Name = "Combat_Menu_Additive", DestinationName = "Combat_Menu" })
InsertGroupInFront({ Name = "Combat_Menu_Overlay_Backing", DestinationName = "Combat_Menu_Additive" })
InsertGroupInFront({ Name = "Combat_Menu_Overlay", DestinationName = "Combat_Menu_Overlay_Backing" })
InsertGroupInFront({ Name = "Combat_Menu_TraitTray_Backing", DestinationName = "Combat_Menu_Overlay" })
InsertGroupInFront({ Name = "Combat_Menu_TraitTray", DestinationName = "Combat_Menu_TraitTray_Backing" })
InsertGroupInFront({ Name = "Combat_Menu_TraitTray_Additive", DestinationName = "Combat_Menu_TraitTray" })
InsertGroupInFront({ Name = "Combat_Menu_TraitTray_Overlay", DestinationName = "Combat_Menu_TraitTray_Additive" })
InsertGroupInFront({ Name = "Combat_Menu_TraitTray_Overlay_Additive", DestinationName = "Combat_Menu_TraitTray_Overlay" })
end
function IsRecordRunDepth( currentRun )
local highestRunDepth = GetHighestPrevRunRepth( currentRun )
if currentRun.RunDepthCache >= highestRunDepth then
return true
end
return false
end
function GetHighestPrevRunRepth( currentRun )
local highestRunDepth = 0
for k, prevRun in pairs( GameState.RunHistory ) do
if prevRun.RunDepthCache > highestRunDepth then
highestRunDepth = prevRun.RunDepthCache
end
end
return highestRunDepth
end
function GetFastestRunClearTime( currentRun )
local fastestTime = 999999
if currentRun.Cleared then
fastestTime = currentRun.GameplayTime
end
for k, prevRun in pairs( GameState.RunHistory ) do
if IsSameMode( prevRun ) and prevRun.RunDepthCache > 44 and prevRun.Cleared and prevRun.GameplayTime ~= nil and prevRun.GameplayTime < fastestTime then
fastestTime = prevRun.GameplayTime
end
end
return fastestTime
end
function GetFastestRunClearTimeWithWeapon( currentRun, weapon )
local fastestTime = nil
if currentRun.Cleared and currentRun.WeaponsCache[weapon] then
fastestTime = currentRun.GameplayTime
end
for k, prevRun in pairs( GameState.RunHistory ) do
if prevRun.WeaponsCache ~= nil and prevRun.WeaponsCache[weapon] then
if IsSameMode( prevRun ) and prevRun.RunDepthCache > 44 and prevRun.Cleared and prevRun.GameplayTime ~= nil and (fastestTime == nil or prevRun.GameplayTime < fastestTime) then
fastestTime = prevRun.GameplayTime
end
end
end
return fastestTime
end
function RunHasOneOfTraits( run, traits )
if run.TraitCache == nil then
return false
end
for k, traitName in pairs( traits ) do
if run.TraitCache[traitName] then
return true
end
end
return false
end
function RunHasTraits( run, traits )
if run.TraitCache == nil then
return false
end
for k, traitName in pairs( traits ) do
if not run.TraitCache[traitName] then
return false
end
end
return true
end
OnUsed{ "ExitDoors",
function( triggerArgs )
AttemptUseDoor( triggerArgs.TriggeredByTable )
end
}
function AttemptUseDoor( door )
if not door.ReadyToUse or not CheckRoomExitsReady( CurrentRun.CurrentRoom ) or CheckSpecialDoorRequirement( door ) ~= nil then
thread( CannotUseDoorPresentation, door )
return
end
if CurrentRun.CurrentRoom.SellTraitShrineUpgrade and CurrentRun.CurrentRoom.SellTraitShop ~= nil and GetNumMetaUpgrades( "ForceSellShrineUpgrade" ) > 0 and not CurrentRun.CurrentRoom.SoldTrait and not IsEmpty( CurrentRun.CurrentRoom.SellOptions ) then
-- if the shrine upgrade is active, blcok the entrance until the well shop is used
if CheckCooldown( "DoorLocked", 1.0 ) then
PlaySound({ Name = door.LockedUseSound or RoomData.BaseRoom.LockedUseSound, Id = door.ObjectId })
thread( InCombatTextArgs, { Text = "ExitBlockedBySellWell", TargetId = CurrentRun.Hero.ObjectId, SkipRise = true, SkipFlash = true, SkipShadow = false, Duration = 1.5, OffsetY = -160, ShadowScaleX = 2.0 } )
thread( PlayVoiceLines, HeroVoiceLines.ExitBlockedBySellWellVoiceLines, true )
end
return false
end
if door.Cost ~= nil then
SpendMoney( door.Cost, door.Name or "Door" )
end
if door.DoorDepthSkip then
CurrentRun.CurrentRoom.DepthSkip = door.DoorDepthSkip
end
if door.DoorBiomeSkip then
CurrentRun.CurrentRoom.NextRoomSet = door.DoorBiomeSkip
end
if door.HealthCost ~= nil then
SacrificeHealth({ SacrificeHealthMin = door.HealthCost, SacrificeHealthMax = door.HealthCost, Silent = true })
end
SetPlayerInvulnerable( "LeaveRoom" )
AddTimerBlock( CurrentRun, "UseDoor" )
if door.OnUsedPresentationFunctionName ~= nil then
local onUsedPresentationFunction = _G[door.OnUsedPresentationFunctionName]
if onUsedPresentationFunction ~= nil then
onUsedPresentationFunction( door.ObjectId, door )
end
end
RemoveTimerBlock( CurrentRun, "UseDoor" )
SetPlayerVulnerable( "LeaveRoom" )
PlaySound({ Name = door.UnlockedUseSound or RoomData.BaseRoom.UnlockedUseSound })
LeaveRoom( CurrentRun, door )
end
function CheckSpecialDoorRequirement( door )
if door == nil then
return "ExitNotActive"
end
if door.HealthCost ~= nil and CurrentRun.Hero.Health <= door.HealthCost then
return "ExitBlockedByHealthReq"
end
if door.ShrinePointReq ~= nil and GetTotalSpentShrinePoints() < door.ShrinePointReq then
return "ExitBlockedByShrinePointReq"
end
if door.Cost ~= nil and CurrentRun.Money < door.Cost then
return "ExitBlockedByMoneyReq"
end
return nil
end
function CheckDoorHealTrait( currentRun )
local maxHealth = currentRun.Hero.MaxHealth
local healAmount = 0
local roomHealFraction = GetTotalHeroTraitValue("DoorHeal")
healAmount = roomHealFraction * maxHealth
healAmount = healAmount + GetNumMetaUpgrades("DoorHealMetaUpgrade") * MetaUpgradeData.DoorHealMetaUpgrade.ChangeValue
healAmount = round( healAmount * CalculateHealingMultiplier())
if healAmount > 0 then
Heal( CurrentRun.Hero, { HealAmount = healAmount, Name = "DoorHeal" } )
end
thread( UpdateHealthUI )
end
function CheckInspectPoints( currentRun, source )
if source.InspectPoints == nil then
return
end
for id, inspectPointData in pairs( source.InspectPoints ) do
ProcessTextLines( inspectPointData.InteractTextLineSets )
inspectPointData.ObjectId = id
if IsInspectPointEligible( currentRun, source, inspectPointData ) then
Activate({ Id = id })
AttachLua({ Id = id, Table = inspectPointData })
inspectPointData.RecordUse = true
if inspectPointData.Hidden ~= nil then
SetAlpha({ Id = id, Fraction = 0.0 })
end
if inspectPointData.Deactivated then
SetAlpha({ Id = id, Fraction = 1.0 })
UseableOn({ Id = id })
inspectPointData.Deactivated = false
end
if inspectPointData.ClearNextInteractLinesOnActivate then
local source = ActiveEnemies[inspectPointData.ClearNextInteractLinesOnActivate]
source.NextInteractLines = nil
UseableOff({ Id = source.ObjectId })
RefreshUseButton( source.ObjectId, source )
StopStatusAnimation( source )
end
elseif inspectPointData.DeactivateIfIneligible then
SetAlpha({ Id = id, Fraction = 0.0 })
UseableOff({ Id = id })
inspectPointData.Deactivated = true
end
end
end
function StartRoom( currentRun, currentRoom )
if currentRoom.EncounterSpecificDataOverwrites ~= nil and currentRoom.EncounterSpecificDataOverwrites[currentRoom.Encounter.Name] ~= nil then
OverwriteTableKeys(currentRoom, currentRoom.EncounterSpecificDataOverwrites[currentRoom.Encounter.Name])
end
if currentRoom.RunOverrides then
OverwriteTableKeys(currentRun, currentRoom.RunOverrides)
end
currentRun.RunDepthCache = currentRun.RunDepthCache or GetRunDepth( currentRun )
currentRun.BiomeDepthCache = currentRun.BiomeDepthCache or GetBiomeDepth( currentRun )
if currentRoom.WingRoom then
currentRun.WingDepth = (currentRun.WingDepth or 0) + 1
else
currentRun.WingDepth = 0
end
if currentRoom.WingEndRoom then
currentRun.CompletedStyxWings = currentRun.CompletedStyxWings + 1
end
DebugAssert({ Condition = not currentRun.Hero.IsDead, Text = "Starting a room with a dead hero!" })
if currentRoom.RichPresence ~= nil then
SetRichPresence({ Key = "status", Value = currentRoom.RichPresence })
SetRichPresence({ Key = "steam_display", Value = currentRoom.RichPresence })
end
AddTimerBlock( currentRun, "StartRoom" )
if currentRoom.TimerBlock ~= nil then
AddTimerBlock( currentRun, currentRoom.TimerBlock )
end
if currentRoom.RemoveTimerBlock ~= nil then
RemoveTimerBlock( currentRun, currentRoom.RemoveTimerBlock )
end
LifeOnKillRecord = {}
DamageRecord = {}
SpawnRecord = {}
HealthRecord = {}
ForceNextRoom = nil
ForceNextEncounter = nil
DebugPrint({ Text = "StartRoom: "..currentRoom.Name.." (RunDepth = "..currentRun.RunDepthCache..")".." (BiomeDepth = "..currentRun.BiomeDepthCache..")".." (Seed = "..GetGlobalRng().seed..")" })
local previousRoom = GetPreviousRoom(currentRun)
if previousRoom ~= nil and previousRoom.StartTime ~= nil then
local timeInPrev = _worldTime - previousRoom.StartTime
--DebugPrint({ Text = "Time in previous: "..timeInPrev.." seconds" })
end
if currentRoom.CameraWalls then
CreateCameraWalls({ })
end
if currentRoom.BreakableOptions ~= nil then
RandomizeBreakables( currentRoom )
end
AddInputBlock({ Name = "StartRoom" })
local firstRoomOfRun = IsEmpty( currentRun.RoomHistory )
SetupHeroObject( currentRun, firstRoomOfRun )
if firstRoomOfRun and not GameState.ActiveOnslaught then
ValidateMaxHealth()
end
currentRoom.StartTime = _worldTime
local prevRoom = GetPreviousRoom( currentRun )
if prevRoom ~= nil and prevRoom.CheckWeaponHistory then
UpdateWeaponHistory(currentRun)
end
SwitchActiveUnit({ Id = currentRun.Hero.ObjectId })
SetupPreActivatedEnemies( currentRun )
AssignObstacles( currentRoom )
LoadSpawnPackages( currentRoom.Encounter )
HandleSecretSpawns( currentRun )
CheckChallengeSwitchItemValidity( currentRun )
StartRoomPreLoadBinks({
Run = currentRun,
Room = currentRoom,
Encounter = currentRoom.Encounter,
ChallengeEncounter = currentRoom.ChallengeEncounter
})
if currentRoom.BreakableValueOptions ~= nil then
HandleBreakableSwap( currentRoom )
end
-- Active ExitDoors
local exitDoorIds = GetInactiveIds({ Name = "ExitDoors" })
local firstDoor = true
while not IsEmpty( exitDoorIds ) do
local doorId = RemoveRandomValue( exitDoorIds )
if firstDoor or RandomChance( currentRun.CurrentRoom.ExtraDoorActivateChance or 1.0 ) then
Activate({ Id = doorId })
firstDoor = false
end
end
ChooseNextRewardStore( currentRun )
FadeOut({ Color = Color.Black, Duration = 0 })
if currentRoom.Encounter.SpawnWaves ~= nil and GetNumMetaUpgrades( "EnemyEliteShrineUpgrade" ) > 0 then
PickRoomEliteTypeUpgrades(currentRoom)
end
RunUnthreadedEvents( currentRoom.StartUnthreadedEvents, currentRoom )
RunUnthreadedEvents( currentRoom.Encounter.StartRoomUnthreadedEvents, currentRoom )
local darknessEarned = 0
for i, traitData in pairs(GetHeroTraitValues( "DarknessPerRoom" )) do
darknessEarned = darknessEarned + traitData.Base + round(traitData.DepthMult * GetRunDepth( currentRun ))
end
AddResource( "MetaPoints", darknessEarned, "DarknessPerRoom", { Silent = true } )
SetupRoomArt( currentRun, currentRoom )
if GetNumMetaUpgrades("BiomeSpeedShrineUpgrade") > 0 then
local currentArea = CurrentRun.CurrentRoom.BiomeMapArea or CurrentRun.CurrentRoom.RoomSetName
local previousArea = nil
if prevRoom then
previousArea = prevRoom.BiomeMapArea or prevRoom.RoomSetName
end
local currentAreaIndex = -1
for biomeListIndex, biomes in ipairs(BiomeList) do
if BiomeList[biomeListIndex] == currentArea then
currentAreaIndex = biomeListIndex
end
end
if previousArea == nil or ( currentArea ~= previousArea and Contains(BiomeTimeLimits.ValidBiomes, currentArea ) and Contains(BiomeTimeLimits.ValidBiomes, previousArea )) then
local additionalTime = BiomeTimeLimits.Timers[GetNumMetaUpgrades("BiomeSpeedShrineUpgrade")][currentAreaIndex]
CurrentRun.BiomeTime = math.max( CurrentRun.BiomeTime, 0 ) + additionalTime
thread( BiomeTimeCheckpointPresentation, CurrentRun, additionalTime )
end
-- if we are in a erebus / chaos / postboss room reset the timer
thread( BiomeSpeedTimerLoop )
end
if GetNumMetaUpgrades("ExtraChanceReplenishMetaUpgrade") > 0 then
local numRegenerationLastStands = 0
for i, lastStand in pairs(CurrentRun.Hero.LastStands) do
if lastStand.Name == "ExtraChanceReplenishMetaUpgrade" then
numRegenerationLastStands = numRegenerationLastStands + 1
end
end
while GetNumMetaUpgrades("ExtraChanceReplenishMetaUpgrade") > numRegenerationLastStands do
AddLastStand({
Name = "ExtraChanceReplenishMetaUpgrade",
Unit = CurrentRun.Hero,
Icon = "ExtraLifeReplenish",
WeaponName = "LastStandMetaUpgradeShield",
HealFraction = MetaUpgradeData.ExtraChanceReplenishMetaUpgrade.HealPercent,
Silent = true
})
numRegenerationLastStands = numRegenerationLastStands + 1
end
CurrentRun.Hero.MaxLastStands = TableLength(CurrentRun.Hero.LastStands)
end
StartRoomPresentation( currentRun, currentRoom, darknessEarned )
RemoveInputBlock({ Name = "StartRoom" })
RemoveTimerBlock( currentRun, "StartRoom" )
StartTriggers( currentRoom, currentRoom.DistanceTriggers )
RunEvents( currentRoom )
RunTraitThreads( currentRun.Hero )
CheckAutoObjectiveSets( currentRun, "RoomStart" )
if currentRoom.Encounter.SpawnDelay ~= nil then
wait( currentRoom.Encounter.SpawnDelay )
end
if currentRoom.DisableWeapons then
DisableCombatControls()
elseif currentRoom.DisableWeaponsExceptDash then
DisableCombatControls()
ToggleControl({ Names = { "Rush" }, Enabled = true })
else
EnableCombatControls()
end
if not currentRoom.HideCombatUI then
ShowCombatUI()
end
DebugAssert({ Condition = CurrentRun.Hero.IsDead or MusicId ~= nil or SecretMusicId ~= nil or AmbientMusicId ~= nil or currentRoom.NoReward or
string.match( currentRoom.Name, "Test" ) ~= nil or string.match( currentRoom.Name, "Reprieve" ) ~= nil or string.match( currentRoom.Name, "B_Story01" ) ~= nil or
currentRoom.Encounter.StartGlobalVoiceLines == "PerfectClearStartVoiceLines" or currentRoom.EndMusicOnEnterDuration ~= nil,
Text = "Room started with no music!" })
CheckDashOverride( currentRoom )
-- Take the room's StartTriggerDistance, otherwise the encounter's
local startTriggerDistance = currentRoom.OverrideStartTriggerDistance or currentRoom.Encounter.StartTriggerDistance
if startTriggerDistance ~= nil and startTriggerDistance > 0 then
local encounterStartIds = GetIdsByType({ Name = "EncounterStartPoint" })
if not IsEmpty( encounterStartIds ) then
local notifyName = "EncounterStartDistance"
NotifyWithinDistanceAny({ Ids = { currentRun.Hero.ObjectId }, DestinationIds = encounterStartIds, Distance = startTriggerDistance, Notify = notifyName })
waitUntil( notifyName )
end
end
StartEncounter( currentRun, currentRoom, currentRoom.Encounter )
CheckInspectPoints( currentRun, currentRoom, currentRoom.Encounter )
StartTriggers( currentRoom, currentRoom.PostCombatDistanceTriggers )
end
function RestoreUnlockRoomExits( currentRun, currentRoom )
LifeOnKillRecord = {}
DamageRecord = {}
SpawnRecord = {}
HealthRecord = {}
DebugPrint({ Text = "RestoreUnlockRoomExits: "..currentRoom.Name.." (RunDepth = "..currentRun.RunDepthCache..")".." (BiomeDepth = "..currentRun.BiomeDepthCache..")".." (Seed = "..GetGlobalRng().seed..")" })
if currentRoom.CameraWalls then
CreateCameraWalls({ })
end
AddInputBlock({ Name = "RestoreUnlockRoomExits" })
SetupHeroObject( currentRun )
SwitchActiveUnit({ Id = currentRun.Hero.ObjectId })
SetupPreActivatedEnemies( currentRun )
DisableRoomTraps()
HandleSecretSpawns( currentRun )
StartRoomPreLoadBinks({
Run = currentRun,
Room = currentRoom,
Encounter = currentRoom.Encounter,
ChallengeEncounter = currentRoom.ChallengeEncounter
})
RestoreObjectStates( currentRoom )
-- Active ExitDoors
local exitDoorIds = GetInactiveIds({ Name = "ExitDoors" })
local firstDoor = true
while not IsEmpty( exitDoorIds ) do
local doorId = RemoveRandomValue( exitDoorIds )
if firstDoor or RandomChance( currentRun.CurrentRoom.ExtraDoorActivateChance or 1.0 ) then
Activate({ Id = doorId })
firstDoor = false
end
end
FadeOut({ Color = Color.Black, Duration = 0 })
SetupRoomArt( currentRun, currentRoom )
CheckDashOverride( currentRoom )
DestroyTraitUI()
wait(0.6) -- Let object restoration transitions finish before fade in
if currentRoom.RestorePresentationFunction ~= nil then
local restorePresentationFunction = _G[currentRoom.RestorePresentationFunction]
restorePresentationFunction( )
end
RestoreUnlockRoomExitsPresentation( currentRun, currentRoom )
--StartTriggers( currentRoom, currentRoom.DistanceTriggers )
--RunEvents( currentRoom )
--RunTraitThreads( currentRun.Hero )
RunUnthreadedEvents( currentRoom.PostCombatReloadEvents, currentRoom )
CheckInspectPoints( currentRun, currentRoom, currentRoom.Encounter )
StartTriggers( currentRoom, currentRoom.PostCombatDistanceTriggers )
wait(0.1)
DoUnlockRoomExits( currentRun, currentRoom )
Destroy({ Ids = GetIds({ Name = "WaterEndingObjectsDestroy" }) })
wait(0.1)
RemoveInputBlock({ Name = "RestoreUnlockRoomExits" })
if currentRoom.DisableWeapons then
DisableCombatControls()
else
EnableCombatControls()
end
if not currentRoom.HideCombatUI then
ShowCombatUI()
end
if GetNumMetaUpgrades("BiomeSpeedShrineUpgrade") > 0 then
thread( BiomeSpeedTimerLoop )
end
end
function LoadSpawnPackages( encounter )
for _, packageData in pairs(GetHeroTraitValues("LoadPackages")) do
if type(packageData) == "table" then
LoadPackages({ Names = packageData })
else
LoadPackages({ Name = packageData })
end
end
if encounter.Spawns == nil then
return
end
for k, spawnData in pairs( encounter.Spawns ) do
LoadPackages({ Name = spawnData.Name })
end
end
function SetupPreActivatedEnemies( currentRun )
for enemyName, enemyData in pairs( EnemyData ) do
local ids = GetIdsByType({ Name = enemyName })
for k, id in pairs( ids ) do
local newEnemy = DeepCopyTable( enemyData )
newEnemy.ObjectId = id
SetupEnemyObject( newEnemy, currentRun )
end
end
end
function SetupHeroObject( currentRun, applyLuaUpgrades )
local heroIds = GetIdsByType({ Name = "_PlayerUnit" })
DebugAssert({ Condition = #heroIds <= 1, Text = "Too many _PlayerUnit objects on map!" })
currentRun.Hero.ObjectId = heroIds[1]
AttachLua({ Id = currentRun.Hero.ObjectId, Table = currentRun.Hero})
AddToGroup({ Id = currentRun.Hero.ObjectId, Name = "HeroTeam" })
GatherAndEquipWeapons( currentRun )
-- Laurel Crown VFX
if currentRun.Hero.AttachedAnimationName ~= nil then
CreateAnimation({ Name = currentRun.Hero.AttachedAnimationName, DestinationId = currentRun.Hero.ObjectId })
end
-- Hero Light
if currentRun.Hero.AttachedLightName ~= nil and not currentRun.CurrentRoom.BlockHeroLight then
local heroGroup = GetGroupName({ Id = currentRun.Hero.ObjectId, DrawGroup = true })
local heroLightGroup = "HeroLight"
local heroLightId = SpawnObstacle({ Name = currentRun.Hero.AttachedLightName, DestinationId = currentRun.Hero.ObjectId, Group = heroLightGroup })
InsertGroupBehind({ Name = heroLightGroup, DestinationName = heroGroup })
SetScale({ Id = heroLightId, Fraction = currentRun.Hero.AttachedLightScale })
SetColor({ Id = heroLightId, Color = currentRun.Hero.AttachedLightColor })
Attach({ Id = heroLightId, DestinationId = currentRun.Hero.ObjectId })
currentRun.Hero.AttachedLightId = heroLightId
end
-- Clear per-room state dictionaries
CurrentRun.Hero.InvulnerableFlags = {}
CurrentRun.InvulnerableFlags = {}
CurrentRun.PhasingFlags = {}
-- Easy mode Check
if ConfigOptionCache.EasyMode then
if not HeroHasTrait( "GodModeTrait") then
AddTraitToHero({ TraitName = "GodModeTrait", SkipUIUpdate = true })
end
else
RemoveTrait( currentRun.Hero, "GodModeTrait" )
end
-- Build all upgrades.
UpdateHeroTraitDictionary()
ApplyMetaUpgrades( currentRun.Hero, applyLuaUpgrades )
ApplyTraitAutoRamp( currentRun.Hero )
ApplyTraitUpgrade( currentRun.Hero, applyLuaUpgrades )
ApplyTraitSetupFunctions( currentRun.Hero )
ApplyMetaModifierHeroUpgrades( currentRun.Hero, applyLuaUpgrades )
ApplyAllTraitWeapons( currentRun.Hero )
for k, trait in pairs( currentRun.Hero.Traits ) do
if trait.RoomCooldown ~= nil then
IncrementTraitCooldown( trait )
end
if trait.TimeCooldown ~= nil then
IncrementTraitCooldown( trait, trait.TimeCooldown)
end
end
-- Completes setup
SetHeroProperties( currentRun )
currentRun.Hero.PlayingVoiceLines = false
currentRun.Hero.QueuedVoiceLines = {}
currentRun.Hero.LastKillTime = nil
currentRun.Hero.StatusAnimation = nil
currentRun.Hero.PrevStatusAnimation = nil
currentRun.Hero.BlockStatusAnimations = nil
currentRun.Hero.FreezeInputKeys = {}
currentRun.Hero.DisableCombatControlsKeys = {}
currentRun.Hero.ActiveEffects = {}
currentRun.Hero.Frozen = false
currentRun.Hero.Mute = false
currentRun.Hero.Reloading = false
currentRun.Hero.KillStealVictimId = nil
currentRun.Hero.KillStolenFromId = nil
currentRun.Hero.ComboCount = 0
currentRun.Hero.ComboReady = false
currentRun.Hero.VacuumRush = false
currentRun.Hero.WeaponSpawns = nil
SetLightBarColor({ PlayerIndex = 1, Color = currentRun.Hero.LightBarColor or HeroData.DefaultHero.LightBarColor });
end
function GetEncounterBinks( currentEncounter, binksToPreload, preloadedEnemyTypes )
if currentEncounter == nil then
return
end
if currentEncounter.EnemySet and currentEncounter.LoadBinksFromEnemySet then
for k, enemyName in pairs( currentEncounter.EnemySet ) do
local enemy = EnemyData[enemyName]
if enemy ~= nil and preloadedEnemyTypes[enemyName] == nil then
if enemy.Binks ~= nil then
for i, binkName in ipairs( enemy.Binks ) do
table.insert( binksToPreload, binkName )
end
end
if enemy.OnDeathSpawnEncounter ~= nil then
local deathSpawnEncounter = EncounterData[enemy.OnDeathSpawnEncounter]
GetEncounterBinks( deathSpawnEncounter, binksToPreload, preloadedEnemyTypes )
end
preloadedEnemyTypes[enemyName] = true
end
end
end
if currentEncounter.SpawnWaves ~= nil then
for waveIndex, wave in pairs( currentEncounter.SpawnWaves ) do
for k, spawnEnemyArgs in pairs( wave.Spawns ) do
local enemyName = spawnEnemyArgs.Name or k or "?"
local enemy = EnemyData[enemyName]
if enemy ~= nil and preloadedEnemyTypes[enemyName] == nil then
if enemy.Binks ~= nil then
for i, binkName in ipairs( enemy.Binks ) do
table.insert( binksToPreload, binkName )
end
end
if enemy.OnDeathSpawnEncounter ~= nil then
local deathSpawnEncounter = EncounterData[enemy.OnDeathSpawnEncounter]
GetEncounterBinks( deathSpawnEncounter, binksToPreload, preloadedEnemyTypes )
end
preloadedEnemyTypes[enemyName] = true
end
end
end
end
if currentEncounter.Spawns ~= nil then
for index, spawnInfo in pairs( currentEncounter.Spawns ) do
local enemyData = EnemyData[spawnInfo.Name]
if enemyData ~= nil and preloadedEnemyTypes[enemyData.Name] == nil then
if enemyData.Binks ~= nil then
for i, binkName in ipairs( enemyData.Binks ) do
table.insert( binksToPreload, binkName )
end
end
if enemyData.OnDeathSpawnEncounter ~= nil then
local deathSpawnEncounter = EncounterData[enemy.OnDeathSpawnEncounter]
GetEncounterBinks( deathSpawnEncounter, binksToPreload, preloadedEnemyTypes )
end
preloadedEnemyTypes[enemyData.Name] = true
end
end
end
end
function StartRoomPreLoadBinks( args )
local currentRun = args.Run
local room = args.Room
local currentEncounter = args.Encounter
local challengeEncounter = args.ChallengeEncounter
local binksToPreload = ShallowCopyTable( currentRun.Hero.Binks )
local weaponBinksToPreload = ShallowCopyTable( currentRun.Hero.WeaponBinks )
local preloadedEnemyTypes = {}
local enemyName = nil
--DebugPrint({ Text = "Preloading binks for encounter:"..currentEncounter.Name })
-- Enemy binks
GetEncounterBinks( currentEncounter, binksToPreload, preloadedEnemyTypes )
GetEncounterBinks( challengeEncounter, binksToPreload, preloadedEnemyTypes )
-- Zagreus weapon binks
for weaponName, equipped in pairs( currentRun.Hero.Weapons ) do
if preloadedEnemyTypes[weaponName] == nil then
local weaponData = GetWeaponData( currentRun.Hero, weaponName )
local heroWeaponSetData = WeaponSets.HeroWeaponSets[weaponName]
if equipped and weaponData ~= nil and weaponData.Binks ~= nil then
for i, binkName in ipairs(weaponData.Binks) do
if not Contains( binksToPreload, binkName ) then
table.insert(binksToPreload, binkName)
end
end
end
if equipped and weaponData ~= nil and weaponData.WeaponBinks ~= nil then
for i, binkName in ipairs(weaponData.WeaponBinks) do
if not Contains( weaponBinksToPreload, binkName ) then
table.insert(weaponBinksToPreload, binkName)
end
end
end
preloadedEnemyTypes[weaponName] = true
end
end
for i, data in pairs( currentRun.Hero.Traits ) do
if data ~= nil and data.Binks ~= nil then
for j, binkName in ipairs(data.Binks) do
if not Contains( binksToPreload, binkName ) then
table.insert( binksToPreload, binkName )
end
end
end
end
if currentEncounter ~= nil and currentEncounter.Binks ~= nil then
for j, binkName in ipairs(currentEncounter.Binks) do
if not Contains( binksToPreload, binkName ) then
table.insert( binksToPreload, binkName )
end
end
end
if room ~= nil and room.Binks ~= nil then
for j, binkName in ipairs(room.Binks) do
if not Contains( binksToPreload, binkName ) then
table.insert( binksToPreload, binkName )
end
end
end
local dedupe = {}
local finalBinksToPreload = {}
for i, name in ipairs(binksToPreload) do
if dedupe[name] == nil then
dedupe[name] = name
table.insert(finalBinksToPreload, name)
end
end
dedupe = {}
local finalWeaponBinksToPreload = {}
for i, name in ipairs(weaponBinksToPreload) do
if dedupe[name] == nil then
dedupe[name] = name
table.insert(finalWeaponBinksToPreload, name)
end
end
-- for i, name in pairs(finalBinksToPreload) do
-- DebugPrint({ Text = "preload: "..name })
-- end
-- for i, name in pairs(finalWeaponBinksToPreload) do
-- DebugPrint({ Text = "WEP preload: "..name })
-- end
PreLoadBinks({ Names = finalBinksToPreload })
if room ~= nil and room.SkipWeaponBinkPreLoading then
DebugPrint({ Text = "StartRoomPreLoadBinks: Skip preloading weapon binks in "..room.Name })
else
PreLoadBinks({ Names = finalWeaponBinksToPreload, Cache = "WeaponCache" })
end
end
function BeginThanatosEncounter()
StartEncounterEffects( CurrentRun )
end
function BeginPerfectClearEncounter()
StartEncounterEffects( CurrentRun )
end
function BeginSurvivalEncounter()
StartEncounterEffects( CurrentRun )
end
function BeginWrappingEncounter()
StartEncounterEffects( CurrentRun )
end
function StartEncounter( currentRun, currentRoom, currentEncounter )
if CurrentRun.CurrentRoom.Encounter.EncounterType ~= "NonCombat" then
ShowSuperMeter()
if CurrentRun.CurrentRoom.Encounter == currentEncounter and currentEncounter ~= currentRoom.ChallengeEncounter and not CurrentRun.CurrentRoom.Encounter.DelayedStart then
StartEncounterEffects( currentRun )
end
end
if currentEncounter.DifficultyRating ~= nil and currentEncounter.EncounterType == "Default" then
DebugPrint({ Text = currentEncounter.Name })
DebugPrint({ Text = " Encounter Difficulty = "..currentEncounter.DifficultyRating })
for waveIndex, wave in pairs(currentEncounter.SpawnWaves) do
DebugPrint({ Text = " Wave #"..waveIndex })
if wave.DifficultyRating ~= nil then
DebugPrint({ Text = " Wave Difficulty = "..wave.DifficultyRating })
end
for k, spawnEnemyArgs in pairs(wave.Spawns) do
local enemyName = spawnEnemyArgs.Name or k or "?"
local enemyCount = spawnEnemyArgs.TotalCount or spawnEnemyArgs.CountMax or "?"
DebugPrint({ Text = " "..enemyName.." "..enemyCount })
end
end
end
currentEncounter.Completed = false
currentEncounter.InProgress = true
if currentEncounter.TimerBlock ~= nil then
AddTimerBlock( currentRun, currentEncounter.TimerBlock )
end
if CurrentRun.Hero.Health / CurrentRun.Hero.MaxHealth <= HealthUI.LowHealthThreshold and not currentRoom.HideLowHealthShroud then
HeroDamageLowHealthPresentation( true )
end
if CurrentRun.CurrentRoom.Encounter == currentEncounter and currentEncounter ~= currentRoom.ChallengeEncounter then
local goldPerRoom = round( GetTotalHeroTraitValue("MoneyPerRoom") )
if HasHeroTraitValue( "BlockMoney" ) then
goldPerRoom = 0
end
if goldPerRoom > 0 then
if not CurrentRun.CurrentRoom.HideEncounterText then
thread( PassiveGoldGainPresentation, goldPerRoom )
end
AddMoney( goldPerRoom, "Hermes Money Trait" )
end
end
StartTriggers( currentEncounter, currentEncounter.DistanceTriggers )
if currentEncounter.UnthreadedEvents == nil then
-- Event set for hand made encounters
currentEncounter.UnthreadedEvents = EncounterSets.EncounterEventsDefault
end
RunEvents( currentEncounter )
StartTriggers( currentEncounter, currentEncounter.PostCombatDistanceTriggers )
currentEncounter.Completed = true
currentRun.EncountersCompletedCache[currentEncounter.Name] = (currentRun.EncountersCompletedCache[currentEncounter.Name] or 0) + 1
GameState.EncountersCompletedCache[currentEncounter.Name] = (GameState.EncountersCompletedCache[currentEncounter.Name] or 0) + 1
-- Check for encounter-end effects
if currentEncounter and currentEncounter.StartTime and not currentEncounter.ClearTime then
currentEncounter.ClearTime = _worldTime - currentEncounter.StartTime
end
EndEncounterEffects( currentRun, currentRoom, currentEncounter )
if not currentEncounter.SkipDisableTrapsOnEnd then
DisableRoomTraps()
end
if currentEncounter ~= nil and currentEncounter.RemoveUpgradeOnEnd ~= nil then
RemoveEnemyUpgrade(currentEncounter.RemoveUpgradeOnEnd, CurrentRun)
end
-- Check for encoutner complete exit
wait( 0.2, RoomThreadName )
if CheckRoomExitsReady( currentRoom ) then
UnlockRoomExits( currentRun, currentRoom )
end
end
function GiveRandomConsumablesSource( source, args )
GiveRandomConsumables( args )
end
function GiveRandomConsumables( args )
args = args or {}
wait( args.Delay, RoomThreadName )
local multiplier = args.LootMultiplier or 1
local range = args.Range or 150
local minForce = args.MinForce or 75
local maxForce = args.MaxForce or 150
local destinationId = args.DestinationId or CurrentRun.Hero.ObjectId
for i, lootData in pairs(args.LootOptions) do
if lootData.Chance ~= nil and RandomChance(lootData.Chance * multiplier) then
local consumableId = SpawnObstacle({ Name = lootData.Name, DestinationId = destinationId, Group = "Standing", OffsetX = RandomFloat(-1 * range, range), OffsetY = RandomFloat(-1 * range, range), ForceToValidLocation = true })
local consumable = CreateConsumableItem( consumableId, lootData.Name, 0 )
if lootData.Overrides ~= nil then
for key, value in pairs( lootData.Overrides ) do
if consumable[key] ~= nil then
consumable[key] = value
end
end
end
ApplyConsumableItemResourceMultiplier( {}, consumable )
ExtractValues( CurrentRun.Hero, consumable, consumable )
if not args.NotRequiredPickup then
ActivatedObjects[consumable.ObjectId] = consumable
end
ApplyUpwardForce({ Id = consumableId, Speed = RandomFloat( 500, 700 ) })
ApplyForce({ Id = consumableId, Speed = RandomFloat( minForce, maxForce ), Angle = RandomFloat( 0, 360 ), SelfApplied = true })
SetObstacleProperty({ Property = "MagnetismWhileBlocked", Value = 0, DestinationId = consumable.ObjectId })
elseif lootData.MinAmount ~= nil and lootData.MaxAmount ~= nil then
local amount = RandomInt(lootData.MinAmount * multiplier , lootData.MaxAmount * multiplier)
if lootData.Name == "Money" then
thread( GushMoney, { Amount = amount, LocationId = destinationId, Source = "GiveRandomConsumables" } )
else
for i = 1, amount do
local consumableId = SpawnObstacle({ Name = lootData.Name, DestinationId = destinationId, Group = "Standing", OffsetX = RandomFloat(-1 * range, range), OffsetY = RandomFloat(-1 * range, range), ForceToValidLocation = true })
local consumable = CreateConsumableItem( consumableId, lootData.Name, 0 )
if lootData.Overrides ~= nil then
for key, value in pairs( lootData.Overrides ) do
if consumable[key] ~= nil then
consumable[key] = value
end
end
-- To match CreateConsumableItemFromData multipliers
if consumable.AddResources ~= nil and consumable.AddResources.MetaPoints ~= nil then
consumable.AddResources.MetaPoints = round( consumable.AddResources.MetaPoints * CalculateMetaPointMultiplier() )
end
if consumable.AddResources ~= nil and consumable.AddResources.Gems ~= nil then
consumable.AddResources.Gems = round( consumable.AddResources.Gems * GetTotalHeroTraitValue( "GemMultiplier", { IsMultiplier = true } ))
end
end
ApplyConsumableItemResourceMultiplier( {}, consumable )
ExtractValues( CurrentRun.Hero, consumable, consumable )
if not args.NotRequiredPickup then
ActivatedObjects[consumable.ObjectId] = consumable
end
ApplyUpwardForce({ Id = consumableId, Speed = RandomFloat( args.UpwardForceMin or 500, args.UpwardForceMax or 700 ) })
ApplyForce({ Id = consumableId, Speed = RandomFloat( args.ForceMin or 75, args.ForceMax or 150 ), Angle = RandomFloat( 0, 360 ), SelfApplied = true })
SetObstacleProperty({ Property = "MagnetismWhileBlocked", Value = 0, DestinationId = consumable.ObjectId })
end
end
end
end
end
function GiveLoot( args )
local lootData = ChooseLoot( { args.ExcludeLootName }, args.ForceLootName )
if lootData ~= nil then
if not args.BoughtFromShop then
for k, trait in pairs( CurrentRun.Hero.Traits ) do
if trait.ForceBoonName == lootData.Name then
trait.Uses = trait.Uses - 1
end
end
end
local loot = CreateLoot(MergeTables({ Name = lootData.Name }, args))
return loot
end
end
function CreateStackLoot( args )
args = args or {}
if args.StackNum == nil then
args.StackNum = 1
end
if CurrentRun ~= nil and CurrentRun.CurrentRoom ~= nil and CurrentRun.CurrentRoom.StackNumOverride then
args.StackNum = CurrentRun.CurrentRoom.StackNumOverride
end
return CreateLoot( MergeTables( args, { Name = "StackUpgrade" } ) )
end
function CreateWeaponLoot( args )
args = args or {}
return CreateLoot( MergeTables( args, { Name = "WeaponUpgrade" } ) )
end
function CreateHermesLoot( args )
args = args or {}
return CreateLoot( MergeTables( args, { Name = "HermesUpgrade" } ) )
end
function IsRarityForcedCommon( name )
if CurrentRun.CurrentRoom.ForceCommonLootFirstRun and GetCompletedRuns() == 0 then
return true
end
local referencedTable = "BoonData"
if name == "StackUpgrade" then
referencedTable = "StackData"
elseif name == "WeaponUpgrade" then
referencedTable = "WeaponData"
end
if CurrentRun.Hero[referencedTable] ~= nil and CurrentRun.Hero[referencedTable].AllowRarityOverride and CurrentRun.CurrentRoom.BoonRaritiesOverride then
return false
end
if CurrentRun.Hero[referencedTable] == nil or CurrentRun.Hero[referencedTable].ForceCommon then
return true
end
return false
end
function GetRarityChances( args )
local name = args.Name
local ignoreTempRarityBonus = args.IgnoreTempRarityBonus
local referencedTable = "BoonData"
if name == "StackUpgrade" then
referencedTable = "StackData"
elseif name == "WeaponUpgrade" then
referencedTable = "WeaponData"
elseif name == "HermesUpgrade" then
referencedTable = "HermesData"
end
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 CurrentRun.CurrentRoom.BoonRaritiesOverride then
legendaryRoll = CurrentRun.CurrentRoom.BoonRaritiesOverride.LegendaryChance or legendaryRoll
heroicRoll = CurrentRun.CurrentRoom.BoonRaritiesOverride.HeroicChance or heroicRoll
epicRoll = CurrentRun.CurrentRoom.BoonRaritiesOverride.EpicChance or epicRoll
rareRoll = CurrentRun.CurrentRoom.BoonRaritiesOverride.RareChance or rareRoll
elseif args.BoonRaritiesOverride then
legendaryRoll = args.BoonRaritiesOverride.LegendaryChance or legendaryRoll
heroicRoll = args.BoonRaritiesOverride.HeroicChance or heroicRoll
epicRoll = args.BoonRaritiesOverride.EpicChance or epicRoll
rareRoll = args.BoonRaritiesOverride.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 = ignoreTempRarityBonus })
for i, rarityTraitData in pairs(rarityTraits) do
if rarityTraitData.RequiredGod == nil or rarityTraitData.RequiredGod == name 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 AllAtLeastRarity( loot, baseRarity )
if IsEmpty(loot.UpgradeOptions) then
return false
end
for i, traitData in pairs( loot.UpgradeOptions ) do
if GetRarityValue( traitData.Rarity ) < GetRarityValue( baseRarity ) then
return false
end
end
return true
end
function HasAtLeastRarity( loot, baseRarity )
if IsEmpty(loot.UpgradeOptions) then
return false
end
for i, traitData in pairs( loot.UpgradeOptions ) do
if GetRarityValue( traitData.Rarity ) >= GetRarityValue( baseRarity ) then
return true
end
end
return false
end
function HasTraitOnLoot( loot, traitName )
for i, traitData in pairs( loot.UpgradeOptions ) do
if traitData.ItemName == traitName then
return true
end
end
return false
end
function HasExchangeOnLoot( loot )
if loot == nil or loot.UpgradeOptions == nil then
return
end
for i, traitData in pairs( loot.UpgradeOptions ) do
if traitData.TraitToReplace ~= nil then
return true
end
end
return false
end
function HasTraitsOnLoot( loot, traitNames )
if loot == nil or loot.UpgradeOptions == nil then
return false
end
for i, traitData in pairs( loot.UpgradeOptions ) do
if Contains(traitNames, traitData.ItemName) then
return true
end
end
return false
end
function CreateLoot( args )
RandomSynchronize()
local lootPoint = args.SpawnPoint or SelectLootSpawnPoint(CurrentRun.CurrentRoom)
if lootPoint == nil then
DebugAssert({ Text = "GiveLoot: trying to give loot but there are no eligible LootPoint objects in room." })
end
local lootData = args.LootData or LootData[args.Name]
local loot = DeepCopyTable( lootData )
local lootId = SpawnObstacle({ Name = lootData.Name, DestinationId = lootPoint, Group = "Standing", OffsetX = args.OffsetX, OffsetY = args.OffsetY })
loot.ObjectId = lootId
loot.BlockExitText = "ExitBlockedByBoon"
loot.BoughtFromShop = args.BoughtFromShop
loot.StackNum = args.StackNum
loot.ExchangeOnlyFromLootName = args.ExchangeOnlyFromLootName
if args.AddBoostedAnimation then
CreateAnimation({ DestinationId = loot.ObjectId, Name = "RoomRewardAvailableRareSparkles" })
end
if args.SuppressFlares then
StopAnimation({ DestinationId = loot.ObjectId, Names = { "BoonOrbSpawn", "BoonOrbSpawn2", "PickupFlare", "PickupFlareA", "PickupFlareA2", "PickupFlareB01", "PickupFlareB02" }, })
end
if IsRarityForcedCommon( args.Name ) or args.ForceCommon then
loot.RarityChances = {}
loot.ForceCommon = true
else
loot.IgnoreTempRarityBonus = args.IgnoreTempRarityBonus
loot.BoonRaritiesOverride = ShallowCopyTable( args.BoonRaritiesOverride )
loot.RarityChances = GetRarityChances( loot )
if not args.IgnoreTempRarityBonus then
loot.RarityBoosted = true
end
end
SetTraitsOnLoot( loot )
AddToGroup({ Id = lootId, Name = "Loot" })
loot.MenuNotify = UIData.BoonMenuId
if not args.DoesNotBlockExit then
ActivatedObjects[lootId] = loot
end
LootObjects[lootId] = loot
local lootId = loot.ObjectId
AttachLua({ Id = lootId, Table = loot })
SetColor({ Id = lootId, Color = loot.LootColor, Duration = 0 })
if not args.SuppressSpawnSounds then
thread( PlayRandomEligibleVoiceLines, { loot.OnSpawnVoiceLines, GlobalVoiceLines.LootGrantedVoiceLines } )
PlaySound({ Name = loot.SpawnSound or "/Leftovers/SFX/AnnouncementPing4", Id = lootId })
end
if args.Cost ~= nil and args.Cost > 0 then
loot.Cost = args.Cost
local costMultiplier = 1 + ( GetNumMetaUpgrades( "ShopPricesShrineUpgrade" ) * ( MetaUpgradeData.ShopPricesShrineUpgrade.ChangeValue - 1 ) )
costMultiplier = costMultiplier * GetTotalHeroTraitValue("StoreCostMultiplier", {IsMultiplier = true})
if costMultiplier ~= 1 then
loot.Cost = round( loot.Cost * costMultiplier )
end
UpdateCostText( loot )
end
return loot
end
function SelectLootSpawnPoint(currentRoom)
if currentRoom.SpawnPoints.Loot == nil or IsEmpty(currentRoom.SpawnPoints.Loot) then
currentRoom.SpawnPoints.Loot = GetIdsByType({ Name = "LootPoint" })
end
if currentRoom.SpawnPoints.Loot == nil or IsEmpty(currentRoom.SpawnPoints.Loot) then
return SelectRoomRewardSpawnPoint( currentRoom )
end
return RemoveRandomValue(currentRoom.SpawnPoints.Loot)
end
function SelectRoomRewardSpawnPoint( currentRoom )
if currentRoom.SpawnRewardOnId then
return currentRoom.SpawnRewardOnId
end
local spawnPointId = GetClosest({ Id = CurrentRun.Hero.ObjectId, DestinationNames = "SpawnPoints" })
if spawnPointId <= 0 then
if currentRoom.SpawnPoints.Loot == nil or IsEmpty(currentRoom.SpawnPoints.Loot) then
currentRoom.SpawnPoints.Loot = GetIdsByType({ Name = "LootPoint" })
end
spawnPointId = RemoveRandomValue(currentRoom.SpawnPoints.Loot) or -1
end
DebugAssert({ Condition = spawnPointId > 0, Text = "No spawn point found for RoomReward!" })
return spawnPointId
end
function CheckMoneyDrop( currentRun, currentRoom, enemy, moneyDropData )
if enemy == nil or moneyDropData == nil then
return
end
if enemy.CharacterInteraction ~= nil or enemy.CharacterInteractions ~= nil then
return
end
if moneyDropData.Chance == nil or not RandomChance( moneyDropData.Chance ) then
return
end
if HasHeroTraitValue( "BlockMoney" ) then
return
end
local moneyMultiplier = GetTotalHeroTraitValue( "MoneyMultiplier", { IsMultiplier = true } )
for key, upgradeName in pairs( CurrentRun.EnemyUpgrades ) do
local upgradeData = EnemyUpgradeData[upgradeName]
if upgradeData.MoneyMultiplierDelta then
moneyMultiplier = moneyMultiplier + (upgradeData.MoneyMultiplier - 1)
end
end
local currentEncounter = currentRoom.Encounter
if currentEncounter ~= nil and currentEncounter.MoneyDropStore and not moneyDropData.IgnoreRoomMoneyStore then
if moneyDropData.MinParcels and moneyDropData.MaxParcels then
local numDrops = RandomInt( moneyDropData.MinParcels, moneyDropData.MaxParcels )
while( currentEncounter.MoneyDropStore > 0 and numDrops > 0 ) do
local amount = RandomInt( moneyDropData.MinValue, moneyDropData.MaxValue ) or 1
amount = amount * moneyMultiplier
amount = round( amount )
if amount <= 0 then
amount = 1
end
DropMoney( amount, { LocationId = enemy.ObjectId, Radius = moneyDropData.Radius, Source = enemy.Name } )
numDrops = numDrops - amount
currentEncounter.MoneyDropStore = currentEncounter.MoneyDropStore - amount
--DebugPrint({ Text = "Money Store: "..tostring( currentEncounter.MoneyDropStore ) })
if currentEncounter.MoneyDropStore <= 0 then
--DebugPrint({ Text = "Money Store Maxed!" })
end
end
end
else
local numDropParcels = RandomInt( moneyDropData.MinParcels, moneyDropData.MaxParcels )
for index = 1, numDropParcels, 1 do
local amount = RandomInt( moneyDropData.MinValue, moneyDropData.MaxValue ) or 1
amount = amount * moneyMultiplier
amount = round( amount )
DropMoney( amount, { LocationId = enemy.ObjectId, Radius = moneyDropData.Radius, Source = enemy.Name } )
--DebugPrint({ Text = "Money Other: "..tostring( amount ) })
end
end
if enemy.MoneyDropGlobalVoiceLines ~= nil then
thread( PlayVoiceLines, GlobalVoiceLines[enemy.MoneyDropGlobalVoiceLines], true )
end
end
function GushConsumables( args )
if args == nil then
args = {}
end
local targetId = args.TargetId or CurrentRun.Hero.ObjectId
local generatedSpawnId = nil
if args.LocationId then
generatedSpawnId = SpawnObstacle({ Name = "BlankObstacle", DestinationId = targetId, Group = "Standing" })
targetId = generatedSpawnId
end
for k = 1, args.Amount or 1 do
local consumableId = SpawnObstacle({ Name = args.ConsumableName, DestinationId = targetId, Group = "Standing", ForceToValidLocation = true, })
local consumable = CreateConsumableItem( consumableId, args.ConsumableName )
ApplyUpwardForce({ Id = consumableId, Speed = args.UpwardSpeed or RandomFloat( args.UpwardSpeedMin or 500, args.UpwardSpeedMax or 700 ) })
local speed = args.Speed or RandomFloat( 75, 560 )
if args.MinSpeed and args.MaxSpeed then
speed = RandomFloat( args.MinSpeed, args.MaxSpeed )
end
ApplyForce({ Id = consumableId, Speed = speed, Angle = args.Angle or RandomFloat( 0, 360 ), SelfApplied = true })
wait( args.Interval )
end
if generatedSpawnId then
Destroy({ Id = generatedSpawnId })
end
end
function CheckAmmoDrop( currentRun, targetId, ammoDropData, numDrops )
if ammoDropData == nil then
return
end
if ammoDropData.Count == nil or ammoDropData.Count <= 0 or numDrops == 0 then
return
end
if ammoDropData.Chance ~= nil and not RandomChance( ammoDropData.Chance ) then
return
end
if ammoDropData.LocationX ~= nil then
targetId = nil
end
if numDrops == nil then
numDrops = ammoDropData.Count
end
local consumableName = "AmmoPack"
for i = 1, numDrops do
ammoDropData.Count = ammoDropData.Count - 1
end
if IsMetaUpgradeActive("ReloadAmmoMetaUpgrade") then
return
end
for i = 1, numDrops do
local offset = {}
if ammoDropData.Angle ~= nil then
offset = CalcOffset( math.rad(ammoDropData.Angle + 180), 48 )
end
local consumableId = SpawnObstacle({ Name = consumableName, DestinationId = targetId, LocationX = ammoDropData.LocationX, LocationY = ammoDropData.LocationY, OffsetX = offset.X, OffsetY = offset.Y, Group = "Standing" })
local consumable = CreateConsumableItem( consumableId, consumableName )
consumable.AddAmmo = 1
ApplyUpwardForce({ Id = consumableId, Speed = RandomFloat( ammoDropData.UpwardForceMin or 500, ammoDropData.UpwardForceMax or 700 ) })
if ammoDropData.ForceMax ~= nil then
ApplyForce({ Id = consumableId, Speed = RandomFloat( ammoDropData.ForceMin, ammoDropData.ForceMax ), Angle = ammoDropData.Angle or RandomFloat( 0, 360 ), SelfApplied = true })
end
local delay = GetTotalHeroTraitValue("AmmoDropUseDelay")
if delay > 0 then
SetInteractProperty({ DestinationId = consumableId, Property = "Cooldown", Value = delay })
thread( DoUseDelay, consumableId, delay )
end
for i, data in pairs(GetHeroTraitValues("AmmoFieldWeapon")) do
thread( FireAmmoWeapon, consumableId, data )
end
thread( EscalateMagnetism, consumable )
end
end
function DoUseDelay( id, delay )
local fadeInTime = math.min( delay, 0.25 )
SetAlpha({ Id = id, Fraction = 0.5, Duration = 0 })
wait( delay - fadeInTime)
SetAlpha({ Id = id, Fraction = 1, Duration = fadeInTime })
end
function EscalateMagnetism( consumable )
if consumable.MagnetismEscalateDelay == nil then
return
end
wait( consumable.MagnetismEscalateDelay - consumable.MagnetismHintRemainingTime , RoomThreadName )
if not IsAlive({ Id = consumable.ObjectId }) then
return
end
CreateAnimation({ Name = "AmmoReturnTimer", DestinationId = consumable.ObjectId })
wait( consumable.MagnetismHintRemainingTime, RoomThreadName )
SetObstacleProperty({ Property = "Magnetism", Value = consumable.MagnetismEscalateAmount, DestinationId = consumable.ObjectId })
end
function RecordObjectState( room, id, property, value )
if room.ObjectStates == nil then
room.ObjectStates = {}
end
if room.ObjectStates[id] == nil then
room.ObjectStates[id] = {}
end
room.ObjectStates[id][property] = value
end
function RestoreObjectStates( room )
if room.ObjectStates == nil then
return
end
for id, objectState in pairs( room.ObjectStates ) do
local unit = ActiveEnemies[id]
if unit ~= nil then
for property, value in pairs( objectState ) do
unit[property] = value
end
end
if objectState.Animation ~= nil then
SetAnimation({ DestinationId = id, Name = objectState.Animation })
end
if objectState.FlipHorizontal then
FlipHorizontal({ Id = id })
end
if objectState.Location ~= nil then
Teleport({ Id = id, OffsetX = objectState.Location.X, OffsetY = objectState.Location.Y })
end
if objectState.Angle ~= nil then
SetGoalAngle({ Id = id, Angle = objectState.Angle, CompleteAngle = true })
end
if objectState.Destroyed then
SetThingProperty({ DestinationId = id, Property = "SuppressSounds", Value = true, DataValue = false })
SetUnitProperty({ DestinationId = id, Property = "OnDeathWeapon", Value = nil, DataValue = true })
Destroy({ Id = id })
else
if objectState.SwapData and ObstacleData[objectState.SwapData] then
local newData = ObstacleData[objectState.SwapData]
local newObject = DeepCopyTable( newData )
newObject.ObjectId = id
AttachLua({ Id = id, Table = newObject })
if newObject.SpawnPropertyChanges ~= nil then
ApplyUnitPropertyChanges( newObject, newObject.SpawnPropertyChanges, true )
end
end
end
end
end
function RandomizeBreakables( currentRoom )
local breakableIds = GetIds({ Name = "Breakables" })
for k, id in pairs( breakableIds ) do
local breakableName = GetRandomValue(currentRoom.BreakableOptions)
if type(breakableName) == "string" then
SetAnimation({ DestinationId = id, Name = breakableName })
RecordObjectState( currentRoom, id, "Animation", breakableName )
if CoinFlip() then
FlipHorizontal({ Id = id })
RecordObjectState( currentRoom, id, "FlipHorizontal", true )
end
end
end
end
function HandleBreakableSwap( currentRoom )
local roomBreakableData = currentRoom.BreakableValueOptions
local legalBreakables = FindAllSwappableBreakables()
local highValueLimit = roomBreakableData.MaxHighValueBreakables or 1
if IsEmpty( legalBreakables ) then
return
end
if TableLength( legalBreakables ) < highValueLimit then
highValueLimit = TableLength( legalBreakables )
end
local chanceMultiplier = 1.0
for k, mutator in pairs( GameState.ActiveMutators ) do
if mutator.BreakableChanceMultiplier ~= nil then
chanceMultiplier = chanceMultiplier * mutator.BreakableChanceMultiplier
end
end
for index = 0, highValueLimit, 1 do
local breakableData = RemoveRandomValue( legalBreakables )
if breakableData == nil then
return
end
local valueOptions = breakableData.ValueOptions
for k, swapOption in ipairs( valueOptions ) do
if swapOption.GameStateRequirements == nil or IsGameStateEligible( CurrentRun, swapOption, swapOption.GameStateRequirements ) then
if RandomChance( swapOption.Chance * chanceMultiplier ) then
SetAnimation({ DestinationId = breakableData.ObjectId, Name = swapOption.Animation, OffsetY = swapOption.OffsetY or 0 })
RecordObjectState( currentRoom, breakableData.ObjectId, "Animation", swapOption.Animation )
breakableData.MoneyDropOnDeath = swapOption.MoneyDropOnDeath
RecordObjectState( currentRoom, breakableData.ObjectId, "MoneyDropOnDeath", breakableData.MoneyDropOnDeath )
DebugPrint({ Text = "HandleBreakableSwap: an up-valued breakable has spawned somewhere in this room." })
OverwriteTableKeys(breakableData, swapOption.DataOverrides)
break
end
end
end
end
end
function FindAllSwappableBreakables()
local legalBreakables = { }
for id, enemy in pairs( ActiveEnemies ) do
if enemy.ValueOptions ~= nil then
legalBreakables[id] = enemy
end
end
return legalBreakables
end
MoneyObjects = { }
function DropMoney( amount, args )
if amount == nil then
return
end
if args.LocationId == nil then
args.LocationId = CurrentRun.Hero.ObjectId
end
args.Radius = args.Radius or 25
local offset = CalcOffset( RandomFloat( 0, 360 ), args.Radius )
if args.Offset ~= nil then
offset.X = offset.X + args.Offset.X
offset.Y = offset.Y + args.Offset.Y
end
if args.OffsetZ ~= nil then
offset.Z = args.OffsetZ
else
offset.Z = 0
end
local moneySize = "MoneySmall"
if amount < 10 then
moneySize = "MoneySmall"
elseif amount < 25 then
moneySize = "MoneyMedium"
else
moneySize = "MoneyLarge"
end
local moneyDropId = SpawnObstacle({ Name = moneySize, DestinationId = args.LocationId, OffsetX = offset.X, OffsetY = offset.Y, OffsetZ = offset.Z, Group = "Standing", TriggerOnSpawn = false, })
MoneyObjects[moneyDropId] = { Amount = amount, Source = args.Source }
PlaySound({ Name = "/Leftovers/Menu Sounds/CoinFlash", Id = moneyDropId, ManagerCap = 28 })
end
OnUsed{ "Money MoneySmall MoneyMedium MoneyLarge",
function( triggerArgs )
if MoneyObjects[triggerArgs.triggeredById] == nil then
DebugAssert({ Condition = false, "Picked up money with no source!" })
end
local money = MoneyObjects[triggerArgs.triggeredById]
AddMoney( money.Amount, money.Source )
MoneyObjects[triggerArgs.triggeredById] = nil
end
}
function SpendMoney( amount, source )
if amount == nil or amount <= 0 then
return
end
CurrentRun.Money = CurrentRun.Money - amount
CurrentRun.MoneySpent = CurrentRun.MoneySpent + amount
CurrentRun.MoneyRecord[source] = (CurrentRun.MoneyRecord[source] or 0) - amount
GameState.LifetimeResourcesSpent.Money = (GameState.LifetimeResourcesSpent.Money or 0) + amount
CreateAnimation({ Name = "SpendMoneyFx", DestinationId = CurrentRun.Hero.ObjectId })
thread( UpdateMoneyUI, CurrentRun.Money )
end
function ZeroSuperMeter()
CurrentRun.Hero.SuperMeter = 0
StopSuper()
end
function ZeroMoney()
CurrentRun.Money = 0
thread( UpdateMoneyUI, CurrentRun.Money )
end
function ClearUpgrades()
CurrentRun.Hero.RecentTraits = {}
CurrentRun.Hero.Traits = {}
CurrentRun.Hero.OnFireWeapons = {}
CurrentRun.Hero.OnSlamWeapons = {}
CurrentRun.Hero.OnDamageWeapons = {}
CurrentRun.Hero.OnKillWeapons = {}
CurrentRun.Hero.LastStands = {}
CurrentRun.Hero.WeaponDataOverride = nil
if CurrentRun.Hero.OutgoingDamageModifiers ~= nil then
for i, modifier in pairs( CurrentRun.Hero.OutgoingDamageModifiers ) do
if modifier.Name and MetaUpgradeData[modifier.Name] == nil then
CurrentRun.Hero.OutgoingDamageModifiers[i] = nil
end
end
end
UpdateNumHiddenTraits()
end
function AddMoney( amount, source )
if amount == nil or round( amount ) <= 0 then
return
end
amount = round( amount )
if CurrentRun.Money == nil then
CurrentRun.Money = 0
end
local sound = "/SFX/GoldCoinPickup"
if HasHeroTraitValue("BlockMoney") then
amount = 0
sound = "/SFX/Player Sounds/DarknessCollectionPickupSMALL"
end
if not CurrentRun.CurrentRoom.HideGoldGainFx then
PlaySound({ Name = sound, ManagerCap = 28 })
end
CurrentRun.Money = CurrentRun.Money + amount
CurrentRun.MoneyRecord[source] = (CurrentRun.MoneyRecord[source] or 0) + amount
GameState.LifetimeResourcesGained.Money = (GameState.LifetimeResourcesGained.Money or 0) + amount
ShowResourceUIs({ CombatOnly = not CurrentRun.Hero.IsDead, UpdateIfShowing = true })
UpdateMoneyUI( CurrentRun.Money )
end
function ProcessInterest( eventSource, args )
if not IsMetaUpgradeActive("InterestMetaUpgrade") then
return
end
args = args or {}
local waitDelay = args.StartDelay or 0
local goldPerRoom = round( GetTotalHeroTraitValue("MoneyPerRoom") )
if HasHeroTraitValue( "BlockMoney" ) then
goldPerRoom = 0
end
if goldPerRoom > 0 then
waitDelay = waitDelay + 0.5
end
wait( waitDelay)
local interest = math.ceil(CurrentRun.Money * ( GetTotalMetaUpgradeChangeValue("InterestMetaUpgrade") - 1 ))
AddMoney( interest, "InterestMetaupgrade")
thread( InCombatText, CurrentRun.Hero.ObjectId, "InterestGainedCombatText", 1.5 , { LuaKey = "TempTextData", LuaValue = { Amount = interest }})
end
function AddRerolls( amount, source, args )
args = args or {}
if args.Thread then
args.Thread = false
thread( AddRerolls, amount, source, args )
return
end
wait( args.Delay )
CurrentRun.NumRerolls = (CurrentRun.NumRerolls or 0) + amount
if CurrentRun.NumRerolls > 0 then
ShowResourceUIs({ CombatOnly = not CurrentRun.Hero.IsDead, UpdateIfShowing = true })
end
UpdateRerollUI( CurrentRun.NumRerolls )
thread( PopOverheadText, { Amount = amount, Text = "RerollAmount", Color = Color.White } )
end
function OnMetaPointsAdded( name, amount, source, args )
args = args or {}
GameState.AccumulatedMetaPointsCache = GameState.LifetimeResourcesGained.MetaPoints or 0
local healMultiplier = GetTotalHeroTraitValue("MetaPointHealMultiplier") + ( GetTotalMetaUpgradeChangeValue("DarknessHealMetaUpgrade") - 1 )
healMultiplier = healMultiplier * CalculateHealingMultiplier() * GetTotalHeroTraitValue("MaxHealthMultiplier", { IsMultiplier = true })
if healMultiplier > 0 and round( healMultiplier * amount ) > 0 then
thread( DelayedHeal, 0.5, round( healMultiplier * amount ), "MetaPointHeal" )
end
end
function AddResource( name, amount, source, args )
if amount == nil or amount == 0 then
return
end
args = args or {}
local resourceData = ResourceData[name]
GameState.Resources[name] = (GameState.Resources[name] or 0) + round( amount )
if not args.NoLifetimeEffect then
GameState.LifetimeResourcesGained[name] = (GameState.LifetimeResourcesGained[name] or 0) + round( amount )
end
if resourceData ~= nil and resourceData.OnAddedFunctionName ~= nil then
local onAddedFunction = _G[resourceData.OnAddedFunctionName]
if onAddedFunction ~= nil then
onAddedFunction( name, amount, source, args )
end
end
ShowResourceUIs({ CombatOnly = not CurrentRun.Hero.IsDead, ForceShowName = name })
UpdateResourceUI( name, GameState.Resources[name] )
if not args.Silent then
if not args.SkipOverheadText and ResourceData[name] and ResourceData[name].GainedText then
local resourceColor = Color.White
if ResourceData[name].Color then
resourceColor = ResourceData[name].Color
end
thread( PopOverheadText, { Amount = amount, Text = ResourceData[name].GainedText, Color = resourceColor } )
end
thread( ResourceGainPresentation, name, amount )
end
end
function SpendResource( name, amount, source, args )
if GameState.Resources[name] == nil or GameState.Resources[name] < amount then
return false
end
args = args or {}
GameState.Resources[name] = (GameState.Resources[name] or 0) - round( amount )
if not args.NoLifetimeEffect then
GameState.LifetimeResourcesSpent[name] = (GameState.LifetimeResourcesSpent[name] or 0) + round( amount )
end
if not args.SkipUpdateResourceUI then
ShowResourceUIs({ CombatOnly = not CurrentRun.Hero.IsDead, UpdateIfShowing = true })
UpdateResourceUI( name, GameState.Resources[name] )
end
if not args.Silent then
if not args.SkipOverheadText then
local text = ResourceData[name].SpendText or "SpendGenericResource"
local color = ResourceData[name].Color or Color.White
thread( PopOverheadText, { Amount = amount, Text = text, Color = color } )
end
thread( ResourceGainPresentation, name, amount * -1 )
end
return true
end
function HasResource( name, amount )
if amount == 0 then
return true
end
amount = amount or 1
if GameState.Resources[name] == nil or GameState.Resources[name] < amount then
return false
end
return true
end
function HasResources( name, args )
if not args then
return true
end
DebugAssert({ Condition = ( type(args) == "table" ), Text = " Has Resources called with non-table value " .. tostring(args) })
for resourceName, amount in pairs(args) do
if not HasResource( resourceName, amount ) then
return false
end
end
return true
end
function OnGiftPointsAdded( name, amount, source, args )
args = args or {}
if amount > 0 then
local healthGained = GetTotalHeroTraitValue("GiftPointHealthMultiplier")
if healthGained > 0 then
AddMaxHealth( healthGained, "DionysusMaxHealthTrait" )
end
end
if not IsEmpty( ActiveEnemies ) then
for id, unit in pairs( ActiveEnemies ) do
if not unit.PostCombatTravel then
SetAvailableUseText( unit )
end
end
end
end
function AddMaxHealth( healthGained, source, args )
args = args or {}
if args.Thread then
args.Thread = false
thread( AddMaxHealth, healthGained, source, args )
return
end
local startingHealth = CurrentRun.Hero.MaxHealth
wait( args.Delay )
healthGained = round(healthGained)
local traitName = "RoomRewardMaxHealthTrait"
if args.NoHealing then
traitName = "RoomRewardEmptyMaxHealthTrait"
end
local healthTraitData = GetProcessedTraitData({ Unit = CurrentRun.Hero, TraitName = traitName })
healthTraitData.PropertyChanges[1].ChangeValue = healthGained
AddTraitToHero({ TraitData = healthTraitData })
healthGained = round(healthGained * GetTotalHeroTraitValue("MaxHealthMultiplier", { IsMultiplier = true }))
if not( args.Silent ) then
MaxHealthIncreaseText({ MaxHealthGained = CurrentRun.Hero.MaxHealth - startingHealth , SpecialText = "MaxHealthIncrease" })
end
end
function OnLockKeysAdded( name, amount, source, args )
args = args or {}
if amount > 0 then
thread( UpdateWeaponKits )
if not CurrentRun.Hero.IsDead and CurrentRun.CurrentRoom ~= nil and CurrentRun.CurrentRoom.ChallengeSwitch ~= nil then
thread( UpdateLockedKeyPresentation, CurrentRun.CurrentRoom.ChallengeSwitch )
end
end
end
function StartEncounterEffects( currentRun )
BuildSuperMeter( currentRun, GetTotalHeroTraitValue("StartingSuperAmount"))
currentRun.CurrentRoom.Encounter.StartTime = _worldTime
CurrentRun.Hero.HitShields = 0
if not currentRun.CurrentRoom.BlockClearRewards then
for i, traitData in pairs( currentRun.Hero.Traits) do
if traitData.PerfectClearDamageBonus then
PerfectClearTraitStartPresentation( traitData )
end
if traitData.FastClearDodgeBonus then
SetupDodgeBonus( currentRun.CurrentRoom.Encounter, traitData )
end
if traitData.EncounterStartWeapon then
FireWeaponFromUnit({ Weapon = traitData.EncounterStartWeapon, Id = CurrentRun.Hero.ObjectId, DestinationId = CurrentRun.Hero.ObjectId })
end
if traitData.GoldBonusDrop then
TraitUIActivateTrait( traitData )
end
if traitData.BossEncounterShieldHits then
if currentRun.CurrentRoom.Encounter.EncounterType == "Boss" then
CurrentRun.Hero.HitShields = traitData.BossEncounterShieldHits
ApplyEffectFromWeapon({ WeaponName = "EurydiceDefenseApplicator", EffectName = "EurydiceDefenseEffect", Id = CurrentRun.Hero.ObjectId, DestinationId = CurrentRun.Hero.ObjectId })
UpdateTraitNumber( traitData )
end
end
end
end
end
function EndEncounterEffects( currentRun, currentRoom, currentEncounter )
if currentEncounter == nil or currentEncounter.EncounterType == "NonCombat" then
return
end
CurrentRun.Hero.HeroTraitValuesCache = {}
if currentEncounter == currentRoom.Encounter or currentEncounter == currentRoom.ChallengeEncounter then
thread( PostEncounterDrainSuper )
StopSuper()
ClearEffect({ Id = currentRun.Hero.ObjectId, Name = "KillDamageBonus"})
if currentRoom.DestroyAssistUnitOnEncounterEndId then
local assistUnit = ActiveEnemies[currentRoom.DestroyAssistUnitOnEncounterEndId]
if assistUnit ~= nil then
Kill( assistUnit )
end
end
if currentRoom.DestroyAssistProjectilesOnEncounterEnd then
ExpireProjectiles({ Name = currentRoom.DestroyAssistProjectilesOnEncounterEnd })
end
if currentRoom.CodexUpdates then
for category, categoryItems in pairs( currentRoom.CodexUpdates ) do
for categoryItem, count in pairs( categoryItems ) do
IncrementCodexValue( category, categoryItem, count )
CheckCodexUnlock( category, categoryItem, true )
end
end
end
if currentRoom.PendingCodexUpdate then
currentRoom.PendingCodexUpdate = false
wait( 0.2, RoomThreadName )
thread( ShowCodexUpdate )
end
end
if currentEncounter == currentRoom.Encounter then
local heroHealth = CurrentRun.Hero.Health
local encounterHealAmount = GetTotalHeroTraitValue("CombatEncounterAbsoluteHeal")
local healthFloor = round(GetNumMetaUpgrades("HealthEncounterEndRegenMetaUpgrade") * MetaUpgradeData.HealthEncounterEndRegenMetaUpgrade.ChangeValue + CurrentRun.Hero.MaxHealth * GetTotalHeroTraitValue("CombatEncounterHealthPercentFloor"))
if (heroHealth + encounterHealAmount) < healthFloor then
encounterHealAmount = healthFloor - heroHealth
end
Heal( CurrentRun.Hero, { HealAmount = encounterHealAmount, Name = "EncounterHeal" } )
local traitsToRemove = {}
for k, trait in pairs( currentRun.Hero.Traits ) do
if trait.BossEncounterShieldHits then
if currentRun.CurrentRoom.Encounter.EncounterType == "Boss" then
CurrentRun.Hero.HitShields = 0
UpdateTraitNumber( trait )
end
end
if trait.RemainingUses ~= nil and trait.UsesAsEncounters and (not trait.UsesRequireSpawnMultiplier or ( trait.UsesRequireSpawnMultiplier and not currentEncounter.BlockSpawnMultipliers )) then
if trait.HoldRemainingRooms ~= nil and trait.HoldRemainingRooms > 0 then
trait.HoldRemainingRooms = trait.HoldRemainingRooms - 1
else
trait.RemainingUses = trait.RemainingUses - 1
if trait.RemainingUses <= 0 then
table.insert( traitsToRemove, trait )
end
end
TraitUIUpdateText( trait )
end
if trait.EncounterPreDamage and not IsEmpty(currentRoom.UsedPreDamageTraits) then
for i, usedTrait in pairs( currentRoom.UsedPreDamageTraits ) do
if AreTraitsIdentical( usedTrait, trait ) then
trait.RemainingUses = trait.RemainingUses - 1
if trait.RemainingUses <= 0 then
table.insert( traitsToRemove, trait )
end
TraitUIUpdateText( trait )
break
end
end
end
end
if not currentRoom.BlockClearRewards then
for k, traitData in pairs(currentRun.Hero.Traits) do
if not currentEncounter.PlayerTookDamage and traitData.PerfectClearDamageBonus then
traitData.AccumulatedDamageBonus = traitData.AccumulatedDamageBonus + (traitData.PerfectClearDamageBonus - 1)
PerfectClearTraitSuccessPresentation( traitData )
CurrentRun.CurrentRoom.PerfectEncounterCleared = true
CheckAchievement( { Name = "AchBuffedButterfly", CurrentValue = traitData.AccumulatedDamageBonus } )
end
if traitData.FastClearThreshold then
local clearTimeThreshold = currentEncounter.FastClearThreshold or traitData.FastClearThreshold
if currentEncounter.ClearTime < clearTimeThreshold and traitData.FastClearDodgeBonus then
local lastDodgeBonus = traitData.AccumulatedDodgeBonus
SetLifeProperty({ Property = "DodgeChance", Value = -1 * lastDodgeBonus, ValueChangeType = "Add", DestinationId = CurrentRun.Hero.ObjectId, DataValue = false })
SetUnitProperty({ Property = "Speed", Value = 1/( 1 + lastDodgeBonus ), ValueChangeType = "Multiply", DestinationId = CurrentRun.Hero.ObjectId })
traitData.AccumulatedDodgeBonus = traitData.AccumulatedDodgeBonus + traitData.FastClearDodgeBonus
SetLifeProperty({ Property = "DodgeChance", Value = traitData.AccumulatedDodgeBonus, ValueChangeType = "Add", DestinationId = CurrentRun.Hero.ObjectId, DataValue = false })
SetUnitProperty({ Property = "Speed", Value = 1 + traitData.AccumulatedDodgeBonus, ValueChangeType = "Multiply", DestinationId = CurrentRun.Hero.ObjectId })
thread( FastClearTraitSuccessPresentation, traitData )
CheckAchievement( { Name = "AchBuffedPlume", CurrentValue = traitData.AccumulatedDodgeBonus } )
end
end
end
end
for k, trait in pairs( traitsToRemove ) do
RemoveTraitData( currentRun.Hero, trait )
end
AdvanceKeepsake()
UpgradeHarvestBoon()
UpgradeChamberStacks()
end
end
function CheckOnRoomClearTraits( currentRun, currentRoom, currentEncounter )
if not currentRoom.BlockClearRewards and not currentEncounter.ProcessedOnRoomClear then
currentEncounter.ProcessedOnRoomClear = true
for k, traitData in pairs(currentRun.Hero.Traits) do
if not currentEncounter.PlayerTookDamage and traitData.PerfectClearDamageBonus then
traitData.AccumulatedDamageBonus = traitData.AccumulatedDamageBonus + (traitData.PerfectClearDamageBonus - 1)
PerfectClearTraitSuccessPresentation( traitData )
CurrentRun.CurrentRoom.PerfectEncounterCleared = true
end
if traitData.FastClearThreshold then
local clearTimeThreshold = currentEncounter.FastClearThreshold or traitData.FastClearThreshold
if currentEncounter.ClearTime < clearTimeThreshold and traitData.FastClearDodgeBonus then
local lastDodgeBonus = traitData.AccumulatedDodgeBonus
SetLifeProperty({ Property = "DodgeChance", Value = -1 * lastDodgeBonus, ValueChangeType = "Add", DestinationId = CurrentRun.Hero.ObjectId, DataValue = false })
SetUnitProperty({ Property = "Speed", Value = 1/( 1 + lastDodgeBonus ), ValueChangeType = "Multiply", DestinationId = CurrentRun.Hero.ObjectId })
traitData.AccumulatedDodgeBonus = traitData.AccumulatedDodgeBonus + traitData.FastClearDodgeBonus
SetLifeProperty({ Property = "DodgeChance", Value = traitData.AccumulatedDodgeBonus, ValueChangeType = "Add", DestinationId = CurrentRun.Hero.ObjectId, DataValue = false })
SetUnitProperty({ Property = "Speed", Value = 1 + traitData.AccumulatedDodgeBonus, ValueChangeType = "Multiply", DestinationId = CurrentRun.Hero.ObjectId })
thread( FastClearTraitSuccessPresentation, traitData )
end
end
end
end
end
function IsCombatEncounterActive( currentRun )
if currentRun.CurrentRoom.AlwaysInCombat then
return true
end
if not currentRun.Hero.IsDead then
if HasTimerBlock( currentRun ) then
local hasExcludedTimer = false
for _, name in pairs( TimerBlockCombatExcludes ) do
if currentRun.BlockTimerFlags[ name ] then
hasExcludedTimer = true
end
end
if not hasExcludedTimer then
return false
end
end
if currentRun.CurrentRoom.StartedChallengeEncounter and not currentRun.CurrentRoom.ChallengeEncounter.Completed then
return true
end
local encounter = currentRun.CurrentRoom.Encounter
if encounter ~= nil then
if encounter.EncounterType == "NonCombat" then
return false
end
if not encounter.InProgress then
return false
end
if encounter.DelayedStart and not encounter.StartTime then
return false
end
if not encounter.Completed then
return true
end
end
end
return false
end
function CheckRoomExitsReady( currentRoom )
if CurrentRun.Hero.IsDead then
return
end
if not IsEmpty( ActivatedObjects ) then
return false
end
if IsScreenOpen( "BoonMenu" ) then
return false
end
if not currentRoom.Encounter.ExitsDontRequireCompleted and not currentRoom.Encounter.Completed then
return false
end
return true
end
function GetRemainingSpawns( currentRun, currentRoom, currentEncounter )
if currentRun.Hero.IsDead then
return 0
end
if currentEncounter.Completed then
return 0
end
if CheckCancelSpawns(currentRun, currentRoom, currentEncounter) then
return 0
end
local remainingSpawns = 0
if currentEncounter.Spawns ~= nil then
for k, spawnInfo in pairs( currentEncounter.Spawns ) do
if spawnInfo.RemainingSpawns == nil then
-- Spawn totals have not been generated yet. Somewhat of an ambiguous case, unsure how to best handle yet.
remainingSpawns = remainingSpawns + 1
else
remainingSpawns = remainingSpawns + spawnInfo.RemainingSpawns
end
end
end
return remainingSpawns
end
function CheckCancelSpawns( currentRun, currentRoom, currentEncounter )
if currentEncounter.CancelSpawns then
return true
end
if currentEncounter.CancelSpawnsOnKill ~= nil then
for k, unitName in pairs(currentEncounter.CancelSpawnsOnKill) do
if currentRoom.Kills ~= nil and currentRoom.Kills[unitName] ~= nil and currentRoom.Kills[unitName] >= 1 then
return true
end
end
end
if currentEncounter.CancelSpawnsOnKillAll ~= nil then
local remainingSpawns = 0
for k, spawnInfo in pairs( currentEncounter.Spawns ) do
if Contains(currentEncounter.CancelSpawnsOnKillAll, spawnInfo.Name) then
local newRemainingSpawns = spawnInfo.RemainingSpawns or 1
remainingSpawns = remainingSpawns + newRemainingSpawns
end
end
if remainingSpawns == 0 then
local killedAll = true
for k, id in pairs(GetIdsByType({ Names = currentEncounter.CancelSpawnsOnKillAll })) do
if ActiveEnemies[id] ~= nil and not ActiveEnemies[id].IsDead then
killedAll = false
end
end
if killedAll then
return true
end
end
end
if currentEncounter.CancelSpawnsOnKillAllTypes ~= nil then
local killCountGoal = TableLength(currentEncounter.CancelSpawnsOnKillAllTypes)
local killCount = 0
for k, unitName in pairs(currentEncounter.CancelSpawnsOnKillAllTypes) do
if currentRoom.Kills ~= nil and currentRoom.Kills[unitName] ~= nil and currentRoom.Kills[unitName] >= 1 then
killCount = killCount + 1
end
end
if killCount >= killCountGoal then
return true
end
end
return false
end
function GetNextSpawn( currentEncounter )
local forcedSpawn = nil
local remainingSpawnInfo = {}
local remainingPrioritySpawnInfo = {}
for k, spawnInfo in orderedPairs( currentEncounter.Spawns ) do
if spawnInfo.InfiniteSpawns or spawnInfo.RemainingSpawns > 0 then
local enemyData = EnemyData[spawnInfo.Name]
if enemyData ~= nil and enemyData.LargeUnitCap ~= nil and enemyData.LargeUnitCap > 0 then
local largeUnitCount = 0
-- @optimization Convert to buckets by type
for enemyId, enemy in pairs( ActiveEnemies ) do
if enemy.LargeUnitCap ~= nil and enemyData.LargeUnitCap > 0 then
largeUnitCount = largeUnitCount + 1
end
end
if largeUnitCount < enemyData.LargeUnitCap then
table.insert( remainingSpawnInfo, spawnInfo )
if spawnInfo.PrioritySpawn then
table.insert( remainingPrioritySpawnInfo, spawnInfo )
end
else
DebugPrint({ Text = "Avoiding LargeUnitCap: "..enemyData.Name })
end
else
table.insert( remainingSpawnInfo, spawnInfo )
if spawnInfo.PrioritySpawn then
table.insert( remainingPrioritySpawnInfo, spawnInfo )
end
end
if spawnInfo.ForceFirst then
forcedSpawn = spawnInfo
end
end
end
if forcedSpawn ~= nil then
return forcedSpawn
end
local randomSpawnInfo = GetRandomValue( remainingPrioritySpawnInfo ) or GetRandomValue( remainingSpawnInfo )
return randomSpawnInfo
end
function ApplyEliteAttribute( enemy, attributeName )
if enemy.EliteAttributeData == nil or enemy.EliteAttributeData[attributeName] == nil then
DebugPrint({ Text=enemy.Name.." does not have the attribute "..attributeName.." defined!" })
return
end
table.insert(enemy.EliteAttributes, attributeName)
if enemy.IsClone and enemy.EliteAttributeData[attributeName].SkipApplyOnClones then
return
end
local attributeData = enemy.EliteAttributeData[attributeName]
local enemyAIData = enemy.DefaultAIData or enemy
if attributeData.DataOverrides ~= nil then
OverwriteTableKeys(enemy, attributeData.DataOverrides)
end
if attributeData.AIDataOverrides ~= nil then
OverwriteTableKeys(enemyAIData, attributeData.AIDataOverrides)
end
if attributeData.AddWeaponOptions ~= nil then
for k, weaponName in pairs(attributeData.AddWeaponOptions) do
table.insert(enemy.WeaponOptions, weaponName)
end
end
if attributeData.AddDumbFireWeaponsOnSpawn ~= nil then
enemy.AddDumbFireWeaponsOnSpawn = enemy.AddDumbFireWeaponsOnSpawn or {}
enemy.AddDumbFireWeaponsOnSpawn = CombineTables(enemy.AddDumbFireWeaponsOnSpawn, attributeData.AddDumbFireWeaponsOnSpawn)
end
if attributeData.AddEnemyOnDeathWeapons ~= nil then
AddEnemyOnDeathWeapons( enemy, attributeData.AddEnemyOnDeathWeapons )
end
if attributeData.WeaponPropertyChanges ~= nil then
for k, weaponName in pairs(enemy.WeaponOptions) do
EquipWeapon({ DestinationId = enemy.ObjectId, Name = weaponName })
ApplyWeaponPropertyChanges( enemy, weaponName, attributeData.WeaponPropertyChanges )
end
end
end
function PickEliteAttributes( currentRoom, enemy )
if currentRoom.Encounter ~= nil and currentRoom.Encounter.BlockEliteAttributes then
return
end
if enemy.EliteAttributeOptions == nil or IsEmpty(enemy.EliteAttributeOptions) then
return
end
enemy.EliteAttributeCount = GetNumMetaUpgrades( "EnemyEliteShrineUpgrade" )
local attributeOptions = {}
for k, attributeName in pairs(enemy.EliteAttributeOptions) do
if IsEliteAttributeEligible(enemy, attributeName) then
table.insert(attributeOptions, attributeName)
end
end
for i=1, enemy.EliteAttributeCount do
if IsEmpty(attributeOptions) then
DebugPrint({ Text="RunManager.lua:795 ".."Ran out of legal Elite Attribute options!" })
break
end
local attributeName = RemoveRandomValue(attributeOptions)
currentRoom.EliteAttributes[enemy.Name] = currentRoom.EliteAttributes[enemy.Name] or {}
table.insert(currentRoom.EliteAttributes[enemy.Name], attributeName)
RemoveAllValues(attributeOptions, attributeName)
if enemy.EliteAttributeData[attributeName].BlockAttributes ~= nil then
for k, blockedAttributeName in pairs(enemy.EliteAttributeData[attributeName].BlockAttributes) do
RemoveAllValues(attributeOptions, blockedAttributeName)
end
end
end
end
function IsEliteAttributeEligible( enemy, attributeName )
local attributeRequirements = enemy.EliteAttributeData[attributeName].Requirements or enemy.EliteAttributeData[attributeName]
if attributeRequirements.RequiresFalseSuperElite and enemy.IsSuperElite then
return false
end
if Contains(CurrentRun.BannedEliteAttributes, attributeName) then
return false
end
if Contains(enemy.BlockAttributes, attributeName) then
return false
end
if not IsGameStateEligible(CurrentRun, enemy.EliteAttributeData[attributeName], attributeRequirements) then
return false
end
return true
end
function SetupEnemyObject( newEnemy, currentRun, args )
local currentRoom = currentRun.CurrentRoom
local skipModifiers = newEnemy.SkipModifiers
local shrineLevel = GetNumMetaUpgrades(newEnemy.ShrineMetaUpgradeName)
local requiredShrineLevel = newEnemy.ShrineMetaUpgradeRequiredLevel or 1
if newEnemy.ShrineDataOverwrites ~= nil and shrineLevel >= requiredShrineLevel then
OverwriteTableKeys(newEnemy, newEnemy.ShrineDataOverwrites)
end
if newEnemy.ShrineDefualtAIDataOverwrites ~= nil and shrineLevel > 0 then
if newEnemy.DefaultAIData == nil then
newEnemy.DefaultAIData = {}
end
OverwriteTableKeys(newEnemy.DefaultAIData, newEnemy.ShrineDefualtAIDataOverwrites)
end
if newEnemy.ShrineWeaponOptionsOverwrite ~= nil and shrineLevel > 0 then
newEnemy.WeaponOptions = newEnemy.ShrineWeaponOptionsOverwrite
end
newEnemy.WeaponHistory = newEnemy.WeaponHistory or {}
if newEnemy.WeaponOptions ~= nil and not newEnemy.SkipSetupSelectWeapon then
newEnemy.WeaponName = SelectWeapon( newEnemy )
EquipWeapon({ Name = newEnemy.WeaponName, DestinationId = newEnemy.ObjectId })
end
if newEnemy.OnHitWeapons ~= nil then
for k, weaponName in pairs(newEnemy.OnHitWeapons) do
EquipWeapon({ Name = weaponName, DestinationId = newEnemy.ObjectId })
end
end
if newEnemy.IsElite then
newEnemy.EliteAttributes = newEnemy.EliteAttributes or {}
--[[if currentRoom.EliteAttributes[newEnemy.Name] == nil then
PickEliteAttributes( currentRoom, newEnemy )
end]]
if currentRoom.EliteAttributes[newEnemy.Name] ~= nil then
for k, attributeName in pairs(currentRoom.EliteAttributes[newEnemy.Name]) do
ApplyEliteAttribute(newEnemy, attributeName)
end
end
end
if newEnemy.AddDumbFireWeaponsOnSpawn ~= nil then
for k, weaponName in pairs( newEnemy.AddDumbFireWeaponsOnSpawn ) do
newEnemy.DumbFireWeapons = newEnemy.DumbFireWeapons or {}
EquipWeapon({ Name = weaponName, DestinationId = newEnemy.ObjectId })
table.insert( newEnemy.DumbFireWeapons, weaponName )
end
end
newEnemy.Groups = newEnemy.Groups or {}
table.insert( newEnemy.Groups, "EnemyTeam" )
if newEnemy.RequiredKill then
table.insert( newEnemy.Groups, "RequiredKillEnemies" )
end
AddToGroup({ Id = newEnemy.ObjectId, Names = newEnemy.Groups })
if newEnemy.RespawnOnDeath then
newEnemy.OriginalSpawnLocationId = SpawnObstacle({ Name = "InvisibleTarget", DestinationId = newEnemy.ObjectId })
end
if not skipModifiers then
ApplyEnemyModifiers( newEnemy, currentRun, args )
end
if newEnemy.AlwaysTraitor then
SwitchAllegiance({ Id = newEnemy.ObjectId })
end
ApplyEnemyTraits( currentRun, newEnemy )
newEnemy.BarXScale = 1
local healthMultiplier = newEnemy.HealthMultiplier or 1
healthMultiplier = healthMultiplier + ( GetNumMetaUpgrades( "EnemyHealthShrineUpgrade" ) * ( MetaUpgradeData.EnemyHealthShrineUpgrade.ChangeValue - 1 ) )
if healthMultiplier ~= 1 and newEnemy.UseShrineUpgrades and newEnemy.MaxHealth ~= nil then
newEnemy.MaxHealth = newEnemy.MaxHealth * healthMultiplier
end
newEnemy.Health = newEnemy.MaxHealth
if newEnemy.PreDamageIfEncounterCompleted ~= nil and HasEncounterOccurred( currentRun, newEnemy.PreDamageIfEncounterCompleted, true ) then
newEnemy.Health = newEnemy.MaxHealth * newEnemy.PreDamagePercent
end
for i, traitData in pairs(CurrentRun.Hero.Traits) do
if traitData.EncounterPreDamage then
local validEnemy = false
local damageData = traitData.EncounterPreDamage
if damageData.EnemyType ~= nil then
if damageData.EnemyType == "Boss" and newEnemy.IsBoss then
validEnemy = true
end
else
validEnemy = true
end
if validEnemy then
newEnemy.Health = newEnemy.Health - ( newEnemy.MaxHealth * damageData.PreDamage )
CurrentRun.CurrentRoom.UsedPreDamageTraits = CurrentRun.CurrentRoom.UsedPreDamageTraits or {}
table.insert( CurrentRun.CurrentRoom.UsedPreDamageTraits, traitData )
end
end
end
if newEnemy.HealthBuffer ~= nil and newEnemy.HealthBuffer > 0 then
local healthBufferMultiplier = newEnemy.HealthBufferMultiplier or 1
newEnemy.HealthBuffer = newEnemy.HealthBuffer * healthMultiplier * healthBufferMultiplier
DoEnemyHealthBuffered( newEnemy )
end
if newEnemy.Phases ~= nil then
newEnemy.CurrentPhase = 1
end
newEnemy.HitShields = 0
if newEnemy.UseShrineUpgrades then
newEnemy.HitShields = GetNumMetaUpgrades( "EnemyShieldShrineUpgrade" )
local eliteSpeedMultiplier = newEnemy.EliteAdditionalSpeedMultiplier or 0
local speedMultiplier = 1 + eliteSpeedMultiplier
if GetNumMetaUpgrades( "EnemySpeedShrineUpgrade" ) > 0 then
if MetaUpgradeData.EnemySpeedShrineUpgrade.ChangeValues and MetaUpgradeData.EnemySpeedShrineUpgrade.ChangeValues[GetNumMetaUpgrades( "EnemySpeedShrineUpgrade" )] then
speedMultiplier = speedMultiplier + (MetaUpgradeData.EnemySpeedShrineUpgrade.ChangeValues[GetNumMetaUpgrades( "EnemySpeedShrineUpgrade" )] - 1)
elseif MetaUpgradeData.EnemySpeedShrineUpgrade.ChangeValue then
speedMultiplier = speedMultiplier + GetNumMetaUpgrades("EnemySpeedShrineUpgrade") * ( MetaUpgradeData.EnemySpeedShrineUpgrade.ChangeValue - 1)
end
end
if speedMultiplier > 1.0 then
newEnemy.SpeedMultiplier = speedMultiplier
SetThingProperty({ Property = "ElapsedTimeMultiplier", Value = newEnemy.SpeedMultiplier, ValueChangeType = "Multiply", DataValue = false, DestinationId = newEnemy.ObjectId })
end
end
if args ~= nil and args.IgnoreAI then
newEnemy.SkipAISetupOnActivate = true
end
if newEnemy.SpeedMin ~= nil and newEnemy.SpeedMax ~= nil then
newEnemy.MoveSpeed = RandomInt( newEnemy.SpeedMin, newEnemy.SpeedMax )
SetUnitProperty({ Property = "Speed", Value = newEnemy.MoveSpeed, DestinationId = newEnemy.ObjectId })
end
if newEnemy.ScaleMin and newEnemy.ScaleMax then
newEnemy.Scale = RandomFloat( newEnemy.ScaleMin, newEnemy.ScaleMax )
SetScale({ Fraction = newEnemy.Scale, Id = newEnemy.ObjectId })
end
if newEnemy.Color ~= nil then
SetColor({ Id = newEnemy.ObjectId, Color = newEnemy.Color, MultiplyBase = true })
end
if newEnemy.SpawnFx ~= nil then
CreateAnimation({ DestinationId = newEnemy.ObjectId, Name = newEnemy.SpawnFx })
end
if newEnemy.SpawnAnimation ~= nil then
SetAnimation({ DestinationId = newEnemy.ObjectId, Name = newEnemy.SpawnAnimation })
end
if newEnemy.AddOutlineImmediately then
if not newEnemy.HasOutline and newEnemy.Outline ~= nil and newEnemy.HealthBuffer ~= nil and newEnemy.HealthBuffer > 0 then
newEnemy.Outline.Id = newEnemy.ObjectId
AddOutline( newEnemy.Outline )
newEnemy.HasOutline = true
end
end
if newEnemy.RequiredKill then
RequiredKillEnemies[newEnemy.ObjectId] = newEnemy
notifyExistingWaiters( "RequiredKillEnemyKilledOrSpawned" )
end
if newEnemy.MoneyDropOnDeath and newEnemy.MoneyDropOnDeath.ValuePerDifficulty and newEnemy.MoneyDropOnDeath.ValuePerDifficulty > 0 then
if newEnemy.GeneratorData ~= nil then
newEnemy.MoneyDropOnDeath.MinValue = newEnemy.MoneyDropOnDeath.ValuePerDifficulty * newEnemy.GeneratorData.DifficultyRating
newEnemy.MoneyDropOnDeath.MaxValue = newEnemy.MoneyDropOnDeath.ValuePerDifficulty * newEnemy.GeneratorData.DifficultyRating * newEnemy.MoneyDropOnDeath.ValuePerDifficultyMaxValueVariance
else
newEnemy.MoneyDropOnDeath.MinValue = 1
newEnemy.MoneyDropOnDeath.MaxValue = 1
end
end
local backstabUpgrades = GetNumMetaUpgrades("BackstabMetaUpgrade")
local ammoDamageUpgrades = GetNumMetaUpgrades("StoredAmmoVulnerabilityMetaUpgrade")
-- Metaupgrades are expressed as values of 1.05, 1.1, etc.
local baseMetaUpgradeChangeValue = (MetaUpgradeData.BackstabMetaUpgrade.ChangeValue - 1)
local enemyVulnerability = 1.0 + backstabUpgrades * baseMetaUpgradeChangeValue
local collisionVulnerability = 1.0 * GetTotalHeroTraitValue("BonusCollisionVulnerability", { IsMultiplier = true })
SetLifeProperty({ Property = "CollisionDamageMultiplier", Value = collisionVulnerability, ValueChangeType = "Multiply", DestinationId = newEnemy.ObjectId, DataValue = false })
if newEnemy.CharacterInteractions ~= nil then
ActivatedObjects[newEnemy.ObjectId] = newEnemy
end
if newEnemy.DistanceTriggers ~= nil then
for k, trigger in ipairs( newEnemy.DistanceTriggers ) do
thread( CheckDistanceTrigger, trigger, newEnemy )
end
end
if newEnemy.RandomFlipHorizontal and CoinFlip() then
FlipHorizontal({ Id = newEnemy.ObjectId })
end
if newEnemy.SelectCustomSpawnOptions ~= nil then
local customSpawnOptionsFunction = _G[newEnemy.SelectCustomSpawnOptions]
customSpawnOptionsFunction( newEnemy )
end
ActiveEnemies[newEnemy.ObjectId] = newEnemy
AttachLua({ Id = newEnemy.ObjectId, Table = newEnemy })
SpawnRecord[newEnemy.Name] = (SpawnRecord[newEnemy.Name] or 0) + 1
GameState.EnemySpawns[newEnemy.Name] = (GameState.EnemySpawns[newEnemy.Name] or 0) + 1
if args == nil or (not args.SkipPresentation and not args.SkipSpawnVoiceLines) then
thread( PlayVoiceLines, newEnemy.OnSpawnVoiceLines, nil, newEnemy )
end
if args ~= nil and args.PreLoadBinks then
LoadEnemyBinks( newEnemy )
end
RunUnthreadedEvents( newEnemy.SpawnUnthreadedEvents, newEnemy )
if newEnemy.AdditionalEnemySetupFunctionName ~= nil then
local additionalSetupFunction = _G[newEnemy.AdditionalEnemySetupFunctionName]
additionalSetupFunction( newEnemy, currentRun, args )
end
if newEnemy.OnSpawnFireFunction ~= nil then
local spawnFunction = _G[newEnemy.OnSpawnFireFunction]
spawnFunction( newEnemy )
end
if newEnemy.PermanentlyEnrage then
newEnemy.PermanentEnraged = true
thread(EnrageUnit, newEnemy, currentRun)
end
if newEnemy.SetLastInvisibilityTimeOnSpawn then
newEnemy.LastInvisibilityTime = _worldTime
else
newEnemy.LastInvisibilityTime = 0
end
if newEnemy.SetLastTeleportTimeOnSpawn then
newEnemy.LastTeleportTime = _worldTime
else
newEnemy.LastTeleportTime = 0
end
end
function LoadEnemyBinks( enemy )
if enemy.Binks ~= nil then
PreLoadBinks({ Names = enemy.Binks })
end
if enemy.OnDeathSpawnEncounter ~= nil and EncounterData[enemy.OnDeathSpawnEncounter] ~= nil then
local binksToPreload = {}
local enemyCache = {}
GetEncounterBinks( EncounterData[enemy.OnDeathSpawnEncounter], binksToPreload, enemyCache )
local dedupe = {}
local finalBinksToPreload = {}
for i, name in ipairs(binksToPreload) do
if dedupe[name] == nil then
dedupe[name] = name
table.insert(finalBinksToPreload, name)
end
end
PreLoadBinks({ Names = finalBinksToPreload })
end
if enemy.OtherEnemyBinks ~= nil then
for k, otherEnemyName in pairs( enemy.OtherEnemyBinks ) do
local enemyData = EnemyData[otherEnemyName]
if enemyData ~= nil then
LoadEnemyBinks( enemyData )
end
end
end
if enemy.SpawnOptions ~= nil then
for k, spawnOption in pairs( enemy.SpawnOptions ) do
local enemyData = EnemyData[spawnOption]
if enemyData ~= nil then
LoadEnemyBinks( enemyData )
end
end
end
end
function CreateTethers( newEnemy )
if newEnemy == nil or newEnemy.Tethers == nil or newEnemy.TetherIds ~= nil then
return
end
newEnemy.TetherIds = {}
local prevTetherId = newEnemy.ObjectId
for k, tether in ipairs( newEnemy.Tethers ) do
local count = tether.Count or 1
for i = 1, count do
local offsetX = nil
local offsetY = nil
if tether.SpawnRadius ~= nil then
offsetX = RandomFloat( -tether.SpawnRadius, tether.SpawnRadius )
offsetY = RandomFloat( -tether.SpawnRadius, tether.SpawnRadius )
end
local tetherId = SpawnObstacle({ Name = tether.Name, DestinationId = newEnemy.ObjectId, Group = tether.GroupName or "Standing", OffsetX = offsetX, offsetY = offsetY })
SetAlpha({ Id = tetherId, Fraction = 0 })
SetAlpha({ Id = tetherId, Fraction = 1.0, Duration = 0.3 })
if tether.Elasticity ~= nil then
Attach({ Id = tetherId, DestinationId = newEnemy.ObjectId, TetherDistance = tether.Distance, TetherElasticity = tether.Elasticity })
else
Attach({ Id = prevTetherId, DestinationId = tetherId, TetherDistance = tether.Distance, TetherRetractSpeed = tether.RetractSpeed, TetherTrackZRatio = tether.TrackZRatio })
end
table.insert( newEnemy.TetherIds, tetherId )
if newEnemy.EliteIcon or ( newEnemy.HealthBuffer ~= nil and newEnemy.HealthBuffer > 0 ) then
newEnemy.Outline.Id = tetherId
if newEnemy.Outline.Thickness > 0 then
AddOutline( newEnemy.Outline )
end
end
prevTetherId = tetherId
end
end
end
OnActivationFinished{
function( triggerArgs )
local unit = triggerArgs.TriggeredByTable
if unit ~= nil and not unit.IsDead then
unit.ActivationFinished = true
if unit.FireWeaponOnActivationFinished then
FireWeaponFromUnit({ Weapon = unit.FireWeaponOnActivationFinished, Id = unit.ObjectId, DestinationId = unit.ObjectId, AutoEquip = true })
end
if not unit.SkipAISetupOnActivate then
SetupAI( CurrentRun, unit )
end
CreateLevelDisplay( unit, CurrentRun )
CreateTethers( unit )
if unit.AttachedAnimationName ~= nil then
CreateAnimation({ Name = unit.AttachedAnimationName, DestinationId = unit.ObjectId, OffsetZ = unit.AttachedAnimationOffsetZ })
end
thread( PlayVoiceLines, unit.OnActivationFinishedVoiceLines, nil, unit )
end
end
}
function ApplyEnemyModifiers(newEnemy, currentRun, args )
EquipSpecialWeapons( newEnemy, newEnemy )
for k, enemyUpgradeName in pairs( CurrentRun.EnemyUpgrades ) do
local upgradeData = EnemyUpgradeData[enemyUpgradeName]
if upgradeData.AddDumbFireWeapons then
EquipDumbFireWeapons( newEnemy, upgradeData )
end
if upgradeData.AddSpecialWeapons then
EquipSpecialWeapons( newEnemy, upgradeData )
end
if upgradeData.AddEnemyOnDeathWeapons then
AddEnemyOnDeathWeapons( newEnemy, upgradeData.AddEnemyOnDeathWeapons )
end
ApplyUnitPropertyChanges( newEnemy, upgradeData.PropertyChanges, true )
end
-- for enemy on spawn traits
for i, traitData in pairs( GetHeroTraitValues("AddOnEnemySpawnWeapon")) do
if traitData.AffectChance == nil or RandomChance(traitData.AffectChance) then
FireWeaponFromUnit({ Weapon = traitData.Weapon, Id = currentRun.Hero.ObjectId, DestinationId = newEnemy.ObjectId, AutoEquip = true })
end
end
if CurrentRun.CurrentRoom.ElapsedTimeMultiplier then
SetThingProperty({ Property = "ElapsedTimeMultiplier", Value = CurrentRun.CurrentRoom.ElapsedTimeMultiplier, ValueChangeType = "Multiply", DataValue = false, DestinationId = newEnemy.ObjectId })
end
end
function SetupAI( currentRun, enemy )
if enemy.AIWakeDelay ~= nil then
wait(enemy.AIWakeDelay)
end
if enemy.GroupAI ~= nil then
if ActiveGroupAIs[enemy.GroupAI] == nil then
ActiveGroupAIs[enemy.GroupAI] = {}
end
table.insert( ActiveGroupAIs[enemy.GroupAI], enemy )
-- Start the thread for the first one found
if #ActiveGroupAIs[enemy.GroupAI] == 1 then
local groupAIFunction = _G[enemy.GroupAI]
thread( groupAIFunction, ActiveGroupAIs[enemy.GroupAI], currentRun )
end
return
end
if enemy.ComboPartnerName ~= nil then
enemy.ComboPartnerId = GetClosestUnitOfType({ Id = enemy.ObjectId, DestinationName = enemy.ComboPartnerName })
end
if enemy.SupportUnitName ~= nil then
SpawnSupportAI( enemy, currentRun )
end
if enemy.AIStages ~= nil then
enemy.AIThreadName = "EnemyAIThread"..enemy.ObjectId
thread( StagedAI, enemy, currentRun )
return
end
local aiBehavior = PickEnemyAI( currentRun, enemy )
if aiBehavior ~= nil then
enemy.AIThreadName = "EnemyAIThread"..enemy.ObjectId
thread( SetAI, aiBehavior, enemy, currentRun )
end
ActivateDumbFireWeapons( currentRun, enemy )
end
function ActivateDumbFireWeapons( currentRun, enemy )
if enemy.DumbFireWeapons ~= nil then
for k, weaponData in pairs( enemy.DumbFireWeapons ) do
if weaponData.Name == nil then
weaponData = WeaponData[weaponData]
thread( DumbFireAttack, enemy, currentRun, weaponData )
else
thread( DumbFireAttack, enemy, currentRun, weaponData )
end
end
end
end
function CheckAvailableTextLines( source, args )
if source.InteractTextLineSets == nil then
return
end
local useableOffFromGift = NeedsUseableOff( source, source.GiftTextLineSets )
if useableOffFromGift then
return
end
if source.NextInteractLines == nil then
local interactLines = GetRandomEligibleTextLines( source, source.InteractTextLineSets, args )
local repeatableLines = GetRandomEligibleTextLines( source, source.RepeatableTextLineSets, args )
if interactLines == "UseableOffSource" or repeatableLines == "UseableOffSource" then
-- Queue nothing
elseif interactLines ~= nil then
SetNextInteractLines( source, interactLines )
elseif repeatableLines ~= nil then
SetNextInteractLines( source, repeatableLines )
end
else
-- Restore status
if source.NextInteractLines.OnQueuedFunctionArgs ~= nil and source.NextInteractLines.OnQueuedFunctionArgs.Restore then
local onQueuedFunction = _G[source.NextInteractLines.OnQueuedFunctionName]
onQueuedFunction( source, source.NextInteractLines.OnQueuedFunctionArgs )
end
if source.NextInteractLines.OnQueuedFunctions ~= nil then
for k, onQueuedFunctionData in pairs( source.NextInteractLines.OnQueuedFunctions ) do
if onQueuedFunctionData.Args.Restore and ( onQueuedFunctionData.GameStateRequirements == nil or IsGameStateEligible( CurrentRun, source, onQueuedFunctionData.GameStateRequirements ) ) then
local onQueuedFunction = _G[onQueuedFunctionData.Name]
onQueuedFunction( source, onQueuedFunctionData.Args )
end
end
end
if source.NextInteractLines.StatusAnimation then
PlayStatusAnimation( source, { Animation = source.NextInteractLines.StatusAnimation } )
end
end
end
function SetAvailableUseText( source )
local genusName = GetGenusName( source )
local canGift = ( GetTextLinesUseableStatus( source, source.GiftTextLineSets ) and CanReceiveGift( source ) and ( HasResource( GetNextGiftResource( genusName ), GetNextGiftResourceQuantity( genusName ))))
local canAssistAction = ( source.AssistInteractText ~= nil )
if source.NextInteractLines ~= nil or canGift or canAssistAction or source.AlwaysShowDefaultUseText then
UseableOn({ Id = source.ObjectId })
RefreshUseButton( source.ObjectId, source )
else
UseableOff({ Id = source.ObjectId })
end
end
function SetNextInteractLines( source, textLines )
if textLines == nil then
return
end
source.NextInteractLines = textLines
if source.NextInteractLines.UseText ~= nil then
source.UseText = source.NextInteractLines.UseText
end
if source.NextInteractLines.InteractDistance ~= nil then
SetInteractProperty({ DestinationId = source.ObjectId, Property = "Distance", Value = source.NextInteractLines.InteractDistance })
end
if textLines.TeleportToId ~= nil then
Teleport({ Id = textLines.TeleportId or source.ObjectId, DestinationId = textLines.TeleportToId, OffsetX = textLines.TeleportOffsetX, OffsetY = textLines.TeleportOffsetY, OnlyIfDestinationExits = true, })
end
if textLines.TeleportWithId ~= nil then
Teleport({ Id = textLines.TeleportWithId, DestinationId = textLines.TeleportToId })
Teleport({ Id = source.ObjectId, DestinationId = textLines.TeleportToId, OffsetX = textLines.TeleportOffsetX, OffsetY = textLines.TeleportOffsetY, OnlyIfDestinationExits = true, })
end
if textLines.AngleTowardTargetId ~= nil then
AngleTowardTarget({ Id = textLines.AngleId or source.ObjectId, DestinationId = textLines.AngleTowardTargetId })
end
if not CurrentRun.NPCInteractions[source.Name] or textLines.UseInitialInteractSetup then
-- Only do first conversation this run
if textLines.OnQueuedFunctionName ~= nil then
local onQueuedFunction = _G[textLines.OnQueuedFunctionName]
onQueuedFunction( source, textLines.OnQueuedFunctionArgs )
end
if textLines.OnQueuedFunctions ~= nil then
for k, onQueuedFunctionData in pairs( textLines.OnQueuedFunctions ) do
if onQueuedFunctionData.GameStateRequirements == nil or IsGameStateEligible( CurrentRun, source, onQueuedFunctionData.GameStateRequirements ) then
local onQueuedFunction = _G[onQueuedFunctionData.Name]
onQueuedFunction( source, onQueuedFunctionData.Args )
end
end
end
if textLines.OnQueuedThreadedFunctionName ~= nil then
local onQueuedFunction = _G[textLines.OnQueuedThreadedFunctionName]
thread( onQueuedFunction, source, textLines.OnQueuedFunctionArgs )
end
if textLines.StatusAnimation then
PlayStatusAnimation( source, { Animation = textLines.StatusAnimation } )
end
end
end
function SetStatusAnimationFromTextLines( source, textLines )
if not source or not textLines then
return
end
if textLines.OnQueuedFunctionName ~= nil or textLines.OnQueuedThreadedFunctionName ~= nil then
local onQueuedFunctionArgs = textLines.OnQueuedFunctionArgs
if onQueuedFunctionArgs and onQueuedFunctionArgs.StatusAnimation then
PlayStatusAnimation( source, { Animation = onQueuedFunctionArgs.StatusAnimation } )
end
end
if textLines.OnQueuedFunctions ~= nil then
for k, onQueuedFunctionData in pairs( textLines.OnQueuedFunctions ) do
if onQueuedFunctionData.Args and onQueuedFunctionData.Args.StatusAnimation and ( onQueuedFunctionData.GameStateRequirements == nil or IsGameStateEligible( CurrentRun, source, onQueuedFunctionData.GameStateRequirements ) ) then
PlayStatusAnimation( source, { Animation = onQueuedFunctionData.Args.StatusAnimation } )
end
end
end
if textLines.StatusAnimation then
PlayStatusAnimation( source, { Animation = textLines.StatusAnimation } )
end
end
function CheckConversations()
-- Check partner conversations first
local allowPartnerConversationsThisRun = true
if CurrentRun.Hero.IsDead and not IsGameStateEligible( CurrentRun, GameData.PartnerConversationRequirements ) then
allowPartnerConversationsThisRun = false
end
--local sortedUnits = CollapseTableOrdered( ActiveEnemies )
local sortedUnits = {}
for k, unitName in ipairs( GameData.ConversationOrder ) do
local unitIds = GetIdsByType({ Name = unitName })
table.sort( unitIds )
for k, unitId in ipairs( unitIds ) do
local unit = ActiveEnemies[unitId]
if unit ~= nil then
table.insert( sortedUnits, unit )
end
end
end
if allowPartnerConversationsThisRun then
for id, unit in ipairs( sortedUnits ) do
if CurrentRun.PartnerConversationName == nil or CurrentRun.PartnerConversationName == unit.Name then
CheckAvailableTextLines( unit, { RequirePartner = true } )
if CheckPartnerConversations( unit ) then
CurrentRun.PartnerConversationName = unit.Name
break
end
end
end
end
-- Then normal conversations
for id, unit in ipairs( sortedUnits ) do
CheckAvailableTextLines( unit, { RequireNoPartner = true } )
SetAvailableUseText( unit )
end
end
function CheckPartnerConversations( unit )
if unit.NextInteractLines == nil then
return false
end
for partnerId, partnerUnit in pairs( ActiveEnemies ) do
if partnerUnit.Name == unit.NextInteractLines.Partner then
local allTextLineSets = MergeTables( partnerUnit.InteractTextLineSets, partnerUnit.RepeatableTextLineSets ) or partnerUnit.InteractTextLineSets
for partnerTextLinesName, partnerTextLines in pairs( allTextLineSets ) do
-- Partner conversation overrides any other conversation chosen
if partnerTextLinesName == unit.NextInteractLines.Name then
SetNextInteractLines( partnerUnit, partnerTextLines )
if unit.NextInteractLines.UseText ~= nil then
partnerUnit.UseText = unit.NextInteractLines.UseText
end
if unit.NextInteractLines.BlockDistanceTriggers ~= nil then
partnerUnit.NextInteractLines.BlockDistanceTriggers = unit.NextInteractLines.BlockDistanceTriggers
end
partnerUnit.CanReceiveGift = false
unit.CanReceiveGift = false
partnerUnit.InPartnerConversation = true
unit.InPartnerConversation = true
return true
end
end
end
end
return false
end
function GetRandomEligibleTextLines( source, textLineSets, args )
if textLineSets == nil then
return nil
end
local useable = true
local superPriorityTextLines = {}
local priorityTextLines = {}
local eligibleTextLines = {}
local eligibleUnplayedTextLines = {}
for textLineSetName, textLineSet in pairs( textLineSets ) do
if CurrentRun.TextLinesRecord[textLineSet.Name] then
-- Conversation completed
if textLineSet.UseableOffSource then
return "UseableOffSource"
end
elseif IsTextLineEligible( CurrentRun, textLineSet, nil, nil, args ) then
-- Conversation available
table.insert( eligibleTextLines, textLineSet )
if not PlayedRandomTextLines[textLineSet.Name] then
table.insert( eligibleUnplayedTextLines, textLineSet )
end
if textLineSet.SuperPriority then
table.insert( superPriorityTextLines, textLineSet )
elseif textLineSet.Priority then
table.insert( priorityTextLines, textLineSet )
end
end
end
if not useable then
return nil
end
if not IsEmpty( superPriorityTextLines ) then
return GetRandomValue( superPriorityTextLines )
end
if not IsEmpty( priorityTextLines ) then
return GetRandomValue( priorityTextLines )
end
local randomLines = nil
if IsEmpty( eligibleUnplayedTextLines ) then
-- All lines played, start the record over
for textLinesName, textLines in pairs( textLineSets ) do
PlayedRandomTextLines[textLinesName] = nil
end
randomLines = GetRandomValue( eligibleTextLines )
else
randomLines = GetRandomValue( eligibleUnplayedTextLines )
end
if randomLines ~= nil then
PlayedRandomTextLines[randomLines.Name] = true
end
return randomLines
end
function GetTextLinesUseableStatus( source, textLineSets )
if textLineSets == nil then
return false
end
local useable = true
local conversationAvailable = false
for k, textLineSet in pairs( textLineSets ) do
if CurrentRun.TextLinesRecord[textLineSet.Name] then
-- Conversation completed
if textLineSet.UseableOffSource then
useable = false
end
elseif IsTextLineEligible( CurrentRun, textLineSet ) then
-- Conversation available
conversationAvailable = true
end
end
return conversationAvailable and useable
end
function NeedsUseableOff( source, textLineSets )
if textLineSets == nil then
return false
end
for k, textLineSet in pairs( textLineSets ) do
if CurrentRun.TextLinesRecord[textLineSet.Name] and textLineSet.UseableOffSource then
return true
end
end
return false
end
function CalcTotalSpawns( currentRun, currentRoom, currentEncounter, spawnInfo )
if spawnInfo.InfiniteSpawns then
return 1
end
if spawnInfo.TotalCount == nil and (spawnInfo.CountMin == nil or spawnInfo.CountMax == nil) then
return 0
end
if spawnInfo.RequiredMetaUpgrade ~= nil and GetNumMetaUpgrades(spawnInfo.RequiredMetaUpgrade) == 0 then
return 0
end
if spawnInfo.RequiredFalseMetaUpgrade ~= nil and GetNumMetaUpgrades(spawnInfo.RequiredMetaUpgrade) > 0 then
return 0
end
local totalSpawns = spawnInfo.TotalCount or RandomInt( spawnInfo.CountMin, spawnInfo.CountMax )
if currentEncounter.EnemyCountDepthRamp ~= nil then
totalSpawns = totalSpawns * ( 1.0 + ( (GetRunDepth( currentRun ) - 1) * currentEncounter.EnemyCountDepthRamp ) )
end
if currentEncounter.EnemyCountRunRamp ~= nil then
totalSpawns = totalSpawns * ( 1.0 + ( GetCompletedRuns() * currentEncounter.EnemyCountRunRamp ) )
end
local spawnMultiplier = 1
if not currentEncounter.BlockSpawnMultipliers then
spawnMultiplier = GetTotalHeroTraitValue("SpawnMultiplier", { IsMultiplier = true })
end
if spawnInfo.EnemyCountShrineModifierName then
local modifierName = spawnInfo.EnemyCountShrineModifierName
totalSpawns = math.floor( totalSpawns + (GetNumMetaUpgrades(modifierName) * spawnInfo.EnemyCountShineModifierAmount ) )
elseif currentEncounter.EnemyCountShrineModifierName then
local modifierName = currentEncounter.EnemyCountShrineModifierName
totalSpawns = math.floor( totalSpawns + (GetNumMetaUpgrades(modifierName) * currentEncounter.EnemyCountShineModifierAmount ) )
end
totalSpawns = totalSpawns * spawnMultiplier
return totalSpawns
end
function PickEnemyAI( currentRun, newEnemy )
local aiOption = nil
-- Use the room's individual overrides
if currentRun.CurrentRoom.AIOverride ~= nil then
aiOption = currentRun.CurrentRoom.AIOverride[newEnemy.Name]
end
-- Otherwise, use the room's full override
if aiOption == nil and currentRun.CurrentRoom.AIFullOverrideName ~= nil and newEnemy.AIOptions ~= nil then
aiOption = _G[currentRun.CurrentRoom.AIFullOverrideName]
end
-- Otherwise use the encounter's overrides
if aiOption == nil and currentRun.CurrentRoom.Encounter ~= nil and currentRun.CurrentRoom.Encounter.Spawns ~= nil then
if currentRun.CurrentRoom.Encounter.Spawns[newEnemy.Name] ~= nil then
aiOption = currentRun.CurrentRoom.Encounter.Spawns[newEnemy.Name].AIOverride
end
end
-- Otherwise pick from the enemy's available AI
if aiOption == nil then
aiOption = GetRandomValue( newEnemy.AIOptions )
end
return aiOption
end
function UnlockRoomExits( run, room, delay )
if run.ExitsReadyFunctionName ~= nil then
RunFunctionName( run.ExitsReadyFunctionName )
end
if room.ExitsUnlocked then
return
end
room.ExitsUnlocked = true
thread( CheckQuestStatus )
thread( CheckProgressAchievements )
if IsEmpty( OfferedExitDoors ) then
return
end
wait( delay or 0.4 ) -- Must wait for post-reward presentation to complete
local heroLocation = GetLocation({ Id = run.Hero.ObjectId })
RecordObjectState( room, run.Hero.ObjectId, "Location", heroLocation )
local heroAngle = GetAngle({ Id = run.Hero.ObjectId })
RecordObjectState( room, run.Hero.ObjectId, "Angle", heroAngle )
if room.Encounter ~= nil and not room.Encounter.SkipExitReadyCheckpoint then
DebugAssert({ Condition = room.Encounter.Completed, Text = "Exits unlocked in incompleted encounter!" })
room.CheckpointInvalidated = false
SaveCheckpoint({ SaveName = "_Temp", DevSaveName = CreateDevSaveName( run, { PostReward = true } ) })
ValidateCheckpoint({ Valid = true })
end
wait(0.02)
DoUnlockRoomExits( run, room )
end
function ChooseNextRewardStore( run )
RandomSynchronize()
local rewardStoreName = nil
local targetMetaRewardsRatio = (run.CurrentRoom.TargetMetaRewardsRatio or run.Hero.TargetMetaRewardsRatio) - ( GetNumMetaUpgrades( "RunProgressRewardMetaUpgrade" ) * ( MetaUpgradeData.RunProgressRewardMetaUpgrade.ChangeValue - 1 ) )
--DebugPrint({ Text = "targetMetaRewardsRatio = "..targetMetaRewardsRatio })
local metaProgressChance = targetMetaRewardsRatio
local currentMetaProgressRatio = CalcMetaProgressRatio( run )
if currentMetaProgressRatio ~= nil then
metaProgressChance = metaProgressChance + (run.Hero.TargetMetaRewardsAdjustSpeed * (targetMetaRewardsRatio - currentMetaProgressRatio))
end
--DebugPrint({ Text = "metaProgressChance = "..metaProgressChance })
if RandomChance( metaProgressChance ) then
rewardStoreName = "MetaProgress"
else
rewardStoreName = "RunProgress"
end
--DebugPrint({ Text = "rewardStoreName = "..rewardStoreName })
run.NextRewardStoreName = rewardStoreName
return rewardStoreName
end
function DoUnlockRoomExits( run, room )
-- Synchronize the RNG to its initial state. Makes room reward choices deterministic on save/load
RandomSynchronize()
local rewardsChosen = {}
local rewardStoreName = run.NextRewardStoreName or ChooseNextRewardStore( run )
local rewardStoreOverrides = {}
local exitDoorsIPairs = CollapseTableOrdered( OfferedExitDoors )
for index, door in ipairs( exitDoorsIPairs ) do
if door.Room == nil then
local roomForDoorData = nil
if door.ForceRoomName ~= nil then
roomForDoorData = RoomData[door.ForceRoomName]
else
roomForDoorData = ChooseNextRoomData( run )
end
local roomForDoor = CreateRoom( roomForDoorData, { SkipChooseReward = true, SkipChooseEncounter = true, })
roomForDoor.NeedsReward = true
door.Room = roomForDoor
end
local roomForDoorName = door.Room.GenusName or door.Room.Name
if door.Room.FirstClearRewardStore ~= nil and IsRoomFirstClearOverShrinePointThreshold( GetEquippedWeapon(), run, roomForDoorName ) then
rewardStoreOverrides[index] = door.Room.FirstClearRewardStore
elseif door.Room.ForcedRewardStore ~= nil then
rewardStoreOverrides[index] = door.Room.ForcedRewardStore
end
if rewardStoreOverrides[index] and not Contains( RewardStoreData.InvalidOverrides, rewardStoreOverrides[index] ) then
rewardStoreName = rewardStoreOverrides[index]
end
wait( 0.02 ) -- Distribute workload
end
if run.CurrentRoom.FirstAppearanceNumExitOverrides ~= nil and not HasSeenRoomEarlierInRun( run, run.CurrentRoom.Name ) then
local randomDoors = ShallowCopyTable( exitDoorsIPairs )
for i = 1, run.CurrentRoom.FirstAppearanceNumExitOverrides do
local randomDoor = RemoveRandomValue( randomDoors )
if randomDoor ~= nil and randomDoor.Room ~= nil then
randomDoor.Room.UseOptionalOverrides = true
for key, value in pairs( randomDoor.Room.OptionalOverrides ) do
randomDoor.Room[key] = value
end
end
end
end
for index, door in ipairs( exitDoorsIPairs ) do
local room = door.Room
if room ~= nil and room.NeedsReward then
if Contains( RewardStoreData.InvalidOverrides, rewardStoreOverrides[index] ) then
room.RewardStoreName = rewardStoreOverrides[index] or defaultRewardStoreName
else
room.RewardStoreName = rewardStoreName or defaultRewardStoreName
end
room.ChosenRewardType = ChooseRoomReward( CurrentRun, room, rewardStoreName, rewardsChosen, { Door = door } )
SetupRoomReward( CurrentRun, room, rewardsChosen, { Door = door, IgnoreForceLootName = room.IgnoreForceLootName } )
table.insert( rewardsChosen, { RewardType = room.ChosenRewardType, ForceLootName = room.ForceLootName })
room.NeedsReward = false
if room.UseOptionalOverrides then
for key, value in pairs( room.OptionalOverrides ) do
room[key] = value
end
end
AssignRoomToExitDoor( door, room )
wait( 0.02 ) -- Distribute workload
end
end
if CurrentRun.CurrentRoom.LogShrineClears then
local roomName = CurrentRun.CurrentRoom.GenusName or CurrentRun.CurrentRoom.Name
SetRoomCleared( GetEquippedWeapon(), roomName, CurrentRun.CurrentRoom.ChosenRewardType )
end
wait( 0.02 ) -- Distribute workload
for index, door in ipairs( exitDoorsIPairs ) do
CreateDoorRewardPreview( door )
thread( ExitDoorUnlockedPresentation, door )
door.ReadyToUse = true
end
if CurrentRun.CurrentRoom.ForceFishing and CurrentRun.CurrentRoom.FishingPointId and IsUseable({ Id = CurrentRun.CurrentRoom.FishingPointId }) then
thread( FishingPointAvailablePresentation, CurrentRun.CurrentRoom )
end
if CurrentRun.CurrentRoom.ChallengeSwitch ~= nil then
local challengeSwitch = CurrentRun.CurrentRoom.ChallengeSwitch
local startingValue = challengeSwitch.StartingValue or 0
if challengeSwitch.RewardType == "Health" then
startingValue = startingValue * CalculateHealingMultiplier()
end
if challengeSwitch.RewardType == "Money" and HasHeroTraitValue("BlockMoney") then
startingValue = 0
end
challengeSwitch.StartingValue = round( startingValue )
challengeSwitch.ReadyToUse = true
challengeSwitch.UseText = challengeSwitch.ChallengeAvailableUseText
RefreshUseButton( challengeSwitch.ObjectId, challengeSwitch )
SetAnimation({ Name = challengeSwitch.UnlockedAnimationName, DestinationId = challengeSwitch.ObjectId })
PlaySound({ Name = "/SFX/ChallengeChestUnlocked", Id = challengeSwitch.ObjectId })
end
if CurrentRun.CurrentRoom.WellShop ~= nil then
CurrentRun.CurrentRoom.WellShop.ReadyToUse = true
CurrentRun.CurrentRoom.WellShop.UseText = CurrentRun.CurrentRoom.WellShop.AvailableUseText
RefreshUseButton( CurrentRun.CurrentRoom.WellShop.ObjectId, CurrentRun.CurrentRoom.WellShop )
SetAnimation({ Name = "WellShopUnlocked", DestinationId = CurrentRun.CurrentRoom.WellShop.ObjectId })
PlaySound({ Name = "/SFX/WellShopUnlocked", Id = CurrentRun.CurrentRoom.WellShop.ObjectId })
end
if CurrentRun.CurrentRoom.SellTraitShop ~= nil then
CurrentRun.CurrentRoom.SellTraitShop.ReadyToUse = true
CurrentRun.CurrentRoom.SellTraitShop.UseText = CurrentRun.CurrentRoom.SellTraitShop.AvailableUseText
RefreshUseButton( CurrentRun.CurrentRoom.SellTraitShop.ObjectId, CurrentRun.CurrentRoom.SellTraitShop )
SetAnimation({ Name = "SellTraitShopUnlocked", DestinationId = CurrentRun.CurrentRoom.SellTraitShop.ObjectId })
PlaySound({ Name = "/SFX/WellShopUnlocked", Id = CurrentRun.CurrentRoom.SellTraitShop.ObjectId })
end
StartTriggers( CurrentRun.CurrentRoom, CurrentRun.CurrentRoom.ExitsUnlockedDistanceTriggers )
end
function AssignRoomToExitDoor( door, room )
door.Room = room
OfferedExitDoors[door.ObjectId] = door
AddToGroup({ Id = door.ObjectId, Name = "ExitDoors" })
CurrentRun.CurrentRoom.OfferedRewards = CurrentRun.CurrentRoom.OfferedRewards or {}
if room.ChosenRewardType ~= nil then
CurrentRun.CurrentRoom.OfferedRewards[door.ObjectId] = { Type = room.ChosenRewardType, ForceLootName = room.ForceLootName, UseOptionalOverrides = room.UseOptionalOverrides }
end
if door.AllowReroll and not room.NoReroll and CheckSpecialDoorRequirement( door ) == nil and room.ChosenRewardType ~= "Shop" and IsMetaUpgradeSelected( "RerollMetaUpgrade" ) then
door.CanBeRerolled = true
end
RefreshUseButton( door.ObjectId, door )
end
function LeaveRoom( currentRun, door )
local nextRoom = door.Room
ZeroSuperMeter()
ClearEffect({ Id = currentRun.Hero.ObjectId, All = true, BlockAll = true, })
StopCurrentStatusAnimation( currentRun.Hero )
currentRun.Hero.BlockStatusAnimations = true
AddTimerBlock( currentRun, "LeaveRoom" )
SetPlayerInvulnerable( "LeaveRoom" )
local ammoIds = GetIdsByType({ Name = "AmmoPack" })
SetObstacleProperty({ Property = "Magnetism", Value = 3000, DestinationIds = ammoIds })
SetObstacleProperty({ Property = "MagnetismSpeedMax", Value = currentRun.Hero.LeaveRoomAmmoMangetismSpeed, DestinationIds = ammoIds })
StopAnimation({ DestinationIds = ammoIds, Name = "AmmoReturnTimer" })
RunUnthreadedEvents( currentRun.CurrentRoom.LeaveUnthreadedEvents, currentRun.CurrentRoom )
if IsRecordRunDepth( currentRun ) then
thread( PlayVoiceLines, GlobalVoiceLines.RecordRunDepthVoiceLines )
end
ResetObjectives()
if currentRun.CurrentRoom.ChallengeEncounter ~= nil and currentRun.CurrentRoom.ChallengeEncounter.InProgress then
currentRun.CurrentRoom.ChallengeEncounter.EndedEarly = true
currentRun.CurrentRoom.ChallengeEncounter.InProgress = false
thread( PlayVoiceLines, HeroVoiceLines.FleeingEncounterVoiceLines, false )
end
if currentRun.CurrentRoom.CloseDoorsOnUse then
CloseDoorForRun( currentRun, door )
end
RemoveRallyHealth()
if not nextRoom.BlockDoorHealFromPrevious then
CheckDoorHealTrait( currentRun )
end
local removedTraits = {}
for _, trait in pairs( currentRun.Hero.Traits ) do
if trait.RemainingUses ~= nil and trait.UsesAsRooms ~= nil and trait.UsesAsRooms then
UseTraitData( currentRun.Hero, trait )
if trait.RemainingUses ~= nil and trait.RemainingUses <= 0 then
table.insert( removedTraits, trait )
end
end
end
for _, trait in pairs( removedTraits ) do
RemoveTraitData( currentRun.Hero, trait )
end
local exitFunctionName = currentRun.CurrentRoom.ExitFunctionName or door.ExitFunctionName or "LeaveRoomPresentation"
local exitFunction = _G[exitFunctionName]
exitFunction( currentRun, door )
TeardownRoomArt( currentRun, currentRoom )
if not currentRun.Hero.IsDead then
--On Zag death cleanup is already processed
CleanupEnemies()
end
killTaggedThreads( RoomThreadName )
killWaitUntilThreads( "RequiredKillEnemyKilledOrSpawned" )
killWaitUntilThreads( "AllRequiredKillEnemiesDead" ) -- Can exist for a TimeChallenge encounter
killWaitUntilThreads( "RequiredEnemyKilled" ) -- Can exist for a TimeChallenge encounter
RemoveTimerBlock( currentRun, "LeaveRoom" )
if currentRun.CurrentRoom.TimerBlock ~= nil then
RemoveTimerBlock( currentRun, currentRun.CurrentRoom.TimerBlock )
end
SetPlayerVulnerable( "LeaveRoom" )
if currentRun.CurrentRoom.SkipLoadNextMap then
return
end
MoneyObjects = {}
OfferedExitDoors = {}
local flipMap = false
if currentRun.CurrentRoom.ExitDirection ~= nil and nextRoom.EntranceDirection ~= nil and nextRoom.EntranceDirection ~= "LeftRight" then
flipMap = nextRoom.EntranceDirection ~= currentRun.CurrentRoom.ExitDirection
else
flipMap = RandomChance( nextRoom.FlipHorizontalChance or 0.5 )
end
nextRoom.Flipped = flipMap
if nextRoom.Encounter == nil then
nextRoom.Encounter = ChooseEncounter( CurrentRun, nextRoom )
RecordEncounter( CurrentRun, nextRoom.Encounter )
end
currentRun.CurrentRoom.EndingHealth = currentRun.Hero.Health
currentRun.CurrentRoom.EndingAmmo = GetWeaponProperty({ Id = currentRun.Hero.ObjectId, WeaponName = "RangedWeapon", Property = "Ammo" })
table.insert( currentRun.RoomHistory, currentRun.CurrentRoom )
UpdateRunHistoryCache( currentRun, currentRun.CurrentRoom )
local previousRoom = currentRun.CurrentRoom
currentRun.CurrentRoom = nextRoom
RunShopGeneration( currentRun.CurrentRoom )
GameState.LocationName = nextRoom.LocationText
RandomSetNextInitSeed()
if not nextRoom.SkipSave then
SaveCheckpoint({ StartNextMap = nextRoom.Name, SaveName = "_Temp", DevSaveName = CreateDevSaveName( currentRun ) })
ValidateCheckpoint({ Value = true })
end
RemoveInputBlock({ Name = "MoveHeroToRoomPosition" })
AddInputBlock({ Name = "MapLoad" })
LoadMap({ Name = nextRoom.Name, ResetBinks = previousRoom.ResetBinksOnExit or currentRun.CurrentRoom.ResetBinksOnEnter, LoadBackgroundColor = currentRun.CurrentRoom.LoadBackgroundColor })
end
function RecordEncounter( run, encounter )
run.EncountersOccurredCache[encounter.Name] = (run.EncountersOccurredCache[encounter.Name] or 0) + 1
GameState.EncountersOccurredCache[encounter.Name] = (GameState.EncountersOccurredCache[encounter.Name] or 0) + 1
run.EncountersOccurredBiomeCache[encounter.Name] = (run.EncountersOccurredBiomeCache[encounter.Name] or 0) + 1
run.EncountersDepthCache[encounter.Name] = run.RunDepthCache
end
function StartTriggers( triggerSource, triggers )
if triggerSource == nil or triggers == nil then
return
end
for k, trigger in ipairs( triggers ) do
thread( CheckDistanceTrigger, trigger, triggerSource )
end
end
function RunEvents( eventSource )
RunThreadedEvents( eventSource.ThreadedEvents, eventSource )
RunUnthreadedEvents( eventSource.PreUnthreadedEvents, eventSource )
RunUnthreadedEvents( eventSource.UnthreadedEvents, eventSource )
RunUnthreadedEvents( eventSource.PostUnthreadedEvents, eventSource )
end
function RunThreadedEvents( events, eventSource )
if events == nil then
return
end
for k, event in ipairs( events ) do
if type(event) == "table" then
if event.GameStateRequirements == nil or IsGameStateEligible( CurrentRun, event.GameStateRequirements ) then
if event.FunctionName ~= nil then
local eventFunction = _G[event.FunctionName]
DebugAssert({ Condition = eventFunction ~= nil, Text = "Unthreaded event function '"..event.FunctionName.."' is not defined.'" })
if eventFunction ~= nil then
manualFunctionCall( event.FunctionName, event.Args )
thread( eventFunction, eventSource, event.Args )
end
elseif event.Function ~= nil then
thread( event.Function, eventSource, event.Args )
end
end
else
thread( event, eventSource )
end
end
end
function RunUnthreadedEvents( events, eventSource )
if events == nil then
return
end
for k, event in ipairs( events, eventSource ) do
if type(event) == "table" then
if event.GameStateRequirements == nil or IsGameStateEligible( CurrentRun, event.GameStateRequirements ) then
if event.FunctionName ~= nil then
manualFunctionCall( event.FunctionName, event.Args )
local eventFunction = _G[event.FunctionName]
DebugAssert({ Condition = eventFunction ~= nil, Text = "Unthreaded event function '"..event.FunctionName.."' is not defined.'" })
if eventFunction ~= nil then
eventFunction( eventSource, event.Args )
if event.BreakIfPlayed then
return
end
end
elseif event.Function ~= nil then
event.Function( eventSource, event.Args )
if event.BreakIfPlayed then
return
end
end
end
else
event( eventSource )
if event.BreakIfPlayed then
return
end
end
end
end
function CheckDistanceTriggerThread( source, args )
thread( CheckDistanceTrigger, args, source )
end
function CheckDistanceTrigger( trigger, triggerSource, id )
local currentRun = CurrentRun
trigger.Name = trigger.Name or triggerSource.Name..(triggerSource.ObjectId or trigger.TriggerGroup or trigger.TriggerObjectType)..(id or "")
local triggerIds = trigger.TriggerIds or { trigger.TriggerId }
if triggerSource.ObjectId ~= nil then
triggerIds = { triggerSource.ObjectId }
end
if trigger.TriggerGroup ~= nil or trigger.TriggerGroups ~= nil then
triggerIds = GetIds({ Name = trigger.TriggerGroup, Names = trigger.TriggerGroups })
elseif trigger.TriggerObjectType ~= nil then
triggerIds = GetIdsByType({ Name = trigger.TriggerObjectType })
end
local actualSource = nil
if triggerIds ~= nil then
triggerId = triggerIds[1]
actualSource = ActiveEnemies[triggerId] -- @refactor Return the actual table from the notify. Need to change so the real source comes through to begin with
end
if actualSource ~= nil and actualSource.NextInteractLines ~= nil and actualSource.NextInteractLines.BlockDistanceTriggers then
return
end
if not IsDistanceTriggerEligible( currentRun, trigger, trigger.GameStateRequirements ) then
return
end
if trigger.PreTriggerVfx ~= nil then
CreateAnimation({ Name = trigger.PreTriggerVfx, DestinationId = triggerSource.ObjectId, OffsetX = triggerSource.AnimOffsetX, OffsetZ = triggerSource.AnimOffsetZ, Group = "Combat_UI_World" })
end
if trigger.PreTriggerAnimation ~= nil then
SetAnimation({ Name = trigger.PreTriggerAnimation, DestinationId = triggerSource.ObjectId })
end
if trigger.PreTriggerSound ~= nil then
triggerSource.PreTriggerSoundId = PlaySound({ Name = trigger.PreTriggerSound, Id = triggerSource.ObjectId })
end
if trigger.PreTriggerSetFlagTrue ~= nil then
GameState.Flags[trigger.PreTriggerSetFlagTrue] = true
end
if trigger.PreTriggerSetFlagFalse ~= nil then
GameState.Flags[trigger.PreTriggerSetFlagFalse] = false
end
if trigger.PreTriggerFunctionName ~= nil then
local preTriggerFunction = _G[trigger.PreTriggerFunctionName]
preTriggerFunction( actualSource or triggerSource, trigger.PreTriggerFunctionArgs )
end
local notifiedIds = {}
local triggeredOnce = false
while not triggeredOnce or trigger.Repeat do
triggeredOnce = true
if triggerSource.ObjectId ~= nil then
triggerIds = { triggerSource.ObjectId }
end
if trigger.TriggerGroup ~= nil then
triggerIds = GetIds({ Name = trigger.TriggerGroup })
elseif trigger.TriggerObjectType ~= nil then
triggerIds = GetIdsByType({ Name = trigger.TriggerObjectType })
end
if trigger.RemoveNotifiedIds then
for id, v in pairs( notifiedIds ) do
RemoveValueAndCollapse( triggerIds, id )
end
end
local notifiedById = 0
if trigger.OutsideDistance ~= nil then
local notifyName = "OutsideDistance"..trigger.Name
NotifyOutsideDistanceAll({ Id = currentRun.Hero.ObjectId, DestinationNames = trigger.TriggerGroup, DestinationIds = triggerIds, Distance = trigger.OutsideDistance, ScaleY = trigger.ScaleY or 1.0, Notify = notifyName })
waitUntil( notifyName )
notifiedById = NotifyResultsTable[notifyName]
end
if trigger.WithinDistance ~= nil then
local notifyName = "WithinDistance"..trigger.Name
local destinationNames = { trigger.TriggerGroup }
if trigger.TriggerGroups ~= nil then
destinationNames = CombineTables( destinationNames, trigger.TriggerGroups )
end
NotifyWithinDistanceAny({ Ids = { currentRun.Hero.ObjectId }, DestinationNames = destinationNames, DestinationIds = triggerIds, Distance = trigger.WithinDistance, ScaleY = trigger.ScaleY or 1.0, Notify = notifyName })
waitUntil( notifyName )
notifiedById = NotifyResultsTable[notifyName]
end
currentRun.TriggerRecord[trigger.Name] = true
notifiedIds[notifiedById] = true
if trigger.ActivateGroup ~= nil then
Activate({ Name = trigger.ActivateGroup })
local ids = GetIds({ Name = trigger.ActivateGroup })
for k, id in pairs( ids ) do
local name = GetName({ Id = id })
local enemyData = EnemyData[name]
if enemyData ~= nil then
local newEnemy = DeepCopyTable( enemyData )
newEnemy.ObjectId = id
SetupEnemyObject( newEnemy, currentRun )
end
end
end
if trigger.DeleteGroup ~= nil then
Destroy({ Ids = GetIds({ Name = trigger.DeleteGroup }) })
end
local triggerId = nil
local triggerTable = nil
if triggerIds ~= nil then
triggerId = triggerIds[1]
triggerTable = ActiveEnemies[triggerId] -- @refactor Return the actual table from the notify
end
if trigger.ChanceToTrigger ~= nil and not RandomChance( trigger.ChanceToTrigger ) then
break
end
if trigger.VoiceLines ~= nil then
thread( PlayVoiceLines, trigger.VoiceLines, nil, triggerTable )
end
if trigger.GlobalVoiceLines ~= nil then
thread( PlayVoiceLines, GlobalVoiceLines[trigger.GlobalVoiceLines], true, triggerTable )
end
if trigger.Stinger ~= nil then
PlaySound({ Name = trigger.Stinger, Delay = 0.5 })
end
if trigger.LockToCharacter ~= nil then
for k, id in pairs( triggerIds ) do
local name = GetName({ Id = id })
local npcData = EnemyData[name]
if npcData.Quip ~= nil then
PlaySound({ Name = npcData.Quip, Id = id })
end
if npcData.HailAnimation ~= nil then
SetAnimation({ Name = npcData.HailAnimation, DestinationId = id })
end
thread( PanToTargetAndBack, id )
end
end
if trigger.SetFlagTrue ~= nil then
GameState.Flags[trigger.SetFlagTrue] = true
end
if trigger.SetFlagFalse ~= nil then
GameState.Flags[trigger.SetFlagFalse] = false
end
if trigger.FunctionName ~= nil then
local triggerFunction = _G[trigger.FunctionName]
manualFunctionCall( trigger.FunctionName, trigger.Args )
thread( triggerFunction, triggerSource, trigger.Args )
elseif trigger.Function ~= nil then
thread( trigger.Function, triggerSource, trigger.Args )
end
if trigger.PresentDevotionChoice ~= nil then
thread( DevotionTestPresentation )
end
-- AlphaObjectOut/In are for a visual prototype & should be replaced; used in DOF experiment in RoomStoryOutside_01
if trigger.AlphaObjectOut ~= nil then
SetAlpha({ Id = trigger.AlphaObjectOut, Fraction = 0.0, Duration = 1.0 })
end
if trigger.AlphaObjectIn ~= nil then
if trigger.AlphaObjectIn == "Self" then
SetAlpha({ Id = notifiedById, Fraction = 1.0, Duration = 1.0 })
else
SetAlpha({ Id = trigger.AlphaObjectIn, Fraction = 1.0, Duration = 1.0 })
end
end
if trigger.ShakeSelf ~= nil then
Flash({ Id = triggerSource.ObjectId, Speed = 1, MinFraction = 0.8, MaxFraction = 0.0, Color = Color.White, ExpireAfterCycle = true })
Shake({ Id = triggerSource.ObjectId, Distance = 3, Speed = 150, Duration = 0.65 })
end
if trigger.PostTriggerVfx ~= nil then
CreateAnimation({ Name = trigger.PostTriggerVfx, DestinationId = triggerSource.ObjectId, OffsetX = triggerSource.AnimOffsetX, OffsetZ = triggerSource.AnimOffsetZ, Group = "Combat_UI_World" })
end
if trigger.PostTriggerAnimation ~= nil then
SetAnimation({ Name = trigger.PostTriggerAnimation, DestinationId = triggerSource.ObjectId })
end
if trigger.PostTriggerAngleTowardTarget ~= nil then
AngleTowardTarget({ Id = triggerSource.ObjectId, DestinationId = CurrentRun.Hero.ObjectId })
end
if trigger.PreTriggerVfx ~= nil then
StopAnimation({ Name = trigger.PreTriggerVfx, DestinationId = triggerSource.ObjectId })
end
if trigger.PreTriggerSound ~= nil then
StopSound({ Id = triggerSource.PreTriggerSoundId, Duration = 0.2 })
end
if trigger.InCombatText ~= nil then
trigger.InCombatText.TargetId = triggerSource.ObjectId
thread( InCombatTextArgs, trigger.InCombatText )
end
if trigger.StatusAnimation ~= nil then
PlayStatusAnimation( triggerSource, { Animation = trigger.StatusAnimation } )
end
if trigger.Emote ~= nil then
PlayEmote( { TargetId = triggerId, AnimationName = trigger.Emote, OffsetZ = triggerSource.EmoteOffsetZ } )
end
if trigger.PostTriggerFunctionName ~= nil then
local postTriggerFunction = _G[trigger.PostTriggerFunctionName]
postTriggerFunction( triggerSource, trigger.PostTriggerFunctionArgs )
end
if trigger.Repeat then
wait(0.01)
local repeatBufferDistance = 10
if trigger.OutsideDistance ~= nil then
local notifyName = "OutsideDistanceRepeatBuffer"..(trigger.Name or trigger.TriggerGroup or trigger.TriggerObjectType)
NotifyWithinDistanceAny({ Ids = { currentRun.Hero.ObjectId }, DestinationNames = { trigger.TriggerGroup }, DestinationIds = triggerIds, Distance = trigger.OutsideDistance - repeatBufferDistance, Notify = notifyName })
waitUntil( notifyName )
end
if trigger.WithinDistance ~= nil then
local notifyName = "WithinDistanceRepeatBuffer"..(trigger.Name or trigger.TriggerGroup or trigger.TriggerObjectType)
NotifyOutsideDistanceAll({ Id = currentRun.Hero.ObjectId, DestinationNames = trigger.TriggerGroup, DestinationIds = triggerIds, Distance = trigger.WithinDistance + repeatBufferDistance, Notify = notifyName })
waitUntil( notifyName )
end
wait(0.01)
if trigger.OnRepeatSetFlagTrue ~= nil then
GameState.Flags[trigger.OnRepeatSetFlagTrue] = true
end
if trigger.OnRepeatSetFlagFalse ~= nil then
GameState.Flags[trigger.OnRepeatSetFlagFalse] = false
end
if trigger.OnRepeatFunctionName ~= nil then
local onRepeatFunction = _G[trigger.OnRepeatFunctionName]
onRepeatFunction( triggerSource, trigger.OnRepeatFunctionArgs )
end
elseif trigger.LeaveDistanceBuffer ~= nil then
if trigger.OutsideDistance ~= nil then
local notifyName = "OutsideDistanceRepeatBuffer"..(trigger.TriggerGroup or trigger.TriggerObjectType)
NotifyWithinDistanceAny({ Ids = { currentRun.Hero.ObjectId }, DestinationNames = { trigger.TriggerGroup }, DestinationIds = triggerIds, Distance = trigger.OutsideDistance - trigger.LeaveDistanceBuffer, Notify = notifyName })
waitUntil( notifyName )
end
if trigger.WithinDistance ~= nil then
local notifyName = "WithinDistanceRepeatBuffer"..(trigger.TriggerGroup or trigger.TriggerObjectType)
NotifyOutsideDistanceAll({ Ids = { currentRun.Hero.ObjectId }, DestinationNames = { trigger.TriggerGroup }, DestinationIds = triggerIds, Distance = trigger.WithinDistance + trigger.LeaveDistanceBuffer, Notify = notifyName })
waitUntil( notifyName )
end
thread( PlayVoiceLines, trigger.LeaveVoiceLines, nil, triggerTable )
end
end
end
function IsDistanceTriggerEligible( currentRun, trigger, requirements )
if not IsGameStateEligible( currentRun, trigger, requirements ) then
return false
end
if trigger.TriggerOnceThisRun and currentRun.TriggerRecord[trigger.Name] then
return false
end
return true
end
function PanToTargetAndBack( targetId )
PanCamera({ Ids = { targetId, CurrentRun.Hero.ObjectId }, Duration = 1.5, EaseIn = 0.05, EaseOut = 0.3 })
wait(3.0)
LockCamera({ Id = CurrentRun.Hero.ObjectId, Duration = 1.25 })
end
function AssignObstacles( eventSource )
if eventSource.ObstacleData == nil then
return
end
for id, obstacleData in pairs( eventSource.ObstacleData ) do
local obstacle = DeepCopyTable( obstacleData )
obstacle.ObjectId = id
if obstacle.Template ~= nil and ObstacleData[obstacle.Template] ~= nil then
obstacle = MergeTables( ObstacleData[obstacle.Template], obstacle )
end
SetupObstacle( obstacle )
end
end
function PlayStatusAnimation( source, args )
local animation = args.Animation
if not ConfigOptionCache.ShowUIAnimations then
return
end
if source.BlockStatusAnimations then
return
end
if animation == source.StatusAnimation then
-- Already playing this
return
end
if not IsAlive({ Id = source.ObjectId }) then
return
end
if source.StatusAnimation ~= nil then
StopAnimation({ Name = source.StatusAnimation, DestinationId = source.ObjectId })
end
source.PrevStatusAnimation = source.StatusAnimation
source.StatusAnimation = animation
CreateAnimation({ Name = animation, DestinationId = source.ObjectId, OffsetX = source.AnimOffsetX, OffsetZ = source.AnimOffsetZ, Group = "Combat_UI_World" })
end
function StopCurrentStatusAnimation( source )
if source.StatusAnimation ~= nil then
StopAnimation({ Name = source.StatusAnimation, DestinationId = source.ObjectId })
source.StatusAnimation = nil
end
end
function StopStatusAnimation( source, animation )
if not ConfigOptionCache.ShowUIAnimations then
return false
end
if source == nil then
return false
end
if animation == nil then
animation = source.StatusAnimation
else
if animation ~= source.StatusAnimation then
-- Not the one currently playing
return false
end
end
if animation ~= nil then
StopAnimation({ Name = animation, DestinationId = source.ObjectId })
end
source.StatusAnimation = nil
if source.PrevStatusAnimation ~= nil and source.PrevStatusAnimation ~= source.StatusAnimation then
source.StatusAnimation = source.PrevStatusAnimation
if IsAlive({ Id = source.ObjectId }) then
CreateAnimation({ Name = source.PrevStatusAnimation, DestinationId = source.ObjectId, OffsetX = source.AnimOffsetX, OffsetZ = source.AnimOffsetZ, Group = "Combat_UI_World" })
source.PrevStatusAnimation = nil
end
end
return true
end
function HandleSecretSpawns( currentRun )
local currentRoom = currentRun.CurrentRoom
RandomSynchronize( 13 )
local secretPointIds = GetIdsByType({ Name = "SecretPoint" })
-- Secret Door
if not IsEmpty( secretPointIds ) and IsSecretDoorEligible( currentRun, currentRoom ) then
currentRoom.ForceSecretDoor = true
UseHeroTraitsWithValue("ForceSecretDoor", true)
local secretRoomData = ChooseNextRoomData( currentRun, { RoomDataSet = RoomSetData.Secrets } )
if secretRoomData ~= nil then
local secretPointId = RemoveRandomValue( secretPointIds )
local secretDoor = DeepCopyTable( ObstacleData.SecretDoor )
secretDoor.ObjectId = SpawnObstacle({ Name = "SecretDoor", Group = "FX_Terrain", DestinationId = secretPointId, AttachedTable = secretDoor })
SetupObstacle( secretDoor )
secretDoor.HealthCost = GetSecretDoorCost()
local secretRoom = CreateRoom( secretRoomData )
AssignRoomToExitDoor( secretDoor, secretRoom )
secretDoor.OnUsedPresentationFunctionName = "SecretDoorUsedPresentation"
currentRun.LastSecretDepth = GetRunDepth( currentRun )
end
end
if not IsEmpty( secretPointIds ) and IsShrinePointDoorEligible( currentRun, currentRoom ) then
currentRoom.ForceShrinePointDoor = true
local shrinePointRoomOptions = currentRoom.ShrinePointRoomOptions or RoomSetData.Base.BaseRoom.ShrinePointRoomOptions
local shrinePointRoomName = GetRandomValue(shrinePointRoomOptions)
local shrinePointRoomData = RoomSetData.Base[shrinePointRoomName]
if shrinePointRoomData ~= nil then
local secretPointId = RemoveRandomValue( secretPointIds )
local shrinePointDoor = DeepCopyTable( ObstacleData.ShrinePointDoor )
shrinePointDoor.ObjectId = SpawnObstacle({ Name = "ShrinePointDoor", Group = "FX_Terrain", DestinationId = secretPointId, AttachedTable = shrinePointDoor })
SetupObstacle( shrinePointDoor )
shrinePointDoor.ShrinePointReq = currentRoom.ShrinePointDoorCost or ( shrinePointDoor.CostBase + ( shrinePointDoor.CostPerDepth * (currentRun.RunDepthCache - 1) ) )
local activeShrinePoints = GetTotalSpentShrinePoints()
local costFontColor = Color.CostAffordable
if shrinePointDoor.ShrinePointReq > activeShrinePoints then
costFontColor = Color.CostUnaffordable
end
local shrinePointRoom = CreateRoom( shrinePointRoomData, { SkipChooseReward = true } )
shrinePointRoom.NeedsReward = true
AssignRoomToExitDoor( shrinePointDoor, shrinePointRoom )
shrinePointDoor.OnUsedPresentationFunctionName = "ShrinePointDoorUsedPresentation"
currentRun.LastShrinePointDoorDepth = GetRunDepth( currentRun )
end
end
local challengeBaseIds = GetIdsByType({ Name = "ChallengeSwitchBase" })
-- Challenge Switch
if not IsEmpty( challengeBaseIds ) and IsChallengeSwitchEligible( currentRun, TableLength( challengeBaseIds )) then
local hasForceTrait = HasHeroTraitValue("ForceChallengeSwitch")
currentRoom.ForceChallengeSwitch = true
UseHeroTraitsWithValue("ForceChallengeSwitch", true)
local challengeBaseId = RemoveRandomValue( challengeBaseIds )
local challengeOptions = {}
for k, challengeName in pairs( EncounterSets.ChallengeOptions ) do
local challengeData = ObstacleData[challengeName]
if challengeData.Requirements == nil or IsGameStateEligible( CurrentRun, challengeData, challengeData.Requirements ) then
table.insert( challengeOptions, challengeName )
end
end
if not IsEmpty( challengeOptions ) then
local challengeType = GetRandomValue( challengeOptions )
local challengeSwitch = DeepCopyTable( ObstacleData[challengeType] )
currentRoom.ChallengeSwitch = challengeSwitch
challengeSwitch.ObjectId = challengeBaseId
local offsetX = challengeSwitch.TextAnchorIdOffsetX
if IsHorizontallyFlipped({ Id = challengeSwitch.ObjectId }) then
offsetX = offsetX * -1
end
challengeSwitch.TextAnchorId = SpawnObstacle({ Name = "BlankObstacle", Group = "Standing", DestinationId = challengeBaseId })
Attach({ Id = challengeSwitch.TextAnchorId, DestinationId = challengeBaseId, OffsetX = offsetX, OffsetY = challengeSwitch.TextAnchorIdOffsetY, OffsetZ = challengeSwitch.TextAnchorIdOffsetZ })
SetThingProperty({ Property = "SortMode", Value = "FromParent", DestinationId = challengeSwitch.TextAnchorId })
local challengeEncounter = ChooseChallengeEncounter(currentRoom)
currentRoom.ChallengeEncounter = challengeEncounter
challengeEncounter.Switch = challengeSwitch
challengeEncounter.SpawnNearId = challengeSwitch.ObjectId
local rewardMultiplier = challengeSwitch.RewardMultiplier or 1
local startingValue = rewardMultiplier * challengeEncounter.StartingValue * (1 + challengeEncounter.ValueDepthRamp * GetRunDepth(CurrentRun)) * GetTotalHeroTraitValue("ChallengeRewardIncrease", {IsMultiplier = true})
challengeSwitch.StartingValue = round( startingValue )
challengeSwitch.ValueTextAnchor = SpawnObstacle({ Name = "BlankObstacle", DestinationId = challengeSwitch.ObjectId })
Attach({ Id = challengeSwitch.ValueTextAnchor, DestinationId = challengeSwitch.ObjectId })
CreateTextBox({ Id = challengeSwitch.ValueTextAnchor, Text = challengeSwitch.ChallengeText, LuaKey = "Amount", OffsetX = -40 , OffsetY = -220, LuaValue = startingValue, Font = "FellType", FontSize = 40, Color = Color.White, OutlineThickness = 1, OutlineColor = {0.0, 0.0, 0.0,1}, TextSymbolScale = 0.5, })
ModifyTextBox({ Id = challengeSwitch.ValueTextAnchor, FadeTarget = 0, FadeDuration = 0 })
if challengeSwitch.KeyCost == nil and challengeSwitch.KeyCostMin ~= nil and challengeSwitch.KeyCostMax ~= nil then
challengeSwitch.KeyCost = RandomInt(challengeSwitch.KeyCostMin, challengeSwitch.KeyCostMax)
end
SetupObstacle( challengeSwitch )
SetAnimation({ DestinationId = challengeSwitch.ObjectId, Name = challengeSwitch.LockedAnimationName })
UseableOn({ Id = challengeSwitch.ObjectId })
if challengeSwitch.SpawnPropertyChanges ~= nil then
ApplyUnitPropertyChanges( challengeSwitch, challengeSwitch.SpawnPropertyChanges, true )
end
currentRun.LastChallengeDepth = currentRun.RunDepthCache
challengeBaseId = nil
end
end
-- Well Shop
if not IsEmpty( challengeBaseIds ) and IsWellShopEligible( currentRun, currentRoom ) then
currentRoom.ForceWellShop = true
local challengeBaseId = RemoveRandomValue( challengeBaseIds )
currentRoom.WellShop = DeepCopyTable( ObstacleData.WellShop )
currentRoom.WellShop.ObjectId = challengeBaseId
SetupObstacle( currentRoom.WellShop )
SetAnimation({ DestinationId = currentRoom.WellShop.ObjectId, Name = "WellShopLocked" })
UseableOn({ Id = currentRoom.WellShop.ObjectId })
if currentRoom.WellShop.SpawnPropertyChanges ~= nil then
ApplyUnitPropertyChanges( currentRoom.WellShop, currentRoom.WellShop.SpawnPropertyChanges, true )
end
currentRun.LastWellShopDepth = currentRun.RunDepthCache
challengeBaseId = nil
end
-- Sell Trait Shop
if not IsEmpty( challengeBaseIds ) and IsSellTraitShopEligible( currentRun, currentRoom ) then
currentRoom.ForceSellTraitShop = true
local challengeBaseId = RemoveRandomValue( challengeBaseIds )
currentRoom.SellTraitShop = DeepCopyTable( ObstacleData.SellTraitShop )
currentRoom.SellTraitShop.ObjectId = challengeBaseId
SetupObstacle( currentRoom.SellTraitShop)
SetAnimation({ DestinationId = currentRoom.SellTraitShop.ObjectId, Name = "SellTraitShopLocked" })
UseableOn({ Id = currentRoom.SellTraitShop.ObjectId })
if currentRoom.SellTraitShop.SpawnPropertyChanges ~= nil then
ApplyUnitPropertyChanges( currentRoom.SellTraitShop, currentRoom.SellTraitShop.SpawnPropertyChanges, true )
end
currentRun.LastSellTraitShopDepth = currentRun.RunDepthCache
challengeBaseId = nil
GenerateSellTraitShop( currentRun, currentRoom )
end
-- Fishing
local fishingPoints = GetInactiveIdsByType({ Name = "FishingPoint" })
if not IsEmpty( fishingPoints ) and IsFishingEligible( currentRun, currentRoom ) then
currentRoom.ForceFishing = true
UseHeroTraitsWithValue("ForceFishingPoint", true)
CurrentRun.CurrentRoom.FishingPointId = GetRandomValue(fishingPoints)
Activate({ Id = CurrentRun.CurrentRoom.FishingPointId })
currentRun.LastFishingPointDepth = GetRunDepth( currentRun )
end
end
function IsFishingEligible( currentRun, currentRoom )
if currentRoom.PersistentFishing and CurrentRun.RoomCreations[currentRoom.Name] then
for roomIndex = #CurrentRun.RoomHistory, 1, -1 do
local room = CurrentRun.RoomHistory[roomIndex]
if room.Name == currentRoom.Name and room.CompletedFishing then
return false
end
end
end
if currentRoom.ForceFishing then
return true
end
if currentRoom.FishingPointForceRequirements ~= nil then
for k, requirements in pairs(currentRoom.FishingPointForceRequirements) do
if IsGameStateEligible( currentRun, currentRoom.FishingPointForceRequirements ) then
return true
end
end
end
if HasHeroTraitValue( "ForceFishingPoint" ) then
return true
end
if not currentRoom.FishingPointChanceSuccess then
return false
end
if not IsGameStateEligible( currentRun, currentRoom.FishingPointRequirements ) and not HeroHasTrait("FishingTrait") then
return false
end
return true
end
function IsSecretDoorEligible( currentRun, currentRoom )
if currentRoom.ForceSecretDoor then
return true
end
if HasHeroTraitValue( "ForceSecretDoor" ) then
return true
end
if not currentRoom.SecretChanceSuccess then
return false
end
if not IsGameStateEligible( currentRun, currentRoom.SecretDoorRequirements ) then
return false
end
return true
end
function IsShrinePointDoorEligible( currentRun, currentRoom )
if currentRoom.ForceShrinePointDoor then
return true
end
if HasHeroTraitValue( "ForceShrinePointDoor" ) then
return true
end
if not currentRoom.ShrinePointDoorChanceSuccess then
return false
end
if not IsGameStateEligible( currentRun, currentRoom.ShrinePointDoorRequirements ) then
return false
end
return true
end
function IsChallengeSwitchEligible( currentRun, numPedestals )
local currentRoom = currentRun.CurrentRoom
if numPedestals ~= nil then
local reservedPedestals = 0
if currentRoom.ForceWellShop then
reservedPedestals = reservedPedestals + 1
end
if currentRoom.ForceSellTraitShop then
reservedPedestals = reservedPedestals + 1
end
if numPedestals <= reservedPedestals then
return false
end
end
if currentRoom.ForceChallengeSwitch then
return true
end
if HasHeroTraitValue("ForceChallengeSwitch") then
return true
end
if not currentRoom.ChallengeChanceSuccess then
return false
end
if not IsGameStateEligible( currentRun, currentRoom.ChallengeSwitchRequirements ) then
return false
end
return true
end
function IsWellShopEligible( currentRun, currentRoom )
if currentRoom.ForceWellShop then
return true
end
if not IsGameStateEligible( currentRun, currentRoom.WellShopRequirements ) then
return false
end
if not currentRoom.WellShopChanceSuccess then
return false
end
return true
end
function IsSellTraitShopEligible( currentRun, currentRoom )
if currentRoom.ForceSellTraitShop then
return true
end
if not IsGameStateEligible( currentRun, currentRoom.SellTraitShopRequirements ) then
return false
end
if not currentRoom.SellTraitShopChanceSuccess then
return false
end
return true
end
function CreateVignette()
if not GetConfigOptionValue({ Name = "DrawBloom" }) then
return
end
ScreenAnchors.Vignette = CreateScreenObstacle({ Name = "BlankObstacle", X = ScreenCenterX, Y = ScreenCenterY, Group = "Vignette" })
SetAnimation({ Name = "VignetteOverlay", DestinationId = ScreenAnchors.Vignette })
end
function DestroyRequiredKills( args )
args = args or {}
local currentEnemies = ShallowCopyTable( RequiredKillEnemies )
for k, enemy in pairs( currentEnemies ) do
if args.SkipIds ~= nil and Contains( args.SkipIds, enemy.ObjectId ) then
-- Skip
else
if args.BlockLoot then
enemy.MoneyDropOnDeath = nil
enemy.AmmoDropOnDeath = nil
enemy.MetaPointDropOnDeath = nil
end
if args.BlockDeathWeapons then
SetUnitProperty({ Property = "OnDeathWeapon", Value = "null", DestinationId = enemy.ObjectId })
end
thread( Kill, enemy )
wait( destroyInterval, RoomThreadName )
end
end
if args.DestroyInterval ~= nil then
DestroyRequiredKills( blockLoot ) -- Call again w/ no interval in case something spawned while waiting (from spawners)
end
end
function DestroyHadesFightObstacles()
for k, enemy in pairs( ActiveEnemies ) do
if enemy.Name == "HadesAmmo" or enemy.Name == "HadesTombstone" then
SetUnitProperty({ Property = "OnDeathWeapon", Value = "null", DestinationId = enemy.ObjectId })
thread( Kill, enemy )
end
end
end
function DisableTrap( enemy )
if enemy.ToggleTrap then
enemy.DisableAIWhenReady = true
if enemy.DisabledAnimation ~= nil then
SetAnimation({ DestinationId = enemy.ObjectId, Name = enemy.DisabledAnimation })
end
elseif enemy.DestroyOnTrapDisable then
Kill( enemy )
end
end
function EnableTrap( enemy )
if enemy.ToggleTrap and enemy.AIDisabled then
enemy.AIDisabled = false
if enemy.AIOptions ~= nil and not IsEmpty(enemy.AIOptions) then
thread(SetAI, GetRandomValue(enemy.AIOptions), enemy, CurrentRun)
end
elseif enemy.DisableAIWhenReady then
if enemy.IdleAnimation ~= nil then
SetAnimation({ Name = enemy.IdleAnimation, DestinationId = enemy.ObjectId })
end
enemy.DisableAIWhenReady = false
end
end
function DisableRoomTraps()
CurrentRun.CurrentRoom.BlockDisableTraps = true
for enemyId, enemy in pairs( ActiveEnemies ) do
DisableTrap( enemy )
end
ExpireProjectiles({ Name = "SmokeTrapWeapon" })
end
function EnableRoomTraps( )
CurrentRun.CurrentRoom.BlockDisableTraps = false
for enemyId, enemy in pairs( ActiveEnemies ) do
EnableTrap(enemy)
end
end
function ConsecrationFieldDeath( triggerArgs, traitDataArgs )
if triggerArgs.name == "ConsecrationField" then
CurrentRun.CurrentRoom.BlockDisableTraps = nil
end
end
function DisableTraps( owner, weaponData, args )
local disabledTraps = {}
if CurrentRun.CurrentRoom.BlockDisableTraps then
return
end
CurrentRun.CurrentRoom.BlockDisableTraps = true
local ids = GetClosestIds({ Id = CurrentRun.Hero.ObjectId, DestinationName = "EnemyTeam", Distance = args.Range, PreciseCollision = true, ScaleY = 0.6, ScaleX = 1 })
for _, id in pairs( ids ) do
local enemy = ActiveEnemies[id]
if enemy and enemy.ToggleTrap then
DisableTrap( enemy )
table.insert( disabledTraps, enemy )
end
end
wait( args.Duration, RoomThreadName )
if IsCombatEncounterActive( CurrentRun ) then
for _, enemy in pairs( disabledTraps ) do
EnableTrap(enemy)
end
end
end
function PickRoomEliteTypeUpgrades( room )
local roomEliteTypes = {}
for k, wave in pairs(room.Encounter.SpawnWaves) do
for index, spawnData in ipairs( wave.Spawns ) do
if EnemyData[spawnData.Name].IsElite then
table.insert(roomEliteTypes, spawnData.Name)
end
end
end
local eliteTypeUpgradeCount = room.EliteTypeUpgradeCount or 1
for i = 1, eliteTypeUpgradeCount do
local eliteType = RemoveRandomValue(roomEliteTypes)
if eliteType ~= nil then
PickEliteAttributes( room, EnemyData[eliteType] )
RemoveAllValues(roomEliteTypes, eliteType)
end
end
end
Import "../Mods/ThanatosControl/ThanatosControl.lua"
Import "../Mods/HadesSpeedrunningModpack/HadesSpeedrunningModpack.lua"
Import "../Mods/HadesSpeedrunningModpack/RngBalanceMenu.lua"
Import "../Mods/HadesSpeedrunningModpack/QualityOfLifeMenu.lua"
Import "../Mods/HadesSpeedrunningModpack/AccessibilityMenu.lua"
Import "../Mods/HadesSpeedrunningModpack/PersonalizationMenu.lua"
Import "../Mods/HadesSpeedrunningModpack/SettingsHashMenu.lua"
Import "../Mods/HadesSpeedrunningModpack/HSMConfigMenu.lua"
Import "../Mods/HadesSpeedrunningModpack/AttributionAndNotes.lua"
Import "../Mods/DoorVisualIndicators/DoorVisualIndicators.lua"
Import "../Mods/ModdedWarning/ModdedWarning.lua"
Import "../Mods/RemoveCutscenes/RemoveCutscenes.lua"
Import "../Mods/ModConfigMenu/ModConfigMenu.lua"
Import "../Mods/EllosStartingBoonSelectorMod/Scripts/EllosBoonSelectorMod.lua"
Import "../Mods/SatyrSackControl/SatyrSackControl.lua"
Import "../Mods/QuickRestart/QuickRestart.lua"
Import "../Mods/ErumiUILib/ErumiUILib.lua"
Import "../Mods/DontGetVorimed/DontGetVorimed.lua"
Import "../Mods/FixedHammers/FixedHammers.lua"
Import "../Mods/MinibossControl/MinibossControl.lua"
Import "../Mods/EmoteMod/EmoteMod.lua"
Import "../Mods/RoomDeterminism/RoomDeterminism.lua"
Import "../Mods/LootChoiceExt/LootChoiceExt.lua"
Import "../Mods/ShowChamberNumber/ShowChamberNumber.lua"
Import "../Mods/InteractableChaos/InteractableChaos.lua"
Import "../Mods/Colorblind/colorblind.lua"
Import "../Mods/PrintUtil/PrintUtil.lua"
Import "../Mods/ModUtil/ModUtilHades.lua"
Import "../Mods/RtaTimer/RtaTimer.lua"
-- MODIFIED by Mod Importer @ 2021-05-29 11:50:21.277266
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment