 6059b5ef66
			
		
	
	6059b5ef66
	
	
	
		
			
			This adds support to most of Varia's 20221101 update. Notably, added Options for: - Objectives - Tourian - RelaxedRoundRobinCF As well as previously unsupported Options: - EscapeRando - RemoveEscapeEnemies - HideItems
		
			
				
	
	
		
			889 lines
		
	
	
		
			34 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			889 lines
		
	
	
		
			34 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| 
 | |
| import math
 | |
| 
 | |
| from ..logic.cache import Cache
 | |
| from ..logic.smbool import SMBool, smboolFalse
 | |
| from ..utils.parameters import Settings, easy, medium, diff2text
 | |
| from ..rom.rom_patches import RomPatches
 | |
| from ..utils.utils import normalizeRounding
 | |
| 
 | |
| 
 | |
| class Helpers(object):
 | |
|     def __init__(self, smbm):
 | |
|         self.smbm = smbm
 | |
| 
 | |
|     # return bool
 | |
|     def haveItemCount(self, item, count):
 | |
|         return self.smbm.itemCount(item) >= count
 | |
| 
 | |
|     # return integer
 | |
|     @Cache.decorator
 | |
|     def energyReserveCount(self):
 | |
|         return self.smbm.itemCount('ETank') + self.smbm.itemCount('Reserve')
 | |
| 
 | |
|     def energyReserveCountOkDiff(self, difficulties, mult=1.0):
 | |
|         if difficulties is None or len(difficulties) == 0:
 | |
|             return smboolFalse
 | |
| 
 | |
|         def f(difficulty):
 | |
|             return self.smbm.energyReserveCountOk(normalizeRounding(difficulty[0] / mult), difficulty=difficulty[1])
 | |
| 
 | |
|         result = f(difficulties[0])
 | |
|         for difficulty in difficulties[1:]:
 | |
|             result = self.smbm.wor(result, f(difficulty))
 | |
|         return result
 | |
| 
 | |
|     def energyReserveCountOkHellRun(self, hellRunName, mult=1.0):
 | |
|         difficulties = Settings.hellRuns[hellRunName]
 | |
|         result = self.energyReserveCountOkDiff(difficulties, mult)
 | |
| 
 | |
|         if result == True:
 | |
|             result.knows = [hellRunName+'HellRun']
 | |
| 
 | |
|         return result
 | |
| 
 | |
|     # gives damage reduction factor with the current suits
 | |
|     # envDmg : if true (default) will return environmental damage reduction
 | |
|     def getDmgReduction(self, envDmg=True):
 | |
|         ret = 1.0
 | |
|         sm = self.smbm
 | |
|         hasVaria = sm.haveItem('Varia')
 | |
|         hasGrav = sm.haveItem('Gravity')
 | |
|         items = []
 | |
|         if RomPatches.has(sm.player, RomPatches.NoGravityEnvProtection):
 | |
|             if hasVaria:
 | |
|                 items = ['Varia']
 | |
|                 if envDmg:
 | |
|                     ret = 4.0
 | |
|                 else:
 | |
|                     ret = 2.0
 | |
|             if hasGrav and not envDmg:
 | |
|                 ret = 4.0
 | |
|                 items = ['Gravity']
 | |
|         elif RomPatches.has(sm.player, RomPatches.ProgressiveSuits):
 | |
|             if hasVaria:
 | |
|                 items.append('Varia')
 | |
|                 ret *= 2
 | |
|             if hasGrav:
 | |
|                 items.append('Gravity')
 | |
|                 ret *= 2
 | |
|         else:
 | |
|             if hasVaria:
 | |
|                 ret = 2.0
 | |
|                 items = ['Varia']
 | |
|             if hasGrav:
 | |
|                 ret = 4.0
 | |
|                 items = ['Gravity']
 | |
|         return (ret, items)
 | |
| 
 | |
|     # higher values for mult means room is that much "easier" (HP mult)
 | |
|     def energyReserveCountOkHardRoom(self, roomName, mult=1.0):
 | |
|         difficulties = Settings.hardRooms[roomName]
 | |
|         (dmgRed, items) = self.getDmgReduction()
 | |
|         mult *= dmgRed
 | |
|         result = self.energyReserveCountOkDiff(difficulties, mult)
 | |
| 
 | |
|         if result == True:
 | |
|             result.knows = ['HardRoom-'+roomName]
 | |
|             if dmgRed != 1.0:
 | |
|                 result._items.append(items)
 | |
|         return result
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def heatProof(self):
 | |
|         sm = self.smbm
 | |
|         return sm.wor(sm.haveItem('Varia'),
 | |
|                       sm.wand(sm.wnot(RomPatches.has(sm.player, RomPatches.NoGravityEnvProtection)),
 | |
|                               sm.wnot(RomPatches.has(sm.player, RomPatches.ProgressiveSuits)),
 | |
|                               sm.haveItem('Gravity')))
 | |
| 
 | |
|     # helper here because we can't define "sublambdas" in locations
 | |
|     def getPiratesPseudoScrewCoeff(self):
 | |
|         sm = self.smbm
 | |
|         ret = 1.0
 | |
|         if RomPatches.has(sm.player, RomPatches.NerfedCharge).bool == True:
 | |
|             ret = 4.0
 | |
|         return ret
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def canFireChargedShots(self):
 | |
|         sm = self.smbm
 | |
|         return sm.wor(sm.haveItem('Charge'), RomPatches.has(sm.player, RomPatches.NerfedCharge))
 | |
| 
 | |
|     # higher values for mult means hell run is that much "easier" (HP mult)
 | |
|     def canHellRun(self, hellRun, mult=1.0, minE=2):
 | |
|         sm = self.smbm
 | |
| 
 | |
|         items = []
 | |
