diff --git a/CREDITS.md b/CREDITS.md index 7f5b700a..0713e7e6 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -34,3 +34,4 @@ If you enjoy this mod and want to help on it, check it out on GitHub: https://gi - [adc](https://steamcommunity.com/id/whytefang/) for creating an awesome new language pack and providing ideas for new functionalities - [CreepSore](https://github.com/CreepSore) for creating the bag of crafting search feature - [Poyo](https://x.com/Poyomama02) for the great thumbnail art +- Pattieburger for Experimental Treatment and Dataminer Stat Change Predictors diff --git a/features/eid_itemprediction.lua b/features/eid_itemprediction.lua index a7352e55..b88aa333 100644 --- a/features/eid_itemprediction.lua +++ b/features/eid_itemprediction.lua @@ -413,9 +413,8 @@ function EID:GlyphOfBalancePrediction(player) end --- Crooked Penny --- -function EID:CrookedPennyPrediction(rng, carBattery) - local rng = EID:RNGNext(rng) - local seed = rng +function EID:CrookedPennyPrediction(seed, carBattery) + seed = EID:RNGNext(seed) -- Repentance makes a new seed by XORing the room's decoration seed if EID.isRepentance then local roomSeed = game:GetLevel():GetCurrentRoom():GetDecorationSeed() @@ -430,13 +429,106 @@ function EID:CrookedPennyPrediction(rng, carBattery) else result = EID:getDescriptionEntry("CrookedPennyTails") -- failures advance crooked penny's RNG an additional time to give the pity penny a seed value - rng = EID:RNGNext(rng) + seed = EID:RNGNext(seed) end - if carBattery then return result .. "#{{Collectible356}} " .. EID:CrookedPennyPrediction(rng) + if carBattery then return result .. "#{{Collectible356}} " .. EID:CrookedPennyPrediction(seed) else return result end end +-- Experimental Treatment Stat Changes (by Pattieburger) +-- Uses only s2,s3,s4 from the ET collectible RNG stream. +-- Outcome pattern: 4 stats UP, 2 stats DOWN, 1 stat unchanged. + +-- Possible Stat change lookup table, for use with modular descriptions (7 stats total). +-- Used by Experimental Treatment and also Dataminer, without the first entry. +local statChangeLookup = { + [1] = {"RedHeart", 1.0 }, -- Health container + [2] = {"Speed", 0.2 }, + [3] = REPENTANCE and {"FireRate", 0.5 } or {"Tears", 0.5 }, + [4] = {"Damage", 1.0 }, + [5] = {"Range", REPENTANCE and 2.5 or 0.5 }, + [6] = {"ShotSpeed", 0.2 }, + [7] = {"Luck", 1.0 }, +} + +function EID:ExperimentalTreatmentRNGCheck(player) + -- s1: current collectible RNG seed (pre-pickup) + local s1 = player:GetCollectibleRNG(CollectibleType.COLLECTIBLE_EXPERIMENTAL_TREATMENT):GetSeed() + + -- simulate advances + local s2 = EID:RNGNext(s1, 5, 9, 7) -- used (NoChange) + local s3 = EID:RNGNext(s2, 5, 9, 7) -- used (Neg1) + local s4 = EID:RNGNext(s3, 5, 9, 7) -- used (Neg2) + + -- NoChange: (s2 % 7) + 1 + local noChange = (s2 % 7) + 1 + -- Neg1: (s3 % 6) + 1, but if it collides with NoChange -> becomes Luck (7) + local neg1 = (s3 % 6) + 1 + if neg1 == noChange then + neg1 = 7 + end + + -- Neg2: (s4 % 5) + 1, with collision rules: + -- - if collides with neg1 -> becomes Shot speed (6) + -- - if collides with noChange -> becomes Luck (7) + local neg2 = (s4 % 5) + 1 + if neg2 == neg1 then + neg2 = 6 + end + if neg2 == noChange then + neg2 = 7 + end + + local statChanges = {} + statChanges[statChangeLookup[neg1][1]] = -statChangeLookup[neg1][2] + statChanges[statChangeLookup[neg2][1]] = -statChangeLookup[neg2][2] + (statChanges[statChangeLookup[neg2][1]] or 0) -- optionally add existing value + + -- Positives are the remaining 4 stats + for stat = 1, 7 do + if stat ~= noChange and stat ~= neg1 and stat ~= neg2 then + statChanges[statChangeLookup[stat][1]] = statChangeLookup[stat][2] + (statChanges[statChangeLookup[stat][1]] or 0) -- optionally add existing value + end + end + + -- Return stat change table + return statChanges +end + +-- Dataminer Stat Changes (by Pattieburger) +-- We "shuffle" the stat table using s1..s5 (mod6, mod5, mod4, mod3, mod2). +-- Result: position 1 = stat UP, position 2 = stat DOWN. +function EID:DataminerRNGCheck(player) + -- s0: current collectible RNG seed (pre-use) + local s0 = player:GetCollectibleRNG(CollectibleType.COLLECTIBLE_DATAMINER):GetSeed() + + -- simulate s1..s5 (do NOT alter game RNG; just compute next seeds) + local s1 = EID:RNGNext(s0, 5, 9, 7) + local s2 = EID:RNGNext(s1, 5, 9, 7) + local s3 = EID:RNGNext(s2, 5, 9, 7) + local s4 = EID:RNGNext(s3, 5, 9, 7) + local s5 = EID:RNGNext(s4, 5, 9, 7) + + -- stat indices: + -- 1=Speed, 2=Fire rate, 3=Damage, 4=Range, 5=Shotspeed, 6=Luck + local statTable = {1,2,3,4,5,6} + local seeds = { s1, s2, s3, s4, s5 } + + for i = 6, 2, -1 do + local seed = seeds[7 - i] -- 6→1, 5→2, 4→3, 3→4, 2→5 + local j = (seed % i) + 1 + statTable[i], statTable[j] = statTable[j], statTable[i] + end + + local upStat = statTable[1] + 1 -- +1 to skip Health container entry + local downStat = statTable[2] + 1 -- +1 to skip Health container entry + + return { + [statChangeLookup[downStat][1]] = -statChangeLookup[downStat][2], + [statChangeLookup[upStat][1]] = statChangeLookup[upStat][2], + } +end + -- Liberty Cap, Broken Syringe, Mom's Lock (extremely similar, simple RNG modulos) -- local libertyItems = {[0] = 21, 71, 120, 121} if EID.isRepentance then libertyItems = {[0] = 12, 71, 120, 121, 342, 398} end diff --git a/features/eid_modifiers.lua b/features/eid_modifiers.lua index 67956cff..d7bae762 100644 --- a/features/eid_modifiers.lua +++ b/features/eid_modifiers.lua @@ -141,6 +141,29 @@ local function BlackRuneCallback(descObj) return VoidCallback(descObj, true) end +local function RandomStatIncreaseCallback(descObj) + for i = 1, #EID.coopAllPlayers do + local player = EID.coopAllPlayers[i] + -- only add description for relevant players + if not EID.InsideItemReminder or (EID.InsideItemReminder and EID.ItemReminderPlayerEntity == player) then + local playerID = player:GetPlayerType() + EID:appendToDescription(descObj, "#"..EID:GetPlayerIcon(playerID) .. " {{ColorIsaac}}"..player:GetName().."{{CR}}#") + + if descObj.ObjSubType == 240 then + -- Experimental Treatment + local itemDataTable = EID:ExperimentalTreatmentRNGCheck(player) + EID:appendToDescription(descObj, EID:GenerateDescriptionFromStatTable(itemDataTable)) + else + -- Dataminer + local itemDataTable = EID:DataminerRNGCheck(player) + EID:appendToDescription(descObj, EID:GenerateDescriptionFromStatTable(itemDataTable)) + end + end + end + + return descObj +end + -- Map each text block of Pandora's Box to the AbsoluteStage number. Second entry is the block used for Alt-Stages local pandoraStages = { [1] = { 1, 1 }, -- B1 @@ -1302,6 +1325,10 @@ local function EIDConditionsAB(descObj) if EID.collectiblesOwned[477] then table.insert(callbacks, VoidCallback) end if EID.collectiblesOwned["5.300.41"] then table.insert(callbacks, BlackRuneCallback) end end + if EID.Config["ItemReminderShowRNGCheats"] then + if descObj.ObjSubType == 240 then table.insert(callbacks, RandomStatIncreaseCallback) end + if descObj.ObjSubType == 481 then table.insert(callbacks, RandomStatIncreaseCallback) end + end elseif descObj.ObjVariant == PickupVariant.PICKUP_TRINKET then if descObj.ObjSubType == 80 then table.insert(callbacks, BlackFeatherCallback) end diff --git a/features/eid_modular_descriptions.lua b/features/eid_modular_descriptions.lua index ef81a68e..9a1c002b 100644 --- a/features/eid_modular_descriptions.lua +++ b/features/eid_modular_descriptions.lua @@ -464,14 +464,24 @@ function EID:GenerateDescription(descriptionObj) if EID.descriptions[EID:getLanguage()].DisableModularDescriptions then return nil end local itemDataTable = EID.ItemData[itemID] + + return EID:GenerateDescriptionFromStatTable(itemDataTable, descriptionObj, itemID) +end + +-- Generates a description for an item based on its modules defined in EID.ItemData +-- Optional: A description object can be provided to apply modular data modifiers +-- Returns an empty string if no modules are defined for the item +function EID:GenerateDescriptionFromStatTable(itemDataTable, descriptionObj, itemID) -- return empty string if no ItemData are defined if not itemDataTable then return "" end - itemDataTable = EID:ApplyModularDataModifier(itemDataTable, descriptionObj) + if descriptionObj then + itemDataTable = EID:ApplyModularDataModifier(itemDataTable, descriptionObj) + end - local sortedModulesPositivePrio, sortedModulesNegativePrio = EID:GetSortedModularDescriptionEntries(itemDataTable, false, true, itemID) + local sortedModulesPositivePrio, sortedModulesNegativePrio = EID:GetSortedModularDescriptionEntries(itemDataTable, false, itemID ~= nil, itemID) local description = ""