|         isHeatProof = sm.heatProof()
 | |
|         if isHeatProof == True:
 | |
|             return isHeatProof
 | |
|         if sm.wand(RomPatches.has(sm.player, RomPatches.ProgressiveSuits), sm.haveItem('Gravity')).bool == True:
 | |
|             # half heat protection
 | |
|             mult *= 2.0
 | |
|             minE /= 2.0
 | |
|             items.append('Gravity')
 | |
|         if self.energyReserveCount() >= minE:
 | |
|             if hellRun != 'LowerNorfair':
 | |
|                 ret = self.energyReserveCountOkHellRun(hellRun, mult)
 | |
|                 if ret.bool == True:
 | |
|                     ret._items.append(items)
 | |
|                 return ret
 | |
|             else:
 | |
|                 tanks = self.energyReserveCount()
 | |
|                 multCF = mult
 | |
|                 if tanks >= 14:
 | |
|                     multCF *= 2.0
 | |
|                 nCF = int(math.ceil(2/multCF))
 | |
|                 ret = sm.wand(self.energyReserveCountOkHellRun(hellRun, mult),
 | |
|                               self.canCrystalFlash(nCF))
 | |
|                 if ret.bool == True:
 | |
|                     if sm.haveItem('Gravity') == True:
 | |
|                         ret.difficulty *= 0.7
 | |
|                         ret._items.append('Gravity')
 | |
|                     elif sm.haveItem('ScrewAttack') == True:
 | |
|                         ret.difficulty *= 0.7
 | |
|                         ret._items.append('ScrewAttack')
 | |
|                 #nPB = self.smbm.itemCount('PowerBomb')
 | |
|                 #print("canHellRun LN. tanks=" + str(tanks) + ", nCF=" + str(nCF) + ", nPB=" + str(nPB) + ", mult=" + str(mult) + ", heatProof=" + str(isHeatProof.bool) + ", ret=" + str(ret))
 | |
|                 return ret
 | |
|         else:
 | |
|             return smboolFalse
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def canMockball(self):
 | |
|         sm = self.smbm
 | |
|         return sm.wand(sm.haveItem('Morph'),
 | |
|                        sm.knowsMockball())
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def canFly(self):
 | |
|         sm = self.smbm
 | |
|         return sm.wor(sm.haveItem('SpaceJump'),
 | |
|                       sm.canInfiniteBombJump())
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def canSimpleShortCharge(self):
 | |
|         sm = self.smbm
 | |
|         return sm.wand(sm.haveItem('SpeedBooster'),
 | |
|                        sm.wor(sm.knowsSimpleShortCharge(),
 | |
|                               sm.knowsShortCharge()))
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def canShortCharge(self):
 | |
|         sm = self.smbm
 | |
|         return sm.wand(sm.haveItem('SpeedBooster'), sm.knowsShortCharge())
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def canUseBombs(self):
 | |
|         sm = self.smbm
 | |
|         return sm.wand(sm.haveItem('Morph'), sm.haveItem('Bomb'))
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def canInfiniteBombJump(self):
 | |
|         sm = self.smbm
 | |
|         return sm.wand(sm.haveItem('Morph'), sm.haveItem('Bomb'), sm.knowsInfiniteBombJump())
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def canInfiniteBombJumpSuitless(self):
 | |
|         sm = self.smbm
 | |
|         return sm.wand(sm.haveItem('Morph'), sm.haveItem('Bomb'), sm.knowsInfiniteBombJumpSuitless())
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def haveMissileOrSuper(self):
 | |
|         sm = self.smbm
 | |
|         return sm.wor(sm.haveItem('Missile'), sm.haveItem('Super'))
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def canOpenRedDoors(self):
 | |
|         sm = self.smbm
 | |
|         return sm.wor(sm.wand(sm.wnot(RomPatches.has(sm.player, RomPatches.RedDoorsMissileOnly)),
 | |
|                               sm.haveMissileOrSuper()),
 | |
|                       sm.haveItem('Missile'))
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def canOpenEyeDoors(self):
 | |
|         sm = self.smbm
 | |
|         return sm.wor(RomPatches.has(sm.player, RomPatches.NoGadoras),
 | |
|                       sm.haveMissileOrSuper())
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def canOpenGreenDoors(self):
 | |
|         return self.smbm.haveItem('Super')
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def canGreenGateGlitch(self):
 | |
|         sm = self.smbm
 | |
|         return sm.wand(sm.haveItem('Super'),
 | |
|                        sm.knowsGreenGateGlitch())
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def canBlueGateGlitch(self):
 | |
|         sm = self.smbm
 | |
|         return sm.wand(sm.haveMissileOrSuper(),
 | |
|                        sm.knowsGreenGateGlitch())
 | |
|     @Cache.decorator
 | |
|     def canUsePowerBombs(self):
 | |
|         sm = self.smbm
 | |
|         return sm.wand(sm.haveItem('Morph'), sm.haveItem('PowerBomb'))
 | |
| 
 | |
|     canOpenYellowDoors = canUsePowerBombs
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def canUseSpringBall(self):
 | |
|         sm = self.smbm
 | |
|         return sm.wand(sm.haveItem('Morph'),
 | |
|                        sm.haveItem('SpringBall'))
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def canSpringBallJump(self):
 | |
|         sm = self.smbm
 | |
|         return sm.wand(sm.canUseSpringBall(),
 | |
|                        sm.knowsSpringBallJump())
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def canDoubleSpringBallJump(self):
 | |
|         sm = self.smbm
 | |
|         return sm.wand(sm.canUseSpringBall(),
 | |
|                        sm.haveItem('HiJump'),
 | |
|                        sm.knowsDoubleSpringBallJump())
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def canSpringBallJumpFromWall(self):
 | |
|         sm = self.smbm
 | |
|         return sm.wand(sm.canUseSpringBall(),
 | |
|                        sm.knowsSpringBallJumpFromWall())
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def canDestroyBombWalls(self):
 | |
|         sm = self.smbm
 | |
|         return sm.wor(sm.wand(sm.haveItem('Morph'),
 | |
|                               sm.wor(sm.haveItem('Bomb'),
 | |
|                                      sm.haveItem('PowerBomb'))),
 | |
|                       sm.haveItem('ScrewAttack'))
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def canDestroyBombWallsUnderwater(self):
 | |
|         sm = self.smbm
 | |
|         return sm.wor(sm.wand(sm.haveItem('Gravity'),
 | |
|                               sm.canDestroyBombWalls()),
 | |
|                       sm.wand(sm.haveItem('Morph'),
 | |
|                               sm.wor(sm.haveItem('Bomb'),
 | |
|                                      sm.haveItem('PowerBomb'))))
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def canPassBombPassages(self):
 | |
|         sm = self.smbm
 | |
|         return sm.wor(sm.canUseBombs(),
 | |
|                       sm.canUsePowerBombs())
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def canMorphJump(self):
 | |
|         # small hop in morph ball form
 | |
|         sm = self.smbm
 | |
|         return sm.wor(sm.canPassBombPassages(), sm.canUseSpringBall())
 | |
| 
 | |
|     def canCrystalFlash(self, n=1):
 | |
|         sm = self.smbm
 | |
|         if not RomPatches.has(sm.player, RomPatches.RoundRobinCF).bool:
 | |
|             ret = sm.wand(sm.canUsePowerBombs(),
 | |
|                           sm.itemCountOk('Missile', 2*n),
 | |
|                           sm.itemCountOk('Super', 2*n),
 | |
|                           sm.itemCountOk('PowerBomb', 2*n+1))
 | |
|         else:
 | |
|             # simplified view of actual patch behavior: only count full refills to stick with base CF logic
 | |
|             nAmmo = 5 * (sm.itemCount('Missile') + sm.itemCount('Super') + sm.itemCount('PowerBomb'))
 | |
|             ret = sm.wand(sm.canUsePowerBombs(),
 | |
|                           SMBool(nAmmo >= 30*n))
 | |
|         if ret:
 | |
|             ret._items.append("{}-CrystalFlash".format(n))
 | |
|         return ret
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def canCrystalFlashClip(self):
 | |
|         sm = self.smbm
 | |
|         return sm.wand(sm.canCrystalFlash(),
 | |
|                        sm.wor(sm.wand(sm.haveItem('Gravity'),
 | |
|                                       sm.canUseBombs(),
 | |
|                                       sm.knowsCrystalFlashClip()),
 | |
|                               sm.wand(sm.knowsSuitlessCrystalFlashClip(),
 | |
|                                       sm.itemCountOk('PowerBomb', 4))))
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def canDoLowGauntlet(self):
 | |
|         sm = self.smbm
 | |
|         return sm.wand(sm.canShortCharge(),
 | |
|                        sm.canUsePowerBombs(),
 | |
|                        sm.itemCountOk('ETank', 1),
 | |
|                        sm.knowsLowGauntlet())
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def canUseHyperBeam(self):
 | |
|         sm = self.smbm
 | |
|         return sm.haveItem('Hyper')
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def getBeamDamage(self):
 | |
|         sm = self.smbm
 | |
|         standardDamage = 20
 | |
| 
 | |
|         if sm.wand(sm.haveItem('Ice'),
 | |
|                    sm.haveItem('Wave'),
 | |
|                    sm.haveItem('Plasma')) == True:
 | |
|             standardDamage = 300
 | |
|         elif sm.wand(sm.haveItem('Wave'),
 | |
|                      sm.haveItem('Plasma')) == True:
 | |
|             standardDamage = 250
 | |
|         elif sm.wand(sm.haveItem('Ice'),
 | |
|                      sm.haveItem('Plasma')) == True:
 | |
|             standardDamage = 200
 | |
|         elif sm.haveItem('Plasma') == True:
 | |
|             standardDamage = 150
 | |
|         elif sm.wand(sm.haveItem('Ice'),
 | |
|                      sm.haveItem('Wave'),
 | |
|                      sm.haveItem('Spazer')) == True:
 | |
|             standardDamage = 100
 | |
|         elif sm.wand(sm.haveItem('Wave'),
 | |
|                      sm.haveItem('Spazer')) == True:
 | |
|             standardDamage = 70
 | |
|         elif sm.wand(sm.haveItem('Ice'),
 | |
|                      sm.haveItem('Spazer')) == True:
 | |
|             standardDamage = 60
 | |
|         elif sm.wand(sm.haveItem('Ice'),
 | |
|                      sm.haveItem('Wave')) == True:
 | |
|             standardDamage = 60
 | |
|         elif sm.haveItem('Wave') == True:
 | |
|             standardDamage = 50
 | |
|         elif sm.haveItem('Spazer') == True:
 | |
|             standardDamage = 40
 | |
|         elif sm.haveItem('Ice') == True:
 | |
|             standardDamage = 30
 | |
| 
 | |
|         return standardDamage
 | |
| 
 | |
|     # returns a tuple with :
 | |
|     #
 | |
|     # - a floating point number : 0 if boss is unbeatable with
 | |
|     # current equipment, and an ammo "margin" (ex : 1.5 means we have 50%
 | |
|     # more firepower than absolutely necessary). Useful to compute boss
 | |
|     # difficulty when not having charge. If player has charge, the actual
 | |
|     # value is not useful, and is guaranteed to be > 2.
 | |
|     #
 | |
|     # - estimation of the fight duration in seconds (well not really, it
 | |
|     # is if you fire and land shots perfectly and constantly), giving info
 | |
|     # to compute boss fight difficulty
 | |
|     def canInflictEnoughDamages(self, bossEnergy, doubleSuper=False, charge=True, power=False, givesDrops=True, ignoreMissiles=False, ignoreSupers=False, missilesOffset=0, supersOffset=0, powerBombsOffset=0):
 | |
|         # TODO: handle special beam attacks ? (http://deanyd.net/sm/index.php?title=Charge_Beam_Combos)
 | |
|         sm = self.smbm
 | |
|         items = []
 | |
| 
 | |
|         # http://deanyd.net/sm/index.php?title=Damage
 | |
|         standardDamage = 0
 | |
|         if sm.canFireChargedShots().bool == True and charge == True:
 | |
|             standardDamage = self.getBeamDamage()
 | |
|             items.append('Charge')
 | |
|         # charge triples the damage
 | |
|         chargeDamage = standardDamage
 | |
|         if sm.haveItem('Charge').bool == True:
 | |
|             chargeDamage *= 3.0
 | |
| 
 | |
|         # missile 100 damages, super missile 300 damages, PBs 200 dmg, 5 in each extension
 | |
|         missilesAmount = max(0, (sm.itemCount('Missile') - missilesOffset)) * 5
 | |
|         if ignoreMissiles == True:
 | |
|             missilesDamage = 0
 | |
|         else:
 | |
|             missilesDamage = missilesAmount * 100
 | |
|             if missilesAmount > 0:
 | |
|                 items.append('Missile')
 | |
|         supersAmount = max(0, (sm.itemCount('Super') - supersOffset)) * 5
 | |
|         if ignoreSupers == True:
 | |
|             oneSuper = 0
 | |
|         else:
 | |
|             oneSuper = 300.0
 | |
|             if supersAmount > 0:
 | |
|                 items.append('Super')
 | |
|         if doubleSuper == True:
 | |
|             oneSuper *= 2
 | |
|         supersDamage = supersAmount * oneSuper
 | |
|         powerDamage = 0
 | |
|         powerAmount = 0
 | |
|         if power == True and sm.haveItem('PowerBomb') == True:
 | |
|             powerAmount = max(0, (sm.itemCount('PowerBomb') - powerBombsOffset)) * 5
 | |
|             powerDamage = powerAmount * 200
 | |
|             items.append('PowerBomb')
 | |
| 
 | |
|         canBeatBoss = chargeDamage > 0 or givesDrops or (missilesDamage + supersDamage + powerDamage) >= bossEnergy
 | |
|         if not canBeatBoss:
 | |
|             return (0, 0, [])
 | |
| 
 | |
|         ammoMargin = (missilesDamage + supersDamage + powerDamage) / bossEnergy
 | |
|         if chargeDamage > 0:
 | |
|             ammoMargin += 2
 | |
| 
 | |
|         missilesDPS = Settings.algoSettings['missilesPerSecond'] * 100.0
 | |
|         supersDPS = Settings.algoSettings['supersPerSecond'] * 300.0
 | |
|         if doubleSuper == True:
 | |
|             supersDPS *= 2
 | |
|         if powerDamage > 0:
 | |
|             powerDPS = Settings.algoSettings['powerBombsPerSecond'] * 200.0
 | |
|         else:
 | |
|             powerDPS = 0.0
 | |
|         chargeDPS = chargeDamage * Settings.algoSettings['chargedShotsPerSecond']
 | |
|         # print("chargeDPS=" + str(chargeDPS))
 | |
|         dpsDict = {
 | |
|             missilesDPS: (missilesAmount, 100.0),
 | |
|             supersDPS: (supersAmount, oneSuper),
 | |
|             powerDPS: (powerAmount, 200.0),
 | |
|             # no boss will take more 10000 charged shots
 | |
|             chargeDPS: (10000, chargeDamage)
 | |
|         }
 | |
|         secs = 0
 | |
|         for dps in sorted(dpsDict, reverse=True):
 | |
|             amount = dpsDict[dps][0]
 | |
|             one = dpsDict[dps][1]
 | |
|             if dps == 0 or one == 0 or amount == 0:
 | |
|                 continue
 | |
|             fire = min(bossEnergy / one, amount)
 | |
|             secs += fire * (one / dps)
 | |
|             bossEnergy -= fire * one
 | |
|             if bossEnergy <= 0:
 | |
|                 break
 | |
|         if bossEnergy > 0:
 | |
|             # print ('!! drops !! ')
 | |
|             secs += bossEnergy * Settings.algoSettings['missileDropsPerMinute'] * 100 / 60
 | |
|             # print('ammoMargin = ' + str(ammoMargin) + ', secs = ' + str(secs))
 | |
| 
 | |
|         return (ammoMargin, secs, items)
 | |
| 
 | |
|     # return diff score, or -1 if below minimum energy in diffTbl
 | |
|     def computeBossDifficulty(self, ammoMargin, secs, diffTbl, energyDiff=0):
 | |
|         sm = self.smbm
 | |
| 
 | |
|         # actual fight duration :
 | |
|         rate = None
 | |
|         if 'Rate' in diffTbl:
 | |
|             rate = float(diffTbl['Rate'])
 | |
|         if rate is None:
 | |
|             duration = 120.0
 | |
|         else:
 | |
|             duration = secs / rate
 | |
|  #       print('rate=' + str(rate) + ', duration=' + str(duration))
 | |
|         (suitsCoeff, items) = sm.getDmgReduction(envDmg=False)
 | |
|         suitsCoeff /= 2.0
 | |
|         energyCount = self.energyReserveCount()
 | |
|         energy = suitsCoeff * (1 + energyCount + energyDiff)
 | |
| #        print("energy="+str(energy)+", energyCount="+str(energyCount)+",energyDiff="+str(energyDiff)+",suitsCoeff="+str(suitsCoeff))
 | |
| 
 | |
|         # add all energy in used items
 | |
|         items += sm.energyReserveCountOk(energyCount).items
 | |
| 
 | |
|         energyDict = None
 | |
|         if 'Energy' in diffTbl:
 | |
|             energyDict = diffTbl['Energy']
 | |
|         difficulty = medium
 | |
|         # get difficulty by energy
 | |
|         if energyDict:
 | |
|             energyDict = {float(k):float(v) for k,v in energyDict.items()}
 | |
|             keyz = sorted(energyDict.keys())
 | |
|             if len(keyz) > 0:
 | |
|                 current = keyz[0]
 | |
|                 if energy < current:
 | |
|                     return (-1, [])
 | |
|                 sup = None
 | |
|                 difficulty = energyDict[current]
 | |
|                 for k in keyz:
 | |
|                     if k > energy:
 | |
|                         sup=k
 | |
|                         break
 | |
|                     current = k
 | |
|                     difficulty = energyDict[k]
 | |
|                 # interpolate if we can
 | |
|                 if energy > current and sup is not None:
 | |
|                     difficulty += (energyDict[sup] - difficulty)/(sup - current) * (energy - current)
 | |
|  #       print("energy=" + str(energy) + ", base diff=" + str(difficulty))
 | |
|         # adjust by fight duration
 | |
|         difficulty *= (duration / 120)
 | |
|         # and by ammo margin
 | |
|         # only augment difficulty in case of no charge, don't lower it.
 | |
|         # if we have charge, ammoMargin will have a huge value (see canInflictEnoughDamages),
 | |
|         # so this does not apply
 | |
|         diffAdjust = (1 - (ammoMargin - Settings.algoSettings['ammoMarginIfNoCharge']))
 | |
|         if diffAdjust > 1:
 | |
|             difficulty *= diffAdjust
 | |
| #        print("final diff: "+str(round(difficulty, 2)))
 | |
| 
 | |
|         return (round(difficulty, 2), items)
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def enoughStuffSporeSpawn(self):
 | |
|         sm = self.smbm
 | |
|         return sm.wor(sm.haveItem('Missile'), sm.haveItem('Super'), sm.haveItem('Charge'))
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def enoughStuffCroc(self):
 | |
|         sm = self.smbm
 | |
|         # say croc has ~5000 energy, and ignore its useless drops
 | |
|         (ammoMargin, secs, items) = self.canInflictEnoughDamages(5000, givesDrops=False)
 | |
|         if ammoMargin == 0:
 | |
|             return sm.wand(sm.knowsLowAmmoCroc(),
 | |
|                            sm.wor(sm.itemCountOk("Missile", 2),
 | |
|                                   sm.wand(sm.haveItem('Missile'),
 | |
|                                           sm.haveItem('Super'))))
 | |
|         else:
 | |
|             return SMBool(True, easy, items=items)
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def enoughStuffBotwoon(self):
 | |
|         sm = self.smbm
 | |
|         (ammoMargin, secs, items) = self.canInflictEnoughDamages(6000, givesDrops=False)
 | |
|         diff = SMBool(True, easy, [], items)
 | |
|         lowStuff = sm.knowsLowStuffBotwoon()
 | |
|         if ammoMargin == 0 and lowStuff.bool:
 | |
|             (ammoMargin, secs, items) = self.canInflictEnoughDamages(3500, givesDrops=False)
 | |
|             diff = SMBool(lowStuff.bool, lowStuff.difficulty, lowStuff.knows, items)
 | |
|         if ammoMargin == 0:
 | |
|             return smboolFalse
 | |
|         fight = sm.wor(sm.energyReserveCountOk(math.ceil(4/sm.getDmgReduction(envDmg=False)[0])),
 | |
|                        lowStuff)
 | |
|         return sm.wandmax(fight, diff)
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def enoughStuffGT(self):
 | |
|         sm = self.smbm
 | |
|         hasBeams = sm.wand(sm.haveItem('Charge'), sm.haveItem('Plasma')).bool
 | |
|         (ammoMargin, secs, items) = self.canInflictEnoughDamages(9000, ignoreMissiles=True, givesDrops=hasBeams)
 | |
|         diff = SMBool(True, easy, [], items)
 | |
|         lowStuff = sm.knowsLowStuffGT()
 | |
|         if ammoMargin == 0 and lowStuff.bool:
 | |
|             (ammoMargin, secs, items) = self.canInflictEnoughDamages(3000, ignoreMissiles=True)
 | |
|             diff = SMBool(lowStuff.bool, lowStuff.difficulty, lowStuff.knows, items)
 | |
|         if ammoMargin == 0:
 | |
|             return smboolFalse
 | |
|         fight = sm.wor(sm.energyReserveCountOk(math.ceil(8/sm.getDmgReduction(envDmg=False)[0])),
 | |
|                        lowStuff)
 | |
|         return sm.wandmax(fight, diff)
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def enoughStuffsRidley(self):
 | |
|         sm = self.smbm
 | |
|         if not sm.haveItem('Morph') and not sm.haveItem('ScrewAttack'):
 | |
|             return smboolFalse
 | |
|         (ammoMargin, secs, ammoItems) = self.canInflictEnoughDamages(18000, doubleSuper=True, power=True, givesDrops=False)
 | |
|         if ammoMargin == 0:
 | |
|             return smboolFalse
 | |
| 
 | |
|         # print('RIDLEY', ammoMargin, secs)
 | |
|         (diff, defenseItems) = self.computeBossDifficulty(ammoMargin, secs,
 | |
|                                                           Settings.bossesDifficulty['Ridley'])
 | |
|         if (sm.onlyBossLeft):
 | |
|             diff = 1
 | |
|         if diff < 0:
 | |
|             return smboolFalse
 | |
|         else:
 | |
|             return SMBool(True, diff, items=ammoItems+defenseItems)
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def enoughStuffsKraid(self):
 | |
|         sm = self.smbm
 | |
|         (ammoMargin, secs, ammoItems) = self.canInflictEnoughDamages(1000)
 | |
|         if ammoMargin == 0:
 | |
|             return smboolFalse
 | |
|         #print('KRAID True ', ammoMargin, secs)
 | |
|         (diff, defenseItems) = self.computeBossDifficulty(ammoMargin, secs,
 | |
|                                                           Settings.bossesDifficulty['Kraid'])
 | |
|         if (sm.onlyBossLeft):
 | |
|             diff = 1
 | |
|         if diff < 0:
 | |
|             return smboolFalse
 | |
| 
 | |
|         return SMBool(True, diff, items=ammoItems+defenseItems)
 | |
| 
 | |
|     def adjustHealthDropDiff(self, difficulty):
 | |
|         (dmgRed, items) = self.getDmgReduction(envDmg=False)
 | |
|         # 2 is Varia suit, considered standard eqt for boss fights
 | |
|         # there's certainly a smarter way to do this but...
 | |
|         if dmgRed < 2:
 | |
|             difficulty *= Settings.algoSettings['dmgReductionDifficultyFactor']
 | |
|         elif dmgRed > 2:
 | |
|             difficulty /= Settings.algoSettings['dmgReductionDifficultyFactor']
 | |
|         return difficulty
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def enoughStuffsDraygon(self):
 | |
|         sm = self.smbm
 | |
|         if not sm.haveItem('Morph') and not sm.haveItem('Gravity'):
 | |
|             return smboolFalse
 | |
|         # some ammo to destroy the turrets during the fight
 | |
|         if not sm.haveMissileOrSuper():
 | |
|             return smboolFalse
 | |
|         (ammoMargin, secs, ammoItems) = self.canInflictEnoughDamages(6000,
 | |
|                                                                      # underestimate missiles/supers in case a CF exit is needed
 | |
|                                                                      missilesOffset=2, supersOffset=2)
 | |
|         # print('DRAY', ammoMargin, secs)
 | |
|         if ammoMargin > 0:
 | |
|             (diff, defenseItems) = self.computeBossDifficulty(ammoMargin, secs,
 | |
|                                                               Settings.bossesDifficulty['Draygon'])
 | |
|             if diff < 0:
 | |
|                 fight = smboolFalse
 | |
|             else:
 | |
|                 fight = SMBool(True, diff, items=ammoItems+defenseItems)
 | |
|             if sm.haveItem('Gravity') == False:
 | |
|                 fight.difficulty *= Settings.algoSettings['draygonNoGravityMalus']
 | |
|             else:
 | |
|                 fight._items.append('Gravity')
 | |
|             if not sm.haveItem('Morph'):
 | |
|                 fight.difficulty *= Settings.algoSettings['draygonNoMorphMalus']
 | |
|             if sm.haveItem('Gravity') and sm.haveItem('ScrewAttack'):
 | |
|                 fight.difficulty /= Settings.algoSettings['draygonScrewBonus']
 | |
|             fight.difficulty = self.adjustHealthDropDiff(fight.difficulty)
 | |
|             if (sm.onlyBossLeft):
 | |
|                 fight.difficulty = 1
 | |
|         else:
 | |
|             fight = smboolFalse
 | |
|         # for grapple kill considers energy drained by wall socket + 2 spankings by Dray
 | |
|         # (original 99 energy used for rounding)
 | |
|         nTanksGrapple = (240/sm.getDmgReduction(envDmg=True)[0] + 2*160/sm.getDmgReduction(envDmg=False)[0])/100
 | |
|         return sm.wor(fight,
 | |
|                       sm.wand(sm.knowsDraygonGrappleKill(),
 | |
|                               sm.haveItem('Grapple'),
 | |
|                               sm.energyReserveCountOk(nTanksGrapple)),
 | |
|                       sm.wand(sm.knowsMicrowaveDraygon(),
 | |
|                               sm.haveItem('Plasma'),
 | |
|                               sm.canFireChargedShots(),
 | |
|                               sm.haveItem('XRayScope')),
 | |
|                       sm.wand(sm.haveItem('Gravity'),
 | |
|                               sm.energyReserveCountOk(3),
 | |
|                               sm.knowsDraygonSparkKill(),
 | |
|                               sm.haveItem('SpeedBooster')))
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def enoughStuffsPhantoon(self):
 | |
|         sm = self.smbm
 | |
|         (ammoMargin, secs, ammoItems) = self.canInflictEnoughDamages(2500, doubleSuper=True)
 | |
|         if ammoMargin == 0:
 | |
|             return smboolFalse
 | |
|         # print('PHANTOON', ammoMargin, secs)
 | |
|         (difficulty, defenseItems) = self.computeBossDifficulty(ammoMargin, secs,
 | |
|                                                                 Settings.bossesDifficulty['Phantoon'])
 | |
|         if difficulty < 0:
 | |
|             return smboolFalse
 | |
|         hasCharge = sm.canFireChargedShots()
 | |
|         hasScrew = sm.haveItem('ScrewAttack')
 | |
|         if hasScrew:
 | |
|             difficulty /= Settings.algoSettings['phantoonFlamesAvoidBonusScrew']
 | |
|             defenseItems += hasScrew.items
 | |
|         elif hasCharge:
 | |
|             difficulty /= Settings.algoSettings['phantoonFlamesAvoidBonusCharge']
 | |
|             defenseItems += hasCharge.items
 | |
|         elif not hasCharge and sm.itemCount('Missile') <= 2: # few missiles is harder
 | |
|             difficulty *= Settings.algoSettings['phantoonLowMissileMalus']
 | |
|         difficulty = self.adjustHealthDropDiff(difficulty)
 | |
|         if (sm.onlyBossLeft):
 | |
|             difficulty = 1
 | |
|         fight = SMBool(True, difficulty, items=ammoItems+defenseItems)
 | |
| 
 | |
|         return sm.wor(fight,
 | |
|                       sm.wand(sm.knowsMicrowavePhantoon(),
 | |
|                               sm.haveItem('Plasma'),
 | |
|                               sm.canFireChargedShots(),
 | |
|                               sm.haveItem('XRayScope')))
 | |
| 
 | |
|     def mbEtankCheck(self):
 | |
|         sm = self.smbm
 | |
|         if sm.wor(RomPatches.has(sm.player, RomPatches.NerfedRainbowBeam), RomPatches.has(sm.player, RomPatches.TourianSpeedup)):
 | |
|             # "add" energy for difficulty calculations
 | |
|             energy = 2.8 if sm.haveItem('Varia') else 2.6
 | |
|             return (True, energy)
 | |
|         nTanks = sm.energyReserveCount()
 | |
|         energyDiff = 0
 | |
|         if sm.haveItem('Varia') == False:
 | |
|             # "remove" 3 etanks (accounting for rainbow beam damage without varia)
 | |
|             if nTanks < 6:
 | |
|                 return (False, 0)
 | |
|             energyDiff = -3
 | |
|         elif nTanks < 3:
 | |
|             return (False, 0)
 | |
|         return (True, energyDiff)
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def enoughStuffsMotherbrain(self):
 | |
|         sm = self.smbm
 | |
|         # MB1 can't be hit by charge beam
 | |
|         (ammoMargin, secs, _) = self.canInflictEnoughDamages(3000, charge=False, givesDrops=False)
 | |
|         if ammoMargin == 0:
 | |
|             return smboolFalse
 | |
|         # requires 10-10 to break the glass
 | |
|         if sm.itemCount('Missile') <= 1 or sm.itemCount('Super') <= 1:
 | |
|             return smboolFalse
 | |
|         # we actually don't give a shit about MB1 difficulty,
 | |
|         # since we embark its health in the following calc
 | |
|         (ammoMargin, secs, ammoItems) = self.canInflictEnoughDamages(18000 + 3000, givesDrops=False)
 | |
|         if ammoMargin == 0:
 | |
|             return smboolFalse
 | |
|         (possible, energyDiff) = self.mbEtankCheck()
 | |
|         if possible == False:
 | |
|             return smboolFalse
 | |
|         # print('MB2', ammoMargin, secs)
 | |
|         #print("ammoMargin: {}, secs: {}, settings: {}, energyDiff: {}".format(ammoMargin, secs, Settings.bossesDifficulty['MotherBrain'], energyDiff))
 | |
|         (diff, defenseItems) = self.computeBossDifficulty(ammoMargin, secs, Settings.bossesDifficulty['MotherBrain'], energyDiff)
 | |
|         if (sm.onlyBossLeft):
 | |
|             diff = 1
 | |
|         if diff < 0:
 | |
|             return smboolFalse
 | |
|         return SMBool(True, diff, items=ammoItems+defenseItems)
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def canPassMetroids(self):
 | |
|         sm = self.smbm
 | |
|         return sm.wor(sm.wand(sm.haveItem('Ice'), sm.haveMissileOrSuper()),
 | |
|                       # to avoid leaving tourian to refill power bombs
 | |
|                       sm.itemCountOk('PowerBomb', 3))
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def canPassZebetites(self):
 | |
|         sm = self.smbm
 | |
|         return sm.wor(sm.wand(sm.haveItem('Ice'), sm.knowsIceZebSkip()),
 | |
|                       sm.wand(sm.haveItem('SpeedBooster'), sm.knowsSpeedZebSkip()),
 | |
|                       # account for one zebetite, refill may be necessary
 | |
|                       SMBool(self.canInflictEnoughDamages(1100, charge=False, givesDrops=False, ignoreSupers=True)[0] >= 1, 0))
 | |
| 
 | |
|     @Cache.decorator
 | |
|     def enoughStuffTourian(self):
 | |
|         sm = self.smbm
 | |
|         ret = self.smbm.wand(sm.wor(RomPatches.has(sm.player, RomPatches.TourianSpeedup),
 | |
|                                     sm.wand(sm.canPassMetroids(), sm.canPassZebetites())),
 | |
|                              sm.canOpenRedDoors(),
 | |
|                              sm.enoughStuffsMotherbrain(),
 | |
|                              sm.wor(RomPatches.has(sm.player, RomPatches.OpenZebetites), sm.haveItem('Morph')))
 | |
|         return ret
 | |
| 
 | |
| class Pickup:
 | |
|     def __init__(self, itemsPickup):
 | |
|         self.itemsPickup = itemsPickup
 | |
|         self.endGameLocations = ["Mother Brain", "Gunship"]
 | |
| 
 | |
|     # don't count end game location in the mix as it's now in the available locations
 | |
|     def enoughMinors(self, smbm, minorLocations):
 | |
|         if self.itemsPickup in ('all', 'all_strict'):
 | |
|             return (len(minorLocations) == 0
 | |
|                     or (len(minorLocations) == 1 and minorLocations[0].Name in self.endGameLocations))
 | |
|         else:
 | |
|             return True
 | |
| 
 | |
|     def enoughMajors(self, smbm, majorLocations):
 | |
|         if self.itemsPickup in ('all', 'all_strict'):
 | |
|             return (len(majorLocations) == 0
 | |
|                     or (len(majorLocations) == 1 and majorLocations[0].Name in self.endGameLocations))
 | |
|         else:
 | |
|             return True
 | |
| 
 | |
| class Bosses:
 | |
|     # bosses helpers to know if they are dead
 | |
|     areaBosses = {
 | |
|         # classic areas
 | |
|         'Brinstar': 'Kraid',
 | |
|         'Norfair': 'Ridley',
 | |
|         'LowerNorfair': 'Ridley',
 | |
|         'WreckedShip': 'Phantoon',
 | |
|         'Maridia': 'Draygon',
 | |
|         # solver areas
 | |
|         'Blue Brinstar': 'Kraid',
 | |
|         'Brinstar Hills': 'Kraid',
 | |
|         'Bubble Norfair': 'Ridley',
 | |
|         'Bubble Norfair Bottom': 'Ridley',
 | |
|         'Bubble Norfair Reserve': 'Ridley',
 | |
|         'Bubble Norfair Speed': 'Ridley',
 | |
|         'Bubble Norfair Wave': 'Ridley',
 | |
|         'Draygon Boss': 'Draygon',
 | |
|         'Green Brinstar': 'Kraid',
 | |
|         'Green Brinstar Reserve': 'Kraid',
 | |
|         'Kraid': 'Kraid',
 | |
|         'Kraid Boss': 'Kraid',
 | |
|         'Left Sandpit': 'Draygon',
 | |
|         'Lower Norfair After Amphitheater': 'Ridley',
 | |
|         'Lower Norfair Before Amphitheater': 'Ridley',
 | |
|         'Lower Norfair Screw Attack': 'Ridley',
 | |
|         'Maridia Forgotten Highway': 'Draygon',
 | |
|         'Maridia Green': 'Draygon',
 | |
|         'Maridia Pink Bottom': 'Draygon',
 | |
|         'Maridia Pink Top': 'Draygon',
 | |
|         'Maridia Sandpits': 'Draygon',
 | |
|         'Norfair Entrance': 'Ridley',
 | |
|         'Norfair Grapple Escape': 'Ridley',
 | |
|         'Norfair Ice': 'Ridley',
 | |
|         'Phantoon Boss': 'Phantoon',
 | |
|         'Pink Brinstar': 'Kraid',
 | |
|         'Red Brinstar': 'Kraid',
 | |
|         'Red Brinstar Top': 'Kraid',
 | |
|         'Ridley Boss': 'Ridley',
 | |
|         'Right Sandpit': 'Draygon',
 | |
|         'Warehouse': 'Kraid',
 | |
|         'WreckedShip': 'Phantoon',
 | |
|         'WreckedShip Back': 'Phantoon',
 | |
|         'WreckedShip Bottom': 'Phantoon',
 | |
|         'WreckedShip Gravity': 'Phantoon',
 | |
|         'WreckedShip Main': 'Phantoon',
 | |
|         'WreckedShip Top': 'Phantoon'
 | |
|     }
 | |
| 
 | |
|     accessPoints = {
 | |
|         "Kraid": "KraidRoomIn",
 | |
|         "Phantoon": "PhantoonRoomIn",
 | |
|         "Draygon": "Draygon Room Bottom",
 | |
|         "Ridley": "RidleyRoomIn",
 | |
|         "SporeSpawn": "Big Pink",
 | |
|         "Crocomire": "Crocomire Room Top",
 | |
|         "Botwoon": "Aqueduct Bottom",
 | |
|         "GoldenTorizo": "Screw Attack Bottom"
 | |
|     }
 | |
| 
 | |
|     @staticmethod
 | |
|     def Golden4():
 | |
|         return ['Draygon', 'Kraid', 'Phantoon', 'Ridley']
 | |
|     
 | |
|     @staticmethod
 | |
|     def miniBosses():
 | |
|         return ['SporeSpawn', 'Crocomire', 'Botwoon', 'GoldenTorizo']
 | |
| 
 | |
|     @staticmethod
 | |
|     def bossDead(sm, boss):
 | |
|         return sm.haveItem(boss)
 | |
| 
 | |
|     @staticmethod
 | |
|     def areaBossDead(sm, area):
 | |
|         if area not in Bosses.areaBosses:
 | |
|             return True
 | |
|         return Bosses.bossDead(sm, Bosses.areaBosses[area])
 | |
| 
 | |
|     @staticmethod
 | |
|     def allBossesDead(smbm):
 | |
|         return smbm.wand(Bosses.bossDead(smbm, 'Kraid'),
 | |
|                          Bosses.bossDead(smbm, 'Phantoon'),
 | |
|                          Bosses.bossDead(smbm, 'Draygon'),
 | |
|                          Bosses.bossDead(smbm, 'Ridley'))
 | |
|     
 | |
|     @staticmethod
 | |
|     def allMiniBossesDead(smbm):
 | |
|         return smbm.wand(Bosses.bossDead(smbm, 'SporeSpawn'),
 | |
|                          Bosses.bossDead(smbm, 'Botwoon'),
 | |
|                          Bosses.bossDead(smbm, 'Crocomire'),
 | |
|                          Bosses.bossDead(smbm, 'GoldenTorizo'))
 | |
| 
 | |
|     @staticmethod
 | |
|     def xBossesDead(smbm, target):
 | |
|         count = sum([1 for boss in Bosses.Golden4() if Bosses.bossDead(smbm, boss)])
 | |
|         return SMBool(count >= target)
 | |
| 
 | |
|     @staticmethod
 | |
|     def xMiniBossesDead(smbm, target):
 | |
|         count = sum([1 for miniboss in Bosses.miniBosses() if Bosses.bossDead(smbm, miniboss)])
 | |
|         return SMBool(count >= target)
 | |
| 
 | |
| def diffValue2txt(diff):
 | |
|     last = 0
 | |
|     for d in sorted(diff2text.keys()):
 | |
|         if diff >= last and diff < d:
 | |
|             return diff2text[last]
 | |
|         last = d
 | |
|     return None
 |